@softize/opus 25.1.0 → 25.3.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 (40) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/bin/cli.mjs +5 -0
  3. package/bin/lib/assistant-schema.d.mts +55 -0
  4. package/bin/lib/assistant-schema.mjs +286 -0
  5. package/bin/lib/check.mjs +173 -0
  6. package/bin/lib/copy.mjs +2 -0
  7. package/bin/lib/gen-manifest.mjs +4 -0
  8. package/bin/lib/gen-runner.mjs +31 -2
  9. package/bin/lib/gen-stubs.mjs +3 -0
  10. package/bin/lib/mcp.mjs +1 -1
  11. package/docs/adr/0053-read-action-declares-the-resource-assistant.md +443 -0
  12. package/docs/adr/0054-action-list-grouping-is-a-declared-server-cut.md +82 -0
  13. package/docs/protocol.md +59 -1
  14. package/package.json +5 -1
  15. package/registry/skills/create-opus-action/SKILL.md +8 -3
  16. package/registry/skills/create-opus-action/references/contract-and-binding.md +24 -1
  17. package/registry/skills/create-opus-action/references/evaluations.md +3 -0
  18. package/registry/skills/test-opus-action/SKILL.md +4 -1
  19. package/registry/skills/test-opus-action/references/evaluations.md +2 -0
  20. package/registry/skills/test-opus-action/references/harness.md +14 -0
  21. package/src/ai/index.ts +3 -0
  22. package/src/ai/resource-context.ts +113 -0
  23. package/src/core/actions.ts +5 -2
  24. package/src/core/assistant.ts +545 -0
  25. package/src/core/contracts.ts +2 -0
  26. package/src/core/index.ts +23 -0
  27. package/src/core/list-grouping.ts +45 -0
  28. package/src/core/runtime.ts +138 -0
  29. package/src/core/types.ts +39 -0
  30. package/src/mcp/index.ts +3 -0
  31. package/src/ui/components/patterns/grouped-list.ts +88 -0
  32. package/src/ui/components/patterns/list.tsx +395 -61
  33. package/src/ui/docs/content/action-list.md +42 -9
  34. package/src/ui/docs/content/actions.md +97 -1
  35. package/src/ui/docs/content/ai.md +4 -2
  36. package/src/ui/docs/content/cli.md +2 -1
  37. package/src/ui/docs/content/mcp.md +3 -1
  38. package/src/ui/drivers/context.tsx +50 -0
  39. package/src/ui/drivers/react.tsx +9 -69
  40. package/src/ui/meta.ts +1 -1
