@lotics/app-sdk 0.100.1 → 0.101.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.
Files changed (108) hide show
  1. package/AGENTS.md +32 -47
  2. package/dist/agent_stream.d.ts +131 -0
  3. package/dist/ask_ai.d.ts +27 -0
  4. package/dist/attachments.d.ts +58 -0
  5. package/dist/chunk-ARV5FAU5.js +1132 -0
  6. package/dist/comments.d.ts +89 -0
  7. package/dist/error_report.d.ts +9 -0
  8. package/dist/folder_pick.d.ts +8 -0
  9. package/dist/geolocation.d.ts +42 -0
  10. package/dist/hooks.d.ts +251 -0
  11. package/dist/{src/index.d.ts → index.d.ts} +13 -22
  12. package/dist/index.js +31309 -0
  13. package/dist/index.js.LEGAL.txt +11 -0
  14. package/dist/members.d.ts +32 -0
  15. package/dist/mock.d.ts +37 -0
  16. package/dist/mount.d.ts +19 -0
  17. package/dist/new_record.d.ts +37 -0
  18. package/dist/open_app.d.ts +12 -0
  19. package/dist/open_external.d.ts +10 -0
  20. package/dist/overlay.d.ts +25 -0
  21. package/dist/queries.d.ts +231 -0
  22. package/dist/recording.d.ts +47 -0
  23. package/dist/recording_state.d.ts +43 -0
  24. package/dist/rename_file.d.ts +13 -0
  25. package/dist/router.d.ts +10 -0
  26. package/dist/router.js +97 -0
  27. package/dist/row.d.ts +87 -0
  28. package/dist/rpc.d.ts +114 -0
  29. package/dist/select.d.ts +24 -0
  30. package/dist/shared_types.d.ts +8 -0
  31. package/dist/store.d.ts +43 -0
  32. package/dist/types.d.ts +36 -0
  33. package/dist/upload/optimize.d.ts +30 -0
  34. package/dist/upload/pipeline.d.ts +36 -0
  35. package/dist/upload/transport.d.ts +19 -0
  36. package/dist/url_params.d.ts +55 -0
  37. package/dist/use_recents.d.ts +15 -0
  38. package/dist/{src/use_url_state.d.ts → use_url_state.d.ts} +0 -2
  39. package/dist/viewer.d.ts +41 -0
  40. package/dist/written.d.ts +77 -0
  41. package/docs/ai.md +74 -133
  42. package/docs/data_fetching.md +209 -290
  43. package/docs/files.md +61 -51
  44. package/docs/members_and_options.md +92 -62
  45. package/docs/mutations.md +135 -205
  46. package/docs/navigation_and_state.md +26 -35
  47. package/docs/queries.md +144 -207
  48. package/docs/recipes.md +21 -45
  49. package/docs/runtime.md +74 -137
  50. package/docs/security.md +8 -11
  51. package/docs/workflows.md +189 -174
  52. package/package.json +27 -28
  53. package/dist/src/agent_stream.d.ts +0 -200
  54. package/dist/src/agent_stream.js +0 -314
  55. package/dist/src/ask_ai.d.ts +0 -40
  56. package/dist/src/ask_ai.js +0 -35
  57. package/dist/src/attachments.d.ts +0 -68
  58. package/dist/src/attachments.js +0 -93
  59. package/dist/src/comments.d.ts +0 -127
  60. package/dist/src/comments.js +0 -192
  61. package/dist/src/download.js +0 -54
  62. package/dist/src/geolocation.d.ts +0 -64
  63. package/dist/src/geolocation.js +0 -96
  64. package/dist/src/hooks.d.ts +0 -781
  65. package/dist/src/hooks.js +0 -860
  66. package/dist/src/index.js +0 -34
  67. package/dist/src/members.d.ts +0 -105
  68. package/dist/src/members.js +0 -62
  69. package/dist/src/mock.d.ts +0 -118
  70. package/dist/src/mock.js +0 -124
  71. package/dist/src/mount.d.ts +0 -47
  72. package/dist/src/mount.js +0 -34
  73. package/dist/src/new_record.d.ts +0 -74
  74. package/dist/src/new_record.js +0 -117
  75. package/dist/src/open_app.d.ts +0 -15
  76. package/dist/src/open_app.js +0 -18
  77. package/dist/src/open_external.d.ts +0 -16
  78. package/dist/src/open_external.js +0 -19
  79. package/dist/src/recording.d.ts +0 -59
  80. package/dist/src/recording.js +0 -30
  81. package/dist/src/recording_state.d.ts +0 -59
  82. package/dist/src/recording_state.js +0 -94
  83. package/dist/src/router.d.ts +0 -17
  84. package/dist/src/router.js +0 -144
  85. package/dist/src/row.d.ts +0 -159
  86. package/dist/src/row.js +0 -254
  87. package/dist/src/rpc.d.ts +0 -207
  88. package/dist/src/rpc.js +0 -904
  89. package/dist/src/select.d.ts +0 -48
  90. package/dist/src/select.js +0 -40
  91. package/dist/src/types.d.ts +0 -115
  92. package/dist/src/types.js +0 -1
  93. package/dist/src/upload/optimize.d.ts +0 -54
  94. package/dist/src/upload/optimize.js +0 -207
  95. package/dist/src/upload/pipeline.d.ts +0 -55
  96. package/dist/src/upload/pipeline.js +0 -52
  97. package/dist/src/upload/transport.d.ts +0 -42
  98. package/dist/src/upload/transport.js +0 -128
  99. package/dist/src/url_params.d.ts +0 -93
  100. package/dist/src/url_params.js +0 -215
  101. package/dist/src/use_optimistic.d.ts +0 -27
  102. package/dist/src/use_optimistic.js +0 -27
  103. package/dist/src/use_recents.d.ts +0 -19
  104. package/dist/src/use_recents.js +0 -71
  105. package/dist/src/use_url_state.js +0 -73
  106. package/dist/src/viewer.d.ts +0 -26
  107. package/dist/src/viewer.js +0 -47
  108. /package/dist/{src/download.d.ts → download.d.ts} +0 -0
