From 014648807f7d5cb5f253a7ef883f3e6e8b81bd0b Mon Sep 17 00:00:00 2001 From: AI_Assistant Date: Fri, 4 Sep 2026 10:13:09 +1000 Subject: [PATCH 1/4] =?UTF-8?q?cmms-embed:=20VITE=5FLIBS=5FSOURCE=3Dstatic?= =?UTF-8?q?=20(not=20off)=20=E2=80=94=20install=20the=20window.kicadLibs?= =?UTF-8?q?=20bridge?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit off leaves libsSource null, so boot.ts never calls installLibsProvider and window.kicadLibs is never installed. A C++ libs-bridge call on schematic open (park site K1, sch_io_pcbjam_lib) then hits undefined -> throws inside a suspending import -> SuspendError -> abort. static installs the bridge (2 built-in example symbols; answers not-found gracefully otherwise), no backend. Standalone-only change; dist/ rebuild, no wasm rebuild. Co-Authored-By: Claude Sonnet 5 --- web/standalone/.env.production | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/web/standalone/.env.production b/web/standalone/.env.production index cd505e5..118ef4d 100644 --- a/web/standalone/.env.production +++ b/web/standalone/.env.production @@ -11,8 +11,13 @@ VITE_API_BASE_URL=/pcb-project-api.php VITE_PROJECT_SOURCE=remote VITE_DOC_SOURCE=api -# No library backend for v1 — the editor boots with empty sym/fp-lib-tables. -VITE_LIBS_SOURCE=off +# No library backend for v1. `static` (not `off`): `off` leaves `libsSource` null so boot.ts +# never calls installLibsProvider → `window.kicadLibs` is never installed → any C++ path that +# resolves a symbol/footprint through the libs bridge on schematic open (park site K1, +# sch_io_pcbjam_lib) calls a method on undefined → throws inside a suspending import → +# SuspendError → abort. `static` installs the bridge backed by 2 built-in example symbols; it +# answers "not found" gracefully for everything else. No backend, no manifest, self-contained. +VITE_LIBS_SOURCE=static # Flat WASM layout, served static by Apache under the mount (no per-tool subdir, # VITE_WASM_MANIFEST intentionally unset). Files: kicad_editor.{js,wasm}, wx.js, From bf1e7a512e2d7e10819315cd9e45be7bcc867f9d Mon Sep 17 00:00:00 2001 From: AI_Assistant Date: Sat, 5 Sep 2026 20:21:17 +1000 Subject: [PATCH 2/4] cmms-embed: kicad AI chat editor-buffer bridge (PCBJam Part 2) web/standalone only, no C++/wasm/shared changes: - lib/cmms-config.ts (new): fetch /api/cmms-config once on boot (dark, aiEnabled, aiEditorKind, csrfToken, entityType, entityId). Absent route / non-2xx / malformed JSON all mean "no widget" - never throws, never blocks boot. Same API base + credentials:"include" as the existing /api/me call. - components/CmmsAiWidget.tsx (new): floating launcher + panel, inert unless aiEnabled. Mounted once in App.tsx alongside VersionBadge. Mirrors render_ai_assistant_editor_embed()'s markup/CSS shape (position, colors) but fixed-position - the draggable/resizable geometry system is CMMS-side static JS this bundle can't import. Follows `dark` from the config fetch, belt-and-braces alongside the existing `?theme=` hand-off (which already wins whenever set). - wasm/collab/ai-bridge.ts (new): the cmms-ai-editor-* postMessage protocol (same shape as the cad-editor / pcb-retrace bridges). get-buffer: yToDoc + docToFile on the active sheet's live Y.Doc (read-only, reflects in-progress edits including a peer's - not last-saved-to-MEMFS). apply: docDelta + applyDeltaToY, treating the AI's proposed file as though a peer had made the edit, through the same item-observer path every real co-edit already takes - NOT a reuse of KicadBinding.seed(), which is a boot-time/reconnect-only function (three branches, each with its own empty/hollow-doc edge cases) that doesn't have a branch for "live populated doc gets replaced with unrelated content", and this subsystem's git history (sibling-restage, paste-collision, ysync 0001-0008) is almost entirely bugfixes in exactly that state-transition space. - WasmTool.tsx: registerActiveCollabDoc(doc, filePath) mirrored onto each of the three existing setPanelDoc(...) call sites (single-room collabHandle, multi-sheet onActiveChange, teardown) - reuses the existing "which doc does the SheetPanel show right now" signal rather than adding new state. Known gaps, flagged not silently skipped: - docDelta only diffs items, not layout/preamble (page size, title block) - matches its own doc comment; an AI edit to those won't apply. Items (symbols/footprints/wires) are the expected AI edit surface for now. - No client-side read-only-viewer gate on apply - a tab opened as a read-only viewer has no local-edit path, but this pushes the AI's delta into the Y.Doc the same as a remote peer would, which a viewer's binding was never checked against. Write authorization is expected to live server-side (the AI tool implementation deciding whether to call write_editor_buffer at all, matching the cad_generation_enabled gate on the cad-editor bridge) - worth an explicit look before this ships broadly. Verified: tsc --noEmit clean, vitest run 469/469 passing, vite build succeeds. No kicad/wxwidgets changes - the existing wasm/ build (vendor-766e2f4-20260904-r4-debug) is unaffected; this is a dist-only release. Co-Authored-By: Claude Sonnet 5 --- web/standalone/src/App.tsx | 4 + .../src/components/CmmsAiWidget.tsx | 130 ++++++++++++++ web/standalone/src/components/WasmTool.tsx | 4 + web/standalone/src/lib/cmms-config.ts | 54 ++++++ web/standalone/src/wasm/collab/ai-bridge.ts | 162 ++++++++++++++++++ 5 files changed, 354 insertions(+) create mode 100644 web/standalone/src/components/CmmsAiWidget.tsx create mode 100644 web/standalone/src/lib/cmms-config.ts create mode 100644 web/standalone/src/wasm/collab/ai-bridge.ts diff --git a/web/standalone/src/App.tsx b/web/standalone/src/App.tsx index 2d550ea..49b27a9 100644 --- a/web/standalone/src/App.tsx +++ b/web/standalone/src/App.tsx @@ -1,5 +1,6 @@ import { useEffect } from "react"; import { Route, Routes, useLocation } from "react-router-dom"; +import { CmmsAiWidget } from "@/components/CmmsAiWidget"; import { VersionBadge } from "@/components/VersionBadge"; import { useChromeHidden } from "@/lib/chrome-visibility"; import { APP_URL } from "@/lib/config"; @@ -34,6 +35,9 @@ export default function App() { {/* Version + source link, bottom-right on every route (home + editor). Keys off the Figma-like hide-UI toggle (hidden is the mobile default). */} {!chromeHidden && } + {/* No-op (fetches config, renders nothing) unless the CMMS embed's + ai_assistant module is on for this tenant — see CmmsAiWidget. */} + ); } diff --git a/web/standalone/src/components/CmmsAiWidget.tsx b/web/standalone/src/components/CmmsAiWidget.tsx new file mode 100644 index 0000000..a4953f3 --- /dev/null +++ b/web/standalone/src/components/CmmsAiWidget.tsx @@ -0,0 +1,130 @@ +// cmms-embed / PCBJam Part 2: floating AI-assistant launcher + panel, for the +// CMMS embed only. Inert (renders nothing) unless `/api/cmms-config` answers +// `aiEnabled: true` — a plain pcbjam.io deploy or a CMMS tenant with the +// module off never fetches or renders anything here beyond the one config +// call. Visual language (position, colors, toggle/panel shape) mirrors +// `render_ai_assistant_editor_embed()` (TT-DB `src/ai_assistant_widget.php`) +// so it reads as the same widget as every other embedded editor; the +// draggable/resizable geometry system that PHP version has is CMMS-side +// static JS this bundle can't import, so this is fixed-position (bottom +// right) — a reasonable v1 given the panel is small relative to the board +// canvas. +import * as React from "react"; +import { fetchCmmsConfig, type CmmsConfig } from "@/lib/cmms-config"; +import { getActiveFilePath, installAiBridge } from "@/wasm/collab/ai-bridge"; +import { setTheme } from "@/lib/theme"; + +export function CmmsAiWidget(): React.ReactElement | null { + const [config, setConfig] = React.useState(null); + const [open, setOpen] = React.useState(false); + + React.useEffect(() => { + let cancelled = false; + fetchCmmsConfig().then((c) => { + if (cancelled) return; + setConfig(c); + if (c.aiEnabled) installAiBridge(); + // Belt-and-braces alongside the `?theme=` hand-off (lib/theme.ts) — that + // param already wins whenever edit.php sets it, so this only matters if + // it's ever absent. Skip if the URL param already ran; setTheme is + // otherwise idempotent (persists + toggles the same `.dark` class). + if (c.dark && !new URLSearchParams(window.location.search).get("theme")) { + setTheme("dark"); + } + }); + return () => { + cancelled = true; + }; + }, []); + + if (!config?.aiEnabled) return null; + + // Computed at render time (not memoized) so re-opening the panel after + // switching sheets sends the file that's actually open now — a context + // hint only; get-buffer carries the real content regardless. + const params = new URLSearchParams({ + editor: "kicad", + editor_entity_type: config.entityType, + editor_entity_id: config.entityId, + editor_filename: getActiveFilePath() ?? "", + page_context: "PCBJam (KiCad editor)", + }); + const src = `/modules/ai_assistant/widget-frame.php?${params.toString()}`; + + return ( +
+ + {open && ( +
+
+ AI Assistant + +
+