@@ -215,6 +215,37 @@ periods: [
215
215
  // presetRange('last7') → { from: 'YYYY-MM-DD', to: 'YYYY-MM-DD' } — o mesmo cálculo do pattern.
216
216
  ```
217
217
 
218
+ ## Agrupamento
219
+
220
+ Declare `grouping.fields` quando uma dimensão fechada puder organizar a tabela sem mudar o recorte
221
+ de negócio. Cada campo precisa ser um filtro `select` com opções `static` ou `dictionary`; texto,
222
+ número, data e lookup não são dimensões de agrupamento. O filtro pode aceitar uma ou várias opções:
223
+ cada consulta de grupo envia um único valor na forma declarada. A lista continua plana por padrão,
224
+ e a pessoa escolhe **Agrupar por** em **Exibição**.
225
+
226
+ ```ts
227
+ filters: {
228
+ status: {
229
+ label: 'Status',
230
+ type: 'select',
231
+ options: { kind: 'dictionary', ref: 'workspace.status' },
232
+ },
233
+ },
234
+ grouping: { fields: ['status'] },
235
+ paginate: 'offset',
236
+ ```
237
+
238
+ O Opus consulta a mesma `ListAction` uma vez para cada opção, aplicando o valor ao filtro e
239
+ preservando busca, período, demais filtros e ordenação. Por isso, o handler precisa devolver
240
+ `total` e a action precisa usar `paginate: 'offset'`: o cabeçalho recolhível apresenta a contagem
241
+ autoritativa, e **Carregar mais** expande apenas aquele grupo. A soma aparece no rodapé, sem paginador
242
+ global. O limite é de doze opções por dimensão. Se um dicionário não estiver disponível ou exceder
243
+ esse limite, a dimensão não é oferecida em **Agrupar por**.
244
+
245
+ O agrupamento pertence à tabela derivada. Views alternativas e o modo `children` continuam planos.
246
+ `groupBy` integra `ActionListState`; `listStateToParams` o serializa como `group=<campo>`, e filtros
247
+ aplicados à mesma dimensão restringem os grupos exibidos.
248
+
218
249
  ## Paginação e carregamento
219
250
 
220
251
  `ActionList` envia `limit`, definido por `pageSize`, e `page` no input. O handler aplica o recorte e
@@ -481,21 +512,23 @@ render(
481
512
 
482
513
  ## Declaração na ListAction
483
514
 
484
- | Chave | O que declara |
485
- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
486
- | `columns` | `{ key, label, type?, sortable?, fit?, hidden?, dateFormat?, dictionary?, empty? }` — a tabela. `hidden` nasce oculta e pode ser exibida pelo seletor de colunas; `dictionary` nomeia o dicionário do provider; `empty` dá o significado da ausência. |
487
- | `filters` | `{ [nome]: { label, type, options?, multiple?, presentation?, searchable?, allowEmpty?, placement?, section?, depends?, … } }` — a toolbar. `placement` classifica o controle como `inline`, `advanced` ou `external`; filtros avançados com `section` são agrupados no diálogo; `external` mantém estado e input, mas delega a apresentação ao consumidor. `presentation: 'toggle'` mostra todas as opções curtas de um `select` de escolha única em um `ToggleGroup`; sem a propriedade, o controle continua sendo `Select`. `searchable` habilita busca local em `select`; `allowEmpty: false` omite a opção vazia. `depends` desabilita/cascateia; options `kind: 'lookup'` busca em uma action. Valor aplicado entra no input com o mesmo nome. |
488
- | `text` | `{ fields }` — liga a busca; convenção: param `q` no input. Ela aparece depois dos filtros inline e imediatamente antes do acesso aos filtros avançados. A busca participa da contagem de controles inline: com mais de um controle, a barra exibe as labels; quando ela é o único recorte, sua label é omitida. |
489
- | `sort` | `{ fields, default }` — ordenação inicial; header ordenável escreve `sort: 'chave:dir'`. |
490
- | `periods` | `{ value, label, default? }[]` — o controle de período (presets + Personalizado com calendário); `default: true` marca o preset inicial, senão vale o primeiro; materializa em `from`/`to` no input. |
515
+ | Chave | O que declara |
516
+ | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
517
+ | `columns` | `{ key, label, type?, sortable?, fit?, hidden?, dateFormat?, dictionary?, empty? }` — a tabela. `hidden` nasce oculta e pode ser exibida pelo seletor de colunas; `dictionary` nomeia o dicionário do provider; `empty` dá o significado da ausência. |
518
+ | `filters` | `{ [nome]: { label, type, options?, multiple?, presentation?, searchable?, allowEmpty?, placement?, section?, depends?, … } }` — a toolbar. `placement` classifica o controle como `inline`, `advanced` ou `external`; filtros avançados com `section` são agrupados no diálogo; `external` mantém estado e input, mas delega a apresentação ao consumidor. `presentation: 'toggle'` mostra todas as opções curtas de um `select` de escolha única em um `ToggleGroup`; sem a propriedade, o controle continua sendo `Select`. `searchable` habilita busca local em `select`; `allowEmpty: false` omite a opção vazia. `depends` desabilita/cascateia; options `kind: 'lookup'` busca em uma action. Valor aplicado entra no input com o mesmo nome. |
519
+ | `text` | `{ fields }` — liga a busca; convenção: param `q` no input. Ela aparece depois dos filtros inline e imediatamente antes do acesso aos filtros avançados. A busca participa da contagem de controles inline: com mais de um controle, a barra exibe as labels; quando ela é o único recorte, sua label é omitida. |
520
+ | `sort` | `{ fields, default }` — ordenação inicial; header ordenável escreve `sort: 'chave:dir'`. |
521
+ | `periods` | `{ value, label, default? }[]` — o controle de período (presets + Personalizado com calendário); `default: true` marca o preset inicial, senão vale o primeiro; materializa em `from`/`to` no input. |
522
+ | `grouping` | `{ fields }` — dimensões fechadas disponíveis em **Agrupar por**. Cada nome referencia um filtro `select` com opções estáticas ou de dicionário. Não define agrupamento inicial. |
491
523
 
492
524
  ## useListAction
493
525
 
494
- Quando o layout não cabe no `ActionList`, `useListAction(action, input)` entrega o mesmo fetch
526
+ Quando o layout não cabe no `ActionList`, `useListAction(action, input, options?)` entrega o mesmo fetch
495
527
  declarativo (`items`, `total`, `error`, `isLoading`, `isFetching`, `refetch`) sob o
496
528
  `QueryClientProvider`, e `presetRange(preset)` materializa os presets de período (`today`, `last7`,
497
529
  `thisMonth`…) em `{ from, to }` no formato `YYYY-MM-DD` — o mesmo cálculo que o pattern faz antes de
498
- chamar a action.
530
+ chamar a action. `options.enabled` suspende a consulta enquanto for `false`; sem a opção, vale
531
+ `true`.
499
532
 
500
533
  ## Propriedades de ActionList
501
534
 
@@ -136,7 +136,7 @@ name, kind
136
136
  → public, requires, authorize, loads
137
137
  → handler
138
138
  → audit, confirm, invalidates, rateLimit, idempotency
139
- → ai, automatable, internal
139
+ → ai, assistant, automatable, internal
140
140
  ```
141
141
 
142
142
  Campos específicos de um kind, como `fields`, `paginate`, `columns`, `filters`, `expand`,
@@ -179,6 +179,102 @@ A autorização é fechada por padrão: sem `public: true`, a action exige usuá
179
179
  `authorize`, exige também a permissão declarada. `automatable: true` permite que um
180
180
  [schedule](/docs/scheduler) execute a action como o sistema, sem abri-la a chamadores anônimos.
181
181
 
182
+ ## Declarar o assistente do recurso
183
+
184
+ Um produto pode oferecer, além do chat geral, uma conversa sobre um recurso específico: um cliente,
185
+ um relatório, um pedido. Essa conversa precisa ler o recurso com a permissão de quem conversa, seguir
186
+ o procedimento próprio daquele tipo de recurso e entregar ao modelo só os dados necessários. Os três
187
+ ficam declarados num lugar só: a action `view` que lê o recurso declara `assistant` e passa a ser a
188
+ **action dona** daquele tipo de recurso.
189
+
190
+ ```ts
191
+ export const customerViewContract = defineContract({
192
+ name: 'customer.view',
193
+ kind: 'view',
194
+ label: 'Ver cliente',
195
+ input: z.object({ id: z.string().uuid() }),
196
+ output: customerViewSchema,
197
+ personalData: true,
198
+ requires: 'customer.read',
199
+ authorize: (ctx) => ctx.can('customer.read'),
200
+ ai: true,
201
+ assistant: {
202
+ resource: 'customer',
203
+ skill: 'review-customer',
204
+ label: 'Assistente do cliente',
205
+ title: 'name',
206
+ context: ['kind', 'stage', 'sources', 'leads.totalCount', 'leads.latest.status'],
207
+ },
208
+ })
209
+ ```
210
+
211
+ | Campo | O que declara |
212
+ | --- | --- |
213
+ | `resource` | O tipo de recurso de que a action é dona, em camelCase. Cada tipo tem uma única action dona. |
214
+ | `skill` | A chave do procedimento que a IA segue ao tratar do recurso, em minúsculas com hífen. O Opus não conhece o procedimento; o produto o mantém e confere que a chave existe. |
215
+ | `label` | O nome do assistente para pessoas. Entra no inventário do `opus copy`. |
216
+ | `title` | O caminho, na saída, do campo que nomeia o recurso. |
217
+ | `context` | Os caminhos, na saída, dos campos que o host pode entregar ao modelo como dado do recurso. |
218
+ | `maxContextChars` | Opcional. O teto do dado entregue, em caracteres de JSON: 4.000 por padrão, de 1.000 a 16.000. |
219
+
220
+ A action dona precisa ser `view`, estar publicada para a IA (`ai`), declarar `personalData` e
221
+ receber só `{ id }`: o `id`, do tipo texto, é o único campo obrigatório da entrada, e outros campos,
222
+ se houver, têm valor padrão ou são opcionais. Os caminhos usam ponto para descer em objetos; um
223
+ caminho que passa por uma lista vale para cada item, e um campo intermediário `null` aparece como
224
+ `null`. Declare em `context` só o que orienta a conversa: o exemplo deixa fora contato, documento e
225
+ endereço, e o nome já vai como `title`. Um caminho que termina num objeto ou numa lista leva tudo o
226
+ que está abaixo dele, inclusive campos acrescentados depois à saída; para conferir num teste o que
227
+ cada caminho alcança, use `assistantContextLeaves`, de `@softize/opus/assistant-schema`, sobre o
228
+ schema de saída do manifest. O `title` precisa apontar para texto ou número.
229
+
230
+ O `opus check` cobra a forma da declaração (regra `assistant-declaration`), o `opus gen` recusa
231
+ `title`, caminho de `context` ou entrada que não conferem com os schemas, e o registro recusa duas
232
+ actions donas do mesmo recurso. O manifest, `runtime.aiTools()` e o MCP (`com.softize.opus/assistant`
233
+ em `_meta`) levam a declaração, e o manifest passa a mostrar também a publicação para a IA.
234
+
235
+ ### Ler o recurso pela action dona
236
+
237
+ O recurso de um assistente é lido, e o acesso a ele é conferido, somente pela action dona.
238
+ `runtime.readResource` executa a action com `{ id }` pelo mesmo `execute` de qualquer chamada; a
239
+ autorização, a auditoria e a marca de dado pessoal são as da action:
240
+
241
+ ```ts
242
+ const read = await runtime.readResource('customer', id, base, { context: true })
243
+ if (!read.ok) return notFound() // o erro da action, com a categoria dela
244
+ read.resource // { kind: 'customer', id, title: 'Maria Souza', label: 'Assistente do cliente', skill: 'review-customer' }
245
+ read.context // { data, truncated, omitted, personalData: true }
246
+ ```
247
+
248
+ Sem `{ context: true }`, a leitura devolve só o descritor, que basta para conferir o acesso e
249
+ mostrar o nome. Um tipo sem action dona devolve `runtime.assistant_resource_unknown` sem executar
250
+ nada. `runtime.assistants()` lista as declarações com a action dona de cada uma, para o produto
251
+ montar rotas e telas sem lista paralela e para conferir, num teste, que cada `skill` existe no
252
+ registro de procedimentos dele. O Opus não enxerga as rotas HTTP que o produto registra por conta
253
+ própria: um teste do produto deve garantir que nenhuma rota confere acesso a recurso sem passar por
254
+ uma action.
255
+
256
+ ### O dado que vai ao modelo
257
+
258
+ `context.data` traz só os caminhos declarados, com a estrutura da saída. Quando o JSON passa do
259
+ teto, o corte acontece por etapas, até caber, e fica registrado em `omitted`:
260
+
261
+ 1. sai o último item da maior lista, repetidamente;
262
+ 2. com as listas vazias, o texto mais longo é encurtado pela metade, até 200 caracteres;
263
+ 3. por fim, saem campos inteiros, do último declarado para o primeiro.
264
+
265
+ Com qualquer corte, `truncated` vale `true`. O título fica fora do teto e é encurtado a 200
266
+ caracteres. Um caminho que falta na saída em execução é omitido com o motivo `missing` e gera um
267
+ aviso no log, sem falhar a leitura. Quem recebe a saída por outro canal, como o MCP, aplica a mesma
268
+ projeção com `projectAssistantContext(assistant, output)`.
269
+
270
+ Entregue o dado ao modelo como bloco delimitado, numa mensagem de dado, nunca no prompt de sistema.
271
+ `resourceDataBlock`, de `@softize/opus/ai`, monta o bloco com o tipo, o id, o título, os dados e,
272
+ quando houve corte ou ausência, `truncated` e `omitted`, e escapa dentro dos valores o que poderia
273
+ fechar o delimitador. Quando o modelo chama a action dona como ferramenta, recebe a saída completa
274
+ do contrato, como em qualquer chamada: a projeção governa só o que o host entrega por iniciativa
275
+ própria. Cada execução de uma action com dado pessoal é uma exposição registrada, então reaproveite
276
+ a leitura que já conferiu o acesso em vez de executar a action de novo só para obter o contexto.
277
+
182
278
  ## Prototipar com dados simulados
183
279
 
184
280
  Para desenhar a interface antes de o backend existir, rode o app em modo design. Nesse modo, toda
@@ -86,8 +86,10 @@ const { text } = await ctx.ai!.run('Quais notas a Empresa X emitiu em junho?')
86
86
  usado na execução e na auditoria; `title` traz a `label` legível da action, quando existe;
87
87
  `description` vem de `ai.description` (ou da `description` da action) e `inputSchema`, do input.
88
88
  `metadata.dataProducts` lista os [Produtos de Dados](/docs/data-products) relacionados e
89
- `metadata.dataProductLabels` associa cada identificador ao seu nome legível. Esses metadados servem ao host: não entram no
90
- prompt e não concedem acesso.
89
+ `metadata.dataProductLabels` associa cada identificador ao seu nome legível. Na action dona de um
90
+ tipo de recurso, `metadata.assistant` traz o [assistente declarado](/docs/actions): o recurso, a
91
+ chave do procedimento, o nome, o campo do título, os campos do dado ao modelo e o teto. Esses
92
+ metadados servem ao host: não entram no prompt e não concedem acesso.
91
93
 
92
94
  O agente só executa nomes que `aiTools()` anunciou; uma chamada a outra action volta como erro
93
95
  para o modelo. Actions marcadas com `ai: { destructive: true }` ou `ai: { requiresConfirmation: true }`
@@ -21,7 +21,8 @@ opus seed check # bindings, dependências, ciclos e scripts paralelos de s
21
21
  ```
22
22
 
23
23
  `opus check` lê o código-fonte sem executá-lo e aplica regras em quatro grupos: actions
24
- (`action-name`, `kind`, `field-order`, `export`, `requires-sem-authorize`, `ignored-by-kind`), UI
24
+ (`action-name`, `kind`, `field-order`, `export`, `requires-sem-authorize`, `ignored-by-kind` e
25
+ `assistant-declaration`, que cobra a forma do [assistente do recurso](/docs/actions)), UI
25
26
  (`ui-structure`, `ui-structure-split`, `ui-semantic-api`, `removed-ui-token`, `unpaired-ui-surface`),
26
27
  [Produtos de Dados](/docs/data-products) (`data-product-export`, `-id`, `-duplicate`, `-version`,
27
28
  `-interfaces`, `-interface`, `-entities` e `-entity`, todas com o prefixo `data-product`) e
@@ -60,7 +60,9 @@ Quando a action declara o que faz com os dados, a tool traz também as anotaçõ
60
60
  `readOnlyHint` vem de `effect`. Numa action que escreve, `destructiveHint` vai sempre explícito, a
61
61
  partir de `ai.destructive`, porque o MCP trata a ausência numa escrita como destrutiva.
62
62
  Em `_meta`, `com.softize.opus/effect` repete o efeito e `com.softize.opus/personal-data` diz se a
63
- resposta contém dado pessoal. Veja [Actions](/docs/actions).
63
+ resposta contém dado pessoal. Na action dona de um tipo de recurso, `com.softize.opus/assistant`
64
+ traz o assistente declarado, para o host conhecê-lo pelo catálogo que já consulta e aplicar à saída
65
+ a mesma projeção com `projectAssistantContext`. Veja [Actions](/docs/actions).
64
66
 
65
67
  `CallTool` recusa com erro qualquer nome fora dessa lista; os demais seguem por `runtime.execute`.
66
68
  A exposição declarada em `ai` decide o que o cliente MCP pode executar, e a autorização do contexto
@@ -0,0 +1,50 @@
1
+ import { createContext, useContext, useMemo, type ReactNode } from 'react'
2
+ import type { ClientAdapter } from '../../core/index.ts'
3
+
4
+ /**
5
+ * Dicionário estrutural pro resolvedor de opções: qualquer coisa com `options()`
6
+ * satisfaz o contrato sem este driver importar o driver de schema.
7
+ */
8
+ export interface DictLike {
9
+ options(): Array<{ value: string; label: string; icon?: string }>
10
+ }
11
+
12
+ interface OpusContextValue {
13
+ client: ClientAdapter
14
+ dicts: Record<string, DictLike>
15
+ }
16
+
17
+ const OpusContext = createContext<OpusContextValue | null>(null)
18
+
19
+ export interface OpusProviderProps {
20
+ client: ClientAdapter
21
+ /** Dicionários por ref usados por filtros e campos declarativos. */
22
+ dicts?: Record<string, DictLike>
23
+ children: ReactNode
24
+ }
25
+
26
+ export function OpusProvider({ client, dicts, children }: OpusProviderProps): ReactNode {
27
+ const value = useMemo<OpusContextValue>(() => ({ client, dicts: dicts ?? {} }), [client, dicts])
28
+ return <OpusContext.Provider value={value}>{children}</OpusContext.Provider>
29
+ }
30
+
31
+ /** Acesso interno ao adapter; não é reexportado por `@softize/opus/ui/react`. */
32
+ export function useClient(hook: string): ClientAdapter {
33
+ const ctx = useContext(OpusContext)
34
+ if (ctx === null) {
35
+ throw new Error(
36
+ `${hook} foi chamado fora de um <OpusProvider> e não tem como executar a action. ` +
37
+ 'Os hooks de action e os componentes que os usam, como ActionForm, ActionList e ActionView, ' +
38
+ 'precisam de um <OpusProvider client={...}> acima deles na árvore. Envolva a aplicação no provider; ' +
39
+ 'em testes e previews, envolva o componente renderizado.',
40
+ )
41
+ }
42
+ return ctx.client
43
+ }
44
+
45
+ const NO_DICTS: Record<string, DictLike> = {}
46
+
47
+ /** Dicionários do provider; fora dele, o resolvedor usa os fallbacks do contrato. */
48
+ export function useDicts(): Record<string, DictLike> {
49
+ return useContext(OpusContext)?.dicts ?? NO_DICTS
50
+ }
@@ -12,12 +12,9 @@
12
12
  */
13
13
 
14
14
  import {
15
- createContext,
16
15
  useCallback,
17
- useContext,
18
16
  useMemo,
19
17
  useState,
20
- type ReactNode,
21
18
  } from 'react'
22
19
  import {
23
20
  useForm,
@@ -32,7 +29,6 @@ import type {
32
29
  ActionContract,
33
30
  ActionDef,
34
31
  ActionError,
35
- ClientAdapter,
36
32
  FormContract,
37
33
  Paginated,
38
34
  } from '../../core/index.ts'
@@ -42,74 +38,16 @@ import {
42
38
  idleAction,
43
39
  idleLookup,
44
40
  } from '../index.ts'
41
+ import { useClient } from './context.tsx'
42
+ export {
43
+ OpusProvider,
44
+ useDicts,
45
+ type DictLike,
46
+ type OpusProviderProps,
47
+ } from './context.tsx'
45
48
  import { zodErrorMapPtBr } from '../lib/zod-pt-br.ts'
46
49
  import { objectSchemaShape } from '../lib/object-schema.ts'
47
50
 
48
- // =============================================================================
49
- // Provider
50
- // =============================================================================
51
-
52
- /**
53
- * Dicionário estrutural pro resolvedor de `options: { kind: 'dictionary', ref }`:
54
- * qualquer coisa com `options()` → `{ value, label, icon? }`. O `DictType` do `t.dict`
55
- * satisfaz direto — sem este driver importar o driver de schema. A metadata opcional
56
- * continua sendo declarada pelo dicionário; o provider não inventa apresentação.
57
- */
58
- export interface DictLike {
59
- options(): Array<{ value: string; label: string; icon?: string }>
60
- }
61
-
62
- interface OpusContextValue {
63
- client: ClientAdapter
64
- dicts: Record<string, DictLike>
65
- }
66
-
67
- const OpusContext = createContext<OpusContextValue | null>(null)
68
-
69
- export interface OpusProviderProps {
70
- client: ClientAdapter
71
- /** Dicionários por ref — resolvem `options: { kind: 'dictionary', ref }` de
72
- * filters/fields (ActionList/ActionForm). Campo de dict sem registro aqui ainda
73
- * resolve pelo fallback (a meta do `t.dict` viaja no schema do contrato). */
74
- dicts?: Record<string, DictLike>
75
- children: ReactNode
76
- }
77
-
78
- export function OpusProvider({
79
- client,
80
- dicts,
81
- children,
82
- }: OpusProviderProps): ReactNode {
83
- const value = useMemo<OpusContextValue>(() => ({ client, dicts: dicts ?? {} }), [client, dicts])
84
- return <OpusContext.Provider value={value}>{children}</OpusContext.Provider>
85
- }
86
-
87
- /**
88
- * O adapter do provider. `hook` nomeia quem chamou, para a mensagem apontar o hook que a
89
- * pessoa vê no stack — muitas vezes chamado por dentro de ActionForm, ActionList ou
90
- * ActionView, e não diretamente pela tela.
91
- */
92
- function useClient(hook: string): ClientAdapter {
93
- const ctx = useContext(OpusContext)
94
- if (ctx === null) {
95
- throw new Error(
96
- `${hook} foi chamado fora de um <OpusProvider> e não tem como executar a action. ` +
97
- 'Os hooks de action e os componentes que os usam, como ActionForm, ActionList e ActionView, ' +
98
- 'precisam de um <OpusProvider client={...}> acima deles na árvore. Envolva a aplicação no provider; ' +
99
- 'em testes e previews, envolva o componente renderizado.',
100
- )
101
- }
102
- return ctx.client
103
- }
104
-
105
- const NO_DICTS: Record<string, DictLike> = {}
106
-
107
- /** Dicts do provider. Default {} — inclusive fora do provider: o resolvedor cai
108
- * nos fallbacks (estáticas do spec, meta do `t.dict`) sem exigir registry. */
109
- export function useDicts(): Record<string, DictLike> {
110
- return useContext(OpusContext)?.dicts ?? NO_DICTS
111
- }
112
-
113
51
  // =============================================================================
114
52
  // useAction
115
53
  // =============================================================================
@@ -265,6 +203,7 @@ export interface UseListActionResult<TItem> {
265
203
  export function useListAction<TItem = unknown, TInput extends Record<string, unknown> = Record<string, unknown>>(
266
204
  action: { name: string; kind: 'list' },
267
205
  input?: TInput,
206
+ options?: { enabled?: boolean },
268
207
  ): UseListActionResult<TItem> {
269
208
  const client = useClient('useListAction')
270
209
 
@@ -278,6 +217,7 @@ export function useListAction<TItem = unknown, TInput extends Record<string, unk
278
217
  // Input mudou (filtro/página) → mantém os itens na tela enquanto busca: o
279
218
  // ActionList esmaece em vez de piscar skeleton. isPending = só o 1º load.
280
219
  placeholderData: keepPreviousData,
220
+ enabled: options?.enabled ?? true,
281
221
  })
282
222
 
283
223
  const refetch = useCallback(async () => {
package/src/ui/meta.ts CHANGED
@@ -388,7 +388,7 @@ export const componentMeta = {
388
388
  name: "action-list",
389
389
  ancestry: "opus",
390
390
  whenToUse:
391
- "Apresente uma coleção pesquisável a partir de uma ListAction. O contrato declara colunas, filtros, período e busca; ActionList coordena visualizações, paginação, seleção em lote e sincronização com a URL. A tabela derivada não cria moldura própria. Em uma página, envolva a lista com Surface para manter título e criação junto da coleção. Para um único recurso, use ActionView.",
391
+ "Apresente uma coleção pesquisável a partir de uma ListAction. O contrato declara colunas, filtros, período, busca e dimensões fechadas de agrupamento; ActionList coordena visualizações, paginação, seleção em lote e sincronização com a URL. A tabela derivada não cria moldura própria. Em uma página, envolva a lista com Surface para manter título e criação junto da coleção. Para um único recurso, use ActionView.",
392
392
  },
393
393
  "action-trigger": {
394
394
  name: "action-trigger",