From ebe06020777dd9b436649abf6eb021b9390fb139 Mon Sep 17 00:00:00 2001 From: claude-ceo-assistant Date: Sun, 10 May 2026 17:59:25 -0700 Subject: [PATCH 01/23] feat(ci): restore staging+main path-filter trigger on publish-runtime (closes #348 Q1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds back the original GitHub workflow's auto-publish trigger that was dropped during the 2026-05-10 .gitea port (#206). Push to main or staging filtered by workspace/** falls into the existing PyPI-latest auto-bump path — no logic changes, just the missing trigger and a comment correction. Caveat: the workflow still requires PYPI_TOKEN as a repository secret (or org-level). Without it the publish step will fail loudly with a descriptive error. Q2 follow-up tracks setting the secret. Refs: molecule-core#348 --- .gitea/workflows/publish-runtime.yml | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/.gitea/workflows/publish-runtime.yml b/.gitea/workflows/publish-runtime.yml index 36c861e8..a414c303 100644 --- a/.gitea/workflows/publish-runtime.yml +++ b/.gitea/workflows/publish-runtime.yml @@ -12,7 +12,12 @@ name: publish-runtime # - Replaced `github.ref_name` (GitHub-only) with `${GITHUB_REF#refs/tags/}` # — Gitea Actions exposes github.ref (the full ref) but not ref_name # - Dropped `merge_group` trigger (Gitea has no merge queue) -# - Dropped `staging` branch trigger (no staging branch exists in this repo) +# +# 2026-05-10 (issue #348): restored `staging`/`main` branch + `workspace/**` +# path-filter trigger and the PyPI-latest auto-bump path that consumes it. +# The 2026-05-10 inline comment "no staging branch exists in this repo" was +# inherited verbatim from the runtime-mirror port and is INCORRECT for +# molecule-core. hongming-pc is blocked on this trigger. # # PyPI publishing: requires PYPI_TOKEN repository secret (or org-level secret). # Set via: repo Settings → Actions → Variables and Secrets → New Secret. @@ -23,6 +28,11 @@ name: publish-runtime on: push: + branches: + - main + - staging + paths: + - "workspace/**" tags: - "runtime-v*" workflow_dispatch: @@ -65,10 +75,9 @@ jobs: VERSION="${GITHUB_REF#refs/tags/runtime-v}" else # Fallback: derive from PyPI latest + patch bump. - # (The staging-push auto-bump trigger is dropped on Gitea — - # no staging branch exists. This fallback path is kept for - # robustness if a future automation uses workflow_dispatch without - # an explicit version input.) + # Used by the restored `push.branches: [main, staging]` + + # `paths: workspace/**` auto-bump trigger (issue #348). Also kept + # for workflow_dispatch invocations that omit the version input. LATEST=$(curl -fsS --retry 3 https://pypi.org/pypi/molecule-ai-workspace-runtime/json \ | python -c "import sys,json; print(json.load(sys.stdin)['info']['version'])") MAJOR=$(echo "$LATEST" | cut -d. -f1) -- 2.52.0 From 637bbb06a1f666dee98bb513ee144b76214fb2f3 Mon Sep 17 00:00:00 2001 From: claude-ceo-assistant Date: Sun, 10 May 2026 18:31:00 -0700 Subject: [PATCH 02/23] fix(ci): split publish-runtime into tags-only + autobump (closes #351) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit publish-runtime.yml has never fired since the .gitea port (0 rows in action_run.workflow_id='publish-runtime.yml' ever), which is why PyPI is still at 0.1.129 despite Gitea having a runtime-v1.0.0 tag. Root cause hypothesis: Gitea Actions evaluates the on.push.paths filter against tag-push events too (no path diff → workflow skipped). PR #349 made this visible by adding the paths trigger, but the same defect existed for the originally-ported tags-only trigger on this Gitea version — hence the runtime-v1.0.0 tag also never published. Fix: split into two files, each with a single unambiguous trigger shape. - publish-runtime.yml : on.push.tags only (the publisher) - publish-runtime-autobump.yml : on.push.branches+paths (NEW; the bumper) The autobump file computes next version from PyPI latest, pushes 'runtime-v$VERSION' tag via DISPATCH_TOKEN (not GITHUB_TOKEN — needed to trigger downstream workflows on Gitea), and exits. The tag push then triggers publish-runtime.yml. Test plan after merge: 1. Push no-op commit to workspace/. Observe autobump fire, push tag. 2. Observe publish-runtime.yml fire on the tag, publish 0.1.130 to PyPI, cascade to template repos. 3. Verify 'action_run' shows >0 rows for both workflow_ids. --- .gitea/workflows/publish-runtime-autobump.yml | 100 ++++++++++++++++++ .gitea/workflows/publish-runtime.yml | 27 +++-- 2 files changed, 117 insertions(+), 10 deletions(-) create mode 100644 .gitea/workflows/publish-runtime-autobump.yml diff --git a/.gitea/workflows/publish-runtime-autobump.yml b/.gitea/workflows/publish-runtime-autobump.yml new file mode 100644 index 00000000..85afdafd --- /dev/null +++ b/.gitea/workflows/publish-runtime-autobump.yml @@ -0,0 +1,100 @@ +name: publish-runtime-autobump + +# Auto-bump-on-workspace-edit half of the publish pipeline. +# +# Why this file exists (issue #351): +# Gitea Actions does not correctly disambiguate `paths:` from `tags:` +# when both are bundled under a single `on.push` key. The result is +# that tag pushes get filtered out and `publish-runtime.yml` never +# fires — `action_run` rows: 0. This was unnoticed pre-2026-05-11 +# because PYPI_TOKEN was absent (publishes would have failed anyway). +# +# Split design: +# - publish-runtime.yml : on.push.tags only (the publisher) +# - publish-runtime-autobump.yml: on.push.branches+paths (this file — the version-bumper) +# +# This file computes the next version from PyPI's latest, pushes a +# `runtime-v$VERSION` tag, and exits. The tag push then triggers +# publish-runtime.yml via its tags-only trigger. +# +# Concurrency: shares the `publish-runtime` group with publish-runtime.yml +# so concurrent workspace pushes serialize at the bump step. Without +# this, two pushes minutes apart could both read PyPI latest=0.1.129 +# and try to tag 0.1.130 simultaneously, only one of which would land. + +on: + push: + branches: + - main + - staging + paths: + - "workspace/**" + +permissions: + contents: write # required to push tags back + +concurrency: + group: publish-runtime + cancel-in-progress: false + +jobs: + autobump-and-tag: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + # Fetch full tag list so the bump logic can sanity-check against + # what's already in this repo (catches collision with prior + # manual tag pushes). + fetch-depth: 0 + + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: "3.11" + + - name: Compute next version from PyPI latest + id: bump + run: | + set -eu + LATEST=$(curl -fsS --retry 3 https://pypi.org/pypi/molecule-ai-workspace-runtime/json \ + | python -c "import sys,json; print(json.load(sys.stdin)['info']['version'])") + MAJOR=$(echo "$LATEST" | cut -d. -f1) + MINOR=$(echo "$LATEST" | cut -d. -f2) + PATCH=$(echo "$LATEST" | cut -d. -f3) + VERSION="${MAJOR}.${MINOR}.$((PATCH+1))" + echo "PyPI latest=$LATEST -> next=$VERSION" + if ! echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then + echo "::error::computed version $VERSION does not match PEP 440 X.Y.Z" + exit 1 + fi + if git tag --list | grep -qx "runtime-v$VERSION"; then + echo "::error::tag runtime-v$VERSION already exists in this repo. Manual intervention required (PyPI and Gitea tag history are out of sync)." + exit 1 + fi + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + + - name: Push runtime-v$VERSION tag + env: + DISPATCH_TOKEN: ${{ secrets.DISPATCH_TOKEN }} + VERSION: ${{ steps.bump.outputs.version }} + GITEA_URL: https://git.moleculesai.app + run: | + set -eu + if [ -z "$DISPATCH_TOKEN" ]; then + echo "::error::DISPATCH_TOKEN secret is not set — needed to push the tag back to molecule-core." + exit 1 + fi + git config user.name "publish-runtime autobump" + git config user.email "publish-runtime@moleculesai.app" + git tag -a "runtime-v$VERSION" \ + -m "Auto-bump on workspace/** edit on $GITHUB_REF" \ + -m "Triggered by: $GITHUB_REF @ $GITHUB_SHA" \ + -m "publish-runtime.yml will pick up this tag and upload to PyPI" + # Push via DISPATCH_TOKEN (a Gitea PAT). Using the bot identity + # ensures the resulting tag-push event is dispatched to + # publish-runtime.yml; act_runner's default GITHUB_TOKEN cannot + # trigger downstream workflows. + git remote set-url origin "${GITEA_URL#https://}" + git remote set-url origin "https://x-access-token:${DISPATCH_TOKEN}@${GITEA_URL#https://}/molecule-ai/molecule-core.git" + git push origin "runtime-v$VERSION" + echo "✓ pushed runtime-v$VERSION — publish-runtime.yml should fire next" diff --git a/.gitea/workflows/publish-runtime.yml b/.gitea/workflows/publish-runtime.yml index a414c303..083b6840 100644 --- a/.gitea/workflows/publish-runtime.yml +++ b/.gitea/workflows/publish-runtime.yml @@ -13,11 +13,23 @@ name: publish-runtime # — Gitea Actions exposes github.ref (the full ref) but not ref_name # - Dropped `merge_group` trigger (Gitea has no merge queue) # -# 2026-05-10 (issue #348): restored `staging`/`main` branch + `workspace/**` -# path-filter trigger and the PyPI-latest auto-bump path that consumes it. -# The 2026-05-10 inline comment "no staging branch exists in this repo" was -# inherited verbatim from the runtime-mirror port and is INCORRECT for -# molecule-core. hongming-pc is blocked on this trigger. +# 2026-05-10 (issue #348): originally restored `staging`/`main` branch + +# `workspace/**` path-filter trigger in PR #349. +# +# 2026-05-11 (issue #351): REVERTED the branches+paths trigger from THIS +# file. Bundling `paths` with `tags` under a single `on.push` key caused +# Gitea Actions to never dispatch the workflow for tag-push events (0 +# runs in `action_run` for workflow_id='publish-runtime.yml' since the +# port, including the runtime-v1.0.0 tag — which is why PyPI is still at +# 0.1.129 despite a v1.0.0 Gitea tag existing). +# +# The auto-bump-on-workspace-edit trigger now lives in +# `.gitea/workflows/publish-runtime-autobump.yml`. That file computes the +# next version from PyPI's latest and pushes a `runtime-v$VERSION` tag, +# which THIS file then picks up via the tags-only trigger below. +# +# This decoupling means Gitea's path-vs-tag evaluator never has to +# disambiguate — each file has a single unambiguous trigger shape. # # PyPI publishing: requires PYPI_TOKEN repository secret (or org-level secret). # Set via: repo Settings → Actions → Variables and Secrets → New Secret. @@ -28,11 +40,6 @@ name: publish-runtime on: push: - branches: - - main - - staging - paths: - - "workspace/**" tags: - "runtime-v*" workflow_dispatch: -- 2.52.0 From 89ef5bdd28eb2749291e3138172f614f1934f04f Mon Sep 17 00:00:00 2001 From: claude-ceo-assistant Date: Sun, 10 May 2026 18:48:28 -0700 Subject: [PATCH 03/23] =?UTF-8?q?fix(ci):=20remove=20workflow=5Fdispatch.i?= =?UTF-8?q?nputs=20(true=20root=20cause=20of=20#351=20=E2=80=94=20Gitea=20?= =?UTF-8?q?parser=20rejects,=20workflow=20ignored)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ROOT CAUSE found in Gitea server logs: actions/workflows.go:DetectWorkflows() [W] ignore invalid workflow "publish-runtime.yml": unknown on type: map["version":{"description":...,"required":true,"type":"string"}] Gitea 1.22.6's workflow parser flattens workflow_dispatch.inputs.* into top-level 'on:' event-keys and rejects the workflow when it doesn't recognize them. Once rejected, the workflow never registers — so NO event triggers it. publish-runtime.yml has 0 runs in action_run since the .gitea port for exactly this reason; the runtime-v1.0.0 tag from yesterday and hongming-pc's runtime-v0.1.130 from tonight both pushed successfully but went nowhere. This supersedes the paths-vs-tags hypothesis from #351 (PR #352). The split is still useful for clarity but was NOT the cause — even the original tags-only port had this same parse failure. Fix: drop the inputs block. workflow_dispatch in Gitea 1.22.6 supports no-input dispatch only. The bash logic for version derivation now uses just two cases: tag-push (strip prefix) or anything-else (PyPI auto-bump). Post-merge verification: - watch for first-ever publish-runtime.yml run in action_run - check Gitea log no longer emits 'ignore invalid workflow' for this file - push a runtime-v0.1.130 tag → workflow fires → PyPI 0.1.130 Refs: #351 (root cause), #348 Q3 (the blocker) --- .gitea/workflows/publish-runtime.yml | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/.gitea/workflows/publish-runtime.yml b/.gitea/workflows/publish-runtime.yml index 083b6840..93124b1e 100644 --- a/.gitea/workflows/publish-runtime.yml +++ b/.gitea/workflows/publish-runtime.yml @@ -43,11 +43,17 @@ on: tags: - "runtime-v*" workflow_dispatch: - inputs: - version: - description: "Version to publish (e.g. 0.1.6). Required for manual dispatch." - required: true - type: string + # 2026-05-11 (root cause of #351 / 0 runs ever): + # Gitea 1.22.6's workflow parser rejects `workflow_dispatch.inputs.version` + # with "unknown on type" — it mis-treats the inputs sub-keys as top-level + # `on:` event types. Log line: + # actions/workflows.go:DetectWorkflows() [W] ignore invalid workflow + # "publish-runtime.yml": unknown on type: map["version": {...}] + # That `[W] ignore invalid workflow` is silent UX — the workflow never + # registers, so it never fires for ANY event (push.tags included). + # Removing the inputs block restores parsing. Manual dispatch from the + # Gitea UI now triggers the PyPI auto-bump fallback in `Derive version` + # below (no `inputs.version` to read). permissions: contents: read @@ -72,19 +78,15 @@ jobs: python-version: "3.11" cache: pip - - name: Derive version (tag, manual input, or PyPI auto-bump) + - name: Derive version (tag or PyPI auto-bump) id: version run: | - if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - VERSION="${{ inputs.version }}" - elif echo "$GITHUB_REF" | grep -q "^refs/tags/runtime-v"; then + if echo "$GITHUB_REF" | grep -q "^refs/tags/runtime-v"; then # Tag is `runtime-vX.Y.Z` — strip the prefix. VERSION="${GITHUB_REF#refs/tags/runtime-v}" else - # Fallback: derive from PyPI latest + patch bump. - # Used by the restored `push.branches: [main, staging]` + - # `paths: workspace/**` auto-bump trigger (issue #348). Also kept - # for workflow_dispatch invocations that omit the version input. + # workflow_dispatch path (no inputs supported on Gitea 1.22.6) or + # any other non-tag trigger: derive from PyPI latest + patch bump. LATEST=$(curl -fsS --retry 3 https://pypi.org/pypi/molecule-ai-workspace-runtime/json \ | python -c "import sys,json; print(json.load(sys.stdin)['info']['version'])") MAJOR=$(echo "$LATEST" | cut -d. -f1) -- 2.52.0 From ae0238ad3910d3bb9369da3825f7904b5d9204f3 Mon Sep 17 00:00:00 2001 From: hongmingwang Date: Sun, 10 May 2026 06:06:24 -0700 Subject: [PATCH 04/23] feat(canvas): mobile-first shell with 6-screen iOS design + responsive desktop fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements the Claude Design handoff (Molecules AI Mobile.html) as a viewport-gated React tree under canvas/src/components/mobile/. < 640px renders the new shell instead of the desktop ReactFlow canvas. Six screens, all bound to live store data: - Home (agent list + filter chips + spawn FAB) - Canvas (mini-graph with pinch-to-zoom + pan + reset) - Detail (status pills, tabs: Overview / Activity / Config / Memory; Activity hits /workspaces/:id/activity) - Chat (textarea composer, IME-safe Enter, sendInFlightRef guard; bootstraps from agentMessages so the prior thread shows on entry) - Comms (live A2A feed via /workspaces/:id/activity + ACTIVITY_LOGGED) - Spawn (bottom sheet; fetches /templates so users pick what's actually installed on their platform) Plus a Me tab for mobile theme/accent/density. Design system (palette.ts + primitives.tsx) ports tokens 1:1 from the handoff: cream + dark palettes, T1-T4 tier chips, status dots with halo, JetBrains Mono for IDs/timestamps. Inter + JetBrains Mono are self-hosted via next/font/google so CSP `font-src 'self'` is honoured. URL routing: routes sync to ?m=&a=; popstate restores route; deep links seed initial state. /?m=detail without ?a collapses to home. Accent override flows through React context (MobileAccentProvider) — not by mutating the static MOL_LIGHT/MOL_DARK singletons. SSR flash: isMobile is tri-state; loading spinner stays up until matchMedia resolves so mobile devices never paint the desktop tree. Desktop responsiveness fixes (separate but ride along): - Toolbar: full-width with overflow-x-auto on mobile, logo text + count hidden < sm, divider/border collapse to sm: only. - SidePanel: full-screen on mobile via matchMedia, resize handle hidden. - Canvas: MiniMap hidden < sm (was overlapping the New Workspace FAB). Tests (51 total, 33 new): - palette.test.ts (12) - normalizeStatus, tierCode, light/dark parity - components.test.ts (10) - toMobileAgent field mapping + classifyForFilter - MobileApp.test.tsx (12) - route stack, deep links, popstate, tab bar hidden on chat, spawn overlay - SidePanel.tabs.test.tsx (18) - regression-clean Verified: tsc --noEmit clean across mobile/, page.tsx, layout.tsx. Not yet verified: live phone browser (needs CP backend hydrated). Co-Authored-By: Claude Opus 4.7 (1M context) --- canvas/src/app/layout.tsx | 18 +- canvas/src/app/page.tsx | 49 +- canvas/src/components/Canvas.tsx | 4 +- canvas/src/components/SidePanel.tsx | 58 +- canvas/src/components/Toolbar.tsx | 14 +- canvas/src/components/mobile/MobileApp.tsx | 210 +++++++ canvas/src/components/mobile/MobileCanvas.tsx | 401 ++++++++++++ canvas/src/components/mobile/MobileChat.tsx | 493 +++++++++++++++ canvas/src/components/mobile/MobileComms.tsx | 368 +++++++++++ canvas/src/components/mobile/MobileDetail.tsx | 589 ++++++++++++++++++ canvas/src/components/mobile/MobileHome.tsx | 208 +++++++ canvas/src/components/mobile/MobileMe.tsx | 194 ++++++ canvas/src/components/mobile/MobileSpawn.tsx | 429 +++++++++++++ .../mobile/__tests__/MobileApp.test.tsx | 211 +++++++ .../mobile/__tests__/components.test.ts | 101 +++ .../mobile/__tests__/palette.test.ts | 68 ++ canvas/src/components/mobile/components.tsx | 444 +++++++++++++ .../src/components/mobile/palette-context.tsx | 40 ++ canvas/src/components/mobile/palette.ts | 147 +++++ canvas/src/components/mobile/primitives.tsx | 278 +++++++++ 20 files changed, 4293 insertions(+), 31 deletions(-) create mode 100644 canvas/src/components/mobile/MobileApp.tsx create mode 100644 canvas/src/components/mobile/MobileCanvas.tsx create mode 100644 canvas/src/components/mobile/MobileChat.tsx create mode 100644 canvas/src/components/mobile/MobileComms.tsx create mode 100644 canvas/src/components/mobile/MobileDetail.tsx create mode 100644 canvas/src/components/mobile/MobileHome.tsx create mode 100644 canvas/src/components/mobile/MobileMe.tsx create mode 100644 canvas/src/components/mobile/MobileSpawn.tsx create mode 100644 canvas/src/components/mobile/__tests__/MobileApp.test.tsx create mode 100644 canvas/src/components/mobile/__tests__/components.test.ts create mode 100644 canvas/src/components/mobile/__tests__/palette.test.ts create mode 100644 canvas/src/components/mobile/components.tsx create mode 100644 canvas/src/components/mobile/palette-context.tsx create mode 100644 canvas/src/components/mobile/palette.ts create mode 100644 canvas/src/components/mobile/primitives.tsx diff --git a/canvas/src/app/layout.tsx b/canvas/src/app/layout.tsx index 21ec7962..04786994 100644 --- a/canvas/src/app/layout.tsx +++ b/canvas/src/app/layout.tsx @@ -1,6 +1,22 @@ import type { Metadata } from "next"; +import { Inter, JetBrains_Mono } from "next/font/google"; import { cookies, headers } from "next/headers"; import "./globals.css"; + +// Self-hosted at build time → CSP-safe (font-src 'self' covers them +// because Next.js serves the .woff2 from /_next/static). Exposed as +// CSS variables so the mobile palette can reference them without +// importing this module. +const interFont = Inter({ + subsets: ["latin"], + display: "swap", + variable: "--font-inter", +}); +const monoFont = JetBrains_Mono({ + subsets: ["latin"], + display: "swap", + variable: "--font-jetbrains", +}); import { AuthGate } from "@/components/AuthGate"; import { CookieConsent } from "@/components/CookieConsent"; import { PurchaseSuccessModal } from "@/components/PurchaseSuccessModal"; @@ -79,7 +95,7 @@ export default async function RootLayout({ dangerouslySetInnerHTML={{ __html: themeBootScript }} /> - + {/* AuthGate is a client component; it checks the session on mount and bounces anonymous users to the control plane's login page diff --git a/canvas/src/app/page.tsx b/canvas/src/app/page.tsx index 0bf8f62c..28cb37d9 100644 --- a/canvas/src/app/page.tsx +++ b/canvas/src/app/page.tsx @@ -4,6 +4,7 @@ import { useEffect, useState } from "react"; import { Canvas } from "@/components/Canvas"; import { Legend } from "@/components/Legend"; import { CommunicationOverlay } from "@/components/CommunicationOverlay"; +import { MobileApp } from "@/components/mobile/MobileApp"; import { Spinner } from "@/components/Spinner"; import { connectSocket, disconnectSocket } from "@/store/socket"; import { useCanvasStore } from "@/store/canvas"; @@ -14,6 +15,23 @@ export default function Home() { const hydrationError = useCanvasStore((s) => s.hydrationError); const setHydrationError = useCanvasStore((s) => s.setHydrationError); const [hydrating, setHydrating] = useState(true); + // < 640px viewport renders the dedicated mobile shell instead of the + // desktop canvas. Tri-state: `null` until matchMedia has resolved, + // then `true|false`. While null we keep the existing loading spinner + // up — that way mobile devices never flash the desktop tree (which + // they would if we defaulted to `false` and only flipped post-mount). + const [isMobile, setIsMobile] = useState(null); + useEffect(() => { + if (typeof window === "undefined" || !window.matchMedia) { + setIsMobile(false); + return; + } + const mq = window.matchMedia("(max-width: 639px)"); + const update = () => setIsMobile(mq.matches); + update(); + mq.addEventListener("change", update); + return () => mq.removeEventListener("change", update); + }, []); // Distinct from hydrationError: platform-down is its own UX path // (different copy, different action — the user's next step is to // check local services, not to retry the API call). Tracked @@ -51,7 +69,10 @@ export default function Home() { }; }, []); - if (hydrating) { + // Hold the spinner while data hydrates OR while the viewport + // resolution hasn't settled yet (avoids a desktop-tree flash on + // mobile devices between SSR-paint and matchMedia). + if (hydrating || isMobile === null) { return (
@@ -66,6 +87,32 @@ export default function Home() { return ; } + if (isMobile) { + return ( + <> + + {hydrationError && ( +
+

{hydrationError}

+ +
+ )} + + ); + } + return ( <> diff --git a/canvas/src/components/Canvas.tsx b/canvas/src/components/Canvas.tsx index 5983b72f..888343b0 100644 --- a/canvas/src/components/Canvas.tsx +++ b/canvas/src/components/Canvas.tsx @@ -308,7 +308,9 @@ function CanvasInner() { showInteractive={false} />