[
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\nindent_style = space\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true\nmax_line_length = 120\nindent_size = 2\n\n[*.md]\ntrim_trailing_whitespace = false\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "content": "name: \"Bug report\"\ndescription: Create a report to help us improve\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Thank you for reporting an issue :pray:.\n\n        This issue tracker is for bugs and issues found with [Bolt.new](https://bolt.new).\n        If you experience issues related to WebContainer, please file an issue in our [WebContainer repo](https://github.com/stackblitz/webcontainer-core), or file an issue in our [StackBlitz core repo](https://github.com/stackblitz/core) for issues with StackBlitz.\n\n        The more information you fill in, the better we can help you.\n  - type: textarea\n    id: description\n    attributes:\n      label: Describe the bug\n      description: Provide a clear and concise description of what you're running into.\n    validations:\n      required: true\n  - type: input\n    id: link\n    attributes:\n      label: Link to the Bolt URL that caused the error\n      description: Please do not delete it after reporting!\n    validations:\n      required: true\n  - type: checkboxes\n    id: checkboxes\n    attributes:\n      label: Validations\n      description: Before submitting the issue, please make sure you do the following\n      options:\n        - label: \"Please make your project public or accessible by URL. This will allow anyone trying to help you to easily reproduce the issue and provide assistance.\"\n          required: true\n  - type: markdown\n    attributes:\n      value: |\n        ![Making your project public](https://github.com/stackblitz/bolt.new/blob/main/public/project-visibility.jpg?raw=true)\n  - type: textarea\n    id: steps\n    attributes:\n      label: Steps to reproduce\n      description: Describe the steps we have to take to reproduce the behavior.\n      placeholder: |\n        1. Go to '...'\n        2. Click on '....'\n        3. Scroll down to '....'\n        4. See error\n    validations:\n      required: true\n  - type: textarea\n    id: expected\n    attributes:\n      label: Expected behavior\n      description: Provide a clear and concise description of what you expected to happen.\n    validations:\n      required: true\n  - type: textarea\n    id: screenshots\n    attributes:\n      label: Screen Recording / Screenshot\n      description: If applicable, **please include a screen recording** (preferably) or screenshot showcasing the issue. This will assist us in resolving your issue <u>quickly</u>.\n  - type: textarea\n    id: platform\n    attributes:\n      label: Platform\n      value: |\n        - OS: [e.g. macOS, Windows, Linux]\n        - Browser: [e.g. Chrome, Safari, Firefox]\n        - Version: [e.g. 91.1]\n  - type: textarea\n    id: additional\n    attributes:\n      label: Additional context\n      description: Add any other context about the problem here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  - name: Bolt.new Help Center\n    url: https://support.bolt.new\n    about: Official central repository for tips, tricks, tutorials, known issues, and best practices for bolt.new usage.\n  - name: Billing Issues\n    url: https://support.bolt.new/Billing-13fd971055d680ebb393cb80973710b6\n    about: Instructions for billing and subscription related support\n  - name: Discord Chat\n    url: https://discord.gg/stackblitz\n    about: Build, share, and learn with other Bolters in real time.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: ''\nassignees: ''\n---\n\n**Is your feature request related to a problem? Please describe:**\n\n<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->\n\n**Describe the solution you'd like:**\n\n<!-- A clear and concise description of what you want to happen. -->\n\n**Describe alternatives you've considered:**\n\n<!-- A clear and concise description of any alternative solutions or features you've considered. -->\n\n**Additional context:**\n\n<!-- Add any other context or screenshots about the feature request here. -->\n"
  },
  {
    "path": ".github/actions/setup-and-build/action.yaml",
    "content": "name: Setup and Build\ndescription: Generic setup action\ninputs:\n  pnpm-version:\n    required: false\n    type: string\n    default: '9.4.0'\n  node-version:\n    required: false\n    type: string\n    default: '20.15.1'\n\nruns:\n  using: composite\n\n  steps:\n    - uses: pnpm/action-setup@v4\n      with:\n        version: ${{ inputs.pnpm-version }}\n        run_install: false\n\n    - name: Set Node.js version to ${{ inputs.node-version }}\n      uses: actions/setup-node@v4\n      with:\n        node-version: ${{ inputs.node-version }}\n        cache: pnpm\n\n    - name: Install dependencies and build project\n      shell: bash\n      run: |\n        pnpm install\n        pnpm run build\n"
  },
  {
    "path": ".github/workflows/ci.yaml",
    "content": "name: CI/CD\n\non:\n  push:\n    branches:\n      - master\n  pull_request:\n\njobs:\n  test:\n    name: Test\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Setup and Build\n        uses: ./.github/actions/setup-and-build\n\n      - name: Run type check\n        run: pnpm run typecheck\n\n      # - name: Run ESLint\n      #   run: pnpm run lint\n\n      - name: Run tests\n        run: pnpm run test\n"
  },
  {
    "path": ".github/workflows/semantic-pr.yaml",
    "content": "name: Semantic Pull Request\non:\n  pull_request_target:\n    types: [opened, reopened, edited, synchronize]\npermissions:\n  pull-requests: read\njobs:\n  main:\n    name: Validate PR Title\n    runs-on: ubuntu-latest\n    steps:\n      # https://github.com/amannn/action-semantic-pull-request/releases/tag/v5.5.3\n      - uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          subjectPattern: ^(?![A-Z]).+$\n          subjectPatternError: |\n            The subject \"{subject}\" found in the pull request title \"{title}\"\n            didn't match the configured pattern. Please ensure that the subject\n            doesn't start with an uppercase character.\n          types: |\n            fix\n            feat\n            chore\n            build\n            ci\n            perf\n            docs\n            refactor\n            revert\n            test\n"
  },
  {
    "path": ".gitignore",
    "content": "logs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndist-ssr\n*.local\n\n.vscode/*\n!.vscode/launch.json\n!.vscode/extensions.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n\n/.cache\n/build\n.env*\n*.vars\n.wrangler\n_worker.bundle\n"
  },
  {
    "path": ".husky/commit-msg",
    "content": "#!/usr/bin/env sh\n\n. \"$(dirname \"$0\")/_/husky.sh\"\n\nnpx commitlint --edit $1\n\nexit 0\n"
  },
  {
    "path": ".prettierignore",
    "content": "pnpm-lock.yaml\n.astro\n"
  },
  {
    "path": ".prettierrc",
    "content": "{\n  \"printWidth\": 120,\n  \"singleQuote\": true,\n  \"useTabs\": false,\n  \"tabWidth\": 2,\n  \"semi\": true,\n  \"bracketSpacing\": true\n}\n"
  },
  {
    "path": ".tool-versions",
    "content": "nodejs 20.15.1\npnpm 9.4.0\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "[![Bolt Open Source Codebase](./public/social_preview_index.jpg)](https://bolt.new)\n\n> Welcome to the **Bolt** open-source codebase! This repo contains a simple example app using the core components from bolt.new to help you get started building **AI-powered software development tools** powered by StackBlitz’s **WebContainer API**.\n\n### Why Build with Bolt + WebContainer API\n\nBy building with the Bolt + WebContainer API you can create browser-based applications that let users **prompt, run, edit, and deploy** full-stack web apps directly in the browser, without the need for virtual machines. With WebContainer API, you can build apps that give AI direct access and full control over a **Node.js server**, **filesystem**, **package manager** and **dev terminal** inside your users browser tab. This powerful combination allows you to create a new class of development tools that support all major JavaScript libraries and Node packages right out of the box, all without remote environments or local installs.\n\n### What’s the Difference Between Bolt (This Repo) and [Bolt.new](https://bolt.new)?\n\n- **Bolt.new**: This is the **commercial product** from StackBlitz—a hosted, browser-based AI development tool that enables users to prompt, run, edit, and deploy full-stack web applications directly in the browser. Built on top of the [Bolt open-source repo](https://github.com/stackblitz/bolt.new) and powered by the StackBlitz **WebContainer API**.\n\n- **Bolt (This Repo)**: This open-source repository provides the core components used to make **Bolt.new**. This repo contains the UI interface for Bolt as well as the server components, built using [Remix Run](https://remix.run/). By leveraging this repo and StackBlitz’s **WebContainer API**, you can create your own AI-powered development tools and full-stack applications that run entirely in the browser.\n\n# Get Started Building with Bolt\n\nBolt combines the capabilities of AI with sandboxed development environments to create a collaborative experience where code can be developed by the assistant and the programmer together. Bolt combines [WebContainer API](https://webcontainers.io/api) with [Claude Sonnet 3.5](https://www.anthropic.com/news/claude-3-5-sonnet) using [Remix](https://remix.run/) and the [AI SDK](https://sdk.vercel.ai/).\n\n### WebContainer API\n\nBolt uses [WebContainers](https://webcontainers.io/) to run generated code in the browser. WebContainers provide Bolt with a full-stack sandbox environment using [WebContainer API](https://webcontainers.io/api). WebContainers run full-stack applications directly in the browser without the cost and security concerns of cloud hosted AI agents. WebContainers are interactive and editable, and enables Bolt's AI to run code and understand any changes from the user.\n\nThe [WebContainer API](https://webcontainers.io) is free for personal and open source usage. If you're building an application for commercial usage, you can learn more about our [WebContainer API commercial usage pricing here](https://stackblitz.com/pricing#webcontainer-api).\n\n### Remix App\n\nBolt is built with [Remix](https://remix.run/) and\ndeployed using [CloudFlare Pages](https://pages.cloudflare.com/) and\n[CloudFlare Workers](https://workers.cloudflare.com/).\n\n### AI SDK Integration\n\nBolt uses the [AI SDK](https://github.com/vercel/ai) to integrate with AI\nmodels. At this time, Bolt supports using Anthropic's Claude Sonnet 3.5.\nYou can get an API key from the [Anthropic API Console](https://console.anthropic.com/) to use with Bolt.\nTake a look at how [Bolt uses the AI SDK](https://github.com/stackblitz/bolt.new/tree/main/app/lib/.server/llm)\n\n## Prerequisites\n\nBefore you begin, ensure you have the following installed:\n\n- Node.js (v20.15.1)\n- pnpm (v9.4.0)\n\n## Setup\n\n1. Clone the repository (if you haven't already):\n\n```bash\ngit clone https://github.com/stackblitz/bolt.new.git\n```\n\n2. Install dependencies:\n\n```bash\npnpm install\n```\n\n3. Create a `.env.local` file in the root directory and add your Anthropic API key:\n\n```\nANTHROPIC_API_KEY=XXX\n```\n\nOptionally, you can set the debug level:\n\n```\nVITE_LOG_LEVEL=debug\n```\n\n**Important**: Never commit your `.env.local` file to version control. It's already included in .gitignore.\n\n## Available Scripts\n\n- `pnpm run dev`: Starts the development server.\n- `pnpm run build`: Builds the project.\n- `pnpm run start`: Runs the built application locally using Wrangler Pages. This script uses `bindings.sh` to set up necessary bindings so you don't have to duplicate environment variables.\n- `pnpm run preview`: Builds the project and then starts it locally, useful for testing the production build. Note, HTTP streaming currently doesn't work as expected with `wrangler pages dev`.\n- `pnpm test`: Runs the test suite using Vitest.\n- `pnpm run typecheck`: Runs TypeScript type checking.\n- `pnpm run typegen`: Generates TypeScript types using Wrangler.\n- `pnpm run deploy`: Builds the project and deploys it to Cloudflare Pages.\n\n## Development\n\nTo start the development server:\n\n```bash\npnpm run dev\n```\n\nThis will start the Remix Vite development server.\n\n## Testing\n\nRun the test suite with:\n\n```bash\npnpm test\n```\n\n## Deployment\n\nTo deploy the application to Cloudflare Pages:\n\n```bash\npnpm run deploy\n```\n\nMake sure you have the necessary permissions and Wrangler is correctly configured for your Cloudflare account.\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2024 StackBlitz, Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "[![Bolt.new: AI-Powered Full-Stack Web Development in the Browser](./public/social_preview_index.jpg)](https://bolt.new)\n\n# Bolt.new: AI-Powered Full-Stack Web Development in the Browser\n\nBolt.new is an AI-powered web development agent that allows you to prompt, run, edit, and deploy full-stack applications directly from your browser—no local setup required. If you're here to build your own AI-powered web dev agent using the Bolt open source codebase, [click here to get started!](./CONTRIBUTING.md)\n\n## What Makes Bolt.new Different\n\nClaude, v0, etc are incredible- but you can't install packages, run backends or edit code. That’s where Bolt.new stands out:\n\n- **Full-Stack in the Browser**: Bolt.new integrates cutting-edge AI models with an in-browser development environment powered by **StackBlitz’s WebContainers**. This allows you to:\n  - Install and run npm tools and libraries (like Vite, Next.js, and more)\n  - Run Node.js servers\n  - Interact with third-party APIs\n  - Deploy to production from chat\n  - Share your work via a URL\n\n- **AI with Environment Control**: Unlike traditional dev environments where the AI can only assist in code generation, Bolt.new gives AI models **complete control** over the entire  environment including the filesystem, node server, package manager, terminal, and browser console. This empowers AI agents to handle the entire app lifecycle—from creation to deployment.\n\nWhether you’re an experienced developer, a PM or designer, Bolt.new allows you to build production-grade full-stack applications with ease.\n\nFor developers interested in building their own AI-powered development tools with WebContainers, check out the open-source Bolt codebase in this repo!\n\n## Tips and Tricks\n\nHere are some tips to get the most out of Bolt.new:\n\n- **Be specific about your stack**: If you want to use specific frameworks or libraries (like Astro, Tailwind, ShadCN, or any other popular JavaScript framework), mention them in your initial prompt to ensure Bolt scaffolds the project accordingly.\n\n- **Use the enhance prompt icon**: Before sending your prompt, try clicking the 'enhance' icon to have the AI model help you refine your prompt, then edit the results before submitting.\n\n- **Scaffold the basics first, then add features**: Make sure the basic structure of your application is in place before diving into more advanced functionality. This helps Bolt understand the foundation of your project and ensure everything is wired up right before building out more advanced functionality.\n\n- **Batch simple instructions**: Save time by combining simple instructions into one message. For example, you can ask Bolt to change the color scheme, add mobile responsiveness, and restart the dev server, all in one go saving you time and reducing API credit consumption significantly.\n\n## FAQs\n\n**Where do I sign up for a paid plan?**  \nBolt.new is free to get started. If you need more AI tokens or want private projects, you can purchase a paid subscription in your [Bolt.new](https://bolt.new) settings, in the lower-left hand corner of the application. \n\n**What happens if I hit the free usage limit?**  \nOnce your free daily token limit is reached, AI interactions are paused until the next day or until you upgrade your plan.\n\n**Is Bolt in beta?**  \nYes, Bolt.new is in beta, and we are actively improving it based on feedback.\n\n**How can I report Bolt.new issues?**  \nCheck out the [Issues section](https://github.com/stackblitz/bolt.new/issues) to report an issue or request a new feature. Please use the search feature to check if someone else has already submitted the same issue/request.\n\n**What frameworks/libraries currently work on Bolt?**  \nBolt.new supports most popular JavaScript frameworks and libraries. If it runs on StackBlitz, it will run on Bolt.new as well.\n\n**How can I add make sure my framework/project works well in bolt?**  \nWe are excited to work with the JavaScript ecosystem to improve functionality in Bolt. Reach out to us via [hello@stackblitz.com](mailto:hello@stackblitz.com) to discuss how we can partner!\n"
  },
  {
    "path": "app/components/chat/Artifact.tsx",
    "content": "import { useStore } from '@nanostores/react';\nimport { AnimatePresence, motion } from 'framer-motion';\nimport { computed } from 'nanostores';\nimport { memo, useEffect, useRef, useState } from 'react';\nimport { createHighlighter, type BundledLanguage, type BundledTheme, type HighlighterGeneric } from 'shiki';\nimport type { ActionState } from '~/lib/runtime/action-runner';\nimport { workbenchStore } from '~/lib/stores/workbench';\nimport { classNames } from '~/utils/classNames';\nimport { cubicEasingFn } from '~/utils/easings';\n\nconst highlighterOptions = {\n  langs: ['shell'],\n  themes: ['light-plus', 'dark-plus'],\n};\n\nconst shellHighlighter: HighlighterGeneric<BundledLanguage, BundledTheme> =\n  import.meta.hot?.data.shellHighlighter ?? (await createHighlighter(highlighterOptions));\n\nif (import.meta.hot) {\n  import.meta.hot.data.shellHighlighter = shellHighlighter;\n}\n\ninterface ArtifactProps {\n  messageId: string;\n}\n\nexport const Artifact = memo(({ messageId }: ArtifactProps) => {\n  const userToggledActions = useRef(false);\n  const [showActions, setShowActions] = useState(false);\n\n  const artifacts = useStore(workbenchStore.artifacts);\n  const artifact = artifacts[messageId];\n\n  const actions = useStore(\n    computed(artifact.runner.actions, (actions) => {\n      return Object.values(actions);\n    }),\n  );\n\n  const toggleActions = () => {\n    userToggledActions.current = true;\n    setShowActions(!showActions);\n  };\n\n  useEffect(() => {\n    if (actions.length && !showActions && !userToggledActions.current) {\n      setShowActions(true);\n    }\n  }, [actions]);\n\n  return (\n    <div className=\"artifact border border-bolt-elements-borderColor flex flex-col overflow-hidden rounded-lg w-full transition-border duration-150\">\n      <div className=\"flex\">\n        <button\n          className=\"flex items-stretch bg-bolt-elements-artifacts-background hover:bg-bolt-elements-artifacts-backgroundHover w-full overflow-hidden\"\n          onClick={() => {\n            const showWorkbench = workbenchStore.showWorkbench.get();\n            workbenchStore.showWorkbench.set(!showWorkbench);\n          }}\n        >\n          <div className=\"px-5 p-3.5 w-full text-left\">\n            <div className=\"w-full text-bolt-elements-textPrimary font-medium leading-5 text-sm\">{artifact?.title}</div>\n            <div className=\"w-full w-full text-bolt-elements-textSecondary text-xs mt-0.5\">Click to open Workbench</div>\n          </div>\n        </button>\n        <div className=\"bg-bolt-elements-artifacts-borderColor w-[1px]\" />\n        <AnimatePresence>\n          {actions.length && (\n            <motion.button\n              initial={{ width: 0 }}\n              animate={{ width: 'auto' }}\n              exit={{ width: 0 }}\n              transition={{ duration: 0.15, ease: cubicEasingFn }}\n              className=\"bg-bolt-elements-artifacts-background hover:bg-bolt-elements-artifacts-backgroundHover\"\n              onClick={toggleActions}\n            >\n              <div className=\"p-4\">\n                <div className={showActions ? 'i-ph:caret-up-bold' : 'i-ph:caret-down-bold'}></div>\n              </div>\n            </motion.button>\n          )}\n        </AnimatePresence>\n      </div>\n      <AnimatePresence>\n        {showActions && actions.length > 0 && (\n          <motion.div\n            className=\"actions\"\n            initial={{ height: 0 }}\n            animate={{ height: 'auto' }}\n            exit={{ height: '0px' }}\n            transition={{ duration: 0.15 }}\n          >\n            <div className=\"bg-bolt-elements-artifacts-borderColor h-[1px]\" />\n            <div className=\"p-5 text-left bg-bolt-elements-actions-background\">\n              <ActionList actions={actions} />\n            </div>\n          </motion.div>\n        )}\n      </AnimatePresence>\n    </div>\n  );\n});\n\ninterface ShellCodeBlockProps {\n  classsName?: string;\n  code: string;\n}\n\nfunction ShellCodeBlock({ classsName, code }: ShellCodeBlockProps) {\n  return (\n    <div\n      className={classNames('text-xs', classsName)}\n      dangerouslySetInnerHTML={{\n        __html: shellHighlighter.codeToHtml(code, {\n          lang: 'shell',\n          theme: 'dark-plus',\n        }),\n      }}\n    ></div>\n  );\n}\n\ninterface ActionListProps {\n  actions: ActionState[];\n}\n\nconst actionVariants = {\n  hidden: { opacity: 0, y: 20 },\n  visible: { opacity: 1, y: 0 },\n};\n\nconst ActionList = memo(({ actions }: ActionListProps) => {\n  return (\n    <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: 0.15 }}>\n      <ul className=\"list-none space-y-2.5\">\n        {actions.map((action, index) => {\n          const { status, type, content } = action;\n          const isLast = index === actions.length - 1;\n\n          return (\n            <motion.li\n              key={index}\n              variants={actionVariants}\n              initial=\"hidden\"\n              animate=\"visible\"\n              transition={{\n                duration: 0.2,\n                ease: cubicEasingFn,\n              }}\n            >\n              <div className=\"flex items-center gap-1.5 text-sm\">\n                <div className={classNames('text-lg', getIconColor(action.status))}>\n                  {status === 'running' ? (\n                    <div className=\"i-svg-spinners:90-ring-with-bg\"></div>\n                  ) : status === 'pending' ? (\n                    <div className=\"i-ph:circle-duotone\"></div>\n                  ) : status === 'complete' ? (\n                    <div className=\"i-ph:check\"></div>\n                  ) : status === 'failed' || status === 'aborted' ? (\n                    <div className=\"i-ph:x\"></div>\n                  ) : null}\n                </div>\n                {type === 'file' ? (\n                  <div>\n                    Create{' '}\n                    <code className=\"bg-bolt-elements-artifacts-inlineCode-background text-bolt-elements-artifacts-inlineCode-text px-1.5 py-1 rounded-md\">\n                      {action.filePath}\n                    </code>\n                  </div>\n                ) : type === 'shell' ? (\n                  <div className=\"flex items-center w-full min-h-[28px]\">\n                    <span className=\"flex-1\">Run command</span>\n                  </div>\n                ) : null}\n              </div>\n              {type === 'shell' && (\n                <ShellCodeBlock\n                  classsName={classNames('mt-1', {\n                    'mb-3.5': !isLast,\n                  })}\n                  code={content}\n                />\n              )}\n            </motion.li>\n          );\n        })}\n      </ul>\n    </motion.div>\n  );\n});\n\nfunction getIconColor(status: ActionState['status']) {\n  switch (status) {\n    case 'pending': {\n      return 'text-bolt-elements-textTertiary';\n    }\n    case 'running': {\n      return 'text-bolt-elements-loader-progress';\n    }\n    case 'complete': {\n      return 'text-bolt-elements-icon-success';\n    }\n    case 'aborted': {\n      return 'text-bolt-elements-textSecondary';\n    }\n    case 'failed': {\n      return 'text-bolt-elements-icon-error';\n    }\n    default: {\n      return undefined;\n    }\n  }\n}\n"
  },
  {
    "path": "app/components/chat/AssistantMessage.tsx",
    "content": "import { memo } from 'react';\nimport { Markdown } from './Markdown';\n\ninterface AssistantMessageProps {\n  content: string;\n}\n\nexport const AssistantMessage = memo(({ content }: AssistantMessageProps) => {\n  return (\n    <div className=\"overflow-hidden w-full\">\n      <Markdown html>{content}</Markdown>\n    </div>\n  );\n});\n"
  },
  {
    "path": "app/components/chat/BaseChat.module.scss",
    "content": ".BaseChat {\n  &[data-chat-visible='false'] {\n    --workbench-inner-width: 100%;\n    --workbench-left: 0;\n\n    .Chat {\n      --at-apply: bolt-ease-cubic-bezier;\n      transition-property: transform, opacity;\n      transition-duration: 0.3s;\n      will-change: transform, opacity;\n      transform: translateX(-50%);\n      opacity: 0;\n    }\n  }\n}\n\n.Chat {\n  opacity: 1;\n}\n"
  },
  {
    "path": "app/components/chat/BaseChat.tsx",
    "content": "import type { Message } from 'ai';\nimport React, { type RefCallback } from 'react';\nimport { ClientOnly } from 'remix-utils/client-only';\nimport { Menu } from '~/components/sidebar/Menu.client';\nimport { IconButton } from '~/components/ui/IconButton';\nimport { Workbench } from '~/components/workbench/Workbench.client';\nimport { classNames } from '~/utils/classNames';\nimport { Messages } from './Messages.client';\nimport { SendButton } from './SendButton.client';\n\nimport styles from './BaseChat.module.scss';\n\ninterface BaseChatProps {\n  textareaRef?: React.RefObject<HTMLTextAreaElement> | undefined;\n  messageRef?: RefCallback<HTMLDivElement> | undefined;\n  scrollRef?: RefCallback<HTMLDivElement> | undefined;\n  showChat?: boolean;\n  chatStarted?: boolean;\n  isStreaming?: boolean;\n  messages?: Message[];\n  enhancingPrompt?: boolean;\n  promptEnhanced?: boolean;\n  input?: string;\n  handleStop?: () => void;\n  sendMessage?: (event: React.UIEvent, messageInput?: string) => void;\n  handleInputChange?: (event: React.ChangeEvent<HTMLTextAreaElement>) => void;\n  enhancePrompt?: () => void;\n}\n\nconst EXAMPLE_PROMPTS = [\n  { text: 'Build a todo app in React using Tailwind' },\n  { text: 'Build a simple blog using Astro' },\n  { text: 'Create a cookie consent form using Material UI' },\n  { text: 'Make a space invaders game' },\n  { text: 'How do I center a div?' },\n];\n\nconst TEXTAREA_MIN_HEIGHT = 76;\n\nexport const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(\n  (\n    {\n      textareaRef,\n      messageRef,\n      scrollRef,\n      showChat = true,\n      chatStarted = false,\n      isStreaming = false,\n      enhancingPrompt = false,\n      promptEnhanced = false,\n      messages,\n      input = '',\n      sendMessage,\n      handleInputChange,\n      enhancePrompt,\n      handleStop,\n    },\n    ref,\n  ) => {\n    const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;\n\n    return (\n      <div\n        ref={ref}\n        className={classNames(\n          styles.BaseChat,\n          'relative flex h-full w-full overflow-hidden bg-bolt-elements-background-depth-1',\n        )}\n        data-chat-visible={showChat}\n      >\n        <ClientOnly>{() => <Menu />}</ClientOnly>\n        <div ref={scrollRef} className=\"flex overflow-y-auto w-full h-full\">\n          <div className={classNames(styles.Chat, 'flex flex-col flex-grow min-w-[var(--chat-min-width)] h-full')}>\n            {!chatStarted && (\n              <div id=\"intro\" className=\"mt-[26vh] max-w-chat mx-auto\">\n                <h1 className=\"text-5xl text-center font-bold text-bolt-elements-textPrimary mb-2\">\n                  Where ideas begin\n                </h1>\n                <p className=\"mb-4 text-center text-bolt-elements-textSecondary\">\n                  Bring ideas to life in seconds or get help on existing projects.\n                </p>\n              </div>\n            )}\n            <div\n              className={classNames('pt-6 px-6', {\n                'h-full flex flex-col': chatStarted,\n              })}\n            >\n              <ClientOnly>\n                {() => {\n                  return chatStarted ? (\n                    <Messages\n                      ref={messageRef}\n                      className=\"flex flex-col w-full flex-1 max-w-chat px-4 pb-6 mx-auto z-1\"\n                      messages={messages}\n                      isStreaming={isStreaming}\n                    />\n                  ) : null;\n                }}\n              </ClientOnly>\n              <div\n                className={classNames('relative w-full max-w-chat mx-auto z-prompt', {\n                  'sticky bottom-0': chatStarted,\n                })}\n              >\n                <div\n                  className={classNames(\n                    'shadow-sm border border-bolt-elements-borderColor bg-bolt-elements-prompt-background backdrop-filter backdrop-blur-[8px] rounded-lg overflow-hidden',\n                  )}\n                >\n                  <textarea\n                    ref={textareaRef}\n                    className={`w-full pl-4 pt-4 pr-16 focus:outline-none resize-none text-md text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary bg-transparent`}\n                    onKeyDown={(event) => {\n                      if (event.key === 'Enter') {\n                        if (event.shiftKey) {\n                          return;\n                        }\n\n                        event.preventDefault();\n\n                        sendMessage?.(event);\n                      }\n                    }}\n                    value={input}\n                    onChange={(event) => {\n                      handleInputChange?.(event);\n                    }}\n                    style={{\n                      minHeight: TEXTAREA_MIN_HEIGHT,\n                      maxHeight: TEXTAREA_MAX_HEIGHT,\n                    }}\n                    placeholder=\"How can Bolt help you today?\"\n                    translate=\"no\"\n                  />\n                  <ClientOnly>\n                    {() => (\n                      <SendButton\n                        show={input.length > 0 || isStreaming}\n                        isStreaming={isStreaming}\n                        onClick={(event) => {\n                          if (isStreaming) {\n                            handleStop?.();\n                            return;\n                          }\n\n                          sendMessage?.(event);\n                        }}\n                      />\n                    )}\n                  </ClientOnly>\n                  <div className=\"flex justify-between text-sm p-4 pt-2\">\n                    <div className=\"flex gap-1 items-center\">\n                      <IconButton\n                        title=\"Enhance prompt\"\n                        disabled={input.length === 0 || enhancingPrompt}\n                        className={classNames({\n                          'opacity-100!': enhancingPrompt,\n                          'text-bolt-elements-item-contentAccent! pr-1.5 enabled:hover:bg-bolt-elements-item-backgroundAccent!':\n                            promptEnhanced,\n                        })}\n                        onClick={() => enhancePrompt?.()}\n                      >\n                        {enhancingPrompt ? (\n                          <>\n                            <div className=\"i-svg-spinners:90-ring-with-bg text-bolt-elements-loader-progress text-xl\"></div>\n                            <div className=\"ml-1.5\">Enhancing prompt...</div>\n                          </>\n                        ) : (\n                          <>\n                            <div className=\"i-bolt:stars text-xl\"></div>\n                            {promptEnhanced && <div className=\"ml-1.5\">Prompt enhanced</div>}\n                          </>\n                        )}\n                      </IconButton>\n                    </div>\n                    {input.length > 3 ? (\n                      <div className=\"text-xs text-bolt-elements-textTertiary\">\n                        Use <kbd className=\"kdb\">Shift</kbd> + <kbd className=\"kdb\">Return</kbd> for a new line\n                      </div>\n                    ) : null}\n                  </div>\n                </div>\n                <div className=\"bg-bolt-elements-background-depth-1 pb-6\">{/* Ghost Element */}</div>\n              </div>\n            </div>\n            {!chatStarted && (\n              <div id=\"examples\" className=\"relative w-full max-w-xl mx-auto mt-8 flex justify-center\">\n                <div className=\"flex flex-col space-y-2 [mask-image:linear-gradient(to_bottom,black_0%,transparent_180%)] hover:[mask-image:none]\">\n                  {EXAMPLE_PROMPTS.map((examplePrompt, index) => {\n                    return (\n                      <button\n                        key={index}\n                        onClick={(event) => {\n                          sendMessage?.(event, examplePrompt.text);\n                        }}\n                        className=\"group flex items-center w-full gap-2 justify-center bg-transparent text-bolt-elements-textTertiary hover:text-bolt-elements-textPrimary transition-theme\"\n                      >\n                        {examplePrompt.text}\n                        <div className=\"i-ph:arrow-bend-down-left\" />\n                      </button>\n                    );\n                  })}\n                </div>\n              </div>\n            )}\n          </div>\n          <ClientOnly>{() => <Workbench chatStarted={chatStarted} isStreaming={isStreaming} />}</ClientOnly>\n        </div>\n      </div>\n    );\n  },\n);\n"
  },
  {
    "path": "app/components/chat/Chat.client.tsx",
    "content": "import { useStore } from '@nanostores/react';\nimport type { Message } from 'ai';\nimport { useChat } from 'ai/react';\nimport { useAnimate } from 'framer-motion';\nimport { memo, useEffect, useRef, useState } from 'react';\nimport { cssTransition, toast, ToastContainer } from 'react-toastify';\nimport { useMessageParser, usePromptEnhancer, useShortcuts, useSnapScroll } from '~/lib/hooks';\nimport { useChatHistory } from '~/lib/persistence';\nimport { chatStore } from '~/lib/stores/chat';\nimport { workbenchStore } from '~/lib/stores/workbench';\nimport { fileModificationsToHTML } from '~/utils/diff';\nimport { cubicEasingFn } from '~/utils/easings';\nimport { createScopedLogger, renderLogger } from '~/utils/logger';\nimport { BaseChat } from './BaseChat';\n\nconst toastAnimation = cssTransition({\n  enter: 'animated fadeInRight',\n  exit: 'animated fadeOutRight',\n});\n\nconst logger = createScopedLogger('Chat');\n\nexport function Chat() {\n  renderLogger.trace('Chat');\n\n  const { ready, initialMessages, storeMessageHistory } = useChatHistory();\n\n  return (\n    <>\n      {ready && <ChatImpl initialMessages={initialMessages} storeMessageHistory={storeMessageHistory} />}\n      <ToastContainer\n        closeButton={({ closeToast }) => {\n          return (\n            <button className=\"Toastify__close-button\" onClick={closeToast}>\n              <div className=\"i-ph:x text-lg\" />\n            </button>\n          );\n        }}\n        icon={({ type }) => {\n          /**\n           * @todo Handle more types if we need them. This may require extra color palettes.\n           */\n          switch (type) {\n            case 'success': {\n              return <div className=\"i-ph:check-bold text-bolt-elements-icon-success text-2xl\" />;\n            }\n            case 'error': {\n              return <div className=\"i-ph:warning-circle-bold text-bolt-elements-icon-error text-2xl\" />;\n            }\n          }\n\n          return undefined;\n        }}\n        position=\"bottom-right\"\n        pauseOnFocusLoss\n        transition={toastAnimation}\n      />\n    </>\n  );\n}\n\ninterface ChatProps {\n  initialMessages: Message[];\n  storeMessageHistory: (messages: Message[]) => Promise<void>;\n}\n\nexport const ChatImpl = memo(({ initialMessages, storeMessageHistory }: ChatProps) => {\n  useShortcuts();\n\n  const textareaRef = useRef<HTMLTextAreaElement>(null);\n\n  const [chatStarted, setChatStarted] = useState(initialMessages.length > 0);\n\n  const { showChat } = useStore(chatStore);\n\n  const [animationScope, animate] = useAnimate();\n\n  const { messages, isLoading, input, handleInputChange, setInput, stop, append } = useChat({\n    api: '/api/chat',\n    onError: (error) => {\n      logger.error('Request failed\\n\\n', error);\n      toast.error('There was an error processing your request');\n    },\n    onFinish: () => {\n      logger.debug('Finished streaming');\n    },\n    initialMessages,\n  });\n\n  const { enhancingPrompt, promptEnhanced, enhancePrompt, resetEnhancer } = usePromptEnhancer();\n  const { parsedMessages, parseMessages } = useMessageParser();\n\n  const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;\n\n  useEffect(() => {\n    chatStore.setKey('started', initialMessages.length > 0);\n  }, []);\n\n  useEffect(() => {\n    parseMessages(messages, isLoading);\n\n    if (messages.length > initialMessages.length) {\n      storeMessageHistory(messages).catch((error) => toast.error(error.message));\n    }\n  }, [messages, isLoading, parseMessages]);\n\n  const scrollTextArea = () => {\n    const textarea = textareaRef.current;\n\n    if (textarea) {\n      textarea.scrollTop = textarea.scrollHeight;\n    }\n  };\n\n  const abort = () => {\n    stop();\n    chatStore.setKey('aborted', true);\n    workbenchStore.abortAllActions();\n  };\n\n  useEffect(() => {\n    const textarea = textareaRef.current;\n\n    if (textarea) {\n      textarea.style.height = 'auto';\n\n      const scrollHeight = textarea.scrollHeight;\n\n      textarea.style.height = `${Math.min(scrollHeight, TEXTAREA_MAX_HEIGHT)}px`;\n      textarea.style.overflowY = scrollHeight > TEXTAREA_MAX_HEIGHT ? 'auto' : 'hidden';\n    }\n  }, [input, textareaRef]);\n\n  const runAnimation = async () => {\n    if (chatStarted) {\n      return;\n    }\n\n    await Promise.all([\n      animate('#examples', { opacity: 0, display: 'none' }, { duration: 0.1 }),\n      animate('#intro', { opacity: 0, flex: 1 }, { duration: 0.2, ease: cubicEasingFn }),\n    ]);\n\n    chatStore.setKey('started', true);\n\n    setChatStarted(true);\n  };\n\n  const sendMessage = async (_event: React.UIEvent, messageInput?: string) => {\n    const _input = messageInput || input;\n\n    if (_input.length === 0 || isLoading) {\n      return;\n    }\n\n    /**\n     * @note (delm) Usually saving files shouldn't take long but it may take longer if there\n     * many unsaved files. In that case we need to block user input and show an indicator\n     * of some kind so the user is aware that something is happening. But I consider the\n     * happy case to be no unsaved files and I would expect users to save their changes\n     * before they send another message.\n     */\n    await workbenchStore.saveAllFiles();\n\n    const fileModifications = workbenchStore.getFileModifcations();\n\n    chatStore.setKey('aborted', false);\n\n    runAnimation();\n\n    if (fileModifications !== undefined) {\n      const diff = fileModificationsToHTML(fileModifications);\n\n      /**\n       * If we have file modifications we append a new user message manually since we have to prefix\n       * the user input with the file modifications and we don't want the new user input to appear\n       * in the prompt. Using `append` is almost the same as `handleSubmit` except that we have to\n       * manually reset the input and we'd have to manually pass in file attachments. However, those\n       * aren't relevant here.\n       */\n      append({ role: 'user', content: `${diff}\\n\\n${_input}` });\n\n      /**\n       * After sending a new message we reset all modifications since the model\n       * should now be aware of all the changes.\n       */\n      workbenchStore.resetAllFileModifications();\n    } else {\n      append({ role: 'user', content: _input });\n    }\n\n    setInput('');\n\n    resetEnhancer();\n\n    textareaRef.current?.blur();\n  };\n\n  const [messageRef, scrollRef] = useSnapScroll();\n\n  return (\n    <BaseChat\n      ref={animationScope}\n      textareaRef={textareaRef}\n      input={input}\n      showChat={showChat}\n      chatStarted={chatStarted}\n      isStreaming={isLoading}\n      enhancingPrompt={enhancingPrompt}\n      promptEnhanced={promptEnhanced}\n      sendMessage={sendMessage}\n      messageRef={messageRef}\n      scrollRef={scrollRef}\n      handleInputChange={handleInputChange}\n      handleStop={abort}\n      messages={messages.map((message, i) => {\n        if (message.role === 'user') {\n          return message;\n        }\n\n        return {\n          ...message,\n          content: parsedMessages[i] || '',\n        };\n      })}\n      enhancePrompt={() => {\n        enhancePrompt(input, (input) => {\n          setInput(input);\n          scrollTextArea();\n        });\n      }}\n    />\n  );\n});\n"
  },
  {
    "path": "app/components/chat/CodeBlock.module.scss",
    "content": ".CopyButtonContainer {\n  button:before {\n    content: 'Copied';\n    font-size: 12px;\n    position: absolute;\n    left: -53px;\n    padding: 2px 6px;\n    height: 30px;\n  }\n}\n"
  },
  {
    "path": "app/components/chat/CodeBlock.tsx",
    "content": "import { memo, useEffect, useState } from 'react';\nimport { bundledLanguages, codeToHtml, isSpecialLang, type BundledLanguage, type SpecialLanguage } from 'shiki';\nimport { classNames } from '~/utils/classNames';\nimport { createScopedLogger } from '~/utils/logger';\n\nimport styles from './CodeBlock.module.scss';\n\nconst logger = createScopedLogger('CodeBlock');\n\ninterface CodeBlockProps {\n  className?: string;\n  code: string;\n  language?: BundledLanguage | SpecialLanguage;\n  theme?: 'light-plus' | 'dark-plus';\n  disableCopy?: boolean;\n}\n\nexport const CodeBlock = memo(\n  ({ className, code, language = 'plaintext', theme = 'dark-plus', disableCopy = false }: CodeBlockProps) => {\n    const [html, setHTML] = useState<string | undefined>(undefined);\n    const [copied, setCopied] = useState(false);\n\n    const copyToClipboard = () => {\n      if (copied) {\n        return;\n      }\n\n      navigator.clipboard.writeText(code);\n\n      setCopied(true);\n\n      setTimeout(() => {\n        setCopied(false);\n      }, 2000);\n    };\n\n    useEffect(() => {\n      if (language && !isSpecialLang(language) && !(language in bundledLanguages)) {\n        logger.warn(`Unsupported language '${language}'`);\n      }\n\n      logger.trace(`Language = ${language}`);\n\n      const processCode = async () => {\n        setHTML(await codeToHtml(code, { lang: language, theme }));\n      };\n\n      processCode();\n    }, [code]);\n\n    return (\n      <div className={classNames('relative group text-left', className)}>\n        <div\n          className={classNames(\n            styles.CopyButtonContainer,\n            'bg-white absolute top-[10px] right-[10px] rounded-md z-10 text-lg flex items-center justify-center opacity-0 group-hover:opacity-100',\n            {\n              'rounded-l-0 opacity-100': copied,\n            },\n          )}\n        >\n          {!disableCopy && (\n            <button\n              className={classNames(\n                'flex items-center bg-transparent p-[6px] justify-center before:bg-white before:rounded-l-md before:text-gray-500 before:border-r before:border-gray-300',\n                {\n                  'before:opacity-0': !copied,\n                  'before:opacity-100': copied,\n                },\n              )}\n              title=\"Copy Code\"\n              onClick={() => copyToClipboard()}\n            >\n              <div className=\"i-ph:clipboard-text-duotone\"></div>\n            </button>\n          )}\n        </div>\n        <div dangerouslySetInnerHTML={{ __html: html ?? '' }}></div>\n      </div>\n    );\n  },\n);\n"
  },
  {
    "path": "app/components/chat/Markdown.module.scss",
    "content": "$font-mono: ui-monospace, 'Fira Code', Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;\n$code-font-size: 13px;\n\n@mixin not-inside-actions {\n  &:not(:has(:global(.actions)), :global(.actions *)) {\n    @content;\n  }\n}\n\n.MarkdownContent {\n  line-height: 1.6;\n  color: var(--bolt-elements-textPrimary);\n\n  > *:not(:last-child) {\n    margin-block-end: 16px;\n  }\n\n  :global(.artifact) {\n    margin: 1.5em 0;\n  }\n\n  :is(h1, h2, h3, h4, h5, h6) {\n    @include not-inside-actions {\n      margin-block-start: 24px;\n      margin-block-end: 16px;\n      font-weight: 600;\n      line-height: 1.25;\n      color: var(--bolt-elements-textPrimary);\n    }\n  }\n\n  h1 {\n    font-size: 2em;\n    border-bottom: 1px solid var(--bolt-elements-borderColor);\n    padding-bottom: 0.3em;\n  }\n\n  h2 {\n    font-size: 1.5em;\n    border-bottom: 1px solid var(--bolt-elements-borderColor);\n    padding-bottom: 0.3em;\n  }\n\n  h3 {\n    font-size: 1.25em;\n  }\n\n  h4 {\n    font-size: 1em;\n  }\n\n  h5 {\n    font-size: 0.875em;\n  }\n\n  h6 {\n    font-size: 0.85em;\n    color: #6a737d;\n  }\n\n  p {\n    white-space: pre-wrap;\n\n    &:not(:last-of-type) {\n      margin-block-start: 0;\n      margin-block-end: 16px;\n    }\n  }\n\n  a {\n    color: var(--bolt-elements-messages-linkColor);\n    text-decoration: none;\n    cursor: pointer;\n\n    &:hover {\n      text-decoration: underline;\n    }\n  }\n\n  :not(pre) > code {\n    font-family: $font-mono;\n    font-size: $code-font-size;\n\n    @include not-inside-actions {\n      border-radius: 6px;\n      padding: 0.2em 0.4em;\n      background-color: var(--bolt-elements-messages-inlineCode-background);\n      color: var(--bolt-elements-messages-inlineCode-text);\n    }\n  }\n\n  pre {\n    padding: 20px 16px;\n    border-radius: 6px;\n  }\n\n  pre:has(> code) {\n    font-family: $font-mono;\n    font-size: $code-font-size;\n    background: transparent;\n    overflow-x: auto;\n    min-width: 0;\n  }\n\n  blockquote {\n    margin: 0;\n    padding: 0 1em;\n    color: var(--bolt-elements-textTertiary);\n    border-left: 0.25em solid var(--bolt-elements-borderColor);\n  }\n\n  :is(ul, ol) {\n    @include not-inside-actions {\n      padding-left: 2em;\n      margin-block-start: 0;\n      margin-block-end: 16px;\n    }\n  }\n\n  ul {\n    @include not-inside-actions {\n      list-style-type: disc;\n    }\n  }\n\n  ol {\n    @include not-inside-actions {\n      list-style-type: decimal;\n    }\n  }\n\n  li {\n    @include not-inside-actions {\n      & + li {\n        margin-block-start: 8px;\n      }\n\n      > *:not(:last-child) {\n        margin-block-end: 16px;\n      }\n    }\n  }\n\n  img {\n    max-width: 100%;\n    box-sizing: border-box;\n  }\n\n  hr {\n    height: 0.25em;\n    padding: 0;\n    margin: 24px 0;\n    background-color: var(--bolt-elements-borderColor);\n    border: 0;\n  }\n\n  table {\n    border-collapse: collapse;\n    width: 100%;\n    margin-block-end: 16px;\n\n    :is(th, td) {\n      padding: 6px 13px;\n      border: 1px solid #dfe2e5;\n    }\n\n    tr:nth-child(2n) {\n      background-color: #f6f8fa;\n    }\n  }\n}\n"
  },
  {
    "path": "app/components/chat/Markdown.tsx",
    "content": "import { memo, useMemo } from 'react';\nimport ReactMarkdown, { type Components } from 'react-markdown';\nimport type { BundledLanguage } from 'shiki';\nimport { createScopedLogger } from '~/utils/logger';\nimport { rehypePlugins, remarkPlugins, allowedHTMLElements } from '~/utils/markdown';\nimport { Artifact } from './Artifact';\nimport { CodeBlock } from './CodeBlock';\n\nimport styles from './Markdown.module.scss';\n\nconst logger = createScopedLogger('MarkdownComponent');\n\ninterface MarkdownProps {\n  children: string;\n  html?: boolean;\n  limitedMarkdown?: boolean;\n}\n\nexport const Markdown = memo(({ children, html = false, limitedMarkdown = false }: MarkdownProps) => {\n  logger.trace('Render');\n\n  const components = useMemo(() => {\n    return {\n      div: ({ className, children, node, ...props }) => {\n        if (className?.includes('__boltArtifact__')) {\n          const messageId = node?.properties.dataMessageId as string;\n\n          if (!messageId) {\n            logger.error(`Invalid message id ${messageId}`);\n          }\n\n          return <Artifact messageId={messageId} />;\n        }\n\n        return (\n          <div className={className} {...props}>\n            {children}\n          </div>\n        );\n      },\n      pre: (props) => {\n        const { children, node, ...rest } = props;\n\n        const [firstChild] = node?.children ?? [];\n\n        if (\n          firstChild &&\n          firstChild.type === 'element' &&\n          firstChild.tagName === 'code' &&\n          firstChild.children[0].type === 'text'\n        ) {\n          const { className, ...rest } = firstChild.properties;\n          const [, language = 'plaintext'] = /language-(\\w+)/.exec(String(className) || '') ?? [];\n\n          return <CodeBlock code={firstChild.children[0].value} language={language as BundledLanguage} {...rest} />;\n        }\n\n        return <pre {...rest}>{children}</pre>;\n      },\n    } satisfies Components;\n  }, []);\n\n  return (\n    <ReactMarkdown\n      allowedElements={allowedHTMLElements}\n      className={styles.MarkdownContent}\n      components={components}\n      remarkPlugins={remarkPlugins(limitedMarkdown)}\n      rehypePlugins={rehypePlugins(html)}\n    >\n      {children}\n    </ReactMarkdown>\n  );\n});\n"
  },
  {
    "path": "app/components/chat/Messages.client.tsx",
    "content": "import type { Message } from 'ai';\nimport React from 'react';\nimport { classNames } from '~/utils/classNames';\nimport { AssistantMessage } from './AssistantMessage';\nimport { UserMessage } from './UserMessage';\n\ninterface MessagesProps {\n  id?: string;\n  className?: string;\n  isStreaming?: boolean;\n  messages?: Message[];\n}\n\nexport const Messages = React.forwardRef<HTMLDivElement, MessagesProps>((props: MessagesProps, ref) => {\n  const { id, isStreaming = false, messages = [] } = props;\n\n  return (\n    <div id={id} ref={ref} className={props.className}>\n      {messages.length > 0\n        ? messages.map((message, index) => {\n            const { role, content } = message;\n            const isUserMessage = role === 'user';\n            const isFirst = index === 0;\n            const isLast = index === messages.length - 1;\n\n            return (\n              <div\n                key={index}\n                className={classNames('flex gap-4 p-6 w-full rounded-[calc(0.75rem-1px)]', {\n                  'bg-bolt-elements-messages-background': isUserMessage || !isStreaming || (isStreaming && !isLast),\n                  'bg-gradient-to-b from-bolt-elements-messages-background from-30% to-transparent':\n                    isStreaming && isLast,\n                  'mt-4': !isFirst,\n                })}\n              >\n                {isUserMessage && (\n                  <div className=\"flex items-center justify-center w-[34px] h-[34px] overflow-hidden bg-white text-gray-600 rounded-full shrink-0 self-start\">\n                    <div className=\"i-ph:user-fill text-xl\"></div>\n                  </div>\n                )}\n                <div className=\"grid grid-col-1 w-full\">\n                  {isUserMessage ? <UserMessage content={content} /> : <AssistantMessage content={content} />}\n                </div>\n              </div>\n            );\n          })\n        : null}\n      {isStreaming && (\n        <div className=\"text-center w-full text-bolt-elements-textSecondary i-svg-spinners:3-dots-fade text-4xl mt-4\"></div>\n      )}\n    </div>\n  );\n});\n"
  },
  {
    "path": "app/components/chat/SendButton.client.tsx",
    "content": "import { AnimatePresence, cubicBezier, motion } from 'framer-motion';\n\ninterface SendButtonProps {\n  show: boolean;\n  isStreaming?: boolean;\n  onClick?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;\n}\n\nconst customEasingFn = cubicBezier(0.4, 0, 0.2, 1);\n\nexport function SendButton({ show, isStreaming, onClick }: SendButtonProps) {\n  return (\n    <AnimatePresence>\n      {show ? (\n        <motion.button\n          className=\"absolute flex justify-center items-center top-[18px] right-[22px] p-1 bg-accent-500 hover:brightness-94 color-white rounded-md w-[34px] h-[34px] transition-theme\"\n          transition={{ ease: customEasingFn, duration: 0.17 }}\n          initial={{ opacity: 0, y: 10 }}\n          animate={{ opacity: 1, y: 0 }}\n          exit={{ opacity: 0, y: 10 }}\n          onClick={(event) => {\n            event.preventDefault();\n            onClick?.(event);\n          }}\n        >\n          <div className=\"text-lg\">\n            {!isStreaming ? <div className=\"i-ph:arrow-right\"></div> : <div className=\"i-ph:stop-circle-bold\"></div>}\n          </div>\n        </motion.button>\n      ) : null}\n    </AnimatePresence>\n  );\n}\n"
  },
  {
    "path": "app/components/chat/UserMessage.tsx",
    "content": "import { modificationsRegex } from '~/utils/diff';\nimport { Markdown } from './Markdown';\n\ninterface UserMessageProps {\n  content: string;\n}\n\nexport function UserMessage({ content }: UserMessageProps) {\n  return (\n    <div className=\"overflow-hidden pt-[4px]\">\n      <Markdown limitedMarkdown>{sanitizeUserMessage(content)}</Markdown>\n    </div>\n  );\n}\n\nfunction sanitizeUserMessage(content: string) {\n  return content.replace(modificationsRegex, '').trim();\n}\n"
  },
  {
    "path": "app/components/editor/codemirror/BinaryContent.tsx",
    "content": "export function BinaryContent() {\n  return (\n    <div className=\"flex items-center justify-center absolute inset-0 z-10 text-sm bg-tk-elements-app-backgroundColor text-tk-elements-app-textColor\">\n      File format cannot be displayed.\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/components/editor/codemirror/CodeMirrorEditor.tsx",
    "content": "import { acceptCompletion, autocompletion, closeBrackets } from '@codemirror/autocomplete';\nimport { defaultKeymap, history, historyKeymap } from '@codemirror/commands';\nimport { bracketMatching, foldGutter, indentOnInput, indentUnit } from '@codemirror/language';\nimport { searchKeymap } from '@codemirror/search';\nimport { Compartment, EditorSelection, EditorState, StateEffect, StateField, type Extension } from '@codemirror/state';\nimport {\n  drawSelection,\n  dropCursor,\n  EditorView,\n  highlightActiveLine,\n  highlightActiveLineGutter,\n  keymap,\n  lineNumbers,\n  scrollPastEnd,\n  showTooltip,\n  tooltips,\n  type Tooltip,\n} from '@codemirror/view';\nimport { memo, useEffect, useRef, useState, type MutableRefObject } from 'react';\nimport type { Theme } from '~/types/theme';\nimport { classNames } from '~/utils/classNames';\nimport { debounce } from '~/utils/debounce';\nimport { createScopedLogger, renderLogger } from '~/utils/logger';\nimport { BinaryContent } from './BinaryContent';\nimport { getTheme, reconfigureTheme } from './cm-theme';\nimport { indentKeyBinding } from './indent';\nimport { getLanguage } from './languages';\n\nconst logger = createScopedLogger('CodeMirrorEditor');\n\nexport interface EditorDocument {\n  value: string;\n  isBinary: boolean;\n  filePath: string;\n  scroll?: ScrollPosition;\n}\n\nexport interface EditorSettings {\n  fontSize?: string;\n  gutterFontSize?: string;\n  tabSize?: number;\n}\n\ntype TextEditorDocument = EditorDocument & {\n  value: string;\n};\n\nexport interface ScrollPosition {\n  top: number;\n  left: number;\n}\n\nexport interface EditorUpdate {\n  selection: EditorSelection;\n  content: string;\n}\n\nexport type OnChangeCallback = (update: EditorUpdate) => void;\nexport type OnScrollCallback = (position: ScrollPosition) => void;\nexport type OnSaveCallback = () => void;\n\ninterface Props {\n  theme: Theme;\n  id?: unknown;\n  doc?: EditorDocument;\n  editable?: boolean;\n  debounceChange?: number;\n  debounceScroll?: number;\n  autoFocusOnDocumentChange?: boolean;\n  onChange?: OnChangeCallback;\n  onScroll?: OnScrollCallback;\n  onSave?: OnSaveCallback;\n  className?: string;\n  settings?: EditorSettings;\n}\n\ntype EditorStates = Map<string, EditorState>;\n\nconst readOnlyTooltipStateEffect = StateEffect.define<boolean>();\n\nconst editableTooltipField = StateField.define<readonly Tooltip[]>({\n  create: () => [],\n  update(_tooltips, transaction) {\n    if (!transaction.state.readOnly) {\n      return [];\n    }\n\n    for (const effect of transaction.effects) {\n      if (effect.is(readOnlyTooltipStateEffect) && effect.value) {\n        return getReadOnlyTooltip(transaction.state);\n      }\n    }\n\n    return [];\n  },\n  provide: (field) => {\n    return showTooltip.computeN([field], (state) => state.field(field));\n  },\n});\n\nconst editableStateEffect = StateEffect.define<boolean>();\n\nconst editableStateField = StateField.define<boolean>({\n  create() {\n    return true;\n  },\n  update(value, transaction) {\n    for (const effect of transaction.effects) {\n      if (effect.is(editableStateEffect)) {\n        return effect.value;\n      }\n    }\n\n    return value;\n  },\n});\n\nexport const CodeMirrorEditor = memo(\n  ({\n    id,\n    doc,\n    debounceScroll = 100,\n    debounceChange = 150,\n    autoFocusOnDocumentChange = false,\n    editable = true,\n    onScroll,\n    onChange,\n    onSave,\n    theme,\n    settings,\n    className = '',\n  }: Props) => {\n    renderLogger.trace('CodeMirrorEditor');\n\n    const [languageCompartment] = useState(new Compartment());\n\n    const containerRef = useRef<HTMLDivElement | null>(null);\n    const viewRef = useRef<EditorView>();\n    const themeRef = useRef<Theme>();\n    const docRef = useRef<EditorDocument>();\n    const editorStatesRef = useRef<EditorStates>();\n    const onScrollRef = useRef(onScroll);\n    const onChangeRef = useRef(onChange);\n    const onSaveRef = useRef(onSave);\n\n    /**\n     * This effect is used to avoid side effects directly in the render function\n     * and instead the refs are updated after each render.\n     */\n    useEffect(() => {\n      onScrollRef.current = onScroll;\n      onChangeRef.current = onChange;\n      onSaveRef.current = onSave;\n      docRef.current = doc;\n      themeRef.current = theme;\n    });\n\n    useEffect(() => {\n      const onUpdate = debounce((update: EditorUpdate) => {\n        onChangeRef.current?.(update);\n      }, debounceChange);\n\n      const view = new EditorView({\n        parent: containerRef.current!,\n        dispatchTransactions(transactions) {\n          const previousSelection = view.state.selection;\n\n          view.update(transactions);\n\n          const newSelection = view.state.selection;\n\n          const selectionChanged =\n            newSelection !== previousSelection &&\n            (newSelection === undefined || previousSelection === undefined || !newSelection.eq(previousSelection));\n\n          if (docRef.current && (transactions.some((transaction) => transaction.docChanged) || selectionChanged)) {\n            onUpdate({\n              selection: view.state.selection,\n              content: view.state.doc.toString(),\n            });\n\n            editorStatesRef.current!.set(docRef.current.filePath, view.state);\n          }\n        },\n      });\n\n      viewRef.current = view;\n\n      return () => {\n        viewRef.current?.destroy();\n        viewRef.current = undefined;\n      };\n    }, []);\n\n    useEffect(() => {\n      if (!viewRef.current) {\n        return;\n      }\n\n      viewRef.current.dispatch({\n        effects: [reconfigureTheme(theme)],\n      });\n    }, [theme]);\n\n    useEffect(() => {\n      editorStatesRef.current = new Map<string, EditorState>();\n    }, [id]);\n\n    useEffect(() => {\n      const editorStates = editorStatesRef.current!;\n      const view = viewRef.current!;\n      const theme = themeRef.current!;\n\n      if (!doc) {\n        const state = newEditorState('', theme, settings, onScrollRef, debounceScroll, onSaveRef, [\n          languageCompartment.of([]),\n        ]);\n\n        view.setState(state);\n\n        setNoDocument(view);\n\n        return;\n      }\n\n      if (doc.isBinary) {\n        return;\n      }\n\n      if (doc.filePath === '') {\n        logger.warn('File path should not be empty');\n      }\n\n      let state = editorStates.get(doc.filePath);\n\n      if (!state) {\n        state = newEditorState(doc.value, theme, settings, onScrollRef, debounceScroll, onSaveRef, [\n          languageCompartment.of([]),\n        ]);\n\n        editorStates.set(doc.filePath, state);\n      }\n\n      view.setState(state);\n\n      setEditorDocument(\n        view,\n        theme,\n        editable,\n        languageCompartment,\n        autoFocusOnDocumentChange,\n        doc as TextEditorDocument,\n      );\n    }, [doc?.value, editable, doc?.filePath, autoFocusOnDocumentChange]);\n\n    return (\n      <div className={classNames('relative h-full', className)}>\n        {doc?.isBinary && <BinaryContent />}\n        <div className=\"h-full overflow-hidden\" ref={containerRef} />\n      </div>\n    );\n  },\n);\n\nexport default CodeMirrorEditor;\n\nCodeMirrorEditor.displayName = 'CodeMirrorEditor';\n\nfunction newEditorState(\n  content: string,\n  theme: Theme,\n  settings: EditorSettings | undefined,\n  onScrollRef: MutableRefObject<OnScrollCallback | undefined>,\n  debounceScroll: number,\n  onFileSaveRef: MutableRefObject<OnSaveCallback | undefined>,\n  extensions: Extension[],\n) {\n  return EditorState.create({\n    doc: content,\n    extensions: [\n      EditorView.domEventHandlers({\n        scroll: debounce((event, view) => {\n          if (event.target !== view.scrollDOM) {\n            return;\n          }\n\n          onScrollRef.current?.({ left: view.scrollDOM.scrollLeft, top: view.scrollDOM.scrollTop });\n        }, debounceScroll),\n        keydown: (event, view) => {\n          if (view.state.readOnly) {\n            view.dispatch({\n              effects: [readOnlyTooltipStateEffect.of(event.key !== 'Escape')],\n            });\n\n            return true;\n          }\n\n          return false;\n        },\n      }),\n      getTheme(theme, settings),\n      history(),\n      keymap.of([\n        ...defaultKeymap,\n        ...historyKeymap,\n        ...searchKeymap,\n        { key: 'Tab', run: acceptCompletion },\n        {\n          key: 'Mod-s',\n          preventDefault: true,\n          run: () => {\n            onFileSaveRef.current?.();\n            return true;\n          },\n        },\n        indentKeyBinding,\n      ]),\n      indentUnit.of('\\t'),\n      autocompletion({\n        closeOnBlur: false,\n      }),\n      tooltips({\n        position: 'absolute',\n        parent: document.body,\n        tooltipSpace: (view) => {\n          const rect = view.dom.getBoundingClientRect();\n\n          return {\n            top: rect.top - 50,\n            left: rect.left,\n            bottom: rect.bottom,\n            right: rect.right + 10,\n          };\n        },\n      }),\n      closeBrackets(),\n      lineNumbers(),\n      scrollPastEnd(),\n      dropCursor(),\n      drawSelection(),\n      bracketMatching(),\n      EditorState.tabSize.of(settings?.tabSize ?? 2),\n      indentOnInput(),\n      editableTooltipField,\n      editableStateField,\n      EditorState.readOnly.from(editableStateField, (editable) => !editable),\n      highlightActiveLineGutter(),\n      highlightActiveLine(),\n      foldGutter({\n        markerDOM: (open) => {\n          const icon = document.createElement('div');\n\n          icon.className = `fold-icon ${open ? 'i-ph-caret-down-bold' : 'i-ph-caret-right-bold'}`;\n\n          return icon;\n        },\n      }),\n      ...extensions,\n    ],\n  });\n}\n\nfunction setNoDocument(view: EditorView) {\n  view.dispatch({\n    selection: { anchor: 0 },\n    changes: {\n      from: 0,\n      to: view.state.doc.length,\n      insert: '',\n    },\n  });\n\n  view.scrollDOM.scrollTo(0, 0);\n}\n\nfunction setEditorDocument(\n  view: EditorView,\n  theme: Theme,\n  editable: boolean,\n  languageCompartment: Compartment,\n  autoFocus: boolean,\n  doc: TextEditorDocument,\n) {\n  if (doc.value !== view.state.doc.toString()) {\n    view.dispatch({\n      selection: { anchor: 0 },\n      changes: {\n        from: 0,\n        to: view.state.doc.length,\n        insert: doc.value,\n      },\n    });\n  }\n\n  view.dispatch({\n    effects: [editableStateEffect.of(editable && !doc.isBinary)],\n  });\n\n  getLanguage(doc.filePath).then((languageSupport) => {\n    if (!languageSupport) {\n      return;\n    }\n\n    view.dispatch({\n      effects: [languageCompartment.reconfigure([languageSupport]), reconfigureTheme(theme)],\n    });\n\n    requestAnimationFrame(() => {\n      const currentLeft = view.scrollDOM.scrollLeft;\n      const currentTop = view.scrollDOM.scrollTop;\n      const newLeft = doc.scroll?.left ?? 0;\n      const newTop = doc.scroll?.top ?? 0;\n\n      const needsScrolling = currentLeft !== newLeft || currentTop !== newTop;\n\n      if (autoFocus && editable) {\n        if (needsScrolling) {\n          // we have to wait until the scroll position was changed before we can set the focus\n          view.scrollDOM.addEventListener(\n            'scroll',\n            () => {\n              view.focus();\n            },\n            { once: true },\n          );\n        } else {\n          // if the scroll position is still the same we can focus immediately\n          view.focus();\n        }\n      }\n\n      view.scrollDOM.scrollTo(newLeft, newTop);\n    });\n  });\n}\n\nfunction getReadOnlyTooltip(state: EditorState) {\n  if (!state.readOnly) {\n    return [];\n  }\n\n  return state.selection.ranges\n    .filter((range) => {\n      return range.empty;\n    })\n    .map((range) => {\n      return {\n        pos: range.head,\n        above: true,\n        strictSide: true,\n        arrow: true,\n        create: () => {\n          const divElement = document.createElement('div');\n          divElement.className = 'cm-readonly-tooltip';\n          divElement.textContent = 'Cannot edit file while AI response is being generated';\n\n          return { dom: divElement };\n        },\n      };\n    });\n}\n"
  },
  {
    "path": "app/components/editor/codemirror/cm-theme.ts",
    "content": "import { Compartment, type Extension } from '@codemirror/state';\nimport { EditorView } from '@codemirror/view';\nimport { vscodeDark, vscodeLight } from '@uiw/codemirror-theme-vscode';\nimport type { Theme } from '~/types/theme.js';\nimport type { EditorSettings } from './CodeMirrorEditor.js';\n\nexport const darkTheme = EditorView.theme({}, { dark: true });\nexport const themeSelection = new Compartment();\n\nexport function getTheme(theme: Theme, settings: EditorSettings = {}): Extension {\n  return [\n    getEditorTheme(settings),\n    theme === 'dark' ? themeSelection.of([getDarkTheme()]) : themeSelection.of([getLightTheme()]),\n  ];\n}\n\nexport function reconfigureTheme(theme: Theme) {\n  return themeSelection.reconfigure(theme === 'dark' ? getDarkTheme() : getLightTheme());\n}\n\nfunction getEditorTheme(settings: EditorSettings) {\n  return EditorView.theme({\n    '&': {\n      fontSize: settings.fontSize ?? '12px',\n    },\n    '&.cm-editor': {\n      height: '100%',\n      background: 'var(--cm-backgroundColor)',\n      color: 'var(--cm-textColor)',\n    },\n    '.cm-cursor': {\n      borderLeft: 'var(--cm-cursor-width) solid var(--cm-cursor-backgroundColor)',\n    },\n    '.cm-scroller': {\n      lineHeight: '1.5',\n      '&:focus-visible': {\n        outline: 'none',\n      },\n    },\n    '.cm-line': {\n      padding: '0 0 0 4px',\n    },\n    '&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground': {\n      backgroundColor: 'var(--cm-selection-backgroundColorFocused) !important',\n      opacity: 'var(--cm-selection-backgroundOpacityFocused, 0.3)',\n    },\n    '&:not(.cm-focused) > .cm-scroller > .cm-selectionLayer .cm-selectionBackground': {\n      backgroundColor: 'var(--cm-selection-backgroundColorBlured)',\n      opacity: 'var(--cm-selection-backgroundOpacityBlured, 0.3)',\n    },\n    '&.cm-focused > .cm-scroller .cm-matchingBracket': {\n      backgroundColor: 'var(--cm-matching-bracket)',\n    },\n    '.cm-activeLine': {\n      background: 'var(--cm-activeLineBackgroundColor)',\n    },\n    '.cm-gutters': {\n      background: 'var(--cm-gutter-backgroundColor)',\n      borderRight: 0,\n      color: 'var(--cm-gutter-textColor)',\n    },\n    '.cm-gutter': {\n      '&.cm-lineNumbers': {\n        fontFamily: 'Roboto Mono, monospace',\n        fontSize: settings.gutterFontSize ?? settings.fontSize ?? '12px',\n        minWidth: '40px',\n      },\n      '& .cm-activeLineGutter': {\n        background: 'transparent',\n        color: 'var(--cm-gutter-activeLineTextColor)',\n      },\n      '&.cm-foldGutter .cm-gutterElement > .fold-icon': {\n        cursor: 'pointer',\n        color: 'var(--cm-foldGutter-textColor)',\n        transform: 'translateY(2px)',\n        '&:hover': {\n          color: 'var(--cm-foldGutter-textColorHover)',\n        },\n      },\n    },\n    '.cm-foldGutter .cm-gutterElement': {\n      padding: '0 4px',\n    },\n    '.cm-tooltip-autocomplete > ul > li': {\n      minHeight: '18px',\n    },\n    '.cm-panel.cm-search label': {\n      marginLeft: '2px',\n      fontSize: '12px',\n    },\n    '.cm-panel.cm-search .cm-button': {\n      fontSize: '12px',\n    },\n    '.cm-panel.cm-search .cm-textfield': {\n      fontSize: '12px',\n    },\n    '.cm-panel.cm-search input[type=checkbox]': {\n      position: 'relative',\n      transform: 'translateY(2px)',\n      marginRight: '4px',\n    },\n    '.cm-panels': {\n      borderColor: 'var(--cm-panels-borderColor)',\n    },\n    '.cm-panels-bottom': {\n      borderTop: '1px solid var(--cm-panels-borderColor)',\n      backgroundColor: 'transparent',\n    },\n    '.cm-panel.cm-search': {\n      background: 'var(--cm-search-backgroundColor)',\n      color: 'var(--cm-search-textColor)',\n      padding: '8px',\n    },\n    '.cm-search .cm-button': {\n      background: 'var(--cm-search-button-backgroundColor)',\n      borderColor: 'var(--cm-search-button-borderColor)',\n      color: 'var(--cm-search-button-textColor)',\n      borderRadius: '4px',\n      '&:hover': {\n        color: 'var(--cm-search-button-textColorHover)',\n      },\n      '&:focus-visible': {\n        outline: 'none',\n        borderColor: 'var(--cm-search-button-borderColorFocused)',\n      },\n      '&:hover:not(:focus-visible)': {\n        background: 'var(--cm-search-button-backgroundColorHover)',\n        borderColor: 'var(--cm-search-button-borderColorHover)',\n      },\n      '&:hover:focus-visible': {\n        background: 'var(--cm-search-button-backgroundColorHover)',\n        borderColor: 'var(--cm-search-button-borderColorFocused)',\n      },\n    },\n    '.cm-panel.cm-search [name=close]': {\n      top: '6px',\n      right: '6px',\n      padding: '0 6px',\n      fontSize: '1rem',\n      backgroundColor: 'var(--cm-search-closeButton-backgroundColor)',\n      color: 'var(--cm-search-closeButton-textColor)',\n      '&:hover': {\n        'border-radius': '6px',\n        color: 'var(--cm-search-closeButton-textColorHover)',\n        backgroundColor: 'var(--cm-search-closeButton-backgroundColorHover)',\n      },\n    },\n    '.cm-search input': {\n      background: 'var(--cm-search-input-backgroundColor)',\n      borderColor: 'var(--cm-search-input-borderColor)',\n      color: 'var(--cm-search-input-textColor)',\n      outline: 'none',\n      borderRadius: '4px',\n      '&:focus-visible': {\n        borderColor: 'var(--cm-search-input-borderColorFocused)',\n      },\n    },\n    '.cm-tooltip': {\n      background: 'var(--cm-tooltip-backgroundColor)',\n      border: '1px solid transparent',\n      borderColor: 'var(--cm-tooltip-borderColor)',\n      color: 'var(--cm-tooltip-textColor)',\n    },\n    '.cm-tooltip.cm-tooltip-autocomplete ul li[aria-selected]': {\n      background: 'var(--cm-tooltip-backgroundColorSelected)',\n      color: 'var(--cm-tooltip-textColorSelected)',\n    },\n    '.cm-searchMatch': {\n      backgroundColor: 'var(--cm-searchMatch-backgroundColor)',\n    },\n    '.cm-tooltip.cm-readonly-tooltip': {\n      padding: '4px',\n      whiteSpace: 'nowrap',\n      backgroundColor: 'var(--bolt-elements-bg-depth-2)',\n      borderColor: 'var(--bolt-elements-borderColorActive)',\n      '& .cm-tooltip-arrow:before': {\n        borderTopColor: 'var(--bolt-elements-borderColorActive)',\n      },\n      '& .cm-tooltip-arrow:after': {\n        borderTopColor: 'transparent',\n      },\n    },\n  });\n}\n\nfunction getLightTheme() {\n  return vscodeLight;\n}\n\nfunction getDarkTheme() {\n  return vscodeDark;\n}\n"
  },
  {
    "path": "app/components/editor/codemirror/indent.ts",
    "content": "import { indentLess } from '@codemirror/commands';\nimport { indentUnit } from '@codemirror/language';\nimport { EditorSelection, EditorState, Line, type ChangeSpec } from '@codemirror/state';\nimport { EditorView, type KeyBinding } from '@codemirror/view';\n\nexport const indentKeyBinding: KeyBinding = {\n  key: 'Tab',\n  run: indentMore,\n  shift: indentLess,\n};\n\nfunction indentMore({ state, dispatch }: EditorView) {\n  if (state.readOnly) {\n    return false;\n  }\n\n  dispatch(\n    state.update(\n      changeBySelectedLine(state, (from, to, changes) => {\n        changes.push({ from, to, insert: state.facet(indentUnit) });\n      }),\n      { userEvent: 'input.indent' },\n    ),\n  );\n\n  return true;\n}\n\nfunction changeBySelectedLine(\n  state: EditorState,\n  cb: (from: number, to: number | undefined, changes: ChangeSpec[], line: Line) => void,\n) {\n  return state.changeByRange((range) => {\n    const changes: ChangeSpec[] = [];\n\n    const line = state.doc.lineAt(range.from);\n\n    // just insert single indent unit at the current cursor position\n    if (range.from === range.to) {\n      cb(range.from, undefined, changes, line);\n    }\n    // handle the case when multiple characters are selected in a single line\n    else if (range.from < range.to && range.to <= line.to) {\n      cb(range.from, range.to, changes, line);\n    } else {\n      let atLine = -1;\n\n      // handle the case when selection spans multiple lines\n      for (let pos = range.from; pos <= range.to; ) {\n        const line = state.doc.lineAt(pos);\n\n        if (line.number > atLine && (range.empty || range.to > line.from)) {\n          cb(line.from, undefined, changes, line);\n          atLine = line.number;\n        }\n\n        pos = line.to + 1;\n      }\n    }\n\n    const changeSet = state.changes(changes);\n\n    return {\n      changes,\n      range: EditorSelection.range(changeSet.mapPos(range.anchor, 1), changeSet.mapPos(range.head, 1)),\n    };\n  });\n}\n"
  },
  {
    "path": "app/components/editor/codemirror/languages.ts",
    "content": "import { LanguageDescription } from '@codemirror/language';\n\nexport const supportedLanguages = [\n  LanguageDescription.of({\n    name: 'TS',\n    extensions: ['ts'],\n    async load() {\n      return import('@codemirror/lang-javascript').then((module) => module.javascript({ typescript: true }));\n    },\n  }),\n  LanguageDescription.of({\n    name: 'JS',\n    extensions: ['js', 'mjs', 'cjs'],\n    async load() {\n      return import('@codemirror/lang-javascript').then((module) => module.javascript());\n    },\n  }),\n  LanguageDescription.of({\n    name: 'TSX',\n    extensions: ['tsx'],\n    async load() {\n      return import('@codemirror/lang-javascript').then((module) => module.javascript({ jsx: true, typescript: true }));\n    },\n  }),\n  LanguageDescription.of({\n    name: 'JSX',\n    extensions: ['jsx'],\n    async load() {\n      return import('@codemirror/lang-javascript').then((module) => module.javascript({ jsx: true }));\n    },\n  }),\n  LanguageDescription.of({\n    name: 'HTML',\n    extensions: ['html'],\n    async load() {\n      return import('@codemirror/lang-html').then((module) => module.html());\n    },\n  }),\n  LanguageDescription.of({\n    name: 'CSS',\n    extensions: ['css'],\n    async load() {\n      return import('@codemirror/lang-css').then((module) => module.css());\n    },\n  }),\n  LanguageDescription.of({\n    name: 'SASS',\n    extensions: ['sass'],\n    async load() {\n      return import('@codemirror/lang-sass').then((module) => module.sass({ indented: true }));\n    },\n  }),\n  LanguageDescription.of({\n    name: 'SCSS',\n    extensions: ['scss'],\n    async load() {\n      return import('@codemirror/lang-sass').then((module) => module.sass({ indented: false }));\n    },\n  }),\n  LanguageDescription.of({\n    name: 'JSON',\n    extensions: ['json'],\n    async load() {\n      return import('@codemirror/lang-json').then((module) => module.json());\n    },\n  }),\n  LanguageDescription.of({\n    name: 'Markdown',\n    extensions: ['md'],\n    async load() {\n      return import('@codemirror/lang-markdown').then((module) => module.markdown());\n    },\n  }),\n  LanguageDescription.of({\n    name: 'Wasm',\n    extensions: ['wat'],\n    async load() {\n      return import('@codemirror/lang-wast').then((module) => module.wast());\n    },\n  }),\n  LanguageDescription.of({\n    name: 'Python',\n    extensions: ['py'],\n    async load() {\n      return import('@codemirror/lang-python').then((module) => module.python());\n    },\n  }),\n  LanguageDescription.of({\n    name: 'C++',\n    extensions: ['cpp'],\n    async load() {\n      return import('@codemirror/lang-cpp').then((module) => module.cpp());\n    },\n  }),\n];\n\nexport async function getLanguage(fileName: string) {\n  const languageDescription = LanguageDescription.matchFilename(supportedLanguages, fileName);\n\n  if (languageDescription) {\n    return await languageDescription.load();\n  }\n\n  return undefined;\n}\n"
  },
  {
    "path": "app/components/header/Header.tsx",
    "content": "import { useStore } from '@nanostores/react';\nimport { ClientOnly } from 'remix-utils/client-only';\nimport { chatStore } from '~/lib/stores/chat';\nimport { classNames } from '~/utils/classNames';\nimport { HeaderActionButtons } from './HeaderActionButtons.client';\nimport { ChatDescription } from '~/lib/persistence/ChatDescription.client';\n\nexport function Header() {\n  const chat = useStore(chatStore);\n\n  return (\n    <header\n      className={classNames(\n        'flex items-center bg-bolt-elements-background-depth-1 p-5 border-b h-[var(--header-height)]',\n        {\n          'border-transparent': !chat.started,\n          'border-bolt-elements-borderColor': chat.started,\n        },\n      )}\n    >\n      <div className=\"flex items-center gap-2 z-logo text-bolt-elements-textPrimary cursor-pointer\">\n        <div className=\"i-ph:sidebar-simple-duotone text-xl\" />\n        <a href=\"/\" className=\"text-2xl font-semibold text-accent flex items-center\">\n          <span className=\"i-bolt:logo-text?mask w-[46px] inline-block\" />\n        </a>\n      </div>\n      <span className=\"flex-1 px-4 truncate text-center text-bolt-elements-textPrimary\">\n        <ClientOnly>{() => <ChatDescription />}</ClientOnly>\n      </span>\n      {chat.started && (\n        <ClientOnly>\n          {() => (\n            <div className=\"mr-1\">\n              <HeaderActionButtons />\n            </div>\n          )}\n        </ClientOnly>\n      )}\n    </header>\n  );\n}\n"
  },
  {
    "path": "app/components/header/HeaderActionButtons.client.tsx",
    "content": "import { useStore } from '@nanostores/react';\nimport { chatStore } from '~/lib/stores/chat';\nimport { workbenchStore } from '~/lib/stores/workbench';\nimport { classNames } from '~/utils/classNames';\n\ninterface HeaderActionButtonsProps {}\n\nexport function HeaderActionButtons({}: HeaderActionButtonsProps) {\n  const showWorkbench = useStore(workbenchStore.showWorkbench);\n  const { showChat } = useStore(chatStore);\n\n  const canHideChat = showWorkbench || !showChat;\n\n  return (\n    <div className=\"flex\">\n      <div className=\"flex border border-bolt-elements-borderColor rounded-md overflow-hidden\">\n        <Button\n          active={showChat}\n          disabled={!canHideChat}\n          onClick={() => {\n            if (canHideChat) {\n              chatStore.setKey('showChat', !showChat);\n            }\n          }}\n        >\n          <div className=\"i-bolt:chat text-sm\" />\n        </Button>\n        <div className=\"w-[1px] bg-bolt-elements-borderColor\" />\n        <Button\n          active={showWorkbench}\n          onClick={() => {\n            if (showWorkbench && !showChat) {\n              chatStore.setKey('showChat', true);\n            }\n\n            workbenchStore.showWorkbench.set(!showWorkbench);\n          }}\n        >\n          <div className=\"i-ph:code-bold\" />\n        </Button>\n      </div>\n    </div>\n  );\n}\n\ninterface ButtonProps {\n  active?: boolean;\n  disabled?: boolean;\n  children?: any;\n  onClick?: VoidFunction;\n}\n\nfunction Button({ active = false, disabled = false, children, onClick }: ButtonProps) {\n  return (\n    <button\n      className={classNames('flex items-center p-1.5', {\n        'bg-bolt-elements-item-backgroundDefault hover:bg-bolt-elements-item-backgroundActive text-bolt-elements-textTertiary hover:text-bolt-elements-textPrimary':\n          !active,\n        'bg-bolt-elements-item-backgroundAccent text-bolt-elements-item-contentAccent': active && !disabled,\n        'bg-bolt-elements-item-backgroundDefault text-alpha-gray-20 dark:text-alpha-white-20 cursor-not-allowed':\n          disabled,\n      })}\n      onClick={onClick}\n    >\n      {children}\n    </button>\n  );\n}\n"
  },
  {
    "path": "app/components/sidebar/HistoryItem.tsx",
    "content": "import * as Dialog from '@radix-ui/react-dialog';\nimport { useEffect, useRef, useState } from 'react';\nimport { type ChatHistoryItem } from '~/lib/persistence';\n\ninterface HistoryItemProps {\n  item: ChatHistoryItem;\n  onDelete?: (event: React.UIEvent) => void;\n}\n\nexport function HistoryItem({ item, onDelete }: HistoryItemProps) {\n  const [hovering, setHovering] = useState(false);\n  const hoverRef = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    let timeout: NodeJS.Timeout | undefined;\n\n    function mouseEnter() {\n      setHovering(true);\n\n      if (timeout) {\n        clearTimeout(timeout);\n      }\n    }\n\n    function mouseLeave() {\n      setHovering(false);\n    }\n\n    hoverRef.current?.addEventListener('mouseenter', mouseEnter);\n    hoverRef.current?.addEventListener('mouseleave', mouseLeave);\n\n    return () => {\n      hoverRef.current?.removeEventListener('mouseenter', mouseEnter);\n      hoverRef.current?.removeEventListener('mouseleave', mouseLeave);\n    };\n  }, []);\n\n  return (\n    <div\n      ref={hoverRef}\n      className=\"group rounded-md text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary hover:bg-bolt-elements-background-depth-3 overflow-hidden flex justify-between items-center px-2 py-1\"\n    >\n      <a href={`/chat/${item.urlId}`} className=\"flex w-full relative truncate block\">\n        {item.description}\n        <div className=\"absolute right-0 z-1 top-0 bottom-0 bg-gradient-to-l from-bolt-elements-background-depth-2 group-hover:from-bolt-elements-background-depth-3 to-transparent w-10 flex justify-end group-hover:w-15 group-hover:from-45%\">\n          {hovering && (\n            <div className=\"flex items-center p-1 text-bolt-elements-textSecondary hover:text-bolt-elements-item-contentDanger\">\n              <Dialog.Trigger asChild>\n                <button\n                  className=\"i-ph:trash scale-110\"\n                  onClick={(event) => {\n                    // we prevent the default so we don't trigger the anchor above\n                    event.preventDefault();\n                    onDelete?.(event);\n                  }}\n                />\n              </Dialog.Trigger>\n            </div>\n          )}\n        </div>\n      </a>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/components/sidebar/Menu.client.tsx",
    "content": "import { motion, type Variants } from 'framer-motion';\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport { toast } from 'react-toastify';\nimport { Dialog, DialogButton, DialogDescription, DialogRoot, DialogTitle } from '~/components/ui/Dialog';\nimport { IconButton } from '~/components/ui/IconButton';\nimport { ThemeSwitch } from '~/components/ui/ThemeSwitch';\nimport { db, deleteById, getAll, chatId, type ChatHistoryItem } from '~/lib/persistence';\nimport { cubicEasingFn } from '~/utils/easings';\nimport { logger } from '~/utils/logger';\nimport { HistoryItem } from './HistoryItem';\nimport { binDates } from './date-binning';\n\nconst menuVariants = {\n  closed: {\n    opacity: 0,\n    visibility: 'hidden',\n    left: '-150px',\n    transition: {\n      duration: 0.2,\n      ease: cubicEasingFn,\n    },\n  },\n  open: {\n    opacity: 1,\n    visibility: 'initial',\n    left: 0,\n    transition: {\n      duration: 0.2,\n      ease: cubicEasingFn,\n    },\n  },\n} satisfies Variants;\n\ntype DialogContent = { type: 'delete'; item: ChatHistoryItem } | null;\n\nexport function Menu() {\n  const menuRef = useRef<HTMLDivElement>(null);\n  const [list, setList] = useState<ChatHistoryItem[]>([]);\n  const [open, setOpen] = useState(false);\n  const [dialogContent, setDialogContent] = useState<DialogContent>(null);\n\n  const loadEntries = useCallback(() => {\n    if (db) {\n      getAll(db)\n        .then((list) => list.filter((item) => item.urlId && item.description))\n        .then(setList)\n        .catch((error) => toast.error(error.message));\n    }\n  }, []);\n\n  const deleteItem = useCallback((event: React.UIEvent, item: ChatHistoryItem) => {\n    event.preventDefault();\n\n    if (db) {\n      deleteById(db, item.id)\n        .then(() => {\n          loadEntries();\n\n          if (chatId.get() === item.id) {\n            // hard page navigation to clear the stores\n            window.location.pathname = '/';\n          }\n        })\n        .catch((error) => {\n          toast.error('Failed to delete conversation');\n          logger.error(error);\n        });\n    }\n  }, []);\n\n  const closeDialog = () => {\n    setDialogContent(null);\n  };\n\n  useEffect(() => {\n    if (open) {\n      loadEntries();\n    }\n  }, [open]);\n\n  useEffect(() => {\n    const enterThreshold = 40;\n    const exitThreshold = 40;\n\n    function onMouseMove(event: MouseEvent) {\n      if (event.pageX < enterThreshold) {\n        setOpen(true);\n      }\n\n      if (menuRef.current && event.clientX > menuRef.current.getBoundingClientRect().right + exitThreshold) {\n        setOpen(false);\n      }\n    }\n\n    window.addEventListener('mousemove', onMouseMove);\n\n    return () => {\n      window.removeEventListener('mousemove', onMouseMove);\n    };\n  }, []);\n\n  return (\n    <motion.div\n      ref={menuRef}\n      initial=\"closed\"\n      animate={open ? 'open' : 'closed'}\n      variants={menuVariants}\n      className=\"flex flex-col side-menu fixed top-0 w-[350px] h-full bg-bolt-elements-background-depth-2 border-r rounded-r-3xl border-bolt-elements-borderColor z-sidebar shadow-xl shadow-bolt-elements-sidebar-dropdownShadow text-sm\"\n    >\n      <div className=\"flex items-center h-[var(--header-height)]\">{/* Placeholder */}</div>\n      <div className=\"flex-1 flex flex-col h-full w-full overflow-hidden\">\n        <div className=\"p-4\">\n          <a\n            href=\"/\"\n            className=\"flex gap-2 items-center bg-bolt-elements-sidebar-buttonBackgroundDefault text-bolt-elements-sidebar-buttonText hover:bg-bolt-elements-sidebar-buttonBackgroundHover rounded-md p-2 transition-theme\"\n          >\n            <span className=\"inline-block i-bolt:chat scale-110\" />\n            Start new chat\n          </a>\n        </div>\n        <div className=\"text-bolt-elements-textPrimary font-medium pl-6 pr-5 my-2\">Your Chats</div>\n        <div className=\"flex-1 overflow-scroll pl-4 pr-5 pb-5\">\n          {list.length === 0 && <div className=\"pl-2 text-bolt-elements-textTertiary\">No previous conversations</div>}\n          <DialogRoot open={dialogContent !== null}>\n            {binDates(list).map(({ category, items }) => (\n              <div key={category} className=\"mt-4 first:mt-0 space-y-1\">\n                <div className=\"text-bolt-elements-textTertiary sticky top-0 z-1 bg-bolt-elements-background-depth-2 pl-2 pt-2 pb-1\">\n                  {category}\n                </div>\n                {items.map((item) => (\n                  <HistoryItem key={item.id} item={item} onDelete={() => setDialogContent({ type: 'delete', item })} />\n                ))}\n              </div>\n            ))}\n            <Dialog onBackdrop={closeDialog} onClose={closeDialog}>\n              {dialogContent?.type === 'delete' && (\n                <>\n                  <DialogTitle>Delete Chat?</DialogTitle>\n                  <DialogDescription asChild>\n                    <div>\n                      <p>\n                        You are about to delete <strong>{dialogContent.item.description}</strong>.\n                      </p>\n                      <p className=\"mt-1\">Are you sure you want to delete this chat?</p>\n                    </div>\n                  </DialogDescription>\n                  <div className=\"px-5 pb-4 bg-bolt-elements-background-depth-2 flex gap-2 justify-end\">\n                    <DialogButton type=\"secondary\" onClick={closeDialog}>\n                      Cancel\n                    </DialogButton>\n                    <DialogButton\n                      type=\"danger\"\n                      onClick={(event) => {\n                        deleteItem(event, dialogContent.item);\n                        closeDialog();\n                      }}\n                    >\n                      Delete\n                    </DialogButton>\n                  </div>\n                </>\n              )}\n            </Dialog>\n          </DialogRoot>\n        </div>\n        <div className=\"flex items-center border-t border-bolt-elements-borderColor p-4\">\n          <ThemeSwitch className=\"ml-auto\" />\n        </div>\n      </div>\n    </motion.div>\n  );\n}\n"
  },
  {
    "path": "app/components/sidebar/date-binning.ts",
    "content": "import { format, isAfter, isThisWeek, isThisYear, isToday, isYesterday, subDays } from 'date-fns';\nimport type { ChatHistoryItem } from '~/lib/persistence';\n\ntype Bin = { category: string; items: ChatHistoryItem[] };\n\nexport function binDates(_list: ChatHistoryItem[]) {\n  const list = _list.toSorted((a, b) => Date.parse(b.timestamp) - Date.parse(a.timestamp));\n\n  const binLookup: Record<string, Bin> = {};\n  const bins: Array<Bin> = [];\n\n  list.forEach((item) => {\n    const category = dateCategory(new Date(item.timestamp));\n\n    if (!(category in binLookup)) {\n      const bin = {\n        category,\n        items: [item],\n      };\n\n      binLookup[category] = bin;\n\n      bins.push(bin);\n    } else {\n      binLookup[category].items.push(item);\n    }\n  });\n\n  return bins;\n}\n\nfunction dateCategory(date: Date) {\n  if (isToday(date)) {\n    return 'Today';\n  }\n\n  if (isYesterday(date)) {\n    return 'Yesterday';\n  }\n\n  if (isThisWeek(date)) {\n    // e.g., \"Monday\"\n    return format(date, 'eeee');\n  }\n\n  const thirtyDaysAgo = subDays(new Date(), 30);\n\n  if (isAfter(date, thirtyDaysAgo)) {\n    return 'Last 30 Days';\n  }\n\n  if (isThisYear(date)) {\n    // e.g., \"July\"\n    return format(date, 'MMMM');\n  }\n\n  // e.g., \"July 2023\"\n  return format(date, 'MMMM yyyy');\n}\n"
  },
  {
    "path": "app/components/ui/Dialog.tsx",
    "content": "import * as RadixDialog from '@radix-ui/react-dialog';\nimport { motion, type Variants } from 'framer-motion';\nimport React, { memo, type ReactNode } from 'react';\nimport { classNames } from '~/utils/classNames';\nimport { cubicEasingFn } from '~/utils/easings';\nimport { IconButton } from './IconButton';\n\nexport { Close as DialogClose, Root as DialogRoot } from '@radix-ui/react-dialog';\n\nconst transition = {\n  duration: 0.15,\n  ease: cubicEasingFn,\n};\n\nexport const dialogBackdropVariants = {\n  closed: {\n    opacity: 0,\n    transition,\n  },\n  open: {\n    opacity: 1,\n    transition,\n  },\n} satisfies Variants;\n\nexport const dialogVariants = {\n  closed: {\n    x: '-50%',\n    y: '-40%',\n    scale: 0.96,\n    opacity: 0,\n    transition,\n  },\n  open: {\n    x: '-50%',\n    y: '-50%',\n    scale: 1,\n    opacity: 1,\n    transition,\n  },\n} satisfies Variants;\n\ninterface DialogButtonProps {\n  type: 'primary' | 'secondary' | 'danger';\n  children: ReactNode;\n  onClick?: (event: React.UIEvent) => void;\n}\n\nexport const DialogButton = memo(({ type, children, onClick }: DialogButtonProps) => {\n  return (\n    <button\n      className={classNames(\n        'inline-flex h-[35px] items-center justify-center rounded-lg px-4 text-sm leading-none focus:outline-none',\n        {\n          'bg-bolt-elements-button-primary-background text-bolt-elements-button-primary-text hover:bg-bolt-elements-button-primary-backgroundHover':\n            type === 'primary',\n          'bg-bolt-elements-button-secondary-background text-bolt-elements-button-secondary-text hover:bg-bolt-elements-button-secondary-backgroundHover':\n            type === 'secondary',\n          'bg-bolt-elements-button-danger-background text-bolt-elements-button-danger-text hover:bg-bolt-elements-button-danger-backgroundHover':\n            type === 'danger',\n        },\n      )}\n      onClick={onClick}\n    >\n      {children}\n    </button>\n  );\n});\n\nexport const DialogTitle = memo(({ className, children, ...props }: RadixDialog.DialogTitleProps) => {\n  return (\n    <RadixDialog.Title\n      className={classNames(\n        'px-5 py-4 flex items-center justify-between border-b border-bolt-elements-borderColor text-lg font-semibold leading-6 text-bolt-elements-textPrimary',\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </RadixDialog.Title>\n  );\n});\n\nexport const DialogDescription = memo(({ className, children, ...props }: RadixDialog.DialogDescriptionProps) => {\n  return (\n    <RadixDialog.Description\n      className={classNames('px-5 py-4 text-bolt-elements-textPrimary text-md', className)}\n      {...props}\n    >\n      {children}\n    </RadixDialog.Description>\n  );\n});\n\ninterface DialogProps {\n  children: ReactNode | ReactNode[];\n  className?: string;\n  onBackdrop?: (event: React.UIEvent) => void;\n  onClose?: (event: React.UIEvent) => void;\n}\n\nexport const Dialog = memo(({ className, children, onBackdrop, onClose }: DialogProps) => {\n  return (\n    <RadixDialog.Portal>\n      <RadixDialog.Overlay onClick={onBackdrop} asChild>\n        <motion.div\n          className=\"bg-black/50 fixed inset-0 z-max\"\n          initial=\"closed\"\n          animate=\"open\"\n          exit=\"closed\"\n          variants={dialogBackdropVariants}\n        />\n      </RadixDialog.Overlay>\n      <RadixDialog.Content asChild>\n        <motion.div\n          className={classNames(\n            'fixed top-[50%] left-[50%] z-max max-h-[85vh] w-[90vw] max-w-[450px] translate-x-[-50%] translate-y-[-50%] border border-bolt-elements-borderColor rounded-lg bg-bolt-elements-background-depth-2 shadow-lg focus:outline-none overflow-hidden',\n            className,\n          )}\n          initial=\"closed\"\n          animate=\"open\"\n          exit=\"closed\"\n          variants={dialogVariants}\n        >\n          {children}\n          <RadixDialog.Close asChild onClick={onClose}>\n            <IconButton icon=\"i-ph:x\" className=\"absolute top-[10px] right-[10px]\" />\n          </RadixDialog.Close>\n        </motion.div>\n      </RadixDialog.Content>\n    </RadixDialog.Portal>\n  );\n});\n"
  },
  {
    "path": "app/components/ui/IconButton.tsx",
    "content": "import { memo } from 'react';\nimport { classNames } from '~/utils/classNames';\n\ntype IconSize = 'sm' | 'md' | 'lg' | 'xl' | 'xxl';\n\ninterface BaseIconButtonProps {\n  size?: IconSize;\n  className?: string;\n  iconClassName?: string;\n  disabledClassName?: string;\n  title?: string;\n  disabled?: boolean;\n  onClick?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;\n}\n\ntype IconButtonWithoutChildrenProps = {\n  icon: string;\n  children?: undefined;\n} & BaseIconButtonProps;\n\ntype IconButtonWithChildrenProps = {\n  icon?: undefined;\n  children: string | JSX.Element | JSX.Element[];\n} & BaseIconButtonProps;\n\ntype IconButtonProps = IconButtonWithoutChildrenProps | IconButtonWithChildrenProps;\n\nexport const IconButton = memo(\n  ({\n    icon,\n    size = 'xl',\n    className,\n    iconClassName,\n    disabledClassName,\n    disabled = false,\n    title,\n    onClick,\n    children,\n  }: IconButtonProps) => {\n    return (\n      <button\n        className={classNames(\n          'flex items-center text-bolt-elements-item-contentDefault bg-transparent enabled:hover:text-bolt-elements-item-contentActive rounded-md p-1 enabled:hover:bg-bolt-elements-item-backgroundActive disabled:cursor-not-allowed',\n          {\n            [classNames('opacity-30', disabledClassName)]: disabled,\n          },\n          className,\n        )}\n        title={title}\n        disabled={disabled}\n        onClick={(event) => {\n          if (disabled) {\n            return;\n          }\n\n          onClick?.(event);\n        }}\n      >\n        {children ? children : <div className={classNames(icon, getIconSize(size), iconClassName)}></div>}\n      </button>\n    );\n  },\n);\n\nfunction getIconSize(size: IconSize) {\n  if (size === 'sm') {\n    return 'text-sm';\n  } else if (size === 'md') {\n    return 'text-md';\n  } else if (size === 'lg') {\n    return 'text-lg';\n  } else if (size === 'xl') {\n    return 'text-xl';\n  } else {\n    return 'text-2xl';\n  }\n}\n"
  },
  {
    "path": "app/components/ui/LoadingDots.tsx",
    "content": "import { memo, useEffect, useState } from 'react';\n\ninterface LoadingDotsProps {\n  text: string;\n}\n\nexport const LoadingDots = memo(({ text }: LoadingDotsProps) => {\n  const [dotCount, setDotCount] = useState(0);\n\n  useEffect(() => {\n    const interval = setInterval(() => {\n      setDotCount((prevDotCount) => (prevDotCount + 1) % 4);\n    }, 500);\n\n    return () => clearInterval(interval);\n  }, []);\n\n  return (\n    <div className=\"flex justify-center items-center h-full\">\n      <div className=\"relative\">\n        <span>{text}</span>\n        <span className=\"absolute left-[calc(100%-12px)]\">{'.'.repeat(dotCount)}</span>\n        <span className=\"invisible\">...</span>\n      </div>\n    </div>\n  );\n});\n"
  },
  {
    "path": "app/components/ui/PanelHeader.tsx",
    "content": "import { memo } from 'react';\nimport { classNames } from '~/utils/classNames';\n\ninterface PanelHeaderProps {\n  className?: string;\n  children: React.ReactNode;\n}\n\nexport const PanelHeader = memo(({ className, children }: PanelHeaderProps) => {\n  return (\n    <div\n      className={classNames(\n        'flex items-center gap-2 bg-bolt-elements-background-depth-2 text-bolt-elements-textSecondary border-b border-bolt-elements-borderColor px-4 py-1 min-h-[34px] text-sm',\n        className,\n      )}\n    >\n      {children}\n    </div>\n  );\n});\n"
  },
  {
    "path": "app/components/ui/PanelHeaderButton.tsx",
    "content": "import { memo } from 'react';\nimport { classNames } from '~/utils/classNames';\n\ninterface PanelHeaderButtonProps {\n  className?: string;\n  disabledClassName?: string;\n  disabled?: boolean;\n  children: string | JSX.Element | Array<JSX.Element | string>;\n  onClick?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;\n}\n\nexport const PanelHeaderButton = memo(\n  ({ className, disabledClassName, disabled = false, children, onClick }: PanelHeaderButtonProps) => {\n    return (\n      <button\n        className={classNames(\n          'flex items-center shrink-0 gap-1.5 px-1.5 rounded-md py-0.5 text-bolt-elements-item-contentDefault bg-transparent enabled:hover:text-bolt-elements-item-contentActive enabled:hover:bg-bolt-elements-item-backgroundActive disabled:cursor-not-allowed',\n          {\n            [classNames('opacity-30', disabledClassName)]: disabled,\n          },\n          className,\n        )}\n        disabled={disabled}\n        onClick={(event) => {\n          if (disabled) {\n            return;\n          }\n\n          onClick?.(event);\n        }}\n      >\n        {children}\n      </button>\n    );\n  },\n);\n"
  },
  {
    "path": "app/components/ui/Slider.tsx",
    "content": "import { motion } from 'framer-motion';\nimport { memo } from 'react';\nimport { classNames } from '~/utils/classNames';\nimport { cubicEasingFn } from '~/utils/easings';\nimport { genericMemo } from '~/utils/react';\n\ninterface SliderOption<T> {\n  value: T;\n  text: string;\n}\n\nexport interface SliderOptions<T> {\n  left: SliderOption<T>;\n  right: SliderOption<T>;\n}\n\ninterface SliderProps<T> {\n  selected: T;\n  options: SliderOptions<T>;\n  setSelected?: (selected: T) => void;\n}\n\nexport const Slider = genericMemo(<T,>({ selected, options, setSelected }: SliderProps<T>) => {\n  const isLeftSelected = selected === options.left.value;\n\n  return (\n    <div className=\"flex items-center flex-wrap shrink-0 gap-1 bg-bolt-elements-background-depth-1 overflow-hidden rounded-full p-1\">\n      <SliderButton selected={isLeftSelected} setSelected={() => setSelected?.(options.left.value)}>\n        {options.left.text}\n      </SliderButton>\n      <SliderButton selected={!isLeftSelected} setSelected={() => setSelected?.(options.right.value)}>\n        {options.right.text}\n      </SliderButton>\n    </div>\n  );\n});\n\ninterface SliderButtonProps {\n  selected: boolean;\n  children: string | JSX.Element | Array<JSX.Element | string>;\n  setSelected: () => void;\n}\n\nconst SliderButton = memo(({ selected, children, setSelected }: SliderButtonProps) => {\n  return (\n    <button\n      onClick={setSelected}\n      className={classNames(\n        'bg-transparent text-sm px-2.5 py-0.5 rounded-full relative',\n        selected\n          ? 'text-bolt-elements-item-contentAccent'\n          : 'text-bolt-elements-item-contentDefault hover:text-bolt-elements-item-contentActive',\n      )}\n    >\n      <span className=\"relative z-10\">{children}</span>\n      {selected && (\n        <motion.span\n          layoutId=\"pill-tab\"\n          transition={{ duration: 0.2, ease: cubicEasingFn }}\n          className=\"absolute inset-0 z-0 bg-bolt-elements-item-backgroundAccent rounded-full\"\n        ></motion.span>\n      )}\n    </button>\n  );\n});\n"
  },
  {
    "path": "app/components/ui/ThemeSwitch.tsx",
    "content": "import { useStore } from '@nanostores/react';\nimport { memo, useEffect, useState } from 'react';\nimport { themeStore, toggleTheme } from '~/lib/stores/theme';\nimport { IconButton } from './IconButton';\n\ninterface ThemeSwitchProps {\n  className?: string;\n}\n\nexport const ThemeSwitch = memo(({ className }: ThemeSwitchProps) => {\n  const theme = useStore(themeStore);\n  const [domLoaded, setDomLoaded] = useState(false);\n\n  useEffect(() => {\n    setDomLoaded(true);\n  }, []);\n\n  return (\n    domLoaded && (\n      <IconButton\n        className={className}\n        icon={theme === 'dark' ? 'i-ph-sun-dim-duotone' : 'i-ph-moon-stars-duotone'}\n        size=\"xl\"\n        title=\"Toggle Theme\"\n        onClick={toggleTheme}\n      />\n    )\n  );\n});\n"
  },
  {
    "path": "app/components/workbench/EditorPanel.tsx",
    "content": "import { useStore } from '@nanostores/react';\nimport { memo, useEffect, useMemo, useRef, useState } from 'react';\nimport { Panel, PanelGroup, PanelResizeHandle, type ImperativePanelHandle } from 'react-resizable-panels';\nimport {\n  CodeMirrorEditor,\n  type EditorDocument,\n  type EditorSettings,\n  type OnChangeCallback as OnEditorChange,\n  type OnSaveCallback as OnEditorSave,\n  type OnScrollCallback as OnEditorScroll,\n} from '~/components/editor/codemirror/CodeMirrorEditor';\nimport { IconButton } from '~/components/ui/IconButton';\nimport { PanelHeader } from '~/components/ui/PanelHeader';\nimport { PanelHeaderButton } from '~/components/ui/PanelHeaderButton';\nimport { shortcutEventEmitter } from '~/lib/hooks';\nimport type { FileMap } from '~/lib/stores/files';\nimport { themeStore } from '~/lib/stores/theme';\nimport { workbenchStore } from '~/lib/stores/workbench';\nimport { classNames } from '~/utils/classNames';\nimport { WORK_DIR } from '~/utils/constants';\nimport { renderLogger } from '~/utils/logger';\nimport { isMobile } from '~/utils/mobile';\nimport { FileBreadcrumb } from './FileBreadcrumb';\nimport { FileTree } from './FileTree';\nimport { Terminal, type TerminalRef } from './terminal/Terminal';\n\ninterface EditorPanelProps {\n  files?: FileMap;\n  unsavedFiles?: Set<string>;\n  editorDocument?: EditorDocument;\n  selectedFile?: string | undefined;\n  isStreaming?: boolean;\n  onEditorChange?: OnEditorChange;\n  onEditorScroll?: OnEditorScroll;\n  onFileSelect?: (value?: string) => void;\n  onFileSave?: OnEditorSave;\n  onFileReset?: () => void;\n}\n\nconst MAX_TERMINALS = 3;\nconst DEFAULT_TERMINAL_SIZE = 25;\nconst DEFAULT_EDITOR_SIZE = 100 - DEFAULT_TERMINAL_SIZE;\n\nconst editorSettings: EditorSettings = { tabSize: 2 };\n\nexport const EditorPanel = memo(\n  ({\n    files,\n    unsavedFiles,\n    editorDocument,\n    selectedFile,\n    isStreaming,\n    onFileSelect,\n    onEditorChange,\n    onEditorScroll,\n    onFileSave,\n    onFileReset,\n  }: EditorPanelProps) => {\n    renderLogger.trace('EditorPanel');\n\n    const theme = useStore(themeStore);\n    const showTerminal = useStore(workbenchStore.showTerminal);\n\n    const terminalRefs = useRef<Array<TerminalRef | null>>([]);\n    const terminalPanelRef = useRef<ImperativePanelHandle>(null);\n    const terminalToggledByShortcut = useRef(false);\n\n    const [activeTerminal, setActiveTerminal] = useState(0);\n    const [terminalCount, setTerminalCount] = useState(1);\n\n    const activeFileSegments = useMemo(() => {\n      if (!editorDocument) {\n        return undefined;\n      }\n\n      return editorDocument.filePath.split('/');\n    }, [editorDocument]);\n\n    const activeFileUnsaved = useMemo(() => {\n      return editorDocument !== undefined && unsavedFiles?.has(editorDocument.filePath);\n    }, [editorDocument, unsavedFiles]);\n\n    useEffect(() => {\n      const unsubscribeFromEventEmitter = shortcutEventEmitter.on('toggleTerminal', () => {\n        terminalToggledByShortcut.current = true;\n      });\n\n      const unsubscribeFromThemeStore = themeStore.subscribe(() => {\n        for (const ref of Object.values(terminalRefs.current)) {\n          ref?.reloadStyles();\n        }\n      });\n\n      return () => {\n        unsubscribeFromEventEmitter();\n        unsubscribeFromThemeStore();\n      };\n    }, []);\n\n    useEffect(() => {\n      const { current: terminal } = terminalPanelRef;\n\n      if (!terminal) {\n        return;\n      }\n\n      const isCollapsed = terminal.isCollapsed();\n\n      if (!showTerminal && !isCollapsed) {\n        terminal.collapse();\n      } else if (showTerminal && isCollapsed) {\n        terminal.resize(DEFAULT_TERMINAL_SIZE);\n      }\n\n      terminalToggledByShortcut.current = false;\n    }, [showTerminal]);\n\n    const addTerminal = () => {\n      if (terminalCount < MAX_TERMINALS) {\n        setTerminalCount(terminalCount + 1);\n        setActiveTerminal(terminalCount);\n      }\n    };\n\n    return (\n      <PanelGroup direction=\"vertical\">\n        <Panel defaultSize={showTerminal ? DEFAULT_EDITOR_SIZE : 100} minSize={20}>\n          <PanelGroup direction=\"horizontal\">\n            <Panel defaultSize={20} minSize={10} collapsible>\n              <div className=\"flex flex-col border-r border-bolt-elements-borderColor h-full\">\n                <PanelHeader>\n                  <div className=\"i-ph:tree-structure-duotone shrink-0\" />\n                  Files\n                </PanelHeader>\n                <FileTree\n                  className=\"h-full\"\n                  files={files}\n                  hideRoot\n                  unsavedFiles={unsavedFiles}\n                  rootFolder={WORK_DIR}\n                  selectedFile={selectedFile}\n                  onFileSelect={onFileSelect}\n                />\n              </div>\n            </Panel>\n            <PanelResizeHandle />\n            <Panel className=\"flex flex-col\" defaultSize={80} minSize={20}>\n              <PanelHeader className=\"overflow-x-auto\">\n                {activeFileSegments?.length && (\n                  <div className=\"flex items-center flex-1 text-sm\">\n                    <FileBreadcrumb pathSegments={activeFileSegments} files={files} onFileSelect={onFileSelect} />\n                    {activeFileUnsaved && (\n                      <div className=\"flex gap-1 ml-auto -mr-1.5\">\n                        <PanelHeaderButton onClick={onFileSave}>\n                          <div className=\"i-ph:floppy-disk-duotone\" />\n                          Save\n                        </PanelHeaderButton>\n                        <PanelHeaderButton onClick={onFileReset}>\n                          <div className=\"i-ph:clock-counter-clockwise-duotone\" />\n                          Reset\n                        </PanelHeaderButton>\n                      </div>\n                    )}\n                  </div>\n                )}\n              </PanelHeader>\n              <div className=\"h-full flex-1 overflow-hidden\">\n                <CodeMirrorEditor\n                  theme={theme}\n                  editable={!isStreaming && editorDocument !== undefined}\n                  settings={editorSettings}\n                  doc={editorDocument}\n                  autoFocusOnDocumentChange={!isMobile()}\n                  onScroll={onEditorScroll}\n                  onChange={onEditorChange}\n                  onSave={onFileSave}\n                />\n              </div>\n            </Panel>\n          </PanelGroup>\n        </Panel>\n        <PanelResizeHandle />\n        <Panel\n          ref={terminalPanelRef}\n          defaultSize={showTerminal ? DEFAULT_TERMINAL_SIZE : 0}\n          minSize={10}\n          collapsible\n          onExpand={() => {\n            if (!terminalToggledByShortcut.current) {\n              workbenchStore.toggleTerminal(true);\n            }\n          }}\n          onCollapse={() => {\n            if (!terminalToggledByShortcut.current) {\n              workbenchStore.toggleTerminal(false);\n            }\n          }}\n        >\n          <div className=\"h-full\">\n            <div className=\"bg-bolt-elements-terminals-background h-full flex flex-col\">\n              <div className=\"flex items-center bg-bolt-elements-background-depth-2 border-y border-bolt-elements-borderColor gap-1.5 min-h-[34px] p-2\">\n                {Array.from({ length: terminalCount }, (_, index) => {\n                  const isActive = activeTerminal === index;\n\n                  return (\n                    <button\n                      key={index}\n                      className={classNames(\n                        'flex items-center text-sm cursor-pointer gap-1.5 px-3 py-2 h-full whitespace-nowrap rounded-full',\n                        {\n                          'bg-bolt-elements-terminals-buttonBackground text-bolt-elements-textPrimary': isActive,\n                          'bg-bolt-elements-background-depth-2 text-bolt-elements-textSecondary hover:bg-bolt-elements-terminals-buttonBackground':\n                            !isActive,\n                        },\n                      )}\n                      onClick={() => setActiveTerminal(index)}\n                    >\n                      <div className=\"i-ph:terminal-window-duotone text-lg\" />\n                      Terminal {terminalCount > 1 && index + 1}\n                    </button>\n                  );\n                })}\n                {terminalCount < MAX_TERMINALS && <IconButton icon=\"i-ph:plus\" size=\"md\" onClick={addTerminal} />}\n                <IconButton\n                  className=\"ml-auto\"\n                  icon=\"i-ph:caret-down\"\n                  title=\"Close\"\n                  size=\"md\"\n                  onClick={() => workbenchStore.toggleTerminal(false)}\n                />\n              </div>\n              {Array.from({ length: terminalCount }, (_, index) => {\n                const isActive = activeTerminal === index;\n\n                return (\n                  <Terminal\n                    key={index}\n                    className={classNames('h-full overflow-hidden', {\n                      hidden: !isActive,\n                    })}\n                    ref={(ref) => {\n                      terminalRefs.current.push(ref);\n                    }}\n                    onTerminalReady={(terminal) => workbenchStore.attachTerminal(terminal)}\n                    onTerminalResize={(cols, rows) => workbenchStore.onTerminalResize(cols, rows)}\n                    theme={theme}\n                  />\n                );\n              })}\n            </div>\n          </div>\n        </Panel>\n      </PanelGroup>\n    );\n  },\n);\n"
  },
  {
    "path": "app/components/workbench/FileBreadcrumb.tsx",
    "content": "import * as DropdownMenu from '@radix-ui/react-dropdown-menu';\nimport { AnimatePresence, motion, type Variants } from 'framer-motion';\nimport { memo, useEffect, useRef, useState } from 'react';\nimport type { FileMap } from '~/lib/stores/files';\nimport { classNames } from '~/utils/classNames';\nimport { WORK_DIR } from '~/utils/constants';\nimport { cubicEasingFn } from '~/utils/easings';\nimport { renderLogger } from '~/utils/logger';\nimport FileTree from './FileTree';\n\nconst WORK_DIR_REGEX = new RegExp(`^${WORK_DIR.split('/').slice(0, -1).join('/').replaceAll('/', '\\\\/')}/`);\n\ninterface FileBreadcrumbProps {\n  files?: FileMap;\n  pathSegments?: string[];\n  onFileSelect?: (filePath: string) => void;\n}\n\nconst contextMenuVariants = {\n  open: {\n    y: 0,\n    opacity: 1,\n    transition: {\n      duration: 0.15,\n      ease: cubicEasingFn,\n    },\n  },\n  close: {\n    y: 6,\n    opacity: 0,\n    transition: {\n      duration: 0.15,\n      ease: cubicEasingFn,\n    },\n  },\n} satisfies Variants;\n\nexport const FileBreadcrumb = memo<FileBreadcrumbProps>(({ files, pathSegments = [], onFileSelect }) => {\n  renderLogger.trace('FileBreadcrumb');\n\n  const [activeIndex, setActiveIndex] = useState<number | null>(null);\n\n  const contextMenuRef = useRef<HTMLDivElement | null>(null);\n  const segmentRefs = useRef<(HTMLSpanElement | null)[]>([]);\n\n  const handleSegmentClick = (index: number) => {\n    setActiveIndex((prevIndex) => (prevIndex === index ? null : index));\n  };\n\n  useEffect(() => {\n    const handleOutsideClick = (event: MouseEvent) => {\n      if (\n        activeIndex !== null &&\n        !contextMenuRef.current?.contains(event.target as Node) &&\n        !segmentRefs.current.some((ref) => ref?.contains(event.target as Node))\n      ) {\n        setActiveIndex(null);\n      }\n    };\n\n    document.addEventListener('mousedown', handleOutsideClick);\n\n    return () => {\n      document.removeEventListener('mousedown', handleOutsideClick);\n    };\n  }, [activeIndex]);\n\n  if (files === undefined || pathSegments.length === 0) {\n    return null;\n  }\n\n  return (\n    <div className=\"flex\">\n      {pathSegments.map((segment, index) => {\n        const isLast = index === pathSegments.length - 1;\n\n        const path = pathSegments.slice(0, index).join('/');\n\n        if (!WORK_DIR_REGEX.test(path)) {\n          return null;\n        }\n\n        const isActive = activeIndex === index;\n\n        return (\n          <div key={index} className=\"relative flex items-center\">\n            <DropdownMenu.Root open={isActive} modal={false}>\n              <DropdownMenu.Trigger asChild>\n                <span\n                  ref={(ref) => (segmentRefs.current[index] = ref)}\n                  className={classNames('flex items-center gap-1.5 cursor-pointer shrink-0', {\n                    'text-bolt-elements-textTertiary hover:text-bolt-elements-textPrimary': !isActive,\n                    'text-bolt-elements-textPrimary underline': isActive,\n                    'pr-4': isLast,\n                  })}\n                  onClick={() => handleSegmentClick(index)}\n                >\n                  {isLast && <div className=\"i-ph:file-duotone\" />}\n                  {segment}\n                </span>\n              </DropdownMenu.Trigger>\n              {index > 0 && !isLast && <span className=\"i-ph:caret-right inline-block mx-1\" />}\n              <AnimatePresence>\n                {isActive && (\n                  <DropdownMenu.Portal>\n                    <DropdownMenu.Content\n                      className=\"z-file-tree-breadcrumb\"\n                      asChild\n                      align=\"start\"\n                      side=\"bottom\"\n                      avoidCollisions={false}\n                    >\n                      <motion.div\n                        ref={contextMenuRef}\n                        initial=\"close\"\n                        animate=\"open\"\n                        exit=\"close\"\n                        variants={contextMenuVariants}\n                      >\n                        <div className=\"rounded-lg overflow-hidden\">\n                          <div className=\"max-h-[50vh] min-w-[300px] overflow-scroll bg-bolt-elements-background-depth-1 border border-bolt-elements-borderColor shadow-sm rounded-lg\">\n                            <FileTree\n                              files={files}\n                              hideRoot\n                              rootFolder={path}\n                              collapsed\n                              allowFolderSelection\n                              selectedFile={`${path}/${segment}`}\n                              onFileSelect={(filePath) => {\n                                setActiveIndex(null);\n                                onFileSelect?.(filePath);\n                              }}\n                            />\n                          </div>\n                        </div>\n                        <DropdownMenu.Arrow className=\"fill-bolt-elements-borderColor\" />\n                      </motion.div>\n                    </DropdownMenu.Content>\n                  </DropdownMenu.Portal>\n                )}\n              </AnimatePresence>\n            </DropdownMenu.Root>\n          </div>\n        );\n      })}\n    </div>\n  );\n});\n"
  },
  {
    "path": "app/components/workbench/FileTree.tsx",
    "content": "import { memo, useEffect, useMemo, useState, type ReactNode } from 'react';\nimport type { FileMap } from '~/lib/stores/files';\nimport { classNames } from '~/utils/classNames';\nimport { createScopedLogger, renderLogger } from '~/utils/logger';\n\nconst logger = createScopedLogger('FileTree');\n\nconst NODE_PADDING_LEFT = 8;\nconst DEFAULT_HIDDEN_FILES = [/\\/node_modules\\//, /\\/\\.next/, /\\/\\.astro/];\n\ninterface Props {\n  files?: FileMap;\n  selectedFile?: string;\n  onFileSelect?: (filePath: string) => void;\n  rootFolder?: string;\n  hideRoot?: boolean;\n  collapsed?: boolean;\n  allowFolderSelection?: boolean;\n  hiddenFiles?: Array<string | RegExp>;\n  unsavedFiles?: Set<string>;\n  className?: string;\n}\n\nexport const FileTree = memo(\n  ({\n    files = {},\n    onFileSelect,\n    selectedFile,\n    rootFolder,\n    hideRoot = false,\n    collapsed = false,\n    allowFolderSelection = false,\n    hiddenFiles,\n    className,\n    unsavedFiles,\n  }: Props) => {\n    renderLogger.trace('FileTree');\n\n    const computedHiddenFiles = useMemo(() => [...DEFAULT_HIDDEN_FILES, ...(hiddenFiles ?? [])], [hiddenFiles]);\n\n    const fileList = useMemo(() => {\n      return buildFileList(files, rootFolder, hideRoot, computedHiddenFiles);\n    }, [files, rootFolder, hideRoot, computedHiddenFiles]);\n\n    const [collapsedFolders, setCollapsedFolders] = useState(() => {\n      return collapsed\n        ? new Set(fileList.filter((item) => item.kind === 'folder').map((item) => item.fullPath))\n        : new Set<string>();\n    });\n\n    useEffect(() => {\n      if (collapsed) {\n        setCollapsedFolders(new Set(fileList.filter((item) => item.kind === 'folder').map((item) => item.fullPath)));\n        return;\n      }\n\n      setCollapsedFolders((prevCollapsed) => {\n        const newCollapsed = new Set<string>();\n\n        for (const folder of fileList) {\n          if (folder.kind === 'folder' && prevCollapsed.has(folder.fullPath)) {\n            newCollapsed.add(folder.fullPath);\n          }\n        }\n\n        return newCollapsed;\n      });\n    }, [fileList, collapsed]);\n\n    const filteredFileList = useMemo(() => {\n      const list = [];\n\n      let lastDepth = Number.MAX_SAFE_INTEGER;\n\n      for (const fileOrFolder of fileList) {\n        const depth = fileOrFolder.depth;\n\n        // if the depth is equal we reached the end of the collaped group\n        if (lastDepth === depth) {\n          lastDepth = Number.MAX_SAFE_INTEGER;\n        }\n\n        // ignore collapsed folders\n        if (collapsedFolders.has(fileOrFolder.fullPath)) {\n          lastDepth = Math.min(lastDepth, depth);\n        }\n\n        // ignore files and folders below the last collapsed folder\n        if (lastDepth < depth) {\n          continue;\n        }\n\n        list.push(fileOrFolder);\n      }\n\n      return list;\n    }, [fileList, collapsedFolders]);\n\n    const toggleCollapseState = (fullPath: string) => {\n      setCollapsedFolders((prevSet) => {\n        const newSet = new Set(prevSet);\n\n        if (newSet.has(fullPath)) {\n          newSet.delete(fullPath);\n        } else {\n          newSet.add(fullPath);\n        }\n\n        return newSet;\n      });\n    };\n\n    return (\n      <div className={classNames('text-sm', className)}>\n        {filteredFileList.map((fileOrFolder) => {\n          switch (fileOrFolder.kind) {\n            case 'file': {\n              return (\n                <File\n                  key={fileOrFolder.id}\n                  selected={selectedFile === fileOrFolder.fullPath}\n                  file={fileOrFolder}\n                  unsavedChanges={unsavedFiles?.has(fileOrFolder.fullPath)}\n                  onClick={() => {\n                    onFileSelect?.(fileOrFolder.fullPath);\n                  }}\n                />\n              );\n            }\n            case 'folder': {\n              return (\n                <Folder\n                  key={fileOrFolder.id}\n                  folder={fileOrFolder}\n                  selected={allowFolderSelection && selectedFile === fileOrFolder.fullPath}\n                  collapsed={collapsedFolders.has(fileOrFolder.fullPath)}\n                  onClick={() => {\n                    toggleCollapseState(fileOrFolder.fullPath);\n                  }}\n                />\n              );\n            }\n            default: {\n              return undefined;\n            }\n          }\n        })}\n      </div>\n    );\n  },\n);\n\nexport default FileTree;\n\ninterface FolderProps {\n  folder: FolderNode;\n  collapsed: boolean;\n  selected?: boolean;\n  onClick: () => void;\n}\n\nfunction Folder({ folder: { depth, name }, collapsed, selected = false, onClick }: FolderProps) {\n  return (\n    <NodeButton\n      className={classNames('group', {\n        'bg-transparent text-bolt-elements-item-contentDefault hover:text-bolt-elements-item-contentActive hover:bg-bolt-elements-item-backgroundActive':\n          !selected,\n        'bg-bolt-elements-item-backgroundAccent text-bolt-elements-item-contentAccent': selected,\n      })}\n      depth={depth}\n      iconClasses={classNames({\n        'i-ph:caret-right scale-98': collapsed,\n        'i-ph:caret-down scale-98': !collapsed,\n      })}\n      onClick={onClick}\n    >\n      {name}\n    </NodeButton>\n  );\n}\n\ninterface FileProps {\n  file: FileNode;\n  selected: boolean;\n  unsavedChanges?: boolean;\n  onClick: () => void;\n}\n\nfunction File({ file: { depth, name }, onClick, selected, unsavedChanges = false }: FileProps) {\n  return (\n    <NodeButton\n      className={classNames('group', {\n        'bg-transparent hover:bg-bolt-elements-item-backgroundActive text-bolt-elements-item-contentDefault': !selected,\n        'bg-bolt-elements-item-backgroundAccent text-bolt-elements-item-contentAccent': selected,\n      })}\n      depth={depth}\n      iconClasses={classNames('i-ph:file-duotone scale-98', {\n        'group-hover:text-bolt-elements-item-contentActive': !selected,\n      })}\n      onClick={onClick}\n    >\n      <div\n        className={classNames('flex items-center', {\n          'group-hover:text-bolt-elements-item-contentActive': !selected,\n        })}\n      >\n        <div className=\"flex-1 truncate pr-2\">{name}</div>\n        {unsavedChanges && <span className=\"i-ph:circle-fill scale-68 shrink-0 text-orange-500\" />}\n      </div>\n    </NodeButton>\n  );\n}\n\ninterface ButtonProps {\n  depth: number;\n  iconClasses: string;\n  children: ReactNode;\n  className?: string;\n  onClick?: () => void;\n}\n\nfunction NodeButton({ depth, iconClasses, onClick, className, children }: ButtonProps) {\n  return (\n    <button\n      className={classNames(\n        'flex items-center gap-1.5 w-full pr-2 border-2 border-transparent text-faded py-0.5',\n        className,\n      )}\n      style={{ paddingLeft: `${6 + depth * NODE_PADDING_LEFT}px` }}\n      onClick={() => onClick?.()}\n    >\n      <div className={classNames('scale-120 shrink-0', iconClasses)}></div>\n      <div className=\"truncate w-full text-left\">{children}</div>\n    </button>\n  );\n}\n\ntype Node = FileNode | FolderNode;\n\ninterface BaseNode {\n  id: number;\n  depth: number;\n  name: string;\n  fullPath: string;\n}\n\ninterface FileNode extends BaseNode {\n  kind: 'file';\n}\n\ninterface FolderNode extends BaseNode {\n  kind: 'folder';\n}\n\nfunction buildFileList(\n  files: FileMap,\n  rootFolder = '/',\n  hideRoot: boolean,\n  hiddenFiles: Array<string | RegExp>,\n): Node[] {\n  const folderPaths = new Set<string>();\n  const fileList: Node[] = [];\n\n  let defaultDepth = 0;\n\n  if (rootFolder === '/' && !hideRoot) {\n    defaultDepth = 1;\n    fileList.push({ kind: 'folder', name: '/', depth: 0, id: 0, fullPath: '/' });\n  }\n\n  for (const [filePath, dirent] of Object.entries(files)) {\n    const segments = filePath.split('/').filter((segment) => segment);\n    const fileName = segments.at(-1);\n\n    if (!fileName || isHiddenFile(filePath, fileName, hiddenFiles)) {\n      continue;\n    }\n\n    let currentPath = '';\n\n    let i = 0;\n    let depth = 0;\n\n    while (i < segments.length) {\n      const name = segments[i];\n      const fullPath = (currentPath += `/${name}`);\n\n      if (!fullPath.startsWith(rootFolder) || (hideRoot && fullPath === rootFolder)) {\n        i++;\n        continue;\n      }\n\n      if (i === segments.length - 1 && dirent?.type === 'file') {\n        fileList.push({\n          kind: 'file',\n          id: fileList.length,\n          name,\n          fullPath,\n          depth: depth + defaultDepth,\n        });\n      } else if (!folderPaths.has(fullPath)) {\n        folderPaths.add(fullPath);\n\n        fileList.push({\n          kind: 'folder',\n          id: fileList.length,\n          name,\n          fullPath,\n          depth: depth + defaultDepth,\n        });\n      }\n\n      i++;\n      depth++;\n    }\n  }\n\n  return sortFileList(rootFolder, fileList, hideRoot);\n}\n\nfunction isHiddenFile(filePath: string, fileName: string, hiddenFiles: Array<string | RegExp>) {\n  return hiddenFiles.some((pathOrRegex) => {\n    if (typeof pathOrRegex === 'string') {\n      return fileName === pathOrRegex;\n    }\n\n    return pathOrRegex.test(filePath);\n  });\n}\n\n/**\n * Sorts the given list of nodes into a tree structure (still a flat list).\n *\n * This function organizes the nodes into a hierarchical structure based on their paths,\n * with folders appearing before files and all items sorted alphabetically within their level.\n *\n * @note This function mutates the given `nodeList` array for performance reasons.\n *\n * @param rootFolder - The path of the root folder to start the sorting from.\n * @param nodeList - The list of nodes to be sorted.\n *\n * @returns A new array of nodes sorted in depth-first order.\n */\nfunction sortFileList(rootFolder: string, nodeList: Node[], hideRoot: boolean): Node[] {\n  logger.trace('sortFileList');\n\n  const nodeMap = new Map<string, Node>();\n  const childrenMap = new Map<string, Node[]>();\n\n  // pre-sort nodes by name and type\n  nodeList.sort((a, b) => compareNodes(a, b));\n\n  for (const node of nodeList) {\n    nodeMap.set(node.fullPath, node);\n\n    const parentPath = node.fullPath.slice(0, node.fullPath.lastIndexOf('/'));\n\n    if (parentPath !== rootFolder.slice(0, rootFolder.lastIndexOf('/'))) {\n      if (!childrenMap.has(parentPath)) {\n        childrenMap.set(parentPath, []);\n      }\n\n      childrenMap.get(parentPath)?.push(node);\n    }\n  }\n\n  const sortedList: Node[] = [];\n\n  const depthFirstTraversal = (path: string): void => {\n    const node = nodeMap.get(path);\n\n    if (node) {\n      sortedList.push(node);\n    }\n\n    const children = childrenMap.get(path);\n\n    if (children) {\n      for (const child of children) {\n        if (child.kind === 'folder') {\n          depthFirstTraversal(child.fullPath);\n        } else {\n          sortedList.push(child);\n        }\n      }\n    }\n  };\n\n  if (hideRoot) {\n    // if root is hidden, start traversal from its immediate children\n    const rootChildren = childrenMap.get(rootFolder) || [];\n\n    for (const child of rootChildren) {\n      depthFirstTraversal(child.fullPath);\n    }\n  } else {\n    depthFirstTraversal(rootFolder);\n  }\n\n  return sortedList;\n}\n\nfunction compareNodes(a: Node, b: Node): number {\n  if (a.kind !== b.kind) {\n    return a.kind === 'folder' ? -1 : 1;\n  }\n\n  return a.name.localeCompare(b.name, undefined, { numeric: true, sensitivity: 'base' });\n}\n"
  },
  {
    "path": "app/components/workbench/PortDropdown.tsx",
    "content": "import { memo, useEffect, useRef } from 'react';\nimport { IconButton } from '~/components/ui/IconButton';\nimport type { PreviewInfo } from '~/lib/stores/previews';\n\ninterface PortDropdownProps {\n  activePreviewIndex: number;\n  setActivePreviewIndex: (index: number) => void;\n  isDropdownOpen: boolean;\n  setIsDropdownOpen: (value: boolean) => void;\n  setHasSelectedPreview: (value: boolean) => void;\n  previews: PreviewInfo[];\n}\n\nexport const PortDropdown = memo(\n  ({\n    activePreviewIndex,\n    setActivePreviewIndex,\n    isDropdownOpen,\n    setIsDropdownOpen,\n    setHasSelectedPreview,\n    previews,\n  }: PortDropdownProps) => {\n    const dropdownRef = useRef<HTMLDivElement>(null);\n\n    // sort previews, preserving original index\n    const sortedPreviews = previews\n      .map((previewInfo, index) => ({ ...previewInfo, index }))\n      .sort((a, b) => a.port - b.port);\n\n    // close dropdown if user clicks outside\n    useEffect(() => {\n      const handleClickOutside = (event: MouseEvent) => {\n        if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {\n          setIsDropdownOpen(false);\n        }\n      };\n\n      if (isDropdownOpen) {\n        window.addEventListener('mousedown', handleClickOutside);\n      } else {\n        window.removeEventListener('mousedown', handleClickOutside);\n      }\n\n      return () => {\n        window.removeEventListener('mousedown', handleClickOutside);\n      };\n    }, [isDropdownOpen]);\n\n    return (\n      <div className=\"relative z-port-dropdown\" ref={dropdownRef}>\n        <IconButton icon=\"i-ph:plug\" onClick={() => setIsDropdownOpen(!isDropdownOpen)} />\n        {isDropdownOpen && (\n          <div className=\"absolute right-0 mt-2 bg-bolt-elements-background-depth-2 border border-bolt-elements-borderColor rounded shadow-sm min-w-[140px] dropdown-animation\">\n            <div className=\"px-4 py-2 border-b border-bolt-elements-borderColor text-sm font-semibold text-bolt-elements-textPrimary\">\n              Ports\n            </div>\n            {sortedPreviews.map((preview) => (\n              <div\n                key={preview.port}\n                className=\"flex items-center px-4 py-2 cursor-pointer hover:bg-bolt-elements-item-backgroundActive\"\n                onClick={() => {\n                  setActivePreviewIndex(preview.index);\n                  setIsDropdownOpen(false);\n                  setHasSelectedPreview(true);\n                }}\n              >\n                <span\n                  className={\n                    activePreviewIndex === preview.index\n                      ? 'text-bolt-elements-item-contentAccent'\n                      : 'text-bolt-elements-item-contentDefault group-hover:text-bolt-elements-item-contentActive'\n                  }\n                >\n                  {preview.port}\n                </span>\n              </div>\n            ))}\n          </div>\n        )}\n      </div>\n    );\n  },\n);\n"
  },
  {
    "path": "app/components/workbench/Preview.tsx",
    "content": "import { useStore } from '@nanostores/react';\nimport { memo, useCallback, useEffect, useRef, useState } from 'react';\nimport { IconButton } from '~/components/ui/IconButton';\nimport { workbenchStore } from '~/lib/stores/workbench';\nimport { PortDropdown } from './PortDropdown';\n\nexport const Preview = memo(() => {\n  const iframeRef = useRef<HTMLIFrameElement>(null);\n  const inputRef = useRef<HTMLInputElement>(null);\n  const [activePreviewIndex, setActivePreviewIndex] = useState(0);\n  const [isPortDropdownOpen, setIsPortDropdownOpen] = useState(false);\n  const hasSelectedPreview = useRef(false);\n  const previews = useStore(workbenchStore.previews);\n  const activePreview = previews[activePreviewIndex];\n\n  const [url, setUrl] = useState('');\n  const [iframeUrl, setIframeUrl] = useState<string | undefined>();\n\n  useEffect(() => {\n    if (!activePreview) {\n      setUrl('');\n      setIframeUrl(undefined);\n\n      return;\n    }\n\n    const { baseUrl } = activePreview;\n\n    setUrl(baseUrl);\n    setIframeUrl(baseUrl);\n  }, [activePreview, iframeUrl]);\n\n  const validateUrl = useCallback(\n    (value: string) => {\n      if (!activePreview) {\n        return false;\n      }\n\n      const { baseUrl } = activePreview;\n\n      if (value === baseUrl) {\n        return true;\n      } else if (value.startsWith(baseUrl)) {\n        return ['/', '?', '#'].includes(value.charAt(baseUrl.length));\n      }\n\n      return false;\n    },\n    [activePreview],\n  );\n\n  const findMinPortIndex = useCallback(\n    (minIndex: number, preview: { port: number }, index: number, array: { port: number }[]) => {\n      return preview.port < array[minIndex].port ? index : minIndex;\n    },\n    [],\n  );\n\n  // when previews change, display the lowest port if user hasn't selected a preview\n  useEffect(() => {\n    if (previews.length > 1 && !hasSelectedPreview.current) {\n      const minPortIndex = previews.reduce(findMinPortIndex, 0);\n\n      setActivePreviewIndex(minPortIndex);\n    }\n  }, [previews]);\n\n  const reloadPreview = () => {\n    if (iframeRef.current) {\n      iframeRef.current.src = iframeRef.current.src;\n    }\n  };\n\n  return (\n    <div className=\"w-full h-full flex flex-col\">\n      {isPortDropdownOpen && (\n        <div className=\"z-iframe-overlay w-full h-full absolute\" onClick={() => setIsPortDropdownOpen(false)} />\n      )}\n      <div className=\"bg-bolt-elements-background-depth-2 p-2 flex items-center gap-1.5\">\n        <IconButton icon=\"i-ph:arrow-clockwise\" onClick={reloadPreview} />\n        <div\n          className=\"flex items-center gap-1 flex-grow bg-bolt-elements-preview-addressBar-background border border-bolt-elements-borderColor text-bolt-elements-preview-addressBar-text rounded-full px-3 py-1 text-sm hover:bg-bolt-elements-preview-addressBar-backgroundHover hover:focus-within:bg-bolt-elements-preview-addressBar-backgroundActive focus-within:bg-bolt-elements-preview-addressBar-backgroundActive\n        focus-within-border-bolt-elements-borderColorActive focus-within:text-bolt-elements-preview-addressBar-textActive\"\n        >\n          <input\n            ref={inputRef}\n            className=\"w-full bg-transparent outline-none\"\n            type=\"text\"\n            value={url}\n            onChange={(event) => {\n              setUrl(event.target.value);\n            }}\n            onKeyDown={(event) => {\n              if (event.key === 'Enter' && validateUrl(url)) {\n                setIframeUrl(url);\n\n                if (inputRef.current) {\n                  inputRef.current.blur();\n                }\n              }\n            }}\n          />\n        </div>\n        {previews.length > 1 && (\n          <PortDropdown\n            activePreviewIndex={activePreviewIndex}\n            setActivePreviewIndex={setActivePreviewIndex}\n            isDropdownOpen={isPortDropdownOpen}\n            setHasSelectedPreview={(value) => (hasSelectedPreview.current = value)}\n            setIsDropdownOpen={setIsPortDropdownOpen}\n            previews={previews}\n          />\n        )}\n      </div>\n      <div className=\"flex-1 border-t border-bolt-elements-borderColor\">\n        {activePreview ? (\n          <iframe ref={iframeRef} className=\"border-none w-full h-full bg-white\" src={iframeUrl} />\n        ) : (\n          <div className=\"flex w-full h-full justify-center items-center bg-white\">No preview available</div>\n        )}\n      </div>\n    </div>\n  );\n});\n"
  },
  {
    "path": "app/components/workbench/Workbench.client.tsx",
    "content": "import { useStore } from '@nanostores/react';\nimport { motion, type HTMLMotionProps, type Variants } from 'framer-motion';\nimport { computed } from 'nanostores';\nimport { memo, useCallback, useEffect } from 'react';\nimport { toast } from 'react-toastify';\nimport {\n  type OnChangeCallback as OnEditorChange,\n  type OnScrollCallback as OnEditorScroll,\n} from '~/components/editor/codemirror/CodeMirrorEditor';\nimport { IconButton } from '~/components/ui/IconButton';\nimport { PanelHeaderButton } from '~/components/ui/PanelHeaderButton';\nimport { Slider, type SliderOptions } from '~/components/ui/Slider';\nimport { workbenchStore, type WorkbenchViewType } from '~/lib/stores/workbench';\nimport { classNames } from '~/utils/classNames';\nimport { cubicEasingFn } from '~/utils/easings';\nimport { renderLogger } from '~/utils/logger';\nimport { EditorPanel } from './EditorPanel';\nimport { Preview } from './Preview';\n\ninterface WorkspaceProps {\n  chatStarted?: boolean;\n  isStreaming?: boolean;\n}\n\nconst viewTransition = { ease: cubicEasingFn };\n\nconst sliderOptions: SliderOptions<WorkbenchViewType> = {\n  left: {\n    value: 'code',\n    text: 'Code',\n  },\n  right: {\n    value: 'preview',\n    text: 'Preview',\n  },\n};\n\nconst workbenchVariants = {\n  closed: {\n    width: 0,\n    transition: {\n      duration: 0.2,\n      ease: cubicEasingFn,\n    },\n  },\n  open: {\n    width: 'var(--workbench-width)',\n    transition: {\n      duration: 0.2,\n      ease: cubicEasingFn,\n    },\n  },\n} satisfies Variants;\n\nexport const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) => {\n  renderLogger.trace('Workbench');\n\n  const hasPreview = useStore(computed(workbenchStore.previews, (previews) => previews.length > 0));\n  const showWorkbench = useStore(workbenchStore.showWorkbench);\n  const selectedFile = useStore(workbenchStore.selectedFile);\n  const currentDocument = useStore(workbenchStore.currentDocument);\n  const unsavedFiles = useStore(workbenchStore.unsavedFiles);\n  const files = useStore(workbenchStore.files);\n  const selectedView = useStore(workbenchStore.currentView);\n\n  const setSelectedView = (view: WorkbenchViewType) => {\n    workbenchStore.currentView.set(view);\n  };\n\n  useEffect(() => {\n    if (hasPreview) {\n      setSelectedView('preview');\n    }\n  }, [hasPreview]);\n\n  useEffect(() => {\n    workbenchStore.setDocuments(files);\n  }, [files]);\n\n  const onEditorChange = useCallback<OnEditorChange>((update) => {\n    workbenchStore.setCurrentDocumentContent(update.content);\n  }, []);\n\n  const onEditorScroll = useCallback<OnEditorScroll>((position) => {\n    workbenchStore.setCurrentDocumentScrollPosition(position);\n  }, []);\n\n  const onFileSelect = useCallback((filePath: string | undefined) => {\n    workbenchStore.setSelectedFile(filePath);\n  }, []);\n\n  const onFileSave = useCallback(() => {\n    workbenchStore.saveCurrentDocument().catch(() => {\n      toast.error('Failed to update file content');\n    });\n  }, []);\n\n  const onFileReset = useCallback(() => {\n    workbenchStore.resetCurrentDocument();\n  }, []);\n\n  return (\n    chatStarted && (\n      <motion.div\n        initial=\"closed\"\n        animate={showWorkbench ? 'open' : 'closed'}\n        variants={workbenchVariants}\n        className=\"z-workbench\"\n      >\n        <div\n          className={classNames(\n            'fixed top-[calc(var(--header-height)+1.5rem)] bottom-6 w-[var(--workbench-inner-width)] mr-4 z-0 transition-[left,width] duration-200 bolt-ease-cubic-bezier',\n            {\n              'left-[var(--workbench-left)]': showWorkbench,\n              'left-[100%]': !showWorkbench,\n            },\n          )}\n        >\n          <div className=\"absolute inset-0 px-6\">\n            <div className=\"h-full flex flex-col bg-bolt-elements-background-depth-2 border border-bolt-elements-borderColor shadow-sm rounded-lg overflow-hidden\">\n              <div className=\"flex items-center px-3 py-2 border-b border-bolt-elements-borderColor\">\n                <Slider selected={selectedView} options={sliderOptions} setSelected={setSelectedView} />\n                <div className=\"ml-auto\" />\n                {selectedView === 'code' && (\n                  <PanelHeaderButton\n                    className=\"mr-1 text-sm\"\n                    onClick={() => {\n                      workbenchStore.toggleTerminal(!workbenchStore.showTerminal.get());\n                    }}\n                  >\n                    <div className=\"i-ph:terminal\" />\n                    Toggle Terminal\n                  </PanelHeaderButton>\n                )}\n                <IconButton\n                  icon=\"i-ph:x-circle\"\n                  className=\"-mr-1\"\n                  size=\"xl\"\n                  onClick={() => {\n                    workbenchStore.showWorkbench.set(false);\n                  }}\n                />\n              </div>\n              <div className=\"relative flex-1 overflow-hidden\">\n                <View\n                  initial={{ x: selectedView === 'code' ? 0 : '-100%' }}\n                  animate={{ x: selectedView === 'code' ? 0 : '-100%' }}\n                >\n                  <EditorPanel\n                    editorDocument={currentDocument}\n                    isStreaming={isStreaming}\n                    selectedFile={selectedFile}\n                    files={files}\n                    unsavedFiles={unsavedFiles}\n                    onFileSelect={onFileSelect}\n                    onEditorScroll={onEditorScroll}\n                    onEditorChange={onEditorChange}\n                    onFileSave={onFileSave}\n                    onFileReset={onFileReset}\n                  />\n                </View>\n                <View\n                  initial={{ x: selectedView === 'preview' ? 0 : '100%' }}\n                  animate={{ x: selectedView === 'preview' ? 0 : '100%' }}\n                >\n                  <Preview />\n                </View>\n              </div>\n            </div>\n          </div>\n        </div>\n      </motion.div>\n    )\n  );\n});\n\ninterface ViewProps extends HTMLMotionProps<'div'> {\n  children: JSX.Element;\n}\n\nconst View = memo(({ children, ...props }: ViewProps) => {\n  return (\n    <motion.div className=\"absolute inset-0\" transition={viewTransition} {...props}>\n      {children}\n    </motion.div>\n  );\n});\n"
  },
  {
    "path": "app/components/workbench/terminal/Terminal.tsx",
    "content": "import { FitAddon } from '@xterm/addon-fit';\nimport { WebLinksAddon } from '@xterm/addon-web-links';\nimport { Terminal as XTerm } from '@xterm/xterm';\nimport { forwardRef, memo, useEffect, useImperativeHandle, useRef } from 'react';\nimport type { Theme } from '~/lib/stores/theme';\nimport { createScopedLogger } from '~/utils/logger';\nimport { getTerminalTheme } from './theme';\n\nconst logger = createScopedLogger('Terminal');\n\nexport interface TerminalRef {\n  reloadStyles: () => void;\n}\n\nexport interface TerminalProps {\n  className?: string;\n  theme: Theme;\n  readonly?: boolean;\n  onTerminalReady?: (terminal: XTerm) => void;\n  onTerminalResize?: (cols: number, rows: number) => void;\n}\n\nexport const Terminal = memo(\n  forwardRef<TerminalRef, TerminalProps>(({ className, theme, readonly, onTerminalReady, onTerminalResize }, ref) => {\n    const terminalElementRef = useRef<HTMLDivElement>(null);\n    const terminalRef = useRef<XTerm>();\n\n    useEffect(() => {\n      const element = terminalElementRef.current!;\n\n      const fitAddon = new FitAddon();\n      const webLinksAddon = new WebLinksAddon();\n\n      const terminal = new XTerm({\n        cursorBlink: true,\n        convertEol: true,\n        disableStdin: readonly,\n        theme: getTerminalTheme(readonly ? { cursor: '#00000000' } : {}),\n        fontSize: 12,\n        fontFamily: 'Menlo, courier-new, courier, monospace',\n      });\n\n      terminalRef.current = terminal;\n\n      terminal.loadAddon(fitAddon);\n      terminal.loadAddon(webLinksAddon);\n      terminal.open(element);\n\n      const resizeObserver = new ResizeObserver(() => {\n        fitAddon.fit();\n        onTerminalResize?.(terminal.cols, terminal.rows);\n      });\n\n      resizeObserver.observe(element);\n\n      logger.info('Attach terminal');\n\n      onTerminalReady?.(terminal);\n\n      return () => {\n        resizeObserver.disconnect();\n        terminal.dispose();\n      };\n    }, []);\n\n    useEffect(() => {\n      const terminal = terminalRef.current!;\n\n      // we render a transparent cursor in case the terminal is readonly\n      terminal.options.theme = getTerminalTheme(readonly ? { cursor: '#00000000' } : {});\n\n      terminal.options.disableStdin = readonly;\n    }, [theme, readonly]);\n\n    useImperativeHandle(ref, () => {\n      return {\n        reloadStyles: () => {\n          const terminal = terminalRef.current!;\n          terminal.options.theme = getTerminalTheme(readonly ? { cursor: '#00000000' } : {});\n        },\n      };\n    }, []);\n\n    return <div className={className} ref={terminalElementRef} />;\n  }),\n);\n"
  },
  {
    "path": "app/components/workbench/terminal/theme.ts",
    "content": "import type { ITheme } from '@xterm/xterm';\n\nconst style = getComputedStyle(document.documentElement);\nconst cssVar = (token: string) => style.getPropertyValue(token) || undefined;\n\nexport function getTerminalTheme(overrides?: ITheme): ITheme {\n  return {\n    cursor: cssVar('--bolt-elements-terminal-cursorColor'),\n    cursorAccent: cssVar('--bolt-elements-terminal-cursorColorAccent'),\n    foreground: cssVar('--bolt-elements-terminal-textColor'),\n    background: cssVar('--bolt-elements-terminal-backgroundColor'),\n    selectionBackground: cssVar('--bolt-elements-terminal-selection-backgroundColor'),\n    selectionForeground: cssVar('--bolt-elements-terminal-selection-textColor'),\n    selectionInactiveBackground: cssVar('--bolt-elements-terminal-selection-backgroundColorInactive'),\n\n    // ansi escape code colors\n    black: cssVar('--bolt-elements-terminal-color-black'),\n    red: cssVar('--bolt-elements-terminal-color-red'),\n    green: cssVar('--bolt-elements-terminal-color-green'),\n    yellow: cssVar('--bolt-elements-terminal-color-yellow'),\n    blue: cssVar('--bolt-elements-terminal-color-blue'),\n    magenta: cssVar('--bolt-elements-terminal-color-magenta'),\n    cyan: cssVar('--bolt-elements-terminal-color-cyan'),\n    white: cssVar('--bolt-elements-terminal-color-white'),\n    brightBlack: cssVar('--bolt-elements-terminal-color-brightBlack'),\n    brightRed: cssVar('--bolt-elements-terminal-color-brightRed'),\n    brightGreen: cssVar('--bolt-elements-terminal-color-brightGreen'),\n    brightYellow: cssVar('--bolt-elements-terminal-color-brightYellow'),\n    brightBlue: cssVar('--bolt-elements-terminal-color-brightBlue'),\n    brightMagenta: cssVar('--bolt-elements-terminal-color-brightMagenta'),\n    brightCyan: cssVar('--bolt-elements-terminal-color-brightCyan'),\n    brightWhite: cssVar('--bolt-elements-terminal-color-brightWhite'),\n\n    ...overrides,\n  };\n}\n"
  },
  {
    "path": "app/entry.client.tsx",
    "content": "import { RemixBrowser } from '@remix-run/react';\nimport { startTransition } from 'react';\nimport { hydrateRoot } from 'react-dom/client';\n\nstartTransition(() => {\n  hydrateRoot(document.getElementById('root')!, <RemixBrowser />);\n});\n"
  },
  {
    "path": "app/entry.server.tsx",
    "content": "import type { AppLoadContext, EntryContext } from '@remix-run/cloudflare';\nimport { RemixServer } from '@remix-run/react';\nimport { isbot } from 'isbot';\nimport { renderToReadableStream } from 'react-dom/server';\nimport { renderHeadToString } from 'remix-island';\nimport { Head } from './root';\nimport { themeStore } from '~/lib/stores/theme';\n\nexport default async function handleRequest(\n  request: Request,\n  responseStatusCode: number,\n  responseHeaders: Headers,\n  remixContext: EntryContext,\n  _loadContext: AppLoadContext,\n) {\n  const readable = await renderToReadableStream(<RemixServer context={remixContext} url={request.url} />, {\n    signal: request.signal,\n    onError(error: unknown) {\n      console.error(error);\n      responseStatusCode = 500;\n    },\n  });\n\n  const body = new ReadableStream({\n    start(controller) {\n      const head = renderHeadToString({ request, remixContext, Head });\n\n      controller.enqueue(\n        new Uint8Array(\n          new TextEncoder().encode(\n            `<!DOCTYPE html><html lang=\"en\" data-theme=\"${themeStore.value}\"><head>${head}</head><body><div id=\"root\" class=\"w-full h-full\">`,\n          ),\n        ),\n      );\n\n      const reader = readable.getReader();\n\n      function read() {\n        reader\n          .read()\n          .then(({ done, value }) => {\n            if (done) {\n              controller.enqueue(new Uint8Array(new TextEncoder().encode(`</div></body></html>`)));\n              controller.close();\n\n              return;\n            }\n\n            controller.enqueue(value);\n            read();\n          })\n          .catch((error) => {\n            controller.error(error);\n            readable.cancel();\n          });\n      }\n      read();\n    },\n\n    cancel() {\n      readable.cancel();\n    },\n  });\n\n  if (isbot(request.headers.get('user-agent') || '')) {\n    await readable.allReady;\n  }\n\n  responseHeaders.set('Content-Type', 'text/html');\n\n  responseHeaders.set('Cross-Origin-Embedder-Policy', 'require-corp');\n  responseHeaders.set('Cross-Origin-Opener-Policy', 'same-origin');\n\n  return new Response(body, {\n    headers: responseHeaders,\n    status: responseStatusCode,\n  });\n}\n"
  },
  {
    "path": "app/lib/.server/llm/api-key.ts",
    "content": "import { env } from 'node:process';\n\nexport function getAPIKey(cloudflareEnv: Env) {\n  /**\n   * The `cloudflareEnv` is only used when deployed or when previewing locally.\n   * In development the environment variables are available through `env`.\n   */\n  return env.ANTHROPIC_API_KEY || cloudflareEnv.ANTHROPIC_API_KEY;\n}\n"
  },
  {
    "path": "app/lib/.server/llm/constants.ts",
    "content": "// see https://docs.anthropic.com/en/docs/about-claude/models\nexport const MAX_TOKENS = 8192;\n\n// limits the number of model responses that can be returned in a single request\nexport const MAX_RESPONSE_SEGMENTS = 2;\n"
  },
  {
    "path": "app/lib/.server/llm/model.ts",
    "content": "import { createAnthropic } from '@ai-sdk/anthropic';\n\nexport function getAnthropicModel(apiKey: string) {\n  const anthropic = createAnthropic({\n    apiKey,\n  });\n\n  return anthropic('claude-3-5-sonnet-20240620');\n}\n"
  },
  {
    "path": "app/lib/.server/llm/prompts.ts",
    "content": "import { MODIFICATIONS_TAG_NAME, WORK_DIR } from '~/utils/constants';\nimport { allowedHTMLElements } from '~/utils/markdown';\nimport { stripIndents } from '~/utils/stripIndent';\n\nexport const getSystemPrompt = (cwd: string = WORK_DIR) => `\nYou are Bolt, an expert AI assistant and exceptional senior software developer with vast knowledge across multiple programming languages, frameworks, and best practices.\n\n<system_constraints>\n  You are operating in an environment called WebContainer, an in-browser Node.js runtime that emulates a Linux system to some degree. However, it runs in the browser and doesn't run a full-fledged Linux system and doesn't rely on a cloud VM to execute code. All code is executed in the browser. It does come with a shell that emulates zsh. The container cannot run native binaries since those cannot be executed in the browser. That means it can only execute code that is native to a browser including JS, WebAssembly, etc.\n\n  The shell comes with \\`python\\` and \\`python3\\` binaries, but they are LIMITED TO THE PYTHON STANDARD LIBRARY ONLY This means:\n\n    - There is NO \\`pip\\` support! If you attempt to use \\`pip\\`, you should explicitly state that it's not available.\n    - CRITICAL: Third-party libraries cannot be installed or imported.\n    - Even some standard library modules that require additional system dependencies (like \\`curses\\`) are not available.\n    - Only modules from the core Python standard library can be used.\n\n  Additionally, there is no \\`g++\\` or any C/C++ compiler available. WebContainer CANNOT run native binaries or compile C/C++ code!\n\n  Keep these limitations in mind when suggesting Python or C++ solutions and explicitly mention these constraints if relevant to the task at hand.\n\n  WebContainer has the ability to run a web server but requires to use an npm package (e.g., Vite, servor, serve, http-server) or use the Node.js APIs to implement a web server.\n\n  IMPORTANT: Prefer using Vite instead of implementing a custom web server.\n\n  IMPORTANT: Git is NOT available.\n\n  IMPORTANT: Prefer writing Node.js scripts instead of shell scripts. The environment doesn't fully support shell scripts, so use Node.js for scripting tasks whenever possible!\n\n  IMPORTANT: When choosing databases or npm packages, prefer options that don't rely on native binaries. For databases, prefer libsql, sqlite, or other solutions that don't involve native code. WebContainer CANNOT execute arbitrary native binaries.\n\n  Available shell commands: cat, chmod, cp, echo, hostname, kill, ln, ls, mkdir, mv, ps, pwd, rm, rmdir, xxd, alias, cd, clear, curl, env, false, getconf, head, sort, tail, touch, true, uptime, which, code, jq, loadenv, node, python3, wasm, xdg-open, command, exit, export, source\n</system_constraints>\n\n<code_formatting_info>\n  Use 2 spaces for code indentation\n</code_formatting_info>\n\n<message_formatting_info>\n  You can make the output pretty by using only the following available HTML elements: ${allowedHTMLElements.map((tagName) => `<${tagName}>`).join(', ')}\n</message_formatting_info>\n\n<diff_spec>\n  For user-made file modifications, a \\`<${MODIFICATIONS_TAG_NAME}>\\` section will appear at the start of the user message. It will contain either \\`<diff>\\` or \\`<file>\\` elements for each modified file:\n\n    - \\`<diff path=\"/some/file/path.ext\">\\`: Contains GNU unified diff format changes\n    - \\`<file path=\"/some/file/path.ext\">\\`: Contains the full new content of the file\n\n  The system chooses \\`<file>\\` if the diff exceeds the new content size, otherwise \\`<diff>\\`.\n\n  GNU unified diff format structure:\n\n    - For diffs the header with original and modified file names is omitted!\n    - Changed sections start with @@ -X,Y +A,B @@ where:\n      - X: Original file starting line\n      - Y: Original file line count\n      - A: Modified file starting line\n      - B: Modified file line count\n    - (-) lines: Removed from original\n    - (+) lines: Added in modified version\n    - Unmarked lines: Unchanged context\n\n  Example:\n\n  <${MODIFICATIONS_TAG_NAME}>\n    <diff path=\"/home/project/src/main.js\">\n      @@ -2,7 +2,10 @@\n        return a + b;\n      }\n\n      -console.log('Hello, World!');\n      +console.log('Hello, Bolt!');\n      +\n      function greet() {\n      -  return 'Greetings!';\n      +  return 'Greetings!!';\n      }\n      +\n      +console.log('The End');\n    </diff>\n    <file path=\"/home/project/package.json\">\n      // full file content here\n    </file>\n  </${MODIFICATIONS_TAG_NAME}>\n</diff_spec>\n\n<artifact_info>\n  Bolt creates a SINGLE, comprehensive artifact for each project. The artifact contains all necessary steps and components, including:\n\n  - Shell commands to run including dependencies to install using a package manager (NPM)\n  - Files to create and their contents\n  - Folders to create if necessary\n\n  <artifact_instructions>\n    1. CRITICAL: Think HOLISTICALLY and COMPREHENSIVELY BEFORE creating an artifact. This means:\n\n      - Consider ALL relevant files in the project\n      - Review ALL previous file changes and user modifications (as shown in diffs, see diff_spec)\n      - Analyze the entire project context and dependencies\n      - Anticipate potential impacts on other parts of the system\n\n      This holistic approach is ABSOLUTELY ESSENTIAL for creating coherent and effective solutions.\n\n    2. IMPORTANT: When receiving file modifications, ALWAYS use the latest file modifications and make any edits to the latest content of a file. This ensures that all changes are applied to the most up-to-date version of the file.\n\n    3. The current working directory is \\`${cwd}\\`.\n\n    4. Wrap the content in opening and closing \\`<boltArtifact>\\` tags. These tags contain more specific \\`<boltAction>\\` elements.\n\n    5. Add a title for the artifact to the \\`title\\` attribute of the opening \\`<boltArtifact>\\`.\n\n    6. Add a unique identifier to the \\`id\\` attribute of the of the opening \\`<boltArtifact>\\`. For updates, reuse the prior identifier. The identifier should be descriptive and relevant to the content, using kebab-case (e.g., \"example-code-snippet\"). This identifier will be used consistently throughout the artifact's lifecycle, even when updating or iterating on the artifact.\n\n    7. Use \\`<boltAction>\\` tags to define specific actions to perform.\n\n    8. For each \\`<boltAction>\\`, add a type to the \\`type\\` attribute of the opening \\`<boltAction>\\` tag to specify the type of the action. Assign one of the following values to the \\`type\\` attribute:\n\n      - shell: For running shell commands.\n\n        - When Using \\`npx\\`, ALWAYS provide the \\`--yes\\` flag.\n        - When running multiple shell commands, use \\`&&\\` to run them sequentially.\n        - ULTRA IMPORTANT: Do NOT re-run a dev command if there is one that starts a dev server and new dependencies were installed or files updated! If a dev server has started already, assume that installing dependencies will be executed in a different process and will be picked up by the dev server.\n\n      - file: For writing new files or updating existing files. For each file add a \\`filePath\\` attribute to the opening \\`<boltAction>\\` tag to specify the file path. The content of the file artifact is the file contents. All file paths MUST BE relative to the current working directory.\n\n    9. The order of the actions is VERY IMPORTANT. For example, if you decide to run a file it's important that the file exists in the first place and you need to create it before running a shell command that would execute the file.\n\n    10. ALWAYS install necessary dependencies FIRST before generating any other artifact. If that requires a \\`package.json\\` then you should create that first!\n\n      IMPORTANT: Add all required dependencies to the \\`package.json\\` already and try to avoid \\`npm i <pkg>\\` if possible!\n\n    11. CRITICAL: Always provide the FULL, updated content of the artifact. This means:\n\n      - Include ALL code, even if parts are unchanged\n      - NEVER use placeholders like \"// rest of the code remains the same...\" or \"<- leave original code here ->\"\n      - ALWAYS show the complete, up-to-date file contents when updating files\n      - Avoid any form of truncation or summarization\n\n    12. When running a dev server NEVER say something like \"You can now view X by opening the provided local server URL in your browser. The preview will be opened automatically or by the user manually!\n\n    13. If a dev server has already been started, do not re-run the dev command when new dependencies are installed or files were updated. Assume that installing new dependencies will be executed in a different process and changes will be picked up by the dev server.\n\n    14. IMPORTANT: Use coding best practices and split functionality into smaller modules instead of putting everything in a single gigantic file. Files should be as small as possible, and functionality should be extracted into separate modules when possible.\n\n      - Ensure code is clean, readable, and maintainable.\n      - Adhere to proper naming conventions and consistent formatting.\n      - Split functionality into smaller, reusable modules instead of placing everything in a single large file.\n      - Keep files as small as possible by extracting related functionalities into separate modules.\n      - Use imports to connect these modules together effectively.\n  </artifact_instructions>\n</artifact_info>\n\nNEVER use the word \"artifact\". For example:\n  - DO NOT SAY: \"This artifact sets up a simple Snake game using HTML, CSS, and JavaScript.\"\n  - INSTEAD SAY: \"We set up a simple Snake game using HTML, CSS, and JavaScript.\"\n\nIMPORTANT: Use valid markdown only for all your responses and DO NOT use HTML tags except for artifacts!\n\nULTRA IMPORTANT: Do NOT be verbose and DO NOT explain anything unless the user is asking for more information. That is VERY important.\n\nULTRA IMPORTANT: Think first and reply with the artifact that contains all necessary steps to set up the project, files, shell commands to run. It is SUPER IMPORTANT to respond with this first.\n\nHere are some examples of correct usage of artifacts:\n\n<examples>\n  <example>\n    <user_query>Can you help me create a JavaScript function to calculate the factorial of a number?</user_query>\n\n    <assistant_response>\n      Certainly, I can help you create a JavaScript function to calculate the factorial of a number.\n\n      <boltArtifact id=\"factorial-function\" title=\"JavaScript Factorial Function\">\n        <boltAction type=\"file\" filePath=\"index.js\">\n          function factorial(n) {\n           ...\n          }\n\n          ...\n        </boltAction>\n\n        <boltAction type=\"shell\">\n          node index.js\n        </boltAction>\n      </boltArtifact>\n    </assistant_response>\n  </example>\n\n  <example>\n    <user_query>Build a snake game</user_query>\n\n    <assistant_response>\n      Certainly! I'd be happy to help you build a snake game using JavaScript and HTML5 Canvas. This will be a basic implementation that you can later expand upon. Let's create the game step by step.\n\n      <boltArtifact id=\"snake-game\" title=\"Snake Game in HTML and JavaScript\">\n        <boltAction type=\"file\" filePath=\"package.json\">\n          {\n            \"name\": \"snake\",\n            \"scripts\": {\n              \"dev\": \"vite\"\n            }\n            ...\n          }\n        </boltAction>\n\n        <boltAction type=\"shell\">\n          npm install --save-dev vite\n        </boltAction>\n\n        <boltAction type=\"file\" filePath=\"index.html\">\n          ...\n        </boltAction>\n\n        <boltAction type=\"shell\">\n          npm run dev\n        </boltAction>\n      </boltArtifact>\n\n      Now you can play the Snake game by opening the provided local server URL in your browser. Use the arrow keys to control the snake. Eat the red food to grow and increase your score. The game ends if you hit the wall or your own tail.\n    </assistant_response>\n  </example>\n\n  <example>\n    <user_query>Make a bouncing ball with real gravity using React</user_query>\n\n    <assistant_response>\n      Certainly! I'll create a bouncing ball with real gravity using React. We'll use the react-spring library for physics-based animations.\n\n      <boltArtifact id=\"bouncing-ball-react\" title=\"Bouncing Ball with Gravity in React\">\n        <boltAction type=\"file\" filePath=\"package.json\">\n          {\n            \"name\": \"bouncing-ball\",\n            \"private\": true,\n            \"version\": \"0.0.0\",\n            \"type\": \"module\",\n            \"scripts\": {\n              \"dev\": \"vite\",\n              \"build\": \"vite build\",\n              \"preview\": \"vite preview\"\n            },\n            \"dependencies\": {\n              \"react\": \"^18.2.0\",\n              \"react-dom\": \"^18.2.0\",\n              \"react-spring\": \"^9.7.1\"\n            },\n            \"devDependencies\": {\n              \"@types/react\": \"^18.0.28\",\n              \"@types/react-dom\": \"^18.0.11\",\n              \"@vitejs/plugin-react\": \"^3.1.0\",\n              \"vite\": \"^4.2.0\"\n            }\n          }\n        </boltAction>\n\n        <boltAction type=\"file\" filePath=\"index.html\">\n          ...\n        </boltAction>\n\n        <boltAction type=\"file\" filePath=\"src/main.jsx\">\n          ...\n        </boltAction>\n\n        <boltAction type=\"file\" filePath=\"src/index.css\">\n          ...\n        </boltAction>\n\n        <boltAction type=\"file\" filePath=\"src/App.jsx\">\n          ...\n        </boltAction>\n\n        <boltAction type=\"shell\">\n          npm run dev\n        </boltAction>\n      </boltArtifact>\n\n      You can now view the bouncing ball animation in the preview. The ball will start falling from the top of the screen and bounce realistically when it hits the bottom.\n    </assistant_response>\n  </example>\n</examples>\n`;\n\nexport const CONTINUE_PROMPT = stripIndents`\n  Continue your prior response. IMPORTANT: Immediately begin from where you left off without any interruptions.\n  Do not repeat any content, including artifact and action tags.\n`;\n"
  },
  {
    "path": "app/lib/.server/llm/stream-text.ts",
    "content": "import { streamText as _streamText, convertToCoreMessages } from 'ai';\nimport { getAPIKey } from '~/lib/.server/llm/api-key';\nimport { getAnthropicModel } from '~/lib/.server/llm/model';\nimport { MAX_TOKENS } from './constants';\nimport { getSystemPrompt } from './prompts';\n\ninterface ToolResult<Name extends string, Args, Result> {\n  toolCallId: string;\n  toolName: Name;\n  args: Args;\n  result: Result;\n}\n\ninterface Message {\n  role: 'user' | 'assistant';\n  content: string;\n  toolInvocations?: ToolResult<string, unknown, unknown>[];\n}\n\nexport type Messages = Message[];\n\nexport type StreamingOptions = Omit<Parameters<typeof _streamText>[0], 'model'>;\n\nexport function streamText(messages: Messages, env: Env, options?: StreamingOptions) {\n  return _streamText({\n    model: getAnthropicModel(getAPIKey(env)),\n    system: getSystemPrompt(),\n    maxTokens: MAX_TOKENS,\n    headers: {\n      'anthropic-beta': 'max-tokens-3-5-sonnet-2024-07-15',\n    },\n    messages: convertToCoreMessages(messages),\n    ...options,\n  });\n}\n"
  },
  {
    "path": "app/lib/.server/llm/switchable-stream.ts",
    "content": "export default class SwitchableStream extends TransformStream {\n  private _controller: TransformStreamDefaultController | null = null;\n  private _currentReader: ReadableStreamDefaultReader | null = null;\n  private _switches = 0;\n\n  constructor() {\n    let controllerRef: TransformStreamDefaultController | undefined;\n\n    super({\n      start(controller) {\n        controllerRef = controller;\n      },\n    });\n\n    if (controllerRef === undefined) {\n      throw new Error('Controller not properly initialized');\n    }\n\n    this._controller = controllerRef;\n  }\n\n  async switchSource(newStream: ReadableStream) {\n    if (this._currentReader) {\n      await this._currentReader.cancel();\n    }\n\n    this._currentReader = newStream.getReader();\n\n    this._pumpStream();\n\n    this._switches++;\n  }\n\n  private async _pumpStream() {\n    if (!this._currentReader || !this._controller) {\n      throw new Error('Stream is not properly initialized');\n    }\n\n    try {\n      while (true) {\n        const { done, value } = await this._currentReader.read();\n\n        if (done) {\n          break;\n        }\n\n        this._controller.enqueue(value);\n      }\n    } catch (error) {\n      console.log(error);\n      this._controller.error(error);\n    }\n  }\n\n  close() {\n    if (this._currentReader) {\n      this._currentReader.cancel();\n    }\n\n    this._controller?.terminate();\n  }\n\n  get switches() {\n    return this._switches;\n  }\n}\n"
  },
  {
    "path": "app/lib/crypto.ts",
    "content": "const encoder = new TextEncoder();\nconst decoder = new TextDecoder();\nconst IV_LENGTH = 16;\n\nexport async function encrypt(key: string, data: string) {\n  const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));\n  const cryptoKey = await getKey(key);\n\n  const ciphertext = await crypto.subtle.encrypt(\n    {\n      name: 'AES-CBC',\n      iv,\n    },\n    cryptoKey,\n    encoder.encode(data),\n  );\n\n  const bundle = new Uint8Array(IV_LENGTH + ciphertext.byteLength);\n\n  bundle.set(new Uint8Array(ciphertext));\n  bundle.set(iv, ciphertext.byteLength);\n\n  return decodeBase64(bundle);\n}\n\nexport async function decrypt(key: string, payload: string) {\n  const bundle = encodeBase64(payload);\n\n  const iv = new Uint8Array(bundle.buffer, bundle.byteLength - IV_LENGTH);\n  const ciphertext = new Uint8Array(bundle.buffer, 0, bundle.byteLength - IV_LENGTH);\n\n  const cryptoKey = await getKey(key);\n\n  const plaintext = await crypto.subtle.decrypt(\n    {\n      name: 'AES-CBC',\n      iv,\n    },\n    cryptoKey,\n    ciphertext,\n  );\n\n  return decoder.decode(plaintext);\n}\n\nasync function getKey(key: string) {\n  return await crypto.subtle.importKey('raw', encodeBase64(key), { name: 'AES-CBC' }, false, ['encrypt', 'decrypt']);\n}\n\nfunction decodeBase64(encoded: Uint8Array) {\n  const byteChars = Array.from(encoded, (byte) => String.fromCodePoint(byte));\n\n  return btoa(byteChars.join(''));\n}\n\nfunction encodeBase64(data: string) {\n  return Uint8Array.from(atob(data), (ch) => ch.codePointAt(0)!);\n}\n"
  },
  {
    "path": "app/lib/fetch.ts",
    "content": "type CommonRequest = Omit<RequestInit, 'body'> & { body?: URLSearchParams };\n\nexport async function request(url: string, init?: CommonRequest) {\n  if (import.meta.env.DEV) {\n    const nodeFetch = await import('node-fetch');\n    const https = await import('node:https');\n\n    const agent = url.startsWith('https') ? new https.Agent({ rejectUnauthorized: false }) : undefined;\n\n    return nodeFetch.default(url, { ...init, agent });\n  }\n\n  return fetch(url, init);\n}\n"
  },
  {
    "path": "app/lib/hooks/index.ts",
    "content": "export * from './useMessageParser';\nexport * from './usePromptEnhancer';\nexport * from './useShortcuts';\nexport * from './useSnapScroll';\n"
  },
  {
    "path": "app/lib/hooks/useMessageParser.ts",
    "content": "import type { Message } from 'ai';\nimport { useCallback, useState } from 'react';\nimport { StreamingMessageParser } from '~/lib/runtime/message-parser';\nimport { workbenchStore } from '~/lib/stores/workbench';\nimport { createScopedLogger } from '~/utils/logger';\n\nconst logger = createScopedLogger('useMessageParser');\n\nconst messageParser = new StreamingMessageParser({\n  callbacks: {\n    onArtifactOpen: (data) => {\n      logger.trace('onArtifactOpen', data);\n\n      workbenchStore.showWorkbench.set(true);\n      workbenchStore.addArtifact(data);\n    },\n    onArtifactClose: (data) => {\n      logger.trace('onArtifactClose');\n\n      workbenchStore.updateArtifact(data, { closed: true });\n    },\n    onActionOpen: (data) => {\n      logger.trace('onActionOpen', data.action);\n\n      // we only add shell actions when when the close tag got parsed because only then we have the content\n      if (data.action.type !== 'shell') {\n        workbenchStore.addAction(data);\n      }\n    },\n    onActionClose: (data) => {\n      logger.trace('onActionClose', data.action);\n\n      if (data.action.type === 'shell') {\n        workbenchStore.addAction(data);\n      }\n\n      workbenchStore.runAction(data);\n    },\n  },\n});\n\nexport function useMessageParser() {\n  const [parsedMessages, setParsedMessages] = useState<{ [key: number]: string }>({});\n\n  const parseMessages = useCallback((messages: Message[], isLoading: boolean) => {\n    let reset = false;\n\n    if (import.meta.env.DEV && !isLoading) {\n      reset = true;\n      messageParser.reset();\n    }\n\n    for (const [index, message] of messages.entries()) {\n      if (message.role === 'assistant') {\n        const newParsedContent = messageParser.parse(message.id, message.content);\n\n        setParsedMessages((prevParsed) => ({\n          ...prevParsed,\n          [index]: !reset ? (prevParsed[index] || '') + newParsedContent : newParsedContent,\n        }));\n      }\n    }\n  }, []);\n\n  return { parsedMessages, parseMessages };\n}\n"
  },
  {
    "path": "app/lib/hooks/usePromptEnhancer.ts",
    "content": "import { useState } from 'react';\nimport { createScopedLogger } from '~/utils/logger';\n\nconst logger = createScopedLogger('usePromptEnhancement');\n\nexport function usePromptEnhancer() {\n  const [enhancingPrompt, setEnhancingPrompt] = useState(false);\n  const [promptEnhanced, setPromptEnhanced] = useState(false);\n\n  const resetEnhancer = () => {\n    setEnhancingPrompt(false);\n    setPromptEnhanced(false);\n  };\n\n  const enhancePrompt = async (input: string, setInput: (value: string) => void) => {\n    setEnhancingPrompt(true);\n    setPromptEnhanced(false);\n\n    const response = await fetch('/api/enhancer', {\n      method: 'POST',\n      body: JSON.stringify({\n        message: input,\n      }),\n    });\n\n    const reader = response.body?.getReader();\n\n    const originalInput = input;\n\n    if (reader) {\n      const decoder = new TextDecoder();\n\n      let _input = '';\n      let _error;\n\n      try {\n        setInput('');\n\n        while (true) {\n          const { value, done } = await reader.read();\n\n          if (done) {\n            break;\n          }\n\n          _input += decoder.decode(value);\n\n          logger.trace('Set input', _input);\n\n          setInput(_input);\n        }\n      } catch (error) {\n        _error = error;\n        setInput(originalInput);\n      } finally {\n        if (_error) {\n          logger.error(_error);\n        }\n\n        setEnhancingPrompt(false);\n        setPromptEnhanced(true);\n\n        setTimeout(() => {\n          setInput(_input);\n        });\n      }\n    }\n  };\n\n  return { enhancingPrompt, promptEnhanced, enhancePrompt, resetEnhancer };\n}\n"
  },
  {
    "path": "app/lib/hooks/useShortcuts.ts",
    "content": "import { useStore } from '@nanostores/react';\nimport { useEffect } from 'react';\nimport { shortcutsStore, type Shortcuts } from '~/lib/stores/settings';\n\nclass ShortcutEventEmitter {\n  #emitter = new EventTarget();\n\n  dispatch(type: keyof Shortcuts) {\n    this.#emitter.dispatchEvent(new Event(type));\n  }\n\n  on(type: keyof Shortcuts, cb: VoidFunction) {\n    this.#emitter.addEventListener(type, cb);\n\n    return () => {\n      this.#emitter.removeEventListener(type, cb);\n    };\n  }\n}\n\nexport const shortcutEventEmitter = new ShortcutEventEmitter();\n\nexport function useShortcuts(): void {\n  const shortcuts = useStore(shortcutsStore);\n\n  useEffect(() => {\n    const handleKeyDown = (event: KeyboardEvent): void => {\n      const { key, ctrlKey, shiftKey, altKey, metaKey } = event;\n\n      for (const name in shortcuts) {\n        const shortcut = shortcuts[name as keyof Shortcuts];\n\n        if (\n          shortcut.key.toLowerCase() === key.toLowerCase() &&\n          (shortcut.ctrlOrMetaKey\n            ? ctrlKey || metaKey\n            : (shortcut.ctrlKey === undefined || shortcut.ctrlKey === ctrlKey) &&\n              (shortcut.metaKey === undefined || shortcut.metaKey === metaKey)) &&\n          (shortcut.shiftKey === undefined || shortcut.shiftKey === shiftKey) &&\n          (shortcut.altKey === undefined || shortcut.altKey === altKey)\n        ) {\n          shortcutEventEmitter.dispatch(name as keyof Shortcuts);\n          event.preventDefault();\n          event.stopPropagation();\n\n          shortcut.action();\n\n          break;\n        }\n      }\n    };\n\n    window.addEventListener('keydown', handleKeyDown);\n\n    return () => {\n      window.removeEventListener('keydown', handleKeyDown);\n    };\n  }, [shortcuts]);\n}\n"
  },
  {
    "path": "app/lib/hooks/useSnapScroll.ts",
    "content": "import { useRef, useCallback } from 'react';\n\nexport function useSnapScroll() {\n  const autoScrollRef = useRef(true);\n  const scrollNodeRef = useRef<HTMLDivElement>();\n  const onScrollRef = useRef<() => void>();\n  const observerRef = useRef<ResizeObserver>();\n\n  const messageRef = useCallback((node: HTMLDivElement | null) => {\n    if (node) {\n      const observer = new ResizeObserver(() => {\n        if (autoScrollRef.current && scrollNodeRef.current) {\n          const { scrollHeight, clientHeight } = scrollNodeRef.current;\n          const scrollTarget = scrollHeight - clientHeight;\n\n          scrollNodeRef.current.scrollTo({\n            top: scrollTarget,\n          });\n        }\n      });\n\n      observer.observe(node);\n    } else {\n      observerRef.current?.disconnect();\n      observerRef.current = undefined;\n    }\n  }, []);\n\n  const scrollRef = useCallback((node: HTMLDivElement | null) => {\n    if (node) {\n      onScrollRef.current = () => {\n        const { scrollTop, scrollHeight, clientHeight } = node;\n        const scrollTarget = scrollHeight - clientHeight;\n\n        autoScrollRef.current = Math.abs(scrollTop - scrollTarget) <= 10;\n      };\n\n      node.addEventListener('scroll', onScrollRef.current);\n\n      scrollNodeRef.current = node;\n    } else {\n      if (onScrollRef.current) {\n        scrollNodeRef.current?.removeEventListener('scroll', onScrollRef.current);\n      }\n\n      scrollNodeRef.current = undefined;\n      onScrollRef.current = undefined;\n    }\n  }, []);\n\n  return [messageRef, scrollRef];\n}\n"
  },
  {
    "path": "app/lib/persistence/ChatDescription.client.tsx",
    "content": "import { useStore } from '@nanostores/react';\nimport { description } from './useChatHistory';\n\nexport function ChatDescription() {\n  return useStore(description);\n}\n"
  },
  {
    "path": "app/lib/persistence/db.ts",
    "content": "import type { Message } from 'ai';\nimport { createScopedLogger } from '~/utils/logger';\nimport type { ChatHistoryItem } from './useChatHistory';\n\nconst logger = createScopedLogger('ChatHistory');\n\n// this is used at the top level and never rejects\nexport async function openDatabase(): Promise<IDBDatabase | undefined> {\n  return new Promise((resolve) => {\n    const request = indexedDB.open('boltHistory', 1);\n\n    request.onupgradeneeded = (event: IDBVersionChangeEvent) => {\n      const db = (event.target as IDBOpenDBRequest).result;\n\n      if (!db.objectStoreNames.contains('chats')) {\n        const store = db.createObjectStore('chats', { keyPath: 'id' });\n        store.createIndex('id', 'id', { unique: true });\n        store.createIndex('urlId', 'urlId', { unique: true });\n      }\n    };\n\n    request.onsuccess = (event: Event) => {\n      resolve((event.target as IDBOpenDBRequest).result);\n    };\n\n    request.onerror = (event: Event) => {\n      resolve(undefined);\n      logger.error((event.target as IDBOpenDBRequest).error);\n    };\n  });\n}\n\nexport async function getAll(db: IDBDatabase): Promise<ChatHistoryItem[]> {\n  return new Promise((resolve, reject) => {\n    const transaction = db.transaction('chats', 'readonly');\n    const store = transaction.objectStore('chats');\n    const request = store.getAll();\n\n    request.onsuccess = () => resolve(request.result as ChatHistoryItem[]);\n    request.onerror = () => reject(request.error);\n  });\n}\n\nexport async function setMessages(\n  db: IDBDatabase,\n  id: string,\n  messages: Message[],\n  urlId?: string,\n  description?: string,\n): Promise<void> {\n  return new Promise((resolve, reject) => {\n    const transaction = db.transaction('chats', 'readwrite');\n    const store = transaction.objectStore('chats');\n\n    const request = store.put({\n      id,\n      messages,\n      urlId,\n      description,\n      timestamp: new Date().toISOString(),\n    });\n\n    request.onsuccess = () => resolve();\n    request.onerror = () => reject(request.error);\n  });\n}\n\nexport async function getMessages(db: IDBDatabase, id: string): Promise<ChatHistoryItem> {\n  return (await getMessagesById(db, id)) || (await getMessagesByUrlId(db, id));\n}\n\nexport async function getMessagesByUrlId(db: IDBDatabase, id: string): Promise<ChatHistoryItem> {\n  return new Promise((resolve, reject) => {\n    const transaction = db.transaction('chats', 'readonly');\n    const store = transaction.objectStore('chats');\n    const index = store.index('urlId');\n    const request = index.get(id);\n\n    request.onsuccess = () => resolve(request.result as ChatHistoryItem);\n    request.onerror = () => reject(request.error);\n  });\n}\n\nexport async function getMessagesById(db: IDBDatabase, id: string): Promise<ChatHistoryItem> {\n  return new Promise((resolve, reject) => {\n    const transaction = db.transaction('chats', 'readonly');\n    const store = transaction.objectStore('chats');\n    const request = store.get(id);\n\n    request.onsuccess = () => resolve(request.result as ChatHistoryItem);\n    request.onerror = () => reject(request.error);\n  });\n}\n\nexport async function deleteById(db: IDBDatabase, id: string): Promise<void> {\n  return new Promise((resolve, reject) => {\n    const transaction = db.transaction('chats', 'readwrite');\n    const store = transaction.objectStore('chats');\n    const request = store.delete(id);\n\n    request.onsuccess = () => resolve(undefined);\n    request.onerror = () => reject(request.error);\n  });\n}\n\nexport async function getNextId(db: IDBDatabase): Promise<string> {\n  return new Promise((resolve, reject) => {\n    const transaction = db.transaction('chats', 'readonly');\n    const store = transaction.objectStore('chats');\n    const request = store.getAllKeys();\n\n    request.onsuccess = () => {\n      const highestId = request.result.reduce((cur, acc) => Math.max(+cur, +acc), 0);\n      resolve(String(+highestId + 1));\n    };\n\n    request.onerror = () => reject(request.error);\n  });\n}\n\nexport async function getUrlId(db: IDBDatabase, id: string): Promise<string> {\n  const idList = await getUrlIds(db);\n\n  if (!idList.includes(id)) {\n    return id;\n  } else {\n    let i = 2;\n\n    while (idList.includes(`${id}-${i}`)) {\n      i++;\n    }\n\n    return `${id}-${i}`;\n  }\n}\n\nasync function getUrlIds(db: IDBDatabase): Promise<string[]> {\n  return new Promise((resolve, reject) => {\n    const transaction = db.transaction('chats', 'readonly');\n    const store = transaction.objectStore('chats');\n    const idList: string[] = [];\n\n    const request = store.openCursor();\n\n    request.onsuccess = (event: Event) => {\n      const cursor = (event.target as IDBRequest<IDBCursorWithValue>).result;\n\n      if (cursor) {\n        idList.push(cursor.value.urlId);\n        cursor.continue();\n      } else {\n        resolve(idList);\n      }\n    };\n\n    request.onerror = () => {\n      reject(request.error);\n    };\n  });\n}\n"
  },
  {
    "path": "app/lib/persistence/index.ts",
    "content": "export * from './db';\nexport * from './useChatHistory';\n"
  },
  {
    "path": "app/lib/persistence/useChatHistory.ts",
    "content": "import { useLoaderData, useNavigate } from '@remix-run/react';\nimport { useState, useEffect } from 'react';\nimport { atom } from 'nanostores';\nimport type { Message } from 'ai';\nimport { toast } from 'react-toastify';\nimport { workbenchStore } from '~/lib/stores/workbench';\nimport { getMessages, getNextId, getUrlId, openDatabase, setMessages } from './db';\n\nexport interface ChatHistoryItem {\n  id: string;\n  urlId?: string;\n  description?: string;\n  messages: Message[];\n  timestamp: string;\n}\n\nconst persistenceEnabled = !import.meta.env.VITE_DISABLE_PERSISTENCE;\n\nexport const db = persistenceEnabled ? await openDatabase() : undefined;\n\nexport const chatId = atom<string | undefined>(undefined);\nexport const description = atom<string | undefined>(undefined);\n\nexport function useChatHistory() {\n  const navigate = useNavigate();\n  const { id: mixedId } = useLoaderData<{ id?: string }>();\n\n  const [initialMessages, setInitialMessages] = useState<Message[]>([]);\n  const [ready, setReady] = useState<boolean>(false);\n  const [urlId, setUrlId] = useState<string | undefined>();\n\n  useEffect(() => {\n    if (!db) {\n      setReady(true);\n\n      if (persistenceEnabled) {\n        toast.error(`Chat persistence is unavailable`);\n      }\n\n      return;\n    }\n\n    if (mixedId) {\n      getMessages(db, mixedId)\n        .then((storedMessages) => {\n          if (storedMessages && storedMessages.messages.length > 0) {\n            setInitialMessages(storedMessages.messages);\n            setUrlId(storedMessages.urlId);\n            description.set(storedMessages.description);\n            chatId.set(storedMessages.id);\n          } else {\n            navigate(`/`, { replace: true });\n          }\n\n          setReady(true);\n        })\n        .catch((error) => {\n          toast.error(error.message);\n        });\n    }\n  }, []);\n\n  return {\n    ready: !mixedId || ready,\n    initialMessages,\n    storeMessageHistory: async (messages: Message[]) => {\n      if (!db || messages.length === 0) {\n        return;\n      }\n\n      const { firstArtifact } = workbenchStore;\n\n      if (!urlId && firstArtifact?.id) {\n        const urlId = await getUrlId(db, firstArtifact.id);\n\n        navigateChat(urlId);\n        setUrlId(urlId);\n      }\n\n      if (!description.get() && firstArtifact?.title) {\n        description.set(firstArtifact?.title);\n      }\n\n      if (initialMessages.length === 0 && !chatId.get()) {\n        const nextId = await getNextId(db);\n\n        chatId.set(nextId);\n\n        if (!urlId) {\n          navigateChat(nextId);\n        }\n      }\n\n      await setMessages(db, chatId.get() as string, messages, urlId, description.get());\n    },\n  };\n}\n\nfunction navigateChat(nextId: string) {\n  /**\n   * FIXME: Using the intended navigate function causes a rerender for <Chat /> that breaks the app.\n   *\n   * `navigate(`/chat/${nextId}`, { replace: true });`\n   */\n  const url = new URL(window.location.href);\n  url.pathname = `/chat/${nextId}`;\n\n  window.history.replaceState({}, '', url);\n}\n"
  },
  {
    "path": "app/lib/runtime/__snapshots__/message-parser.spec.ts.snap",
    "content": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`StreamingMessageParser > valid artifacts with actions > should correctly parse chunks and strip out bolt artifacts (0) > onActionClose 1`] = `\n{\n  \"action\": {\n    \"content\": \"npm install\",\n    \"type\": \"shell\",\n  },\n  \"actionId\": \"0\",\n  \"artifactId\": \"artifact_1\",\n  \"messageId\": \"message_1\",\n}\n`;\n\nexports[`StreamingMessageParser > valid artifacts with actions > should correctly parse chunks and strip out bolt artifacts (0) > onActionOpen 1`] = `\n{\n  \"action\": {\n    \"content\": \"\",\n    \"type\": \"shell\",\n  },\n  \"actionId\": \"0\",\n  \"artifactId\": \"artifact_1\",\n  \"messageId\": \"message_1\",\n}\n`;\n\nexports[`StreamingMessageParser > valid artifacts with actions > should correctly parse chunks and strip out bolt artifacts (0) > onArtifactClose 1`] = `\n{\n  \"id\": \"artifact_1\",\n  \"messageId\": \"message_1\",\n  \"title\": \"Some title\",\n}\n`;\n\nexports[`StreamingMessageParser > valid artifacts with actions > should correctly parse chunks and strip out bolt artifacts (0) > onArtifactOpen 1`] = `\n{\n  \"id\": \"artifact_1\",\n  \"messageId\": \"message_1\",\n  \"title\": \"Some title\",\n}\n`;\n\nexports[`StreamingMessageParser > valid artifacts with actions > should correctly parse chunks and strip out bolt artifacts (1) > onActionClose 1`] = `\n{\n  \"action\": {\n    \"content\": \"npm install\",\n    \"type\": \"shell\",\n  },\n  \"actionId\": \"0\",\n  \"artifactId\": \"artifact_1\",\n  \"messageId\": \"message_1\",\n}\n`;\n\nexports[`StreamingMessageParser > valid artifacts with actions > should correctly parse chunks and strip out bolt artifacts (1) > onActionClose 2`] = `\n{\n  \"action\": {\n    \"content\": \"some content\n\",\n    \"filePath\": \"index.js\",\n    \"type\": \"file\",\n  },\n  \"actionId\": \"1\",\n  \"artifactId\": \"artifact_1\",\n  \"messageId\": \"message_1\",\n}\n`;\n\nexports[`StreamingMessageParser > valid artifacts with actions > should correctly parse chunks and strip out bolt artifacts (1) > onActionOpen 1`] = `\n{\n  \"action\": {\n    \"content\": \"\",\n    \"type\": \"shell\",\n  },\n  \"actionId\": \"0\",\n  \"artifactId\": \"artifact_1\",\n  \"messageId\": \"message_1\",\n}\n`;\n\nexports[`StreamingMessageParser > valid artifacts with actions > should correctly parse chunks and strip out bolt artifacts (1) > onActionOpen 2`] = `\n{\n  \"action\": {\n    \"content\": \"\",\n    \"filePath\": \"index.js\",\n    \"type\": \"file\",\n  },\n  \"actionId\": \"1\",\n  \"artifactId\": \"artifact_1\",\n  \"messageId\": \"message_1\",\n}\n`;\n\nexports[`StreamingMessageParser > valid artifacts with actions > should correctly parse chunks and strip out bolt artifacts (1) > onArtifactClose 1`] = `\n{\n  \"id\": \"artifact_1\",\n  \"messageId\": \"message_1\",\n  \"title\": \"Some title\",\n}\n`;\n\nexports[`StreamingMessageParser > valid artifacts with actions > should correctly parse chunks and strip out bolt artifacts (1) > onArtifactOpen 1`] = `\n{\n  \"id\": \"artifact_1\",\n  \"messageId\": \"message_1\",\n  \"title\": \"Some title\",\n}\n`;\n\nexports[`StreamingMessageParser > valid artifacts without actions > should correctly parse chunks and strip out bolt artifacts (0) > onArtifactClose 1`] = `\n{\n  \"id\": \"artifact_1\",\n  \"messageId\": \"message_1\",\n  \"title\": \"Some title\",\n}\n`;\n\nexports[`StreamingMessageParser > valid artifacts without actions > should correctly parse chunks and strip out bolt artifacts (0) > onArtifactOpen 1`] = `\n{\n  \"id\": \"artifact_1\",\n  \"messageId\": \"message_1\",\n  \"title\": \"Some title\",\n}\n`;\n\nexports[`StreamingMessageParser > valid artifacts without actions > should correctly parse chunks and strip out bolt artifacts (1) > onArtifactClose 1`] = `\n{\n  \"id\": \"artifact_1\",\n  \"messageId\": \"message_1\",\n  \"title\": \"Some title\",\n}\n`;\n\nexports[`StreamingMessageParser > valid artifacts without actions > should correctly parse chunks and strip out bolt artifacts (1) > onArtifactOpen 1`] = `\n{\n  \"id\": \"artifact_1\",\n  \"messageId\": \"message_1\",\n  \"title\": \"Some title\",\n}\n`;\n\nexports[`StreamingMessageParser > valid artifacts without actions > should correctly parse chunks and strip out bolt artifacts (2) > onArtifactClose 1`] = `\n{\n  \"id\": \"artifact_1\",\n  \"messageId\": \"message_1\",\n  \"title\": \"Some title\",\n}\n`;\n\nexports[`StreamingMessageParser > valid artifacts without actions > should correctly parse chunks and strip out bolt artifacts (2) > onArtifactOpen 1`] = `\n{\n  \"id\": \"artifact_1\",\n  \"messageId\": \"message_1\",\n  \"title\": \"Some title\",\n}\n`;\n\nexports[`StreamingMessageParser > valid artifacts without actions > should correctly parse chunks and strip out bolt artifacts (3) > onArtifactClose 1`] = `\n{\n  \"id\": \"artifact_1\",\n  \"messageId\": \"message_1\",\n  \"title\": \"Some title\",\n}\n`;\n\nexports[`StreamingMessageParser > valid artifacts without actions > should correctly parse chunks and strip out bolt artifacts (3) > onArtifactOpen 1`] = `\n{\n  \"id\": \"artifact_1\",\n  \"messageId\": \"message_1\",\n  \"title\": \"Some title\",\n}\n`;\n\nexports[`StreamingMessageParser > valid artifacts without actions > should correctly parse chunks and strip out bolt artifacts (4) > onArtifactClose 1`] = `\n{\n  \"id\": \"artifact_1\",\n  \"messageId\": \"message_1\",\n  \"title\": \"Some title\",\n}\n`;\n\nexports[`StreamingMessageParser > valid artifacts without actions > should correctly parse chunks and strip out bolt artifacts (4) > onArtifactOpen 1`] = `\n{\n  \"id\": \"artifact_1\",\n  \"messageId\": \"message_1\",\n  \"title\": \"Some title\",\n}\n`;\n\nexports[`StreamingMessageParser > valid artifacts without actions > should correctly parse chunks and strip out bolt artifacts (5) > onArtifactClose 1`] = `\n{\n  \"id\": \"artifact_1\",\n  \"messageId\": \"message_1\",\n  \"title\": \"Some title\",\n}\n`;\n\nexports[`StreamingMessageParser > valid artifacts without actions > should correctly parse chunks and strip out bolt artifacts (5) > onArtifactOpen 1`] = `\n{\n  \"id\": \"artifact_1\",\n  \"messageId\": \"message_1\",\n  \"title\": \"Some title\",\n}\n`;\n\nexports[`StreamingMessageParser > valid artifacts without actions > should correctly parse chunks and strip out bolt artifacts (6) > onArtifactClose 1`] = `\n{\n  \"id\": \"artifact_1\",\n  \"messageId\": \"message_1\",\n  \"title\": \"Some title\",\n}\n`;\n\nexports[`StreamingMessageParser > valid artifacts without actions > should correctly parse chunks and strip out bolt artifacts (6) > onArtifactOpen 1`] = `\n{\n  \"id\": \"artifact_1\",\n  \"messageId\": \"message_1\",\n  \"title\": \"Some title\",\n}\n`;\n"
  },
  {
    "path": "app/lib/runtime/action-runner.ts",
    "content": "import { WebContainer } from '@webcontainer/api';\nimport { map, type MapStore } from 'nanostores';\nimport * as nodePath from 'node:path';\nimport type { BoltAction } from '~/types/actions';\nimport { createScopedLogger } from '~/utils/logger';\nimport { unreachable } from '~/utils/unreachable';\nimport type { ActionCallbackData } from './message-parser';\n\nconst logger = createScopedLogger('ActionRunner');\n\nexport type ActionStatus = 'pending' | 'running' | 'complete' | 'aborted' | 'failed';\n\nexport type BaseActionState = BoltAction & {\n  status: Exclude<ActionStatus, 'failed'>;\n  abort: () => void;\n  executed: boolean;\n  abortSignal: AbortSignal;\n};\n\nexport type FailedActionState = BoltAction &\n  Omit<BaseActionState, 'status'> & {\n    status: Extract<ActionStatus, 'failed'>;\n    error: string;\n  };\n\nexport type ActionState = BaseActionState | FailedActionState;\n\ntype BaseActionUpdate = Partial<Pick<BaseActionState, 'status' | 'abort' | 'executed'>>;\n\nexport type ActionStateUpdate =\n  | BaseActionUpdate\n  | (Omit<BaseActionUpdate, 'status'> & { status: 'failed'; error: string });\n\ntype ActionsMap = MapStore<Record<string, ActionState>>;\n\nexport class ActionRunner {\n  #webcontainer: Promise<WebContainer>;\n  #currentExecutionPromise: Promise<void> = Promise.resolve();\n\n  actions: ActionsMap = map({});\n\n  constructor(webcontainerPromise: Promise<WebContainer>) {\n    this.#webcontainer = webcontainerPromise;\n  }\n\n  addAction(data: ActionCallbackData) {\n    const { actionId } = data;\n\n    const actions = this.actions.get();\n    const action = actions[actionId];\n\n    if (action) {\n      // action already added\n      return;\n    }\n\n    const abortController = new AbortController();\n\n    this.actions.setKey(actionId, {\n      ...data.action,\n      status: 'pending',\n      executed: false,\n      abort: () => {\n        abortController.abort();\n        this.#updateAction(actionId, { status: 'aborted' });\n      },\n      abortSignal: abortController.signal,\n    });\n\n    this.#currentExecutionPromise.then(() => {\n      this.#updateAction(actionId, { status: 'running' });\n    });\n  }\n\n  async runAction(data: ActionCallbackData) {\n    const { actionId } = data;\n    const action = this.actions.get()[actionId];\n\n    if (!action) {\n      unreachable(`Action ${actionId} not found`);\n    }\n\n    if (action.executed) {\n      return;\n    }\n\n    this.#updateAction(actionId, { ...action, ...data.action, executed: true });\n\n    this.#currentExecutionPromise = this.#currentExecutionPromise\n      .then(() => {\n        return this.#executeAction(actionId);\n      })\n      .catch((error) => {\n        console.error('Action failed:', error);\n      });\n  }\n\n  async #executeAction(actionId: string) {\n    const action = this.actions.get()[actionId];\n\n    this.#updateAction(actionId, { status: 'running' });\n\n    try {\n      switch (action.type) {\n        case 'shell': {\n          await this.#runShellAction(action);\n          break;\n        }\n        case 'file': {\n          await this.#runFileAction(action);\n          break;\n        }\n      }\n\n      this.#updateAction(actionId, { status: action.abortSignal.aborted ? 'aborted' : 'complete' });\n    } catch (error) {\n      this.#updateAction(actionId, { status: 'failed', error: 'Action failed' });\n\n      // re-throw the error to be caught in the promise chain\n      throw error;\n    }\n  }\n\n  async #runShellAction(action: ActionState) {\n    if (action.type !== 'shell') {\n      unreachable('Expected shell action');\n    }\n\n    const webcontainer = await this.#webcontainer;\n\n    const process = await webcontainer.spawn('jsh', ['-c', action.content], {\n      env: { npm_config_yes: true },\n    });\n\n    action.abortSignal.addEventListener('abort', () => {\n      process.kill();\n    });\n\n    process.output.pipeTo(\n      new WritableStream({\n        write(data) {\n          console.log(data);\n        },\n      }),\n    );\n\n    const exitCode = await process.exit;\n\n    logger.debug(`Process terminated with code ${exitCode}`);\n  }\n\n  async #runFileAction(action: ActionState) {\n    if (action.type !== 'file') {\n      unreachable('Expected file action');\n    }\n\n    const webcontainer = await this.#webcontainer;\n\n    let folder = nodePath.dirname(action.filePath);\n\n    // remove trailing slashes\n    folder = folder.replace(/\\/+$/g, '');\n\n    if (folder !== '.') {\n      try {\n        await webcontainer.fs.mkdir(folder, { recursive: true });\n        logger.debug('Created folder', folder);\n      } catch (error) {\n        logger.error('Failed to create folder\\n\\n', error);\n      }\n    }\n\n    try {\n      await webcontainer.fs.writeFile(action.filePath, action.content);\n      logger.debug(`File written ${action.filePath}`);\n    } catch (error) {\n      logger.error('Failed to write file\\n\\n', error);\n    }\n  }\n\n  #updateAction(id: string, newState: ActionStateUpdate) {\n    const actions = this.actions.get();\n\n    this.actions.setKey(id, { ...actions[id], ...newState });\n  }\n}\n"
  },
  {
    "path": "app/lib/runtime/message-parser.spec.ts",
    "content": "import { describe, expect, it, vi } from 'vitest';\nimport { StreamingMessageParser, type ActionCallback, type ArtifactCallback } from './message-parser';\n\ninterface ExpectedResult {\n  output: string;\n  callbacks?: {\n    onArtifactOpen?: number;\n    onArtifactClose?: number;\n    onActionOpen?: number;\n    onActionClose?: number;\n  };\n}\n\ndescribe('StreamingMessageParser', () => {\n  it('should pass through normal text', () => {\n    const parser = new StreamingMessageParser();\n    expect(parser.parse('test_id', 'Hello, world!')).toBe('Hello, world!');\n  });\n\n  it('should allow normal HTML tags', () => {\n    const parser = new StreamingMessageParser();\n    expect(parser.parse('test_id', 'Hello <strong>world</strong>!')).toBe('Hello <strong>world</strong>!');\n  });\n\n  describe('no artifacts', () => {\n    it.each<[string | string[], ExpectedResult | string]>([\n      ['Foo bar', 'Foo bar'],\n      ['Foo bar <', 'Foo bar '],\n      ['Foo bar <p', 'Foo bar <p'],\n      [['Foo bar <', 's', 'p', 'an>some text</span>'], 'Foo bar <span>some text</span>'],\n    ])('should correctly parse chunks and strip out bolt artifacts (%#)', (input, expected) => {\n      runTest(input, expected);\n    });\n  });\n\n  describe('invalid or incomplete artifacts', () => {\n    it.each<[string | string[], ExpectedResult | string]>([\n      ['Foo bar <b', 'Foo bar '],\n      ['Foo bar <ba', 'Foo bar <ba'],\n      ['Foo bar <bol', 'Foo bar '],\n      ['Foo bar <bolt', 'Foo bar '],\n      ['Foo bar <bolta', 'Foo bar <bolta'],\n      ['Foo bar <boltA', 'Foo bar '],\n      ['Foo bar <boltArtifacs></boltArtifact>', 'Foo bar <boltArtifacs></boltArtifact>'],\n      ['Before <oltArtfiact>foo</boltArtifact> After', 'Before <oltArtfiact>foo</boltArtifact> After'],\n      ['Before <boltArtifactt>foo</boltArtifact> After', 'Before <boltArtifactt>foo</boltArtifact> After'],\n    ])('should correctly parse chunks and strip out bolt artifacts (%#)', (input, expected) => {\n      runTest(input, expected);\n    });\n  });\n\n  describe('valid artifacts without actions', () => {\n    it.each<[string | string[], ExpectedResult | string]>([\n      [\n        'Some text before <boltArtifact title=\"Some title\" id=\"artifact_1\">foo bar</boltArtifact> Some more text',\n        {\n          output: 'Some text before  Some more text',\n          callbacks: { onArtifactOpen: 1, onArtifactClose: 1, onActionOpen: 0, onActionClose: 0 },\n        },\n      ],\n      [\n        ['Some text before <boltArti', 'fact', ' title=\"Some title\" id=\"artifact_1\">foo</boltArtifact> Some more text'],\n        {\n          output: 'Some text before  Some more text',\n          callbacks: { onArtifactOpen: 1, onArtifactClose: 1, onActionOpen: 0, onActionClose: 0 },\n        },\n      ],\n      [\n        [\n          'Some text before <boltArti',\n          'fac',\n          't title=\"Some title\" id=\"artifact_1\"',\n          ' ',\n          '>',\n          'foo</boltArtifact> Some more text',\n        ],\n        {\n          output: 'Some text before  Some more text',\n          callbacks: { onArtifactOpen: 1, onArtifactClose: 1, onActionOpen: 0, onActionClose: 0 },\n        },\n      ],\n      [\n        [\n          'Some text before <boltArti',\n          'fact',\n          ' title=\"Some title\" id=\"artifact_1\"',\n          ' >fo',\n          'o</boltArtifact> Some more text',\n        ],\n        {\n          output: 'Some text before  Some more text',\n          callbacks: { onArtifactOpen: 1, onArtifactClose: 1, onActionOpen: 0, onActionClose: 0 },\n        },\n      ],\n      [\n        [\n          'Some text before <boltArti',\n          'fact tit',\n          'le=\"Some ',\n          'title\" id=\"artifact_1\">fo',\n          'o',\n          '<',\n          '/boltArtifact> Some more text',\n        ],\n        {\n          output: 'Some text before  Some more text',\n          callbacks: { onArtifactOpen: 1, onArtifactClose: 1, onActionOpen: 0, onActionClose: 0 },\n        },\n      ],\n      [\n        [\n          'Some text before <boltArti',\n          'fact title=\"Some title\" id=\"artif',\n          'act_1\">fo',\n          'o<',\n          '/boltArtifact> Some more text',\n        ],\n        {\n          output: 'Some text before  Some more text',\n          callbacks: { onArtifactOpen: 1, onArtifactClose: 1, onActionOpen: 0, onActionClose: 0 },\n        },\n      ],\n      [\n        'Before <boltArtifact title=\"Some title\" id=\"artifact_1\">foo</boltArtifact> After',\n        {\n          output: 'Before  After',\n          callbacks: { onArtifactOpen: 1, onArtifactClose: 1, onActionOpen: 0, onActionClose: 0 },\n        },\n      ],\n    ])('should correctly parse chunks and strip out bolt artifacts (%#)', (input, expected) => {\n      runTest(input, expected);\n    });\n  });\n\n  describe('valid artifacts with actions', () => {\n    it.each<[string | string[], ExpectedResult | string]>([\n      [\n        'Before <boltArtifact title=\"Some title\" id=\"artifact_1\"><boltAction type=\"shell\">npm install</boltAction></boltArtifact> After',\n        {\n          output: 'Before  After',\n          callbacks: { onArtifactOpen: 1, onArtifactClose: 1, onActionOpen: 1, onActionClose: 1 },\n        },\n      ],\n      [\n        'Before <boltArtifact title=\"Some title\" id=\"artifact_1\"><boltAction type=\"shell\">npm install</boltAction><boltAction type=\"file\" filePath=\"index.js\">some content</boltAction></boltArtifact> After',\n        {\n          output: 'Before  After',\n          callbacks: { onArtifactOpen: 1, onArtifactClose: 1, onActionOpen: 2, onActionClose: 2 },\n        },\n      ],\n    ])('should correctly parse chunks and strip out bolt artifacts (%#)', (input, expected) => {\n      runTest(input, expected);\n    });\n  });\n});\n\nfunction runTest(input: string | string[], outputOrExpectedResult: string | ExpectedResult) {\n  let expected: ExpectedResult;\n\n  if (typeof outputOrExpectedResult === 'string') {\n    expected = { output: outputOrExpectedResult };\n  } else {\n    expected = outputOrExpectedResult;\n  }\n\n  const callbacks = {\n    onArtifactOpen: vi.fn<ArtifactCallback>((data) => {\n      expect(data).toMatchSnapshot('onArtifactOpen');\n    }),\n    onArtifactClose: vi.fn<ArtifactCallback>((data) => {\n      expect(data).toMatchSnapshot('onArtifactClose');\n    }),\n    onActionOpen: vi.fn<ActionCallback>((data) => {\n      expect(data).toMatchSnapshot('onActionOpen');\n    }),\n    onActionClose: vi.fn<ActionCallback>((data) => {\n      expect(data).toMatchSnapshot('onActionClose');\n    }),\n  };\n\n  const parser = new StreamingMessageParser({\n    artifactElement: () => '',\n    callbacks,\n  });\n\n  let message = '';\n\n  let result = '';\n\n  const chunks = Array.isArray(input) ? input : input.split('');\n\n  for (const chunk of chunks) {\n    message += chunk;\n\n    result += parser.parse('message_1', message);\n  }\n\n  for (const name in expected.callbacks) {\n    const callbackName = name;\n\n    expect(callbacks[callbackName as keyof typeof callbacks]).toHaveBeenCalledTimes(\n      expected.callbacks[callbackName as keyof typeof expected.callbacks] ?? 0,\n    );\n  }\n\n  expect(result).toEqual(expected.output);\n}\n"
  },
  {
    "path": "app/lib/runtime/message-parser.ts",
    "content": "import type { ActionType, BoltAction, BoltActionData, FileAction, ShellAction } from '~/types/actions';\nimport type { BoltArtifactData } from '~/types/artifact';\nimport { createScopedLogger } from '~/utils/logger';\nimport { unreachable } from '~/utils/unreachable';\n\nconst ARTIFACT_TAG_OPEN = '<boltArtifact';\nconst ARTIFACT_TAG_CLOSE = '</boltArtifact>';\nconst ARTIFACT_ACTION_TAG_OPEN = '<boltAction';\nconst ARTIFACT_ACTION_TAG_CLOSE = '</boltAction>';\n\nconst logger = createScopedLogger('MessageParser');\n\nexport interface ArtifactCallbackData extends BoltArtifactData {\n  messageId: string;\n}\n\nexport interface ActionCallbackData {\n  artifactId: string;\n  messageId: string;\n  actionId: string;\n  action: BoltAction;\n}\n\nexport type ArtifactCallback = (data: ArtifactCallbackData) => void;\nexport type ActionCallback = (data: ActionCallbackData) => void;\n\nexport interface ParserCallbacks {\n  onArtifactOpen?: ArtifactCallback;\n  onArtifactClose?: ArtifactCallback;\n  onActionOpen?: ActionCallback;\n  onActionClose?: ActionCallback;\n}\n\ninterface ElementFactoryProps {\n  messageId: string;\n}\n\ntype ElementFactory = (props: ElementFactoryProps) => string;\n\nexport interface StreamingMessageParserOptions {\n  callbacks?: ParserCallbacks;\n  artifactElement?: ElementFactory;\n}\n\ninterface MessageState {\n  position: number;\n  insideArtifact: boolean;\n  insideAction: boolean;\n  currentArtifact?: BoltArtifactData;\n  currentAction: BoltActionData;\n  actionId: number;\n}\n\nexport class StreamingMessageParser {\n  #messages = new Map<string, MessageState>();\n\n  constructor(private _options: StreamingMessageParserOptions = {}) {}\n\n  parse(messageId: string, input: string) {\n    let state = this.#messages.get(messageId);\n\n    if (!state) {\n      state = {\n        position: 0,\n        insideAction: false,\n        insideArtifact: false,\n        currentAction: { content: '' },\n        actionId: 0,\n      };\n\n      this.#messages.set(messageId, state);\n    }\n\n    let output = '';\n    let i = state.position;\n    let earlyBreak = false;\n\n    while (i < input.length) {\n      if (state.insideArtifact) {\n        const currentArtifact = state.currentArtifact;\n\n        if (currentArtifact === undefined) {\n          unreachable('Artifact not initialized');\n        }\n\n        if (state.insideAction) {\n          const closeIndex = input.indexOf(ARTIFACT_ACTION_TAG_CLOSE, i);\n\n          const currentAction = state.currentAction;\n\n          if (closeIndex !== -1) {\n            currentAction.content += input.slice(i, closeIndex);\n\n            let content = currentAction.content.trim();\n\n            if ('type' in currentAction && currentAction.type === 'file') {\n              content += '\\n';\n            }\n\n            currentAction.content = content;\n\n            this._options.callbacks?.onActionClose?.({\n              artifactId: currentArtifact.id,\n              messageId,\n\n              /**\n               * We decrement the id because it's been incremented already\n               * when `onActionOpen` was emitted to make sure the ids are\n               * the same.\n               */\n              actionId: String(state.actionId - 1),\n\n              action: currentAction as BoltAction,\n            });\n\n            state.insideAction = false;\n            state.currentAction = { content: '' };\n\n            i = closeIndex + ARTIFACT_ACTION_TAG_CLOSE.length;\n          } else {\n            break;\n          }\n        } else {\n          const actionOpenIndex = input.indexOf(ARTIFACT_ACTION_TAG_OPEN, i);\n          const artifactCloseIndex = input.indexOf(ARTIFACT_TAG_CLOSE, i);\n\n          if (actionOpenIndex !== -1 && (artifactCloseIndex === -1 || actionOpenIndex < artifactCloseIndex)) {\n            const actionEndIndex = input.indexOf('>', actionOpenIndex);\n\n            if (actionEndIndex !== -1) {\n              state.insideAction = true;\n\n              state.currentAction = this.#parseActionTag(input, actionOpenIndex, actionEndIndex);\n\n              this._options.callbacks?.onActionOpen?.({\n                artifactId: currentArtifact.id,\n                messageId,\n                actionId: String(state.actionId++),\n                action: state.currentAction as BoltAction,\n              });\n\n              i = actionEndIndex + 1;\n            } else {\n              break;\n            }\n          } else if (artifactCloseIndex !== -1) {\n            this._options.callbacks?.onArtifactClose?.({ messageId, ...currentArtifact });\n\n            state.insideArtifact = false;\n            state.currentArtifact = undefined;\n\n            i = artifactCloseIndex + ARTIFACT_TAG_CLOSE.length;\n          } else {\n            break;\n          }\n        }\n      } else if (input[i] === '<' && input[i + 1] !== '/') {\n        let j = i;\n        let potentialTag = '';\n\n        while (j < input.length && potentialTag.length < ARTIFACT_TAG_OPEN.length) {\n          potentialTag += input[j];\n\n          if (potentialTag === ARTIFACT_TAG_OPEN) {\n            const nextChar = input[j + 1];\n\n            if (nextChar && nextChar !== '>' && nextChar !== ' ') {\n              output += input.slice(i, j + 1);\n              i = j + 1;\n              break;\n            }\n\n            const openTagEnd = input.indexOf('>', j);\n\n            if (openTagEnd !== -1) {\n              const artifactTag = input.slice(i, openTagEnd + 1);\n\n              const artifactTitle = this.#extractAttribute(artifactTag, 'title') as string;\n              const artifactId = this.#extractAttribute(artifactTag, 'id') as string;\n\n              if (!artifactTitle) {\n                logger.warn('Artifact title missing');\n              }\n\n              if (!artifactId) {\n                logger.warn('Artifact id missing');\n              }\n\n              state.insideArtifact = true;\n\n              const currentArtifact = {\n                id: artifactId,\n                title: artifactTitle,\n              } satisfies BoltArtifactData;\n\n              state.currentArtifact = currentArtifact;\n\n              this._options.callbacks?.onArtifactOpen?.({ messageId, ...currentArtifact });\n\n              const artifactFactory = this._options.artifactElement ?? createArtifactElement;\n\n              output += artifactFactory({ messageId });\n\n              i = openTagEnd + 1;\n            } else {\n              earlyBreak = true;\n            }\n\n            break;\n          } else if (!ARTIFACT_TAG_OPEN.startsWith(potentialTag)) {\n            output += input.slice(i, j + 1);\n            i = j + 1;\n            break;\n          }\n\n          j++;\n        }\n\n        if (j === input.length && ARTIFACT_TAG_OPEN.startsWith(potentialTag)) {\n          break;\n        }\n      } else {\n        output += input[i];\n        i++;\n      }\n\n      if (earlyBreak) {\n        break;\n      }\n    }\n\n    state.position = i;\n\n    return output;\n  }\n\n  reset() {\n    this.#messages.clear();\n  }\n\n  #parseActionTag(input: string, actionOpenIndex: number, actionEndIndex: number) {\n    const actionTag = input.slice(actionOpenIndex, actionEndIndex + 1);\n\n    const actionType = this.#extractAttribute(actionTag, 'type') as ActionType;\n\n    const actionAttributes = {\n      type: actionType,\n      content: '',\n    };\n\n    if (actionType === 'file') {\n      const filePath = this.#extractAttribute(actionTag, 'filePath') as string;\n\n      if (!filePath) {\n        logger.debug('File path not specified');\n      }\n\n      (actionAttributes as FileAction).filePath = filePath;\n    } else if (actionType !== 'shell') {\n      logger.warn(`Unknown action type '${actionType}'`);\n    }\n\n    return actionAttributes as FileAction | ShellAction;\n  }\n\n  #extractAttribute(tag: string, attributeName: string): string | undefined {\n    const match = tag.match(new RegExp(`${attributeName}=\"([^\"]*)\"`, 'i'));\n    return match ? match[1] : undefined;\n  }\n}\n\nconst createArtifactElement: ElementFactory = (props) => {\n  const elementProps = [\n    'class=\"__boltArtifact__\"',\n    ...Object.entries(props).map(([key, value]) => {\n      return `data-${camelToDashCase(key)}=${JSON.stringify(value)}`;\n    }),\n  ];\n\n  return `<div ${elementProps.join(' ')}></div>`;\n};\n\nfunction camelToDashCase(input: string) {\n  return input.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();\n}\n"
  },
  {
    "path": "app/lib/stores/chat.ts",
    "content": "import { map } from 'nanostores';\n\nexport const chatStore = map({\n  started: false,\n  aborted: false,\n  showChat: true,\n});\n"
  },
  {
    "path": "app/lib/stores/editor.ts",
    "content": "import { atom, computed, map, type MapStore, type WritableAtom } from 'nanostores';\nimport type { EditorDocument, ScrollPosition } from '~/components/editor/codemirror/CodeMirrorEditor';\nimport type { FileMap, FilesStore } from './files';\n\nexport type EditorDocuments = Record<string, EditorDocument>;\n\ntype SelectedFile = WritableAtom<string | undefined>;\n\nexport class EditorStore {\n  #filesStore: FilesStore;\n\n  selectedFile: SelectedFile = import.meta.hot?.data.selectedFile ?? atom<string | undefined>();\n  documents: MapStore<EditorDocuments> = import.meta.hot?.data.documents ?? map({});\n\n  currentDocument = computed([this.documents, this.selectedFile], (documents, selectedFile) => {\n    if (!selectedFile) {\n      return undefined;\n    }\n\n    return documents[selectedFile];\n  });\n\n  constructor(filesStore: FilesStore) {\n    this.#filesStore = filesStore;\n\n    if (import.meta.hot) {\n      import.meta.hot.data.documents = this.documents;\n      import.meta.hot.data.selectedFile = this.selectedFile;\n    }\n  }\n\n  setDocuments(files: FileMap) {\n    const previousDocuments = this.documents.value;\n\n    this.documents.set(\n      Object.fromEntries<EditorDocument>(\n        Object.entries(files)\n          .map(([filePath, dirent]) => {\n            if (dirent === undefined || dirent.type === 'folder') {\n              return undefined;\n            }\n\n            const previousDocument = previousDocuments?.[filePath];\n\n            return [\n              filePath,\n              {\n                value: dirent.content,\n                filePath,\n                scroll: previousDocument?.scroll,\n              },\n            ] as [string, EditorDocument];\n          })\n          .filter(Boolean) as Array<[string, EditorDocument]>,\n      ),\n    );\n  }\n\n  setSelectedFile(filePath: string | undefined) {\n    this.selectedFile.set(filePath);\n  }\n\n  updateScrollPosition(filePath: string, position: ScrollPosition) {\n    const documents = this.documents.get();\n    const documentState = documents[filePath];\n\n    if (!documentState) {\n      return;\n    }\n\n    this.documents.setKey(filePath, {\n      ...documentState,\n      scroll: position,\n    });\n  }\n\n  updateFile(filePath: string, newContent: string) {\n    const documents = this.documents.get();\n    const documentState = documents[filePath];\n\n    if (!documentState) {\n      return;\n    }\n\n    const currentContent = documentState.value;\n    const contentChanged = currentContent !== newContent;\n\n    if (contentChanged) {\n      this.documents.setKey(filePath, {\n        ...documentState,\n        value: newContent,\n      });\n    }\n  }\n}\n"
  },
  {
    "path": "app/lib/stores/files.ts",
    "content": "import type { PathWatcherEvent, WebContainer } from '@webcontainer/api';\nimport { getEncoding } from 'istextorbinary';\nimport { map, type MapStore } from 'nanostores';\nimport { Buffer } from 'node:buffer';\nimport * as nodePath from 'node:path';\nimport { bufferWatchEvents } from '~/utils/buffer';\nimport { WORK_DIR } from '~/utils/constants';\nimport { computeFileModifications } from '~/utils/diff';\nimport { createScopedLogger } from '~/utils/logger';\nimport { unreachable } from '~/utils/unreachable';\n\nconst logger = createScopedLogger('FilesStore');\n\nconst utf8TextDecoder = new TextDecoder('utf8', { fatal: true });\n\nexport interface File {\n  type: 'file';\n  content: string;\n  isBinary: boolean;\n}\n\nexport interface Folder {\n  type: 'folder';\n}\n\ntype Dirent = File | Folder;\n\nexport type FileMap = Record<string, Dirent | undefined>;\n\nexport class FilesStore {\n  #webcontainer: Promise<WebContainer>;\n\n  /**\n   * Tracks the number of files without folders.\n   */\n  #size = 0;\n\n  /**\n   * @note Keeps track all modified files with their original content since the last user message.\n   * Needs to be reset when the user sends another message and all changes have to be submitted\n   * for the model to be aware of the changes.\n   */\n  #modifiedFiles: Map<string, string> = import.meta.hot?.data.modifiedFiles ?? new Map();\n\n  /**\n   * Map of files that matches the state of WebContainer.\n   */\n  files: MapStore<FileMap> = import.meta.hot?.data.files ?? map({});\n\n  get filesCount() {\n    return this.#size;\n  }\n\n  constructor(webcontainerPromise: Promise<WebContainer>) {\n    this.#webcontainer = webcontainerPromise;\n\n    if (import.meta.hot) {\n      import.meta.hot.data.files = this.files;\n      import.meta.hot.data.modifiedFiles = this.#modifiedFiles;\n    }\n\n    this.#init();\n  }\n\n  getFile(filePath: string) {\n    const dirent = this.files.get()[filePath];\n\n    if (dirent?.type !== 'file') {\n      return undefined;\n    }\n\n    return dirent;\n  }\n\n  getFileModifications() {\n    return computeFileModifications(this.files.get(), this.#modifiedFiles);\n  }\n\n  resetFileModifications() {\n    this.#modifiedFiles.clear();\n  }\n\n  async saveFile(filePath: string, content: string) {\n    const webcontainer = await this.#webcontainer;\n\n    try {\n      const relativePath = nodePath.relative(webcontainer.workdir, filePath);\n\n      if (!relativePath) {\n        throw new Error(`EINVAL: invalid file path, write '${relativePath}'`);\n      }\n\n      const oldContent = this.getFile(filePath)?.content;\n\n      if (!oldContent) {\n        unreachable('Expected content to be defined');\n      }\n\n      await webcontainer.fs.writeFile(relativePath, content);\n\n      if (!this.#modifiedFiles.has(filePath)) {\n        this.#modifiedFiles.set(filePath, oldContent);\n      }\n\n      // we immediately update the file and don't rely on the `change` event coming from the watcher\n      this.files.setKey(filePath, { type: 'file', content, isBinary: false });\n\n      logger.info('File updated');\n    } catch (error) {\n      logger.error('Failed to update file content\\n\\n', error);\n\n      throw error;\n    }\n  }\n\n  async #init() {\n    const webcontainer = await this.#webcontainer;\n\n    webcontainer.internal.watchPaths(\n      { include: [`${WORK_DIR}/**`], exclude: ['**/node_modules', '.git'], includeContent: true },\n      bufferWatchEvents(100, this.#processEventBuffer.bind(this)),\n    );\n  }\n\n  #processEventBuffer(events: Array<[events: PathWatcherEvent[]]>) {\n    const watchEvents = events.flat(2);\n\n    for (const { type, path, buffer } of watchEvents) {\n      // remove any trailing slashes\n      const sanitizedPath = path.replace(/\\/+$/g, '');\n\n      switch (type) {\n        case 'add_dir': {\n          // we intentionally add a trailing slash so we can distinguish files from folders in the file tree\n          this.files.setKey(sanitizedPath, { type: 'folder' });\n          break;\n        }\n        case 'remove_dir': {\n          this.files.setKey(sanitizedPath, undefined);\n\n          for (const [direntPath] of Object.entries(this.files)) {\n            if (direntPath.startsWith(sanitizedPath)) {\n              this.files.setKey(direntPath, undefined);\n            }\n          }\n\n          break;\n        }\n        case 'add_file':\n        case 'change': {\n          if (type === 'add_file') {\n            this.#size++;\n          }\n\n          let content = '';\n\n          /**\n           * @note This check is purely for the editor. The way we detect this is not\n           * bullet-proof and it's a best guess so there might be false-positives.\n           * The reason we do this is because we don't want to display binary files\n           * in the editor nor allow to edit them.\n           */\n          const isBinary = isBinaryFile(buffer);\n\n          if (!isBinary) {\n            content = this.#decodeFileContent(buffer);\n          }\n\n          this.files.setKey(sanitizedPath, { type: 'file', content, isBinary });\n\n          break;\n        }\n        case 'remove_file': {\n          this.#size--;\n          this.files.setKey(sanitizedPath, undefined);\n          break;\n        }\n        case 'update_directory': {\n          // we don't care about these events\n          break;\n        }\n      }\n    }\n  }\n\n  #decodeFileContent(buffer?: Uint8Array) {\n    if (!buffer || buffer.byteLength === 0) {\n      return '';\n    }\n\n    try {\n      return utf8TextDecoder.decode(buffer);\n    } catch (error) {\n      console.log(error);\n      return '';\n    }\n  }\n}\n\nfunction isBinaryFile(buffer: Uint8Array | undefined) {\n  if (buffer === undefined) {\n    return false;\n  }\n\n  return getEncoding(convertToBuffer(buffer), { chunkLength: 100 }) === 'binary';\n}\n\n/**\n * Converts a `Uint8Array` into a Node.js `Buffer` by copying the prototype.\n * The goal is to  avoid expensive copies. It does create a new typed array\n * but that's generally cheap as long as it uses the same underlying\n * array buffer.\n */\nfunction convertToBuffer(view: Uint8Array): Buffer {\n  const buffer = new Uint8Array(view.buffer, view.byteOffset, view.byteLength);\n\n  Object.setPrototypeOf(buffer, Buffer.prototype);\n\n  return buffer as Buffer;\n}\n"
  },
  {
    "path": "app/lib/stores/previews.ts",
    "content": "import type { WebContainer } from '@webcontainer/api';\nimport { atom } from 'nanostores';\n\nexport interface PreviewInfo {\n  port: number;\n  ready: boolean;\n  baseUrl: string;\n}\n\nexport class PreviewsStore {\n  #availablePreviews = new Map<number, PreviewInfo>();\n  #webcontainer: Promise<WebContainer>;\n\n  previews = atom<PreviewInfo[]>([]);\n\n  constructor(webcontainerPromise: Promise<WebContainer>) {\n    this.#webcontainer = webcontainerPromise;\n\n    this.#init();\n  }\n\n  async #init() {\n    const webcontainer = await this.#webcontainer;\n\n    webcontainer.on('port', (port, type, url) => {\n      let previewInfo = this.#availablePreviews.get(port);\n\n      if (type === 'close' && previewInfo) {\n        this.#availablePreviews.delete(port);\n        this.previews.set(this.previews.get().filter((preview) => preview.port !== port));\n\n        return;\n      }\n\n      const previews = this.previews.get();\n\n      if (!previewInfo) {\n        previewInfo = { port, ready: type === 'open', baseUrl: url };\n        this.#availablePreviews.set(port, previewInfo);\n        previews.push(previewInfo);\n      }\n\n      previewInfo.ready = type === 'open';\n      previewInfo.baseUrl = url;\n\n      this.previews.set([...previews]);\n    });\n  }\n}\n"
  },
  {
    "path": "app/lib/stores/settings.ts",
    "content": "import { map } from 'nanostores';\nimport { workbenchStore } from './workbench';\n\nexport interface Shortcut {\n  key: string;\n  ctrlKey?: boolean;\n  shiftKey?: boolean;\n  altKey?: boolean;\n  metaKey?: boolean;\n  ctrlOrMetaKey?: boolean;\n  action: () => void;\n}\n\nexport interface Shortcuts {\n  toggleTerminal: Shortcut;\n}\n\nexport interface Settings {\n  shortcuts: Shortcuts;\n}\n\nexport const shortcutsStore = map<Shortcuts>({\n  toggleTerminal: {\n    key: 'j',\n    ctrlOrMetaKey: true,\n    action: () => workbenchStore.toggleTerminal(),\n  },\n});\n\nexport const settingsStore = map<Settings>({\n  shortcuts: shortcutsStore.get(),\n});\n\nshortcutsStore.subscribe((shortcuts) => {\n  settingsStore.set({\n    ...settingsStore.get(),\n    shortcuts,\n  });\n});\n"
  },
  {
    "path": "app/lib/stores/terminal.ts",
    "content": "import type { WebContainer, WebContainerProcess } from '@webcontainer/api';\nimport { atom, type WritableAtom } from 'nanostores';\nimport type { ITerminal } from '~/types/terminal';\nimport { newShellProcess } from '~/utils/shell';\nimport { coloredText } from '~/utils/terminal';\n\nexport class TerminalStore {\n  #webcontainer: Promise<WebContainer>;\n  #terminals: Array<{ terminal: ITerminal; process: WebContainerProcess }> = [];\n\n  showTerminal: WritableAtom<boolean> = import.meta.hot?.data.showTerminal ?? atom(false);\n\n  constructor(webcontainerPromise: Promise<WebContainer>) {\n    this.#webcontainer = webcontainerPromise;\n\n    if (import.meta.hot) {\n      import.meta.hot.data.showTerminal = this.showTerminal;\n    }\n  }\n\n  toggleTerminal(value?: boolean) {\n    this.showTerminal.set(value !== undefined ? value : !this.showTerminal.get());\n  }\n\n  async attachTerminal(terminal: ITerminal) {\n    try {\n      const shellProcess = await newShellProcess(await this.#webcontainer, terminal);\n      this.#terminals.push({ terminal, process: shellProcess });\n    } catch (error: any) {\n      terminal.write(coloredText.red('Failed to spawn shell\\n\\n') + error.message);\n      return;\n    }\n  }\n\n  onTerminalResize(cols: number, rows: number) {\n    for (const { process } of this.#terminals) {\n      process.resize({ cols, rows });\n    }\n  }\n}\n"
  },
  {
    "path": "app/lib/stores/theme.ts",
    "content": "import { atom } from 'nanostores';\n\nexport type Theme = 'dark' | 'light';\n\nexport const kTheme = 'bolt_theme';\n\nexport function themeIsDark() {\n  return themeStore.get() === 'dark';\n}\n\nexport const DEFAULT_THEME = 'light';\n\nexport const themeStore = atom<Theme>(initStore());\n\nfunction initStore() {\n  if (!import.meta.env.SSR) {\n    const persistedTheme = localStorage.getItem(kTheme) as Theme | undefined;\n    const themeAttribute = document.querySelector('html')?.getAttribute('data-theme');\n\n    return persistedTheme ?? (themeAttribute as Theme) ?? DEFAULT_THEME;\n  }\n\n  return DEFAULT_THEME;\n}\n\nexport function toggleTheme() {\n  const currentTheme = themeStore.get();\n  const newTheme = currentTheme === 'dark' ? 'light' : 'dark';\n\n  themeStore.set(newTheme);\n\n  localStorage.setItem(kTheme, newTheme);\n\n  document.querySelector('html')?.setAttribute('data-theme', newTheme);\n}\n"
  },
  {
    "path": "app/lib/stores/workbench.ts",
    "content": "import { atom, map, type MapStore, type ReadableAtom, type WritableAtom } from 'nanostores';\nimport type { EditorDocument, ScrollPosition } from '~/components/editor/codemirror/CodeMirrorEditor';\nimport { ActionRunner } from '~/lib/runtime/action-runner';\nimport type { ActionCallbackData, ArtifactCallbackData } from '~/lib/runtime/message-parser';\nimport { webcontainer } from '~/lib/webcontainer';\nimport type { ITerminal } from '~/types/terminal';\nimport { unreachable } from '~/utils/unreachable';\nimport { EditorStore } from './editor';\nimport { FilesStore, type FileMap } from './files';\nimport { PreviewsStore } from './previews';\nimport { TerminalStore } from './terminal';\n\nexport interface ArtifactState {\n  id: string;\n  title: string;\n  closed: boolean;\n  runner: ActionRunner;\n}\n\nexport type ArtifactUpdateState = Pick<ArtifactState, 'title' | 'closed'>;\n\ntype Artifacts = MapStore<Record<string, ArtifactState>>;\n\nexport type WorkbenchViewType = 'code' | 'preview';\n\nexport class WorkbenchStore {\n  #previewsStore = new PreviewsStore(webcontainer);\n  #filesStore = new FilesStore(webcontainer);\n  #editorStore = new EditorStore(this.#filesStore);\n  #terminalStore = new TerminalStore(webcontainer);\n\n  artifacts: Artifacts = import.meta.hot?.data.artifacts ?? map({});\n\n  showWorkbench: WritableAtom<boolean> = import.meta.hot?.data.showWorkbench ?? atom(false);\n  currentView: WritableAtom<WorkbenchViewType> = import.meta.hot?.data.currentView ?? atom('code');\n  unsavedFiles: WritableAtom<Set<string>> = import.meta.hot?.data.unsavedFiles ?? atom(new Set<string>());\n  modifiedFiles = new Set<string>();\n  artifactIdList: string[] = [];\n\n  constructor() {\n    if (import.meta.hot) {\n      import.meta.hot.data.artifacts = this.artifacts;\n      import.meta.hot.data.unsavedFiles = this.unsavedFiles;\n      import.meta.hot.data.showWorkbench = this.showWorkbench;\n      import.meta.hot.data.currentView = this.currentView;\n    }\n  }\n\n  get previews() {\n    return this.#previewsStore.previews;\n  }\n\n  get files() {\n    return this.#filesStore.files;\n  }\n\n  get currentDocument(): ReadableAtom<EditorDocument | undefined> {\n    return this.#editorStore.currentDocument;\n  }\n\n  get selectedFile(): ReadableAtom<string | undefined> {\n    return this.#editorStore.selectedFile;\n  }\n\n  get firstArtifact(): ArtifactState | undefined {\n    return this.#getArtifact(this.artifactIdList[0]);\n  }\n\n  get filesCount(): number {\n    return this.#filesStore.filesCount;\n  }\n\n  get showTerminal() {\n    return this.#terminalStore.showTerminal;\n  }\n\n  toggleTerminal(value?: boolean) {\n    this.#terminalStore.toggleTerminal(value);\n  }\n\n  attachTerminal(terminal: ITerminal) {\n    this.#terminalStore.attachTerminal(terminal);\n  }\n\n  onTerminalResize(cols: number, rows: number) {\n    this.#terminalStore.onTerminalResize(cols, rows);\n  }\n\n  setDocuments(files: FileMap) {\n    this.#editorStore.setDocuments(files);\n\n    if (this.#filesStore.filesCount > 0 && this.currentDocument.get() === undefined) {\n      // we find the first file and select it\n      for (const [filePath, dirent] of Object.entries(files)) {\n        if (dirent?.type === 'file') {\n          this.setSelectedFile(filePath);\n          break;\n        }\n      }\n    }\n  }\n\n  setShowWorkbench(show: boolean) {\n    this.showWorkbench.set(show);\n  }\n\n  setCurrentDocumentContent(newContent: string) {\n    const filePath = this.currentDocument.get()?.filePath;\n\n    if (!filePath) {\n      return;\n    }\n\n    const originalContent = this.#filesStore.getFile(filePath)?.content;\n    const unsavedChanges = originalContent !== undefined && originalContent !== newContent;\n\n    this.#editorStore.updateFile(filePath, newContent);\n\n    const currentDocument = this.currentDocument.get();\n\n    if (currentDocument) {\n      const previousUnsavedFiles = this.unsavedFiles.get();\n\n      if (unsavedChanges && previousUnsavedFiles.has(currentDocument.filePath)) {\n        return;\n      }\n\n      const newUnsavedFiles = new Set(previousUnsavedFiles);\n\n      if (unsavedChanges) {\n        newUnsavedFiles.add(currentDocument.filePath);\n      } else {\n        newUnsavedFiles.delete(currentDocument.filePath);\n      }\n\n      this.unsavedFiles.set(newUnsavedFiles);\n    }\n  }\n\n  setCurrentDocumentScrollPosition(position: ScrollPosition) {\n    const editorDocument = this.currentDocument.get();\n\n    if (!editorDocument) {\n      return;\n    }\n\n    const { filePath } = editorDocument;\n\n    this.#editorStore.updateScrollPosition(filePath, position);\n  }\n\n  setSelectedFile(filePath: string | undefined) {\n    this.#editorStore.setSelectedFile(filePath);\n  }\n\n  async saveFile(filePath: string) {\n    const documents = this.#editorStore.documents.get();\n    const document = documents[filePath];\n\n    if (document === undefined) {\n      return;\n    }\n\n    await this.#filesStore.saveFile(filePath, document.value);\n\n    const newUnsavedFiles = new Set(this.unsavedFiles.get());\n    newUnsavedFiles.delete(filePath);\n\n    this.unsavedFiles.set(newUnsavedFiles);\n  }\n\n  async saveCurrentDocument() {\n    const currentDocument = this.currentDocument.get();\n\n    if (currentDocument === undefined) {\n      return;\n    }\n\n    await this.saveFile(currentDocument.filePath);\n  }\n\n  resetCurrentDocument() {\n    const currentDocument = this.currentDocument.get();\n\n    if (currentDocument === undefined) {\n      return;\n    }\n\n    const { filePath } = currentDocument;\n    const file = this.#filesStore.getFile(filePath);\n\n    if (!file) {\n      return;\n    }\n\n    this.setCurrentDocumentContent(file.content);\n  }\n\n  async saveAllFiles() {\n    for (const filePath of this.unsavedFiles.get()) {\n      await this.saveFile(filePath);\n    }\n  }\n\n  getFileModifcations() {\n    return this.#filesStore.getFileModifications();\n  }\n\n  resetAllFileModifications() {\n    this.#filesStore.resetFileModifications();\n  }\n\n  abortAllActions() {\n    // TODO: what do we wanna do and how do we wanna recover from this?\n  }\n\n  addArtifact({ messageId, title, id }: ArtifactCallbackData) {\n    const artifact = this.#getArtifact(messageId);\n\n    if (artifact) {\n      return;\n    }\n\n    if (!this.artifactIdList.includes(messageId)) {\n      this.artifactIdList.push(messageId);\n    }\n\n    this.artifacts.setKey(messageId, {\n      id,\n      title,\n      closed: false,\n      runner: new ActionRunner(webcontainer),\n    });\n  }\n\n  updateArtifact({ messageId }: ArtifactCallbackData, state: Partial<ArtifactUpdateState>) {\n    const artifact = this.#getArtifact(messageId);\n\n    if (!artifact) {\n      return;\n    }\n\n    this.artifacts.setKey(messageId, { ...artifact, ...state });\n  }\n\n  async addAction(data: ActionCallbackData) {\n    const { messageId } = data;\n\n    const artifact = this.#getArtifact(messageId);\n\n    if (!artifact) {\n      unreachable('Artifact not found');\n    }\n\n    artifact.runner.addAction(data);\n  }\n\n  async runAction(data: ActionCallbackData) {\n    const { messageId } = data;\n\n    const artifact = this.#getArtifact(messageId);\n\n    if (!artifact) {\n      unreachable('Artifact not found');\n    }\n\n    artifact.runner.runAction(data);\n  }\n\n  #getArtifact(id: string) {\n    const artifacts = this.artifacts.get();\n    return artifacts[id];\n  }\n}\n\nexport const workbenchStore = new WorkbenchStore();\n"
  },
  {
    "path": "app/lib/webcontainer/auth.client.ts",
    "content": "/**\n * This client-only module that contains everything related to auth and is used\n * to avoid importing `@webcontainer/api` in the server bundle.\n */\n\nexport { auth, type AuthAPI } from '@webcontainer/api';\n"
  },
  {
    "path": "app/lib/webcontainer/index.ts",
    "content": "import { WebContainer } from '@webcontainer/api';\nimport { WORK_DIR_NAME } from '~/utils/constants';\n\ninterface WebContainerContext {\n  loaded: boolean;\n}\n\nexport const webcontainerContext: WebContainerContext = import.meta.hot?.data.webcontainerContext ?? {\n  loaded: false,\n};\n\nif (import.meta.hot) {\n  import.meta.hot.data.webcontainerContext = webcontainerContext;\n}\n\nexport let webcontainer: Promise<WebContainer> = new Promise(() => {\n  // noop for ssr\n});\n\nif (!import.meta.env.SSR) {\n  webcontainer =\n    import.meta.hot?.data.webcontainer ??\n    Promise.resolve()\n      .then(() => {\n        return WebContainer.boot({ workdirName: WORK_DIR_NAME });\n      })\n      .then((webcontainer) => {\n        webcontainerContext.loaded = true;\n        return webcontainer;\n      });\n\n  if (import.meta.hot) {\n    import.meta.hot.data.webcontainer = webcontainer;\n  }\n}\n"
  },
  {
    "path": "app/root.tsx",
    "content": "import { useStore } from '@nanostores/react';\nimport type { LinksFunction } from '@remix-run/cloudflare';\nimport { Links, Meta, Outlet, Scripts, ScrollRestoration } from '@remix-run/react';\nimport tailwindReset from '@unocss/reset/tailwind-compat.css?url';\nimport { themeStore } from './lib/stores/theme';\nimport { stripIndents } from './utils/stripIndent';\nimport { createHead } from 'remix-island';\nimport { useEffect } from 'react';\n\nimport reactToastifyStyles from 'react-toastify/dist/ReactToastify.css?url';\nimport globalStyles from './styles/index.scss?url';\nimport xtermStyles from '@xterm/xterm/css/xterm.css?url';\n\nimport 'virtual:uno.css';\n\nexport const links: LinksFunction = () => [\n  {\n    rel: 'icon',\n    href: '/favicon.svg',\n    type: 'image/svg+xml',\n  },\n  { rel: 'stylesheet', href: reactToastifyStyles },\n  { rel: 'stylesheet', href: tailwindReset },\n  { rel: 'stylesheet', href: globalStyles },\n  { rel: 'stylesheet', href: xtermStyles },\n  {\n    rel: 'preconnect',\n    href: 'https://fonts.googleapis.com',\n  },\n  {\n    rel: 'preconnect',\n    href: 'https://fonts.gstatic.com',\n    crossOrigin: 'anonymous',\n  },\n  {\n    rel: 'stylesheet',\n    href: 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap',\n  },\n];\n\nconst inlineThemeCode = stripIndents`\n  setTutorialKitTheme();\n\n  function setTutorialKitTheme() {\n    let theme = localStorage.getItem('bolt_theme');\n\n    if (!theme) {\n      theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';\n    }\n\n    document.querySelector('html')?.setAttribute('data-theme', theme);\n  }\n`;\n\nexport const Head = createHead(() => (\n  <>\n    <meta charSet=\"utf-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n    <Meta />\n    <Links />\n    <script dangerouslySetInnerHTML={{ __html: inlineThemeCode }} />\n  </>\n));\n\nexport function Layout({ children }: { children: React.ReactNode }) {\n  const theme = useStore(themeStore);\n\n  useEffect(() => {\n    document.querySelector('html')?.setAttribute('data-theme', theme);\n  }, [theme]);\n\n  return (\n    <>\n      {children}\n      <ScrollRestoration />\n      <Scripts />\n    </>\n  );\n}\n\nexport default function App() {\n  return <Outlet />;\n}\n"
  },
  {
    "path": "app/routes/_index.tsx",
    "content": "import { json, type MetaFunction } from '@remix-run/cloudflare';\nimport { ClientOnly } from 'remix-utils/client-only';\nimport { BaseChat } from '~/components/chat/BaseChat';\nimport { Chat } from '~/components/chat/Chat.client';\nimport { Header } from '~/components/header/Header';\n\nexport const meta: MetaFunction = () => {\n  return [{ title: 'Bolt' }, { name: 'description', content: 'Talk with Bolt, an AI assistant from StackBlitz' }];\n};\n\nexport const loader = () => json({});\n\nexport default function Index() {\n  return (\n    <div className=\"flex flex-col h-full w-full\">\n      <Header />\n      <ClientOnly fallback={<BaseChat />}>{() => <Chat />}</ClientOnly>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/routes/api.chat.ts",
    "content": "import { type ActionFunctionArgs } from '@remix-run/cloudflare';\nimport { MAX_RESPONSE_SEGMENTS, MAX_TOKENS } from '~/lib/.server/llm/constants';\nimport { CONTINUE_PROMPT } from '~/lib/.server/llm/prompts';\nimport { streamText, type Messages, type StreamingOptions } from '~/lib/.server/llm/stream-text';\nimport SwitchableStream from '~/lib/.server/llm/switchable-stream';\n\nexport async function action(args: ActionFunctionArgs) {\n  return chatAction(args);\n}\n\nasync function chatAction({ context, request }: ActionFunctionArgs) {\n  const { messages } = await request.json<{ messages: Messages }>();\n\n  const stream = new SwitchableStream();\n\n  try {\n    const options: StreamingOptions = {\n      toolChoice: 'none',\n      onFinish: async ({ text: content, finishReason }) => {\n        if (finishReason !== 'length') {\n          return stream.close();\n        }\n\n        if (stream.switches >= MAX_RESPONSE_SEGMENTS) {\n          throw Error('Cannot continue message: Maximum segments reached');\n        }\n\n        const switchesLeft = MAX_RESPONSE_SEGMENTS - stream.switches;\n\n        console.log(`Reached max token limit (${MAX_TOKENS}): Continuing message (${switchesLeft} switches left)`);\n\n        messages.push({ role: 'assistant', content });\n        messages.push({ role: 'user', content: CONTINUE_PROMPT });\n\n        const result = await streamText(messages, context.cloudflare.env, options);\n\n        return stream.switchSource(result.toAIStream());\n      },\n    };\n\n    const result = await streamText(messages, context.cloudflare.env, options);\n\n    stream.switchSource(result.toAIStream());\n\n    return new Response(stream.readable, {\n      status: 200,\n      headers: {\n        contentType: 'text/plain; charset=utf-8',\n      },\n    });\n  } catch (error) {\n    console.log(error);\n\n    throw new Response(null, {\n      status: 500,\n      statusText: 'Internal Server Error',\n    });\n  }\n}\n"
  },
  {
    "path": "app/routes/api.enhancer.ts",
    "content": "import { type ActionFunctionArgs } from '@remix-run/cloudflare';\nimport { StreamingTextResponse, parseStreamPart } from 'ai';\nimport { streamText } from '~/lib/.server/llm/stream-text';\nimport { stripIndents } from '~/utils/stripIndent';\n\nconst encoder = new TextEncoder();\nconst decoder = new TextDecoder();\n\nexport async function action(args: ActionFunctionArgs) {\n  return enhancerAction(args);\n}\n\nasync function enhancerAction({ context, request }: ActionFunctionArgs) {\n  const { message } = await request.json<{ message: string }>();\n\n  try {\n    const result = await streamText(\n      [\n        {\n          role: 'user',\n          content: stripIndents`\n          I want you to improve the user prompt that is wrapped in \\`<original_prompt>\\` tags.\n\n          IMPORTANT: Only respond with the improved prompt and nothing else!\n\n          <original_prompt>\n            ${message}\n          </original_prompt>\n        `,\n        },\n      ],\n      context.cloudflare.env,\n    );\n\n    const transformStream = new TransformStream({\n      transform(chunk, controller) {\n        const processedChunk = decoder\n          .decode(chunk)\n          .split('\\n')\n          .filter((line) => line !== '')\n          .map(parseStreamPart)\n          .map((part) => part.value)\n          .join('');\n\n        controller.enqueue(encoder.encode(processedChunk));\n      },\n    });\n\n    const transformedStream = result.toAIStream().pipeThrough(transformStream);\n\n    return new StreamingTextResponse(transformedStream);\n  } catch (error) {\n    console.log(error);\n\n    throw new Response(null, {\n      status: 500,\n      statusText: 'Internal Server Error',\n    });\n  }\n}\n"
  },
  {
    "path": "app/routes/chat.$id.tsx",
    "content": "import { json, type LoaderFunctionArgs } from '@remix-run/cloudflare';\nimport { default as IndexRoute } from './_index';\n\nexport async function loader(args: LoaderFunctionArgs) {\n  return json({ id: args.params.id });\n}\n\nexport default IndexRoute;\n"
  },
  {
    "path": "app/styles/animations.scss",
    "content": ".animated {\n  animation-fill-mode: both;\n  animation-duration: var(--animate-duration, 0.2s);\n  animation-timing-function: cubic-bezier(0, 0, 0.2, 1);\n\n  &.fadeInRight {\n    animation-name: fadeInRight;\n  }\n\n  &.fadeOutRight {\n    animation-name: fadeOutRight;\n  }\n}\n\n@keyframes fadeInRight {\n  from {\n    opacity: 0;\n    transform: translate3d(100%, 0, 0);\n  }\n\n  to {\n    opacity: 1;\n    transform: translate3d(0, 0, 0);\n  }\n}\n\n@keyframes fadeOutRight {\n  from {\n    opacity: 1;\n  }\n\n  to {\n    opacity: 0;\n    transform: translate3d(100%, 0, 0);\n  }\n}\n\n.dropdown-animation {\n  opacity: 0;\n  animation: fadeMoveDown 0.15s forwards;\n  animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n}\n\n@keyframes fadeMoveDown {\n  to {\n    opacity: 1;\n    transform: translateY(6px);\n  }\n}\n"
  },
  {
    "path": "app/styles/components/code.scss",
    "content": ".actions .shiki {\n  background-color: var(--bolt-elements-actions-code-background) !important;\n}\n\n.shiki {\n  &:not(:has(.actions), .actions *) {\n    background-color: var(--bolt-elements-messages-code-background) !important;\n  }\n}\n"
  },
  {
    "path": "app/styles/components/editor.scss",
    "content": ":root {\n  --cm-backgroundColor: var(--bolt-elements-editor-backgroundColor, var(--bolt-elements-bg-depth-1));\n  --cm-textColor: var(--bolt-elements-editor-textColor, var(--bolt-elements-textPrimary));\n\n  /* Gutter */\n\n  --cm-gutter-backgroundColor: var(--bolt-elements-editor-gutter-backgroundColor, var(--cm-backgroundColor));\n  --cm-gutter-textColor: var(--bolt-elements-editor-gutter-textColor, var(--bolt-elements-textSecondary));\n  --cm-gutter-activeLineTextColor: var(--bolt-elements-editor-gutter-activeLineTextColor, var(--cm-gutter-textColor));\n\n  /* Fold Gutter */\n\n  --cm-foldGutter-textColor: var(--bolt-elements-editor-foldGutter-textColor, var(--cm-gutter-textColor));\n  --cm-foldGutter-textColorHover: var(--bolt-elements-editor-foldGutter-textColorHover, var(--cm-gutter-textColor));\n\n  /* Active Line */\n\n  --cm-activeLineBackgroundColor: var(--bolt-elements-editor-activeLineBackgroundColor, rgb(224 231 235 / 30%));\n\n  /* Cursor */\n\n  --cm-cursor-width: 2px;\n  --cm-cursor-backgroundColor: var(--bolt-elements-editor-cursorColor, var(--bolt-elements-textSecondary));\n\n  /* Matching Brackets */\n\n  --cm-matching-bracket: var(--bolt-elements-editor-matchingBracketBackgroundColor, rgb(50 140 130 / 0.3));\n\n  /* Selection */\n\n  --cm-selection-backgroundColorFocused: var(--bolt-elements-editor-selection-backgroundColor, #42b4ff);\n  --cm-selection-backgroundOpacityFocused: var(--bolt-elements-editor-selection-backgroundOpacity, 0.3);\n  --cm-selection-backgroundColorBlured: var(--bolt-elements-editor-selection-inactiveBackgroundColor, #c9e9ff);\n  --cm-selection-backgroundOpacityBlured: var(--bolt-elements-editor-selection-inactiveBackgroundOpacity, 0.3);\n\n  /* Panels */\n\n  --cm-panels-borderColor: var(--bolt-elements-editor-panels-borderColor, var(--bolt-elements-borderColor));\n\n  /* Search */\n\n  --cm-search-backgroundColor: var(--bolt-elements-editor-search-backgroundColor, var(--cm-backgroundColor));\n  --cm-search-textColor: var(--bolt-elements-editor-search-textColor, var(--bolt-elements-textSecondary));\n  --cm-search-closeButton-backgroundColor: var(--bolt-elements-editor-search-closeButton-backgroundColor, transparent);\n\n  --cm-search-closeButton-backgroundColorHover: var(\n    --bolt-elements-editor-search-closeButton-backgroundColorHover,\n    var(--bolt-elements-item-backgroundActive)\n  );\n\n  --cm-search-closeButton-textColor: var(\n    --bolt-elements-editor-search-closeButton-textColor,\n    var(--bolt-elements-item-contentDefault)\n  );\n\n  --cm-search-closeButton-textColorHover: var(\n    --bolt-elements-editor-search-closeButton-textColorHover,\n    var(--bolt-elements-item-contentActive)\n  );\n\n  --cm-search-button-backgroundColor: var(\n    --bolt-elements-editor-search-button-backgroundColor,\n    var(--bolt-elements-item-backgroundDefault)\n  );\n\n  --cm-search-button-backgroundColorHover: var(\n    --bolt-elements-editor-search-button-backgroundColorHover,\n    var(--bolt-elements-item-backgroundActive)\n  );\n\n  --cm-search-button-textColor: var(--bolt-elements-editor-search-button-textColor, var(--bolt-elements-textSecondary));\n\n  --cm-search-button-textColorHover: var(\n    --bolt-elements-editor-search-button-textColorHover,\n    var(--bolt-elements-textPrimary)\n  );\n\n  --cm-search-button-borderColor: var(--bolt-elements-editor-search-button-borderColor, transparent);\n  --cm-search-button-borderColorHover: var(--bolt-elements-editor-search-button-borderColorHover, transparent);\n\n  --cm-search-button-borderColorFocused: var(\n    --bolt-elements-editor-search-button-borderColorFocused,\n    var(--bolt-elements-borderColorActive)\n  );\n\n  --cm-search-input-backgroundColor: var(--bolt-elements-editor-search-input-backgroundColor, transparent);\n  --cm-search-input-textColor: var(--bolt-elements-editor-search-input-textColor, var(--bolt-elements-textPrimary));\n  --cm-search-input-borderColor: var(--bolt-elements-editor-search-input-borderColor, var(--bolt-elements-borderColor));\n\n  --cm-search-input-borderColorFocused: var(\n    --bolt-elements-editor-search-input-borderColorFocused,\n    var(--bolt-elements-borderColorActive)\n  );\n\n  /* Tooltip */\n\n  --cm-tooltip-backgroundColor: var(--bolt-elements-editor-tooltip-backgroundColor, var(--cm-backgroundColor));\n  --cm-tooltip-textColor: var(--bolt-elements-editor-tooltip-textColor, var(--bolt-elements-textPrimary));\n\n  --cm-tooltip-backgroundColorSelected: var(\n    --bolt-elements-editor-tooltip-backgroundColorSelected,\n    theme('colors.alpha.accent.30')\n  );\n\n  --cm-tooltip-textColorSelected: var(\n    --bolt-elements-editor-tooltip-textColorSelected,\n    var(--bolt-elements-textPrimary)\n  );\n\n  --cm-tooltip-borderColor: var(--bolt-elements-editor-tooltip-borderColor, var(--bolt-elements-borderColor));\n\n  --cm-searchMatch-backgroundColor: var(--bolt-elements-editor-searchMatch-backgroundColor, rgba(234, 92, 0, 0.33));\n}\n\nhtml[data-theme='light'] {\n  --bolt-elements-editor-gutter-textColor: #237893;\n  --bolt-elements-editor-gutter-activeLineTextColor: var(--bolt-elements-textPrimary);\n  --bolt-elements-editor-foldGutter-textColorHover: var(--bolt-elements-textPrimary);\n  --bolt-elements-editor-activeLineBackgroundColor: rgb(50 53 63 / 5%);\n  --bolt-elements-editor-tooltip-backgroundColorSelected: theme('colors.alpha.accent.20');\n  --bolt-elements-editor-search-button-backgroundColor: theme('colors.gray.100');\n  --bolt-elements-editor-search-button-backgroundColorHover: theme('colors.alpha.gray.10');\n}\n\nhtml[data-theme='dark'] {\n  --cm-backgroundColor: var(--bolt-elements-bg-depth-2);\n  --bolt-elements-editor-gutter-textColor: var(--bolt-elements-textTertiary);\n  --bolt-elements-editor-gutter-activeLineTextColor: var(--bolt-elements-textSecondary);\n  --bolt-elements-editor-selection-inactiveBackgroundOpacity: 0.3;\n  --bolt-elements-editor-activeLineBackgroundColor: rgb(50 53 63 / 50%);\n  --bolt-elements-editor-foldGutter-textColorHover: var(--bolt-elements-textPrimary);\n  --bolt-elements-editor-matchingBracketBackgroundColor: rgba(66, 180, 255, 0.3);\n  --bolt-elements-editor-search-button-backgroundColor: theme('colors.gray.800');\n  --bolt-elements-editor-search-button-backgroundColorHover: theme('colors.alpha.white.10');\n}\n"
  },
  {
    "path": "app/styles/components/resize-handle.scss",
    "content": "[data-resize-handle] {\n  position: relative;\n\n  &[data-panel-group-direction='horizontal']:after {\n    content: '';\n    position: absolute;\n    top: 0;\n    bottom: 0;\n    left: -6px;\n    right: -5px;\n    z-index: $zIndexMax;\n  }\n\n  &[data-panel-group-direction='vertical']:after {\n    content: '';\n    position: absolute;\n    left: 0;\n    right: 0;\n    top: -5px;\n    bottom: -6px;\n    z-index: $zIndexMax;\n  }\n\n  &[data-resize-handle-state='hover']:after,\n  &[data-resize-handle-state='drag']:after {\n    background-color: #8882;\n  }\n}\n"
  },
  {
    "path": "app/styles/components/terminal.scss",
    "content": ".xterm {\n  padding: 1rem;\n}\n"
  },
  {
    "path": "app/styles/components/toast.scss",
    "content": ".Toastify__toast {\n  --at-apply: shadow-md;\n\n  background-color: var(--bolt-elements-bg-depth-2);\n  color: var(--bolt-elements-textPrimary);\n  border: 1px solid var(--bolt-elements-borderColor);\n}\n\n.Toastify__close-button {\n  color: var(--bolt-elements-item-contentDefault);\n  opacity: 1;\n  transition: none;\n\n  &:hover {\n    color: var(--bolt-elements-item-contentActive);\n  }\n}\n"
  },
  {
    "path": "app/styles/index.scss",
    "content": "@import './variables.scss';\n@import './z-index.scss';\n@import './animations.scss';\n@import './components/terminal.scss';\n@import './components/resize-handle.scss';\n@import './components/code.scss';\n@import './components/editor.scss';\n@import './components/toast.scss';\n\nhtml,\nbody {\n  height: 100%;\n  width: 100%;\n}\n"
  },
  {
    "path": "app/styles/variables.scss",
    "content": "/* Color Tokens Light Theme */\n:root,\n:root[data-theme='light'] {\n  --bolt-elements-borderColor: theme('colors.alpha.gray.10');\n  --bolt-elements-borderColorActive: theme('colors.accent.600');\n\n  --bolt-elements-bg-depth-1: theme('colors.white');\n  --bolt-elements-bg-depth-2: theme('colors.gray.50');\n  --bolt-elements-bg-depth-3: theme('colors.gray.200');\n  --bolt-elements-bg-depth-4: theme('colors.alpha.gray.5');\n\n  --bolt-elements-textPrimary: theme('colors.gray.950');\n  --bolt-elements-textSecondary: theme('colors.gray.600');\n  --bolt-elements-textTertiary: theme('colors.gray.500');\n\n  --bolt-elements-code-background: theme('colors.gray.100');\n  --bolt-elements-code-text: theme('colors.gray.950');\n\n  --bolt-elements-button-primary-background: theme('colors.alpha.accent.10');\n  --bolt-elements-button-primary-backgroundHover: theme('colors.alpha.accent.20');\n  --bolt-elements-button-primary-text: theme('colors.accent.500');\n\n  --bolt-elements-button-secondary-background: theme('colors.alpha.gray.5');\n  --bolt-elements-button-secondary-backgroundHover: theme('colors.alpha.gray.10');\n  --bolt-elements-button-secondary-text: theme('colors.gray.950');\n\n  --bolt-elements-button-danger-background: theme('colors.alpha.red.10');\n  --bolt-elements-button-danger-backgroundHover: theme('colors.alpha.red.20');\n  --bolt-elements-button-danger-text: theme('colors.red.500');\n\n  --bolt-elements-item-contentDefault: theme('colors.alpha.gray.50');\n  --bolt-elements-item-contentActive: theme('colors.gray.950');\n  --bolt-elements-item-contentAccent: theme('colors.accent.700');\n  --bolt-elements-item-contentDanger: theme('colors.red.500');\n  --bolt-elements-item-backgroundDefault: rgba(0, 0, 0, 0);\n  --bolt-elements-item-backgroundActive: theme('colors.alpha.gray.5');\n  --bolt-elements-item-backgroundAccent: theme('colors.alpha.accent.10');\n  --bolt-elements-item-backgroundDanger: theme('colors.alpha.red.10');\n\n  --bolt-elements-loader-background: theme('colors.alpha.gray.10');\n  --bolt-elements-loader-progress: theme('colors.accent.500');\n\n  --bolt-elements-artifacts-background: theme('colors.white');\n  --bolt-elements-artifacts-backgroundHover: theme('colors.alpha.gray.2');\n  --bolt-elements-artifacts-borderColor: var(--bolt-elements-borderColor);\n  --bolt-elements-artifacts-inlineCode-background: theme('colors.gray.100');\n  --bolt-elements-artifacts-inlineCode-text: var(--bolt-elements-textPrimary);\n\n  --bolt-elements-actions-background: theme('colors.white');\n  --bolt-elements-actions-code-background: theme('colors.gray.800');\n\n  --bolt-elements-messages-background: theme('colors.gray.100');\n  --bolt-elements-messages-linkColor: theme('colors.accent.500');\n  --bolt-elements-messages-code-background: theme('colors.gray.800');\n  --bolt-elements-messages-inlineCode-background: theme('colors.gray.200');\n  --bolt-elements-messages-inlineCode-text: theme('colors.gray.800');\n\n  --bolt-elements-icon-success: theme('colors.green.500');\n  --bolt-elements-icon-error: theme('colors.red.500');\n  --bolt-elements-icon-primary: theme('colors.gray.950');\n  --bolt-elements-icon-secondary: theme('colors.gray.600');\n  --bolt-elements-icon-tertiary: theme('colors.gray.500');\n\n  --bolt-elements-dividerColor: theme('colors.gray.100');\n\n  --bolt-elements-prompt-background: theme('colors.alpha.white.80');\n\n  --bolt-elements-sidebar-dropdownShadow: theme('colors.alpha.gray.10');\n  --bolt-elements-sidebar-buttonBackgroundDefault: theme('colors.alpha.accent.10');\n  --bolt-elements-sidebar-buttonBackgroundHover: theme('colors.alpha.accent.20');\n  --bolt-elements-sidebar-buttonText: theme('colors.accent.700');\n\n  --bolt-elements-preview-addressBar-background: theme('colors.gray.100');\n  --bolt-elements-preview-addressBar-backgroundHover: theme('colors.alpha.gray.5');\n  --bolt-elements-preview-addressBar-backgroundActive: theme('colors.white');\n  --bolt-elements-preview-addressBar-text: var(--bolt-elements-textSecondary);\n  --bolt-elements-preview-addressBar-textActive: var(--bolt-elements-textPrimary);\n\n  --bolt-elements-terminals-background: theme('colors.white');\n  --bolt-elements-terminals-buttonBackground: var(--bolt-elements-bg-depth-4);\n\n  --bolt-elements-cta-background: theme('colors.gray.100');\n  --bolt-elements-cta-text: theme('colors.gray.950');\n\n  /* Terminal Colors */\n  --bolt-terminal-background: var(--bolt-elements-terminals-background);\n  --bolt-terminal-foreground: #333333;\n  --bolt-terminal-selection-background: #00000040;\n  --bolt-terminal-black: #000000;\n  --bolt-terminal-red: #cd3131;\n  --bolt-terminal-green: #00bc00;\n  --bolt-terminal-yellow: #949800;\n  --bolt-terminal-blue: #0451a5;\n  --bolt-terminal-magenta: #bc05bc;\n  --bolt-terminal-cyan: #0598bc;\n  --bolt-terminal-white: #555555;\n  --bolt-terminal-brightBlack: #686868;\n  --bolt-terminal-brightRed: #cd3131;\n  --bolt-terminal-brightGreen: #00bc00;\n  --bolt-terminal-brightYellow: #949800;\n  --bolt-terminal-brightBlue: #0451a5;\n  --bolt-terminal-brightMagenta: #bc05bc;\n  --bolt-terminal-brightCyan: #0598bc;\n  --bolt-terminal-brightWhite: #a5a5a5;\n}\n\n/* Color Tokens Dark Theme */\n:root,\n:root[data-theme='dark'] {\n  --bolt-elements-borderColor: theme('colors.alpha.white.10');\n  --bolt-elements-borderColorActive: theme('colors.accent.500');\n\n  --bolt-elements-bg-depth-1: theme('colors.gray.950');\n  --bolt-elements-bg-depth-2: theme('colors.gray.900');\n  --bolt-elements-bg-depth-3: theme('colors.gray.800');\n  --bolt-elements-bg-depth-4: theme('colors.alpha.white.5');\n\n  --bolt-elements-textPrimary: theme('colors.white');\n  --bolt-elements-textSecondary: theme('colors.gray.400');\n  --bolt-elements-textTertiary: theme('colors.gray.500');\n\n  --bolt-elements-code-background: theme('colors.gray.800');\n  --bolt-elements-code-text: theme('colors.white');\n\n  --bolt-elements-button-primary-background: theme('colors.alpha.accent.10');\n  --bolt-elements-button-primary-backgroundHover: theme('colors.alpha.accent.20');\n  --bolt-elements-button-primary-text: theme('colors.accent.500');\n\n  --bolt-elements-button-secondary-background: theme('colors.alpha.white.5');\n  --bolt-elements-button-secondary-backgroundHover: theme('colors.alpha.white.10');\n  --bolt-elements-button-secondary-text: theme('colors.white');\n\n  --bolt-elements-button-danger-background: theme('colors.alpha.red.10');\n  --bolt-elements-button-danger-backgroundHover: theme('colors.alpha.red.20');\n  --bolt-elements-button-danger-text: theme('colors.red.500');\n\n  --bolt-elements-item-contentDefault: theme('colors.alpha.white.50');\n  --bolt-elements-item-contentActive: theme('colors.white');\n  --bolt-elements-item-contentAccent: theme('colors.accent.500');\n  --bolt-elements-item-contentDanger: theme('colors.red.500');\n  --bolt-elements-item-backgroundDefault: rgba(255, 255, 255, 0);\n  --bolt-elements-item-backgroundActive: theme('colors.alpha.white.10');\n  --bolt-elements-item-backgroundAccent: theme('colors.alpha.accent.10');\n  --bolt-elements-item-backgroundDanger: theme('colors.alpha.red.10');\n\n  --bolt-elements-loader-background: theme('colors.alpha.gray.10');\n  --bolt-elements-loader-progress: theme('colors.accent.500');\n\n  --bolt-elements-artifacts-background: theme('colors.gray.900');\n  --bolt-elements-artifacts-backgroundHover: theme('colors.alpha.white.5');\n  --bolt-elements-artifacts-borderColor: var(--bolt-elements-borderColor);\n  --bolt-elements-artifacts-inlineCode-background: theme('colors.gray.800');\n  --bolt-elements-artifacts-inlineCode-text: theme('colors.white');\n\n  --bolt-elements-actions-background: theme('colors.gray.900');\n  --bolt-elements-actions-code-background: theme('colors.gray.800');\n\n  --bolt-elements-messages-background: theme('colors.gray.800');\n  --bolt-elements-messages-linkColor: theme('colors.accent.500');\n  --bolt-elements-messages-code-background: theme('colors.gray.900');\n  --bolt-elements-messages-inlineCode-background: theme('colors.gray.700');\n  --bolt-elements-messages-inlineCode-text: var(--bolt-elements-textPrimary);\n\n  --bolt-elements-icon-success: theme('colors.green.400');\n  --bolt-elements-icon-error: theme('colors.red.400');\n  --bolt-elements-icon-primary: theme('colors.gray.950');\n  --bolt-elements-icon-secondary: theme('colors.gray.600');\n  --bolt-elements-icon-tertiary: theme('colors.gray.500');\n\n  --bolt-elements-dividerColor: theme('colors.gray.100');\n\n  --bolt-elements-prompt-background: theme('colors.alpha.gray.80');\n\n  --bolt-elements-sidebar-dropdownShadow: theme('colors.alpha.gray.30');\n  --bolt-elements-sidebar-buttonBackgroundDefault: theme('colors.alpha.accent.10');\n  --bolt-elements-sidebar-buttonBackgroundHover: theme('colors.alpha.accent.20');\n  --bolt-elements-sidebar-buttonText: theme('colors.accent.500');\n\n  --bolt-elements-preview-addressBar-background: var(--bolt-elements-bg-depth-1);\n  --bolt-elements-preview-addressBar-backgroundHover: theme('colors.alpha.white.5');\n  --bolt-elements-preview-addressBar-backgroundActive: var(--bolt-elements-bg-depth-1);\n  --bolt-elements-preview-addressBar-text: var(--bolt-elements-textSecondary);\n  --bolt-elements-preview-addressBar-textActive: var(--bolt-elements-textPrimary);\n\n  --bolt-elements-terminals-background: var(--bolt-elements-bg-depth-1);\n  --bolt-elements-terminals-buttonBackground: var(--bolt-elements-bg-depth-3);\n\n  --bolt-elements-cta-background: theme('colors.alpha.white.10');\n  --bolt-elements-cta-text: theme('colors.white');\n\n  /* Terminal Colors */\n  --bolt-terminal-background: var(--bolt-elements-terminals-background);\n  --bolt-terminal-foreground: #eff0eb;\n  --bolt-terminal-selection-background: #97979b33;\n  --bolt-terminal-black: #000000;\n  --bolt-terminal-red: #ff5c57;\n  --bolt-terminal-green: #5af78e;\n  --bolt-terminal-yellow: #f3f99d;\n  --bolt-terminal-blue: #57c7ff;\n  --bolt-terminal-magenta: #ff6ac1;\n  --bolt-terminal-cyan: #9aedfe;\n  --bolt-terminal-white: #f1f1f0;\n  --bolt-terminal-brightBlack: #686868;\n  --bolt-terminal-brightRed: #ff5c57;\n  --bolt-terminal-brightGreen: #5af78e;\n  --bolt-terminal-brightYellow: #f3f99d;\n  --bolt-terminal-brightBlue: #57c7ff;\n  --bolt-terminal-brightMagenta: #ff6ac1;\n  --bolt-terminal-brightCyan: #9aedfe;\n  --bolt-terminal-brightWhite: #f1f1f0;\n}\n\n/*\n * Element Tokens\n *\n * Hierarchy: Element Token -> (Element Token | Color Tokens) -> Primitives\n */\n:root {\n  --header-height: 54px;\n  --chat-max-width: 37rem;\n  --chat-min-width: 640px;\n  --workbench-width: min(calc(100% - var(--chat-min-width)), 1536px);\n  --workbench-inner-width: var(--workbench-width);\n  --workbench-left: calc(100% - var(--workbench-width));\n\n  /* Toasts */\n  --toastify-color-progress-success: var(--bolt-elements-icon-success);\n  --toastify-color-progress-error: var(--bolt-elements-icon-error);\n\n  /* Terminal */\n  --bolt-elements-terminal-backgroundColor: var(--bolt-terminal-background);\n  --bolt-elements-terminal-textColor: var(--bolt-terminal-foreground);\n  --bolt-elements-terminal-cursorColor: var(--bolt-terminal-foreground);\n  --bolt-elements-terminal-selection-backgroundColor: var(--bolt-terminal-selection-background);\n  --bolt-elements-terminal-color-black: var(--bolt-terminal-black);\n  --bolt-elements-terminal-color-red: var(--bolt-terminal-red);\n  --bolt-elements-terminal-color-green: var(--bolt-terminal-green);\n  --bolt-elements-terminal-color-yellow: var(--bolt-terminal-yellow);\n  --bolt-elements-terminal-color-blue: var(--bolt-terminal-blue);\n  --bolt-elements-terminal-color-magenta: var(--bolt-terminal-magenta);\n  --bolt-elements-terminal-color-cyan: var(--bolt-terminal-cyan);\n  --bolt-elements-terminal-color-white: var(--bolt-terminal-white);\n  --bolt-elements-terminal-color-brightBlack: var(--bolt-terminal-brightBlack);\n  --bolt-elements-terminal-color-brightRed: var(--bolt-terminal-brightRed);\n  --bolt-elements-terminal-color-brightGreen: var(--bolt-terminal-brightGreen);\n  --bolt-elements-terminal-color-brightYellow: var(--bolt-terminal-brightYellow);\n  --bolt-elements-terminal-color-brightBlue: var(--bolt-terminal-brightBlue);\n  --bolt-elements-terminal-color-brightMagenta: var(--bolt-terminal-brightMagenta);\n  --bolt-elements-terminal-color-brightCyan: var(--bolt-terminal-brightCyan);\n  --bolt-elements-terminal-color-brightWhite: var(--bolt-terminal-brightWhite);\n}\n"
  },
  {
    "path": "app/styles/z-index.scss",
    "content": "$zIndexMax: 999;\n\n.z-logo {\n  z-index: $zIndexMax - 1;\n}\n\n.z-sidebar {\n  z-index: $zIndexMax - 2;\n}\n\n.z-port-dropdown {\n  z-index: $zIndexMax - 3;\n}\n\n.z-iframe-overlay {\n  z-index: $zIndexMax - 4;\n}\n\n.z-prompt {\n  z-index: 2;\n}\n\n.z-workbench {\n  z-index: 3;\n}\n\n.z-file-tree-breadcrumb {\n  z-index: $zIndexMax - 1;\n}\n\n.z-max {\n  z-index: $zIndexMax;\n}\n"
  },
  {
    "path": "app/types/actions.ts",
    "content": "export type ActionType = 'file' | 'shell';\n\nexport interface BaseAction {\n  content: string;\n}\n\nexport interface FileAction extends BaseAction {\n  type: 'file';\n  filePath: string;\n}\n\nexport interface ShellAction extends BaseAction {\n  type: 'shell';\n}\n\nexport type BoltAction = FileAction | ShellAction;\n\nexport type BoltActionData = BoltAction | BaseAction;\n"
  },
  {
    "path": "app/types/artifact.ts",
    "content": "export interface BoltArtifactData {\n  id: string;\n  title: string;\n}\n"
  },
  {
    "path": "app/types/terminal.ts",
    "content": "export interface ITerminal {\n  readonly cols?: number;\n  readonly rows?: number;\n\n  reset: () => void;\n  write: (data: string) => void;\n  onData: (cb: (data: string) => void) => void;\n}\n"
  },
  {
    "path": "app/types/theme.ts",
    "content": "export type Theme = 'dark' | 'light';\n"
  },
  {
    "path": "app/utils/buffer.ts",
    "content": "export function bufferWatchEvents<T extends unknown[]>(timeInMs: number, cb: (events: T[]) => unknown) {\n  let timeoutId: number | undefined;\n  let events: T[] = [];\n\n  // keep track of the processing of the previous batch so we can wait for it\n  let processing: Promise<unknown> = Promise.resolve();\n\n  const scheduleBufferTick = () => {\n    timeoutId = self.setTimeout(async () => {\n      // we wait until the previous batch is entirely processed so events are processed in order\n      await processing;\n\n      if (events.length > 0) {\n        processing = Promise.resolve(cb(events));\n      }\n\n      timeoutId = undefined;\n      events = [];\n    }, timeInMs);\n  };\n\n  return (...args: T) => {\n    events.push(args);\n\n    if (!timeoutId) {\n      scheduleBufferTick();\n    }\n  };\n}\n"
  },
  {
    "path": "app/utils/classNames.ts",
    "content": "/**\n * Copyright (c) 2018 Jed Watson.\n * Licensed under the MIT License (MIT), see:\n *\n * @link http://jedwatson.github.io/classnames\n */\n\ntype ClassNamesArg = undefined | string | Record<string, boolean> | ClassNamesArg[];\n\n/**\n * A simple JavaScript utility for conditionally joining classNames together.\n *\n * @param args A series of classes or object with key that are class and values\n * that are interpreted as boolean to decide whether or not the class\n * should be included in the final class.\n */\nexport function classNames(...args: ClassNamesArg[]): string {\n  let classes = '';\n\n  for (const arg of args) {\n    classes = appendClass(classes, parseValue(arg));\n  }\n\n  return classes;\n}\n\nfunction parseValue(arg: ClassNamesArg) {\n  if (typeof arg === 'string' || typeof arg === 'number') {\n    return arg;\n  }\n\n  if (typeof arg !== 'object') {\n    return '';\n  }\n\n  if (Array.isArray(arg)) {\n    return classNames(...arg);\n  }\n\n  let classes = '';\n\n  for (const key in arg) {\n    if (arg[key]) {\n      classes = appendClass(classes, key);\n    }\n  }\n\n  return classes;\n}\n\nfunction appendClass(value: string, newClass: string | undefined) {\n  if (!newClass) {\n    return value;\n  }\n\n  if (value) {\n    return value + ' ' + newClass;\n  }\n\n  return value + newClass;\n}\n"
  },
  {
    "path": "app/utils/constants.ts",
    "content": "export const WORK_DIR_NAME = 'project';\nexport const WORK_DIR = `/home/${WORK_DIR_NAME}`;\nexport const MODIFICATIONS_TAG_NAME = 'bolt_file_modifications';\n"
  },
  {
    "path": "app/utils/debounce.ts",
    "content": "export function debounce<Args extends any[]>(fn: (...args: Args) => void, delay = 100) {\n  if (delay === 0) {\n    return fn;\n  }\n\n  let timer: number | undefined;\n\n  return function <U>(this: U, ...args: Args) {\n    const context = this;\n\n    clearTimeout(timer);\n\n    timer = window.setTimeout(() => {\n      fn.apply(context, args);\n    }, delay);\n  };\n}\n"
  },
  {
    "path": "app/utils/diff.ts",
    "content": "import { createTwoFilesPatch } from 'diff';\nimport type { FileMap } from '~/lib/stores/files';\nimport { MODIFICATIONS_TAG_NAME } from './constants';\n\nexport const modificationsRegex = new RegExp(\n  `^<${MODIFICATIONS_TAG_NAME}>[\\\\s\\\\S]*?<\\\\/${MODIFICATIONS_TAG_NAME}>\\\\s+`,\n  'g',\n);\n\ninterface ModifiedFile {\n  type: 'diff' | 'file';\n  content: string;\n}\n\ntype FileModifications = Record<string, ModifiedFile>;\n\nexport function computeFileModifications(files: FileMap, modifiedFiles: Map<string, string>) {\n  const modifications: FileModifications = {};\n\n  let hasModifiedFiles = false;\n\n  for (const [filePath, originalContent] of modifiedFiles) {\n    const file = files[filePath];\n\n    if (file?.type !== 'file') {\n      continue;\n    }\n\n    const unifiedDiff = diffFiles(filePath, originalContent, file.content);\n\n    if (!unifiedDiff) {\n      // files are identical\n      continue;\n    }\n\n    hasModifiedFiles = true;\n\n    if (unifiedDiff.length > file.content.length) {\n      // if there are lots of changes we simply grab the current file content since it's smaller than the diff\n      modifications[filePath] = { type: 'file', content: file.content };\n    } else {\n      // otherwise we use the diff since it's smaller\n      modifications[filePath] = { type: 'diff', content: unifiedDiff };\n    }\n  }\n\n  if (!hasModifiedFiles) {\n    return undefined;\n  }\n\n  return modifications;\n}\n\n/**\n * Computes a diff in the unified format. The only difference is that the header is omitted\n * because it will always assume that you're comparing two versions of the same file and\n * it allows us to avoid the extra characters we send back to the llm.\n *\n * @see https://www.gnu.org/software/diffutils/manual/html_node/Unified-Format.html\n */\nexport function diffFiles(fileName: string, oldFileContent: string, newFileContent: string) {\n  let unifiedDiff = createTwoFilesPatch(fileName, fileName, oldFileContent, newFileContent);\n\n  const patchHeaderEnd = `--- ${fileName}\\n+++ ${fileName}\\n`;\n  const headerEndIndex = unifiedDiff.indexOf(patchHeaderEnd);\n\n  if (headerEndIndex >= 0) {\n    unifiedDiff = unifiedDiff.slice(headerEndIndex + patchHeaderEnd.length);\n  }\n\n  if (unifiedDiff === '') {\n    return undefined;\n  }\n\n  return unifiedDiff;\n}\n\n/**\n * Converts the unified diff to HTML.\n *\n * Example:\n *\n * ```html\n * <bolt_file_modifications>\n * <diff path=\"/home/project/index.js\">\n * - console.log('Hello, World!');\n * + console.log('Hello, Bolt!');\n * </diff>\n * </bolt_file_modifications>\n * ```\n */\nexport function fileModificationsToHTML(modifications: FileModifications) {\n  const entries = Object.entries(modifications);\n\n  if (entries.length === 0) {\n    return undefined;\n  }\n\n  const result: string[] = [`<${MODIFICATIONS_TAG_NAME}>`];\n\n  for (const [filePath, { type, content }] of entries) {\n    result.push(`<${type} path=${JSON.stringify(filePath)}>`, content, `</${type}>`);\n  }\n\n  result.push(`</${MODIFICATIONS_TAG_NAME}>`);\n\n  return result.join('\\n');\n}\n"
  },
  {
    "path": "app/utils/easings.ts",
    "content": "import { cubicBezier } from 'framer-motion';\n\nexport const cubicEasingFn = cubicBezier(0.4, 0, 0.2, 1);\n"
  },
  {
    "path": "app/utils/logger.ts",
    "content": "export type DebugLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error';\n\ntype LoggerFunction = (...messages: any[]) => void;\n\ninterface Logger {\n  trace: LoggerFunction;\n  debug: LoggerFunction;\n  info: LoggerFunction;\n  warn: LoggerFunction;\n  error: LoggerFunction;\n  setLevel: (level: DebugLevel) => void;\n}\n\nlet currentLevel: DebugLevel = import.meta.env.VITE_LOG_LEVEL ?? import.meta.env.DEV ? 'debug' : 'info';\n\nconst isWorker = 'HTMLRewriter' in globalThis;\nconst supportsColor = !isWorker;\n\nexport const logger: Logger = {\n  trace: (...messages: any[]) => log('trace', undefined, messages),\n  debug: (...messages: any[]) => log('debug', undefined, messages),\n  info: (...messages: any[]) => log('info', undefined, messages),\n  warn: (...messages: any[]) => log('warn', undefined, messages),\n  error: (...messages: any[]) => log('error', undefined, messages),\n  setLevel,\n};\n\nexport function createScopedLogger(scope: string): Logger {\n  return {\n    trace: (...messages: any[]) => log('trace', scope, messages),\n    debug: (...messages: any[]) => log('debug', scope, messages),\n    info: (...messages: any[]) => log('info', scope, messages),\n    warn: (...messages: any[]) => log('warn', scope, messages),\n    error: (...messages: any[]) => log('error', scope, messages),\n    setLevel,\n  };\n}\n\nfunction setLevel(level: DebugLevel) {\n  if ((level === 'trace' || level === 'debug') && import.meta.env.PROD) {\n    return;\n  }\n\n  currentLevel = level;\n}\n\nfunction log(level: DebugLevel, scope: string | undefined, messages: any[]) {\n  const levelOrder: DebugLevel[] = ['trace', 'debug', 'info', 'warn', 'error'];\n\n  if (levelOrder.indexOf(level) < levelOrder.indexOf(currentLevel)) {\n    return;\n  }\n\n  const allMessages = messages.reduce((acc, current) => {\n    if (acc.endsWith('\\n')) {\n      return acc + current;\n    }\n\n    if (!acc) {\n      return current;\n    }\n\n    return `${acc} ${current}`;\n  }, '');\n\n  if (!supportsColor) {\n    console.log(`[${level.toUpperCase()}]`, allMessages);\n\n    return;\n  }\n\n  const labelBackgroundColor = getColorForLevel(level);\n  const labelTextColor = level === 'warn' ? 'black' : 'white';\n\n  const labelStyles = getLabelStyles(labelBackgroundColor, labelTextColor);\n  const scopeStyles = getLabelStyles('#77828D', 'white');\n\n  const styles = [labelStyles];\n\n  if (typeof scope === 'string') {\n    styles.push('', scopeStyles);\n  }\n\n  console.log(`%c${level.toUpperCase()}${scope ? `%c %c${scope}` : ''}`, ...styles, allMessages);\n}\n\nfunction getLabelStyles(color: string, textColor: string) {\n  return `background-color: ${color}; color: white; border: 4px solid ${color}; color: ${textColor};`;\n}\n\nfunction getColorForLevel(level: DebugLevel): string {\n  switch (level) {\n    case 'trace':\n    case 'debug': {\n      return '#77828D';\n    }\n    case 'info': {\n      return '#1389FD';\n    }\n    case 'warn': {\n      return '#FFDB6C';\n    }\n    case 'error': {\n      return '#EE4744';\n    }\n    default: {\n      return 'black';\n    }\n  }\n}\n\nexport const renderLogger = createScopedLogger('Render');\n"
  },
  {
    "path": "app/utils/markdown.ts",
    "content": "import rehypeRaw from 'rehype-raw';\nimport remarkGfm from 'remark-gfm';\nimport type { PluggableList, Plugin } from 'unified';\nimport rehypeSanitize, { defaultSchema, type Options as RehypeSanitizeOptions } from 'rehype-sanitize';\nimport { SKIP, visit } from 'unist-util-visit';\nimport type { UnistNode, UnistParent } from 'node_modules/unist-util-visit/lib';\n\nexport const allowedHTMLElements = [\n  'a',\n  'b',\n  'blockquote',\n  'br',\n  'code',\n  'dd',\n  'del',\n  'details',\n  'div',\n  'dl',\n  'dt',\n  'em',\n  'h1',\n  'h2',\n  'h3',\n  'h4',\n  'h5',\n  'h6',\n  'hr',\n  'i',\n  'ins',\n  'kbd',\n  'li',\n  'ol',\n  'p',\n  'pre',\n  'q',\n  'rp',\n  'rt',\n  'ruby',\n  's',\n  'samp',\n  'source',\n  'span',\n  'strike',\n  'strong',\n  'sub',\n  'summary',\n  'sup',\n  'table',\n  'tbody',\n  'td',\n  'tfoot',\n  'th',\n  'thead',\n  'tr',\n  'ul',\n  'var',\n];\n\nconst rehypeSanitizeOptions: RehypeSanitizeOptions = {\n  ...defaultSchema,\n  tagNames: allowedHTMLElements,\n  attributes: {\n    ...defaultSchema.attributes,\n    div: [...(defaultSchema.attributes?.div ?? []), 'data*', ['className', '__boltArtifact__']],\n  },\n  strip: [],\n};\n\nexport function remarkPlugins(limitedMarkdown: boolean) {\n  const plugins: PluggableList = [remarkGfm];\n\n  if (limitedMarkdown) {\n    plugins.unshift(limitedMarkdownPlugin);\n  }\n\n  return plugins;\n}\n\nexport function rehypePlugins(html: boolean) {\n  const plugins: PluggableList = [];\n\n  if (html) {\n    plugins.push(rehypeRaw, [rehypeSanitize, rehypeSanitizeOptions]);\n  }\n\n  return plugins;\n}\n\nconst limitedMarkdownPlugin: Plugin = () => {\n  return (tree, file) => {\n    const contents = file.toString();\n\n    visit(tree, (node: UnistNode, index, parent: UnistParent) => {\n      if (\n        index == null ||\n        ['paragraph', 'text', 'inlineCode', 'code', 'strong', 'emphasis'].includes(node.type) ||\n        !node.position\n      ) {\n        return true;\n      }\n\n      let value = contents.slice(node.position.start.offset, node.position.end.offset);\n\n      if (node.type === 'heading') {\n        value = `\\n${value}`;\n      }\n\n      parent.children[index] = {\n        type: 'text',\n        value,\n      } as any;\n\n      return [SKIP, index] as const;\n    });\n  };\n};\n"
  },
  {
    "path": "app/utils/mobile.ts",
    "content": "export function isMobile() {\n  // we use sm: as the breakpoint for mobile. It's currently set to 640px\n  return globalThis.innerWidth < 640;\n}\n"
  },
  {
    "path": "app/utils/promises.ts",
    "content": "export function withResolvers<T>(): PromiseWithResolvers<T> {\n  if (typeof Promise.withResolvers === 'function') {\n    return Promise.withResolvers();\n  }\n\n  let resolve!: (value: T | PromiseLike<T>) => void;\n  let reject!: (reason?: any) => void;\n\n  const promise = new Promise<T>((_resolve, _reject) => {\n    resolve = _resolve;\n    reject = _reject;\n  });\n\n  return {\n    resolve,\n    reject,\n    promise,\n  };\n}\n"
  },
  {
    "path": "app/utils/react.ts",
    "content": "import { memo } from 'react';\n\nexport const genericMemo: <T extends keyof JSX.IntrinsicElements | React.JSXElementConstructor<any>>(\n  component: T,\n  propsAreEqual?: (prevProps: React.ComponentProps<T>, nextProps: React.ComponentProps<T>) => boolean,\n) => T & { displayName?: string } = memo;\n"
  },
  {
    "path": "app/utils/shell.ts",
    "content": "import type { WebContainer } from '@webcontainer/api';\nimport type { ITerminal } from '~/types/terminal';\nimport { withResolvers } from './promises';\n\nexport async function newShellProcess(webcontainer: WebContainer, terminal: ITerminal) {\n  const args: string[] = [];\n\n  // we spawn a JSH process with a fallback cols and rows in case the process is not attached yet to a visible terminal\n  const process = await webcontainer.spawn('/bin/jsh', ['--osc', ...args], {\n    terminal: {\n      cols: terminal.cols ?? 80,\n      rows: terminal.rows ?? 15,\n    },\n  });\n\n  const input = process.input.getWriter();\n  const output = process.output;\n\n  const jshReady = withResolvers<void>();\n\n  let isInteractive = false;\n\n  output.pipeTo(\n    new WritableStream({\n      write(data) {\n        if (!isInteractive) {\n          const [, osc] = data.match(/\\x1b\\]654;([^\\x07]+)\\x07/) || [];\n\n          if (osc === 'interactive') {\n            // wait until we see the interactive OSC\n            isInteractive = true;\n\n            jshReady.resolve();\n          }\n        }\n\n        terminal.write(data);\n      },\n    }),\n  );\n\n  terminal.onData((data) => {\n    if (isInteractive) {\n      input.write(data);\n    }\n  });\n\n  await jshReady.promise;\n\n  return process;\n}\n"
  },
  {
    "path": "app/utils/stripIndent.ts",
    "content": "export function stripIndents(value: string): string;\nexport function stripIndents(strings: TemplateStringsArray, ...values: any[]): string;\nexport function stripIndents(arg0: string | TemplateStringsArray, ...values: any[]) {\n  if (typeof arg0 !== 'string') {\n    const processedString = arg0.reduce((acc, curr, i) => {\n      acc += curr + (values[i] ?? '');\n      return acc;\n    }, '');\n\n    return _stripIndents(processedString);\n  }\n\n  return _stripIndents(arg0);\n}\n\nfunction _stripIndents(value: string) {\n  return value\n    .split('\\n')\n    .map((line) => line.trim())\n    .join('\\n')\n    .trimStart()\n    .replace(/[\\r\\n]$/, '');\n}\n"
  },
  {
    "path": "app/utils/terminal.ts",
    "content": "const reset = '\\x1b[0m';\n\nexport const escapeCodes = {\n  reset,\n  clear: '\\x1b[g',\n  red: '\\x1b[1;31m',\n};\n\nexport const coloredText = {\n  red: (text: string) => `${escapeCodes.red}${text}${reset}`,\n};\n"
  },
  {
    "path": "app/utils/unreachable.ts",
    "content": "export function unreachable(message: string): never {\n  throw new Error(`Unreachable: ${message}`);\n}\n"
  },
  {
    "path": "bindings.sh",
    "content": "#!/bin/bash\n\nbindings=\"\"\n\nwhile IFS= read -r line || [ -n \"$line\" ]; do\n  if [[ ! \"$line\" =~ ^# ]] && [[ -n \"$line\" ]]; then\n    name=$(echo \"$line\" | cut -d '=' -f 1)\n    value=$(echo \"$line\" | cut -d '=' -f 2-)\n    value=$(echo $value | sed 's/^\"\\(.*\\)\"$/\\1/')\n    bindings+=\"--binding ${name}=${value} \"\n  fi\ndone < .env.local\n\nbindings=$(echo $bindings | sed 's/[[:space:]]*$//')\n\necho $bindings\n"
  },
  {
    "path": "eslint.config.mjs",
    "content": "import blitzPlugin from '@blitz/eslint-plugin';\nimport { jsFileExtensions } from '@blitz/eslint-plugin/dist/configs/javascript.js';\nimport { getNamingConventionRule, tsFileExtensions } from '@blitz/eslint-plugin/dist/configs/typescript.js';\n\nexport default [\n  {\n    ignores: ['**/dist', '**/node_modules', '**/.wrangler', '**/bolt/build'],\n  },\n  ...blitzPlugin.configs.recommended(),\n  {\n    rules: {\n      '@blitz/catch-error-name': 'off',\n      '@typescript-eslint/no-this-alias': 'off',\n      '@typescript-eslint/no-empty-object-type': 'off',\n    },\n  },\n  {\n    files: ['**/*.tsx'],\n    rules: {\n      ...getNamingConventionRule({}, true),\n    },\n  },\n  {\n    files: ['**/*.d.ts'],\n    rules: {\n      '@typescript-eslint/no-empty-object-type': 'off',\n    },\n  },\n  {\n    files: [...tsFileExtensions, ...jsFileExtensions, '**/*.tsx'],\n    ignores: ['functions/*'],\n    rules: {\n      'no-restricted-imports': [\n        'error',\n        {\n          patterns: [\n            {\n              group: ['../'],\n              message: `Relative imports are not allowed. Please use '~/' instead.`,\n            },\n          ],\n        },\n      ],\n    },\n  },\n];\n"
  },
  {
    "path": "functions/[[path]].ts",
    "content": "import type { ServerBuild } from '@remix-run/cloudflare';\nimport { createPagesFunctionHandler } from '@remix-run/cloudflare-pages';\n\n// @ts-ignore because the server build file is generated by `remix vite:build`\nimport * as serverBuild from '../build/server';\n\nexport const onRequest = createPagesFunctionHandler({\n  build: serverBuild as unknown as ServerBuild,\n});\n"
  },
  {
    "path": "load-context.ts",
    "content": "import { type PlatformProxy } from 'wrangler';\n\ntype Cloudflare = Omit<PlatformProxy<Env>, 'dispose'>;\n\ndeclare module '@remix-run/cloudflare' {\n  interface AppLoadContext {\n    cloudflare: Cloudflare;\n  }\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"bolt\",\n  \"description\": \"StackBlitz AI Agent\",\n  \"private\": true,\n  \"license\": \"MIT\",\n  \"packageManager\": \"pnpm@9.4.0\",\n  \"sideEffects\": false,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"deploy\": \"npm run build && wrangler pages deploy\",\n    \"build\": \"remix vite:build\",\n    \"dev\": \"remix vite:dev\",\n    \"test\": \"vitest --run\",\n    \"test:watch\": \"vitest\",\n    \"lint\": \"eslint --cache --cache-location ./node_modules/.cache/eslint .\",\n    \"lint:fix\": \"npm run lint -- --fix\",\n    \"start\": \"bindings=$(./bindings.sh) && wrangler pages dev ./build/client $bindings\",\n    \"typecheck\": \"tsc\",\n    \"typegen\": \"wrangler types\",\n    \"preview\": \"pnpm run build && pnpm run start\"\n  },\n  \"engines\": {\n    \"node\": \">=18.18.0\"\n  },\n  \"dependencies\": {\n    \"@ai-sdk/anthropic\": \"^0.0.39\",\n    \"@codemirror/autocomplete\": \"^6.17.0\",\n    \"@codemirror/commands\": \"^6.6.0\",\n    \"@codemirror/lang-cpp\": \"^6.0.2\",\n    \"@codemirror/lang-css\": \"^6.2.1\",\n    \"@codemirror/lang-html\": \"^6.4.9\",\n    \"@codemirror/lang-javascript\": \"^6.2.2\",\n    \"@codemirror/lang-json\": \"^6.0.1\",\n    \"@codemirror/lang-markdown\": \"^6.2.5\",\n    \"@codemirror/lang-python\": \"^6.1.6\",\n    \"@codemirror/lang-sass\": \"^6.0.2\",\n    \"@codemirror/lang-wast\": \"^6.0.2\",\n    \"@codemirror/language\": \"^6.10.2\",\n    \"@codemirror/search\": \"^6.5.6\",\n    \"@codemirror/state\": \"^6.4.1\",\n    \"@codemirror/view\": \"^6.28.4\",\n    \"@iconify-json/ph\": \"^1.1.13\",\n    \"@iconify-json/svg-spinners\": \"^1.1.2\",\n    \"@lezer/highlight\": \"^1.2.0\",\n    \"@nanostores/react\": \"^0.7.2\",\n    \"@radix-ui/react-dialog\": \"^1.1.1\",\n    \"@radix-ui/react-dropdown-menu\": \"^2.1.1\",\n    \"@remix-run/cloudflare\": \"^2.10.2\",\n    \"@remix-run/cloudflare-pages\": \"^2.10.2\",\n    \"@remix-run/react\": \"^2.10.2\",\n    \"@uiw/codemirror-theme-vscode\": \"^4.23.0\",\n    \"@unocss/reset\": \"^0.61.0\",\n    \"@webcontainer/api\": \"1.3.0-internal.10\",\n    \"@xterm/addon-fit\": \"^0.10.0\",\n    \"@xterm/addon-web-links\": \"^0.11.0\",\n    \"@xterm/xterm\": \"^5.5.0\",\n    \"ai\": \"^3.3.4\",\n    \"date-fns\": \"^3.6.0\",\n    \"diff\": \"^5.2.0\",\n    \"framer-motion\": \"^11.2.12\",\n    \"isbot\": \"^4.1.0\",\n    \"istextorbinary\": \"^9.5.0\",\n    \"jose\": \"^5.6.3\",\n    \"nanostores\": \"^0.10.3\",\n    \"react\": \"^18.2.0\",\n    \"react-dom\": \"^18.2.0\",\n    \"react-hotkeys-hook\": \"^4.5.0\",\n    \"react-markdown\": \"^9.0.1\",\n    \"react-resizable-panels\": \"^2.0.20\",\n    \"react-toastify\": \"^10.0.5\",\n    \"rehype-raw\": \"^7.0.0\",\n    \"rehype-sanitize\": \"^6.0.0\",\n    \"remark-gfm\": \"^4.0.0\",\n    \"remix-island\": \"^0.2.0\",\n    \"remix-utils\": \"^7.6.0\",\n    \"shiki\": \"^1.9.1\",\n    \"unist-util-visit\": \"^5.0.0\"\n  },\n  \"devDependencies\": {\n    \"@blitz/eslint-plugin\": \"0.1.0\",\n    \"@cloudflare/workers-types\": \"^4.20240620.0\",\n    \"@remix-run/dev\": \"^2.10.0\",\n    \"@types/diff\": \"^5.2.1\",\n    \"@types/react\": \"^18.2.20\",\n    \"@types/react-dom\": \"^18.2.7\",\n    \"fast-glob\": \"^3.3.2\",\n    \"is-ci\": \"^3.0.1\",\n    \"node-fetch\": \"^3.3.2\",\n    \"prettier\": \"^3.3.2\",\n    \"typescript\": \"^5.5.2\",\n    \"unified\": \"^11.0.5\",\n    \"unocss\": \"^0.61.3\",\n    \"vite\": \"^5.3.1\",\n    \"vite-plugin-node-polyfills\": \"^0.22.0\",\n    \"vite-plugin-optimize-css-modules\": \"^1.1.0\",\n    \"vite-tsconfig-paths\": \"^4.3.2\",\n    \"vitest\": \"^2.0.1\",\n    \"wrangler\": \"^3.63.2\",\n    \"zod\": \"^3.23.8\"\n  },\n  \"resolutions\": {\n    \"@typescript-eslint/utils\": \"^8.0.0-alpha.30\"\n  }\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ESNext\"],\n    \"types\": [\"@remix-run/cloudflare\", \"vite/client\", \"@cloudflare/workers-types/2023-07-01\"],\n    \"isolatedModules\": true,\n    \"esModuleInterop\": true,\n    \"jsx\": \"react-jsx\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Bundler\",\n    \"resolveJsonModule\": true,\n    \"target\": \"ESNext\",\n    \"strict\": true,\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"verbatimModuleSyntax\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"~/*\": [\"./app/*\"]\n    },\n\n    // vite takes care of building everything, not tsc\n    \"noEmit\": true\n  },\n  \"include\": [\n    \"**/*.ts\",\n    \"**/*.tsx\",\n    \"**/.server/**/*.ts\",\n    \"**/.server/**/*.tsx\",\n    \"**/.client/**/*.ts\",\n    \"**/.client/**/*.tsx\"\n  ]\n}\n"
  },
  {
    "path": "types/istextorbinary.d.ts",
    "content": "/**\n * @note For some reason the types aren't picked up from node_modules so I declared the module here\n * with only the function that we use.\n */\ndeclare module 'istextorbinary' {\n  export interface EncodingOpts {\n    /** Defaults to 24 */\n    chunkLength?: number;\n\n    /** If not provided, will check the start, beginning, and end */\n    chunkBegin?: number;\n  }\n\n  export function getEncoding(buffer: Buffer | null, opts?: EncodingOpts): 'utf8' | 'binary' | null;\n}\n"
  },
  {
    "path": "uno.config.ts",
    "content": "import { globSync } from 'fast-glob';\nimport fs from 'node:fs/promises';\nimport { basename } from 'node:path';\nimport { defineConfig, presetIcons, presetUno, transformerDirectives } from 'unocss';\n\nconst iconPaths = globSync('./icons/*.svg');\n\nconst collectionName = 'bolt';\n\nconst customIconCollection = iconPaths.reduce(\n  (acc, iconPath) => {\n    const [iconName] = basename(iconPath).split('.');\n\n    acc[collectionName] ??= {};\n    acc[collectionName][iconName] = async () => fs.readFile(iconPath, 'utf8');\n\n    return acc;\n  },\n  {} as Record<string, Record<string, () => Promise<string>>>,\n);\n\nconst BASE_COLORS = {\n  white: '#FFFFFF',\n  gray: {\n    50: '#FAFAFA',\n    100: '#F5F5F5',\n    200: '#E5E5E5',\n    300: '#D4D4D4',\n    400: '#A3A3A3',\n    500: '#737373',\n    600: '#525252',\n    700: '#404040',\n    800: '#262626',\n    900: '#171717',\n    950: '#0A0A0A',\n  },\n  accent: {\n    50: '#EEF9FF',\n    100: '#D8F1FF',\n    200: '#BAE7FF',\n    300: '#8ADAFF',\n    400: '#53C4FF',\n    500: '#2BA6FF',\n    600: '#1488FC',\n    700: '#0D6FE8',\n    800: '#1259BB',\n    900: '#154E93',\n    950: '#122F59',\n  },\n  green: {\n    50: '#F0FDF4',\n    100: '#DCFCE7',\n    200: '#BBF7D0',\n    300: '#86EFAC',\n    400: '#4ADE80',\n    500: '#22C55E',\n    600: '#16A34A',\n    700: '#15803D',\n    800: '#166534',\n    900: '#14532D',\n    950: '#052E16',\n  },\n  orange: {\n    50: '#FFFAEB',\n    100: '#FEEFC7',\n    200: '#FEDF89',\n    300: '#FEC84B',\n    400: '#FDB022',\n    500: '#F79009',\n    600: '#DC6803',\n    700: '#B54708',\n    800: '#93370D',\n    900: '#792E0D',\n  },\n  red: {\n    50: '#FEF2F2',\n    100: '#FEE2E2',\n    200: '#FECACA',\n    300: '#FCA5A5',\n    400: '#F87171',\n    500: '#EF4444',\n    600: '#DC2626',\n    700: '#B91C1C',\n    800: '#991B1B',\n    900: '#7F1D1D',\n    950: '#450A0A',\n  },\n};\n\nconst COLOR_PRIMITIVES = {\n  ...BASE_COLORS,\n  alpha: {\n    white: generateAlphaPalette(BASE_COLORS.white),\n    gray: generateAlphaPalette(BASE_COLORS.gray[900]),\n    red: generateAlphaPalette(BASE_COLORS.red[500]),\n    accent: generateAlphaPalette(BASE_COLORS.accent[500]),\n  },\n};\n\nexport default defineConfig({\n  shortcuts: {\n    'bolt-ease-cubic-bezier': 'ease-[cubic-bezier(0.4,0,0.2,1)]',\n    'transition-theme': 'transition-[background-color,border-color,color] duration-150 bolt-ease-cubic-bezier',\n    kdb: 'bg-bolt-elements-code-background text-bolt-elements-code-text py-1 px-1.5 rounded-md',\n    'max-w-chat': 'max-w-[var(--chat-max-width)]',\n  },\n  rules: [\n    /**\n     * This shorthand doesn't exist in Tailwind and we overwrite it to avoid\n     * any conflicts with minified CSS classes.\n     */\n    ['b', {}],\n  ],\n  theme: {\n    colors: {\n      ...COLOR_PRIMITIVES,\n      bolt: {\n        elements: {\n          borderColor: 'var(--bolt-elements-borderColor)',\n          borderColorActive: 'var(--bolt-elements-borderColorActive)',\n          background: {\n            depth: {\n              1: 'var(--bolt-elements-bg-depth-1)',\n              2: 'var(--bolt-elements-bg-depth-2)',\n              3: 'var(--bolt-elements-bg-depth-3)',\n              4: 'var(--bolt-elements-bg-depth-4)',\n            },\n          },\n          textPrimary: 'var(--bolt-elements-textPrimary)',\n          textSecondary: 'var(--bolt-elements-textSecondary)',\n          textTertiary: 'var(--bolt-elements-textTertiary)',\n          code: {\n            background: 'var(--bolt-elements-code-background)',\n            text: 'var(--bolt-elements-code-text)',\n          },\n          button: {\n            primary: {\n              background: 'var(--bolt-elements-button-primary-background)',\n              backgroundHover: 'var(--bolt-elements-button-primary-backgroundHover)',\n              text: 'var(--bolt-elements-button-primary-text)',\n            },\n            secondary: {\n              background: 'var(--bolt-elements-button-secondary-background)',\n              backgroundHover: 'var(--bolt-elements-button-secondary-backgroundHover)',\n              text: 'var(--bolt-elements-button-secondary-text)',\n            },\n            danger: {\n              background: 'var(--bolt-elements-button-danger-background)',\n              backgroundHover: 'var(--bolt-elements-button-danger-backgroundHover)',\n              text: 'var(--bolt-elements-button-danger-text)',\n            },\n          },\n          item: {\n            contentDefault: 'var(--bolt-elements-item-contentDefault)',\n            contentActive: 'var(--bolt-elements-item-contentActive)',\n            contentAccent: 'var(--bolt-elements-item-contentAccent)',\n            contentDanger: 'var(--bolt-elements-item-contentDanger)',\n            backgroundDefault: 'var(--bolt-elements-item-backgroundDefault)',\n            backgroundActive: 'var(--bolt-elements-item-backgroundActive)',\n            backgroundAccent: 'var(--bolt-elements-item-backgroundAccent)',\n            backgroundDanger: 'var(--bolt-elements-item-backgroundDanger)',\n          },\n          actions: {\n            background: 'var(--bolt-elements-actions-background)',\n            code: {\n              background: 'var(--bolt-elements-actions-code-background)',\n            },\n          },\n          artifacts: {\n            background: 'var(--bolt-elements-artifacts-background)',\n            backgroundHover: 'var(--bolt-elements-artifacts-backgroundHover)',\n            borderColor: 'var(--bolt-elements-artifacts-borderColor)',\n            inlineCode: {\n              background: 'var(--bolt-elements-artifacts-inlineCode-background)',\n              text: 'var(--bolt-elements-artifacts-inlineCode-text)',\n            },\n          },\n          messages: {\n            background: 'var(--bolt-elements-messages-background)',\n            linkColor: 'var(--bolt-elements-messages-linkColor)',\n            code: {\n              background: 'var(--bolt-elements-messages-code-background)',\n            },\n            inlineCode: {\n              background: 'var(--bolt-elements-messages-inlineCode-background)',\n              text: 'var(--bolt-elements-messages-inlineCode-text)',\n            },\n          },\n          icon: {\n            success: 'var(--bolt-elements-icon-success)',\n            error: 'var(--bolt-elements-icon-error)',\n            primary: 'var(--bolt-elements-icon-primary)',\n            secondary: 'var(--bolt-elements-icon-secondary)',\n            tertiary: 'var(--bolt-elements-icon-tertiary)',\n          },\n          preview: {\n            addressBar: {\n              background: 'var(--bolt-elements-preview-addressBar-background)',\n              backgroundHover: 'var(--bolt-elements-preview-addressBar-backgroundHover)',\n              backgroundActive: 'var(--bolt-elements-preview-addressBar-backgroundActive)',\n              text: 'var(--bolt-elements-preview-addressBar-text)',\n              textActive: 'var(--bolt-elements-preview-addressBar-textActive)',\n            },\n          },\n          terminals: {\n            background: 'var(--bolt-elements-terminals-background)',\n            buttonBackground: 'var(--bolt-elements-terminals-buttonBackground)',\n          },\n          dividerColor: 'var(--bolt-elements-dividerColor)',\n          loader: {\n            background: 'var(--bolt-elements-loader-background)',\n            progress: 'var(--bolt-elements-loader-progress)',\n          },\n          prompt: {\n            background: 'var(--bolt-elements-prompt-background)',\n          },\n          sidebar: {\n            dropdownShadow: 'var(--bolt-elements-sidebar-dropdownShadow)',\n            buttonBackgroundDefault: 'var(--bolt-elements-sidebar-buttonBackgroundDefault)',\n            buttonBackgroundHover: 'var(--bolt-elements-sidebar-buttonBackgroundHover)',\n            buttonText: 'var(--bolt-elements-sidebar-buttonText)',\n          },\n          cta: {\n            background: 'var(--bolt-elements-cta-background)',\n            text: 'var(--bolt-elements-cta-text)',\n          },\n        },\n      },\n    },\n  },\n  transformers: [transformerDirectives()],\n  presets: [\n    presetUno({\n      dark: {\n        light: '[data-theme=\"light\"]',\n        dark: '[data-theme=\"dark\"]',\n      },\n    }),\n    presetIcons({\n      warn: true,\n      collections: {\n        ...customIconCollection,\n      },\n    }),\n  ],\n});\n\n/**\n * Generates an alpha palette for a given hex color.\n *\n * @param hex - The hex color code (without alpha) to generate the palette from.\n * @returns An object where keys are opacity percentages and values are hex colors with alpha.\n *\n * Example:\n *\n * ```\n * {\n *   '1': '#FFFFFF03',\n *   '2': '#FFFFFF05',\n *   '3': '#FFFFFF08',\n * }\n * ```\n */\nfunction generateAlphaPalette(hex: string) {\n  return [1, 2, 3, 4, 5, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100].reduce(\n    (acc, opacity) => {\n      const alpha = Math.round((opacity / 100) * 255)\n        .toString(16)\n        .padStart(2, '0');\n\n      acc[opacity] = `${hex}${alpha}`;\n\n      return acc;\n    },\n    {} as Record<number, string>,\n  );\n}\n"
  },
  {
    "path": "vite.config.ts",
    "content": "import { cloudflareDevProxyVitePlugin as remixCloudflareDevProxy, vitePlugin as remixVitePlugin } from '@remix-run/dev';\nimport UnoCSS from 'unocss/vite';\nimport { defineConfig, type ViteDevServer } from 'vite';\nimport { nodePolyfills } from 'vite-plugin-node-polyfills';\nimport { optimizeCssModules } from 'vite-plugin-optimize-css-modules';\nimport tsconfigPaths from 'vite-tsconfig-paths';\n\nexport default defineConfig((config) => {\n  return {\n    build: {\n      target: 'esnext',\n    },\n    plugins: [\n      nodePolyfills({\n        include: ['path', 'buffer'],\n      }),\n      config.mode !== 'test' && remixCloudflareDevProxy(),\n      remixVitePlugin({\n        future: {\n          v3_fetcherPersist: true,\n          v3_relativeSplatPath: true,\n          v3_throwAbortReason: true,\n        },\n      }),\n      UnoCSS(),\n      tsconfigPaths(),\n      chrome129IssuePlugin(),\n      config.mode === 'production' && optimizeCssModules({ apply: 'build' }),\n    ],\n  };\n});\n\nfunction chrome129IssuePlugin() {\n  return {\n    name: 'chrome129IssuePlugin',\n    configureServer(server: ViteDevServer) {\n      server.middlewares.use((req, res, next) => {\n        const raw = req.headers['user-agent']?.match(/Chrom(e|ium)\\/([0-9]+)\\./);\n\n        if (raw) {\n          const version = parseInt(raw[2], 10);\n\n          if (version === 129) {\n            res.setHeader('content-type', 'text/html');\n            res.end(\n              '<body><h1>Please use Chrome Canary for testing.</h1><p>Chrome 129 has an issue with JavaScript modules & Vite local development, see <a href=\"https://github.com/stackblitz/bolt.new/issues/86#issuecomment-2395519258\">for more information.</a></p><p><b>Note:</b> This only impacts <u>local development</u>. `pnpm run build` and `pnpm run start` will work fine in this browser.</p></body>',\n            );\n\n            return;\n          }\n        }\n\n        next();\n      });\n    },\n  };\n}\n"
  },
  {
    "path": "worker-configuration.d.ts",
    "content": "interface Env {\n  ANTHROPIC_API_KEY: string;\n}\n"
  },
  {
    "path": "wrangler.toml",
    "content": "#:schema node_modules/wrangler/config-schema.json\nname = \"bolt\"\ncompatibility_flags = [\"nodejs_compat\"]\ncompatibility_date = \"2024-07-01\"\npages_build_output_dir = \"./build/client\"\n"
  }
]