Skip to content

feat(frontend): phase 0 foundation — pnpm workspace, tooling reconcile, Go embed handler#162

Open
AgentWrapper wants to merge 1 commit into
mainfrom
session/ao-16
Open

feat(frontend): phase 0 foundation — pnpm workspace, tooling reconcile, Go embed handler#162
AgentWrapper wants to merge 1 commit into
mainfrom
session/ao-16

Conversation

@AgentWrapper

Copy link
Copy Markdown
Contributor

Phase 0 foundation for the Go-backed AO web rewrite. Pure scaffolding — establishes the workspace, tooling, and the daemon's static-serving seam. No UI components, design tokens, API client, MuxProvider, or status shim.

Stack choices (locked)

Vite + React 19 + TypeScript + TanStack Router (code-based) + TanStack Query + Tailwind v4.

Why code-based routing over the file-based plugin: the code-based route tree (apps/web/src/router.tsx) keeps routing explicit and reviewable, avoids a generated routeTree.gen.ts artifact in the tree, and skips a build-time codegen step in the Vite pipeline — a better fit while the route surface is small and hand-curated. We can adopt the file-based plugin later if the tree grows.

@xterm/xterm is present as an apps/web dependency but intentionally not wired this session.

In scope

Task 0.1 — pnpm workspace + 4 packages

  • frontend/ is now an @aoagents/frontend pnpm workspace (packageManager: pnpm@9.15.4), replacing the old Electron shell. Removed: frontend/{package-lock.json,tsconfig.json,src/main.ts} (Electron). frontend/ is pnpm-only — never npm/yarn.
  • apps/web (@aoagents/ao-web) — main SPA; boots a scaffolding live placeholder with the TanStack Router root + Query provider wired but empty, Tailwind v4 via @tailwindcss/vite.
  • apps/landing (@aoagents/ao-landing) — empty Vite scaffold.
  • packages/core (@aoagents/core) — pure TS, DOM-free lib (enforces no-React), entry stub.
  • packages/runtime (@aoagents/runtime) — React layer depending on core, entry stub (establishes the runtime→core edge).
  • Shared tsconfig.base.json, flat eslint.config.js, .prettierrc. Versions pinned; pnpm-lock.yaml committed.

Task 0.2 — root tooling reconcile (root stays npm)

  • Thin delegators in root package.json: web:dev, web:build, web:test, lint:web, format, build:frontend (all pnpm -C frontend …). Backend lint preserved as lint:backend; lint now runs backend then web. api*/sqlc and the openapi-typescript dep untouched.
  • AGENTS.md documents the npm-root / pnpm-frontend split and the new layout.
  • .gitignore: frontend/node_modules, frontend/**/dist, frontend/**/.tanstack, plus the webui dist carve-out.

Task 0.8 — Go embed handler + build coupling

  • New backend/internal/webui: //go:embed dist + Handler() serving the SPA. Unknown non-asset paths fall back to index.html (so TanStack Router deep links survive a hard refresh); a miss under /assets/* returns 404. Committed placeholder index.html + .gitkeep so the embed compiles with no frontend build.
  • Wired into httpd/router.go as the last-mounted catch-all (GET/HEAD), after all /api/v1/*, /healthz, /readyz, /mux, /shutdown routes. API 404s stay JSON (they resolve inside the /api/v1 subrouter).
  • webui_test.go: index fallback for unknown path, 404 for /assets/missing.js, index for /.
  • build:frontend builds ao-web and copies dist into the embed dir via scripts/copy-webui.mjs.

Out of scope (later sessions)

  • UI components, design tokens, API client, MuxProvider, status shim, xterm wiring.
  • Porting the legacy reference UI (packages/web from the legacy repo).
  • The existing Next.js landing under frontend/src/landing is left in place (tied to the react-doctor CI job) and ported into apps/landing later. frontend/src/api/schema.ts also stays (api-drift CI).

Verification

  • pnpm installpnpm -r typecheck, pnpm lint, ao-web build (Tailwind CSS emitted), vitest (2 passed) — all green.
  • pnpm --filter @aoagents/ao-web dev boots and serves the stub (curl'd / + transformed /src/main.tsx).
  • build:frontendgo build embeds the real bundle; placeholder restored for the commit.
  • cd backend && go build ./... && go vet ./... && go test ./... pass; gofmt clean.

🤖 Generated with Claude Code

…e, Go embed handler

Establish the foundation for the Go-backed AO web rewrite. Pure scaffolding —
no UI components, design tokens, API client, MuxProvider, or status shim yet.

Task 0.1 — pnpm workspace + 4 packages:
- frontend/ becomes an @aoagents/frontend pnpm workspace (pnpm@9.15.4),
  replacing the old Electron shell (electron package.json/tsconfig/main.ts
  removed; npm package-lock dropped — frontend is pnpm-only).
- apps/web (@aoagents/ao-web): Vite + React 19 + TanStack Router (code-based)
  + TanStack Query + Tailwind v4. Boots a "scaffolding live" placeholder with
  the Router root + Query provider wired but empty.
- apps/landing (@aoagents/ao-landing): empty Vite scaffold (the existing
  Next.js landing under frontend/src/landing is left in place — it is tied to
  the react-doctor CI job — and is ported in a later session).
- packages/core (@aoagents/core): pure TS, DOM-free lib, entry stub only.
- packages/runtime (@aoagents/runtime): React layer depending on core, stub.
- Shared tsconfig.base.json, flat eslint.config.js, .prettierrc. Pinned versions
  + committed pnpm-lock.yaml.

Task 0.2 — root tooling reconcile (root stays npm):
- Thin delegation scripts: web:dev/web:build/web:test, lint:web, format,
  build:frontend, all via pnpm -C frontend. Existing backend lint preserved as
  lint:backend; lint now runs backend then web. api*/sqlc untouched.
