@lowdefy/server-dev 0.0.0-experimental-20260720135014 → 0.0.0-experimental-20260722143027

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. package/{pages/_app.js → client/App.jsx} +21 -33
  2. package/client/Inspector.jsx +206 -0
  3. package/{lib/client/Page.js → client/Page.jsx} +5 -5
  4. package/{lib/client/Reload.js → client/Reload.jsx} +5 -7
  5. package/client/Routing.jsx +106 -0
  6. package/client/feedback/FeedbackMount.jsx +83 -0
  7. package/client/feedback/FeedbackOverlay.jsx +901 -0
  8. package/client/feedback/captureTabScreenshot.js +84 -0
  9. package/client/feedback/copyToClipboard.js +45 -0
  10. package/client/feedback/drawAnnotationsSvg.js +119 -0
  11. package/client/feedback/elementInspect.js +99 -0
  12. package/client/feedback/feedbackStyles.js +301 -0
  13. package/client/feedback/pinResponsiveImages.js +59 -0
  14. package/client/feedback/sendFeedback.js +39 -0
  15. package/client/feedback/useFeedbackToggle.js +70 -0
  16. package/client/main.jsx +42 -0
  17. package/client/openInEditor/OpenInEditorListener.jsx +261 -0
  18. package/client/openInEditor/findBlockLocation.js +51 -0
  19. package/lib/build/app.js +8 -1
  20. package/lib/build/appMeta.js +8 -1
  21. package/lib/build/auth.js +8 -1
  22. package/lib/build/config.js +8 -1
  23. package/lib/build/i18n.js +8 -1
  24. package/lib/build/logger.js +8 -1
  25. package/lib/build/theme.js +8 -1
  26. package/lib/client/auth/{Auth.js → Auth.jsx} +12 -5
  27. package/lib/client/auth/AuthConfigured.jsx +155 -0
  28. package/lib/client/auth/AuthNotConfigured.js +22 -2
  29. package/lib/client/setPageId.js +6 -5
  30. package/lib/client/utils/useMutateCache.js +14 -1
  31. package/lib/client/utils/usePageConfig.js +31 -11
  32. package/lib/docs/captureAnnotatedScreenshot.js +88 -0
  33. package/lib/docs/checkpointPaths.js +91 -0
  34. package/lib/docs/checkpointStore.js +234 -0
  35. package/lib/docs/checkpointToMocks.js +65 -0
  36. package/lib/docs/clientErrorStore.js +36 -0
  37. package/lib/docs/configCheckpoints.test.mjs +156 -0
  38. package/lib/docs/createConfigCheckpoint.js +92 -0
  39. package/lib/docs/createDocsMcpServer.js +506 -0
  40. package/lib/docs/createDocsMcpServer.test.mjs +123 -0
  41. package/lib/docs/devMockRegistry.js +68 -0
  42. package/lib/docs/docs.test.mjs +448 -0
  43. package/lib/docs/enrichFeedback.js +71 -0
  44. package/lib/docs/evalOperator.js +39 -0
  45. package/lib/docs/evalOperator.test.mjs +112 -0
  46. package/lib/docs/evalOperatorHeadless.js +87 -0
  47. package/lib/docs/evalOperatorHeadless.test.mjs +72 -0
  48. package/lib/docs/evalOperatorInTab.js +48 -0
  49. package/lib/docs/findConfig.js +260 -0
  50. package/lib/docs/formatFeedback.js +121 -0
  51. package/lib/docs/formatFeedback.test.mjs +204 -0
  52. package/lib/docs/getAppMap.js +180 -0
  53. package/lib/docs/getBrowser.js +115 -0
  54. package/lib/docs/getBuildStatus.js +37 -0
  55. package/lib/docs/getCoreDoc.js +63 -0
  56. package/lib/docs/getDocsManifest.js +40 -0
  57. package/lib/docs/getExamples.js +58 -0
  58. package/lib/docs/getOverview.js +115 -0
  59. package/lib/docs/getPageConfig.js +53 -0
  60. package/lib/docs/getPluginDoc.js +55 -0
  61. package/lib/docs/getSchema.js +65 -0
  62. package/lib/docs/inspectState.js +41 -0
  63. package/lib/docs/inspectState.test.mjs +91 -0
  64. package/lib/docs/inspectStateFromTab.js +40 -0
  65. package/lib/docs/inspectStateHeadless.js +84 -0
  66. package/lib/docs/inspectStateHeadless.test.mjs +60 -0
  67. package/lib/docs/isWriteRequestsAllowed.js +50 -0
  68. package/lib/docs/listConfigCheckpoints.js +55 -0
  69. package/lib/docs/listPlugins.js +80 -0
  70. package/lib/docs/listTypes.js +105 -0
  71. package/lib/docs/loadState.js +139 -0
  72. package/lib/docs/mapPageBuildErrors.js +28 -0
  73. package/lib/docs/normalizeTypeKind.js +40 -0
  74. package/{pages/api/endpoints/[...endpointId].js → lib/docs/readBuildArtifact.js} +15 -12
  75. package/lib/docs/readPageArtifact.js +26 -0
  76. package/lib/docs/resolvePluginDir.js +49 -0
  77. package/lib/docs/revertConfigCheckpoint.js +73 -0
  78. package/lib/docs/runRequest.js +141 -0
  79. package/lib/docs/saveAnnotatedScreenshot.js +43 -0
  80. package/lib/docs/scaffoldPage.js +75 -0
  81. package/lib/docs/scaffoldPage.test.mjs +97 -0
  82. package/lib/docs/screenshotPage.js +109 -0
  83. package/lib/docs/screenshotPage.test.mjs +59 -0
  84. package/lib/docs/screenshotPageClip.test.mjs +102 -0
  85. package/lib/docs/searchDocs.js +67 -0
  86. package/lib/docs/setupTestFixtures.mjs +335 -0
  87. package/lib/docs/snapshotState.js +59 -0
  88. package/lib/docs/stateCheckpoints.test.mjs +348 -0
  89. package/lib/docs/tabAvailable.js +28 -0
  90. package/lib/docs/tabChannel.js +119 -0
  91. package/lib/docs/tabChannel.test.mjs +130 -0
  92. package/lib/server/auth/createSystemContext.js +70 -0
  93. package/lib/server/auth/getAuth.js +46 -0
  94. package/lib/server/auth/getHeadlessUser.js +39 -0
  95. package/lib/server/auth/{getMockSession.js → getMockUser.js} +10 -19
  96. package/lib/server/auth/{getAuthOptions.js → getStrategies.js} +14 -9
  97. package/lib/server/auth/headlessUser.js +30 -0
  98. package/lib/server/createLowdefyContext.js +133 -0
  99. package/lib/server/jitPageBuilder.js +36 -11
  100. package/lib/server/loadDynamicJsMap.js +47 -0
  101. package/lib/server/log/logRequest.js +1 -3
  102. package/manager/getContext.mjs +3 -7
  103. package/manager/processes/initialBuild.mjs +0 -2
  104. package/manager/processes/installPlugins.mjs +19 -11
  105. package/manager/processes/lowdefyBuild.mjs +29 -7
  106. package/manager/processes/lowdefyBuild.test.mjs +102 -0
  107. package/manager/processes/readDotEnv.mjs +13 -1
  108. package/manager/processes/shutdownServer.mjs +6 -6
  109. package/manager/processes/startServer.mjs +8 -7
  110. package/manager/processes/startWatchers.mjs +2 -2
  111. package/manager/run.mjs +41 -0
  112. package/manager/utils/createCustomPluginMessagesMap.mjs +8 -1
  113. package/manager/utils/createCustomPluginTypesMap.mjs +3 -2
  114. package/manager/utils/formatNoticeBox.mjs +30 -0
  115. package/manager/utils/{getNextBin.mjs → getViteBin.mjs} +7 -10
  116. package/manager/utils/updatePageTailwindCss.mjs +7 -1
  117. package/manager/utils/writeBuildStatus.mjs +39 -0
  118. package/manager/utils/writeBuildStatus.test.mjs +76 -0
  119. package/manager/watchers/lowdefyBuildWatcher.mjs +0 -1
  120. package/manager/watchers/moduleBuildWatcher.mjs +0 -1
  121. package/manager/watchers/{nextBuildWatcher.mjs → serverArtifactWatcher.mjs} +18 -22
  122. package/package.json +63 -47
  123. package/package.original.json +66 -55
  124. package/postcss.config.cjs +1 -0
  125. package/src/app.js +188 -0
  126. package/src/html/renderDevPage.js +86 -0
  127. package/src/lib/getPathSegments.js +25 -0
  128. package/src/lib/safeScriptJson.js +38 -0
  129. package/src/lib/serveBuildJs.js +30 -0
  130. package/src/middleware/apiContext.js +32 -0
  131. package/src/middleware/errorHandler.js +55 -0
  132. package/src/routes/agent.js +64 -0
  133. package/src/routes/auth.js +43 -0
  134. package/src/routes/clientError.js +57 -0
  135. package/src/routes/cron.js +44 -0
  136. package/src/routes/detached.js +51 -0
  137. package/src/routes/devInspect.js +94 -0
  138. package/{pages/[[...pageId]].js → src/routes/devTools.js} +4 -2
  139. package/{pages/404.js → src/routes/docs/appMap.js} +6 -2
  140. package/{pages/api/dev-tools.js → src/routes/docs/buildStatus.js} +5 -4
  141. package/src/routes/docs/checkpointsCreate.js +28 -0
  142. package/src/routes/docs/checkpointsList.js +23 -0
  143. package/{pages/api/icons/dynamic.js → src/routes/docs/checkpointsRevert.js} +11 -12
  144. package/src/routes/docs/content.js +31 -0
  145. package/src/routes/docs/evalOperator.js +46 -0
  146. package/src/routes/docs/examples.js +33 -0
  147. package/src/routes/docs/find.js +30 -0
  148. package/src/routes/docs/index.js +23 -0
  149. package/src/routes/docs/inspectState.js +34 -0
  150. package/src/routes/docs/loadState.js +38 -0
  151. package/src/routes/docs/mcp.js +30 -0
  152. package/src/routes/docs/pageConfig.js +36 -0
  153. package/src/routes/docs/pluginDoc.js +33 -0
  154. package/src/routes/docs/plugins.js +23 -0
  155. package/src/routes/docs/runRequest.js +34 -0
  156. package/{lib/server/auth/getServerSession.js → src/routes/docs/schema.js} +17 -15
  157. package/src/routes/docs/screenshot.js +66 -0
  158. package/src/routes/docs/search.js +27 -0
  159. package/src/routes/docs/snapshotState.js +36 -0
  160. package/src/routes/docs/stateCheckpointsList.js +26 -0
  161. package/src/routes/docs/types.js +33 -0
  162. package/src/routes/endpoints.js +76 -0
  163. package/src/routes/feedback.js +110 -0
  164. package/src/routes/feedback.test.mjs +204 -0
  165. package/src/routes/iconsDynamic.js +25 -0
  166. package/src/routes/jitPage.js +99 -0
  167. package/src/routes/jsEnv.js +30 -0
  168. package/{pages/api → src/routes}/ping.js +4 -2
  169. package/src/routes/reload.js +81 -0
  170. package/src/routes/request.js +66 -0
  171. package/{pages/api → src/routes}/root.js +4 -4
  172. package/src/routes/usage.js +46 -0
  173. package/src/routes/user.js +27 -0
  174. package/src/routes/websocket.js +33 -0
  175. package/src/websocket/devWebSocket.js +78 -0
  176. package/vite.config.js +111 -0
  177. package/.eslintrc.yaml +0 -1
  178. package/lib/client/App.js +0 -80
  179. package/lib/client/auth/AuthConfigured.js +0 -61
  180. package/lib/server/apiWrapper.js +0 -116
  181. package/lib/server/compileCss.js +0 -39
  182. package/manager/processes/compileCss.mjs +0 -54
  183. package/manager/processes/nextBuild.mjs +0 -50
  184. package/next.config.js +0 -25
  185. package/pages/_document.js +0 -107
  186. package/pages/api/agent/[...path].js +0 -86
  187. package/pages/api/auth/[...nextauth].js +0 -48
  188. package/pages/api/client-error.js +0 -45
  189. package/pages/api/js/[env].js +0 -42
  190. package/pages/api/page/[...pageId].js +0 -74
  191. package/pages/api/reload.js +0 -53
  192. package/pages/api/request/[...path].js +0 -41
  193. /package/lib/client/{BuildErrorPage.js → BuildErrorPage.jsx} +0 -0
  194. /package/lib/client/{BuildingPage.js → BuildingPage.jsx} +0 -0
  195. /package/lib/client/{ErrorBar.js → ErrorBar.jsx} +0 -0
  196. /package/lib/client/{InstallingPluginsPage.js → InstallingPluginsPage.jsx} +0 -0
  197. /package/lib/client/{RestartingPage.js → RestartingPage.jsx} +0 -0
