Skip to content
Closed
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
21 changes: 21 additions & 0 deletions config/apps/theme/bootstrap.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# @package apps.theme
# Bootstrap 5 look approximated with design tokens (no Bootstrap CSS needed).
name: bootstrap
import_url: ""
tokens:
color-bg: "#ffffff"
color-surface: "#f8f9fa"
color-fg: "#212529"
color-muted: "#6c757d"
color-border: "#dee2e6"
color-primary: "#0d6efd"
color-on-primary: "#ffffff"
color-accent: "#198754"
color-danger: "#dc3545"
color-neutral: "#6c757d"
color-btn-fg: "#ffffff"
font-family: "system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif"
font-heading: "inherit"
font-size-base: "16px"
radius: "0.375rem"
space: "8px"
22 changes: 22 additions & 0 deletions config/apps/theme/challenging_font.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# @package apps.theme
# Faithful port of the former todo `challenging_font` appearance variant:
# the default palette rendered in a hard-to-read script face.
name: challenging_font
import_url: ""
tokens:
color-bg: "#ffffff"
color-surface: "#f4f5f7"
color-fg: "#1a1a1a"
color-muted: "#6b7280"
color-border: "#dfe1e6"
color-primary: "#2563eb"
color-on-primary: "#ffffff"
color-accent: "#2563eb"
color-danger: "#b22222"
color-neutral: "#36454f"
color-btn-fg: "#ffffff"
font-family: "'Brush Script MT', cursive"
font-heading: "inherit"
font-size-base: "16px"
radius: "8px"
space: "8px"
21 changes: 21 additions & 0 deletions config/apps/theme/dark.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# @package apps.theme
# Faithful port of the former todo `dark_theme` appearance variant.
name: dark
import_url: ""
tokens:
color-bg: "#ffffff"
color-surface: "#000000"
color-fg: "#ffffff"
color-muted: "#9ca3af"
color-border: "#ffffff"
color-primary: "#000000"
color-on-primary: "#ffffff"
color-accent: "#ffffff"
color-danger: "#ffffff"
color-neutral: "#ffffff"
color-btn-fg: "#000000"
font-family: "'Times New Roman', serif"
font-heading: "inherit"
font-size-base: "16px"
radius: "8px"
space: "8px"
23 changes: 23 additions & 0 deletions config/apps/theme/default.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# @package apps.theme
# Shared design tokens. Selected globally with `apps/theme=<name>` or per app
# with `apps.<app>.theme=<name>`. Every `tokens` entry becomes a CSS custom
# property (`color-primary` -> `--color-primary`) consumed via `var(--...)`.
name: default
import_url: ""
tokens:
color-bg: "#ffffff"
color-surface: "#f4f5f7"
color-fg: "#1a1a1a"
color-muted: "#6b7280"
color-border: "#dfe1e6"
color-primary: "#2563eb" # add / primary action
color-on-primary: "#ffffff"
color-accent: "#2563eb" # save action
color-danger: "#b22222" # remove / destructive
color-neutral: "#36454f" # edit / secondary
color-btn-fg: "#ffffff" # text on edit/remove/save buttons
font-family: "'Times New Roman', serif"
font-heading: "inherit"
font-size-base: "16px"
radius: "8px"
space: "8px"
21 changes: 21 additions & 0 deletions config/apps/theme/material.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# @package apps.theme
# Material Design look approximated with design tokens (no MUI CSS needed).
name: material
import_url: ""
tokens:
color-bg: "#ffffff"
color-surface: "#f5f5f5"
color-fg: "#212121"
color-muted: "#757575"
color-border: "#e0e0e0"
color-primary: "#6200ee"
color-on-primary: "#ffffff"
color-accent: "#03dac6"
color-danger: "#b00020"
color-neutral: "#757575"
color-btn-fg: "#ffffff"
font-family: "'Roboto', 'Helvetica Neue', sans-serif"
font-heading: "inherit"
font-size-base: "16px"
radius: "4px"
space: "8px"
21 changes: 21 additions & 0 deletions config/apps/theme/mono.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# @package apps.theme
# Faithful port of the former todo `black_and_white` appearance variant.
name: mono
import_url: ""
tokens:
color-bg: "#000000"
color-surface: "#ffffff"
color-fg: "#000000"
color-muted: "#000000"
color-border: "#000000"
color-primary: "#000000"
color-on-primary: "#ffffff"
color-accent: "#000000"
color-danger: "#000000"
color-neutral: "#000000"
color-btn-fg: "#ffffff"
font-family: "'Times New Roman', serif"
font-heading: "inherit"
font-size-base: "16px"
radius: "8px"
space: "8px"
21 changes: 21 additions & 0 deletions config/apps/theme/solarized.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# @package apps.theme
# Solarized Light (Ethan Schoonover palette).
name: solarized
import_url: ""
tokens:
color-bg: "#fdf6e3"
color-surface: "#eee8d5"
color-fg: "#657b83"
color-muted: "#93a1a1"
color-border: "#93a1a1"
color-primary: "#268bd2"
color-on-primary: "#fdf6e3"
color-accent: "#2aa198"
color-danger: "#dc322f"
color-neutral: "#586e75"
color-btn-fg: "#fdf6e3"
font-family: "'Inter', system-ui, sans-serif"
font-heading: "inherit"
font-size-base: "16px"
radius: "8px"
space: "8px"
3 changes: 3 additions & 0 deletions config/apps/todo/layout/default.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# @package apps.todo
# Structure only -- no colors/fonts (those come from the shared theme).
layout: default
4 changes: 4 additions & 0 deletions config/apps/todo/layout/kanban_board.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# @package apps.todo
# Structure only -- colors/fonts come from the shared theme. Select with
# `apps/todo/layout=kanban_board`.
Comment on lines +2 to +3
layout: kanban_board
144 changes: 144 additions & 0 deletions dev.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
"""
Copyright (c) Meta Platforms, Inc. and affiliates.
All rights reserved.
This source code is licensed under the license found in the
LICENSE file in the root directory of this source tree.
"""
Comment thread
aaronsmulktis marked this conversation as resolved.

