@colixsystems/widget-sdk 0.146.0 → 0.148.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/README.md +36 -1
- package/dist/contract.cjs +77 -1
- package/dist/contract.js +77 -1
- package/dist/hooks.js +190 -0
- package/dist/host.d.ts +15 -18
- package/dist/index.d.ts +54 -3
- package/dist/index.js +2 -0
- package/dist/index.native.js +2 -0
- package/dist/linter.cjs +20 -0
- package/dist/linter.js +20 -0
- package/dist/navigation.cjs +69 -56
- package/dist/navigation.js +69 -56
- package/dist/property-schema.js +6 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -47,6 +47,7 @@ The data layer lives in **four separate domain-client packages**, each instantia
|
|
|
47
47
|
| **DATASTORE** | `useDatastoreSchema(tableId)` | `{ schema, loading, error, refetch }` | `schema(tableId)` — `datastore.read:<table>` |
|
|
48
48
|
| **DATASTORE** | `useBoundColumns(tableId, shape, props)` | `{ columns, resolved, missing, loading, error }` | `schema(tableId)` (built on `useDatastoreSchema`) — `datastore.read:<table>`. Resolves author-bound column NAMES from `props` by exact name → case-insensitive name → first unclaimed column matching `shape[key].dataType`, so a column an author renamed after install still resolves instead of `record[props.titleField]` reading `undefined`. `columns` holds the resolved NAME (`record[columns.titleField]`); `resolved` holds the full `Column`; `missing` lists non-`optional` keys that never resolved. Falsy `tableId` collapses to `{ columns: {}, resolved: {}, missing: Object.keys(shape), loading: false, error: null }`. |
|
|
49
49
|
| **DATASTORE** | `useInterpretDraft(tableId)` | `{ interpret, interpreting, error, result, available }` | `interpret(tableId, body)` — `datastore.read:<table>`. Turns ONE sentence a user typed ("walk at 11 am tomorrow") into DRAFT column values so a form can prefill itself. IMPERATIVE: call `interpret(text, { fields?, timeZone? })` from an event handler, never on mount. It DRAFTS and writes nothing — show the values for review, then submit through `useDatastoreMutation().create`. Resolves to `{ values, unresolved }`; `values` is keyed by column NAME (the shape `create()` takes) and `unresolved` names the fields the sentence did not state. Only text / number / boolean / date / datetime / array columns are drafted — `FILE`, `RELATION`, `USER` and `USER_GROUP` carry ids and are never guessed. Fails closed to an empty draft. **Every call spends the workspace's AI credits** and is rate-limited per actor, so call it once per user action (never on mount or in a render loop); once the workspace runs out the call is refused with a generic 429 — an app user is deliberately **not** told the workspace's billing state, since they have never heard of an AI credit and cannot buy one. Never surface a raw error to the person filling the form: say drafting is unavailable and keep every field editable by hand. `available` is false where the host brokers no interpreter. |
|
|
50
|
+
| **AGENTS** | `useAgent(agentRef)` | `{ send, reset, messages, sending, error, available }` | `ctx.agents` — `ai.invoke:*`. Hold a conversation with one of the WORKSPACE's own AI agents (an author-written name + system prompt). IMPERATIVE: call `send(text)` from an event handler, never on mount — the first send opens the conversation, so a widget nobody types into costs nothing. `messages` is the running transcript, oldest first. You **cannot** set or read the system prompt, replay an edited transcript, or pick a model: the server assembles every turn from the stored prompt plus the stored history, and `content` is the only thing that reaches the wire. `reset()` drops the thread (a "new chat" affordance). **Every send spends the workspace's AI credits** and is rate-limited per caller AND per workspace. Rejects with `AgentError`; branch on `.code` — `AI_QUOTA_EXCEEDED` (429) is the workspace's credit ceiling and will NOT clear on a retry, unlike `RATE_LIMITED`, so never offer "try again" for it, and `AGENT_DISABLED` (409) means the author turned it off, so hide the affordance. Never surface a raw error or the workspace's billing state to an app user. `available` is false when no agent is bound or the host brokers none — render a visible unbound state, never a blank box. |
|
|
50
51
|
| **DATASTORE** | `useDatastoreMutation(table)` | `{ create, update, delete }` | `records(table).{ create, update (PATCH), delete }` — `datastore.write:*`. **To CLEAR a cell, pass it empty** — `update(id, { when: null })` (also `""` or `[]`) empties that cell and it reads back as `null` (`[]` on an array column). There is no separate clear call, so never write a sentinel such as `0` or a placeholder date to mean "no value". `false` and `0` are values, not clears, and a REQUIRED column cannot be cleared. |
|
|
51
52
|
| **DATASTORE** | `useDatastoreSubscription(table, handlers, options)` (optional: options) | `{ status }` — `"connecting" \| "live" \| "reconnecting" \| "fallback"` | `records(table).subscribe` — `datastore.read:<table>`. Live `onCreated` / `onUpdated` / `onDeleted` off the REQ-RT-07 socket; never throws, resolving to `{ status: "fallback" }` so the widget polls instead. A whole-table subscribe is gated on read-EVERY-row, because one envelope reaches every subscriber of the table — so for a table governed by per-record grants pass `options.scope`: `{ kind: "record", record_id }` for one row, or `{ kind: "parent", relation_column, record_id }` for the rows whose RELATION column points at that parent (the column must carry `inheritAcl`, else the subscribe reports `"fallback"`). Re-subscribes on the scope's VALUES, so a fresh object literal each render is fine. |
|
|
52
53
|
| **DATASTORE** | `useRecordPermissions(tableId, recordId)` | `{ permissions, loading, error, grant, revoke, update, refetch }` | `records(table).permissions(record).{ list, grant, update, revoke }` — `acl.write:records` (+ `can_grant` on the record) |
|
|
@@ -71,7 +72,41 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
71
72
|
|
|
72
73
|
## Status
|
|
73
74
|
|
|
74
|
-
`v0.
|
|
75
|
+
`v0.148.0` — pre-publish. The package surface (types, function names, export paths) is the v1 contract; runtime behaviour for some hooks is stubbed (each hook documents what's wired and what isn't). It is **not yet published to npm**.
|
|
76
|
+
|
|
77
|
+
### What's new in 0.148.0 (contract 1.116.0)
|
|
78
|
+
|
|
79
|
+
**New `useAgent(agentRef)` hook — a widget can talk to the WORKSPACE's own AI agent (sc-7991).** A new `agents` host client (`@colixsystems/agents-client`), a new `agentRef` propertySchema type and a new `ai.invoke:*` scope. Everything is additive; nothing existing changes shape.
|
|
80
|
+
|
|
81
|
+
A workspace **agent** is a name plus a system prompt its author wrote in the Studio. This hook lets any widget hold a conversation with one, without touching HTTP and without ever holding an API key.
|
|
82
|
+
|
|
83
|
+
```js
|
|
84
|
+
const { send, reset, messages, sending, error, available } = useAgent(props.agentId);
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**What a widget deliberately cannot do.** You cannot set the system prompt, send your own transcript, or pick a model, and no API surface returns the prompt. The server assembles every turn from the stored prompt plus the stored history, and `content` is the only field that reaches the wire — not because the rest is filtered out, but because nothing else is ever sent. That guarantee is about the WIRE, not about secrecy: a model can be talked into repeating its own prompt, so an agent's prompt is not a secret store — never put credentials, or data the app's users may not see, in one. `messages` is the transcript this widget has seen; the real one is the server's.
|
|
88
|
+
|
|
89
|
+
**Imperative, like `useInterpretDraft`.** It never fires on mount, and the FIRST `send` opens the conversation — so a widget an author places and nobody types into costs the workspace nothing.
|
|
90
|
+
|
|
91
|
+
**Bind it with an `agentRef` property.** Declare `{ type: "agentRef" }` in your `propertySchema` and the Studio renders an agent picker; the value is the agent id you pass to `useAgent`. It is a bare UUID like `pageRef` / `groupRef`, so Copy Workspace carries a bound widget over to the copied workspace's own agent.
|
|
92
|
+
|
|
93
|
+
**Every send spends the workspace's AI credits**, and is rate-limited per caller *and* per workspace. Branch on `AgentError.code`: `AI_QUOTA_EXCEEDED` (429) is the credit ceiling and will **not** clear on a retry — offering "try again" there is worse than useless — where `RATE_LIMITED` (429) will. `AGENT_DISABLED` (409) means the author switched the agent off, so hide the affordance rather than retrying. Never show an app user a raw error or the workspace's billing state: they have not heard of an AI credit and cannot buy one.
|
|
94
|
+
|
|
95
|
+
**`available: false` is a render state, not an error.** It means no agent is bound, or this host brokers no agents client (an unbound canvas preview). Render a visible unbound state — never a blank box.
|
|
96
|
+
|
|
97
|
+
The linter rejects `useAgent()` unless your manifest declares `ai.invoke:*` in `requestedScopes`.
|
|
98
|
+
|
|
99
|
+
### What's new in 0.147.0 (contract 1.115.0)
|
|
100
|
+
|
|
101
|
+
**BREAKING: the bottom navigation bar is themed by `footer` alone — `resolveFooterTokens` reads no `sidebar` (sc-8033).** Every colour fell back to the rail's, so restyling the sidebar restyled a page's bottom bar and the two could not be dressed apart. That is the defect sc-7951 set out to fix and only half did: it gave the strip a Design panel, but left the panel's "Active tab style" offering a **Follow the sidebar** option and every colour still inheriting. The resolver now reads `theme.footer` and nothing else, and the Studio panel offers the strip's **eight** settings with no inherit anywhere.
|
|
102
|
+
|
|
103
|
+
- **The overloaded `activeColor` is retired** for **`footer.tabActiveBackgroundColor`** and **`footer.tabActiveTextColor`**. One key meant the FILL under `"filled"` and the LABEL under `"accent"`, which is exactly why no author could set both. They are independent now, and each style's *defaults* are what it already rendered — `"filled"`: the brand primary behind a white label; `"accent"`: no surface, a brand-primary label and top edge. So all four colours stay meaningful under both styles.
|
|
104
|
+
- **`activeTint` is gone from `FooterTokens`.** The returned **`activeColor`** is the active tab's label, glyph and accent edge — one colour for all three, because a mark and its label disagreeing about which colour means "you are here" would be two marks.
|
|
105
|
+
- **`backgroundColor` and `textColor` are resolved strings**, not raw-or-null. Both hosts kept their own `#ffffff` / `#475569` defaults for them; a default that lives in each host is the drift §8 exists to prevent, and these had already diverged once. `borderColor` stays nullable — no line, no colour.
|
|
106
|
+
- **`borderWidth` admits `0` here alone**, where `resolveSidebarTokens` keeps sc-7965's 1–8 clamp. The strip's thickness is a Studio control offered down to zero, so a stored zero is an author saying "no line" rather than the junk that clamp refuses; it resolves into the existing no-divider state, so neither host needed new drawing code.
|
|
107
|
+
|
|
108
|
+
**No shipped app moved.** A backfill (`20270123000000_sc8033_footer_tokens_uncouple`) materialised into each workspace's `footer` block exactly what it had been inheriting, and split every stored `activeColor` onto the key that says which half it meant. Nothing is written where the old chain ended in a default the new one also ends in.
|
|
109
|
+
|
|
75
110
|
|
|
76
111
|
### What's new in 0.146.0 (contract 1.114.0)
|
|
77
112
|
|
package/dist/contract.cjs
CHANGED
|
@@ -1172,6 +1172,47 @@ const HOOKS = [
|
|
|
1172
1172
|
requiredContextSlice: ["datastore.interpret"],
|
|
1173
1173
|
scopes: ["datastore.read:<table>"],
|
|
1174
1174
|
},
|
|
1175
|
+
{
|
|
1176
|
+
name: "useAgent",
|
|
1177
|
+
signature: "useAgent(agentRef)",
|
|
1178
|
+
description:
|
|
1179
|
+
"sc-7991 — hold a conversation with one of the WORKSPACE's own AI " +
|
|
1180
|
+
"agents (an author-written name + system prompt). IMPERATIVE: call " +
|
|
1181
|
+
"send(text) from an event handler, never on mount — the first send " +
|
|
1182
|
+
"opens the conversation, so a widget nobody types into costs nothing. " +
|
|
1183
|
+
"`messages` is the running transcript, oldest first, each " +
|
|
1184
|
+
"{ id, role, content, created_at } with role 'user' | 'assistant'. " +
|
|
1185
|
+
"You CANNOT set the system prompt, replay an edited transcript, " +
|
|
1186
|
+
"or pick a model. No API returns the prompt — but that is a guarantee " +
|
|
1187
|
+
"about the WIRE, not secrecy: a model can be talked into repeating its " +
|
|
1188
|
+
"own prompt, so never put credentials or hidden data in one. " +
|
|
1189
|
+
"or pick a model: the server assembles every turn from the stored " +
|
|
1190
|
+
"prompt plus the stored history, and `content` is the only thing that " +
|
|
1191
|
+
"goes on the wire. `agentRef` is an agent id an author bound through an " +
|
|
1192
|
+
"`agentRef` property — when it is missing, or the host brokers no " +
|
|
1193
|
+
"agents client, `available` is false and send() rejects UNAVAILABLE; " +
|
|
1194
|
+
"render a visible unbound state, NEVER a blank box. reset() drops the " +
|
|
1195
|
+
"thread and clears the transcript (a 'new chat' affordance). " +
|
|
1196
|
+
"Every send spends the workspace's AI CREDITS and is rate-limited per " +
|
|
1197
|
+
"caller AND per workspace, so call it once per user action. " +
|
|
1198
|
+
"AgentError.code distinguishes the refusals that share a status: " +
|
|
1199
|
+
"AI_QUOTA_EXCEEDED (429) is the workspace's credit ceiling and will " +
|
|
1200
|
+
"NOT clear on a retry — never offer 'try again' for it — where " +
|
|
1201
|
+
"RATE_LIMITED (429) will; AGENT_DISABLED (409) means the author turned " +
|
|
1202
|
+
"it off, so hide the affordance rather than retrying. NEVER surface a " +
|
|
1203
|
+
"raw error or the workspace's billing state to an app user. " +
|
|
1204
|
+
"Reads ctx.agents.",
|
|
1205
|
+
returnShape: {
|
|
1206
|
+
send: "(text) => Promise<{ id, role, content, created_at } | null>",
|
|
1207
|
+
reset: "() => void",
|
|
1208
|
+
messages: "Array<{ id, conversation_id, role, content, created_at }>",
|
|
1209
|
+
sending: "boolean",
|
|
1210
|
+
error: "AgentError | null",
|
|
1211
|
+
available: "boolean // false when no agent is bound, or the host brokers none",
|
|
1212
|
+
},
|
|
1213
|
+
requiredContextSlice: ["agents.conversations"],
|
|
1214
|
+
scopes: ["ai.invoke:*"],
|
|
1215
|
+
},
|
|
1175
1216
|
{
|
|
1176
1217
|
name: "useDatastoreMutation",
|
|
1177
1218
|
signature: "useDatastoreMutation(tableId)",
|
|
@@ -2271,6 +2312,21 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
2271
2312
|
required: false,
|
|
2272
2313
|
fields: { params: "object", set: "function" },
|
|
2273
2314
|
},
|
|
2315
|
+
agents: {
|
|
2316
|
+
description:
|
|
2317
|
+
"sc-7991 — injected @colixsystems/agents-client instance. " +
|
|
2318
|
+
"{ conversations(agentId) -> { create(), list(query), remove(conversationId), " +
|
|
2319
|
+
"messages(conversationId) -> { list(query), send({ content }) } } }. " +
|
|
2320
|
+
"Backs useAgent(). The workspace's agent owns the system prompt and the " +
|
|
2321
|
+
"server owns the transcript, so `send` puts ONLY `content` on the wire — " +
|
|
2322
|
+
"a prompt, a history or a model id cannot be smuggled through. " +
|
|
2323
|
+
"A conversation belongs to the CALLER (the app user, or the studio user " +
|
|
2324
|
+
"previewing on the canvas); another user's id is a 404, never a 403. " +
|
|
2325
|
+
"Absent on a host that brokers no agents (an unbound preview), where " +
|
|
2326
|
+
"useAgent() degrades to available: false rather than throwing.",
|
|
2327
|
+
required: false,
|
|
2328
|
+
fields: { conversations: "function" },
|
|
2329
|
+
},
|
|
2274
2330
|
datastore: {
|
|
2275
2331
|
description:
|
|
2276
2332
|
"Injected @colixsystems/datastore-client instance. " +
|
|
@@ -4054,6 +4110,10 @@ const CONTRACT = deepFreeze({
|
|
|
4054
4110
|
// and the inherited colours are hex-guarded on the way out as well as in.
|
|
4055
4111
|
// Every colour still falls back to the sidebar's: an untouched workspace is
|
|
4056
4112
|
// unchanged.
|
|
4113
|
+
// 1.116.0: additive (sc-7991) — `useAgent(agentRef)` + the `agents` host
|
|
4114
|
+
// client + the `agentRef` propertySchema type + the `ai.invoke:*` scope. A
|
|
4115
|
+
// widget can hold a conversation with one of the WORKSPACE's own AI agents.
|
|
4116
|
+
// Nothing existing changes shape.
|
|
4057
4117
|
// 1.114.0: behaviour (sc-7965) — `resolveFooterTokens` / `resolveSidebarTokens`
|
|
4058
4118
|
// now return a `borderWidth` CLAMPED to 1-8 instead of any finite number.
|
|
4059
4119
|
// The bound lived only in the export (`coerceBorderWidth(...) ?? 1`), so a
|
|
@@ -4064,7 +4124,23 @@ const CONTRACT = deepFreeze({
|
|
|
4064
4124
|
// Zero clamps up for the same reason the export already rendered 1 there —
|
|
4065
4125
|
// the divider's COLOUR is its switch. Every legal 1-8 value is unchanged,
|
|
4066
4126
|
// and only a hand-edited or pre-coercer blob can hold anything else.
|
|
4067
|
-
|
|
4127
|
+
// 1.115.0: BREAKING on the pre-1.0 channel (sc-8033) — `resolveFooterTokens`
|
|
4128
|
+
// reads `theme.footer` and NOTHING else. Every colour used to fall back to
|
|
4129
|
+
// `theme.sidebar`, so restyling the rail restyled a page's bottom
|
|
4130
|
+
// navigation bar and the two could not be dressed apart — the defect
|
|
4131
|
+
// sc-7951 set out to fix and only half did. The wire block's overloaded
|
|
4132
|
+
// `activeColor` (the FILL under "filled", the LABEL under "accent") is
|
|
4133
|
+
// retired for `tabActiveBackgroundColor` + `tabActiveTextColor`, two
|
|
4134
|
+
// independent halves; `activeTint` folds into the returned `activeColor`,
|
|
4135
|
+
// which is now the active tab's label, glyph and accent edge. The returned
|
|
4136
|
+
// `backgroundColor` and `textColor` are resolved strings rather than
|
|
4137
|
+
// raw-or-null, so neither host keeps a default of its own. `borderWidth`
|
|
4138
|
+
// admits 0 HERE alone — the strip's thickness is a Studio control offered
|
|
4139
|
+
// down to zero, and zero resolves into the existing no-divider state —
|
|
4140
|
+
// where the rail's and the header's keep sc-7965's 1-8 clamp. A backfill
|
|
4141
|
+
// (`20270123000000_sc8033_footer_tokens_uncouple`) materialised what each
|
|
4142
|
+
// workspace was inheriting, so no shipped app moved.
|
|
4143
|
+
version: "1.116.0",
|
|
4068
4144
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
4069
4145
|
hooks: HOOKS,
|
|
4070
4146
|
primitives: PRIMITIVES,
|
package/dist/contract.js
CHANGED
|
@@ -1172,6 +1172,47 @@ const HOOKS = [
|
|
|
1172
1172
|
requiredContextSlice: ["datastore.interpret"],
|
|
1173
1173
|
scopes: ["datastore.read:<table>"],
|
|
1174
1174
|
},
|
|
1175
|
+
{
|
|
1176
|
+
name: "useAgent",
|
|
1177
|
+
signature: "useAgent(agentRef)",
|
|
1178
|
+
description:
|
|
1179
|
+
"sc-7991 — hold a conversation with one of the WORKSPACE's own AI " +
|
|
1180
|
+
"agents (an author-written name + system prompt). IMPERATIVE: call " +
|
|
1181
|
+
"send(text) from an event handler, never on mount — the first send " +
|
|
1182
|
+
"opens the conversation, so a widget nobody types into costs nothing. " +
|
|
1183
|
+
"`messages` is the running transcript, oldest first, each " +
|
|
1184
|
+
"{ id, role, content, created_at } with role 'user' | 'assistant'. " +
|
|
1185
|
+
"You CANNOT set the system prompt, replay an edited transcript, " +
|
|
1186
|
+
"or pick a model. No API returns the prompt — but that is a guarantee " +
|
|
1187
|
+
"about the WIRE, not secrecy: a model can be talked into repeating its " +
|
|
1188
|
+
"own prompt, so never put credentials or hidden data in one. " +
|
|
1189
|
+
"or pick a model: the server assembles every turn from the stored " +
|
|
1190
|
+
"prompt plus the stored history, and `content` is the only thing that " +
|
|
1191
|
+
"goes on the wire. `agentRef` is an agent id an author bound through an " +
|
|
1192
|
+
"`agentRef` property — when it is missing, or the host brokers no " +
|
|
1193
|
+
"agents client, `available` is false and send() rejects UNAVAILABLE; " +
|
|
1194
|
+
"render a visible unbound state, NEVER a blank box. reset() drops the " +
|
|
1195
|
+
"thread and clears the transcript (a 'new chat' affordance). " +
|
|
1196
|
+
"Every send spends the workspace's AI CREDITS and is rate-limited per " +
|
|
1197
|
+
"caller AND per workspace, so call it once per user action. " +
|
|
1198
|
+
"AgentError.code distinguishes the refusals that share a status: " +
|
|
1199
|
+
"AI_QUOTA_EXCEEDED (429) is the workspace's credit ceiling and will " +
|
|
1200
|
+
"NOT clear on a retry — never offer 'try again' for it — where " +
|
|
1201
|
+
"RATE_LIMITED (429) will; AGENT_DISABLED (409) means the author turned " +
|
|
1202
|
+
"it off, so hide the affordance rather than retrying. NEVER surface a " +
|
|
1203
|
+
"raw error or the workspace's billing state to an app user. " +
|
|
1204
|
+
"Reads ctx.agents.",
|
|
1205
|
+
returnShape: {
|
|
1206
|
+
send: "(text) => Promise<{ id, role, content, created_at } | null>",
|
|
1207
|
+
reset: "() => void",
|
|
1208
|
+
messages: "Array<{ id, conversation_id, role, content, created_at }>",
|
|
1209
|
+
sending: "boolean",
|
|
1210
|
+
error: "AgentError | null",
|
|
1211
|
+
available: "boolean // false when no agent is bound, or the host brokers none",
|
|
1212
|
+
},
|
|
1213
|
+
requiredContextSlice: ["agents.conversations"],
|
|
1214
|
+
scopes: ["ai.invoke:*"],
|
|
1215
|
+
},
|
|
1175
1216
|
{
|
|
1176
1217
|
name: "useDatastoreMutation",
|
|
1177
1218
|
signature: "useDatastoreMutation(tableId)",
|
|
@@ -2267,6 +2308,21 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
2267
2308
|
required: false,
|
|
2268
2309
|
fields: { params: "object", set: "function" },
|
|
2269
2310
|
},
|
|
2311
|
+
agents: {
|
|
2312
|
+
description:
|
|
2313
|
+
"sc-7991 — injected @colixsystems/agents-client instance. " +
|
|
2314
|
+
"{ conversations(agentId) -> { create(), list(query), remove(conversationId), " +
|
|
2315
|
+
"messages(conversationId) -> { list(query), send({ content }) } } }. " +
|
|
2316
|
+
"Backs useAgent(). The workspace's agent owns the system prompt and the " +
|
|
2317
|
+
"server owns the transcript, so `send` puts ONLY `content` on the wire — " +
|
|
2318
|
+
"a prompt, a history or a model id cannot be smuggled through. " +
|
|
2319
|
+
"A conversation belongs to the CALLER (the app user, or the studio user " +
|
|
2320
|
+
"previewing on the canvas); another user's id is a 404, never a 403. " +
|
|
2321
|
+
"Absent on a host that brokers no agents (an unbound preview), where " +
|
|
2322
|
+
"useAgent() degrades to available: false rather than throwing.",
|
|
2323
|
+
required: false,
|
|
2324
|
+
fields: { conversations: "function" },
|
|
2325
|
+
},
|
|
2270
2326
|
datastore: {
|
|
2271
2327
|
description:
|
|
2272
2328
|
"Injected @colixsystems/datastore-client instance. " +
|
|
@@ -4050,6 +4106,10 @@ const CONTRACT = deepFreeze({
|
|
|
4050
4106
|
// and the inherited colours are hex-guarded on the way out as well as in.
|
|
4051
4107
|
// Every colour still falls back to the sidebar's: an untouched workspace is
|
|
4052
4108
|
// unchanged.
|
|
4109
|
+
// 1.116.0: additive (sc-7991) — `useAgent(agentRef)` + the `agents` host
|
|
4110
|
+
// client + the `agentRef` propertySchema type + the `ai.invoke:*` scope. A
|
|
4111
|
+
// widget can hold a conversation with one of the WORKSPACE's own AI agents.
|
|
4112
|
+
// Nothing existing changes shape.
|
|
4053
4113
|
// 1.114.0: behaviour (sc-7965) — `resolveFooterTokens` / `resolveSidebarTokens`
|
|
4054
4114
|
// now return a `borderWidth` CLAMPED to 1-8 instead of any finite number.
|
|
4055
4115
|
// The bound lived only in the export (`coerceBorderWidth(...) ?? 1`), so a
|
|
@@ -4060,7 +4120,23 @@ const CONTRACT = deepFreeze({
|
|
|
4060
4120
|
// Zero clamps up for the same reason the export already rendered 1 there —
|
|
4061
4121
|
// the divider's COLOUR is its switch. Every legal 1-8 value is unchanged,
|
|
4062
4122
|
// and only a hand-edited or pre-coercer blob can hold anything else.
|
|
4063
|
-
|
|
4123
|
+
// 1.115.0: BREAKING on the pre-1.0 channel (sc-8033) — `resolveFooterTokens`
|
|
4124
|
+
// reads `theme.footer` and NOTHING else. Every colour used to fall back to
|
|
4125
|
+
// `theme.sidebar`, so restyling the rail restyled a page's bottom
|
|
4126
|
+
// navigation bar and the two could not be dressed apart — the defect
|
|
4127
|
+
// sc-7951 set out to fix and only half did. The wire block's overloaded
|
|
4128
|
+
// `activeColor` (the FILL under "filled", the LABEL under "accent") is
|
|
4129
|
+
// retired for `tabActiveBackgroundColor` + `tabActiveTextColor`, two
|
|
4130
|
+
// independent halves; `activeTint` folds into the returned `activeColor`,
|
|
4131
|
+
// which is now the active tab's label, glyph and accent edge. The returned
|
|
4132
|
+
// `backgroundColor` and `textColor` are resolved strings rather than
|
|
4133
|
+
// raw-or-null, so neither host keeps a default of its own. `borderWidth`
|
|
4134
|
+
// admits 0 HERE alone — the strip's thickness is a Studio control offered
|
|
4135
|
+
// down to zero, and zero resolves into the existing no-divider state —
|
|
4136
|
+
// where the rail's and the header's keep sc-7965's 1-8 clamp. A backfill
|
|
4137
|
+
// (`20270123000000_sc8033_footer_tokens_uncouple`) materialised what each
|
|
4138
|
+
// workspace was inheriting, so no shipped app moved.
|
|
4139
|
+
version: "1.116.0",
|
|
4064
4140
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
4065
4141
|
hooks: HOOKS,
|
|
4066
4142
|
primitives: PRIMITIVES,
|
package/dist/hooks.js
CHANGED
|
@@ -2694,6 +2694,196 @@ export function useInterpretDraft(tableId) {
|
|
|
2694
2694
|
return { interpret, interpreting, error, result, available };
|
|
2695
2695
|
}
|
|
2696
2696
|
|
|
2697
|
+
/**
|
|
2698
|
+
* sc-7991 — structured error thrown by `useAgent().send`. Carries a stable
|
|
2699
|
+
* `code` so widgets branch without parsing message strings; mirrors the shape
|
|
2700
|
+
* of `TranslateError` / `NotificationError`.
|
|
2701
|
+
*
|
|
2702
|
+
* `code` is one of:
|
|
2703
|
+
* - "UNAVAILABLE" — the host brokers no agents client, or no agent
|
|
2704
|
+
* is bound (the widget should render its unbound
|
|
2705
|
+
* state, not an error)
|
|
2706
|
+
* - "AGENT_NOT_FOUND" — 404 (the bound agent was deleted)
|
|
2707
|
+
* - "AGENT_DISABLED" — 409 (the author turned it off; it still exists)
|
|
2708
|
+
* - "CONVERSATION_NOT_FOUND"— 404 (a stale thread; `reset()` starts a new one)
|
|
2709
|
+
* - "INVALID_MESSAGE" — 400 (empty, or past the length cap)
|
|
2710
|
+
* - "AI_QUOTA_EXCEEDED" — 429 (the WORKSPACE is out of AI credit; this
|
|
2711
|
+
* does NOT clear by retrying — say so)
|
|
2712
|
+
* - "RATE_LIMITED" — 429 (too many turns — retry shortly)
|
|
2713
|
+
* - "AI_UNAVAILABLE" — 503 (no AI provider configured)
|
|
2714
|
+
* - "AI_GENERATION_FAILED" — 502 (the provider did not answer)
|
|
2715
|
+
* - "AUTH_REQUIRED" — 401 (no signed-in app user)
|
|
2716
|
+
* - "INTERNAL" — anything else (network, 5xx)
|
|
2717
|
+
*/
|
|
2718
|
+
export class AgentError extends Error {
|
|
2719
|
+
constructor(code, message, opts) {
|
|
2720
|
+
super(message);
|
|
2721
|
+
this.name = "AgentError";
|
|
2722
|
+
this.code = code;
|
|
2723
|
+
if (opts && opts.cause) this.cause = opts.cause;
|
|
2724
|
+
}
|
|
2725
|
+
}
|
|
2726
|
+
|
|
2727
|
+
function toAgentError(err) {
|
|
2728
|
+
if (err instanceof AgentError) return err;
|
|
2729
|
+
const status =
|
|
2730
|
+
err && err.response && typeof err.response.status === "number"
|
|
2731
|
+
? err.response.status
|
|
2732
|
+
: err && typeof err.status === "number"
|
|
2733
|
+
? err.status
|
|
2734
|
+
: null;
|
|
2735
|
+
const bodyCode =
|
|
2736
|
+
(err && err.response && err.response.data && err.response.data.code) ||
|
|
2737
|
+
(err && typeof err.code === "string" ? err.code : null);
|
|
2738
|
+
const bodyMessage =
|
|
2739
|
+
(err && err.response && err.response.data && err.response.data.message) ||
|
|
2740
|
+
(err && err.message) ||
|
|
2741
|
+
"The agent could not answer";
|
|
2742
|
+
let code = "INTERNAL";
|
|
2743
|
+
if (typeof bodyCode === "string" && bodyCode) code = bodyCode;
|
|
2744
|
+
else if (status === 401) code = "AUTH_REQUIRED";
|
|
2745
|
+
else if (status === 429) code = "RATE_LIMITED";
|
|
2746
|
+
else if (status === 400) code = "INVALID_MESSAGE";
|
|
2747
|
+
else if (status === 409) code = "AGENT_DISABLED";
|
|
2748
|
+
else if (status === 404) code = "AGENT_NOT_FOUND";
|
|
2749
|
+
else if (status === 502) code = "AI_GENERATION_FAILED";
|
|
2750
|
+
else if (status === 503) code = "AI_UNAVAILABLE";
|
|
2751
|
+
return new AgentError(code, bodyMessage, { cause: err });
|
|
2752
|
+
}
|
|
2753
|
+
|
|
2754
|
+
/**
|
|
2755
|
+
* sc-7991 — hold a conversation with a workspace-authored AI agent.
|
|
2756
|
+
* Returns `{ send, reset, messages, sending, error, available }`.
|
|
2757
|
+
*
|
|
2758
|
+
* send(text) → Promise<{ role, content }> the agent's reply
|
|
2759
|
+
*
|
|
2760
|
+
* The hook is IMPERATIVE — it NEVER fires on mount; the widget calls `send`
|
|
2761
|
+
* from an event handler. The first `send` opens the conversation, so an author
|
|
2762
|
+
* who places the widget and never types costs the workspace nothing.
|
|
2763
|
+
*
|
|
2764
|
+
* `messages` is the running transcript this widget has seen, oldest first,
|
|
2765
|
+
* each `{ id, role, content, created_at }` with `role` of "user" | "assistant".
|
|
2766
|
+
* The SERVER owns the real transcript and the system prompt: the widget sends
|
|
2767
|
+
* one message and can neither read nor set the prompt.
|
|
2768
|
+
*
|
|
2769
|
+
* `agentRef` is the agent id an author bound through an `agentRef` property.
|
|
2770
|
+
* When it is absent, or the host brokers no agents client (an unbound canvas
|
|
2771
|
+
* preview), `available` is false and `send` rejects UNAVAILABLE rather than
|
|
2772
|
+
* throwing at render — the widget must render a visible unbound state, never a
|
|
2773
|
+
* blank box (CLAUDE.md §8).
|
|
2774
|
+
*
|
|
2775
|
+
* Routes through the injected `@colixsystems/agents-client` at `ctx.agents`,
|
|
2776
|
+
* so the Player and the Expo export resolve against the identical client.
|
|
2777
|
+
*/
|
|
2778
|
+
export function useAgent(agentRef) {
|
|
2779
|
+
const ctx = useWidgetContextOrThrow("useAgent");
|
|
2780
|
+
const available =
|
|
2781
|
+
Boolean(agentRef) &&
|
|
2782
|
+
Boolean(ctx.agents) &&
|
|
2783
|
+
typeof ctx.agents.conversations === "function";
|
|
2784
|
+
|
|
2785
|
+
const clientRef = useRef(available ? ctx.agents : null);
|
|
2786
|
+
clientRef.current = available ? ctx.agents : null;
|
|
2787
|
+
const agentIdRef = useRef(agentRef);
|
|
2788
|
+
agentIdRef.current = agentRef;
|
|
2789
|
+
// The open thread, held as the in-flight PROMISE rather than the id.
|
|
2790
|
+
//
|
|
2791
|
+
// A ref alone only fixes the SEQUENTIAL case. `if (!id) { id = await create() }`
|
|
2792
|
+
// reads the guard before the await and writes it after, so two overlapping
|
|
2793
|
+
// sends — a double-tap, or a widget sending on each keystroke — both see null,
|
|
2794
|
+
// both open a conversation, and both spend a paid turn. Latching the promise
|
|
2795
|
+
// makes the second caller await the FIRST create instead of starting another.
|
|
2796
|
+
const conversationRef = useRef(null);
|
|
2797
|
+
|
|
2798
|
+
const [messages, setMessages] = useState([]);
|
|
2799
|
+
const [sending, setSending] = useState(false);
|
|
2800
|
+
const [error, setError] = useState(null);
|
|
2801
|
+
|
|
2802
|
+
// Set during render when the binding changes; consumed below. A setState in
|
|
2803
|
+
// the render body would be a cascading re-render, so the flag is applied to
|
|
2804
|
+
// the value this render returns instead.
|
|
2805
|
+
const pendingResetRef = useRef(false);
|
|
2806
|
+
|
|
2807
|
+
// A changed binding is a different agent, so neither the open thread nor the
|
|
2808
|
+
// transcript carries — leaving the messages would render agent A's
|
|
2809
|
+
// conversation underneath agent B.
|
|
2810
|
+
const boundRef = useRef(agentRef);
|
|
2811
|
+
if (boundRef.current !== agentRef) {
|
|
2812
|
+
boundRef.current = agentRef;
|
|
2813
|
+
conversationRef.current = null;
|
|
2814
|
+
pendingResetRef.current = true;
|
|
2815
|
+
}
|
|
2816
|
+
|
|
2817
|
+
const reset = useCallback(() => {
|
|
2818
|
+
conversationRef.current = null;
|
|
2819
|
+
setMessages([]);
|
|
2820
|
+
setError(null);
|
|
2821
|
+
}, []);
|
|
2822
|
+
|
|
2823
|
+
const send = useCallback(async (text) => {
|
|
2824
|
+
const client = clientRef.current;
|
|
2825
|
+
const agentId = agentIdRef.current;
|
|
2826
|
+
if (!client || !agentId) {
|
|
2827
|
+
throw new AgentError("UNAVAILABLE", "No agent is available for this host");
|
|
2828
|
+
}
|
|
2829
|
+
const content = typeof text === "string" ? text.trim() : "";
|
|
2830
|
+
if (!content) {
|
|
2831
|
+
throw new AgentError("INVALID_MESSAGE", "A message is required");
|
|
2832
|
+
}
|
|
2833
|
+
|
|
2834
|
+
setSending(true);
|
|
2835
|
+
setError(null);
|
|
2836
|
+
try {
|
|
2837
|
+
const thread = client.conversations(agentId);
|
|
2838
|
+
if (!conversationRef.current) {
|
|
2839
|
+
// Assigned BEFORE the first await, so a concurrent send joins this one.
|
|
2840
|
+
conversationRef.current = thread.create().then((created) => {
|
|
2841
|
+
const id = created && created.id ? created.id : null;
|
|
2842
|
+
if (!id) {
|
|
2843
|
+
throw new AgentError("INTERNAL", "The conversation could not be opened");
|
|
2844
|
+
}
|
|
2845
|
+
return id;
|
|
2846
|
+
});
|
|
2847
|
+
}
|
|
2848
|
+
let conversationId;
|
|
2849
|
+
try {
|
|
2850
|
+
conversationId = await conversationRef.current;
|
|
2851
|
+
} catch (err) {
|
|
2852
|
+
// A failed open must not wedge the widget on a rejected promise.
|
|
2853
|
+
conversationRef.current = null;
|
|
2854
|
+
throw err;
|
|
2855
|
+
}
|
|
2856
|
+
const turn = await thread.messages(conversationId).send({ content });
|
|
2857
|
+
const userMessage = turn && turn.user_message ? turn.user_message : null;
|
|
2858
|
+
const reply = turn && turn.reply ? turn.reply : null;
|
|
2859
|
+
setMessages((prior) =>
|
|
2860
|
+
prior.concat(userMessage ? [userMessage] : [], reply ? [reply] : []),
|
|
2861
|
+
);
|
|
2862
|
+
setSending(false);
|
|
2863
|
+
return reply;
|
|
2864
|
+
} catch (err) {
|
|
2865
|
+
const e = toAgentError(err);
|
|
2866
|
+
// A stale thread is recoverable on the next send rather than permanently
|
|
2867
|
+
// broken: drop it so the widget opens a fresh one.
|
|
2868
|
+
if (e.code === "CONVERSATION_NOT_FOUND") conversationRef.current = null;
|
|
2869
|
+
setError(e);
|
|
2870
|
+
setSending(false);
|
|
2871
|
+
throw e;
|
|
2872
|
+
}
|
|
2873
|
+
}, []);
|
|
2874
|
+
|
|
2875
|
+
// The binding changed this render: hand back the empty transcript now rather
|
|
2876
|
+
// than one frame of the previous agent's conversation.
|
|
2877
|
+
let visibleMessages = messages;
|
|
2878
|
+
if (pendingResetRef.current) {
|
|
2879
|
+
pendingResetRef.current = false;
|
|
2880
|
+
visibleMessages = [];
|
|
2881
|
+
if (messages.length > 0) setMessages([]);
|
|
2882
|
+
}
|
|
2883
|
+
|
|
2884
|
+
return { send, reset, messages: visibleMessages, sending, error, available };
|
|
2885
|
+
}
|
|
2886
|
+
|
|
2697
2887
|
/**
|
|
2698
2888
|
* Datastore mutation hook. Returns { create, update, delete }, each method
|
|
2699
2889
|
* returning a Promise. Routes through the injected
|
package/dist/host.d.ts
CHANGED
|
@@ -173,35 +173,32 @@ export function normaliseNavigation(navigation: unknown): ResolvedNavigation;
|
|
|
173
173
|
*/
|
|
174
174
|
export function menuItemCap(menuType: string): number | null;
|
|
175
175
|
|
|
176
|
-
/** The app's bottom edge — a page's own bottom navigation bar.
|
|
177
|
-
*
|
|
178
|
-
* divider is drawn at all. */
|
|
176
|
+
/** The app's bottom edge — a page's own bottom navigation bar. Every colour is
|
|
177
|
+
* resolved, so no host keeps a default of its own; a `null` `borderColor`
|
|
178
|
+
* means no divider is drawn at all. */
|
|
179
179
|
export interface FooterTokens {
|
|
180
|
-
backgroundColor: string
|
|
181
|
-
textColor: string
|
|
182
|
-
/**
|
|
183
|
-
*
|
|
184
|
-
*
|
|
180
|
+
backgroundColor: string;
|
|
181
|
+
textColor: string;
|
|
182
|
+
/** The active tab's label, its glyph, and — under "accent" — the edge that
|
|
183
|
+
* marks it. One colour for all three: a mark and its label disagreeing about
|
|
184
|
+
* which colour means "you are here" would be two marks, not one. */
|
|
185
185
|
activeColor: string;
|
|
186
|
+
/** `null` when no divider is drawn, which a zero `borderWidth` also means. */
|
|
186
187
|
borderColor: string | null;
|
|
187
188
|
borderWidth: number | null;
|
|
188
189
|
activeStyle: "filled" | "accent";
|
|
189
190
|
/** Whether a tab's glyph sits beside its label or above it (sc-7951). */
|
|
190
191
|
tabLayout: "inline" | "stacked";
|
|
191
|
-
/** The active tab's surface, or `null` for none
|
|
192
|
-
*
|
|
192
|
+
/** The active tab's surface, or `null` for none — which is what "accent"
|
|
193
|
+
* resolves to unless the author names one. */
|
|
193
194
|
activeSurface: string | null;
|
|
194
|
-
/** The active tab's label and glyph. White only under an unnamed "filled"
|
|
195
|
-
* surface, which is the pairing both hosts hard-coded. */
|
|
196
|
-
activeTint: string;
|
|
197
195
|
}
|
|
198
196
|
|
|
199
197
|
/**
|
|
200
|
-
* Resolves the footer strip's tokens from a whole `theme_config`.
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
* from nothing — a vertical rail item has neither.
|
|
198
|
+
* Resolves the footer strip's tokens from a whole `theme_config`. It reads
|
|
199
|
+
* `theme.footer` and NOTHING else (sc-8033): the colours used to fall back to
|
|
200
|
+
* the sidebar's, so restyling the rail restyled the strip and the two could not
|
|
201
|
+
* be dressed apart.
|
|
205
202
|
*/
|
|
206
203
|
export function resolveFooterTokens(theme: unknown): FooterTokens;
|
|
207
204
|
|
package/dist/index.d.ts
CHANGED
|
@@ -663,6 +663,8 @@ export interface WidgetContext<TProps = unknown> {
|
|
|
663
663
|
notifications: NotificationsClient;
|
|
664
664
|
/** Injected @colixsystems/identification-client; backs useIdentification. */
|
|
665
665
|
identification: IdentificationClient;
|
|
666
|
+
/** sc-7991 — injected @colixsystems/agents-client backing useAgent(). */
|
|
667
|
+
agents?: { conversations(agentId: string): unknown };
|
|
666
668
|
/** Host child-node renderer; backs WidgetTree / useChildRenderer. */
|
|
667
669
|
renderer: { renderNode(node: unknown): unknown };
|
|
668
670
|
events: { emit(eventName: string, payload?: unknown): void };
|
|
@@ -1204,6 +1206,36 @@ export function useInterpretDraft(
|
|
|
1204
1206
|
tableId: string | null | undefined,
|
|
1205
1207
|
): InterpretDraftApi;
|
|
1206
1208
|
|
|
1209
|
+
export interface AgentMessage {
|
|
1210
|
+
id: string;
|
|
1211
|
+
conversation_id: string;
|
|
1212
|
+
/** The system prompt is NOT part of the transcript — it lives on the agent. */
|
|
1213
|
+
role: "user" | "assistant";
|
|
1214
|
+
content: string;
|
|
1215
|
+
created_at: string;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
export interface AgentApi {
|
|
1219
|
+
/** Imperative — call from an event handler. The first send opens the thread. */
|
|
1220
|
+
send(text: string): Promise<AgentMessage | null>;
|
|
1221
|
+
/** Drop the open thread and clear the transcript this widget has seen. */
|
|
1222
|
+
reset(): void;
|
|
1223
|
+
/** Oldest first. The SERVER owns the real transcript. */
|
|
1224
|
+
messages: AgentMessage[];
|
|
1225
|
+
sending: boolean;
|
|
1226
|
+
error: AgentError | null;
|
|
1227
|
+
/** False when no agent is bound, or the host brokers no agents client. */
|
|
1228
|
+
available: boolean;
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
/**
|
|
1232
|
+
* sc-7991 — hold a conversation with a workspace-authored AI agent.
|
|
1233
|
+
* IMPERATIVE: never fires on mount, so an unused widget costs nothing. The
|
|
1234
|
+
* widget sends ONE message; the system prompt and the real transcript stay
|
|
1235
|
+
* server-side and are never readable or settable from here.
|
|
1236
|
+
*/
|
|
1237
|
+
export function useAgent(agentRef: string | null | undefined): AgentApi;
|
|
1238
|
+
|
|
1207
1239
|
// REQ-RT-07 realtime subscription transport state.
|
|
1208
1240
|
export type DatastoreSubscriptionStatus =
|
|
1209
1241
|
| "connecting"
|
|
@@ -1926,10 +1958,29 @@ export class DirectoryError extends Error {
|
|
|
1926
1958
|
}
|
|
1927
1959
|
|
|
1928
1960
|
/**
|
|
1929
|
-
* sc-
|
|
1930
|
-
*
|
|
1931
|
-
*
|
|
1961
|
+
* sc-7991 — structured error thrown by `useAgent().send`. Branch on `code`:
|
|
1962
|
+
* `AI_QUOTA_EXCEEDED` is the workspace's credit ceiling and will NOT clear on
|
|
1963
|
+
* a retry, unlike `RATE_LIMITED`; `AGENT_DISABLED` means the author switched
|
|
1964
|
+
* the agent off; `AI_AGENTS_DISABLED` means the feature is not released for
|
|
1965
|
+
* this workspace. Never show an app user a raw code or the billing state.
|
|
1932
1966
|
*/
|
|
1967
|
+
export class AgentError extends Error {
|
|
1968
|
+
code:
|
|
1969
|
+
| "UNAVAILABLE"
|
|
1970
|
+
| "AGENT_NOT_FOUND"
|
|
1971
|
+
| "AGENT_DISABLED"
|
|
1972
|
+
| "CONVERSATION_NOT_FOUND"
|
|
1973
|
+
| "INVALID_MESSAGE"
|
|
1974
|
+
| "AI_QUOTA_EXCEEDED"
|
|
1975
|
+
| "RATE_LIMITED"
|
|
1976
|
+
| "AI_UNAVAILABLE"
|
|
1977
|
+
| "AI_GENERATION_FAILED"
|
|
1978
|
+
| "AUTH_REQUIRED"
|
|
1979
|
+
| "INTERNAL"
|
|
1980
|
+
| (string & {});
|
|
1981
|
+
constructor(code: string, message: string, opts?: { cause?: unknown });
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1933
1984
|
export class TranslateError extends Error {
|
|
1934
1985
|
code:
|
|
1935
1986
|
| "UNSUPPORTED"
|
package/dist/index.js
CHANGED
|
@@ -28,6 +28,7 @@ export {
|
|
|
28
28
|
NotificationError,
|
|
29
29
|
DirectoryError,
|
|
30
30
|
PermissionError,
|
|
31
|
+
AgentError,
|
|
31
32
|
useDatastoreQuery,
|
|
32
33
|
useDatastoreRecord,
|
|
33
34
|
useDatastoreSchema,
|
|
@@ -35,6 +36,7 @@ export {
|
|
|
35
36
|
useStableQuery,
|
|
36
37
|
useCanWrite,
|
|
37
38
|
useInterpretDraft,
|
|
39
|
+
useAgent,
|
|
38
40
|
useAsset,
|
|
39
41
|
useAssetsByTag,
|
|
40
42
|
useFilestoreFiles,
|
package/dist/index.native.js
CHANGED
|
@@ -28,6 +28,7 @@ export {
|
|
|
28
28
|
NotificationError,
|
|
29
29
|
DirectoryError,
|
|
30
30
|
PermissionError,
|
|
31
|
+
AgentError,
|
|
31
32
|
useDatastoreQuery,
|
|
32
33
|
useDatastoreRecord,
|
|
33
34
|
useDatastoreSchema,
|
|
@@ -35,6 +36,7 @@ export {
|
|
|
35
36
|
useStableQuery,
|
|
36
37
|
useCanWrite,
|
|
37
38
|
useInterpretDraft,
|
|
39
|
+
useAgent,
|
|
38
40
|
useAsset,
|
|
39
41
|
useAssetsByTag,
|
|
40
42
|
useFilestoreFiles,
|
package/dist/linter.cjs
CHANGED
|
@@ -565,6 +565,26 @@ function _scopeRules(source, manifest) {
|
|
|
565
565
|
});
|
|
566
566
|
}
|
|
567
567
|
}
|
|
568
|
+
|
|
569
|
+
// sc-7991 — every send spends the workspace's AI credits, so the manifest has
|
|
570
|
+
// to declare the capability. This gate is ADVISORY, not an authorization
|
|
571
|
+
// boundary: unlike users/groups (minted into X-Widget-Scopes and checked
|
|
572
|
+
// server-side), nothing re-checks ai.invoke at call time, so a widget that
|
|
573
|
+
// evades this regex still reaches the endpoint. What actually bounds abuse is
|
|
574
|
+
// the app-user JWT, the per-actor and per-tenant limiters and the AI quota
|
|
575
|
+
// gate. The scope's real job is DISCLOSURE — it is what the marketplace
|
|
576
|
+
// install consent screen shows.
|
|
577
|
+
if (/\buseAgent\s*\(/.test(source)) {
|
|
578
|
+
const invokes = declared.has("ai.invoke:*") || declared.has("ai.invoke");
|
|
579
|
+
if (!invokes) {
|
|
580
|
+
findings.push({
|
|
581
|
+
rule: "scope-required-for-useAgent",
|
|
582
|
+
label: "useAgent() requires `ai.invoke:*` in manifest.requestedScopes",
|
|
583
|
+
line: 0,
|
|
584
|
+
snippet: "",
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
}
|
|
568
588
|
const lines = source.split(/\r?\n/);
|
|
569
589
|
let firedUsersWrite = false;
|
|
570
590
|
let firedUsersDelete = false;
|
package/dist/linter.js
CHANGED
|
@@ -528,6 +528,26 @@ function _scopeRules(source, manifest) {
|
|
|
528
528
|
});
|
|
529
529
|
}
|
|
530
530
|
}
|
|
531
|
+
|
|
532
|
+
// sc-7991 — every send spends the workspace's AI credits, so the manifest has
|
|
533
|
+
// to declare the capability. This gate is ADVISORY, not an authorization
|
|
534
|
+
// boundary: unlike users/groups (minted into X-Widget-Scopes and checked
|
|
535
|
+
// server-side), nothing re-checks ai.invoke at call time, so a widget that
|
|
536
|
+
// evades this regex still reaches the endpoint. What actually bounds abuse is
|
|
537
|
+
// the app-user JWT, the per-actor and per-tenant limiters and the AI quota
|
|
538
|
+
// gate. The scope's real job is DISCLOSURE — it is what the marketplace
|
|
539
|
+
// install consent screen shows.
|
|
540
|
+
if (/\buseAgent\s*\(/.test(source)) {
|
|
541
|
+
const invokes = declared.has("ai.invoke:*") || declared.has("ai.invoke");
|
|
542
|
+
if (!invokes) {
|
|
543
|
+
findings.push({
|
|
544
|
+
rule: "scope-required-for-useAgent",
|
|
545
|
+
label: "useAgent() requires `ai.invoke:*` in manifest.requestedScopes",
|
|
546
|
+
line: 0,
|
|
547
|
+
snippet: "",
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
}
|
|
531
551
|
const lines = source.split(/\r?\n/);
|
|
532
552
|
let firedUsersWrite = false;
|
|
533
553
|
let firedUsersDelete = false;
|
package/dist/navigation.cjs
CHANGED
|
@@ -71,78 +71,76 @@ function menuItemCap(menuType) {
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
// REQ-NAV-LOCAL: the app's BOTTOM EDGE — the tokens a page's own bottom
|
|
74
|
-
// navigation bar is drawn from.
|
|
75
|
-
// workspace that never opens the panel keeps the appearance it has.
|
|
74
|
+
// navigation bar is drawn from.
|
|
76
75
|
//
|
|
77
76
|
// sc-7753: the global quick bar these tokens were introduced for is retired;
|
|
78
77
|
// the bar that reads them now is the page's own (`local_nav = BOTTOM_BAR`).
|
|
79
78
|
//
|
|
80
|
-
// sc-
|
|
81
|
-
// used to
|
|
82
|
-
//
|
|
79
|
+
// sc-8033: the block is SELF-CONTAINED — it reads `theme.footer` and nothing
|
|
80
|
+
// else. Every colour used to fall back to the rail's, so restyling the sidebar
|
|
81
|
+
// restyled the strip and the two could not be dressed apart; that is the defect
|
|
82
|
+
// sc-7951 set out to fix and only half did. A one-off backfill materialised
|
|
83
|
+
// what each workspace was inheriting, so no shipped app lost its look when the
|
|
84
|
+
// chain went (migration `20260918000000_footer_tokens_uncouple`).
|
|
85
|
+
//
|
|
86
|
+
// The eight authorable keys are the eight controls the Design panel offers, one
|
|
87
|
+
// for one: `backgroundColor`, `textColor`, `tabActiveBackgroundColor`,
|
|
88
|
+
// `tabActiveTextColor`, `borderColor`, `borderWidth`, `activeStyle`,
|
|
89
|
+
// `tabLayout`.
|
|
83
90
|
function resolveFooterTokens(theme) {
|
|
84
91
|
const config = isPlainObject(theme) ? theme : {};
|
|
85
92
|
const footer = isPlainObject(config.footer) ? config.footer : {};
|
|
86
|
-
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
// so this inherits from nothing and unset means "no surface of its own".
|
|
100
|
-
const namedSurface = hexOrNull(footer.tabActiveBackgroundColor);
|
|
93
|
+
// Hex-guarded on read: `theme_config` is an opaque blob an operator can
|
|
94
|
+
// hand-edit, and the EXPORT already refused a malformed colour here while the
|
|
95
|
+
// Player passed it to a style object — so the guard is what stops the two
|
|
96
|
+
// hosts painting a stored junk value differently.
|
|
97
|
+
const accent = footer.activeStyle === "accent";
|
|
98
|
+
const borderColor = hexOrNull(footer.borderColor);
|
|
99
|
+
// sc-8033: the strip's thickness is authorable down to 0, where the rail's
|
|
100
|
+
// and the header's clamp up to 1 (sc-7965). The panel OFFERS zero, so zero is
|
|
101
|
+
// a choice rather than the junk value that clamp refuses — and it resolves
|
|
102
|
+
// into the existing no-divider state, so neither host needs a second way to
|
|
103
|
+
// draw nothing.
|
|
104
|
+
const borderWidth = borderColor ? footerBorderWidthOr(footer.borderWidth) : 0;
|
|
105
|
+
const divider = borderWidth > 0;
|
|
101
106
|
return {
|
|
102
|
-
backgroundColor:
|
|
103
|
-
textColor:
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
activeColor,
|
|
107
|
-
// REQ-THEME-LOOK: the divider's COLOUR is its switch. Unset draws no line at
|
|
108
|
-
// all — which the native strip did not honour before this, ruling a
|
|
109
|
-
// hairline across a tinted bar that had asked for none.
|
|
110
|
-
borderColor,
|
|
111
|
-
borderWidth: borderColor ? borderWidthOr(footer.borderWidth, sidebar.borderWidth) : null,
|
|
107
|
+
backgroundColor: hexOr(footer.backgroundColor, DEFAULT_FOOTER_SURFACE),
|
|
108
|
+
textColor: hexOr(footer.textColor, DEFAULT_CHROME_TEXT),
|
|
109
|
+
borderColor: divider ? borderColor : null,
|
|
110
|
+
borderWidth: divider ? borderWidth : null,
|
|
112
111
|
activeStyle: accent ? "accent" : "filled",
|
|
113
112
|
// sc-7951: bottom tabs are the only strip tall enough to stack the two, so
|
|
114
113
|
// the choice is the footer's alone.
|
|
115
114
|
tabLayout: footer.tabLayout === "stacked" ? "stacked" : "inline",
|
|
116
|
-
// sc-
|
|
117
|
-
// re-derives
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
activeSurface:
|
|
123
|
-
|
|
115
|
+
// sc-8033: the active tab's two halves are two KEYS, resolved here so
|
|
116
|
+
// neither host re-derives them. They used to be one overloaded
|
|
117
|
+
// `activeColor` — the fill under "filled", the label under "accent" — which
|
|
118
|
+
// is why no author could set both. Only the DEFAULTS still consult the
|
|
119
|
+
// style, and they are exactly what each style shipped: a brand-primary fill
|
|
120
|
+
// under a white label, or no fill under a brand-primary label and edge.
|
|
121
|
+
activeSurface: accent
|
|
122
|
+
? hexOrNull(footer.tabActiveBackgroundColor)
|
|
123
|
+
: hexOr(footer.tabActiveBackgroundColor, brandPrimary(config)),
|
|
124
|
+
// The active tab's label, its glyph, and — under "accent" — the edge that
|
|
125
|
+
// marks it. One colour for all three: a mark and its label disagreeing
|
|
126
|
+
// about which colour means "you are here" would be two marks, not one.
|
|
127
|
+
activeColor: hexOr(
|
|
128
|
+
footer.tabActiveTextColor,
|
|
129
|
+
accent ? brandPrimary(config) : FILLED_TAB_LABEL,
|
|
130
|
+
),
|
|
124
131
|
};
|
|
125
132
|
}
|
|
126
133
|
|
|
127
134
|
// REQ-THEME-14 — the rail's and the app bar's tokens, resolved once for both
|
|
128
135
|
// hosts.
|
|
129
136
|
//
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
// what remains of the old shape. An unset `sidebar.backgroundColor` painted #ffffff on web and the app
|
|
138
|
-
// background in the export, and an unset `topBar.textColor` painted the brand
|
|
139
|
-
// primary on web and slate-700 in the export. A default that lives in the
|
|
140
|
-
// resolver cannot drift; one that lives in each host already had. The footer's
|
|
141
|
-
// three remaining raw colours want this same shape next.
|
|
142
|
-
//
|
|
143
|
-
// Every default is what the WEB Player renders today, so the Player does not
|
|
144
|
-
// move and the export converges onto it. That is the direction the footer was
|
|
145
|
-
// converged in too (compiler.service's footerBg defaults to the web's #ffffff).
|
|
137
|
+
// Every default here is what the WEB Player renders, so the Player does not
|
|
138
|
+
// move and the export converges onto it. An unset `sidebar.backgroundColor`
|
|
139
|
+
// painted #ffffff on web and the app background in the export, and an unset
|
|
140
|
+
// `topBar.textColor` painted the brand primary on web and slate-700 in the
|
|
141
|
+
// export. A default that lives in the resolver cannot drift; one that lives in
|
|
142
|
+
// each host already had — which is why `resolveFooterTokens` above stopped
|
|
143
|
+
// returning raw-or-null too (sc-8033).
|
|
146
144
|
//
|
|
147
145
|
// sc-6596 — the ONE exception, and it moves BOTH hosts: an unset rail or bar
|
|
148
146
|
// takes the page's own colour. Converging on the Player's white left a themed
|
|
@@ -151,13 +149,16 @@ function resolveFooterTokens(theme) {
|
|
|
151
149
|
const DEFAULT_PRIMARY_COLOR = "#3b82f6";
|
|
152
150
|
// The web nav item's `text-slate-600`.
|
|
153
151
|
const DEFAULT_CHROME_TEXT = "#475569";
|
|
152
|
+
// The strip's own surface. Both hosts already defaulted to this separately (the
|
|
153
|
+
// web `footerBg`, the compiler's `#ffffff`); sc-8033 resolved it here instead.
|
|
154
|
+
const DEFAULT_FOOTER_SURFACE = "#ffffff";
|
|
154
155
|
// The web mobile header's icon colour, `text-slate-700`.
|
|
155
156
|
const DEFAULT_TOP_BAR_TINT = "#334155";
|
|
156
157
|
// The page beneath the chrome — the web's `DEFAULT_APP_BACKGROUND_COLOR`
|
|
157
158
|
// (frontend/src/utils/theme.js) and the compiler's, which are the same slate-50.
|
|
158
159
|
const DEFAULT_APP_SURFACE = "#f8fafc";
|
|
159
|
-
//
|
|
160
|
-
//
|
|
160
|
+
// A filled tab's label where the author names none — the literal both hosts
|
|
161
|
+
// hard-coded behind an unnamed "filled" surface before the resolver decided it.
|
|
161
162
|
const FILLED_TAB_LABEL = "#ffffff";
|
|
162
163
|
|
|
163
164
|
// A gradient shows only when BOTH stops are a flat hex — the rule both hosts'
|
|
@@ -377,6 +378,18 @@ function resolveTopBarTokens(theme) {
|
|
|
377
378
|
// COLOUR is its switch, so a coloured 0px edge is a contradiction, not "off".
|
|
378
379
|
const BORDER_WIDTH_MIN = 1;
|
|
379
380
|
const BORDER_WIDTH_MAX = 8;
|
|
381
|
+
|
|
382
|
+
// The BOTTOM strip's own range, which starts at zero. Its thickness is a
|
|
383
|
+
// control the Design panel offers down to 0, so a stored zero is an author
|
|
384
|
+
// saying "no line" rather than the junk value the clamp above refuses — and
|
|
385
|
+
// `resolveFooterTokens` turns it back into the no-divider state both hosts
|
|
386
|
+
// already draw, so the extra bound costs neither host a branch (sc-8033).
|
|
387
|
+
function footerBorderWidthOr(value) {
|
|
388
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
389
|
+
return BORDER_WIDTH_MIN;
|
|
390
|
+
}
|
|
391
|
+
return Math.min(Math.max(Math.round(value), 0), BORDER_WIDTH_MAX);
|
|
392
|
+
}
|
|
380
393
|
function borderWidthOr(...values) {
|
|
381
394
|
for (const value of values) {
|
|
382
395
|
if (typeof value === "number" && Number.isFinite(value)) {
|
package/dist/navigation.js
CHANGED
|
@@ -62,78 +62,76 @@ export function menuItemCap(menuType) {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
// REQ-NAV-LOCAL: the app's BOTTOM EDGE — the tokens a page's own bottom
|
|
65
|
-
// navigation bar is drawn from.
|
|
66
|
-
// workspace that never opens the panel keeps the appearance it has.
|
|
65
|
+
// navigation bar is drawn from.
|
|
67
66
|
//
|
|
68
67
|
// sc-7753: the global quick bar these tokens were introduced for is retired;
|
|
69
68
|
// the bar that reads them now is the page's own (`local_nav = BOTTOM_BAR`).
|
|
70
69
|
//
|
|
71
|
-
// sc-
|
|
72
|
-
// used to
|
|
73
|
-
//
|
|
70
|
+
// sc-8033: the block is SELF-CONTAINED — it reads `theme.footer` and nothing
|
|
71
|
+
// else. Every colour used to fall back to the rail's, so restyling the sidebar
|
|
72
|
+
// restyled the strip and the two could not be dressed apart; that is the defect
|
|
73
|
+
// sc-7951 set out to fix and only half did. A one-off backfill materialised
|
|
74
|
+
// what each workspace was inheriting, so no shipped app lost its look when the
|
|
75
|
+
// chain went (migration `20260918000000_footer_tokens_uncouple`).
|
|
76
|
+
//
|
|
77
|
+
// The eight authorable keys are the eight controls the Design panel offers, one
|
|
78
|
+
// for one: `backgroundColor`, `textColor`, `tabActiveBackgroundColor`,
|
|
79
|
+
// `tabActiveTextColor`, `borderColor`, `borderWidth`, `activeStyle`,
|
|
80
|
+
// `tabLayout`.
|
|
74
81
|
export function resolveFooterTokens(theme) {
|
|
75
82
|
const config = isPlainObject(theme) ? theme : {};
|
|
76
83
|
const footer = isPlainObject(config.footer) ? config.footer : {};
|
|
77
|
-
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
// so this inherits from nothing and unset means "no surface of its own".
|
|
91
|
-
const namedSurface = hexOrNull(footer.tabActiveBackgroundColor);
|
|
84
|
+
// Hex-guarded on read: `theme_config` is an opaque blob an operator can
|
|
85
|
+
// hand-edit, and the EXPORT already refused a malformed colour here while the
|
|
86
|
+
// Player passed it to a style object — so the guard is what stops the two
|
|
87
|
+
// hosts painting a stored junk value differently.
|
|
88
|
+
const accent = footer.activeStyle === "accent";
|
|
89
|
+
const borderColor = hexOrNull(footer.borderColor);
|
|
90
|
+
// sc-8033: the strip's thickness is authorable down to 0, where the rail's
|
|
91
|
+
// and the header's clamp up to 1 (sc-7965). The panel OFFERS zero, so zero is
|
|
92
|
+
// a choice rather than the junk value that clamp refuses — and it resolves
|
|
93
|
+
// into the existing no-divider state, so neither host needs a second way to
|
|
94
|
+
// draw nothing.
|
|
95
|
+
const borderWidth = borderColor ? footerBorderWidthOr(footer.borderWidth) : 0;
|
|
96
|
+
const divider = borderWidth > 0;
|
|
92
97
|
return {
|
|
93
|
-
backgroundColor:
|
|
94
|
-
textColor:
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
activeColor,
|
|
98
|
-
// REQ-THEME-LOOK: the divider's COLOUR is its switch. Unset draws no line at
|
|
99
|
-
// all — which the native strip did not honour before this, ruling a
|
|
100
|
-
// hairline across a tinted bar that had asked for none.
|
|
101
|
-
borderColor,
|
|
102
|
-
borderWidth: borderColor ? borderWidthOr(footer.borderWidth, sidebar.borderWidth) : null,
|
|
98
|
+
backgroundColor: hexOr(footer.backgroundColor, DEFAULT_FOOTER_SURFACE),
|
|
99
|
+
textColor: hexOr(footer.textColor, DEFAULT_CHROME_TEXT),
|
|
100
|
+
borderColor: divider ? borderColor : null,
|
|
101
|
+
borderWidth: divider ? borderWidth : null,
|
|
103
102
|
activeStyle: accent ? "accent" : "filled",
|
|
104
103
|
// sc-7951: bottom tabs are the only strip tall enough to stack the two, so
|
|
105
104
|
// the choice is the footer's alone.
|
|
106
105
|
tabLayout: footer.tabLayout === "stacked" ? "stacked" : "inline",
|
|
107
|
-
// sc-
|
|
108
|
-
// re-derives
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
activeSurface:
|
|
114
|
-
|
|
106
|
+
// sc-8033: the active tab's two halves are two KEYS, resolved here so
|
|
107
|
+
// neither host re-derives them. They used to be one overloaded
|
|
108
|
+
// `activeColor` — the fill under "filled", the label under "accent" — which
|
|
109
|
+
// is why no author could set both. Only the DEFAULTS still consult the
|
|
110
|
+
// style, and they are exactly what each style shipped: a brand-primary fill
|
|
111
|
+
// under a white label, or no fill under a brand-primary label and edge.
|
|
112
|
+
activeSurface: accent
|
|
113
|
+
? hexOrNull(footer.tabActiveBackgroundColor)
|
|
114
|
+
: hexOr(footer.tabActiveBackgroundColor, brandPrimary(config)),
|
|
115
|
+
// The active tab's label, its glyph, and — under "accent" — the edge that
|
|
116
|
+
// marks it. One colour for all three: a mark and its label disagreeing
|
|
117
|
+
// about which colour means "you are here" would be two marks, not one.
|
|
118
|
+
activeColor: hexOr(
|
|
119
|
+
footer.tabActiveTextColor,
|
|
120
|
+
accent ? brandPrimary(config) : FILLED_TAB_LABEL,
|
|
121
|
+
),
|
|
115
122
|
};
|
|
116
123
|
}
|
|
117
124
|
|
|
118
125
|
// REQ-THEME-14 — the rail's and the app bar's tokens, resolved once for both
|
|
119
126
|
// hosts.
|
|
120
127
|
//
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
// what remains of the old shape. An unset `sidebar.backgroundColor` painted #ffffff on web and the app
|
|
129
|
-
// background in the export, and an unset `topBar.textColor` painted the brand
|
|
130
|
-
// primary on web and slate-700 in the export. A default that lives in the
|
|
131
|
-
// resolver cannot drift; one that lives in each host already had. The footer's
|
|
132
|
-
// three remaining raw colours want this same shape next.
|
|
133
|
-
//
|
|
134
|
-
// Every default is what the WEB Player renders today, so the Player does not
|
|
135
|
-
// move and the export converges onto it. That is the direction the footer was
|
|
136
|
-
// converged in too (compiler.service's footerBg defaults to the web's #ffffff).
|
|
128
|
+
// Every default here is what the WEB Player renders, so the Player does not
|
|
129
|
+
// move and the export converges onto it. An unset `sidebar.backgroundColor`
|
|
130
|
+
// painted #ffffff on web and the app background in the export, and an unset
|
|
131
|
+
// `topBar.textColor` painted the brand primary on web and slate-700 in the
|
|
132
|
+
// export. A default that lives in the resolver cannot drift; one that lives in
|
|
133
|
+
// each host already had — which is why `resolveFooterTokens` above stopped
|
|
134
|
+
// returning raw-or-null too (sc-8033).
|
|
137
135
|
//
|
|
138
136
|
// sc-6596 — the ONE exception, and it moves BOTH hosts: an unset rail or bar
|
|
139
137
|
// takes the page's own colour. Converging on the Player's white left a themed
|
|
@@ -142,13 +140,16 @@ export function resolveFooterTokens(theme) {
|
|
|
142
140
|
const DEFAULT_PRIMARY_COLOR = "#3b82f6";
|
|
143
141
|
// The web nav item's `text-slate-600`.
|
|
144
142
|
const DEFAULT_CHROME_TEXT = "#475569";
|
|
143
|
+
// The strip's own surface. Both hosts already defaulted to this separately (the
|
|
144
|
+
// web `footerBg`, the compiler's `#ffffff`); sc-8033 resolved it here instead.
|
|
145
|
+
const DEFAULT_FOOTER_SURFACE = "#ffffff";
|
|
145
146
|
// The web mobile header's icon colour, `text-slate-700`.
|
|
146
147
|
const DEFAULT_TOP_BAR_TINT = "#334155";
|
|
147
148
|
// The page beneath the chrome — the web's `DEFAULT_APP_BACKGROUND_COLOR`
|
|
148
149
|
// (frontend/src/utils/theme.js) and the compiler's, which are the same slate-50.
|
|
149
150
|
const DEFAULT_APP_SURFACE = "#f8fafc";
|
|
150
|
-
//
|
|
151
|
-
//
|
|
151
|
+
// A filled tab's label where the author names none — the literal both hosts
|
|
152
|
+
// hard-coded behind an unnamed "filled" surface before the resolver decided it.
|
|
152
153
|
const FILLED_TAB_LABEL = "#ffffff";
|
|
153
154
|
|
|
154
155
|
// A gradient shows only when BOTH stops are a flat hex — the rule both hosts'
|
|
@@ -368,6 +369,18 @@ export function resolveTopBarTokens(theme) {
|
|
|
368
369
|
// COLOUR is its switch, so a coloured 0px edge is a contradiction, not "off".
|
|
369
370
|
const BORDER_WIDTH_MIN = 1;
|
|
370
371
|
const BORDER_WIDTH_MAX = 8;
|
|
372
|
+
|
|
373
|
+
// The BOTTOM strip's own range, which starts at zero. Its thickness is a
|
|
374
|
+
// control the Design panel offers down to 0, so a stored zero is an author
|
|
375
|
+
// saying "no line" rather than the junk value the clamp above refuses — and
|
|
376
|
+
// `resolveFooterTokens` turns it back into the no-divider state both hosts
|
|
377
|
+
// already draw, so the extra bound costs neither host a branch (sc-8033).
|
|
378
|
+
function footerBorderWidthOr(value) {
|
|
379
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
380
|
+
return BORDER_WIDTH_MIN;
|
|
381
|
+
}
|
|
382
|
+
return Math.min(Math.max(Math.round(value), 0), BORDER_WIDTH_MAX);
|
|
383
|
+
}
|
|
371
384
|
function borderWidthOr(...values) {
|
|
372
385
|
for (const value of values) {
|
|
373
386
|
if (typeof value === "number" && Number.isFinite(value)) {
|
package/dist/property-schema.js
CHANGED
|
@@ -23,6 +23,11 @@ const VALID_TYPES = new Set([
|
|
|
23
23
|
// value is the raw uuid string so the tenant-copy walker can remap it
|
|
24
24
|
// through `_remapJson` without per-prop hooks.
|
|
25
25
|
"groupRef",
|
|
26
|
+
// sc-7991: `agentRef` is a picker for one of the workspace's AI agents,
|
|
27
|
+
// emitting a bare TenantAiAgent UUID into the page JSON. A widget reads it
|
|
28
|
+
// and calls `useAgent(agentRef)`. Bare uuid string like `groupRef`/`pageRef`,
|
|
29
|
+
// so tenant-copy remaps it through the layout walk's merged id map.
|
|
30
|
+
"agentRef",
|
|
26
31
|
// REQ-WDG-PAGEREF: `pageRef` is a page picker that emits a bare AppPage
|
|
27
32
|
// UUID into the page JSON. Renders a page `<select>` in the Studio
|
|
28
33
|
// Properties Panel; a widget reads it and navigates via
|
|
@@ -148,6 +153,7 @@ function coerceLeaf(def, value, path, errors) {
|
|
|
148
153
|
case "recordBinding":
|
|
149
154
|
case "groupRef":
|
|
150
155
|
case "pageRef":
|
|
156
|
+
case "agentRef":
|
|
151
157
|
case "richText":
|
|
152
158
|
case "asset":
|
|
153
159
|
case "folderRef":
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colixsystems/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.148.0",
|
|
4
4
|
"description": "Common widget interface for AppStudio. Implements WidgetManifest, WidgetContext, property schema, and helper hooks.",
|
|
5
5
|
"homepage": "https://github.com/Colix-AB/AppStudio",
|
|
6
6
|
"type": "module",
|