@lotics/app-sdk 0.122.1 → 0.122.3

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 CHANGED
@@ -24,7 +24,7 @@ This file is the index. The **exact type** of anything is its shipped declaratio
24
24
  | [docs/navigation_and_state.md](./docs/navigation_and_state.md) | `AppRouter` (embedded/standalone URL model, the screens and `drawers` it reports to the host), `useUrlState` + `urlParam` codecs, `useRecents`, `useFolderPick`. |
25
25
  | [docs/ai.md](./docs/ai.md) | `useAgentRun` (structured vs free-text, streaming parts, the agent's ask-back), `askAi`, `useAiContext`, and what the member's own chat agent can do with the app while it is open. |
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, why a per-input bound is a tenancy floor rather than an authorization check. |
27
- | [docs/runtime.md](./docs/runtime.md) | `mount()` and the errors it reports to the host (`reportAppError`), the two transports, the API address a standalone bundle reads out of its own page (`<meta name="lotics-api-base">`), `rpc()`, the mock harness (`fixture` + `?__mock=1` — queries, workflows and agents, a mocked agent's run played line by line), `openExternal`/`openApp`/`downloadFile`, geofencing, the peer dependencies. |
27
+ | [docs/runtime.md](./docs/runtime.md) | `mount()` and the errors it reports to the host (`reportAppError`), the two transports, the API address a standalone bundle reads out of its own page (`<meta name="lotics-api-base">`), `rpc()`, the mock harness (`fixture` + `?__mock=1` — queries, workflows and agents, a mocked agent's run played line by line), `openExternal`/`openApp`/`peekApp`/`downloadFile`, geofencing, the peer dependencies. |
28
28
 
29
29
  ## Non-negotiables (each detailed in its doc)
30
30
 
@@ -1635,6 +1635,10 @@ function rpcStandalone(op, payload) {
1635
1635
  return standalonePredictionMiss(payload);
1636
1636
  case "openExternal":
1637
1637
  return standaloneOpenExternal(payload);
1638
+ case "peekApp":
1639
+ return Promise.reject(
1640
+ new Error("peekApp needs the Lotics host: a standalone app has no sibling apps to open. Gate the control on isEmbedded().")
1641
+ );
1638
1642
  case "openApp":
1639
1643
  return Promise.reject(
1640
1644
  new Error("openApp needs the Lotics host: a standalone app has no sibling apps to open. Gate the control on isEmbedded().")
package/dist/index.d.ts CHANGED
@@ -24,7 +24,7 @@ export type { GeofenceZone, GeoCoords, GeofenceOutcome, GeofenceOptions } from "
24
24
  export { rpc, isEmbedded } from "./rpc.js";
25
25
  export type { RpcOp, AiContextValue, AiContextRecordRef } from "./rpc.js";
26
26
  export { openExternal } from "./open_external.js";
27
- export { openApp } from "./open_app.js";
27
+ export { openApp, peekApp } from "./open_app.js";
28
28
  export { askAi, type AskAiArgs } from "./ask_ai.js";
29
29
  export { downloadFile } from "./download.js";
30
30
  export { renameFile } from "./rename_file.js";
package/dist/index.js CHANGED
@@ -40,7 +40,7 @@ import {
40
40
  sumDecimal,
41
41
  tiersAscend,
42
42
  urlParam
43
- } from "./chunk-MK4DBY6R.js";
43
+ } from "./chunk-I2BOTPL6.js";
44
44
 
45
45
  // src/mount.tsx
46
46
  import { createRoot } from "react-dom/client";
@@ -29212,6 +29212,7 @@ function useAppContext() {
29212
29212
  openableApps: data?.openable_apps ?? null,
29213
29213
  commentsEnabled: data?.comments_enabled ?? false,
29214
29214
  recordingEnabled: data?.recording_enabled === true,
29215
+ peekAppEnabled: data?.peek_app_enabled === true,
29215
29216
  timezone: data?.timezone,
29216
29217
  currency: data?.default_currency,
29217
29218
  resolved: data !== void 0,
@@ -33031,6 +33032,9 @@ function openExternal(url2) {
33031
33032
  function openApp(appId, route = "/") {
33032
33033
  return rpc("openApp", { app_id: appId, route });
33033
33034
  }
33035
+ function peekApp(appId, route = "/") {
33036
+ return rpc("peekApp", { app_id: appId, route });
33037
+ }
33034
33038
 
33035
33039
  // src/ask_ai.ts
33036
33040
  function askAi(args) {
@@ -33244,6 +33248,7 @@ export {
33244
33248
  newRecordId,
33245
33249
  openApp,
33246
33250
  openExternal,
33251
+ peekApp,
33247
33252
  queryAll,
33248
33253
  readCreatedAt,
33249
33254
  readFiles,
@@ -10,3 +10,15 @@
10
10
  * Rejects standalone; gate on `isEmbedded()`.
11
11
  */
12
12
  export declare function openApp(appId: string, route?: string): Promise<void>;
13
+ /**
14
+ * Open sibling app `appId`'s own `route` in a sheet over this app, which stays as it was beneath. The sibling draws its
15
+ * record itself, in its own frame, under its own workflows and permissions.
16
+ *
17
+ * ```tsx
18
+ * import { peekApp } from "@lotics/app-sdk";
19
+ * await peekApp(PARTIES_APP_ID, `/${party.id}`);
20
+ * ```
21
+ *
22
+ * Rejects standalone, and on a host that does not state `useAppContext().peekAppEnabled`.
23
+ */
24
+ export declare function peekApp(appId: string, route?: string): Promise<void>;
package/dist/router.js CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  isEmbedded,
3
3
  postHostNotification,
4
4
  setUrlParams
5
- } from "./chunk-MK4DBY6R.js";
5
+ } from "./chunk-I2BOTPL6.js";
6
6
 
7
7
  // src/router.tsx
8
8
  import { useEffect } from "react";
package/dist/rpc.d.ts CHANGED
@@ -9,7 +9,7 @@ import { type UrlParams, type UrlParamsPatch } from "./url_params.js";
9
9
  * app → host: { id, op, payload }
10
10
  * host → app: { id, type: "result", data } | { id, type: "error", message }
11
11
  */
12
- export type RpcOp = "query" | "export" | "field_options" | "workflow" | "agentRuns" | "agentRun.get" | "agentRun.cancel" | "upload" | "file.rename" | "members" | "context" | "write_model" | "prediction_miss" | "openExternal" | "openApp" | "askAi" | "urlState.get" | "urlState.set" | "folder.get" | "folder.set" | "comments.list" | "comments.create" | "comments.update" | "comments.delete" | "comments.counts" | "activity.list" | "activity.undo" | "recording.start" | "recording.stop";
12
+ export type RpcOp = "query" | "export" | "field_options" | "workflow" | "agentRuns" | "agentRun.get" | "agentRun.cancel" | "upload" | "file.rename" | "members" | "context" | "write_model" | "prediction_miss" | "openExternal" | "openApp" | "peekApp" | "askAi" | "urlState.get" | "urlState.set" | "folder.get" | "folder.set" | "comments.list" | "comments.create" | "comments.update" | "comments.delete" | "comments.counts" | "activity.list" | "activity.undo" | "recording.start" | "recording.stop";
13
13
  export interface AgentRunPayload {
14
14
  alias: string;
15
15
  session_id: string;
@@ -72,6 +72,8 @@ export interface AppContext {
72
72
  /** True only from a host with `recording.*`, for a signed-in member, on an
73
73
  * instance that transcribes; absent reads as false. */
74
74
  recording_enabled?: boolean;
75
+ /** True only from a host that serves `peekApp`; absent reads as false. */
76
+ peek_app_enabled?: boolean;
75
77
  /** This app's recordings the host still tracks, by alias. */
76
78
  recordings?: Record<string, unknown>;
77
79
  /** Who in the organization is away, or will be, and who stands in — absent from an older host and for a visitor from
package/dist/viewer.d.ts CHANGED
@@ -23,6 +23,8 @@ export declare function useAppContext(): {
23
23
  }[] | null;
24
24
  commentsEnabled: boolean;
25
25
  recordingEnabled: boolean;
26
+ /** The host draws another app's record in a sheet over this one (`peekApp`). */
27
+ peekAppEnabled: boolean;
26
28
  timezone: string | undefined;
27
29
  currency: string | undefined;
28
30
  resolved: boolean;
package/docs/ai.md CHANGED
@@ -358,7 +358,7 @@ workflows (run_app_workflow):
358
358
  `?` marks an input the caller may omit, `[]` one that takes a list, a
359
359
  `select` spells out every legal value as `label=value` — **the agent sends the
360
360
  value** — and a `record_link` names the table an id must come from. Both tools
361
- take an opaque inputs object, so this listing is the only place any of it is
361
+ take an opaque object (a query's `params`, a workflow's `inputs`), so this listing is the only place any of it is
362
362
  legible.
363
363
 
364
364
  The app's own `description` heads that listing as `about:`, on both surfaces. It
@@ -369,7 +369,7 @@ Wire shapes per output column type:
369
369
  | Column type | Cell value on the wire |
370
370
  |---|---|
371
371
  | `text` | `string` |
372
- | `number` | `number` (numeric database strings are normalized server-side; a rare decimal too precise for float64 stays a string — `row.num` parses both) |
372
+ | `number` | `number` (numeric database strings are normalized server-side; a decimal with more digits than a double holds rounds to the double) |
373
373
  | `boolean` | `boolean` |
374
374
  | `date` | `"YYYY-MM-DD"` — a calendar day, no time component (a reduced-precision date field may also carry `"YYYY-MM"` or `"YYYY"`; see below) |
375
375
  | `datetime` | `"YYYY-MM-DDTHH:mm"` — workspace wall-clock, minute precision |
@@ -262,7 +262,7 @@ context, read once as the app opens. Display only: the workflow body re-checks w
262
262
  (`current_member_stands_in_for(...)`).
263
263
 
264
264
  `openableApps` lists the workspace's apps the viewer can open, each by its `id` and its `name` — what a
265
- way through to a sibling app (`openApp`) is offered by. `null` where the host did not say (standalone, a
265
+ way through to a sibling app (`openApp`, `peekApp`) is offered by. `null` where the host did not say (standalone, a
266
266
  render, an older host, or a page that had not listed its apps when the app opened). Display only: the
267
267
  app opened checks the viewer itself.
268
268
 
@@ -424,7 +424,7 @@ const { reverted, kept } = await undoRecordChange("job", recordId, entry.id);
424
424
  the call arrives, on a row their own row rule covers, and the record's lock, the table's checks
425
425
  and its write rules answer as for any save of theirs. `undoable` is `false`, and the call
426
426
  refused, for the entry that made the record, an entry that is an undo, one already put back, and
427
- a viewer who may not write the record.
427
+ a viewer who may not write the record; a record in the trash is refused until it is restored.
428
428
  The undo is an entry of its own; the answer arrives once the record's reads are read anew.
429
429
  - **Pages**: newest first, about thirty entries each; `next` is where the trail goes on —
430
430
  `undefined` at its end. A page may hold fewer entries and still say `next` where the trail it read
package/docs/runtime.md CHANGED
@@ -3,7 +3,7 @@
3
3
  How an app boots and talks to the platform: **`mount()`** (with the design-time mock harness),
4
4
  the **two transports** the SDK switches between (app code never branches), the raw **`rpc()`**
5
5
  escape hatch, and the browser capabilities the sandbox would otherwise block — **`openExternal`**,
6
- **`openApp`**, **`downloadFile`**, and **`requestGeofencedLocation`** / **`isWithinZone`**.
6
+ **`openApp`** / **`peekApp`**, **`downloadFile`**, and **`requestGeofencedLocation`** / **`isWithinZone`**.
7
7
 
8
8
  ## `mount()` — the entry point
9
9
 
@@ -216,6 +216,7 @@ surfaces:
216
216
  | `agentRuns` (session history) | **no** — the product host doesn't implement it either (`"Unknown RPC op: agentRuns"`) | rejects — a standalone caller is anonymous, and a history is its member's (`"App agent runs require an authenticated member …"`) |
217
217
  | `askAi` | yes | rejects — `"askAi is only available when the app runs inside Lotics"` |
218
218
  | `openApp` | yes — routes to the sibling in the same tab | rejects — `"openApp needs the Lotics host …"` |
219
+ | `peekApp` | yes, when the context states `peek_app_enabled` — opens the sibling in a sheet over this app | rejects — `"peekApp needs the Lotics host …"` |
219
220
  | `recording.start`, `recording.stop` | yes, when the context states `recording_enabled` | rejects — `"Recording needs the Lotics host and a signed-in member …"` |
220
221
 
221
222
  **Limitation:** Don't build a session-log UI on `useAgentRuns`; keep the log in app
@@ -257,6 +258,7 @@ semantics are documented:
257
258
  | `context` | `{}` | app identity (below) | this doc |
258
259
  | `openExternal` | `{ url }` | `void` | this doc |
259
260
  | `openApp` | `{ app_id, route }` | `void` | this doc |
261
+ | `peekApp` | `{ app_id, route }` | `void` | this doc |
260
262
  | `askAi` | prompt/files/records seed | `void` | [ai](./ai.md) |
261
263
  | `agentRuns` | `{ session_id, limit?, offset? }` | `{ runs }` | [ai](./ai.md) |
262
264
  | `agentRun.get` | `{ run_id }` | `{ run }` | [ai](./ai.md) |
@@ -269,7 +271,7 @@ The **streaming** agent-run op is *not* reachable through `rpc()` — its
269
271
  response is a chunk stream, not a single value; it's internal to `useAgentRun`.
270
272
 
271
273
  `rpc("context", {})` resolves the viewer and the workspace:
272
- `{ member_id, comments_enabled, timezone, default_currency, recording_enabled?, recordings? }`. `member_id` is the
274
+ `{ member_id, comments_enabled, timezone, default_currency, recording_enabled?, peek_app_enabled?, recordings? }`. `member_id` is the
273
275
  signed-in member when embedded, `null` standalone; `timezone` is the workspace's
274
276
  IANA zone and `default_currency` its ISO 4217 code, from the host when embedded
275
277
  and from `/v1/apps/by-subdomain/{subdomain}` standalone. Read them through
@@ -341,6 +343,25 @@ declares it (`/` for its register); it is the target's to define.
341
343
  to go. A hop to the app's own id is refused — navigate inside an app with
342
344
  its own router.
343
345
 
346
+ ## `peekApp()` — a sibling's record, over this app
347
+
348
+ ```tsx
349
+ import { peekApp, useAppContext } from "@lotics/app-sdk";
350
+ const { peekAppEnabled } = useAppContext();
351
+ if (peekAppEnabled) await peekApp(PARTIES_APP_ID, `/${party.id}`);
352
+ ```
353
+
354
+ `peekApp(appId: string, route?: string): Promise<void>` (`dist/open_app.d.ts`). The host opens `route` of `appId` in
355
+ a side sheet over this app — a panel from the right on a desk, a full-height sheet on a phone — and this app stays as
356
+ it was beneath. The sibling draws its own record, in its own frame, under its own workflows and permissions; this app
357
+ never draws a second copy of it. The sheet's head carries the sibling's name, "Open in <name>" (what `openApp` does,
358
+ closing the sheet) and a close; Escape closes it too. One sheet at a time: a `peekApp` from inside the sheet replaces it.
359
+
360
+ - **Checked at the bridge**, as `openApp`'s target: the id by shape, the route as an in-app path. A hop to the app's own
361
+ id, and to an app the host's `openableApps` does not list, is refused.
362
+ - **By transport**: embedded in the product host opens the sheet; standalone rejects (`peekApp needs the Lotics host …`).
363
+ A host that does not serve the op states no `peekAppEnabled` in `context` — gate the control on it, else offer `openApp`.
364
+
344
365
  ## `downloadFile()` — save browser-built bytes
345
366
 
346
367
  ```tsx
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/app-sdk",
3
- "version": "0.122.1",
3
+ "version": "0.122.3",
4
4
  "description": "The SDK a Lotics custom-code app reads and writes through — typed hooks over the host bridge, cell readers, mount() and AppRouter",
5
5
  "type": "module",
6
6
  "exports": {