@lotics/app-sdk 0.120.3 → 0.122.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 CHANGED
@@ -20,7 +20,7 @@ This file is the index. The **exact type** of anything is its shipped declaratio
20
20
  | [docs/workflows.md](./docs/workflows.md) | **The workflow-BODY reference** — the JS subset a body may use, opaque `fld_*`/`opt_*` keys, every step form, the accepted sugar, helpers, record-write surfaces, the traps and the verify loop. |
21
21
  | [docs/recipes.md](./docs/recipes.md) | Task-shaped how-tos — returning a generated file, returning structured data, parameterized lookups, composable optional filters, cell decoding, testing an AI action without spending credits. |
22
22
  | [docs/files.md](./docs/files.md) | Files end to end — `useFileUpload` and its `fidelity` and a photo's `place`, `renameFile` (a new file over the same bytes), `useAttachments`/`useAttachmentPiles`, `readFiles` and presigned URLs (a bearer credential — never logged or persisted), workflow-generated files, naming a zip's entries, the delivery bounds. |
23
- | [docs/members_and_options.md](./docs/members_and_options.md) | People, select options and comments — `useMembers`, `useFieldOptions`, `useViewer`, `useWorkspaceTimezone`/`useWorkspaceCurrency`, `useAppContext`, the workspace's zone at the root, `useComments`, a record's activity (`useRecordActivity`). |
23
+ | [docs/members_and_options.md](./docs/members_and_options.md) | People, select options and comments — `useMembers`, `useFieldOptions`, `useViewer`, `useWorkspaceTimezone`/`useWorkspaceCurrency`, `useAppContext`, the workspace's zone at the root, `useComments`, a record's activity and the undo of one change (`useRecordActivity`, `undoRecordChange`). |
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. |
@@ -1,4 +1,4 @@
1
- import type { ActivityEntry } from "./shared_types.js";
1
+ import type { ActivityEntry, ActivityUndoResult } from "./shared_types.js";
2
2
  export interface RecordActivityState {
3
3
  /** Newest first. */
4
4
  entries: ActivityEntry[];
@@ -6,7 +6,7 @@ export interface RecordActivityState {
6
6
  next: string | undefined;
7
7
  loading: boolean;
8
8
  error: string | null;
9
- /** A signed-in member; standalone nothing is read and `entries` stays empty. */
9
+ /** A signed-in member of the app's organization; for anyone else nothing is read and `entries` stays empty. */
10
10
  available: boolean;
11
11
  refetch: () => void;
12
12
  }
@@ -25,3 +25,16 @@ export interface RecordActivityOptions {
25
25
  * ```
26
26
  */
27
27
  export declare function useRecordActivity(entity: string, record_id: string, options?: RecordActivityOptions): RecordActivityState;
28
+ /**
29
+ * Put one change entry of the record's activity back, as the viewer's own save: each field the entry changed returns
30
+ * to what it held before it where the record still holds what the entry wrote. The answer names the fields put back
31
+ * (`reverted`) and those left as they stand (`kept`), each with its `reason` — `changed_since`, or `refused` where the
32
+ * table refused the write (a lock, a check) — and arrives once the record's reads are read anew. Refused for an entry
33
+ * whose `undoable` is false: the record's making, an undo, an entry already put back, and a viewer who may not write
34
+ * the record.
35
+ *
36
+ * ```tsx
37
+ * const { reverted, kept } = await undoRecordChange("job", recordId, entry.id);
38
+ * ```
39
+ */
40
+ export declare function undoRecordChange(entity: string, record_id: string, entry_id: string): Promise<ActivityUndoResult>;
@@ -1656,6 +1656,7 @@ function rpcStandalone(op, payload) {
1656
1656
  case "comments.counts":
1657
1657
  return rejectCommentsStandalone();
1658
1658
  case "activity.list":
1659
+ case "activity.undo":
1659
1660
  return Promise.reject(new Error("A record's activity is available only in embedded apps \u2014 a signed-in member is required."));
1660
1661
  case "recording.start":
1661
1662
  case "recording.stop":
package/dist/index.d.ts CHANGED
@@ -11,9 +11,9 @@ export type { ExportedFile } from "./shared_types.js";
11
11
  export type { AttachedFile, AttachmentPiles, AttachmentPilesOptions, AttachmentsOptions, AttachmentsState } from "./attachments.js";
12
12
  export { useComments, useCommentCounts } from "./comments.js";
13
13
  export type { AppComment, AppCommentAuthor, AppCommentFile, CommentsState, UseCommentsArgs, CommentCountsState, UseCommentCountsArgs, } from "./comments.js";
14
- export { useRecordActivity } from "./activity.js";
14
+ export { undoRecordChange, useRecordActivity } from "./activity.js";
15
15
  export type { RecordActivityOptions, RecordActivityState } from "./activity.js";
16
- export type { ActivityActor, ActivityActorKind, ActivityChange, ActivityComment, ActivityEntry, ActivityValue, RecordActivityPage, } from "./shared_types.js";
16
+ export type { ActivityActor, ActivityActorKind, ActivityChange, ActivityComment, ActivityEntry, ActivityUndoResult, ActivityValue, RecordActivityPage, UndoKeptReason, } from "./shared_types.js";
17
17
  export { useAppContext, useViewer, useWorkspaceCurrency, useWorkspaceTimezone } from "./viewer.js";
18
18
  export type { AppAbsence } from "./shared_types.js";
19
19
  export { useRecording, useRecordings } from "./recording.js";
package/dist/index.js CHANGED
@@ -40,7 +40,7 @@ import {
40
40
  sumDecimal,
41
41
  tiersAscend,
42
42
  urlParam
43
- } from "./chunk-ABDSJ25E.js";
43
+ } from "./chunk-MK4DBY6R.js";
44
44
 
45
45
  // src/mount.tsx
46
46
  import { createRoot } from "react-dom/client";
@@ -611,6 +611,14 @@ function readNow(aliases) {
611
611
  function refetchReads(aliases) {
612
612
  for (const slot of readNow(aliases)) void ask(slot, true);
613
613
  }
614
+ async function readsAnswered(aliases) {
615
+ await Promise.all(
616
+ readNow(aliases).map(async (slot) => {
617
+ await ask(slot, true);
618
+ while (slot.inflight !== null) await slot.inflight;
619
+ })
620
+ );
621
+ }
614
622
  function askInOnePaint(read2) {
615
623
  if (read2.length === 0) return;
616
624
  const paint = { waiting: new Set(read2), held: [], drawn: false, cap: setTimeout(() => draw(paint), PAINT_CAP_MS) };
@@ -1840,13 +1848,16 @@ async function callEach(arr, cb) {
1840
1848
  let next = 0;
1841
1849
  let failed = false;
1842
1850
  const work = async () => {
1843
- while (!failed && next < arr.length) {
1851
+ while (next < arr.length && !failed) {
1844
1852
  const i = next;
1845
1853
  next += 1;
1846
- results[i] = await Promise.resolve().then(() => cb(arr[i], i)).catch((error51) => {
1854
+ try {
1855
+ const result = cb(arr[i], i);
1856
+ results[i] = result instanceof Promise ? await result : result;
1857
+ } catch (error51) {
1847
1858
  failed = true;
1848
1859
  throw error51;
1849
- });
1860
+ }
1850
1861
  }
1851
1862
  };
1852
1863
  const extra = arr.length > 1 ? cb.workers?.claim(arr.length - 1) ?? 0 : 0;
@@ -2039,7 +2050,8 @@ function find(arr, fieldOrCb, value) {
2039
2050
  }
2040
2051
  async function findByCallback(arr, cb) {
2041
2052
  for (let i = 0; i < arr.length; i++) {
2042
- if (await cb(arr[i], i)) return arr[i];
2053
+ const hit = cb(arr[i], i);
2054
+ if (hit instanceof Promise ? await hit : hit) return arr[i];
2043
2055
  }
2044
2056
  return void 0;
2045
2057
  }
@@ -2275,7 +2287,8 @@ function some(arr, fieldOrCb, value) {
2275
2287
  assertCallbackInput("some", arr);
2276
2288
  return (async () => {
2277
2289
  for (let i = 0; i < arr.length; i++) {
2278
- if (await fieldOrCb(arr[i], i)) return true;
2290
+ const hit = fieldOrCb(arr[i], i);
2291
+ if (hit instanceof Promise ? await hit : hit) return true;
2279
2292
  }
2280
2293
  return false;
2281
2294
  })();
@@ -2295,7 +2308,8 @@ function every(arr, fieldOrCb, value) {
2295
2308
  assertCallbackInput("every", arr);
2296
2309
  return (async () => {
2297
2310
  for (let i = 0; i < arr.length; i++) {
2298
- if (!await fieldOrCb(arr[i], i)) return false;
2311
+ const hit = fieldOrCb(arr[i], i);
2312
+ if (!(hit instanceof Promise ? await hit : hit)) return false;
2299
2313
  }
2300
2314
  return true;
2301
2315
  })();
@@ -2390,7 +2404,8 @@ async function reduce(arr, callback, initial) {
2390
2404
  assertCallbackInput("reduce", arr);
2391
2405
  let acc = initial;
2392
2406
  for (let i = 0; i < arr.length; i++) {
2393
- acc = await callback(acc, arr[i], i);
2407
+ const next = callback(acc, arr[i], i);
2408
+ acc = next instanceof Promise ? await next : next;
2394
2409
  }
2395
2410
  return acc;
2396
2411
  }
@@ -6113,11 +6128,9 @@ var HELPER_COST = {
6113
6128
  typeOf: "reads"
6114
6129
  };
6115
6130
  function helperCost(name) {
6116
- return isHelperName(name) ? HELPER_COST[name] : "builds";
6117
- }
6118
- function isHelperName(name) {
6119
- return Object.prototype.hasOwnProperty.call(HELPER_COST, name);
6131
+ return HELPER_COST_BY_NAME.get(name) ?? "builds";
6120
6132
  }
6133
+ var HELPER_COST_BY_NAME = new Map(Object.entries(HELPER_COST));
6121
6134
  function createTimezoneAwareFunctions(defaultTimezone) {
6122
6135
  return {
6123
6136
  // Number functions
@@ -26395,6 +26408,9 @@ var appQueryDeclarationSchema = zod_default.object({
26395
26408
  ),
26396
26409
  templates: zod_default.record(zod_default.string().regex(APP_ALIAS_REGEX), appExportTemplateSchema).optional().describe(
26397
26410
  "Document templates an export of this query's rows may fill, by the name the export asks for. An export names one of these or none; a template it does not declare is refused."
26411
+ ),
26412
+ screen_only: zod_default.literal(true).optional().describe(
26413
+ "The query feeds one control of the app's screen and answers no question a person asks. It runs like any other; a catalog of what an agent may ask leaves it out."
26398
26414
  )
26399
26415
  });
26400
26416
  var appAgentWritesSchema = zod_default.object({
@@ -26736,6 +26752,15 @@ var TOOL_INPUT_SEMANTICS = {
26736
26752
  // same position `query_records.field_keys` holds.
26737
26753
  return_fields: { kind: "field_key_refs", table_from: "table_id" }
26738
26754
  },
26755
+ import_rows: {
26756
+ table_id: { kind: "table_ref", rows: "change" },
26757
+ match: { kind: "field_key_refs", table_from: "table_id" },
26758
+ // Each row's `values` are keyed by field, a link cell possibly `{ lookup }`; the tool resolves both at execute.
26759
+ rows: { kind: "dynamic" },
26760
+ create: { kind: "scalar" },
26761
+ // `fields` is keyed by field; the tool resolves it at execute.
26762
+ conflicts: { kind: "dynamic" }
26763
+ },
26739
26764
  delete_records: {
26740
26765
  // THE ONE CALL THAT TAKES ROWS. Every column of the row goes with it, so
26741
26766
  // nothing field-keyed can carry the fact and it rides the table position —
@@ -26771,6 +26796,22 @@ var TOOL_INPUT_SEMANTICS = {
26771
26796
  changes: { kind: "dynamic" },
26772
26797
  reason: { kind: "text" }
26773
26798
  },
26799
+ // The record names its own table; an entry's id and a cursor are the trail's.
26800
+ get_record_activity: {
26801
+ record_id: { kind: "record_ref" },
26802
+ after: { kind: "scalar" }
26803
+ },
26804
+ undo_record_change: {
26805
+ record_id: { kind: "record_ref" },
26806
+ entry_id: { kind: "scalar" }
26807
+ },
26808
+ // `table_id` names the table whose rows the undo changes, moves to the trash and brings back; a run's id is the
26809
+ // trail's.
26810
+ undo_record_changes: {
26811
+ table_id: { kind: "table_ref", rows: "remove" },
26812
+ record_ids: { kind: "record_refs" },
26813
+ workflow_execution_id: { kind: "scalar" }
26814
+ },
26774
26815
  // ─── Query surfaces ────────────────────────────────────────────────────
26775
26816
  query_records: {
26776
26817
  table_id: { kind: "table_ref", rows: "none" },
@@ -27340,7 +27381,11 @@ function resolveLambdaItemFrame(frames, name) {
27340
27381
  }
27341
27382
  return void 0;
27342
27383
  }
27384
+ function lambdaItemAnchor(ctx, name) {
27385
+ return resolveLambdaItemFrame(ctx.lambda_items, name)?.anchor ?? null;
27386
+ }
27343
27387
  function readRecordAnchor(source, path, ctx) {
27388
+ if (source.from === "lambda_param") return lambdaItemAnchor(ctx, source.name);
27344
27389
  return anchorForRead(source, path, ctx, /* @__PURE__ */ new Set());
27345
27390
  }
27346
27391
  function anchorForRead(source, path, ctx, seen) {
@@ -27350,7 +27395,7 @@ function anchorForRead(source, path, ctx, seen) {
27350
27395
  case "foreach_item":
27351
27396
  return resolveNamedForeachFrame(ctx.foreach_items, source.name)?.anchor ?? null;
27352
27397
  case "lambda_param":
27353
- return resolveLambdaItemFrame(ctx.lambda_items, source.name)?.anchor ?? null;
27398
+ return lambdaItemAnchor(ctx, source.name);
27354
27399
  case "trigger_record":
27355
27400
  return rootAnchor(ctx.trigger_record);
27356
27401
  case "trigger_record_prev":
@@ -27661,7 +27706,7 @@ function assertCheap(budget, ...values2) {
27661
27706
  function charge(ctx, bytes) {
27662
27707
  const budget = ctx.evaluation_budget;
27663
27708
  assertUnsettled(budget);
27664
- if (Date.now() > ctx.run_budget.deadline_ms) throw new WorkflowTimeBudgetError(ctx.run_budget.subject, ctx.run_budget.budget_ms);
27709
+ if (readsClock(ctx) && Date.now() > ctx.run_budget.deadline_ms) throw new WorkflowTimeBudgetError(ctx.run_budget.subject, ctx.run_budget.budget_ms);
27665
27710
  if (budget.bounded && budget.built + bytes > MAX_IN_PLACE_BYTES) throw new ExpressionNotCheapError();
27666
27711
  budget.built += bytes;
27667
27712
  chargeBuilt(ctx.run_budget, bytes);
@@ -27680,8 +27725,8 @@ function callbackWorkers() {
27680
27725
  };
27681
27726
  }
27682
27727
  function evaluateWorkflowExpression(expr2, ctx, options = {}) {
27683
- const budget = { workers: callbackWorkers(), bounded: options.bounded === true, built: 0, settled: false };
27684
- return evaluateExpression(expr2, { ...ctx, evaluation_budget: budget }).finally(() => {
27728
+ const budget = { workers: callbackWorkers(), bounded: options.bounded === true, built: 0, clock_checks: 0, helpers: /* @__PURE__ */ new Map(), settled: false };
27729
+ return evaluateRoot(expr2, { ...ctx, evaluation_budget: budget, anchor_context: evalAnchorContext(ctx) }).finally(() => {
27685
27730
  budget.settled = true;
27686
27731
  }).then((answer) => {
27687
27732
  if (budget.bounded && expandedSize(answer, MAX_IN_PLACE_BYTES) > MAX_IN_PLACE_BYTES) throw new ExpressionNotCheapError();
@@ -27691,8 +27736,19 @@ function evaluateWorkflowExpression(expr2, ctx, options = {}) {
27691
27736
  throw error51;
27692
27737
  });
27693
27738
  }
27694
- async function evaluateExpression(expr2, ctx) {
27739
+ async function evaluateRoot(expr2, ctx) {
27695
27740
  for (let pause = checkClock(ctx); pause !== null; pause = checkClock(ctx)) await pause;
27741
+ return evaluateExpression(expr2, ctx);
27742
+ }
27743
+ function failBeside(error51, started) {
27744
+ void Promise.allSettled(started);
27745
+ throw error51;
27746
+ }
27747
+ async function startBeside(expr2, ctx) {
27748
+ return evaluateExpression(expr2, ctx);
27749
+ }
27750
+ function evaluateExpression(expr2, ctx) {
27751
+ assertUnsettled(ctx.evaluation_budget);
27696
27752
  switch (expr2.kind) {
27697
27753
  case "lit":
27698
27754
  return expr2.value;
@@ -27705,31 +27761,75 @@ async function evaluateExpression(expr2, ctx) {
27705
27761
  case "unary":
27706
27762
  return evaluateUnary(expr2.op, expr2.arg, ctx);
27707
27763
  case "ternary": {
27708
- const cond = await evaluateExpression(expr2.cond, ctx);
27709
- return cond ? evaluateExpression(expr2.then, ctx) : evaluateExpression(expr2.else, ctx);
27764
+ const cond = evaluateExpression(expr2.cond, ctx);
27765
+ return cond instanceof Promise ? branchAfter(cond, expr2.then, expr2.else, ctx) : evaluateExpression(cond ? expr2.then : expr2.else, ctx);
27710
27766
  }
27711
27767
  case "template":
27712
27768
  return evaluateTemplate(expr2.parts, ctx);
27713
27769
  case "lambda":
27714
27770
  return evaluateLambda(expr2.params, expr2.body, ctx, []);
27715
- case "array_literal": {
27716
- const elements = await Promise.all(expr2.elements.map((el) => evaluateExpression(el, ctx)));
27717
- charge(ctx, builtSize(elements));
27718
- return elements;
27719
- }
27720
- case "object_literal": {
27721
- const out = {};
27722
- for (const entry of expr2.entries) defineOwn(out, entry.key, await evaluateExpression(entry.value, ctx));
27723
- charge(ctx, builtSize(out));
27724
- return out;
27725
- }
27771
+ case "array_literal":
27772
+ return evaluateArrayLiteral(expr2.elements, ctx);
27773
+ case "object_literal":
27774
+ return fillObjectLiteral({}, expr2.entries, 0, ctx);
27726
27775
  default:
27727
27776
  return assertNever(expr2);
27728
27777
  }
27729
27778
  }
27779
+ async function branchAfter(cond, then, otherwise, ctx) {
27780
+ return evaluateExpression(await cond ? then : otherwise, ctx);
27781
+ }
27782
+ function evaluateArrayLiteral(elements, ctx) {
27783
+ const values2 = [];
27784
+ let pending = false;
27785
+ try {
27786
+ for (const element of elements) {
27787
+ const value = evaluateExpression(element, ctx);
27788
+ if (value instanceof Promise) pending = true;
27789
+ values2.push(value);
27790
+ }
27791
+ } catch (error51) {
27792
+ return failBeside(error51, values2);
27793
+ }
27794
+ return pending ? chargeAfter(values2, ctx) : chargedArray(values2, ctx);
27795
+ }
27796
+ async function chargeAfter(values2, ctx) {
27797
+ return chargedArray(await Promise.all(values2), ctx);
27798
+ }
27799
+ function chargedArray(elements, ctx) {
27800
+ charge(ctx, builtSize(elements));
27801
+ return elements;
27802
+ }
27803
+ function fillObjectLiteral(out, entries3, from, ctx) {
27804
+ for (let at2 = from; at2 < entries3.length; at2++) {
27805
+ const entry = entries3[at2];
27806
+ const value = evaluateExpression(entry.value, ctx);
27807
+ if (value instanceof Promise) return fillObjectAfter(out, entries3, at2, value, ctx);
27808
+ defineOwn(out, entry.key, value);
27809
+ }
27810
+ charge(ctx, builtSize(out));
27811
+ return out;
27812
+ }
27813
+ async function fillObjectAfter(out, entries3, at2, value, ctx) {
27814
+ defineOwn(out, entries3[at2].key, await value);
27815
+ return fillObjectLiteral(out, entries3, at2 + 1, ctx);
27816
+ }
27730
27817
  function checkClock(ctx) {
27731
27818
  assertUnsettled(ctx.evaluation_budget);
27732
- return runClock(ctx.run_budget);
27819
+ return readsClock(ctx) ? runClock(ctx.run_budget) : null;
27820
+ }
27821
+ var ISOLATED_CLOCK_STRIDE = 16;
27822
+ function readsClock(ctx) {
27823
+ if (ctx.run_budget.host === "shared") return true;
27824
+ const budget = ctx.evaluation_budget;
27825
+ budget.clock_checks += 1;
27826
+ if (budget.clock_checks % ISOLATED_CLOCK_STRIDE !== 1) return false;
27827
+ if (budget.clock_checks === 1) {
27828
+ queueMicrotask(() => {
27829
+ budget.clock_checks = 0;
27830
+ });
27831
+ }
27832
+ return true;
27733
27833
  }
27734
27834
  function assertUnsettled(budget) {
27735
27835
  if (budget.settled) {
@@ -27739,21 +27839,19 @@ function assertUnsettled(budget) {
27739
27839
  }
27740
27840
  }
27741
27841
  function evaluateLambda(params, body, ctx, items) {
27742
- const call = async (...args) => {
27743
- const frame2 = {};
27744
- for (let i = 0; i < params.length; i++) defineOwn(frame2, params[i], args[i]);
27745
- const stack = ctx.lambda_params ?? [];
27746
- const newStack = [...stack, frame2];
27747
- const innerCtx = {
27748
- ...ctx,
27749
- lambda_params: newStack,
27750
- // The VALUE frame and the ANCHOR frames are pushed together: a body that
27751
- // can read the item must read it through the same table the save layer
27752
- // typed it with, and a parameter that is not the item must shadow an
27753
- // outer one of the same name in both stacks alike.
27754
- lambda_items: items.length === 0 ? ctx.lambda_items : [...ctx.lambda_items ?? [], ...items]
27755
- };
27756
- return evaluateExpression(body, innerCtx);
27842
+ const lambdaItems = items.length === 0 ? ctx.lambda_items : [...ctx.lambda_items ?? [], ...items];
27843
+ const bodyCtx = {
27844
+ ...ctx,
27845
+ lambda_items: lambdaItems,
27846
+ anchor_context: { ...ctx.anchor_context, lambda_items: lambdaItems ?? [] }
27847
+ };
27848
+ const stack = ctx.lambda_params ?? [];
27849
+ const call = (...args) => {
27850
+ const pause = checkClock(bodyCtx);
27851
+ if (pause !== null) return pause.then(() => call(...args));
27852
+ const frame2 = /* @__PURE__ */ Object.create(null);
27853
+ for (let i = 0; i < params.length; i++) frame2[params[i]] = args[i];
27854
+ return evaluateExpression(body, { ...bodyCtx, lambda_params: [...stack, frame2] });
27757
27855
  };
27758
27856
  return Object.assign(call, { workers: ctx.evaluation_budget.workers });
27759
27857
  }
@@ -27773,9 +27871,6 @@ function evalAnchorContext(ctx) {
27773
27871
  trigger_changes: ctx.trigger_table
27774
27872
  };
27775
27873
  }
27776
- function resolveReadAnchor(source, path, ctx) {
27777
- return readRecordAnchor(source, path, evalAnchorContext(ctx));
27778
- }
27779
27874
  function resolveReadRoot(source, ctx) {
27780
27875
  switch (source.from) {
27781
27876
  case "trigger":
@@ -27885,29 +27980,37 @@ function resolveForeachFrame(ctx, name, at2) {
27885
27980
  { kind: "read", at: at2, extra: { name } }
27886
27981
  );
27887
27982
  }
27888
- async function evaluateRead(source, path, coerce, ctx) {
27889
- let current = source.from === "value" ? await evaluateExpression(source.expr, ctx) : resolveReadRoot(source, ctx);
27890
- const anchor = resolveReadAnchor(source, path, ctx);
27891
- let currentTable = null;
27892
- let prevFieldForSegment;
27893
- let terminalField;
27894
- for (let i = 0; i < path.length; i++) {
27983
+ function evaluateRead(source, path, coerce, ctx) {
27984
+ const root = source.from === "value" ? evaluateExpression(source.expr, ctx) : resolveReadRoot(source, ctx);
27985
+ return root instanceof Promise ? readAfter(root, source, path, coerce, ctx) : walkPath(path, coerce, ctx, readRecordAnchor(source, path, ctx.anchor_context), 0, root, null, void 0, void 0);
27986
+ }
27987
+ async function readAfter(root, source, path, coerce, ctx) {
27988
+ const value = await root;
27989
+ return walkPath(path, coerce, ctx, readRecordAnchor(source, path, ctx.anchor_context), 0, value, null, void 0, void 0);
27990
+ }
27991
+ function walkPath(path, coerce, ctx, anchor, from, start, startTable, startField, startTerminal) {
27992
+ let current = start;
27993
+ let table = startTable;
27994
+ let prevField = startField;
27995
+ let terminalField = startTerminal;
27996
+ for (let i = from; i < path.length; i++) {
27895
27997
  const segment = path[i];
27896
27998
  const isLast = i === path.length - 1;
27897
- if (anchor && i === anchor.envelopeLen) currentTable ??= anchor.table;
27999
+ if (anchor && i === anchor.envelopeLen) table ??= anchor.table;
27898
28000
  const carried = carriedTable(current, ctx);
27899
- if (carried?.at === "self") currentTable = carried.table;
27900
- const fieldForSegment = (segment.at === "field" || segment.at === "key") && currentTable ? findField(currentTable, segment.key) : void 0;
28001
+ if (carried?.at === "self") table = carried.table;
28002
+ const field = (segment.at === "field" || segment.at === "key") && table ? findField(table, segment.key) : void 0;
27901
28003
  if (isLast) {
27902
- terminalField = fieldForSegment ?? (segment.at === "key" && (segment.key === "next_value" || segment.key === "prev_value") ? prevFieldForSegment : void 0);
28004
+ terminalField = field ?? (segment.at === "key" && (segment.key === "next_value" || segment.key === "prev_value") ? prevField : void 0);
27903
28005
  }
27904
- current = await walkSegment(current, segment, ctx);
27905
- currentTable = advanceTable(segment, currentTable, ctx);
27906
- if (carried?.at === "data" && isKeyNamed(segment, "data")) currentTable = carried.table;
27907
- prevFieldForSegment = fieldForSegment;
27908
- if (current === void 0 || current === null) {
27909
- return current;
28006
+ const walked = walkSegment(current, segment, ctx);
28007
+ if (walked instanceof Promise) {
28008
+ return walkPending({ path, coerce, ctx, anchor, table, carried, field, terminal_field: terminalField }, i, walked);
27910
28009
  }
28010
+ table = tableAfter(segment, table, carried, ctx);
28011
+ prevField = field;
28012
+ current = walked;
28013
+ if (current === void 0 || current === null) return current;
27911
28014
  }
27912
28015
  if (terminalField && isScalarWrappedField(terminalField)) {
27913
28016
  current = unwrapScalarField(current);
@@ -27917,6 +28020,17 @@ async function evaluateRead(source, path, coerce, ctx) {
27917
28020
  }
27918
28021
  return current;
27919
28022
  }
28023
+ async function walkPending(read2, at2, walked) {
28024
+ const { path, coerce, ctx, anchor, carried, field, terminal_field: terminalField } = read2;
28025
+ const current = await walked;
28026
+ if (current === void 0 || current === null) return current;
28027
+ const table = tableAfter(path[at2], read2.table, carried, ctx);
28028
+ return walkPath(path, coerce, ctx, anchor, at2 + 1, current, table, field, terminalField);
28029
+ }
28030
+ function tableAfter(segment, table, carried, ctx) {
28031
+ const advanced = advanceTable(segment, table, ctx);
28032
+ return carried?.at === "data" && isKeyNamed(segment, "data") ? carried.table : advanced;
28033
+ }
27920
28034
  function isScalarWrappedField(field) {
27921
28035
  if (field.type === "formula") return field.formula.options !== void 0;
27922
28036
  return (field.type === "select" || field.type === "select_member") && field.multi !== true;
@@ -28022,7 +28136,7 @@ function advanceTable(segment, currentTable, ctx) {
28022
28136
  function findField(table, fieldKey) {
28023
28137
  return table.fields.find((f) => f.key === fieldKey);
28024
28138
  }
28025
- async function walkSegment(value, segment, ctx) {
28139
+ function walkSegment(value, segment, ctx) {
28026
28140
  switch (segment.at) {
28027
28141
  case "key":
28028
28142
  case "field": {
@@ -28043,12 +28157,14 @@ async function walkSegment(value, segment, ctx) {
28043
28157
  case "index": {
28044
28158
  if (value === null || value === void 0) return void 0;
28045
28159
  if (Array.isArray(value) || typeof value === "string") {
28046
- const idx = typeof segment.index === "number" ? segment.index : await resolveIndexExpression(segment.index, ctx, "array index");
28047
- return value[idx];
28160
+ if (typeof segment.index === "number") return value[segment.index];
28161
+ const idx = resolveIndexExpression(segment.index, ctx, "array index");
28162
+ return idx instanceof Promise ? idx.then((at2) => value[at2]) : value[idx];
28048
28163
  }
28049
28164
  if (typeof value === "object") {
28050
- const key = typeof segment.index === "number" ? String(segment.index) : await resolveObjectKeyExpression(segment.index, ctx);
28051
- return ownMember(value, key);
28165
+ if (typeof segment.index === "number") return ownMember(value, String(segment.index));
28166
+ const key = resolveObjectKeyExpression(segment.index, ctx);
28167
+ return key instanceof Promise ? key.then((named) => ownMember(value, named)) : ownMember(value, key);
28052
28168
  }
28053
28169
  throw new WorkflowEvalError(
28054
28170
  `Cannot index ${typeof value} \u2014 only an array or an object can be read by index.`,
@@ -28059,8 +28175,11 @@ async function walkSegment(value, segment, ctx) {
28059
28175
  return walkLinked(value, segment, ctx);
28060
28176
  }
28061
28177
  }
28062
- async function resolveObjectKeyExpression(expr2, ctx) {
28063
- const value = await evaluateExpression(expr2, ctx);
28178
+ function resolveObjectKeyExpression(expr2, ctx) {
28179
+ const value = evaluateExpression(expr2, ctx);
28180
+ return value instanceof Promise ? value.then(objectKey) : objectKey(value);
28181
+ }
28182
+ function objectKey(value) {
28064
28183
  if (typeof value === "string") return value;
28065
28184
  if (typeof value === "number" && Number.isFinite(value)) return String(value);
28066
28185
  throw new WorkflowEvalError(
@@ -28072,8 +28191,11 @@ async function resolveObjectKeyExpression(expr2, ctx) {
28072
28191
  );
28073
28192
  }
28074
28193
  var VALUE_PREVIEW_LENGTH = 120;
28075
- async function resolveIndexExpression(expr2, ctx, context) {
28076
- const value = await evaluateExpression(expr2, ctx);
28194
+ function resolveIndexExpression(expr2, ctx, context) {
28195
+ const value = evaluateExpression(expr2, ctx);
28196
+ return value instanceof Promise ? value.then((settled) => arrayIndex(settled, context)) : arrayIndex(value, context);
28197
+ }
28198
+ function arrayIndex(value, context) {
28077
28199
  if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
28078
28200
  throw new WorkflowEvalError(
28079
28201
  `Computed ${context} must evaluate to a non-negative integer, got ${describeValue(value)}`,
@@ -28145,46 +28267,77 @@ async function fetchRecordsBounded(ids, reading, ctx) {
28145
28267
  }
28146
28268
  return results;
28147
28269
  }
28148
- async function evaluateCall(fn, args, ctx) {
28270
+ function evaluateCall(fn, args, ctx) {
28271
+ const pause = checkClock(ctx);
28272
+ if (pause !== null) return pause.then(() => evaluateCall(fn, args, ctx));
28273
+ const helper = resolveHelper(fn, ctx);
28274
+ const values2 = [];
28275
+ let pending = false;
28276
+ try {
28277
+ for (const arg of args) {
28278
+ const value = arg.kind === "lambda" ? evaluateLambda(arg.params, arg.body, ctx, callbackItemFrames(fn, args, arg.params, ctx.anchor_context)) : evaluateExpression(arg, ctx);
28279
+ if (value instanceof Promise) pending = true;
28280
+ values2.push(value);
28281
+ }
28282
+ } catch (error51) {
28283
+ return failBeside(error51, values2);
28284
+ }
28285
+ return pending ? callAfter(helper, values2, ctx) : callHelper(helper, values2, ctx);
28286
+ }
28287
+ function resolveHelper(fn, ctx) {
28288
+ const known = ctx.evaluation_budget.helpers.get(fn);
28289
+ if (known !== void 0) return known;
28149
28290
  if (!Object.prototype.hasOwnProperty.call(ctx.helpers, fn)) {
28150
28291
  throw new WorkflowEvalError(`Unknown helper function "${fn}"`, {
28151
28292
  kind: "call",
28152
28293
  extra: { fn }
28153
28294
  });
28154
28295
  }
28155
- const helper = ctx.helpers[fn];
28156
- const anchors = args.some((arg) => arg.kind === "lambda") ? evalAnchorContext(ctx) : null;
28157
- const evaluatedArgs = await Promise.all(
28158
- args.map(
28159
- (arg) => arg.kind === "lambda" && anchors !== null ? evaluateLambda(arg.params, arg.body, ctx, callbackItemFrames(fn, args, arg.params, anchors)) : evaluateExpression(arg, ctx)
28160
- )
28161
- );
28296
+ const resolved = { call: ctx.helpers[fn], cost: helperCost(fn) };
28297
+ ctx.evaluation_budget.helpers.set(fn, resolved);
28298
+ return resolved;
28299
+ }
28300
+ async function callAfter(helper, pending, ctx) {
28301
+ const values2 = await Promise.all(pending);
28162
28302
  for (let pause = checkClock(ctx); pause !== null; pause = checkClock(ctx)) await pause;
28163
- const cost = helperCost(fn);
28164
- if (cost === "parses") charge(ctx, parsedSize(evaluatedArgs[0]));
28165
- const answer = await helper(...evaluatedArgs);
28303
+ return callHelper(helper, values2, ctx);
28304
+ }
28305
+ function callHelper({ call, cost }, values2, ctx) {
28306
+ if (cost === "parses") charge(ctx, parsedSize(values2[0]));
28307
+ const answer = Reflect.apply(call, void 0, values2);
28308
+ return answer instanceof Promise ? chargeAnswerAfter(cost, answer, values2, ctx) : chargeAnswer(cost, answer, values2, ctx);
28309
+ }
28310
+ async function chargeAnswerAfter(cost, answer, values2, ctx) {
28311
+ return chargeAnswer(cost, await answer, values2, ctx);
28312
+ }
28313
+ function chargeAnswer(cost, answer, values2, ctx) {
28166
28314
  if (cost !== "builds" && cost !== "nested") return answer;
28167
- const handedBack = evaluatedArgs.indexOf(answer);
28168
- if (handedBack !== -1) return evaluatedArgs[handedBack];
28169
- charge(ctx, builtSize(answer) + (cost === "nested" ? builtSize(evaluatedArgs[0]) : 0));
28315
+ const handedBack = values2.indexOf(answer);
28316
+ if (handedBack !== -1) return values2[handedBack];
28317
+ charge(ctx, builtSize(answer) + (cost === "nested" ? builtSize(values2[0]) : 0));
28170
28318
  return answer;
28171
28319
  }
28172
- async function evaluateBinary(op, leftExpr, rightExpr, ctx) {
28173
- if (op === "and") {
28174
- const left2 = await evaluateExpression(leftExpr, ctx);
28175
- if (!left2) return left2;
28176
- return evaluateExpression(rightExpr, ctx);
28177
- }
28178
- if (op === "or") {
28179
- const left2 = await evaluateExpression(leftExpr, ctx);
28180
- if (left2) return left2;
28181
- return evaluateExpression(rightExpr, ctx);
28182
- }
28183
- const [left, right] = await Promise.all([
28184
- evaluateExpression(leftExpr, ctx),
28185
- evaluateExpression(rightExpr, ctx)
28186
- ]);
28320
+ function evaluateBinary(op, leftExpr, rightExpr, ctx) {
28321
+ if (op === "and" || op === "or") {
28322
+ const first2 = evaluateExpression(leftExpr, ctx);
28323
+ return first2 instanceof Promise ? shortCircuitAfter(first2, op, rightExpr, ctx) : shortCircuit(op, first2, rightExpr, ctx);
28324
+ }
28325
+ const left = evaluateExpression(leftExpr, ctx);
28326
+ const right = left instanceof Promise ? startBeside(rightExpr, ctx) : evaluateExpression(rightExpr, ctx);
28327
+ return left instanceof Promise || right instanceof Promise ? binaryAfter(op, left, right, ctx) : applyBinary(op, left, right, ctx);
28328
+ }
28329
+ function shortCircuit(op, left, rightExpr, ctx) {
28330
+ return (op === "and" ? !left : left) ? left : evaluateExpression(rightExpr, ctx);
28331
+ }
28332
+ async function shortCircuitAfter(left, op, rightExpr, ctx) {
28333
+ return shortCircuit(op, await left, rightExpr, ctx);
28334
+ }
28335
+ async function binaryAfter(op, left, right, ctx) {
28336
+ const [leftValue, rightValue] = await Promise.all([left, right]);
28187
28337
  for (let pause = checkClock(ctx); pause !== null; pause = checkClock(ctx)) await pause;
28338
+ return applyBinary(op, leftValue, rightValue, ctx);
28339
+ }
28340
+ function applyBinary(op, left, right, ctx) {
28188
28341
  switch (op) {
28189
28342
  case "eq":
28190
28343
  case "neq":
@@ -28291,8 +28444,14 @@ function describeValue(value) {
28291
28444
  }
28292
28445
  return value.length > 0 && length2 <= VALUE_PREVIEW_LENGTH ? `array ${value.join(",")}` : "array";
28293
28446
  }
28294
- async function evaluateUnary(op, argExpr, ctx) {
28295
- const value = await evaluateExpression(argExpr, ctx);
28447
+ function evaluateUnary(op, argExpr, ctx) {
28448
+ const value = evaluateExpression(argExpr, ctx);
28449
+ return value instanceof Promise ? unaryAfter(op, value) : applyUnary(op, value);
28450
+ }
28451
+ async function unaryAfter(op, value) {
28452
+ return applyUnary(op, await value);
28453
+ }
28454
+ function applyUnary(op, value) {
28296
28455
  switch (op) {
28297
28456
  case "not":
28298
28457
  return !value;
@@ -28306,11 +28465,26 @@ async function evaluateUnary(op, argExpr, ctx) {
28306
28465
  return -value;
28307
28466
  }
28308
28467
  }
28309
- async function evaluateTemplate(parts, ctx) {
28310
- const evaluated2 = await Promise.all(
28311
- parts.map((part) => part.t === "text" ? part.s : evaluateExpression(part.e, ctx))
28312
- );
28468
+ function evaluateTemplate(parts, ctx) {
28469
+ const evaluated2 = [];
28470
+ let pending = false;
28471
+ try {
28472
+ for (const part of parts) {
28473
+ const value = part.t === "text" ? part.s : evaluateExpression(part.e, ctx);
28474
+ if (value instanceof Promise) pending = true;
28475
+ evaluated2.push(value);
28476
+ }
28477
+ } catch (error51) {
28478
+ return failBeside(error51, evaluated2);
28479
+ }
28480
+ return pending ? templateAfter(evaluated2, ctx) : writeTemplate(evaluated2, ctx);
28481
+ }
28482
+ async function templateAfter(pending, ctx) {
28483
+ const evaluated2 = await Promise.all(pending);
28313
28484
  for (let pause = checkClock(ctx); pause !== null; pause = checkClock(ctx)) await pause;
28485
+ return writeTemplate(evaluated2, ctx);
28486
+ }
28487
+ function writeTemplate(evaluated2, ctx) {
28314
28488
  assertCheap(ctx.evaluation_budget, ...evaluated2);
28315
28489
  assertTextWithin("template", ...evaluated2);
28316
28490
  const text2 = evaluated2.map(stringifyForTemplate).join("");
@@ -29032,6 +29206,7 @@ function useAppContext() {
29032
29206
  const { data, error: error51, refetch } = useKey(APP_CONTEXT_KEY, readContext, { focus: false, arrival: false });
29033
29207
  return {
29034
29208
  memberId: data?.member_id ?? null,
29209
+ insider: data !== void 0 && data.member_id !== null && data.group_ids !== void 0,
29035
29210
  groupIds: data?.group_ids ?? null,
29036
29211
  absences: data?.absences ?? null,
29037
29212
  openableApps: data?.openable_apps ?? null,
@@ -32717,8 +32892,7 @@ function oneQueryAlias(entity) {
32717
32892
  var NO_ENTRIES = [];
32718
32893
  function useRecordActivity(entity, record_id, options = {}) {
32719
32894
  const { after } = options;
32720
- const { memberId, resolved } = useAppContext();
32721
- const available = memberId !== null;
32895
+ const { insider: available, resolved } = useAppContext();
32722
32896
  const page = useKey(
32723
32897
  available ? JSON.stringify(["activity", entity, record_id, after ?? null]) : null,
32724
32898
  () => rpc("activity.list", { entity, record_id, ...after === void 0 ? {} : { after } }),
@@ -32734,6 +32908,12 @@ function useRecordActivity(entity, record_id, options = {}) {
32734
32908
  refetch: page.refetch
32735
32909
  };
32736
32910
  }
32911
+ async function undoRecordChange(entity, record_id, entry_id) {
32912
+ const result = await rpc("activity.undo", { entity, record_id, entry_id });
32913
+ refetchReads([listQueryAlias(entity)]);
32914
+ await readsAnswered([oneQueryAlias(entity)]);
32915
+ return result;
32916
+ }
32737
32917
 
32738
32918
  // src/recording.ts
32739
32919
  import { useCallback as useCallback7, useMemo as useMemo4, useSyncExternalStore as useSyncExternalStore3 } from "react";
@@ -33077,6 +33257,7 @@ export {
33077
33257
  requestGeofencedLocation,
33078
33258
  row,
33079
33259
  rpc,
33260
+ undoRecordChange,
33080
33261
  urlParam,
33081
33262
  useAgentRun,
33082
33263
  useAgentRuns,
package/dist/router.js CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  isEmbedded,
3
3
  postHostNotification,
4
4
  setUrlParams
5
- } from "./chunk-ABDSJ25E.js";
5
+ } from "./chunk-MK4DBY6R.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" | "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" | "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;
@@ -3,7 +3,8 @@
3
3
  * `scripts/build_sdk.mjs`, because that package is private and ships in no
4
4
  * tarball. Written at every build, never edited.
5
5
  */
6
- export type ActivityEntry = { changes: { field_id: string; label: string; before: string | number | boolean | { key: string; label: string; }[] | { id: string; display: string; }[] | { id: string; name: string | null; image?: string | undefined; }[] | { id: string; filename: string; mime_type: string; file_storage_key?: string | undefined; url?: string | undefined; thumbnail_url?: string | undefined; preview_url?: string | undefined; size?: number | null | undefined; created_at?: string | undefined; document_template_id?: string | undefined; }[] | null; after: string | number | boolean | { key: string; label: string; }[] | { id: string; display: string; }[] | { id: string; name: string | null; image?: string | undefined; }[] | { id: string; filename: string; mime_type: string; file_storage_key?: string | undefined; url?: string | undefined; thumbnail_url?: string | undefined; preview_url?: string | undefined; size?: number | null | undefined; created_at?: string | undefined; document_template_id?: string | undefined; }[] | null; removed?: true | undefined; }[]; id: string; actor: { kind: "member" | "agent" | "workflow" | "import"; id: string; name: string; image?: string | undefined; }; at: string; until: string; kind: "change"; created?: true | undefined; } | { comment: { id: string; body: string; mentions: { id: string; name: string; }[]; files?: { id: string; filename: string; mime_type: string; file_storage_key?: string | undefined; url?: string | undefined; thumbnail_url?: string | undefined; preview_url?: string | undefined; size?: number | null | undefined; created_at?: string | undefined; document_template_id?: string | undefined; }[] | undefined; }; id: string; actor: { kind: "member" | "agent" | "workflow" | "import"; id: string; name: string; image?: string | undefined; }; at: string; until: string; kind: "comment"; };
6
+ export type ActivityEntry = { undoable: boolean; changes: { field_id: string; label: string; before: string | number | boolean | { key: string; label: string; }[] | { id: string; display: string; }[] | { id: string; name: string | null; image?: string | undefined; }[] | { id: string; filename: string; mime_type: string; file_storage_key?: string | undefined; url?: string | undefined; thumbnail_url?: string | undefined; preview_url?: string | undefined; size?: number | null | undefined; created_at?: string | undefined; document_template_id?: string | undefined; }[] | null; after: string | number | boolean | { key: string; label: string; }[] | { id: string; display: string; }[] | { id: string; name: string | null; image?: string | undefined; }[] | { id: string; filename: string; mime_type: string; file_storage_key?: string | undefined; url?: string | undefined; thumbnail_url?: string | undefined; preview_url?: string | undefined; size?: number | null | undefined; created_at?: string | undefined; document_template_id?: string | undefined; }[] | null; removed?: true | undefined; }[]; id: string; actor: { kind: "member" | "agent" | "workflow" | "import"; id: string; name: string; image?: string | undefined; }; at: string; until: string; kind: "change"; created?: true | undefined; undo?: true | undefined; undone?: { by: { kind: "member" | "agent" | "workflow" | "import"; id: string; name: string; image?: string | undefined; }; at: string; } | undefined; } | { comment: { id: string; body: string; mentions: { id: string; name: string; }[]; files?: { id: string; filename: string; mime_type: string; file_storage_key?: string | undefined; url?: string | undefined; thumbnail_url?: string | undefined; preview_url?: string | undefined; size?: number | null | undefined; created_at?: string | undefined; document_template_id?: string | undefined; }[] | undefined; }; id: string; actor: { kind: "member" | "agent" | "workflow" | "import"; id: string; name: string; image?: string | undefined; }; at: string; until: string; kind: "comment"; };
7
+ export type ActivityUndoResult = { reverted: { field_id: string; label: string; }[]; kept: { field_id: string; label: string; reason: "changed_since" | "refused"; }[]; };
7
8
  export type AppAbsence = { member: { id: string; name: string; }; stand_in: { id: string; name: string; }; starts_on: string; ends_on: string; group_ids: string[]; };
8
9
  /** The file an app export made, as a link to download it: no workspace file, so no id, and kept a day. */
9
10
  export type ExportedFile = {
package/dist/store.d.ts CHANGED
@@ -4,6 +4,8 @@ export type Fetcher<T> = (askedAt: number, current: T | undefined) => Promise<T>
4
4
  export declare function tick(): number;
5
5
  /** Ask again every key reading one of `aliases` — every query key where none is named; an unmounted one on its next mount. */
6
6
  export declare function refetchReads(aliases: readonly string[] | undefined): void;
7
+ /** `refetchReads`, settled once every key a screen reads now holds an answer asked after this call — or its refusal. */
8
+ export declare function readsAnswered(aliases: readonly string[]): Promise<void>;
7
9
  /** The host's push, naming the aliases whose tables moved: each key reading one asked again (`refetchReads`), in one
8
10
  * paint. */
9
11
  export declare function refetchPushed(aliases: readonly string[]): void;
package/dist/viewer.d.ts CHANGED
@@ -10,6 +10,8 @@ export declare function readContext(): Promise<AppContext>;
10
10
  */
11
11
  export declare function useAppContext(): {
12
12
  memberId: string | null;
13
+ /** The viewer is a signed-in member of the app's organization: the host named them and told their groups. */
14
+ insider: boolean;
13
15
  /** The viewer's member groups, by id; `null` where the host did not say. */
14
16
  groupIds: readonly string[] | null;
15
17
  /** Who in the organization is away, or will be, and who stands in; `null` where the host did not say. */
@@ -373,7 +373,7 @@ table's id: read it off a row of the query that renders the badges (`rows[0]?.__
373
373
  rather than pasting a `tbl_` id, which a copy of the app cannot carry. Pass `undefined` while no
374
374
  row is on screen — no request is sent and `counts` is `{}`.
375
375
 
376
- ## Record activity: `useRecordActivity`
376
+ ## Record activity: `useRecordActivity`, `undoRecordChange`
377
377
 
378
378
  Who changed which of a record's fields, and who commented on it — one entry per save
379
379
  (`dist/activity.d.ts`). For a JSON app: `entity` is the alias the app's spec names the record's
@@ -383,6 +383,8 @@ entity under.
383
383
  const { entries, next, loading, error, available, refetch } = useRecordActivity("job", recordId);
384
384
  // The page past it, mounted where the reader asks for more:
385
385
  const older = useRecordActivity("job", recordId, { after: next });
386
+ // One change entry put back, as the viewer's own save:
387
+ const { reverted, kept } = await undoRecordChange("job", recordId, entry.id);
386
388
  ```
387
389
 
388
390
  - **Authority: the app's own read of the record.** The server reaches the record through the
@@ -392,8 +394,8 @@ const older = useRecordActivity("job", recordId, { after: next });
392
394
  entry. A field taken out of the app while its table keeps it stays hidden; a field gone from its
393
395
  table appears where the version serving when it was written read it, marked `removed`. Comments
394
396
  appear only where the app declares `comments`.
395
- - **Members of the app's organization only** — the trail names its people. Standalone, `available`
396
- is `false` and nothing is read.
397
+ - **Members of the app's organization only** — the trail names its people. For a public visitor,
398
+ a member of another organization, and standalone, `available` is `false` and nothing is asked.
397
399
  - **One entry per save.** Edits by one actor on one record less than ten minutes apart read as one
398
400
  entry: each field from its value before the first edit to its value after the last, a field edited
399
401
  back to where it began dropped (and an entry left with nothing). A comment, another actor or a
@@ -401,7 +403,9 @@ const older = useRecordActivity("job", recordId, { after: next });
401
403
  its maker's edits after it, however soon, are entries of their own. `at` is the entry's first
402
404
  write, `until` its last.
403
405
  - **An entry** (`ActivityEntry`) is `kind: "change"` with `changes`, or `kind: "comment"` with
404
- `comment: { id, body, mentions }`. `actor`: `{ kind, id, name, image? }` — `member` (a person),
406
+ `comment: { id, body, mentions }`. A change entry also says `undoable` — the viewer may put it
407
+ back — `undone: { by, at }` once someone has, and `undo: true` where it is itself the save that
408
+ put an earlier one back. `actor`: `{ kind, id, name, image? }` — `member` (a person),
405
409
  `agent` (an AI acting for a member: the chat, an app's agent, an MCP client — named by that
406
410
  member), `workflow` (a workflow on its own, named by the workflow) or `import` (a script over the
407
411
  API or the CLI, named by the member whose key ran it). `name` is empty where nothing resolves it.
@@ -411,6 +415,17 @@ const older = useRecordActivity("job", recordId, { after: next });
411
415
  `[{ key, label }]` (`readSelect`; each option's label as it was when written), a member field
412
416
  `[{ id, name, image? }]`, a link `[{ id, display }]` (`readLinks`), files presigned (`readFiles`),
413
417
  `null` for empty.
418
+ - **`undoRecordChange(entity, record_id, entry_id)`** puts one change entry back: each field it
419
+ changed that the app reads returns to what it held before the entry, where the record still holds
420
+ what the entry wrote. It answers `{ reverted, kept }`: the fields put back (`{ field_id, label }`),
421
+ and those left as they stand with a `reason` — `changed_since` (someone changed it since) or
422
+ `refused` (the table refused the write: a lock, a check). A field already holding what it held
423
+ before is in neither. The write is the viewer's own save: they hold `write_data` on the table as
424
+ the call arrives, on a row their own row rule covers, and the record's lock, the table's checks
425
+ and its write rules answer as for any save of theirs. `undoable` is `false`, and the call
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.
428
+ The undo is an entry of its own; the answer arrives once the record's reads are read anew.
414
429
  - **Pages**: newest first, about thirty entries each; `next` is where the trail goes on —
415
430
  `undefined` at its end. A page may hold fewer entries and still say `next` where the trail it read
416
431
  held little the reader may see.
@@ -428,4 +443,5 @@ const older = useRecordActivity("job", recordId, { after: next });
428
443
  | `useViewer` | member id (view-as target) | `null` |
429
444
  | `useWorkspaceTimezone` / `useWorkspaceCurrency` | ✓ | ✓ |
430
445
  | `useComments` / `useCommentCounts` | ✓ when capability declared | ✗ `available: false` |
431
- | `useRecordActivity` | ✓ the app's organization's members | ✗ `available: false` |
446
+ | `useRecordActivity` | ✓ the app's organization's members | ✗ `available: false`, nothing asked |
447
+ | `undoRecordChange` | ✓ a member holding the table's write, on a row they may write | ✗ rejects |
package/docs/runtime.md CHANGED
@@ -210,6 +210,7 @@ surfaces:
210
210
  |---|---|---|
211
211
  | `query`, `field_options`, `workflow`, `members`, `context`, `upload`, `urlState.get/set`, `openExternal` | yes | yes |
212
212
  | `comments.*` | yes | rejects — `"Comments are available only in embedded apps — a signed-in member is required."` |
213
+ | `activity.list`, `activity.undo` | yes, for a member of the app's organization; `activity.undo` for one who may write the record. A host that does not carry the op answers `"Unknown RPC op: activity.undo"` | rejects — `"A record's activity is available only in embedded apps — a signed-in member is required."` |
213
214
  | `agentRun` (streaming, internal to `useAgentRun`) | yes | yes |
214
215
  | `agentRun.get`, `agentRun.cancel` | yes | yes |
215
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 …"`) |
package/docs/security.md CHANGED
@@ -103,6 +103,7 @@ A publicly-shared app (its own origin, or its public link) is reachable by **any
103
103
  | Agent runs (`useAgentRun`) | **No** — rejected: agent runs require an authenticated member, and each member's run history is private to them (a guessed session id cannot read another member's thread) |
