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
20 changes: 10 additions & 10 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,18 @@ __pycache__/
# Generated code — regenerated inside the image by `rbt
# generate`.
backend/api/
web/api/
frontend/api/

# Node sources / deps / caches — we only copy `web/dist/`
# Node sources / deps / caches — we only copy `frontend/dist/`
# into the image.
web/node_modules/
web/ui/
web/src/
web/package.json
web/package-lock.json
web/tsconfig*.json
web/vite.config.ts
web/index.css
frontend/node_modules/
frontend/mcp/
frontend/src/
frontend/package.json
frontend/package-lock.json
frontend/tsconfig*.json
frontend/vite.config.ts
frontend/index.css

# VCS.
.git/
Expand Down
6 changes: 3 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ __pycache__/

# Generated API code (from `rbt generate`).
backend/api/
web/api/
frontend/api/

# Reboot runtime state.
.rbt/
Expand All @@ -21,8 +21,8 @@ web/api/
.pytest_cache/

# Node / web build artifacts.
web/node_modules/
web/dist/
frontend/node_modules/
frontend/dist/

# IDE / editor.
.idea/
Expand Down
29 changes: 17 additions & 12 deletions .rbtrc
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ generate api/
# Tell `rbt generate` where to put generated files.
generate --python=backend/api/

# Generate React bindings for web apps (into "web/api/").
generate --react=web/api
generate --react-extensions
# Generate React bindings for web apps (into "frontend/api/").
generate --react=frontend/api

# Watch if any source files are modified.
dev run --watch=backend/**/*.py
dev run --watch=frontend/dist/**/*.html

# Tell `rbt` that this is a Python application.
dev run --python
Expand All @@ -24,23 +24,28 @@ dev run --application=backend/src/main.py
dev run --default-config=hmr

# Hot Module Replacement (HMR): Vite dev server proxied through Envoy.
# Run Vite in a separate terminal: cd web && npm run dev
# Envoy routes "/__/web/**" to Vite for HMR support.
dev run:hmr --mcp-frontend-host=http://localhost:4444
# Run Vite in a separate terminal: cd frontend && npm run dev
# Envoy routes "/__/frontend/**" to Vite for HMR support.
dev run --frontend-root-path=frontend

# Dist mode: serve pre-built artifacts from "web/dist/" (no Vite HMR).
dev run:hmr --frontend-host=http://localhost:4444

# Dist mode: serve pre-built artifacts from "frontend/dist/" (no Vite HMR).
# Usage: uv run rbt dev run --config=dist
# Requires: cd web && npm run build
dev run:dist --mcp-frontend-host=""
# Requires: cd frontend && npm run build
dev run:dist --frontend-dist-path=frontend/dist

# When expunging, expunge that state we've saved.
dev expunge --application-name=agent-wiki

# Production (`rbt serve run`) settings, used by the Dockerfile
# when deploying to the Reboot Cloud. In production `web/dist/`
# is served from disk, so no `--mcp-frontend-host` is needed
# (that flag is dev-only anyway).
# when deploying to the Reboot Cloud. In production the built
# `frontend/dist/` is served on disk by the static-file server
# (`--frontend-dist-path`); the `--frontend-host` Vite dev server
# is dev-only.
serve run --python
serve run --frontend-dist-path=frontend/dist
serve run --frontend-root-path=frontend
serve run --application-name=agent-wiki
serve run --application=backend/src/main.py

Expand Down
6 changes: 3 additions & 3 deletions BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ filegroup(
".pytest_cache/**/*",
".mypy_cache/**/*",
"backend/api/**/*",
"web/api/**/*",
"web/node_modules/**/*",
"web/dist/**/*",
"frontend/api/**/*",
"frontend/node_modules/**/*",
"frontend/dist/**/*",
],
),
visibility = ["//visibility:public"],
Expand Down
10 changes: 5 additions & 5 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Dockerfile for deploying agent-wiki to Reboot Cloud.
#
# Prerequisite: run `cd web && npm install && npm run build`
# locally before `docker build` so that `web/dist/` contains the
# bundled UIs. This image copies that prebuilt bundle rather
# Prerequisite: run `cd frontend && npm install && npm run build`
# locally before `docker build` so that `frontend/dist/` contains
# the bundled UIs. This image copies that prebuilt bundle rather
# than installing Node and rebuilding it here.
FROM ghcr.io/reboot-dev/reboot-base:1.2.1
FROM ghcr.io/reboot-dev/reboot-base:1.3.0

WORKDIR /app

Expand Down Expand Up @@ -32,7 +32,7 @@ RUN rbt generate
COPY backend/src/ backend/src/

# Copy the prebuilt web bundle.
COPY web/dist/ web/dist/
COPY frontend/dist/ frontend/dist/