@@ -14,14 +14,7 @@
14
14
  limitations under the License.
15
15
  */
16
16
 
17
- // CSS layer order — MUST be the first CSS import. Turbopack treats this as critical
18
- // CSS that loads before hydration, locking the cascade priority (antd > base/preflight)
19
- // before antd's StyleProvider injects @layer antd {} at runtime.
20
- import '../build/layer-order.css';
21
-
22
17
  import React, { Suspense, useCallback, useEffect, useRef, useState } from 'react';
23
- import dynamic from 'next/dynamic';
24
- import { useRouter } from 'next/router';
25
18
  import useSWR from 'swr';
26
19
 
27
20
  import { ErrorBoundary } from '@lowdefy/block-utils';
@@ -33,13 +26,10 @@ import { XProvider } from '@ant-design/x';
33
26
  import antdLocaleLoaders from '../build/i18n/antdLocales.js';
34
27
  import antdXLocaleLoaders from '../build/i18n/antdXLocales.js';
35
28
  import dayjsLocaleMap from '../build/i18n/dayjsLocales.js';
36
- import Auth from '../lib/client/auth/Auth.js';
37
- import ErrorBar from '../lib/client/ErrorBar.js';
29
+ import Auth from '../lib/client/auth/Auth.jsx';
30
+ import ErrorBar from '../lib/client/ErrorBar.jsx';
38
31
  import request from '../lib/client/utils/request.js';