@@ -1,781 +0,0 @@
1
- import { type ImageFidelity } from "./upload/optimize.js";
2
- import { type AttachmentsOptions, type AttachmentsState } from "./attachments.js";
3
- import { type AiContextValue } from "./rpc.js";
4
- import { type AgentUIPart, type PendingChoice, type AgentRunLanding } from "./agent_stream.js";
5
- import type { AppWorkflows, AppWorkflowResults, AppQueries, AppQueryColumns, AppAgents, AppAgentResults } from "./types.js";
6
- import type { ResolvedMember } from "./members.js";
7
- import type { ResolvedOption } from "./select.js";
8
- export type { AgentRunState, AgentUIPart, PendingChoice, ChoiceQuestion, ChoiceOption, AskUserChoiceOutput, AgentRunLanding } from "./agent_stream.js";
9
- export { buildChoiceOutput } from "./agent_stream.js";
10
- /**
11
- * One row of a query result whose COLUMNS are not known: a dynamic alias, or a
12
- * query whose AST names no projection (a bare `from_table`). Where codegen could
13
- * read them, `RowOf` states them instead and there is no index signature — see
14
- * that type, which is where the reason lives.
15
- *
16
- * The projected values stay `unknown` either way: a column's TYPE is the field's
17
- * and the manifest does not carry it, so the cell readers (`row.text`,
18
- * `readLinks`, `readFiles`, …) are what narrow them.
19
- *
20
- * The `__source_*` columns are different in kind: the compiler injects them at
21
- * every layer, and they are the only way to address the RECORD a row came from
22
- * — what `useComments`, a workflow's `record_id` input, and `useAiContext`'s
23
- * record refs all need. Typed as `unknown` they were unusable without a cast,
24
- * which is how this SDK's own examples came to show code that does not compile.
25
- *
26
- * They are OPTIONAL, and that is the honest shape rather than a hedge: a
27
- * grouped query collapses rows, so its output has no originating record and the
28
- * compiler emits no addressing columns for it (the platform's own `record_id`
29
- * filter refuses such a query for exactly this reason). Narrow before use —
30
- * on an aggregate row these are genuinely absent, not merely unproven.
31
- *
32
- * The line for what belongs here is "consumed RAW", not "starts with
33
- * `__source_`". A row also carries `__source_locked`, `__created_at` /
34
- * `__updated_at` and per-projection `__src_field_*`, and each of those has a
35
- * reader that owns its decoding (`readLocked`, the `row.*` helpers) — a reader
36
- * IS the narrowing, so a type here would duplicate it. Only these two are
37
- * handed straight to a `record_id` / `table_id` parameter with nothing in
38
- * between, which is why only these two needed a type.
39
- */
40
- export interface QueryRow {
41
- [column: string]: unknown;
42
- /** The record this row came from. Absent on a grouped/aggregated row. */
43
- __source_record_id?: string;
44
- /** The table that record lives in. Absent on a grouped/aggregated row. */
45
- __source_table_id?: string;
46
- }
47
- /**
48
- * The row type of alias `K`: the columns that alias PROJECTS, plus the two
49
- * addressing columns, and nothing else.
50
- *
51
- * `.lotics/app_queries.d.ts` already names every projected column
52
- * (`AppQueryColumns`), read off the query's AST by the same rule the server
53
- * names them by. A read is bounded to that union for the same reason a
54
- * `filter`/`sort` key is — except the failure is quieter. A key the query does
55
- * not carry is refused by the server; a COLUMN it does not carry comes back
56
- * `undefined`, which every cell reader answers with its empty value, so a
57
- * misspelt column, a renamed field and a projection moved to another alias all
58
- * render as a blank cell in a member's browser with no error anywhere. Removing
59
- * the index signature is what turns that silence into a `tsc` error, which is
60
- * what `lotics app check` refuses to ship.
61
- *
62
- * An alias with no `AppQueryColumns` entry keeps `QueryRow` — a union that could
63
- * be wrong is worse than none, the rule `ColumnKeyOf` already follows.
64
- */
65
- export type RowOf<K extends string> = K extends keyof AppQueryColumns ? {
66
- [C in AppQueryColumns[K] & string]: unknown;
67
- } & Pick<QueryRow, "__source_record_id" | "__source_table_id"> : QueryRow;
68
- /** Fields shared by every query hook's return value. */
69
- interface QueryStateBase {
70
- /**
71
- * True only on the initial load — a request is in flight and there are no
72
- * rows yet. Stays false during background revalidation and while typing a new
73
- * query (the previous rows remain visible), so consumers never blank data to a
74
- * spinner on refetch. Use `isValidating` for a subtle refetch indicator.
75
- */
76
- loading: boolean;
77
- /** True whenever any request is in flight (initial load or revalidation). */
78
- isValidating: boolean;
79
- error: string | null;
80
- /**
81
- * Re-run the query. Use after a known mutation point — a successful
82
- * `useWorkflow(alias)()` call — to pull the latest state.
83
- */
84
- refetch: () => void;
85
- }
86
- /** Return value of `useQuery` — a single fetch, no pagination. */
87
- interface QueryState<R> extends QueryStateBase {
88
- rows: R[];
89
- /**
90
- * True when the limit cut the result — more rows matched than arrived, so
91
- * `rows` is SHORT and anything folded from it (a total, a ratio, a KPI strip)
92
- * is under-reported. Without it the cap is invisible: no error, no empty
93
- * state, just numbers that look right.
94
- *
95
- * The limit is your `pageSize` when you set one and the server's row cap
96
- * otherwise. False while loading and under a fixture.
97
- */
98
- truncated: boolean;
99
- }
100
- /** Return value of `useInfiniteQuery` — append/load-more. */
101
- interface InfiniteQueryState<R> extends QueryStateBase {
102
- /** All loaded pages, flattened and accumulated. */
103
- rows: R[];
104
- /**
105
- * Fetch the next page and append it to `rows`. No-op when there are no more
106
- * rows (`hasMore` is false). `loadingMore` is true while it runs.
107
- */
108
- loadMore: () => void;
109
- /** True when the last page came back full, so more rows may exist. */
110
- hasMore: boolean;
111
- /** True while a `loadMore` request is in flight. */
112
- loadingMore: boolean;
113
- }
114
- /** Return value of `usePaginatedQuery` — page-model with a total. */
115
- interface PaginatedQueryState<R> extends QueryStateBase {
116
- /** Rows of the current page only (≤ `pageSize`). */
117
- rows: R[];
118
- /** Total rows in the filtered set (across all pages). `undefined` until the
119
- * count resolves. */
120
- total: number | undefined;
121
- /** `ceil(total / pageSize)`, or `undefined` until the count resolves. */
122
- totalPages: number | undefined;
123
- /** Current 0-indexed page. */
124
- page: number;
125
- pageSize: number;
126
- /** True when a next page exists. */
127
- hasMore: boolean;
128
- /** Jump to a page (0-indexed). Clamped at 0. */
129
- setPage: (page: number) => void;
130
- }
131
- /**
132
- * One sort key — the wire shape of the query RPC's `sort`. The server applies
133
- * these AFTER the named query, bounded to the query's output columns (an
134
- * un-projected `field_key` is rejected), so an app can sort by any column it
135
- * actually selects without the query template declaring it.
136
- */
137
- export interface QuerySortKey<C extends string = string> {
138
- /** One of the query's projected outputs — `AppQueryColumns[alias]` on a
139
- * typed alias, so a column the query does not carry is a compile error
140
- * rather than the server's request-time refusal. */
141
- field_key: C;
142
- order: "asc" | "desc";
143
- /**
144
- * Where rows BLANK in this column sit — `"bottom"` when omitted, which is what
145
- * you want for "the ones with a value first".
146
- *
147
- * It earns its place on a MULTI-KEY sort over columns that are blank by
148
- * position rather than by accident — a pipeline's per-step date stamps, say,
149
- * where the first non-blank column IS the row's rank. Reading such a ladder
150
- * from the top with blanks at the bottom orders it furthest-along-first; the
151
- * reverse reading needs blanks on TOP, and with only the default there is no
152
- * way to express it, so the column can be sorted one way and not the other.
153
- */
154
- blank_position?: "top" | "bottom";
155
- }
156
- /** A filter condition over one output column (wire shape of a filter node).
157
- * `C` is the key's type — see `QuerySortKey`. */
158
- export interface QueryFilterFieldCondition<C extends string = string> {
159
- node_type: "condition";
160
- field_key: C;
161
- type?: string;
162
- operator: string;
163
- value?: unknown;
164
- }
165
- /**
166
- * A condition on the row's OWN id rather than on a field — the fetch-by-id
167
- * filter. It carries no `field_key` because a record has no field holding its
168
- * own id, and the engine dispatches on `type` before it reads one.
169
- */
170
- export interface QueryFilterRecordIdCondition {
171
- node_type: "condition";
172
- type: "record_id";
173
- operator: string;
174
- value?: unknown;
175
- }
176
- export type QueryFilterCondition<C extends string = string> = QueryFilterFieldCondition<C> | QueryFilterRecordIdCondition;
177
- /** A boolean group of filter nodes (wire shape — recursive). */
178
- export interface QueryFilterGroup<C extends string = string> {
179
- node_type: "group";
180
- logic: "and" | "or";
181
- children: Array<QueryFilterCondition<C> | QueryFilterGroup<C>>;
182
- }
183
- /**
184
- * Runtime filter applied AFTER the named query, bounded to its output columns
185
- * (same exposure invariant as `sort`) — at compile time through `C`, and again
186
- * on the server. Build a group from per-column filters with
187
- * `columnFilterToConditions` (`@lotics/ui/column_filter`).
188
- */
189
- export type QueryFilter<C extends string = string> = QueryFilterCondition<C> | QueryFilterGroup<C>;
190
- /**
191
- * The type a runtime `filter`/`sort` key takes on alias `K`: the union codegen
192
- * wrote into `AppQueryColumns`, else `string` (the server's check is then the
193
- * only one). A computed key must be narrowed to the union to compile.
194
- */
195
- export type ColumnKeyOf<K extends string> = K extends keyof AppQueryColumns ? AppQueryColumns[K] & string : string;
196
- /** Options shared by every query hook. `C` is the filter/sort key's type — see
197
- * `ColumnKeyOf`. */
198
- export interface BaseQueryOptions<C extends string = string> {
199
- /**
200
- * When `false`, the query does not run: `rows` stays empty, `loading` is
201
- * false, and no request is sent. Flip it back to `true` to fetch. This is the
202
- * primitive for search-as-you-type (skip until the user types) and for detail
203
- * queries (skip until a row is selected) — a parameterized search filter
204
- * matches everything on an empty term, so an always-on query would dump the
205
- * whole table on first paint. Default `true`.
206
- */
207
- enabled?: boolean;
208
- /**
209
- * When `false`, the query does not auto-refetch on window focus / tab return /
210
- * network reconnect (`refetch()` still works). Default `true`, right for
211
- * dashboards that should stay fresh. Set `false` for transient queries — a
212
- * search bound to an ephemeral term, or on-demand detail — where a refocus
213
- * re-run is wasted work and a visible reload.
214
- */
215
- revalidateOnFocus?: boolean;
216
- /**
217
- * Sort the result by output columns at runtime. Changing it re-queries (it is
218
- * part of the cache key). Empty/omitted leaves the query's own order intact.
219
- */
220
- sort?: QuerySortKey<C>[];
221
- /**
222
- * Filter the result by output columns at runtime. Changing it re-queries.
223
- * Compose from per-column UI filters via `columnFilterToConditions`.
224
- */
225
- filter?: QueryFilter<C>;
226
- }
227
- /** Options for `useQuery` — a single fetch. */
228
- export interface QueryOptions<C extends string = string> extends BaseQueryOptions<C> {
229
- /** Max rows to fetch in the one request (a cap, not pagination). The server
230
- * still clamps to its own maximum. Omit to fetch up to the server cap. */
231
- pageSize?: number;
232
- }
233
- /** Options for `useInfiniteQuery` — append/load-more. */
234
- export interface InfiniteQueryOptions<C extends string = string> extends BaseQueryOptions<C> {
235
- /** Rows per page. `loadMore()` appends the next page. */
236
- pageSize: number;
237
- }
238
- /**
239
- * Options for `useCount` — one number, no rows.
240
- *
241
- * `sort` and `pageSize` are absent rather than ignored. A count is a single-row
242
- * COUNT over the filtered set; ordering it and paginating it are meaningless,
243
- * and an option a hook silently drops is worse than one that will not compile.
244
- */
245
- export type CountOptions<C extends string = string> = Omit<BaseQueryOptions<C>, "sort">;
246
- /** Return value of `useCount` — the size of a filtered set, and nothing else. */
247
- interface CountState extends QueryStateBase {
248
- /** Rows in the filtered set. `undefined` until the count resolves. */
249
- total: number | undefined;
250
- }
251
- /** Options for `usePaginatedQuery` — page-model with a total. */
252
- export interface PaginatedQueryOptions<C extends string = string> extends BaseQueryOptions<C> {
253
- /** Rows per page. Default 25. */
254
- pageSize?: number;
255
- /**
256
- * The total, when the SCREEN already knows it — which suppresses the hook's
257
- * own count request entirely.
258
- *
259
- * Three states, all of them in the type: **omitted** → the hook counts;
260
- * **`null`** → yours, not resolved yet; **a number** → yours, use it. `null`
261
- * is what makes the option usable at all, because the natural source is an
262
- * aggregate that is still loading on the first render — with only
263
- * "number-or-nothing" the hook would fire the count it exists to avoid and
264
- * throw the result away the moment the real number landed. So read it as
265
- * `?? null`:
266
- *
267
- * ```tsx
268
- * const summary = useQuery("orderStats", params); // one ungrouped aggregate
269
- * const rows = usePaginatedQuery("orders", params, {
270
- * pageSize: 100,
271
- * total: (summary.rows[0]?.row_count as number | undefined) ?? null,
272
- * });
273
- * ```
274
- *
275
- * Until the number arrives the hook reports `total: undefined` and `hasMore`
276
- * falls back to "the page came back full" — exactly its behaviour while a
277
- * count is in flight.
278
- *
279
- * Worth reaching for when a screen already renders the same figure: a `count`
280
- * request re-executes the whole named query server-side, so a summary reading
281
- * "N items" beside a table of those N rows is otherwise paying for that number
282
- * twice, and both executions grow with the filtered set.
283
- *
284
- * The number must count the SAME set the query returns — the hook derives
285
- * `totalPages` and `hasMore` from it and cannot tell that it doesn't.
286
- *
287
- * `refetch()` does not refresh it; it is yours, so refresh its source.
288
- */
289
- total?: number | null;
290
- }
291
- /**
292
- * Trigger a workflow by alias from the app's manifest.
293
- *
294
- * The alias must be declared in `package.json` "lotics.workflows" and synced
295
- * to `apps.workflows` on the last deploy.
296
- *
297
- * Per-app CLI codegen (`lotics app pull` / `app dev` / `app deploy`) writes
298
- * `.lotics/app_workflows.d.ts` augmenting `AppWorkflows` with the declared
299
- * alias → input-type map. Result:
300
- * - Undeclared alias → compile-time error at the `useWorkflow("...")` site
301
- * - Declared with full `{workflow_id, inputs}` form → callable typed as
302
- * `(inputs: <DeclaredShape>) => Promise<WorkflowResult>`
303
- * - Declared with shorthand (bare workflow_id) → callable typed as
304
- * `(inputs?: Record<string, unknown>) => Promise<WorkflowResult>` (untyped inputs)
305
- *
306
- * ```tsx
307
- * const issue = useWorkflow("issueInvoiceStorageDrop");
308
- * // Guard the addressing column: a grouped query's rows carry none, and the
309
- * // workflow would be handed `undefined` where it declares a record.
310
- * if (row.__source_record_id) await issue({ record_id: row.__source_record_id });
311
- * ```
312
- */
313
- export declare function useWorkflow<K extends keyof AppWorkflows & string>(alias: K): UseWorkflowFn<K>;
314
- export declare function useWorkflow(alias: string): (inputs?: Record<string, unknown>) => Promise<WorkflowResult>;
315
- type ResultDataOf<K extends string> = K extends keyof AppWorkflowResults ? AppWorkflowResults[K] : unknown;
316
- type UseWorkflowFn<K extends keyof AppWorkflows & string> = AppWorkflows[K] extends Record<string, unknown> ? AppWorkflows[K] extends Record<string, never> ? (inputs?: Record<string, never>) => Promise<WorkflowResult<ResultDataOf<K>>> : (inputs: AppWorkflows[K]) => Promise<WorkflowResult<ResultDataOf<K>>> : (inputs?: Record<string, unknown>) => Promise<WorkflowResult<ResultDataOf<K>>>;
317
- /**
318
- * Result of an app-workflow run — the execute endpoint's response. `files` holds
319
- * any document a workflow step generated (e.g. via a `generate_*_from_template`
320
- * tool), resolved for download: read `files[0].url` and pass it to `openExternal`.
321
- * A workflow that generates no file resolves with `files` absent.
322
- *
323
- * `data` is the structured value the workflow returned via `return({ data })`,
324
- * typed per the alias's declared `outputs` schema (`unknown` when none was declared).
325
- *
326
- * A transport/gateway failure (a Cloudflare 524 timeout on a long run, any 5xx,
327
- * or a non-JSON error page) **resolves** with `{ status: "error", message }` —
328
- * a body-free, friendly message — rather than rejecting with a raw HTML body.
329
- * So an app handles every failure (handled workflow error AND transport error)
330
- * by checking `result.status === "error"`; it never receives gateway HTML.
331
- */
332
- export interface WorkflowResult<TData = unknown> {
333
- status: "success" | "error";
334
- message?: string;
335
- files?: UploadedFile[];
336
- data?: TData;
337
- /**
338
- * Per-input refusals, keyed by the INPUT name the alias declares — what a
339
- * form wires straight onto the control that is wrong
340
- * (`<FormField error={result.field_errors?.ly_do}>`), where `message` can
341
- * only say it at the dialog's scope.
342
- *
343
- * Either half of the round trip fills it: the SERVER, when the payload does
344
- * not match what the alias declares, and the workflow's own
345
- * `return({ field_errors })`. One key, so a screen wires the control once
346
- * rather than branching on which half refused. A step whose write the
347
- * target table refuses (its `before_*` workflow, a unique tuple) keys its
348
- * sentence by the FIELD key (`fld_…`) instead, since the table has never
349
- * heard of the input. Absent when nothing named a field — and an older
350
- * server names none.
351
- */
352
- field_errors?: Record<string, string>;
353
- }
354
- /**
355
- * The (params, opts) tail of a query hook for alias `K`: `params` optional when
356
- * the alias declares none, required otherwise, then an optional `opts` — a
357
- * variadic tuple so `f("a", { … })` and `f("a", params, { … })` both type-check.
358
- * ONE conditional signature, never a typed overload beside a loose
359
- * `alias: string` one: a rejected filter key would fall through to the loose
360
- * overload and compile.
361
- */
362
- type QueryArgs<K extends string, O> = K extends keyof AppQueries ? AppQueries[K] extends Record<string, never> ? [params?: Record<string, never>, opts?: O] : [params: AppQueries[K], opts?: O] : [params?: Record<string, unknown>, opts?: O];
363
- /**
364
- * Read rows from a query the app's author declared in `lotics.queries` — a
365
- * single fetch, no pagination. For long lists use `usePaginatedQuery`
366
- * (numbered pages + total) or `useInfiniteQuery` (load-more).
367
- *
368
- * The app never sends a raw query AST — it invokes a named query by alias and
369
- * fills the template's declared `{{params.x}}` value holes. The server holds
370
- * the canonical AST; this is what bounds a public app's data exposure to
371
- * exactly the queries the manifest declares.
372
- *
373
- * ```tsx
374
- * const { rows, loading } = useQuery("openOrders", { status: "open" });
375
- * ```
376
- *
377
- * Per-app CLI codegen writes `.lotics/app_queries.d.ts` augmenting `AppQueries`
378
- * with the declared alias → param-type map, so an undeclared alias is a
379
- * compile-time error and params are typed per the manifest. The same file names
380
- * the alias's projected columns, which is what bounds a ROW read — see `RowOf`.
381
- */
382
- export declare function useQuery<K extends string>(alias: K, ...args: QueryArgs<K, QueryOptions<ColumnKeyOf<K>>>): QueryState<RowOf<K>>;
383
- /** The resolved option set of one select column, plus an index for value
384
- * rendering. The companion to a query row, for select fields. */
385
- export interface FieldOptions {
386
- /** The source field's display name — e.g. a picker/section label. */
387
- label: string;
388
- /**
389
- * Every option of the field — `{ key, label, color, mark? }`. Includes options not
390
- * present in any current row, so a freshly-added option appears in a picker
391
- * without an app change, and a removed one drops out.
392
- */
393
- options: ResolvedOption[];
394
- /**
395
- * Resolve one option by key — for COLORING A STORED VALUE: pair with
396
- * `readSelect(cell)[0]?.key`. `undefined` for an unknown key (option removed
397
- * after the cell was written); render the cell's own label with a neutral
398
- * badge in that case.
399
- */
400
- byKey: (key: string) => ResolvedOption | undefined;
401
- }
402
- /** Return value of `useFieldOptions`. `C` is the column key's type — the alias's
403
- * projected outputs, see `ColumnKeyOf`. */
404
- export interface FieldOptionsState<C extends string = string> {
405
- /**
406
- * Resolved option sets keyed by the query's OUTPUT column name — the alias's
407
- * own columns, so a key belonging to another query is a `tsc` error rather
408
- * than a `[]` picker nobody can open.
409
- *
410
- * EVERY key is optional, and that is the shape rather than a hedge: a select
411
- * column the server could not resolve to a source field (a UNION output whose
412
- * arms disagree, a computed column) is genuinely absent, and a non-select
413
- * column never had options. Read through it (`fields.status?.options ?? []`).
414
- */
415
- fields: Partial<Record<C, FieldOptions>>;
416
- loading: boolean;
417
- isValidating: boolean;
418
- error: string | null;
419
- /** Re-fetch — after a known field-config change (rare). */
420
- refetch: () => void;
421
- }
422
- /** Options for `useFieldOptions`. */
423
- export interface FieldOptionsOptions {
424
- /** Defer the fetch until true — e.g. a picker that only needs options once an
425
- * edit drawer opens. Defaults to true. */
426
- enabled?: boolean;
427
- }
428
- /**
429
- * Resolve the full option set (key, label, color) of a named query's `select`
430
- * columns — the picker companion to `useQuery`. Where a query CELL carries only
431
- * the options a record actually holds (key + label, no color), this returns each
432
- * select column's COMPLETE option list with colors, straight from the field
433
- * config — so it populates a dropdown AND colors a stored value, and a freshly
434
- * added/removed option flows through with no app change.
435
- *
436
- * Addressed by the same alias you query: the option sets resolve from the named
437
- * query's output columns, scoped exactly like running it, and `fields` is keyed
438
- * by that same union — the alias that CARRIES the column is the one to ask, and
439
- * asking another is a compile error instead of an empty picker. A column the
440
- * server can't map to a source select field (UNION output, computed column) is
441
- * absent, so every key is optional.
442
- *
443
- * ```tsx
444
- * const { fields } = useFieldOptions("records");
445
- * // populate + color a picker:
446
- * <Select variant="native" options={fields.status?.options ?? []}
447
- * renderOptionContent={(o) => <Status option={o} />} />
448
- * // color a stored value:
449
- * <Status option={fields.status?.byKey(readSelect(r.status)[0]?.key ?? "")} />
450
- * ```
451
- */
452
- export declare function useFieldOptions<K extends keyof AppQueries & string>(alias: K, opts?: FieldOptionsOptions): FieldOptionsState<ColumnKeyOf<K>>;
453
- export declare function useFieldOptions(alias: string, opts?: FieldOptionsOptions): FieldOptionsState;
454
- /**
455
- * Like `useQuery` but append/load-more: the first render loads one page and
456
- * `loadMore()` appends the next, accumulating into `rows` (infinite scroll).
457
- * For numbered pages + a total, use `usePaginatedQuery`.
458
- *
459
- * ```tsx
460
- * const { rows, loadMore, hasMore } = useInfiniteQuery("feed", {}, { pageSize: 30 });
461
- * ```
462
- */
463
- export declare function useInfiniteQuery<K extends string>(alias: K, ...args: QueryArgs<K, InfiniteQueryOptions<ColumnKeyOf<K>>>): InfiniteQueryState<RowOf<K>>;
464
- /**
465
- * Page-model query with a total — the data hook behind a numbered, jumpable
466
- * table (pairs with `@lotics/ui/pagination`). It owns the page
467
- * cursor and fetches two things: the current page of rows, and a `count` over
468
- * the filtered set (keyed independently of page + sort, so paging and
469
- * re-sorting never recount). The `(params, filter)` tuple is the result-set
470
- * identity: changing it resets to page 0 AND recounts; changing only `sort`
471
- * does neither.
472
- *
473
- * ```tsx
474
- * const { rows, total, page, setPage, hasMore } =
475
- * usePaginatedQuery("orders", { q }, { pageSize: 25, sort, filter });
476
- * ```
477
- */
478
- export declare function usePaginatedQuery<K extends string>(alias: K, ...args: QueryArgs<K, PaginatedQueryOptions<ColumnKeyOf<K>>>): PaginatedQueryState<RowOf<K>>;
479
- /**
480
- * HOW MANY rows a query matches — one number, no rows fetched.
481
- *
482
- * The shape behind a facet chip, a queue badge, a "N awaiting approval" tile:
483
- * the screen wants the size of a set it is not listing. Reach for it instead of
484
- * the two things that used to stand in for it, both of which are worse:
485
- *
486
- * - `usePaginatedQuery(alias, params, { pageSize: 1 })` buys a row nobody
487
- * renders — two requests for one integer, against a server that bounds how
488
- * many app queries run at once.
489
- * - A hand-rolled `rpc("query", { count: true })` is one request and leaves the
490
- * cache: it does not dedupe with the page beside it, does not revalidate on
491
- * focus, and never hears the host's post-write refetch. The number then goes
492
- * stale over a set that has moved while everything around it updates, which
493
- * is the failure worth avoiding — a count that is quietly wrong costs more
494
- * than a count that costs a request.
495
- *
496
- * ONE COUNT PER FILTERED SET. The cache key is `(alias, params, filter)` — the
497
- * very key `usePaginatedQuery` counts under — so a table and a badge over the
498
- * same set issue ONE count between them, and a page click or a re-sort reuses
499
- * it (a count is sort- and page-independent).
500
- *
501
- * **N counts over one source should not be N hooks.** Each declared query
502
- * re-executes its whole `from` tree, so four facets mounted as four `useCount`s
503
- * are four full scans that land in the same burst. When the counts differ only
504
- * by a bucket the rows can be grouped on, one `group` query returns them all in
505
- * a single scan and folds client-side (queries.md §10) — and its figure can be
506
- * handed to `usePaginatedQuery`'s `total` so the list stops counting too. This
507
- * hook is for the count that has no sibling to group with.
508
- *
509
- * ```tsx
510
- * const { total } = useCount("orders", { q }, { filter: unpaidFilter });
511
- * return <Status label={total == null ? "…" : `${total}`} />;
512
- * ```
513
- */
514
- export declare function useCount<K extends string>(alias: K, ...args: QueryArgs<K, CountOptions<ColumnKeyOf<K>>>): CountState;
515
- /** A file the host has stored and resolved serving URLs for. */
516
- export interface UploadedFile {
517
- id: string;
518
- filename: string;
519
- mime_type: string;
520
- url?: string;
521
- thumbnail_url?: string;
522
- }
523
- interface FileUploadState {
524
- /**
525
- * Upload one file. Resolves to the stored file; pass `UploadedFile.id` into
526
- * a `useWorkflow` call to attach it to a record. Rejects on failure — the
527
- * file is never partially stored.
528
- *
529
- * `fidelity` says how much of the image must survive storage; it defaults to
530
- * `"high"`, which keeps the text of a photographed document legible. Only
531
- * photographs are affected — a PDF, Word or Excel file is stored untouched at
532
- * every step. Pass `"standard"` for bulk visual capture (a forty-photo survey,
533
- * where the volume is what costs you), or `"original"` when the pixels
534
- * themselves are the evidence.
535
- */
536
- upload: (file: File, options?: {
537
- fidelity?: ImageFidelity;
538
- }) => Promise<UploadedFile>;
539
- /** True while any upload from this hook is in flight. */
540
- uploading: boolean;
541
- /** Message of the most recent failed upload, cleared when a new one starts. */
542
- error: string | null;
543
- }
544
- /**
545
- * Upload files from an app. The bytes are stored via a presigned
546
- * direct-to-storage upload the host mediates; the API server never proxies
547
- * them. Works the same in a public (anonymous) app and a member-facing one.
548
- *
549
- * ```tsx
550
- * const { upload, uploading } = useFileUpload();
551
- * const submit = useWorkflow("submitApplication");
552
- * const cccd = await upload(file);
553
- * await submit({ ...fields, cccd_file_id: cccd.id });
554
- * ```
555
- */
556
- export declare function useFileUpload(): FileUploadState;
557
- /**
558
- * Attachments with the optimistic-preview UX: a local object-URL preview shows
559
- * the INSTANT a file is added, the upload runs in the background, and the
560
- * stored `file_id` lands in `files` when it completes. Picking is the app's
561
- * (button / paste / drop) — pass the resulting `File[]` to `add`, or to
562
- * `attach` where a write has to wait for the ids.
563
- *
564
- * WHICH LIFECYCLE is the caller's one decision. A composer accumulates and
565
- * clears when it sends; a record's section passes what its row now holds as
566
- * `landed`, and each entry leaves the queue as the pile takes it over.
567
- *
568
- * ```tsx
569
- * const { files, add, remove, clear, uploading, fileIds } = useAttachments();
570
- * const design = useWorkflow("design");
571
- * // attach: <Button icon="paperclip" onPress={() => pickFiles({ accept: "image/*" }).then(add)} />
572
- * // preview: map each AttachedFile to a @lotics/ui DisplayFile (snake_case → camelCase) — the
573
- * // app owns this data→UI adapter; the SDK never imports @lotics/ui:
574
- * // files.map((f) => (
575
- * // <FileThumbnail
576
- * // file={{ id: f.id, filename: f.filename, mimeType: f.mime_type, url: f.preview_url }}
577
- * // uploading={f.status === "uploading"} onRemove={() => remove(f.id)} />
578
- * // ))
579
- * // send: design({ photo: fileIds[0] }); clear();
580
- * ```
581
- *
582
- * The queue is `useAttachmentQueue`, bound here to the app's own upload.
583
- */
584
- export declare function useAttachments(options?: AttachmentsOptions): AttachmentsState;
585
- /**
586
- * Publish a slice of the CURRENT SCREEN's view state to the app's ambient chat
587
- * agent, so a member chatting alongside the app gets an agent that knows what
588
- * they are looking at — which list is filtered to what, which record is open,
589
- * what is typed into a form. Declarative and lifecycle-bound: mounting or
590
- * changing `context` pushes it to the host; unmounting, renaming the `slot`, or
591
- * passing `null` clears it. Independent components may publish different `slot`s
592
- * concurrently (a list screen + an open detail drawer); the newest value per
593
- * slot wins.
594
- *
595
- * **Push-only, and a SNAPSHOT of what the app already RENDERED to this member** —
596
- * never a channel for chat to pull app-authority data. `records` are passed as
597
- * raw `{ table_id, record_id }` refs (unresolved); the member's own chat agent
598
- * acts on them only where that member's IAM already allows. The host feeds
599
- * `description`/`data` into the agent's prompt as clearly-labeled DATA, never as
600
- * instructions.
601
- *
602
- * Host-enforced caps (exceeding them truncates/drops — never an error): `slot`
603
- * ≤ 50 chars; `description` ≤ 1000 chars (truncated with "…"); `records` ≤ 20;
604
- * `data` must JSON-serialize to ≤ 2000 chars or the `data` field is dropped (the
605
- * description is kept); ≤ 8 slots per app (a 9th evicts the least-recently
606
- * updated).
607
- *
608
- * ```tsx
609
- * useAiContext("orders_list", {
610
- * description: `Viewing ${rows.length} orders filtered to status=open, sorted by due date.`,
611
- * // `flatMap` + the guard, not `map`: the addressing columns are absent on a
612
- * // GROUPED query's rows, so a ref built without checking carries `undefined`
613
- * // and points the agent at nothing.
614
- * records: rows.flatMap((r) =>
615
- * r.__source_table_id && r.__source_record_id
616
- * ? [{ table_id: r.__source_table_id, record_id: r.__source_record_id }]
617
- * : [],
618
- * ),
619
- * data: { filter: "status=open", sort: "due_date desc" },
620
- * });
621
- * ```
622
- *
623
- * No-ops with no embedding host (standalone on the app's own origin — there is no
624
- * chat surface to inform) and in mock mode. Since 0.52.
625
- */
626
- export declare function useAiContext(slot: string, context: AiContextValue | null): void;
627
- interface MembersState {
628
- /** Members of the app's organization, for assign / member-picker UIs. */
629
- members: ResolvedMember[];
630
- loading: boolean;
631
- error: string | null;
632
- }
633
- /** Options for `useMembers`. */
634
- export interface MembersOptions {
635
- /**
636
- * Restrict to one member group, as `GRP.<group>` from `.lotics/app_fields.ts`
637
- * (a pasted `grp_…` id is refused by `lotics app check` — it resolves to
638
- * nothing in a copy of the app). The group must be declared on a `member`
639
- * workflow input's `group` — listing an undeclared group errors. Omit to list
640
- * the whole org roster.
641
- */
642
- group?: string;
643
- }
644
- /**
645
- * List the members of the app's organization — the candidate set for an
646
- * "assign to a member" picker. Each member is `{ id, name, email, image }`
647
- * (`image` = avatar URL, may be null). Resolves through the host (member-only;
648
- * an anonymous public visitor gets an error). Names may be empty for members
649
- * without a display name set — fall back to `email`.
650
- *
651
- * Gated: the app must DECLARE that it works with members — it needs a workflow
652
- * whose manifest declares a `member`-typed input. Passing `{ group }` restricts
653
- * to that group, and is only honored if some member input declares that
654
- * `group` — so an app can only list (and assign into) groups it declares.
655
- *
656
- * ```tsx
657
- * const { members } = useMembers({ group: GRP.sale });
658
- * // <Select variant="native" options={members.map((m) => ({
659
- * // value: m.id, label: m.name || m.email || m.id, image: m.image,
660
- * // }))} />
661
- * ```
662
- */
663
- export declare function useMembers(opts?: MembersOptions): MembersState;
664
- type AgentOutputOf<K extends string> = K extends keyof AppAgentResults ? AppAgentResults[K] : unknown;
665
- /** Options for one `run(...)` call — the app-owned session key it belongs to. */
666
- export interface AgentRunOptions {
667
- /** Groups this run with prior runs in the same working session; the agent
668
- * re-reads them for context. Mint a new id to "clear context". */
669
- sessionId: string;
670
- /** Deliberately abort the run in flight and start this one in its place.
671
- * Without it, `run()` is SINGLE-FLIGHT: a call while a run is streaming
672
- * returns the in-flight run's promise instead of starting (and billing) a
673
- * second run — so an accidental double-press resolves with the first run's
674
- * result. The aborted-and-replaced run still executes and bills server-side;
675
- * replacement is a deliberate act, never a side effect of an extra click. */
676
- replace?: boolean;
677
- }
678
- /** The live state + controls returned by `useAgentRun`. */
679
- export interface UseAgentRun<TInput, TOutput> {
680
- /** Start a run — streams progress into this hook's state and resolves with how
681
- * the leg ENDED (`settled` / `parked` / `failed` / `aborted`). Read `kind`
682
- * rather than the hook's state: this value is a snapshot at settle, whereas
683
- * the state has not committed yet when the promise resolves. SINGLE-FLIGHT —
684
- * a second call while one is streaming joins the first (see `replace`). */
685
- run: (input: TInput, opts: AgentRunOptions) => Promise<AgentRunLanding<TOutput>>;
686
- /** Stop listening locally (no server effect) — the run keeps executing
687
- * server-side and its result lands in the session history. Used on unmount. */
688
- abort: () => void;
689
- /** Explicitly stop the run server-side (saves tokens) AND locally. Wire a
690
- * user-facing "Stop" button to this, not `abort`. */
691
- cancel: () => void;
692
- /** `awaiting_input` = the run is PARKED on a question the agent asked
693
- * (`pendingChoice` carries it); `answerChoice` continues the run. */
694
- status: "idle" | "streaming" | "awaiting_input" | "completed" | "error";
695
- /** The ordered run transcript as ai-sdk `parts` — prose interleaved with tool
696
- * calls, in stream order. Feed straight to `@lotics/ui` `AgentRun`:
697
- * `<AgentRun parts={run.parts} state={run.status === "streaming" ? "streaming" : run.status === "error" ? "error" : "done"} />`
698
- * — no hand-assembly. */
699
- parts: AgentUIPart[];
700
- /** The agent's pending ask — non-null exactly while `status` is
701
- * `awaiting_input`. `questions` maps 1:1 onto `@lotics/ui`'s `ClarifyWizard`
702
- * (`{question, options: {label, description}[], allow_custom}`). */
703
- pendingChoice: PendingChoice | null;
704
- /** Answer the pending ask and CONTINUE the run — one `{value, custom}` per
705
- * question, aligned by index (exactly what `ClarifyWizard`'s `onSubmit`
706
- * yields). Streams the continuation into the same `parts` and resolves like
707
- * `run` — a landing, which may be `parked` again for a follow-up ask. Rejects
708
- * when nothing is pending, or when the server refuses the answer (the run
709
- * stays parked; show the error and keep the wizard open). */
710
- answerChoice: (answers: {
711
- value: string;
712
- custom: boolean;
713
- }[]) => Promise<AgentRunLanding<TOutput>>;
714
- /** The agent's ANSWER prose (every `text` part concatenated), accumulating live —
715
- * excludes thinking (`reasoning` is its own part). For a FREE-TEXT agent this IS
716
- * the result; a structured agent's result is `output`. Derived from `parts`. */
717
- text: string;
718
- /** The structured result once the run completes — the agent's `submit_result`
719
- * output. `undefined` when the run produced none (a free-text agent, or one that
720
- * finished without submitting); a free-text answer lives in `text`, never here.
721
- * So it is safe to treat a present `output` as the declared shape — but still
722
- * validate untrusted inner fields (the model authored the `submit_result` body,
723
- * so e.g. an array field may be missing/mistyped). */
724
- output?: TOutput;
725
- error?: string;
726
- }
727
- /**
728
- * Run a streaming agent declared in `package.json` lotics.agents and invoked by
729
- * alias. `run(input, { sessionId })` starts it; progress streams into `status` +
730
- * the ordered `parts` transcript (feed straight to `@lotics/ui` `AgentRun`). A
731
- * STRUCTURED agent's result lands in `output`; a FREE-TEXT agent's answer is the
732
- * transcript's prose (`text`). Read the session's history with `useAgentRuns`.
733
- *
734
- * ```tsx
735
- * const recognize = useAgentRun("recognize");
736
- * await recognize.run({ image_file_id }, { sessionId });
737
- * // <AgentRun parts={recognize.parts} state={recognize.status === "streaming" ? "streaming" : "done"} />
738
- * // then read recognize.output (structured) or recognize.text (free-text)
739
- * ```
740
- */
741
- export declare function useAgentRun<K extends keyof AppAgents & string>(alias: K): UseAgentRun<AppAgents[K], AgentOutputOf<K>>;
742
- export declare function useAgentRun(alias: string): UseAgentRun<Record<string, unknown>, unknown>;
743
- /** One persisted run in a session's history (the `/agent-runs` row shape). */
744
- export interface AgentRunRecord {
745
- id: string;
746
- agent_alias: string;
747
- session_id: string;
748
- status: string;
749
- input: Record<string, unknown> | null;
750
- output: unknown;
751
- error_message: string | null;
752
- started_at: string;
753
- completed_at: string | null;
754
- /** Single-run GET only, while `status` is "awaiting_input": the pending ask
755
- * derived server-side from the transcript — lets a reconnecting client
756
- * rebuild the question without the live stream. */
757
- pending_interactive?: {
758
- tool_call_id: string;
759
- tool_name: string;
760
- input: unknown;
761
- } | null;
762
- }
763
- interface AgentRunsState {
764
- runs: AgentRunRecord[];
765
- loading: boolean;
766
- error: string | null;
767
- refetch: () => void;
768
- }
769
- /**
770
- * The run history of a session, oldest-first — the persisted outputs the app
771
- * renders as a session log (NOT a chat: a flat list of past runs). Refetch
772
- * after a `run(...)` completes to pull in the new one.
773
- *
774
- * ```tsx
775
- * const { runs } = useAgentRuns(sessionId);
776
- * ```
777
- */
778
- export declare function useAgentRuns(sessionId: string, opts?: {
779
- enabled?: boolean;
780
- revalidateOnFocus?: boolean;
781
- }): AgentRunsState;