104
104
  | Comments (`useComments`) | **No** — members-only |
105
105
  | Record activity (`useRecordActivity`) | **No** — the app's organization's members only |
106
+ | Undo of a record's change (`undoRecordChange`) | **No** — a member of the app's organization holding `write_data` on the record's table, on a row their own row rule covers |
106
107
  | Member roster (`useMembers`) | **No** — same-org members only (below) |
107
108
 
108
109
  **Warning — `is_current_member` on a public app resolves to the app owner.** An anonymous request has no viewer to bind, so both `is_current_member` and `current_member in_any_group` fall back to the owner's identity and the owner's groups. A "my items" query shows every anonymous visitor the *owner's* items, and a manager-only read template gated on a group the owner belongs to **passes for every anonymous visitor**. Never rely on viewer-bound predicates in a public app.
package/docs/workflows.md CHANGED
@@ -392,7 +392,7 @@ body does not look like what you typed.
392
392
  | `arr.at(i)` | `at(arr, i)` — negative counts from the end |
393
393
  | `arr.filter(Boolean)` | `compact(arr)` — drops every falsy element |
394
394
  | `[...a, b]` | `concat(a, [b])` |
395
- | `{ ...a, b: 1 }` | `merge(a, { b: 1 })` — later sources win, as in JS |
395
+ | `{ ...a, b: 1 }` | `merge(merge({}, a), { b: 1 })` — later sources win, as in JS — and reads back as the spread, typed as you wrote it |
396
396
  | `{ table_id }` | `{ table_id: table_id }` — in expressions **and** tool inputs |
397
397
  | `undefined` | `null` |
398
398
  | `xs.push(a, b);` | `xs = concat(xs, [a, b])` — statement form |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/app-sdk",
3
- "version": "0.120.3",
3
+ "version": "0.122.0",
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": {