Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ RUN apt-get update \

RUN curl -fsSL https://code-server.dev/install.sh | sh

# agent-browser drives a real Chrome (For Testing) for the live browser tab.
# Chrome is downloaded into the runtime user's home so the server can launch it.
RUN curl -fsSL https://deb.nodesource.com/setup_24.x | bash - \
&& apt-get install -y --no-install-recommends nodejs \
&& npm install -g agent-browser \
&& HOME=/home/remote-coder agent-browser install --with-deps \
&& rm -rf /var/lib/apt/lists/*

RUN useradd --create-home --shell /bin/bash remote-coder \
&& mkdir -p \
/app/database \
Expand Down
12 changes: 12 additions & 0 deletions app/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,18 @@ class Settings(BaseSettings):
vscode_max_sessions: int = 4
vscode_extra_args: str = ""

browser_enabled: bool = True
browser_binary: str = "agent-browser"
browser_bind_host: str = "127.0.0.1"
browser_stream_quality: int = 60
browser_stream_max_fps: int = 30
browser_headless: bool = True
browser_start_timeout_seconds: int = 30
browser_session_ttl_seconds: int = 3600
browser_max_sessions: int = 2
browser_allowed_domains: str = ""
browser_extra_args: str = ""

@model_validator(mode="after")
def _derive_vscode_user_data_root(self) -> "Settings":
if not self.vscode_user_data_root:
Expand Down
6 changes: 6 additions & 0 deletions app/core/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,12 @@ class ErrorCode(StrEnum):
VSCODE_START_FAILED = "VSCODE_START_FAILED"
VSCODE_PROXY_FAILED = "VSCODE_PROXY_FAILED"
VSCODE_FORBIDDEN = "VSCODE_FORBIDDEN"
BROWSER_DISABLED = "BROWSER_DISABLED"
BROWSER_START_FAILED = "BROWSER_START_FAILED"
BROWSER_PROXY_FAILED = "BROWSER_PROXY_FAILED"
BROWSER_FORBIDDEN = "BROWSER_FORBIDDEN"
BROWSER_NAVIGATION_FAILED = "BROWSER_NAVIGATION_FAILED"
BROWSER_COMMAND_FAILED = "BROWSER_COMMAND_FAILED"


class FieldError(BaseModel):
Expand Down
12 changes: 12 additions & 0 deletions app/core/i18n.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@
"VSCODE_START_FAILED": "Could not start the VS Code server",
"VSCODE_PROXY_FAILED": "Could not reach the VS Code server",
"VSCODE_FORBIDDEN": "Cross-site request rejected",
"BROWSER_DISABLED": "The browser is disabled",
"BROWSER_START_FAILED": "Could not start the browser",
"BROWSER_PROXY_FAILED": "Could not reach the browser",
"BROWSER_FORBIDDEN": "Cross-site request rejected",
"BROWSER_NAVIGATION_FAILED": "Could not navigate the browser",
"BROWSER_COMMAND_FAILED": "Could not control the browser",
},
"vi": {
"INVALID_CREDENTIALS": "Tên đăng nhập hoặc mật khẩu không đúng",
Expand Down Expand Up @@ -143,6 +149,12 @@
"VSCODE_START_FAILED": "Không thể khởi động VS Code server",
"VSCODE_PROXY_FAILED": "Không thể kết nối tới VS Code server",
"VSCODE_FORBIDDEN": "Từ chối yêu cầu cross-site",
"BROWSER_DISABLED": "Trình duyệt đang bị tắt",
"BROWSER_START_FAILED": "Không thể khởi động trình duyệt",
"BROWSER_PROXY_FAILED": "Không thể kết nối tới trình duyệt",
"BROWSER_FORBIDDEN": "Từ chối yêu cầu cross-site",
"BROWSER_NAVIGATION_FAILED": "Không thể điều hướng trình duyệt",
"BROWSER_COMMAND_FAILED": "Không thể điều khiển trình duyệt",
},
}

Expand Down
8 changes: 7 additions & 1 deletion app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
from app.modules.ai_providers.router import router as ai_providers_router
from app.modules.auth.router import router as auth_router
from app.modules.auth.service import ensure_admin_user
from app.modules.browser import service as browser_service
from app.modules.browser.router import router as browser_router
from app.modules.git.router import router as git_router
from app.modules.health.router import router as health_router
from app.modules.projects.router import router as projects_router
Expand Down Expand Up @@ -66,6 +68,9 @@ async def _reap_idle_sessions(settings: Settings) -> None:
await asyncio.to_thread(
vscode_service.manager.reap_idle, settings.vscode_session_ttl_seconds
)
await asyncio.to_thread(
browser_service.manager.reap_idle, settings.browser_session_ttl_seconds
)


def create_app(settings: Settings | None = None) -> FastAPI:
Expand All @@ -84,7 +89,7 @@ async def lifespan(app: FastAPI):
with suppress(asyncio.CancelledError):
await reaper
await asyncio.to_thread(vscode_service.manager.kill_all)

await asyncio.to_thread(browser_service.manager.kill_all)
app = FastAPI(
title=settings.app_name,
debug=settings.debug,
Expand All @@ -105,6 +110,7 @@ async def _apply_accept_language(request: Request, call_next):
app.include_router(terminal_router, prefix=settings.api_prefix)
app.include_router(terminal_global_router, prefix=settings.api_prefix)
app.include_router(vscode_router, prefix=settings.api_prefix)
app.include_router(browser_router, prefix=settings.api_prefix)
app.include_router(agents_router, prefix=settings.api_prefix)
app.include_router(ai_providers_router, prefix=settings.api_prefix)
app.include_router(ai_chat_router, prefix=settings.api_prefix)
Expand Down
Empty file added app/modules/browser/__init__.py
Empty file.
32 changes: 32 additions & 0 deletions app/modules/browser/force_same_tab.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Injected into every page of the streamed browser session.
//
// The panel is a single viewport with no tab bar, so a native "new tab" would
// navigate somewhere the user cannot see. Keep simple navigations in the
// current tab so clicking a link always shows where it leads.
(function () {
try {
window.addEventListener(
'click',
(event) => {
const anchor =
event.target && event.target.closest ? event.target.closest('a[target]') : null
if (anchor) anchor.target = '_self'
},
true,
)

const nativeOpen = window.open.bind(window)
window.open = function (url, name, features) {
// Popup-style calls (with features, e.g. OAuth login) keep the native
// behavior so page scripts can still use the returned window; only simple
// navigations are redirected into the current tab.
if (url && !features) {
window.location.href = url
return null
}
return nativeOpen(url, name, features)
}
} catch (error) {
void error
}
})()
Loading
Loading