Quick demo app, providing access to documentation, a text editor, and terminal in one place, with an AI agent to tie them together.
Local web app: Flask backend, HTML templates (Jinja2), CSS under app/static/css/, and React (JSX) built with Vite into app/static/dist/.
Pydantic AI provides a framework for for AI agents tool calling and output parsing, prompts under app/prompts/.
e2b provides cloud terminal instances.
Firecrawl fetches static versions or pages that are not iframe embeddable.
Requires Python 3.10+ and Node.js 18+.
From the project root:
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txtcd frontend
npm install
npm run buildThe build writes app/static/dist/assets/main.js and main.css. Flask serves them as /static/dist/assets/.... Run npm run build whenever you change frontend/src/ before loading the app via Flask (unless you use the Vite dev server; see below).
Flask reads configuration from the process environment. The following API keys should be set in a .env file or by export.
SECRET_KEY: signing key for sessions and cookies. Defaults to a dev-only value; set explicitly in production.OPENAI_API_KEY: required for a agent calls (app/ai_calls.py). Pydantic-ai uses the provider’s usual credential environment variables, and can be modified to use an alternate model provider.FIRECRAWL_API_KEY: used for fetching documentation sites that are not iframe-embeddable.E2B_API_KEY: required to use e2b instances as remote terminals.- altenatively, ensure that
TERMINAL_PROVIDERis set tolocal(default).
- altenatively, ensure that
This application does not install pip requirements (or anything else) on local terminals, so requirements should be installed there as well.
flask --app app:create_app run --debug --host 127.0.0.1Use --host 127.0.0.1 (not 0.0.0.0) so the sidebar IPython terminal (WebSocket + PTY) is not exposed on your LAN. The terminal runs shell code as the same OS user as Flask.
Alternatives:
python run.pypython -m app- Rebuild on save + Flask: in
frontend/, runnpm run build:watchin one terminal and Flask in another. Refresh the browser after each build. - Vite dev server:
cd frontend && npm run dev(e.g. http://localhost:5173/).vite.config.jsproxies/apiand/wstohttp://127.0.0.1:5000— run Flask on port 5000 while using Vite for HMR. The Viteindex.htmlonly mounts React; for the full Jinja layout, use Flask.
Optional and deployment-related environment variables are read in app/__init__.py. SECRET_KEY, OPENAI_API_KEY, FIRECRAWL_API_KEY, and E2B_API_KEY are summarized under Environment above.
FLASK_SESSION_COOKIE_SECURE:1or0— set session cookiesSecurewhen the site is served over HTTPS (default0).SESSION_LIFETIME_DAYS: signed session lifetime in days (default31).FLASK_SESSION_TYPE: Flask-Session backend type (defaultfilesystem).FLASK_SESSION_FILE_DIR: directory for filesystem-backed sessions (defaults under the app instance path).
TERMINAL_PROVIDER:local(default),e2b, ordisabled. See Terminal providers.TERMINAL_ALLOW_REMOTE:1or0— relax localhost-only assumptions when you intend remote access (default0).E2B_TEMPLATE_NAME: E2B sandbox template id (defaultinteractive-docs-ipython).E2B_ALLOW_INTERNET_ACCESS:1or0— allow outbound network from E2B sandboxes (default1in application code). Set0when dependencies are baked into the template and you want no runtime egress. Server-side pip installs into the live sandbox require1.
TERMINAL_REQUIRE_TOKEN:1or0— require a minted token on/ws/terminal(defaults1fore2b,0forlocal).TERMINAL_ENFORCE_ORIGIN:1or0— validateOriginon the terminal WebSocket (default1).TERMINAL_WS_TOKEN_TTL_SECONDS: one-time token lifetime in seconds (default60).
TERMINAL_MAX_SESSION_SECONDS: maximum WebSocket session length (default3600;0disables).TERMINAL_IDLE_TIMEOUT_SECONDS: idle disconnect for E2B sessions (default300seconds;0disables).TERMINAL_IDLE_TIMEOUT_SECONDS_LOCAL: idle disconnect for local PTY sessions only (default1200;0disables).TERMINAL_MAX_INBOUND_BYTES: maximum inbound WebSocket message size (default65536).TERMINAL_MAX_PIP_REQUIREMENTS_LINES: max lines accepted for a pip requirements batch (default50).TERMINAL_MAX_PIP_REQUIREMENT_LINE_CHARS: max characters per requirement line (default200).TERMINAL_PIP_INSTALL_TIMEOUT_SECONDS: timeout for server-side pip installs into an E2B sandbox (default600).
- The lower-right sidebar runs
ipythonin the browser over xterm.js, a/ws/terminalWebSocket, and a PTY.ipythonis listed inrequirements.txt. - Unix only (macOS / Linux). On Windows the socket responds with an unsupported message instead of spawning a PTY.
- Security: arbitrary code execution as the Flask OS user. Treat as localhost-only, single-user tooling; do not expose without isolation, authentication, and hardening.
- Code:
app/terminal_session.py,app/terminal_routes.py.
| Mode | Behavior |
|---|---|
TERMINAL_PROVIDER=local (default) |
PTY + ipython on the same host as Flask. Intended for local development; access is guarded for localhost. |
TERMINAL_PROVIDER=e2b |
Terminal runs inside an E2B sandbox (better fit for deployment). Requires E2B_API_KEY and the e2b package from requirements.txt. |
TERMINAL_PROVIDER=disabled |
Terminal WebSocket is disabled. |
- Install dependencies:
pip install -r requirements.txt(includese2b). - Set
TERMINAL_PROVIDER=e2b,E2B_API_KEY, and optionallyE2B_TEMPLATE_NAME(defaults and tuning: Key Settings).
Stock E2B images may not include ipython. Build a template that installs it so the REPL works without relying on runtime network access:
python e2b/build_template.pyPoint E2B_TEMPLATE_NAME at that template (for example interactive-docs-ipython). After dependencies are baked in, you can run with E2B_ALLOW_INTERNET_ACCESS=0 if you want no outbound network from sandboxes. Application default for that flag is 1 (see Key Settings).
When TERMINAL_PROVIDER=e2b and E2B_ALLOW_INTERNET_ACCESS=1, backend code can install packages into the current browser session’s sandbox via app.terminal_pip.pip_install_requirements_into_session_sandbox (requirements as a list of strings; on success you get exit_code, stdout, stderr, and normalized_requirements). There is no public HTTP route for ad-hoc installs—call this from your chat flow or another trusted server path.
With token and origin checks enabled (defaults favor safety for e2b):
- The client calls
POST /api/terminal/tokenfor a short-lived, one-time token. - The WebSocket connects to
/ws/terminal?token=.... - The server rejects bad
Originheaders and invalid or expired tokens. POST /api/terminal/killcan stop the active sandbox (for example when the terminal UI unmounts); the client does not call it automatically on tab visibility changes.
Environment variables: Key Settings → Terminal WebSocket security.
While the tab is visible, the browser sends periodic {"type":"ping"} messages so the server can refresh idle timers and mitigate some proxy timeouts. Numeric limits are listed under Key Settings → Terminal limits and pip install guards.
The home chat bar sits at the bottom of the main (left) column. It uses MUI X Chat (@mui/x-chat) and Material UI. @mui/x-chat is alpha on npm—pin or upgrade deliberately. It pulls in @mui/icons-material (declared in frontend/package.json).
POST /api/chat— JSON body includesconversationId,message(id,role,parts), and optional editor and documentation URL fields the UI sends (seeapp/chat.py).- Response:
application/x-ndjson; each line is one JSON object (start,text-delta,text-end,ui-state,finish, …) for MUI X Chat’s stream processor. - Model: requests are handled with Pydantic AI in
app/ai_calls.py(OPENAI_API_KEYand related provider env vars per Environment).
app/__init__.py—create_app()app/routes.py— blueprintmainapp/chat.py— blueprintchat, prefix/api;POST /api/chatstreaming NDJSONapp/ai_calls.py— Pydantic AI agent and tools for chatapp/terminal_routes.py— HTTP routes plus WebSocket/ws/terminal(flask-sock)app/templates/— Jinja; React mounts at#main-chat-rootinpage__chat-barapp/static/— CSS and Vite output underapp/static/dist/frontend/— Vite + React source;npm run buildwritesapp/static/dist/