39
-
40
- // Full Tailwind CSS — also loaded via <link href="tailwind-jit.css"> in _document.js
41
- // for hot-reloading. The Turbopack chunk provides layer ordering guarantee on initial load.
42
- import '../build/globals.css';
32
+ import Routing from './Routing.jsx';
43
33
 
44
34
  function ThemeTokenResolver({ lowdefyRef, children }) {
45
35
  const { token } = antdTheme.useToken();
@@ -50,12 +40,11 @@ function ThemeTokenResolver({ lowdefyRef, children }) {
50
40
  return children;
51
41
  }
52
42
 
53
- function App({ Component }) {
54
- const router = useRouter();
43
+ function App({ router }) {
55
44
  const lowdefyRef = useRef({});
56
45
  const [runtimeErrors, setRuntimeErrors] = useState([]);
57
- // Subscribe to rootConfig SWR cache — deduplicates with inner App.js fetch.
58
- // Without suspense so _app.js doesn't suspend — just re-renders when data arrives.
46
+ // Subscribe to rootConfig SWR cache — deduplicates with Routing's fetch.
47
+ // Without suspense so App doesn't suspend — just re-renders when data arrives.
59
48
  const { data: rootConfig } = useSWR(`${router.basePath}/api/root`, (url) => request({ url }));
60
49
  if (rootConfig?.theme) {
61
50
  lowdefyRef.current.theme = rootConfig.theme;
@@ -66,7 +55,11 @@ function App({ Component }) {
66
55
  configDarkMode: lowdefyRef.current.theme?.darkMode,
67
56
  });
68
57
 
69
- const { active: activeLocale, antdLocale, antdXLocale } = useLocale({
58
+ const {
59
+ active: activeLocale,
60
+ antdLocale,
61
+ antdXLocale,
62
+ } = useLocale({
70
63
  i18n: rootConfig?.i18n,
71
64
  antdLocaleLoaders,
72
65
  antdXLocaleLoaders,
@@ -99,12 +92,10 @@ function App({ Component }) {
99
92
  ]);
100
93
  }, []);
101
94
 
102
- // Clear runtime errors on route change
95
+ // Clear runtime errors on navigation
103
96
  useEffect(() => {
104
- const clearErrors = () => setRuntimeErrors([]);
105
- router.events.on('routeChangeStart', clearErrors);
106
- return () => router.events.off('routeChangeStart', clearErrors);
107
- }, [router.events]);
97
+ return router.subscribe(() => setRuntimeErrors([]));
98
+ }, [router]);
108
99
 
109
100
  const handleError = useCallback((error) => {
110
101
  if (lowdefyRef.current?._internal?.handleError) {
@@ -115,10 +106,11 @@ function App({ Component }) {
115
106
  }, []);
116
107
 
117
108
  // XProvider extends antd's ConfigProvider; merging antd + antd-X locale packs
118
- // gives X components their built-in strings alongside antd's.
119
- const mergedLocale = antdLocale || antdXLocale
120
- ? { ...(antdLocale ?? {}), ...(antdXLocale ?? {}) }
121
- : undefined;
109
+ // gives X components (Bubble, Sender, Conversations, ...) their built-in strings
110
+ // alongside antd's. antd X ships only en_US + zh_CN; other locales fall back
111
+ // to en_US for X-native strings.
112
+ const mergedLocale =
113
+ antdLocale || antdXLocale ? { ...(antdLocale ?? {}), ...(antdXLocale ?? {}) } : undefined;
122
114
 
123
115
  return (
124
116
  <StyleProvider layer>
@@ -153,7 +145,7 @@ function App({ Component }) {
153
145
  >
154
146
  <Auth>
155
147
  {(auth) => {
156
- return <Component auth={auth} lowdefy={lowdefyRef.current} />;
148
+ return <Routing auth={auth} lowdefy={lowdefyRef.current} router={router} />;
157
149
  }}
158
150
  </Auth>
159
151
  </Suspense>
@@ -166,8 +158,4 @@ function App({ Component }) {
166
158
  );
167
159
  }
168
160
 
169
- const DynamicApp = dynamic(() => Promise.resolve(App), {
170
- ssr: false,
171
- });
172
-
173
- export default DynamicApp;
161
+ export default App;
@@ -0,0 +1,206 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ import { useEffect, useRef } from 'react';
18
+
19
+ import { type, serializer } from '@lowdefy/helpers';
20
+
21
+ // Dev-only agent-state-xray channel — a sibling of Reload.jsx, but with its
22
+ // own /api/reload SSE connection (rather than reusing Reload's) so that
23
+ // connection can carry ?pageId=<pageId>. routes/reload.js only registers a
24
+ // connection as an inspectable tab (lib/docs/tabChannel.js) when that query
25
+ // param is present, which keeps Reload.jsx's plain reload/ping connection
26
+ // out of the tab registry entirely.
27
+ //
28
+ // pageId tracking: this effect depends on `pageId`, so React tears down and
29
+ // reopens the EventSource (a fresh connection, fresh tab id server-side)
30
+ // whenever the developer navigates to a different page — see the longer
31
+ // design note in src/routes/reload.js.
32
+ //
33
+ // Never let a bad payload or a plugin's operator error crash the app this is
34
+ // piggybacking on — every handler is wrapped, and the component itself
35
+ // always renders null.
36
+ function postResult({ basePath, requestId, result }) {
37
+ try {
38
+ fetch(`${basePath}/api/dev-inspect`, {
39
+ method: 'POST',
40
+ headers: { 'Content-Type': 'application/json' },
41
+ body: JSON.stringify({ requestId, result }),
42
+ }).catch(() => {
43
+ // Best-effort — a failed callback just leaves the agent's request to
44
+ // time out server-side.
45
+ });
46
+ } catch {
47
+ // JSON.stringify or fetch throwing synchronously — still best-effort.
48
+ }
49
+ }
50
+
51
+ function buildSnapshot({ lowdefy, pageId }) {
52
+ const context = lowdefy?.contexts?.[`page:${pageId}`];
53
+ if (type.isNone(context)) {
54
+ return { error: `No live context for page "${pageId}".` };
55
+ }
56
+ return serializer.serializeToString({
57
+ pageId,
58
+ state: context.state,
59
+ requests: context.requests,
60
+ eventLog: (context.eventLog ?? []).slice(-50),
61
+ global: lowdefy.lowdefyGlobal,
62
+ user: lowdefy.user,
63
+ input: lowdefy.inputs?.[`page:${pageId}`],
64
+ urlQuery: window.location.search,
65
+ });
66
+ }
67
+
68
+ // Dev-only agent-state-xray: puts a human tester's real browser tab into a
69
+ // checkpoint's recorded state when it loads a page with `?_checkpoint=<name>`
70
+ // in the URL. Fetches the checkpoint's state part from
71
+ // GET <basePath>/api/dev-inspect/checkpoint/<name> (src/routes/devInspect.js
72
+ // — that fetch also loads the checkpoint's recorded requests into
73
+ // devMockRegistry server-side, so this tab's own requests replay recorded
74
+ // data), then injects state the same way e2e-utils' setState does:
75
+ // context._internal.State.set(...) + context._internal.update(). Polls
76
+ // briefly for the page context to exist since this can run before the page
77
+ // has finished mounting. Never lets a bad checkpoint or a missing context
78
+ // break the app — every step is try/catch, and failures just leave the app
79
+ // running unmodified.
80
+ async function bootstrapFromCheckpoint({ basePath, pageId, checkpointName, isCancelled }) {
81
+ try {
82
+ const response = await fetch(
83
+ `${basePath}/api/dev-inspect/checkpoint/${encodeURIComponent(checkpointName)}`
84
+ );
85
+ const body = await response.json();
86
+ if (!response.ok || body?.error) {
87
+ // eslint-disable-next-line no-console
88
+ console.error(
89
+ `Failed to load checkpoint "${checkpointName}": ${body?.error ?? response.statusText}`
90
+ );
91
+ return;
92
+ }
93
+ const payload = serializer.deserialize(body);
94
+
95
+ const deadline = Date.now() + 5000;
96
+ let context;
97
+ while (!isCancelled() && Date.now() < deadline) {
98
+ context = window.lowdefy?.contexts?.[`page:${pageId}`];
99
+ if (context) {
100
+ break;
101
+ }
102
+ // eslint-disable-next-line no-await-in-loop
103
+ await new Promise((resolve) => setTimeout(resolve, 100));
104
+ }
105
+ if (isCancelled() || type.isNone(context)) {
106
+ return;
107
+ }
108
+
109
+ Object.entries(payload.state ?? {}).forEach(([key, value]) => {
110
+ context._internal.State.set(key, value);
111
+ });
112
+ context._internal.update();
113
+ } catch (error) {
114
+ // eslint-disable-next-line no-console
115
+ console.error(`Failed to load checkpoint "${checkpointName}":`, error);
116
+ }
117
+ }
118
+
119
+ function evalExpression({ lowdefy, pageId, expression }) {
120
+ const context = lowdefy?.contexts?.[`page:${pageId}`];
121
+ if (type.isNone(context)) {
122
+ return { error: `No live context for page "${pageId}".` };
123
+ }
124
+ // Callers may pass the operator expression as a JSON-serializable object,
125
+ // or (since it travels as a JSON string end-to-end from an MCP tool
126
+ // argument) as a JSON string that needs one more parse.
127
+ const input = type.isString(expression) ? JSON.parse(expression) : expression;
128
+ const { output, errors } = context._internal.parser.parse({ input, location: 'agent_eval' });
129
+ return {
130
+ value: serializer.serializeToString(output),
131
+ errors: errors.map((error) => error.message),
132
+ };
133
+ }
134
+
135
+ const Inspector = ({ basePath, lowdefy, pageId }) => {
136
+ const pageIdRef = useRef(pageId);
137
+ pageIdRef.current = pageId;
138
+
139
+ useEffect(() => {
140
+ if (type.isNone(pageId)) {
141
+ return undefined;
142
+ }
143
+
144
+ const sse = new EventSource(`${basePath}/api/reload?pageId=${encodeURIComponent(pageId)}`);
145
+
146
+ sse.addEventListener('inspect-request', (message) => {
147
+ let requestId;
148
+ try {
149
+ const data = JSON.parse(message.data);
150
+ requestId = data.requestId;
151
+ const targetPageId = data.pageId ?? pageIdRef.current;
152
+ const result = buildSnapshot({ lowdefy, pageId: targetPageId });
153
+ postResult({ basePath, requestId, result });
154
+ } catch (error) {
155
+ postResult({ basePath, requestId, result: { error: error.message } });
156
+ }
157
+ });
158
+
159
+ sse.addEventListener('eval-request', (message) => {
160
+ let requestId;
161
+ try {
162
+ const data = JSON.parse(message.data);
163
+ requestId = data.requestId;
164
+ const targetPageId = data.pageId ?? pageIdRef.current;
165
+ const result = evalExpression({
166
+ lowdefy,
167
+ pageId: targetPageId,
168
+ expression: data.expression,
169
+ });
170
+ postResult({ basePath, requestId, result });
171
+ } catch (error) {
172
+ postResult({ basePath, requestId, result: { error: error.message } });
173
+ }
174
+ });
175
+
176
+ return () => {
177
+ sse.close();
178
+ };
179
+ }, [basePath, lowdefy, pageId]);
180
+
181
+ useEffect(() => {
182
+ if (type.isNone(pageId)) {
183
+ return undefined;
184
+ }
185
+ const checkpointName = new URLSearchParams(window.location.search).get('_checkpoint');
186
+ if (type.isNone(checkpointName)) {
187
+ return undefined;
188
+ }
189
+
190
+ let cancelled = false;
191
+ bootstrapFromCheckpoint({
192
+ basePath,
193
+ pageId,
194
+ checkpointName,
195
+ isCancelled: () => cancelled,
196
+ });
197
+
198
+ return () => {
199
+ cancelled = true;
200
+ };
201
+ }, [basePath, pageId]);
202
+
203
+ return null;
204
+ };
205
+
206
+ export default Inspector;
@@ -18,10 +18,10 @@ import React, { useEffect, useRef } from 'react';
18
18
  import { GenIcon } from 'react-icons/lib';
19
19
  import Client from '@lowdefy/client';
20
20
 
21
- import BuildErrorPage from './BuildErrorPage.js';
22
- import InstallingPluginsPage from './InstallingPluginsPage.js';
23
- import RestartingPage from './RestartingPage.js';
24
- import usePageConfig from './utils/usePageConfig.js';
21
+ import BuildErrorPage from '../lib/client/BuildErrorPage.jsx';
22
+ import InstallingPluginsPage from '../lib/client/InstallingPluginsPage.jsx';
23
+ import RestartingPage from '../lib/client/RestartingPage.jsx';
24
+ import usePageConfig from '../lib/client/utils/usePageConfig.js';
25
25
 
26
26
  const Page = ({
27
27
  auth,
@@ -48,7 +48,7 @@ const Page = ({
48
48
  }, [pageConfig?._warnings, lowdefy]);
49
49
 
50
50
  if (!pageConfig) {
51
- router.replace(`/404`);
51
+ router.replace({ pathname: '/404' });
52
52
  return '';
53
53
  }
54
54
  if (pageConfig.buildError) {
@@ -16,9 +16,12 @@
16
16
 
17
17
  import React, { useEffect, useState } from 'react';
18
18
 
19
- import useMutateCache from './utils/useMutateCache.js';
20
- import waitForRestartedServer from './utils/waitForRestartedServer.js';
19
+ import useMutateCache from '../lib/client/utils/useMutateCache.js';
20
+ import waitForRestartedServer from '../lib/client/utils/waitForRestartedServer.js';
21
21
 
22
+ // SSE listener — config rebuilds notify via /api/reload. Tailwind CSS updates
23
+ // arrive through Vite HMR now (globals.css is in the dev module graph), so
24
+ // the old tailwind-jit.css link cache-bust is gone.
22
25
  const Reload = ({ children, basePath, lowdefy }) => {
23
26
  const [reset, setReset] = useState(false);
24
27
  const [restarting, setRestarting] = useState(false);
@@ -34,11 +37,6 @@ const Reload = ({ children, basePath, lowdefy }) => {
34
37
  if (lowdefy._internal?.initialised) {
35
38
  lowdefy._internal.initialised = false;
36
39
  }
37
- // Refresh JIT CSS link to pick up newly compiled Tailwind classes
38
- const cssLink = document.getElementById('tailwind-jit-css');
39
- if (cssLink) {
40
- cssLink.href = `${basePath}/tailwind-jit.css?v=${Date.now()}`;
41
- }
42
40
  setReset(true);
43
41
  console.log('Reloaded config.');
44
42
  }, 600);
@@ -0,0 +1,106 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ import React, { Suspense, useEffect, useState } from 'react';
18
+
19
+ import Head from '@lowdefy/client/adapters/Head.js';
20
+ import createLinkComponent from '@lowdefy/client/adapters/Link.js';
21
+
22
+ import BuildingPage from '../lib/client/BuildingPage.jsx';
23
+ import FeedbackMount from './feedback/FeedbackMount.jsx';
24
+ import Inspector from './Inspector.jsx';
25
+ import OpenInEditorListener from './openInEditor/OpenInEditorListener.jsx';
26
+ import Reload from './Reload.jsx';
27
+ import Page from './Page.jsx';
28
+ import setPageId from '../lib/client/setPageId.js';
29
+ import { bumpNavVersion, getReloadVersion } from '../lib/client/utils/useMutateCache.js';
30
+ import useRootConfig from '../lib/client/utils/useRootConfig.js';
31
+
32
+ import actions from '../build/plugins/actions.js';
33
+ import blockMetas from '../build/plugins/blockMetas.json';
34
+ import blocks from '../build/plugins/blocks.js';
35
+ import icons from '../build/plugins/icons.js';
36
+ import operators from '../build/plugins/operators/client.js';
37
+ import staticJsMap from '../build/plugins/operators/clientJsMap.js';
38
+
39
+ // Replaces lib/client/App.js — page resolution driven by the custom router
40
+ // instead of next/router, everything else preserved.
41
+ function Routing({ auth, lowdefy, router }) {
42
+ const { data: rootConfig } = useRootConfig(router.basePath);
43
+ const [location, setLocation] = useState(() => router.getLocation());
44
+
45
+ useEffect(() => {
46
+ return router.subscribe((location) => {
47
+ // Dynamic pages re-resolve per navigation — bump before setLocation so
48
+ // the page config SWR key changes with the navigation. The router
49
+ // notifies with a fresh location object per event, so same-URL
50
+ // navigations already produce a state change.
51
+ bumpNavVersion();
52
+ setLocation(location);
53
+ });
54
+ }, [router]);
55
+
56
+ const [Link] = useState(() => createLinkComponent({ router }));
57
+
58
+ if (rootConfig?.theme) {
59
+ lowdefy.theme = rootConfig.theme;
60
+ }
61
+
62
+ const { redirect, pageId } = setPageId(location, rootConfig);
63
+ useEffect(() => {
64
+ if (redirect) {
65
+ router.replace({ pathname: `/${pageId}` });
66
+ }
67
+ }, [redirect, pageId, router]);
68
+ if (redirect) {
69
+ return '';
70
+ }
71
+
72
+ return (
73
+ <>
74
+ <Inspector basePath={router.basePath} lowdefy={lowdefy} pageId={pageId} />
75
+ <FeedbackMount basePath={router.basePath} lowdefy={lowdefy} pageId={pageId} />
76
+ <OpenInEditorListener basePath={router.basePath} pageId={pageId} />
77
+ <Reload basePath={router.basePath} lowdefy={lowdefy}>
78
+ {(resetContext) => (
79
+ <Suspense key={`${pageId}_${getReloadVersion()}`} fallback={<BuildingPage />}>
80
+ <Page
81
+ auth={auth}
82
+ Components={{ Head, Link }}
83
+ config={{
84
+ rootConfig,
85
+ }}
86
+ jsMap={staticJsMap}
87
+ lowdefy={lowdefy}
88
+ pageId={pageId}
89
+ resetContext={resetContext}
90
+ router={router}
91
+ types={{
92
+ actions,
93
+ blockMetas,
94
+ blocks,
95
+ icons,
96
+ operators,
97
+ }}
98
+ />
99
+ </Suspense>
100
+ )}
101
+ </Reload>
102
+ </>
103
+ );
104
+ }
105
+
106
+ export default Routing;
@@ -0,0 +1,83 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ import React, { Suspense, useEffect, useState } from 'react';
18
+
19
+ import useFeedbackToggle from './useFeedbackToggle.js';
20
+
21
+ // Logged once per page load, not once per mount — React StrictMode / HMR can
22
+ // remount this component without a real reload.
23
+ let bannerLogged = false;
24
+
25
+ function logBannerOnce() {
26
+ if (bannerLogged) {
27
+ return;
28
+ }
29
+ bannerLogged = true;
30
+ try {
31
+ // eslint-disable-next-line no-console
32
+ console.info(
33
+ '%cLowdefy Annotate%c — press Cmd/Ctrl+/ to point, draw, and copy annotations for your coding agent.',
34
+ 'color: #4f9cf9; font-weight: bold;',
35
+ 'color: inherit;'
36
+ );
37
+ // eslint-disable-next-line no-console
38
+ console.info(
39
+ '%cLowdefy Open Code%c — Cmd/Ctrl+click any element to open its yaml in VS Code.',
40
+ 'color: #4f9cf9; font-weight: bold;',
41
+ 'color: inherit;'
42
+ );
43
+ } catch {
44
+ // Never let a console-styling quirk break the app.
45
+ }
46
+ }
47
+
48
+ // Code-split: the overlay (state machine, drawing SVG, review tray) only
49
+ // loads once the developer actually opts in.
50
+ const LazyOverlay = React.lazy(() => import('./FeedbackOverlay.jsx'));
51
+
52
+ // Dev-only, always mounted alongside Inspector.jsx. Mirrors its resilience
53
+ // contract — nothing here may ever throw into the app it's riding along
54
+ // with. Stays inert until the developer presses Cmd/Ctrl+/, at which point
55
+ // the overlay mounts.
56
+ function FeedbackMount({ basePath, lowdefy, pageId }) {
57
+ const [active, setActive] = useState(false);
58
+
59
+ useFeedbackToggle({
60
+ onToggle: () => setActive((current) => !current),
61
+ });
62
+
63
+ useEffect(() => {
64
+ logBannerOnce();
65
+ }, []);
66
+
67
+ return (
68
+ <>
69
+ {active && (
70
+ <Suspense fallback={null}>
71
+ <LazyOverlay
72
+ basePath={basePath}
73
+ lowdefy={lowdefy}
74
+ pageId={pageId}
75
+ onClose={() => setActive(false)}
76
+ />
77
+ </Suspense>
78
+ )}
79
+ </>
80
+ );
81
+ }
82
+
83
+ export default FeedbackMount;