- AGENTS.md: documents the npm-root / pnpm-frontend split and new layout.
- .gitignore: frontend node_modules/dist/.tanstack + webui dist carve-out.

Task 0.8 — Go embed handler + build coupling:
- backend/internal/webui: //go:embed dist + Handler() serving the SPA, with
  index.html fallback for unknown paths (TanStack Router deep links survive a
  refresh) and a 404 reserved for missing /assets/*. Committed placeholder
  index.html + .gitkeep so the embed compiles with no frontend build.
- Wired into httpd/router.go as the last-mounted catch-all (GET/HEAD), after
  all API/health/control/terminal routes; API 404s stay JSON.
- webui_test.go covers index fallback, /assets 404, and root.
- build:frontend builds ao-web and copies dist into the embed dir via
  scripts/copy-webui.mjs.

Verified: pnpm install + typecheck + lint + web build + vitest green; vite dev
boots and renders the stub; cd backend && go build ./... && go vet ./... &&
go test ./... pass.
@greptile-apps

greptile-apps Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Phase 0 scaffolding: establishes a pnpm workspace at frontend/ (4 packages), reconciles root npm and pnpm tooling, and wires a Go //go:embed static-file handler as the last-mounted catch-all after all API routes. No UI components or functional runtime changes are included.

  • apps/web boots a React 19 + TanStack Router + Tailwind v4 stub; packages/core and packages/runtime are empty stubs establishing the dependency edge.
  • backend/internal/webui adds the SPA fallback handler: known static assets are served directly, unknown non-asset paths fall back to index.html (deep-link support), and /assets/* misses return 404.
  • scripts/copy-webui.mjs copies the Vite build output into the Go embed directory; a placeholder index.html is committed so the embed compiles without a prior frontend build.

Confidence Score: 4/5

Safe to merge with the copy-script fix applied; the Go embed handler, router wiring, and workspace scaffolding are all correct.

The Go embed handler and chi router integration are straightforward and well-tested. The main concern is in copy-webui.mjs: the clear loop only preserves .gitkeep despite the comment claiming index.html is also preserved — after build:frontend the tracked placeholder is silently replaced, making it easy to accidentally commit the Vite-built file. The webui_test.go CI check would catch that if it slipped through, but the script behaviour contradicts its own documentation. The remaining issues (stale RawPath on the URL clone, exact peer dep version for React) are minor and don't affect runtime correctness.

scripts/copy-webui.mjs — the clear loop and its comment need to agree on whether index.html is preserved

Important Files Changed

Filename Overview
scripts/copy-webui.mjs Copy script only skips .gitkeep during the clear loop but the header comment claims index.html is also preserved — the placeholder is actually deleted and replaced by the Vite build, so git shows it as modified after build:frontend runs.
backend/internal/webui/webui.go New SPA embed handler; logic is correct for the three cases (root, asset miss, unknown path). One minor issue: r.URL.RawPath is not cleared when rewriting to "/", leaving an inconsistent URL state.
backend/internal/webui/webui_test.go Three tests cover the three handler branches (SPA fallback, asset 404, root). Tests depend on the placeholder index.html containing "frontend not built".
backend/internal/httpd/router.go mountWebUI added as the last-mounted catch-all; mount order and GET/HEAD-only registration are correct.
frontend/packages/runtime/package.json Peer dependency specifier for react/react-dom is an exact version ("19.2.7") rather than a range; will produce peer dep warnings for any consumer on a patch release above 19.2.7.
frontend/apps/web/src/router.tsx Code-based TanStack Router tree; root route + index route constructed correctly; module augmentation for type inference present.
frontend/apps/web/vite.config.ts Vite + Vitest config unified in one file via vitest/config re-export; test environment set to node (no DOM needed for route-tree smoke test).
frontend/eslint.config.js Flat ESLint config for the pnpm workspace; legacy src/ excluded correctly; lean for phase 0 with React plugins deferred.
.gitignore frontend/**/dist/ and .tanstack ignores added; embed dist/ un-ignores structured correctly with sentinel carve-outs for .gitkeep and index.html.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Chi as chi Router
    participant API as /api/v1/* subrouter
    participant Ctrl as health/control/mux routes
    participant WebUI as webui.Handler()
    participant FS as embed.FS (dist/)

    Client->>Chi: GET /api/v1/agents
    Chi->>API: matched → JSON response
    API-->>Client: 200 application/json

    Client->>Chi: GET /healthz
    Chi->>Ctrl: matched → JSON probe
    Ctrl-->>Client: 200

    Client->>Chi: GET /sessions/abc/deep/link
    Chi->>WebUI: "catch-all /*"
    WebUI->>FS: fs.Stat("sessions/abc/deep/link")
    FS-->>WebUI: error (not found)
    WebUI->>WebUI: "rewrite URL.Path = "/""
    WebUI->>FS: fileServer.ServeHTTP → index.html
    FS-->>Client: 200 text/html (SPA shell)

    Client->>Chi: GET /assets/app.abc.js
    Chi->>WebUI: "catch-all /*"
    WebUI->>FS: fs.Stat("assets/app.abc.js")
    FS-->>WebUI: error (not found)
    WebUI-->>Client: 404 (asset miss, not SPA route)
Loading

Reviews (1): Last reviewed commit: "feat(frontend): phase 0 foundation — pnp..." | Re-trigger Greptile

Comment thread scripts/copy-webui.mjs
Comment on lines +21 to +26
// Clear previous build output but keep the .gitkeep sentinel so the directory
// (and the //go:embed dist target) always exists in a fresh checkout.
for (const entry of readdirSync(dest)) {
if (entry === ".gitkeep") continue;
rmSync(join(dest, entry), { recursive: true, force: true });
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 The header comment says "The committed placeholder index.html and the .gitkeep sentinel are preserved" but the clear loop only skips .gitkeepindex.html is deleted and then replaced by whatever cpSync copies from the Vite output. After running build:frontend, git status shows the tracked index.html as modified, making it easy to accidentally stage and commit the Vite-built file. If that happens, webui_test.go's "frontend not built" assertion breaks CI — which is a safety net, but the comment actively misleads about the script's actual behaviour. Fix the comment (or also skip index.html if the intent truly is to preserve the placeholder).

Suggested change
// Clear previous build output but keep the .gitkeep sentinel so the directory
// (and the //go:embed dist target) always exists in a fresh checkout.
for (const entry of readdirSync(dest)) {
if (entry === ".gitkeep") continue;
rmSync(join(dest, entry), { recursive: true, force: true });
}
// Clear previous build output but keep the .gitkeep sentinel and the
// placeholder index.html so the directory (and the //go:embed dist target)
// always exists in a fresh checkout, and so git does not see a modification
// after build:frontend runs.
for (const entry of readdirSync(dest)) {
if (entry === ".gitkeep" || entry === "index.html") continue;
rmSync(join(dest, entry), { recursive: true, force: true });
}

Comment on lines +52 to +53
r = r.Clone(r.Context())
r.URL.Path = "/"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 After rewriting the path to "/", r.URL.RawPath is not cleared. If the original request carried a percent-encoded path (e.g. /foo%2Fbar), RawPath retains that value while Path is "/". http.URL.EscapedPath() — which some middleware and redirect logic calls — would then return the stale encoded value instead of "/". Setting RawPath = "" restores consistency so EscapedPath() falls back to Path.

Suggested change
r = r.Clone(r.Context())
r.URL.Path = "/"
r = r.Clone(r.Context())
r.URL.Path = "/"
r.URL.RawPath = ""

Comment on lines +20 to +23
"peerDependencies": {
"react": "19.2.7",
"react-dom": "19.2.7"
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 The peer dependency version for React is pinned to the exact patch release "19.2.7". Any consumer on 19.2.8+ (or a different 19.x patch) will receive a peer dependency warning from pnpm. A caret range "^19.2.7" (or ">=19") is the conventional choice and matches how the React team ships compatible patch/minor updates.

Suggested change
"peerDependencies": {
"react": "19.2.7",
"react-dom": "19.2.7"
},
"peerDependencies": {
"react": "^19.2.7",
"react-dom": "^19.2.7"
},

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants