@lotics/app-sdk 0.89.0 → 0.90.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.
- package/AGENTS.md +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/open_app.d.ts +15 -0
- package/dist/src/open_app.js +18 -0
- package/dist/src/rpc.d.ts +1 -1
- package/dist/src/rpc.js +4 -0
- package/docs/navigation_and_state.md +1 -0
- package/docs/runtime.md +34 -0
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -24,7 +24,7 @@ signature; open the file.**
|
|
|
24
24
|
| [docs/navigation_and_state.md](./docs/navigation_and_state.md) | `AppRouter` (embedded/standalone URL model), `useUrlState` + `urlParam` codecs, `useRecents`. |
|
|
25
25
|
| [docs/ai.md](./docs/ai.md) | `useAgentRun` (structured vs free-text, streaming ai-sdk `parts` → `AgentRun`, the agent's ask-back — `pendingChoice`/`answerChoice` over the parked `awaiting_input` state — and the `AgentRunLanding` every leg resolves), `askAi` — plus the fields-vs-file razor for choosing between them — and `useAiContext` (push the current screen's view state to the member's ambient chat agent; caps, push-only semantics; a chat mutation refetches your queries through the realtime channel, not a separate poke). **A `file` input carries its own content** — no reader tool to declare. **An agent reaches record DATA only through its declared `query_aliases` / `workflow_aliases`.** Also **what the member's own chat agent can do with your app while it is open** — the alias catalog it reads and how to shape a mutating alias for it. |
|
|
26
26
|
| [docs/security.md](./docs/security.md) | **Read before shipping** — the owner-principal model, `is_current_member` scoping, write attribution, group gates, public-app bounds, what runtime refinement cannot widen, and why a per-input bound is a tenancy floor rather than an authorization check (a caller-supplied id must be intersected with the record server-side). |
|
|
27
|
-
| [docs/runtime.md](./docs/runtime.md) | `mount()`, the two transports, `rpc()`, the design-time mock harness (`fixture` + `?__mock=1` — queries AND workflows, so an AI screen's in-flight/done/error states are reviewable without running or paying for anything), `openExternal`/`downloadFile`, geofencing, and the publish chain for SDK contributors. |
|
|
27
|
+
| [docs/runtime.md](./docs/runtime.md) | `mount()`, the two transports, `rpc()`, the design-time mock harness (`fixture` + `?__mock=1` — queries AND workflows, so an AI screen's in-flight/done/error states are reviewable without running or paying for anything), `openExternal`/`openApp`/`downloadFile`, geofencing, and the publish chain for SDK contributors. |
|
|
28
28
|
|
|
29
29
|
## Non-negotiables (each detailed in its doc)
|
|
30
30
|
|
package/dist/src/index.d.ts
CHANGED
|
@@ -26,6 +26,7 @@ export type { GeofenceZone, GeoCoords, GeofenceOutcome, GeofenceOptions } from "
|
|
|
26
26
|
export { rpc, isEmbedded } from "./rpc.js";
|
|
27
27
|
export type { RpcOp, AiContextValue, AiContextRecordRef } from "./rpc.js";
|
|
28
28
|
export { openExternal } from "./open_external.js";
|
|
29
|
+
export { openApp } from "./open_app.js";
|
|
29
30
|
export { askAi, type AskAiArgs } from "./ask_ai.js";
|
|
30
31
|
export { downloadFile } from "./download.js";
|
|
31
32
|
export { readMembers } from "./members.js";
|
package/dist/src/index.js
CHANGED
|
@@ -21,6 +21,7 @@ export { useViewer } from "./viewer.js";
|
|
|
21
21
|
export { requestGeofencedLocation, isWithinZone } from "./geolocation.js";
|
|
22
22
|
export { rpc, isEmbedded } from "./rpc.js";
|
|
23
23
|
export { openExternal } from "./open_external.js";
|
|
24
|
+
export { openApp } from "./open_app.js";
|
|
24
25
|
export { askAi } from "./ask_ai.js";
|
|
25
26
|
export { downloadFile } from "./download.js";
|
|
26
27
|
export { readMembers } from "./members.js";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Open a record's screen in the sibling app that owns it — the cross-app hop.
|
|
3
|
+
* The host owns app routing and lands the viewer on `route` inside `appId`,
|
|
4
|
+
* same tab, chrome kept. `route` is the target app's own in-app path, exactly
|
|
5
|
+
* as its router declares it (`/` for its register).
|
|
6
|
+
*
|
|
7
|
+
* ```tsx
|
|
8
|
+
* import { openApp } from "@lotics/app-sdk";
|
|
9
|
+
* await openApp(PLANNING_APP_ID, `/plan/${record.id}`);
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* Standalone (`<slug>.lotics.app`) has no sibling apps, so the call rejects
|
|
13
|
+
* there — gate the control on `isEmbedded()`.
|
|
14
|
+
*/
|
|
15
|
+
export declare function openApp(appId: string, route?: string): Promise<void>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { rpc } from "./rpc.js";
|
|
2
|
+
/**
|
|
3
|
+
* Open a record's screen in the sibling app that owns it — the cross-app hop.
|
|
4
|
+
* The host owns app routing and lands the viewer on `route` inside `appId`,
|
|
5
|
+
* same tab, chrome kept. `route` is the target app's own in-app path, exactly
|
|
6
|
+
* as its router declares it (`/` for its register).
|
|
7
|
+
*
|
|
8
|
+
* ```tsx
|
|
9
|
+
* import { openApp } from "@lotics/app-sdk";
|
|
10
|
+
* await openApp(PLANNING_APP_ID, `/plan/${record.id}`);
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* Standalone (`<slug>.lotics.app`) has no sibling apps, so the call rejects
|
|
14
|
+
* there — gate the control on `isEmbedded()`.
|
|
15
|
+
*/
|
|
16
|
+
export function openApp(appId, route = "/") {
|
|
17
|
+
return rpc("openApp", { app_id: appId, route });
|
|
18
|
+
}
|
package/dist/src/rpc.d.ts
CHANGED
|
@@ -20,7 +20,7 @@ import { type UrlParams, type UrlParamsPatch } from "./url_params.js";
|
|
|
20
20
|
* app → host: { id, op, payload }
|
|
21
21
|
* host → app: { id, type: "result", data } | { id, type: "error", message }
|
|
22
22
|
*/
|
|
23
|
-
export type RpcOp = "query" | "field_options" | "workflow" | "agentRuns" | "agentRun.get" | "agentRun.cancel" | "upload" | "members" | "context" | "openExternal" | "askAi" | "urlState.get" | "urlState.set" | "comments.list" | "comments.create" | "comments.update" | "comments.delete" | "comments.counts";
|
|
23
|
+
export type RpcOp = "query" | "field_options" | "workflow" | "agentRuns" | "agentRun.get" | "agentRun.cancel" | "upload" | "members" | "context" | "openExternal" | "openApp" | "askAi" | "urlState.get" | "urlState.set" | "comments.list" | "comments.create" | "comments.update" | "comments.delete" | "comments.counts";
|
|
24
24
|
/** Payload for starting a streaming agent run. */
|
|
25
25
|
export interface AgentRunPayload {
|
|
26
26
|
alias: string;
|
package/dist/src/rpc.js
CHANGED
|
@@ -666,6 +666,10 @@ function rpcStandalone(op, payload) {
|
|
|
666
666
|
return standaloneContext();
|
|
667
667
|
case "openExternal":
|
|
668
668
|
return standaloneOpenExternal(payload);
|
|
669
|
+
case "openApp":
|
|
670
|
+
// A standalone app is one page at its own address: no host routing
|
|
671
|
+
// between apps, no sibling to reach.
|
|
672
|
+
return Promise.reject(new Error("openApp needs the Lotics host: a standalone app has no sibling apps to open. Gate the control on isEmbedded()."));
|
|
669
673
|
case "askAi":
|
|
670
674
|
// The chat surface lives in the Lotics host — a standalone page has
|
|
671
675
|
// nowhere to hand off to.
|
|
@@ -287,3 +287,4 @@ default `JSON.stringify`) and `max` (default **5**).
|
|
|
287
287
|
| `urlParam`, `UrlParamCodec`, `OptionalUrlParamCodec`, `UrlParams`, `UrlParamValue` | `@lotics/app-sdk` | `dist/src/url_params.d.ts` |
|
|
288
288
|
| `useRecents`, `RecentsApi`, `RecentsOptions` | `@lotics/app-sdk` | `dist/src/use_recents.d.ts` |
|
|
289
289
|
| `isEmbedded` | `@lotics/app-sdk` | `dist/src/rpc.d.ts` |
|
|
290
|
+
| `openApp` | `@lotics/app-sdk` | `dist/src/open_app.d.ts` |
|
package/docs/runtime.md
CHANGED
|
@@ -185,6 +185,7 @@ surfaces:
|
|
|
185
185
|
| `agentRun.get`, `agentRun.cancel` | yes | **no** — the dev forwarder doesn't implement them (`"Unknown RPC op: …"`) | yes |
|
|
186
186
|
| `agentRuns` (session history) | **no** — the product host doesn't implement it either (`"Unknown RPC op: agentRuns"`) | **no** | yes |
|
|
187
187
|
| `askAi` | yes | **no** — `"Unknown RPC op: askAi"` | rejects — `"askAi is only available when the app runs inside Lotics"` |
|
|
188
|
+
| `openApp` | yes — routes to the sibling in the same tab | yes — opens the sibling on the web app in a new tab (one app is served locally) | rejects — `"openApp needs the Lotics host …"` |
|
|
188
189
|
|
|
189
190
|
**Limitation:** Don't build a session-log UI on `useAgentRuns`; keep the log in app
|
|
190
191
|
state from live `useAgentRun` results (see [ai](./ai.md)). Server-side *cancel*
|
|
@@ -226,6 +227,7 @@ semantics are documented:
|
|
|
226
227
|
| `members` | `{ group? }` | `{ members }` | [members & options](./members_and_options.md) |
|
|
227
228
|
| `context` | `{}` | app identity (below) | this doc |
|
|
228
229
|
| `openExternal` | `{ url }` | `void` | this doc |
|
|
230
|
+
| `openApp` | `{ app_id, route }` | `void` | this doc |
|
|
229
231
|
| `askAi` | prompt/files/records seed | `void` | [ai](./ai.md) |
|
|
230
232
|
| `agentRuns` | `{ session_id, limit?, offset? }` | `{ runs }` | [ai](./ai.md) |
|
|
231
233
|
| `agentRun.get` | `{ run_id }` | `{ run }` | [ai](./ai.md) |
|
|
@@ -267,6 +269,38 @@ page (standalone). Opens in a new tab with `noopener,noreferrer`.
|
|
|
267
269
|
`FilePreview`/`FileGalleryModal` (see [files](./files.md)); `openExternal` is
|
|
268
270
|
"leave the app".
|
|
269
271
|
|
|
272
|
+
## `openApp()` — the cross-app hop
|
|
273
|
+
|
|
274
|
+
```tsx
|
|
275
|
+
import { openApp, isEmbedded } from "@lotics/app-sdk";
|
|
276
|
+
await openApp(PLANNING_APP_ID, `/plan/${record.id}`);
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
`openApp(appId: string, route?: string): Promise<void>` (`dist/src/open_app.d.ts`).
|
|
280
|
+
A workspace built as several apps around one spine shows another app's record
|
|
281
|
+
read-only with a way THROUGH to the app that owns it; this is the way through.
|
|
282
|
+
The host owns app routing, so it lands the viewer on `route` inside `appId` in
|
|
283
|
+
the **same tab**, chrome kept — `_loc` carries the screen exactly as
|
|
284
|
+
`AppRouter` mirrors it, so the target boots at that record rather than at its
|
|
285
|
+
root. `route` is the target app's own in-app path, exactly as its router
|
|
286
|
+
declares it (`/` for its register); it is the target's to define.
|
|
287
|
+
|
|
288
|
+
- **Checked at the bridge, host-side**: the id by shape (`app_…`), the route as
|
|
289
|
+
an in-app path — starts with `/`, never `//` or a scheme. An app never
|
|
290
|
+
assembles the host's URL itself.
|
|
291
|
+
- **By transport**: embedded routes in place; `lotics app dev` serves one app
|
|
292
|
+
and has no shell to route inside, so it opens the sibling on the web app in a
|
|
293
|
+
new tab; standalone rejects (`openApp needs the Lotics host …`) — gate the
|
|
294
|
+
control on `isEmbedded()`.
|
|
295
|
+
- A sibling's id is this workspace's: a copy of the suite has different ones,
|
|
296
|
+
and the portability gate refuses a concrete `app_` in `src/` and in every
|
|
297
|
+
`.md`. The id reaches the app as data — a field the model binds, a query's
|
|
298
|
+
row — never as a literal.
|
|
299
|
+
- Put it on a control the person presses. A hop on mount pushes the host's
|
|
300
|
+
history without a gesture, and two apps that each do it leave Back nowhere
|
|
301
|
+
to go. A hop to the app's own id is refused — navigate inside an app with
|
|
302
|
+
its own router.
|
|
303
|
+
|
|
270
304
|
## `downloadFile()` — save browser-built bytes
|
|
271
305
|
|
|
272
306
|
```tsx
|