feat(frontend): phase 0 foundation — pnpm workspace, tooling reconcile, Go embed handler#162
feat(frontend): phase 0 foundation — pnpm workspace, tooling reconcile, Go embed handler#162AgentWrapper wants to merge 1 commit into
Conversation
…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 SummaryPhase 0 scaffolding: establishes a pnpm workspace at
Confidence Score: 4/5Safe 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
Sequence DiagramsequenceDiagram
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)
Reviews (1): Last reviewed commit: "feat(frontend): phase 0 foundation — pnp..." | Re-trigger Greptile |
| // 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 }); | ||
| } |
There was a problem hiding this comment.
The header comment says "The committed placeholder index.html and the .gitkeep sentinel are preserved" but the clear loop only skips
.gitkeep — index.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).
| // 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 }); | |
| } |
| r = r.Clone(r.Context()) | ||
| r.URL.Path = "/" |
There was a problem hiding this comment.
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.
| r = r.Clone(r.Context()) | |
| r.URL.Path = "/" | |
| r = r.Clone(r.Context()) | |
| r.URL.Path = "/" | |
| r.URL.RawPath = "" |
| "peerDependencies": { | ||
| "react": "19.2.7", | ||
| "react-dom": "19.2.7" | ||
| }, |
There was a problem hiding this comment.
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.
| "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!
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 generatedrouteTree.gen.tsartifact 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/xtermis present as anapps/webdependency but intentionally not wired this session.In scope
Task 0.1 — pnpm workspace + 4 packages
frontend/is now an@aoagents/frontendpnpm 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 ascaffolding liveplaceholder 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 oncore, entry stub (establishes the runtime→core edge).tsconfig.base.json, flateslint.config.js,.prettierrc. Versions pinned;pnpm-lock.yamlcommitted.Task 0.2 — root tooling reconcile (root stays npm)
package.json:web:dev,web:build,web:test,lint:web,format,build:frontend(allpnpm -C frontend …). Backend lint preserved aslint:backend;lintnow runs backend then web.api*/sqlcand theopenapi-typescriptdep untouched.AGENTS.mddocuments the npm-root / pnpm-frontend split and the new layout..gitignore:frontend/node_modules,frontend/**/dist,frontend/**/.tanstack, plus the webuidistcarve-out.Task 0.8 — Go embed handler + build coupling
backend/internal/webui://go:embed dist+Handler()serving the SPA. Unknown non-asset paths fall back toindex.html(so TanStack Router deep links survive a hard refresh); a miss under/assets/*returns 404. Committed placeholderindex.html+.gitkeepso the embed compiles with no frontend build.httpd/router.goas the last-mounted catch-all (GET/HEAD), after all/api/v1/*,/healthz,/readyz,/mux,/shutdownroutes. API 404s stay JSON (they resolve inside the/api/v1subrouter).webui_test.go: index fallback for unknown path, 404 for/assets/missing.js, index for/.build:frontendbuildsao-weband copiesdistinto the embed dir viascripts/copy-webui.mjs.Out of scope (later sessions)
MuxProvider, status shim, xterm wiring.packages/webfrom the legacy repo).frontend/src/landingis left in place (tied to thereact-doctorCI job) and ported intoapps/landinglater.frontend/src/api/schema.tsalso stays (api-drift CI).Verification
pnpm install→pnpm -r typecheck,pnpm lint,ao-webbuild (Tailwind CSS emitted),vitest(2 passed) — all green.pnpm --filter @aoagents/ao-web devboots and serves the stub (curl'd/+ transformed/src/main.tsx).build:frontend→go buildembeds the real bundle; placeholder restored for the commit.cd backend && go build ./... && go vet ./... && go test ./...pass;gofmtclean.🤖 Generated with Claude Code