Compare commits

..

4 Commits

Author SHA1 Message Date
Zamil Majdy
2fdc311293 Test check 2026-01-23 10:37:37 -05:00
Ubbe
7892590b12 feat(frontend): refine copilot loading states (#11827)
## Changes 🏗️

- Make the loading UX better when switching between chats or loading a
new chat
- Make session/chat management logic more manageable
- Improving "Deep thinking" loading states
- Fix bug that happened when returning to chat after navigating away

## Checklist 📋

### For code changes:
- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
  - [x] Run the app locally and test the above
2026-01-23 18:25:45 +07:00
Bently
82d7134fc6 feat(blocks): Add ClaudeCodeBlock for executing tasks via Claude Code in E2B sandbox (#11761)
Introduces a new ClaudeCodeBlock that enables execution of coding tasks
using Anthropic's Claude Code in an E2B sandbox. This block unlocks
powerful agentic coding capabilities - Claude Code can autonomously
create files, install packages, run commands, and build complete
applications within a secure sandboxed environment.

Changes 🏗️

- New file backend/blocks/claude_code.py:
  - ClaudeCodeBlock - Execute tasks using Claude Code in an E2B sandbox
- Dual credential support: E2B API key (sandbox) + Anthropic API key
(Claude Code)
- Session continuation support via session_id, sandbox_id, and
conversation_history
- Automatic file extraction with path, relative_path, name, and content
fields
  - Configurable timeout, setup commands, and working directory
- dispose_sandbox option to keep sandbox alive for multi-turn
conversations

Checklist 📋

For code changes:

- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
- [x] Create and execute ClaudeCodeBlock with a simple prompt ("Create a
hello world HTML file")
- [x] Verify files output includes correct path, relative_path, name,
and content
- [x] Test session continuation by passing session_id and sandbox_id
back
- [x] Build "Any API → Instant App" demo agent combining Firecrawl +
ClaudeCodeBlock + GitHub blocks
- [x] Verify generated files are pushed to GitHub with correct folder
structure using relative_path

Here are two example agents i made that can be used to test this agent,
they require github, anthropic and e2b access via api keys that are set
via the user/on the platform is testing on dev

The first agent is my

Any API → Instant App
"Transform any API documentation into a fully functional web
application. Just provide a docs URL and get a complete, ready-to-deploy
app pushed to a new GitHub repository."

[Any API → Instant
App_v36.json](https://github.com/user-attachments/files/24600326/Any.API.Instant.App_v36.json)


The second agent is my
Idea to project
"Simply enter your coding project's idea and this agent will make all of
the base initial code needed for you to start working on that project
and place it on github for you!"

[Idea to
project_v11.json](https://github.com/user-attachments/files/24600346/Idea.to.project_v11.json)

If you have any questions or issues let me know.

References
https://e2b.dev/blog/python-guide-run-claude-code-in-an-e2b-sandbox

https://github.com/e2b-dev/e2b-cookbook/tree/main/examples/anthropic-claude-code-in-sandbox-python
https://code.claude.com/docs/en/cli-reference

I tried to use E2b's "anthropic-claude-code" template but it kept
complaining it was out of date, so I make it manually spin up a E2b
instance and make it install the latest claude code and it uses that
2026-01-23 10:05:32 +00:00
Nicholas Tindle
90466908a8 refactor(docs): restructure platform docs for GitBook and remove MkDo… (#11825)
<!-- Clearly explain the need for these changes: -->
we met some reality when merging into the docs site but this fixes it
### Changes 🏗️
updates paths, adds some guides
<!-- Concisely describe all of the changes made in this pull request:
-->
update to match reality
### Checklist 📋

#### For code changes:
- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
  <!-- Put your test plan here: -->
  - [x] deploy it and validate

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Aligns block integrations documentation with GitBook.
> 
> - Changes generator default output to
`docs/integrations/block-integrations` and writes overview `README.md`
and `SUMMARY.md` at `docs/integrations/`
> - Adds GitBook frontmatter and hint syntax to overview; prefixes block
links with `block-integrations/`
> - Introduces `generate_summary_md` to build GitBook navigation
(including optional `guides/`)
> - Preserves per-block manual sections and adds optional `extras` +
file-level `additional_content`
> - Updates sync checker to validate parent `README.md` and `SUMMARY.md`
> - Rewrites `docs/integrations/README.md` with GitBook frontmatter and
updated links; adds `docs/integrations/SUMMARY.md`
> - Adds new guides: `guides/llm-providers.md`,
`guides/voice-providers.md`
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
fdb7ff8111. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: bobby.gaffin <bobby.gaffin@agpt.co>
2026-01-23 06:18:16 +00:00
32 changed files with 1369 additions and 1000 deletions

View File

@@ -165,7 +165,7 @@ async def client(server, test_user: str) -> AsyncGenerator[httpx.AsyncClient, No
@pytest.mark.asyncio(loop_scope="session")
async def test_authorize_creates_code_in_database(
async def test_authorize_creates_code_in_database_test(
client: httpx.AsyncClient,
test_user: str,
test_oauth_app: dict,

View File

@@ -0,0 +1,659 @@
import json
import shlex
import uuid
from typing import Literal, Optional
from e2b import AsyncSandbox as BaseAsyncSandbox
from pydantic import BaseModel, SecretStr
from backend.data.block import (
Block,
BlockCategory,
BlockOutput,
BlockSchemaInput,
BlockSchemaOutput,
)
from backend.data.model import (
APIKeyCredentials,
CredentialsField,
CredentialsMetaInput,
SchemaField,
)
from backend.integrations.providers import ProviderName
class ClaudeCodeExecutionError(Exception):
"""Exception raised when Claude Code execution fails.
Carries the sandbox_id so it can be returned to the user for cleanup
when dispose_sandbox=False.
"""
def __init__(self, message: str, sandbox_id: str = ""):
super().__init__(message)
self.sandbox_id = sandbox_id
# Test credentials for E2B
TEST_E2B_CREDENTIALS = APIKeyCredentials(
id="01234567-89ab-cdef-0123-456789abcdef",
provider="e2b",
api_key=SecretStr("mock-e2b-api-key"),
title="Mock E2B API key",
expires_at=None,
)
TEST_E2B_CREDENTIALS_INPUT = {
"provider": TEST_E2B_CREDENTIALS.provider,
"id": TEST_E2B_CREDENTIALS.id,
"type": TEST_E2B_CREDENTIALS.type,
"title": TEST_E2B_CREDENTIALS.title,
}
# Test credentials for Anthropic
TEST_ANTHROPIC_CREDENTIALS = APIKeyCredentials(
id="2e568a2b-b2ea-475a-8564-9a676bf31c56",
provider="anthropic",
api_key=SecretStr("mock-anthropic-api-key"),
title="Mock Anthropic API key",
expires_at=None,
)
TEST_ANTHROPIC_CREDENTIALS_INPUT = {
"provider": TEST_ANTHROPIC_CREDENTIALS.provider,
"id": TEST_ANTHROPIC_CREDENTIALS.id,
"type": TEST_ANTHROPIC_CREDENTIALS.type,
"title": TEST_ANTHROPIC_CREDENTIALS.title,
}
class ClaudeCodeBlock(Block):
"""
Execute tasks using Claude Code (Anthropic's AI coding assistant) in an E2B sandbox.
Claude Code can create files, install tools, run commands, and perform complex
coding tasks autonomously within a secure sandbox environment.
"""
# Use base template - we'll install Claude Code ourselves for latest version
DEFAULT_TEMPLATE = "base"
class Input(BlockSchemaInput):
e2b_credentials: CredentialsMetaInput[
Literal[ProviderName.E2B], Literal["api_key"]
] = CredentialsField(
description=(
"API key for the E2B platform to create the sandbox. "
"Get one on the [e2b website](https://e2b.dev/docs)"
),
)
anthropic_credentials: CredentialsMetaInput[
Literal[ProviderName.ANTHROPIC], Literal["api_key"]
] = CredentialsField(
description=(
"API key for Anthropic to power Claude Code. "
"Get one at [Anthropic's website](https://console.anthropic.com)"
),
)
prompt: str = SchemaField(
description=(
"The task or instruction for Claude Code to execute. "
"Claude Code can create files, install packages, run commands, "
"and perform complex coding tasks."
),
placeholder="Create a hello world index.html file",
default="",
advanced=False,
)
timeout: int = SchemaField(
description=(
"Sandbox timeout in seconds. Claude Code tasks can take "
"a while, so set this appropriately for your task complexity. "
"Note: This only applies when creating a new sandbox. "
"When reconnecting to an existing sandbox via sandbox_id, "
"the original timeout is retained."
),
default=300, # 5 minutes default
advanced=True,
)
setup_commands: list[str] = SchemaField(
description=(
"Optional shell commands to run before executing Claude Code. "
"Useful for installing dependencies or setting up the environment."
),
default_factory=list,
advanced=True,
)
working_directory: str = SchemaField(
description="Working directory for Claude Code to operate in.",
default="/home/user",
advanced=True,
)
# Session/continuation support
session_id: str = SchemaField(
description=(
"Session ID to resume a previous conversation. "
"Leave empty for a new conversation. "
"Use the session_id from a previous run to continue that conversation."
),
default="",
advanced=True,
)
sandbox_id: str = SchemaField(
description=(
"Sandbox ID to reconnect to an existing sandbox. "
"Required when resuming a session (along with session_id). "
"Use the sandbox_id from a previous run where dispose_sandbox was False."
),
default="",
advanced=True,
)
conversation_history: str = SchemaField(
description=(
"Previous conversation history to continue from. "
"Use this to restore context on a fresh sandbox if the previous one timed out. "
"Pass the conversation_history output from a previous run."
),
default="",
advanced=True,
)
dispose_sandbox: bool = SchemaField(
description=(
"Whether to dispose of the sandbox immediately after execution. "
"Set to False if you want to continue the conversation later "
"(you'll need both sandbox_id and session_id from the output)."
),
default=True,
advanced=True,
)
class FileOutput(BaseModel):
"""A file extracted from the sandbox."""
path: str
relative_path: str # Path relative to working directory (for GitHub, etc.)
name: str
content: str
class Output(BlockSchemaOutput):
response: str = SchemaField(
description="The output/response from Claude Code execution"
)
files: list["ClaudeCodeBlock.FileOutput"] = SchemaField(
description=(
"List of text files created/modified by Claude Code during this execution. "
"Each file has 'path', 'relative_path', 'name', and 'content' fields."
)
)
conversation_history: str = SchemaField(
description=(
"Full conversation history including this turn. "
"Pass this to conversation_history input to continue on a fresh sandbox "
"if the previous sandbox timed out."
)
)
session_id: str = SchemaField(
description=(
"Session ID for this conversation. "
"Pass this back along with sandbox_id to continue the conversation."
)
)
sandbox_id: Optional[str] = SchemaField(
description=(
"ID of the sandbox instance. "
"Pass this back along with session_id to continue the conversation. "
"This is None if dispose_sandbox was True (sandbox was disposed)."
),
default=None,
)
error: str = SchemaField(description="Error message if execution failed")
def __init__(self):
super().__init__(
id="4e34f4a5-9b89-4326-ba77-2dd6750b7194",
description=(
"Execute tasks using Claude Code in an E2B sandbox. "
"Claude Code can create files, install tools, run commands, "
"and perform complex coding tasks autonomously."
),
categories={BlockCategory.DEVELOPER_TOOLS, BlockCategory.AI},
input_schema=ClaudeCodeBlock.Input,
output_schema=ClaudeCodeBlock.Output,
test_credentials={
"e2b_credentials": TEST_E2B_CREDENTIALS,
"anthropic_credentials": TEST_ANTHROPIC_CREDENTIALS,
},
test_input={
"e2b_credentials": TEST_E2B_CREDENTIALS_INPUT,
"anthropic_credentials": TEST_ANTHROPIC_CREDENTIALS_INPUT,
"prompt": "Create a hello world HTML file",
"timeout": 300,
"setup_commands": [],
"working_directory": "/home/user",
"session_id": "",
"sandbox_id": "",
"conversation_history": "",
"dispose_sandbox": True,
},
test_output=[
("response", "Created index.html with hello world content"),
(
"files",
[
{
"path": "/home/user/index.html",
"relative_path": "index.html",
"name": "index.html",
"content": "<html>Hello World</html>",
}
],
),
(
"conversation_history",
"User: Create a hello world HTML file\n"
"Claude: Created index.html with hello world content",
),
("session_id", str),
("sandbox_id", None), # None because dispose_sandbox=True in test_input
],
test_mock={
"execute_claude_code": lambda *args, **kwargs: (
"Created index.html with hello world content", # response
[
ClaudeCodeBlock.FileOutput(
path="/home/user/index.html",
relative_path="index.html",
name="index.html",
content="<html>Hello World</html>",
)
], # files
"User: Create a hello world HTML file\n"
"Claude: Created index.html with hello world content", # conversation_history
"test-session-id", # session_id
"sandbox_id", # sandbox_id
),
},
)
async def execute_claude_code(
self,
e2b_api_key: str,
anthropic_api_key: str,
prompt: str,
timeout: int,
setup_commands: list[str],
working_directory: str,
session_id: str,
existing_sandbox_id: str,
conversation_history: str,
dispose_sandbox: bool,
) -> tuple[str, list["ClaudeCodeBlock.FileOutput"], str, str, str]:
"""
Execute Claude Code in an E2B sandbox.
Returns:
Tuple of (response, files, conversation_history, session_id, sandbox_id)
"""
# Validate that sandbox_id is provided when resuming a session
if session_id and not existing_sandbox_id:
raise ValueError(
"sandbox_id is required when resuming a session with session_id. "
"The session state is stored in the original sandbox. "
"If the sandbox has timed out, use conversation_history instead "
"to restore context on a fresh sandbox."
)
sandbox = None
sandbox_id = ""
try:
# Either reconnect to existing sandbox or create a new one
if existing_sandbox_id:
# Reconnect to existing sandbox for conversation continuation
sandbox = await BaseAsyncSandbox.connect(
sandbox_id=existing_sandbox_id,
api_key=e2b_api_key,
)
else:
# Create new sandbox
sandbox = await BaseAsyncSandbox.create(
template=self.DEFAULT_TEMPLATE,
api_key=e2b_api_key,
timeout=timeout,
envs={"ANTHROPIC_API_KEY": anthropic_api_key},
)
# Install Claude Code from npm (ensures we get the latest version)
install_result = await sandbox.commands.run(
"npm install -g @anthropic-ai/claude-code@latest",
timeout=120, # 2 min timeout for install
)
if install_result.exit_code != 0:
raise Exception(
f"Failed to install Claude Code: {install_result.stderr}"
)
# Run any user-provided setup commands
for cmd in setup_commands:
setup_result = await sandbox.commands.run(cmd)
if setup_result.exit_code != 0:
raise Exception(
f"Setup command failed: {cmd}\n"
f"Exit code: {setup_result.exit_code}\n"
f"Stdout: {setup_result.stdout}\n"
f"Stderr: {setup_result.stderr}"
)
# Capture sandbox_id immediately after creation/connection
# so it's available for error recovery if dispose_sandbox=False
sandbox_id = sandbox.sandbox_id
# Generate or use provided session ID
current_session_id = session_id if session_id else str(uuid.uuid4())
# Build base Claude flags
base_flags = "-p --dangerously-skip-permissions --output-format json"
# Add conversation history context if provided (for fresh sandbox continuation)
history_flag = ""
if conversation_history and not session_id:
# Inject previous conversation as context via system prompt
# Use consistent escaping via _escape_prompt helper
escaped_history = self._escape_prompt(
f"Previous conversation context: {conversation_history}"
)
history_flag = f" --append-system-prompt {escaped_history}"
# Build Claude command based on whether we're resuming or starting new
# Use shlex.quote for working_directory and session IDs to prevent injection
safe_working_dir = shlex.quote(working_directory)
if session_id:
# Resuming existing session (sandbox still alive)
safe_session_id = shlex.quote(session_id)
claude_command = (
f"cd {safe_working_dir} && "
f"echo {self._escape_prompt(prompt)} | "
f"claude --resume {safe_session_id} {base_flags}"
)
else:
# New session with specific ID
safe_current_session_id = shlex.quote(current_session_id)
claude_command = (
f"cd {safe_working_dir} && "
f"echo {self._escape_prompt(prompt)} | "
f"claude --session-id {safe_current_session_id} {base_flags}{history_flag}"
)
# Capture timestamp before running Claude Code to filter files later
# Capture timestamp 1 second in the past to avoid race condition with file creation
timestamp_result = await sandbox.commands.run(
"date -u -d '1 second ago' +%Y-%m-%dT%H:%M:%S"
)
if timestamp_result.exit_code != 0:
raise RuntimeError(
f"Failed to capture timestamp: {timestamp_result.stderr}"
)
start_timestamp = (
timestamp_result.stdout.strip() if timestamp_result.stdout else None
)
result = await sandbox.commands.run(
claude_command,
timeout=0, # No command timeout - let sandbox timeout handle it
)
# Check for command failure
if result.exit_code != 0:
error_msg = result.stderr or result.stdout or "Unknown error"
raise Exception(
f"Claude Code command failed with exit code {result.exit_code}:\n"
f"{error_msg}"
)
raw_output = result.stdout or ""
# Parse JSON output to extract response and build conversation history
response = ""
new_conversation_history = conversation_history or ""
try:
# The JSON output contains the result
output_data = json.loads(raw_output)
response = output_data.get("result", raw_output)
# Build conversation history entry
turn_entry = f"User: {prompt}\nClaude: {response}"
if new_conversation_history:
new_conversation_history = (
f"{new_conversation_history}\n\n{turn_entry}"
)
else:
new_conversation_history = turn_entry
except json.JSONDecodeError:
# If not valid JSON, use raw output
response = raw_output
turn_entry = f"User: {prompt}\nClaude: {response}"
if new_conversation_history:
new_conversation_history = (
f"{new_conversation_history}\n\n{turn_entry}"
)
else:
new_conversation_history = turn_entry
# Extract files created/modified during this run
files = await self._extract_files(
sandbox, working_directory, start_timestamp
)
return (
response,
files,
new_conversation_history,
current_session_id,
sandbox_id,
)
except Exception as e:
# Wrap exception with sandbox_id so caller can access/cleanup
# the preserved sandbox when dispose_sandbox=False
raise ClaudeCodeExecutionError(str(e), sandbox_id) from e
finally:
if dispose_sandbox and sandbox:
await sandbox.kill()
async def _extract_files(
self,
sandbox: BaseAsyncSandbox,
working_directory: str,
since_timestamp: str | None = None,
) -> list["ClaudeCodeBlock.FileOutput"]:
"""
Extract text files created/modified during this Claude Code execution.
Args:
sandbox: The E2B sandbox instance
working_directory: Directory to search for files
since_timestamp: ISO timestamp - only return files modified after this time
Returns:
List of FileOutput objects with path, relative_path, name, and content
"""
files: list[ClaudeCodeBlock.FileOutput] = []
# Text file extensions we can safely read as text
text_extensions = {
".txt",
".md",
".html",
".htm",
".css",
".js",
".ts",
".jsx",
".tsx",
".json",
".xml",
".yaml",
".yml",
".toml",
".ini",
".cfg",
".conf",
".py",
".rb",
".php",
".java",
".c",
".cpp",
".h",
".hpp",
".cs",
".go",
".rs",
".swift",
".kt",
".scala",
".sh",
".bash",
".zsh",
".sql",
".graphql",
".env",
".gitignore",
".dockerfile",
"Dockerfile",
".vue",
".svelte",
".astro",
".mdx",
".rst",
".tex",
".csv",
".log",
}
try:
# List files recursively using find command
# Exclude node_modules and .git directories, but allow hidden files
# like .env and .gitignore (they're filtered by text_extensions later)
# Filter by timestamp to only get files created/modified during this run
safe_working_dir = shlex.quote(working_directory)
timestamp_filter = ""
if since_timestamp:
timestamp_filter = f"-newermt {shlex.quote(since_timestamp)} "
find_result = await sandbox.commands.run(
f"find {safe_working_dir} -type f "
f"{timestamp_filter}"
f"-not -path '*/node_modules/*' "
f"-not -path '*/.git/*' "
f"2>/dev/null"
)
if find_result.stdout:
for file_path in find_result.stdout.strip().split("\n"):
if not file_path:
continue
# Check if it's a text file we can read
is_text = any(
file_path.endswith(ext) for ext in text_extensions
) or file_path.endswith("Dockerfile")
if is_text:
try:
content = await sandbox.files.read(file_path)
# Handle bytes or string
if isinstance(content, bytes):
content = content.decode("utf-8", errors="replace")
# Extract filename from path
file_name = file_path.split("/")[-1]
# Calculate relative path by stripping working directory
relative_path = file_path
if file_path.startswith(working_directory):
relative_path = file_path[len(working_directory) :]
# Remove leading slash if present
if relative_path.startswith("/"):
relative_path = relative_path[1:]
files.append(
ClaudeCodeBlock.FileOutput(
path=file_path,
relative_path=relative_path,
name=file_name,
content=content,
)
)
except Exception:
# Skip files that can't be read
pass
except Exception:
# If file extraction fails, return empty results
pass
return files
def _escape_prompt(self, prompt: str) -> str:
"""Escape the prompt for safe shell execution."""
# Use single quotes and escape any single quotes in the prompt
escaped = prompt.replace("'", "'\"'\"'")
return f"'{escaped}'"
async def run(
self,
input_data: Input,
*,
e2b_credentials: APIKeyCredentials,
anthropic_credentials: APIKeyCredentials,
**kwargs,
) -> BlockOutput:
try:
(
response,
files,
conversation_history,
session_id,
sandbox_id,
) = await self.execute_claude_code(
e2b_api_key=e2b_credentials.api_key.get_secret_value(),
anthropic_api_key=anthropic_credentials.api_key.get_secret_value(),
prompt=input_data.prompt,
timeout=input_data.timeout,
setup_commands=input_data.setup_commands,
working_directory=input_data.working_directory,
session_id=input_data.session_id,
existing_sandbox_id=input_data.sandbox_id,
conversation_history=input_data.conversation_history,
dispose_sandbox=input_data.dispose_sandbox,
)
yield "response", response
# Always yield files (empty list if none) to match Output schema
yield "files", [f.model_dump() for f in files]
# Always yield conversation_history so user can restore context on fresh sandbox
yield "conversation_history", conversation_history
# Always yield session_id so user can continue conversation
yield "session_id", session_id
# Always yield sandbox_id (None if disposed) to match Output schema
yield "sandbox_id", sandbox_id if not input_data.dispose_sandbox else None
except ClaudeCodeExecutionError as e:
yield "error", str(e)
# If sandbox was preserved (dispose_sandbox=False), yield sandbox_id
# so user can reconnect to or clean up the orphaned sandbox
if not input_data.dispose_sandbox and e.sandbox_id:
yield "sandbox_id", e.sandbox_id
except Exception as e:
yield "error", str(e)

View File

@@ -0,0 +1,41 @@
"use client";
import { createContext, useContext, useRef, type ReactNode } from "react";
interface NewChatContextValue {
onNewChatClick: () => void;
setOnNewChatClick: (handler?: () => void) => void;
performNewChat?: () => void;
setPerformNewChat: (handler?: () => void) => void;
}
const NewChatContext = createContext<NewChatContextValue | null>(null);
export function NewChatProvider({ children }: { children: ReactNode }) {
const onNewChatRef = useRef<(() => void) | undefined>();
const performNewChatRef = useRef<(() => void) | undefined>();
const contextValueRef = useRef<NewChatContextValue>({
onNewChatClick() {
onNewChatRef.current?.();
},
setOnNewChatClick(handler?: () => void) {
onNewChatRef.current = handler;
},
performNewChat() {
performNewChatRef.current?.();
},
setPerformNewChat(handler?: () => void) {
performNewChatRef.current = handler;
},
});
return (
<NewChatContext.Provider value={contextValueRef.current}>
{children}
</NewChatContext.Provider>
);
}
export function useNewChat() {
return useContext(NewChatContext);
}

View File

@@ -1,8 +1,10 @@
"use client";
import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner";
import { ChatLoader } from "@/components/contextual/Chat/components/ChatLoader/ChatLoader";
import { NAVBAR_HEIGHT_PX } from "@/lib/constants";
import type { ReactNode } from "react";
import { useEffect } from "react";
import { useNewChat } from "../../NewChatContext";
import { DesktopSidebar } from "./components/DesktopSidebar/DesktopSidebar";
import { LoadingState } from "./components/LoadingState/LoadingState";
import { MobileDrawer } from "./components/MobileDrawer/MobileDrawer";
@@ -33,10 +35,25 @@ export function CopilotShell({ children }: Props) {
isReadyToShowContent,
} = useCopilotShell();
const newChatContext = useNewChat();
const handleNewChatClickWrapper =
newChatContext?.onNewChatClick || handleNewChat;
useEffect(
function registerNewChatHandler() {
if (!newChatContext) return;
newChatContext.setPerformNewChat(handleNewChat);
return function cleanup() {
newChatContext.setPerformNewChat(undefined);
};
},
[newChatContext, handleNewChat],
);
if (!isLoggedIn) {
return (
<div className="flex h-full items-center justify-center">
<LoadingSpinner size="large" />
<ChatLoader />
</div>
);
}
@@ -55,7 +72,7 @@ export function CopilotShell({ children }: Props) {
isFetchingNextPage={isFetchingNextPage}
onSelectSession={handleSelectSession}
onFetchNextPage={fetchNextPage}
onNewChat={handleNewChat}
onNewChat={handleNewChatClickWrapper}
hasActiveSession={Boolean(hasActiveSession)}
/>
)}
@@ -77,7 +94,7 @@ export function CopilotShell({ children }: Props) {
isFetchingNextPage={isFetchingNextPage}
onSelectSession={handleSelectSession}
onFetchNextPage={fetchNextPage}
onNewChat={handleNewChat}
onNewChat={handleNewChatClickWrapper}
onClose={handleCloseDrawer}
onOpenChange={handleDrawerOpenChange}
hasActiveSession={Boolean(hasActiveSession)}

View File

@@ -148,13 +148,15 @@ export function useCopilotShell() {
setHasAutoSelectedSession(false);
}
const isLoading = isSessionsLoading && accumulatedSessions.length === 0;
return {
isMobile,
isDrawerOpen,
isLoggedIn,
hasActiveSession:
Boolean(currentSessionId) && (!isOnHomepage || Boolean(paramSessionId)),
isLoading: isSessionsLoading || !areAllSessionsLoaded,
isLoading,
sessions: visibleSessions,
currentSessionId: sidebarSelectedSessionId,
handleSelectSession,

View File

@@ -1,5 +1,28 @@
import type { User } from "@supabase/supabase-js";
export type PageState =
| { type: "welcome" }
| { type: "newChat" }
| { type: "creating"; prompt: string }
| { type: "chat"; sessionId: string; initialPrompt?: string };
export function getInitialPromptFromState(
pageState: PageState,
storedInitialPrompt: string | undefined,
) {
if (storedInitialPrompt) return storedInitialPrompt;
if (pageState.type === "creating") return pageState.prompt;
if (pageState.type === "chat") return pageState.initialPrompt;
}
export function shouldResetToWelcome(pageState: PageState) {
return (
pageState.type !== "newChat" &&
pageState.type !== "creating" &&
pageState.type !== "welcome"
);
}
export function getGreetingName(user?: User | null): string {
if (!user) return "there";
const metadata = user.user_metadata as Record<string, unknown> | undefined;

View File

@@ -1,6 +1,11 @@
import type { ReactNode } from "react";
import { NewChatProvider } from "./NewChatContext";
import { CopilotShell } from "./components/CopilotShell/CopilotShell";
export default function CopilotLayout({ children }: { children: ReactNode }) {
return <CopilotShell>{children}</CopilotShell>;
return (
<NewChatProvider>
<CopilotShell>{children}</CopilotShell>
</NewChatProvider>
);
}

View File

@@ -1,142 +1,35 @@
"use client";
import { postV2CreateSession } from "@/app/api/__generated__/endpoints/chat/chat";
import { Skeleton } from "@/components/__legacy__/ui/skeleton";
import { Button } from "@/components/atoms/Button/Button";
import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner";
import { Text } from "@/components/atoms/Text/Text";
import { Chat } from "@/components/contextual/Chat/Chat";
import { ChatInput } from "@/components/contextual/Chat/components/ChatInput/ChatInput";
import { getHomepageRoute } from "@/lib/constants";
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
import {
Flag,
type FlagValues,
useGetFlag,
} from "@/services/feature-flags/use-get-flag";
import { useFlags } from "launchdarkly-react-client-sdk";
import { useRouter, useSearchParams } from "next/navigation";
import { useEffect, useMemo, useRef, useState } from "react";
import { getGreetingName, getQuickActions } from "./helpers";
type PageState =
| { type: "welcome" }
| { type: "creating"; prompt: string }
| { type: "chat"; sessionId: string; initialPrompt?: string };
import { ChatLoader } from "@/components/contextual/Chat/components/ChatLoader/ChatLoader";
import { Dialog } from "@/components/molecules/Dialog/Dialog";
import { useCopilotPage } from "./useCopilotPage";
export default function CopilotPage() {
const router = useRouter();
const searchParams = useSearchParams();
const { user, isLoggedIn, isUserLoading } = useSupabase();
const { state, handlers } = useCopilotPage();
const {
greetingName,
quickActions,
isLoading,
pageState,
isNewChatModalOpen,
isReady,
} = state;
const {
handleQuickAction,
startChatWithPrompt,
handleSessionNotFound,
handleStreamingChange,
handleCancelNewChat,
proceedWithNewChat,
handleNewChatModalOpen,
} = handlers;
const isChatEnabled = useGetFlag(Flag.CHAT);
const flags = useFlags<FlagValues>();
const homepageRoute = getHomepageRoute(isChatEnabled);
const envEnabled = process.env.NEXT_PUBLIC_LAUNCHDARKLY_ENABLED === "true";
const clientId = process.env.NEXT_PUBLIC_LAUNCHDARKLY_CLIENT_ID;
const isLaunchDarklyConfigured = envEnabled && Boolean(clientId);
const isFlagReady =
!isLaunchDarklyConfigured || flags[Flag.CHAT] !== undefined;
const [pageState, setPageState] = useState<PageState>({ type: "welcome" });
const initialPromptRef = useRef<Map<string, string>>(new Map());
const urlSessionId = searchParams.get("sessionId");
// Sync with URL sessionId (preserve initialPrompt from ref)
useEffect(
function syncSessionFromUrl() {
if (urlSessionId) {
// If we're already in chat state with this sessionId, don't overwrite
if (pageState.type === "chat" && pageState.sessionId === urlSessionId) {
return;
}
// Get initialPrompt from ref or current state
const storedInitialPrompt = initialPromptRef.current.get(urlSessionId);
const currentInitialPrompt =
storedInitialPrompt ||
(pageState.type === "creating"
? pageState.prompt
: pageState.type === "chat"
? pageState.initialPrompt
: undefined);
if (currentInitialPrompt) {
initialPromptRef.current.set(urlSessionId, currentInitialPrompt);
}
setPageState({
type: "chat",
sessionId: urlSessionId,
initialPrompt: currentInitialPrompt,
});
} else if (pageState.type === "chat") {
setPageState({ type: "welcome" });
}
},
[urlSessionId],
);
useEffect(
function ensureAccess() {
if (!isFlagReady) return;
if (isChatEnabled === false) {
router.replace(homepageRoute);
}
},
[homepageRoute, isChatEnabled, isFlagReady, router],
);
const greetingName = useMemo(
function getName() {
return getGreetingName(user);
},
[user],
);
const quickActions = useMemo(function getActions() {
return getQuickActions();
}, []);
async function startChatWithPrompt(prompt: string) {
if (!prompt?.trim()) return;
if (pageState.type === "creating") return;
const trimmedPrompt = prompt.trim();
setPageState({ type: "creating", prompt: trimmedPrompt });
try {
// Create session
const sessionResponse = await postV2CreateSession({
body: JSON.stringify({}),
});
if (sessionResponse.status !== 200 || !sessionResponse.data?.id) {
throw new Error("Failed to create session");
}
const sessionId = sessionResponse.data.id;
// Store initialPrompt in ref so it persists across re-renders
initialPromptRef.current.set(sessionId, trimmedPrompt);
// Update URL and show Chat with initial prompt
// Chat will handle sending the message and streaming
window.history.replaceState(null, "", `/copilot?sessionId=${sessionId}`);
setPageState({ type: "chat", sessionId, initialPrompt: trimmedPrompt });
} catch (error) {
console.error("[CopilotPage] Failed to start chat:", error);
setPageState({ type: "welcome" });
}
}
function handleQuickAction(action: string) {
startChatWithPrompt(action);
}
function handleSessionNotFound() {
router.replace("/copilot");
}
if (!isFlagReady || isChatEnabled === false || !isLoggedIn) {
if (!isReady) {
return null;
}
@@ -150,7 +43,55 @@ export default function CopilotPage() {
urlSessionId={pageState.sessionId}
initialPrompt={pageState.initialPrompt}
onSessionNotFound={handleSessionNotFound}
onStreamingChange={handleStreamingChange}
/>
<Dialog
title="Interrupt current chat?"
styling={{ maxWidth: 300, width: "100%" }}
controlled={{
isOpen: isNewChatModalOpen,
set: handleNewChatModalOpen,
}}
onClose={handleCancelNewChat}
>
<Dialog.Content>
<div className="flex flex-col gap-4">
<Text variant="body">
The current chat response will be interrupted. Are you sure you
want to start a new chat?
</Text>
<Dialog.Footer>
<Button
type="button"
variant="outline"
onClick={handleCancelNewChat}
>
Cancel
</Button>
<Button
type="button"
variant="primary"
onClick={proceedWithNewChat}
>
Start new chat
</Button>
</Dialog.Footer>
</div>
</Dialog.Content>
</Dialog>
</div>
);
}
if (pageState.type === "newChat") {
return (
<div className="flex h-full flex-1 flex-col items-center justify-center bg-[#f8f8f9]">
<div className="flex flex-col items-center gap-4">
<ChatLoader />
<Text variant="body" className="text-zinc-500">
Loading your chats...
</Text>
</div>
</div>
);
}
@@ -158,18 +99,18 @@ export default function CopilotPage() {
// Show loading state while creating session and sending first message
if (pageState.type === "creating") {
return (
<div className="flex h-full flex-1 flex-col items-center justify-center bg-[#f8f8f9] px-6 py-10">
<LoadingSpinner size="large" />
<Text variant="body" className="mt-4 text-zinc-500">
Starting your chat...
</Text>
<div className="flex h-full flex-1 flex-col items-center justify-center bg-[#f8f8f9]">
<div className="flex flex-col items-center gap-4">
<ChatLoader />
<Text variant="body" className="text-zinc-500">
Loading your chats...
</Text>
</div>
</div>
);
}
// Show Welcome screen
const isLoading = isUserLoading;
return (
<div className="flex h-full flex-1 items-center justify-center overflow-y-auto bg-[#f8f8f9] px-6 py-10">
<div className="w-full text-center">

View File

@@ -0,0 +1,266 @@
import { postV2CreateSession } from "@/app/api/__generated__/endpoints/chat/chat";
import { useToast } from "@/components/molecules/Toast/use-toast";
import { getHomepageRoute } from "@/lib/constants";
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
import {
Flag,
type FlagValues,
useGetFlag,
} from "@/services/feature-flags/use-get-flag";
import * as Sentry from "@sentry/nextjs";
import { useFlags } from "launchdarkly-react-client-sdk";
import { useRouter } from "next/navigation";
import { useEffect, useReducer } from "react";
import { useNewChat } from "./NewChatContext";
import { getGreetingName, getQuickActions, type PageState } from "./helpers";
import { useCopilotURLState } from "./useCopilotURLState";
type CopilotState = {
pageState: PageState;
isStreaming: boolean;
isNewChatModalOpen: boolean;
initialPrompts: Record<string, string>;
previousSessionId: string | null;
};
type CopilotAction =
| { type: "setPageState"; pageState: PageState }
| { type: "setStreaming"; isStreaming: boolean }
| { type: "setNewChatModalOpen"; isOpen: boolean }
| { type: "setInitialPrompt"; sessionId: string; prompt: string }
| { type: "setPreviousSessionId"; sessionId: string | null };
function isSamePageState(next: PageState, current: PageState) {
if (next.type !== current.type) return false;
if (next.type === "creating" && current.type === "creating") {
return next.prompt === current.prompt;
}
if (next.type === "chat" && current.type === "chat") {
return (
next.sessionId === current.sessionId &&
next.initialPrompt === current.initialPrompt
);
}
return true;
}
function copilotReducer(
state: CopilotState,
action: CopilotAction,
): CopilotState {
if (action.type === "setPageState") {
if (isSamePageState(action.pageState, state.pageState)) return state;
return { ...state, pageState: action.pageState };
}
if (action.type === "setStreaming") {
if (action.isStreaming === state.isStreaming) return state;
return { ...state, isStreaming: action.isStreaming };
}
if (action.type === "setNewChatModalOpen") {
if (action.isOpen === state.isNewChatModalOpen) return state;
return { ...state, isNewChatModalOpen: action.isOpen };
}
if (action.type === "setInitialPrompt") {
if (state.initialPrompts[action.sessionId] === action.prompt) return state;
return {
...state,
initialPrompts: {
...state.initialPrompts,
[action.sessionId]: action.prompt,
},
};
}
if (action.type === "setPreviousSessionId") {
if (state.previousSessionId === action.sessionId) return state;
return { ...state, previousSessionId: action.sessionId };
}
return state;
}
export function useCopilotPage() {
const router = useRouter();
const { user, isLoggedIn, isUserLoading } = useSupabase();
const { toast } = useToast();
const isChatEnabled = useGetFlag(Flag.CHAT);
const flags = useFlags<FlagValues>();
const homepageRoute = getHomepageRoute(isChatEnabled);
const envEnabled = process.env.NEXT_PUBLIC_LAUNCHDARKLY_ENABLED === "true";
const clientId = process.env.NEXT_PUBLIC_LAUNCHDARKLY_CLIENT_ID;
const isLaunchDarklyConfigured = envEnabled && Boolean(clientId);
const isFlagReady =
!isLaunchDarklyConfigured || flags[Flag.CHAT] !== undefined;
const [state, dispatch] = useReducer(copilotReducer, {
pageState: { type: "welcome" },
isStreaming: false,
isNewChatModalOpen: false,
initialPrompts: {},
previousSessionId: null,
});
const newChatContext = useNewChat();
const greetingName = getGreetingName(user);
const quickActions = getQuickActions();
function setPageState(pageState: PageState) {
dispatch({ type: "setPageState", pageState });
}
function setInitialPrompt(sessionId: string, prompt: string) {
dispatch({ type: "setInitialPrompt", sessionId, prompt });
}
function setPreviousSessionId(sessionId: string | null) {
dispatch({ type: "setPreviousSessionId", sessionId });
}
const { setUrlSessionId } = useCopilotURLState({
pageState: state.pageState,
initialPrompts: state.initialPrompts,
previousSessionId: state.previousSessionId,
setPageState,
setInitialPrompt,
setPreviousSessionId,
});
useEffect(
function registerNewChatHandler() {
if (!newChatContext) return;
newChatContext.setOnNewChatClick(handleNewChatClick);
return function cleanup() {
newChatContext.setOnNewChatClick(undefined);
};
},
[newChatContext, handleNewChatClick],
);
useEffect(
function transitionNewChatToWelcome() {
if (state.pageState.type === "newChat") {
function setWelcomeState() {
dispatch({ type: "setPageState", pageState: { type: "welcome" } });
}
const timer = setTimeout(setWelcomeState, 300);
return function cleanup() {
clearTimeout(timer);
};
}
},
[state.pageState.type],
);
useEffect(
function ensureAccess() {
if (!isFlagReady) return;
if (isChatEnabled === false) {
router.replace(homepageRoute);
}
},
[homepageRoute, isChatEnabled, isFlagReady, router],
);
async function startChatWithPrompt(prompt: string) {
if (!prompt?.trim()) return;
if (state.pageState.type === "creating") return;
const trimmedPrompt = prompt.trim();
dispatch({
type: "setPageState",
pageState: { type: "creating", prompt: trimmedPrompt },
});
try {
const sessionResponse = await postV2CreateSession({
body: JSON.stringify({}),
});
if (sessionResponse.status !== 200 || !sessionResponse.data?.id) {
throw new Error("Failed to create session");
}
const sessionId = sessionResponse.data.id;
dispatch({
type: "setInitialPrompt",
sessionId,
prompt: trimmedPrompt,
});
await setUrlSessionId(sessionId, { shallow: false });
dispatch({
type: "setPageState",
pageState: { type: "chat", sessionId, initialPrompt: trimmedPrompt },
});
} catch (error) {
console.error("[CopilotPage] Failed to start chat:", error);
toast({ title: "Failed to start chat", variant: "destructive" });
Sentry.captureException(error);
dispatch({ type: "setPageState", pageState: { type: "welcome" } });
}
}
function handleQuickAction(action: string) {
startChatWithPrompt(action);
}
function handleSessionNotFound() {
router.replace("/copilot");
}
function handleStreamingChange(isStreamingValue: boolean) {
dispatch({ type: "setStreaming", isStreaming: isStreamingValue });
}
async function proceedWithNewChat() {
dispatch({ type: "setNewChatModalOpen", isOpen: false });
if (newChatContext?.performNewChat) {
newChatContext.performNewChat();
return;
}
try {
await setUrlSessionId(null, { shallow: false });
} catch (error) {
console.error("[CopilotPage] Failed to clear session:", error);
}
router.replace("/copilot");
}
function handleCancelNewChat() {
dispatch({ type: "setNewChatModalOpen", isOpen: false });
}
function handleNewChatModalOpen(isOpen: boolean) {
dispatch({ type: "setNewChatModalOpen", isOpen });
}
function handleNewChatClick() {
if (state.isStreaming) {
dispatch({ type: "setNewChatModalOpen", isOpen: true });
} else {
proceedWithNewChat();
}
}
return {
state: {
greetingName,
quickActions,
isLoading: isUserLoading,
pageState: state.pageState,
isNewChatModalOpen: state.isNewChatModalOpen,
isReady: isFlagReady && isChatEnabled !== false && isLoggedIn,
},
handlers: {
handleQuickAction,
startChatWithPrompt,
handleSessionNotFound,
handleStreamingChange,
handleCancelNewChat,
proceedWithNewChat,
handleNewChatModalOpen,
},
};
}

View File

@@ -0,0 +1,80 @@
import { parseAsString, useQueryState } from "nuqs";
import { useLayoutEffect } from "react";
import {
getInitialPromptFromState,
type PageState,
shouldResetToWelcome,
} from "./helpers";
interface UseCopilotUrlStateArgs {
pageState: PageState;
initialPrompts: Record<string, string>;
previousSessionId: string | null;
setPageState: (pageState: PageState) => void;
setInitialPrompt: (sessionId: string, prompt: string) => void;
setPreviousSessionId: (sessionId: string | null) => void;
}
export function useCopilotURLState({
pageState,
initialPrompts,
previousSessionId,
setPageState,
setInitialPrompt,
setPreviousSessionId,
}: UseCopilotUrlStateArgs) {
const [urlSessionId, setUrlSessionId] = useQueryState(
"sessionId",
parseAsString,
);
function syncSessionFromUrl() {
if (urlSessionId) {
if (pageState.type === "chat" && pageState.sessionId === urlSessionId) {
setPreviousSessionId(urlSessionId);
return;
}
const storedInitialPrompt = initialPrompts[urlSessionId];
const currentInitialPrompt = getInitialPromptFromState(
pageState,
storedInitialPrompt,
);
if (currentInitialPrompt) {
setInitialPrompt(urlSessionId, currentInitialPrompt);
}
setPageState({
type: "chat",
sessionId: urlSessionId,
initialPrompt: currentInitialPrompt,
});
setPreviousSessionId(urlSessionId);
return;
}
const wasInChat = previousSessionId !== null && pageState.type === "chat";
setPreviousSessionId(null);
if (wasInChat) {
setPageState({ type: "newChat" });
return;
}
if (shouldResetToWelcome(pageState)) {
setPageState({ type: "welcome" });
}
}
useLayoutEffect(syncSessionFromUrl, [
urlSessionId,
pageState.type,
previousSessionId,
initialPrompts,
]);
return {
urlSessionId,
setUrlSessionId,
};
}

View File

@@ -13,6 +13,7 @@ export interface ChatProps {
urlSessionId?: string | null;
initialPrompt?: string;
onSessionNotFound?: () => void;
onStreamingChange?: (isStreaming: boolean) => void;
}
export function Chat({
@@ -20,6 +21,7 @@ export function Chat({
urlSessionId,
initialPrompt,
onSessionNotFound,
onStreamingChange,
}: ChatProps) {
const hasHandledNotFoundRef = useRef(false);
const {
@@ -73,6 +75,7 @@ export function Chat({
initialMessages={messages}
initialPrompt={initialPrompt}
className="flex-1"
onStreamingChange={onStreamingChange}
/>
)}
</main>

View File

@@ -4,6 +4,7 @@ import { Text } from "@/components/atoms/Text/Text";
import { Dialog } from "@/components/molecules/Dialog/Dialog";
import { useBreakpoint } from "@/lib/hooks/useBreakpoint";
import { cn } from "@/lib/utils";
import { useEffect } from "react";
import { ChatInput } from "../ChatInput/ChatInput";
import { MessageList } from "../MessageList/MessageList";
import { useChatContainer } from "./useChatContainer";
@@ -13,6 +14,7 @@ export interface ChatContainerProps {
initialMessages: SessionDetailResponse["messages"];
initialPrompt?: string;
className?: string;
onStreamingChange?: (isStreaming: boolean) => void;
}
export function ChatContainer({
@@ -20,6 +22,7 @@ export function ChatContainer({
initialMessages,
initialPrompt,
className,
onStreamingChange,
}: ChatContainerProps) {
const {
messages,
@@ -36,6 +39,10 @@ export function ChatContainer({
initialPrompt,
});
useEffect(() => {
onStreamingChange?.(isStreaming);
}, [isStreaming, onStreamingChange]);
const breakpoint = useBreakpoint();
const isMobile =
breakpoint === "base" || breakpoint === "sm" || breakpoint === "md";

View File

@@ -1,12 +1,7 @@
import { Text } from "@/components/atoms/Text/Text";
export function ChatLoader() {
return (
<Text
variant="small"
className="bg-gradient-to-r from-neutral-600 via-neutral-500 to-neutral-600 bg-[length:200%_100%] bg-clip-text text-xs text-transparent [animation:shimmer_2s_ease-in-out_infinite]"
>
Taking a bit more time...
</Text>
<div className="flex items-center gap-2">
<div className="h-5 w-5 animate-loader rounded-full bg-black" />
</div>
);
}

View File

@@ -7,7 +7,6 @@ import {
ArrowsClockwiseIcon,
CheckCircleIcon,
CheckIcon,
CopyIcon,
} from "@phosphor-icons/react";
import { useRouter } from "next/navigation";
import { useCallback, useState } from "react";
@@ -340,11 +339,26 @@ export function ChatMessage({
size="icon"
onClick={handleCopy}
aria-label="Copy message"
className="p-1"
>
{copied ? (
<CheckIcon className="size-4 text-green-600" />
) : (
<CopyIcon className="size-4 text-zinc-600" />
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="size-3 text-zinc-600"
>
<rect width="14" height="14" x="8" y="8" rx="2" ry="2" />
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />
</svg>
)}
</Button>
)}

View File

@@ -1,7 +1,6 @@
import { cn } from "@/lib/utils";
import { useEffect, useRef, useState } from "react";
import { AIChatBubble } from "../AIChatBubble/AIChatBubble";
import { ChatLoader } from "../ChatLoader/ChatLoader";
export interface ThinkingMessageProps {
className?: string;
@@ -9,7 +8,9 @@ export interface ThinkingMessageProps {
export function ThinkingMessage({ className }: ThinkingMessageProps) {
const [showSlowLoader, setShowSlowLoader] = useState(false);
const [showCoffeeMessage, setShowCoffeeMessage] = useState(false);
const timerRef = useRef<NodeJS.Timeout | null>(null);
const coffeeTimerRef = useRef<NodeJS.Timeout | null>(null);
useEffect(() => {
if (timerRef.current === null) {
@@ -18,11 +19,21 @@ export function ThinkingMessage({ className }: ThinkingMessageProps) {
}, 8000);
}
if (coffeeTimerRef.current === null) {
coffeeTimerRef.current = setTimeout(() => {
setShowCoffeeMessage(true);
}, 10000);
}
return () => {
if (timerRef.current) {
clearTimeout(timerRef.current);
timerRef.current = null;
}
if (coffeeTimerRef.current) {
clearTimeout(coffeeTimerRef.current);
coffeeTimerRef.current = null;
}
};
}, []);
@@ -37,16 +48,16 @@ export function ThinkingMessage({ className }: ThinkingMessageProps) {
<div className="flex min-w-0 flex-1 flex-col">
<AIChatBubble>
<div className="transition-all duration-500 ease-in-out">
{showSlowLoader ? (
<ChatLoader />
{showCoffeeMessage ? (
<span className="inline-block animate-shimmer bg-gradient-to-r from-neutral-400 via-neutral-600 to-neutral-400 bg-[length:200%_100%] bg-clip-text text-transparent">
This could take a few minutes, grab a coffee
</span>
) : showSlowLoader ? (
<span className="inline-block animate-shimmer bg-gradient-to-r from-neutral-400 via-neutral-600 to-neutral-400 bg-[length:200%_100%] bg-clip-text text-transparent">
Taking a bit more time...
</span>
) : (
<span
className="inline-block bg-gradient-to-r from-neutral-400 via-neutral-600 to-neutral-400 bg-clip-text text-transparent"
style={{
backgroundSize: "200% 100%",
animation: "shimmer 2s ease-in-out infinite",
}}
>
<span className="inline-block animate-shimmer bg-gradient-to-r from-neutral-400 via-neutral-600 to-neutral-400 bg-[length:200%_100%] bg-clip-text text-transparent">
Thinking...
</span>
)}

View File

@@ -157,12 +157,21 @@ const config = {
backgroundPosition: "-200% 0",
},
},
loader: {
"0%": {
boxShadow: "0 0 0 0 rgba(0, 0, 0, 0.25)",
},
"100%": {
boxShadow: "0 0 0 30px rgba(0, 0, 0, 0)",
},
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
"fade-in": "fade-in 0.2s ease-out",
shimmer: "shimmer 2s ease-in-out infinite",
loader: "loader 1s infinite",
},
transitionDuration: {
"2000": "2000ms",

View File

@@ -216,6 +216,7 @@ Below is a comprehensive list of all available blocks, categorized by their prim
| [AI Text Summarizer](block-integrations/llm.md#ai-text-summarizer) | A block that summarizes long texts using a Large Language Model (LLM), with configurable focus topics and summary styles |
| [AI Video Generator](block-integrations/fal/ai_video_generator.md#ai-video-generator) | Generate videos using FAL AI models |
| [Bannerbear Text Overlay](block-integrations/bannerbear/text_overlay.md#bannerbear-text-overlay) | Add text overlay to images using Bannerbear templates |
| [Claude Code](block-integrations/llm.md#claude-code) | Execute tasks using Claude Code in an E2B sandbox |
| [Code Generation](block-integrations/llm.md#code-generation) | Generate or refactor code using OpenAI's Codex (Responses API) |
| [Create Talking Avatar Video](block-integrations/llm.md#create-talking-avatar-video) | This block integrates with D-ID to create video clips and retrieve their URLs |
| [Exa Answer](block-integrations/exa/answers.md#exa-answer) | Get an LLM answer to a question informed by Exa search results |

View File

@@ -0,0 +1,67 @@
# Claude Code Execution
## What it is
The Claude Code block executes complex coding tasks using Anthropic's Claude Code AI assistant in a secure E2B sandbox environment.
## What it does
This block allows you to delegate coding tasks to Claude Code, which can autonomously create files, install packages, run commands, and build complete applications within a sandboxed environment. Claude Code can handle multi-step development tasks and maintain conversation context across multiple turns.
## How it works
When activated, the block:
1. Creates or connects to an E2B sandbox (a secure, isolated Linux environment)
2. Installs the latest version of Claude Code in the sandbox
3. Optionally runs setup commands to prepare the environment
4. Executes your prompt using Claude Code, which can:
- Create and edit files
- Install dependencies (npm, pip, etc.)
- Run terminal commands
- Build and test applications
5. Extracts all text files created/modified during execution
6. Returns the response and files, optionally keeping the sandbox alive for follow-up tasks
The block supports conversation continuation through three mechanisms:
- **Same sandbox continuation** (via `session_id` + `sandbox_id`): Resume on the same live sandbox
- **Fresh sandbox continuation** (via `conversation_history`): Restore context on a new sandbox if the previous one timed out
- **Dispose control** (`dispose_sandbox` flag): Keep sandbox alive for multi-turn conversations
## Inputs
| Input | Description |
|-------|-------------|
| E2B Credentials | API key for the E2B platform to create the sandbox. Get one at [e2b.dev](https://e2b.dev/docs) |
| Anthropic Credentials | API key for Anthropic to power Claude Code. Get one at [Anthropic's website](https://console.anthropic.com) |
| Prompt | The task or instruction for Claude Code to execute. Claude Code can create files, install packages, run commands, and perform complex coding tasks |
| Timeout | Sandbox timeout in seconds (default: 300). Set higher for complex tasks. Note: Only applies when creating a new sandbox |
| Setup Commands | Optional shell commands to run before executing Claude Code (e.g., installing dependencies) |
| Working Directory | Working directory for Claude Code to operate in (default: /home/user) |
| Session ID | Session ID to resume a previous conversation. Leave empty for new conversations |
| Sandbox ID | Sandbox ID to reconnect to an existing sandbox. Required when resuming a session |
| Conversation History | Previous conversation history to restore context on a fresh sandbox if the previous one timed out |
| Dispose Sandbox | Whether to dispose of the sandbox after execution (default: true). Set to false to continue conversations later |
## Outputs
| Output | Description |
|--------|-------------|
| Response | The output/response from Claude Code execution |
| Files | List of text files created/modified during execution. Each file includes path, relative_path, name, and content fields |
| Conversation History | Full conversation history including this turn. Use to restore context on a fresh sandbox |
| Session ID | Session ID for this conversation. Pass back with sandbox_id to continue the conversation |
| Sandbox ID | ID of the sandbox instance (null if disposed). Pass back with session_id to continue the conversation |
| Error | Error message if execution failed |
## Possible use case
**API Documentation to Full Application:**
A product team wants to quickly prototype applications based on API documentation. They create an agent that:
1. Uses Firecrawl to fetch API documentation from a URL
2. Passes the docs to Claude Code with a prompt like "Create a web app that demonstrates all the key features of this API"
3. Claude Code builds a complete application with HTML/CSS/JS frontend, proper error handling, and example API calls
4. The Files output is used with GitHub blocks to push the generated code to a new repository
The team can then iterate on the application by passing the sandbox_id and session_id back to Claude Code with refinement requests like "Add authentication" or "Improve the UI", and Claude Code will modify the existing files in the same sandbox.
**Multi-turn Development:**
A developer uses Claude Code to scaffold a new project:
- Turn 1: "Create a Python FastAPI project with user authentication" (dispose_sandbox=false)
- Turn 2: Uses the returned session_id + sandbox_id to ask "Add rate limiting middleware"
- Turn 3: Continues with "Add comprehensive tests"
Each turn builds on the previous work in the same sandbox environment.

View File

@@ -523,6 +523,62 @@ Summarizing lengthy research papers or articles to quickly grasp the main points
---
## Claude Code
### What it is
Execute tasks using Claude Code in an E2B sandbox. Claude Code can create files, install tools, run commands, and perform complex coding tasks autonomously.
### How it works
<!-- MANUAL: how_it_works -->
When activated, the block:
1. Creates or connects to an E2B sandbox (a secure, isolated Linux environment)
2. Installs the latest version of Claude Code in the sandbox
3. Optionally runs setup commands to prepare the environment
4. Executes your prompt using Claude Code, which can create/edit files, install dependencies, run terminal commands, and build applications
5. Extracts all text files created/modified during execution
6. Returns the response and files, optionally keeping the sandbox alive for follow-up tasks
The block supports conversation continuation through three mechanisms:
- **Same sandbox continuation** (via `session_id` + `sandbox_id`): Resume on the same live sandbox
- **Fresh sandbox continuation** (via `conversation_history`): Restore context on a new sandbox if the previous one timed out
- **Dispose control** (`dispose_sandbox` flag): Keep sandbox alive for multi-turn conversations
<!-- END MANUAL -->
### Inputs
| Input | Description | Type | Required |
|-------|-------------|------|----------|
| prompt | The task or instruction for Claude Code to execute. Claude Code can create files, install packages, run commands, and perform complex coding tasks. | str | No |
| timeout | Sandbox timeout in seconds. Claude Code tasks can take a while, so set this appropriately for your task complexity. Note: This only applies when creating a new sandbox. When reconnecting to an existing sandbox via sandbox_id, the original timeout is retained. | int | No |
| setup_commands | Optional shell commands to run before executing Claude Code. Useful for installing dependencies or setting up the environment. | List[str] | No |
| working_directory | Working directory for Claude Code to operate in. | str | No |
| session_id | Session ID to resume a previous conversation. Leave empty for a new conversation. Use the session_id from a previous run to continue that conversation. | str | No |
| sandbox_id | Sandbox ID to reconnect to an existing sandbox. Required when resuming a session (along with session_id). Use the sandbox_id from a previous run where dispose_sandbox was False. | str | No |
| conversation_history | Previous conversation history to continue from. Use this to restore context on a fresh sandbox if the previous one timed out. Pass the conversation_history output from a previous run. | str | No |
| dispose_sandbox | Whether to dispose of the sandbox immediately after execution. Set to False if you want to continue the conversation later (you'll need both sandbox_id and session_id from the output). | bool | No |
### Outputs
| Output | Description | Type |
|--------|-------------|------|
| error | Error message if execution failed | str |
| response | The output/response from Claude Code execution | str |
| files | List of text files created/modified by Claude Code during this execution. Each file has 'path', 'relative_path', 'name', and 'content' fields. | List[FileOutput] |
| conversation_history | Full conversation history including this turn. Pass this to conversation_history input to continue on a fresh sandbox if the previous sandbox timed out. | str |
| session_id | Session ID for this conversation. Pass this back along with sandbox_id to continue the conversation. | str |
| sandbox_id | ID of the sandbox instance. Pass this back along with session_id to continue the conversation. This is None if dispose_sandbox was True (sandbox was disposed). | str |
### Possible use case
<!-- MANUAL: use_case -->
**API Documentation to Full Application**: A product team wants to quickly prototype applications based on API documentation. They fetch API docs with Firecrawl, pass them to Claude Code with a prompt like "Create a web app that demonstrates all the key features of this API", and Claude Code builds a complete application with HTML/CSS/JS frontend, proper error handling, and example API calls. The Files output can then be pushed to GitHub.
**Multi-turn Development**: A developer uses Claude Code to scaffold a new project iteratively - Turn 1: "Create a Python FastAPI project with user authentication" (dispose_sandbox=false), Turn 2: Uses the returned session_id + sandbox_id to ask "Add rate limiting middleware", Turn 3: Continues with "Add comprehensive tests". Each turn builds on the previous work in the same sandbox environment.
**Automated Code Review and Fixes**: An agent receives code from a PR, sends it to Claude Code with "Review this code for bugs and security issues, then fix any problems you find", and Claude Code analyzes the code, makes fixes, and returns the corrected files ready to commit.
<!-- END MANUAL -->
---
## Code Generation
### What it is

View File

@@ -2,34 +2,20 @@
* [What is the AutoGPT Platform?](what-is-autogpt-platform.md)
## Using the Platform
## Getting Started
* [Getting Started (Cloud)](getting-started-cloud.md)
* [AutoPilot](autopilot.md)
* [Agent Builder Guide](agent-builder-guide.md)
* [Agent Library](agent-library.md)
* [Marketplace](marketplace.md)
* [Scheduling & Triggers](scheduling-and-triggers.md)
* [Templates](templates.md)
* [Credits & Billing](credits-and-billing.md)
* [Integrations & Credentials](integrations-and-credentials.md)
* [Data Flow & Execution](data-flow-and-execution.md)
* [Sharing & Exporting Agents](sharing-and-exporting.md)
## Self-Hosting
* [Setting Up AutoGPT (Self-Host)](getting-started.md)
* [Setting Up Auto-GPT (Local Host)](getting-started.md)
* [AutoGPT Platform Installer](installer.md)
* [Advanced Setup](advanced_setup.md)
## Tutorials
* [Create a Basic Agent](create-basic-agent.md)
* [Edit an Agent](edit-agent.md)
* [Delete an Agent](delete-agent.md)
* [Download & Import an Agent](download-agent-from-marketplace-local.md)
* [Create a Basic Agent](create-basic-agent.md)
* [Submit an Agent to the Marketplace](submit-agent-to-marketplace.md)
## Advanced Setup
* [Advanced Setup](advanced_setup.md)
## Building Blocks
* [Agent Blocks Overview](agent-blocks.md)

View File

@@ -1,105 +0,0 @@
# Agent Builder Guide
## Overview
The Agent Builder is the visual editor where you design and build agents by connecting blocks together on a canvas. Each block is a single action — like generating text, calling an API, or processing data — and you wire them together to create automated workflows.
**URL:** [platform.agpt.co/build](https://platform.agpt.co/build)
## The Builder Interface
When you open the builder, you'll see:
- **Canvas**: The main workspace where you place and connect blocks
- **Blocks Menu**: A panel on the left-hand side where you browse and search for blocks
- **Save Button**: Save your agent with a title and description
## Working with Blocks
### Types of Blocks
Blocks fall into three categories:
| Type | Description |
|------|-------------|
| **Input Blocks** | Define what information the agent needs when it runs. These become the input fields users fill in when starting a task. Types include text inputs, file inputs, and more. |
| **Action Blocks** | Perform operations — AI text generation, image creation, API calls, data processing, and hundreds of integrations with external platforms. |
| **Output Blocks** | Define what the agent returns as its result. These become the visible output when a task completes. |
Input and output blocks define the **schema** of your agent — they determine what users see when running the agent. All other blocks inside the agent are internal and not exposed to the user at runtime.
{% hint style="info" %}
There is also a special type of input block: **Trigger Blocks**. These allow your agent to be activated by external events via webhooks rather than manual input. See [Scheduling & Triggers](scheduling-and-triggers.md) for details.
{% endhint %}
### Adding Blocks
1. Open the **Blocks menu** on the left-hand side of the builder
2. Browse categories or use the search bar to find a specific block
3. Click on a block to add it to the canvas
There are hundreds of blocks available, integrating with many platforms and services.
### Connecting Blocks
Blocks have **input pins** and **output pins**. Pins are typed — they handle specific data types like text, numbers, files, and more.
To connect blocks:
1. Click on an **output pin** of one block
2. Drag to an **input pin** of another block (or simply click the output pin, then click the input pin)
3. A **connection line** will appear between the two pins
When the agent runs, data flows along these connections. This is visually represented by a **coloured bead** that slides along the connection line from the output pin to the input pin.
### Configuring Blocks
Many blocks have settings you can configure directly on the block. For example:
- The **AI Text Generator** block lets you choose which language model to use
- **Integration blocks** may require credentials (see [Integrations & Credentials](integrations-and-credentials.md))
- Some blocks allow you to hardcode values on their input pins instead of connecting them to other blocks
If a block requires a credential you haven't connected yet, a **credential bar** will appear prompting you to add it (via OAuth, API key, or username/password depending on the service).
## Saving Your Agent
To save your agent:
- Press **Ctrl+S**, or
- Click the **Save button** in the builder
When saving, you can provide a **title** and **description** for the agent. This is a local save to your personal library. For publishing to the public marketplace, see [Publishing to the Marketplace](marketplace.md#publishing-an-agent).
{% hint style="info" %}
There is currently no draft vs. saved state — saving an agent immediately updates it in your library.
{% endhint %}
## Navigating Back to Your Library
After saving, click the **Agents** button in the navigation bar to return to your library and find your agent.
## Editing an Existing Agent
To open an existing agent in the builder:
1. Go to your **Agent Library** (click **Agents** in the nav bar)
2. Click on the agent you want to edit
3. Click the **three dots** menu (⋯) on the far right-hand side of the screen
4. Select **Edit Agent**
This will open the agent in the builder with all its existing blocks and connections.
## Error Handling
When a block fails during execution, it produces data on its **error pin**. As the agent creator, you decide how to handle errors:
- **Surface the error**: Connect the error pin to an output block so the error is returned as the agent's result
- **Handle gracefully**: Connect the error pin to other blocks that provide fallback behaviour, ensuring the agent continues working even when individual blocks encounter problems
## Tips
- **Name your input and output blocks clearly** — these names become the labels users see when running your agent
- **Test incrementally** — save and run your agent frequently as you build to catch issues early
- **Use the search** in the blocks menu to quickly find what you need among hundreds of available blocks
- **Check the pin types** — connections only work between compatible pin types

View File

@@ -1,93 +0,0 @@
# Agent Library
## Overview
The Agent Library is your personal collection of agents. From here you can run agents, view task history, set up schedules and triggers, edit agents, and manage your collection.
**URL:** [platform.agpt.co/library](https://platform.agpt.co/library)
**Access:** Click the **Agents** button in the navigation bar.
## Library View
Your library displays all of your agents, including agents you've built yourself and agents you've added from the marketplace. Each agent shows its name and key information at a glance.
### Favouriting Agents
To keep important agents easy to find, click the **heart icon** on any agent to favourite it. Favourited agents are pinned to the top of your library.
{% hint style="info" %}
There is currently no folder system for organising agents. Use favourites to pin your most-used agents to the top.
{% endhint %}
## Agent Detail View
Click on any agent to open its detail view. This screen provides:
- **Tasks**: A full history of every time this agent has been executed
- **Scheduled**: A list of active schedules for this agent
- **Templates**: Saved input configurations for quick re-runs
These are accessible via tabs on the left-hand side of the agent screen (e.g., `Tasks 286 | Scheduled 1 | Templates 0`).
### Agent Actions Menu
Click the **three dots** (⋯) on the far right-hand side of the agent screen to access:
| Action | Description |
|--------|-------------|
| **Edit Agent** | Opens the agent in the builder for editing |
| **Delete Agent** | Permanently removes the agent from your library |
| **Export Agent to File** | Downloads the agent as a file you can share with others |
## Running an Agent
To run an agent manually:
1. Open the agent from your library
2. Click **New Task**
3. Fill in the required input fields
4. Click **Start Task**
The task will begin executing immediately. You can watch the progress and view the results once it completes.
{% hint style="warning" %}
If the agent uses **trigger blocks** instead of standard input blocks, the **New Task** button is replaced with **New Trigger**. See [Scheduling & Triggers](scheduling-and-triggers.md) for details.
{% endhint %}
## Viewing Task Results
Every time an agent runs, it creates a **task**. To view task results:
1. Open the agent from your library
2. In the left-hand pane, browse the list of completed tasks
3. Click on a task to view its details
A task detail view shows:
- **Inputs**: The values that were provided when the task started
- **Outputs**: The results the agent produced
- **Cost**: The total credit cost for this task execution, displayed at the top of the task
You can also **share a task** by copying its URL, which allows others to view the task output directly.
## Uploading an Agent
To import an agent from a file:
1. Go to your Agent Library
2. Click **Upload Agent** at the top
3. Select the agent file from your computer
The agent will be added to your library and can be run or edited like any other agent. This is useful for importing agents shared by other users outside of the marketplace.
## Deleting an Agent
1. Open the agent from your library
2. Click the **three dots** (⋯) on the far right
3. Select **Delete Agent**
4. Confirm the deletion when prompted
{% hint style="danger" %}
Deleting an agent is permanent and cannot be undone. Make sure you want to remove it before confirming.
{% endhint %}

View File

@@ -1,62 +0,0 @@
# AutoPilot
## Overview
AutoPilot is your AI assistant built directly into the AutoGPT Platform. It can perform virtually any action on the platform through natural conversation — from running agents to generating images, conducting research, and even building entire agents for you.
## Accessing AutoPilot
AutoPilot is always available by clicking the **Home** button in the top-left of the navigation bar, or by navigating directly to [platform.agpt.co](https://platform.agpt.co).
## What AutoPilot Can Do
AutoPilot has access to the full power of the platform. Here's what it can help you with:
### Run Agents
Ask AutoPilot to run any agent in your library. It will handle filling in the inputs and executing the task for you.
### Build Agents
Describe the workflow you want and AutoPilot will create an agent for you in the builder. You can also ask it to edit your existing agents or modify agents you've added from the marketplace.
### Browse the Marketplace
Ask AutoPilot to find agents for a specific use case and it will search the marketplace for you.
### Execute Blocks Directly
AutoPilot can run individual blocks without building a full agent, giving it direct access to around **400 tools and counting**. This means you can:
- **Conduct research** with Perplexity
- **Generate images** with the latest image models
- **Edit pictures** using AI image editing blocks
- **Generate videos** using video generation blocks
- **Run any model** on inference services like Replicate
- **Make custom HTTP requests** to any API
- **Write and execute code**, including delegating coding tasks to Claude Code
### Manage Your Library
AutoPilot can help you manage your agent library, view task results, set up schedules, and more.
## Tips for Using AutoPilot
- **Be specific**: The more detail you provide, the better AutoPilot can assist you. Instead of "make me an agent", try "build me an agent that takes a blog topic as input, generates an outline with Claude, then writes the full article".
- **Iterate**: You can refine results by asking follow-up questions or requesting changes.
- **Explore capabilities**: If you're unsure whether AutoPilot can do something, just ask — it has access to a vast number of tools through the platform's block system.
## AutoPilot vs. the Agent Builder
AutoPilot and the Agent Builder are two ways to achieve the same things on the platform. AutoPilot can do anything the builder can — including creating and editing full, reusable agents — and it can also run blocks directly without building an agent first.
| | AutoPilot | Agent Builder |
|---|-----------|---------------|
| **Who is it for** | Everyone — no technical knowledge required | Technical users with a grasp of visual programming |
| **Best for** | Natural language interaction, doing things fast, and accessing blocks directly | Hands-on visual control over exactly how an agent is wired together |
| **How it works** | Conversational — describe what you want | Visual — drag, drop, and connect blocks on a canvas |
| **Can build agents** | Yes — describe what you want and it builds the agent for you | Yes — you build the agent manually |
| **Can edit agents** | Yes — including agents from the marketplace | Yes — full visual editing |
| **Block access** | Can run any block directly without building an agent | Blocks must be connected into an agent workflow |
Choose whichever approach suits you. Many users use both — AutoPilot for speed, and the builder when they want fine-grained visual control over their workflow.

View File

@@ -1,55 +0,0 @@
# Credits & Billing
## Overview
The AutoGPT Platform uses a credit system to manage usage. Credits are consumed when blocks execute during agent runs. This guide explains how credits work, how pricing is determined, and how to monitor your spending.
{% hint style="info" %}
The platform is currently in a **pre-release closed beta**. Pricing is subject to change.
{% endhint %}
## How Credits Work
Credits are consumed on a **per-block-run** basis. Each time a block executes during an agent run, it costs a certain number of credits. The price of a block covers its compute, development, and operational costs — there are no separate charges for infrastructure or API usage.
### Block Pricing
Block prices vary depending on the block:
- **Fixed-price blocks**: Some blocks have a flat price regardless of how they are configured (e.g., basic data processing blocks)
- **Variable-price blocks**: Some blocks have a price that changes based on the settings you choose. For example, the **AI Text Generator** block's price changes depending on which large language model you select
{% hint style="info" %}
The current pricing system charges a flat rate per model for AI blocks — you are **not** charged per token.
{% endhint %}
Users are not charged for anything else on the platform beyond block execution. There are no subscription fees, storage fees, or platform access fees.
## Checking Your Balance
Your credit balance is displayed in the **top-right corner** of the screen at all times, visible from any page on the platform.
## Viewing Task Costs
To see how many credits a specific agent run consumed:
1. Go to your [Agent Library](agent-library.md)
2. Open the agent
3. Click on a completed task in the left-hand pane
4. The **total credit cost** for that task is displayed at the top of the task detail view
{% hint style="info" %}
There is no centralised ledger for browsing all credit spend across your account. Credit costs are available on a per-task basis within each agent.
{% endhint %}
## Running Out of Credits
There are no hard limits on usage beyond your credit balance. If your credit balance reaches zero:
- **Running agents will stop executing**
- **Scheduled agents will not run** until credits are replenished
- You will need to add more credits to continue using the platform
## Adding Credits
Credits can be added through the platform. Navigate to your profile settings to manage your credit balance.

View File

@@ -1,84 +0,0 @@
# Data Flow & Execution
## Overview
Understanding how agents execute is key to building effective workflows. This guide explains how data flows through an agent, what determines execution order, and how to work with lists and errors.
## Execution Order
Agent execution is entirely **determined by data flow**. There is no separate execution flow or ordering mechanism — data dependencies are the only thing that controls which block runs when.
### How It Works
1. **Execution starts from input blocks**, which yield their data when the agent is triggered (either manually or via a trigger/schedule)
2. The next block to run is whichever block has **all of its connected inputs satisfied**
3. This continues block by block, following the data flow, until all blocks have executed or an unhandled error occurs
4. **Output blocks** collect the final results and present them to the user
### Required Inputs
A block will only execute when:
- All **connected input pins** have received data from their upstream blocks
- All **required input pins** have values — either from a connection or from a hardcoded value set directly on the block
This means you can have blocks that don't depend on each other execute in any order, while blocks that depend on the output of another block will always wait.
## Working with Pins
### Pin Types
Input and output pins are typed. Common types include:
- **Text**: String values
- **Number**: Numeric values
- **File**: File uploads or downloads
- **List**: Arrays of items
- **Boolean**: True/false values
- **Object**: Structured data
Connections can only be made between compatible pin types.
### Data Flow Visualisation
When an agent is running, you can see data moving through the workflow in real time. Data flow is represented by a **coloured bead** that slides along each connection line from the output pin to the input pin, giving you a clear visual of what's happening.
## Working with Lists
Blocks can handle list data in flexible ways:
- **Outputting lists**: Some blocks produce a list of items as their output. You can choose to receive the full list as a single output or receive individual items one at a time.
- **Iterating over lists**: You can send a list into a block that iterates through its contents, yielding each item one by one. This is useful for processing each item in a list independently.
This makes it straightforward to build agents that process batches of data — for example, fetching a list of URLs and then processing each one through an AI block.
## Error Handling
When a block fails during execution, it does **not** automatically stop the entire agent. Instead:
1. The failed block produces data on its **error pin**
2. What happens next depends on how you've wired the agent
### Handling Errors Gracefully
You have full control over error handling through the block connections:
- **Surface the error**: Connect the error pin to an output block to return the error as part of the agent's result. This is useful for debugging or when you want users to see what went wrong.
- **Handle and continue**: Connect the error pin to other blocks that provide fallback behaviour. For example, retry with different settings, use a default value, or route to an alternative workflow path.
- **Ignore the error**: If the error pin is not connected, the error data is simply not propagated. Downstream blocks that depend on the failed block's normal output pins will not execute (since their inputs won't be satisfied).
{% hint style="info" %}
Building robust agents means thinking about what happens when things go wrong. Consider connecting error pins to output blocks during development so you can see any issues, then add proper error handling once your agent is working.
{% endhint %}
## Execution Summary
| Concept | How It Works |
|---------|-------------|
| **Execution order** | Determined entirely by data flow — blocks run when all inputs are ready |
| **Starting point** | Input blocks yield data first |
| **Ending point** | Output blocks collect final results |
| **Parallel execution** | Blocks with no dependencies on each other can execute in any order |
| **Error handling** | Failed blocks yield data on their error pin — you decide what to do with it |
| **Lists** | Can be processed as a whole or iterated item by item |
| **Visual feedback** | Coloured beads slide along connection lines during execution |

View File

@@ -1,79 +0,0 @@
# Getting Started with AutoGPT (Cloud)
## Introduction
This guide will get you up and running on the hosted AutoGPT Platform at [platform.agpt.co](https://platform.agpt.co). No installation, Docker, or API keys required — just sign up and start building.
{% hint style="info" %}
Looking to self-host instead? See the [Self-Hosting Guide](getting-started.md).
{% endhint %}
## Creating Your Account
1. Navigate to [platform.agpt.co](https://platform.agpt.co)
2. Click **Sign Up** and create your account
3. Once signed in, you'll land on the **AutoPilot** home screen
That's it — you're ready to go. The cloud platform comes with built-in credits and pre-configured API keys for services like OpenAI and Replicate, so you can start using AI blocks immediately without providing your own keys.
## Platform Navigation
The AutoGPT Platform has four main areas, accessible from the navigation bar at the top of every screen:
| Nav Button | URL | Description |
|------------|-----|-------------|
| **Home** | [platform.agpt.co](https://platform.agpt.co) | AutoPilot — your AI assistant for the platform |
| **Build** | [platform.agpt.co/build](https://platform.agpt.co/build) | Agent Builder — visual editor for creating agents |
| **Agents** | [platform.agpt.co/library](https://platform.agpt.co/library) | Your Agent Library — all your saved agents |
| **Marketplace** | [platform.agpt.co/marketplace](https://platform.agpt.co/marketplace) | Community marketplace for discovering agents |
Your **credit balance** is displayed in the top-right corner of the screen at all times. Your **profile menu** (top-right avatar) gives you access to account settings, integrations, and agent publishing.
## What to Do First
Here are three great ways to get started:
### Option 1: Chat with AutoPilot
Click **Home** in the nav bar to open AutoPilot. You can ask it to do almost anything on the platform — browse the marketplace, run agents, build agents, generate images, conduct research, and more. It's the fastest way to experience the platform.
### Option 2: Add an Agent from the Marketplace
1. Click **Marketplace** in the nav bar
2. Browse or search for an agent that interests you
3. Click on an agent to view its details
4. Click **Add to Library**
5. Navigate to **Agents** to find it in your library
6. Click on the agent and press **New Task** to run it
### Option 3: Build Your Own Agent
1. Click **Build** in the nav bar to open the Agent Builder
2. Open the Blocks menu on the left-hand side
3. Add an **Input Block**, an **AI Text Generator Block**, and an **Output Block**
4. Connect them together by dragging between their pins
5. Press **Ctrl+S** or click the save button to save your agent
6. Navigate to **Agents** to find and run it
For a detailed walkthrough, see [Creating a Basic Agent](create-basic-agent.md).
## Key Concepts
Before diving deeper, here are the core concepts you'll encounter:
- **Agent**: An automated workflow you design to perform specific tasks. Agents are made up of connected blocks.
- **Block**: A single action within an agent — such as generating text with AI, sending an email, or looking up data. There are hundreds of blocks integrating with many platforms.
- **Task**: A single execution of an agent. When you run an agent with a set of inputs, that creates a task. You can view the task's inputs, outputs, and credit cost.
- **AutoPilot**: Your AI assistant that can perform any action on the platform through natural conversation.
- **Marketplace**: A public library of community-built agents you can add to your own library.
- **Credits**: The currency used to run blocks. Each block has its own price. Your balance is shown in the top-right corner.
## Next Steps
| Guide | Description |
|-------|-------------|
| [AutoPilot](autopilot.md) | Learn what AutoPilot can do for you |
| [Agent Builder Guide](agent-builder-guide.md) | Master the visual agent builder |
| [Agent Library](agent-library.md) | Manage your agents, tasks, and schedules |
| [Marketplace](marketplace.md) | Discover and share agents |
| [Credits & Billing](credits-and-billing.md) | Understand the credit system |

View File

@@ -1,67 +0,0 @@
# Integrations & Credentials
## Overview
Many blocks on the AutoGPT Platform integrate with external services like Google, GitHub, Linear, Twitter, and more. These integrations require credentials — such as OAuth connections, API keys, or username/password pairs — to access your accounts on those services.
This guide explains how credentials work on the platform, how to add them, and how to manage them.
## How Credentials Work
### Platform-Provided Credentials
On the cloud-hosted platform at [platform.agpt.co](https://platform.agpt.co), many credentials are **provided by default**. Services like OpenAI, Anthropic, and Replicate are pre-configured, so you can use AI blocks and many other features without providing your own API keys.
### User-Provided Credentials
For services tied to your personal accounts — such as Google, Linear, GitHub, or Twitter — you'll need to connect your own credentials. This only needs to be done **once per service per account**. After connecting, all agents that use that service will automatically have access.
## Adding Credentials
Credentials are added **in context** — when you encounter a block that needs them, rather than from a central setup page.
### When Building an Agent
If you add a block to the builder that requires a credential you haven't connected yet, a **credential bar** will appear on the block prompting you to add it.
### When Running an Agent
If you run an agent (including marketplace agents) that requires credentials, one of the input fields will be the credential selector. This only appears for services you haven't connected yet.
### Credential Types
Depending on the service, you'll be prompted to authenticate in one of three ways:
| Type | Description | Example Services |
|------|-------------|------------------|
| **OAuth** | Click to authorise via the service's login page | Google, GitHub, Twitter |
| **API Key** | Paste your API key from the service's dashboard | Linear, OpenAI (if self-hosting) |
| **Username & Password** | Enter your account credentials | Varies by service |
{% hint style="info" %}
You only need to connect a credential **once per service**. For example, after adding your Linear API key, every agent that uses Linear blocks will have access automatically.
{% endhint %}
## Managing Credentials
### Viewing Connected Integrations
To view and manage your connected integrations:
1. Click your **profile picture** in the top-right corner
2. Select **Integrations**
3. You'll see a list of all your connected integrations
**URL:** [platform.agpt.co/profile/integrations](https://platform.agpt.co/profile/integrations)
### Removing a Credential
From the integrations page, you can **browse** your list of connected integrations and **delete** any you no longer need.
{% hint style="warning" %}
You **cannot add** new integrations from the integrations management screen. New credentials are only added when you encounter a block or agent that requires them.
{% endhint %}
## Self-Hosted Credentials
If you're running the platform locally via self-hosting, you'll need to provide your own API keys for all services, including AI providers. These are configured in the `autogpt_platform/backend/.env` file. See the [Self-Hosting Guide](getting-started.md) for details.

View File

@@ -1,82 +0,0 @@
# Marketplace
## Overview
The AutoGPT Marketplace is a public library of agents created by the community and by the AutoGPT team. You can discover agents for a wide range of use cases, add them to your library with one click, and even publish your own agents for others to use.
**URL:** [platform.agpt.co/marketplace](https://platform.agpt.co/marketplace)
**Access:** Click **Marketplace** in the navigation bar.
## Browsing the Marketplace
The marketplace allows you to:
- **Search** for agents by name or keyword
- **Browse by category** to explore different use cases
- **View agent details** by clicking on any agent
### Agent Detail Page
Each agent in the marketplace has a dedicated page that includes:
- **Title and description** of what the agent does
- **Creator information** — who built the agent and a link to their other agents
- **Video** (when available) — a demonstration of the agent in action
- **Output showcase** (when available) — examples of the agent's results
- **Instructions** — guidance on how to run the agent and what to expect
## Adding an Agent to Your Library
When you find an agent you want to use:
1. Click on the agent to open its marketplace page
2. Click the **Add to Library** button
The agent will immediately appear in your [Agent Library](agent-library.md), where you can run it, schedule it, or edit it.
{% hint style="info" %}
If you already have the agent in your library, the button changes to **See Runs**, which takes you directly to that agent in your library.
{% endhint %}
### For Self-Hosted Users
If you are not logged into the cloud platform and are instead self-hosting, you will see a **Download** button instead of "Add to Library". This downloads the agent as a file which you can import into your local instance. See [Download & Import an Agent](download-agent-from-marketplace-local.md) for details.
## Publishing an Agent
You can publish your own agents to the marketplace for the community to discover and use.
### How to Publish
1. Click your **profile picture** in the top-right corner of any screen
2. Select **Publish an Agent**
3. Choose the agent you want to publish from your list of recent agents
4. Fill out the publishing form:
| Field | Description |
|-------|-------------|
| **Title** | The name of your agent as it will appear in the marketplace |
| **Subheader** | A short tagline for your agent |
| **Slug** | A URL-friendly identifier (e.g., `resume-rewriter`) |
| **Thumbnail Images** | Upload images — the first image becomes the marketplace thumbnail |
| **YouTube Video Link** | Optional — a video demonstrating your agent |
| **Category** | Select the most relevant category for your agent |
| **Description** | A detailed explanation of what your agent does |
| **Agent Output Demo** | Optional — a short video showing the agent's results in action |
| **Instructions** | Explain to users how to run this agent and what to expect |
| **Recommended Schedule** | Suggest when users should run this agent for best results |
5. Click **Submit for Review**
### Review Process
After submission, a member of the AutoGPT team will review your agent against curation standards. If approved, your agent will be published to the marketplace and visible to all users.
{% hint style="info" %}
Creator monetisation (earning money from your published agents) is planned but not yet implemented.
{% endhint %}
## Editing Marketplace Agents
When you add a marketplace agent to your library, you get your own copy. You are free to edit it in the builder just like any agent you've created yourself. Your changes only affect your copy — the original marketplace listing is not modified.

View File

@@ -1,96 +0,0 @@
# Scheduling & Triggers
## Overview
AutoGPT agents can be run on demand, on a recurring schedule, or automatically in response to external events via webhooks. This guide covers both scheduling and trigger-based execution.
## Scheduling an Agent
Scheduling lets you run an agent automatically on a recurring basis with pre-configured inputs.
### Setting Up a Schedule
1. Go to your [Agent Library](agent-library.md) and open the agent you want to schedule
2. Click **New Task** (the same button used for manual runs)
3. Fill in all the input fields with the values you want the agent to use
4. At the bottom of the input form, you'll see two buttons: **Start Task** and **Schedule Task**
5. Click **Schedule Task**
### Configuring the Schedule
The schedule configuration screen allows you to set:
| Setting | Description |
|---------|-------------|
| **Schedule Name** | A descriptive name for this schedule |
| **Repeats** | Frequency — e.g., Weekly |
| **Repeats On** | Select specific days (individual days, weekdays, weekends, or select all) |
| **At** | The time of day to run (hour and minute) |
| **Timezone** | Schedule runs in your local timezone, displayed on screen |
**Example:** Run "Keyword SEO Expert" every weekday at 9:00 AM CST.
### Managing Schedules
After creating a schedule, you can view and manage it on your agent's detail page:
- Open the agent from your library
- Select the **Scheduled** tab on the left-hand side
- The tab shows the count of active schedules (e.g., `Scheduled 1`)
## Triggers (Webhook-Based Execution)
Triggers allow external services or your own code to start an agent automatically by sending data to a webhook URL.
### How Triggers Work
Unlike standard input blocks, **trigger blocks** are a special type of input block. When you add a trigger block to your agent in the builder, the agent's execution model changes — instead of being started manually, it waits for incoming webhook events.
### Setting Up a Trigger
#### Step 1: Add a Trigger Block in the Builder
1. Open the [Agent Builder](agent-builder-guide.md)
2. From the blocks menu, add a **trigger block** to your agent
3. Connect it to the rest of your workflow like any other input block
4. Save your agent
#### Step 2: Configure the Trigger in Your Library
1. Go to your [Agent Library](agent-library.md) and open the agent
2. Click **New Trigger** (this replaces the "New Task" button for trigger-based agents)
3. Give the trigger a **name** and **description**
#### Step 3: Copy the Webhook URL
Once the trigger is created, you'll see a status panel:
> **Trigger Status**
>
> Status: **Active**
>
> This trigger is ready to be used. Use the Webhook URL below to set up the trigger connection with the service of your choosing.
>
> **Webhook URL:** `https://backend.agpt.co/api/integrations/generic_webhook/webhooks/...`
Copy this webhook URL and provide it to the external platform or code that will be sending events to trigger your agent.
{% hint style="info" %}
Trigger-based agents cannot be started manually with the "New Task" button. The only way to execute them is by sending data to the webhook URL.
{% endhint %}
### Example Use Cases
- **GitHub webhook**: Trigger an agent whenever a pull request is opened
- **Payment processor**: Run an agent when a new payment is received
- **Form submission**: Process data when a user submits a form on your website
- **Custom integration**: Send data from any service that supports webhooks
## Schedule vs. Trigger
| | Schedule | Trigger |
|---|----------|---------|
| **How it starts** | Automatically at configured times | When an external event sends data to the webhook URL |
| **Input source** | Pre-configured when the schedule is created | Provided by the incoming webhook payload |
| **Use case** | Recurring tasks with fixed inputs (daily reports, weekly summaries) | Event-driven tasks (new PR opened, form submitted, payment received) |
| **Setup** | Through the "New Task" → "Schedule Task" flow | Through trigger blocks in the builder + "New Trigger" in the library |

View File

@@ -1,48 +0,0 @@
# Sharing & Exporting Agents
## Overview
There are several ways to share agents and their outputs with others on the AutoGPT Platform. This guide covers all the available options.
## Sharing Options
### Share Task Output via URL
Every completed task has a unique URL that can be shared with others. To share a task result:
1. Go to your [Agent Library](agent-library.md) and open the agent
2. Click on the completed task you want to share
3. Copy the task URL from your browser's address bar
4. Send the URL to anyone — they can view the task's inputs and outputs directly
### Publish to the Marketplace
The most visible way to share an agent is to publish it to the [Marketplace](marketplace.md). Published agents are discoverable by all platform users and go through a review process by the AutoGPT team.
See [Publishing an Agent](marketplace.md#publishing-an-agent) for the full guide.
### Export as a File
For sharing agents privately — without publishing to the marketplace — you can export an agent as a file:
1. Go to your [Agent Library](agent-library.md) and open the agent
2. Click the **three dots** (⋯) on the far right
3. Select **Export Agent to File**
4. The agent file will download to your computer
You can then send this file to anyone via email, messaging, or any other method.
### Import a Shared File
To import an agent file someone has shared with you:
1. Go to your [Agent Library](agent-library.md)
2. Click **Upload Agent** at the top
3. Select the agent file
4. The agent will be added to your library
## Teams & Collaboration
{% hint style="info" %}
Team and organisation features are not yet available on the platform. Currently, the only way to share agents privately between users is through the file export/import process described above.
{% endhint %}

View File

@@ -1,43 +0,0 @@
# Templates
## Overview
Templates are saved input configurations that let you re-run an agent with the same inputs quickly. Instead of filling in every input field each time, you can save a set of inputs as a template and trigger it again with one click.
## Creating a Template
Templates are created from previously completed tasks:
1. Go to your [Agent Library](agent-library.md) and open the agent
2. Find a completed task in the left-hand pane whose inputs you want to reuse
3. Click the **three dots** (⋯) on the task
4. Select **Save as Template**
5. Give the template a **name** and **description**
6. Click **Save**
The template captures all the input values that were used for that task.
## Using a Template
To run an agent using a saved template:
1. Open the agent from your library
2. Switch to the **Templates** tab on the left-hand side
3. Click on the template you want to use
4. The agent will run with the saved input values
## Managing Templates
Templates are listed under the **Templates** tab on your agent's detail page. The tab shows the count of available templates (e.g., `Templates 3`).
## When to Use Templates
Templates are particularly useful for:
- **Recurring tasks**: When you run an agent regularly with the same inputs (e.g., a daily SEO report for the same keyword)
- **Standard configurations**: When you've found input settings that work well and want to reuse them
- **Quick re-runs**: When you want to repeat a successful task without re-entering all the inputs
{% hint style="info" %}
Templates save the **input values** only — they do not save any settings or modifications to the agent itself. If you edit the agent's blocks or connections, existing templates will still use the saved input values with the updated agent design.
{% endhint %}

View File

@@ -78,5 +78,5 @@ This strategy allows us to share previously closed-source components, fostering
## Ready to Get Started?
* **Cloud:** Read the [Getting Started (Cloud)](getting-started-cloud.md) guide to start using the hosted platform immediately.
* **Self-Host:** Read the [Self-Hosting Guide](getting-started.md) to run the platform on your own infrastructure.
* Read the [Getting Started docs](getting-started.md) to self-host.
* [Join the waitlist](https://agpt.co/waitlist) for the cloud-hosted beta.