"""
Hot-reloading development server.

./scripts/dev.sh # or:
uv run uvicorn dev:app --reload --reload-dir src --reload-dir config --port 5001

Save any file under ``src/`` or ``config/`` and the server restarts and the
browser refreshes itself. Not for evals -- use ``launch.py`` for those.

## Why this file exists rather than `serve(reload=True)`

``serve()`` already defaults to ``reload=True``; ``launcher.launch_apps()``
turns it off deliberately, and just flipping it back gives a server that
reloads into a broken state. Two reasons, both worth knowing before touching
this:

**1. The app is configured imperatively, after import.** ``launch.py`` exposes
a bare ``app``; the routes and config are attached later by
``initialize_routes_and_configure_task()`` inside ``launcher.launch()``.
Uvicorn's reloader restarts the worker and re-imports the app from its import
string, which runs none of that -- a freshly imported ``app`` has 7 routes and
no ``.config`` at all, so the reloaded server would 404 every app and crash on
the first request that touches configuration.

This module fixes that by doing the configuration at *module level*, so
re-importing it is enough to rebuild a fully wired app.

**2. Re-seeding a database that already has rows fails.** ``set_environment()``
loops ``insert()`` over the configured rows without clearing first, so a second
run hits unique-constraint violations, and the code editor bails out with
"folder already exists. This is undesired!!!" instead of re-seeding. Since the
reloader restarts the process against the same directory on disk, every reload
after the first would hit both.

So this module wipes its own database directory before initializing. Each
reload therefore starts from the configured seed state -- which is also what an
eval episode does, so what you are looking at matches what an agent would get.
The cost is that runtime state does not survive a reload: pinned apps, theme
mode and any todos you ticked all reset. That is a fair trade while iterating
on rendering, and the wrong one if you are debugging state, in which case run
``launch.py`` normally.
"""
import os
import shutil
import tempfile
from pathlib import Path

from hydra import compose, initialize
from starlette.routing import WebSocketRoute

from fasthtml.live_reload import LiveReloadJs, live_reload_ws

from open_apps.apps.start_page.main import (
app,
initialize_routes_and_configure_task,
)

REPO_ROOT = Path(__file__).resolve().parent

