@lotics/app-sdk 0.44.0 → 0.46.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
@@ -112,6 +112,15 @@ Pick by intent. (→ open the `.d.ts` for the exact signature.)
112
112
  **`openExternal(url)`** opens a link in a new tab (scheme-validated; host-mediated in the embed).
113
113
  **`downloadFile(name, data, mime?)`** saves browser-built bytes (a `Uint8Array | Blob | string`) —
114
114
  call it synchronously from the click that produced them.
115
+ - **Chat handoff** — **`askAi({ prompt?, file_ids?, record_ids?, context? })`** opens the Lotics
116
+ messenger on a FRESH chat seeded with the given files (attached + previewed beside the chat),
117
+ records (the agent can read/act on them under the viewer's authority), a prefilled *editable*
118
+ prompt, and free-text grounding; the host stamps the app's identity automatically. Nothing runs
119
+ until the user presses send. **The razor**: outcome lands in *fields* → `useAgentRun` + a
120
+ review surface (the app's own structured commit path); outcome is a *file or an open-ended
121
+ answer* (edit this invoice, draft an email about this shipment) → `askAi`. Embedded-only —
122
+ rejects in standalone/dev. Name the task in `prompt` (Word/Excel bytes aren't inlined; a clear
123
+ brief makes the agent read the file first).
115
124
 
116
125
  ### Decoding query cells — never hand-roll the serialization contract
117
126
 
@@ -124,7 +133,12 @@ Pick by intent. (→ open the `.d.ts` for the exact signature.)
124
133
  - **`readSelect(cell)`** → `ResolvedOption[]` `{ key, label, color? }` (a cell carries key+label; the
125
134
  `color` is populated by `useFieldOptions`, not the cell). **`readMembers(cell)`** → `{ id, name,
126
135
  email? }[]`. **`readLinks(cell)` / `row.link`** → `{ id, display }`. **`readFiles(cell)`** →
127
- `AppFile[]` with presigned `url` + `thumbnail_url` (24 h). **`readLocked(row)`** `boolean` from the
136
+ `AppFile[]` with presigned `url` + `thumbnail_url` (24 h) **+ `size` (bytes) + `created_at` (ISO
137
+ date-added), resolved from the file object at read**. Surface size/date as **dedicated sortable
138
+ `Table` columns over the RAW numeric** — the `tpl_documents` document-register pattern (a `Size`
139
+ column `align:"right"` + an `Added` column, formatted only at display; NOT a crammed `FileRow` `meta`
140
+ string, which sorts wrong — "8.4 MB" < "96 KB"). `size` is absent for older files until backfilled —
141
+ render it only when present. **`readLocked(row)`** → `boolean` from the
128
142
  `__source_locked` addressing column.
129
143
  - **`project` only what you render** and `filter` server-side: a bare `from_table` ships every column
130
144
  — incl. `files` with storage keys — to the client (over-exposure + presign-500 at scale). A code
@@ -0,0 +1,40 @@
1
+ export interface AskAiArgs {
2
+ /** Prefills the chat composer — the user sees, edits, and sends it
3
+ * themselves. Nothing runs until they do. */
4
+ prompt?: string;
5
+ /** Files to attach to the chat (by id, from `readFiles(cell)`). The host
6
+ * resolves each id itself and opens the file preview beside the chat. */
7
+ file_ids?: string[];
8
+ /** Records the chat should know about (ids from query rows). The host
9
+ * resolves them to table context — the agent can then read and act on
10
+ * them under the signed-in user's authority. */
11
+ record_ids?: string[];
12
+ /** Free-text grounding for the agent ("Shipment SGN-2481, customer …").
13
+ * The host stamps the app's identity alongside it automatically. */
14
+ context?: string;
15
+ }
16
+ /**
17
+ * Hand off to the Lotics chat agent — opens the messenger on a FRESH chat
18
+ * seeded with the given files, records, and prompt.
19
+ *
20
+ * Use this for dialogue-shaped work: iterating on a document ("edit this
21
+ * invoice"), drafting from record context, open-ended questions. For
22
+ * structured judgment that commits back into the app's own data
23
+ * (extract/check/match/rank), use `useAgentRun` with a review surface
24
+ * instead — the outcome of `askAi` lands in chat, not in your workflows.
25
+ *
26
+ * The user stays in control: the prompt is only prefilled, attachments are
27
+ * visible in the composer, and nothing is sent until they press send.
28
+ *
29
+ * ```tsx
30
+ * import { askAi } from "@lotics/app-sdk";
31
+ * await askAi({
32
+ * file_ids: [file.id],
33
+ * record_ids: [row.id],
34
+ * prompt: "Update the header of this invoice to match our letterhead.",
35
+ * });
36
+ * ```
37
+ *
38
+ * Only available embedded in Lotics — rejects in standalone/dev mode.
39
+ */
40
+ export declare function askAi(args: AskAiArgs): Promise<void>;
@@ -0,0 +1,35 @@
1
+ import { rpc } from "./rpc.js";
2
+ /**
3
+ * Hand off to the Lotics chat agent — opens the messenger on a FRESH chat
4
+ * seeded with the given files, records, and prompt.
5
+ *
6
+ * Use this for dialogue-shaped work: iterating on a document ("edit this
7
+ * invoice"), drafting from record context, open-ended questions. For
8
+ * structured judgment that commits back into the app's own data
9
+ * (extract/check/match/rank), use `useAgentRun` with a review surface
10
+ * instead — the outcome of `askAi` lands in chat, not in your workflows.
11
+ *
12
+ * The user stays in control: the prompt is only prefilled, attachments are
13
+ * visible in the composer, and nothing is sent until they press send.
14
+ *
15
+ * ```tsx
16
+ * import { askAi } from "@lotics/app-sdk";
17
+ * await askAi({
18
+ * file_ids: [file.id],
19
+ * record_ids: [row.id],
20
+ * prompt: "Update the header of this invoice to match our letterhead.",
21
+ * });
22
+ * ```
23
+ *
24
+ * Only available embedded in Lotics — rejects in standalone/dev mode.
25
+ */
26
+ export function askAi(args) {
27
+ const hasPayload = (args.prompt ?? "") !== "" ||
28
+ (args.file_ids?.length ?? 0) > 0 ||
29
+ (args.record_ids?.length ?? 0) > 0 ||
30
+ (args.context ?? "") !== "";
31
+ if (!hasPayload) {
32
+ return Promise.reject(new Error("askAi requires at least one of prompt, file_ids, record_ids, context"));
33
+ }
34
+ return rpc("askAi", args);
35
+ }
@@ -26,6 +26,7 @@ export type { GeofenceZone, GeoCoords, GeofenceOutcome, GeofenceOptions } from "
26
26
  export { rpc, isEmbedded } from "./rpc.js";
27
27
  export type { RpcOp } from "./rpc.js";
28
28
  export { openExternal } from "./open_external.js";
29
+ export { askAi, type AskAiArgs } from "./ask_ai.js";
29
30
  export { downloadFile } from "./download.js";
30
31
  export { readMembers } from "./members.js";
31
32
  export type { ResolvedMember } 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 { askAi } from "./ask_ai.js";
24
25
  export { downloadFile } from "./download.js";
25
26
  export { readMembers } from "./members.js";
26
27
  export { readSelect } from "./select.js";
package/dist/src/row.d.ts CHANGED
@@ -68,6 +68,16 @@ export interface AppFile {
68
68
  url: string;
69
69
  /** Presigned thumbnail URL for images, when the server produced one. */
70
70
  thumbnail_url?: string;
71
+ /** Byte size of the file, resolved from the file object at serving time. Absent
72
+ * for older files not yet backfilled — show a size only when present. Prefer a
73
+ * dedicated sortable `Table` column over the RAW number (the `tpl_documents`
74
+ * register: a right-aligned "Size" column, formatted at display) — not a crammed
75
+ * `FileRow` meta string, which sorts wrong ("8.4 MB" < "96 KB"). */
76
+ size?: number | null;
77
+ /** ISO upload timestamp (date-added), resolved at serving time. Show as a
78
+ * dedicated "Added" `Table` column (format with `formatDate`), sortable over the
79
+ * raw ISO value. */
80
+ created_at?: string;
71
81
  }
72
82
  /**
73
83
  * files field → the attached files with their presigned `url` (empty if none).
package/dist/src/row.js CHANGED
@@ -123,12 +123,16 @@ export function readFiles(v) {
123
123
  const filename = f.filename;
124
124
  const mime = f.mime_type;
125
125
  const thumb = f.thumbnail_url;
126
+ const size = f.size;
127
+ const created_at = f.created_at;
126
128
  out.push({
127
129
  id,
128
130
  filename: typeof filename === "string" ? filename : "",
129
131
  mime_type: typeof mime === "string" ? mime : "",
130
132
  url,
131
133
  thumbnail_url: typeof thumb === "string" ? thumb : undefined,
134
+ size: typeof size === "number" ? size : undefined,
135
+ created_at: typeof created_at === "string" ? created_at : undefined,
132
136
  });
133
137
  }
134
138
  return out;
package/dist/src/rpc.d.ts CHANGED
@@ -19,7 +19,7 @@ import { type UrlParams, type UrlParamsPatch } from "./url_params.js";
19
19
  * app → host: { id, op, payload }
20
20
  * host → app: { id, type: "result", data } | { id, type: "error", message }
21
21
  */
22
- export type RpcOp = "query" | "field_options" | "workflow" | "agentRuns" | "agentRun.get" | "agentRun.cancel" | "upload" | "members" | "context" | "openExternal" | "urlState.get" | "urlState.set" | "comments.list" | "comments.create" | "comments.update" | "comments.delete" | "comments.counts";
22
+ 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
23
  /** Payload for starting a streaming agent run. */
24
24
  export interface AgentRunPayload {
25
25
  alias: string;
package/dist/src/rpc.js CHANGED
@@ -427,6 +427,10 @@ function rpcStandalone(op, payload) {
427
427
  return standaloneContext();
428
428
  case "openExternal":
429
429
  return standaloneOpenExternal(payload);
430
+ case "askAi":
431
+ // The chat surface lives in the Lotics host — a standalone page has
432
+ // nowhere to hand off to.
433
+ return Promise.reject(new Error("askAi is only available when the app runs inside Lotics"));
430
434
  case "urlState.get":
431
435
  // Standalone is a top-level page — its own query string IS the store.
432
436
  return Promise.resolve(parseSearch(window.location.search));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/app-sdk",
3
- "version": "0.44.0",
3
+ "version": "0.46.0",
4
4
  "description": "Runtime SDK for Lotics custom-code apps — typed hooks, postMessage bridge, mount entry point",
5
5
  "type": "module",
6
6
  "exports": {