Repository: bytebot-ai/bytebot
Branch: main
Commit: 3d37894ce07e
Files: 316
Total size: 857.6 KB
Directory structure:
gitextract_hhcv8dmu/
├── .github/
│ └── workflows/
│ ├── build-agent.yaml
│ ├── build-desktop.yaml
│ └── build-ui.yaml
├── .gitignore
├── .prettierignore
├── LICENSE
├── README.md
├── docker/
│ ├── bytebot-desktop.Dockerfile
│ ├── docker-compose-claude-code.yml
│ ├── docker-compose.core.yml
│ ├── docker-compose.development.yml
│ ├── docker-compose.proxy.yml
│ └── docker-compose.yml
├── docs/
│ ├── api-reference/
│ │ ├── agent/
│ │ │ ├── tasks.mdx
│ │ │ └── ui.mdx
│ │ ├── computer-use/
│ │ │ ├── examples.mdx
│ │ │ ├── openapi.json
│ │ │ └── unified-endpoint.mdx
│ │ ├── endpoint/
│ │ │ ├── create.mdx
│ │ │ ├── delete.mdx
│ │ │ ├── get.mdx
│ │ │ └── webhook.mdx
│ │ ├── introduction.mdx
│ │ └── openapi.json
│ ├── core-concepts/
│ │ ├── agent-system.mdx
│ │ ├── architecture.mdx
│ │ ├── desktop-environment.mdx
│ │ └── rpa-comparison.mdx
│ ├── deployment/
│ │ ├── helm.mdx
│ │ ├── litellm.mdx
│ │ └── railway.mdx
│ ├── docs.json
│ ├── guides/
│ │ ├── password-management.mdx
│ │ ├── takeover-mode.mdx
│ │ └── task-creation.mdx
│ ├── introduction.mdx
│ ├── quickstart.mdx
│ └── rest-api/
│ ├── computer-use.mdx
│ ├── examples.mdx
│ ├── input-tracking.mdx
│ └── introduction.mdx
├── helm/
│ ├── Chart.yaml
│ ├── README.md
│ ├── charts/
│ │ ├── bytebot-agent/
│ │ │ ├── Chart.yaml
│ │ │ ├── templates/
│ │ │ │ ├── _helpers.tpl
│ │ │ │ ├── deployment.yaml
│ │ │ │ ├── ingress.yaml
│ │ │ │ ├── secret.yaml
│ │ │ │ └── service.yaml
│ │ │ └── values.yaml
│ │ ├── bytebot-desktop/
│ │ │ ├── Chart.yaml
│ │ │ ├── templates/
│ │ │ │ ├── _helpers.tpl
│ │ │ │ ├── deployment.yaml
│ │ │ │ ├── ingress.yaml
│ │ │ │ ├── pvc.yaml
│ │ │ │ └── service.yaml
│ │ │ └── values.yaml
│ │ ├── bytebot-llm-proxy/
│ │ │ ├── Chart.yaml
│ │ │ ├── templates/
│ │ │ │ ├── _helpers.tpl
│ │ │ │ ├── configmap.yaml
│ │ │ │ ├── deployment.yaml
│ │ │ │ ├── ingress.yaml
│ │ │ │ ├── secret.yaml
│ │ │ │ └── service.yaml
│ │ │ └── values.yaml
│ │ ├── bytebot-ui/
│ │ │ ├── Chart.yaml
│ │ │ ├── templates/
│ │ │ │ ├── _helpers.tpl
│ │ │ │ ├── deployment.yaml
│ │ │ │ ├── hpa.yaml
│ │ │ │ ├── ingress.yaml
│ │ │ │ └── service.yaml
│ │ │ └── values.yaml
│ │ └── postgresql/
│ │ ├── Chart.yaml
│ │ ├── templates/
│ │ │ ├── _helpers.tpl
│ │ │ ├── deployment.yaml
│ │ │ ├── secret.yaml
│ │ │ └── service.yaml
│ │ └── values.yaml
│ ├── templates/
│ │ ├── NOTES.txt
│ │ └── ingress.yaml
│ ├── values-proxy.yaml
│ ├── values-simple.yaml
│ └── values.yaml
└── packages/
├── bytebot-agent/
│ ├── .dockerignore
│ ├── .gitignore
│ ├── .prettierrc
│ ├── Dockerfile
│ ├── eslint.config.mjs
│ ├── nest-cli.json
│ ├── package.json
│ ├── prisma/
│ │ ├── migrations/
│ │ │ ├── 20250328022708_initial_migration/
│ │ │ │ └── migration.sql
│ │ │ ├── 20250413053912_message_role/
│ │ │ │ └── migration.sql
│ │ │ ├── 20250522200556_updated_task_structure/
│ │ │ │ └── migration.sql
│ │ │ ├── 20250523162632_add_scheduling/
│ │ │ │ └── migration.sql
│ │ │ ├── 20250529003255_tasks_control/
│ │ │ │ └── migration.sql
│ │ │ ├── 20250530012753_tasks_control/
│ │ │ │ └── migration.sql
│ │ │ ├── 20250619013027_add_better_auth_schema/
│ │ │ │ └── migration.sql
│ │ │ ├── 20250622195148_add_user_to_task/
│ │ │ │ └── migration.sql
│ │ │ ├── 20250706223912_model_picker/
│ │ │ │ └── migration.sql
│ │ │ ├── 20250722041608_files/
│ │ │ │ └── migration.sql
│ │ │ ├── 20250820172813_remove_auth/
│ │ │ │ └── migration.sql
│ │ │ └── migration_lock.toml
│ │ └── schema.prisma
│ ├── src/
│ │ ├── agent/
│ │ │ ├── agent.analytics.ts
│ │ │ ├── agent.computer-use.ts
│ │ │ ├── agent.constants.ts
│ │ │ ├── agent.module.ts
│ │ │ ├── agent.processor.ts
│ │ │ ├── agent.scheduler.ts
│ │ │ ├── agent.tools.ts
│ │ │ ├── agent.types.ts
│ │ │ └── input-capture.service.ts
│ │ ├── anthropic/
│ │ │ ├── anthropic.constants.ts
│ │ │ ├── anthropic.module.ts
│ │ │ ├── anthropic.service.ts
│ │ │ └── anthropic.tools.ts
│ │ ├── app.controller.ts
│ │ ├── app.module.ts
│ │ ├── app.service.ts
│ │ ├── google/
│ │ │ ├── google.constants.ts
│ │ │ ├── google.module.ts
│ │ │ ├── google.service.ts
│ │ │ └── google.tools.ts
│ │ ├── main.ts
│ │ ├── messages/
│ │ │ ├── messages.module.ts
│ │ │ └── messages.service.ts
│ │ ├── openai/
│ │ │ ├── openai.constants.ts
│ │ │ ├── openai.module.ts
│ │ │ ├── openai.service.ts
│ │ │ └── openai.tools.ts
│ │ ├── prisma/
│ │ │ ├── prisma.module.ts
│ │ │ └── prisma.service.ts
│ │ ├── proxy/
│ │ │ ├── proxy.module.ts
│ │ │ ├── proxy.service.ts
│ │ │ └── proxy.tools.ts
│ │ ├── summaries/
│ │ │ ├── summaries.modue.ts
│ │ │ └── summaries.service.ts
│ │ └── tasks/
│ │ ├── dto/
│ │ │ ├── add-task-message.dto.ts
│ │ │ ├── create-task.dto.ts
│ │ │ └── update-task.dto.ts
│ │ ├── tasks.controller.ts
│ │ ├── tasks.gateway.ts
│ │ ├── tasks.module.ts
│ │ └── tasks.service.ts
│ ├── tsconfig.build.json
│ └── tsconfig.json
├── bytebot-agent-cc/
│ ├── .dockerignore
│ ├── .gitignore
│ ├── .prettierrc
│ ├── Dockerfile
│ ├── eslint.config.mjs
│ ├── nest-cli.json
│ ├── package.json
│ ├── prisma/
│ │ ├── migrations/
│ │ │ ├── 20250328022708_initial_migration/
│ │ │ │ └── migration.sql
│ │ │ ├── 20250413053912_message_role/
│ │ │ │ └── migration.sql
│ │ │ ├── 20250522200556_updated_task_structure/
│ │ │ │ └── migration.sql
│ │ │ ├── 20250523162632_add_scheduling/
│ │ │ │ └── migration.sql
│ │ │ ├── 20250529003255_tasks_control/
│ │ │ │ └── migration.sql
│ │ │ ├── 20250530012753_tasks_control/
│ │ │ │ └── migration.sql
│ │ │ ├── 20250619013027_add_better_auth_schema/
│ │ │ │ └── migration.sql
│ │ │ ├── 20250622195148_add_user_to_task/
│ │ │ │ └── migration.sql
│ │ │ ├── 20250706223912_model_picker/
│ │ │ │ └── migration.sql
│ │ │ ├── 20250722041608_files/
│ │ │ │ └── migration.sql
│ │ │ ├── 20250820172813_remove_auth/
│ │ │ │ └── migration.sql
│ │ │ └── migration_lock.toml
│ │ └── schema.prisma
│ ├── src/
│ │ ├── agent/
│ │ │ ├── agent.analytics.ts
│ │ │ ├── agent.computer-use.ts
│ │ │ ├── agent.constants.ts
│ │ │ ├── agent.module.ts
│ │ │ ├── agent.processor.ts
│ │ │ ├── agent.scheduler.ts
│ │ │ ├── agent.tools.ts
│ │ │ ├── agent.types.ts
│ │ │ └── input-capture.service.ts
│ │ ├── app.controller.ts
│ │ ├── app.module.ts
│ │ ├── app.service.ts
│ │ ├── main.ts
│ │ ├── messages/
│ │ │ ├── messages.module.ts
│ │ │ └── messages.service.ts
│ │ ├── prisma/
│ │ │ ├── prisma.module.ts
│ │ │ └── prisma.service.ts
│ │ └── tasks/
│ │ ├── dto/
│ │ │ ├── add-task-message.dto.ts
│ │ │ ├── create-task.dto.ts
│ │ │ └── update-task.dto.ts
│ │ ├── tasks.controller.ts
│ │ ├── tasks.gateway.ts
│ │ ├── tasks.module.ts
│ │ └── tasks.service.ts
│ ├── tsconfig.build.json
│ └── tsconfig.json
├── bytebot-llm-proxy/
│ ├── Dockerfile
│ └── litellm-config.yaml
├── bytebot-ui/
│ ├── .dockerignore
│ ├── .gitignore
│ ├── .prettierrc.json
│ ├── Dockerfile
│ ├── components.json
│ ├── eslint.config.mjs
│ ├── next.config.ts
│ ├── package.json
│ ├── postcss.config.mjs
│ ├── server.ts
│ ├── src/
│ │ ├── app/
│ │ │ ├── api/
│ │ │ │ └── [[...path]]/
│ │ │ │ └── route.ts
│ │ │ ├── desktop/
│ │ │ │ └── page.tsx
│ │ │ ├── globals.css
│ │ │ ├── layout.tsx
│ │ │ ├── page.tsx
│ │ │ └── tasks/
│ │ │ ├── [id]/
│ │ │ │ └── page.tsx
│ │ │ └── page.tsx
│ │ ├── components/
│ │ │ ├── VirtualDesktopStatusHeader.tsx
│ │ │ ├── layout/
│ │ │ │ └── Header.tsx
│ │ │ ├── messages/
│ │ │ │ ├── AssistantMessage.tsx
│ │ │ │ ├── ChatContainer.tsx
│ │ │ │ ├── ChatInput.tsx
│ │ │ │ ├── MessageAvatar.tsx
│ │ │ │ ├── MessageGroup.tsx
│ │ │ │ ├── UserMessage.tsx
│ │ │ │ └── content/
│ │ │ │ ├── ComputerToolContent.tsx
│ │ │ │ ├── ComputerToolContentNormal.tsx
│ │ │ │ ├── ComputerToolContentTakeOver.tsx
│ │ │ │ ├── ComputerToolUtils.tsx
│ │ │ │ ├── ErrorContent.tsx
│ │ │ │ ├── ImageContent.tsx
│ │ │ │ ├── MessageContent.tsx
│ │ │ │ └── TextContent.tsx
│ │ │ ├── screenshot/
│ │ │ │ └── ScreenshotViewer.tsx
│ │ │ ├── tasks/
│ │ │ │ ├── TaskItem.tsx
│ │ │ │ ├── TaskList.tsx
│ │ │ │ └── TaskTabs.tsx
│ │ │ ├── ui/
│ │ │ │ ├── TopicPopover.tsx
│ │ │ │ ├── button.tsx
│ │ │ │ ├── card.tsx
│ │ │ │ ├── copy-button.tsx
│ │ │ │ ├── desktop-container.tsx
│ │ │ │ ├── dropdown-menu.tsx
│ │ │ │ ├── input.tsx
│ │ │ │ ├── label.tsx
│ │ │ │ ├── loader.tsx
│ │ │ │ ├── pagination.tsx
│ │ │ │ ├── popover.tsx
│ │ │ │ ├── scroll-area.tsx
│ │ │ │ ├── select.tsx
│ │ │ │ ├── separator.tsx
│ │ │ │ ├── switch.tsx
│ │ │ │ └── text-shimmer.tsx
│ │ │ └── vnc/
│ │ │ └── VncViewer.tsx
│ │ ├── constants/
│ │ │ └── ui.constants.ts
│ │ ├── hooks/
│ │ │ ├── useChatSession.ts
│ │ │ ├── useScrollScreenshot.ts
│ │ │ └── useWebSocket.ts
│ │ ├── lib/
│ │ │ └── utils.ts
│ │ ├── types/
│ │ │ └── index.ts
│ │ └── utils/
│ │ ├── clipboard.ts
│ │ ├── screenshotUtils.ts
│ │ ├── stringUtils.ts
│ │ └── taskUtils.ts
│ └── tsconfig.json
├── bytebotd/
│ ├── .dockerignore
│ ├── .prettierrc
│ ├── Dockerfile
│ ├── eslint.config.mjs
│ ├── nest-cli.json
│ ├── package.json
│ ├── root/
│ │ ├── etc/
│ │ │ ├── firefox/
│ │ │ │ └── policies/
│ │ │ │ └── policies.json
│ │ │ ├── lightdm/
│ │ │ │ └── lightdm.conf.d/
│ │ │ │ └── 50-autologin.conf
│ │ │ ├── supervisor/
│ │ │ │ └── conf.d/
│ │ │ │ └── supervisord.conf
│ │ │ └── thunderbird/
│ │ │ └── policies/
│ │ │ └── policies.json
│ │ ├── home/
│ │ │ └── user/
│ │ │ ├── .config/
│ │ │ │ └── xfce4/
│ │ │ │ ├── desktop/
│ │ │ │ │ └── icons.screen0-1264x913.rc
│ │ │ │ ├── helpers.rc
│ │ │ │ ├── terminal/
│ │ │ │ │ └── accels.scm
│ │ │ │ └── xfconf/
│ │ │ │ └── xfce-perchannel-xml/
│ │ │ │ ├── displays.xml
│ │ │ │ ├── thunar.xml
│ │ │ │ ├── xfce4-appfinder.xml
│ │ │ │ ├── xfce4-desktop.xml
│ │ │ │ ├── xfce4-keyboard-shortcuts.xml
│ │ │ │ ├── xfce4-notifyd.xml
│ │ │ │ ├── xfce4-panel.xml
│ │ │ │ └── xfwm4.xml
│ │ │ └── .xsessionrc
│ │ └── usr/
│ │ └── share/
│ │ └── applications/
│ │ ├── 1password.desktop
│ │ ├── code.desktop
│ │ ├── firefox.desktop
│ │ ├── terminal.desktop
│ │ └── thunderbird.desktop
│ ├── src/
│ │ ├── app.controller.ts
│ │ ├── app.module.ts
│ │ ├── app.service.ts
│ │ ├── computer-use/
│ │ │ ├── computer-use.controller.ts
│ │ │ ├── computer-use.module.ts
│ │ │ ├── computer-use.service.ts
│ │ │ └── dto/
│ │ │ ├── base.dto.ts
│ │ │ ├── computer-action-validation.pipe.ts
│ │ │ └── computer-action.dto.ts
│ │ ├── input-tracking/
│ │ │ ├── input-tracking.controller.ts
│ │ │ ├── input-tracking.gateway.ts
│ │ │ ├── input-tracking.helpers.ts
│ │ │ ├── input-tracking.module.ts
│ │ │ └── input-tracking.service.ts
│ │ ├── main.ts
│ │ ├── mcp/
│ │ │ ├── bytebot-mcp.module.ts
│ │ │ ├── compressor.ts
│ │ │ ├── computer-use.tools.ts
│ │ │ └── index.ts
│ │ └── nut/
│ │ ├── nut.module.ts
│ │ └── nut.service.ts
│ ├── tsconfig.build.json
│ └── tsconfig.json
└── shared/
├── package.json
├── src/
│ ├── index.ts
│ ├── types/
│ │ ├── computerAction.types.ts
│ │ └── messageContent.types.ts
│ └── utils/
│ ├── computerAction.utils.ts
│ └── messageContent.utils.ts
└── tsconfig.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/build-agent.yaml
================================================
name: Build Agent
on:
push:
branches:
- main
paths:
- "packages/bytebot-agent/**"
- "packages/shared/**"
permissions:
contents: read
packages: write
jobs:
docker:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/bytebot-ai/bytebot-agent
tags: type=edge
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v6
env:
BUILDX_NO_DEFAULT_ATTESTATIONS: 1
DOCKER_BUILD_SUMMARY: false
DOCKER_BUILD_RECORD_UPLOAD: false
with:
context: ./packages
file: ./packages/bytebot-agent/Dockerfile
platforms: linux/amd64,linux/arm64
push: true
cache-from: type=gha
cache-to: type=gha,mode=max
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
================================================
FILE: .github/workflows/build-desktop.yaml
================================================
name: Build Desktop
on:
push:
branches:
- main
paths:
- "docker/**"
- "packages/bytebotd/**"
permissions:
contents: read
packages: write
jobs:
docker:
runs-on: ubuntu-22.04
steps:
# 1. Check out code
- uses: actions/checkout@v4
# 2. Enable QEMU so the amd64 runner can cross‑build arm64
- uses: docker/setup-qemu-action@v3
# 3. Set up Buildx builder
- uses: docker/setup-buildx-action@v3
# 4. Generate OCI labels + the single "edge" tag
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/bytebot-ai/bytebot-desktop
tags: type=edge
# 5. Log in to GHCR
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# 6. Build & push a multi‑arch image
- name: Build and push
uses: docker/build-push-action@v6
env:
BUILDX_NO_DEFAULT_ATTESTATIONS: 1 # hide "unknown/unknown" in GHCR
DOCKER_BUILD_SUMMARY: false # keep logs concise
DOCKER_BUILD_RECORD_UPLOAD: false
with:
context: ./packages/
file: ./packages/bytebotd/Dockerfile
platforms: linux/amd64,linux/arm64 # build both archs in one go
push: true
cache-from: type=gha
cache-to: type=gha,mode=max
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
================================================
FILE: .github/workflows/build-ui.yaml
================================================
name: Build UI
on:
push:
branches:
- main
paths:
- "packages/bytebot-ui/**"
- "packages/shared/**"
permissions:
contents: read
packages: write
jobs:
docker:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/bytebot-ai/bytebot-ui
tags: type=edge
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v6
env:
BUILDX_NO_DEFAULT_ATTESTATIONS: 1
DOCKER_BUILD_SUMMARY: false
DOCKER_BUILD_RECORD_UPLOAD: false
with:
context: ./packages
file: ./packages/bytebot-ui/Dockerfile
platforms: linux/amd64,linux/arm64
push: true
cache-from: type=gha
cache-to: type=gha,mode=max
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
================================================
FILE: .gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
*.qcow2
*.iso
*.img
*.vdi
*.vmdk
*.vhdx
*.vhd
# compiled output
agent/dist
agent/node_modules
agent/build
# Logs
logs
*.log
npm-debug.log*
pnpm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# OS
.DS_Store
# Tests
agent/coverage
agent/.nyc_output
# IDEs and editors
agent/.idea
agent/.project
agent/.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# dotenv environment variable files
.env.development.local
.env.test.local
.env.production.local
.env.local
# temp directory
.temp
.tmp
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# QEMU
*.qcow2
================================================
FILE: .prettierignore
================================================
# Ignore formatting in docs folder
/docs/**
================================================
FILE: LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: README.md
================================================
# Bytebot: Open-Source AI Desktop Agent
**An AI that has its own computer to complete tasks for you**
[](https://railway.com/deploy/bytebot?referralCode=L9lKXQ)
[](https://github.com/bytebot-ai/bytebot/tree/main/docker)
[](LICENSE)
[](https://discord.com/invite/d9ewZkWPTP)
[🌐 Website](https://bytebot.ai) • [📚 Documentation](https://docs.bytebot.ai) • [💬 Discord](https://discord.com/invite/d9ewZkWPTP) • [𝕏 Twitter](https://x.com/bytebot_ai)
[Deutsch](https://zdoc.app/de/bytebot-ai/bytebot) |
[Español](https://zdoc.app/es/bytebot-ai/bytebot) |
[français](https://zdoc.app/fr/bytebot-ai/bytebot) |
[日本語](https://zdoc.app/ja/bytebot-ai/bytebot) |
[한국어](https://zdoc.app/ko/bytebot-ai/bytebot) |
[Português](https://zdoc.app/pt/bytebot-ai/bytebot) |
[Русский](https://zdoc.app/ru/bytebot-ai/bytebot) |
[中文](https://zdoc.app/zh/bytebot-ai/bytebot)
---
https://github.com/user-attachments/assets/f271282a-27a3-43f3-9b99-b34007fdd169
https://github.com/user-attachments/assets/72a43cf2-bd87-44c5-a582-e7cbe176f37f
## What is a Desktop Agent?
A desktop agent is an AI that has its own computer. Unlike browser-only agents or traditional RPA tools, Bytebot comes with a full virtual desktop where it can:
- Use any application (browsers, email clients, office tools, IDEs)
- Download and organize files with its own file system
- Log into websites and applications using password managers
- Read and process documents, PDFs, and spreadsheets
- Complete complex multi-step workflows across different programs
Think of it as a virtual employee with their own computer who can see the screen, move the mouse, type on the keyboard, and complete tasks just like a human would.
## Why Give AI Its Own Computer?
When AI has access to a complete desktop environment, it unlocks capabilities that aren't possible with browser-only agents or API integrations:
### Complete Task Autonomy
Give Bytebot a task like "Download all invoices from our vendor portals and organize them into a folder" and it will:
- Open the browser
- Navigate to each portal
- Handle authentication (including 2FA via password managers)
- Download the files to its local file system
- Organize them into a folder
### Process Documents
Upload files directly to Bytebot's desktop and it can:
- Read entire PDFs into its context
- Extract data from complex documents
- Cross-reference information across multiple files
- Create new documents based on analysis
- Handle formats that APIs can't access
### Use Real Applications
Bytebot isn't limited to web interfaces. It can:
- Use desktop applications like text editors, VS Code, or email clients
- Run scripts and command-line tools
- Install new software as needed
- Configure applications for specific workflows
## Quick Start
### Deploy in 2 Minutes
**Option 1: Railway (Easiest)**
[](https://railway.com/deploy/bytebot?referralCode=L9lKXQ)
Just click and add your AI provider API key.
**Option 2: Docker Compose**
```bash
git clone https://github.com/bytebot-ai/bytebot.git
cd bytebot
# Add your AI provider key (choose one)
echo "ANTHROPIC_API_KEY=sk-ant-..." > docker/.env
# Or: echo "OPENAI_API_KEY=sk-..." > docker/.env
# Or: echo "GEMINI_API_KEY=..." > docker/.env
docker-compose -f docker/docker-compose.yml up -d
# Open http://localhost:9992
```
[Full deployment guide →](https://docs.bytebot.ai/quickstart)
## How It Works
Bytebot consists of four integrated components:
1. **Virtual Desktop**: A complete Ubuntu Linux environment with pre-installed applications
2. **AI Agent**: Understands your tasks and controls the desktop to complete them
3. **Task Interface**: Web UI where you create tasks and watch Bytebot work
4. **APIs**: REST endpoints for programmatic task creation and desktop control
### Key Features
- **Natural Language Tasks**: Just describe what you need done
- **File Uploads**: Drop files onto tasks for Bytebot to process
- **Live Desktop View**: Watch Bytebot work in real-time
- **Takeover Mode**: Take control when you need to help or configure something
- **Password Manager Support**: Install 1Password, Bitwarden, etc. for automatic authentication
- **Persistent Environment**: Install programs and they stay available for future tasks
## Example Tasks
### Basic Examples
```
"Go to Wikipedia and create a summary of quantum computing"
"Research flights from NYC to London and create a comparison document"
"Take screenshots of the top 5 news websites"
```
### Document Processing
```
"Read the uploaded contracts.pdf and extract all payment terms and deadlines"
"Process these 5 invoice PDFs and create a summary report"
"Download and analyze the latest financial report and answer: What were the key risks mentioned?"
```
### Multi-Application Workflows
```
"Download last month's bank statements from our three banks and consolidate them"
"Check all our vendor portals for new invoices and create a summary report"
"Log into our CRM, export the customer list, and update records in the ERP system"
```
## Programmatic Control
### Create Tasks via API
```python
import requests
# Simple task
response = requests.post('http://localhost:9991/tasks', json={
'description': 'Download the latest sales report and create a summary'
})
# Task with file upload
files = {'files': open('contracts.pdf', 'rb')}
response = requests.post('http://localhost:9991/tasks',
data={'description': 'Review these contracts for important dates'},
files=files
)
```
### Direct Desktop Control
```bash
# Take a screenshot
curl -X POST http://localhost:9990/computer-use \
-H "Content-Type: application/json" \
-d '{"action": "screenshot"}'
# Click at specific coordinates
curl -X POST http://localhost:9990/computer-use \
-H "Content-Type: application/json" \
-d '{"action": "click_mouse", "coordinate": [500, 300]}'
```
[Full API documentation →](https://docs.bytebot.ai/api-reference/introduction)
## Setting Up Your Desktop Agent
### 1. Deploy Bytebot
Use one of the deployment methods above to get Bytebot running.
### 2. Configure the Desktop
Use the Desktop tab in the UI to:
- Install additional programs you need
- Set up password managers for authentication
- Configure applications with your preferences
- Log into websites you want Bytebot to access
### 3. Start Giving Tasks
Create tasks in natural language and watch Bytebot complete them using the configured desktop.
## Use Cases
### Business Process Automation
- Invoice processing and data extraction
- Multi-system data synchronization
- Report generation from multiple sources
- Compliance checking across platforms
### Development & Testing
- Automated UI testing
- Cross-browser compatibility checks
- Documentation generation with screenshots
- Code deployment verification
### Research & Analysis
- Competitive analysis across websites
- Data gathering from multiple sources
- Document analysis and summarization
- Market research compilation
## Architecture
Bytebot is built with:
- **Desktop**: Ubuntu 22.04 with XFCE, Firefox, VS Code, and other tools
- **Agent**: NestJS service that coordinates AI and desktop actions
- **UI**: Next.js application for task management
- **AI Support**: Works with Anthropic Claude, OpenAI GPT, Google Gemini
- **Deployment**: Docker containers for easy self-hosting
## Why Self-Host?
- **Data Privacy**: Everything runs on your infrastructure
- **Full Control**: Customize the desktop environment as needed
- **No Limits**: Use your own AI API keys without platform restrictions
- **Flexibility**: Install any software, access any systems
## Advanced Features
### Multiple AI Providers
Use any AI provider through our [LiteLLM integration](https://docs.bytebot.ai/deployment/litellm):
- Azure OpenAI
- AWS Bedrock
- Local models via Ollama
- 100+ other providers
### Enterprise Deployment
Deploy on Kubernetes with Helm:
```bash
# Clone the repository
git clone https://github.com/bytebot-ai/bytebot.git
cd bytebot
# Install with Helm
helm install bytebot ./helm \
--set agent.env.ANTHROPIC_API_KEY=sk-ant-...
```
[Enterprise deployment guide →](https://docs.bytebot.ai/deployment/helm)
## Community & Support
- **Discord**: [Join our community](https://discord.com/invite/d9ewZkWPTP) for help and discussions
- **Documentation**: Comprehensive guides at [docs.bytebot.ai](https://docs.bytebot.ai)
- **GitHub Issues**: Report bugs and request features
## Contributing
We welcome contributions! Whether it's:
- 🐛 Bug fixes
- ✨ New features
- 📚 Documentation improvements
- 🌐 Translations
Please:
1. Check existing [issues](https://github.com/bytebot-ai/bytebot/issues) first
2. Open an issue to discuss major changes
3. Submit PRs with clear descriptions
4. Join our [Discord](https://discord.com/invite/d9ewZkWPTP) to discuss ideas
## License
Bytebot is open source under the Apache 2.0 license.
---
**Give your AI its own computer. See what it can do.**
[](https://railway.com/deploy/bytebot?referralCode=L9lKXQ)
Built by [Tantl Labs](https://tantl.com) and the open source community
================================================
FILE: docker/bytebot-desktop.Dockerfile
================================================
# Extend the pre-built bytebot-desktop image
FROM ghcr.io/bytebot-ai/bytebot-desktop:edge
# Add additional packages, applications, or customizations here
# Expose the bytebotd service port
EXPOSE 9990
# Start the bytebotd service
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf", "-n"]
================================================
FILE: docker/docker-compose-claude-code.yml
================================================
name: bytebot
services:
bytebot-desktop:
# Build from source
build:
context: ../packages/
dockerfile: bytebotd/Dockerfile
# Use pre-built image
image: ghcr.io/bytebot-ai/bytebot-desktop:edge
shm_size: "2g"
container_name: bytebot-desktop
restart: unless-stopped
hostname: computer
privileged: true
ports:
- "9990:9990" # bytebotd service & noVNC
environment:
- DISPLAY=:0
networks:
- bytebot-network
postgres:
image: postgres:16-alpine
container_name: bytebot-postgres
restart: unless-stopped
ports:
- "5432:5432"
environment:
- POSTGRES_PASSWORD=postgres
- POSTGRES_USER=postgres
- POSTGRES_DB=bytebotdb
networks:
- bytebot-network
volumes:
- postgres_data:/var/lib/postgresql/data
bytebot-agent-cc:
build:
context: ../packages/
dockerfile: bytebot-agent-cc/Dockerfile
container_name: bytebot-agent-cc
restart: unless-stopped
ports:
- "9991:9991"
environment:
- DATABASE_URL=${DATABASE_URL:-postgresql://postgres:postgres@postgres:5432/bytebotdb}
- BYTEBOT_DESKTOP_BASE_URL=${BYTEBOT_DESKTOP_BASE_URL:-http://bytebot-desktop:9990}
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
depends_on:
- postgres
networks:
- bytebot-network
bytebot-ui:
build:
context: ../packages/
dockerfile: bytebot-ui/Dockerfile
args:
- BYTEBOT_AGENT_BASE_URL=${BYTEBOT_AGENT_BASE_URL:-http://bytebot-agent-cc:9991}
- BYTEBOT_DESKTOP_VNC_URL=${BYTEBOT_DESKTOP_VNC_URL:-http://bytebot-desktop:9990/websockify}
# Use pre-built image
image: ghcr.io/bytebot-ai/bytebot-ui:edge
container_name: bytebot-ui
restart: unless-stopped
ports:
- "9992:9992"
environment:
- NODE_ENV=production
- BYTEBOT_AGENT_BASE_URL=${BYTEBOT_AGENT_BASE_URL:-http://bytebot-agent-cc:9991}
- BYTEBOT_DESKTOP_VNC_URL=${BYTEBOT_DESKTOP_VNC_URL:-http://bytebot-desktop:9990/websockify}
depends_on:
- bytebot-agent-cc
networks:
- bytebot-network
networks:
bytebot-network:
driver: bridge
volumes:
postgres_data:
================================================
FILE: docker/docker-compose.core.yml
================================================
name: bytebot
services:
bytebot-desktop:
# Build from source
build:
context: ../packages/
dockerfile: bytebotd/Dockerfile
# Use pre-built image
image: ghcr.io/bytebot-ai/bytebot-desktop:edge
shm_size: "2g"
container_name: bytebot-desktop
restart: unless-stopped
hostname: computer
privileged: true
ports:
- "9990:9990" # bytebotd service & noVNC
environment:
- DISPLAY=:0
================================================
FILE: docker/docker-compose.development.yml
================================================
## docker-compose file that spins up a bytebot-desktop container
## and a postgres container. bytebot-ui and bytebot-agent are not included
## in this file, and can be run separately using npm, allowing for
## easier local development.
name: bytebot
services:
bytebot-desktop:
# Build from source
build:
context: ../packages/
dockerfile: bytebotd/Dockerfile
# Use pre-built image
image: ghcr.io/bytebot-ai/bytebot-desktop:edge
shm_size: "2g"
container_name: bytebot-desktop
restart: unless-stopped
hostname: computer
privileged: true
ports:
- "9990:9990" # bytebotd service & noVNC
environment:
- DISPLAY=:0
networks:
- bytebot-network
postgres:
image: postgres:16-alpine
container_name: bytebot-postgres
restart: unless-stopped
ports:
- "5432:5432"
environment:
- POSTGRES_PASSWORD=postgres
- POSTGRES_USER=postgres
- POSTGRES_DB=bytebotdb
networks:
- bytebot-network
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
bytebot-network:
driver: bridge
volumes:
postgres_data:
================================================
FILE: docker/docker-compose.proxy.yml
================================================
name: bytebot
services:
bytebot-desktop:
# Build from source
build:
context: ../packages/
dockerfile: bytebotd/Dockerfile
# Use pre-built image
image: ghcr.io/bytebot-ai/bytebot-desktop:edge
shm_size: "2g"
container_name: bytebot-desktop
restart: unless-stopped
hostname: computer
privileged: true
ports:
- "9990:9990" # bytebotd service & noVNC
environment:
- DISPLAY=:0
networks:
- bytebot-network
postgres:
image: postgres:16-alpine
container_name: bytebot-postgres
restart: unless-stopped
ports:
- "5432:5432"
environment:
- POSTGRES_PASSWORD=postgres
- POSTGRES_USER=postgres
- POSTGRES_DB=bytebotdb
networks:
- bytebot-network
volumes:
- postgres_data:/var/lib/postgresql/data
bytebot-agent:
build:
context: ../packages/
dockerfile: bytebot-agent/Dockerfile
# Use pre-built image
image: ghcr.io/bytebot-ai/bytebot-agent:edge
container_name: bytebot-agent
restart: unless-stopped
ports:
- "9991:9991"
environment:
- DATABASE_URL=${DATABASE_URL:-postgresql://postgres:postgres@postgres:5432/bytebotdb}
- BYTEBOT_DESKTOP_BASE_URL=${BYTEBOT_DESKTOP_BASE_URL:-http://bytebot-desktop:9990}
- BYTEBOT_LLM_PROXY_URL=${BYTEBOT_LLM_PROXY_URL:-http://bytebot-llm-proxy:4000}
depends_on:
- postgres
networks:
- bytebot-network
bytebot-llm-proxy:
build:
context: ../packages/
dockerfile: bytebot-llm-proxy/Dockerfile
ports:
- "4000:4000"
environment:
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
- OPENAI_API_KEY=${OPENAI_API_KEY}
- GEMINI_API_KEY=${GEMINI_API_KEY}
networks:
- bytebot-network
bytebot-ui:
build:
context: ../packages/
dockerfile: bytebot-ui/Dockerfile
args:
- BYTEBOT_AGENT_BASE_URL=${BYTEBOT_AGENT_BASE_URL:-http://bytebot-agent:9991}
- BYTEBOT_DESKTOP_VNC_URL=${BYTEBOT_DESKTOP_VNC_URL:-http://bytebot-desktop:9990/websockify}
# Use pre-built image
image: ghcr.io/bytebot-ai/bytebot-ui:edge
container_name: bytebot-ui
restart: unless-stopped
ports:
- "9992:9992"
environment:
- NODE_ENV=production
- BYTEBOT_AGENT_BASE_URL=${BYTEBOT_AGENT_BASE_URL:-http://bytebot-agent:9991}
- BYTEBOT_DESKTOP_VNC_URL=${BYTEBOT_DESKTOP_VNC_URL:-http://bytebot-desktop:9990/websockify}
depends_on:
- bytebot-agent
networks:
- bytebot-network
networks:
bytebot-network:
driver: bridge
volumes:
postgres_data:
================================================
FILE: docker/docker-compose.yml
================================================
name: bytebot
services:
bytebot-desktop:
# Build from source
build:
context: ../packages/
dockerfile: bytebotd/Dockerfile
# Use pre-built image
image: ghcr.io/bytebot-ai/bytebot-desktop:edge
shm_size: "2g"
container_name: bytebot-desktop
restart: unless-stopped
hostname: computer
privileged: true
ports:
- "9990:9990" # bytebotd service & noVNC
environment:
- DISPLAY=:0
networks:
- bytebot-network
postgres:
image: postgres:16-alpine
container_name: bytebot-postgres
restart: unless-stopped
ports:
- "5432:5432"
environment:
- POSTGRES_PASSWORD=postgres
- POSTGRES_USER=postgres
- POSTGRES_DB=bytebotdb
networks:
- bytebot-network
volumes:
- postgres_data:/var/lib/postgresql/data
bytebot-agent:
build:
context: ../packages/
dockerfile: bytebot-agent/Dockerfile
# Use pre-built image
image: ghcr.io/bytebot-ai/bytebot-agent:edge
container_name: bytebot-agent
restart: unless-stopped
ports:
- "9991:9991"
environment:
- DATABASE_URL=${DATABASE_URL:-postgresql://postgres:postgres@postgres:5432/bytebotdb}
- BYTEBOT_DESKTOP_BASE_URL=${BYTEBOT_DESKTOP_BASE_URL:-http://bytebot-desktop:9990}
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
- OPENAI_API_KEY=${OPENAI_API_KEY}
- GEMINI_API_KEY=${GEMINI_API_KEY}
depends_on:
- postgres
networks:
- bytebot-network
bytebot-ui:
build:
context: ../packages/
dockerfile: bytebot-ui/Dockerfile
args:
- BYTEBOT_AGENT_BASE_URL=${BYTEBOT_AGENT_BASE_URL:-http://bytebot-agent:9991}
- BYTEBOT_DESKTOP_VNC_URL=${BYTEBOT_DESKTOP_VNC_URL:-http://bytebot-desktop:9990/websockify}
# Use pre-built image
image: ghcr.io/bytebot-ai/bytebot-ui:edge
container_name: bytebot-ui
restart: unless-stopped
ports:
- "9992:9992"
environment:
- NODE_ENV=production
- BYTEBOT_AGENT_BASE_URL=${BYTEBOT_AGENT_BASE_URL:-http://bytebot-agent:9991}
- BYTEBOT_DESKTOP_VNC_URL=${BYTEBOT_DESKTOP_VNC_URL:-http://bytebot-desktop:9990/websockify}
depends_on:
- bytebot-agent
networks:
- bytebot-network
networks:
bytebot-network:
driver: bridge
volumes:
postgres_data:
================================================
FILE: docs/api-reference/agent/tasks.mdx
================================================
---
title: 'Tasks API'
description: 'Reference documentation for the Bytebot Agent Tasks API'
---
## Tasks API
The Tasks API allows you to manage tasks in the Bytebot agent system. It's available at `http://localhost:9991/tasks` when running the full agent setup.
## Task Model
```typescript
{
id: string;
description: string;
status: 'PENDING' | 'IN_PROGRESS' | 'NEEDS_HELP' | 'NEEDS_REVIEW' | 'COMPLETED' | 'CANCELLED' | 'FAILED';
priority: 'LOW' | 'MEDIUM' | 'HIGH' | 'URGENT';
createdAt: string;
updatedAt: string;
}
```
## Endpoints
### Create Task
Create a new task for the agent to process.
Create a new task
#### Request Body
```json
{
"description": "This is a description of the task",
"priority": "MEDIUM" // Optional: LOW, MEDIUM, HIGH, URGENT
}
```
#### With File Upload
To upload files with a task, use `multipart/form-data`:
```bash
curl -X POST http://localhost:9991/tasks \
-F "description=Analyze the uploaded contracts and extract key terms" \
-F "priority=HIGH" \
-F "files=@contract1.pdf" \
-F "files=@contract2.pdf"
```
Uploaded files are automatically saved to the desktop and can be referenced in the task description.
#### Response
```json
{
"id": "task-123",
"description": "This is a description of the task",
"status": "PENDING",
"priority": "MEDIUM",
"createdAt": "2025-04-14T12:00:00Z",
"updatedAt": "2025-04-14T12:00:00Z"
}
```
### Get All Tasks
Retrieve a list of all tasks.
Get all tasks
#### Response
```json
[
{
"id": "task-123",
"description": "This is a description of the task",
"status": "PENDING",
"priority": "MEDIUM",
"createdAt": "2025-04-14T12:00:00Z",
"updatedAt": "2025-04-14T12:00:00Z"
},
// ...more tasks
]
```
### Get In-Progress Task
Retrieve the currently in-progress task, if any.
Get the currently in-progress task
#### Response
```json
{
"id": "task-123",
"description": "This is a description of the task",
"status": "IN_PROGRESS",
"priority": "MEDIUM",
"createdAt": "2025-04-14T12:00:00Z",
"updatedAt": "2025-04-14T12:00:00Z"
}
```
If no task is in progress, the response will be `null`.
### Get Task by ID
Retrieve a specific task by its ID.
Get a task by ID
#### Response
```json
{
"id": "task-123",
"description": "This is a description of the task",
"status": "PENDING",
"priority": "MEDIUM",
"createdAt": "2025-04-14T12:00:00Z",
"updatedAt": "2025-04-14T12:00:00Z",
"messages": [
{
"id": "msg-456",
"content": [
{
"type": "text",
"text": "This is a message"
}
],
"role": "USER",
"taskId": "task-123",
"createdAt": "2025-04-14T12:05:00Z",
"updatedAt": "2025-04-14T12:05:00Z"
}
// ...more messages
]
}
```
### Update Task
Update an existing task.
Update a task
#### Request Body
```json
{
"status": "COMPLETED",
"priority": "HIGH"
}
```
#### Response
```json
{
"id": "task-123",
"description": "This is a description of the task",
"status": "COMPLETED",
"priority": "HIGH",
"createdAt": "2025-04-14T12:00:00Z",
"updatedAt": "2025-04-14T12:01:00Z"
}
```
### Delete Task
Delete a task.
Delete a task
#### Response
Status code `204 No Content` with an empty response body.
## Message Content Structure
Messages in the Bytebot agent system use a content block structure compatible with Anthropic's Claude API:
```typescript
type MessageContent = MessageContentBlock[];
interface MessageContentBlock {
type: string;
[key: string]: any;
}
interface TextContentBlock {
type: "text";
text: string;
}
interface ImageContentBlock {
type: "image";
source: {
type: "base64";
media_type: string;
data: string;
};
}
```
## Error Responses
The API may return the following error responses:
| Status Code | Description |
|-------------|--------------------------------------------|
| `400` | Bad Request - Invalid parameters |
| `404` | Not Found - Resource does not exist |
| `500` | Internal Server Error - Server side error |
Example error response:
```json
{
"statusCode": 404,
"message": "Task with ID task-123 not found",
"error": "Not Found"
}
```
## Code Examples
```javascript JavaScript
const axios = require('axios');
async function createTask(description) {
const response = await axios.post('http://localhost:9991/tasks', {
description
});
return response.data;
}
async function findInProgressTask() {
const response = await axios.get('http://localhost:9991/tasks/in-progress');
return response.data;
}
// Example usage
async function main() {
// Create a new task
const task = await createTask('Compare React, Vue, and Angular for a new project');
console.log('Created task:', task);
// Get current in-progress task
const inProgressTask = await findInProgressTask();
console.log('In progress task:', inProgressTask);
}
```
```python Python
import requests
def create_task(description):
response = requests.post(
"http://localhost:9991/tasks",
json={
"description": description
}
)
return response.json()
def find_in_progress_task():
response = requests.get("http://localhost:9991/tasks/in-progress")
return response.json()
# Example usage
def main():
# Create a new task
task = create_task("Compare React, Vue, and Angular for a new project")
print(f"Created task: {task}")
# Get current in-progress task
in_progress_task = find_in_progress_task()
print(f"In progress task: {in_progress_task}")
```
```curl cURL
# Create a new task
curl -X POST http://localhost:9991/tasks \
-H "Content-Type: application/json" \
-d '{
"description": "Compare React, Vue, and Angular for a new project"
}'
# Get current in-progress task
curl -X GET http://localhost:9991/tasks/in-progress
```
================================================
FILE: docs/api-reference/agent/ui.mdx
================================================
---
title: 'Task UI'
description: 'Documentation for the Bytebot Task UI'
---
## Bytebot Task UI
The Bytebot Task UI provides a web-based interface for interacting with the Bytebot agent system. It combines a action feed with an embedded noVNC viewer, allowing you to watch it perform task on the desktop in real-time.
## Accessing the UI
When running the full Bytebot agent system, the Task UI is available at:
```
http://localhost:9992
```
## UI Components
### Task Management Panel
The task management panel allows you to:
- Create new tasks
- View existing tasks
- See task status and priority
- Select a task to work on
### Task Interface
The main task interface provides:
- Task history with the agent
- Support for markdown formatting in messages
- Automatic scrolling to new messages
### Desktop Viewer
The embedded noVNC viewer displays:
- Real-time view of the desktop environment
- Visual feedback of agent actions
- Option to expand to take over the desktop
- Connection status indicator
## Features
### Task Creation
To create a new task:
1. Enter a description for the task
2. Click "Start Task" button (or press Enter)
### Conversation Controls
The task interface supports:
- Text messages with markdown formatting
- Viewing image content in messages
- Displaying tool use actions
- Showing tool results
### Desktop Interaction
While primarily for viewing, the desktop panel allows:
- Taking over the desktop
- Real-time monitoring of agent actions
## Message Types
The task interface displays different types of messages based on Bytebot's content block structure:
- **User Messages**: Your instructions and queries
- **Assistant Messages**: Responses from the agent, which may include:
- **Text Content Blocks**: Markdown-formatted text responses
- **Image Content Blocks**: Images generated or captured
- **Tool Use Content Blocks**: Computer actions being performed
- **Tool Result Content Blocks**: Results of computer actions
The message content structure follows this format:
```typescript
interface Message {
id: string;
content: MessageContentBlock[];
role: Role; // "USER" or "ASSISTANT"
createdAt?: string;
}
interface MessageContentBlock {
type: string;
[key: string]: any;
}
interface TextContentBlock extends MessageContentBlock {
type: "text";
text: string;
}
interface ImageContentBlock extends MessageContentBlock {
type: "image";
source: {
type: "base64";
media_type: string;
data: string;
};
}
```
## Technical Details
The Bytebot Task UI is built with:
- **Next.js**: React framework for the frontend
- **Tailwind CSS**: For styling
- **ReactMarkdown**: For rendering markdown content
- **noVNC**: For the embedded desktop viewer
## Troubleshooting
### Connection Issues
If you experience connection issues:
1. Ensure all Bytebot services are running
2. Check that ports 9990, 9991, and 9992 are accessible
3. Try refreshing the browser
4. Check browser console for error messages
### Desktop Viewer Issues
If the desktop viewer is not displaying:
1. Ensure the Bytebot container is running
2. Check that the noVNC service is accessible at port 9990
### Message Display Issues
If messages are not displaying correctly:
1. Check that the message content is properly formatted
2. Ensure the agent service is processing task correctly
3. Check the browser console for any rendering errors
4. Try refreshing the browser
================================================
FILE: docs/api-reference/computer-use/examples.mdx
================================================
---
title: "Computer Use API Examples"
description: "Code examples for common automation scenarios using the Bytebot API"
---
## Basic Examples
Here are some practical examples of how to use the Computer Use API in different programming languages.
### Using cURL
```bash Opening a Web Browser
# Move to Firefox/Chrome icon in the dock and click it
curl -X POST http://localhost:9990/computer-use \
-H "Content-Type: application/json" \
-d '{"action": "move_mouse", "coordinates": {"x": 100, "y": 960}}'
curl -X POST http://localhost:9990/computer-use \
-H "Content-Type: application/json" \
-d '{"action": "click_mouse", "button": "left", "clickCount": 1}'
````
```bash Taking and Saving a Screenshot
# Take a screenshot
response=$(curl -s -X POST http://localhost:9990/computer-use \
-H "Content-Type: application/json" \
-d '{"action": "screenshot"}')
# Extract the base64 image data and save to a file
echo $response | jq -r '.data.image' | base64 -d > screenshot.png
````
```bash Typing and Keyboard Shortcuts
# Type text in a text editor
curl -X POST http://localhost:9990/computer-use \
-H "Content-Type: application/json" \
-d '{"action": "type_text", "text": "Hello, this is an automated test!", "delay": 30}'
# Press Ctrl+S to save
curl -X POST http://localhost:9990/computer-use \
-H "Content-Type: application/json" \
-d '{"action": "press_keys", "key": "s", "modifiers": ["control"]}'
```
### Python Examples
```python Basic Automation
import requests
import json
import base64
import time
from io import BytesIO
from PIL import Image
def control_computer(action, **params):
url = "http://localhost:9990/computer-use"
data = {"action": action, **params}
response = requests.post(url, json=data)
return response.json()
# Open a web browser by clicking an icon
control_computer("move_mouse", coordinates={"x": 100, "y": 960})
control_computer("click_mouse", button="left")
# Wait for the browser to open
control_computer("wait", duration=2000)
# Type a URL
control_computer("type_text", text="https://example.com")
control_computer("press_keys", key="enter")
````
```python Screenshot and Analysis
import requests
import json
import base64
import cv2
import numpy as np
from PIL import Image
from io import BytesIO
def take_screenshot():
url = "http://localhost:9990/computer-use"
data = {"action": "screenshot"}
response = requests.post(url, json=data)
if response.json()["success"]:
img_data = base64.b64decode(response.json()["data"]["image"])
image = Image.open(BytesIO(img_data))
return np.array(image)
return None
# Take a screenshot
img = take_screenshot()
# Convert to grayscale for analysis
if img is not None:
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# Save the screenshot
cv2.imwrite("screenshot.png", img)
# Perform image analysis (example: find edges)
edges = cv2.Canny(gray, 100, 200)
cv2.imwrite("edges.png", edges)
````
```python Web Form Automation
import requests
import time
def control_computer(action, **params):
url = "http://localhost:9990/computer-use"
data = {"action": action, **params}
response = requests.post(url, json=data)
return response.json()
def fill_web_form(form_fields):
# Click on the first form field
control_computer("move_mouse", coordinates=form_fields[0])
control_computer("click_mouse", button="left")
# Fill out each field
for i, field in enumerate(form_fields):
# Input the field value
control_computer("type_text", text=field["value"])
# If not the last field, press Tab to move to next field
if i < len(form_fields) - 1:
control_computer("press_keys", key="tab")
time.sleep(0.5)
# Submit the form by pressing Enter
control_computer("press_keys", key="enter")
# Example form fields with coordinates and values
form_fields = [
{"x": 500, "y": 300, "value": "John Doe"},
{"x": 500, "y": 350, "value": "john@example.com"},
{"x": 500, "y": 400, "value": "Password123"}
]
fill_web_form(form_fields)
```
### JavaScript/Node.js Examples
```javascript Basic Automation
const axios = require('axios');
async function controlComputer(action, params = {}) {
const url = "http://localhost:9990/computer-use";
const data = { action, ...params };
try {
const response = await axios.post(url, data);
return response.data;
} catch (error) {
console.error('Error:', error.message);
return { success: false, error: error.message };
}
}
// Example: Automate opening an application and typing
async function automateTextEditor() {
try {
// Open text editor by clicking its icon
await controlComputer("move_mouse", { coordinates: { x: 150, y: 960 } });
await controlComputer("click_mouse", { button: "left" });
// Wait for it to open
await controlComputer("wait", { duration: 2000 });
// Type some text
await controlComputer("type_text", {
text: "This is an automated test using Node.js and Bytebot",
delay: 30
});
console.log("Automation completed successfully");
} catch (error) {
console.error("Automation failed:", error);
}
}
automateTextEditor();
````
```javascript Advanced: Screenshot Comparison
const axios = require('axios');
const fs = require('fs');
const { createCanvas, loadImage } = require('canvas');
const pixelmatch = require('pixelmatch');
async function controlComputer(action, params = {}) {
const url = "http://localhost:9990/computer-use";
const data = { action, ...params };
try {
const response = await axios.post(url, data);
return response.data;
} catch (error) {
console.error('Error:', error.message);
return { success: false, error: error.message };
}
}
async function compareScreenshots() {
try {
// Take first screenshot
const screenshot1 = await controlComputer("screenshot");
// Do some actions
await controlComputer("move_mouse", { coordinates: { x: 500, y: 500 } });
await controlComputer("click_mouse", { button: "left" });
await controlComputer("wait", { duration: 1000 });
// Take second screenshot
const screenshot2 = await controlComputer("screenshot");
// Compare screenshots
if (screenshot1.success && screenshot2.success) {
const img1Data = Buffer.from(screenshot1.data.image, 'base64');
const img2Data = Buffer.from(screenshot2.data.image, 'base64');
fs.writeFileSync('screenshot1.png', img1Data);
fs.writeFileSync('screenshot2.png', img2Data);
// Now you could load and compare these images
// This requires additional image comparison libraries
console.log('Screenshots saved for comparison');
}
} catch (error) {
console.error("Screenshot comparison failed:", error);
}
}
compareScreenshots();
````
## File Operations
### Writing Files
These examples show how to write files to the desktop environment:
```python Python
import requests
import base64
def write_file(path, content):
url = "http://localhost:9990/computer-use"
# Encode content to base64
encoded_content = base64.b64encode(content.encode('utf-8')).decode('utf-8')
data = {
"action": "write_file",
"path": path,
"data": encoded_content
}
response = requests.post(url, json=data)
return response.json()
# Write a text file
result = write_file("/home/user/hello.txt", "Hello, Bytebot!")
print(result) # {'success': True, 'message': 'File written successfully...'}
# Write to desktop (relative path)
result = write_file("report.txt", "Daily report content")
print(result) # File will be written to /home/user/Desktop/report.txt
```
```javascript JavaScript
const axios = require('axios');
async function writeFile(path, content) {
const url = "http://localhost:9990/computer-use";
// Encode content to base64
const encodedContent = Buffer.from(content, 'utf-8').toString('base64');
const data = {
action: "write_file",
path: path,
data: encodedContent
};
const response = await axios.post(url, data);
return response.data;
}
// Write a text file
writeFile("/home/user/notes.txt", "Meeting notes...")
.then(result => console.log(result))
.catch(error => console.error(error));
// Write HTML file to desktop
const htmlContent = 'Hello ';
writeFile("index.html", htmlContent)
.then(result => console.log("HTML file created"));
```
### Reading Files
These examples show how to read files from the desktop environment:
```python Python
import requests
import base64
def read_file(path):
url = "http://localhost:9990/computer-use"
data = {
"action": "read_file",
"path": path
}
response = requests.post(url, json=data)
result = response.json()
if result['success']:
# Decode the base64 content
content = base64.b64decode(result['data']).decode('utf-8')
return {
'content': content,
'name': result['name'],
'size': result['size'],
'mediaType': result['mediaType']
}
else:
return result
# Read a text file
file_data = read_file("/home/user/hello.txt")
print(f"Content: {file_data['content']}")
print(f"Size: {file_data['size']} bytes")
print(f"Type: {file_data['mediaType']}")
```
```javascript JavaScript
const axios = require('axios');
async function readFile(path) {
const url = "http://localhost:9990/computer-use";
const data = {
action: "read_file",
path: path
};
const response = await axios.post(url, data);
const result = response.data;
if (result.success) {
// Decode the base64 content
const content = Buffer.from(result.data, 'base64').toString('utf-8');
return {
content: content,
name: result.name,
size: result.size,
mediaType: result.mediaType
};
} else {
throw new Error(result.message);
}
}
// Read a file from desktop
readFile("report.txt")
.then(fileData => {
console.log(`Content: ${fileData.content}`);
console.log(`Size: ${fileData.size} bytes`);
console.log(`Type: ${fileData.mediaType}`);
})
.catch(error => console.error("Error reading file:", error));
```
## Automation Recipes
### Browser Automation
This example demonstrates how to automate browser interactions:
```python
import requests
import time
def control_computer(action, **params):
url = "http://localhost:9990/computer-use"
data = {"action": action, **params}
response = requests.post(url, json=data)
return response.json()
def automate_browser():
# Open browser (assuming browser icon is at position x=100, y=960)
control_computer("move_mouse", coordinates={"x": 100, "y": 960})
control_computer("click_mouse", button="left")
time.sleep(3) # Wait for browser to open
# Type URL
control_computer("type_text", text="https://example.com")
control_computer("press_keys", key="enter")
time.sleep(2) # Wait for page to load
# Take screenshot of the loaded page
screenshot = control_computer("screenshot")
# Click on a link (coordinates would need to be adjusted for your target)
control_computer("move_mouse", coordinates={"x": 300, "y": 400})
control_computer("click_mouse", button="left")
time.sleep(2)
# Scroll down
control_computer("scroll", direction="down", scrollCount=5)
automate_browser()
```
### Form Filling Automation
This example shows how to automate filling out a form in a web application:
```javascript
const axios = require("axios");
async function controlComputer(action, params = {}) {
const url = "http://localhost:9990/computer-use";
const data = { action, ...params };
const response = await axios.post(url, data);
return response.data;
}
async function fillForm() {
// Click first input field
await controlComputer("move_mouse", { coordinates: { x: 400, y: 300 } });
await controlComputer("click_mouse", { button: "left" });
// Type name
await controlComputer("type_text", { text: "John Doe" });
// Tab to next field
await controlComputer("press_keys", { key: "tab" });
// Type email
await controlComputer("type_text", { text: "john@example.com" });
// Tab to next field
await controlComputer("press_keys", { key: "tab" });
// Type message
await controlComputer("type_text", {
text: "This is an automated message sent using Bytebot's Computer Use API",
delay: 30,
});
// Tab to submit button
await controlComputer("press_keys", { key: "tab" });
// Press Enter to submit
await controlComputer("press_keys", { key: "enter" });
}
fillForm().catch(console.error);
```
## Integration with Testing Frameworks
The Computer Use API can be integrated with popular testing frameworks:
### Selenium Alternative
Bytebot can serve as an alternative to Selenium for web testing:
```python
import requests
import time
import json
class BytebotWebDriver:
def __init__(self, base_url="http://localhost:9990"):
self.base_url = base_url
def control_computer(self, action, **params):
url = f"{self.base_url}/computer-use"
data = {"action": action, **params}
response = requests.post(url, json=data)
return response.json()
def open_browser(self, browser_icon_coords):
self.control_computer("move_mouse", coordinates=browser_icon_coords)
self.control_computer("click_mouse", button="left")
time.sleep(3) # Wait for browser to open
def navigate_to(self, url):
self.control_computer("type_text", text=url)
self.control_computer("press_keys", key="enter")
time.sleep(2) # Wait for page to load
def click_element(self, coords):
self.control_computer("move_mouse", coordinates=coords)
self.control_computer("click_mouse", button="left")
def type_text(self, text):
self.control_computer("type_text", text=text)
def press_keys(self, key, modifiers=None):
params = {"key": key}
if modifiers:
params["modifiers"] = modifiers
self.control_computer("press_keys", **params)
def take_screenshot(self):
return self.control_computer("screenshot")
# Usage example
driver = BytebotWebDriver()
driver.open_browser({"x": 100, "y": 960})
driver.navigate_to("https://example.com")
driver.click_element({"x": 300, "y": 400})
driver.type_text("Hello Bytebot!")
```
================================================
FILE: docs/api-reference/computer-use/openapi.json
================================================
{
"openapi": "3.1.0",
"info": {
"title": "Bytebot Computer Use API",
"version": "1.0.0",
"description": "Control the Bytebot virtual desktop via a single endpoint"
},
"paths": {
"/computer-use": {
"post": {
"summary": "Execute a computer action",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ComputerAction"
}
}
}
},
"responses": {
"200": {
"description": "Successful response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ComputerActionResponse"
}
}
}
},
"500": {
"description": "Error executing action",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"status": {"type": "string"},
"error": {"type": "string"}
}
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"Coordinates": {
"type": "object",
"properties": {
"x": {"type": "number"},
"y": {"type": "number"}
},
"required": ["x", "y"]
},
"Button": {
"type": "string",
"enum": ["left", "right", "middle"]
},
"Press": {
"type": "string",
"enum": ["up", "down"]
},
"ScrollDirection": {
"type": "string",
"enum": ["up", "down", "left", "right"]
},
"MoveMouseAction": {
"type": "object",
"properties": {
"action": {"enum": ["move_mouse"]},
"coordinates": {"$ref": "#/components/schemas/Coordinates"}
},
"required": ["action", "coordinates"]
},
"TraceMouseAction": {
"type": "object",
"properties": {
"action": {"enum": ["trace_mouse"]},
"path": {
"type": "array",
"items": {"$ref": "#/components/schemas/Coordinates"}
},
"holdKeys": {
"type": "array",
"items": {"type": "string"}
}
},
"required": ["action", "path"]
},
"ClickMouseAction": {
"type": "object",
"properties": {
"action": {"enum": ["click_mouse"]},
"coordinates": {"$ref": "#/components/schemas/Coordinates"},
"button": {"$ref": "#/components/schemas/Button"},
"holdKeys": {
"type": "array",
"items": {"type": "string"}
},
"clickCount": {"type": "integer", "minimum": 1}
},
"required": ["action", "button", "clickCount"]
},
"PressMouseAction": {
"type": "object",
"properties": {
"action": {"enum": ["press_mouse"]},
"coordinates": {"$ref": "#/components/schemas/Coordinates"},
"button": {"$ref": "#/components/schemas/Button"},
"press": {"$ref": "#/components/schemas/Press"}
},
"required": ["action", "button", "press"]
},
"DragMouseAction": {
"type": "object",
"properties": {
"action": {"enum": ["drag_mouse"]},
"path": {
"type": "array",
"items": {"$ref": "#/components/schemas/Coordinates"}
},
"button": {"$ref": "#/components/schemas/Button"},
"holdKeys": {
"type": "array",
"items": {"type": "string"}
}
},
"required": ["action", "path", "button"]
},
"ScrollAction": {
"type": "object",
"properties": {
"action": {"enum": ["scroll"]},
"coordinates": {"$ref": "#/components/schemas/Coordinates"},
"direction": {"$ref": "#/components/schemas/ScrollDirection"},
"scrollCount": {"type": "integer", "minimum": 1},
"holdKeys": {
"type": "array",
"items": {"type": "string"}
}
},
"required": ["action", "direction", "scrollCount"]
},
"TypeKeysAction": {
"type": "object",
"properties": {
"action": {"enum": ["type_keys"]},
"keys": {
"type": "array",
"items": {"type": "string"}
},
"delay": {"type": "integer", "minimum": 0}
},
"required": ["action", "keys"]
},
"PressKeysAction": {
"type": "object",
"properties": {
"action": {"enum": ["press_keys"]},
"keys": {
"type": "array",
"items": {"type": "string"}
},
"press": {"$ref": "#/components/schemas/Press"}
},
"required": ["action", "keys", "press"]
},
"TypeTextAction": {
"type": "object",
"properties": {
"action": {"enum": ["type_text"]},
"text": {"type": "string"},
"delay": {"type": "integer", "minimum": 0}
},
"required": ["action", "text"]
},
"WaitAction": {
"type": "object",
"properties": {
"action": {"enum": ["wait"]},
"duration": {"type": "integer", "minimum": 0}
},
"required": ["action", "duration"]
},
"ScreenshotAction": {
"type": "object",
"properties": {
"action": {"enum": ["screenshot"]}
},
"required": ["action"]
},
"CursorPositionAction": {
"type": "object",
"properties": {
"action": {"enum": ["cursor_position"]}
},
"required": ["action"]
},
"ComputerAction": {
"oneOf": [
{"$ref": "#/components/schemas/MoveMouseAction"},
{"$ref": "#/components/schemas/TraceMouseAction"},
{"$ref": "#/components/schemas/ClickMouseAction"},
{"$ref": "#/components/schemas/PressMouseAction"},
{"$ref": "#/components/schemas/DragMouseAction"},
{"$ref": "#/components/schemas/ScrollAction"},
{"$ref": "#/components/schemas/TypeKeysAction"},
{"$ref": "#/components/schemas/PressKeysAction"},
{"$ref": "#/components/schemas/TypeTextAction"},
{"$ref": "#/components/schemas/WaitAction"},
{"$ref": "#/components/schemas/ScreenshotAction"},
{"$ref": "#/components/schemas/CursorPositionAction"}
],
"discriminator": {
"propertyName": "action",
"mapping": {
"move_mouse": "#/components/schemas/MoveMouseAction",
"trace_mouse": "#/components/schemas/TraceMouseAction",
"click_mouse": "#/components/schemas/ClickMouseAction",
"press_mouse": "#/components/schemas/PressMouseAction",
"drag_mouse": "#/components/schemas/DragMouseAction",
"scroll": "#/components/schemas/ScrollAction",
"type_keys": "#/components/schemas/TypeKeysAction",
"press_keys": "#/components/schemas/PressKeysAction",
"type_text": "#/components/schemas/TypeTextAction",
"wait": "#/components/schemas/WaitAction",
"screenshot": "#/components/schemas/ScreenshotAction",
"cursor_position": "#/components/schemas/CursorPositionAction"
}
}
},
"ScreenshotResponse": {
"type": "object",
"properties": {
"image": {
"type": "string",
"description": "Base64 encoded PNG"
}
},
"required": ["image"]
},
"CursorPosition": {
"type": "object",
"properties": {
"x": {"type": "number"},
"y": {"type": "number"}
},
"required": ["x", "y"]
},
"ComputerActionResponse": {
"type": "object",
"properties": {
"success": {"type": "boolean"},
"data": {
"oneOf": [
{"$ref": "#/components/schemas/ScreenshotResponse"},
{"$ref": "#/components/schemas/CursorPosition"}
]
}
},
"required": ["success"]
}
}
}
}
================================================
FILE: docs/api-reference/computer-use/unified-endpoint.mdx
================================================
---
title: "Unified Computer Actions API"
description: "Control all aspects of the desktop environment with a single endpoint"
---
## Overview
The unified computer action API allows for granular control over all aspects of the Bytebot virtual desktop environment through a single endpoint. It replaces multiple specific endpoints with a unified interface that handles various computer actions like mouse movements, clicks, key presses, and more.
## Endpoint
| Method | URL | Description |
| ------ | ---------------- | ----------------------------------------------- |
| POST | `/computer-use` | Execute computer actions in the virtual desktop |
## Request Format
All requests to the unified endpoint follow this format:
```json
{
"action": "action_name",
...action-specific parameters
}
```
The `action` parameter determines which operation to perform, and the remaining parameters depend on the specific action.
## Available Actions
### move_mouse
Move the mouse cursor to a specific position.
**Parameters:**
| Parameter | Type | Required | Description |
| --------------- | ------ | -------- | --------------------------------- |
| `coordinates` | Object | Yes | The target coordinates to move to |
| `coordinates.x` | Number | Yes | X coordinate |
| `coordinates.y` | Number | Yes | Y coordinate |
**Example:**
```json
{
"action": "move_mouse",
"coordinates": {
"x": 100,
"y": 200
}
}
```
### trace_mouse
Move the mouse along a path of coordinates.
**Parameters:**
| Parameter | Type | Required | Description |
| ------------ | ------ | -------- | ----------------------------------------------- |
| `path` | Array | Yes | Array of coordinate objects for the mouse path |
| `path[].x` | Number | Yes | X coordinate for each point in the path |
| `path[].y` | Number | Yes | Y coordinate for each point in the path |
| `holdKeys` | Array | No | Keys to hold while moving along the path |
**Example:**
```json
{
"action": "trace_mouse",
"path": [
{ "x": 100, "y": 100 },
{ "x": 150, "y": 150 },
{ "x": 200, "y": 200 }
],
"holdKeys": ["shift"]
}
```
### click_mouse
Perform a mouse click at the current or specified position.
**Parameters:**
| Parameter | Type | Required | Description |
| --------------- | ------ | -------- | ----------------------------------------------------- |
| `coordinates` | Object | No | The coordinates to click (uses current if omitted) |
| `coordinates.x` | Number | Yes* | X coordinate |
| `coordinates.y` | Number | Yes* | Y coordinate |
| `button` | String | Yes | Mouse button: 'left', 'right', or 'middle' |
| `clickCount` | Number | Yes | Number of clicks to perform |
| `holdKeys` | Array | No | Keys to hold while clicking (e.g., ['ctrl', 'shift']) |
**Example:**
```json
{
"action": "click_mouse",
"coordinates": {
"x": 150,
"y": 250
},
"button": "left",
"clickCount": 2
}
```
### press_mouse
Press or release a mouse button at the current or specified position.
**Parameters:**
| Parameter | Type | Required | Description |
| --------------- | ------ | -------- | -------------------------------------------------------- |
| `coordinates` | Object | No | The coordinates to press/release (uses current if omitted) |
| `coordinates.x` | Number | Yes* | X coordinate |
| `coordinates.y` | Number | Yes* | Y coordinate |
| `button` | String | Yes | Mouse button: 'left', 'right', or 'middle' |
| `press` | String | Yes | Action: 'up' or 'down' |
**Example:**
```json
{
"action": "press_mouse",
"coordinates": {
"x": 150,
"y": 250
},
"button": "left",
"press": "down"
}
```
### drag_mouse
Click and drag the mouse from one point to another.
**Parameters:**
| Parameter | Type | Required | Description |
| ------------ | ------ | -------- | --------------------------------------------- |
| `path` | Array | Yes | Array of coordinate objects for the drag path |
| `path[].x` | Number | Yes | X coordinate for each point in the path |
| `path[].y` | Number | Yes | Y coordinate for each point in the path |
| `button` | String | Yes | Mouse button: 'left', 'right', or 'middle' |
| `holdKeys` | Array | No | Keys to hold while dragging |
**Example:**
```json
{
"action": "drag_mouse",
"path": [
{ "x": 100, "y": 100 },
{ "x": 200, "y": 200 }
],
"button": "left"
}
```
### scroll
Scroll up, down, left, or right.
**Parameters:**
| Parameter | Type | Required | Description |
| --------------- | ------ | -------- | ------------------------------------------------------ |
| `coordinates` | Object | No | The coordinates to scroll at (uses current if omitted) |
| `coordinates.x` | Number | Yes* | X coordinate |
| `coordinates.y` | Number | Yes* | Y coordinate |
| `direction` | String | Yes | Scroll direction: 'up', 'down', 'left', 'right' |
| `scrollCount` | Number | Yes | Number of scroll steps |
| `holdKeys` | Array | No | Keys to hold while scrolling |
**Example:**
```json
{
"action": "scroll",
"direction": "down",
"scrollCount": 5
}
```
### type_keys
Type a sequence of keyboard keys.
**Parameters:**
| Parameter | Type | Required | Description |
| --------- | ------ | -------- | -------------------------------------- |
| `keys` | Array | Yes | Array of keys to type in sequence |
| `delay` | Number | No | Delay between key presses (ms) |
**Example:**
```json
{
"action": "type_keys",
"keys": ["a", "b", "c", "enter"],
"delay": 50
}
```
### press_keys
Press or release keyboard keys.
**Parameters:**
| Parameter | Type | Required | Description |
| --------- | ------ | -------- | ------------------------------------------ |
| `keys` | Array | Yes | Array of keys to press or release |
| `press` | String | Yes | Action: 'up' or 'down' |
**Example:**
```json
{
"action": "press_keys",
"keys": ["ctrl", "shift", "esc"],
"press": "down"
}
```
### type_text
Type a text string with optional delay.
**Parameters:**
| Parameter | Type | Required | Description |
| --------- | ------ | -------- | ----------------------------------------------------- |
| `text` | String | Yes | The text to type |
| `delay` | Number | No | Delay between characters in milliseconds (default: 0) |
**Example:**
```json
{
"action": "type_text",
"text": "Hello, Bytebot!",
"delay": 50
}
```
### paste_text
Paste text to the current cursor position. This is especially useful for special characters that aren't on the standard keyboard.
**Parameters:**
| Parameter | Type | Required | Description |
| --------- | ------ | -------- | ------------------------------------------------------------------------ |
| `text` | String | Yes | The text to paste, including special characters and emojis |
**Example:**
```json
{
"action": "paste_text",
"text": "Special characters: ©®™€¥£ émojis 🎉"
}
```
### wait
Wait for a specified duration.
**Parameters:**
| Parameter | Type | Required | Description |
| ---------- | ------ | -------- | ----------------------------- |
| `duration` | Number | Yes | Wait duration in milliseconds |
**Example:**
```json
{
"action": "wait",
"duration": 2000
}
```
### screenshot
Capture a screenshot of the desktop.
**Parameters:** None required
**Example:**
```json
{
"action": "screenshot"
}
```
### cursor_position
Get the current position of the mouse cursor.
**Parameters:** None required
**Example:**
```json
{
"action": "cursor_position"
}
```
### application
Switch between different applications or navigate to the desktop/directory.
**Parameters:**
| Parameter | Type | Required | Description |
| ------------- | ------ | -------- | ------------------------------------------------------------------------ |
| `application` | String | Yes | The application to switch to. See available options below. |
**Available Applications:**
- `firefox` - Mozilla Firefox web browser
- `1password` - Password manager
- `thunderbird` - Email client
- `vscode` - Visual Studio Code editor
- `terminal` - Terminal/console application
- `desktop` - Switch to desktop
- `directory` - File manager/directory browser
**Example:**
```json
{
"action": "application",
"application": "firefox"
}
```
### write_file
Write a file to the desktop environment filesystem.
**Parameters:**
| Parameter | Type | Required | Description |
| --------- | ------ | -------- | -------------------------------------------------------------- |
| `path` | String | Yes | File path (absolute or relative to /home/user/Desktop) |
| `data` | String | Yes | Base64 encoded file content |
**Example:**
```json
{
"action": "write_file",
"path": "/home/user/documents/example.txt",
"data": "SGVsbG8gV29ybGQh"
}
```
### read_file
Read a file from the desktop environment filesystem.
**Parameters:**
| Parameter | Type | Required | Description |
| --------- | ------ | -------- | -------------------------------------------------------------- |
| `path` | String | Yes | File path (absolute or relative to /home/user/Desktop) |
**Example:**
```json
{
"action": "read_file",
"path": "/home/user/documents/example.txt"
}
```
## Response Format
The response format varies depending on the action performed.
### Standard Response
Most actions return a simple success response:
```json
{
"success": true
}
```
### Screenshot Response
```json
{
"success": true,
"data": {
"image": "base64_encoded_image_data"
}
}
```
### Cursor Position Response
```json
{
"success": true,
"data": {
"x": 123,
"y": 456
}
}
```
### Write File Response
```json
{
"success": true,
"message": "File written successfully to: /home/user/documents/example.txt"
}
```
### Read File Response
```json
{
"success": true,
"data": "SGVsbG8gV29ybGQh",
"name": "example.txt",
"size": 12,
"mediaType": "text/plain"
}
```
### Error Response
```json
{
"success": false,
"error": "Error message"
}
```
## Code Examples
### JavaScript/Node.js Example
```javascript
const axios = require('axios');
const bytebot = {
baseUrl: 'http://localhost:9990/computer-use/computer',
async action(params) {
try {
const response = await axios.post(this.baseUrl, params);
return response.data;
} catch (error) {
console.error('Error:', error.response?.data || error.message);
throw error;
}
},
// Convenience methods
async moveMouse(x, y) {
return this.action({
action: 'move_mouse',
coordinates: { x, y }
});
},
async clickMouse(x, y, button = 'left') {
return this.action({
action: 'click_mouse',
coordinates: { x, y },
button
});
},
async typeText(text) {
return this.action({
action: 'type_text',
text
});
},
async pasteText(text) {
return this.action({
action: 'paste_text',
text
});
},
async switchApplication(application) {
return this.action({
action: 'application',
application
});
},
async screenshot() {
return this.action({ action: 'screenshot' });
}
};
// Example usage:
async function example() {
// Switch to Firefox
await bytebot.switchApplication('firefox');
// Navigate to a website
await bytebot.moveMouse(100, 35);
await bytebot.clickMouse(100, 35);
await bytebot.typeText('https://example.com');
await bytebot.action({
action: 'press_keys',
keys: ['enter'],
press: 'down'
});
// Wait for page to load
await bytebot.action({
action: 'wait',
duration: 2000
});
// Paste some special characters
await bytebot.pasteText('© 2025 Example Corp™ - €100');
// Take a screenshot
const result = await bytebot.screenshot();
console.log('Screenshot taken!');
}
example().catch(console.error);
================================================
FILE: docs/api-reference/endpoint/create.mdx
================================================
---
title: 'Create Plant'
openapi: 'POST /plants'
---
================================================
FILE: docs/api-reference/endpoint/delete.mdx
================================================
---
title: 'Delete Plant'
openapi: 'DELETE /plants/{id}'
---
================================================
FILE: docs/api-reference/endpoint/get.mdx
================================================
---
title: 'Get Plants'
openapi: 'GET /plants'
---
================================================
FILE: docs/api-reference/endpoint/webhook.mdx
================================================
---
title: 'New Plant'
openapi: 'WEBHOOK /plant/webhook'
---
================================================
FILE: docs/api-reference/introduction.mdx
================================================
---
title: "API Reference"
description: "Overview of the Bytebot API endpoints for programmatic control"
---
# Bytebot API Overview
Bytebot provides two main APIs for programmatic control:
## 1. Agent API (Task Management)
The Agent API runs on port 9991 and provides high-level task management:
Create, manage, and monitor AI-powered tasks programmatically
WebSocket connections and real-time updates for custom UIs
### Agent API Base URL
```
http://localhost:9991
```
### Example Task Creation
```bash
curl -X POST http://localhost:9991/tasks \
-H "Content-Type: application/json" \
-d '{
"description": "Download invoices from webmail and organize by date",
"priority": "HIGH"
}'
```
## 2. Desktop API (Direct Control)
The Desktop API runs on port 9990 and provides low-level desktop control:
Direct control of mouse, keyboard, and screen capture
Code examples for common automation scenarios
### Desktop API Base URL
```
http://localhost:9990
```
### Example Desktop Control
```bash
curl -X POST http://localhost:9990/computer-use \
-H "Content-Type: application/json" \
-d '{"action": "screenshot"}'
```
### MCP Support
The Desktop API also exposes an MCP (Model Context Protocol) endpoint:
```
http://localhost:9990/mcp
```
Connect your MCP client to access desktop control tools over SSE.
## Authentication
- **Local Access**: No authentication required by default
- **Remote Access**: Configure authentication based on your security requirements
- **Production**: Implement API keys, OAuth, or other authentication methods
## Response Formats
### Agent API Response
```json
{
"id": "task-123",
"status": "RUNNING",
"description": "Your task description",
"messages": [...],
"createdAt": "2024-01-01T00:00:00Z"
}
```
### Desktop API Response
```json
{
"success": true,
"data": { ... }, // Response data specific to the action
"error": null // Error message if success is false
}
```
## Error Handling
Both APIs use standard HTTP status codes:
| Status Code | Description |
| ----------- | ------------------------------------ |
| 200 | Success |
| 201 | Created (new resource) |
| 400 | Bad Request - Invalid parameters |
| 401 | Unauthorized - Authentication failed |
| 404 | Not Found - Resource doesn't exist |
| 500 | Internal Server Error |
## Rate Limiting
- **Agent API**: No hard limits, but consider task queue capacity
- **Desktop API**: No rate limiting, but rapid actions may impact desktop performance
## Best Practices
1. **Use Agent API for high-level automation** - Let the AI handle complexity
2. **Use Desktop API for precise control** - When you need exact actions
3. **Combine both APIs** - Create tasks via Agent API, monitor via Desktop API
4. **Handle errors gracefully** - Implement retry logic for transient failures
5. **Monitor resource usage** - Both APIs can be resource-intensive
## Next Steps
Get your APIs running
See the APIs in action
================================================
FILE: docs/api-reference/openapi.json
================================================
{
"openapi": "3.1.0",
"info": {
"title": "OpenAPI Plant Store",
"description": "A sample API that uses a plant store as an example to demonstrate features in the OpenAPI specification",
"license": {
"name": "MIT"
},
"version": "1.0.0"
},
"servers": [
{
"url": "http://sandbox.mintlify.com"
}
],
"security": [
{
"bearerAuth": []
}
],
"paths": {
"/plants": {
"get": {
"description": "Returns all plants from the system that the user has access to",
"parameters": [
{
"name": "limit",
"in": "query",
"description": "The maximum number of results to return",
"schema": {
"type": "integer",
"format": "int32"
}
}
],
"responses": {
"200": {
"description": "Plant response",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Plant"
}
}
}
}
},
"400": {
"description": "Unexpected error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
},
"post": {
"description": "Creates a new plant in the store",
"requestBody": {
"description": "Plant to add to the store",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/NewPlant"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "plant response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Plant"
}
}
}
},
"400": {
"description": "unexpected error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
},
"/plants/{id}": {
"delete": {
"description": "Deletes a single plant based on the ID supplied",
"parameters": [
{
"name": "id",
"in": "path",
"description": "ID of plant to delete",
"required": true,
"schema": {
"type": "integer",
"format": "int64"
}
}
],
"responses": {
"204": {
"description": "Plant deleted",
"content": {}
},
"400": {
"description": "unexpected error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
}
},
"webhooks": {
"/plant/webhook": {
"post": {
"description": "Information about a new plant added to the store",
"requestBody": {
"description": "Plant added to the store",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/NewPlant"
}
}
}
},
"responses": {
"200": {
"description": "Return a 200 status to indicate that the data was received successfully"
}
}
}
}
},
"components": {
"schemas": {
"Plant": {
"required": [
"name"
],
"type": "object",
"properties": {
"name": {
"description": "The name of the plant",
"type": "string"
},
"tag": {
"description": "Tag to specify the type",
"type": "string"
}
}
},
"NewPlant": {
"allOf": [
{
"$ref": "#/components/schemas/Plant"
},
{
"required": [
"id"
],
"type": "object",
"properties": {
"id": {
"description": "Identification number of the plant",
"type": "integer",
"format": "int64"
}
}
}
]
},
"Error": {
"required": [
"error",
"message"
],
"type": "object",
"properties": {
"error": {
"type": "integer",
"format": "int32"
},
"message": {
"type": "string"
}
}
}
},
"securitySchemes": {
"bearerAuth": {
"type": "http",
"scheme": "bearer"
}
}
}
}
================================================
FILE: docs/core-concepts/agent-system.mdx
================================================
---
title: "Agent System"
description: "The AI brain that powers your self-hosted desktop automation"
---
## Overview
The Bytebot Agent System transforms a simple desktop container into an intelligent, autonomous computer user. By combining Claude AI with structured task management, it can understand natural language requests and execute complex workflows just like a human would.
## How the AI Agent Works
### The Brain: Multi-Model AI Integration
At the heart of Bytebot is a flexible AI integration that supports multiple models. Choose the AI that best fits your needs:
**Anthropic Claude** (Default):
- Best for complex reasoning and visual understanding
- Excellent at following detailed instructions
- Superior performance on desktop automation tasks
**OpenAI GPT Models**:
- Fast and reliable for general automation
- Strong code understanding and generation
- Cost-effective for routine tasks
**Google Gemini**:
- Efficient for high-volume tasks
- Good balance of speed and capability
- Excellent multilingual support
The agent with any model:
1. **Understands Context**: Processes your natural language requests with full conversation history
2. **Plans Actions**: Breaks down complex tasks into executable computer actions
3. **Adapts in Real-time**: Adjusts its approach based on what it sees on screen
4. **Learns from Feedback**: Improves task execution through conversation
### Conversation Flow
"Research competitors for my SaaS product and create a comparison table"
The AI model understands the request and plans: open browser → search → visit sites → extract data → create document
The agent controls the desktop: clicking, typing, taking screenshots, reading content
Real-time status updates and asks for clarification when needed
Completes the task and provides the output (files, screenshots, summaries)
## Task Management System
### Task Lifecycle
Tasks move through a structured lifecycle:
```mermaid
graph LR
A[Created] --> B[Queued]
B --> C[Running]
C --> D[Needs Help]
C --> E[Completed]
C --> F[Failed]
D --> C
```
### Task Properties
Each task contains:
- **Description**: What needs to be done
- **Priority**: Urgent, High, Medium, or Low
- **Status**: Current state in the lifecycle
- **Type**: Immediate or Scheduled
- **History**: All messages and actions taken
### Smart Task Processing
The agent processes tasks intelligently:
1. **Priority Queue**: Urgent tasks run first
2. **Error Recovery**: Automatically retries failed actions
3. **Human in the Loop**: Asks for help when stuck
4. **Context Preservation**: Maintains conversation history across sessions
## Real-world Capabilities
### What the Agent Can Do
- Browse websites
- Fill out forms
- Extract data
- Download files
- Monitor changes
- Create documents
- Edit spreadsheets
- Generate reports
- Organize files
- Convert formats
- Access webmail through browser
- Read and extract information
- Fill contact forms
- Navigate communication portals
- Handle verification flows
- Extract from PDFs
- Process CSV files
- Create visualizations
- Generate summaries
- Transform data
## Technical Architecture
### Core Components
1. **NestJS Agent Service**
- Integrates with multiple AI provider APIs (Anthropic, OpenAI, Google)
- Handles WebSocket connections
- Coordinates with desktop API
2. **Message System**
- Structured conversation format
- Supports text and images
- Maintains full context
- Enables rich interactions
3. **Database Schema**
```sql
Tasks: id, description, status, priority, timestamps
Messages: id, task_id, role, content, timestamps
Summaries: id, task_id, content, parent_id
```
4. **Computer Action Bridge**
- Translates AI decisions to desktop actions
- Handles screenshots and feedback
- Manages action timing
- Provides error handling
### API Endpoints
Key endpoints for programmatic control:
```typescript
// Create a new task
POST /tasks
{
"description": "Your task description",
"priority": "HIGH",
"type": "IMMEDIATE"
}
// Get task status
GET /tasks/:id
// Send a message
POST /tasks/:id/messages
{
"content": "Additional instructions"
}
// Get task history
GET /tasks/:id/messages
```
## Chat UI Features
The web interface provides:
### Real-time Interaction
- Live chat with the AI agent
- Instant status updates
- Progress indicators
- Error notifications
### Visual Feedback
- Embedded desktop viewer
- Screenshot history
- Action replay
- Task timeline
### Task Management
- Create and prioritize tasks
- View active and completed tasks
- Export conversation logs
- Manage task queues
## Security & Privacy
### Data Isolation
- All processing happens in your infrastructure
- No data sent to external services (except your chosen AI provider API)
- Conversations stored locally
- Complete audit trail
### Access Control
- Configurable authentication
- API key management
- Network isolation options
## Extending the Agent
### Integration Points
- External API calls via the Agent API
- Custom AI prompts for specialized workflows
- MCP protocol support for tool integration
### Best Practices
1. **Clear Instructions**: Be specific about desired outcomes
2. **Break Down Complex Tasks**: Use multiple smaller tasks for better results
3. **Provide Context**: Include relevant files or URLs
4. **Monitor Progress**: Watch the desktop view for real-time feedback
5. **Review Results**: Verify outputs meet requirements
## Troubleshooting
- Check your AI provider API key is valid
- Verify agent service is running
- Review logs for errors
- Ensure sufficient API credits/quota with your provider
- Monitor system resources
- Check network latency
- Reduce screenshot frequency
- Optimize AI prompts for your chosen model
- Consider switching to a faster model (e.g., Gemini Flash)
## Next Steps
Get your agent running
Integrate with your apps
See what's possible
Optimize your workflows
================================================
FILE: docs/core-concepts/architecture.mdx
================================================
---
title: "Architecture"
description: "How Bytebot's desktop agent works under the hood"
---
## Overview
Bytebot is a self-hosted AI desktop agent built with a modular architecture. It combines a Linux desktop environment with AI to create an autonomous computer user that can perform tasks through natural language instructions.
## System Architecture
The system consists of four main components that work together:
### 1. Bytebot Desktop Container
The foundation of the system - a virtual Linux desktop that provides:
- **Ubuntu 22.04 LTS** base for stability and compatibility
- **XFCE4 Desktop** for a lightweight, responsive UI
- **bytebotd Daemon** - The automation service built on nutjs that executes computer actions
- **Pre-installed Applications**: Firefox ESR, Thunderbird, text editors, and development tools
- **noVNC** for remote desktop access
**Key Features:**
- Runs completely isolated from your host system
- Consistent environment across different platforms
- Can be customized with additional software
- Accessible via REST API on port 9990
- MCP SSE endpoint available at `/mcp`
- Uses shared types from `@bytebot/shared` package
### 2. AI Agent Service
The brain of the system - orchestrates tasks using an LLM:
- **NestJS Framework** for robust, scalable backend
- **LLM Integration** supporting Anthropic Claude, OpenAI GPT, and Google Gemini models
- **WebSocket Support** for real-time updates
- **Computer Use API Client** to control the desktop
- **Prisma ORM** for database operations
- **Tool definitions** for computer actions (mouse, keyboard, screenshots)
**Responsibilities:**
- Interprets natural language requests
- Plans sequences of computer actions
- Manages task state and progress
- Handles errors and retries
- Provides real-time task updates via WebSocket
### 3. Web Task Interface
The user interface for interacting with your AI agent:
- **Next.js 15 Application** with TypeScript for type safety
- **Embedded VNC Viewer** to watch the desktop in action
- **Task Management** UI with status badges
- **WebSocket Connections** for live updates
- **Reusable components** for consistent UI
- **API utilities** for streamlined server communication
**Features:**
- Task creation and management interface
- Desktop tab for direct manual control
- Real-time desktop viewer with takeover mode
- Task history and status tracking
- Responsive design for all devices
### 4. PostgreSQL Database
Persistent storage for the agent system:
- **Tasks Table**: Stores task details, status, and metadata
- **Messages Table**: Stores AI conversation history
- **Prisma ORM** for type-safe database access
## Data Flow
### Task Execution Flow
User describes a task in natural language via the chat UI
Agent service creates a task record and adds it to the processing queue
The LLM analyzes the task and generates a plan of computer actions
Agent sends computer actions to bytebotd via REST API or MCP
bytebotd executes actions (mouse, keyboard, screenshots) on the desktop
Agent receives results, updates task status, and continues or completes
Results and status updates are sent back to the user in real-time
### Communication Protocols
```mermaid
graph LR
A[Tasks UI] -->|WebSocket| B[Agent Service]
A -->|HTTP Proxy| C[Desktop VNC]
B -->|REST/MCP| D[Desktop API]
B -->|SQL| E[PostgreSQL]
B -->|HTTPS| F[LLM Provider]
D -->|IPC| G[bytebotd]
```
## Security Architecture
### Isolation Layers
1. **Container Isolation**
- Each desktop runs in its own Docker container
- No access to host filesystem by default
- Network isolation with explicit port mapping
2. **Process Isolation**
- bytebotd runs as non-root user
- Separate processes for different services
- Resource limits enforced by Docker
3. **Network Security**
- Services only accessible from localhost by default
- Can be configured with authentication
- HTTPS/WSS for external connections
### API Security
- **Desktop API**: No authentication by default (localhost only). Supports REST and MCP.
- **Agent API**: Can be secured with API keys
- **Database**: Password protected, not exposed externally
Default configuration is for development. For production:
- Enable authentication on all APIs
- Use HTTPS/WSS for all connections
- Implement network policies
- Rotate credentials regularly
## Deployment Patterns
### Single User (Development)
```yaml
Services: All on one machine
Scale: 1 instance each
Use Case: Personal automation, development
Resources: 4GB RAM, 2 CPU cores
```
### Production Deployment
```yaml
Services: All services on dedicated hardware
Scale: Single instance (1 agent, 1 desktop)
Use Case: Business automation
Resources: 8GB+ RAM, 4+ CPU cores
```
### Enterprise Deployment
```yaml
Services: Kubernetes orchestration
Scale: Single instance with high availability
Use Case: Organization-wide automation
Resources: Dedicated nodes
```
## Extension Points
### Custom Tools
Add specialized software to the desktop:
```dockerfile
FROM bytebot/desktop:latest
RUN apt-get update && apt-get install -y \
your-custom-tools
```
### AI Integrations
Extend agent capabilities:
- Custom tools for the LLM
- Additional AI models
- Specialized prompts
- Domain-specific knowledge
## Performance Considerations
### Resource Usage
- **Desktop Container**: ~1GB RAM idle, 2GB+ active
- **Agent Service**: ~256MB RAM
- **UI Service**: ~128MB RAM
- **Database**: ~256MB RAM
### Optimization Tips
1. Allocate sufficient resources to containers
2. Limit concurrent tasks to prevent overload
3. Monitor resource usage regularly
4. Use LiteLLM proxy for provider flexibility
## Next Steps
Learn about the AI agent capabilities
Explore the virtual desktop environment
Integrate with your applications
Deploy your own instance
================================================
FILE: docs/core-concepts/desktop-environment.mdx
================================================
---
title: "Desktop Environment"
description: "The virtual Linux desktop where Bytebot performs tasks"
---
## Overview
The Bytebot Desktop Environment (also called Bytebot Core) is a complete Linux desktop that runs in a Docker container. This is where Bytebot does its work - clicking buttons, typing text, browsing websites, and using applications just like you would.
## Why a Virtual Desktop?
### Complete Isolation
- **No Risk to Host**: All actions happen inside the container
- **Sandboxed Environment**: Desktop can't access your host system
- **Easy Reset**: Destroy and recreate in seconds
- **Clean Workspace**: Each restart provides a fresh environment
### Consistency Everywhere
- **Platform Independent**: Same environment on Mac, Windows, or Linux
- **Reproducible**: Identical setup every time
- **Version Control**: Pin specific versions for stability
- **No Dependencies**: Everything included in the container
### Built for Automation
- **Predictable UI**: Consistent element positioning
- **Clean Environment**: No popups or distractions
- **Automation-Ready**: Optimized for programmatic control
- **Fast Startup**: Desktop ready in seconds
## Technical Stack
### Base System
- **Ubuntu 22.04 LTS**: Stable, well-supported Linux distribution
- **XFCE4 Desktop**: Lightweight, responsive desktop environment
- **X11 Display Server**: Standard Linux graphics system
- **supervisord**: Service management
### Pre-installed Software
- Firefox ESR (Extended Support Release)
- Pre-configured for automation
- Clean profile without distractions
- Text editor
- Office tools
- PDF viewer
- File manager
- Thunderbird email client
- Terminal emulator
- 1Password password manager
- Visual Studio Code (VSCode)
- Git version control
- Python 3 environment
### Core Services
1. **bytebotd Daemon**
- Runs on port 9990
- Handles all automation requests
- Built on nutjs framework
- Provides REST API
2. **noVNC Web Client**
- Browser-based desktop access
- No client installation needed
- WebSocket proxy included
3. **Supervisor**
- Process management
- Service monitoring
- Automatic restarts
- Log management
## Desktop Features
### Display Configuration
```bash
# Resolution
1920x1080 @ 24-bit color
```
### User Environment
- **Username**: `user`
- **Home Directory**: `/home/user`
- **Sudo Access**: Yes (passwordless)
- **Desktop Session**: Auto-login enabled
### File System
```
/home/user/
├── Desktop/ # Desktop shortcuts
├── Documents/ # User documents
├── Downloads/ # Browser downloads
├── .config/ # Application configs
└── .local/ # User data
```
## Accessing the Desktop
### Web Browser (Recommended)
Navigate to `http://localhost:9990/vnc` for instant access:
- No software installation required
- Works on any device with a browser
- Supports touch devices
- Clipboard sharing
### MCP Control
The core container also exposes an [MCP](https://github.com/rekog-labs/MCP-Nest) endpoint.
Connect your MCP client to `http://localhost:9990/mcp` to invoke these tools over SSE.
```json
{
"mcpServers": {
"bytebot": {
"command": "npx",
"args": [
"mcp-remote",
"http://127.0.0.1:9990/mcp",
"--transport",
"http-first"
]
}
}
}
```
### Direct API Control
Most efficient for automation:
```bash
# Take a screenshot
curl -X POST http://localhost:9990/computer-use \
-H "Content-Type: application/json" \
-d '{"action": "screenshot"}'
# Move mouse
curl -X POST http://localhost:9990/computer-use \
-H "Content-Type: application/json" \
-d '{"action": "move_mouse", "coordinate": {"x": 500, "y": 300}}'
```
## Customization
### Adding Software
Create a custom Dockerfile:
```dockerfile
FROM ghcr.io/bytebot-ai/bytebot-desktop:edge
# Install additional packages
RUN apt-get update && apt-get install -y \
slack-desktop \
zoom \
your-custom-app
# Copy configuration files
COPY configs/ /home/user/.config/
```
## Performance Optimization
### Resource Allocation
```yaml
# Recommended settings
deploy:
resources:
limits:
cpus: '2'
memory: 4G
reservations:
cpus: '1'
memory: 2G
```
## Security Hardening
Default configuration prioritizes ease of use. For production, apply these security measures:
### Essential Security Steps
1. **Change Default Passwords**
```bash
# Set user password
passwd bytebot
```
2. **Limit Network Access**
```yaml
# Whitelist specific domains
environment:
- ALLOWED_DOMAINS=company.com,trusted-site.com
# Or restrict to local network only
ports:
- "10.0.0.0/8:9990:9990"
```
## Troubleshooting
Check logs:
```bash
docker logs bytebot-desktop
```
Common issues:
- Insufficient memory
- Port conflicts
- Display server errors
Monitor resources:
```bash
docker stats bytebot-desktop
```
Solutions:
- Increase memory allocation
- Check disk space
- Update container image
## Best Practices
1. **Regular Updates**: Keep the base image updated for security patches
2. **Persistent Storage**: Mount volumes for important data
3. **Backup Configurations**: Save customizations outside the container
4. **Monitor Resources**: Track CPU/memory usage
5. **Clean Temporary Files**: Periodic cleanup for performance
## Next Steps
Deploy your first agent
Control the desktop programmatically
Add AI capabilities
Set up authentication
================================================
FILE: docs/core-concepts/rpa-comparison.mdx
================================================
---
title: "Bytebot vs Traditional RPA"
description: "How Bytebot revolutionizes enterprise automation beyond traditional RPA tools"
---
# The Next Generation of Enterprise Automation
Bytebot represents a fundamental shift in how businesses approach process automation. While traditional RPA tools like UiPath, Automation Anywhere, and Blue Prism require extensive scripting and brittle workflows, Bytebot leverages AI to understand and execute tasks like a human would.
## Traditional RPA Limitations
Traditional RPA breaks when UI elements change even slightly
Requires specialized developers and lengthy implementation cycles
Constant updates needed as applications evolve
Can't handle unexpected scenarios or variations
## How Bytebot is Different
### Visual Intelligence vs Element Mapping
**Traditional RPA:**
```xml
```
**Bytebot:**
```
"Click the blue Submit button at the bottom of the form"
```
Bytebot understands interfaces visually, just like a human. It doesn't rely on fragile technical selectors that break with every update.
### Natural Language vs Complex Scripting
**Traditional RPA Workflow:**
- Design in Studio
- Map every element
- Script error handling
- Test extensively
- Deploy with fingers crossed
- Fix when it breaks (often)
**Bytebot Workflow:**
- Describe what you need
- Bytebot figures it out
- Handles errors intelligently
- Adapts to changes automatically
## Real-World Enterprise Examples
### Financial Services Automation
```csharp
// 500+ lines of code to handle one banking portal
var loginPage = new LoginPageObject();
loginPage.WaitForElement("username", 30);
loginPage.EnterText("username", credentials.User);
loginPage.EnterText("password", credentials.Pass);
// Handle 2FA with complex conditional logic
if (loginPage.Has2FAPrompt()) {
var method = loginPage.Get2FAMethod();
switch(method) {
case "SMS":
// 50 more lines of code
case "Email":
// 50 more lines of code
case "Authenticator":
// 50 more lines of code
}
}
// Download statements with exact selectors
navigation.ClickElement("xpath://div[@id='acct-menu']");
navigation.ClickElement("xpath://a[contains(@href,'statements')]");
// ... continues for hundreds more lines
```
```
Task: "Log into Chase banking portal, navigate to statements,
download all statements from last month for account ending in 4521,
and save them to Finance/BankStatements/Chase/"
That's it. Bytebot handles everything - including 2FA - automatically.
```
### Multi-System Integration
A FinTech company needed to automate operators who:
1. Log into multiple banking portals with 2FA
2. Download transaction files
3. Run proprietary scripts on those files
4. Upload results to internal systems
**Traditional RPA Challenge:**
- 6 months to implement
- Breaks monthly with UI changes
- Requires dedicated maintenance team
- Can't handle new banks without development
- Complex 2FA handling logic for each bank
**Bytebot Solution:**
- Deployed in 1 week
- Adapts to UI changes automatically
- 2FA handled automatically via password manager
- New banks added with simple instructions
- Zero manual intervention required
## Performance Comparison
| Metric | Traditional RPA | Bytebot |
|--------|----------------|---------|
| **Implementation Time** | 3-6 months | 1-2 weeks |
| **Developer Requirement** | RPA specialists | Any technical user |
| **Maintenance Effort** | 40% of dev time | Near zero |
| **Handling UI Changes** | Breaks immediately | Adapts automatically |
| **Error Recovery** | Pre-scripted only | Intelligent adaptation |
| **New Process Addition** | Weeks of development | Minutes to describe |
| **Cost** | $100k+ annually | Self-hosted on your infrastructure |
## Common RPA Migration Patterns
### 1. Invoice Processing
**Before (UiPath):**
- 2000+ lines of workflow XML
- Breaks when vendor portal updates
- Requires exact folder structures
- Failed on unexpected popups
**After (Bytebot):**
- One paragraph description
- Handles portal changes
- Asks for help when needed
- Processes variations intelligently
### 2. Compliance Reporting
**Before (Automation Anywhere):**
- Complex bot orchestration
- Separate bots per system
- Rigid scheduling
- No flexibility
**After (Bytebot):**
- Single unified workflow
- Natural language instructions
- Dynamic adaptation
- Human collaboration when needed
### 3. Data Migration
**Before (Blue Prism):**
- Massive process definitions
- Exact field mapping required
- Breaks on data variations
- Limited error handling
**After (Bytebot):**
- Describe the mapping rules
- Handles variations intelligently
- Asks for clarification
- Visual validation included
## Integration with Existing RPA
Bytebot can work alongside existing RPA investments:
```mermaid
graph LR
A[Legacy RPA] -->|Handles stable processes| B[Structured Systems]
C[Bytebot] -->|Handles complex/changing processes| D[Dynamic Systems]
C -->|Takes over when RPA fails| A
E[Human Operator] -->|Guides via takeover mode| C
```
## Enterprise Architecture
### Deployment Options
Deploy in your data center for maximum security and compliance
Use your AWS/Azure/GCP infrastructure with full control
Process sensitive data locally, leverage cloud for scaling
Completely isolated deployment for classified environments
### Security & Compliance
- **Data Sovereignty**: All processing on your infrastructure
- **Audit Trails**: Complete logs of every action
- **Access Control**: Integrate with your IAM/SSO
- **Compliance**: SOC2, HIPAA, PCI-DSS compatible deployments
## Getting Started with Migration
List your current RPA workflows, especially:
- Those that break frequently
- Require regular maintenance
- Handle multiple systems
- Need human decision points
Pick one problematic workflow:
- Document the business process
- Deploy Bytebot
- Describe the task naturally
- Compare results
As confidence grows:
- Migrate more complex processes
- Retire brittle RPA bots
- Reduce maintenance overhead
- Scale across departments
## Next Steps
Deploy Bytebot in your environment
View source code and contribute
Join our Discord for support
Get help with enterprise deployments
**Ready to move beyond traditional RPA?** Bytebot brings human-like intelligence to process automation, eliminating the brittleness and complexity of traditional tools while delivering enterprise-grade reliability and security.
================================================
FILE: docs/deployment/helm.mdx
================================================
---
title: "Helm Deployment"
description: "Deploy Bytebot on Kubernetes using Helm charts"
---
# Deploy Bytebot on Kubernetes with Helm
Helm provides a simple way to deploy Bytebot on Kubernetes clusters.
## Prerequisites
- Kubernetes cluster (1.19+)
- Helm 3.x installed
- kubectl configured
- 8GB+ available memory in cluster
## Quick Start
```bash
git clone https://github.com/bytebot-ai/bytebot.git
cd bytebot
```
Create a `values.yaml` file with at least one API key:
```yaml
bytebot-agent:
apiKeys:
anthropic:
value: "sk-ant-your-key-here"
# Optional: Add more providers
# openai:
# value: "sk-your-key-here"
# gemini:
# value: "your-key-here"
```
```bash
helm install bytebot ./helm \
--namespace bytebot \
--create-namespace \
-f values.yaml
```
```bash
# Port-forward for local access
kubectl port-forward -n bytebot svc/bytebot-ui 9992:9992
# Access at http://localhost:9992
```
## Basic Configuration
### API Keys
Configure at least one AI provider:
```yaml
bytebot-agent:
apiKeys:
anthropic:
value: "sk-ant-your-key-here"
openai:
value: "sk-your-key-here"
gemini:
value: "your-key-here"
```
### Resource Limits (Optional)
Adjust resources based on your needs:
```yaml
# Desktop container (where automation runs)
desktop:
resources:
requests:
memory: "2Gi"
cpu: "1"
limits:
memory: "4Gi"
cpu: "2"
# Agent (AI orchestration)
agent:
resources:
requests:
memory: "1Gi"
cpu: "500m"
```
### External Access (Optional)
Enable ingress for domain-based access:
```yaml
ui:
ingress:
enabled: true
hostname: bytebot.your-domain.com
tls: true
```
## Accessing Bytebot
### Local Access (Recommended)
```bash
kubectl port-forward -n bytebot svc/bytebot-ui 9992:9992
```
Access at: http://localhost:9992
### External Access
If you configured ingress:
- Access at: https://bytebot.your-domain.com
## Verifying Deployment
Check that all pods are running:
```bash
kubectl get pods -n bytebot
```
Expected output:
```
NAME READY STATUS RESTARTS AGE
bytebot-agent-xxxxx 1/1 Running 0 2m
bytebot-desktop-xxxxx 1/1 Running 0 2m
bytebot-postgresql-0 1/1 Running 0 2m
bytebot-ui-xxxxx 1/1 Running 0 2m
```
## Troubleshooting
### Pods Not Starting
Check pod status:
```bash
kubectl describe pod -n bytebot
```
Common issues:
- Insufficient memory/CPU: Check node resources with `kubectl top nodes`
- Missing API keys: Verify your values.yaml configuration
### Connection Issues
Test service connectivity:
```bash
kubectl logs -n bytebot deployment/bytebot-agent
```
### View Logs
```bash
# All logs
kubectl logs -n bytebot -l app=bytebot --tail=100
# Specific component
kubectl logs -n bytebot deployment/bytebot-agent
```
## Upgrading
```bash
# Update your values.yaml as needed, then:
helm upgrade bytebot ./helm -n bytebot -f values.yaml
```
## Uninstalling
```bash
# Remove Bytebot
helm uninstall bytebot -n bytebot
# Clean up namespace
kubectl delete namespace bytebot
```
## Advanced Configuration
If using Kubernetes secret management (Vault, Sealed Secrets, etc.):
```yaml
bytebot-agent:
apiKeys:
anthropic:
useExisting: true
secretName: "my-api-keys"
secretKey: "anthropic-key"
```
Create the secret manually:
```bash
kubectl create secret generic my-api-keys \
--namespace bytebot \
--from-literal=anthropic-key="sk-ant-your-key"
```
For centralized LLM management, use the included LiteLLM proxy:
```bash
helm install bytebot ./helm \
-f values-proxy.yaml \
--namespace bytebot \
--create-namespace \
--set bytebot-llm-proxy.env.ANTHROPIC_API_KEY="your-key"
```
This provides:
- Centralized API key management
- Request routing and load balancing
- Rate limiting and retry logic
Configure persistent storage:
```yaml
desktop:
persistence:
enabled: true
size: "20Gi"
storageClass: "fast-ssd"
postgresql:
persistence:
size: "20Gi"
storageClass: "fast-ssd"
```
```yaml
# Network policies
networkPolicy:
enabled: true
# Pod security
podSecurityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 1000
# Enable authentication
auth:
enabled: true
type: "basic"
username: "admin"
password: "changeme" # Use secrets in production!
```
## Next Steps
Integrate Bytebot with your applications
Use any LLM provider with Bytebot
**Need help?** Join our [Discord community](https://discord.com/invite/d9ewZkWPTP) or check our [GitHub discussions](https://github.com/bytebot-ai/bytebot/discussions).
================================================
FILE: docs/deployment/litellm.mdx
================================================
---
title: "LiteLLM Integration"
description: "Use any LLM provider with Bytebot through LiteLLM proxy"
---
# Connect Any LLM to Bytebot with LiteLLM
LiteLLM acts as a unified proxy that lets you use 100+ LLM providers with Bytebot - including Azure OpenAI, AWS Bedrock, Anthropic, Hugging Face, Ollama, and more. This guide shows you how to set up LiteLLM with Bytebot.
## Why Use LiteLLM?
Use Azure, AWS, GCP, Anthropic, OpenAI, Cohere, and local models
Monitor spending across all providers in one place
Distribute requests across multiple models and providers
Automatic failover when primary models are unavailable
## Quick Start with Bytebot's Built-in LiteLLM Proxy
Bytebot includes a pre-configured LiteLLM proxy service that makes it easy to use any LLM provider. Here's how to set it up:
The easiest way is to use the proxy-enabled Docker Compose file:
```bash
# Clone Bytebot
git clone https://github.com/bytebot-ai/bytebot.git
cd bytebot
# Set up your API keys in docker/.env
cat > docker/.env << EOF
# Add any combination of these keys
ANTHROPIC_API_KEY=sk-ant-your-key-here
OPENAI_API_KEY=sk-your-key-here
GEMINI_API_KEY=your-key-here
EOF
# Start Bytebot with LiteLLM proxy
docker-compose -f docker/docker-compose.proxy.yml up -d
```
This automatically:
- Starts the `bytebot-llm-proxy` service on port 4000
- Configures the agent to use the proxy via `BYTEBOT_LLM_PROXY_URL`
- Makes all configured models available through the proxy
To add custom models or providers, edit the LiteLLM config:
```yaml
# packages/bytebot-llm-proxy/litellm-config.yaml
model_list:
# Add Azure OpenAI
- model_name: azure-gpt-4o
litellm_params:
model: azure/gpt-4o-deployment
api_base: https://your-resource.openai.azure.com/
api_key: os.environ/AZURE_API_KEY
api_version: "2024-02-15-preview"
# Add AWS Bedrock
- model_name: claude-bedrock
litellm_params:
model: bedrock/anthropic.claude-3-5-sonnet
aws_region_name: us-east-1
# Add local models via Ollama
- model_name: local-llama
litellm_params:
model: ollama/llama3:70b
api_base: http://host.docker.internal:11434
```
Then rebuild:
```bash
docker-compose -f docker/docker-compose.proxy.yml up -d --build
```
The Bytebot agent automatically queries the proxy for available models:
```bash
# Check available models through Bytebot API
curl http://localhost:9991/tasks/models
# Or directly from LiteLLM proxy
curl http://localhost:4000/model/info
```
The UI will show all available models in the model selector.
## How It Works
### Architecture
```mermaid
graph LR
A[Bytebot UI] -->|Select Model| B[Bytebot Agent]
B -->|BYTEBOT_LLM_PROXY_URL| C[LiteLLM Proxy :4000]
C -->|Route Request| D[Anthropic API]
C -->|Route Request| E[OpenAI API]
C -->|Route Request| F[Google API]
C -->|Route Request| G[Any Provider]
```
### Key Components
1. **bytebot-llm-proxy Service**: A LiteLLM instance running in Docker that:
- Runs on port 4000 within the Bytebot network
- Uses the config from `packages/bytebot-llm-proxy/litellm-config.yaml`
- Inherits API keys from environment variables
2. **Agent Integration**: The Bytebot agent:
- Checks for `BYTEBOT_LLM_PROXY_URL` environment variable
- If set, queries the proxy at `/model/info` for available models
- Routes all LLM requests through the proxy
3. **Pre-configured Models**: Out of the box support for:
- Anthropic: Claude Opus 4, Claude Sonnet 4
- OpenAI: GPT-4.1, GPT-4o
- Google: Gemini 2.5 Pro, Gemini 2.5 Flash
## Provider Configurations
### Azure OpenAI
```yaml
model_list:
- model_name: azure-gpt-4o
litellm_params:
model: azure/gpt-4o-deployment-name
api_base: https://your-resource.openai.azure.com/
api_key: your-azure-key
api_version: "2024-02-15-preview"
- model_name: azure-gpt-4o-vision
litellm_params:
model: azure/gpt-4o-deployment-name
api_base: https://your-resource.openai.azure.com/
api_key: your-azure-key
api_version: "2024-02-15-preview"
supports_vision: true
```
### AWS Bedrock
```yaml
model_list:
- model_name: claude-bedrock
litellm_params:
model: bedrock/anthropic.claude-3-5-sonnet-20241022-v2:0
aws_region_name: us-east-1
# Uses AWS credentials from environment
- model_name: llama-bedrock
litellm_params:
model: bedrock/meta.llama3-70b-instruct-v1:0
aws_region_name: us-east-1
```
### Google Vertex AI
```yaml
model_list:
- model_name: gemini-vertex
litellm_params:
model: vertex_ai/gemini-1.5-pro
vertex_project: your-gcp-project
vertex_location: us-central1
# Uses GCP credentials from environment
```
### Local Models (Ollama)
```yaml
model_list:
- model_name: local-llama
litellm_params:
model: ollama/llama3:70b
api_base: http://ollama:11434
- model_name: local-mixtral
litellm_params:
model: ollama/mixtral:8x7b
api_base: http://ollama:11434
```
### Hugging Face
```yaml
model_list:
- model_name: hf-llama
litellm_params:
model: huggingface/meta-llama/Llama-3-70b-chat-hf
api_key: hf_your_token
```
## Advanced Features
### Load Balancing
Distribute requests across multiple providers:
```yaml
model_list:
- model_name: gpt-4o
litellm_params:
model: gpt-4o
api_key: sk-openai-key
- model_name: gpt-4o # Same name for load balancing
litellm_params:
model: azure/gpt-4o
api_base: https://azure.openai.azure.com/
api_key: azure-key
router_settings:
routing_strategy: "least-busy" # or "round-robin", "latency-based"
```
### Fallback Models
Configure automatic failover:
```yaml
model_list:
- model_name: primary-model
litellm_params:
model: claude-3-5-sonnet-20241022
api_key: sk-ant-key
- model_name: fallback-model
litellm_params:
model: gpt-4o
api_key: sk-openai-key
router_settings:
model_group_alias:
"smart-model": ["primary-model", "fallback-model"]
# Use "smart-model" in Bytebot config
```
### Cost Controls
Set spending limits and track usage:
```yaml
general_settings:
master_key: sk-litellm-master
database_url: "postgresql://user:pass@localhost:5432/litellm"
# Budget limits
max_budget: 100 # $100 monthly limit
budget_duration: "30d"
# Per-model limits
model_max_budget:
gpt-4o: 50
claude-3-5-sonnet: 50
litellm_settings:
callbacks: ["langfuse"] # For detailed tracking
```
### Rate Limiting
Prevent API overuse:
```yaml
model_list:
- model_name: rate-limited-gpt
litellm_params:
model: gpt-4o
api_key: sk-key
rpm: 100 # Requests per minute
tpm: 100000 # Tokens per minute
```
## Alternative Setup: External LiteLLM Proxy
If you prefer to run LiteLLM separately or have an existing LiteLLM deployment:
### Option 1: Modify docker-compose.yml
```yaml
# docker-compose.yml (without built-in proxy)
services:
bytebot-agent:
environment:
# Point to your external LiteLLM instance
- BYTEBOT_LLM_PROXY_URL=http://your-litellm-server:4000
# ... rest of config
```
### Option 2: Use Environment Variable
```bash
# Set the proxy URL before starting
export BYTEBOT_LLM_PROXY_URL=http://your-litellm-server:4000
# Start normally
docker-compose -f docker/docker-compose.yml up -d
```
### Option 3: Run Standalone LiteLLM
```bash
# Run your own LiteLLM instance
docker run -d \
--name litellm-external \
-p 4000:4000 \
-v $(pwd)/custom-config.yaml:/app/config.yaml \
-e ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY \
ghcr.io/berriai/litellm:main \
--config /app/config.yaml
# Then start Bytebot with:
export BYTEBOT_LLM_PROXY_URL=http://localhost:4000
docker-compose up -d
```
## Kubernetes Setup
Deploy with Helm:
```yaml
# litellm-values.yaml
replicaCount: 2
image:
repository: ghcr.io/berriai/litellm
tag: main
service:
type: ClusterIP
port: 4000
config:
model_list:
- model_name: claude-3-5-sonnet
litellm_params:
model: claude-3-5-sonnet-20241022
api_key: ${ANTHROPIC_API_KEY}
general_settings:
master_key: ${LITELLM_MASTER_KEY}
# Then in Bytebot values.yaml:
agent:
openai:
enabled: true
apiKey: "${LITELLM_MASTER_KEY}"
baseUrl: "http://litellm:4000/v1"
model: "claude-3-5-sonnet"
```
## Monitoring & Debugging
### LiteLLM Dashboard
Access metrics and logs:
```bash
# Port forward to dashboard
kubectl port-forward svc/litellm 4000:4000
# Access at http://localhost:4000/ui
# Login with your master_key
```
### Debug Requests
Enable detailed logging:
```yaml
litellm_settings:
debug: true
detailed_debug: true
general_settings:
master_key: sk-key
store_model_in_db: true # Store request history
```
### Common Issues
Check model name matches exactly:
```bash
curl http://localhost:4000/v1/models \
-H "Authorization: Bearer sk-key"
```
Verify master key in both LiteLLM and Bytebot:
```bash
# Test LiteLLM
curl http://localhost:4000/v1/chat/completions \
-H "Authorization: Bearer sk-key" \
-H "Content-Type: application/json" \
-d '{"model": "your-model", "messages": [{"role": "user", "content": "test"}]}'
```
Check latency per provider:
```yaml
router_settings:
routing_strategy: "latency-based"
enable_pre_call_checks: true
```
## Best Practices
### Model Selection for Bytebot
Choose models with strong vision capabilities for best results:
- Claude 3.5 Sonnet (Best overall)
- GPT-4o (Good vision + reasoning)
- Gemini 1.5 Pro (Large context)
- Claude 3.5 Haiku (Fast + cheap)
- GPT-4o mini (Good balance)
- Gemini 1.5 Flash (Very fast)
- LLaVA (Vision support)
- Qwen-VL (Vision support)
- CogVLM (Vision support)
### Performance Optimization
```yaml
# Optimize for Bytebot workloads
router_settings:
routing_strategy: "latency-based"
cooldown_time: 60 # Seconds before retrying failed provider
num_retries: 2
request_timeout: 600 # 10 minutes for complex tasks
# Cache for repeated requests
cache: true
cache_params:
type: "redis"
host: "redis"
port: 6379
ttl: 3600 # 1 hour
```
### Security
```yaml
general_settings:
master_key: ${LITELLM_MASTER_KEY}
# IP allowlist
allowed_ips: ["10.0.0.0/8", "172.16.0.0/12"]
# Audit logging
store_model_in_db: true
# Encryption
encrypt_keys: true
# Headers to forward
forward_headers: ["X-Request-ID", "X-User-ID"]
```
## Next Steps
Full list of 100+ providers
Official LiteLLM proxy server documentation
Complete LiteLLM documentation
**Pro tip:** Start with a single provider, then add more as needed. LiteLLM makes it easy to switch or combine models without changing Bytebot configuration.
================================================
FILE: docs/deployment/railway.mdx
================================================
---
title: "Deploying Bytebot on Railway"
description: "Comprehensive guide to deploying the full Bytebot stack on Railway using the official 1-click template"
---
> **TL;DR –** Click the button below, add your AI API key (Anthropic, OpenAI, or Google), and your personal Bytebot instance will be live in ~2 minutes.
[](https://railway.com/deploy/bytebot?referralCode=L9lKXQ)
---
## Why Railway?
Railway provides a zero-ops PaaS experience with private networking and per-service logs that perfectly fits Bytebot’s multi-container architecture. The official template wires every service together using the latest container images pushed to the `edge` branch.
---
## What Gets Deployed
| Service | Container Image (edge) | Port | Exposed? | Purpose |
| ---------------- | -------------------------------------------------------------------- | ---- | -------- | ------------------------------------ |
| **bytebot-ui** | `ghcr.io/bytebot-ai/bytebot-ui:edge` | 9992 | **Yes** | Next.js web UI rendered to the world |
| **bytebot-agent**| `ghcr.io/bytebot-ai/bytebot-agent:edge` | 9991 | No | Task orchestration & LLM calls |
| **bytebot-desktop**| `ghcr.io/bytebot-ai/bytebot-desktop:edge` | 9990 | No | Containerised Ubuntu + XFCE desktop |
| **postgres** | `postgres:14-alpine` | 5432 | No | Persistence layer |
All internal traffic flows through Railway’s [private networking](https://docs.railway.com/guides/private-networking). Only `bytebot-ui` is assigned a public domain.
---
## Step-by-Step Walk-through
Click the **Deploy on Railway** button above or visit [https://railway.com/deploy/bytebot?referralCode=L9lKXQ](https://railway.com/deploy/bytebot?referralCode=L9lKXQ).
For the bytebot-agent resource, add your AI API key (choose at least one):
- **Anthropic**: Paste into `ANTHROPIC_API_KEY` for Claude models
- **OpenAI**: Paste into `OPENAI_API_KEY` for GPT models
- **Google**: Paste into `GEMINI_API_KEY` for Gemini models
Keep other defaults as is.
Press **Deploy**. Railway will pull the pre-built images, create the Postgres database and link all services on a private network.
When the build logs show *"bytebot-ui: ready"*, click the generated URL (e.g. `https://bytebot-ui-prod.up.railway.app`). You should see the task interface. Create a task and watch the desktop stream!
_Tip: You can tail logs for each service from the Railway dashboard._
The first deploy downloads several container layers – expect ~2 minutes. Subsequent redeploys are much faster.
---
## Private Networking & Security
• **Private networking** ensures that the agent, desktop and database can communicate securely without exposing their ports to the internet.
• **Public exposure** is limited to the UI which serves static assets and proxies WebSocket traffic.
• **Add authentication** by placing the UI behind Railway’s built-in password protection or an external provider (e.g. Cloudflare Access, Auth0, OAuth proxy).
• You can also point a custom domain to the UI from the Railway dashboard and enable Cloudflare for WAF/CDN protection.
---
## Customisation & Scaling
1. **Change images** – Fork the repo, push your own images and edit the template’s `Dockerfile` references.
2. **Increase resources** – Each service has an independent CPU/RAM slider in Railway. Bump up the desktop or agent if you plan heavy automations.
---
## Troubleshooting
| Symptom | Likely Cause | Fix |
| ------- | ------------ | ---- |
| Web UI shows “connecting…” | Desktop not ready or private networking mis-config | Wait for `bytebot-desktop` container to finish starting, or restart service |
| Agent errors `401` or `403` | Missing/invalid API key | Re-enter your AI provider's API key in Railway variables |
| Slow desktop video | Free Railway plan throttling | Upgrade plan or reduce screen resolution in desktop settings |
---
## Next Steps
• Explore the [REST APIs](/api-reference/introduction) to script tasks programmatically.
• Join our [Discord](https://discord.com/invite/d9ewZkWPTP) community for support and showcase your automations!
================================================
FILE: docs/docs.json
================================================
{
"$schema": "https://mintlify.com/docs.json",
"theme": "mint",
"name": "Bytebot - Self-Hosted AI Desktop Agent",
"colors": {
"primary": "#000000",
"light": "#fbfaf9",
"dark": "#000000"
},
"favicon": "/favicon.svg",
"navigation": {
"tabs": [
{
"tab": "Documentation",
"groups": [
{
"group": "Getting Started",
"pages": ["introduction", "quickstart"]
},
{
"group": "User Guides",
"pages": [
"guides/task-creation",
"guides/password-management",
"guides/takeover-mode"
]
},
{
"group": "Deployment",
"pages": [
"deployment/railway",
"deployment/helm",
"deployment/litellm"
]
},
{
"group": "Core Concepts",
"pages": [
"core-concepts/architecture",
"core-concepts/agent-system",
"core-concepts/desktop-environment",
"core-concepts/rpa-comparison"
]
}
]
},
{
"tab": "API Reference",
"groups": [
{
"group": "Overview",
"pages": ["api-reference/introduction"]
},
{
"group": "Agent API",
"pages": [
"api-reference/agent/tasks",
"api-reference/agent/ui"
]
},
{
"group": "Computer Control API",
"pages": [
"api-reference/computer-use/unified-endpoint",
"api-reference/computer-use/examples"
]
}
]
}
],
"global": {
"anchors": [
{
"anchor": "GitHub",
"href": "https://github.com/bytebot-ai/bytebot",
"icon": "github"
},
{
"anchor": "Discord",
"href": "https://discord.gg/zcb5wA2t4u",
"icon": "discord"
},
{
"anchor": "Twitter",
"href": "https://x.com/bytebot_ai",
"icon": "twitter"
},
{
"anchor": "Blog",
"href": "https://bytebot.ai/blog",
"icon": "newspaper"
}
]
}
},
"logo": {
"light": "/logo/bytebot_transparent_logo_dark.svg",
"dark": "/logo/bytebot_transparent_logo_white.svg"
},
"navbar": {
"links": [
{
"label": "Support",
"href": "https://discord.gg/zcb5wA2t4u"
}
],
"primary": {
"type": "button",
"label": "Get Started",
"href": "https://github.com/bytebot-ai/bytebot"
}
},
"footer": {
"socials": {
"github": "https://github.com/bytebot-ai/bytebot",
"twitter": "https://twitter.com/bytebotai",
"discord": "https://discord.gg/zcb5wA2t4u"
}
},
"metadata": {
"og:title": "Bytebot - Self-Hosted AI Desktop Agent",
"og:description": "Automate any computer task with natural language using your own AI desktop agent",
"og:image": "/images/agent-architecture.png",
"twitter:card": "summary_large_image"
}
}
================================================
FILE: docs/guides/password-management.mdx
================================================
---
title: "Password Management & 2FA"
description: "How Bytebot handles authentication automatically using password managers"
---
# Automated Authentication with Bytebot
Bytebot can handle authentication automatically - including passwords, 2FA, and even complex multi-step authentication flows - when you set up a password manager extension.
**Important**: Password manager extensions are not enabled by default. You need to install them manually using the desktop view.
## How It Works
Bytebot comes with 1Password built-in and supports any browser-based password manager extension. It can:
- Automatically fill passwords from the password manager
- Handle 2FA codes (TOTP/authenticator apps)
- Manage multiple accounts across different systems
- Work with SSO and federated authentication
- Store and use API keys and tokens
## Setting Up Password Management
### Option 1: 1Password (Recommended)
1. Go to the Desktop tab in Bytebot UI
2. Open Firefox
3. Install the 1Password extension from the Firefox Add-ons store
4. Sign in to your 1Password account (or create a dedicated one for Bytebot)
In your 1Password admin panel:
1. Create a vault called "Bytebot Automation"
2. Add the credentials Bytebot needs
3. Share the vault with Bytebot's account
4. Set appropriate permissions (read-only recommended)
The 1Password extension will automatically:
- Detect login forms
- Fill credentials
- Handle 2FA codes
- Submit forms
### Option 2: Other Password Managers
You can use any browser-based password manager by installing it through the Desktop view:
1. Open Desktop tab
2. Launch Firefox
3. Install Bitwarden extension from Firefox Add-ons
4. Log in to your Bitwarden account
5. Configure auto-fill settings in Bitwarden preferences
1. Open Desktop tab
2. Launch Firefox
3. Install LastPass extension from Firefox Add-ons
4. Log in with your enterprise account
5. Accept any shared folders for automation credentials
1. Open Desktop tab
2. Install KeePassXC application if needed
3. Install KeePassXC browser extension in Firefox
4. Configure browser integration
5. Load your KeePass database
## Handling Different Authentication Types
### Standard Username/Password
```yaml
# Task description
Task: "Log into our CRM system and export the customer list"
# Bytebot automatically:
1. Navigates to login page
2. Password manager detects form
3. Auto-fills credentials
4. Submits login
5. Proceeds with task
```
### Time-based 2FA (TOTP)
```yaml
# Task description
Task: "Access the banking portal and download statements"
# Bytebot handles:
1. Enters username/password from password manager
2. When 2FA prompt appears
3. Password manager provides TOTP code
4. Enters code automatically
5. Completes authentication
```
### Complex Multi-Step Auth
```yaml
# Task description
Task: "Log into the government portal (uses email verification)"
# Bytebot can:
1. Fill initial credentials
2. Handle "send code to email" flows
3. Access webmail account (also in password manager)
4. Retrieve verification code from webmail
5. Complete authentication
```
## Enterprise Setup Guide
### Centralized Credential Management
Set up dedicated service accounts for Bytebot:
```
- bytebot-finance@company.com (banking portals)
- bytebot-hr@company.com (HR systems)
- bytebot-ops@company.com (operational tools)
```
Structure your password manager:
```
Bytebot Vaults/
├── Financial Systems/
│ ├── Banking Portal A
│ ├── Banking Portal B
│ └── Payment Processor
├── Internal Tools/
│ ├── ERP System
│ ├── CRM Platform
│ └── HR Portal
└── External Services/
├── Vendor Portal 1
├── Government Site
└── Partner System
```
Configure automatic password rotation:
```javascript
// Example automation for password rotation
{
"schedule": "monthly",
"task": "For each credential in 'Rotation Required' vault,
update password in the system and save new password"
}
```
### Security Best Practices
Only share credentials Bytebot needs for specific tasks
Enable password manager audit logs to track access
Separate vaults by sensitivity level and department
Audit Bytebot's credential access monthly
## Common Authentication Scenarios
### Banking and Financial Systems
```yaml
Scenario: Daily bank reconciliation across 5 banks
Setup:
- Each bank credential in password manager
- 2FA seeds stored for TOTP generation
- Bytebot's IP whitelisted at banks
Task: "Log into each bank account, download yesterday's
transactions, and consolidate into daily report"
Result: Fully automated, no human intervention needed
```
### Government and Compliance Portals
```yaml
Scenario: Weekly regulatory filings
Setup:
- Service account with 2FA enabled
- Password manager has TOTP seed
- Security questions stored as notes
Task: "Log into state tax portal, file weekly sales tax
report using data from tax_data.csv"
Handles: Password, 2FA, security questions, CAPTCHAs
```
### Multi-Tenant SaaS Platforms
```yaml
Scenario: Managing multiple client accounts
Setup:
- Credentials for each tenant/client
- Organized in password manager by client
- Naming convention: client-platform-role
Task: "For each client in client_list.txt, log into their
Shopify account and export this month's orders"
Scales: Handles 100+ accounts seamlessly
```
## Advanced Authentication Features
### SSO and SAML Integration
```yaml
# Bytebot can handle SSO flows
Task: "Log into Salesforce using Okta SSO"
Process:
1. Navigate to Salesforce
2. Click "Log in with SSO"
3. Redirect to Okta
4. Password manager fills Okta credentials
5. Handle any 2FA on Okta
6. Redirect back to Salesforce
7. Continue with task
```
### API Key Management
```yaml
# Store API keys in password manager
Password Entry: "OpenAI API Key"
- Username: "api"
- Password: "sk-proj-..."
- Notes: "Rate limit: 10000/day"
# Use in tasks
Task: "Configure the application to use our OpenAI API key
from the password manager"
```
### Certificate-Based Auth
```yaml
# For systems requiring certificates
Setup:
1. Store certificate password in manager
2. Mount certificate file to Bytebot
3. Configure browser to use certificate
Task: "Access the enterprise portal that requires
client certificate authentication"
```
## Troubleshooting Authentication
**Solutions:**
- Ensure extension is installed and logged in
- Check site is saved in password manager
- Verify auto-fill settings are enabled
- Try refreshing the page
**Common causes:**
- Time sync issues (check system clock)
- Wrong TOTP seed saved
- Site using non-standard 2FA
**Fix:**
```bash
# Sync system time
docker exec bytebot-desktop ntpdate -s time.nist.gov
```
**Solutions:**
- Enable "remember me" if available
- Increase session timeout in target system
- Break long tasks into smaller chunks
- Use API access where possible
## Integration Examples
### Finance Automation Script
```python
# Example: Automated invoice collection
tasks = [
{
"description": "Log into vendor portal A and download all pending invoices",
"credentials": "vault://Financial Systems/Vendor Portal A"
},
{
"description": "Log into vendor portal B and download all pending invoices",
"credentials": "vault://Financial Systems/Vendor Portal B"
},
{
"description": "Process all downloaded invoices through our AP system",
"credentials": "vault://Internal Tools/AP System"
}
]
# Bytebot handles all authentication automatically
```
### Compliance Automation
```yaml
Daily Compliance Check:
Morning:
- Log into regulatory portal (2FA enabled)
- Download new compliance updates
- Check our status
If Non-Compliant:
- Log into internal system
- Create compliance ticket
- Notify compliance team
All credentials managed automatically
```
## Best Practices Summary
✅ **DO:**
- Use dedicated service accounts for Bytebot
- Organize credentials in logical vaults
- Enable 2FA on all accounts (Bytebot handles it!)
- Rotate passwords regularly
- Monitor access logs
❌ **DON'T:**
- Share personal credentials with Bytebot
- Store passwords in task descriptions
- Disable 2FA for convenience
- Use the same password across systems
- Ignore authentication errors
## Next Steps
See auth in action
Programmatic credential management
**Game Changer**: With proper password manager setup, Bytebot can handle even the most complex authentication flows automatically. No more manual intervention for 2FA, no more sharing passwords insecurely, and no more authentication bottlenecks in your automation workflows!
================================================
FILE: docs/guides/takeover-mode.mdx
================================================
---
title: "Takeover Mode"
description: "Take control of the desktop when you need to guide or assist Bytebot"
---
# Takeover Mode: Human-AI Collaboration
Takeover mode lets you take control of the desktop to help Bytebot when needed. There are two ways to use it:
## 1. During Task Execution
In the task detail view, you can hit the takeover button to:
- Interrupt the agent if it's going down the wrong path
- Guide it towards the correct solution
- Resolve issues when it's stumbling on something
## 2. Automatic Activation
Takeover mode is automatically enabled when a task status is set to "needs help" - this happens when the agent realizes it can't accomplish something on its own.
## How Actions Are Recorded
All your actions during takeover (clicks, drags, scrolls, typing, key presses) are automatically logged in the same unified action space that the agent uses. This means Bytebot understands and learns from everything you do.
## Desktop Tab for Setup
Outside of tasks, there's a dedicated **Desktop** tab on the main page that provides:
- Free-ranging access to the desktop
- Nothing is recorded in this mode
- Perfect for:
- Installing programs
- Logging into apps or websites
- Setting up the desktop environment
- General desktop maintenance
## Activating Takeover Mode
### Method 1: Manual Takeover During Tasks
While Bytebot is working on a task, click on the task to open the detail view.
Hit the takeover button to interrupt the agent and take control.
Perform the necessary actions to get past the obstacle or show the correct path.
Click to release control and let Bytebot continue from where you left off.
### Method 2: Automatic When Help Needed
When Bytebot sets a task status to "needs help":
- Takeover mode is automatically enabled
- You'll see a notification that Bytebot needs assistance
- Take control to help resolve the issue
- Bytebot will continue once you release control
## Common Use Cases
### 1. Complex UI Navigation
**Scenario**: Working with proprietary or complex software
**Steps**:
1. Let Bytebot open the application
2. Take control to navigate complex interfaces
3. Use the chat to explain what you're doing
4. Return control for Bytebot to continue
**Example**: "Open our internal CRM, I'll show you how to navigate to the reports section"
### 2. Error Recovery
**Scenario**: Bytebot encounters an error or gets stuck
**Steps**:
1. Notice Bytebot is struggling
2. Take control to resolve the issue
3. Guide it past the problem
4. Explain what went wrong in chat
5. Return control to let Bytebot continue
**Example**: "Let me handle this unexpected popup that's blocking the workflow"
### 3. Teaching by Demonstration
**Scenario**: Complex multi-step processes
**Steps**:
1. Take control when you need to demonstrate
2. Perform the task normally (no need to move slowly)
3. Use chat to explain what you're clicking and why
4. Return control
5. Ask Bytebot to repeat the process
**Example**: "Watch me navigate through our vendor portal to find the invoice section"
**Important**: Screenshots are taken for every action during takeover mode. Do not enter any data that you don't want captured in screenshots.
## Best Practices
### Do's ✅
- **Use Chat While Taking Over**: Type messages explaining what you're doing and why
- **Explain Your Clicks**: Share context about UI elements and their purpose
- **Return Control Before Leaving**: Always release control before exiting the task detail view
- **Test Understanding**: Ask Bytebot to summarize what it learned
### Don'ts ❌
- **Enter Data You Don't Want Captured**: Screenshots are taken of all actions
- **Skip Chat Explanations**: Context helps Bytebot learn patterns
- **Leave Task View While in Control**: This will leave the task stuck in takeover mode
- **Assume Knowledge**: Explain application-specific workflows
**No Need to Move Slowly**: Bytebot captures the state before and after each action, so you can work at normal speed.
## Summary
Takeover mode provides flexibility when you need to guide Bytebot or handle situations it can't manage alone. Whether you're navigating complex interfaces, recovering from errors, or teaching new workflows, takeover mode ensures you're always in control when needed.
================================================
FILE: docs/guides/task-creation.mdx
================================================
---
title: "Task Creation & Management"
description: "Master the art of creating effective tasks and managing them through completion"
---
# Creating and Managing Tasks in Bytebot
This guide will walk you through everything you need to know about creating tasks that Bytebot can execute effectively, and managing them through their lifecycle.
## Understanding Tasks
A task is any job you want Bytebot to complete. Tasks can be:
- **Simple**: "Log in to GitHub" or "Visit example.com" (uses one program)
- **Complex**: "Download invoices from email and save them to a folder" (uses multiple programs)
- **File-based**: "Read the uploaded PDF and extract all email addresses" (processes uploaded files)
- **Collaborative**: "Process invoices, ask me to handle special approvals"
## Working with Files
Bytebot has powerful file handling capabilities that make it perfect for document processing and data analysis tasks.
### Uploading Files with Tasks
When creating a task, you can upload files that will be automatically saved to the desktop instance. This is incredibly useful for:
- **Document Processing**: Upload PDFs, spreadsheets, or documents for Bytebot to analyze
- **Data Analysis**: Provide CSV files or datasets for processing
- **Template Filling**: Upload forms or templates that need to be completed
- **Batch Operations**: Upload multiple files for bulk processing
**Game Changer**: Bytebot can read entire files, including PDFs, directly into the LLM context. This means it can process large amounts of data quickly and understand complex documents without manual extraction.
### File Upload Examples
1. Click the attachment button when creating a task
2. Select files to upload (PDFs, CSVs, images, etc.)
3. Files are automatically saved to the desktop
4. Reference them in your task description:
```
"Read the uploaded contracts.pdf and extract all payment terms,
then create a summary spreadsheet with vendor names and terms"
```
```bash
# Upload files with task creation (multipart/form-data)
curl -X POST http://localhost:9991/tasks \
-F "description=Analyze the uploaded financial statements and create a summary" \
-F "priority=HIGH" \
-F "files=@financial_statements_2024.pdf" \
-F "files=@budget_comparison.xlsx"
```
### File Processing Capabilities
- Extract text from PDFs
- Read entire PDFs into context
- Parse forms and contracts
- Extract tables and data
- Read Excel/CSV files
- Analyze data patterns
- Generate reports
- Cross-reference multiple sheets
- Summarize long documents
- Extract key information
- Compare multiple files
- Answer questions about content
- Process multiple files
- Apply same analysis to each
- Consolidate results
- Generate unified reports
## Creating Your First Task
### Using the Web UI
Navigate to `http://localhost:9992`
In the input field on the left side, type what you want done. For example:
```
Log in to my GitHub account and check for new notifications
```
Press the arrow button or hit Enter. Bytebot will start loading and begin working on your task.
### Using the API
```bash
curl -X POST http://localhost:9991/tasks \
-H "Content-Type: application/json" \
-d '{
"description": "Download all PDF invoices from my email and organize by date",
"priority": "HIGH",
"type": "IMMEDIATE"
}'
```
## Writing Effective Task Descriptions
### The Golden Rules
❌ "Do some research"
✅ "Research top 5 CRM tools for small businesses"
❌ "Fill out the form"
✅ "Fill out the contact form on example.com with test data"
❌ "Organize files"
✅ "Organize files in Downloads folder by type into subfolders"
❌ "Do multiple unrelated things"
✅ "Focus on a single objective with clear steps"
### Task Description Templates
#### Enterprise Process Automation
```
Log into [system] and:
1. [Navigate to specific section]
2. [Download/Extract data]
3. [Process through other system]
4. [Update records/Generate report]
Handle any [specific scenarios]
Example:
Log into our banking portal and:
1. Navigate to wire transfers section
2. Download all pending wire confirmations
3. Match against our ERP payment records
4. Flag any discrepancies in the reconciliation sheet
(Bytebot handles all authentication including 2FA automatically via password manager)
```
#### Multi-Application Workflow
```
Access [System A] to get [data]
Then in [System B]:
1. [Process the data]
2. [Update records]
Finally in [System C]:
1. [Verify updates]
2. [Generate confirmation]
Example:
Access Salesforce to get list of new customers from today
Then in NetSuite:
1. Create customer records with billing info
2. Set up payment terms
Finally in our shipping system:
1. Verify addresses are valid
2. Generate welcome kit shipping labels
```
#### Compliance & Audit Task
```
For each [entity] in [source]:
1. Check [compliance requirement]
2. Document [specific data]
3. Flag any [violations/issues]
Generate report showing [metrics]
Example:
For each vendor in our approved vendor list:
1. Check their insurance certificates are current
2. Document expiration dates and coverage amounts
3. Flag any expiring within 30 days
Generate report showing compliance percentage by category
```
## Managing Active Tasks
### Task States
Tasks move through these states:
1. **Created** → Task is defined but not started
2. **Queued** → Waiting for agent availability
3. **Running** → Actively being worked on
4. **Needs Help** → Requires human input
5. **Completed** → Successfully finished
6. **Failed** → Could not be completed
### Monitoring Progress
#### Real-time Updates
Watch Bytebot work through the task detail viewer:
- **Green dot**: Task is actively running
- **Status messages**: Current step being executed
- **Desktop view**: See what Bytebot sees in real-time
#### Chat Messages
Bytebot provides updates like:
```
Assistant: I'm now searching for project management tools...
Assistant: Found 15 options, filtering by your criteria...
Assistant: Creating the comparison table with 5 tools...
```
### Interacting with Running Tasks
#### Providing Additional Information
```
User: "Also include free tier options in your research"
Assistant: "I'll add a column for free tier availability to the comparison table."
```
#### Clarifying Instructions
```
Assistant: "I found multiple forms on this page. Which one should I fill out?"
User: "Use the 'Contact Sales' form on the right side"
```
#### Modifying Tasks
```
User: "Actually, make it top 10 tools instead of top 5"
Assistant: "I'll expand my research to include 10 tools in the comparison."
```
## Advanced Task Management
### Task Dependencies
Chain tasks that depend on each other:
```
Task 1: "Download the invoice from the vendor portal"
Task 2: "Open the downloaded invoice and extract the total amount"
Task 3: "Enter the amount into our accounting system"
```
## Best Practices
### Do's ✅
1. **Start Simple**: Test with basic tasks before complex ones
2. **Provide Examples**: "Format it like the report from last week"
3. **Include Credentials Safely**: Use takeover mode for passwords
4. **Set Realistic Expectations**: Complex tasks take time
5. **Review Results**: Always verify important outputs
### Don'ts ❌
1. **Overload Single Tasks**: Break complex workflows into steps
2. **Assume Knowledge**: Explain custom applications
3. **Skip Context**: Always provide necessary background
4. **Ignore Errors**: Address issues promptly
5. **Rush Critical Tasks**: Allow time for careful execution
## Task Examples by Category
### 📄 Document Processing & Analysis
```
"Read the uploaded contract.pdf and extract all key terms including payment schedules, deliverables, and termination clauses. Create a summary document with these details."
"Process all the uploaded invoice PDFs, extract vendor names, amounts, and due dates, then create a consolidated Excel spreadsheet sorted by due date."
"Analyze the uploaded financial_report.pdf and answer these questions: What was the revenue growth? What are the main risk factors mentioned? What is the debt-to-equity ratio?"
"Read through the uploaded employee_handbook.pdf and create a checklist of all compliance requirements mentioned in the document."
```
### 🏦 Enterprise Automation (RPA-Style Workflows)
```
"Log into our banking portal, download all transaction files from last month, save them to the Finance/Statements folder, then run the reconciliation script on each file."
(Note: Bytebot handles all authentication including 2FA automatically using the built-in password manager)
"Access the vendor portal at supplier.example.com, navigate to the invoice section, download all pending invoices, extract the data into our standard template, and upload to the AP system."
"Open our legacy ERP system, export the customer list, then for each customer, look them up in the new CRM and update their status and last contact date."
```
### 📊 Financial Operations & Data Analysis
```
"Read the uploaded bank_statements folder containing 12 monthly PDFs, extract all transactions over $10,000, and create a summary report showing patterns and anomalies."
"Log into each of our 5 bank accounts, download the daily statements, consolidate them into a single cash position report, and save to the shared finance folder."
"Process the uploaded expense_reports.zip file, review all reports over $1,000, create a summary with policy violations flagged, and prepare for approval."
"Navigate to the tax authority website, download all GST/VAT returns for Q4, extract the figures, and populate our tax reconciliation spreadsheet."
```
### 🔄 Multi-System Integration
```
"Pull today's orders from Shopify, create corresponding entries in NetSuite, update inventory in our WMS, and trigger shipping labels in ShipStation."
"Extract employee data from Workday, cross-reference with our access control system, identify discrepancies, and create tickets for IT to resolve."
"Log into our insurance portal, download policy documents for all active policies, extract key dates and coverage amounts, update our risk management database."
```
### 📈 Compliance & Reporting
```
"Access all state regulatory websites for our operating regions, check for new compliance updates since last month, download relevant documents, and create a summary report."
"Log into our various SaaS tools (list provided), export user access reports, consolidate into a single audit trail, and flag any terminated employees still with access."
"Navigate to customer portal, download all SLA performance reports, extract metrics, compare against our internal data, and highlight discrepancies."
```
### 🤝 Development & QA Integration
```
"After the code agent deploys the new feature, test the complete user journey from signup to checkout, take screenshots at each step, and verify against the design specs."
"Run through all test scenarios in our QA checklist, but for any failures, have the code agent analyze the error and attempt a fix, then retest automatically."
"Monitor our staging environment, when a new build is deployed, automatically run the smoke test suite and create a visual regression report comparing to production."
```
## Troubleshooting Common Issues
**Possible causes**:
- Waiting for slow page/app to load
- Encountered unexpected popup
- Unclear next step
**Solutions**:
- Check desktop viewer for current state
- Provide clarification via chat
- Use takeover mode to help
- Cancel and restart with clearer instructions
**Possible causes**:
- Ambiguous instructions
- Website/app changed
- Misunderstood context
**Solutions**:
- Review task description for clarity
- Provide specific examples
- Break into smaller subtasks
- Use takeover mode to demonstrate
**Possible causes**:
- Invalid URL or application
- Missing prerequisites
- System resource issues
**Solutions**:
- Verify URLs and application names
- Ensure required files/data exist
- Check system resources
- Review error messages in chat
## Task Management Tips
### Organizing Multiple Tasks
1. **Use Clear Naming**: Include date, category, or project
2. **Group Related Tasks**: Process similar tasks together
3. **Priority Management**: Reserve 'Urgent' for true emergencies
4. **Regular Reviews**: Check completed tasks for quality
### Performance Optimization
- **Batch Similar Tasks**: Group web research, data entry, etc.
- **Prepare Resources**: Have files/data ready before starting
- **Clear Desktop**: Minimize distractions and popups
- **Stable Environment**: Ensure good internet and system resources
### Learning from Tasks
After each task:
1. Review the approach Bytebot took
2. Note any inefficiencies
3. Refine future task descriptions
4. Build a library of effective prompts
## Next Steps
Learn human-AI collaboration
Automate task creation
**Pro Tip**: Start with simple tasks to understand Bytebot's capabilities, then gradually increase complexity as you learn what works best.
================================================
FILE: docs/introduction.mdx
================================================
---
title: Introduction
description: "Open source AI desktop agent that automates any computer task"
---
## What is Bytebot?
Bytebot is an open-source AI agent that can control a computer desktop to complete tasks for you. It runs in Docker containers on your own infrastructure, giving you a virtual assistant that can:
- Use any desktop application (browser, email, office tools, etc.)
- Process uploaded files including PDFs, spreadsheets, and documents
- Read entire files directly into the LLM context for rapid analysis
- Automate repetitive tasks like data entry and form filling
- Handle complex workflows that span multiple applications
- Work 24/7 without human supervision
Simply describe what you need done in plain English, and Bytebot will figure out how to do it – clicking buttons, typing text, navigating websites, reading documents, and completing tasks just like a human would.
## Why Bytebot Over Traditional RPA?
Unlike UiPath or similar tools, no need to design flowcharts or write scripts - just describe tasks naturally
AI-powered understanding means Bytebot adapts to UI changes without breaking
Can read and understand any interface, not just pre-mapped elements
Handles unexpected popups, errors, and variations automatically
## Why Self-Host Bytebot?
Your tasks and data never leave your infrastructure. Everything runs locally
on your servers.
Customize the desktop environment, install any applications, and configure
to your exact needs.
Use your own LLM API keys without platform restrictions or additional fees.
Each desktop runs in its own container, completely isolated from your host
system.
## Real-World Use Cases
### Enterprise Automation (RPA Replacement)
Bytebot is the next generation of RPA (Robotic Process Automation). It handles the same complex workflows as traditional tools like UiPath, but with AI-powered adaptability and automatic authentication:
- **Financial Operations**: Automate banking portal access (including 2FA when password manager extensions are configured), download transaction files, and process them through multiple systems
- **Compliance Workflows**: Navigate government websites, download regulatory documents, extract data, and update compliance tracking systems
- **Multi-System Integration**: Bridge legacy systems that lack APIs by automating the UI interactions between them
- **Vendor Management**: Log into supplier portals, download invoices, reconcile with internal systems, and process payments
### Business Process Automation
- **Data Reconciliation**: Pull reports from multiple SaaS platforms, cross-reference data, and generate consolidated reports
- **Customer Onboarding**: Navigate between CRM, banking, and verification systems to complete new customer setup
- **Purchase Order Processing**: Extract POs from webmail portals, enter into ERP systems, and update inventory databases
- **HR Operations**: Collect employee data from various systems, update records, and ensure consistency across platforms
### Development & QA Integration
Bytebot becomes even more powerful when combined with coding agents:
- **Full-Stack Testing**: Use a coding agent to generate code, then have Bytebot visually test and validate the output
- **Automated Debugging**: Let Bytebot reproduce user-reported issues while a coding agent analyzes and fixes the code
- **End-to-End Development**: Code agents write features, Bytebot tests them, creating a complete development loop
- **Visual Regression Testing**: Automatically detect UI changes across deployments with screenshot comparisons
## How It Works
Simply tell Bytebot what you want done in natural language through the tasks
interface
Bytebot understands your request and breaks it down into specific computer
actions
Bytebot executes the task on its virtual desktop using the keyboard
and mouse
Monitor it working in real-time through the task detail view, or let it
complete tasks independently.
Receive the completed task output, screenshots, or confirmation of
completion
## Architecture Overview
Bytebot consists of four integrated components working together:
Ubuntu 22.04 with XFCE4, VSCode, Firefox, Thunderbird email client, and automation daemon (bytebotd)
NestJS service that uses LLMs (Anthropic Claude, OpenAI GPT, Google Gemini) to plan and execute tasks
Next.js web app for creating and managing tasks
Programmatic access to both task management and direct desktop control
## Getting Started
Get Bytebot running in 2 minutes
Understand how it all fits together
Integrate with your applications
## Key Features
### 🤖 Natural Language Control
Just tell Bytebot what you need done. No coding or complex automation tools required.
### 🖥️ Full Desktop Access
Bytebot can use any application you can install - browsers, office tools, custom software.
### 🔒 Complete Privacy
Runs entirely on your infrastructure. Your data never leaves your servers.
### 🔄 Two Operating Modes
- **Autonomous Mode**: Bytebot completes tasks independently
- **Takeover Mode**: You can step in and take control when needed
### 🖱️ Direct Desktop Access
- **Desktop Tab**: Free-form access to the virtual desktop for setup, installing programs, or manual operations
- **Task View**: Watch and interact with Bytebot during task execution
### 🚀 Easy Deployment
- One-click deployment on Railway
- Docker Compose for self-hosting
- Helm charts for Kubernetes
### 🔌 Developer-Friendly
- REST APIs for programmatic control
- Task management API
- Extensible architecture
- MCP (Model Context Protocol) support
## Community & Support
Join our community for help, tips, and discussions
Report issues, contribute, or star the project
**Ready to give your AI its own computer?** Start with our [Quick Start
Guide](/quickstart) to have your own AI desktop agent running in minutes.
================================================
FILE: docs/quickstart.mdx
================================================
---
title: "Quick Start"
description: "Get your AI desktop agent running in 2 minutes"
---
# Choose Your Deployment Method
Bytebot can be deployed in several ways depending on your needs:
## ☁️ One-click Deploy on Railway
[](https://railway.com/deploy/bytebot?referralCode=L9lKXQ)
Click the Deploy Now button in the Bytebot template on Railway.
Enter either your `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, or `GEMINI_API_KEY` for the bytebot-agent resource.
Hit **Deploy**. Railway will build the stack, wire the services together via private networking and output a public URL for the UI. Your agent should be ready within a couple of minutes!
Need more details? See the full Railway deployment guide .
## 🐳 Self-host with Docker Compose
## Prerequisites
- Docker ≥ 20.10
- Docker Compose
- 4GB+ RAM available
- AI API key from one of these providers:
- Anthropic ([get one here](https://console.anthropic.com)) - Claude models
- OpenAI ([get one here](https://platform.openai.com/api-keys)) - GPT models
- Google ([get one here](https://makersuite.google.com/app/apikey)) - Gemini models
## 🚀 2-Minute Setup
Get your self-hosted AI desktop agent running with just three commands:
```bash
git clone https://github.com/bytebot-ai/bytebot.git
cd bytebot
# Configure your AI provider (choose one):
echo "ANTHROPIC_API_KEY=your_api_key_here" > docker/.env # For Claude
# echo "OPENAI_API_KEY=your_api_key_here" > docker/.env # For OpenAI
# echo "GEMINI_API_KEY=your_api_key_here" > docker/.env # For Gemini
```
```bash
docker-compose -f docker/docker-compose.yml up -d
```
This starts all four services:
- **Bytebot Desktop**: Containerized Linux environment
- **AI Agent**: LLM-powered task processor (supports Claude, GPT, or Gemini)
- **Chat UI**: Web interface for interaction
- **Database**: PostgreSQL for persistence
Navigate to [http://localhost:9992](http://localhost:9992) to access the Bytebot UI.
**Two ways to interact:**
1. **Tasks**: Enter task descriptions to have Bytebot work autonomously
2. **Desktop**: Direct access to the virtual desktop for manual control
Try asking:
- "Open Firefox and search for the weather forecast"
- "Take a screenshot of the desktop"
- "Create a text file with today's date"
**First time?** The initial startup may take 2-3 minutes as Docker downloads
the images. Subsequent starts will be much faster.
## 🎯 What You Just Deployed
You now have a complete AI desktop automation system with:
**🔐 Password Manager Support**: Bytebot can handle authentication automatically when you install a password manager extension. See our [password management guide](/guides/password-management) for setup instructions.
- Understands natural language
- Plans and executes tasks
- Adapts to errors
- Works autonomously
- Full Ubuntu environment
- Browser, office tools
- File system access
- Application support
- Create and manage tasks
- Real-time desktop view
- Conversation history
- Takeover mode
- Programmatic control
- Task management API
- Direct desktop access
- MCP protocol support
## 🚀 Your First Tasks
Now let's see Bytebot in action! Try these example tasks:
### Simple Tasks (Test the Basics)
"Take a screenshot of the desktop"
"Open Firefox and go to google.com"
"Create a text file called 'hello.txt' with today's date"
"Check the system information and tell me the OS version"
### Advanced Tasks (See the Power)
"Find the top 5 AI news stories today and create a summary document"
"Go to hacker news, find the top 10 stories, and save them to a CSV file"
"Upload a PDF contract and extract all payment terms and deadlines"
"Search for 'machine learning tutorials', open the first 3 results in tabs, and take screenshots of each"
## Accessing Your Services
| Service | URL | Purpose |
| ---------------- | ------------------------------------------------------------------------ | --------------------------------------------- |
| **Tasks UI** | [http://localhost:9992](http://localhost:9992) | Main interface for interacting with the agent |
| **Agent API** | [http://localhost:9991/tasks](http://localhost:9991/tasks) | REST API for programmatic task creation |
| **Desktop API** | [http://localhost:9990/computer-use](http://localhost:9990/computer-use) | Low-level desktop control API |
| **MCP SSE** | [http://localhost:9990/mcp](http://localhost:9990/mcp) | Connect MCP clients for tool access |
## ☸️ Deploy with Helm
See our [Helm deployment guide](/deployment/helm) for Kubernetes installation.
## 🖥️ Desktop Container Only
If you just want the virtual desktop without the AI agent:
```bash
# Using pre-built image (recommended)
docker-compose -f docker/docker-compose.core.yml pull
docker-compose -f docker/docker-compose.core.yml up -d
```
Or build locally:
```bash
docker-compose -f docker/docker-compose.core.yml up -d --build
```
Access the desktop at [http://localhost:9990/vnc](http://localhost:9990/vnc)
## Managing Your Agent
### View Logs
Monitor what your agent is doing:
```bash
# All services
docker-compose -f docker/docker-compose.yml logs -f
# Just the agent
docker-compose -f docker/docker-compose.yml logs -f bytebot-agent
```
### Stop Services
```bash
docker-compose -f docker/docker-compose.yml down
```
### Update to Latest
```bash
docker-compose -f docker/docker-compose.yml pull
docker-compose -f docker/docker-compose.yml up -d
```
### Reset Everything
Remove all data and start fresh:
```bash
docker-compose -f docker/docker-compose.yml down -v
```
## Quick API Examples
### Create a Task via API
```bash
# Simple task
curl -X POST http://localhost:9991/tasks \
-H "Content-Type: application/json" \
-d '{
"description": "Search for flights from NYC to London next month",
"priority": "MEDIUM"
}'
# Task with file upload
curl -X POST http://localhost:9991/tasks \
-F "description=Read this contract and summarize the key terms" \
-F "priority=HIGH" \
-F "files=@contract.pdf"
```
### Direct Desktop Control
```bash
# Take a screenshot
curl -X POST http://localhost:9990/computer-use \
-H "Content-Type: application/json" \
-d '{"action": "screenshot"}'
# Type text
curl -X POST http://localhost:9990/computer-use \
-H "Content-Type: application/json" \
-d '{"action": "type_text", "text": "Hello, Bytebot!"}'
```
## Troubleshooting
Check Docker is running and you have enough resources:
```bash
docker info
docker-compose -f docker/docker-compose.yml logs
```
Ensure all services are running:
```bash
docker-compose -f docker/docker-compose.yml ps
```
All services should show as "Up".
Check your API key is set correctly:
```bash
cat docker/.env
docker-compose -f docker/docker-compose.yml logs bytebot-agent
```
Ensure you're using a valid API key from Anthropic, OpenAI, or Google.
## 📚 Next Steps
Learn how to create and manage tasks effectively
Take control when you need to guide Bytebot
Use any LLM provider with Bytebot
Automate Bytebot with your applications
## 🔧 Configuration Options
### Environment Variables
```bash
# Choose one AI provider:
ANTHROPIC_API_KEY=sk-ant-... # For Claude models
OPENAI_API_KEY=sk-... # For GPT models
GEMINI_API_KEY=... # For Gemini models
# Optional: Use specific models
ANTHROPIC_MODEL=claude-3-5-sonnet-20241022 # Default
OPENAI_MODEL=gpt-4o
GEMINI_MODEL=gemini-1.5-flash
```
```bash
# Change default ports if needed
# Edit docker-compose.yml ports section:
# bytebot-ui:
# ports:
# - "8080:9992" # Change 8080 to your desired port
```
```bash
# To use multiple LLM providers, use the proxy setup:
docker-compose -f docker/docker-compose.proxy.yml up -d
# This includes a pre-configured LiteLLM proxy
```
**Need help?** Join our [Discord
community](https://discord.com/invite/d9ewZkWPTP) for support and to share
what you're building!
================================================
FILE: docs/rest-api/computer-use.mdx
================================================
---
title: "Computer Action"
openapi: "POST /computer-use"
description: "Execute computer actions in the virtual desktop environment"
---
Execute actions like mouse movements, clicks, keyboard input, and screenshots in the Bytebot desktop environment.
## Request
The type of computer action to perform. Must be one of: `move_mouse`, `trace_mouse`,
`click_mouse`, `press_mouse`, `drag_mouse`, `scroll`, `type_keys`, `press_keys`,
`type_text`, `wait`, `screenshot`, `cursor_position`.
### Mouse Actions
The target coordinates to move to.
X coordinate (horizontal position)
Y coordinate (vertical position)
**Example Request**
```json
{
"action": "move_mouse",
"coordinates": {
"x": 100,
"y": 200
}
}
```
Array of coordinate objects for the mouse path.
X coordinate
Y coordinate
{" "}
Keys to hold while moving the mouse along the path.
**Example Request**
```json
{
"action": "trace_mouse",
"path": [
{ "x": 100, "y": 100 },
{ "x": 150, "y": 150 },
{ "x": 200, "y": 200 }
],
"holdKeys": ["shift"]
}
```
The coordinates to click (uses current cursor position if omitted).
X coordinate (horizontal position)
Y coordinate (vertical position)
{" "}
Mouse button to click. Must be one of: `left`, `right`, `middle`.
{" "}
Number of clicks to perform.
{" "}
Keys to hold while clicking (e.g., ['ctrl', 'shift'])
Key name
**Example Request**
```json
{
"action": "click_mouse",
"coordinates": {
"x": 150,
"y": 250
},
"button": "left",
"clickCount": 2
}
```
The coordinates to press/release (uses current cursor position if omitted).
X coordinate (horizontal position)
Y coordinate (vertical position)
{" "}
Mouse button to press/release. Must be one of: `left`, `right`, `middle`.
{" "}
Whether to press or release the button. Must be one of: `up`, `down`.
**Example Request**
```json
{
"action": "press_mouse",
"coordinates": {
"x": 150,
"y": 250
},
"button": "left",
"press": "down"
}
```
Array of coordinate objects for the drag path.
X coordinate
Y coordinate
{" "}
Mouse button to use for dragging. Must be one of: `left`, `right`, `middle`.
{" "}
Keys to hold while dragging.
**Example Request**
```json
{
"action": "drag_mouse",
"path": [
{ "x": 100, "y": 100 },
{ "x": 200, "y": 200 }
],
"button": "left"
}
```
The coordinates to scroll at (uses current cursor position if omitted).
X coordinate
Y coordinate
{" "}
Scroll direction. Must be one of: `up`, `down`, `left`, `right`.
{" "}
Number of scroll steps to perform.
{" "}
Keys to hold while scrolling.
**Example Request**
```json
{
"action": "scroll",
"direction": "down",
"scrollCount": 5
}
```
### Keyboard Actions
Array of keys to type in sequence.
Key name
{" "}
Delay between key presses in milliseconds.
**Example Request**
```json
{
"action": "type_keys",
"keys": ["a", "b", "c", "enter"],
"delay": 50
}
```
Array of keys to press or release.
Key name
{" "}
Whether to press or release the keys. Must be one of: `up`, `down`.
**Example Request**
```json
{
"action": "press_keys",
"keys": ["ctrl", "shift", "esc"],
"press": "down"
}
```
The text string to type.
{" "}
Delay between characters in milliseconds.
**Example Request**
```json
{
"action": "type_text",
"text": "Hello, Bytebot!",
"delay": 50
}
```
The text to paste. Useful for special characters that aren't on the standard keyboard.
**Example Request**
```json
{
"action": "paste_text",
"text": "Special characters: ©®™€¥£ émojis 🎉"
}
```
### System Actions
Wait duration in milliseconds.
**Example Request**
```json
{
"action": "wait",
"duration": 2000
}
```
No parameters required.
**Example Request**
```json
{
"action": "screenshot"
}
```
No parameters required.
**Example Request**
```json
{
"action": "cursor_position"
}
```
The application to switch to. Available options: `firefox`, `1password`, `thunderbird`, `vscode`, `terminal`, `desktop`, `directory`.
**Example Request**
```json
{
"action": "application",
"application": "firefox"
}
```
**Available Applications:**
- `firefox` - Mozilla Firefox web browser
- `1password` - Password manager
- `thunderbird` - Email client
- `vscode` - Visual Studio Code editor
- `terminal` - Terminal/console application
- `desktop` - Switch to desktop
- `directory` - File manager/directory browser
## Response
Responses vary based on the action performed:
### Default Response
Most actions return a simple success response:
```json
{
"success": true
}
```
### Screenshot Response
Returns the screenshot as a base64 encoded string:
```json
{
"success": true,
"data": {
"image": "base64_encoded_image_data"
}
}
```
### Cursor Position Response
Returns the current cursor position:
```json
{
"success": true,
"data": {
"x": 123,
"y": 456
}
}
```
### Error Response
```json
{
"success": false,
"error": "Error message"
}
```
### Code Examples
```bash cURL
curl -X POST http://localhost:9990/computer-use \
-H "Content-Type: application/json" \
-d '{"action": "move_mouse", "coordinates": {"x": 100, "y": 200}}'
```
```python Python
import requests
def control_computer(action, **params):
url = "http://localhost:9990/computer-use"
data = {"action": action, **params}
response = requests.post(url, json=data)
return response.json()
# Move the mouse
result = control_computer("move_mouse", coordinates={"x": 100, "y": 100})
print(result)
```
```javascript JavaScript
const axios = require("axios");
async function controlComputer(action, params = {}) {
const url = "http://localhost:9990/computer-use";
const data = { action, ...params };
const response = await axios.post(url, data);
return response.data;
}
// Move mouse example
controlComputer("move_mouse", { coordinates: { x: 100, y: 100 } })
.then((result) => console.log(result))
.catch((error) => console.error("Error:", error));
```
================================================
FILE: docs/rest-api/examples.mdx
================================================
---
title: "Usage Examples"
description: "Code examples for common automation scenarios using the Bytebot REST API"
---
This page provides practical examples of how to use the Bytebot REST API in different programming languages and scenarios.
## Language Examples
### cURL Examples
```bash Open Application and Navigate
# Open an application (like Firefox)
curl -X POST http://localhost:9990/computer-use \
-H "Content-Type: application/json" \
-d '{"action": "move_mouse", "coordinates": {"x": 100, "y": 950}}'
curl -X POST http://localhost:9990/computer-use \
-H "Content-Type: application/json" \
-d '{"action": "click_mouse", "button": "left", "clickCount": 2}'
# Wait for application to open
curl -X POST http://localhost:9990/computer-use \
-H "Content-Type: application/json" \
-d '{"action": "wait", "duration": 150}'
# Type URL in address bar
curl -X POST http://localhost:9990/computer-use \
-H "Content-Type: application/json" \
-d '{"action": "type_text", "text": "https://example.com"}'
# Press Enter to navigate
curl -X POST http://localhost:9990/computer-use \
-H "Content-Type: application/json" \
-d '{"action": "typ_keys", "keys": ["enter"]}'
````
```bash Take and Save Screenshot
# Take a screenshot
response=$(curl -s -X POST http://localhost:9990/computer-use \
-H "Content-Type: application/json" \
-d '{"action": "screenshot"}')
# Extract the base64 image data and save to a file
echo $response | jq -r '.data.image' | base64 -d > screenshot.png
echo "Screenshot saved to screenshot.png"
````
```bash Copy and Paste Text
# Select text with triple click (selects a paragraph)
curl -X POST http://localhost:9990/computer-use \
-H "Content-Type: application/json" \
-d '{"action": "move_mouse", "coordinates": {"x": 400, "y": 300}}'
curl -X POST http://localhost:9990/computer-use \
-H "Content-Type: application/json" \
-d '{"action": "click_mouse", "button": "left", "clickCount": 3}'
# Copy with Ctrl+C
curl -X POST http://localhost:9990/computer-use \
-H "Content-Type: application/json" \
-d '{"action": "press_keys", "keys": ["ctrl", "c"], "press": "down"}'
curl -X POST http://localhost:9990/computer-use \
-H "Content-Type: application/json" \
-d '{"action": "press_keys", "keys": ["ctrl", "c"], "press": "up"}'
# Click elsewhere to paste
curl -X POST http://localhost:9990/computer-use \
-H "Content-Type: application/json" \
-d '{"action": "move_mouse", "coordinates": {"x": 400, "y": 500}}'
curl -X POST http://localhost:9990/computer-use \
-H "Content-Type: application/json" \
-d '{"action": "click_mouse", "button": "left"}'
# Paste with Ctrl+V
curl -X POST http://localhost:9990/computer-use \
-H "Content-Type: application/json" \
-d '{"action": "press_keys", "keys": ["ctrl", "v"], "press": "down"}'
curl -X POST http://localhost:9990/computer-use \
-H "Content-Type: application/json" \
-d '{"action": "press_keys", "keys": ["ctrl", "v"], "press": "up"}'
```
### Python Examples
```python Web Form Automation
import requests
import time
def control_computer(action, **params):
url = "http://localhost:9990/computer-use"
data = {"action": action, **params}
response = requests.post(url, json=data)
return response.json()
def fill_web_form(): # Navigate to a form (e.g., login form)
control_computer("move_mouse", coordinates={"x": 500, "y": 300})
control_computer("click_mouse", button="left")
# Type username
control_computer("type_text", text="user@example.com")
# Tab to password field
control_computer("type_keys", keys=["tab"])
# Type password
control_computer("type_text", text="secure_password")
# Tab to login button
control_computer("type_keys", keys=["tab"])
# Press Enter to submit
control_computer("type_keys", keys=["enter"])
# Wait for page to load
control_computer("wait", duration=2000)
print("Form submitted successfully")
# Run the automation
# fill_web_form()
````
```python File Upload Dialog
import requests
import time
def control_computer(action, **params):
url = "http://localhost:9990/computer-use"
data = {"action": action, **params}
response = requests.post(url, json=data)
return response.json()
def upload_file(file_path="/path/to/file.txt"):
# Click the upload button
control_computer("move_mouse", coordinates={"x": 400, "y": 300})
control_computer("click_mouse", button="left")
# Wait for file dialog to appear
control_computer("wait", duration=1000)
# Type the file path
control_computer("type_text", text=file_path)
# Press Enter to confirm
control_computer("press_keys", keys=["enter"], press="down")
control_computer("press_keys", keys=["enter"], press="up")
# Wait for upload to complete
control_computer("wait", duration=3000)
# Take screenshot of result
result = control_computer("screenshot")
if result["success"]:
print("File upload completed and screenshot taken")
# Run the automation
# upload_file("/Users/username/Documents/example.pdf")
````
```python Screenshot Monitoring
import requests
import base64
import time
from io import BytesIO
from PIL import Image
def take_screenshot():
url = "http://localhost:9990/computer-use"
response = requests.post(url, json={"action": "screenshot"})
if response.json()["success"]:
img_data = base64.b64decode(response.json()["data"]["image"])
return Image.open(BytesIO(img_data))
return None
def monitor_for_changes(interval=5, duration=60):
"""Monitor the screen for changes at regular intervals"""
first_screenshot = take_screenshot()
if not first_screenshot:
print("Failed to take initial screenshot")
return
first_screenshot.save("baseline.png")
print("Baseline screenshot saved")
end_time = time.time() + duration
screenshot_count = 1
while time.time() < end_time:
time.sleep(interval)
current = take_screenshot()
if current:
filename = f"screenshot_{screenshot_count}.png"
current.save(filename)
print(f"Saved {filename}")
screenshot_count += 1
print(f"Monitoring completed. Saved {screenshot_count} screenshots.")
# Run the monitoring for 30 seconds, taking a screenshot every 5 seconds
# monitor_for_changes(interval=5, duration=30)
```
### JavaScript/Node.js Examples
```javascript Browser Navigation
const axios = require('axios');
async function controlComputer(action, params = {}) {
const url = "http://localhost:9990/computer-use";
const data = { action, ...params };
try {
const response = await axios.post(url, data);
return response.data;
} catch (error) {
console.error('Error:', error.message);
throw error;
}
}
async function navigateToWebsite(url) {
console.log(`Navigating to ${url}...`);
// Open Firefox/Chrome by clicking on dock icon
await controlComputer("move_mouse", { coordinates: { x: 100, y: 950 } });
await controlComputer("click_mouse", { button: "left" });
// Wait for browser to open
await controlComputer("wait", { duration: 2000 });
// Click in URL bar (usually near the top)
await controlComputer("move_mouse", { coordinates: { x: 400, y: 60 } });
await controlComputer("click_mouse", { button: "left" });
// Select all existing text (Cmd+A on Mac, Ctrl+A elsewhere)
await controlComputer("press_keys", { keys: ["ctrl"], press: "down" });
await controlComputer("press_keys", { keys: ["a"], press: "down" });
await controlComputer("press_keys", { keys: ["a"], press: "up" });
await controlComputer("press_keys", { keys: ["ctrl"], press: "up" });
// Type the URL
await controlComputer("type_text", { text: url });
// Press Enter to navigate
await controlComputer("press_keys", { keys: ["enter"], press: "down" });
await controlComputer("press_keys", { keys: ["enter"], press: "up" });
// Wait for page to load
await controlComputer("wait", { duration: 3000 });
console.log("Navigation completed");
}
// Usage
// navigateToWebsite("https://example.com").catch(console.error);
````
```javascript Data Entry Automation
const axios = require('axios');
async function controlComputer(action, params = {}) {
const url = "http://localhost:9990/computer-use";
const data = { action, ...params };
try {
const response = await axios.post(url, data);
return response.data;
} catch (error) {
console.error('Error:', error.message);
throw error;
}
}
// Function to fill a data entry form
async function fillDataEntryForm(formData) {
// Click on the first form field
await controlComputer("move_mouse", { coordinates: { x: 400, y: 250 } });
await controlComputer("click_mouse", { button: "left" });
// Fill each field and press Tab to move to the next
for (const [index, value] of formData.entries()) {
// Type the value
await controlComputer("type_text", { text: value });
// If not the last field, press Tab to move to next field
if (index < formData.length - 1) {
await controlComputer("press_keys", { keys: ["tab"], press: "down" });
await controlComputer("press_keys", { keys: ["tab"], press: "up" });
await controlComputer("wait", { duration: 300 });
}
}
// Find and click the submit button
await controlComputer("move_mouse", { coordinates: { x: 400, y: 500 } });
await controlComputer("click_mouse", { button: "left" });
// Take a screenshot of the result
const result = await controlComputer("screenshot");
console.log("Form submitted successfully");
return result;
}
// Example form data
const formFields = [
"John Doe", // Name
"john.doe@example.com", // Email
"123 Main St", // Address
"555-123-4567" // Phone
];
// Usage
// fillDataEntryForm(formFields).catch(console.error);
````
```javascript UI Testing
const axios = require("axios");
const fs = require("fs");
async function controlComputer(action, params = {}) {
const url = "http://localhost:9990/computer-use";
const data = { action, ...params };
try {
const response = await axios.post(url, data);
return response.data;
} catch (error) {
console.error("Error:", error.message);
throw error;
}
}
// Simple UI testing framework
async function testUIElement(name, options) {
const { x, y, expectedResult } = options;
console.log(`Testing UI element: ${name}`);
// Take screenshot before interaction
const beforeShot = await controlComputer("screenshot");
fs.writeFileSync(
`before_${name}.png`,
Buffer.from(beforeShot.data.image, "base64")
);
// Click the element
await controlComputer("move_mouse", { coordinates: { x, y } });
await controlComputer("click_mouse", { button: "left" });
// Wait for any UI changes
await controlComputer("wait", { duration: 1000 });
// Take screenshot after interaction
const afterShot = await controlComputer("screenshot");
fs.writeFileSync(
`after_${name}.png`,
Buffer.from(afterShot.data.image, "base64")
);
console.log(`Test for ${name} completed. Screenshots saved.`);
if (expectedResult && typeof expectedResult === "function") {
// Call custom verification function if provided
await expectedResult();
}
}
// Example UI elements to test
const uiElements = [
{ name: "login_button", x: 500, y: 400 },
{ name: "menu_icon", x: 50, y: 50 },
{ name: "close_dialog", x: 800, y: 200 },
];
// Run tests sequentially
async function runUITests() {
for (const element of uiElements) {
await testUIElement(element.name, element);
// Add some delay between tests
await controlComputer("wait", { duration: 2000 });
}
console.log("All UI tests completed");
}
// Usage
// runUITests().catch(console.error);
```
### Common Automation Scenarios
### Browser Automation Workflow
This example demonstrates a complete browser workflow, opening a site and interacting with it:
```python
import requests
import time
def control_computer(action, **params):
url = "http://localhost:9990/computer-use"
data = {"action": action, **params}
response = requests.post(url, json=data)
return response.json()
def browser_workflow():
# Open browser (assuming browser icon is at position x=100, y=960)
control_computer("move_mouse", coordinates={"x": 100, "y": 960})
control_computer("click_mouse", button="left")
time.sleep(3) # Wait for browser to open
# Type URL and navigate
control_computer("type_text", text="https://example.com")
control_computer("press_keys", key="enter")
time.sleep(2) # Wait for page to load
# Take screenshot of the loaded page
screenshot = control_computer("screenshot")
# Click on a link (coordinates would need to be adjusted for your target)
control_computer("move_mouse", coordinates={"x": 300, "y": 400})
control_computer("click_mouse", button="left")
time.sleep(2)
# Scroll down
control_computer("scroll", direction="down", amount=500)
# Fill a search box
control_computer("move_mouse", coordinates={"x": 600, "y": 200})
control_computer("click_mouse", button="left")
control_computer("type_text", text="search query")
control_computer("press_keys", key="enter")
browser_workflow()
```
### Form Filling Workflow
This example shows a complete form-filling process:
```javascript
const axios = require("axios");
async function controlComputer(action, params = {}) {
const url = "http://localhost:9990/computer-use";
const data = { action, ...params };
const response = await axios.post(url, data);
return response.data;
}
async function fillForm() {
// Navigate to form page
await controlComputer("move_mouse", { coordinates: { x: 100, y: 960 } });
await controlComputer("click_mouse", { button: "left" });
await controlComputer("wait", { duration: 3000 });
await controlComputer("type_text", { text: "https://example.com/form" });
await controlComputer("press_keys", { key: "enter" });
await controlComputer("wait", { duration: 2000 });
// Fill form
// Name field
await controlComputer("move_mouse", { coordinates: { x: 400, y: 250 } });
await controlComputer("click_mouse", { button: "left" });
// Type the value
await controlComputer("type_text", { text: "John Doe" });
// Email field (tab to next field)
await controlComputer("press_keys", { keys: ["tab"], press: "down" });
await controlComputer("press_keys", { keys: ["tab"], press: "up" });
await controlComputer("type_text", { text: "john@example.com" });
// Message field (tab to next field)
await controlComputer("press_keys", { keys: ["tab"], press: "down" });
await controlComputer("press_keys", { keys: ["tab"], press: "up" });
await controlComputer("type_text", {
text: "This is an automated message sent using Bytebot's Computer Use API",
delay: 30,
});
// Submit form
await controlComputer("press_keys", { keys: ["tab"], press: "down" });
await controlComputer("press_keys", { keys: ["tab"], press: "up" });
await controlComputer("press_keys", { key: "enter" });
// Take screenshot of confirmation page
await controlComputer("wait", { duration: 2000 });
const screenshot = await controlComputer("screenshot");
console.log("Form submitted successfully");
}
fillForm().catch(console.error);
```
### Automation Framework Integration
You can create a reusable automation framework with Bytebot:
```python
import requests
import time
import json
class BytebotDriver:
"""A Selenium-like driver for Bytebot"""
def __init__(self, base_url="http://localhost:9990"):
self.base_url = base_url
def control_computer(self, action, **params):
url = f"{self.base_url}/computer-use"
data = {"action": action, **params}
response = requests.post(url, json=data)
return response.json()
def open_browser(self, browser_icon_coords):
"""Open a browser by clicking its icon"""
self.control_computer("move_mouse", coordinates=browser_icon_coords)
self.control_computer("click_mouse", button="left")
time.sleep(3) # Wait for browser to open
def navigate_to(self, url):
"""Navigate to a URL in the browser"""
self.control_computer("type_text", text=url)
self.control_computer("press_keys", key="enter")
time.sleep(2) # Wait for page to load
def click_element(self, coords):
"""Click an element at the specified coordinates"""
self.control_computer("move_mouse", coordinates=coords)
self.control_computer("click_mouse", button="left")
def type_text(self, text):
"""Type text at the current cursor position"""
self.control_computer("type_text", text=text)
def press_key(self, key, modifiers=None):
"""Press a keyboard key with optional modifiers"""
params = {"key": key}
if modifiers:
params["modifiers"] = modifiers
self.control_computer("press_keys", **params)
def take_screenshot(self):
"""Take a screenshot of the desktop"""
return self.control_computer("screenshot")
def scroll(self, direction, amount):
"""Scroll in the specified direction"""
self.control_computer("scroll", direction=direction, amount=amount)
# Example usage
driver = BytebotDriver()
driver.open_browser({"x": 100, "y": 960})
driver.navigate_to("https://example.com")
driver.click_element({"x": 300, "y": 400})
driver.type_text("Hello Bytebot!")
driver.press_key("enter")
result = driver.take_screenshot()
print(f"Screenshot captured: {result['success']}")
```
================================================
FILE: docs/rest-api/input-tracking.mdx
================================================
---
title: "Input Tracking"
openapi: "POST /input-tracking/start"
description: "Start and stop input tracking on the Bytebot desktop"
---
The Bytebot daemon can monitor mouse and keyboard events through the
`InputTracking` module. Tracking is disabled by default and can be toggled
via the REST API. Tracked actions are streamed over WebSockets so that the
agent can store them as messages.
## Start Tracking
`POST /input-tracking/start`
Begins capturing input events. The endpoint returns a simple status object:
```json
{
"status": "started"
}
```
## Stop Tracking
`POST /input-tracking/stop`
Stops capturing events and clears any internal buffers. The response is
similar to the start endpoint:
```json
{
"status": "stopped"
}
```
## WebSocket Stream
When tracking is active, actions are emitted on the `input_action` channel of
the WebSocket server running on the daemon. Clients can connect to the daemon
and listen for these events to persist them as needed.
================================================
FILE: docs/rest-api/introduction.mdx
================================================
---
title: "Introduction"
description: "Overview of the Bytebot REST API"
---
## Bytebot REST API
Bytebot's core functionality is exposed through its REST API, which provides endpoints for interacting with the desktop environment. The API allows for programmatic control of mouse movement, keyboard input, and screen capture.
### Base URL
All API endpoints are relative to the base URL:
```
http://localhost:9990
```
The port can be configured when running the container.
### Authentication
The Bytebot API does not require authentication by default when accessed locally. For remote access, standard network security practices should be implemented.
### Response Format
All API responses follow a standard JSON format:
```json
{
"success": true,
"data": { ... }, // Response data specific to the action
"error": null // Error message if success is false
}
```
### Error Handling
When an error occurs, the API returns:
```json
{
"success": false,
"data": null,
"error": "Detailed error message"
}
```
Common HTTP status codes:
| Status Code | Description |
| ----------- | -------------------------------- |
| 200 | Success |
| 400 | Bad Request - Invalid parameters |
| 500 | Internal Server Error |
### Available Endpoints
Execute desktop automation actions like mouse movements, clicks, keyboard
input, and screenshots
Control and stream keyboard and mouse events from the desktop
Code examples and snippets for common automation scenarios
### Rate Limiting
The API currently does not implement rate limiting, but excessive requests may impact performance of the virtual desktop environment.
================================================
FILE: helm/Chart.yaml
================================================
apiVersion: v2
name: bytebot
description: Bytebot - Complete deployment package
type: application
version: 0.1.0
appVersion: "edge"
dependencies:
- name: postgresql
version: 0.1.0
repository: file://./charts/postgresql
condition: postgresql.enabled
- name: bytebot-desktop
version: 0.1.0
repository: file://./charts/bytebot-desktop
- name: bytebot-agent
version: 0.1.0
repository: file://./charts/bytebot-agent
- name: bytebot-ui
version: 0.1.0
repository: file://./charts/bytebot-ui
- name: bytebot-llm-proxy
version: 0.1.0
repository: file://./charts/bytebot-llm-proxy
condition: bytebot-llm-proxy.enabled
keywords:
- bytebot
- automation
- remote-desktop
maintainers:
- name: Bytebot Team
sources:
- https://github.com/bytebot-ai/bytebot
================================================
FILE: helm/README.md
================================================
# Bytebot Helm Charts
This directory contains Helm charts for deploying Bytebot on Kubernetes.
## Documentation
For complete deployment instructions, see:
**[Helm Deployment Guide](https://docs.bytebot.ai/deployment/helm)**
## Quick Start
```bash
# Clone repository
git clone https://github.com/bytebot-ai/bytebot.git
cd bytebot
# Create values.yaml with your API key(s)
cat > values.yaml <('BYTEBOT_ANALYTICS_ENDPOINT');
if (!this.endpoint) {
this.logger.warn(
'BYTEBOT_ANALYTICS_ENDPOINT is not set. Analytics service disabled.',
);
}
}
@OnEvent('task.cancel')
@OnEvent('task.failed')
@OnEvent('task.completed')
async handleTaskEvent(payload: { taskId: string }) {
if (!this.endpoint) return;
try {
const task = await this.tasksService.findById(payload.taskId);
const messages = await this.messagesService.findEvery(payload.taskId);
await fetch(this.endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...task, messages }),
});
} catch (error: any) {
this.logger.error(
`Failed to send analytics for task ${payload.taskId}: ${error.message}`,
error.stack,
);
}
}
}
================================================
FILE: packages/bytebot-agent/src/agent/agent.computer-use.ts
================================================
import {
Button,
Coordinates,
Press,
ComputerToolUseContentBlock,
ToolResultContentBlock,
MessageContentType,
isScreenshotToolUseBlock,
isCursorPositionToolUseBlock,
isMoveMouseToolUseBlock,
isTraceMouseToolUseBlock,
isClickMouseToolUseBlock,
isPressMouseToolUseBlock,
isDragMouseToolUseBlock,
isScrollToolUseBlock,
isTypeKeysToolUseBlock,
isPressKeysToolUseBlock,
isTypeTextToolUseBlock,
isWaitToolUseBlock,
isApplicationToolUseBlock,
isPasteTextToolUseBlock,
isReadFileToolUseBlock,
} from '@bytebot/shared';
import { Logger } from '@nestjs/common';
const BYTEBOT_DESKTOP_BASE_URL = process.env.BYTEBOT_DESKTOP_BASE_URL as string;
export async function handleComputerToolUse(
block: ComputerToolUseContentBlock,
logger: Logger,
): Promise {
logger.debug(
`Handling computer tool use: ${block.name}, tool_use_id: ${block.id}`,
);
if (isScreenshotToolUseBlock(block)) {
logger.debug('Processing screenshot request');
try {
logger.debug('Taking screenshot');
const image = await screenshot();
logger.debug('Screenshot captured successfully');
return {
type: MessageContentType.ToolResult,
tool_use_id: block.id,
content: [
{
type: MessageContentType.Image,
source: {
data: image,
media_type: 'image/png',
type: 'base64',
},
},
],
};
} catch (error) {
logger.error(`Screenshot failed: ${error.message}`, error.stack);
return {
type: MessageContentType.ToolResult,
tool_use_id: block.id,
content: [
{
type: MessageContentType.Text,
text: 'ERROR: Failed to take screenshot',
},
],
is_error: true,
};
}
}
if (isCursorPositionToolUseBlock(block)) {
logger.debug('Processing cursor position request');
try {
logger.debug('Getting cursor position');
const position = await cursorPosition();
logger.debug(`Cursor position obtained: ${position.x}, ${position.y}`);
return {
type: MessageContentType.ToolResult,
tool_use_id: block.id,
content: [
{
type: MessageContentType.Text,
text: `Cursor position: ${position.x}, ${position.y}`,
},
],
};
} catch (error) {
logger.error(
`Getting cursor position failed: ${error.message}`,
error.stack,
);
return {
type: MessageContentType.ToolResult,
tool_use_id: block.id,
content: [
{
type: MessageContentType.Text,
text: 'ERROR: Failed to get cursor position',
},
],
is_error: true,
};
}
}
try {
if (isMoveMouseToolUseBlock(block)) {
await moveMouse(block.input);
}
if (isTraceMouseToolUseBlock(block)) {
await traceMouse(block.input);
}
if (isClickMouseToolUseBlock(block)) {
await clickMouse(block.input);
}
if (isPressMouseToolUseBlock(block)) {
await pressMouse(block.input);
}
if (isDragMouseToolUseBlock(block)) {
await dragMouse(block.input);
}
if (isScrollToolUseBlock(block)) {
await scroll(block.input);
}
if (isTypeKeysToolUseBlock(block)) {
await typeKeys(block.input);
}
if (isPressKeysToolUseBlock(block)) {
await pressKeys(block.input);
}
if (isTypeTextToolUseBlock(block)) {
await typeText(block.input);
}
if (isPasteTextToolUseBlock(block)) {
await pasteText(block.input);
}
if (isWaitToolUseBlock(block)) {
await wait(block.input);
}
if (isApplicationToolUseBlock(block)) {
await application(block.input);
}
if (isReadFileToolUseBlock(block)) {
logger.debug(`Reading file: ${block.input.path}`);
const result = await readFile(block.input);
if (result.success && result.data) {
// Return document content block
return {
type: MessageContentType.ToolResult,
tool_use_id: block.id,
content: [
{
type: MessageContentType.Document,
source: {
type: 'base64',
media_type: result.mediaType || 'application/octet-stream',
data: result.data,
},
name: result.name || 'file',
size: result.size,
},
],
};
} else {
// Return error message
return {
type: MessageContentType.ToolResult,
tool_use_id: block.id,
content: [
{
type: MessageContentType.Text,
text: result.message || 'Error reading file',
},
],
is_error: true,
};
}
}
let image: string | null = null;
try {
// Wait before taking screenshot to allow UI to settle
const delayMs = 750; // 750ms delay
logger.debug(`Waiting ${delayMs}ms before taking screenshot`);
await new Promise((resolve) => setTimeout(resolve, delayMs));
logger.debug('Taking screenshot');
image = await screenshot();
logger.debug('Screenshot captured successfully');
} catch (error) {
logger.error('Failed to take screenshot', error);
}
logger.debug(`Tool execution successful for tool_use_id: ${block.id}`);
const toolResult: ToolResultContentBlock = {
type: MessageContentType.ToolResult,
tool_use_id: block.id,
content: [
{
type: MessageContentType.Text,
text: 'Tool executed successfully',
},
],
};
if (image) {
toolResult.content.push({
type: MessageContentType.Image,
source: {
data: image,
media_type: 'image/png',
type: 'base64',
},
});
}
return toolResult;
} catch (error) {
logger.error(
`Error executing ${block.name} tool: ${error.message}`,
error.stack,
);
return {
type: MessageContentType.ToolResult,
tool_use_id: block.id,
content: [
{
type: MessageContentType.Text,
text: `Error executing ${block.name} tool: ${error.message}`,
},
],
is_error: true,
};
}
}
async function moveMouse(input: { coordinates: Coordinates }): Promise {
const { coordinates } = input;
console.log(
`Moving mouse to coordinates: [${coordinates.x}, ${coordinates.y}]`,
);
try {
await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'move_mouse',
coordinates,
}),
});
} catch (error) {
console.error('Error in move_mouse action:', error);
throw error;
}
}
async function traceMouse(input: {
path: Coordinates[];
holdKeys?: string[];
}): Promise {
const { path, holdKeys } = input;
console.log(
`Tracing mouse to path: ${path} ${holdKeys ? `with holdKeys: ${holdKeys}` : ''}`,
);
try {
await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'trace_mouse',
path,
holdKeys,
}),
});
} catch (error) {
console.error('Error in trace_mouse action:', error);
throw error;
}
}
async function clickMouse(input: {
coordinates?: Coordinates;
button: Button;
holdKeys?: string[];
clickCount: number;
}): Promise {
const { coordinates, button, holdKeys, clickCount } = input;
console.log(
`Clicking mouse ${button} ${clickCount} times ${coordinates ? `at coordinates: [${coordinates.x}, ${coordinates.y}] ` : ''} ${holdKeys ? `with holdKeys: ${holdKeys}` : ''}`,
);
try {
await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'click_mouse',
coordinates,
button,
holdKeys: holdKeys && holdKeys.length > 0 ? holdKeys : undefined,
clickCount,
}),
});
} catch (error) {
console.error('Error in click_mouse action:', error);
throw error;
}
}
async function pressMouse(input: {
coordinates?: Coordinates;
button: Button;
press: Press;
}): Promise {
const { coordinates, button, press } = input;
console.log(
`Pressing mouse ${button} ${press} ${coordinates ? `at coordinates: [${coordinates.x}, ${coordinates.y}]` : ''}`,
);
try {
await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'press_mouse',
coordinates,
button,
press,
}),
});
} catch (error) {
console.error('Error in press_mouse action:', error);
throw error;
}
}
async function dragMouse(input: {
path: Coordinates[];
button: Button;
holdKeys?: string[];
}): Promise {
const { path, button, holdKeys } = input;
console.log(
`Dragging mouse to path: ${path} ${holdKeys ? `with holdKeys: ${holdKeys}` : ''}`,
);
try {
await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'drag_mouse',
path,
button,
holdKeys: holdKeys && holdKeys.length > 0 ? holdKeys : undefined,
}),
});
} catch (error) {
console.error('Error in drag_mouse action:', error);
throw error;
}
}
async function scroll(input: {
coordinates?: Coordinates;
direction: 'up' | 'down' | 'left' | 'right';
scrollCount: number;
holdKeys?: string[];
}): Promise {
const { coordinates, direction, scrollCount, holdKeys } = input;
console.log(
`Scrolling ${direction} ${scrollCount} times ${coordinates ? `at coordinates: [${coordinates.x}, ${coordinates.y}]` : ''}`,
);
try {
await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'scroll',
coordinates,
direction,
scrollCount,
holdKeys: holdKeys && holdKeys.length > 0 ? holdKeys : undefined,
}),
});
} catch (error) {
console.error('Error in scroll action:', error);
throw error;
}
}
async function typeKeys(input: {
keys: string[];
delay?: number;
}): Promise {
const { keys, delay } = input;
console.log(`Typing keys: ${keys}`);
try {
await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'type_keys',
keys,
delay,
}),
});
} catch (error) {
console.error('Error in type_keys action:', error);
throw error;
}
}
async function pressKeys(input: {
keys: string[];
press: Press;
}): Promise {
const { keys, press } = input;
console.log(`Pressing keys: ${keys}`);
try {
await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'press_keys',
keys,
press,
}),
});
} catch (error) {
console.error('Error in press_keys action:', error);
throw error;
}
}
async function typeText(input: {
text: string;
delay?: number;
}): Promise {
const { text, delay } = input;
console.log(`Typing text: ${text}`);
try {
await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'type_text',
text,
delay,
}),
});
} catch (error) {
console.error('Error in type_text action:', error);
throw error;
}
}
async function pasteText(input: { text: string }): Promise {
const { text } = input;
console.log(`Pasting text: ${text}`);
try {
await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'paste_text',
text,
}),
});
} catch (error) {
console.error('Error in paste_text action:', error);
throw error;
}
}
async function wait(input: { duration: number }): Promise {
const { duration } = input;
console.log(`Waiting for ${duration}ms`);
try {
await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'wait',
duration,
}),
});
} catch (error) {
console.error('Error in wait action:', error);
throw error;
}
}
async function cursorPosition(): Promise {
console.log('Getting cursor position');
try {
const response = await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'cursor_position',
}),
});
const data = await response.json();
return { x: data.x, y: data.y };
} catch (error) {
console.error('Error in cursor_position action:', error);
throw error;
}
}
async function screenshot(): Promise {
console.log('Taking screenshot');
try {
const requestBody = {
action: 'screenshot',
};
const response = await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(requestBody),
});
if (!response.ok) {
throw new Error(`Failed to take screenshot: ${response.statusText}`);
}
const data = await response.json();
if (!data.image) {
throw new Error('Failed to take screenshot: No image data received');
}
return data.image; // Base64 encoded image
} catch (error) {
console.error('Error in screenshot action:', error);
throw error;
}
}
async function application(input: { application: string }): Promise {
const { application } = input;
console.log(`Opening application: ${application}`);
try {
await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'application',
application,
}),
});
} catch (error) {
console.error('Error in application action:', error);
throw error;
}
}
async function readFile(input: { path: string }): Promise<{
success: boolean;
data?: string;
name?: string;
size?: number;
mediaType?: string;
message?: string;
}> {
const { path } = input;
console.log(`Reading file: ${path}`);
try {
const response = await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'read_file',
path,
}),
});
if (!response.ok) {
throw new Error(`Failed to read file: ${response.statusText}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error('Error in read_file action:', error);
return {
success: false,
message: `Error reading file: ${error.message}`,
};
}
}
export async function writeFile(input: {
path: string;
content: string;
}): Promise<{ success: boolean; message?: string }> {
const { path, content } = input;
console.log(`Writing file: ${path}`);
try {
// Content is always base64 encoded
const base64Data = content;
const response = await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'write_file',
path,
data: base64Data,
}),
});
if (!response.ok) {
throw new Error(`Failed to write file: ${response.statusText}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error('Error in write_file action:', error);
return {
success: false,
message: `Error writing file: ${error.message}`,
};
}
}
================================================
FILE: packages/bytebot-agent/src/agent/agent.constants.ts
================================================
export const DEFAULT_DISPLAY_SIZE = {
width: 1280,
height: 960,
};
export const SUMMARIZATION_SYSTEM_PROMPT = `You are a helpful assistant that summarizes conversations for long-running tasks.
Your job is to create concise summaries that preserve all important information, tool usage, and key decisions.
Focus on:
- Task progress and completed actions
- Important tool calls and their results
- Key decisions made
- Any errors or issues encountered
- Current state and what remains to be done
Provide a structured summary that can be used as context for continuing the task.`;
export const AGENT_SYSTEM_PROMPT = `
You are **Bytebot**, a highly-reliable AI engineer operating a virtual computer whose display measures ${DEFAULT_DISPLAY_SIZE.width} x ${DEFAULT_DISPLAY_SIZE.height} pixels.
The current date is ${new Date().toLocaleDateString()}. The current time is ${new Date().toLocaleTimeString()}. The current timezone is ${Intl.DateTimeFormat().resolvedOptions().timeZone}.
────────────────────────
AVAILABLE APPLICATIONS
────────────────────────
On the computer, the following applications are available:
Firefox Browser -- The default web browser, use it to navigate to websites.
Thunderbird -- The default email client, use it to send and receive emails (if you have an account).
1Password -- The password manager, use it to store and retrieve your passwords (if you have an account).
Visual Studio Code -- The default code editor, use it to create and edit files.
Terminal -- The default terminal, use it to run commands.
File Manager -- The default file manager, use it to navigate and manage files.
Trash -- The default trash
ALL APPLICATIONS ARE GUI BASED, USE THE COMPUTER TOOLS TO INTERACT WITH THEM. ONLY ACCESS THE APPLICATIONS VIA THEIR DESKTOP ICONS.
*Never* use keyboard shortcuts to switch between applications, only use \`computer_application\` to switch between the default applications.
────────────────────────
CORE WORKING PRINCIPLES
────────────────────────
1. **Observe First** - *Always* invoke \`computer_screenshot\` before your first action **and** whenever the UI may have changed. Screenshot before every action when filling out forms. Never act blindly. When opening documents or PDFs, scroll through at least the first page to confirm it is the correct document.
2. **Navigate applications** = *Always* invoke \`computer_application\` to switch between the default applications.
3. **Human-Like Interaction**
• Move in smooth, purposeful paths; click near the visual centre of targets.
• Double-click desktop icons to open them.
• Type realistic, context-appropriate text with \`computer_type_text\` (for short strings) or \`computer_paste_text\` (for long strings), or shortcuts with \`computer_type_keys\`.
4. **Valid Keys Only** -
Use **exactly** the identifiers listed in **VALID KEYS** below when supplying \`keys\` to \`computer_type_keys\` or \`computer_press_keys\`. All identifiers come from nut-tree's \`Key\` enum; they are case-sensitive and contain *no spaces*.
5. **Verify Every Step** - After each action:
a. Take another screenshot.
b. Confirm the expected state before continuing. If it failed, retry sensibly (try again, and then try 2 different methods) before calling \`set_task_status\` with \`"status":"needs_help"\`.
6. **Efficiency & Clarity** - Combine related key presses; prefer scrolling or dragging over many small moves; minimise unnecessary waits.
7. **Stay Within Scope** - Do nothing the user didn't request; don't suggest unrelated tasks. For form and login fields, don't fill in random data, unless explicitly told to do so.
8. **Security** - If you see a password, secret key, or other sensitive information (or the user shares it with you), do not repeat it in conversation. When typing sensitive information, use \`computer_type_text\` with \`isSensitive\` set to \`true\`.
9. **Consistency & Persistence** - Even if the task is repetitive, do not end the task until the user's goal is completely met. For bulk operations, maintain focus and continue until all items are processed.
────────────────────────
REPETITIVE TASK HANDLING
────────────────────────
When performing repetitive tasks (e.g., "visit each profile", "process all items"):
1. **Track Progress** - Maintain a mental count of:
• Total items to process (if known)
• Items completed so far
• Current item being processed
• Any errors encountered
2. **Batch Processing** - For large sets:
• Process in groups of 10-20 items
• Take brief pauses between batches to prevent system overload
• Continue until ALL items are processed
3. **Error Recovery** - If an item fails:
• Note the error but continue with the next item
• Keep a list of failed items to report at the end
• Don't let one failure stop the entire operation
4. **Progress Updates** - Every 10-20 items:
• Brief status: "Processed 20/100 profiles, continuing..."
• No need for detailed reports unless requested
5. **Completion Criteria** - The task is NOT complete until:
• All items in the set are processed, OR
• You reach a clear endpoint (e.g., "No more profiles to load"), OR
• The user explicitly tells you to stop
6. **State Management** - If the task might span multiple tabs/pages:
• Save progress to a file periodically
• Include timestamps and item identifiers
────────────────────────
TASK LIFECYCLE TEMPLATE
────────────────────────
1. **Prepare** - Initial screenshot → plan → estimate scope if possible.
2. **Execute Loop** - For each sub-goal: Screenshot → Think → Act → Verify.
3. **Batch Loop** - For repetitive tasks:
• While items remain:
- Process batch of 10-20 items
- Update progress counter
- Check for stop conditions
- Brief status update
• Continue until ALL done
4. **Switch Applications** - If you need to switch between the default applications, reach the home directory, or return to the desktop, invoke
\`\`\`json
{ "name": "computer_application", "input": { "application": "application name" } }
\`\`\`
It will open (or focus if it is already open) the application, in fullscreen.
The application name must be one of the following: firefox, thunderbird, 1password, vscode, terminal, directory, desktop.
5. **Create other tasks** - If you need to create additional separate tasks, invoke
\`\`\`json
{ "name": "create_task", "input": { "description": "Subtask description", "type": "IMMEDIATE", "priority": "MEDIUM" } }
\`\`\`
The other tasks will be executed in the order they are created, after the current task is completed. Only create separate tasks if they are not related to the current task.
6. **Schedule future tasks** - If you need to schedule a task to run in the future, invoke
\`\`\`json
{ "name": "create_task", "input": { "description": "Subtask description", "type": "SCHEDULED", "scheduledFor": , "priority": "MEDIUM" } }
\`\`\`
Only schedule tasks if they must be run in the future. Do not schedule tasks that can be run immediately.
7. **Read Files** - If you need to read file contents, invoke
\`\`\`json
{ "name": "computer_read_file", "input": { "path": "/path/to/file" } }
\`\`\`
This tool reads files and returns them as document content blocks with base64 data, supporting various file types including documents (PDF, DOCX, TXT, etc.) and images (PNG, JPG, etc.).
8. **Ask for Help** - If you need clarification, or if you are unable to fully complete the task, invoke
\`\`\`json
{ "name": "set_task_status", "input": { "status": "needs_help", "description": "Summary of help or clarification needed" } }
\`\`\`
9. **Cleanup** - When the user's goal is met:
• Close every window, file, or app you opened so the desktop is tidy.
• Return to an idle desktop/background.
10. **Terminate** - ONLY ONCE THE USER'S GOAL IS COMPLETELY MET, As your final tool call and message, invoke
\`\`\`json
{ "name": "set_task_status", "input": { "status": "completed", "description": "Summary of the task" } }
\`\`\`
No further actions or messages will follow this call.
**IMPORTANT**: For bulk operations like "visit each profile in the directory":
- Do NOT mark as completed after just a few profiles
- Continue until you've processed ALL profiles or reached a clear end
- If there are 100+ profiles, process them ALL
- Only stop when explicitly told or when there are genuinely no more items
────────────────────────
VALID KEYS
────────────────────────
A, Add, AudioForward, AudioMute, AudioNext, AudioPause, AudioPlay, AudioPrev, AudioRandom, AudioRepeat, AudioRewind, AudioStop, AudioVolDown, AudioVolUp,
B, Backslash, Backspace,
C, CapsLock, Clear, Comma,
D, Decimal, Delete, Divide, Down,
E, End, Enter, Equal, Escape, F,
F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12, F13, F14, F15, F16, F17, F18, F19, F20, F21, F22, F23, F24,
Fn,
G, Grave,
H, Home,
I, Insert,
J, K, L, Left, LeftAlt, LeftBracket, LeftCmd, LeftControl, LeftShift, LeftSuper, LeftWin,
M, Menu, Minus, Multiply,
N, Num0, Num1, Num2, Num3, Num4, Num5, Num6, Num7, Num8, Num9, NumLock,
NumPad0, NumPad1, NumPad2, NumPad3, NumPad4, NumPad5, NumPad6, NumPad7, NumPad8, NumPad9,
O, P, PageDown, PageUp, Pause, Period, Print,
Q, Quote,
R, Return, Right, RightAlt, RightBracket, RightCmd, RightControl, RightShift, RightSuper, RightWin,
S, ScrollLock, Semicolon, Slash, Space, Subtract,
T, Tab,
U, Up,
V, W, X, Y, Z
Remember: **accuracy over speed, clarity and consistency over cleverness**.
Think before each move, keep the desktop clean when you're done, and **always** finish with \`set_task_status\`. Don't ask follow-up questions after completing the task.
**For repetitive tasks**: Persistence is key. Continue until ALL items are processed, not just the first few.
`;
================================================
FILE: packages/bytebot-agent/src/agent/agent.module.ts
================================================
import { Module } from '@nestjs/common';
import { TasksModule } from '../tasks/tasks.module';
import { MessagesModule } from '../messages/messages.module';
import { AnthropicModule } from '../anthropic/anthropic.module';
import { AgentProcessor } from './agent.processor';
import { ConfigModule } from '@nestjs/config';
import { AgentScheduler } from './agent.scheduler';
import { InputCaptureService } from './input-capture.service';
import { OpenAIModule } from '../openai/openai.module';
import { GoogleModule } from '../google/google.module';
import { SummariesModule } from 'src/summaries/summaries.modue';
import { AgentAnalyticsService } from './agent.analytics';
import { ProxyModule } from 'src/proxy/proxy.module';
@Module({
imports: [
ConfigModule,
TasksModule,
MessagesModule,
SummariesModule,
AnthropicModule,
OpenAIModule,
GoogleModule,
ProxyModule,
],
providers: [
AgentProcessor,
AgentScheduler,
InputCaptureService,
AgentAnalyticsService,
],
exports: [AgentProcessor],
})
export class AgentModule {}
================================================
FILE: packages/bytebot-agent/src/agent/agent.processor.ts
================================================
import { TasksService } from '../tasks/tasks.service';
import { MessagesService } from '../messages/messages.service';
import { Injectable, Logger } from '@nestjs/common';
import {
Message,
Role,
Task,
TaskPriority,
TaskStatus,
TaskType,
} from '@prisma/client';
import { AnthropicService } from '../anthropic/anthropic.service';
import {
isComputerToolUseContentBlock,
isSetTaskStatusToolUseBlock,
isCreateTaskToolUseBlock,
SetTaskStatusToolUseBlock,
} from '@bytebot/shared';
import {
MessageContentBlock,
MessageContentType,
ToolResultContentBlock,
TextContentBlock,
} from '@bytebot/shared';
import { InputCaptureService } from './input-capture.service';
import { OnEvent } from '@nestjs/event-emitter';
import { OpenAIService } from '../openai/openai.service';
import { GoogleService } from '../google/google.service';
import {
BytebotAgentModel,
BytebotAgentService,
BytebotAgentResponse,
} from './agent.types';
import {
AGENT_SYSTEM_PROMPT,
SUMMARIZATION_SYSTEM_PROMPT,
} from './agent.constants';
import { SummariesService } from '../summaries/summaries.service';
import { handleComputerToolUse } from './agent.computer-use';
import { ProxyService } from '../proxy/proxy.service';
@Injectable()
export class AgentProcessor {
private readonly logger = new Logger(AgentProcessor.name);
private currentTaskId: string | null = null;
private isProcessing = false;
private abortController: AbortController | null = null;
private services: Record = {};
constructor(
private readonly tasksService: TasksService,
private readonly messagesService: MessagesService,
private readonly summariesService: SummariesService,
private readonly anthropicService: AnthropicService,
private readonly openaiService: OpenAIService,
private readonly googleService: GoogleService,
private readonly proxyService: ProxyService,
private readonly inputCaptureService: InputCaptureService,
) {
this.services = {
anthropic: this.anthropicService,
openai: this.openaiService,
google: this.googleService,
proxy: this.proxyService,
};
this.logger.log('AgentProcessor initialized');
}
/**
* Check if the processor is currently processing a task
*/
isRunning(): boolean {
return this.isProcessing;
}
/**
* Get the current task ID being processed
*/
getCurrentTaskId(): string | null {
return this.currentTaskId;
}
@OnEvent('task.takeover')
handleTaskTakeover({ taskId }: { taskId: string }) {
this.logger.log(`Task takeover event received for task ID: ${taskId}`);
// If the agent is still processing this task, abort any in-flight operations
if (this.currentTaskId === taskId && this.isProcessing) {
this.abortController?.abort();
}
// Always start capturing user input so that emitted actions are received
this.inputCaptureService.start(taskId);
}
@OnEvent('task.resume')
handleTaskResume({ taskId }: { taskId: string }) {
if (this.currentTaskId === taskId && this.isProcessing) {
this.logger.log(`Task resume event received for task ID: ${taskId}`);
this.abortController = new AbortController();
void this.runIteration(taskId);
}
}
@OnEvent('task.cancel')
async handleTaskCancel({ taskId }: { taskId: string }) {
this.logger.log(`Task cancel event received for task ID: ${taskId}`);
await this.stopProcessing();
}
processTask(taskId: string) {
this.logger.log(`Starting processing for task ID: ${taskId}`);
if (this.isProcessing) {
this.logger.warn('AgentProcessor is already processing another task');
return;
}
this.isProcessing = true;
this.currentTaskId = taskId;
this.abortController = new AbortController();
// Kick off the first iteration without blocking the caller
void this.runIteration(taskId);
}
/**
* Runs a single iteration of task processing and schedules the next
* iteration via setImmediate while the task remains RUNNING.
*/
private async runIteration(taskId: string): Promise {
if (!this.isProcessing) {
return;
}
try {
const task: Task = await this.tasksService.findById(taskId);
if (task.status !== TaskStatus.RUNNING) {
this.logger.log(
`Task processing completed for task ID: ${taskId} with status: ${task.status}`,
);
this.isProcessing = false;
this.currentTaskId = null;
return;
}
this.logger.log(`Processing iteration for task ID: ${taskId}`);
// Refresh abort controller for this iteration to avoid accumulating
// "abort" listeners on a single AbortSignal across iterations.
this.abortController = new AbortController();
const latestSummary = await this.summariesService.findLatest(taskId);
const unsummarizedMessages =
await this.messagesService.findUnsummarized(taskId);
const messages = [
...(latestSummary
? [
{
id: '',
createdAt: new Date(),
updatedAt: new Date(),
taskId,
summaryId: null,
role: Role.USER,
content: [
{
type: MessageContentType.Text,
text: latestSummary.content,
},
],
},
]
: []),
...unsummarizedMessages,
];
this.logger.debug(
`Sending ${messages.length} messages to LLM for processing`,
);
const model = task.model as unknown as BytebotAgentModel;
let agentResponse: BytebotAgentResponse;
const service = this.services[model.provider];
if (!service) {
this.logger.warn(
`No service found for model provider: ${model.provider}`,
);
await this.tasksService.update(taskId, {
status: TaskStatus.FAILED,
});
this.isProcessing = false;
this.currentTaskId = null;
return;
}
agentResponse = await service.generateMessage(
AGENT_SYSTEM_PROMPT,
messages,
model.name,
true,
this.abortController.signal,
);
const messageContentBlocks = agentResponse.contentBlocks;
this.logger.debug(
`Received ${messageContentBlocks.length} content blocks from LLM`,
);
if (messageContentBlocks.length === 0) {
this.logger.warn(
`Task ID: ${taskId} received no content blocks from LLM, marking as failed`,
);
await this.tasksService.update(taskId, {
status: TaskStatus.FAILED,
});
this.isProcessing = false;
this.currentTaskId = null;
return;
}
await this.messagesService.create({
content: messageContentBlocks,
role: Role.ASSISTANT,
taskId,
});
// Calculate if we need to summarize based on token usage
const contextWindow = model.contextWindow || 200000; // Default to 200k if not specified
const contextThreshold = contextWindow * 0.75;
const shouldSummarize =
agentResponse.tokenUsage.totalTokens >= contextThreshold;
if (shouldSummarize) {
try {
// After we've successfully generated a response, we can summarize the unsummarized messages
const summaryResponse = await service.generateMessage(
SUMMARIZATION_SYSTEM_PROMPT,
[
...messages,
{
id: '',
createdAt: new Date(),
updatedAt: new Date(),
taskId,
summaryId: null,
role: Role.USER,
content: [
{
type: MessageContentType.Text,
text: 'Respond with a summary of the messages above. Do not include any additional information.',
},
],
},
],
model.name,
false,
this.abortController.signal,
);
const summaryContentBlocks = summaryResponse.contentBlocks;
this.logger.debug(
`Received ${summaryContentBlocks.length} summary content blocks from LLM`,
);
const summaryContent = summaryContentBlocks
.filter(
(block: MessageContentBlock) =>
block.type === MessageContentType.Text,
)
.map((block: TextContentBlock) => block.text)
.join('\n');
const summary = await this.summariesService.create({
content: summaryContent,
taskId,
});
await this.messagesService.attachSummary(taskId, summary.id, [
...messages.map((message) => {
return message.id;
}),
]);
this.logger.log(
`Generated summary for task ${taskId} due to token usage (${agentResponse.tokenUsage.totalTokens}/${contextWindow})`,
);
} catch (error: any) {
this.logger.error(
`Error summarizing messages for task ID: ${taskId}`,
error.stack,
);
}
}
this.logger.debug(
`Token usage for task ${taskId}: ${agentResponse.tokenUsage.totalTokens}/${contextWindow} (${Math.round((agentResponse.tokenUsage.totalTokens / contextWindow) * 100)}%)`,
);
const generatedToolResults: ToolResultContentBlock[] = [];
let setTaskStatusToolUseBlock: SetTaskStatusToolUseBlock | null = null;
for (const block of messageContentBlocks) {
if (isComputerToolUseContentBlock(block)) {
const result = await handleComputerToolUse(block, this.logger);
generatedToolResults.push(result);
}
if (isCreateTaskToolUseBlock(block)) {
const type = block.input.type?.toUpperCase() as TaskType;
const priority = block.input.priority?.toUpperCase() as TaskPriority;
await this.tasksService.create({
description: block.input.description,
type,
createdBy: Role.ASSISTANT,
...(block.input.scheduledFor && {
scheduledFor: new Date(block.input.scheduledFor),
}),
model: task.model,
priority,
});
generatedToolResults.push({
type: MessageContentType.ToolResult,
tool_use_id: block.id,
content: [
{
type: MessageContentType.Text,
text: 'The task has been created',
},
],
});
}
if (isSetTaskStatusToolUseBlock(block)) {
setTaskStatusToolUseBlock = block;
generatedToolResults.push({
type: MessageContentType.ToolResult,
tool_use_id: block.id,
is_error: block.input.status === 'failed',
content: [
{
type: MessageContentType.Text,
text: block.input.description,
},
],
});
}
}
if (generatedToolResults.length > 0) {
await this.messagesService.create({
content: generatedToolResults,
role: Role.USER,
taskId,
});
}
// Update the task status after all tool results have been generated if we have a set task status tool use block
if (setTaskStatusToolUseBlock) {
switch (setTaskStatusToolUseBlock.input.status) {
case 'completed':
await this.tasksService.update(taskId, {
status: TaskStatus.COMPLETED,
completedAt: new Date(),
});
break;
case 'needs_help':
await this.tasksService.update(taskId, {
status: TaskStatus.NEEDS_HELP,
});
break;
}
}
// Schedule the next iteration without blocking
if (this.isProcessing) {
setImmediate(() => this.runIteration(taskId));
}
} catch (error: any) {
if (error?.name === 'BytebotAgentInterrupt') {
this.logger.warn(`Processing aborted for task ID: ${taskId}`);
} else {
this.logger.error(
`Error during task processing iteration for task ID: ${taskId} - ${error.message}`,
error.stack,
);
await this.tasksService.update(taskId, {
status: TaskStatus.FAILED,
});
this.isProcessing = false;
this.currentTaskId = null;
}
}
}
async stopProcessing(): Promise {
if (!this.isProcessing) {
return;
}
this.logger.log(`Stopping execution of task ${this.currentTaskId}`);
// Signal any in-flight async operations to abort
this.abortController?.abort();
await this.inputCaptureService.stop();
this.isProcessing = false;
this.currentTaskId = null;
}
}
================================================
FILE: packages/bytebot-agent/src/agent/agent.scheduler.ts
================================================
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
import { TasksService } from '../tasks/tasks.service';
import { AgentProcessor } from './agent.processor';
import { TaskStatus } from '@prisma/client';
import { writeFile } from './agent.computer-use';
@Injectable()
export class AgentScheduler implements OnModuleInit {
private readonly logger = new Logger(AgentScheduler.name);
constructor(
private readonly tasksService: TasksService,
private readonly agentProcessor: AgentProcessor,
) {}
async onModuleInit() {
this.logger.log('AgentScheduler initialized');
await this.handleCron();
}
@Cron(CronExpression.EVERY_5_SECONDS)
async handleCron() {
const now = new Date();
const scheduledTasks = await this.tasksService.findScheduledTasks();
for (const scheduledTask of scheduledTasks) {
if (scheduledTask.scheduledFor && scheduledTask.scheduledFor < now) {
this.logger.debug(
`Task ID: ${scheduledTask.id} is scheduled for ${scheduledTask.scheduledFor}, queuing it`,
);
await this.tasksService.update(scheduledTask.id, {
queuedAt: now,
});
}
}
if (this.agentProcessor.isRunning()) {
return;
}
// Find the highest priority task to execute
const task = await this.tasksService.findNextTask();
if (task) {
if (task.files.length > 0) {
this.logger.debug(
`Task ID: ${task.id} has files, writing them to the desktop`,
);
for (const file of task.files) {
await writeFile({
path: `/home/user/Desktop/${file.name}`,
content: file.data, // file.data is already base64 encoded in the database
});
}
}
await this.tasksService.update(task.id, {
status: TaskStatus.RUNNING,
executedAt: new Date(),
});
this.logger.debug(`Processing task ID: ${task.id}`);
this.agentProcessor.processTask(task.id);
}
}
}
================================================
FILE: packages/bytebot-agent/src/agent/agent.tools.ts
================================================
/**
* Common schema definitions for reuse
*/
const coordinateSchema = {
type: 'object' as const,
properties: {
x: {
type: 'number' as const,
description: 'The x-coordinate',
},
y: {
type: 'number' as const,
description: 'The y-coordinate',
},
},
required: ['x', 'y'],
};
const holdKeysSchema = {
type: 'array' as const,
items: { type: 'string' as const },
description: 'Optional array of keys to hold during the action',
nullable: true,
};
const buttonSchema = {
type: 'string' as const,
enum: ['left', 'right', 'middle'],
description: 'The mouse button',
};
/**
* Tool definitions for mouse actions
*/
export const _moveMouseTool = {
name: 'computer_move_mouse',
description: 'Moves the mouse cursor to the specified coordinates',
input_schema: {
type: 'object' as const,
properties: {
coordinates: {
...coordinateSchema,
description: 'Target coordinates for mouse movement',
},
},
required: ['coordinates'],
},
};
export const _traceMouseTool = {
name: 'computer_trace_mouse',
description: 'Moves the mouse cursor along a specified path of coordinates',
input_schema: {
type: 'object' as const,
properties: {
path: {
type: 'array' as const,
items: coordinateSchema,
description: 'Array of coordinate objects representing the path',
},
holdKeys: holdKeysSchema,
},
required: ['path'],
},
};
export const _clickMouseTool = {
name: 'computer_click_mouse',
description:
'Performs a mouse click at the specified coordinates or current position',
input_schema: {
type: 'object' as const,
properties: {
coordinates: {
...coordinateSchema,
description:
'Optional click coordinates (defaults to current position)',
nullable: true,
},
button: buttonSchema,
holdKeys: holdKeysSchema,
clickCount: {
type: 'integer' as const,
description: 'Number of clicks to perform (e.g., 2 for double-click)',
default: 1,
},
},
required: ['button', 'clickCount'],
},
};
export const _pressMouseTool = {
name: 'computer_press_mouse',
description: 'Presses or releases a specified mouse button',
input_schema: {
type: 'object' as const,
properties: {
coordinates: {
...coordinateSchema,
description: 'Optional coordinates (defaults to current position)',
nullable: true,
},
button: buttonSchema,
press: {
type: 'string' as const,
enum: ['up', 'down'],
description: 'Whether to press down or release up',
},
},
required: ['button', 'press'],
},
};
export const _dragMouseTool = {
name: 'computer_drag_mouse',
description: 'Drags the mouse along a path while holding a button',
input_schema: {
type: 'object' as const,
properties: {
path: {
type: 'array' as const,
items: coordinateSchema,
description: 'Array of coordinates representing the drag path',
},
button: buttonSchema,
holdKeys: holdKeysSchema,
},
required: ['path', 'button'],
},
};
export const _scrollTool = {
name: 'computer_scroll',
description: 'Scrolls the mouse wheel in the specified direction',
input_schema: {
type: 'object' as const,
properties: {
coordinates: {
...coordinateSchema,
description: 'Coordinates where the scroll should occur',
},
direction: {
type: 'string' as const,
enum: ['up', 'down', 'left', 'right'],
description: 'The direction to scroll',
},
scrollCount: {
type: 'integer' as const,
description: 'Number of scroll steps',
},
holdKeys: holdKeysSchema,
},
required: ['coordinates', 'direction', 'scrollCount'],
},
};
/**
* Tool definitions for keyboard actions
*/
export const _typeKeysTool = {
name: 'computer_type_keys',
description: 'Types a sequence of keys (useful for keyboard shortcuts)',
input_schema: {
type: 'object' as const,
properties: {
keys: {
type: 'array' as const,
items: { type: 'string' as const },
description: 'Array of key names to type in sequence',
},
delay: {
type: 'number' as const,
description: 'Optional delay in milliseconds between key presses',
nullable: true,
},
},
required: ['keys'],
},
};
export const _pressKeysTool = {
name: 'computer_press_keys',
description:
'Presses or releases specific keys (useful for holding modifiers)',
input_schema: {
type: 'object' as const,
properties: {
keys: {
type: 'array' as const,
items: { type: 'string' as const },
description: 'Array of key names to press or release',
},
press: {
type: 'string' as const,
enum: ['up', 'down'],
description: 'Whether to press down or release up',
},
},
required: ['keys', 'press'],
},
};
export const _typeTextTool = {
name: 'computer_type_text',
description:
'Types a string of text character by character. Use this tool for strings less than 25 characters, or passwords/sensitive form fields.',
input_schema: {
type: 'object' as const,
properties: {
text: {
type: 'string' as const,
description: 'The text string to type',
},
delay: {
type: 'number' as const,
description: 'Optional delay in milliseconds between characters',
nullable: true,
},
isSensitive: {
type: 'boolean' as const,
description: 'Flag to indicate sensitive information',
nullable: true,
},
},
required: ['text'],
},
};
export const _pasteTextTool = {
name: 'computer_paste_text',
description:
'Copies text to the clipboard and pastes it. Use this tool for typing long text strings or special characters not on the standard keyboard.',
input_schema: {
type: 'object' as const,
properties: {
text: {
type: 'string' as const,
description: 'The text string to type',
},
isSensitive: {
type: 'boolean' as const,
description: 'Flag to indicate sensitive information',
nullable: true,
},
},
required: ['text'],
},
};
/**
* Tool definitions for utility actions
*/
export const _waitTool = {
name: 'computer_wait',
description: 'Pauses execution for a specified duration',
input_schema: {
type: 'object' as const,
properties: {
duration: {
type: 'integer' as const,
enum: [500],
description: 'The duration to wait in milliseconds',
},
},
required: ['duration'],
},
};
export const _screenshotTool = {
name: 'computer_screenshot',
description: 'Captures a screenshot of the current screen',
input_schema: {
type: 'object' as const,
properties: {},
},
};
export const _cursorPositionTool = {
name: 'computer_cursor_position',
description: 'Gets the current (x, y) coordinates of the mouse cursor',
input_schema: {
type: 'object' as const,
properties: {},
},
};
export const _applicationTool = {
name: 'computer_application',
description: 'Opens or focuses an application and ensures it is fullscreen',
input_schema: {
type: 'object' as const,
properties: {
application: {
type: 'string' as const,
enum: [
'firefox',
'1password',
'thunderbird',
'vscode',
'terminal',
'desktop',
'directory',
],
description: 'The application to open or focus',
},
},
required: ['application'],
},
};
/**
* Tool definitions for task management
*/
export const _setTaskStatusTool = {
name: 'set_task_status',
description: 'Sets the status of the current task',
input_schema: {
type: 'object' as const,
properties: {
status: {
type: 'string' as const,
enum: ['completed', 'needs_help'],
description: 'The status of the task',
},
description: {
type: 'string' as const,
description:
'If the task is completed, a summary of the task. If the task needs help, a description of the issue or clarification needed.',
},
},
required: ['status', 'description'],
},
};
export const _createTaskTool = {
name: 'create_task',
description: 'Creates a new task',
input_schema: {
type: 'object' as const,
properties: {
description: {
type: 'string' as const,
description: 'The description of the task',
},
type: {
type: 'string' as const,
enum: ['IMMEDIATE', 'SCHEDULED'],
description: 'The type of the task (defaults to IMMEDIATE)',
},
scheduledFor: {
type: 'string' as const,
format: 'date-time',
description: 'RFC 3339 / ISO 8601 datetime for scheduled tasks',
},
priority: {
type: 'string' as const,
enum: ['LOW', 'MEDIUM', 'HIGH', 'URGENT'],
description: 'The priority of the task (defaults to MEDIUM)',
},
},
required: ['description'],
},
};
/**
* Tool definition for reading files
*/
export const _readFileTool = {
name: 'computer_read_file',
description:
'Reads a file from the specified path and returns it as a document content block with base64 encoded data',
input_schema: {
type: 'object' as const,
properties: {
path: {
type: 'string' as const,
description: 'The file path to read from',
},
},
required: ['path'],
},
};
/**
* Export all tools as an array
*/
export const agentTools = [
_moveMouseTool,
_traceMouseTool,
_clickMouseTool,
_pressMouseTool,
_dragMouseTool,
_scrollTool,
_typeKeysTool,
_pressKeysTool,
_typeTextTool,
_pasteTextTool,
_waitTool,
_screenshotTool,
_applicationTool,
_cursorPositionTool,
_setTaskStatusTool,
_createTaskTool,
_readFileTool,
];
================================================
FILE: packages/bytebot-agent/src/agent/agent.types.ts
================================================
import { Message } from '@prisma/client';
import { MessageContentBlock } from '@bytebot/shared';
export interface BytebotAgentResponse {
contentBlocks: MessageContentBlock[];
tokenUsage: {
inputTokens: number;
outputTokens: number;
totalTokens: number;
};
}
export interface BytebotAgentService {
generateMessage(
systemPrompt: string,
messages: Message[],
model: string,
useTools: boolean,
signal?: AbortSignal,
): Promise;
}
export interface BytebotAgentModel {
provider: 'anthropic' | 'openai' | 'google' | 'proxy';
name: string;
title: string;
contextWindow?: number;
}
export class BytebotAgentInterrupt extends Error {
constructor() {
super('BytebotAgentInterrupt');
this.name = 'BytebotAgentInterrupt';
}
}
================================================
FILE: packages/bytebot-agent/src/agent/input-capture.service.ts
================================================
import { Injectable, Logger } from '@nestjs/common';
import { io, Socket } from 'socket.io-client';
import { randomUUID } from 'crypto';
import {
convertClickMouseActionToToolUseBlock,
convertDragMouseActionToToolUseBlock,
convertPressKeysActionToToolUseBlock,
convertPressMouseActionToToolUseBlock,
convertScrollActionToToolUseBlock,
convertTypeKeysActionToToolUseBlock,
convertTypeTextActionToToolUseBlock,
ImageContentBlock,
MessageContentBlock,
MessageContentType,
ScreenshotToolUseBlock,
ToolResultContentBlock,
UserActionContentBlock,
} from '@bytebot/shared';
import { Role } from '@prisma/client';
import { MessagesService } from '../messages/messages.service';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class InputCaptureService {
private readonly logger = new Logger(InputCaptureService.name);
private socket: Socket | null = null;
private capturing = false;
constructor(
private readonly messagesService: MessagesService,
private readonly configService: ConfigService,
) {}
isCapturing() {
return this.capturing;
}
start(taskId: string) {
if (this.socket?.connected && this.capturing) return;
if (this.socket && !this.socket.connected) {
this.socket.connect();
return;
}
const baseUrl = this.configService.get('BYTEBOT_DESKTOP_BASE_URL');
if (!baseUrl) {
this.logger.warn('BYTEBOT_DESKTOP_BASE_URL missing.');
return;
}
this.socket = io(baseUrl, { transports: ['websocket'] });
this.socket.on('connect', () => {
this.logger.log('Input socket connected');
this.capturing = true;
});
this.socket.on(
'screenshotAndAction',
async (shot: { image: string }, action: any) => {
if (!this.capturing || !taskId) return;
// The gateway only sends a click_mouse or drag_mouse action together with screenshots for now.
if (action.action !== 'click_mouse' && action.action !== 'drag_mouse')
return;
const userActionBlock: UserActionContentBlock = {
type: MessageContentType.UserAction,
content: [
{
type: MessageContentType.Image,
source: {
data: shot.image,
media_type: 'image/png',
type: 'base64',
},
},
],
};
const toolUseId = randomUUID();
switch (action.action) {
case 'drag_mouse':
userActionBlock.content.push(
convertDragMouseActionToToolUseBlock(action, toolUseId),
);
break;
case 'click_mouse':
userActionBlock.content.push(
convertClickMouseActionToToolUseBlock(action, toolUseId),
);
break;
}
await this.messagesService.create({
content: [userActionBlock],
role: Role.USER,
taskId,
});
},
);
this.socket.on('action', async (action: any) => {
if (!this.capturing || !taskId) return;
const toolUseId = randomUUID();
const userActionBlock: UserActionContentBlock = {
type: MessageContentType.UserAction,
content: [],
};
switch (action.action) {
case 'drag_mouse':
userActionBlock.content.push(
convertDragMouseActionToToolUseBlock(action, toolUseId),
);
break;
case 'press_mouse':
userActionBlock.content.push(
convertPressMouseActionToToolUseBlock(action, toolUseId),
);
break;
case 'type_keys':
userActionBlock.content.push(
convertTypeKeysActionToToolUseBlock(action, toolUseId),
);
break;
case 'press_keys':
userActionBlock.content.push(
convertPressKeysActionToToolUseBlock(action, toolUseId),
);
break;
case 'type_text':
userActionBlock.content.push(
convertTypeTextActionToToolUseBlock(action, toolUseId),
);
break;
case 'scroll':
userActionBlock.content.push(
convertScrollActionToToolUseBlock(action, toolUseId),
);
break;
default:
this.logger.warn(`Unknown action ${action.action}`);
}
if (userActionBlock.content.length > 0) {
await this.messagesService.create({
content: [userActionBlock],
role: Role.USER,
taskId,
});
}
});
this.socket.on('disconnect', () => {
this.logger.log('Input socket disconnected');
this.capturing = false;
});
}
async stop() {
if (!this.socket) return;
if (this.socket.connected) this.socket.disconnect();
else this.socket.removeAllListeners();
this.socket = null;
this.capturing = false;
}
}
================================================
FILE: packages/bytebot-agent/src/anthropic/anthropic.constants.ts
================================================
import { BytebotAgentModel } from '../agent/agent.types';
export const ANTHROPIC_MODELS: BytebotAgentModel[] = [
{
provider: 'anthropic',
name: 'claude-opus-4-1-20250805',
title: 'Claude Opus 4.1',
contextWindow: 200000,
},
{
provider: 'anthropic',
name: 'claude-sonnet-4-20250514',
title: 'Claude Sonnet 4',
contextWindow: 200000,
},
];
export const DEFAULT_MODEL = ANTHROPIC_MODELS[0];
================================================
FILE: packages/bytebot-agent/src/anthropic/anthropic.module.ts
================================================
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { AnthropicService } from './anthropic.service';
@Module({
imports: [ConfigModule],
providers: [AnthropicService],
exports: [AnthropicService],
})
export class AnthropicModule {}
================================================
FILE: packages/bytebot-agent/src/anthropic/anthropic.service.ts
================================================
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import Anthropic, { APIUserAbortError } from '@anthropic-ai/sdk';
import {
MessageContentBlock,
MessageContentType,
TextContentBlock,
ToolUseContentBlock,
ThinkingContentBlock,
RedactedThinkingContentBlock,
isUserActionContentBlock,
isComputerToolUseContentBlock,
} from '@bytebot/shared';
import { DEFAULT_MODEL } from './anthropic.constants';
import { Message, Role } from '@prisma/client';
import { anthropicTools } from './anthropic.tools';
import {
BytebotAgentService,
BytebotAgentInterrupt,
BytebotAgentResponse,
} from '../agent/agent.types';
@Injectable()
export class AnthropicService implements BytebotAgentService {
private readonly anthropic: Anthropic;
private readonly logger = new Logger(AnthropicService.name);
constructor(private readonly configService: ConfigService) {
const apiKey = this.configService.get('ANTHROPIC_API_KEY');
if (!apiKey) {
this.logger.warn(
'ANTHROPIC_API_KEY is not set. AnthropicService will not work properly.',
);
}
this.anthropic = new Anthropic({
apiKey: apiKey || 'dummy-key-for-initialization',
});
}
async generateMessage(
systemPrompt: string,
messages: Message[],
model: string = DEFAULT_MODEL.name,
useTools: boolean = true,
signal?: AbortSignal,
): Promise {
try {
const maxTokens = 8192;
// Convert our message content blocks to Anthropic's expected format
const anthropicMessages = this.formatMessagesForAnthropic(messages);
// add cache_control to last tool
anthropicTools[anthropicTools.length - 1].cache_control = {
type: 'ephemeral',
};
// Make the API call
const response = await this.anthropic.messages.create(
{
model,
max_tokens: maxTokens * 2,
thinking: { type: 'disabled' },
system: [
{
type: 'text',
text: systemPrompt,
cache_control: { type: 'ephemeral' },
},
],
messages: anthropicMessages,
tools: useTools ? anthropicTools : [],
},
{ signal },
);
// Convert Anthropic's response to our message content blocks format
return {
contentBlocks: this.formatAnthropicResponse(response.content),
tokenUsage: {
inputTokens: response.usage.input_tokens,
outputTokens: response.usage.output_tokens,
totalTokens:
response.usage.input_tokens + response.usage.output_tokens,
},
};
} catch (error) {
this.logger.log(error);
if (error instanceof APIUserAbortError) {
this.logger.log('Anthropic API call aborted');
throw new BytebotAgentInterrupt();
}
this.logger.error(
`Error sending message to Anthropic: ${error.message}`,
error.stack,
);
throw error;
}
}
/**
* Convert our MessageContentBlock format to Anthropic's message format
*/
private formatMessagesForAnthropic(
messages: Message[],
): Anthropic.MessageParam[] {
const anthropicMessages: Anthropic.MessageParam[] = [];
// Process each message content block
for (const [index, message] of messages.entries()) {
const messageContentBlocks = message.content as MessageContentBlock[];
const content: Anthropic.ContentBlockParam[] = [];
if (
messageContentBlocks.every((block) => isUserActionContentBlock(block))
) {
const userActionContentBlocks = messageContentBlocks.flatMap(
(block) => block.content,
);
for (const block of userActionContentBlocks) {
if (isComputerToolUseContentBlock(block)) {
content.push({
type: 'text',
text: `User performed action: ${block.name}\n${JSON.stringify(block.input, null, 2)}`,
});
} else {
content.push(block as Anthropic.ContentBlockParam);
}
}
} else {
content.push(
...messageContentBlocks.map(
(block) => block as Anthropic.ContentBlockParam,
),
);
}
if (index === messages.length - 1) {
content[content.length - 1]['cache_control'] = {
type: 'ephemeral',
};
}
anthropicMessages.push({
role: message.role === Role.USER ? 'user' : 'assistant',
content: content,
});
}
return anthropicMessages;
}
/**
* Convert Anthropic's response content to our MessageContentBlock format
*/
private formatAnthropicResponse(
content: Anthropic.ContentBlock[],
): MessageContentBlock[] {
return content.map((block) => {
switch (block.type) {
case 'text':
return {
type: MessageContentType.Text,
text: block.text,
} as TextContentBlock;
case 'tool_use':
return {
type: MessageContentType.ToolUse,
id: block.id,
name: block.name,
input: block.input,
} as ToolUseContentBlock;
case 'thinking':
return {
type: MessageContentType.Thinking,
thinking: block.thinking,
signature: block.signature,
} as ThinkingContentBlock;
case 'redacted_thinking':
return {
type: MessageContentType.RedactedThinking,
data: block.data,
} as RedactedThinkingContentBlock;
}
});
}
}
================================================
FILE: packages/bytebot-agent/src/anthropic/anthropic.tools.ts
================================================
import Anthropic from '@anthropic-ai/sdk';
import { agentTools } from '../agent/agent.tools';
/**
* Converts an agent tool definition to an Anthropic.Tool
*/
function agentToolToAnthropicTool(agentTool: any): Anthropic.Tool {
return agentTool as Anthropic.Tool;
}
/**
* Creates a mapped object of tools by name
*/
const toolMap = agentTools.reduce(
(acc, tool) => {
const anthropicTool = agentToolToAnthropicTool(tool);
const camelCaseName = tool.name
.split('_')
.map((part, index) => {
if (index === 0) return part;
if (part === 'computer') return '';
return part.charAt(0).toUpperCase() + part.slice(1);
})
.join('')
.replace(/^computer/, '');
acc[camelCaseName + 'Tool'] = anthropicTool;
return acc;
},
{} as Record,
);
// Export individual tools with proper names
export const moveMouseTool = toolMap.moveMouseTool;
export const traceMouseTool = toolMap.traceMouseTool;
export const clickMouseTool = toolMap.clickMouseTool;
export const pressMouseTool = toolMap.pressMouseTool;
export const dragMouseTool = toolMap.dragMouseTool;
export const scrollTool = toolMap.scrollTool;
export const typeKeysTool = toolMap.typeKeysTool;
export const pressKeysTool = toolMap.pressKeysTool;
export const typeTextTool = toolMap.typeTextTool;
export const pasteTextTool = toolMap.pasteTextTool;
export const waitTool = toolMap.waitTool;
export const screenshotTool = toolMap.screenshotTool;
export const cursorPositionTool = toolMap.cursorPositionTool;
export const setTaskStatusTool = toolMap.setTaskStatusTool;
export const createTaskTool = toolMap.createTaskTool;
export const applicationTool = toolMap.applicationTool;
// Array of all tools
export const anthropicTools: Anthropic.Tool[] = agentTools.map(
agentToolToAnthropicTool,
);
================================================
FILE: packages/bytebot-agent/src/app.controller.ts
================================================
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
}
================================================
FILE: packages/bytebot-agent/src/app.module.ts
================================================
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { AgentModule } from './agent/agent.module';
import { TasksModule } from './tasks/tasks.module';
import { MessagesModule } from './messages/messages.module';
import { AnthropicModule } from './anthropic/anthropic.module';
import { OpenAIModule } from './openai/openai.module';
import { GoogleModule } from './google/google.module';
import { PrismaModule } from './prisma/prisma.module';
import { ConfigModule } from '@nestjs/config';
import { ScheduleModule } from '@nestjs/schedule';
import { EventEmitterModule } from '@nestjs/event-emitter';
import { SummariesModule } from './summaries/summaries.modue';
import { ProxyModule } from './proxy/proxy.module';
@Module({
imports: [
ScheduleModule.forRoot(),
EventEmitterModule.forRoot(),
ConfigModule.forRoot({
isGlobal: true,
}),
AgentModule,
TasksModule,
MessagesModule,
SummariesModule,
AnthropicModule,
OpenAIModule,
GoogleModule,
ProxyModule,
PrismaModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
================================================
FILE: packages/bytebot-agent/src/app.service.ts
================================================
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}
================================================
FILE: packages/bytebot-agent/src/google/google.constants.ts
================================================
import { BytebotAgentModel } from '../agent/agent.types';
export const GOOGLE_MODELS: BytebotAgentModel[] = [
{
provider: 'google',
name: 'gemini-2.5-pro',
title: 'Gemini 2.5 Pro',
contextWindow: 1000000,
},
{
provider: 'google',
name: 'gemini-2.5-flash',
title: 'Gemini 2.5 Flash',
contextWindow: 1000000,
},
];
export const DEFAULT_MODEL = GOOGLE_MODELS[0];
================================================
FILE: packages/bytebot-agent/src/google/google.module.ts
================================================
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { GoogleService } from './google.service';
@Module({
imports: [ConfigModule],
providers: [GoogleService],
exports: [GoogleService],
})
export class GoogleModule {}
================================================
FILE: packages/bytebot-agent/src/google/google.service.ts
================================================
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import {
isComputerToolUseContentBlock,
isImageContentBlock,
isUserActionContentBlock,
MessageContentBlock,
MessageContentType,
TextContentBlock,
ThinkingContentBlock,
ToolUseContentBlock,
} from '@bytebot/shared';
import {
BytebotAgentService,
BytebotAgentInterrupt,
BytebotAgentResponse,
} from '../agent/agent.types';
import { Message, Role } from '@prisma/client';
import { googleTools } from './google.tools';
import {
Content,
GenerateContentResponse,
GoogleGenAI,
Part,
} from '@google/genai';
import { v4 as uuid } from 'uuid';
import { DEFAULT_MODEL } from './google.constants';
@Injectable()
export class GoogleService implements BytebotAgentService {
private readonly google: GoogleGenAI;
private readonly logger = new Logger(GoogleService.name);
constructor(private readonly configService: ConfigService) {
const apiKey = this.configService.get('GEMINI_API_KEY');
if (!apiKey) {
this.logger.warn(
'GEMINI_API_KEY is not set. GoogleService will not work properly.',
);
}
this.google = new GoogleGenAI({
apiKey: apiKey || 'dummy-key-for-initialization',
});
}
async generateMessage(
systemPrompt: string,
messages: Message[],
model: string = DEFAULT_MODEL.name,
useTools: boolean = true,
signal?: AbortSignal,
): Promise {
try {
const maxTokens = 8192;
// Convert our message content blocks to Anthropic's expected format
const googleMessages = this.formatMessagesForGoogle(messages);
const response: GenerateContentResponse =
await this.google.models.generateContent({
model,
contents: googleMessages,
config: {
thinkingConfig: {
thinkingBudget: 24576,
},
maxOutputTokens: maxTokens,
systemInstruction: systemPrompt,
tools: useTools
? [
{
functionDeclarations: googleTools,
},
]
: [],
abortSignal: signal,
},
});
const candidate = response.candidates?.[0];
if (!candidate) {
throw new Error('No candidate found in response');
}
const content = candidate.content;
if (!content) {
throw new Error('No content found in candidate');
}
if (!content.parts) {
throw new Error('No parts found in content');
}
return {
contentBlocks: this.formatGoogleResponse(content.parts),
tokenUsage: {
inputTokens: response.usageMetadata?.promptTokenCount || 0,
outputTokens: response.usageMetadata?.candidatesTokenCount || 0,
totalTokens: response.usageMetadata?.totalTokenCount || 0,
},
};
} catch (error) {
if (error.message.includes('AbortError')) {
throw new BytebotAgentInterrupt();
}
this.logger.error(
`Error sending message to Google Gemini: ${error.message}`,
error.stack,
);
throw error;
}
}
/**
* Convert our MessageContentBlock format to Google Gemini's message format
*/
private formatMessagesForGoogle(messages: Message[]): Content[] {
const googleMessages: Content[] = [];
// Process each message content block
for (const message of messages) {
const messageContentBlocks = message.content as MessageContentBlock[];
const parts: Part[] = [];
if (
messageContentBlocks.every((block) => isUserActionContentBlock(block))
) {
const userActionContentBlocks = messageContentBlocks.flatMap(
(block) => block.content,
);
for (const block of userActionContentBlocks) {
if (isComputerToolUseContentBlock(block)) {
parts.push({
text: `User performed action: ${block.name}\n${JSON.stringify(block.input, null, 2)}`,
});
} else if (isImageContentBlock(block)) {
parts.push({
inlineData: {
data: block.source.data,
mimeType: block.source.media_type,
},
});
}
}
} else {
for (const block of messageContentBlocks) {
switch (block.type) {
case MessageContentType.Text:
parts.push({
text: block.text,
});
break;
case MessageContentType.ToolUse:
parts.push({
functionCall: {
id: block.id,
name: block.name,
args: block.input,
},
});
break;
case MessageContentType.Image:
parts.push({
inlineData: {
data: block.source.data,
mimeType: block.source.media_type,
},
});
break;
case MessageContentType.ToolResult: {
const toolResultContentBlock = block.content[0];
if (toolResultContentBlock.type === MessageContentType.Image) {
parts.push({
functionResponse: {
id: block.tool_use_id,
name: 'screenshot',
response: {
...(!block.is_error && {
output: 'screenshot successful',
}),
...(block.is_error && { error: block.content[0] }),
},
},
});
parts.push({
inlineData: {
data: toolResultContentBlock.source.data,
mimeType: toolResultContentBlock.source.media_type,
},
});
break;
}
parts.push({
functionResponse: {
id: block.tool_use_id,
name: this.getToolName(block.tool_use_id, messages),
response: {
...(!block.is_error && { output: block.content[0] }),
...(block.is_error && { error: block.content[0] }),
},
},
});
break;
}
case MessageContentType.Thinking:
parts.push({
text: block.thinking,
thoughtSignature: block.signature,
thought: true,
});
break;
default:
parts.push({
text: JSON.stringify(block),
});
break;
}
}
}
googleMessages.push({
role: message.role === Role.USER ? 'user' : 'model',
parts: parts,
});
}
return googleMessages;
}
// Find the content block with the tool_use_id and return the name
private getToolName(
tool_use_id: string,
messages: Message[],
): string | undefined {
const toolMessage = messages.find((message) =>
(message.content as MessageContentBlock[]).some(
(block) =>
block.type === MessageContentType.ToolUse && block.id === tool_use_id,
),
);
if (!toolMessage) {
return undefined;
}
const toolBlock = (toolMessage.content as MessageContentBlock[]).find(
(block) =>
block.type === MessageContentType.ToolUse && block.id === tool_use_id,
);
if (!toolBlock) {
return undefined;
}
return (toolBlock as ToolUseContentBlock).name;
}
/**
* Convert Google Gemini's response content to our MessageContentBlock format
*/
private formatGoogleResponse(parts: Part[]): MessageContentBlock[] {
return parts.map((part) => {
if (part.text) {
return {
type: MessageContentType.Text,
text: part.text,
} as TextContentBlock;
}
if (part.thought) {
return {
type: MessageContentType.Thinking,
signature: part.thoughtSignature,
thinking: part.text,
} as ThinkingContentBlock;
}
if (part.functionCall) {
return {
type: MessageContentType.ToolUse,
id: part.functionCall.id || uuid(),
name: part.functionCall.name,
input: part.functionCall.args,
} as ToolUseContentBlock;
}
this.logger.warn(`Unknown content type from Google: ${part}`);
return {
type: MessageContentType.Text,
text: JSON.stringify(part),
} as TextContentBlock;
});
}
}
================================================
FILE: packages/bytebot-agent/src/google/google.tools.ts
================================================
import { FunctionDeclaration, Type } from '@google/genai';
import { agentTools } from '../agent/agent.tools';
/**
* Converts JSON Schema type to Google Genai Type
*/
function jsonSchemaTypeToGoogleType(type: string): Type {
switch (type) {
case 'string':
return Type.STRING;
case 'number':
return Type.NUMBER;
case 'integer':
return Type.INTEGER;
case 'boolean':
return Type.BOOLEAN;
case 'array':
return Type.ARRAY;
case 'object':
return Type.OBJECT;
default:
return Type.STRING;
}
}
/**
* Converts JSON Schema to Google Genai parameter schema
*/
function convertJsonSchemaToGoogleSchema(schema: any): any {
if (!schema) return {};
const result: any = {
type: jsonSchemaTypeToGoogleType(schema.type),
};
if (schema.description) {
result.description = schema.description;
}
// Only include enum if the property type is string; otherwise it is invalid for Google GenAI
if (schema.type === 'string' && schema.enum && Array.isArray(schema.enum)) {
result.enum = schema.enum;
}
if (schema.nullable) {
result.nullable = true;
}
if (schema.type === 'array' && schema.items) {
result.items = convertJsonSchemaToGoogleSchema(schema.items);
}
if (schema.type === 'object' && schema.properties) {
result.properties = {};
for (const [key, value] of Object.entries(schema.properties)) {
result.properties[key] = convertJsonSchemaToGoogleSchema(value);
}
if (schema.required) {
result.required = schema.required;
}
}
return result;
}
/**
* Converts an agent tool definition to a Google FunctionDeclaration
*/
function agentToolToGoogleTool(agentTool: any): FunctionDeclaration {
const parameters = convertJsonSchemaToGoogleSchema(agentTool.input_schema);
return {
name: agentTool.name,
description: agentTool.description,
parameters,
};
}
/**
* Creates a mapped object of tools by name
*/
const toolMap = agentTools.reduce(
(acc, tool) => {
const googleTool = agentToolToGoogleTool(tool);
const camelCaseName = tool.name
.split('_')
.map((part, index) => {
if (index === 0) return part;
if (part === 'computer') return '';
return part.charAt(0).toUpperCase() + part.slice(1);
})
.join('')
.replace(/^computer/, '');
acc[camelCaseName + 'Tool'] = googleTool;
return acc;
},
{} as Record,
);
// Export individual tools with proper names
export const moveMouseTool = toolMap.moveMouseTool;
export const traceMouseTool = toolMap.traceMouseTool;
export const clickMouseTool = toolMap.clickMouseTool;
export const pressMouseTool = toolMap.pressMouseTool;
export const dragMouseTool = toolMap.dragMouseTool;
export const scrollTool = toolMap.scrollTool;
export const typeKeysTool = toolMap.typeKeysTool;
export const pressKeysTool = toolMap.pressKeysTool;
export const typeTextTool = toolMap.typeTextTool;
export const pasteTextTool = toolMap.pasteTextTool;
export const waitTool = toolMap.waitTool;
export const screenshotTool = toolMap.screenshotTool;
export const cursorPositionTool = toolMap.cursorPositionTool;
export const setTaskStatusTool = toolMap.setTaskStatusTool;
export const createTaskTool = toolMap.createTaskTool;
export const applicationTool = toolMap.applicationTool;
// Array of all tools
export const googleTools: FunctionDeclaration[] = agentTools.map(
agentToolToGoogleTool,
);
================================================
FILE: packages/bytebot-agent/src/main.ts
================================================
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { webcrypto } from 'crypto';
import { json, urlencoded } from 'express';
// Polyfill for crypto global (required by @nestjs/schedule)
if (!globalThis.crypto) {
globalThis.crypto = webcrypto as any;
}
async function bootstrap() {
console.log('Starting bytebot-agent application...');
try {
const app = await NestFactory.create(AppModule);
// Configure body parser with increased payload size limit (50MB)
app.use(json({ limit: '50mb' }));
app.use(urlencoded({ limit: '50mb', extended: true }));
// Enable CORS
app.enableCors({
origin: '*',
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'PATCH'],
});
await app.listen(process.env.PORT ?? 9991);
} catch (error) {
console.error('Error starting application:', error);
}
}
bootstrap();
================================================
FILE: packages/bytebot-agent/src/messages/messages.module.ts
================================================
import { Module, forwardRef } from '@nestjs/common';
import { MessagesService } from './messages.service';
import { PrismaModule } from '../prisma/prisma.module';
import { TasksModule } from '../tasks/tasks.module';
@Module({
imports: [PrismaModule, forwardRef(() => TasksModule)],
providers: [MessagesService],
exports: [MessagesService],
})
export class MessagesModule {}
================================================
FILE: packages/bytebot-agent/src/messages/messages.service.ts
================================================
import {
Injectable,
NotFoundException,
Inject,
forwardRef,
} from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { Message, Role, Prisma } from '@prisma/client';
import {
MessageContentBlock,
isComputerToolUseContentBlock,
isToolResultContentBlock,
isUserActionContentBlock,
} from '@bytebot/shared';
import { TasksGateway } from '../tasks/tasks.gateway';
// Extended message type for processing
export interface ProcessedMessage extends Message {
take_over?: boolean;
}
export interface GroupedMessages {
role: Role;
messages: ProcessedMessage[];
take_over?: boolean;
}
@Injectable()
export class MessagesService {
constructor(
private prisma: PrismaService,
@Inject(forwardRef(() => TasksGateway))
private readonly tasksGateway: TasksGateway,
) {}
async create(data: {
content: MessageContentBlock[];
role: Role;
taskId: string;
}): Promise {
const message = await this.prisma.message.create({
data: {
content: data.content as Prisma.InputJsonValue,
role: data.role,
taskId: data.taskId,
},
});
this.tasksGateway.emitNewMessage(data.taskId, message);
return message;
}
async findEvery(taskId: string): Promise {
return this.prisma.message.findMany({
where: {
taskId,
},
orderBy: {
createdAt: 'asc',
},
});
}
async findAll(
taskId: string,
options?: {
limit?: number;
page?: number;
},
): Promise {
const { limit = 10, page = 1 } = options || {};
// Calculate offset based on page and limit
const offset = (page - 1) * limit;
return this.prisma.message.findMany({
where: {
taskId,
},
orderBy: {
createdAt: 'asc',
},
take: limit,
skip: offset,
});
}
async findUnsummarized(taskId: string): Promise {
return this.prisma.message.findMany({
where: {
taskId,
// find messages that don't have a summaryId
summaryId: null,
},
orderBy: { createdAt: 'asc' },
});
}
async attachSummary(
taskId: string,
summaryId: string,
messageIds: string[],
): Promise {
if (messageIds.length === 0) {
return;
}
await this.prisma.message.updateMany({
where: { taskId, id: { in: messageIds } },
data: { summaryId },
});
}
/**
* Groups back-to-back messages from the same role and take_over status
*/
private groupBackToBackMessages(
messages: ProcessedMessage[],
): GroupedMessages[] {
const groupedConversation: GroupedMessages[] = [];
let currentGroup: GroupedMessages | null = null;
for (const message of messages) {
const role = message.role;
const isTakeOver = message.take_over || false;
// If this is the first message, role is different, or take_over status is different from the previous group
if (
!currentGroup ||
currentGroup.role !== role ||
currentGroup.take_over !== isTakeOver
) {
// Save the previous group if it exists
if (currentGroup) {
groupedConversation.push(currentGroup);
}
// Start a new group
currentGroup = {
role: role,
messages: [message],
take_over: isTakeOver,
};
} else {
// Same role and take_over status as previous, merge the content
currentGroup.messages.push(message);
}
}
// Add the last group
if (currentGroup) {
groupedConversation.push(currentGroup);
}
return groupedConversation;
}
/**
* Filters and processes messages, adding take_over flags where appropriate
* Only text messages from the user should appear as user messages
* Computer tool use messages should be shown as assistant messages with take_over flag
*/
private filterMessages(messages: Message[]): ProcessedMessage[] {
const filteredMessages: ProcessedMessage[] = [];
for (const message of messages) {
const processedMessage: ProcessedMessage = { ...message };
const contentBlocks = message.content as MessageContentBlock[];
// If the role is a user message and all the content blocks are tool result blocks or they are take over actions
if (message.role === Role.USER) {
if (contentBlocks.every((block) => isToolResultContentBlock(block))) {
// Pure tool results should be shown as assistant messages
processedMessage.role = Role.ASSISTANT;
} else if (
contentBlocks.every((block) => isUserActionContentBlock(block))
) {
// Extract computer tool use (take over actions) from the user action content blocks and show them as assistant messages with take_over flag
processedMessage.content = contentBlocks
.flatMap((block) => {
return block.content;
})
.filter((block) => isComputerToolUseContentBlock(block));
processedMessage.role = Role.ASSISTANT;
processedMessage.take_over = true;
}
// If there are text blocks mixed with tool blocks, keep as user message
// Only pure text messages from user should remain as user messages
}
filteredMessages.push(processedMessage);
}
return filteredMessages;
}
/**
* Returns raw messages without any processing
*/
async findRawMessages(
taskId: string,
options?: {
limit?: number;
page?: number;
},
): Promise {
return this.findAll(taskId, options);
}
/**
* Returns processed and grouped messages for the chat UI
*/
async findProcessedMessages(
taskId: string,
options?: {
limit?: number;
page?: number;
},
): Promise {
const messages = await this.findAll(taskId, options);
const filteredMessages = this.filterMessages(messages);
return this.groupBackToBackMessages(filteredMessages);
}
}
================================================
FILE: packages/bytebot-agent/src/openai/openai.constants.ts
================================================
import { BytebotAgentModel } from 'src/agent/agent.types';
export const OPENAI_MODELS: BytebotAgentModel[] = [
{
provider: 'openai',
name: 'o3-2025-04-16',
title: 'o3',
contextWindow: 200000,
},
{
provider: 'openai',
name: 'gpt-4.1-2025-04-14',
title: 'GPT-4.1',
contextWindow: 1047576,
},
];
export const DEFAULT_MODEL = OPENAI_MODELS[0];
================================================
FILE: packages/bytebot-agent/src/openai/openai.module.ts
================================================
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { OpenAIService } from './openai.service';
@Module({
imports: [ConfigModule],
providers: [OpenAIService],
exports: [OpenAIService],
})
export class OpenAIModule {}
================================================
FILE: packages/bytebot-agent/src/openai/openai.service.ts
================================================
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import OpenAI, { APIUserAbortError } from 'openai';
import {
MessageContentBlock,
MessageContentType,
TextContentBlock,
ToolUseContentBlock,
ToolResultContentBlock,
ThinkingContentBlock,
isUserActionContentBlock,
isComputerToolUseContentBlock,
isImageContentBlock,
} from '@bytebot/shared';
import { DEFAULT_MODEL } from './openai.constants';
import { Message, Role } from '@prisma/client';
import { openaiTools } from './openai.tools';
import {
BytebotAgentService,
BytebotAgentInterrupt,
BytebotAgentResponse,
} from '../agent/agent.types';
@Injectable()
export class OpenAIService implements BytebotAgentService {
private readonly openai: OpenAI;
private readonly logger = new Logger(OpenAIService.name);
constructor(private readonly configService: ConfigService) {
const apiKey = this.configService.get('OPENAI_API_KEY');
if (!apiKey) {
this.logger.warn(
'OPENAI_API_KEY is not set. OpenAIService will not work properly.',
);
}
this.openai = new OpenAI({
apiKey: apiKey || 'dummy-key-for-initialization',
});
}
async generateMessage(
systemPrompt: string,
messages: Message[],
model: string = DEFAULT_MODEL.name,
useTools: boolean = true,
signal?: AbortSignal,
): Promise {
const isReasoning = model.startsWith('o');
try {
const openaiMessages = this.formatMessagesForOpenAI(messages);
const maxTokens = 8192;
const response = await this.openai.responses.create(
{
model,
max_output_tokens: maxTokens,
input: openaiMessages,
instructions: systemPrompt,
tools: useTools ? openaiTools : [],
reasoning: isReasoning ? { effort: 'medium' } : null,
store: false,
include: isReasoning ? ['reasoning.encrypted_content'] : [],
},
{ signal },
);
return {
contentBlocks: this.formatOpenAIResponse(response.output),
tokenUsage: {
inputTokens: response.usage?.input_tokens || 0,
outputTokens: response.usage?.output_tokens || 0,
totalTokens: response.usage?.total_tokens || 0,
},
};
} catch (error: any) {
console.log('error', error);
console.log('error name', error.name);
if (error instanceof APIUserAbortError) {
this.logger.log('OpenAI API call aborted');
throw new BytebotAgentInterrupt();
}
this.logger.error(
`Error sending message to OpenAI: ${error.message}`,
error.stack,
);
throw error;
}
}
private formatMessagesForOpenAI(
messages: Message[],
): OpenAI.Responses.ResponseInputItem[] {
const openaiMessages: OpenAI.Responses.ResponseInputItem[] = [];
for (const message of messages) {
const messageContentBlocks = message.content as MessageContentBlock[];
if (
messageContentBlocks.every((block) => isUserActionContentBlock(block))
) {
const userActionContentBlocks = messageContentBlocks.flatMap(
(block) => block.content,
);
for (const block of userActionContentBlocks) {
if (isComputerToolUseContentBlock(block)) {
openaiMessages.push({
type: 'message',
role: 'user',
content: [
{
type: 'input_text',
text: `User performed action: ${block.name}\n${JSON.stringify(block.input, null, 2)}`,
},
],
});
} else if (isImageContentBlock(block)) {
openaiMessages.push({
role: 'user',
type: 'message',
content: [
{
type: 'input_image',
detail: 'high',
image_url: `data:${block.source.media_type};base64,${block.source.data}`,
},
],
} as OpenAI.Responses.ResponseInputItem.Message);
}
}
} else {
// Convert content blocks to OpenAI format
for (const block of messageContentBlocks) {
switch (block.type) {
case MessageContentType.Text: {
if (message.role === Role.USER) {
openaiMessages.push({
type: 'message',
role: 'user',
content: [
{
type: 'input_text',
text: block.text,
},
],
} as OpenAI.Responses.ResponseInputItem.Message);
} else {
openaiMessages.push({
type: 'message',
role: 'assistant',
content: [
{
type: 'output_text',
text: block.text,
},
],
} as OpenAI.Responses.ResponseOutputMessage);
}
break;
}
case MessageContentType.ToolUse:
// For assistant messages with tool use, convert to function call
if (message.role === Role.ASSISTANT) {
const toolBlock = block as ToolUseContentBlock;
openaiMessages.push({
type: 'function_call',
call_id: toolBlock.id,
name: toolBlock.name,
arguments: JSON.stringify(toolBlock.input),
} as OpenAI.Responses.ResponseFunctionToolCall);
}
break;
case MessageContentType.Thinking: {
const thinkingBlock = block;
openaiMessages.push({
type: 'reasoning',
id: thinkingBlock.signature,
encrypted_content: thinkingBlock.thinking,
summary: [],
} as OpenAI.Responses.ResponseReasoningItem);
break;
}
case MessageContentType.ToolResult: {
// Handle tool results as function call outputs
const toolResult = block;
// Tool results should be added as separate items in the response
toolResult.content.forEach((content) => {
if (content.type === MessageContentType.Text) {
openaiMessages.push({
type: 'function_call_output',
call_id: toolResult.tool_use_id,
output: content.text,
} as OpenAI.Responses.ResponseInputItem.FunctionCallOutput);
}
if (content.type === MessageContentType.Image) {
openaiMessages.push({
type: 'function_call_output',
call_id: toolResult.tool_use_id,
output: 'screenshot',
} as OpenAI.Responses.ResponseInputItem.FunctionCallOutput);
openaiMessages.push({
role: 'user',
type: 'message',
content: [
{
type: 'input_image',
detail: 'high',
image_url: `data:${content.source.media_type};base64,${content.source.data}`,
},
],
} as OpenAI.Responses.ResponseInputItem.Message);
}
});
break;
}
default:
// Handle unknown content types as text
openaiMessages.push({
role: 'user',
type: 'message',
content: [
{
type: 'input_text',
text: JSON.stringify(block),
},
],
} as OpenAI.Responses.ResponseInputItem.Message);
}
}
}
}
return openaiMessages;
}
private formatOpenAIResponse(
response: OpenAI.Responses.ResponseOutputItem[],
): MessageContentBlock[] {
const contentBlocks: MessageContentBlock[] = [];
for (const item of response) {
// Check the type of the output item
switch (item.type) {
case 'message':
// Handle ResponseOutputMessage
const message = item;
for (const content of message.content) {
if ('text' in content) {
// ResponseOutputText
contentBlocks.push({
type: MessageContentType.Text,
text: content.text,
} as TextContentBlock);
} else if ('refusal' in content) {
// ResponseOutputRefusal
contentBlocks.push({
type: MessageContentType.Text,
text: `Refusal: ${content.refusal}`,
} as TextContentBlock);
}
}
break;
case 'function_call':
// Handle ResponseFunctionToolCall
const toolCall = item;
contentBlocks.push({
type: MessageContentType.ToolUse,
id: toolCall.call_id,
name: toolCall.name,
input: JSON.parse(toolCall.arguments),
} as ToolUseContentBlock);
break;
case 'file_search_call':
case 'web_search_call':
case 'computer_call':
case 'reasoning':
const reasoning = item as OpenAI.Responses.ResponseReasoningItem;
if (reasoning.encrypted_content) {
contentBlocks.push({
type: MessageContentType.Thinking,
thinking: reasoning.encrypted_content,
signature: reasoning.id,
} as ThinkingContentBlock);
}
break;
case 'image_generation_call':
case 'code_interpreter_call':
case 'local_shell_call':
case 'mcp_call':
case 'mcp_list_tools':
case 'mcp_approval_request':
// Handle other tool types as text for now
this.logger.warn(
`Unsupported response output item type: ${item.type}`,
);
contentBlocks.push({
type: MessageContentType.Text,
text: JSON.stringify(item),
} as TextContentBlock);
break;
default:
// Handle unknown types
this.logger.warn(
`Unknown response output item type: ${JSON.stringify(item)}`,
);
contentBlocks.push({
type: MessageContentType.Text,
text: JSON.stringify(item),
} as TextContentBlock);
}
}
return contentBlocks;
}
}
================================================
FILE: packages/bytebot-agent/src/openai/openai.tools.ts
================================================
import OpenAI from 'openai';
import { agentTools } from '../agent/agent.tools';
function agentToolToOpenAITool(agentTool: any): OpenAI.Responses.FunctionTool {
return {
type: 'function',
name: agentTool.name,
description: agentTool.description,
parameters: agentTool.input_schema,
} as OpenAI.Responses.FunctionTool;
}
/**
* Creates a mapped object of tools by name
*/
const toolMap = agentTools.reduce(
(acc, tool) => {
const anthropicTool = agentToolToOpenAITool(tool);
const camelCaseName = tool.name
.split('_')
.map((part, index) => {
if (index === 0) return part;
if (part === 'computer') return '';
return part.charAt(0).toUpperCase() + part.slice(1);
})
.join('')
.replace(/^computer/, '');
acc[camelCaseName + 'Tool'] = anthropicTool;
return acc;
},
{} as Record,
);
// Export individual tools with proper names
export const moveMouseTool = toolMap.moveMouseTool;
export const traceMouseTool = toolMap.traceMouseTool;
export const clickMouseTool = toolMap.clickMouseTool;
export const pressMouseTool = toolMap.pressMouseTool;
export const dragMouseTool = toolMap.dragMouseTool;
export const scrollTool = toolMap.scrollTool;
export const typeKeysTool = toolMap.typeKeysTool;
export const pressKeysTool = toolMap.pressKeysTool;
export const typeTextTool = toolMap.typeTextTool;
export const pasteTextTool = toolMap.pasteTextTool;
export const waitTool = toolMap.waitTool;
export const screenshotTool = toolMap.screenshotTool;
export const cursorPositionTool = toolMap.cursorPositionTool;
export const setTaskStatusTool = toolMap.setTaskStatusTool;
export const createTaskTool = toolMap.createTaskTool;
export const applicationTool = toolMap.applicationTool;
// Array of all tools
export const openaiTools: OpenAI.Responses.FunctionTool[] = agentTools.map(
agentToolToOpenAITool,
);
================================================
FILE: packages/bytebot-agent/src/prisma/prisma.module.ts
================================================
import { Global, Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';
@Global()
@Module({
providers: [PrismaService],
exports: [PrismaService],
})
export class PrismaModule {}
================================================
FILE: packages/bytebot-agent/src/prisma/prisma.service.ts
================================================
import { Injectable, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
constructor() {
super();
}
async onModuleInit() {
await this.$connect();
}
}
================================================
FILE: packages/bytebot-agent/src/proxy/proxy.module.ts
================================================
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { ProxyService } from './proxy.service';
@Module({
imports: [ConfigModule],
providers: [ProxyService],
exports: [ProxyService],
})
export class ProxyModule {}
================================================
FILE: packages/bytebot-agent/src/proxy/proxy.service.ts
================================================
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import OpenAI, { APIUserAbortError } from 'openai';
import {
ChatCompletionMessageParam,
ChatCompletionContentPart,
} from 'openai/resources/chat/completions';
import {
MessageContentBlock,
MessageContentType,
TextContentBlock,
ToolUseContentBlock,
ToolResultContentBlock,
ImageContentBlock,
isUserActionContentBlock,
isComputerToolUseContentBlock,
isImageContentBlock,
ThinkingContentBlock,
} from '@bytebot/shared';
import { Message, Role } from '@prisma/client';
import { proxyTools } from './proxy.tools';
import {
BytebotAgentService,
BytebotAgentInterrupt,
BytebotAgentResponse,
} from '../agent/agent.types';
@Injectable()
export class ProxyService implements BytebotAgentService {
private readonly openai: OpenAI;
private readonly logger = new Logger(ProxyService.name);
constructor(private readonly configService: ConfigService) {
const proxyUrl = this.configService.get('BYTEBOT_LLM_PROXY_URL');
if (!proxyUrl) {
this.logger.warn(
'BYTEBOT_LLM_PROXY_URL is not set. ProxyService will not work properly.',
);
}
// Initialize OpenAI client with proxy configuration
this.openai = new OpenAI({
apiKey: 'dummy-key-for-proxy',
baseURL: proxyUrl,
});
}
/**
* Main method to generate messages using the Chat Completions API
*/
async generateMessage(
systemPrompt: string,
messages: Message[],
model: string,
useTools: boolean = true,
signal?: AbortSignal,
): Promise {
// Convert messages to Chat Completion format
const chatMessages = this.formatMessagesForChatCompletion(
systemPrompt,
messages,
);
try {
// Prepare the Chat Completion request
const completionRequest: OpenAI.Chat.ChatCompletionCreateParams = {
model,
messages: chatMessages,
max_tokens: 8192,
...(useTools && { tools: proxyTools }),
reasoning_effort: 'high',
};
// Make the API call
const completion = await this.openai.chat.completions.create(
completionRequest,
{ signal },
);
// Process the response
const choice = completion.choices[0];
if (!choice || !choice.message) {
throw new Error('No valid response from Chat Completion API');
}
// Convert response to MessageContentBlocks
const contentBlocks = this.formatChatCompletionResponse(choice.message);
return {
contentBlocks,
tokenUsage: {
inputTokens: completion.usage?.prompt_tokens || 0,
outputTokens: completion.usage?.completion_tokens || 0,
totalTokens: completion.usage?.total_tokens || 0,
},
};
} catch (error: any) {
if (error instanceof APIUserAbortError) {
this.logger.log('Chat Completion API call aborted');
throw new BytebotAgentInterrupt();
}
this.logger.error(
`Error sending message to proxy: ${error.message}`,
error.stack,
);
throw error;
}
}
/**
* Convert Bytebot messages to Chat Completion format
*/
private formatMessagesForChatCompletion(
systemPrompt: string,
messages: Message[],
): ChatCompletionMessageParam[] {
const chatMessages: ChatCompletionMessageParam[] = [];
// Add system message
chatMessages.push({
role: 'system',
content: systemPrompt,
});
// Process each message
for (const message of messages) {
const messageContentBlocks = message.content as MessageContentBlock[];
// Handle user actions specially
if (
messageContentBlocks.every((block) => isUserActionContentBlock(block))
) {
const userActionBlocks = messageContentBlocks.flatMap(
(block) => block.content,
);
for (const block of userActionBlocks) {
if (isComputerToolUseContentBlock(block)) {
chatMessages.push({
role: 'user',
content: `User performed action: ${block.name}\n${JSON.stringify(
block.input,
null,
2,
)}`,
});
} else if (isImageContentBlock(block)) {
chatMessages.push({
role: 'user',
content: [
{
type: 'image_url',
image_url: {
url: `data:${block.source.media_type};base64,${block.source.data}`,
detail: 'high',
},
},
],
});
}
}
} else {
for (const block of messageContentBlocks) {
switch (block.type) {
case MessageContentType.Text: {
chatMessages.push({
role: message.role === Role.USER ? 'user' : 'assistant',
content: block.text,
});
break;
}
case MessageContentType.Image: {
const imageBlock = block as ImageContentBlock;
chatMessages.push({
role: 'user',
content: [
{
type: 'image_url',
image_url: {
url: `data:${imageBlock.source.media_type};base64,${imageBlock.source.data}`,
detail: 'high',
},
},
],
});
break;
}
case MessageContentType.ToolUse: {
const toolBlock = block as ToolUseContentBlock;
chatMessages.push({
role: 'assistant',
tool_calls: [
{
id: toolBlock.id,
type: 'function',
function: {
name: toolBlock.name,
arguments: JSON.stringify(toolBlock.input),
},
},
],
});
break;
}
case MessageContentType.Thinking: {
const thinkingBlock = block as ThinkingContentBlock;
const message: ChatCompletionMessageParam = {
role: 'assistant',
content: null,
};
message['reasoning_content'] = thinkingBlock.thinking;
chatMessages.push(message);
break;
}
case MessageContentType.ToolResult: {
const toolResultBlock = block as ToolResultContentBlock;
if (
toolResultBlock.content.every(
(content) => content.type === MessageContentType.Image,
)
) {
chatMessages.push({
role: 'tool',
tool_call_id: toolResultBlock.tool_use_id,
content: 'screenshot',
});
}
toolResultBlock.content.forEach((content) => {
if (content.type === MessageContentType.Text) {
chatMessages.push({
role: 'tool',
tool_call_id: toolResultBlock.tool_use_id,
content: content.text,
});
}
if (content.type === MessageContentType.Image) {
chatMessages.push({
role: 'user',
content: [
{
type: 'text',
text: 'Screenshot',
},
{
type: 'image_url',
image_url: {
url: `data:${content.source.media_type};base64,${content.source.data}`,
detail: 'high',
},
},
],
});
}
});
break;
}
}
}
}
}
return chatMessages;
}
/**
* Convert Chat Completion response to MessageContentBlocks
*/
private formatChatCompletionResponse(
message: OpenAI.Chat.ChatCompletionMessage,
): MessageContentBlock[] {
const contentBlocks: MessageContentBlock[] = [];
// Handle text content
if (message.content) {
contentBlocks.push({
type: MessageContentType.Text,
text: message.content,
} as TextContentBlock);
}
if (message['reasoning_content']) {
contentBlocks.push({
type: MessageContentType.Thinking,
thinking: message['reasoning_content'],
signature: message['reasoning_content'],
} as ThinkingContentBlock);
}
// Handle tool calls
if (message.tool_calls && message.tool_calls.length > 0) {
for (const toolCall of message.tool_calls) {
if (toolCall.type === 'function') {
let parsedInput = {};
try {
parsedInput = JSON.parse(toolCall.function.arguments || '{}');
} catch (e) {
this.logger.warn(
`Failed to parse tool call arguments: ${toolCall.function.arguments}`,
);
parsedInput = {};
}
contentBlocks.push({
type: MessageContentType.ToolUse,
id: toolCall.id,
name: toolCall.function.name,
input: parsedInput,
} as ToolUseContentBlock);
}
}
}
// Handle refusal
if (message.refusal) {
contentBlocks.push({
type: MessageContentType.Text,
text: `Refusal: ${message.refusal}`,
} as TextContentBlock);
}
return contentBlocks;
}
}
================================================
FILE: packages/bytebot-agent/src/proxy/proxy.tools.ts
================================================
import { ChatCompletionTool } from 'openai/resources';
import { agentTools } from '../agent/agent.tools';
/**
* Converts an agent tool definition to OpenAI Chat Completion tool format
*/
function agentToolToChatCompletionTool(agentTool: any): ChatCompletionTool {
return {
type: 'function',
function: {
name: agentTool.name,
description: agentTool.description,
parameters: agentTool.input_schema,
},
};
}
/**
* Convert tool name from snake_case to camelCase
*/
function convertToCamelCase(name: string): string {
return name
.split('_')
.map((part, index) => {
if (index === 0) return part;
if (part === 'computer') return '';
return part.charAt(0).toUpperCase() + part.slice(1);
})
.join('')
.replace(/^computer/, '');
}
/**
* All tools converted to Chat Completion format
*/
export const proxyTools: ChatCompletionTool[] = agentTools.map((tool) =>
agentToolToChatCompletionTool(tool),
);
/**
* Individual tool exports for selective usage
*/
const toolMap = agentTools.reduce(
(acc, tool) => {
const chatCompletionTool = agentToolToChatCompletionTool(tool);
const camelCaseName = convertToCamelCase(tool.name);
acc[camelCaseName + 'Tool'] = chatCompletionTool;
return acc;
},
{} as Record,
);
// Export individual tools with proper names
export const moveMouseTool = toolMap.moveMouseTool;
export const traceMouseTool = toolMap.traceMouseTool;
export const clickMouseTool = toolMap.clickMouseTool;
export const pressMouseTool = toolMap.pressMouseTool;
export const dragMouseTool = toolMap.dragMouseTool;
export const scrollTool = toolMap.scrollTool;
export const typeKeysTool = toolMap.typeKeysTool;
export const pressKeysTool = toolMap.pressKeysTool;
export const typeTextTool = toolMap.typeTextTool;
export const pasteTextTool = toolMap.pasteTextTool;
export const waitTool = toolMap.waitTool;
export const screenshotTool = toolMap.screenshotTool;
export const cursorPositionTool = toolMap.cursorPositionTool;
export const setTaskStatusTool = toolMap.setTaskStatusTool;
export const createTaskTool = toolMap.createTaskTool;
export const applicationTool = toolMap.applicationTool;
================================================
FILE: packages/bytebot-agent/src/summaries/summaries.modue.ts
================================================
import { Module } from '@nestjs/common';
import { PrismaModule } from '../prisma/prisma.module';
import { SummariesService } from './summaries.service';
@Module({
imports: [PrismaModule],
providers: [SummariesService],
exports: [SummariesService],
})
export class SummariesModule {}
================================================
FILE: packages/bytebot-agent/src/summaries/summaries.service.ts
================================================
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { Summary } from '@prisma/client';
@Injectable()
export class SummariesService {
constructor(private prisma: PrismaService) {}
async create(data: {
taskId: string;
content: string;
parentId?: string;
}): Promise {
return this.prisma.summary.create({
data: {
taskId: data.taskId,
content: data.content,
...(data.parentId ? { parentId: data.parentId } : {}),
},
});
}
async findLatest(taskId: string): Promise {
return this.prisma.summary.findFirst({
where: { taskId },
orderBy: { createdAt: 'desc' },
});
}
async findAll(taskId: string): Promise {
return this.prisma.summary.findMany({
where: { taskId },
orderBy: { createdAt: 'asc' },
});
}
}
================================================
FILE: packages/bytebot-agent/src/tasks/dto/add-task-message.dto.ts
================================================
import { IsNotEmpty, IsString } from 'class-validator';
export class AddTaskMessageDto {
@IsNotEmpty()
@IsString()
message: string;
}
================================================
FILE: packages/bytebot-agent/src/tasks/dto/create-task.dto.ts
================================================
import {
IsArray,
IsDate,
IsNotEmpty,
IsNumber,
IsOptional,
IsString,
ValidateNested,
} from 'class-validator';
import { Type } from 'class-transformer';
import { Role, TaskPriority, TaskType } from '@prisma/client';
export class TaskFileDto {
@IsNotEmpty()
@IsString()
name: string;
@IsNotEmpty()
@IsString()
base64: string;
@IsNotEmpty()
@IsString()
type: string;
@IsNotEmpty()
@IsNumber()
size: number;
}
export class CreateTaskDto {
@IsNotEmpty()
@IsString()
description: string;
@IsOptional()
@IsString()
type?: TaskType;
@IsOptional()
@IsDate()
scheduledFor?: Date;
@IsOptional()
@IsString()
priority?: TaskPriority;
@IsOptional()
@IsString()
createdBy?: Role;
@IsOptional()
model?: any;
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => TaskFileDto)
files?: TaskFileDto[];
}
================================================
FILE: packages/bytebot-agent/src/tasks/dto/update-task.dto.ts
================================================
import { IsEnum, IsOptional } from 'class-validator';
import { TaskPriority, TaskStatus } from '@prisma/client';
export class UpdateTaskDto {
@IsOptional()
@IsEnum(TaskStatus)
status?: TaskStatus;
@IsOptional()
@IsEnum(TaskPriority)
priority?: TaskPriority;
@IsOptional()
queuedAt?: Date;
@IsOptional()
executedAt?: Date;
@IsOptional()
completedAt?: Date;
}
================================================
FILE: packages/bytebot-agent/src/tasks/tasks.controller.ts
================================================
import {
Controller,
Get,
Post,
Body,
Param,
Delete,
HttpStatus,
HttpCode,
Query,
HttpException,
} from '@nestjs/common';
import { TasksService } from './tasks.service';
import { CreateTaskDto } from './dto/create-task.dto';
import { Message, Task } from '@prisma/client';
import { AddTaskMessageDto } from './dto/add-task-message.dto';
import { MessagesService } from '../messages/messages.service';
import { ANTHROPIC_MODELS } from '../anthropic/anthropic.constants';
import { OPENAI_MODELS } from '../openai/openai.constants';
import { GOOGLE_MODELS } from '../google/google.constants';
import { BytebotAgentModel } from 'src/agent/agent.types';
const geminiApiKey = process.env.GEMINI_API_KEY;
const anthropicApiKey = process.env.ANTHROPIC_API_KEY;
const openaiApiKey = process.env.OPENAI_API_KEY;
const proxyUrl = process.env.BYTEBOT_LLM_PROXY_URL;
const models = [
...(anthropicApiKey ? ANTHROPIC_MODELS : []),
...(openaiApiKey ? OPENAI_MODELS : []),
...(geminiApiKey ? GOOGLE_MODELS : []),
];
@Controller('tasks')
export class TasksController {
constructor(
private readonly tasksService: TasksService,
private readonly messagesService: MessagesService,
) {}
@Post()
@HttpCode(HttpStatus.CREATED)
async create(@Body() createTaskDto: CreateTaskDto): Promise {
return this.tasksService.create(createTaskDto);
}
@Get()
async findAll(
@Query('page') page?: string,
@Query('limit') limit?: string,
@Query('status') status?: string,
@Query('statuses') statuses?: string,
): Promise<{ tasks: Task[]; total: number; totalPages: number }> {
const pageNum = page ? parseInt(page, 10) : 1;
const limitNum = limit ? parseInt(limit, 10) : 10;
// Handle both single status and multiple statuses
let statusFilter: string[] | undefined;
if (statuses) {
statusFilter = statuses.split(',');
} else if (status) {
statusFilter = [status];
}
return this.tasksService.findAll(pageNum, limitNum, statusFilter);
}
@Get('models')
async getModels() {
if (proxyUrl) {
try {
const response = await fetch(`${proxyUrl}/model/info`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new HttpException(
`Failed to fetch models from proxy: ${response.statusText}`,
HttpStatus.BAD_GATEWAY,
);
}
const proxyModels = await response.json();
// Map proxy response to BytebotAgentModel format
const models: BytebotAgentModel[] = proxyModels.data.map(
(model: any) => ({
provider: 'proxy',
name: model.litellm_params.model,
title: model.model_name,
contextWindow: 128000,
}),
);
return models;
} catch (error) {
if (error instanceof HttpException) {
throw error;
}
throw new HttpException(
`Error fetching models: ${error.message}`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
return models;
}
@Get(':id')
async findById(@Param('id') id: string): Promise {
return this.tasksService.findById(id);
}
@Get(':id/messages')
async taskMessages(
@Param('id') taskId: string,
@Query('limit') limit?: string,
@Query('page') page?: string,
): Promise {
const options = {
limit: limit ? parseInt(limit, 10) : undefined,
page: page ? parseInt(page, 10) : undefined,
};
const messages = await this.messagesService.findAll(taskId, options);
return messages;
}
@Post(':id/messages')
@HttpCode(HttpStatus.CREATED)
async addTaskMessage(
@Param('id') taskId: string,
@Body() guideTaskDto: AddTaskMessageDto,
): Promise {
return this.tasksService.addTaskMessage(taskId, guideTaskDto);
}
@Get(':id/messages/raw')
async taskRawMessages(
@Param('id') taskId: string,
@Query('limit') limit?: string,
@Query('page') page?: string,
): Promise {
const options = {
limit: limit ? parseInt(limit, 10) : undefined,
page: page ? parseInt(page, 10) : undefined,
};
return this.messagesService.findRawMessages(taskId, options);
}
@Get(':id/messages/processed')
async taskProcessedMessages(
@Param('id') taskId: string,
@Query('limit') limit?: string,
@Query('page') page?: string,
) {
const options = {
limit: limit ? parseInt(limit, 10) : undefined,
page: page ? parseInt(page, 10) : undefined,
};
return this.messagesService.findProcessedMessages(taskId, options);
}
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
async delete(@Param('id') id: string): Promise {
await this.tasksService.delete(id);
}
@Post(':id/takeover')
@HttpCode(HttpStatus.OK)
async takeOver(@Param('id') taskId: string): Promise {
return this.tasksService.takeOver(taskId);
}
@Post(':id/resume')
@HttpCode(HttpStatus.OK)
async resume(@Param('id') taskId: string): Promise {
return this.tasksService.resume(taskId);
}
@Post(':id/cancel')
@HttpCode(HttpStatus.OK)
async cancel(@Param('id') taskId: string): Promise {
return this.tasksService.cancel(taskId);
}
}
================================================
FILE: packages/bytebot-agent/src/tasks/tasks.gateway.ts
================================================
import {
WebSocketGateway,
WebSocketServer,
SubscribeMessage,
OnGatewayConnection,
OnGatewayDisconnect,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
import { Injectable } from '@nestjs/common';
@Injectable()
@WebSocketGateway({
cors: {
origin: '*',
methods: ['GET', 'POST'],
},
})
export class TasksGateway implements OnGatewayConnection, OnGatewayDisconnect {
@WebSocketServer()
server: Server;
handleConnection(client: Socket) {
console.log(`Client connected: ${client.id}`);
}
handleDisconnect(client: Socket) {
console.log(`Client disconnected: ${client.id}`);
}
@SubscribeMessage('join_task')
handleJoinTask(client: Socket, taskId: string) {
client.join(`task_${taskId}`);
console.log(`Client ${client.id} joined task ${taskId}`);
}
@SubscribeMessage('leave_task')
handleLeaveTask(client: Socket, taskId: string) {
client.leave(`task_${taskId}`);
console.log(`Client ${client.id} left task ${taskId}`);
}
emitTaskUpdate(taskId: string, task: any) {
this.server.to(`task_${taskId}`).emit('task_updated', task);
}
emitNewMessage(taskId: string, message: any) {
this.server.to(`task_${taskId}`).emit('new_message', message);
}
emitTaskCreated(task: any) {
this.server.emit('task_created', task);
}
emitTaskDeleted(taskId: string) {
this.server.emit('task_deleted', taskId);
}
}
================================================
FILE: packages/bytebot-agent/src/tasks/tasks.module.ts
================================================
import { Module } from '@nestjs/common';
import { TasksController } from './tasks.controller';
import { TasksService } from './tasks.service';
import { TasksGateway } from './tasks.gateway';
import { PrismaModule } from '../prisma/prisma.module';
import { MessagesModule } from '../messages/messages.module';
@Module({
imports: [PrismaModule, MessagesModule],
controllers: [TasksController],
providers: [TasksService, TasksGateway],
exports: [TasksService, TasksGateway],
})
export class TasksModule {}
================================================
FILE: packages/bytebot-agent/src/tasks/tasks.service.ts
================================================
import {
Injectable,
NotFoundException,
Logger,
BadRequestException,
Inject,
forwardRef,
} from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { CreateTaskDto } from './dto/create-task.dto';
import { UpdateTaskDto } from './dto/update-task.dto';
import {
Task,
Role,
Prisma,
TaskStatus,
TaskType,
TaskPriority,
File,
} from '@prisma/client';
import { AddTaskMessageDto } from './dto/add-task-message.dto';
import { TasksGateway } from './tasks.gateway';
import { ConfigService } from '@nestjs/config';
import { EventEmitter2 } from '@nestjs/event-emitter';
@Injectable()
export class TasksService {
private readonly logger = new Logger(TasksService.name);
constructor(
readonly prisma: PrismaService,
@Inject(forwardRef(() => TasksGateway))
private readonly tasksGateway: TasksGateway,
private readonly configService: ConfigService,
private readonly eventEmitter: EventEmitter2,
) {
this.logger.log('TasksService initialized');
}
async create(createTaskDto: CreateTaskDto): Promise {
this.logger.log(
`Creating new task with description: ${createTaskDto.description}`,
);
const task = await this.prisma.$transaction(async (prisma) => {
// Create the task first
this.logger.debug('Creating task record in database');
const task = await prisma.task.create({
data: {
description: createTaskDto.description,
type: createTaskDto.type || TaskType.IMMEDIATE,
priority: createTaskDto.priority || TaskPriority.MEDIUM,
status: TaskStatus.PENDING,
createdBy: createTaskDto.createdBy || Role.USER,
model: createTaskDto.model,
...(createTaskDto.scheduledFor
? { scheduledFor: createTaskDto.scheduledFor }
: {}),
},
});
this.logger.log(`Task created successfully with ID: ${task.id}`);
let filesDescription = '';
// Save files if provided
if (createTaskDto.files && createTaskDto.files.length > 0) {
this.logger.debug(
`Saving ${createTaskDto.files.length} file(s) for task ID: ${task.id}`,
);
filesDescription += `\n`;
const filePromises = createTaskDto.files.map((file) => {
// Extract base64 data without the data URL prefix
const base64Data = file.base64.includes('base64,')
? file.base64.split('base64,')[1]
: file.base64;
filesDescription += `\nFile ${file.name} written to desktop.`;
return prisma.file.create({
data: {
name: file.name,
type: file.type || 'application/octet-stream',
size: file.size,
data: base64Data,
taskId: task.id,
},
});
});
await Promise.all(filePromises);
this.logger.debug(`Files saved successfully for task ID: ${task.id}`);
}
// Create the initial system message
this.logger.debug(`Creating initial message for task ID: ${task.id}`);
await prisma.message.create({
data: {
content: [
{
type: 'text',
text: `${createTaskDto.description} ${filesDescription}`,
},
] as Prisma.InputJsonValue,
role: Role.USER,
taskId: task.id,
},
});
this.logger.debug(`Initial message created for task ID: ${task.id}`);
return task;
});
this.tasksGateway.emitTaskCreated(task);
return task;
}
async findScheduledTasks(): Promise {
return this.prisma.task.findMany({
where: {
scheduledFor: {
not: null,
},
queuedAt: null,
},
orderBy: [{ scheduledFor: 'asc' }],
});
}
async findNextTask(): Promise<(Task & { files: File[] }) | null> {
const task = await this.prisma.task.findFirst({
where: {
status: {
in: [TaskStatus.RUNNING, TaskStatus.PENDING],
},
},
orderBy: [
{ executedAt: 'asc' },
{ priority: 'desc' },
{ queuedAt: 'asc' },
{ createdAt: 'asc' },
],
include: {
files: true,
},
});
if (task) {
this.logger.log(
`Found existing task with ID: ${task.id}, and status ${task.status}. Resuming.`,
);
}
return task;
}
async findAll(
page = 1,
limit = 10,
statuses?: string[],
): Promise<{ tasks: Task[]; total: number; totalPages: number }> {
this.logger.log(
`Retrieving tasks - page: ${page}, limit: ${limit}, statuses: ${statuses?.join(',')}`,
);
const skip = (page - 1) * limit;
const whereClause: Prisma.TaskWhereInput =
statuses && statuses.length > 0
? { status: { in: statuses as TaskStatus[] } }
: {};
const [tasks, total] = await Promise.all([
this.prisma.task.findMany({
where: whereClause,
orderBy: {
createdAt: 'desc',
},
skip,
take: limit,
}),
this.prisma.task.count({ where: whereClause }),
]);
const totalPages = Math.ceil(total / limit);
this.logger.debug(`Retrieved ${tasks.length} tasks out of ${total} total`);
return { tasks, total, totalPages };
}
async findById(id: string): Promise {
this.logger.log(`Retrieving task by ID: ${id}`);
try {
const task = await this.prisma.task.findUnique({
where: { id },
include: {
files: true,
},
});
if (!task) {
this.logger.warn(`Task with ID: ${id} not found`);
throw new NotFoundException(`Task with ID ${id} not found`);
}
this.logger.debug(`Retrieved task with ID: ${id}`);
return task;
} catch (error: any) {
this.logger.error(`Error retrieving task ID: ${id} - ${error.message}`);
this.logger.error(error.stack);
throw error;
}
}
async update(id: string, updateTaskDto: UpdateTaskDto): Promise {
this.logger.log(`Updating task with ID: ${id}`);
this.logger.debug(`Update data: ${JSON.stringify(updateTaskDto)}`);
const existingTask = await this.findById(id);
if (!existingTask) {
this.logger.warn(`Task with ID: ${id} not found for update`);
throw new NotFoundException(`Task with ID ${id} not found`);
}
let updatedTask = await this.prisma.task.update({
where: { id },
data: updateTaskDto,
});
if (updateTaskDto.status === TaskStatus.COMPLETED) {
this.eventEmitter.emit('task.completed', { taskId: id });
} else if (updateTaskDto.status === TaskStatus.NEEDS_HELP) {
updatedTask = await this.takeOver(id);
} else if (updateTaskDto.status === TaskStatus.FAILED) {
this.eventEmitter.emit('task.failed', { taskId: id });
}
this.logger.log(`Successfully updated task ID: ${id}`);
this.logger.debug(`Updated task: ${JSON.stringify(updatedTask)}`);
this.tasksGateway.emitTaskUpdate(id, updatedTask);
return updatedTask;
}
async delete(id: string): Promise {
this.logger.log(`Deleting task with ID: ${id}`);
const deletedTask = await this.prisma.task.delete({
where: { id },
});
this.logger.log(`Successfully deleted task ID: ${id}`);
this.tasksGateway.emitTaskDeleted(id);
return deletedTask;
}
async addTaskMessage(taskId: string, addTaskMessageDto: AddTaskMessageDto) {
const task = await this.findById(taskId);
if (!task) {
this.logger.warn(`Task with ID: ${taskId} not found for guiding`);
throw new NotFoundException(`Task with ID ${taskId} not found`);
}
const message = await this.prisma.message.create({
data: {
content: [{ type: 'text', text: addTaskMessageDto.message }],
role: Role.USER,
taskId,
},
});
this.tasksGateway.emitNewMessage(taskId, message);
return task;
}
async resume(taskId: string): Promise {
this.logger.log(`Resuming task ID: ${taskId}`);
const task = await this.findById(taskId);
if (!task) {
throw new NotFoundException(`Task with ID ${taskId} not found`);
}
if (task.control !== Role.USER) {
throw new BadRequestException(`Task ${taskId} is not under user control`);
}
const updatedTask = await this.prisma.task.update({
where: { id: taskId },
data: {
control: Role.ASSISTANT,
status: TaskStatus.RUNNING,
},
});
try {
await fetch(
`${this.configService.get('BYTEBOT_DESKTOP_BASE_URL')}/input-tracking/stop`,
{ method: 'POST' },
);
} catch (error) {
this.logger.error('Failed to stop input tracking', error);
}
// Broadcast resume event so AgentProcessor can react
this.eventEmitter.emit('task.resume', { taskId });
this.logger.log(`Task ${taskId} resumed`);
this.tasksGateway.emitTaskUpdate(taskId, updatedTask);
return updatedTask;
}
async takeOver(taskId: string): Promise {
this.logger.log(`Taking over control for task ID: ${taskId}`);
const task = await this.findById(taskId);
if (!task) {
throw new NotFoundException(`Task with ID ${taskId} not found`);
}
if (task.control !== Role.ASSISTANT) {
throw new BadRequestException(
`Task ${taskId} is not under agent control`,
);
}
const updatedTask = await this.prisma.task.update({
where: { id: taskId },
data: {
control: Role.USER,
},
});
try {
await fetch(
`${this.configService.get('BYTEBOT_DESKTOP_BASE_URL')}/input-tracking/start`,
{ method: 'POST' },
);
} catch (error) {
this.logger.error('Failed to start input tracking', error);
}
// Broadcast takeover event so AgentProcessor can react
this.eventEmitter.emit('task.takeover', { taskId });
this.logger.log(`Task ${taskId} takeover initiated`);
this.tasksGateway.emitTaskUpdate(taskId, updatedTask);
return updatedTask;
}
async cancel(taskId: string): Promise {
this.logger.log(`Cancelling task ID: ${taskId}`);
const task = await this.findById(taskId);
if (!task) {
throw new NotFoundException(`Task with ID ${taskId} not found`);
}
if (
task.status === TaskStatus.COMPLETED ||
task.status === TaskStatus.FAILED ||
task.status === TaskStatus.CANCELLED
) {
throw new BadRequestException(
`Task ${taskId} is already completed, failed, or cancelled`,
);
}
const updatedTask = await this.prisma.task.update({
where: { id: taskId },
data: {
status: TaskStatus.CANCELLED,
},
});
// Broadcast cancel event so AgentProcessor can cancel processing
this.eventEmitter.emit('task.cancel', { taskId });
this.logger.log(`Task ${taskId} cancelled and marked as failed`);
this.tasksGateway.emitTaskUpdate(taskId, updatedTask);
return updatedTask;
}
}
================================================
FILE: packages/bytebot-agent/tsconfig.build.json
================================================
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
}
================================================
FILE: packages/bytebot-agent/tsconfig.json
================================================
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "ES2021",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true,
"strictNullChecks": true,
"forceConsistentCasingInFileNames": true,
"noImplicitAny": false,
"strictBindCallApply": false,
"noFallthroughCasesInSwitch": false
}
}
================================================
FILE: packages/bytebot-agent-cc/.dockerignore
================================================
**/node_modules
**/dist
**/.git
**/.vscode
**/.env*
**/npm-debug.log
**/yarn-debug.log
**/yarn-error.log
**/package-lock.json
================================================
FILE: packages/bytebot-agent-cc/.gitignore
================================================
# compiled output
/dist
/node_modules
/build
# Logs
logs
*.log
npm-debug.log*
pnpm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# OS
.DS_Store
# Tests
/coverage
/.nyc_output
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# temp directory
.temp
.tmp
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
================================================
FILE: packages/bytebot-agent-cc/.prettierrc
================================================
{
"singleQuote": true,
"trailingComma": "all"
}
================================================
FILE: packages/bytebot-agent-cc/Dockerfile
================================================
# Base image
FROM node:20-alpine
# Create app directory
WORKDIR /app
# Copy app source
COPY ./shared ./shared
COPY ./bytebot-agent-cc/ ./bytebot-agent-cc/
WORKDIR /app/bytebot-agent-cc
# Install dependencies
RUN npm install
RUN npm run build
# Run the application
CMD ["npm", "run", "start:prod"]
================================================
FILE: packages/bytebot-agent-cc/eslint.config.mjs
================================================
// @ts-check
import eslint from '@eslint/js';
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
import globals from 'globals';
import tseslint from 'typescript-eslint';
export default tseslint.config(
{
ignores: ['eslint.config.mjs'],
},
eslint.configs.recommended,
...tseslint.configs.recommendedTypeChecked,
eslintPluginPrettierRecommended,
{
languageOptions: {
globals: {
...globals.node,
...globals.jest,
},
ecmaVersion: 5,
sourceType: 'module',
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
},
{
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-floating-promises': 'warn',
'@typescript-eslint/no-unsafe-argument': 'warn'
},
},
);
================================================
FILE: packages/bytebot-agent-cc/nest-cli.json
================================================
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
}
}
================================================
FILE: packages/bytebot-agent-cc/package.json
================================================
{
"name": "bytebot-agent",
"version": "0.0.1",
"description": "",
"author": "",
"private": true,
"license": "UNLICENSED",
"scripts": {
"prisma:dev": "npx prisma migrate dev && npx prisma generate",
"prisma:prod": "npx prisma migrate deploy && npx prisma generate",
"build": "npm run build --prefix ../shared && npx prisma generate && nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "npm run build --prefix ../shared && nest start",
"start:dev": "npm run build --prefix ../shared && nest start --watch",
"start:debug": "npm run build --prefix ../shared && nest start --debug --watch",
"start:prod": "npm run build --prefix ../shared && npx prisma migrate deploy && npx prisma generate && node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@anthropic-ai/claude-code": "^1.0.105",
"@anthropic-ai/sdk": "^0.39.0",
"@bytebot/shared": "../shared",
"@google/genai": "^1.8.0",
"@nestjs/common": "^11.0.1",
"@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.0.1",
"@nestjs/event-emitter": "^3.0.1",
"@nestjs/platform-express": "^11.1.5",
"@nestjs/platform-socket.io": "^11.1.1",
"@nestjs/schedule": "^6.0.0",
"@nestjs/websockets": "^11.1.1",
"@prisma/client": "^6.16.1",
"@thallesp/nestjs-better-auth": "^1.0.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.2",
"openai": "^5.8.2",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
"socket.io": "^4.8.1",
"socket.io-client": "^4.8.1",
"zod": "^4.0.5"
},
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.18.0",
"@nestjs/cli": "^11.0.0",
"@nestjs/schematics": "^11.0.0",
"@nestjs/testing": "^11.0.1",
"@swc/cli": "^0.6.0",
"@swc/core": "^1.10.7",
"@types/express": "^5.0.0",
"@types/jest": "^29.5.14",
"@types/node": "^22.10.7",
"@types/supertest": "^6.0.2",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-prettier": "^5.2.2",
"globals": "^15.14.0",
"jest": "^29.7.0",
"prettier": "^3.4.2",
"prisma": "^6.16.1",
"source-map-support": "^0.5.21",
"supertest": "^7.0.0",
"ts-jest": "^29.2.5",
"ts-loader": "^9.5.2",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.7.3",
"typescript-eslint": "^8.20.0"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
},
"overrides": {
"openai": {
"zod": "^4.0.5"
}
},
"engines": {
"node": "20"
}
}
================================================
FILE: packages/bytebot-agent-cc/prisma/migrations/20250328022708_initial_migration/migration.sql
================================================
-- CreateEnum
CREATE TYPE "TaskStatus" AS ENUM ('PENDING', 'IN_PROGRESS', 'NEEDS_HELP', 'NEEDS_REVIEW', 'COMPLETED', 'CANCELLED', 'FAILED');
-- CreateEnum
CREATE TYPE "TaskPriority" AS ENUM ('LOW', 'MEDIUM', 'HIGH', 'URGENT');
-- CreateEnum
CREATE TYPE "MessageType" AS ENUM ('USER', 'ASSISTANT');
-- CreateTable
CREATE TABLE "Task" (
"id" TEXT NOT NULL,
"description" TEXT NOT NULL,
"status" "TaskStatus" NOT NULL DEFAULT 'PENDING',
"priority" "TaskPriority" NOT NULL DEFAULT 'MEDIUM',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Task_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Summary" (
"id" TEXT NOT NULL,
"content" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"taskId" TEXT NOT NULL,
"parentId" TEXT,
CONSTRAINT "Summary_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Message" (
"id" TEXT NOT NULL,
"content" JSONB NOT NULL,
"type" "MessageType" NOT NULL DEFAULT 'ASSISTANT',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"taskId" TEXT NOT NULL,
"summaryId" TEXT,
CONSTRAINT "Message_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "Summary" ADD CONSTRAINT "Summary_taskId_fkey" FOREIGN KEY ("taskId") REFERENCES "Task"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Summary" ADD CONSTRAINT "Summary_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "Summary"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Message" ADD CONSTRAINT "Message_taskId_fkey" FOREIGN KEY ("taskId") REFERENCES "Task"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Message" ADD CONSTRAINT "Message_summaryId_fkey" FOREIGN KEY ("summaryId") REFERENCES "Summary"("id") ON DELETE SET NULL ON UPDATE CASCADE;
================================================
FILE: packages/bytebot-agent-cc/prisma/migrations/20250413053912_message_role/migration.sql
================================================
/*
Warnings:
- You are about to drop the column `type` on the `Message` table. All the data in the column will be lost.
*/
-- CreateEnum
CREATE TYPE "MessageRole" AS ENUM ('USER', 'ASSISTANT');
-- AlterTable
ALTER TABLE "Message" DROP COLUMN "type",
ADD COLUMN "role" "MessageRole" NOT NULL DEFAULT 'ASSISTANT';
-- DropEnum
DROP TYPE "MessageType";
================================================
FILE: packages/bytebot-agent-cc/prisma/migrations/20250522200556_updated_task_structure/migration.sql
================================================
-- CreateEnum
CREATE TYPE "Role" AS ENUM ('USER', 'ASSISTANT');
-- CreateEnum
CREATE TYPE "TaskType" AS ENUM ('IMMEDIATE', 'SCHEDULED');
-- AlterEnum
BEGIN;
CREATE TYPE "TaskStatus_new" AS ENUM ('PENDING', 'RUNNING', 'NEEDS_HELP', 'NEEDS_REVIEW', 'COMPLETED', 'CANCELLED', 'FAILED');
ALTER TABLE "Task" ALTER COLUMN "status" DROP DEFAULT;
ALTER TABLE "Task" ALTER COLUMN "status" TYPE "TaskStatus_new" USING (CASE "status"::text WHEN 'IN_PROGRESS' THEN 'RUNNING' ELSE "status"::text END::"TaskStatus_new");
ALTER TYPE "TaskStatus" RENAME TO "TaskStatus_old";
ALTER TYPE "TaskStatus_new" RENAME TO "TaskStatus";
DROP TYPE "TaskStatus_old";
ALTER TABLE "Task" ALTER COLUMN "status" SET DEFAULT 'PENDING';
COMMIT;
-- DropForeignKey
ALTER TABLE "Message" DROP CONSTRAINT "Message_taskId_fkey";
-- DropForeignKey
ALTER TABLE "Summary" DROP CONSTRAINT "Summary_taskId_fkey";
-- AlterTable
ALTER TABLE "Message" ADD COLUMN "new_role" "Role" NOT NULL DEFAULT 'ASSISTANT';
UPDATE "Message"
SET "new_role" = CASE
WHEN lower("role"::text) = 'user' THEN 'USER'::"Role"
WHEN lower("role"::text) = 'assistant' THEN 'ASSISTANT'::"Role"
ELSE 'ASSISTANT'::"Role"
END;
-- Step 3: Drop the old 'role' column.
ALTER TABLE "Message" DROP COLUMN "role";
-- Step 4: Rename 'new_role' to 'role'.
ALTER TABLE "Message" RENAME COLUMN "new_role" TO "role";
-- AlterTable
ALTER TABLE "Task" ADD COLUMN "completedAt" TIMESTAMP(3),
ADD COLUMN "createdBy" "Role" NOT NULL DEFAULT 'USER',
ADD COLUMN "error" TEXT,
ADD COLUMN "executedAt" TIMESTAMP(3),
ADD COLUMN "result" JSONB,
ADD COLUMN "type" "TaskType" NOT NULL DEFAULT 'IMMEDIATE';
-- DropEnum
DROP TYPE "MessageRole";
-- AddForeignKey
ALTER TABLE "Summary" ADD CONSTRAINT "Summary_taskId_fkey" FOREIGN KEY ("taskId") REFERENCES "Task"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Message" ADD CONSTRAINT "Message_taskId_fkey" FOREIGN KEY ("taskId") REFERENCES "Task"("id") ON DELETE CASCADE ON UPDATE CASCADE;
================================================
FILE: packages/bytebot-agent-cc/prisma/migrations/20250523162632_add_scheduling/migration.sql
================================================
-- AlterTable
ALTER TABLE "Task" ADD COLUMN "queuedAt" TIMESTAMP(3),
ADD COLUMN "scheduledFor" TIMESTAMP(3);
================================================
FILE: packages/bytebot-agent-cc/prisma/migrations/20250529003255_tasks_control/migration.sql
================================================
-- AlterTable
ALTER TABLE "Task" ADD COLUMN "control" "Role" NOT NULL DEFAULT 'USER';
================================================
FILE: packages/bytebot-agent-cc/prisma/migrations/20250530012753_tasks_control/migration.sql
================================================
-- AlterTable
ALTER TABLE "Task" ALTER COLUMN "control" SET DEFAULT 'ASSISTANT';
================================================
FILE: packages/bytebot-agent-cc/prisma/migrations/20250619013027_add_better_auth_schema/migration.sql
================================================
-- AlterTable
ALTER TABLE "Message" ADD COLUMN "userId" TEXT;
-- CreateTable
CREATE TABLE "User" (
"id" TEXT NOT NULL,
"name" TEXT,
"email" TEXT NOT NULL,
"emailVerified" BOOLEAN NOT NULL DEFAULT false,
"image" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Session" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"token" TEXT NOT NULL,
"expiresAt" TIMESTAMP(3) NOT NULL,
"ipAddress" TEXT,
"userAgent" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Session_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Account" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"accountId" TEXT NOT NULL,
"providerId" TEXT NOT NULL,
"accessToken" TEXT,
"refreshToken" TEXT,
"accessTokenExpiresAt" TIMESTAMP(3),
"refreshTokenExpiresAt" TIMESTAMP(3),
"scope" TEXT,
"idToken" TEXT,
"password" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Account_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Verification" (
"id" TEXT NOT NULL,
"identifier" TEXT NOT NULL,
"value" TEXT NOT NULL,
"expiresAt" TIMESTAMP(3) NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Verification_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
-- CreateIndex
CREATE UNIQUE INDEX "Session_token_key" ON "Session"("token");
-- CreateIndex
CREATE UNIQUE INDEX "Account_providerId_accountId_key" ON "Account"("providerId", "accountId");
-- CreateIndex
CREATE UNIQUE INDEX "Verification_identifier_value_key" ON "Verification"("identifier", "value");
-- AddForeignKey
ALTER TABLE "Message" ADD CONSTRAINT "Message_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
================================================
FILE: packages/bytebot-agent-cc/prisma/migrations/20250622195148_add_user_to_task/migration.sql
================================================
-- AlterTable
ALTER TABLE "Task" ADD COLUMN "userId" TEXT;
-- AddForeignKey
ALTER TABLE "Task" ADD CONSTRAINT "Task_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
================================================
FILE: packages/bytebot-agent-cc/prisma/migrations/20250706223912_model_picker/migration.sql
================================================
-- AlterTable: add `model` column as JSONB (nullable initially)
ALTER TABLE "Task" ADD COLUMN "model" JSONB;
-- Backfill existing tasks with the default Anthropic Claude Opus 4 model
UPDATE "Task"
SET "model" = jsonb_build_object(
'provider', 'anthropic',
'name', 'claude-opus-4-20250514',
'title', 'Claude Opus 4'
)
WHERE "model" IS NULL;
-- Enforce NOT NULL constraint now that data is populated
ALTER TABLE "Task" ALTER COLUMN "model" SET NOT NULL;
================================================
FILE: packages/bytebot-agent-cc/prisma/migrations/20250722041608_files/migration.sql
================================================
-- CreateTable
CREATE TABLE "File" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"type" TEXT NOT NULL,
"size" INTEGER NOT NULL,
"data" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"taskId" TEXT NOT NULL,
CONSTRAINT "File_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "File" ADD CONSTRAINT "File_taskId_fkey" FOREIGN KEY ("taskId") REFERENCES "Task"("id") ON DELETE CASCADE ON UPDATE CASCADE;
================================================
FILE: packages/bytebot-agent-cc/prisma/migrations/20250820172813_remove_auth/migration.sql
================================================
/*
Warnings:
- You are about to drop the column `userId` on the `Message` table. All the data in the column will be lost.
- You are about to drop the column `userId` on the `Task` table. All the data in the column will be lost.
- You are about to drop the `Account` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `Session` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `User` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `Verification` table. If the table is not empty, all the data it contains will be lost.
*/
-- DropForeignKey
ALTER TABLE "public"."Account" DROP CONSTRAINT "Account_userId_fkey";
-- DropForeignKey
ALTER TABLE "public"."Message" DROP CONSTRAINT "Message_userId_fkey";
-- DropForeignKey
ALTER TABLE "public"."Session" DROP CONSTRAINT "Session_userId_fkey";
-- DropForeignKey
ALTER TABLE "public"."Task" DROP CONSTRAINT "Task_userId_fkey";
-- AlterTable
ALTER TABLE "public"."Message" DROP COLUMN "userId";
-- AlterTable
ALTER TABLE "public"."Task" DROP COLUMN "userId";
-- DropTable
DROP TABLE "public"."Account";
-- DropTable
DROP TABLE "public"."Session";
-- DropTable
DROP TABLE "public"."User";
-- DropTable
DROP TABLE "public"."Verification";
================================================
FILE: packages/bytebot-agent-cc/prisma/migrations/migration_lock.toml
================================================
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "postgresql"
================================================
FILE: packages/bytebot-agent-cc/prisma/schema.prisma
================================================
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
enum TaskStatus {
PENDING
RUNNING
NEEDS_HELP
NEEDS_REVIEW
COMPLETED
CANCELLED
FAILED
}
enum TaskPriority {
LOW
MEDIUM
HIGH
URGENT
}
enum Role {
USER
ASSISTANT
}
enum TaskType {
IMMEDIATE
SCHEDULED
}
model Task {
id String @id @default(uuid())
description String
type TaskType @default(IMMEDIATE)
status TaskStatus @default(PENDING)
priority TaskPriority @default(MEDIUM)
control Role @default(ASSISTANT)
createdAt DateTime @default(now())
createdBy Role @default(USER)
scheduledFor DateTime?
updatedAt DateTime @updatedAt
executedAt DateTime?
completedAt DateTime?
queuedAt DateTime?
error String?
result Json?
// Example:
// { "provider": "anthropic", "name": "claude-opus-4-20250514", "title": "Claude Opus 4" }
model Json
messages Message[]
summaries Summary[]
files File[]
}
model Summary {
id String @id @default(uuid())
content String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
messages Message[] // One-to-many relationship: one Summary has many Messages
task Task @relation(fields: [taskId], references: [id], onDelete: Cascade)
taskId String
// Self-referential relationship
parentSummary Summary? @relation("SummaryHierarchy", fields: [parentId], references: [id])
parentId String?
childSummaries Summary[] @relation("SummaryHierarchy")
}
model Message {
id String @id @default(uuid())
// Content field follows Anthropic's content blocks structure
// Example:
// [
// {"type": "text", "text": "Hello world"},
// {"type": "image", "source": {"type": "base64", "media_type": "image/jpeg", "data": "..."}}
// ]
content Json
role Role @default(ASSISTANT)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
task Task @relation(fields: [taskId], references: [id], onDelete: Cascade)
taskId String
summary Summary? @relation(fields: [summaryId], references: [id])
summaryId String? // Optional foreign key to Summary
}
model File {
id String @id @default(uuid())
name String
type String // MIME type
size Int // Size in bytes
data String // Base64 encoded file data
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
task Task @relation(fields: [taskId], references: [id], onDelete: Cascade)
taskId String
}
================================================
FILE: packages/bytebot-agent-cc/src/agent/agent.analytics.ts
================================================
import { Injectable, Logger } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { ConfigService } from '@nestjs/config';
import { TasksService } from '../tasks/tasks.service';
import { MessagesService } from '../messages/messages.service';
@Injectable()
export class AgentAnalyticsService {
private readonly logger = new Logger(AgentAnalyticsService.name);
private readonly endpoint?: string;
constructor(
private readonly tasksService: TasksService,
private readonly messagesService: MessagesService,
configService: ConfigService,
) {
this.endpoint = configService.get('BYTEBOT_ANALYTICS_ENDPOINT');
if (!this.endpoint) {
this.logger.warn(
'BYTEBOT_ANALYTICS_ENDPOINT is not set. Analytics service disabled.',
);
}
}
@OnEvent('task.cancel')
@OnEvent('task.failed')
@OnEvent('task.completed')
async handleTaskEvent(payload: { taskId: string }) {
if (!this.endpoint) return;
try {
const task = await this.tasksService.findById(payload.taskId);
const messages = await this.messagesService.findEvery(payload.taskId);
await fetch(this.endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...task, messages }),
});
} catch (error: any) {
this.logger.error(
`Failed to send analytics for task ${payload.taskId}: ${error.message}`,
error.stack,
);
}
}
}
================================================
FILE: packages/bytebot-agent-cc/src/agent/agent.computer-use.ts
================================================
import {
Button,
Coordinates,
Press,
ComputerToolUseContentBlock,
ToolResultContentBlock,
MessageContentType,
isScreenshotToolUseBlock,
isCursorPositionToolUseBlock,
isMoveMouseToolUseBlock,
isTraceMouseToolUseBlock,
isClickMouseToolUseBlock,
isPressMouseToolUseBlock,
isDragMouseToolUseBlock,
isScrollToolUseBlock,
isTypeKeysToolUseBlock,
isPressKeysToolUseBlock,
isTypeTextToolUseBlock,
isWaitToolUseBlock,
isApplicationToolUseBlock,
isPasteTextToolUseBlock,
isReadFileToolUseBlock,
} from '@bytebot/shared';
import { Logger } from '@nestjs/common';
const BYTEBOT_DESKTOP_BASE_URL = process.env.BYTEBOT_DESKTOP_BASE_URL as string;
export async function handleComputerToolUse(
block: ComputerToolUseContentBlock,
logger: Logger,
): Promise {
logger.debug(
`Handling computer tool use: ${block.name}, tool_use_id: ${block.id}`,
);
if (isScreenshotToolUseBlock(block)) {
logger.debug('Processing screenshot request');
try {
logger.debug('Taking screenshot');
const image = await screenshot();
logger.debug('Screenshot captured successfully');
return {
type: MessageContentType.ToolResult,
tool_use_id: block.id,
content: [
{
type: MessageContentType.Image,
source: {
data: image,
media_type: 'image/png',
type: 'base64',
},
},
],
};
} catch (error) {
logger.error(`Screenshot failed: ${error.message}`, error.stack);
return {
type: MessageContentType.ToolResult,
tool_use_id: block.id,
content: [
{
type: MessageContentType.Text,
text: 'ERROR: Failed to take screenshot',
},
],
is_error: true,
};
}
}
if (isCursorPositionToolUseBlock(block)) {
logger.debug('Processing cursor position request');
try {
logger.debug('Getting cursor position');
const position = await cursorPosition();
logger.debug(`Cursor position obtained: ${position.x}, ${position.y}`);
return {
type: MessageContentType.ToolResult,
tool_use_id: block.id,
content: [
{
type: MessageContentType.Text,
text: `Cursor position: ${position.x}, ${position.y}`,
},
],
};
} catch (error) {
logger.error(
`Getting cursor position failed: ${error.message}`,
error.stack,
);
return {
type: MessageContentType.ToolResult,
tool_use_id: block.id,
content: [
{
type: MessageContentType.Text,
text: 'ERROR: Failed to get cursor position',
},
],
is_error: true,
};
}
}
try {
if (isMoveMouseToolUseBlock(block)) {
await moveMouse(block.input);
}
if (isTraceMouseToolUseBlock(block)) {
await traceMouse(block.input);
}
if (isClickMouseToolUseBlock(block)) {
await clickMouse(block.input);
}
if (isPressMouseToolUseBlock(block)) {
await pressMouse(block.input);
}
if (isDragMouseToolUseBlock(block)) {
await dragMouse(block.input);
}
if (isScrollToolUseBlock(block)) {
await scroll(block.input);
}
if (isTypeKeysToolUseBlock(block)) {
await typeKeys(block.input);
}
if (isPressKeysToolUseBlock(block)) {
await pressKeys(block.input);
}
if (isTypeTextToolUseBlock(block)) {
await typeText(block.input);
}
if (isPasteTextToolUseBlock(block)) {
await pasteText(block.input);
}
if (isWaitToolUseBlock(block)) {
await wait(block.input);
}
if (isApplicationToolUseBlock(block)) {
await application(block.input);
}
if (isReadFileToolUseBlock(block)) {
logger.debug(`Reading file: ${block.input.path}`);
const result = await readFile(block.input);
if (result.success && result.data) {
// Return document content block
return {
type: MessageContentType.ToolResult,
tool_use_id: block.id,
content: [
{
type: MessageContentType.Document,
source: {
type: 'base64',
media_type: result.mediaType || 'application/octet-stream',
data: result.data,
},
name: result.name || 'file',
size: result.size,
},
],
};
} else {
// Return error message
return {
type: MessageContentType.ToolResult,
tool_use_id: block.id,
content: [
{
type: MessageContentType.Text,
text: result.message || 'Error reading file',
},
],
is_error: true,
};
}
}
let image: string | null = null;
try {
logger.debug('Taking screenshot');
image = await screenshot();
logger.debug('Screenshot captured successfully');
} catch (error) {
logger.error('Failed to take screenshot', error);
}
logger.debug(`Tool execution successful for tool_use_id: ${block.id}`);
const toolResult: ToolResultContentBlock = {
type: MessageContentType.ToolResult,
tool_use_id: block.id,
content: [
{
type: MessageContentType.Text,
text: 'Tool executed successfully',
},
],
};
if (image) {
toolResult.content.push({
type: MessageContentType.Image,
source: {
data: image,
media_type: 'image/png',
type: 'base64',
},
});
}
return toolResult;
} catch (error) {
logger.error(
`Error executing ${block.name} tool: ${error.message}`,
error.stack,
);
return {
type: MessageContentType.ToolResult,
tool_use_id: block.id,
content: [
{
type: MessageContentType.Text,
text: `Error executing ${block.name} tool: ${error.message}`,
},
],
is_error: true,
};
}
}
async function moveMouse(input: { coordinates: Coordinates }): Promise {
const { coordinates } = input;
console.log(
`Moving mouse to coordinates: [${coordinates.x}, ${coordinates.y}]`,
);
try {
await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'move_mouse',
coordinates,
}),
});
} catch (error) {
console.error('Error in move_mouse action:', error);
throw error;
}
}
async function traceMouse(input: {
path: Coordinates[];
holdKeys?: string[];
}): Promise {
const { path, holdKeys } = input;
console.log(
`Tracing mouse to path: ${path} ${holdKeys ? `with holdKeys: ${holdKeys}` : ''}`,
);
try {
await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'trace_mouse',
path,
holdKeys,
}),
});
} catch (error) {
console.error('Error in trace_mouse action:', error);
throw error;
}
}
async function clickMouse(input: {
coordinates?: Coordinates;
button: Button;
holdKeys?: string[];
clickCount: number;
}): Promise {
const { coordinates, button, holdKeys, clickCount } = input;
console.log(
`Clicking mouse ${button} ${clickCount} times ${coordinates ? `at coordinates: [${coordinates.x}, ${coordinates.y}] ` : ''} ${holdKeys ? `with holdKeys: ${holdKeys}` : ''}`,
);
try {
await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'click_mouse',
coordinates,
button,
holdKeys: holdKeys && holdKeys.length > 0 ? holdKeys : undefined,
clickCount,
}),
});
} catch (error) {
console.error('Error in click_mouse action:', error);
throw error;
}
}
async function pressMouse(input: {
coordinates?: Coordinates;
button: Button;
press: Press;
}): Promise {
const { coordinates, button, press } = input;
console.log(
`Pressing mouse ${button} ${press} ${coordinates ? `at coordinates: [${coordinates.x}, ${coordinates.y}]` : ''}`,
);
try {
await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'press_mouse',
coordinates,
button,
press,
}),
});
} catch (error) {
console.error('Error in press_mouse action:', error);
throw error;
}
}
async function dragMouse(input: {
path: Coordinates[];
button: Button;
holdKeys?: string[];
}): Promise {
const { path, button, holdKeys } = input;
console.log(
`Dragging mouse to path: ${path} ${holdKeys ? `with holdKeys: ${holdKeys}` : ''}`,
);
try {
await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'drag_mouse',
path,
button,
holdKeys: holdKeys && holdKeys.length > 0 ? holdKeys : undefined,
}),
});
} catch (error) {
console.error('Error in drag_mouse action:', error);
throw error;
}
}
async function scroll(input: {
coordinates?: Coordinates;
direction: 'up' | 'down' | 'left' | 'right';
scrollCount: number;
holdKeys?: string[];
}): Promise {
const { coordinates, direction, scrollCount, holdKeys } = input;
console.log(
`Scrolling ${direction} ${scrollCount} times ${coordinates ? `at coordinates: [${coordinates.x}, ${coordinates.y}]` : ''}`,
);
try {
await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'scroll',
coordinates,
direction,
scrollCount,
holdKeys: holdKeys && holdKeys.length > 0 ? holdKeys : undefined,
}),
});
} catch (error) {
console.error('Error in scroll action:', error);
throw error;
}
}
async function typeKeys(input: {
keys: string[];
delay?: number;
}): Promise {
const { keys, delay } = input;
console.log(`Typing keys: ${keys}`);
try {
await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'type_keys',
keys,
delay,
}),
});
} catch (error) {
console.error('Error in type_keys action:', error);
throw error;
}
}
async function pressKeys(input: {
keys: string[];
press: Press;
}): Promise {
const { keys, press } = input;
console.log(`Pressing keys: ${keys}`);
try {
await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'press_keys',
keys,
press,
}),
});
} catch (error) {
console.error('Error in press_keys action:', error);
throw error;
}
}
async function typeText(input: {
text: string;
delay?: number;
}): Promise {
const { text, delay } = input;
console.log(`Typing text: ${text}`);
try {
await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'type_text',
text,
delay,
}),
});
} catch (error) {
console.error('Error in type_text action:', error);
throw error;
}
}
async function pasteText(input: { text: string }): Promise {
const { text } = input;
console.log(`Pasting text: ${text}`);
try {
await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'paste_text',
text,
}),
});
} catch (error) {
console.error('Error in paste_text action:', error);
throw error;
}
}
async function wait(input: { duration: number }): Promise {
const { duration } = input;
console.log(`Waiting for ${duration}ms`);
try {
await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'wait',
duration,
}),
});
} catch (error) {
console.error('Error in wait action:', error);
throw error;
}
}
async function cursorPosition(): Promise {
console.log('Getting cursor position');
try {
const response = await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'cursor_position',
}),
});
const data = await response.json();
return { x: data.x, y: data.y };
} catch (error) {
console.error('Error in cursor_position action:', error);
throw error;
}
}
async function screenshot(): Promise {
console.log('Taking screenshot');
try {
const requestBody = {
action: 'screenshot',
};
const response = await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(requestBody),
});
if (!response.ok) {
throw new Error(`Failed to take screenshot: ${response.statusText}`);
}
const data = await response.json();
if (!data.image) {
throw new Error('Failed to take screenshot: No image data received');
}
return data.image; // Base64 encoded image
} catch (error) {
console.error('Error in screenshot action:', error);
throw error;
}
}
async function application(input: { application: string }): Promise {
const { application } = input;
console.log(`Opening application: ${application}`);
try {
await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'application',
application,
}),
});
} catch (error) {
console.error('Error in application action:', error);
throw error;
}
}
async function readFile(input: { path: string }): Promise<{
success: boolean;
data?: string;
name?: string;
size?: number;
mediaType?: string;
message?: string;
}> {
const { path } = input;
console.log(`Reading file: ${path}`);
try {
const response = await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'read_file',
path,
}),
});
if (!response.ok) {
throw new Error(`Failed to read file: ${response.statusText}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error('Error in read_file action:', error);
return {
success: false,
message: `Error reading file: ${error.message}`,
};
}
}
export async function writeFile(input: {
path: string;
content: string;
}): Promise<{ success: boolean; message?: string }> {
const { path, content } = input;
console.log(`Writing file: ${path}`);
try {
// Content is always base64 encoded
const base64Data = content;
const response = await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'write_file',
path,
data: base64Data,
}),
});
if (!response.ok) {
throw new Error(`Failed to write file: ${response.statusText}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error('Error in write_file action:', error);
return {
success: false,
message: `Error writing file: ${error.message}`,
};
}
}
================================================
FILE: packages/bytebot-agent-cc/src/agent/agent.constants.ts
================================================
export const DEFAULT_DISPLAY_SIZE = {
width: 1280,
height: 960,
};
export const SUMMARIZATION_SYSTEM_PROMPT = `You are a helpful assistant that summarizes conversations for long-running tasks.
Your job is to create concise summaries that preserve all important information, tool usage, and key decisions.
Focus on:
- Task progress and completed actions
- Important tool calls and their results
- Key decisions made
- Any errors or issues encountered
- Current state and what remains to be done
Provide a structured summary that can be used as context for continuing the task.`;
export const AGENT_SYSTEM_PROMPT = `
You are **Bytebot**, a highly-reliable AI engineer operating a virtual computer whose display measures ${DEFAULT_DISPLAY_SIZE.width} x ${DEFAULT_DISPLAY_SIZE.height} pixels.
The current date is ${new Date().toLocaleDateString()}. The current time is ${new Date().toLocaleTimeString()}. The current timezone is ${Intl.DateTimeFormat().resolvedOptions().timeZone}.
────────────────────────
AVAILABLE APPLICATIONS
────────────────────────
On the desktop, the following applications are available:
Firefox Browser -- The default web browser, use it to navigate to websites.
Thunderbird -- The default email client, use it to send and receive emails (if you have an account).
1Password -- The password manager, use it to store and retrieve your passwords (if you have an account).
Visual Studio Code -- The default code editor, use it to create and edit files.
Terminal -- The default terminal, use it to run commands.
File Manager -- The default file manager, use it to navigate and manage files.
Trash -- The default trash
ALL APPLICATIONS ARE GUI BASED, USE THE COMPUTER TOOLS TO INTERACT WITH THEM. ONLY ACCESS THE APPLICATIONS VIA THEIR DESKTOP ICONS.
*Never* use keyboard shortcuts to switch between applications, only use \`computer_application\` to switch between the default applications.
────────────────────────
CORE WORKING PRINCIPLES
────────────────────────
1. **Observe First** - *Always* invoke \`computer_screenshot\` before your first action **and** whenever the UI may have changed. Screenshot before every action when filling out forms. Never act blindly. When opening documents or PDFs, scroll through at least the first page to confirm it is the correct document.
2. **Navigate applications** = *Always* invoke \`computer_application\` to switch between the default applications.
3. **Human-Like Interaction**
• Move in smooth, purposeful paths; click near the visual centre of targets.
• Double-click desktop icons to open them.
• Type realistic, context-appropriate text with \`computer_type_text\` (for short strings) or \`computer_paste_text\` (for long strings), or shortcuts with \`computer_type_keys\`.
4. **Valid Keys Only** -
Use **exactly** the identifiers listed in **VALID KEYS** below when supplying \`keys\` to \`computer_type_keys\` or \`computer_press_keys\`. All identifiers come from nut-tree's \`Key\` enum; they are case-sensitive and contain *no spaces*.
5. **Verify Every Step** - After each action:
a. Take another screenshot.
b. Confirm the expected state before continuing. If it failed, retry sensibly (try again, and then try 2 different methods).
6. **Efficiency & Clarity** - Combine related key presses; prefer scrolling or dragging over many small moves; minimise unnecessary waits.
7. **Stay Within Scope** - Do nothing the user didn't request; don't suggest unrelated tasks. For form and login fields, don't fill in random data, unless explicitly told to do so.
8. **Security** - If you see a password, secret key, or other sensitive information (or the user shares it with you), do not repeat it in conversation. When typing sensitive information, use \`computer_type_text\` with \`isSensitive\` set to \`true\`.
9. **Consistency & Persistence** - Even if the task is repetitive, do not end the task until the user's goal is completely met. For bulk operations, maintain focus and continue until all items are processed.
────────────────────────
REPETITIVE TASK HANDLING
────────────────────────
When performing repetitive tasks (e.g., "visit each profile", "process all items"):
1. **Track Progress** - Maintain a mental count of:
• Total items to process (if known)
• Items completed so far
• Current item being processed
• Any errors encountered
2. **Batch Processing** - For large sets:
• Process in groups of 10-20 items
• Take brief pauses between batches to prevent system overload
• Continue until ALL items are processed
3. **Error Recovery** - If an item fails:
• Note the error but continue with the next item
• Keep a list of failed items to report at the end
• Don't let one failure stop the entire operation
4. **Progress Updates** - Every 10-20 items:
• Brief status: "Processed 20/100 profiles, continuing..."
• No need for detailed reports unless requested
5. **Completion Criteria** - The task is NOT complete until:
• All items in the set are processed, OR
• You reach a clear endpoint (e.g., "No more profiles to load"), OR
• The user explicitly tells you to stop
6. **State Management** - If the task might span multiple tabs/pages:
• Save progress to a file periodically
• Include timestamps and item identifiers
────────────────────────
TASK LIFECYCLE TEMPLATE
────────────────────────
1. **Prepare** - Initial screenshot → plan → estimate scope if possible.
2. **Execute Loop** - For each sub-goal: Screenshot → Think → Act → Verify.
3. **Batch Loop** - For repetitive tasks:
• While items remain:
- Process batch of 10-20 items
- Update progress counter
- Check for stop conditions
- Brief status update
• Continue until ALL done
4. **Switch Applications** - If you need to switch between the default applications, reach the home directory, or return to the desktop, invoke
\`\`\`json
{ "name": "computer_application", "input": { "application": "application name" } }
\`\`\`
It will open (or focus if it is already open) the application, in fullscreen.
The application name must be one of the following: firefox, thunderbird, 1password, vscode, terminal, directory, desktop.
5. **Create other tasks** - If you need to create additional separate tasks, invoke
\`\`\`json
{ "name": "create_task", "input": { "description": "Subtask description", "type": "IMMEDIATE", "priority": "MEDIUM" } }
\`\`\`
The other tasks will be executed in the order they are created, after the current task is completed. Only create separate tasks if they are not related to the current task.
6. **Schedule future tasks** - If you need to schedule a task to run in the future, invoke
\`\`\`json
{ "name": "create_task", "input": { "description": "Subtask description", "type": "SCHEDULED", "scheduledFor": , "priority": "MEDIUM" } }
\`\`\`
Only schedule tasks if they must be run in the future. Do not schedule tasks that can be run immediately.
7. **Read Files** - If you need to read file contents, invoke
\`\`\`json
{ "name": "computer_read_file", "input": { "path": "/path/to/file" } }
\`\`\`
This tool reads files and returns them as document content blocks with base64 data, supporting various file types including documents (PDF, DOCX, TXT, etc.) and images (PNG, JPG, etc.).
8. **Cleanup** - When the user's goal is met:
• Close every window, file, or app you opened so the desktop is tidy.
• Return to an idle desktop/background.
**IMPORTANT**: For bulk operations like "visit each profile in the directory":
- Do NOT mark as completed after just a few profiles
- Continue until you've processed ALL profiles or reached a clear end
- If there are 100+ profiles, process them ALL
- Only stop when explicitly told or when there are genuinely no more items
────────────────────────
VALID KEYS
────────────────────────
A, Add, AudioForward, AudioMute, AudioNext, AudioPause, AudioPlay, AudioPrev, AudioRandom, AudioRepeat, AudioRewind, AudioStop, AudioVolDown, AudioVolUp,
B, Backslash, Backspace,
C, CapsLock, Clear, Comma,
D, Decimal, Delete, Divide, Down,
E, End, Enter, Equal, Escape, F,
F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12, F13, F14, F15, F16, F17, F18, F19, F20, F21, F22, F23, F24,
Fn,
G, Grave,
H, Home,
I, Insert,
J, K, L, Left, LeftAlt, LeftBracket, LeftCmd, LeftControl, LeftShift, LeftSuper, LeftWin,
M, Menu, Minus, Multiply,
N, Num0, Num1, Num2, Num3, Num4, Num5, Num6, Num7, Num8, Num9, NumLock,
NumPad0, NumPad1, NumPad2, NumPad3, NumPad4, NumPad5, NumPad6, NumPad7, NumPad8, NumPad9,
O, P, PageDown, PageUp, Pause, Period, Print,
Q, Quote,
R, Return, Right, RightAlt, RightBracket, RightCmd, RightControl, RightShift, RightSuper, RightWin,
S, ScrollLock, Semicolon, Slash, Space, Subtract,
T, Tab,
U, Up,
V, W, X, Y, Z
Remember: **accuracy over speed, clarity and consistency over cleverness**.
**For repetitive tasks**: Persistence is key. Continue until ALL items are processed, not just the first few.
`;
================================================
FILE: packages/bytebot-agent-cc/src/agent/agent.module.ts
================================================
import { Module } from '@nestjs/common';
import { TasksModule } from '../tasks/tasks.module';
import { MessagesModule } from '../messages/messages.module';
import { AgentProcessor } from './agent.processor';
import { ConfigModule } from '@nestjs/config';
import { AgentScheduler } from './agent.scheduler';
import { InputCaptureService } from './input-capture.service';
import { AgentAnalyticsService } from './agent.analytics';
@Module({
imports: [ConfigModule, TasksModule, MessagesModule],
providers: [
AgentProcessor,
AgentScheduler,
InputCaptureService,
AgentAnalyticsService,
],
exports: [AgentProcessor],
})
export class AgentModule {}
================================================
FILE: packages/bytebot-agent-cc/src/agent/agent.processor.ts
================================================
import { TasksService } from '../tasks/tasks.service';
import { MessagesService } from '../messages/messages.service';
import { Injectable, Logger } from '@nestjs/common';
import {
Message,
Role,
Task,
TaskPriority,
TaskStatus,
TaskType,
} from '@prisma/client';
import {
isComputerToolUseContentBlock,
isSetTaskStatusToolUseBlock,
isCreateTaskToolUseBlock,
SetTaskStatusToolUseBlock,
RedactedThinkingContentBlock,
ThinkingContentBlock,
ToolUseContentBlock,
} from '@bytebot/shared';
import {
MessageContentBlock,
MessageContentType,
ToolResultContentBlock,
TextContentBlock,
} from '@bytebot/shared';
import { InputCaptureService } from './input-capture.service';
import { OnEvent } from '@nestjs/event-emitter';
import {
BytebotAgentModel,
BytebotAgentService,
BytebotAgentResponse,
} from './agent.types';
import {
AGENT_SYSTEM_PROMPT,
SUMMARIZATION_SYSTEM_PROMPT,
} from './agent.constants';
import { query } from '@anthropic-ai/claude-code';
import Anthropic from '@anthropic-ai/sdk';
@Injectable()
export class AgentProcessor {
private readonly logger = new Logger(AgentProcessor.name);
private currentTaskId: string | null = null;
private isProcessing = false;
private abortController: AbortController | null = null;
private readonly BYTEBOT_DESKTOP_BASE_URL = process.env
.BYTEBOT_DESKTOP_BASE_URL as string;
constructor(
private readonly tasksService: TasksService,
private readonly messagesService: MessagesService,
private readonly inputCaptureService: InputCaptureService,
) {
this.logger.log('AgentProcessor initialized');
}
/**
* Check if the processor is currently processing a task
*/
isRunning(): boolean {
return this.isProcessing;
}
/**
* Get the current task ID being processed
*/
getCurrentTaskId(): string | null {
return this.currentTaskId;
}
@OnEvent('task.takeover')
handleTaskTakeover({ taskId }: { taskId: string }) {
this.logger.log(`Task takeover event received for task ID: ${taskId}`);
// If the agent is still processing this task, abort any in-flight operations
if (this.currentTaskId === taskId && this.isProcessing) {
this.abortController?.abort();
}
// Always start capturing user input so that emitted actions are received
this.inputCaptureService.start(taskId);
}
@OnEvent('task.resume')
handleTaskResume({ taskId }: { taskId: string }) {
if (this.currentTaskId === taskId && this.isProcessing) {
this.logger.log(`Task resume event received for task ID: ${taskId}`);
this.abortController = new AbortController();
void this.runIteration(taskId);
}
}
@OnEvent('task.cancel')
async handleTaskCancel({ taskId }: { taskId: string }) {
this.logger.log(`Task cancel event received for task ID: ${taskId}`);
await this.stopProcessing();
}
processTask(taskId: string) {
this.logger.log(`Starting processing for task ID: ${taskId}`);
if (this.isProcessing) {
this.logger.warn('AgentProcessor is already processing another task');
return;
}
this.isProcessing = true;
this.currentTaskId = taskId;
this.abortController = new AbortController();
// Kick off the first iteration without blocking the caller
void this.runIteration(taskId);
}
/**
* Convert Anthropic's response content to our MessageContentBlock format
*/
private formatAnthropicResponse(
content: Anthropic.ContentBlock[],
): MessageContentBlock[] {
// filter out tool_use blocks that aren't computer tool uses
content = content.filter(
(block) =>
block.type !== 'tool_use' || block.name.startsWith('mcp__desktop__'),
);
return content.map((block) => {
switch (block.type) {
case 'text':
return {
type: MessageContentType.Text,
text: block.text,
} as TextContentBlock;
case 'tool_use':
return {
type: MessageContentType.ToolUse,
id: block.id,
name: block.name.replace('mcp__desktop__', ''),
input: block.input,
} as ToolUseContentBlock;
case 'thinking':
return {
type: MessageContentType.Thinking,
thinking: block.thinking,
signature: block.signature,
} as ThinkingContentBlock;
case 'redacted_thinking':
return {
type: MessageContentType.RedactedThinking,
data: block.data,
} as RedactedThinkingContentBlock;
}
});
}
/**
* Runs a single iteration of task processing and schedules the next
* iteration via setImmediate while the task remains RUNNING.
*/
private async runIteration(taskId: string): Promise {
if (!this.isProcessing) {
return;
}
try {
const task: Task = await this.tasksService.findById(taskId);
if (task.status !== TaskStatus.RUNNING) {
this.logger.log(
`Task processing completed for task ID: ${taskId} with status: ${task.status}`,
);
this.isProcessing = false;
this.currentTaskId = null;
return;
}
this.logger.log(`Processing iteration for task ID: ${taskId}`);
// Refresh abort controller for this iteration to avoid accumulating
// "abort" listeners on a single AbortSignal across iterations.
this.abortController = new AbortController();
for await (const message of query({
prompt: task.description,
options: {
abortController: this.abortController,
appendSystemPrompt: AGENT_SYSTEM_PROMPT,
permissionMode: 'bypassPermissions',
mcpServers: {
desktop: {
type: 'sse',
url: `${this.BYTEBOT_DESKTOP_BASE_URL}/mcp`,
},
},
},
})) {
let messageContentBlocks: MessageContentBlock[] = [];
let role: Role = Role.ASSISTANT;
switch (message.type) {
case 'user': {
if (Array.isArray(message.message.content)) {
messageContentBlocks = message.message
.content as MessageContentBlock[];
} else if (typeof message.message.content === 'string') {
messageContentBlocks = [
{
type: MessageContentType.Text,
text: message.message.content,
} as TextContentBlock,
];
}
role = Role.USER;
break;
}
case 'assistant': {
messageContentBlocks = this.formatAnthropicResponse(
message.message.content,
);
break;
}
case 'system':
break;
case 'result': {
switch (message.subtype) {
case 'success':
await this.tasksService.update(taskId, {
status: TaskStatus.COMPLETED,
completedAt: new Date(),
});
break;
case 'error_max_turns':
case 'error_during_execution':
await this.tasksService.update(taskId, {
status: TaskStatus.NEEDS_HELP,
});
break;
}
break;
}
}
this.logger.debug(
`Received ${messageContentBlocks.length} content blocks from LLM`,
);
if (messageContentBlocks.length > 0) {
await this.messagesService.create({
content: messageContentBlocks,
role,
taskId,
});
}
}
} catch (error: any) {
if (error?.message === 'Claude Code process aborted by user') {
this.logger.warn(`Processing aborted for task ID: ${taskId}`);
} else {
this.logger.error(
`Error during task processing iteration for task ID: ${taskId} - ${error.message}`,
error.stack,
);
await this.tasksService.update(taskId, {
status: TaskStatus.FAILED,
});
this.isProcessing = false;
this.currentTaskId = null;
}
}
}
async stopProcessing(): Promise {
if (!this.isProcessing) {
return;
}
this.logger.log(`Stopping execution of task ${this.currentTaskId}`);
// Signal any in-flight async operations to abort
this.abortController?.abort();
await this.inputCaptureService.stop();
this.isProcessing = false;
this.currentTaskId = null;
}
}
================================================
FILE: packages/bytebot-agent-cc/src/agent/agent.scheduler.ts
================================================
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
import { TasksService } from '../tasks/tasks.service';
import { AgentProcessor } from './agent.processor';
import { TaskStatus } from '@prisma/client';
import { writeFile } from './agent.computer-use';
@Injectable()
export class AgentScheduler implements OnModuleInit {
private readonly logger = new Logger(AgentScheduler.name);
constructor(
private readonly tasksService: TasksService,
private readonly agentProcessor: AgentProcessor,
) {}
async onModuleInit() {
this.logger.log('AgentScheduler initialized');
await this.handleCron();
}
@Cron(CronExpression.EVERY_5_SECONDS)
async handleCron() {
const now = new Date();
const scheduledTasks = await this.tasksService.findScheduledTasks();
for (const scheduledTask of scheduledTasks) {
if (scheduledTask.scheduledFor && scheduledTask.scheduledFor < now) {
this.logger.debug(
`Task ID: ${scheduledTask.id} is scheduled for ${scheduledTask.scheduledFor}, queuing it`,
);
await this.tasksService.update(scheduledTask.id, {
queuedAt: now,
});
}
}
if (this.agentProcessor.isRunning()) {
return;
}
// Find the highest priority task to execute
const task = await this.tasksService.findNextTask();
if (task) {
if (task.files.length > 0) {
this.logger.debug(
`Task ID: ${task.id} has files, writing them to the desktop`,
);
for (const file of task.files) {
await writeFile({
path: `/home/user/Desktop/${file.name}`,
content: file.data, // file.data is already base64 encoded in the database
});
}
}
await this.tasksService.update(task.id, {
status: TaskStatus.RUNNING,
executedAt: new Date(),
});
this.logger.debug(`Processing task ID: ${task.id}`);
this.agentProcessor.processTask(task.id);
}
}
}
================================================
FILE: packages/bytebot-agent-cc/src/agent/agent.tools.ts
================================================
/**
* Common schema definitions for reuse
*/
const coordinateSchema = {
type: 'object' as const,
properties: {
x: {
type: 'number' as const,
description: 'The x-coordinate',
},
y: {
type: 'number' as const,
description: 'The y-coordinate',
},
},
required: ['x', 'y'],
};
const holdKeysSchema = {
type: 'array' as const,
items: { type: 'string' as const },
description: 'Optional array of keys to hold during the action',
nullable: true,
};
const buttonSchema = {
type: 'string' as const,
enum: ['left', 'right', 'middle'],
description: 'The mouse button',
};
/**
* Tool definitions for mouse actions
*/
export const _moveMouseTool = {
name: 'computer_move_mouse',
description: 'Moves the mouse cursor to the specified coordinates',
input_schema: {
type: 'object' as const,
properties: {
coordinates: {
...coordinateSchema,
description: 'Target coordinates for mouse movement',
},
},
required: ['coordinates'],
},
};
export const _traceMouseTool = {
name: 'computer_trace_mouse',
description: 'Moves the mouse cursor along a specified path of coordinates',
input_schema: {
type: 'object' as const,
properties: {
path: {
type: 'array' as const,
items: coordinateSchema,
description: 'Array of coordinate objects representing the path',
},
holdKeys: holdKeysSchema,
},
required: ['path'],
},
};
export const _clickMouseTool = {
name: 'computer_click_mouse',
description:
'Performs a mouse click at the specified coordinates or current position',
input_schema: {
type: 'object' as const,
properties: {
coordinates: {
...coordinateSchema,
description:
'Optional click coordinates (defaults to current position)',
nullable: true,
},
button: buttonSchema,
holdKeys: holdKeysSchema,
clickCount: {
type: 'integer' as const,
description: 'Number of clicks to perform (e.g., 2 for double-click)',
default: 1,
},
},
required: ['button', 'clickCount'],
},
};
export const _pressMouseTool = {
name: 'computer_press_mouse',
description: 'Presses or releases a specified mouse button',
input_schema: {
type: 'object' as const,
properties: {
coordinates: {
...coordinateSchema,
description: 'Optional coordinates (defaults to current position)',
nullable: true,
},
button: buttonSchema,
press: {
type: 'string' as const,
enum: ['up', 'down'],
description: 'Whether to press down or release up',
},
},
required: ['button', 'press'],
},
};
export const _dragMouseTool = {
name: 'computer_drag_mouse',
description: 'Drags the mouse along a path while holding a button',
input_schema: {
type: 'object' as const,
properties: {
path: {
type: 'array' as const,
items: coordinateSchema,
description: 'Array of coordinates representing the drag path',
},
button: buttonSchema,
holdKeys: holdKeysSchema,
},
required: ['path', 'button'],
},
};
export const _scrollTool = {
name: 'computer_scroll',
description: 'Scrolls the mouse wheel in the specified direction',
input_schema: {
type: 'object' as const,
properties: {
coordinates: {
...coordinateSchema,
description: 'Coordinates where the scroll should occur',
},
direction: {
type: 'string' as const,
enum: ['up', 'down', 'left', 'right'],
description: 'The direction to scroll',
},
scrollCount: {
type: 'integer' as const,
description: 'Number of scroll steps',
},
holdKeys: holdKeysSchema,
},
required: ['coordinates', 'direction', 'scrollCount'],
},
};
/**
* Tool definitions for keyboard actions
*/
export const _typeKeysTool = {
name: 'computer_type_keys',
description: 'Types a sequence of keys (useful for keyboard shortcuts)',
input_schema: {
type: 'object' as const,
properties: {
keys: {
type: 'array' as const,
items: { type: 'string' as const },
description: 'Array of key names to type in sequence',
},
delay: {
type: 'number' as const,
description: 'Optional delay in milliseconds between key presses',
nullable: true,
},
},
required: ['keys'],
},
};
export const _pressKeysTool = {
name: 'computer_press_keys',
description:
'Presses or releases specific keys (useful for holding modifiers)',
input_schema: {
type: 'object' as const,
properties: {
keys: {
type: 'array' as const,
items: { type: 'string' as const },
description: 'Array of key names to press or release',
},
press: {
type: 'string' as const,
enum: ['up', 'down'],
description: 'Whether to press down or release up',
},
},
required: ['keys', 'press'],
},
};
export const _typeTextTool = {
name: 'computer_type_text',
description:
'Types a string of text character by character. Use this tool for strings less than 25 characters, or passwords/sensitive form fields.',
input_schema: {
type: 'object' as const,
properties: {
text: {
type: 'string' as const,
description: 'The text string to type',
},
delay: {
type: 'number' as const,
description: 'Optional delay in milliseconds between characters',
nullable: true,
},
isSensitive: {
type: 'boolean' as const,
description: 'Flag to indicate sensitive information',
nullable: true,
},
},
required: ['text'],
},
};
export const _pasteTextTool = {
name: 'computer_paste_text',
description:
'Copies text to the clipboard and pastes it. Use this tool for typing long text strings or special characters not on the standard keyboard.',
input_schema: {
type: 'object' as const,
properties: {
text: {
type: 'string' as const,
description: 'The text string to type',
},
isSensitive: {
type: 'boolean' as const,
description: 'Flag to indicate sensitive information',
nullable: true,
},
},
required: ['text'],
},
};
/**
* Tool definitions for utility actions
*/
export const _waitTool = {
name: 'computer_wait',
description: 'Pauses execution for a specified duration',
input_schema: {
type: 'object' as const,
properties: {
duration: {
type: 'integer' as const,
enum: [500],
description: 'The duration to wait in milliseconds',
},
},
required: ['duration'],
},
};
export const _screenshotTool = {
name: 'computer_screenshot',
description: 'Captures a screenshot of the current screen',
input_schema: {
type: 'object' as const,
properties: {},
},
};
export const _cursorPositionTool = {
name: 'computer_cursor_position',
description: 'Gets the current (x, y) coordinates of the mouse cursor',
input_schema: {
type: 'object' as const,
properties: {},
},
};
export const _applicationTool = {
name: 'computer_application',
description: 'Opens or focuses an application and ensures it is fullscreen',
input_schema: {
type: 'object' as const,
properties: {
application: {
type: 'string' as const,
enum: [
'firefox',
'1password',
'thunderbird',
'vscode',
'terminal',
'desktop',
'directory',
],
description: 'The application to open or focus',
},
},
required: ['application'],
},
};
/**
* Tool definitions for task management
*/
export const _setTaskStatusTool = {
name: 'set_task_status',
description: 'Sets the status of the current task',
input_schema: {
type: 'object' as const,
properties: {
status: {
type: 'string' as const,
enum: ['completed', 'needs_help'],
description: 'The status of the task',
},
description: {
type: 'string' as const,
description:
'If the task is completed, a summary of the task. If the task needs help, a description of the issue or clarification needed.',
},
},
required: ['status', 'description'],
},
};
export const _createTaskTool = {
name: 'create_task',
description: 'Creates a new task',
input_schema: {
type: 'object' as const,
properties: {
description: {
type: 'string' as const,
description: 'The description of the task',
},
type: {
type: 'string' as const,
enum: ['IMMEDIATE', 'SCHEDULED'],
description: 'The type of the task (defaults to IMMEDIATE)',
},
scheduledFor: {
type: 'string' as const,
format: 'date-time',
description: 'RFC 3339 / ISO 8601 datetime for scheduled tasks',
},
priority: {
type: 'string' as const,
enum: ['LOW', 'MEDIUM', 'HIGH', 'URGENT'],
description: 'The priority of the task (defaults to MEDIUM)',
},
},
required: ['description'],
},
};
/**
* Tool definition for reading files
*/
export const _readFileTool = {
name: 'computer_read_file',
description:
'Reads a file from the specified path and returns it as a document content block with base64 encoded data',
input_schema: {
type: 'object' as const,
properties: {
path: {
type: 'string' as const,
description: 'The file path to read from',
},
},
required: ['path'],
},
};
/**
* Export all tools as an array
*/
export const agentTools = [
_moveMouseTool,
_traceMouseTool,
_clickMouseTool,
_pressMouseTool,
_dragMouseTool,
_scrollTool,
_typeKeysTool,
_pressKeysTool,
_typeTextTool,
_pasteTextTool,
_waitTool,
_screenshotTool,
_applicationTool,
_cursorPositionTool,
_setTaskStatusTool,
_createTaskTool,
_readFileTool,
];
================================================
FILE: packages/bytebot-agent-cc/src/agent/agent.types.ts
================================================
import { Message } from '@prisma/client';
import { MessageContentBlock } from '@bytebot/shared';
export interface BytebotAgentResponse {
contentBlocks: MessageContentBlock[];
tokenUsage: {
inputTokens: number;
outputTokens: number;
totalTokens: number;
};
}
export interface BytebotAgentService {
generateMessage(
systemPrompt: string,
messages: Message[],
model: string,
useTools: boolean,
signal?: AbortSignal,
): Promise;
}
export interface BytebotAgentModel {
provider: 'anthropic' | 'openai' | 'google' | 'proxy';
name: string;
title: string;
contextWindow?: number;
}
export class BytebotAgentInterrupt extends Error {
constructor() {
super('BytebotAgentInterrupt');
this.name = 'BytebotAgentInterrupt';
}
}
================================================
FILE: packages/bytebot-agent-cc/src/agent/input-capture.service.ts
================================================
import { Injectable, Logger } from '@nestjs/common';
import { io, Socket } from 'socket.io-client';
import { randomUUID } from 'crypto';
import {
convertClickMouseActionToToolUseBlock,
convertDragMouseActionToToolUseBlock,
convertPressKeysActionToToolUseBlock,
convertPressMouseActionToToolUseBlock,
convertScrollActionToToolUseBlock,
convertTypeKeysActionToToolUseBlock,
convertTypeTextActionToToolUseBlock,
ImageContentBlock,
MessageContentBlock,
MessageContentType,
ScreenshotToolUseBlock,
ToolResultContentBlock,
UserActionContentBlock,
} from '@bytebot/shared';
import { Role } from '@prisma/client';
import { MessagesService } from '../messages/messages.service';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class InputCaptureService {
private readonly logger = new Logger(InputCaptureService.name);
private socket: Socket | null = null;
private capturing = false;
constructor(
private readonly messagesService: MessagesService,
private readonly configService: ConfigService,
) {}
isCapturing() {
return this.capturing;
}
start(taskId: string) {
if (this.socket?.connected && this.capturing) return;
if (this.socket && !this.socket.connected) {
this.socket.connect();
return;
}
const baseUrl = this.configService.get('BYTEBOT_DESKTOP_BASE_URL');
if (!baseUrl) {
this.logger.warn('BYTEBOT_DESKTOP_BASE_URL missing.');
return;
}
this.socket = io(baseUrl, { transports: ['websocket'] });
this.socket.on('connect', () => {
this.logger.log('Input socket connected');
this.capturing = true;
});
this.socket.on(
'screenshotAndAction',
async (shot: { image: string }, action: any) => {
if (!this.capturing || !taskId) return;
// The gateway only sends a click_mouse or drag_mouse action together with screenshots for now.
if (action.action !== 'click_mouse' && action.action !== 'drag_mouse')
return;
const userActionBlock: UserActionContentBlock = {
type: MessageContentType.UserAction,
content: [
{
type: MessageContentType.Image,
source: {
data: shot.image,
media_type: 'image/png',
type: 'base64',
},
},
],
};
const toolUseId = randomUUID();
switch (action.action) {
case 'drag_mouse':
userActionBlock.content.push(
convertDragMouseActionToToolUseBlock(action, toolUseId),
);
break;
case 'click_mouse':
userActionBlock.content.push(
convertClickMouseActionToToolUseBlock(action, toolUseId),
);
break;
}
await this.messagesService.create({
content: [userActionBlock],
role: Role.USER,
taskId,
});
},
);
this.socket.on('action', async (action: any) => {
if (!this.capturing || !taskId) return;
const toolUseId = randomUUID();
const userActionBlock: UserActionContentBlock = {
type: MessageContentType.UserAction,
content: [],
};
switch (action.action) {
case 'drag_mouse':
userActionBlock.content.push(
convertDragMouseActionToToolUseBlock(action, toolUseId),
);
break;
case 'press_mouse':
userActionBlock.content.push(
convertPressMouseActionToToolUseBlock(action, toolUseId),
);
break;
case 'type_keys':
userActionBlock.content.push(
convertTypeKeysActionToToolUseBlock(action, toolUseId),
);
break;
case 'press_keys':
userActionBlock.content.push(
convertPressKeysActionToToolUseBlock(action, toolUseId),
);
break;
case 'type_text':
userActionBlock.content.push(
convertTypeTextActionToToolUseBlock(action, toolUseId),
);
break;
case 'scroll':
userActionBlock.content.push(
convertScrollActionToToolUseBlock(action, toolUseId),
);
break;
default:
this.logger.warn(`Unknown action ${action.action}`);
}
if (userActionBlock.content.length > 0) {
await this.messagesService.create({
content: [userActionBlock],
role: Role.USER,
taskId,
});
}
});
this.socket.on('disconnect', () => {
this.logger.log('Input socket disconnected');
this.capturing = false;
});
}
async stop() {
if (!this.socket) return;
if (this.socket.connected) this.socket.disconnect();
else this.socket.removeAllListeners();
this.socket = null;
this.capturing = false;
}
}
================================================
FILE: packages/bytebot-agent-cc/src/app.controller.ts
================================================
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
}
================================================
FILE: packages/bytebot-agent-cc/src/app.module.ts
================================================
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { AgentModule } from './agent/agent.module';
import { TasksModule } from './tasks/tasks.module';
import { MessagesModule } from './messages/messages.module';
import { PrismaModule } from './prisma/prisma.module';
import { ConfigModule } from '@nestjs/config';
import { ScheduleModule } from '@nestjs/schedule';
import { EventEmitterModule } from '@nestjs/event-emitter';
@Module({
imports: [
ScheduleModule.forRoot(),
EventEmitterModule.forRoot(),
ConfigModule.forRoot({
isGlobal: true,
}),
AgentModule,
TasksModule,
MessagesModule,
PrismaModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
================================================
FILE: packages/bytebot-agent-cc/src/app.service.ts
================================================
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}
================================================
FILE: packages/bytebot-agent-cc/src/main.ts
================================================
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { webcrypto } from 'crypto';
import { json, urlencoded } from 'express';
// Polyfill for crypto global (required by @nestjs/schedule)
if (!globalThis.crypto) {
globalThis.crypto = webcrypto as any;
}
async function bootstrap() {
console.log('Starting bytebot-agent application...');
try {
const app = await NestFactory.create(AppModule);
// Configure body parser with increased payload size limit (50MB)
app.use(json({ limit: '50mb' }));
app.use(urlencoded({ limit: '50mb', extended: true }));
// Set global prefix for all routes
app.setGlobalPrefix('api');
// Enable CORS
app.enableCors({
origin: '*',
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'PATCH'],
});
await app.listen(process.env.PORT ?? 9991);
} catch (error) {
console.error('Error starting application:', error);
}
}
bootstrap();
================================================
FILE: packages/bytebot-agent-cc/src/messages/messages.module.ts
================================================
import { Module, forwardRef } from '@nestjs/common';
import { MessagesService } from './messages.service';
import { PrismaModule } from '../prisma/prisma.module';
import { TasksModule } from '../tasks/tasks.module';
@Module({
imports: [PrismaModule, forwardRef(() => TasksModule)],
providers: [MessagesService],
exports: [MessagesService],
})
export class MessagesModule {}
================================================
FILE: packages/bytebot-agent-cc/src/messages/messages.service.ts
================================================
import {
Injectable,
NotFoundException,
Inject,
forwardRef,
} from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { Message, Role, Prisma } from '@prisma/client';
import {
MessageContentBlock,
isComputerToolUseContentBlock,
isToolResultContentBlock,
isUserActionContentBlock,
} from '@bytebot/shared';
import { TasksGateway } from '../tasks/tasks.gateway';
// Extended message type for processing
export interface ProcessedMessage extends Message {
take_over?: boolean;
}
export interface GroupedMessages {
role: Role;
messages: ProcessedMessage[];
take_over?: boolean;
}
@Injectable()
export class MessagesService {
constructor(
private prisma: PrismaService,
@Inject(forwardRef(() => TasksGateway))
private readonly tasksGateway: TasksGateway,
) {}
async create(data: {
content: MessageContentBlock[];
role: Role;
taskId: string;
}): Promise {
const message = await this.prisma.message.create({
data: {
content: data.content as Prisma.InputJsonValue,
role: data.role,
taskId: data.taskId,
},
});
this.tasksGateway.emitNewMessage(data.taskId, message);
return message;
}
async findEvery(taskId: string): Promise {
return this.prisma.message.findMany({
where: {
taskId,
},
orderBy: {
createdAt: 'asc',
},
});
}
async findAll(
taskId: string,
options?: {
limit?: number;
page?: number;
},
): Promise {
const { limit = 10, page = 1 } = options || {};
// Calculate offset based on page and limit
const offset = (page - 1) * limit;
return this.prisma.message.findMany({
where: {
taskId,
},
orderBy: {
createdAt: 'asc',
},
take: limit,
skip: offset,
});
}
async findUnsummarized(taskId: string): Promise {
return this.prisma.message.findMany({
where: {
taskId,
// find messages that don't have a summaryId
summaryId: null,
},
orderBy: { createdAt: 'asc' },
});
}
async attachSummary(
taskId: string,
summaryId: string,
messageIds: string[],
): Promise {
if (messageIds.length === 0) {
return;
}
await this.prisma.message.updateMany({
where: { taskId, id: { in: messageIds } },
data: { summaryId },
});
}
/**
* Groups back-to-back messages from the same role and take_over status
*/
private groupBackToBackMessages(
messages: ProcessedMessage[],
): GroupedMessages[] {
const groupedConversation: GroupedMessages[] = [];
let currentGroup: GroupedMessages | null = null;
for (const message of messages) {
const role = message.role;
const isTakeOver = message.take_over || false;
// If this is the first message, role is different, or take_over status is different from the previous group
if (
!currentGroup ||
currentGroup.role !== role ||
currentGroup.take_over !== isTakeOver
) {
// Save the previous group if it exists
if (currentGroup) {
groupedConversation.push(currentGroup);
}
// Start a new group
currentGroup = {
role: role,
messages: [message],
take_over: isTakeOver,
};
} else {
// Same role and take_over status as previous, merge the content
currentGroup.messages.push(message);
}
}
// Add the last group
if (currentGroup) {
groupedConversation.push(currentGroup);
}
return groupedConversation;
}
/**
* Filters and processes messages, adding take_over flags where appropriate
* Only text messages from the user should appear as user messages
* Computer tool use messages should be shown as assistant messages with take_over flag
*/
private filterMessages(messages: Message[]): ProcessedMessage[] {
const filteredMessages: ProcessedMessage[] = [];
for (const message of messages) {
const processedMessage: ProcessedMessage = { ...message };
const contentBlocks = message.content as MessageContentBlock[];
// If the role is a user message and all the content blocks are tool result blocks or they are take over actions
if (message.role === Role.USER) {
if (contentBlocks.every((block) => isToolResultContentBlock(block))) {
// Pure tool results should be shown as assistant messages
processedMessage.role = Role.ASSISTANT;
} else if (
contentBlocks.every((block) => isUserActionContentBlock(block))
) {
// Extract computer tool use (take over actions) from the user action content blocks and show them as assistant messages with take_over flag
processedMessage.content = contentBlocks
.flatMap((block) => {
return block.content;
})
.filter((block) => isComputerToolUseContentBlock(block));
processedMessage.role = Role.ASSISTANT;
processedMessage.take_over = true;
}
// If there are text blocks mixed with tool blocks, keep as user message
// Only pure text messages from user should remain as user messages
}
filteredMessages.push(processedMessage);
}
return filteredMessages;
}
/**
* Returns raw messages without any processing
*/
async findRawMessages(
taskId: string,
options?: {
limit?: number;
page?: number;
},
): Promise {
return this.findAll(taskId, options);
}
/**
* Returns processed and grouped messages for the chat UI
*/
async findProcessedMessages(
taskId: string,
options?: {
limit?: number;
page?: number;
},
): Promise {
const messages = await this.findAll(taskId, options);
const filteredMessages = this.filterMessages(messages);
return this.groupBackToBackMessages(filteredMessages);
}
}
================================================
FILE: packages/bytebot-agent-cc/src/prisma/prisma.module.ts
================================================
import { Global, Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';
@Global()
@Module({
providers: [PrismaService],
exports: [PrismaService],
})
export class PrismaModule {}
================================================
FILE: packages/bytebot-agent-cc/src/prisma/prisma.service.ts
================================================
import { Injectable, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
constructor() {
super();
}
async onModuleInit() {
await this.$connect();
}
}
================================================
FILE: packages/bytebot-agent-cc/src/tasks/dto/add-task-message.dto.ts
================================================
import { IsNotEmpty, IsString } from 'class-validator';
export class AddTaskMessageDto {
@IsNotEmpty()
@IsString()
message: string;
}
================================================
FILE: packages/bytebot-agent-cc/src/tasks/dto/create-task.dto.ts
================================================
import {
IsArray,
IsDate,
IsNotEmpty,
IsNumber,
IsOptional,
IsString,
ValidateNested,
} from 'class-validator';
import { Type } from 'class-transformer';
import { Role, TaskPriority, TaskType } from '@prisma/client';
export class TaskFileDto {
@IsNotEmpty()
@IsString()
name: string;
@IsNotEmpty()
@IsString()
base64: string;
@IsNotEmpty()
@IsString()
type: string;
@IsNotEmpty()
@IsNumber()
size: number;
}
export class CreateTaskDto {
@IsNotEmpty()
@IsString()
description: string;
@IsOptional()
@IsString()
type?: TaskType;
@IsOptional()
@IsDate()
scheduledFor?: Date;
@IsOptional()
@IsString()
priority?: TaskPriority;
@IsOptional()
@IsString()
createdBy?: Role;
@IsOptional()
model?: any;
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => TaskFileDto)
files?: TaskFileDto[];
}
================================================
FILE: packages/bytebot-agent-cc/src/tasks/dto/update-task.dto.ts
================================================
import { IsEnum, IsOptional } from 'class-validator';
import { TaskPriority, TaskStatus } from '@prisma/client';
export class UpdateTaskDto {
@IsOptional()
@IsEnum(TaskStatus)
status?: TaskStatus;
@IsOptional()
@IsEnum(TaskPriority)
priority?: TaskPriority;
@IsOptional()
queuedAt?: Date;
@IsOptional()
executedAt?: Date;
@IsOptional()
completedAt?: Date;
}
================================================
FILE: packages/bytebot-agent-cc/src/tasks/tasks.controller.ts
================================================
import {
Controller,
Get,
Post,
Body,
Param,
Delete,
HttpStatus,
HttpCode,
Query,
} from '@nestjs/common';
import { TasksService } from './tasks.service';
import { CreateTaskDto } from './dto/create-task.dto';
import { Message, Task } from '@prisma/client';
import { AddTaskMessageDto } from './dto/add-task-message.dto';
import { MessagesService } from '../messages/messages.service';
import { BytebotAgentModel } from 'src/agent/agent.types';
@Controller('tasks')
export class TasksController {
constructor(
private readonly tasksService: TasksService,
private readonly messagesService: MessagesService,
) {}
@Post()
@HttpCode(HttpStatus.CREATED)
async create(@Body() createTaskDto: CreateTaskDto): Promise {
return this.tasksService.create(createTaskDto);
}
@Get()
async findAll(
@Query('page') page?: string,
@Query('limit') limit?: string,
@Query('status') status?: string,
@Query('statuses') statuses?: string,
): Promise<{ tasks: Task[]; total: number; totalPages: number }> {
const pageNum = page ? parseInt(page, 10) : 1;
const limitNum = limit ? parseInt(limit, 10) : 10;
// Handle both single status and multiple statuses
let statusFilter: string[] | undefined;
if (statuses) {
statusFilter = statuses.split(',');
} else if (status) {
statusFilter = [status];
}
return this.tasksService.findAll(pageNum, limitNum, statusFilter);
}
@Get('models')
async getModels() {
return [
{
provider: 'anthropic',
name: 'claude-code',
title: 'Claude Code',
contextWindow: 200000,
},
];
}
@Get(':id')
async findById(@Param('id') id: string): Promise {
return this.tasksService.findById(id);
}
@Get(':id/messages')
async taskMessages(
@Param('id') taskId: string,
@Query('limit') limit?: string,
@Query('page') page?: string,
): Promise {
const options = {
limit: limit ? parseInt(limit, 10) : undefined,
page: page ? parseInt(page, 10) : undefined,
};
const messages = await this.messagesService.findAll(taskId, options);
return messages;
}
@Post(':id/messages')
@HttpCode(HttpStatus.CREATED)
async addTaskMessage(
@Param('id') taskId: string,
@Body() guideTaskDto: AddTaskMessageDto,
): Promise {
return this.tasksService.addTaskMessage(taskId, guideTaskDto);
}
@Get(':id/messages/raw')
async taskRawMessages(
@Param('id') taskId: string,
@Query('limit') limit?: string,
@Query('page') page?: string,
): Promise {
const options = {
limit: limit ? parseInt(limit, 10) : undefined,
page: page ? parseInt(page, 10) : undefined,
};
return this.messagesService.findRawMessages(taskId, options);
}
@Get(':id/messages/processed')
async taskProcessedMessages(
@Param('id') taskId: string,
@Query('limit') limit?: string,
@Query('page') page?: string,
) {
const options = {
limit: limit ? parseInt(limit, 10) : undefined,
page: page ? parseInt(page, 10) : undefined,
};
return this.messagesService.findProcessedMessages(taskId, options);
}
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
async delete(@Param('id') id: string): Promise {
await this.tasksService.delete(id);
}
@Post(':id/takeover')
@HttpCode(HttpStatus.OK)
async takeOver(@Param('id') taskId: string): Promise {
return this.tasksService.takeOver(taskId);
}
@Post(':id/resume')
@HttpCode(HttpStatus.OK)
async resume(@Param('id') taskId: string): Promise {
return this.tasksService.resume(taskId);
}
@Post(':id/cancel')
@HttpCode(HttpStatus.OK)
async cancel(@Param('id') taskId: string): Promise {
return this.tasksService.cancel(taskId);
}
}
================================================
FILE: packages/bytebot-agent-cc/src/tasks/tasks.gateway.ts
================================================
import {
WebSocketGateway,
WebSocketServer,
SubscribeMessage,
OnGatewayConnection,
OnGatewayDisconnect,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
import { Injectable } from '@nestjs/common';
@Injectable()
@WebSocketGateway({
cors: {
origin: '*',
methods: ['GET', 'POST'],
},
})
export class TasksGateway implements OnGatewayConnection, OnGatewayDisconnect {
@WebSocketServer()
server: Server;
handleConnection(client: Socket) {
console.log(`Client connected: ${client.id}`);
}
handleDisconnect(client: Socket) {
console.log(`Client disconnected: ${client.id}`);
}
@SubscribeMessage('join_task')
handleJoinTask(client: Socket, taskId: string) {
client.join(`task_${taskId}`);
console.log(`Client ${client.id} joined task ${taskId}`);
}
@SubscribeMessage('leave_task')
handleLeaveTask(client: Socket, taskId: string) {
client.leave(`task_${taskId}`);
console.log(`Client ${client.id} left task ${taskId}`);
}
emitTaskUpdate(taskId: string, task: any) {
this.server.to(`task_${taskId}`).emit('task_updated', task);
}
emitNewMessage(taskId: string, message: any) {
this.server.to(`task_${taskId}`).emit('new_message', message);
}
emitTaskCreated(task: any) {
this.server.emit('task_created', task);
}
emitTaskDeleted(taskId: string) {
this.server.emit('task_deleted', taskId);
}
}
================================================
FILE: packages/bytebot-agent-cc/src/tasks/tasks.module.ts
================================================
import { Module } from '@nestjs/common';
import { TasksController } from './tasks.controller';
import { TasksService } from './tasks.service';
import { TasksGateway } from './tasks.gateway';
import { PrismaModule } from '../prisma/prisma.module';
import { MessagesModule } from '../messages/messages.module';
@Module({
imports: [PrismaModule, MessagesModule],
controllers: [TasksController],
providers: [TasksService, TasksGateway],
exports: [TasksService, TasksGateway],
})
export class TasksModule {}
================================================
FILE: packages/bytebot-agent-cc/src/tasks/tasks.service.ts
================================================
import {
Injectable,
NotFoundException,
Logger,
BadRequestException,
Inject,
forwardRef,
} from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { CreateTaskDto } from './dto/create-task.dto';
import { UpdateTaskDto } from './dto/update-task.dto';
import {
Task,
Role,
Prisma,
TaskStatus,
TaskType,
TaskPriority,
File,
} from '@prisma/client';
import { AddTaskMessageDto } from './dto/add-task-message.dto';
import { TasksGateway } from './tasks.gateway';
import { ConfigService } from '@nestjs/config';
import { EventEmitter2 } from '@nestjs/event-emitter';
@Injectable()
export class TasksService {
private readonly logger = new Logger(TasksService.name);
constructor(
readonly prisma: PrismaService,
@Inject(forwardRef(() => TasksGateway))
private readonly tasksGateway: TasksGateway,
private readonly configService: ConfigService,
private readonly eventEmitter: EventEmitter2,
) {
this.logger.log('TasksService initialized');
}
async create(createTaskDto: CreateTaskDto): Promise {
this.logger.log(
`Creating new task with description: ${createTaskDto.description}`,
);
const task = await this.prisma.$transaction(async (prisma) => {
// Create the task first
this.logger.debug('Creating task record in database');
const task = await prisma.task.create({
data: {
description: createTaskDto.description,
type: createTaskDto.type || TaskType.IMMEDIATE,
priority: createTaskDto.priority || TaskPriority.MEDIUM,
status: TaskStatus.PENDING,
createdBy: createTaskDto.createdBy || Role.USER,
model: createTaskDto.model,
...(createTaskDto.scheduledFor
? { scheduledFor: createTaskDto.scheduledFor }
: {}),
},
});
this.logger.log(`Task created successfully with ID: ${task.id}`);
let filesDescription = '';
// Save files if provided
if (createTaskDto.files && createTaskDto.files.length > 0) {
this.logger.debug(
`Saving ${createTaskDto.files.length} file(s) for task ID: ${task.id}`,
);
filesDescription += `\n`;
const filePromises = createTaskDto.files.map((file) => {
// Extract base64 data without the data URL prefix
const base64Data = file.base64.includes('base64,')
? file.base64.split('base64,')[1]
: file.base64;
filesDescription += `\nFile ${file.name} written to desktop.`;
return prisma.file.create({
data: {
name: file.name,
type: file.type || 'application/octet-stream',
size: file.size,
data: base64Data,
taskId: task.id,
},
});
});
await Promise.all(filePromises);
this.logger.debug(`Files saved successfully for task ID: ${task.id}`);
}
// Create the initial system message
this.logger.debug(`Creating initial message for task ID: ${task.id}`);
await prisma.message.create({
data: {
content: [
{
type: 'text',
text: `${createTaskDto.description} ${filesDescription}`,
},
] as Prisma.InputJsonValue,
role: Role.USER,
taskId: task.id,
},
});
this.logger.debug(`Initial message created for task ID: ${task.id}`);
return task;
});
this.tasksGateway.emitTaskCreated(task);
return task;
}
async findScheduledTasks(): Promise {
return this.prisma.task.findMany({
where: {
scheduledFor: {
not: null,
},
queuedAt: null,
},
orderBy: [{ scheduledFor: 'asc' }],
});
}
async findNextTask(): Promise<(Task & { files: File[] }) | null> {
const task = await this.prisma.task.findFirst({
where: {
status: {
in: [TaskStatus.RUNNING, TaskStatus.PENDING],
},
},
orderBy: [
{ executedAt: 'asc' },
{ priority: 'desc' },
{ queuedAt: 'asc' },
{ createdAt: 'asc' },
],
include: {
files: true,
},
});
if (task) {
this.logger.log(
`Found existing task with ID: ${task.id}, and status ${task.status}. Resuming.`,
);
}
return task;
}
async findAll(
page = 1,
limit = 10,
statuses?: string[],
): Promise<{ tasks: Task[]; total: number; totalPages: number }> {
this.logger.log(
`Retrieving tasks - page: ${page}, limit: ${limit}, statuses: ${statuses?.join(',')}`,
);
const skip = (page - 1) * limit;
const whereClause: Prisma.TaskWhereInput =
statuses && statuses.length > 0
? { status: { in: statuses as TaskStatus[] } }
: {};
const [tasks, total] = await Promise.all([
this.prisma.task.findMany({
where: whereClause,
orderBy: {
createdAt: 'desc',
},
skip,
take: limit,
}),
this.prisma.task.count({ where: whereClause }),
]);
const totalPages = Math.ceil(total / limit);
this.logger.debug(`Retrieved ${tasks.length} tasks out of ${total} total`);
return { tasks, total, totalPages };
}
async findById(id: string): Promise {
this.logger.log(`Retrieving task by ID: ${id}`);
try {
const task = await this.prisma.task.findUnique({
where: { id },
include: {
files: true,
},
});
if (!task) {
this.logger.warn(`Task with ID: ${id} not found`);
throw new NotFoundException(`Task with ID ${id} not found`);
}
this.logger.debug(`Retrieved task with ID: ${id}`);
return task;
} catch (error: any) {
this.logger.error(`Error retrieving task ID: ${id} - ${error.message}`);
this.logger.error(error.stack);
throw error;
}
}
async update(id: string, updateTaskDto: UpdateTaskDto): Promise {
this.logger.log(`Updating task with ID: ${id}`);
this.logger.debug(`Update data: ${JSON.stringify(updateTaskDto)}`);
const existingTask = await this.findById(id);
if (!existingTask) {
this.logger.warn(`Task with ID: ${id} not found for update`);
throw new NotFoundException(`Task with ID ${id} not found`);
}
let updatedTask = await this.prisma.task.update({
where: { id },
data: updateTaskDto,
});
if (updateTaskDto.status === TaskStatus.COMPLETED) {
this.eventEmitter.emit('task.completed', { taskId: id });
} else if (updateTaskDto.status === TaskStatus.NEEDS_HELP) {
updatedTask = await this.takeOver(id);
} else if (updateTaskDto.status === TaskStatus.FAILED) {
this.eventEmitter.emit('task.failed', { taskId: id });
}
this.logger.log(`Successfully updated task ID: ${id}`);
this.logger.debug(`Updated task: ${JSON.stringify(updatedTask)}`);
this.tasksGateway.emitTaskUpdate(id, updatedTask);
return updatedTask;
}
async delete(id: string): Promise {
this.logger.log(`Deleting task with ID: ${id}`);
const deletedTask = await this.prisma.task.delete({
where: { id },
});
this.logger.log(`Successfully deleted task ID: ${id}`);
this.tasksGateway.emitTaskDeleted(id);
return deletedTask;
}
async addTaskMessage(taskId: string, addTaskMessageDto: AddTaskMessageDto) {
const task = await this.findById(taskId);
if (!task) {
this.logger.warn(`Task with ID: ${taskId} not found for guiding`);
throw new NotFoundException(`Task with ID ${taskId} not found`);
}
const message = await this.prisma.message.create({
data: {
content: [{ type: 'text', text: addTaskMessageDto.message }],
role: Role.USER,
taskId,
},
});
this.tasksGateway.emitNewMessage(taskId, message);
return task;
}
async resume(taskId: string): Promise {
this.logger.log(`Resuming task ID: ${taskId}`);
const task = await this.findById(taskId);
if (!task) {
throw new NotFoundException(`Task with ID ${taskId} not found`);
}
if (task.control !== Role.USER) {
throw new BadRequestException(`Task ${taskId} is not under user control`);
}
const updatedTask = await this.prisma.task.update({
where: { id: taskId },
data: {
control: Role.ASSISTANT,
status: TaskStatus.RUNNING,
},
});
try {
await fetch(
`${this.configService.get('BYTEBOT_DESKTOP_BASE_URL')}/input-tracking/stop`,
{ method: 'POST' },
);
} catch (error) {
this.logger.error('Failed to stop input tracking', error);
}
// Broadcast resume event so AgentProcessor can react
this.eventEmitter.emit('task.resume', { taskId });
this.logger.log(`Task ${taskId} resumed`);
this.tasksGateway.emitTaskUpdate(taskId, updatedTask);
return updatedTask;
}
async takeOver(taskId: string): Promise {
this.logger.log(`Taking over control for task ID: ${taskId}`);
const task = await this.findById(taskId);
if (!task) {
throw new NotFoundException(`Task with ID ${taskId} not found`);
}
if (task.control !== Role.ASSISTANT) {
throw new BadRequestException(
`Task ${taskId} is not under agent control`,
);
}
const updatedTask = await this.prisma.task.update({
where: { id: taskId },
data: {
control: Role.USER,
},
});
try {
await fetch(
`${this.configService.get('BYTEBOT_DESKTOP_BASE_URL')}/input-tracking/start`,
{ method: 'POST' },
);
} catch (error) {
this.logger.error('Failed to start input tracking', error);
}
// Broadcast takeover event so AgentProcessor can react
this.eventEmitter.emit('task.takeover', { taskId });
this.logger.log(`Task ${taskId} takeover initiated`);
this.tasksGateway.emitTaskUpdate(taskId, updatedTask);
return updatedTask;
}
async cancel(taskId: string): Promise {
this.logger.log(`Cancelling task ID: ${taskId}`);
const task = await this.findById(taskId);
if (!task) {
throw new NotFoundException(`Task with ID ${taskId} not found`);
}
if (
task.status === TaskStatus.COMPLETED ||
task.status === TaskStatus.FAILED ||
task.status === TaskStatus.CANCELLED
) {
throw new BadRequestException(
`Task ${taskId} is already completed, failed, or cancelled`,
);
}
const updatedTask = await this.prisma.task.update({
where: { id: taskId },
data: {
status: TaskStatus.CANCELLED,
},
});
// Broadcast cancel event so AgentProcessor can cancel processing
this.eventEmitter.emit('task.cancel', { taskId });
this.logger.log(`Task ${taskId} cancelled and marked as failed`);
this.tasksGateway.emitTaskUpdate(taskId, updatedTask);
return updatedTask;
}
}
================================================
FILE: packages/bytebot-agent-cc/tsconfig.build.json
================================================
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
}
================================================
FILE: packages/bytebot-agent-cc/tsconfig.json
================================================
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "ES2021",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true,
"strictNullChecks": true,
"forceConsistentCasingInFileNames": true,
"noImplicitAny": false,
"strictBindCallApply": false,
"noFallthroughCasesInSwitch": false
}
}
================================================
FILE: packages/bytebot-llm-proxy/Dockerfile
================================================
FROM ghcr.io/berriai/litellm:main-stable
# Add custom config into the image
COPY ./bytebot-llm-proxy/litellm-config.yaml /app/config.yaml
CMD ["--config", "/app/config.yaml", "--port", "4000"]
================================================
FILE: packages/bytebot-llm-proxy/litellm-config.yaml
================================================
model_list:
# Anthropic Models
- model_name: claude-opus-4
litellm_params:
model: anthropic/claude-opus-4-20250514
api_key: os.environ/ANTHROPIC_API_KEY
- model_name: claude-sonnet-4
litellm_params:
model: anthropic/claude-sonnet-4-20250514
api_key: os.environ/ANTHROPIC_API_KEY
# OpenAI Models
- model_name: gpt-4.1
litellm_params:
model: openai/gpt-4.1
api_key: os.environ/OPENAI_API_KEY
- model_name: gpt-4o
litellm_params:
model: openai/gpt-4o
api_key: os.environ/OPENAI_API_KEY
# Gemini Models
- model_name: gemini-2.5-pro
litellm_params:
model: gemini/gemini-2.5-pro
api_key: os.environ/GEMINI_API_KEY
- model_name: gemini-2.5-flash
litellm_params:
model: gemini/gemini-2.5-flash
api_key: os.environ/GEMINI_API_KEY
================================================
FILE: packages/bytebot-ui/.dockerignore
================================================
**/node_modules
**/dist
**/.next
**/.git
**/.vscode
**/.env*
**/npm-debug.log
**/yarn-debug.log
**/yarn-error.log
**/package-lock.json
================================================
FILE: packages/bytebot-ui/.gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
================================================
FILE: packages/bytebot-ui/.prettierrc.json
================================================
{
"plugins": ["prettier-plugin-tailwindcss"]
}
================================================
FILE: packages/bytebot-ui/Dockerfile
================================================
# Base image
FROM node:20-alpine
# Declare build arguments
ARG BYTEBOT_AGENT_BASE_URL
ARG BYTEBOT_DESKTOP_VNC_URL
# Set environment variables for the build process
ENV BYTEBOT_AGENT_BASE_URL=${BYTEBOT_AGENT_BASE_URL}
ENV BYTEBOT_DESKTOP_VNC_URL=${BYTEBOT_DESKTOP_VNC_URL}
# Create app directory
WORKDIR /app
# Copy app source
COPY ./shared ./shared
COPY ./bytebot-ui/ ./bytebot-ui
WORKDIR /app/bytebot-ui
# Install dependencies
RUN npm install
RUN npm run build
# Run the application
CMD ["npm", "run", "start"]
================================================
FILE: packages/bytebot-ui/components.json
================================================
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/app/globals.css",
"baseColor": "stone",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}
================================================
FILE: packages/bytebot-ui/eslint.config.mjs
================================================
import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
});
const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript"),
];
export default eslintConfig;
================================================
FILE: packages/bytebot-ui/next.config.ts
================================================
import type { NextConfig } from "next";
import dotenv from "dotenv";
dotenv.config();
const nextConfig: NextConfig = {
transpilePackages: ["@bytebot/shared"],
};
export default nextConfig;
================================================
FILE: packages/bytebot-ui/package.json
================================================
{
"name": "bytebot-ui",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "npm run build --prefix ../shared && tsx server.ts",
"build": "npm run build --prefix ../shared && next build",
"start": "npm run build --prefix ../shared && tsx server.ts",
"lint": "next lint"
},
"dependencies": {
"@anthropic-ai/sdk": "^0.39.0",
"@bytebot/shared": "../shared",
"@hugeicons/core-free-icons": "^1.0.14",
"@hugeicons/react": "^1.0.5",
"@prisma/client": "^6.5.0",
"@radix-ui/react-dropdown-menu": "^2.1.15",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-popover": "^1.1.11",
"@radix-ui/react-scroll-area": "^1.2.3",
"@radix-ui/react-select": "^2.2.2",
"@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-switch": "^1.1.3",
"@types/express": "^5.0.1",
"@types/http-proxy": "^1.17.16",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"dotenv": "^16.4.7",
"express": "^4.21.2",
"http-proxy": "^1.18.1",
"http-proxy-middleware": "^3.0.5",
"lucide-react": "^0.517.0",
"motion": "^12.12.1",
"next": ">=15.4.7",
"next-themes": "^0.4.6",
"next-transpile-modules": "^10.0.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-markdown": "^10.1.0",
"react-vnc": "^3.1.0",
"socket.io-client": "^4.8.1",
"tailwind-merge": "^3.0.2",
"tsx": "^4.19.3",
"tw-animate-css": "^1.2.4",
"zod": "^3.24.2"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@tailwindcss/postcss": "^4.1.3",
"@types/node": "^20.17.27",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "15.2.3",
"prettier": "^3.5.3",
"prettier-plugin-tailwindcss": "^0.6.11",
"prisma": "^6.5.0",
"tailwindcss": "^4",
"typescript": "^5"
}
}
================================================
FILE: packages/bytebot-ui/postcss.config.mjs
================================================
const config = {
plugins: ["@tailwindcss/postcss"],
};
export default config;
================================================
FILE: packages/bytebot-ui/server.ts
================================================
import express from "express";
import { createProxyMiddleware } from "http-proxy-middleware";
import { createProxyServer } from "http-proxy";
import next from "next";
import { createServer } from "http";
import dotenv from "dotenv";
// Load environment variables
dotenv.config();
const dev = process.env.NODE_ENV !== "production";
const hostname = process.env.HOSTNAME || "localhost";
const port = parseInt(process.env.PORT || "9992", 10);
// Backend URLs
const BYTEBOT_AGENT_BASE_URL = process.env.BYTEBOT_AGENT_BASE_URL;
const BYTEBOT_DESKTOP_VNC_URL = process.env.BYTEBOT_DESKTOP_VNC_URL;
const app = next({ dev, hostname, port });
app
.prepare()
.then(() => {
const handle = app.getRequestHandler();
const nextUpgradeHandler = app.getUpgradeHandler();
const vncProxy = createProxyServer({ changeOrigin: true, ws: true });
const expressApp = express();
const server = createServer(expressApp);
// WebSocket proxy for Socket.IO connections to backend
const tasksProxy = createProxyMiddleware({
target: BYTEBOT_AGENT_BASE_URL,
ws: true,
pathRewrite: { "^/api/proxy/tasks": "/socket.io" },
});
// Apply HTTP proxies
expressApp.use("/api/proxy/tasks", tasksProxy);
expressApp.use("/api/proxy/websockify", (req, res) => {
console.log("Proxying websockify request");
// Rewrite path
const targetUrl = new URL(BYTEBOT_DESKTOP_VNC_URL!);
req.url =
targetUrl.pathname +
(req.url?.replace(/^\/api\/proxy\/websockify/, "") || "");
vncProxy.web(req, res, {
target: `${targetUrl.protocol}//${targetUrl.host}`,
});
});
// Handle all other requests with Next.js
expressApp.all("*", (req, res) => handle(req, res));
// Properly upgrade WebSocket connections
server.on("upgrade", (request, socket, head) => {
const { pathname } = new URL(
request.url!,
`http://${request.headers.host}`,
);
if (pathname.startsWith("/api/proxy/tasks")) {
return tasksProxy.upgrade(request, socket as any, head);
}
if (pathname.startsWith("/api/proxy/websockify")) {
const targetUrl = new URL(BYTEBOT_DESKTOP_VNC_URL!);
request.url =
targetUrl.pathname +
(request.url?.replace(/^\/api\/proxy\/websockify/, "") || "");
console.log("Proxying websockify upgrade request: ", request.url);
return vncProxy.ws(request, socket as any, head, {
target: `${targetUrl.protocol}//${targetUrl.host}`,
});
}
nextUpgradeHandler(request, socket, head);
});
server.listen(port, hostname, () => {
console.log(`> Ready on http://${hostname}:${port}`);
});
})
.catch((err) => {
console.error("Server failed to start:", err);
process.exit(1);
});
================================================
FILE: packages/bytebot-ui/src/app/api/[[...path]]/route.ts
================================================
import { NextRequest } from "next/server";
/* -------------------------------------------------------------------- */
/* generic proxy helper */
/* -------------------------------------------------------------------- */
async function proxy(req: NextRequest, path: string[]): Promise {
const BASE_URL = process.env.BYTEBOT_AGENT_BASE_URL!;
const subPath = path.length ? path.join("/") : "";
const url = `${BASE_URL}/${subPath}${req.nextUrl.search}`;
// Extract cookies from the incoming request
const cookies = req.headers.get("cookie");
const init: RequestInit = {
method: req.method,
headers: {
"Content-Type": "application/json",
...(cookies && { Cookie: cookies }),
},
body:
req.method === "GET" || req.method === "HEAD"
? undefined
: await req.text(),
};
const res = await fetch(url, init);
const body = await res.text();
// Extract Set-Cookie headers from the backend response
const setCookieHeaders = res.headers.getSetCookie?.() || [];
// Create response headers
const responseHeaders = new Headers({
"Content-Type": "application/json",
});
// Add Set-Cookie headers if they exist
setCookieHeaders.forEach((cookie) => {
responseHeaders.append("Set-Cookie", cookie);
});
return new Response(body, {
status: res.status,
headers: responseHeaders,
});
}
/* -------------------------------------------------------------------- */
/* route handlers */
/* -------------------------------------------------------------------- */
type PathParams = Promise<{ path?: string[] }>; // <- Promise is the key
async function handler(req: NextRequest, { params }: { params: PathParams }) {
const { path } = await params;
return proxy(req, path ?? []);
}
export const GET = handler;
export const POST = handler;
export const PUT = handler;
export const PATCH = handler;
export const DELETE = handler;
export const OPTIONS = handler;
export const HEAD = handler;
================================================
FILE: packages/bytebot-ui/src/app/desktop/page.tsx
================================================
"use client";
import React from "react";
import { Header } from "@/components/layout/Header";
import { DesktopContainer } from "@/components/ui/desktop-container";
export default function DesktopPage() {
return (
{/* Main container */}
{/* No action buttons for desktop page */}
);
}
================================================
FILE: packages/bytebot-ui/src/app/globals.css
================================================
@import "tailwindcss";
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
@theme inline {
--color-background: var(--color-bytebot-bronze-light-4);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
/* Colors */
/* Base */
--color-bytebot-white: rgba(255, 255, 255, 1);
--color-bytebot-transparent: rgba(255, 255, 255, 0);
--color-bytebot-black: rgba(0, 0, 0, 1);
/* Bronze light */
--color-bytebot-bronze-light-1: rgba(251, 249, 249, 1);
--color-bytebot-bronze-light-2: rgba(246, 244, 244, 1);
--color-bytebot-bronze-light-3: rgba(241, 239, 238, 1);
--color-bytebot-bronze-light-4: rgba(236, 233, 232, 1);
--color-bytebot-bronze-light-5: rgba(230, 225, 224, 1);
--color-bytebot-bronze-light-6: rgba(224, 218, 217, 1);
--color-bytebot-bronze-light-7: rgba(218, 212, 210, 1);
--color-bytebot-bronze-light-8: rgba(209, 201, 199, 1);
--color-bytebot-bronze-light-9: rgba(152, 141, 139, 1);
--color-bytebot-bronze-light-10: rgba(141, 130, 128, 1);
--color-bytebot-bronze-light-11: rgba(106, 99, 98, 1);
--color-bytebot-bronze-light-12: rgba(45, 42, 42, 1);
--color-bytebot-bronze-light-a1: rgba(71, 0, 0, 0.02500000037252903);
--color-bytebot-bronze-light-a2: rgba(36, 12, 0, 0.04500000178813934);
--color-bytebot-bronze-light-a3: rgba(51, 17, 0, 0.06800000369548798);
--color-bytebot-bronze-light-a4: rgba(51, 13, 0, 0.09000000357627869);
--color-bytebot-bronze-light-a5: rgba(40, 7, 0, 0.11999999731779099);
--color-bytebot-bronze-light-a6: rgba(44, 7, 0, 0.15000000596046448);
--color-bytebot-bronze-light-a7: rgba(40, 8, 0, 0.17389999330043793);
--color-bytebot-bronze-light-a8: rgba(41, 8, 0, 0.2175000011920929);
--color-bytebot-bronze-light-a9: rgba(28, 4, 0, 0.45500001311302185);
--color-bytebot-bronze-light-a10: rgba(26, 4, 0, 0.49799999594688416);
--color-bytebot-bronze-light-a11: rgba(13, 2, 0, 0.6100000143051147);
--color-bytebot-bronze-light-a12: rgba(4, 2, 2, 0.8399999737739563);
/* Bronze dark */
--color-bytebot-bronze-dark-1: rgba(19, 16, 16, 1);
--color-bytebot-bronze-dark-2: rgba(27, 25, 24, 1);
--color-bytebot-bronze-dark-3: rgba(37, 34, 33, 1);
--color-bytebot-bronze-dark-4: rgba(45, 41, 40, 1);
--color-bytebot-bronze-dark-5: rgba(53, 48, 47, 1);
--color-bytebot-bronze-dark-6: rgba(57, 51, 50, 1);
--color-bytebot-bronze-dark-7: rgba(77, 70, 69, 1);
--color-bytebot-bronze-dark-8: rgba(103, 94, 92, 1);
--color-bytebot-bronze-dark-9: rgba(118, 107, 106, 1);
--color-bytebot-bronze-dark-10: rgba(132, 121, 119, 1);
--color-bytebot-bronze-dark-11: rgba(187, 178, 176, 1);
--color-bytebot-bronze-dark-12: rgba(239, 238, 237, 1);
--color-bytebot-bronze-dark-a1: rgba(187, 62, 0, 0.0117647061124444);
--color-bytebot-bronze-dark-a2: rgba(249, 203, 180, 0.04313725605607033);
--color-bytebot-bronze-dark-a3: rgba(249, 214, 202, 0.08627451211214066);
--color-bytebot-bronze-dark-a4: rgba(255, 221, 213, 0.11764705926179886);
--color-bytebot-bronze-dark-a5: rgba(253, 220, 214, 0.15294118225574493);
--color-bytebot-bronze-dark-a6: rgba(252, 222, 217, 0.1679999977350235);
--color-bytebot-bronze-dark-a7: rgba(253, 225, 221, 0.2549019753932953);
--color-bytebot-bronze-dark-a8: rgba(253, 228, 223, 0.364705890417099);
--color-bytebot-bronze-dark-a9: rgba(253, 228, 225, 0.4274509847164154);
--color-bytebot-bronze-dark-a10: rgba(253, 231, 227, 0.48627451062202454);
--color-bytebot-bronze-dark-a11: rgba(254, 241, 238, 0.7176470756530762);
--color-bytebot-bronze-dark-a12: rgba(255, 254, 253, 0.9333333373069763);
/* Red light */
--color-bytebot-red-light-9: rgba(229, 72, 77, 1);
--color-bytebot-red-light-1: rgba(255, 252, 252, 1);
--color-bytebot-red-light-2: rgba(255, 247, 247, 1);
--color-bytebot-red-light-3: rgba(254, 235, 236, 1);
--color-bytebot-red-light-4: rgba(255, 219, 220, 1);
--color-bytebot-red-light-5: rgba(255, 205, 206, 1);
--color-bytebot-red-light-6: rgba(253, 189, 190, 1);
--color-bytebot-red-light-7: rgba(244, 169, 170, 1);
--color-bytebot-red-light-8: rgba(235, 142, 144, 1);
--color-bytebot-red-light-10: rgba(220, 62, 66, 1);
--color-bytebot-red-light-11: rgba(206, 44, 49, 1);
--color-bytebot-red-light-12: rgba(100, 23, 35, 1);
/* Red dark */
--color-bytebot-red-dark-1: rgba(23, 15, 14, 1);
--color-bytebot-red-dark-2: rgba(32, 19, 18, 1);
--color-bytebot-red-dark-3: rgba(59, 18, 18, 1);
--color-bytebot-red-dark-4: rgba(80, 15, 19, 1);
--color-bytebot-red-dark-5: rgba(97, 23, 26, 1);
--color-bytebot-red-dark-6: rgba(115, 36, 37, 1);
--color-bytebot-red-dark-7: rgba(140, 52, 52, 1);
--color-bytebot-red-dark-8: rgba(181, 69, 70, 1);
--color-bytebot-red-dark-9: rgba(229, 72, 77, 1);
--color-bytebot-red-dark-10: rgba(230, 86, 91, 1);
--color-bytebot-red-dark-11: rgba(255, 143, 139, 1);
--color-bytebot-red-dark-12: rgba(255, 210, 206, 1);
/* Green */
--color-bytebot-green-3: rgba(232, 247, 228, 1);
--color-bytebot-green-4: rgba(218, 242, 211, 1);
--color-bytebot-green-5: rgba(200, 234, 190, 1);
--color-bytebot-green-6: rgba(178, 223, 165, 1);
--color-bytebot-green-7: rgba(148, 208, 130, 1);
--color-bytebot-green-8: rgba(103, 188, 77, 1);
--color-bytebot-green-9: rgba(77, 175, 41, 1);
--color-bytebot-green-10: rgba(68, 162, 32, 1);
--color-bytebot-green-11: rgba(43, 128, 0, 1);
--color-bytebot-green-12: rgba(33, 61, 24, 1);
--color-bytebot-green-2: rgba(245, 251, 244, 1);
--color-bytebot-green-1: rgba(251, 254, 250, 1);
--color-bytebot-green-a1: rgba(51, 204, 0, 0.019600000232458115);
--color-bytebot-green-a2: rgba(24, 163, 0, 0.04309999942779541);
--color-bytebot-green-a3: rgba(38, 180, 0, 0.10589999705553055);
--color-bytebot-green-a4: rgba(41, 180, 0, 0.17249999940395355);
--color-bytebot-green-a5: rgba(40, 173, 0, 0.2549000084400177);
--color-bytebot-green-a6: rgba(37, 165, 1, 0.3528999984264374);
--color-bytebot-green-a7: rgba(37, 160, 0, 0.490200012922287);
--color-bytebot-green-a8: rgba(37, 159, 0, 0.6980000138282776);
--color-bytebot-green-a9: rgba(43, 160, 0, 0.8392000198364258);
--color-bytebot-green-a10: rgba(41, 149, 0, 0.8744999766349792);
--color-bytebot-green-a11: rgba(43, 128, 0, 1);
--color-bytebot-green-a12: rgba(10, 41, 0, 0.9059000015258789);
/* letterSpacing */
--letter-spacing-wide: 0.02em;
--letter-spacing-normal: 0em;
--letter-spacing-narrow: -0.009999999776482582em;
--letter-spacing-narrow: -0.01em;
/* Shadow */
--shadow-bytebot: 0px 0px 0px 1.5px #FFF inset;
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.147 0.004 49.25);
--card: oklch(1 0 0);
--card-foreground: oklch(0.147 0.004 49.25);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.147 0.004 49.25);
--primary: oklch(0.216 0.006 56.043);
--primary-foreground: oklch(0.985 0.001 106.423);
--secondary: oklch(0.97 0.001 106.424);
--secondary-foreground: oklch(0.216 0.006 56.043);
--muted: oklch(0.97 0.001 106.424);
--muted-foreground: oklch(0.553 0.013 58.071);
--accent: oklch(0.97 0.001 106.424);
--accent-foreground: oklch(0.216 0.006 56.043);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.923 0.003 48.717);
--input: oklch(0.923 0.003 48.717);
--ring: oklch(0.709 0.01 56.259);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0.001 106.423);
--sidebar-foreground: oklch(0.147 0.004 49.25);
--sidebar-primary: oklch(0.216 0.006 56.043);
--sidebar-primary-foreground: oklch(0.985 0.001 106.423);
--sidebar-accent: oklch(0.97 0.001 106.424);
--sidebar-accent-foreground: oklch(0.216 0.006 56.043);
--sidebar-border: oklch(0.923 0.003 48.717);
--sidebar-ring: oklch(0.709 0.01 56.259);
}
/* .dark {
--background: oklch(0.147 0.004 49.25);
--foreground: oklch(0.985 0.001 106.423);
--card: oklch(0.216 0.006 56.043);
--card-foreground: oklch(0.985 0.001 106.423);
--popover: oklch(0.216 0.006 56.043);
--popover-foreground: oklch(0.985 0.001 106.423);
--primary: oklch(0.923 0.003 48.717);
--primary-foreground: oklch(0.216 0.006 56.043);
--secondary: oklch(0.268 0.007 34.298);
--secondary-foreground: oklch(0.985 0.001 106.423);
--muted: oklch(0.268 0.007 34.298);
--muted-foreground: oklch(0.709 0.01 56.259);
--accent: oklch(0.268 0.007 34.298);
--accent-foreground: oklch(0.985 0.001 106.423);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.553 0.013 58.071);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.216 0.006 56.043);
--sidebar-foreground: oklch(0.985 0.001 106.423);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0.001 106.423);
--sidebar-accent: oklch(0.268 0.007 34.298);
--sidebar-accent-foreground: oklch(0.985 0.001 106.423);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.553 0.013 58.071);
} */
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}
.hide-scrollbar {
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE 10+ */
}
.hide-scrollbar::-webkit-scrollbar {
display: none; /* Chrome, Safari, Opera */
}
================================================
FILE: packages/bytebot-ui/src/app/layout.tsx
================================================
import type React from "react";
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Bytebot",
description: "Bytebot is the container for desktop agents.",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
{children}
);
}
================================================
FILE: packages/bytebot-ui/src/app/page.tsx
================================================
"use client";
import React, { useState, useEffect, useRef } from "react";
import Image from "next/image";
import { Header } from "@/components/layout/Header";
import { ChatInput } from "@/components/messages/ChatInput";
import { useRouter } from "next/navigation";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { startTask } from "@/utils/taskUtils";
import { Model } from "@/types";
import { TaskList } from "@/components/tasks/TaskList";
interface StockPhotoProps {
src: string;
alt?: string;
}
const StockPhoto: React.FC = ({
src,
alt = "Decorative image",
}) => {
return (
);
};
interface FileWithBase64 {
name: string;
base64: string;
type: string;
size: number;
}
export default function Home() {
const [input, setInput] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [models, setModels] = useState([]);
const [selectedModel, setSelectedModel] = useState(null);
const [uploadedFiles, setUploadedFiles] = useState([]);
const router = useRouter();
const [activePopoverIndex, setActivePopoverIndex] = useState(
null,
);
const popoverRef = useRef(null);
const buttonsRef = useRef(null);
useEffect(() => {
fetch("/api/tasks/models")
.then((res) => res.json())
.then((data) => {
setModels(data);
if (data.length > 0) setSelectedModel(data[0]);
})
.catch((err) => console.error("Failed to load models", err));
}, []);
// Close popover when clicking outside or pressing ESC
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
popoverRef.current &&
!popoverRef.current.contains(event.target as Node) &&
buttonsRef.current &&
!buttonsRef.current.contains(event.target as Node)
) {
setActivePopoverIndex(null);
}
};
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") {
setActivePopoverIndex(null);
}
};
if (activePopoverIndex !== null) {
document.addEventListener("mousedown", handleClickOutside);
document.addEventListener("keydown", handleKeyDown);
}
return () => {
document.removeEventListener("mousedown", handleClickOutside);
document.removeEventListener("keydown", handleKeyDown);
};
}, [activePopoverIndex]);
const handleSend = async () => {
if (!input.trim()) return;
setIsLoading(true);
try {
if (!selectedModel) throw new Error("No model selected");
// Send request to start a new task
const taskData: {
description: string;
model: Model;
files?: FileWithBase64[];
} = {
description: input,
model: selectedModel,
};
// Include files if any are uploaded
if (uploadedFiles.length > 0) {
taskData.files = uploadedFiles;
}
const task = await startTask(taskData);
if (task && task.id) {
// Redirect to the task page
router.push(`/tasks/${task.id}`);
} else {
// Handle error
console.error("Failed to create task");
}
} catch (error) {
console.error("Error sending message:", error);
} finally {
setIsLoading(false);
}
};
const handleFileUpload = (files: FileWithBase64[]) => {
setUploadedFiles(files);
};
return (
{/* Desktop grid layout (50/50 split) - only visible on large screens */}
{/* Main content area */}
What can I help you get done?
setSelectedModel(
models.find((m) => m.name === val) || null,
)
}
>
{models.map((m) => (
{m.title}
))}
{/* Stock photo area - centered in its grid cell */}
{/* Mobile layout - only visible on small/medium screens */}
What can I help you get done?
setSelectedModel(
models.find((m) => m.name === val) || null,
)
}
>
{models.map((m) => (
{m.title}
))}
);
}
================================================
FILE: packages/bytebot-ui/src/app/tasks/[id]/page.tsx
================================================
"use client";
import React, { useEffect, useRef } from "react";
import { Header } from "@/components/layout/Header";
import { ChatContainer } from "@/components/messages/ChatContainer";
import { DesktopContainer } from "@/components/ui/desktop-container";
import { useChatSession } from "@/hooks/useChatSession";
import { useScrollScreenshot } from "@/hooks/useScrollScreenshot";
import { useParams, useRouter } from "next/navigation";
import { Role, TaskStatus } from "@/types";
import { HugeiconsIcon } from "@hugeicons/react";
import {
MoreVerticalCircle01Icon,
WavingHand01Icon,
} from "@hugeicons/core-free-icons";
import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
} from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
import { VirtualDesktopStatus } from "@/components/VirtualDesktopStatusHeader";
export default function TaskPage() {
const params = useParams();
const router = useRouter();
const taskId = params.id as string;
const chatContainerRef = useRef(null);
const {
messages,
groupedMessages,
taskStatus,
control,
input,
setInput,
isLoading,
isLoadingSession,
isLoadingMoreMessages,
hasMoreMessages,
loadMoreMessages,
handleAddMessage,
handleTakeOverTask,
handleResumeTask,
handleCancelTask,
currentTaskId,
} = useChatSession({ initialTaskId: taskId });
// Determine if task is inactive (show screenshot) or active (show VNC)
function isTaskInactive(): boolean {
return (
taskStatus === TaskStatus.COMPLETED ||
taskStatus === TaskStatus.FAILED ||
taskStatus === TaskStatus.CANCELLED
);
}
// Determine if user can take control
function canTakeOver(): boolean {
return control === Role.ASSISTANT && taskStatus === TaskStatus.RUNNING;
}
// Determine if user has control or is in takeover mode
function hasUserControl(): boolean {
return (
control === Role.USER &&
(taskStatus === TaskStatus.RUNNING ||
taskStatus === TaskStatus.NEEDS_HELP)
);
}
// Determine if task can be cancelled
function canCancel(): boolean {
return (
taskStatus === TaskStatus.RUNNING || taskStatus === TaskStatus.NEEDS_HELP
);
}
// Determine VNC mode - interactive when user has control, view-only otherwise
function vncViewOnly(): boolean {
return !hasUserControl();
}
// Use scroll screenshot hook for inactive tasks
const { currentScreenshot } = useScrollScreenshot({
messages,
scrollContainerRef: chatContainerRef,
});
// For inactive tasks, auto-load all messages for proper screenshot navigation
useEffect(() => {
if (isTaskInactive() && hasMoreMessages && !isLoadingMoreMessages) {
loadMoreMessages();
}
}, [
isTaskInactive(),
hasMoreMessages,
isLoadingMoreMessages,
loadMoreMessages,
]);
// Map each message ID to its flat index for screenshot scroll logic
const messageIdToIndex = React.useMemo(() => {
const map: Record = {};
messages.forEach((msg, idx) => {
map[msg.id] = idx;
});
return map;
}, [messages]);
// Redirect if task ID doesn't match current task
useEffect(() => {
if (currentTaskId && currentTaskId !== taskId) {
router.push(`/tasks/${currentTaskId}`);
}
}, [currentTaskId, taskId, router]);
return (
{/* Main container */}
{
if (
taskStatus === TaskStatus.RUNNING &&
control === Role.USER
)
return "user_control";
if (taskStatus === TaskStatus.RUNNING) return "running";
if (taskStatus === TaskStatus.NEEDS_HELP)
return "needs_attention";
if (taskStatus === TaskStatus.FAILED) return "failed";
if (taskStatus === TaskStatus.CANCELLED) return "canceled";
if (taskStatus === TaskStatus.COMPLETED) return "completed";
// You may want to add a scheduled state if you have that info
return "pending";
})() as VirtualDesktopStatus
}
>
{canTakeOver() && (
}
>
Take Over
)}
{hasUserControl() && (
Proceed
)}
{canCancel() && (
Cancel
)}
{/* Chat Area */}
{/* Messages scrollable area */}
);
}
================================================
FILE: packages/bytebot-ui/src/app/tasks/page.tsx
================================================
"use client";
import React, { useEffect, useState } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { Header } from "@/components/layout/Header";
import { TaskItem } from "@/components/tasks/TaskItem";
import { TaskTabs, TabKey, TAB_CONFIGS } from "@/components/tasks/TaskTabs";
import { Pagination } from "@/components/ui/pagination";
import { fetchTasks, fetchTaskCounts } from "@/utils/taskUtils";
import { Task } from "@/types";
import { Button } from "@/components/ui/button";
import Link from "next/link";
import { Suspense } from "react";
function TasksPageContent() {
const router = useRouter();
const searchParams = useSearchParams();
const [tasks, setTasks] = useState([]);
const [isLoading, setIsLoading] = useState(true);
// Initialize activeTab from URL params
const getInitialTab = (): TabKey => {
const tabParam = searchParams.get("tab");
if (tabParam && Object.keys(TAB_CONFIGS).includes(tabParam)) {
return tabParam as TabKey;
}
return "ALL";
};
const [activeTab, setActiveTab] = useState(getInitialTab);
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(0);
const [total, setTotal] = useState(0);
const [taskCounts, setTaskCounts] = useState>({
ALL: 0,
ACTIVE: 0,
COMPLETED: 0,
CANCELLED_FAILED: 0,
});
const PAGE_SIZE = 10;
useEffect(() => {
const loadTasks = async () => {
setIsLoading(true);
try {
const statuses =
activeTab === "ALL" ? undefined : TAB_CONFIGS[activeTab].statuses;
const result = await fetchTasks({
page: currentPage,
limit: PAGE_SIZE,
statuses,
});
setTasks(result.tasks);
setTotal(result.total);
setTotalPages(result.totalPages);
} catch (error) {
console.error("Failed to load tasks:", error);
} finally {
setIsLoading(false);
}
};
loadTasks();
}, [currentPage, activeTab]);
useEffect(() => {
const loadTaskCounts = async () => {
try {
const counts = await fetchTaskCounts();
setTaskCounts(counts);
} catch (error) {
console.error("Failed to load task counts:", error);
}
};
loadTaskCounts();
}, []);
// Sync activeTab with URL params when they change
useEffect(() => {
const tabParam = searchParams.get("tab");
const newTab: TabKey =
tabParam && Object.keys(TAB_CONFIGS).includes(tabParam)
? (tabParam as TabKey)
: "ALL";
if (newTab !== activeTab) {
setActiveTab(newTab);
setCurrentPage(1);
}
}, [searchParams, activeTab]);
const handleTabChange = (tab: TabKey) => {
setActiveTab(tab);
setCurrentPage(1);
// Update URL with the new tab
const newSearchParams = new URLSearchParams(searchParams);
if (tab === "ALL") {
newSearchParams.delete("tab");
} else {
newSearchParams.set("tab", tab);
}
const newUrl = `/tasks${newSearchParams.toString() ? `?${newSearchParams.toString()}` : ""}`;
router.push(newUrl, { scroll: false });
};
const handlePageChange = (page: number) => {
setCurrentPage(page);
};
return (
Tasks
{!isLoading && (
)}
{isLoading ? (
) : tasks.length === 0 ? (
No tasks yet
Get started by creating a first task
+ New Task
) : (
<>
{tasks.map((task) => (
))}
{totalPages > 1 && (
)}
>
)}
);
}
function TasksPageFallback() {
return (
);
}
export default function TasksPage() {
return (
}>
);
}
================================================
FILE: packages/bytebot-ui/src/components/VirtualDesktopStatusHeader.tsx
================================================
import React from "react";
import Image from "next/image";
import { cn } from "@/lib/utils";
// Status types based on the image
export type VirtualDesktopStatus =
| "running"
| "needs_attention"
| "failed"
| "canceled"
| "pending"
| "user_control"
| "completed"
| "live_view";
interface StatusConfig {
dot: React.ReactNode;
text: string;
gradient: string;
subtext: string;
}
const statusConfig: Record = {
live_view: {
dot: (
),
text: "Live Desktop View",
gradient: "from-gray-700 to-gray-900",
subtext: "",
},
running: {
dot: (
),
text: "Running",
gradient: "from-green-700 to-green-900",
subtext: "Task in progress",
},
needs_attention: {
dot: (
),
text: "Needs Attention",
gradient: "from-yellow-600 to-orange-700",
subtext: "Task needs attention",
},
failed: {
dot: (
),
text: "Failed",
gradient: "from-red-700 to-red-900",
subtext: "Task failed",
},
canceled: {
dot: (
),
text: "Canceled",
gradient: "from-gray-400 to-gray-600",
subtext: "Task canceled",
},
pending: {
dot: (
),
text: "Pending",
gradient: "from-gray-400 to-gray-600",
subtext: "Task pending",
},
user_control: {
dot: (
),
text: "Running",
gradient: "from-pink-500 to-fuchsia-700",
subtext: "You took control",
},
completed: {
dot: (
),
text: "Completed",
gradient: "from-green-700 to-green-900",
subtext: "Task completed",
},
};
export interface VirtualDesktopStatusHeaderProps {
status: VirtualDesktopStatus;
subtext?: string; // allow override
className?: string;
}
export const VirtualDesktopStatusHeader: React.FC<
VirtualDesktopStatusHeaderProps
> = ({ status, subtext, className }) => {
const config = statusConfig[status];
return (
{config.dot}
{config.text}
{config.subtext && (
{subtext || config.subtext}
)}
);
};
================================================
FILE: packages/bytebot-ui/src/components/layout/Header.tsx
================================================
import React, { useState, useEffect } from "react";
import Link from "next/link";
import Image from "next/image";
import { useTheme } from "next-themes";
import { HugeiconsIcon } from "@hugeicons/react";
import {
DocumentCodeIcon,
TaskDaily01Icon,
Home01Icon,
ComputerIcon,
} from "@hugeicons/core-free-icons";
import { usePathname } from "next/navigation";
export function Header() {
const { resolvedTheme } = useTheme();
const [mounted, setMounted] = useState(false);
const pathname = usePathname();
// After mounting, we can safely show the theme-dependent content
useEffect(() => {
setMounted(true);
}, []);
// Function to determine if a link is active
const isActive = (path: string) => {
if (path === "/") {
return pathname === "/";
}
return pathname?.startsWith(path);
};
// Get classes for navigation links based on active state
const getLinkClasses = (path: string) => {
const baseClasses =
"flex items-center gap-1.5 transition-colors px-3 py-1.5 rounded-lg";
const activeClasses =
"bg-bytebot-bronze-light-a3 text-bytebot-bronze-light-12";
const inactiveClasses =
"text-bytebot-bronze-dark-9 hover:bg-bytebot-bronze-light-a1 hover:text-bytebot-bronze-light-12";
return `${baseClasses} ${isActive(path) ? activeClasses : inactiveClasses}`;
};
return (
);
}
================================================
FILE: packages/bytebot-ui/src/components/messages/AssistantMessage.tsx
================================================
import React from "react";
import { GroupedMessages, TaskStatus } from "@/types";
import { MessageAvatar } from "./MessageAvatar";
import { MessageContent } from "./content/MessageContent";
import { isToolResultContentBlock, isImageContentBlock } from "@bytebot/shared";
import Image from "next/image";
import { cn } from "@/lib/utils";
interface AssistantMessageProps {
group: GroupedMessages;
taskStatus: TaskStatus;
messageIdToIndex: Record;
}
export function AssistantMessage({
group,
taskStatus,
messageIdToIndex,
}: AssistantMessageProps) {
return (
{group.take_over ? (
{group.messages.map((message) => (
{/* Render hidden divs for each screenshot block */}
{message.content.map((block, blockIndex) => {
if (
isToolResultContentBlock(block) &&
block.content &&
block.content.length > 0
) {
// Check ALL content items in the tool result, not just the first one
const markers: React.ReactNode[] = [];
block.content.forEach((contentItem, contentIndex) => {
if (isImageContentBlock(contentItem)) {
markers.push(
);
}
});
return markers;
}
return null;
})}
))}
) : (
{group.messages.map((message) => (
{/* Render hidden divs for each screenshot block */}
{message.content.map((block, blockIndex) => {
if (
isToolResultContentBlock(block) &&
!block.is_error &&
block.content &&
block.content.length > 0
) {
// Check ALL content items in the tool result, not just the first one
const markers: React.ReactNode[] = [];
block.content.forEach((contentItem, contentIndex) => {
if (isImageContentBlock(contentItem)) {
markers.push(
);
}
});
return markers;
}
return null;
})}
))}
)}
);
}
================================================
FILE: packages/bytebot-ui/src/components/messages/ChatContainer.tsx
================================================
import React, { useRef, useEffect, useCallback, Fragment } from "react";
import { Role, TaskStatus, GroupedMessages } from "@/types";
import { MessageGroup } from "./MessageGroup";
import { TextShimmer } from "../ui/text-shimmer";
import { MessageAvatar } from "./MessageAvatar";
import { Loader } from "../ui/loader";
import { ChatInput } from "./ChatInput";
interface ChatContainerProps {
scrollRef?: React.RefObject;
messageIdToIndex: Record;
taskId: string;
input: string;
setInput: (value: string) => void;
isLoading: boolean;
handleAddMessage: () => Promise;
groupedMessages: GroupedMessages[];
taskStatus: TaskStatus;
control: Role;
isLoadingSession: boolean;
isLoadingMoreMessages: boolean;
hasMoreMessages: boolean;
loadMoreMessages: () => Promise;
}
export function ChatContainer({
scrollRef,
messageIdToIndex,
input,
setInput,
isLoading,
handleAddMessage,
groupedMessages,
taskStatus,
control,
isLoadingSession,
isLoadingMoreMessages,
hasMoreMessages,
loadMoreMessages,
}: ChatContainerProps) {
const messagesEndRef = useRef(null);
// Infinite scroll handler
const handleScroll = useCallback(() => {
if (!scrollRef?.current || !loadMoreMessages) {
return;
}
const container = scrollRef.current;
// Check if user scrolled to the bottom (within 20px - much more sensitive)
const { scrollTop, scrollHeight, clientHeight } = container;
const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
if (distanceFromBottom <= 20 && hasMoreMessages && !isLoadingMoreMessages) {
loadMoreMessages();
}
}, [scrollRef, loadMoreMessages, hasMoreMessages, isLoadingMoreMessages]);
// Add scroll event listener
useEffect(() => {
const container = scrollRef?.current;
if (container) {
container.addEventListener("scroll", handleScroll);
return () => container.removeEventListener("scroll", handleScroll);
}
}, [handleScroll, scrollRef]);
// This effect runs whenever the grouped messages array changes
useEffect(() => {
if (
taskStatus === TaskStatus.RUNNING ||
taskStatus === TaskStatus.NEEDS_HELP
) {
scrollToBottom();
}
}, [taskStatus, groupedMessages]);
// Function to scroll to the bottom of the messages
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
};
return (
{isLoadingSession ? (
) : groupedMessages.length > 0 ? (
<>
{/* Content area - scrolling handled by parent */}
{groupedMessages.map((group, groupIndex) => (
))}
{taskStatus === TaskStatus.RUNNING &&
control === Role.ASSISTANT && (
)}
{/* Loading indicator for infinite scroll at bottom */}
{isLoadingMoreMessages && (
)}
{/* This empty div is the target for scrolling */}
{/* Fixed chat input at bottom */}
{[TaskStatus.RUNNING, TaskStatus.NEEDS_HELP].includes(taskStatus) && (
)}
>
) : (
)}
);
}
================================================
FILE: packages/bytebot-ui/src/components/messages/ChatInput.tsx
================================================
import React, { useRef, useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import { HugeiconsIcon } from "@hugeicons/react";
import { ArrowRight02Icon, Attachment01Icon, Cancel01Icon } from "@hugeicons/core-free-icons";
import { cn } from "@/lib/utils";
interface FileWithBase64 {
name: string;
base64: string;
type: string;
size: number;
}
interface ChatInputProps {
input: string;
isLoading: boolean;
onInputChange: (value: string) => void;
onSend: () => void;
onFileUpload?: (files: FileWithBase64[]) => void;
minLines?: number;
placeholder?: string;
}
export function ChatInput({
input,
isLoading,
onInputChange,
onSend,
onFileUpload,
minLines = 1,
placeholder = "Give Bytebot a task to work on...",
}: ChatInputProps) {
const textareaRef = useRef(null);
const fileInputRef = useRef(null);
const [selectedFiles, setSelectedFiles] = useState([]);
const [errorMessage, setErrorMessage] = useState("");
const MAX_FILES = 5;
const MAX_FILE_SIZE = 30 * 1024 * 1024; // 30MB per file in bytes
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
onSend();
};
const handleFileSelect = async (e: React.ChangeEvent) => {
const files = e.target.files;
if (!files || files.length === 0) return;
setErrorMessage("");
// Check max files limit
if (selectedFiles.length + files.length > MAX_FILES) {
setErrorMessage(`Maximum ${MAX_FILES} files allowed`);
e.target.value = '';
return;
}
// Check individual file sizes
const oversizedFiles: string[] = [];
for (let i = 0; i < files.length; i++) {
const file = files[i];
if (file.size > MAX_FILE_SIZE) {
oversizedFiles.push(`${file.name} (${formatFileSize(file.size)})`);
}
}
if (oversizedFiles.length > 0) {
setErrorMessage(`File(s) exceed 30MB limit: ${oversizedFiles.join(', ')}`);
e.target.value = '';
return;
}
const newFiles: FileWithBase64[] = [];
for (let i = 0; i < files.length; i++) {
const file = files[i];
const base64 = await convertToBase64(file);
newFiles.push({
name: file.name,
base64: base64,
type: file.type,
size: file.size,
});
}
const updatedFiles = [...selectedFiles, ...newFiles];
setSelectedFiles(updatedFiles);
if (onFileUpload) {
onFileUpload(updatedFiles);
}
// Reset the input
e.target.value = '';
};
const convertToBase64 = (file: File): Promise => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result as string);
reader.onerror = (error) => reject(error);
});
};
const removeFile = (index: number) => {
const updatedFiles = selectedFiles.filter((_, i) => i !== index);
setSelectedFiles(updatedFiles);
setErrorMessage("");
if (onFileUpload) {
onFileUpload(updatedFiles);
}
};
const triggerFileInput = () => {
fileInputRef.current?.click();
};
const formatFileSize = (bytes: number): string => {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
// Auto-resize textarea based on content
useEffect(() => {
const textarea = textareaRef.current;
if (!textarea) return;
// Reset height to auto to get the correct scrollHeight
textarea.style.height = "auto";
// Calculate minimum height based on minLines
const lineHeight = 24; // approximate line height in pixels
const minHeight = lineHeight * minLines + 12;
// Set height to scrollHeight or minHeight, whichever is larger
const newHeight = Math.max(textarea.scrollHeight, minHeight);
textarea.style.height = `${newHeight}px`;
}, [input, minLines]);
// Determine button position based on minLines
const buttonPositionClass =
minLines > 1 ? "bottom-1.5" : "top-1/2 -translate-y-1/2";
return (
{errorMessage && (
{errorMessage}
)}
{selectedFiles.length > 0 && (
{selectedFiles.length} / {MAX_FILES} files
Max 30MB per file
{selectedFiles.map((file, index) => (
{file.name}
removeFile(index)}
className="ml-1 rounded-sm hover:bg-gray-200"
>
))}
)}
);
}
================================================
FILE: packages/bytebot-ui/src/components/messages/MessageAvatar.tsx
================================================
import React from "react";
import Image from "next/image";
import { HugeiconsIcon } from "@hugeicons/react";
import { User03Icon } from "@hugeicons/core-free-icons";
import { Role } from "@/types";
interface MessageAvatarProps {
role: Role;
}
export function MessageAvatar({ role }: MessageAvatarProps) {
const baseClasses = "flex flex-shrink-0 items-center justify-center rounded-md border border-bytebot-bronze-light-7 bg-bytebot-bronze-light-1 h-[28px] w-[28px]";
if (role === Role.ASSISTANT) {
return (
);
}
return (
);
}
================================================
FILE: packages/bytebot-ui/src/components/messages/MessageGroup.tsx
================================================
import React from "react";
import { Role, TaskStatus } from "@/types";
import { GroupedMessages } from "@/types";
import { AssistantMessage } from "./AssistantMessage";
import { UserMessage } from "./UserMessage";
interface MessageGroupProps {
group: GroupedMessages;
taskStatus: TaskStatus;
messageIdToIndex: Record;
}
export function MessageGroup({ group, taskStatus, messageIdToIndex }: MessageGroupProps) {
if (group.role === Role.ASSISTANT) {
return ;
}
return ;
}
================================================
FILE: packages/bytebot-ui/src/components/messages/UserMessage.tsx
================================================
import React from "react";
import ReactMarkdown from "react-markdown";
import { GroupedMessages } from "@/types";
import { MessageAvatar } from "./MessageAvatar";
import {
isTextContentBlock,
isToolResultContentBlock,
isImageContentBlock,
} from "@bytebot/shared";
interface UserMessageProps {
group: GroupedMessages;
messageIdToIndex: Record;
}
export function UserMessage({ group, messageIdToIndex }: UserMessageProps) {
if (messageIdToIndex[group.messages[0].id] === 0) {
return (
{group.messages.map((message) => (
{/* Render hidden divs for each screenshot block */}
{message.content.map((block, blockIndex) => {
if (
isToolResultContentBlock(block) &&
block.content &&
block.content.length > 0
) {
// Check ALL content items in the tool result, not just the first one
const markers: React.ReactNode[] = [];
block.content.forEach((contentItem, contentIndex) => {
if (isImageContentBlock(contentItem)) {
markers.push(
);
}
});
return markers;
}
return null;
})}
{message.content.map((block, index) => (
{isTextContentBlock(block) && (
{block.text}
)}
))}
))}
);
}
return (
{group.messages.map((message) => (
{/* Render hidden divs for each screenshot block */}
{message.content.map((block, blockIndex) => {
if (
isToolResultContentBlock(block) &&
block.content &&
block.content.length > 0
) {
// Check ALL content items in the tool result, not just the first one
const markers: React.ReactNode[] = [];
block.content.forEach((contentItem, contentIndex) => {
if (isImageContentBlock(contentItem)) {
markers.push(
);
}
});
return markers;
}
return null;
})}
{message.content.map((block, index) => (
{isTextContentBlock(block) && (
{block.text}
)}
))}
))}
);
}
================================================
FILE: packages/bytebot-ui/src/components/messages/content/ComputerToolContent.tsx
================================================
import React from "react";
import { ComputerToolUseContentBlock } from "@bytebot/shared";
import { ComputerToolContentTakeOver } from "./ComputerToolContentTakeOver";
import { ComputerToolContentNormal } from "./ComputerToolContentNormal";
interface ComputerToolContentProps {
block: ComputerToolUseContentBlock;
isTakeOver?: boolean;
}
export function ComputerToolContent({ block, isTakeOver = false }: ComputerToolContentProps) {
if (isTakeOver) {
return ;
}
return ;
}
================================================
FILE: packages/bytebot-ui/src/components/messages/content/ComputerToolContentNormal.tsx
================================================
import React from "react";
import { HugeiconsIcon } from "@hugeicons/react";
import {
ComputerToolUseContentBlock,
isTypeKeysToolUseBlock,
isTypeTextToolUseBlock,
isPressKeysToolUseBlock,
isWaitToolUseBlock,
isScrollToolUseBlock,
isApplicationToolUseBlock,
Application,
isPasteTextToolUseBlock,
isReadFileToolUseBlock,
} from "@bytebot/shared";
import { getIcon, getLabel } from "./ComputerToolUtils";
interface ComputerToolContentNormalProps {
block: ComputerToolUseContentBlock;
}
const applicationMap: Record = {
firefox: "Firefox",
"1password": "1Password",
thunderbird: "Thunderbird",
vscode: "Visual Studio Code",
terminal: "Terminal",
directory: "File Manager",
desktop: "Desktop",
};
function ToolDetailsNormal({ block }: { block: ComputerToolUseContentBlock }) {
const baseClasses =
"px-1 py-0.5 text-[12px] text-bytebot-bronze-light-11 bg-bytebot-red-light-1 border border-bytebot-bronze-light-7 rounded-md";
return (
<>
{isApplicationToolUseBlock(block) && (
{applicationMap[block.input.application as Application]}
)}
{/* Text for type and key actions */}
{(isTypeKeysToolUseBlock(block) || isPressKeysToolUseBlock(block)) && (
{String(block.input.keys.join(" + "))}
)}
{(isTypeTextToolUseBlock(block) || isPasteTextToolUseBlock(block)) && (
{String(
block.input.isSensitive
? "●".repeat(block.input.text.length)
: block.input.text,
)}
)}
{/* Duration for wait actions */}
{isWaitToolUseBlock(block) && (
{`${block.input.duration}ms`}
)}
{/* Coordinates for click/mouse actions */}
{block.input.coordinates && (
{(block.input.coordinates as { x: number; y: number }).x},{" "}
{(block.input.coordinates as { x: number; y: number }).y}
)}
{/* Start and end coordinates for path actions */}
{"path" in block.input &&
Array.isArray(block.input.path) &&
block.input.path.every(
(point) => point.x !== undefined && point.y !== undefined,
) && (
From: {block.input.path[0].x}, {block.input.path[0].y} → To:{" "}
{block.input.path[block.input.path.length - 1].x},{" "}
{block.input.path[block.input.path.length - 1].y}
)}
{/* Scroll information */}
{isScrollToolUseBlock(block) && (
{String(block.input.direction)} {Number(block.input.scrollCount)}
)}
{/* File information */}
{isReadFileToolUseBlock(block) && (
{block.input.path}
)}
>
);
}
export function ComputerToolContentNormal({
block,
}: ComputerToolContentNormalProps) {
// Don't render screenshot tool use blocks here - they're handled separately
if (getLabel(block) === "Screenshot") {
return null;
}
return (
);
}
================================================
FILE: packages/bytebot-ui/src/components/messages/content/ComputerToolContentTakeOver.tsx
================================================
import React from "react";
import { HugeiconsIcon } from "@hugeicons/react";
import {
ComputerToolUseContentBlock,
isTypeKeysToolUseBlock,
isTypeTextToolUseBlock,
isPressKeysToolUseBlock,
isWaitToolUseBlock,
isScrollToolUseBlock,
} from "@bytebot/shared";
import { getIcon, getLabel } from "./ComputerToolUtils";
interface ComputerToolContentTakeOverProps {
block: ComputerToolUseContentBlock;
}
function ToolDetailsTakeOver({ block }: { block: ComputerToolUseContentBlock }) {
const baseClasses = "px-1 py-0.5 text-xs text-fuchsia-600 bg-bytebot-red-light-1 border border-bytebot-bronze-light-7 rounded-md";
return (
<>
{/* Text for type and key actions */}
{(isTypeKeysToolUseBlock(block) || isPressKeysToolUseBlock(block)) && (
{String(block.input.keys.join("+"))}
)}
{isTypeTextToolUseBlock(block) && (
{String(
block.input.isSensitive
? "●".repeat(block.input.text.length)
: block.input.text,
)}
)}
{/* Duration for wait actions */}
{isWaitToolUseBlock(block) && (
{`${block.input.duration}ms`}
)}
{/* Coordinates for click/mouse actions */}
{block.input.coordinates && (
{(block.input.coordinates as { x: number; y: number }).x},
{" "}
{(block.input.coordinates as { x: number; y: number }).y}
)}
{/* Start and end coordinates for path actions */}
{"path" in block.input &&
Array.isArray(block.input.path) &&
block.input.path.every(
(point) => point.x !== undefined && point.y !== undefined,
) && (
From: {block.input.path[0].x}, {block.input.path[0].y} → To:{" "}
{block.input.path[block.input.path.length - 1].x},{" "}
{block.input.path[block.input.path.length - 1].y}
)}
{/* Scroll information */}
{isScrollToolUseBlock(block) && (
{String(block.input.direction)} {Number(block.input.scrollCount)}
)}
>
);
}
export function ComputerToolContentTakeOver({ block }: ComputerToolContentTakeOverProps) {
// Don't render screenshot tool use blocks here - they're handled separately
if (getLabel(block) === "Screenshot") {
return null;
}
return (
);
}
================================================
FILE: packages/bytebot-ui/src/components/messages/content/ComputerToolUtils.tsx
================================================
import {
Camera01Icon,
User03Icon,
Cursor02Icon,
TypeCursorIcon,
MouseRightClick06Icon,
TimeQuarter02Icon,
BrowserIcon,
FilePasteIcon,
FileIcon,
} from "@hugeicons/core-free-icons";
import {
ComputerToolUseContentBlock,
isScreenshotToolUseBlock,
isWaitToolUseBlock,
isTypeKeysToolUseBlock,
isTypeTextToolUseBlock,
isPressKeysToolUseBlock,
isMoveMouseToolUseBlock,
isScrollToolUseBlock,
isCursorPositionToolUseBlock,
isClickMouseToolUseBlock,
isDragMouseToolUseBlock,
isPressMouseToolUseBlock,
isTraceMouseToolUseBlock,
isApplicationToolUseBlock,
isPasteTextToolUseBlock,
isReadFileToolUseBlock,
} from "@bytebot/shared";
// Define the IconType for proper type checking
export type IconType =
| typeof Camera01Icon
| typeof User03Icon
| typeof Cursor02Icon
| typeof TypeCursorIcon
| typeof MouseRightClick06Icon
| typeof TimeQuarter02Icon
| typeof BrowserIcon
| typeof FilePasteIcon
| typeof FileIcon;
export function getIcon(block: ComputerToolUseContentBlock): IconType {
if (isScreenshotToolUseBlock(block)) {
return Camera01Icon;
}
if (isWaitToolUseBlock(block)) {
return TimeQuarter02Icon;
}
if (
isTypeKeysToolUseBlock(block) ||
isTypeTextToolUseBlock(block) ||
isPressKeysToolUseBlock(block)
) {
return TypeCursorIcon;
}
if (isPasteTextToolUseBlock(block)) {
return FilePasteIcon;
}
if (
isMoveMouseToolUseBlock(block) ||
isScrollToolUseBlock(block) ||
isCursorPositionToolUseBlock(block) ||
isClickMouseToolUseBlock(block) ||
isDragMouseToolUseBlock(block) ||
isPressMouseToolUseBlock(block) ||
isTraceMouseToolUseBlock(block)
) {
if (block.input.button === "right") {
return MouseRightClick06Icon;
}
return Cursor02Icon;
}
if (isApplicationToolUseBlock(block)) {
return BrowserIcon;
}
if (isReadFileToolUseBlock(block)) {
return FileIcon;
}
return User03Icon;
}
export function getLabel(block: ComputerToolUseContentBlock) {
if (isScreenshotToolUseBlock(block)) {
return "Screenshot";
}
if (isWaitToolUseBlock(block)) {
return "Wait";
}
if (isTypeKeysToolUseBlock(block)) {
return "Keys";
}
if (isTypeTextToolUseBlock(block)) {
return "Type";
}
if (isPasteTextToolUseBlock(block)) {
return "Paste";
}
if (isPressKeysToolUseBlock(block)) {
return "Press Keys";
}
if (isMoveMouseToolUseBlock(block)) {
return "Move Mouse";
}
if (isScrollToolUseBlock(block)) {
return "Scroll";
}
if (isCursorPositionToolUseBlock(block)) {
return "Cursor Position";
}
if (isClickMouseToolUseBlock(block)) {
const button = block.input.button;
if (button === "left") {
if (block.input.clickCount === 2) {
return "Double Click";
}
if (block.input.clickCount === 3) {
return "Triple Click";
}
return "Click";
}
return `${block.input.button?.charAt(0).toUpperCase() + block.input.button?.slice(1)} Click`;
}
if (isDragMouseToolUseBlock(block)) {
return "Drag";
}
if (isPressMouseToolUseBlock(block)) {
return "Press Mouse";
}
if (isTraceMouseToolUseBlock(block)) {
return "Trace Mouse";
}
if (isApplicationToolUseBlock(block)) {
return "Open Application";
}
if (isReadFileToolUseBlock(block)) {
return "Read File";
}
return "Unknown";
}
================================================
FILE: packages/bytebot-ui/src/components/messages/content/ErrorContent.tsx
================================================
import React from "react";
import { isTextContentBlock, ToolResultContentBlock } from "@bytebot/shared";
import { AlertCircleIcon } from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
interface ErrorContentProps {
block: ToolResultContentBlock;
}
export function ErrorContent({ block }: ErrorContentProps) {
return (
{isTextContentBlock(block.content?.[0])
? block.content?.[0].text
: "Error running tool"}
);
}
================================================
FILE: packages/bytebot-ui/src/components/messages/content/ImageContent.tsx
================================================
import React from "react";
import Image from "next/image";
import { HugeiconsIcon } from "@hugeicons/react";
import { Camera01Icon } from "@hugeicons/core-free-icons";
import { ImageContentBlock } from "@bytebot/shared";
interface ImageContentProps {
block: ImageContentBlock;
}
export function ImageContent({ block }: ImageContentProps) {
// Use a fixed size for the image since width/height are not available on block.source
const width = 250;
const height = 250;
return (
);
}
================================================
FILE: packages/bytebot-ui/src/components/messages/content/MessageContent.tsx
================================================
import React from "react";
import {
MessageContentBlock,
isTextContentBlock,
isImageContentBlock,
isComputerToolUseContentBlock,
isToolResultContentBlock,
} from "@bytebot/shared";
import { TextContent } from "./TextContent";
import { ImageContent } from "./ImageContent";
import { ComputerToolContent } from "./ComputerToolContent";
import { ErrorContent } from "./ErrorContent";
interface MessageContentProps {
content: MessageContentBlock[];
isTakeOver?: boolean;
}
export function MessageContent({
content,
isTakeOver = false,
}: MessageContentProps) {
// Filter content blocks and check if any visible content remains
const visibleBlocks = content.filter((block) => {
// Filter logic from the original code
if (
isToolResultContentBlock(block) &&
block.content &&
block.content.some((contentBlock) => isImageContentBlock(contentBlock))
) {
return true;
}
if (
isToolResultContentBlock(block) &&
block.tool_use_id !== "set_task_status" &&
!block.is_error
) {
return false;
}
return true;
});
// Skip rendering if no visible content
if (visibleBlocks.length === 0) {
return null;
}
return (
{visibleBlocks.map((block, index) => (
{isTextContentBlock(block) && }
{isToolResultContentBlock(block) &&
!block.is_error &&
block.content.map((contentBlock, contentBlockIndex) => {
if (isImageContentBlock(contentBlock)) {
return (
);
}
return null;
})}
{isComputerToolUseContentBlock(block) && (
)}
{isToolResultContentBlock(block) && block.is_error && (
)}
{isToolResultContentBlock(block) &&
!block.is_error &&
block.tool_use_id === "set_task_status" &&
block.content?.[0].type === "text" && (
)}
))}
);
}
================================================
FILE: packages/bytebot-ui/src/components/messages/content/TextContent.tsx
================================================
import React from "react";
import ReactMarkdown from "react-markdown";
import { TextContentBlock } from "@bytebot/shared";
interface TextContentProps {
block: TextContentBlock;
}
export function TextContent({ block }: TextContentProps) {
return (
(
{children}
),
h2: ({ children }) => (
{children}
),
h3: ({ children }) => (
{children}
),
h4: ({ children }) => (
{children}
),
h5: ({ children }) => (
{children}
),
h6: ({ children }) => (
{children}
),
p: ({ children }) => (
{children}
),
ul: ({ children }) => (
),
ol: ({ children }) => (
{children}
),
li: ({ children }) => (
{children}
),
blockquote: ({ children }) => (
{children}
),
code: ({ children, className }) => {
const isInline = !className;
return isInline ? (
{children}
) : (
{children}
);
},
pre: ({ children }) => (
{children}
),
strong: ({ children }) => (
{children}
),
em: ({ children }) => (
{children}
),
a: ({ children, href }) => (
{children}
),
}}
>
{block.text}
);
}
================================================
FILE: packages/bytebot-ui/src/components/screenshot/ScreenshotViewer.tsx
================================================
import React, { useState, useEffect } from 'react';
import Image from 'next/image';
import { ScreenshotData } from '@/utils/screenshotUtils';
interface ScreenshotViewerProps {
screenshot: ScreenshotData | null;
className?: string;
}
export function ScreenshotViewer({ screenshot, className = '' }: ScreenshotViewerProps) {
const [currentScreenshot, setCurrentScreenshot] = useState(screenshot);
useEffect(() => {
if (screenshot?.id !== currentScreenshot?.id) {
setCurrentScreenshot(screenshot);
}
}, [screenshot, currentScreenshot]);
if (!currentScreenshot) {
return (
📷
No screenshots available
Screenshots will appear here when the task has run
);
}
return (
);
}
================================================
FILE: packages/bytebot-ui/src/components/tasks/TaskItem.tsx
================================================
import React from "react";
import { Task, TaskStatus } from "@/types";
import { format } from "date-fns";
import { capitalizeFirstChar } from "@/utils/stringUtils";
import { HugeiconsIcon } from "@hugeicons/react";
import {
Tick02Icon,
CancelCircleIcon,
AlertCircleIcon,
} from "@hugeicons/core-free-icons";
import { Loader } from "@/components/ui/loader";
import Link from "next/link";
interface TaskItemProps {
task: Task;
}
interface StatusIconConfig {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
icon?: any; // HugeIcons IconSvgObject type
color?: string;
useLoader?: boolean;
}
const STATUS_CONFIGS: Record = {
[TaskStatus.COMPLETED]: {
icon: Tick02Icon,
color: "text-bytebot-green-8",
},
[TaskStatus.RUNNING]: {
useLoader: true,
},
[TaskStatus.NEEDS_HELP]: {
icon: AlertCircleIcon,
color: "text-[#FF9D00]",
},
[TaskStatus.PENDING]: {
useLoader: true,
},
[TaskStatus.FAILED]: {
icon: AlertCircleIcon,
color: "text-bytebot-red-light-9",
},
[TaskStatus.NEEDS_REVIEW]: {
icon: AlertCircleIcon,
color: "text-[#FF9D00]",
},
[TaskStatus.CANCELLED]: {
icon: CancelCircleIcon,
color: "text-bytebot-bronze-light-10",
},
};
export const TaskItem: React.FC = ({ task }) => {
// Format date to match the screenshot (e.g., "Today 9:13am" or "April 13, 2025, 12:01pm")
const formatDate = (dateString: string) => {
const date = new Date(dateString);
const today = new Date();
const isToday =
date.getDate() === today.getDate() &&
date.getMonth() === today.getMonth() &&
date.getFullYear() === today.getFullYear();
const formatString = isToday ? `'Today' h:mma` : "MMMM d, yyyy h:mma";
const formatted = format(date, formatString).toLowerCase();
return capitalizeFirstChar(formatted);
};
const StatusIcon = ({ status }: { status: TaskStatus }) => {
const config = STATUS_CONFIGS[status];
if (!config) return null;
const { icon, color, useLoader } = config;
if (useLoader) {
return (
);
}
return (
);
};
return (
{capitalizeFirstChar(task.description)}
{formatDate(task.createdAt)}
);
};
================================================
FILE: packages/bytebot-ui/src/components/tasks/TaskList.tsx
================================================
"use client";
import React, { useEffect, useState, useCallback } from "react";
import { TaskItem } from "@/components/tasks/TaskItem";
import { fetchTasks } from "@/utils/taskUtils";
import { Task } from "@/types";
import { useWebSocket } from "@/hooks/useWebSocket";
interface TaskListProps {
limit?: number;
className?: string;
title?: string;
description?: string;
showHeader?: boolean;
}
export const TaskList: React.FC = ({
limit = 5,
className = "",
title = "Latest Tasks",
description,
showHeader = true,
}) => {
const [tasks, setTasks] = useState([]);
const [isLoading, setIsLoading] = useState(true);
// WebSocket handlers for real-time updates
const handleTaskUpdate = useCallback((updatedTask: Task) => {
setTasks(prev =>
prev.map(task =>
task.id === updatedTask.id ? updatedTask : task
)
);
}, []);
const handleTaskCreated = useCallback((newTask: Task) => {
setTasks(prev => {
const updated = [newTask, ...prev];
return updated.slice(0, limit);
});
}, [limit]);
const handleTaskDeleted = useCallback((taskId: string) => {
setTasks(prev => prev.filter(task => task.id !== taskId));
}, []);
// Initialize WebSocket for task list updates
useWebSocket({
onTaskUpdate: handleTaskUpdate,
onTaskCreated: handleTaskCreated,
onTaskDeleted: handleTaskDeleted,
});
useEffect(() => {
const loadTasks = async () => {
setIsLoading(true);
try {
const result = await fetchTasks({ limit });
setTasks(result.tasks);
} catch (error) {
console.error("Failed to load tasks:", error);
} finally {
setIsLoading(false);
}
};
loadTasks();
}, [limit]);
return (
{showHeader && (
)}
{isLoading ? (
) : tasks.length === 0 ? (
No tasks available
Your completed tasks will appear here
) : (
{tasks.map((task) => (
))}
)}
);
};
================================================
FILE: packages/bytebot-ui/src/components/tasks/TaskTabs.tsx
================================================
import React from "react";
import { TaskStatus } from "@/types";
import { HugeiconsIcon } from "@hugeicons/react";
import {
Tick02Icon,
CursorProgress04Icon,
MultiplicationSignIcon,
ListViewIcon,
} from "@hugeicons/core-free-icons";
type TabKey = "ALL" | "ACTIVE" | "COMPLETED" | "CANCELLED_FAILED";
interface TaskTabsProps {
activeTab: TabKey;
onTabChange: (tab: TabKey) => void;
taskCounts: Record;
}
interface TabConfig {
label: string;
icon:
| typeof Tick02Icon
| typeof CursorProgress04Icon
| typeof MultiplicationSignIcon
| typeof ListViewIcon;
color: string;
statuses: TaskStatus[];
}
const TAB_CONFIGS: Record = {
ALL: {
label: "All",
icon: ListViewIcon,
color: "text-bytebot-bronze-light-10",
statuses: Object.values(TaskStatus),
},
ACTIVE: {
label: "Active",
icon: CursorProgress04Icon,
color: "text-bytebot-bronze-light-10",
statuses: [
TaskStatus.PENDING,
TaskStatus.RUNNING,
TaskStatus.NEEDS_HELP,
TaskStatus.NEEDS_REVIEW,
],
},
COMPLETED: {
label: "Completed",
icon: Tick02Icon,
color: "text-bytebot-bronze-light-10",
statuses: [TaskStatus.COMPLETED],
},
CANCELLED_FAILED: {
label: "Cancelled/Failed",
icon: MultiplicationSignIcon,
color: "text-bytebot-bronze-light-10",
statuses: [TaskStatus.CANCELLED, TaskStatus.FAILED],
},
};
export const TaskTabs: React.FC = ({
activeTab,
onTabChange,
taskCounts,
}) => {
const tabs = Object.entries(TAB_CONFIGS) as [TabKey, TabConfig][];
return (
{tabs.map(([tabKey, config]) => {
const isActive = activeTab === tabKey;
const count = taskCounts[tabKey] || 0;
return (
onTabChange(tabKey)}
className={`flex cursor-pointer items-center space-x-2 border-b-2 px-4 py-3 whitespace-nowrap transition-colors ${
isActive
? "border-bytebot-bronze-dark-7 text-bytebot-bronze-dark-7"
: "text-bytebot-bronze-light-10 hover:text-bytebot-bronze-dark-7 border-transparent"
}`}
>
{config.label}
{count > 0 && (
{count}
)}
);
})}
);
};
// Export the TabKey type and TAB_CONFIGS for use in other components
export type { TabKey };
export { TAB_CONFIGS };
================================================
FILE: packages/bytebot-ui/src/components/ui/TopicPopover.tsx
================================================
"use client";
import React, { useEffect, useRef, ReactElement } from "react";
interface TopicPopoverProps {
children: React.ReactNode;
onOpenChange?: (isOpen: boolean) => void;
isActive?: boolean;
}
export const TopicPopover: React.FC = ({
children,
onOpenChange,
isActive = false,
}) => {
const [isOpen, setIsOpen] = React.useState(false);
const popoverRef = useRef(null);
// Sync with parent's active state
useEffect(() => {
setIsOpen(isActive);
}, [isActive]);
// Close popover when clicking outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (popoverRef.current && !popoverRef.current.contains(event.target as Node)) {
setIsOpen(false);
if (onOpenChange) {
onOpenChange(false);
}
}
};
if (isOpen) {
document.addEventListener("mousedown", handleClickOutside);
}
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [isOpen, onOpenChange]);
const handleToggle = () => {
const newState = !isOpen;
setIsOpen(newState);
if (onOpenChange) {
onOpenChange(newState);
}
};
// Create a modified version of the button with updated text color
const modifiedChildren = React.Children.map(children, (child) => {
// Only process React elements (not strings, numbers, etc.)
if (!React.isValidElement(child)) return child;
// Cast to ReactElement to access props properly
const element = child as ReactElement<{ className?: string }>;
// Get the existing className
const existingClassName = element.props.className || '';
// Replace text-bytebot-bronze-light-11 with text-bytebot-bronze-light-12 when open
const updatedClassName = isOpen
? existingClassName.replace('text-bytebot-bronze-light-11', 'text-bytebot-bronze-light-12')
: existingClassName;
// Clone the element with the updated className
return React.cloneElement(element, {
...element.props,
className: updatedClassName
});
});
return (
);
};
================================================
FILE: packages/bytebot-ui/src/components/ui/button.tsx
================================================
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"cursor-pointer inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
destructive:
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-transparent border-bytebot-bronze-light-7 shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
type ButtonProps = React.ComponentProps<"button"> &
VariantProps & {
asChild?: boolean
icon?: React.ReactNode
iconPosition?: "left" | "right"
}
function Button({
className,
variant,
size,
asChild = false,
icon,
iconPosition = "left",
children,
...props
}: ButtonProps) {
const Comp = asChild ? Slot : "button"
return (
{icon && iconPosition === "left" && (
{icon}
)}
{children}
{icon && iconPosition === "right" && (
{icon}
)}
)
}
export { Button, buttonVariants }
================================================
FILE: packages/bytebot-ui/src/components/ui/card.tsx
================================================
import * as React from "react"
import { cn } from "@/lib/utils"
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
)
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
)
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
)
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
)
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
)
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
)
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
)
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
}
================================================
FILE: packages/bytebot-ui/src/components/ui/copy-button.tsx
================================================
import React, { useState } from 'react';
import { HugeiconsIcon } from '@hugeicons/react';
import { Copy01Icon } from '@hugeicons/core-free-icons';
import { Button } from './button';
import { copyToClipboard } from '@/utils/clipboard';
import { cn } from '@/lib/utils';
interface CopyButtonProps {
text: string;
className?: string;
size?: 'sm' | 'icon';
variant?: 'ghost' | 'outline' | 'secondary';
}
export function CopyButton({
text,
className,
size = 'icon',
variant = 'ghost'
}: CopyButtonProps) {
const [copied, setCopied] = useState(false);
const handleCopy = async (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
const success = await copyToClipboard(text);
if (success) {
setCopied(true);
// Reset the copied state after 2 seconds
setTimeout(() => setCopied(false), 2000);
}
};
return (
{copied ? (
✓
) : (
)}
);
}
================================================
FILE: packages/bytebot-ui/src/components/ui/desktop-container.tsx
================================================
import React, { useRef, useEffect, useState } from "react";
import { VncViewer } from "@/components/vnc/VncViewer";
import { ScreenshotViewer } from "@/components/screenshot/ScreenshotViewer";
import { ScreenshotData } from "@/utils/screenshotUtils";
import {
VirtualDesktopStatusHeader,
VirtualDesktopStatus,
} from "@/components/VirtualDesktopStatusHeader";
interface DesktopContainerProps {
children?: React.ReactNode;
screenshot?: ScreenshotData | null;
viewOnly?: boolean;
className?: string;
status?: VirtualDesktopStatus;
}
export const DesktopContainer: React.FC = ({
children,
screenshot,
viewOnly = false,
className = "",
status = "running",
}) => {
const containerRef = useRef(null);
const [containerSize, setContainerSize] = useState({ width: 0, height: 0 });
const [isMounted, setIsMounted] = useState(false);
// Set isMounted to true after component mounts
useEffect(() => {
setIsMounted(true);
}, []);
// Calculate the container size on mount and window resize
useEffect(() => {
if (!isMounted) return;
const updateSize = () => {
if (!containerRef.current) return;
const parentWidth =
containerRef.current.parentElement?.offsetWidth ||
containerRef.current.offsetWidth;
const parentHeight =
containerRef.current.parentElement?.offsetHeight ||
containerRef.current.offsetHeight;
// Calculate the maximum size while maintaining 1280:960 aspect ratio
let width, height;
const aspectRatio = 1280 / 960;
if (parentWidth / parentHeight > aspectRatio) {
// Width is the limiting factor
height = parentHeight;
width = height * aspectRatio;
} else {
// Height is the limiting factor
width = parentWidth;
height = width / aspectRatio;
}
// Cap at maximum dimensions
width = Math.min(width, 1280);
height = Math.min(height, 960);
setContainerSize({ width, height });
};
updateSize();
window.addEventListener("resize", updateSize);
return () => window.removeEventListener("resize", updateSize);
}, [isMounted]);
return (
{/* Header */}
{/* Status Header */}
{/* Actions */}
{children}
);
};
================================================
FILE: packages/bytebot-ui/src/components/ui/dropdown-menu.tsx
================================================
"use client"
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { HugeiconsIcon } from "@hugeicons/react"
import { Tick01Icon, ArrowRight01Icon } from "@hugeicons/core-free-icons"
import { cn } from "@/lib/utils"
const DropdownMenu = DropdownMenuPrimitive.Root
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
const DropdownMenuGroup = DropdownMenuPrimitive.Group
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
{children}
))
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, sideOffset = 4, ...props }, ref) => (
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, children, checked, ...props }, ref) => (
{children}
))
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, children, ...props }, ref) => (
{children}
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
const DropdownMenuLabel = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes) => {
return (
)
}
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
}
================================================
FILE: packages/bytebot-ui/src/components/ui/input.tsx
================================================
import * as React from "react"
import { cn } from "@/lib/utils"
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
)
}
export { Input }
================================================
FILE: packages/bytebot-ui/src/components/ui/label.tsx
================================================
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)
const Label = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef &
VariantProps
>(({ className, ...props }, ref) => (
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }
================================================
FILE: packages/bytebot-ui/src/components/ui/loader.tsx
================================================
import React from "react";
import Image from "next/image";
import { cn } from "@/lib/utils";
interface LoaderProps {
size?: number;
className?: string;
}
export const Loader: React.FC = ({
size = 16,
className
}) => {
return (
);
};
================================================
FILE: packages/bytebot-ui/src/components/ui/pagination.tsx
================================================
import React from "react";
import { HugeiconsIcon } from "@hugeicons/react";
import { ArrowLeft02Icon, ArrowRight02Icon } from "@hugeicons/core-free-icons";
import { Button } from "./button";
interface PaginationProps {
currentPage: number;
totalPages: number;
onPageChange: (page: number) => void;
total: number;
pageSize: number;
}
export const Pagination: React.FC = ({
currentPage,
totalPages,
onPageChange,
total,
pageSize,
}) => {
if (totalPages <= 1) return null;
const startItem = (currentPage - 1) * pageSize + 1;
const endItem = Math.min(currentPage * pageSize, total);
const getVisiblePages = () => {
const delta = 2;
const range = [];
const rangeWithDots = [];
for (
let i = Math.max(2, currentPage - delta);
i <= Math.min(totalPages - 1, currentPage + delta);
i++
) {
range.push(i);
}
if (currentPage - delta > 2) {
rangeWithDots.push(1, "...");
} else {
rangeWithDots.push(1);
}
rangeWithDots.push(...range);
if (currentPage + delta < totalPages - 1) {
rangeWithDots.push("...", totalPages);
} else {
rangeWithDots.push(totalPages);
}
return rangeWithDots;
};
const visiblePages = getVisiblePages();
return (
Showing {startItem} to {endItem} of {total} results
onPageChange(currentPage - 1)}
disabled={currentPage === 1}
className="flex items-center space-x-1"
>
Previous
{visiblePages.map((page, index) => {
if (page === "...") {
return (
...
);
}
const pageNum = page as number;
const isCurrentPage = pageNum === currentPage;
return (
onPageChange(pageNum)}
className={`min-w-[40px] ${
isCurrentPage
? "bg-bytebot-bronze-dark-7 text-white"
: "text-bytebot-bronze-light-11 hover:text-bytebot-bronze-dark-7"
}`}
>
{pageNum}
);
})}
onPageChange(currentPage + 1)}
disabled={currentPage === totalPages}
className="flex items-center space-x-1"
>
Next
);
};
================================================
FILE: packages/bytebot-ui/src/components/ui/popover.tsx
================================================
"use client"
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import { cn } from "@/lib/utils"
const Popover = PopoverPrimitive.Root
const PopoverTrigger = PopoverPrimitive.Trigger
const PopoverContent = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
))
PopoverContent.displayName = PopoverPrimitive.Content.displayName
export { Popover, PopoverTrigger, PopoverContent }
================================================
FILE: packages/bytebot-ui/src/components/ui/scroll-area.tsx
================================================
"use client"
import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
import { cn } from "@/lib/utils"
function ScrollArea({
className,
children,
...props
}: React.ComponentProps) {
return (
{children}
)
}
function ScrollBar({
className,
orientation = "vertical",
...props
}: React.ComponentProps) {
return (
)
}
export { ScrollArea, ScrollBar }
================================================
FILE: packages/bytebot-ui/src/components/ui/select.tsx
================================================
"use client"
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { HugeiconsIcon } from '@hugeicons/react'
import { ArrowDown01Icon, Tick02Icon } from '@hugeicons/core-free-icons'
import { cn } from "@/lib/utils"
const Select = SelectPrimitive.Root
const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, children, ...props }, ref) => (
{children}
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectContent = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, children, position = "popper", ...props }, ref) => (
{children}
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, children, ...props }, ref) => (
{children}
))
SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
}
================================================
FILE: packages/bytebot-ui/src/components/ui/separator.tsx
================================================
"use client"
import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"
import { cn } from "@/lib/utils"
function Separator({
className,
orientation = "horizontal",
decorative = true,
...props
}: React.ComponentProps) {
return (
)
}
export { Separator }
================================================
FILE: packages/bytebot-ui/src/components/ui/switch.tsx
================================================
"use client"
import * as React from "react"
import * as SwitchPrimitive from "@radix-ui/react-switch"
import { cn } from "@/lib/utils"
function Switch({
className,
...props
}: React.ComponentProps) {
return (
)
}
export { Switch }
================================================
FILE: packages/bytebot-ui/src/components/ui/text-shimmer.tsx
================================================
"use client";
import React, { useMemo, type JSX } from "react";
import { motion } from "motion/react";
import { cn } from "@/lib/utils";
export type TextShimmerProps = {
children: string;
as?: React.ElementType;
className?: string;
duration?: number;
spread?: number;
};
function TextShimmerComponent({
children,
as: Component = "p",
className,
duration = 2,
spread = 2,
}: TextShimmerProps) {
const MotionComponent = motion.create(
Component as keyof JSX.IntrinsicElements,
);
const dynamicSpread = useMemo(() => {
return children.length * spread;
}, [children, spread]);
return (
{children}
);
}
export const TextShimmer = React.memo(TextShimmerComponent);
================================================
FILE: packages/bytebot-ui/src/components/vnc/VncViewer.tsx
================================================
"use client";
import React, { useRef, useEffect, useState } from "react";
interface VncViewerProps {
viewOnly?: boolean;
}
export function VncViewer({ viewOnly = true }: VncViewerProps) {
const containerRef = useRef(null);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const [VncComponent, setVncComponent] = useState(null);
const [wsUrl, setWsUrl] = useState(null);
useEffect(() => {
// Dynamically import the VncScreen component only on the client side
import("react-vnc").then(({ VncScreen }) => {
setVncComponent(() => VncScreen);
});
}, []);
useEffect(() => {
if (typeof window === "undefined") return; // SSR safety‑net
const proto = window.location.protocol === "https:" ? "wss" : "ws";
setWsUrl(`${proto}://${window.location.host}/api/proxy/websockify`);
}, []);
return (
{VncComponent && wsUrl && (
)}
);
}
================================================
FILE: packages/bytebot-ui/src/constants/ui.constants.ts
================================================
/**
* UI constants for consistent styling and configuration
*/
export const UI_CONSTANTS = {
// Animation durations
ANIMATION: {
SPIN_DURATION: '3s',
TRANSITION_DURATION: '150ms',
},
// Common class names
CLASSES: {
LOADING_SPINNER: 'animate-[spin_3s_linear_infinite]',
TRANSITION_DEFAULT: 'transition-colors',
},
// Date formatting
DATE_FORMAT: {
TIME_12H: 'h:mma',
FULL_DATE: 'MMMM d, yyyy, h:mma',
TODAY_PREFIX: "'Today'",
},
// Component defaults
DEFAULTS: {
TASK_LIST_LIMIT: 5,
LOADING_SPINNER_SIZE: 'h-6 w-6',
},
} as const;
export type UIConstants = typeof UI_CONSTANTS;
================================================
FILE: packages/bytebot-ui/src/hooks/useChatSession.ts
================================================
import { useState, useEffect, useRef, useCallback } from "react";
import { Message, Role, TaskStatus, Task, GroupedMessages } from "@/types";
import {
addMessage,
fetchTaskMessages,
fetchTaskProcessedMessages,
fetchTaskById,
takeOverTask,
resumeTask,
cancelTask,
} from "@/utils/taskUtils";
import { MessageContentType } from "@bytebot/shared";
import { useWebSocket } from "./useWebSocket";
interface UseChatSessionProps {
initialTaskId?: string;
}
export function useChatSession({ initialTaskId }: UseChatSessionProps = {}) {
const [taskStatus, setTaskStatus] = useState(TaskStatus.PENDING);
const [control, setControl] = useState(Role.ASSISTANT);
const [messages, setMessages] = useState([]);
const [groupedMessages, setGroupedMessages] = useState([]);
const [input, setInput] = useState("");
const [currentTaskId, setCurrentTaskId] = useState(
initialTaskId || null,
);
const [isLoading, setIsLoading] = useState(false);
const [isLoadingSession, setIsLoadingSession] = useState(true);
const [isLoadingMoreMessages, setIsLoadingMoreMessages] = useState(false);
const [currentPage, setCurrentPage] = useState(1);
const [hasMoreMessages, setHasMoreMessages] = useState(true);
const processedMessageIds = useRef>(new Set());
// WebSocket event handlers
const handleTaskUpdate = useCallback(
(task: Task) => {
if (task.id === currentTaskId) {
setTaskStatus(task.status);
setControl(task.control);
}
},
[currentTaskId],
);
// Function to reload grouped messages
const reloadGroupedMessages = useCallback(async () => {
if (!currentTaskId) return;
try {
const processedMessages = await fetchTaskProcessedMessages(
currentTaskId,
{
limit: 1000, // Get more messages for grouped view
page: 1,
},
);
setGroupedMessages(processedMessages);
} catch (error) {
console.error("Error reloading grouped messages:", error);
}
}, [currentTaskId]);
const handleNewMessage = useCallback(
(message: Message) => {
// Only add message if it's not already processed and belongs to current task
if (
!processedMessageIds.current.has(message.id) &&
message.taskId === currentTaskId
) {
console.log("Adding new message from WebSocket:", message);
processedMessageIds.current.add(message.id);
setMessages((prev) => [...prev, message]);
// Reload grouped messages to reflect the new message
reloadGroupedMessages();
}
},
[currentTaskId, reloadGroupedMessages],
);
const handleTaskCreated = useCallback((task: Task) => {
console.log("New task created:", task);
}, []);
const handleTaskDeleted = useCallback(
(taskId: string) => {
if (taskId === currentTaskId) {
console.log("Current task was deleted");
setCurrentTaskId(null);
setMessages([]);
processedMessageIds.current = new Set();
}
},
[currentTaskId],
);
// Initialize WebSocket connection
const { joinTask, leaveTask } = useWebSocket({
onTaskUpdate: handleTaskUpdate,
onNewMessage: handleNewMessage,
onTaskCreated: handleTaskCreated,
onTaskDeleted: handleTaskDeleted,
});
// Load more messages function for infinite scroll
const loadMoreMessages = useCallback(async () => {
if (!currentTaskId || isLoadingMoreMessages || !hasMoreMessages) {
console.log("loadMoreMessages early return");
return;
}
setIsLoadingMoreMessages(true);
try {
const nextPage = currentPage + 1;
const newMessages = await fetchTaskMessages(currentTaskId, {
limit: 10,
page: nextPage,
});
if (newMessages.length === 0) {
setHasMoreMessages(false);
} else {
// Append new messages to the end of the list (newer messages)
const formattedMessages = newMessages.map((msg: Message) => ({
id: msg.id,
content: msg.content,
role: msg.role,
createdAt: msg.createdAt,
}));
// Filter out any messages we already have
const uniqueMessages = formattedMessages.filter(
(msg) => !processedMessageIds.current.has(msg.id),
);
if (uniqueMessages.length > 0) {
// Add message IDs to processed set
uniqueMessages.forEach((msg: Message) => {
processedMessageIds.current.add(msg.id);
});
setMessages((prev) => [...prev, ...uniqueMessages]);
setCurrentPage(nextPage);
}
// If we got fewer messages than requested, we've reached the end
if (newMessages.length < 10) {
setHasMoreMessages(false);
}
}
} catch (error) {
console.error("Error loading more messages:", error);
} finally {
setIsLoadingMoreMessages(false);
}
}, [currentTaskId, currentPage, isLoadingMoreMessages, hasMoreMessages]);
// Load task ID from URL parameter or fetch the latest task on initial render
useEffect(() => {
const loadSession = async () => {
setIsLoadingSession(true);
try {
if (initialTaskId) {
// If we have an initial task ID (from URL), fetch that specific task
console.log(`Fetching specific task: ${initialTaskId}`);
const task = await fetchTaskById(initialTaskId);
// Load raw messages for compatibility and processed messages for chat UI
const messages = await fetchTaskMessages(initialTaskId, {
limit: 10,
page: 1,
});
const processedMessages = await fetchTaskProcessedMessages(
initialTaskId,
{
limit: 1000, // Get more messages for grouped view
page: 1,
},
);
if (task) {
console.log(`Found task: ${task.id}`);
setCurrentTaskId(task.id);
setTaskStatus(task.status); // Set the task status when loading
setControl(task.control);
// Set grouped messages for chat UI
setGroupedMessages(processedMessages);
// If the task has messages, add them to the messages state for compatibility
if (messages && messages.length > 0) {
// Process all messages
const formattedMessages = messages.map((msg: Message) => ({
id: msg.id,
content: msg.content,
role: msg.role,
createdAt: msg.createdAt,
}));
// Add message IDs to processed set
formattedMessages.forEach((msg: Message) => {
processedMessageIds.current.add(msg.id);
});
setMessages(formattedMessages);
setCurrentPage(1);
// If we got fewer messages than requested, we've reached the end
if (messages.length < 10) {
setHasMoreMessages(false);
} else {
setHasMoreMessages(true);
}
} else {
setCurrentPage(1);
setHasMoreMessages(false);
}
} else {
console.log(`Task with ID ${initialTaskId} not found`);
}
}
} catch (error) {
console.error("Error loading session:", error);
} finally {
setIsLoadingSession(false);
}
};
loadSession();
}, [initialTaskId]);
// Join/leave WebSocket task rooms when task ID changes
useEffect(() => {
if (currentTaskId) {
console.log(`Joining WebSocket room for task: ${currentTaskId}`);
joinTask(currentTaskId);
} else {
console.log("Leaving WebSocket task room");
leaveTask();
}
}, [currentTaskId, joinTask, leaveTask]);
const handleAddMessage = async () => {
if (!input.trim()) return;
setIsLoading(true);
try {
const message = input;
setInput("");
// Send request to start a new task or continue existing task
const response = await addMessage(currentTaskId!, message);
if (!response) {
// Add error message to chat
const errorMessage: Message = {
id: Date.now().toString(),
content: [
{
type: MessageContentType.Text,
text: "Sorry, there was an error processing your request. Please try again.",
},
],
role: Role.ASSISTANT,
};
processedMessageIds.current.add(errorMessage.id);
setMessages((prev) => [...prev, errorMessage]);
}
} finally {
setIsLoading(false);
}
};
const handleTakeOverTask = async () => {
if (!currentTaskId) return;
try {
const updatedTask = await takeOverTask(currentTaskId);
if (updatedTask) {
setControl(updatedTask.control);
}
} catch (error) {
console.error("Error taking over task:", error);
}
};
const handleResumeTask = async () => {
if (!currentTaskId) return;
try {
const updatedTask = await resumeTask(currentTaskId);
if (updatedTask) {
setControl(updatedTask.control);
}
} catch (error) {
console.error("Error resuming task:", error);
}
};
const handleCancelTask = async () => {
if (!currentTaskId) return;
try {
const updatedTask = await cancelTask(currentTaskId);
if (updatedTask) {
setTaskStatus(updatedTask.status);
setControl(updatedTask.control);
}
} catch (error) {
console.error("Error cancelling task:", error);
}
};
return {
messages,
groupedMessages,
taskStatus,
control,
input,
setInput,
currentTaskId,
isLoading,
isLoadingSession,
isLoadingMoreMessages,
hasMoreMessages,
loadMoreMessages,
handleAddMessage,
handleTakeOverTask,
handleResumeTask,
handleCancelTask,
};
}
================================================
FILE: packages/bytebot-ui/src/hooks/useScrollScreenshot.ts
================================================
import { useState, useEffect, useCallback, useRef } from 'react';
import { Message } from '@/types';
import { extractScreenshots, getScreenshotForScrollPosition, ScreenshotData } from '@/utils/screenshotUtils';
interface UseScrollScreenshotProps {
messages: Message[];
scrollContainerRef: React.RefObject;
}
export function useScrollScreenshot({ messages, scrollContainerRef }: UseScrollScreenshotProps) {
const [currentScreenshot, setCurrentScreenshot] = useState(null);
const [allScreenshots, setAllScreenshots] = useState([]);
const lastScrollTime = useRef(0);
// Extract screenshots whenever messages change
useEffect(() => {
const screenshots = extractScreenshots(messages);
setAllScreenshots(screenshots);
// Only set initial screenshot if we don't have one yet
if (screenshots.length > 0 && !currentScreenshot) {
setTimeout(() => {
const initialScreenshot = getScreenshotForScrollPosition(
screenshots,
messages,
scrollContainerRef.current
);
if (initialScreenshot) {
setCurrentScreenshot(initialScreenshot);
} else {
setCurrentScreenshot(screenshots[screenshots.length - 1]);
}
}, 100);
} else if (screenshots.length === 0) {
setCurrentScreenshot(null);
} else if (screenshots.length > 0 && currentScreenshot) {
// When messages update, trigger a re-check
setTimeout(() => {
if (scrollContainerRef.current) {
const event = new Event('scroll');
scrollContainerRef.current.dispatchEvent(event);
}
}, 300);
}
}, [messages, scrollContainerRef]);
// After initial render, force a re-check for screenshot markers using MutationObserver
useEffect(() => {
if (!scrollContainerRef.current) return;
const container = scrollContainerRef.current;
let mutationTimeout: NodeJS.Timeout;
const observer = new MutationObserver(() => {
clearTimeout(mutationTimeout);
mutationTimeout = setTimeout(() => {
const event = new Event('scroll');
container.dispatchEvent(event);
}, 200);
});
observer.observe(container, { childList: true, subtree: true });
return () => {
clearTimeout(mutationTimeout);
observer.disconnect();
};
}, [scrollContainerRef, allScreenshots.length]);
// Handle scroll events to update current screenshot
const handleScroll = useCallback((scrollElement: HTMLElement) => {
if (allScreenshots.length === 0) return;
const now = Date.now();
if (now - lastScrollTime.current < 100) return;
lastScrollTime.current = now;
setTimeout(() => {
if ((Date.now() - now) <= 150 && allScreenshots.length > 0) {
setCurrentScreenshot(prevScreenshot => {
const screenshot = getScreenshotForScrollPosition(allScreenshots, messages, scrollElement);
if (screenshot && screenshot.id !== prevScreenshot?.id) {
return screenshot;
}
return prevScreenshot;
});
}
}, 50);
}, [allScreenshots, messages]);
// Attach scroll listener
useEffect(() => {
const container = scrollContainerRef.current;
if (!container) return;
const scrollHandler = (e: Event) => {
// Only handle scroll events from the actual container
if (e.target === container) {
handleScroll(container);
}
};
// Only attach to the container itself
container.addEventListener('scroll', scrollHandler, { passive: true });
return () => container.removeEventListener('scroll', scrollHandler);
}, [handleScroll, scrollContainerRef]);
return {
currentScreenshot,
allScreenshots,
hasScreenshots: allScreenshots.length > 0,
};
}
================================================
FILE: packages/bytebot-ui/src/hooks/useWebSocket.ts
================================================
import { useEffect, useRef, useCallback } from "react";
import { io, Socket } from "socket.io-client";
import { Message, Task } from "@/types";
interface UseWebSocketProps {
onTaskUpdate?: (task: Task) => void;
onNewMessage?: (message: Message) => void;
onTaskCreated?: (task: Task) => void;
onTaskDeleted?: (taskId: string) => void;
}
export function useWebSocket({
onTaskUpdate,
onNewMessage,
onTaskCreated,
onTaskDeleted,
}: UseWebSocketProps = {}) {
const socketRef = useRef(null);
const currentTaskIdRef = useRef(null);
const connect = useCallback(() => {
if (socketRef.current?.connected) {
return socketRef.current;
}
// Connect to the WebSocket server
const socket = io({
path: "/api/proxy/tasks",
transports: ["websocket"],
autoConnect: true,
reconnection: true,
reconnectionAttempts: 5,
reconnectionDelay: 1000,
});
socket.on("connect", () => {
console.log("Connected to WebSocket server");
});
socket.on("disconnect", () => {
console.log("Disconnected from WebSocket server");
});
socket.on("task_updated", (task: Task) => {
console.log("Task updated:", task);
onTaskUpdate?.(task);
});
socket.on("new_message", (message: Message) => {
console.log("New message:", message);
onNewMessage?.(message);
});
socket.on("task_created", (task: Task) => {
console.log("Task created:", task);
onTaskCreated?.(task);
});
socket.on("task_deleted", (taskId: string) => {
console.log("Task deleted:", taskId);
onTaskDeleted?.(taskId);
});
socketRef.current = socket;
return socket;
}, [onTaskUpdate, onNewMessage, onTaskCreated, onTaskDeleted]);
const joinTask = useCallback(
(taskId: string) => {
const socket = socketRef.current || connect();
if (currentTaskIdRef.current) {
socket.emit("leave_task", currentTaskIdRef.current);
}
socket.emit("join_task", taskId);
currentTaskIdRef.current = taskId;
console.log(`Joined task room: ${taskId}`);
},
[connect],
);
const leaveTask = useCallback(() => {
const socket = socketRef.current;
if (socket && currentTaskIdRef.current) {
socket.emit("leave_task", currentTaskIdRef.current);
console.log(`Left task room: ${currentTaskIdRef.current}`);
currentTaskIdRef.current = null;
}
}, []);
const disconnect = useCallback(() => {
if (socketRef.current) {
socketRef.current.disconnect();
socketRef.current = null;
currentTaskIdRef.current = null;
}
}, []);
// Initialize connection on mount
useEffect(() => {
connect();
return () => {
disconnect();
};
}, [connect, disconnect]);
return {
socket: socketRef.current,
joinTask,
leaveTask,
disconnect,
isConnected: socketRef.current?.connected || false,
};
}
================================================
FILE: packages/bytebot-ui/src/lib/utils.ts
================================================
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
================================================
FILE: packages/bytebot-ui/src/types/index.ts
================================================
import { MessageContentBlock } from "@bytebot/shared";
export enum Role {
USER = "USER",
ASSISTANT = "ASSISTANT",
}
// Message interface
export interface Message {
id: string;
content: MessageContentBlock[];
role: Role;
taskId?: string;
createdAt?: string;
take_over?: boolean;
}
// Grouped messages interface for processed endpoint
export interface GroupedMessages {
role: Role;
messages: Message[];
take_over?: boolean;
}
export interface Model {
provider: string;
name: string;
title: string;
}
// Task related enums and types
export enum TaskStatus {
PENDING = "PENDING",
RUNNING = "RUNNING",
NEEDS_HELP = "NEEDS_HELP",
NEEDS_REVIEW = "NEEDS_REVIEW",
COMPLETED = "COMPLETED",
CANCELLED = "CANCELLED",
FAILED = "FAILED",
}
export enum TaskPriority {
LOW = "LOW",
MEDIUM = "MEDIUM",
HIGH = "HIGH",
URGENT = "URGENT",
}
export enum TaskType {
IMMEDIATE = "IMMEDIATE",
SCHEDULED = "SCHEDULED",
}
export interface FileWithBase64 {
name: string;
base64: string;
type: string;
size: number;
}
export interface File {
id: string;
name: string;
type: string;
size: number;
data: string;
createdAt: string;
updatedAt: string;
taskId: string;
}
export interface Task {
id: string;
description: string;
type: TaskType;
status: TaskStatus;
priority: TaskPriority;
control: Role;
createdBy: Role;
createdAt: string;
updatedAt: string;
scheduledFor?: string;
executedAt?: string;
completedAt?: string;
queuedAt?: string;
error?: string;
result?: unknown;
model: Model;
files?: File[];
}
================================================
FILE: packages/bytebot-ui/src/utils/clipboard.ts
================================================
/**
* Copy text to clipboard using the modern Clipboard API
* @param text The text to copy to clipboard
* @returns Promise true if successful, false otherwise
*/
export async function copyToClipboard(text: string): Promise {
try {
// Check if the Clipboard API is available
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(text);
return true;
} else {
// Fallback for older browsers or non-secure contexts
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
textArea.style.top = '-999999px';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
const successful = document.execCommand('copy');
document.body.removeChild(textArea);
return successful;
}
} catch (error) {
console.error('Failed to copy text to clipboard:', error);
return false;
}
}
================================================
FILE: packages/bytebot-ui/src/utils/screenshotUtils.ts
================================================
import { Message } from "@/types";
import { isToolResultContentBlock, isImageContentBlock } from "@bytebot/shared";
export interface ScreenshotData {
id: string;
base64Data: string;
messageIndex: number;
blockIndex: number;
}
/**
* Extracts all screenshots from messages
*/
export function extractScreenshots(messages: Message[]): ScreenshotData[] {
const screenshots: ScreenshotData[] = [];
messages.forEach((message, messageIndex) => {
message.content.forEach((block, blockIndex) => {
// Check if this is a tool result block with images
if (isToolResultContentBlock(block) && block.content && block.content.length > 0) {
// Check ALL content items in the tool result, not just the first one
block.content.forEach((contentItem, contentIndex) => {
if (isImageContentBlock(contentItem)) {
screenshots.push({
id: `${message.id}-${blockIndex}-${contentIndex}`,
base64Data: contentItem.source.data,
messageIndex,
blockIndex,
});
}
});
}
});
});
return screenshots;
}
/**
* Gets the screenshot that should be displayed based on scroll position
*/
export function getScreenshotForScrollPosition(
screenshots: ScreenshotData[],
messages: Message[],
scrollContainer: HTMLElement | null
): ScreenshotData | null {
if (!scrollContainer || screenshots.length === 0) {
return screenshots[screenshots.length - 1] || null; // Default to last screenshot
}
// Get all screenshot marker elements in the scroll container
const screenshotElements = scrollContainer.querySelectorAll('[data-message-index][data-block-index]');
if (screenshotElements.length === 0) {
return screenshots[screenshots.length - 1] || null;
}
const containerScrollTop = scrollContainer.scrollTop;
const containerHeight = scrollContainer.clientHeight;
// Find the screenshot marker that's most visible at 350px down from the top of the container
const targetViewPosition = 350; // 350px down from top
let bestVisibleMessageIndex = -1; // Start with -1 to detect when no markers are found
let bestVisibleBlockIndex = -1;
let bestVisibility = 0;
let minDistanceFromTarget = Infinity;
let lastMarkerMessageIndex = -1;
let lastMarkerBlockIndex = -1;
screenshotElements.forEach((element) => {
const messageIndex = parseInt((element as HTMLElement).dataset.messageIndex || '0');
const blockIndex = parseInt((element as HTMLElement).dataset.blockIndex || '0');
const elementTop = (element as HTMLElement).offsetTop;
const elementHeight = (element as HTMLElement).offsetHeight;
const elementBottom = elementTop + elementHeight;
// Keep track of the last (bottommost) marker
if (messageIndex > lastMarkerMessageIndex ||
(messageIndex === lastMarkerMessageIndex && blockIndex > lastMarkerBlockIndex)) {
lastMarkerMessageIndex = messageIndex;
lastMarkerBlockIndex = blockIndex;
}
// Distance from top of container (accounting for scroll)
const distanceFromViewportTop = elementTop - containerScrollTop;
const distanceFromViewportBottom = elementBottom - containerScrollTop;
// Check if element is visible in viewport
const isVisible = distanceFromViewportTop < containerHeight &&
distanceFromViewportBottom > 0;
if (isVisible) {
// Calculate how much of this element is visible
const visibleTop = Math.max(0, distanceFromViewportTop);
const visibleBottom = Math.min(containerHeight, distanceFromViewportBottom);
const visibleHeight = Math.max(0, visibleBottom - visibleTop);
const visibility = elementHeight === 0 ? 1 : visibleHeight / elementHeight;
// Calculate distance from our target position (150px down)
const elementCenter = distanceFromViewportTop + (elementHeight / 2);
const distanceFromTarget = Math.abs(elementCenter - targetViewPosition);
// Prefer elements that are closer to our target position and more visible
if (visibility > 0.1 &&
(distanceFromTarget < minDistanceFromTarget ||
(distanceFromTarget === minDistanceFromTarget && visibility > bestVisibility))) {
bestVisibility = visibility;
bestVisibleMessageIndex = messageIndex;
bestVisibleBlockIndex = blockIndex;
minDistanceFromTarget = distanceFromTarget;
}
}
});
// If no markers are visible, check if we've scrolled past all markers
if (bestVisibleMessageIndex === -1 && lastMarkerMessageIndex !== -1) {
// Check if we're scrolled past the last marker
const lastMarker = Array.from(screenshotElements).find(element => {
const msgIdx = parseInt((element as HTMLElement).dataset.messageIndex || '0');
const blockIdx = parseInt((element as HTMLElement).dataset.blockIndex || '0');
return msgIdx === lastMarkerMessageIndex && blockIdx === lastMarkerBlockIndex;
});
if (lastMarker) {
const lastMarkerTop = (lastMarker as HTMLElement).offsetTop;
if (containerScrollTop > lastMarkerTop) {
// We're scrolled past the last marker, use it
bestVisibleMessageIndex = lastMarkerMessageIndex;
bestVisibleBlockIndex = lastMarkerBlockIndex;
}
}
}
// If still no marker found, return null to keep current screenshot
if (bestVisibleMessageIndex === -1) {
return null;
}
// Find the most recent screenshot at or before the best visible marker
let bestScreenshot: ScreenshotData | null = null;
for (const screenshot of screenshots) {
if (
screenshot.messageIndex < bestVisibleMessageIndex ||
(screenshot.messageIndex === bestVisibleMessageIndex && screenshot.blockIndex <= bestVisibleBlockIndex)
) {
bestScreenshot = screenshot;
}
}
return bestScreenshot;
}
================================================
FILE: packages/bytebot-ui/src/utils/stringUtils.ts
================================================
/**
* Capitalizes the first character of a string
* @param str The string to capitalize
* @returns The string with the first character capitalized
*/
export function capitalizeFirstChar(str: string): string {
if (!str || str.length === 0) return str;
return str.charAt(0).toUpperCase() + str.slice(1);
}
================================================
FILE: packages/bytebot-ui/src/utils/taskUtils.ts
================================================
import { Message, Task, Model, GroupedMessages, FileWithBase64, TaskStatus } from "@/types";
/**
* Base configuration for API requests
*/
const API_CONFIG = {
baseUrl: "/api",
headers: {
"Content-Type": "application/json",
},
credentials: "include" as RequestCredentials,
};
/**
* Generic API request handler
*/
async function apiRequest(
endpoint: string,
options: RequestInit = {},
): Promise {
try {
const response = await fetch(`${API_CONFIG.baseUrl}${endpoint}`, {
...options,
headers: {
...API_CONFIG.headers,
...options.headers,
},
credentials: API_CONFIG.credentials,
});
if (!response.ok) {
throw new Error(
`API request failed: ${response.status} ${response.statusText}`,
);
}
return await response.json();
} catch (error) {
console.error(`Error in API request to ${endpoint}:`, error);
return null;
}
}
/**
* Build query string from parameters
*/
function buildQueryString(
params: Record,
): string {
const searchParams = new URLSearchParams();
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
searchParams.append(key, value.toString());
}
});
const queryString = searchParams.toString();
return queryString ? `?${queryString}` : "";
}
/**
* Fetches messages for a specific task
*/
export async function fetchTaskMessages(
taskId: string,
options?: {
limit?: number;
page?: number;
},
): Promise {
const queryString = options ? buildQueryString(options) : "";
const result = await apiRequest(
`/tasks/${taskId}/messages${queryString}`,
{ method: "GET" },
);
return result || [];
}
/**
* Fetches raw messages for a specific task (unprocessed)
*/
export async function fetchTaskRawMessages(
taskId: string,
options?: {
limit?: number;
page?: number;
},
): Promise {
const queryString = options ? buildQueryString(options) : "";
const result = await apiRequest(
`/tasks/${taskId}/messages/raw${queryString}`,
{ method: "GET" },
);
return result || [];
}
/**
* Fetches processed and grouped messages for a specific task (for chat UI)
*/
export async function fetchTaskProcessedMessages(
taskId: string,
options?: {
limit?: number;
page?: number;
},
): Promise {
const queryString = options ? buildQueryString(options) : "";
const result = await apiRequest(
`/tasks/${taskId}/messages/processed${queryString}`,
{ method: "GET" },
);
return result || [];
}
/**
* Fetches a specific task by ID
*/
export async function fetchTaskById(taskId: string): Promise {
return apiRequest(`/tasks/${taskId}`, { method: "GET" });
}
/**
* Sends a message to start a new task
*/
export async function startTask(data: {
description: string;
model: Model;
files?: FileWithBase64[];
}): Promise {
return apiRequest("/tasks", {
method: "POST",
body: JSON.stringify(data),
});
}
/**
* Guides an existing task with a message
*/
export async function addMessage(
taskId: string,
message: string,
): Promise {
return apiRequest(`/tasks/${taskId}/messages`, {
method: "POST",
body: JSON.stringify({ message }),
});
}
/**
* Fetches all tasks with optional pagination and filtering
*/
export async function fetchTasks(options?: {
page?: number;
limit?: number;
status?: string;
statuses?: string[];
}): Promise<{ tasks: Task[]; total: number; totalPages: number }> {
const params: Record = {};
if (options?.page) params.page = options.page;
if (options?.limit) params.limit = options.limit;
if (options?.status) params.status = options.status;
if (options?.statuses && options.statuses.length > 0) {
params.statuses = options.statuses.join(',');
}
const queryString = Object.keys(params).length > 0 ? buildQueryString(params) : "";
const result = await apiRequest<{ tasks: Task[]; total: number; totalPages: number }>(
`/tasks${queryString}`,
{ method: "GET" }
);
return result || { tasks: [], total: 0, totalPages: 0 };
}
/**
* Fetches task counts for grouped tabs
*/
export async function fetchTaskCounts(): Promise> {
try {
const allTasksResult = await fetchTasks();
// Define the status groups
const statusGroups = {
ALL: Object.values(TaskStatus),
ACTIVE: [TaskStatus.PENDING, TaskStatus.RUNNING, TaskStatus.NEEDS_HELP, TaskStatus.NEEDS_REVIEW],
COMPLETED: [TaskStatus.COMPLETED],
CANCELLED_FAILED: [TaskStatus.CANCELLED, TaskStatus.FAILED],
};
const counts: Record = {
ALL: allTasksResult.total,
ACTIVE: 0,
COMPLETED: 0,
CANCELLED_FAILED: 0,
};
// Fetch counts for each group
const groupPromises = Object.entries(statusGroups).map(async ([groupKey, statuses]) => {
if (groupKey === 'ALL') {
return { groupKey, count: allTasksResult.total };
}
const result = await fetchTasks({ statuses, limit: 1 });
return { groupKey, count: result.total };
});
const groupCounts = await Promise.all(groupPromises);
groupCounts.forEach(({ groupKey, count }) => {
counts[groupKey] = count;
});
return counts;
} catch (error) {
console.error("Failed to fetch task counts:", error);
return {
ALL: 0,
ACTIVE: 0,
COMPLETED: 0,
CANCELLED_FAILED: 0,
};
}
}
export async function fetchModels(): Promise {
try {
const response = await fetch("/api/tasks/models", {
method: "GET",
headers: { "Content-Type": "application/json" },
credentials: "include",
});
if (!response.ok) {
throw new Error("Failed to fetch models");
}
return await response.json();
} catch (error) {
console.error("Error fetching models:", error);
return [];
}
}
/**
* Takes over control of a task
*/
export async function takeOverTask(taskId: string): Promise {
return apiRequest(`/tasks/${taskId}/takeover`, { method: "POST" });
}
/**
* Resumes a paused or stopped task
*/
export async function resumeTask(taskId: string): Promise {
return apiRequest(`/tasks/${taskId}/resume`, { method: "POST" });
}
/**
* Cancels a running task
*/
export async function cancelTask(taskId: string): Promise {
return apiRequest(`/tasks/${taskId}/cancel`, { method: "POST" });
}
================================================
FILE: packages/bytebot-ui/tsconfig.json
================================================
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"],
"@bytebot/shared": ["../shared"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
================================================
FILE: packages/bytebotd/.dockerignore
================================================
**/node_modules
**/dist
**/.next
**/.git
**/.vscode
**/.env*
**/npm-debug.log
**/yarn-debug.log
**/yarn-error.log
**/package-lock.json
================================================
FILE: packages/bytebotd/.prettierrc
================================================
{
"singleQuote": true,
"trailingComma": "all"
}
================================================
FILE: packages/bytebotd/Dockerfile
================================================
# -----------------------------------------------------------------------------
# Bytebot Dockerfile - Virtual Desktop Environment
# -----------------------------------------------------------------------------
# Base image
FROM ubuntu:22.04
# -----------------------------------------------------------------------------
# 1. Environment setup
# -----------------------------------------------------------------------------
# Set non-interactive installation
ARG DEBIAN_FRONTEND=noninteractive
# Configure display for X11 applications
ENV DISPLAY=:0
# -----------------------------------------------------------------------------
# 2. System dependencies installation
# -----------------------------------------------------------------------------
RUN apt-get update && apt-get install -y \
# X11 / VNC
xvfb x11vnc xauth x11-xserver-utils \
x11-apps sudo software-properties-common \
# Desktop environment
xfce4 xfce4-goodies dbus wmctrl \
# Display manager with autologin capability
lightdm \
# Development tools
python3 python3-pip curl wget git vim \
# Utilities
supervisor netcat-openbsd \
# Applications
xpdf gedit xpaint \
# Libraries
libxtst-dev \
# Remove unneeded dependencies
&& apt-get remove -y light-locker xfce4-screensaver xfce4-power-manager || true \
# Clean up to reduce image size
&& apt-get clean && rm -rf /var/lib/apt/lists/*
RUN mkdir -p /run/dbus && \
# Generate a machine-id so dbus-daemon doesn't complain
dbus-uuidgen --ensure=/etc/machine-id
# -----------------------------------------------------------------------------
# 3. Additional software installation
# -----------------------------------------------------------------------------
# Install Firefox
RUN apt-get update && apt-get install -y \
# Install necessary for adding PPA
software-properties-common apt-transport-https wget gnupg \
# Install Additional Graphics Libraries
mesa-utils \
libgl1-mesa-dri \
libgl1-mesa-glx \
# Install Sandbox Capabilities
libcap2-bin \
# Install Fonts
fontconfig \
fonts-dejavu \
fonts-liberation \
fonts-freefont-ttf \
&& add-apt-repository -y ppa:mozillateam/ppa \
&& apt-get update \
&& apt-get install -y firefox-esr thunderbird \
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
# Set Firefox as default browser system-wide
&& update-alternatives --install /usr/bin/x-www-browser x-www-browser /usr/bin/firefox-esr 200 \
&& update-alternatives --set x-www-browser /usr/bin/firefox-esr \
&& fc-cache -f -v
# Install 1Password based on architecture
RUN ARCH=$(dpkg --print-architecture) && \
if [ "$ARCH" = "amd64" ]; then \
# Install from APT repository for AMD64
curl -sS https://downloads.1password.com/linux/keys/1password.asc | \
gpg --dearmor --output /usr/share/keyrings/1password-archive-keyring.gpg && \
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/1password-archive-keyring.gpg] https://downloads.1password.com/linux/debian/amd64 stable main" | \
tee /etc/apt/sources.list.d/1password.list && \
mkdir -p /etc/debsig/policies/AC2D62742012EA22/ && \
curl -sS https://downloads.1password.com/linux/debian/debsig/1password.pol | \
tee /etc/debsig/policies/AC2D62742012EA22/1password.pol && \
mkdir -p /usr/share/debsig/keyrings/AC2D62742012EA22 && \
curl -sS https://downloads.1password.com/linux/keys/1password.asc | \
gpg --dearmor --output /usr/share/debsig/keyrings/AC2D62742012EA22/debsig.gpg && \
apt-get update && apt-get install -y 1password && \
apt-get clean && rm -rf /var/lib/apt/lists/*; \
elif [ "$ARCH" = "arm64" ]; then \
# Install from tar.gz for ARM64
apt-get update && apt-get install -y \
libgtk-3-0 libnotify4 libnss3 libxss1 libxtst6 xdg-utils \
libatspi2.0-0 libdrm2 libgbm1 libxcb-dri3-0 libxkbcommon0 \
libsecret-1-0 && \
apt-get clean && rm -rf /var/lib/apt/lists/* && \
curl -sSL https://downloads.1password.com/linux/tar/beta/aarch64/1password-latest.tar.gz -o /tmp/1password.tar.gz && \
# Extract the full 1Password bundle so libraries like libffmpeg.so remain in their expected relative paths
mkdir -p /opt/1password && \
tar -xzf /tmp/1password.tar.gz -C /opt/1password --strip-components=1 && \
# Link the main executable into the global PATH
ln -sf /opt/1password/1password /usr/bin/1password && \
chmod +x /opt/1password/1password && \
# Copy icons to standard locations
mkdir -p /usr/share/pixmaps /usr/share/icons/hicolor/512x512/apps /usr/share/icons/hicolor/256x256/apps && \
find /opt/1password -name "*1password*.png" -o -name "*1password*.svg" | while read icon; do \
if [[ "$icon" == *"512"* ]]; then \
cp "$icon" /usr/share/icons/hicolor/512x512/apps/1password.png 2>/dev/null || true; \
elif [[ "$icon" == *"256"* ]]; then \
cp "$icon" /usr/share/icons/hicolor/256x256/apps/1password.png 2>/dev/null || true; \
fi; \
cp "$icon" /usr/share/pixmaps/1password.png 2>/dev/null || true; \
done && \
# Clean up temporary files
rm -rf /tmp/1password.tar.gz && \
# Update icon cache
gtk-update-icon-cache -f -t /usr/share/icons/hicolor 2>/dev/null || true; \
else \
echo "1Password is not available for $ARCH architecture."; \
fi
# Install Visual Studio Code
RUN ARCH=$(dpkg --print-architecture) && \
if [ "$ARCH" = "amd64" ]; then \
apt-get update && apt-get install -y wget gpg apt-transport-https software-properties-common && \
wget -qO- https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor -o /usr/share/keyrings/ms_vscode.gpg && \
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/ms_vscode.gpg] https://packages.microsoft.com/repos/code stable main" > /etc/apt/sources.list.d/vscode.list && \
apt-get update && apt-get install -y code && \
apt-get clean && rm -rf /var/lib/apt/lists/* ; \
elif [ "$ARCH" = "arm64" ]; then \
apt-get update && apt-get install -y wget gpg && \
wget -qO /tmp/code_arm64.deb https://update.code.visualstudio.com/latest/linux-deb-arm64/stable && \
apt-get install -y /tmp/code_arm64.deb && \
rm -f /tmp/code_arm64.deb && \
apt-get clean && rm -rf /var/lib/apt/lists/* ; \
else \
echo "VSCode is not available for $ARCH architecture."; \
fi
# Install Node.js
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
&& apt-get update \
&& apt-get install -y nodejs \
&& apt-get clean && rm -rf /var/lib/apt/lists/*
# Upgrade pip
RUN pip3 install --upgrade pip
# -----------------------------------------------------------------------------
# 4. VNC and remote access setup
# -----------------------------------------------------------------------------
# Install noVNC and websockify
RUN git clone https://github.com/novnc/noVNC.git /opt/noVNC \
&& git clone https://github.com/novnc/websockify.git /opt/websockify \
&& cd /opt/websockify \
&& pip3 install --break-system-packages .
# -----------------------------------------------------------------------------
# 5. Application setup (bytebotd)
# -----------------------------------------------------------------------------
# Copy package files first to leverage Docker cache
# Install dependencies required to build libnut-core and uiohook-napi
RUN apt-get update && \
apt-get install -y \
cmake \
libx11-dev \
libxtst-dev \
libxinerama-dev \
libxi-dev \
libxt-dev \
libxrandr-dev \
libxkbcommon-dev \
libxkbcommon-x11-dev \
xclip \
git build-essential && \
rm -rf /var/lib/apt/lists/*
COPY ./shared/ /bytebot/shared/
COPY ./bytebotd/ /bytebot/bytebotd/
WORKDIR /bytebot/bytebotd
RUN npm install --build-from-source
RUN npm rebuild uiohook-napi --build-from-source
RUN npm run build
WORKDIR /compile
RUN git clone https://github.com/ZachJW34/libnut-core.git && \
cd libnut-core && \
npm install && \
npm run build:release
# replace /bytebotd/node_modules/@nut-tree-fork/libnut-linux/build/Release/libnut.node with /compile/libnut-core/build/Release/libnut.node
RUN rm -f /bytebot/bytebotd/node_modules/@nut-tree-fork/libnut-linux/build/Release/libnut.node && \
cp /compile/libnut-core/build/Release/libnut.node /bytebot/bytebotd/node_modules/@nut-tree-fork/libnut-linux/build/Release/libnut.node
RUN rm -rf /compile
WORKDIR /bytebot/bytebotd
# -----------------------------------------------------------------------------
# 7. User setup and autologin configuration
# -----------------------------------------------------------------------------
# Create non-root user
RUN useradd -ms /bin/bash user && echo "user ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
RUN mkdir -p /var/run/dbus && \
chmod 755 /var/run/dbus && \
chown user:user /var/run/dbus
RUN mkdir -p /tmp/bytebot-screenshots && \
chown -R user:user /tmp/bytebot-screenshots
# -----------------------------------------------------------------------------
# Copy staged system files and keep sane permissions
# -----------------------------------------------------------------------------
# 1. Ensure everything under /bytebotd/root is owned by root (files + dirs)
# 2. Set *files* to 0644 and *directories* to 0755 so that applications can
# traverse directories (execute bit!) while keeping the contents read-only.
# 3. Copy the tree to /
RUN chown -R root:root /bytebot/bytebotd/root && \
find /bytebot/bytebotd/root -type f -exec chmod 644 {} + && \
find /bytebot/bytebotd/root -type d -exec chmod 755 {} + && \
find /bytebot/bytebotd/root -type f -executable -exec chmod +x {} + && \
cp -a /bytebot/bytebotd/root/. /
RUN chown -R user:user /home/user
RUN chmod -R 755 /home/user
RUN mkdir -p /home/user/Desktop && \
cp -f /usr/share/applications/firefox.desktop /home/user/Desktop/ && \
cp -f /usr/share/applications/thunderbird.desktop /home/user/Desktop/ && \
cp -f /usr/share/applications/1password.desktop /home/user/Desktop/ && \
cp -f /usr/share/applications/code.desktop /home/user/Desktop/ && \
cp -f /usr/share/applications/terminal.desktop /home/user/Desktop/ && \
chmod +x /home/user/Desktop/*.desktop && \
chown user:user /home/user/Desktop/*.desktop
RUN mkdir -p /home/user/.config /home/user/.local/share /home/user/.cache \
&& chown -R user:user /home/user/.config /home/user/.local /home/user/.cache
WORKDIR /home/user
# -----------------------------------------------------------------------------
# 8. Port configuration and runtime
# -----------------------------------------------------------------------------
# - Port 9990: bytebotd and external noVNC websocket
EXPOSE 9990
# Start supervisor to manage all services
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf", "-n"]
================================================
FILE: packages/bytebotd/eslint.config.mjs
================================================
// @ts-check
import eslint from '@eslint/js';
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
import globals from 'globals';
import tseslint from 'typescript-eslint';
export default tseslint.config(
{
ignores: ['eslint.config.mjs'],
},
eslint.configs.recommended,
...tseslint.configs.recommendedTypeChecked,
eslintPluginPrettierRecommended,
{
languageOptions: {
globals: {
...globals.node,
...globals.jest,
},
ecmaVersion: 5,
sourceType: 'module',
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
},
{
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-floating-promises': 'warn',
'@typescript-eslint/no-unsafe-argument': 'warn'
},
},
);
================================================
FILE: packages/bytebotd/nest-cli.json
================================================
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
}
}
================================================
FILE: packages/bytebotd/package.json
================================================
{
"name": "bytebotd",
"version": "0.0.1",
"email": "support@bytebot.ai",
"description": "Bytebot daemon",
"homepage": "https://bytebot.ai",
"author": {
"name": "Bytebot",
"email": "support@bytebot.ai"
},
"private": true,
"license": "UNLICENSED",
"scripts": {
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"build": "npm run build --prefix ../shared && nest build",
"compile": "tsc",
"start": "npm run build --prefix ../shared && nest start",
"start:dev": "npm run build --prefix ../shared && nest start --watch",
"start:debug": "npm run build --prefix ../shared && nest start --debug --watch",
"start:prod": "npm run build --prefix ../shared && node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@bytebot/shared": "file:../shared",
"@nestjs/common": "^11.1.2",
"@nestjs/config": "^4.0.1",
"@nestjs/core": "^11.0.1",
"@nestjs/platform-express": "^11.1.3",
"@nestjs/platform-socket.io": "^11.1.2",
"@nestjs/serve-static": "^5.0.3",
"@nestjs/websockets": "^11.1.2",
"@nut-tree-fork/nut-js": "^4.2.6",
"@rekog/mcp-nest": "^1.6.2",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.2",
"http-proxy-middleware": "^3.0.5",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
"sharp": "^0.34.2",
"socket.io": "^4.8.1",
"uiohook-napi": "^1.5.4"
},
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.18.0",
"@nestjs/cli": "^11.0.0",
"@nestjs/schematics": "^11.0.0",
"@nestjs/testing": "^11.0.1",
"@swc/cli": "^0.6.0",
"@swc/core": "^1.10.7",
"@types/express": "^5.0.0",
"@types/jest": "^29.5.14",
"@types/node": "^22.13.14",
"@types/supertest": "^6.0.2",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-prettier": "^5.2.2",
"globals": "^15.14.0",
"jest": "^29.7.0",
"prettier": "^3.4.2",
"source-map-support": "^0.5.21",
"supertest": "^7.0.0",
"ts-jest": "^29.2.5",
"ts-loader": "^9.5.2",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.7.3",
"typescript-eslint": "^8.20.0"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}
================================================
FILE: packages/bytebotd/root/etc/firefox/policies/policies.json
================================================
{
"policies": {
"FirefoxHome": {
"Search": true,
"TopSites": false,
"SponsoredTopSites": false,
"Highlights": false,
"Pocket": false,
"SponsoredPocket": false,
"Snippets": false,
"Locked": true
},
"SkipTermsOfUse": true,
"OfferToSaveLoginsDefault": false,
"OfferToSaveLogins": false,
"PasswordManagerEnabled": false,
"PDFjs": { "Enabled": true, "EnablePermissions": true },
"Notifications": { "BlockNewRequests": true, "Locked": true },
"UserMessaging": {
"ExtensionRecommendations": false,
"FeatureRecommendations": false,
"UrlbarInterventions": false,
"SkipOnboarding": true,
"MoreFromMozilla": false,
"FirefoxLabs": false
}
}
}
================================================
FILE: packages/bytebotd/root/etc/lightdm/lightdm.conf.d/50-autologin.conf
================================================
[Seat:*]
autologin-user=user
autologin-user-timeout=0
autologin-session=xfce
================================================
FILE: packages/bytebotd/root/etc/supervisor/conf.d/supervisord.conf
================================================
[supervisord]
user=root
nodaemon=true
logfile=/dev/stdout
logfile_maxbytes=0
loglevel=info
[program:set-hostname]
command=bash -c "sudo hostname computer"
autostart=true
autorestart=false
startsecs=0
priority=1
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
redirect_stderr=true
[program:dbus]
command=/usr/bin/dbus-daemon --system --nofork
priority=1
autostart=true
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
redirect_stderr=true
[program:xvfb]
command=Xvfb :0 -screen 0 1280x960x24 -ac -nolisten tcp
user=user
autostart=true
autorestart=true
startsecs=5
priority=10
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
redirect_stderr=true
[program:xfce4]
user=user
command=sh -c 'sleep 5 && \
export XDG_CONFIG_HOME=$HOME/.config && \
export XDG_DATA_HOME=$HOME/.local/share && \
export XDG_CACHE_HOME=$HOME/.cache && \
export XDG_CONFIG_DIRS=/etc/xdg && \
export XDG_DATA_DIRS=/usr/share && \
exec dbus-launch --exit-with-session startxfce4'
environment=DISPLAY=":0",HOME="/home/user"
autostart=true
autorestart=true
startsecs=5
priority=20
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
redirect_stderr=true
depends_on=xvfb
[program:x11vnc]
command=x11vnc -display :0 -N -forever -shared -rfbport 5900
user=user
autostart=true
autorestart=true
startsecs=5
priority=30
environment=DISPLAY=":0"
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
redirect_stderr=true
depends_on=xfce4
[program:websockify]
command=websockify 6080 localhost:5900
autostart=true
autorestart=true
startsecs=5
priority=40
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
redirect_stderr=true
depends_on=x11vnc
[program:bytebotd]
user=user
command=node /bytebot/bytebotd/dist/main.js
directory=/bytebot/bytebotd
autostart=true
autorestart=true
startsecs=5
priority=60
environment=DISPLAY=":0"
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
redirect_stderr=true
depends_on=websockify
[eventlistener:startup]
command=echo "All services started successfully"
events=PROCESS_STATE_RUNNING
buffer_size=100
================================================
FILE: packages/bytebotd/root/etc/thunderbird/policies/policies.json
================================================
{
"policies": {
"UserMessaging": {
"ExtensionRecommendations": false,
"FeatureRecommendations": false,
"UrlbarInterventions": false,
"SkipOnboarding": true,
"MoreFromMozilla": false
},
"OverrideFirstRunPage": "",
"OverridePostUpdatePage": "",
"InAppNotification": {
"DonationEnabled": false,
"SurveyEnabled": false,
"MessageEnabled": false
}
}
}
================================================
FILE: packages/bytebotd/root/home/user/.config/xfce4/desktop/icons.screen0-1264x913.rc
================================================
[xfdesktop-version-4.10.3+-rcfile_format]
4.10.3+=true
[/home/user/Desktop/firefox.desktop]
row=0
col=0
[/home/user/Desktop/thunderbird.desktop]
row=1
col=0
[/home/user/Desktop/1password.desktop]
row=2
col=0
[/home/user/Desktop/code.desktop]
row=3
col=0
[/home/user/Desktop/terminal.desktop]
row=4
col=0
[/home/user]
row=5
col=0
[Trash]
row=6
col=0
================================================
FILE: packages/bytebotd/root/home/user/.config/xfce4/helpers.rc
================================================
TerminalEmulator=xfce4-terminal
WebBrowser=firefox-esr
FileManager=thunar
MailReader=thunderbird
================================================
FILE: packages/bytebotd/root/home/user/.config/xfce4/terminal/accels.scm
================================================
; xfce4-terminal GtkAccelMap rc-file -*- scheme -*-
; this file is an automated accelerator map dump
;
(gtk_accel_path "/terminal-window/goto-tab-2" "2")
(gtk_accel_path "/terminal-window/goto-tab-6" "6")
; (gtk_accel_path "/terminal-window/copy-input" "")
; (gtk_accel_path "/terminal-window/close-other-tabs" "")
; (gtk_accel_path "/terminal-window/move-tab-right" "Page_Down")
(gtk_accel_path "/terminal-window/goto-tab-7" "7")
; (gtk_accel_path "/terminal-window/set-title-color" "")
; (gtk_accel_path "/terminal-window/edit-menu" "")
; (gtk_accel_path "/terminal-window/zoom-menu" "")
(gtk_accel_path "/terminal-window/goto-tab-1" "1")
; (gtk_accel_path "/terminal-window/fullscreen" "F11")
; (gtk_accel_path "/terminal-window/read-only" "")
(gtk_accel_path "/terminal-window/goto-tab-5" "5")
; (gtk_accel_path "/terminal-window/preferences" "")
; (gtk_accel_path "