@lotics/cli 0.55.0 → 0.56.0

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.
@@ -24,9 +24,11 @@
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
- * urlState (useUrlState + router adapter): the iframe reads/writes the wrapper's
28
- * address bar via urlState.get/set/go; set push/replace and go drive history,
29
- * and back/forward broadcast { type: "url-state", params } back to the iframe.
27
+ * urlState (useUrlState): the iframe reads/writes the wrapper's address bar via
28
+ * urlState.get/set; set writes in place (replaceState), and browser
29
+ * back/forward broadcast { type: "url-state", params } back to the iframe.
30
+ * (In-app routing isn't here — the app owns the iframe's own url; see
31
+ * @lotics/app-sdk/router.)
30
32
  */
31
33
  export interface WrapperPageArgs {
32
34
  app_name: string;
@@ -24,9 +24,11 @@
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
- * urlState (useUrlState + router adapter): the iframe reads/writes the wrapper's
28
- * address bar via urlState.get/set/go; set push/replace and go drive history,
29
- * and back/forward broadcast { type: "url-state", params } back to the iframe.
27
+ * urlState (useUrlState): the iframe reads/writes the wrapper's address bar via
28
+ * urlState.get/set; set writes in place (replaceState), and browser
29
+ * back/forward broadcast { type: "url-state", params } back to the iframe.
30
+ * (In-app routing isn't here — the app owns the iframe's own url; see
31
+ * @lotics/app-sdk/router.)
30
32
  */
31
33
  export function buildWrapperPage(args) {
32
34
  const { app_name, app_id, workspace_id, vite_url, api_url } = args;
@@ -78,9 +80,28 @@ export function buildWrapperPage(args) {
78
80
  const iframe = document.getElementById("app");
79
81
 
80
82
  // Pass the wrapper's own origin to the app via ?lotics_host= so the app
81
- // SDK can origin-lock its postMessage bridge.
82
- iframe.src = VITE_URL + (VITE_URL.indexOf("?") >= 0 ? "&" : "?")
83
- + "lotics_host=" + encodeURIComponent(window.location.origin);
83
+ // SDK can origin-lock its postMessage bridge, and bake the saved screen
84
+ // (the wrapper url's _loc, kept current by AppRouter's mirror) into the src
85
+ // path so a refresh boots the app at that screen — mirrors the production
86
+ // host. _loc is honoured only when it resolves same-origin to the Vite
87
+ // server (a url param flowing into an iframe src is a redirect vector);
88
+ // anything else falls back to the app root.
89
+ const appSrc = new URL(VITE_URL);
90
+ const savedLoc = new URLSearchParams(window.location.search).get("_loc");
91
+ if (savedLoc) {
92
+ try {
93
+ const resolved = new URL(savedLoc, VITE_ORIGIN);
94
+ if (resolved.origin === VITE_ORIGIN) {
95
+ appSrc.pathname = resolved.pathname;
96
+ appSrc.search = resolved.search;
97
+ appSrc.hash = resolved.hash;
98
+ }
99
+ } catch (e) {
100
+ // malformed _loc -> app root
101
+ }
102
+ }
103
+ appSrc.searchParams.set("lotics_host", window.location.origin);
104
+ iframe.src = appSrc.toString();
84
105
 
85
106
  async function rpc(op, payload) {
86
107
  const res = await fetch("/_rpc", {
@@ -185,7 +206,6 @@ export function buildWrapperPage(args) {
185
206
  });
186
207
  return out;
187
208
  }
188
- var lastPushAt = 0;
189
209
  function handleUrlStateSet(payload) {
190
210
  const params = (payload && payload.params) || {};
191
211
  const sp = new URLSearchParams(window.location.search);
@@ -198,21 +218,9 @@ export function buildWrapperPage(args) {
198
218
  });
199
219
  const qs = sp.toString();
200
220
  const url = window.location.pathname + (qs ? "?" + qs : "") + window.location.hash;
201
- // push adds a back-able entry (the router adapter); replace for filter
202
- // churn. Flood guard: coerce sub-100ms pushes to replace. pushState/
203
- // replaceState don't fire popstate, so no echo. Mirrors the production host.
204
- const now = Date.now();
205
- if (payload && payload.push && now - lastPushAt > 100) {
206
- lastPushAt = now;
207
- window.history.pushState(null, "", url);
208
- } else {
209
- window.history.replaceState(null, "", url);
210
- }
211
- return undefined;
212
- }
213
- function handleUrlStateGo(payload) {
214
- const delta = payload && payload.delta;
215
- if (typeof delta === "number") window.history.go(delta);
221
+ // View-state writes in place never a history entry. replaceState doesn't
222
+ // fire popstate, so no echo. Mirrors the production host.
223
+ window.history.replaceState(null, "", url);
216
224
  return undefined;
217
225
  }
218
226
 
@@ -281,8 +289,6 @@ export function buildWrapperPage(args) {
281
289
  ? readUrlParams()
282
290
  : msg.op === "urlState.set"
283
291
  ? handleUrlStateSet(msg.payload)
284
- : msg.op === "urlState.go"
285
- ? handleUrlStateGo(msg.payload)
286
292
  : await rpc(msg.op, msg.payload);
287
293
  const ms = Math.round(performance.now() - startedAt);
288
294
  console.debug("[lotics-dev] " + msg.op + " " + ms + "ms", data);
@@ -300,9 +306,9 @@ export function buildWrapperPage(args) {
300
306
  }
301
307
  });
302
308
 
303
- // Browser/adapter back/forward → broadcast the new params so useUrlState and
304
- // the router adapter re-hydrate (the app's own set writes use push/replace
305
- // State — no popstate — so there's no echo). Mirrors the production host.
309
+ // Browser back/forward → broadcast the new params so useUrlState re-hydrates
310
+ // (the app's own set writes use replaceState — no popstate — so there's no
311
+ // echo). Mirrors the production host.
306
312
  window.addEventListener("popstate", function () {
307
313
  iframe.contentWindow.postMessage(
308
314
  { type: "url-state", params: readUrlParams() },
package/dist/src/cli.js CHANGED
@@ -30574,10 +30574,10 @@ mount(
30574
30574
  path: "src/App.tsx",
30575
30575
  // Default scaffold = a minimal in-app router example (a list screen and a
30576
30576
  // detail screen), so a new app starts with the recommended routing shape:
30577
- // - The app uses react-router via `AppRouter` (from @lotics/app-sdk/router),
30578
- // which makes screens real addressable URLs — embedded, they live in the
30579
- // host address bar (?_loc=…) and the browser Back/Forward walk screens;
30580
- // standalone, real path URLs.
30577
+ // - The app uses react-router via `AppRouter` (from @lotics/app-sdk/router).
30578
+ // The app owns its own url — embedded, that's the iframe's own url (the
30579
+ // host never sees it, so navigation never reloads the app); standalone,
30580
+ // real path URLs. Browser Back/Forward walk app screens in both.
30581
30581
  // - The full-container layout pattern is preserved in `Screen`: an outer
30582
30582
  // <View flex:1> claims the iframe height (index.html sets
30583
30583
  // html/body/#root to 100% + #root is a flex column). Keep that chain
@@ -30592,11 +30592,13 @@ import { Card } from "@lotics/ui/card";
30592
30592
  import { Text } from "@lotics/ui/text";
30593
30593
  import { Button } from "@lotics/ui/button";
30594
30594
 
30595
- // AppRouter makes the app's screens real, addressable URLs \u2014 write plain
30596
- // react-router (useNavigate / useParams / <Link>) and it handles both modes:
30597
- // - Embedded in the Lotics host: the current screen lives in the host address
30598
- // bar (?_loc=\u2026) \u2014 shareable + refresh-survivable \u2014 and the browser Back /
30599
- // Forward buttons walk app screens (then leave the app).
30595
+ // AppRouter makes the app's screens real URLs \u2014 write plain react-router
30596
+ // (useNavigate / useParams / <Link>) and it handles both modes. The app owns its
30597
+ // own url, so navigation never reloads the app:
30598
+ // - Embedded in the Lotics host: the app drives the iframe's OWN url (invisible
30599
+ // to the user, never seen by the host). Browser Back / Forward walk app
30600
+ // screens; the screen is also mirrored to the host url, so it's shareable and
30601
+ // survives a full refresh (handled by AppRouter \u2014 no extra code).
30600
30602
  // - Standalone at <slug>.lotics.app: a normal browser router with real path URLs.
30601
30603
 
30602
30604
  const ITEMS = [
@@ -30843,10 +30845,11 @@ the full export list at https://www.npmjs.com/package/@lotics/ui.
30843
30845
  ## Routing
30844
30846
 
30845
30847
  \`src/App.tsx\` ships a minimal in-app router. Write plain react-router and wrap
30846
- your routes in \`AppRouter\` from \`@lotics/app-sdk/router\` \u2014 it makes screens real,
30847
- addressable URLs in both modes: embedded in the Lotics host the current screen
30848
- lives in the host address bar (\`?_loc=\u2026\`, shareable + refresh-survivable) and the
30849
- browser Back/Forward walk app screens; standalone (\`<slug>.lotics.app\`) it's a
30848
+ your routes in \`AppRouter\` from \`@lotics/app-sdk/router\` \u2014 the app owns its own
30849
+ url, so navigation never reloads the app. Embedded in the Lotics host it drives
30850
+ the iframe's own url (invisible to the user, never seen by the host); browser
30851
+ Back/Forward walk app screens, and the screen is mirrored to the host url so it's
30852
+ shareable and survives a full refresh. Standalone (\`<slug>.lotics.app\`) it's a
30850
30853
  normal browser router with real path URLs. A single-screen app can drop the
30851
30854
  router and render one screen directly.
30852
30855
 
@@ -31056,9 +31059,28 @@ function buildWrapperPage(args) {
31056
31059
  const iframe = document.getElementById("app");
31057
31060
 
31058
31061
  // Pass the wrapper's own origin to the app via ?lotics_host= so the app
31059
- // SDK can origin-lock its postMessage bridge.
31060
- iframe.src = VITE_URL + (VITE_URL.indexOf("?") >= 0 ? "&" : "?")
31061
- + "lotics_host=" + encodeURIComponent(window.location.origin);
31062
+ // SDK can origin-lock its postMessage bridge, and bake the saved screen
31063
+ // (the wrapper url's _loc, kept current by AppRouter's mirror) into the src
31064
+ // path so a refresh boots the app at that screen \u2014 mirrors the production
31065
+ // host. _loc is honoured only when it resolves same-origin to the Vite
31066
+ // server (a url param flowing into an iframe src is a redirect vector);
31067
+ // anything else falls back to the app root.
31068
+ const appSrc = new URL(VITE_URL);
31069
+ const savedLoc = new URLSearchParams(window.location.search).get("_loc");
31070
+ if (savedLoc) {
31071
+ try {
31072
+ const resolved = new URL(savedLoc, VITE_ORIGIN);
31073
+ if (resolved.origin === VITE_ORIGIN) {
31074
+ appSrc.pathname = resolved.pathname;
31075
+ appSrc.search = resolved.search;
31076
+ appSrc.hash = resolved.hash;
31077
+ }
31078
+ } catch (e) {
31079
+ // malformed _loc -> app root
31080
+ }
31081
+ }
31082
+ appSrc.searchParams.set("lotics_host", window.location.origin);
31083
+ iframe.src = appSrc.toString();
31062
31084
 
31063
31085
  async function rpc(op, payload) {
31064
31086
  const res = await fetch("/_rpc", {
@@ -31163,7 +31185,6 @@ function buildWrapperPage(args) {
31163
31185
  });
31164
31186
  return out;
31165
31187
  }
31166
- var lastPushAt = 0;
31167
31188
  function handleUrlStateSet(payload) {
31168
31189
  const params = (payload && payload.params) || {};
31169
31190
  const sp = new URLSearchParams(window.location.search);
@@ -31176,21 +31197,9 @@ function buildWrapperPage(args) {
31176
31197
  });
31177
31198
  const qs = sp.toString();
31178
31199
  const url = window.location.pathname + (qs ? "?" + qs : "") + window.location.hash;
31179
- // push adds a back-able entry (the router adapter); replace for filter
31180
- // churn. Flood guard: coerce sub-100ms pushes to replace. pushState/
31181
- // replaceState don't fire popstate, so no echo. Mirrors the production host.
31182
- const now = Date.now();
31183
- if (payload && payload.push && now - lastPushAt > 100) {
31184
- lastPushAt = now;
31185
- window.history.pushState(null, "", url);
31186
- } else {
31187
- window.history.replaceState(null, "", url);
31188
- }
31189
- return undefined;
31190
- }
31191
- function handleUrlStateGo(payload) {
31192
- const delta = payload && payload.delta;
31193
- if (typeof delta === "number") window.history.go(delta);
31200
+ // View-state writes in place \u2014 never a history entry. replaceState doesn't
31201
+ // fire popstate, so no echo. Mirrors the production host.
31202
+ window.history.replaceState(null, "", url);
31194
31203
  return undefined;
31195
31204
  }
31196
31205
 
@@ -31259,8 +31268,6 @@ function buildWrapperPage(args) {
31259
31268
  ? readUrlParams()
31260
31269
  : msg.op === "urlState.set"
31261
31270
  ? handleUrlStateSet(msg.payload)
31262
- : msg.op === "urlState.go"
31263
- ? handleUrlStateGo(msg.payload)
31264
31271
  : await rpc(msg.op, msg.payload);
31265
31272
  const ms = Math.round(performance.now() - startedAt);
31266
31273
  console.debug("[lotics-dev] " + msg.op + " " + ms + "ms", data);
@@ -31278,9 +31285,9 @@ function buildWrapperPage(args) {
31278
31285
  }
31279
31286
  });
31280
31287
 
31281
- // Browser/adapter back/forward \u2192 broadcast the new params so useUrlState and
31282
- // the router adapter re-hydrate (the app's own set writes use push/replace
31283
- // State \u2014 no popstate \u2014 so there's no echo). Mirrors the production host.
31288
+ // Browser back/forward \u2192 broadcast the new params so useUrlState re-hydrates
31289
+ // (the app's own set writes use replaceState \u2014 no popstate \u2014 so there's no
31290
+ // echo). Mirrors the production host.
31284
31291
  window.addEventListener("popstate", function () {
31285
31292
  iframe.contentWindow.postMessage(
31286
31293
  { type: "url-state", params: readUrlParams() },
@@ -379,10 +379,10 @@ mount(
379
379
  path: "src/App.tsx",
380
380
  // Default scaffold = a minimal in-app router example (a list screen and a
381
381
  // detail screen), so a new app starts with the recommended routing shape:
382
- // - The app uses react-router via `AppRouter` (from @lotics/app-sdk/router),
383
- // which makes screens real addressable URLs — embedded, they live in the
384
- // host address bar (?_loc=…) and the browser Back/Forward walk screens;
385
- // standalone, real path URLs.
382
+ // - The app uses react-router via `AppRouter` (from @lotics/app-sdk/router).
383
+ // The app owns its own url — embedded, that's the iframe's own url (the
384
+ // host never sees it, so navigation never reloads the app); standalone,
385
+ // real path URLs. Browser Back/Forward walk app screens in both.
386
386
  // - The full-container layout pattern is preserved in `Screen`: an outer
387
387
  // <View flex:1> claims the iframe height (index.html sets
388
388
  // html/body/#root to 100% + #root is a flex column). Keep that chain
@@ -397,11 +397,13 @@ import { Card } from "@lotics/ui/card";
397
397
  import { Text } from "@lotics/ui/text";
398
398
  import { Button } from "@lotics/ui/button";
399
399
 
400
- // AppRouter makes the app's screens real, addressable URLs — write plain
401
- // react-router (useNavigate / useParams / <Link>) and it handles both modes:
402
- // - Embedded in the Lotics host: the current screen lives in the host address
403
- // bar (?_loc=…) shareable + refresh-survivable and the browser Back /
404
- // Forward buttons walk app screens (then leave the app).
400
+ // AppRouter makes the app's screens real URLs — write plain react-router
401
+ // (useNavigate / useParams / <Link>) and it handles both modes. The app owns its
402
+ // own url, so navigation never reloads the app:
403
+ // - Embedded in the Lotics host: the app drives the iframe's OWN url (invisible
404
+ // to the user, never seen by the host). Browser Back / Forward walk app
405
+ // screens; the screen is also mirrored to the host url, so it's shareable and
406
+ // survives a full refresh (handled by AppRouter — no extra code).
405
407
  // - Standalone at <slug>.lotics.app: a normal browser router with real path URLs.
406
408
 
407
409
  const ITEMS = [
@@ -648,10 +650,11 @@ the full export list at https://www.npmjs.com/package/@lotics/ui.
648
650
  ## Routing
649
651
 
650
652
  \`src/App.tsx\` ships a minimal in-app router. Write plain react-router and wrap
651
- your routes in \`AppRouter\` from \`@lotics/app-sdk/router\` — it makes screens real,
652
- addressable URLs in both modes: embedded in the Lotics host the current screen
653
- lives in the host address bar (\`?_loc=…\`, shareable + refresh-survivable) and the
654
- browser Back/Forward walk app screens; standalone (\`<slug>.lotics.app\`) it's a
653
+ your routes in \`AppRouter\` from \`@lotics/app-sdk/router\` — the app owns its own
654
+ url, so navigation never reloads the app. Embedded in the Lotics host it drives
655
+ the iframe's own url (invisible to the user, never seen by the host); browser
656
+ Back/Forward walk app screens, and the screen is mirrored to the host url so it's
657
+ shareable and survives a full refresh. Standalone (\`<slug>.lotics.app\`) it's a
655
658
  normal browser router with real path URLs. A single-screen app can drop the
656
659
  router and render one screen directly.
657
660
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/cli",
3
- "version": "0.55.0",
3
+ "version": "0.56.0",
4
4
  "description": "Lotics SDK and CLI for AI agents",
5
5
  "type": "module",
6
6
  "bin": {