#: Deliberately OUTSIDE the repository.
#:
#: The code editor seeds .py files into its database directory on every
#: startup. Put that anywhere the reloader can see and you get an infinite
#: loop: seed -> watcher fires -> restart -> seed. Observed exactly that with
#: the directory at ./.dev, and note it happened even though uvicorn reported
#: watching only src/ and config/ -- so confining it by reload-dir alone is not
#: something to rely on. A path outside the tree cannot be watched by accident.
DEV_DIR = Path(tempfile.gettempdir()) / "openapps-dev"

#: Empty by default so this boots whatever the current branch's default config
#: is. Hardcoding a layout or theme here would tie the dev server to config
#: groups that only exist on some branches, and it would fail to start on the
#: ones where they do not.
#:
#: Pass a space-separated list of Hydra overrides to pick something else:
#: OPENAPPS_DEV_OVERRIDES="apps/theme=dark apps/todo/layout=kanban_board"
DEFAULT_OVERRIDES = ""
Comment on lines +82 to +84


def _build() -> None:
"""Compose config, reset state, and wire up the app. Runs on every import."""
overrides = os.environ.get("OPENAPPS_DEV_OVERRIDES", DEFAULT_OVERRIDES).split()

# Wipe first. set_environment() cannot re-seed over existing rows, and the
# reloader hands us the same directory every time.
shutil.rmtree(DEV_DIR, ignore_errors=True)
DEV_DIR.mkdir(parents=True, exist_ok=True)

with initialize(version_base=None, config_path="config"):
config = compose(
config_name="config",
overrides=[f"logs_dir={DEV_DIR}", "use_wandb=False", *overrides],
)
Path(config.databases_dir).mkdir(parents=True, exist_ok=True)
initialize_routes_and_configure_task(config.apps)

_install_live_reload()

print(f"\n dev server ready — overrides: {' '.join(overrides) or '(none)'}")
print(f" state dir: {DEV_DIR} (wiped on every reload)\n")


def _install_live_reload() -> None:
"""Add the ``/live-reload`` socket and the client snippet.

FastHTML ships this as ``FastHTMLWithLiveReload``, a ``FastHTML`` subclass.
Swapping the app's class just for development would mean the thing you are
testing is not the thing that runs in an eval, so the two halves are
attached to the existing app instead: a websocket route, and a script that
reloads the page when that socket drops and comes back.

Guarded because this module is re-imported on every reload in some
execution paths, and a duplicate route or a second copy of the script would
accumulate.
"""
if not any(getattr(r, "path", None) == "/live-reload" for r in app.routes):
app.routes.append(WebSocketRoute("/live-reload", live_reload_ws))
if not any("live-reload" in str(h) for h in app.hdrs):
app.hdrs.append(LiveReloadJs())


_build()


if __name__ == "__main__":
# Convenience path so `python dev.py` works as well as the uvicorn command.
# Passes the module import string, not the object, because the reloader
# needs something it can re-import in the restarted worker.
import uvicorn

uvicorn.run(
"dev:app",
host="localhost",
port=int(os.environ.get("OPENAPPS_DEV_PORT", 5001)),
reload=True,
reload_dirs=[str(REPO_ROOT / "src"), str(REPO_ROOT / "config")],
)
24 changes: 24 additions & 0 deletions scripts/dev.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/usr/bin/env bash
#
# Hot-reloading dev server. Save anything under src/ or config/ and the server
# restarts and the browser refreshes itself.
#
# ./scripts/dev.sh
# OPENAPPS_DEV_PORT=5005 ./scripts/dev.sh
# OPENAPPS_DEV_OVERRIDES="apps/theme=meta_dark" ./scripts/dev.sh
#
Comment thread
aaronsmulktis marked this conversation as resolved.
# See dev.py for why this cannot just be `serve(reload=True)`.
set -euo pipefail
cd "$(dirname "${BASH_SOURCE[0]}")/.."

PORT="${OPENAPPS_DEV_PORT:-5001}"
echo " http://localhost:${PORT}"

# The import string, not the app object: the reloader restarts the worker and
# has to be able to re-import it.
exec uv run uvicorn dev:app \
--reload \
--reload-dir src \
--reload-dir config \
--host localhost \
--port "${PORT}"
Loading
Loading