@lotics/cli 0.54.0 → 0.54.2

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.
@@ -86,7 +86,10 @@ export async function startDevServer(args) {
86
86
  // ── HTTP server ────────────────────────────────────────────────────────
87
87
  const server = http.createServer(async (req, res) => {
88
88
  const url = req.url ?? "/";
89
- if (req.method === "GET" && (url === "/" || url === "/index.html")) {
89
+ // Route by pathname `useUrlState` writes a query string onto the wrapper
90
+ // URL, so a refresh / shared link requests `/?…`; the page must still serve.
91
+ const pathname = url.split("?")[0];
92
+ if (req.method === "GET" && (pathname === "/" || pathname === "/index.html")) {
90
93
  res.writeHead(200, {
91
94
  "Content-Type": "text/html; charset=utf-8",
92
95
  "Cache-Control": "no-cache, no-store, must-revalidate",
@@ -24,6 +24,9 @@
24
24
  * wrapper → iframe: { id, type: "result", data } | { id, type: "error", message }
25
25
  * streaming (op "agentRun"): { id, type: "stream-chunk", chunk } * → { id, type: "stream-end" };
26
26
  * the iframe aborts with { id, type: "abort" }.
27
+ * url-state (useUrlState): the iframe reads/writes the wrapper's address bar
28
+ * via the urlState.get/set ops; the wrapper pushes { type: "url-state",
29
+ * params } (no id) on back/forward.
27
30
  */
28
31
  export interface WrapperPageArgs {
29
32
  app_name: string;
@@ -24,6 +24,9 @@
24
24
  * wrapper → iframe: { id, type: "result", data } | { id, type: "error", message }
25
25
  * streaming (op "agentRun"): { id, type: "stream-chunk", chunk } * → { id, type: "stream-end" };
26
26
  * the iframe aborts with { id, type: "abort" }.
27
+ * url-state (useUrlState): the iframe reads/writes the wrapper's address bar
28
+ * via the urlState.get/set ops; the wrapper pushes { type: "url-state",
29
+ * params } (no id) on back/forward.
27
30
  */
28
31
  export function buildWrapperPage(args) {
29
32
  const { app_name, app_id, workspace_id, vite_url, api_url } = args;
@@ -165,6 +168,42 @@ export function buildWrapperPage(args) {
165
168
  return undefined;
166
169
  }
167
170
 
171
+ // urlState.get/set — useUrlState keeps app view-state (filters, search) in
172
+ // THIS wrapper page's address bar so it survives refresh and is shareable,
173
+ // mirroring the production host. Handled locally (the URL is the store; no
174
+ // API hop). The wrapper page is its own top-level page, so its query string
175
+ // is entirely the app's. Repeated keys become arrays. Parse/merge MUST
176
+ // match @lotics/app-sdk parseSearch/serializeMerge and the production host
177
+ // (inline JS here — no module system to share the trivial copy).
178
+ function readUrlParams() {
179
+ const sp = new URLSearchParams(window.location.search);
180
+ const out = {};
181
+ sp.forEach(function (_value, key) {
182
+ if (key in out) return;
183
+ const all = sp.getAll(key);
184
+ out[key] = all.length > 1 ? all : all[0];
185
+ });
186
+ return out;
187
+ }
188
+ function handleUrlStateSet(payload) {
189
+ const params = (payload && payload.params) || {};
190
+ const sp = new URLSearchParams(window.location.search);
191
+ Object.keys(params).forEach(function (key) {
192
+ sp.delete(key);
193
+ const value = params[key];
194
+ if (value === undefined || value === null) return;
195
+ if (Array.isArray(value)) value.forEach(function (item) { sp.append(key, item); });
196
+ else sp.append(key, value);
197
+ });
198
+ const qs = sp.toString();
199
+ const url = window.location.pathname + (qs ? "?" + qs : "") + window.location.hash;
200
+ // pushState/replaceState don't fire popstate, so no echo — the app
201
+ // updated itself optimistically on write.
202
+ if (payload && payload.push) window.history.pushState(null, "", url);
203
+ else window.history.replaceState(null, "", url);
204
+ return undefined;
205
+ }
206
+
168
207
  // Streaming agent runs (op "agentRun"): the response is a stream, so it
169
208
  // can't use rpc()'s single JSON round-trip. POST /_agent_run, read the SSE
170
209
  // body, and forward chunks to the iframe as the SDK's bridged streaming
@@ -226,6 +265,10 @@ export function buildWrapperPage(args) {
226
265
  ? await handleUpload(msg.payload)
227
266
  : msg.op === "openExternal"
228
267
  ? handleOpenExternal(msg.payload)
268
+ : msg.op === "urlState.get"
269
+ ? readUrlParams()
270
+ : msg.op === "urlState.set"
271
+ ? handleUrlStateSet(msg.payload)
229
272
  : await rpc(msg.op, msg.payload);
230
273
  const ms = Math.round(performance.now() - startedAt);
231
274
  console.debug("[lotics-dev] " + msg.op + " " + ms + "ms", data);
@@ -242,6 +285,16 @@ export function buildWrapperPage(args) {
242
285
  );
243
286
  }
244
287
  });
288
+
289
+ // Push the URL back into the app on back/forward so useUrlState
290
+ // re-hydrates (the app's own writes use replace/pushState — no popstate —
291
+ // so there's no echo). Matches the production host.
292
+ window.addEventListener("popstate", function () {
293
+ iframe.contentWindow.postMessage(
294
+ { type: "url-state", params: readUrlParams() },
295
+ VITE_ORIGIN
296
+ );
297
+ });
245
298
  })();
246
299
  </script>
247
300
  </body>
package/dist/src/cli.js CHANGED
@@ -30241,7 +30241,7 @@ import { spawn as spawn2 } from "node:child_process";
30241
30241
  import { tmpdir } from "node:os";
30242
30242
 
30243
30243
  // src/starter_template.ts
30244
- var STARTER_FALLBACK_UI_VERSION = "2.3.0";
30244
+ var STARTER_FALLBACK_UI_VERSION = "6.1.0";
30245
30245
  var STARTER_FALLBACK_SDK_VERSION = "0.11.0";
30246
30246
  var STARTER_REACT_NATIVE_VERSION = "0.85.3";
30247
30247
  function buildStarterTemplate(args) {
@@ -30409,8 +30409,18 @@ export default defineConfig({
30409
30409
  // otherwise serves them without a synthesized default export ("does not
30410
30410
  // provide an export named 'default'"), blanking the iframe. (Production/rollup
30411
30411
  // resolves the interop already, so this is dev-only.)
30412
+ //
30413
+ // \`react-markdown\` (@lotics/ui's Markdown renderer, reached by AgentRun and
30414
+ // any markdown surface) pulls a transitive CJS dep, \`style-to-js\`, whose
30415
+ // default export the dev optimizer won't synthesize unbundled ("does not
30416
+ // provide an export named 'default'") \u2014 blanking the iframe the moment a
30417
+ // markdown component mounts. Pre-bundling react-markdown folds the whole
30418
+ // subtree (incl. style-to-js) into one interop'd chunk; remark-gfm (imported
30419
+ // alongside it) is pre-bundled for the same consistency. Both are regular
30420
+ // deps of @lotics/ui (hoisted), so they need no package.json entry here.
30412
30421
  include: [
30413
30422
  "react-native-svg",
30423
+ "react-markdown", "remark-gfm",
30414
30424
  "react-native-web", "@react-native/normalize-colors",
30415
30425
  "inline-style-prefixer/lib/createPrefixer",
30416
30426
  "inline-style-prefixer/lib/plugins/crossFade",
@@ -30533,15 +30543,23 @@ export default defineConfig({
30533
30543
  content: `import "@lotics/ui/index.css";
30534
30544
  import "@lotics/ui/fonts.css";
30535
30545
  import { PortalHost } from "@lotics/ui/portal";
30546
+ import { LoticsLocaleProvider, vi } from "@lotics/ui/locale";
30536
30547
  import { mount } from "@lotics/app-sdk";
30537
30548
  import App from "./App";
30538
30549
 
30539
30550
  // PortalHost is the render target for @lotics/ui overlays (Popover, Tooltip,
30540
30551
  // Dialog). Without it, Portal renders nothing \u2014 keep it wrapping the app.
30552
+ //
30553
+ // LoticsLocaleProvider supplies the kit's own strings (Pagination, sort
30554
+ // headers, select-all, the Drawer's record nav\u2026) in one language \u2014 Vietnamese
30555
+ // by default, since Lotics apps are Vietnamese. Swap to \`en\` (or your own
30556
+ // LoticsLocale pack) for another language; remove it to fall back to English.
30541
30557
  mount(
30542
- <PortalHost>
30543
- <App />
30544
- </PortalHost>,
30558
+ <LoticsLocaleProvider locale={vi}>
30559
+ <PortalHost>
30560
+ <App />
30561
+ </PortalHost>
30562
+ </LoticsLocaleProvider>,
30545
30563
  );
30546
30564
  `
30547
30565
  },
@@ -31060,6 +31078,42 @@ function buildWrapperPage(args) {
31060
31078
  return undefined;
31061
31079
  }
31062
31080
 
31081
+ // urlState.get/set \u2014 useUrlState keeps app view-state (filters, search) in
31082
+ // THIS wrapper page's address bar so it survives refresh and is shareable,
31083
+ // mirroring the production host. Handled locally (the URL is the store; no
31084
+ // API hop). The wrapper page is its own top-level page, so its query string
31085
+ // is entirely the app's. Repeated keys become arrays. Parse/merge MUST
31086
+ // match @lotics/app-sdk parseSearch/serializeMerge and the production host
31087
+ // (inline JS here \u2014 no module system to share the trivial copy).
31088
+ function readUrlParams() {
31089
+ const sp = new URLSearchParams(window.location.search);
31090
+ const out = {};
31091
+ sp.forEach(function (_value, key) {
31092
+ if (key in out) return;
31093
+ const all = sp.getAll(key);
31094
+ out[key] = all.length > 1 ? all : all[0];
31095
+ });
31096
+ return out;
31097
+ }
31098
+ function handleUrlStateSet(payload) {
31099
+ const params = (payload && payload.params) || {};
31100
+ const sp = new URLSearchParams(window.location.search);
31101
+ Object.keys(params).forEach(function (key) {
31102
+ sp.delete(key);
31103
+ const value = params[key];
31104
+ if (value === undefined || value === null) return;
31105
+ if (Array.isArray(value)) value.forEach(function (item) { sp.append(key, item); });
31106
+ else sp.append(key, value);
31107
+ });
31108
+ const qs = sp.toString();
31109
+ const url = window.location.pathname + (qs ? "?" + qs : "") + window.location.hash;
31110
+ // pushState/replaceState don't fire popstate, so no echo \u2014 the app
31111
+ // updated itself optimistically on write.
31112
+ if (payload && payload.push) window.history.pushState(null, "", url);
31113
+ else window.history.replaceState(null, "", url);
31114
+ return undefined;
31115
+ }
31116
+
31063
31117
  // Streaming agent runs (op "agentRun"): the response is a stream, so it
31064
31118
  // can't use rpc()'s single JSON round-trip. POST /_agent_run, read the SSE
31065
31119
  // body, and forward chunks to the iframe as the SDK's bridged streaming
@@ -31121,6 +31175,10 @@ function buildWrapperPage(args) {
31121
31175
  ? await handleUpload(msg.payload)
31122
31176
  : msg.op === "openExternal"
31123
31177
  ? handleOpenExternal(msg.payload)
31178
+ : msg.op === "urlState.get"
31179
+ ? readUrlParams()
31180
+ : msg.op === "urlState.set"
31181
+ ? handleUrlStateSet(msg.payload)
31124
31182
  : await rpc(msg.op, msg.payload);
31125
31183
  const ms = Math.round(performance.now() - startedAt);
31126
31184
  console.debug("[lotics-dev] " + msg.op + " " + ms + "ms", data);
@@ -31137,6 +31195,16 @@ function buildWrapperPage(args) {
31137
31195
  );
31138
31196
  }
31139
31197
  });
31198
+
31199
+ // Push the URL back into the app on back/forward so useUrlState
31200
+ // re-hydrates (the app's own writes use replace/pushState \u2014 no popstate \u2014
31201
+ // so there's no echo). Matches the production host.
31202
+ window.addEventListener("popstate", function () {
31203
+ iframe.contentWindow.postMessage(
31204
+ { type: "url-state", params: readUrlParams() },
31205
+ VITE_ORIGIN
31206
+ );
31207
+ });
31140
31208
  })();
31141
31209
  </script>
31142
31210
  </body>
@@ -31195,7 +31263,8 @@ async function startDevServer(args) {
31195
31263
  process.once("exit", killViteOnExit);
31196
31264
  const server = http.createServer(async (req, res) => {
31197
31265
  const url = req.url ?? "/";
31198
- if (req.method === "GET" && (url === "/" || url === "/index.html")) {
31266
+ const pathname = url.split("?")[0];
31267
+ if (req.method === "GET" && (pathname === "/" || pathname === "/index.html")) {
31199
31268
  res.writeHead(200, {
31200
31269
  "Content-Type": "text/html; charset=utf-8",
31201
31270
  "Cache-Control": "no-cache, no-store, must-revalidate"
@@ -11,8 +11,10 @@
11
11
  * Conventions (locked decisions):
12
12
  * - Vite + React + TypeScript (strict mode).
13
13
  * - Entry: src/App.tsx with `export default`. main.tsx wires
14
- * `mount(<PortalHost><App/></PortalHost>)` — PortalHost is the render
15
- * target @lotics/ui overlays (Popover/Tooltip/Dialog) need and imports
14
+ * `mount(<LoticsLocaleProvider locale={vi}><PortalHost><App/></PortalHost></LoticsLocaleProvider>)` —
15
+ * LoticsLocaleProvider sets the kit's strings to Vietnamese (Lotics apps are
16
+ * Vietnamese; swap to `en` for English); PortalHost is the render target
17
+ * @lotics/ui overlays (Popover/Tooltip/Dialog) need — and imports
16
18
  * @lotics/ui/index.css (base style reset) + @lotics/ui/fonts.css
17
19
  * (path-independent Inter @font-face bundle — Text renders unstyled without it).
18
20
  * - Vite default `base: "/"` so emitted asset URLs are absolute; the render
@@ -39,7 +41,7 @@ export interface StarterFile {
39
41
  * scaffolds resolve the live version via `fetchLatestNpmVersion` and only
40
42
  * fall back here when the lookup fails.
41
43
  */
42
- export declare const STARTER_FALLBACK_UI_VERSION = "2.3.0";
44
+ export declare const STARTER_FALLBACK_UI_VERSION = "6.1.0";
43
45
  export declare const STARTER_FALLBACK_SDK_VERSION = "0.11.0";
44
46
  /**
45
47
  * react-native pin for scaffolded apps. Matches the monorepo frontend's pin so
@@ -11,8 +11,10 @@
11
11
  * Conventions (locked decisions):
12
12
  * - Vite + React + TypeScript (strict mode).
13
13
  * - Entry: src/App.tsx with `export default`. main.tsx wires
14
- * `mount(<PortalHost><App/></PortalHost>)` — PortalHost is the render
15
- * target @lotics/ui overlays (Popover/Tooltip/Dialog) need and imports
14
+ * `mount(<LoticsLocaleProvider locale={vi}><PortalHost><App/></PortalHost></LoticsLocaleProvider>)` —
15
+ * LoticsLocaleProvider sets the kit's strings to Vietnamese (Lotics apps are
16
+ * Vietnamese; swap to `en` for English); PortalHost is the render target
17
+ * @lotics/ui overlays (Popover/Tooltip/Dialog) need — and imports
16
18
  * @lotics/ui/index.css (base style reset) + @lotics/ui/fonts.css
17
19
  * (path-independent Inter @font-face bundle — Text renders unstyled without it).
18
20
  * - Vite default `base: "/"` so emitted asset URLs are absolute; the render
@@ -35,7 +37,7 @@
35
37
  * scaffolds resolve the live version via `fetchLatestNpmVersion` and only
36
38
  * fall back here when the lookup fails.
37
39
  */
38
- export const STARTER_FALLBACK_UI_VERSION = "2.3.0";
40
+ export const STARTER_FALLBACK_UI_VERSION = "6.1.0";
39
41
  export const STARTER_FALLBACK_SDK_VERSION = "0.11.0";
40
42
  /**
41
43
  * react-native pin for scaffolded apps. Matches the monorepo frontend's pin so
@@ -209,8 +211,18 @@ export default defineConfig({
209
211
  // otherwise serves them without a synthesized default export ("does not
210
212
  // provide an export named 'default'"), blanking the iframe. (Production/rollup
211
213
  // resolves the interop already, so this is dev-only.)
214
+ //
215
+ // \`react-markdown\` (@lotics/ui's Markdown renderer, reached by AgentRun and
216
+ // any markdown surface) pulls a transitive CJS dep, \`style-to-js\`, whose
217
+ // default export the dev optimizer won't synthesize unbundled ("does not
218
+ // provide an export named 'default'") — blanking the iframe the moment a
219
+ // markdown component mounts. Pre-bundling react-markdown folds the whole
220
+ // subtree (incl. style-to-js) into one interop'd chunk; remark-gfm (imported
221
+ // alongside it) is pre-bundled for the same consistency. Both are regular
222
+ // deps of @lotics/ui (hoisted), so they need no package.json entry here.
212
223
  include: [
213
224
  "react-native-svg",
225
+ "react-markdown", "remark-gfm",
214
226
  "react-native-web", "@react-native/normalize-colors",
215
227
  "inline-style-prefixer/lib/createPrefixer",
216
228
  "inline-style-prefixer/lib/plugins/crossFade",
@@ -333,15 +345,23 @@ export default defineConfig({
333
345
  content: `import "@lotics/ui/index.css";
334
346
  import "@lotics/ui/fonts.css";
335
347
  import { PortalHost } from "@lotics/ui/portal";
348
+ import { LoticsLocaleProvider, vi } from "@lotics/ui/locale";
336
349
  import { mount } from "@lotics/app-sdk";
337
350
  import App from "./App";
338
351
 
339
352
  // PortalHost is the render target for @lotics/ui overlays (Popover, Tooltip,
340
353
  // Dialog). Without it, Portal renders nothing — keep it wrapping the app.
354
+ //
355
+ // LoticsLocaleProvider supplies the kit's own strings (Pagination, sort
356
+ // headers, select-all, the Drawer's record nav…) in one language — Vietnamese
357
+ // by default, since Lotics apps are Vietnamese. Swap to \`en\` (or your own
358
+ // LoticsLocale pack) for another language; remove it to fall back to English.
341
359
  mount(
342
- <PortalHost>
343
- <App />
344
- </PortalHost>,
360
+ <LoticsLocaleProvider locale={vi}>
361
+ <PortalHost>
362
+ <App />
363
+ </PortalHost>
364
+ </LoticsLocaleProvider>,
345
365
  );
346
366
  `,
347
367
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/cli",
3
- "version": "0.54.0",
3
+ "version": "0.54.2",
4
4
  "description": "Lotics SDK and CLI for AI agents",
5
5
  "type": "module",
6
6
  "bin": {