# Make the Pydantic API definitions in `api/` and the generated
# Reboot bindings in `backend/api/` both importable. They share
Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,13 @@ for manual fix-ups).
uv sync

# Install web dependencies.
cd web && npm install && cd ..
cd frontend && npm install && cd ..

# Generate API code (Python + React bindings).
uv run rbt generate

# Build the React UIs.
cd web && npm run build && cd ..
cd frontend && npm run build && cd ..
```

The librarian runs on Anthropic's Claude via Pydantic AI, so
Expand Down Expand Up @@ -114,7 +114,7 @@ project directory):
uv run rbt dev run

# Terminal 2: start the Vite dev server for Hot Module Replacement.
cd web && npm run dev
cd frontend && npm run dev
```

Then open <http://localhost:9991> and follow the setup wizard to
Expand Down Expand Up @@ -184,9 +184,9 @@ agent-wiki/
│ ├── main.py # Application entrypoint.
│ └── servicers/wiki.py # User/Wiki/Page/Transcript servicers
│ # plus the Pydantic AI librarian agent.
└── web/
└── frontend/
├── api/ # Generated React bindings.
└── ui/
└── mcp/
├── wiki_view/ # Renders a Wiki's markdown body.
├── page_view/ # Renders a Page's markdown body.
├── transcript_view/ # Renders a raw Transcript.
Expand Down
6 changes: 3 additions & 3 deletions api/agent_wiki/v1/wiki.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ class TranscriptUpdateRequest(Model):
methods=Methods(
show_wiki=UI(
request=None,
path="web/ui/wiki_view",
path="frontend/mcp/wiki_view",
title="Wiki",
description="Open the Wiki viewer UI to "
"render the Wiki's markdown body.",
Expand Down Expand Up @@ -285,7 +285,7 @@ class TranscriptUpdateRequest(Model):
methods=Methods(
show_page=UI(
request=None,
path="web/ui/page_view",
path="frontend/mcp/page_view",
title="Page",
description="Open the Page viewer UI to "
"render the Page's markdown body.",
Expand Down Expand Up @@ -333,7 +333,7 @@ class TranscriptUpdateRequest(Model):
methods=Methods(
show_transcript=UI(
request=None,
path="web/ui/transcript_view",
path="frontend/mcp/transcript_view",
title="Transcript",
description="Open the Transcript viewer "
"UI to read the raw conversation "
Expand Down
10 changes: 3 additions & 7 deletions backend/tests/wiki_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@
)
from pydantic_ai.models.function import AgentInfo, FunctionModel
from reboot.aio.applications import Application
from reboot.aio.auth.oauth_providers import Anonymous
from reboot.aio.tests import OAuthProviderForTest, Reboot
from reboot.aio.tests import Reboot
from servicers import wiki as wiki_module
from servicers.wiki import (
PageServicer,
Expand Down Expand Up @@ -89,15 +88,12 @@ async def asyncSetUp(self) -> None:
await self.rbt.up(
Application(
servicers=APPLICATION_SERVICERS,
oauth=OAuthProviderForTest(Anonymous()),
),
)
self.user_id = "alice"
self.context = self.rbt.create_external_context(
self.context = await self.rbt.create_external_context_as(
name=f"test-{self.id()}",
bearer_token=self.rbt.make_valid_oauth_access_token(
user_id=self.user_id,
),
user_id=self.user_id,
)
# `User` is an auto-constructed state type: in
# production the MCP session's "new session" hook
Expand Down
72 changes: 72 additions & 0 deletions frontend/.tests/type_check.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
#!/bin/bash

# Type-checks this example's frontend end-to-end: it generates the
# typed Reboot clients from the example's own `.rbtrc` using the
# `rbt` CLI, installs the frontend's JavaScript dependencies
# overlaid with the locally built Reboot npm packages, and runs
# `tsc` over the frontend project. This applies the checks a
# developer's editor and the Vite toolchain apply (`strict`,
# `verbatimModuleSyntax`, ...), which the esbuild-based bundling
# steps skip.

set -e # Exit if a command exits with an error.
set -u # Treat expanding an unset variable as an error.
set -x # Echo executed commands to help debug failures.

# The harness copies the whole example into a sandbox and runs us
# from its root, where the top-level `.rbtrc`, `api/`, and
# `pyproject.toml` live. We generate from here and only
# `cd frontend` later, for the type check.
ls -l .rbtrc pyproject.toml frontend/tsconfig.json 2> /dev/null > /dev/null || {
echo "ERROR: could not find the example's files. Invoke this from"
echo "the example's root. Current working directory is '$(pwd)'."
exit 1
}

# Set up the Python environment: the example is a Python project, so
# we generate with the Python `rbt`, which reads the example's own
# `.rbtrc` and emits the Python and React clients in one pass — the
# React one into `frontend/api/`. In a Bazel test we install the
# locally built wheel over the pinned version.
if [[ -n "${REBOOT_WHL_FILE:-}" ]]; then
uv add --no-sync "${SANDBOX_ROOT}${REBOOT_WHL_FILE}"
fi

# Force a fresh virtualenv. A pre-existing `.venv/` has its original
# creation path baked into its `activate` script and console-script
# shebangs, which breaks them when the venv is used from a different
# location; `uv sync` alone can't repair a relocated venv. Nuking
# and re-syncing guarantees the venv lives at the current path.
rm -rf .venv
uv sync
source .venv/bin/activate

# When running in a Bazel test, our `.rbtrc` file ends up in a very
# deep directory structure, which can result in "path too long"
# errors from RocksDB. Explicitly specify a shorter path.
RBT_FLAGS="--state-directory=$(mktemp -d)"

# Generate the clients from the example's `.rbtrc`. The frontend
# imports the generated `frontend/api/` modules, so this must run
# before the type check.
rbt $RBT_FLAGS generate

# Install the frontend's JavaScript dependencies. In a Bazel test we
# overlay the locally built Reboot npm packages with `--no-save` so
# the check exercises the in-repo client rather than a published
# release; `npm install` still resolves the rest (React, Vite, ...)
# from `package.json`.
cd frontend
if [[ -n "${REBOOT_NPM_PACKAGE:-}" ]]; then
npm install --no-save \
"${SANDBOX_ROOT}${REBOOT_NPM_PACKAGE}" \
"${SANDBOX_ROOT}${REBOOT_API_NPM_PACKAGE}" \
"${SANDBOX_ROOT}${REBOOT_WEB_NPM_PACKAGE}" \
"${SANDBOX_ROOT}${REBOOT_REACT_NPM_PACKAGE}"
else
npm install
fi

# Run the frontend's real build command, so we exercise the exact
# command a developer runs.
npm run build
39 changes: 39 additions & 0 deletions frontend/build.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Builds every Reboot UI in this frontend in one shot, so
// `npm run build` keeps working as UIs are added or removed.
//
// `vite.config.ts` knows how to build a *single* target, selected by
// the `RBT_BUILD_TARGET` env var (`mcp:<name>` for one MCP UI, or
// `web` for the SPA). This script is the discover-and-loop around it:
// it finds every `mcp/<name>/index.html`, builds each into
// `dist/mcp/<name>/index.html`, then builds the `web/` SPA into
// `dist/web/` if a `web/index.html` exists.
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
import { build } from "vite";

const __dirname = path.dirname(fileURLToPath(import.meta.url));

// Auto-discover MCP UIs: every `mcp/<name>/` with an `index.html`.
const mcpDir = path.resolve(__dirname, "mcp");
const mcpNames = fs.existsSync(mcpDir)
? fs
.readdirSync(mcpDir)
.filter((name) => fs.existsSync(path.resolve(mcpDir, name, "index.html")))
: [];

// Build each MCP UI. `vite.config.ts` reads the target from
// `RBT_BUILD_TARGET`; we leave Vite's `mode` at its build default
// (`production`) so a `web/.env.production` would be honored.
for (const name of mcpNames) {
console.log(`Building mcp/${name} ...`);
process.env.RBT_BUILD_TARGET = `mcp:${name}`;
await build();
}

// Build the standalone `web/` SPA if it exists.
if (fs.existsSync(path.resolve(__dirname, "web", "index.html"))) {
console.log("Building web ...");
process.env.RBT_BUILD_TARGET = "web";
await build();
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
26 changes: 24 additions & 2 deletions web/ui/page_view/App.tsx → frontend/mcp/page_view/App.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,32 @@
import { type FC } from "react";
import { usePage } from "@api/agent_wiki/v1/wiki_rbt_react";
import { type UsePageApi, usePage } from "@api/agent_wiki/v1/wiki_rbt_react";
import { Markdown } from "../_shared/Markdown";
import css from "./App.module.css";

export const PageView: FC = () => {
const page = usePage();
const { page, isLoading } = usePage();

if (isLoading) {
return (
<div className={css.container}>
<div className={css.loading}>loading...</div>
</div>
);
}

if (page === undefined) {
console.error("No default Page id was available; cannot render.");
return (
<div className={css.container}>
<div className={css.loading}>An error occurred, sorry about that!</div>
</div>
);
}

return <Page page={page} />;
};

const Page: FC<{ page: UsePageApi }> = ({ page }) => {
const { response, isLoading } = page.useGet();

if (isLoading && response === undefined) {
Expand Down
File renamed without changes.
File renamed without changes.
Loading
Loading