@softize/opus 18.5.0 → 20.0.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 (151) hide show
  1. package/CHANGELOG.md +275 -0
  2. package/bin/cli.mjs +5 -0
  3. package/bin/lib/check.mjs +198 -10
  4. package/bin/lib/copy.mjs +29 -0
  5. package/bin/lib/mcp.mjs +1 -1
  6. package/docs/adr/0023-surfaces-share-one-anatomy-and-two-layouts.md +298 -0
  7. package/docs/adr/0024-fill-height-propagates-by-context.md +118 -0
  8. package/docs/data-products.md +15 -9
  9. package/docs/protocol.md +3 -4
  10. package/docs/releasing.md +2 -2
  11. package/package.json +2 -2
  12. package/registry/skills/create-opus-action/scripts/scaffold.mjs +2 -1
  13. package/registry/templates/app/package.json +1 -1
  14. package/src/core/runtime.ts +96 -8
  15. package/src/core/types.ts +5 -2
  16. package/src/ui/components/patterns/action-form-card.tsx +11 -1
  17. package/src/ui/components/patterns/content-header.tsx +16 -12
  18. package/src/ui/components/patterns/fill-region.ts +150 -0
  19. package/src/ui/components/patterns/heading-level.ts +16 -0
  20. package/src/ui/components/patterns/list.tsx +103 -73
  21. package/src/ui/components/patterns/page.tsx +20 -4
  22. package/src/ui/components/patterns/pane.tsx +294 -0
  23. package/src/ui/components/patterns/presentation.tsx +81 -54
  24. package/src/ui/components/patterns/sidebar.tsx +13 -72
  25. package/src/ui/components/patterns/split.tsx +51 -38
  26. package/src/ui/components/patterns/surface-anatomy.ts +246 -0
  27. package/src/ui/components/patterns/surface-header.tsx +51 -6
  28. package/src/ui/components/patterns/surface-media.tsx +69 -0
  29. package/src/ui/components/patterns/trigger.tsx +12 -2
  30. package/src/ui/components/primitives/alert.tsx +9 -1
  31. package/src/ui/components/primitives/avatar.tsx +7 -3
  32. package/src/ui/components/primitives/card.tsx +91 -29
  33. package/src/ui/components/primitives/carousel.tsx +34 -5
  34. package/src/ui/components/primitives/checkbox.tsx +11 -3
  35. package/src/ui/components/primitives/command.tsx +14 -5
  36. package/src/ui/components/primitives/copyable.tsx +120 -10
  37. package/src/ui/components/primitives/detail.tsx +10 -13
  38. package/src/ui/components/primitives/dialog.tsx +179 -45
  39. package/src/ui/components/primitives/drawer.tsx +131 -24
  40. package/src/ui/components/primitives/empty.tsx +1 -1
  41. package/src/ui/components/primitives/icon-picker.tsx +6 -2
  42. package/src/ui/components/primitives/icon-tile.tsx +7 -0
  43. package/src/ui/components/primitives/input-group.tsx +9 -2
  44. package/src/ui/components/primitives/input-otp.tsx +1 -1
  45. package/src/ui/components/primitives/kbd.tsx +1 -1
  46. package/src/ui/components/primitives/label.tsx +3 -0
  47. package/src/ui/components/primitives/menu.tsx +5 -0
  48. package/src/ui/components/primitives/metric-card.tsx +98 -39
  49. package/src/ui/components/primitives/popover.tsx +1 -1
  50. package/src/ui/components/primitives/progress.tsx +9 -1
  51. package/src/ui/components/primitives/slider.tsx +62 -11
  52. package/src/ui/components/primitives/truncate.tsx +10 -1
  53. package/src/ui/docs/content/accordion.md +74 -27
  54. package/src/ui/docs/content/action-form-card.md +24 -15
  55. package/src/ui/docs/content/action-form-dialog.md +17 -7
  56. package/src/ui/docs/content/action-form.md +92 -77
  57. package/src/ui/docs/content/action-list-dialog.md +75 -38
  58. package/src/ui/docs/content/action-list.md +134 -52
  59. package/src/ui/docs/content/action-trigger.md +27 -10
  60. package/src/ui/docs/content/action-view.md +14 -14
  61. package/src/ui/docs/content/actions.md +161 -66
  62. package/src/ui/docs/content/ai.md +43 -44
  63. package/src/ui/docs/content/alert.md +83 -29
  64. package/src/ui/docs/content/ask.md +51 -26
  65. package/src/ui/docs/content/aspect-ratio.md +22 -24
  66. package/src/ui/docs/content/audit.md +23 -19
  67. package/src/ui/docs/content/auth.md +38 -19
  68. package/src/ui/docs/content/avatar.md +54 -5
  69. package/src/ui/docs/content/badge.md +29 -9
  70. package/src/ui/docs/content/breadcrumb.md +21 -8
  71. package/src/ui/docs/content/button.md +73 -43
  72. package/src/ui/docs/content/calendar.md +134 -26
  73. package/src/ui/docs/content/card.md +252 -35
  74. package/src/ui/docs/content/carousel.md +37 -6
  75. package/src/ui/docs/content/chat.md +118 -37
  76. package/src/ui/docs/content/checkbox.md +73 -7
  77. package/src/ui/docs/content/cli.md +94 -55
  78. package/src/ui/docs/content/collapsible.md +43 -19
  79. package/src/ui/docs/content/command.md +30 -8
  80. package/src/ui/docs/content/composer.md +23 -8
  81. package/src/ui/docs/content/content.md +37 -7
  82. package/src/ui/docs/content/copyable.md +39 -8
  83. package/src/ui/docs/content/customization.md +55 -46
  84. package/src/ui/docs/content/cycle.md +33 -18
  85. package/src/ui/docs/content/data-products.md +115 -0
  86. package/src/ui/docs/content/data-state.md +20 -6
  87. package/src/ui/docs/content/data.md +20 -19
  88. package/src/ui/docs/content/detail.md +12 -5
  89. package/src/ui/docs/content/dialog.md +219 -28
  90. package/src/ui/docs/content/dictionary-value.md +4 -4
  91. package/src/ui/docs/content/dock.md +54 -19
  92. package/src/ui/docs/content/dot.md +35 -5
  93. package/src/ui/docs/content/drawer.md +193 -22
  94. package/src/ui/docs/content/empty-value.md +20 -6
  95. package/src/ui/docs/content/empty.md +24 -3
  96. package/src/ui/docs/content/events.md +76 -16
  97. package/src/ui/docs/content/executions.md +9 -9
  98. package/src/ui/docs/content/field.md +21 -6
  99. package/src/ui/docs/content/getting-started.md +44 -34
  100. package/src/ui/docs/content/icon-picker.md +19 -13
  101. package/src/ui/docs/content/icon-tile.md +28 -5
  102. package/src/ui/docs/content/input-otp.md +58 -7
  103. package/src/ui/docs/content/input.md +22 -3
  104. package/src/ui/docs/content/item.md +53 -6
  105. package/src/ui/docs/content/kbd.md +5 -1
  106. package/src/ui/docs/content/label.md +13 -4
  107. package/src/ui/docs/content/log.md +5 -5
  108. package/src/ui/docs/content/markdown.md +5 -4
  109. package/src/ui/docs/content/mcp.md +49 -23
  110. package/src/ui/docs/content/menu.md +95 -10
  111. package/src/ui/docs/content/metric-card.md +68 -21
  112. package/src/ui/docs/content/observability.md +10 -9
  113. package/src/ui/docs/content/page.md +125 -11
  114. package/src/ui/docs/content/pagination.md +13 -5
  115. package/src/ui/docs/content/pane.md +300 -0
  116. package/src/ui/docs/content/popover.md +42 -5
  117. package/src/ui/docs/content/presentation.md +91 -47
  118. package/src/ui/docs/content/progress.md +27 -14
  119. package/src/ui/docs/content/queue.md +39 -21
  120. package/src/ui/docs/content/radio-group.md +26 -4
  121. package/src/ui/docs/content/router.md +29 -10
  122. package/src/ui/docs/content/runtime.md +90 -54
  123. package/src/ui/docs/content/scheduler.md +53 -21
  124. package/src/ui/docs/content/scroll-area.md +42 -35
  125. package/src/ui/docs/content/select.md +78 -33
  126. package/src/ui/docs/content/semantic-context.md +21 -2
  127. package/src/ui/docs/content/separator.md +15 -9
  128. package/src/ui/docs/content/sidebar.md +25 -45
  129. package/src/ui/docs/content/skeleton.md +13 -4
  130. package/src/ui/docs/content/slider.md +37 -9
  131. package/src/ui/docs/content/spinner.md +4 -3
  132. package/src/ui/docs/content/split.md +34 -20
  133. package/src/ui/docs/content/storage.md +8 -9
  134. package/src/ui/docs/content/switch.md +10 -7
  135. package/src/ui/docs/content/table.md +46 -8
  136. package/src/ui/docs/content/tabs.md +14 -4
  137. package/src/ui/docs/content/testing.md +76 -31
  138. package/src/ui/docs/content/textarea.md +5 -4
  139. package/src/ui/docs/content/toast.md +48 -7
  140. package/src/ui/docs/content/toggle.md +3 -3
  141. package/src/ui/docs/content/tokens.md +15 -26
  142. package/src/ui/docs/content/tooltip.md +18 -2
  143. package/src/ui/docs/content/truncate.md +36 -6
  144. package/src/ui/docs/content/ui.md +74 -22
  145. package/src/ui/docs/content/upgrading.md +45 -36
  146. package/src/ui/docs/plugin.ts +36 -2
  147. package/src/ui/docs/registry.tsx +14 -2
  148. package/src/ui/drivers/react.tsx +16 -8
  149. package/src/ui/meta.ts +38 -32
  150. package/src/ui/react.tsx +14 -0
  151. package/src/vite/design.ts +16 -4
@@ -1,11 +1,14 @@
1
1
  ## Conversa com resposta integral
2
2
 
3
- Um chat mínimo: lista de mensagens + composer. A conversa é gerenciada por dentro (estado, loading, auto-scroll; **Enter** envia, **Shift+Enter** quebra linha) — a inteligência vem da prop `send`. Com o composer vazio, **↑** recupera as mensagens anteriores do usuário e **↓** volta em direção ao rascunho; durante a edição, as setas continuam movendo o cursor normalmente. O `greeting` é o estado vazio (centrado; some quando a conversa começa e não entra no transcript). Dê altura ao container.
3
+ Use `Chat` para uma conversa completa: transcript, composer, indicador de atividade e rolagem
4
+ automática até a última mensagem. No modo mais simples, o componente guarda as mensagens e chama
5
+ `send` com o histórico a cada envio; o texto devolvido entra no transcript como resposta. Dê altura
6
+ ao contêiner, porque o transcript rola dentro dele.
4
7
 
5
8
  ```tsx preview
6
9
  <div className="h-96 rounded-lg border">
7
10
  <Chat
8
- greeting="Oi! Sou o assistente. Pergunte algo."
11
+ greeting="Pergunte sobre os workspaces da sua equipe."
9
12
  send={async (messages) => {
10
13
  await new Promise((r) => setTimeout(r, 500))
11
14
  const last = messages[messages.length - 1]
@@ -15,12 +18,17 @@ Um chat mínimo: lista de mensagens + composer. A conversa é gerenciada por den
15
18
  </div>
16
19
  ```
17
20
 
21
+ **Enter** envia e **Shift+Enter** quebra a linha. Com o composer vazio, **↑** recupera as mensagens
22
+ anteriores da pessoa e **↓** volta em direção ao rascunho; durante a edição, as setas continuam
23
+ movendo o cursor. `greeting` ocupa o transcript vazio, some quando a conversa começa e não entra no
24
+ histórico enviado a `send`.
25
+
18
26
  ## Estado vazio composto
19
27
 
20
28
  Quando uma frase não basta — marca, nome do agente e saudação administrável, por exemplo — o
21
29
  app compõe o vazio com os slots de `Empty` e entrega em `empty`. O nó ocupa o mesmo lugar do
22
- `greeting`, vence quando os dois existem e some quando a conversa começa. Sem a moldura `Empty`
23
- em volta: o chat já é a estrutura, e a moldura tracejada é para região de criação.
30
+ `greeting`, vence quando os dois existem e some quando a conversa começa. Não envolva o conteúdo em
31
+ `Empty`: o chat já é a estrutura, e a moldura tracejada é para região de criação.
24
32
 
25
33
  ```tsx preview
26
34
  <div className="h-72 rounded-lg border">
@@ -30,8 +38,8 @@ em volta: o chat já é a estrutura, e a moldura tracejada é para região de cr
30
38
  <EmptyMedia variant="icon">
31
39
  <Sparkles />
32
40
  </EmptyMedia>
33
- <EmptyTitle>Copilot</EmptyTitle>
34
- <EmptyDescription>Pergunte sobre vendas, estoque, oficina ou pessoas.</EmptyDescription>
41
+ <EmptyTitle>Assistente da equipe</EmptyTitle>
42
+ <EmptyDescription>Pergunte sobre os workspaces, os membros e as atividades da equipe.</EmptyDescription>
35
43
  </EmptyHeader>
36
44
  }
37
45
  send={async () => 'Pronto.'}
@@ -39,40 +47,52 @@ em volta: o chat já é a estrutura, e a moldura tracejada é para região de cr
39
47
  </div>
40
48
  ```
41
49
 
42
- ## O backend (o agente)
50
+ ## O backend
43
51
 
44
- O `send` posta a conversa no seu endpoint, que resolve o usuário e roda o agente sobre as actions `ai:enabled` — **como o usuário logado**:
52
+ O `send` posta a conversa no endpoint do aplicativo, que resolve o usuário e executa o agente
53
+ sobre as actions expostas à IA, com a autorização desse usuário:
45
54
 
46
55
  ```ts
47
56
  // Rota do app (server): recebe { messages }, devolve { reply }.
48
57
  async function chatRoute(req) {
49
- const base = await auth.resolveContext(req) // quem é o usuário
58
+ const base = await auth.resolveContext(req) // Quem é o usuário.
50
59
  const { messages } = req.body
51
- const { text } = await runtime.aiFor(base)!.run(messages) // o agente age nas actions
60
+ const { text } = await runtime.aiFor(base)!.run(messages) // O agente usa as actions.
52
61
  return { reply: text }
53
62
  }
54
63
  ```
55
64
 
56
65
  ```tsx
57
- // No cliente, o send bate nessa rota:
66
+ // No cliente, o send chama essa rota:
58
67
  <Chat send={async (messages) => (await api.post('/chat', { messages })).reply} />
59
68
  ```
60
69
 
61
- O agente só executa actions marcadas com `ai.enabled` e, nelas, aplica o `authorize` com o contexto do usuário; uma action sem `authorize` fica aberta a qualquer usuário autenticado. Actions com `ai.destructive` ou `ai.requiresConfirmation` são recusadas nesse exemplo, porque ele não passa `confirm`; para aprovar essas chamadas, use `run(messages, { confirm })`. Para expor as actions ao agente, marque-as com `ai: { enabled: true }` no contrato — ver o recurso **IA generativa**.
70
+ O agente só executa actions expostas à IA (`ai: true` ou um objeto `ai` sem `enabled: false`) e,
71
+ nelas, aplica o `authorize` com o contexto do usuário; uma action sem `authorize` fica aberta a qualquer usuário autenticado. Actions com
72
+ `ai.destructive` ou `ai.requiresConfirmation` são recusadas nesse exemplo, porque ele não passa
73
+ `confirm`; para aprovar essas chamadas, use `run(messages, { confirm })`. Para expor uma action ao
74
+ agente e conhecer as demais opções do runtime, consulte a página de [IA generativa](/docs/ai).
75
+
76
+ ## Streaming de eventos
77
+
78
+ `send` também pode devolver um `AsyncIterable<ChatEvent>`. Assim, a resposta chega aos poucos e a
79
+ conversa mostra o que o agente está fazendo. O protocolo tem quatro eventos:
62
80
 
63
- ## Streaming — eventos de conversa
81
+ - `{ type: 'text', delta }` acrescenta um trecho à resposta em andamento.
82
+ - `{ type: 'tool', name, detail? }` informa a ferramenta em uso. O Chat mostra esse passo no
83
+ indicador de atividade, com o rótulo de `humanizeTool`, e não o grava no transcript.
84
+ - `{ type: 'artifact', kind, ref, title? }` acrescenta ao transcript algo produzido na conversa,
85
+ apresentado por `renderArtifact` ou, sem ele, como link.
86
+ - `{ type: 'done', ok, error? }` encerra o turno; com `ok: false`, o transcript recebe a mensagem de
87
+ erro.
64
88
 
65
- O `send` também aceita devolver um **`AsyncIterable<ChatEvent>`** (o protocolo de
66
- `docs/chat-event-protocol.md`): o texto chega incremental, o tool em uso vira o
67
- **indicador vivo** (pontinhos + rótulo humano — nunca uma mensagem no transcript) e
68
- artefatos produzidos na conversa rendem pelo `renderArtifact` do app. O backend liga em
69
- `runtime.aiFor(base).runStream(...)` e streama por SSE; o `Promise<string>` de sempre
70
- continua valendo — é o caso degenerado do protocolo.
89
+ No backend, `runtime.aiFor(base).runStream(...)` emite esses eventos, que podem seguir por SSE.
90
+ Devolver `Promise<string>` continua válido: equivale a um único `text` seguido de `done`.
71
91
 
72
92
  ```tsx preview
73
93
  <div className="h-96 rounded-lg border">
74
94
  <Chat
75
- greeting="Oi! Pergunte algo — a resposta chega em streaming."
95
+ greeting="Pergunte algo e acompanhe a resposta chegando aos poucos."
76
96
  send={async function* (messages) {
77
97
  yield { type: 'tool', name: 'query' }
78
98
  await new Promise((r) => setTimeout(r, 600))
@@ -88,32 +108,93 @@ continua valendo — é o caso degenerado do protocolo.
88
108
  </div>
89
109
  ```
90
110
 
91
- Reidratação: passe `initialMessages` com o histórico persistido e troque a `key` do
92
- componente ao trocar de conversa. O rótulo do indicador é customizável por
93
- `humanizeTool={(name) => '…'}`.
111
+ Para retomar uma conversa salva, passe `initialMessages` com o histórico persistido e troque a
112
+ `key` do componente ao trocar de conversa. `kickoff` inicia a conversa pelo assistente, uma vez,
113
+ quando o transcript nasce vazio.
94
114
 
95
- Itens controlados podem carregar `id` e `createdAt`. O Chat os projeta no elemento da
96
- mensagem e oferece `renderMessageActions` para ações contextuais discretas tanto nas mensagens
97
- enviadas quanto nas recebidas. O slot não busca nem conhece detalhes: o app deve carregar
98
- informação complementar somente após a interação da pessoa e aplicar novamente sua autorização.
115
+ ## Conversa controlada pelo aplicativo
116
+
117
+ Quando o aplicativo já é dono do transcript — por exemplo, porque o servidor mantém a conversa e a
118
+ reenvia a cada reconexão —, passe `messages` em vez de `send`. Nesse modo, `onSend` recebe o texto
119
+ enviado, `busy` bloqueia novos envios enquanto o turno corre e `activity` controla o indicador.
120
+ `busy` não bloqueia a digitação: a pessoa pode escrever a próxima mensagem enquanto espera.
121
+ Retornar `false` em `onSend` devolve o texto ao composer, para que nada se perca em uma falha de
122
+ envio.
123
+
124
+ Os itens podem carregar `id` e `createdAt`. `renderMessageActions` acrescenta ações discretas às
125
+ mensagens enviadas e recebidas, visíveis em hover ou foco. O slot não busca dados: carregue
126
+ informação complementar somente depois da interação da pessoa e aplique novamente a autorização.
127
+ No exemplo, `activity=""` mostra somente os pontos, sem rótulo, e cada mensagem oferece a cópia do
128
+ texto.
129
+
130
+ ```tsx preview
131
+ const nextId = useRef(3)
132
+ const [items, setItems] = useState<ChatTranscriptItem[]>([
133
+ { id: '1', role: 'user', content: 'Qual é o status da Empresa X?' },
134
+ { id: '2', role: 'assistant', content: 'A Empresa X está **ativa**, com 3 agentes.' },
135
+ ])
136
+ const [busy, setBusy] = useState(false)
137
+
138
+ render(
139
+ <div className="h-96 rounded-lg border">
140
+ <Chat
141
+ messages={items}
142
+ busy={busy}
143
+ activity={busy ? '' : undefined}
144
+ onSend={async (text) => {
145
+ setItems((list) => [...list, { id: String(nextId.current++), role: 'user', content: text }])
146
+ setBusy(true)
147
+ await new Promise((r) => setTimeout(r, 800))
148
+ setItems((list) => [
149
+ ...list,
150
+ { id: String(nextId.current++), role: 'assistant', content: 'Mensagem registrada.' },
151
+ ])
152
+ setBusy(false)
153
+ }}
154
+ renderMessageActions={(message) => (
155
+ <Copyable
156
+ value={message.content}
157
+ aria-label="Copiar mensagem"
158
+ className="text-muted-foreground hover:text-foreground"
159
+ />
160
+ )}
161
+ />
162
+ </div>,
163
+ )
164
+ ```
165
+
166
+ `activity` aceita `undefined`, que esconde o indicador; `null`, que mostra “Pensando…”; uma string
167
+ com o rótulo já humanizado pelo aplicativo; ou `''`, que mostra somente os pontos e mantém
168
+ “Pensando…” como nome acessível. No modo autogerenciado, o Chat decide o indicador sozinho e ignora
169
+ `busy` e `activity`.
99
170
 
100
171
  ## Propriedades de Chat
101
172
 
102
173
  | Propriedade | Tipo | Padrão | Descrição |
103
174
  |---|---|---|---|
104
- | `send` | `(messages: ChatMessage[]) => Promise<string> \| AsyncIterable<ChatEvent>` | | Modo autogerenciado: envia o histórico e devolve a resposta inteira ou um stream de eventos. Ignorado no modo controlado. |
105
- | `messages` | `ChatTranscriptItem[]` | | Modo controlado: o transcript vem do app; com ele, `onSend`, `busy` e `activity` assumem. |
175
+ | `send` | `(messages: ChatMessage[]) => Promise<string> \| AsyncIterable<ChatEvent>` | | Modo autogerenciado: recebe o histórico com a nova mensagem e devolve a resposta inteira ou um stream de eventos. Ignorado no modo controlado. |
176
+ | `messages` | `ChatTranscriptItem[]` | | Modo controlado: o transcript vem do aplicativo. Com ele, `onSend`, `busy` e `activity` passam a valer. |
106
177
  | `onSend` | `(text: string) => void \| boolean \| Promise<void \| boolean>` | | Modo controlado: recebe o texto enviado; retornar `false` devolve o texto ao composer. |
107
- | `busy` | `boolean` | | Modo controlado: trava o composer enquanto o turno corre. |
108
- | `activity` | `string \| null` | `undefined` | Indicador vivo: `null` mostra “Pensando…”, string mostra o rótulo, `''` centraliza somente os pontos; `undefined` esconde. |
109
- | `notice` | `ReactNode` | | Aviso do app acima do composer (credencial, agente desatualizado…). |
110
- | `composerActions` | `ReactNode` | | Seletores discretos na barra do composer (agente, app, escopo). |
178
+ | `busy` | `boolean` | `false` | Modo controlado: bloqueia o envio enquanto o turno corre; a digitação continua liberada. |
179
+ | `activity` | `string \| null` | `undefined` | Modo controlado: indicador de atividade. `undefined` esconde, `null` mostra “Pensando…”, uma string mostra o rótulo e `''` mostra somente os pontos. |
180
+ | `notice` | `ReactNode` | | Aviso do aplicativo acima do composer, como credencial expirada ou agente desatualizado. |
181
+ | `composerActions` | `ReactNode` | | Seletores discretos na barra do composer, como agente, aplicativo ou escopo. |
111
182
  | `composerClassName` | `string` | | Ajusta o contêiner externo do composer sem alcançar o DOM interno. |
112
183
  | `greeting` | `string` | | Texto do estado vazio; some quando a conversa começa. |
113
- | `empty` | `ReactNode` | | Estado vazio composto pelo app; vence `greeting` quando os dois existem. |
184
+ | `empty` | `ReactNode` | | Estado vazio composto pelo aplicativo; vence `greeting` quando os dois existem. |
114
185
  | `initialMessages` | `ChatMessage[]` | | Histórico inicial do modo autogerenciado; troque a `key` ao trocar de conversa. |
115
186
  | `kickoff` | `() => Promise<string> \| AsyncIterable<ChatEvent>` | | Conversa que começa pelo assistente, uma vez, quando o transcript nasce vazio. |
116
- | `renderArtifact` | `(artifact: ChatArtifact) => ReactNode` | link com o título | Render do evento `artifact`. |
117
- | `renderMessageActions` | `(message: ChatTranscriptMessage) => ReactNode` | | Ações contextuais da mensagem; ficam visíveis em hover ou foco. |
118
- | `humanizeTool` | `(name: string, detail?: string) => string` | pt-BR embutido | Rótulo humano do tool em uso no indicador vivo. |
187
+ | `renderArtifact` | `(artifact: ChatArtifact) => ReactNode` | link com o título | Apresentação do evento `artifact`. |
188
+ | `renderMessageActions` | `(message: ChatTranscriptMessage) => ReactNode` | | Ações contextuais de mensagens enviadas e recebidas; ficam visíveis em hover ou foco. |
189
+ | `humanizeTool` | `(name: string, detail?: string) => string` | rótulos em pt-BR | Rótulo do indicador para o evento `tool` no modo autogerenciado. |
119
190
  | `placeholder` | `string` | `'Escreva uma mensagem…'` | Placeholder do composer. |
191
+ | `className` | `string` | | Classes da raiz, que ocupa a altura do contêiner (`h-full`). |
192
+
193
+ ## Tipos do transcript
194
+
195
+ | Tipo | Forma | Uso |
196
+ |---|---|---|
197
+ | `ChatMessage` | `{ id?, createdAt?, role: 'user' \| 'assistant', content }` | Histórico enviado a `send` e aceito por `initialMessages`. `createdAt` é um instante ISO-8601. |
198
+ | `ChatTranscriptItem` | `ChatMessage` com `role` também `'error'`, ou `{ id?, createdAt?, role: 'artifact', artifact: ChatArtifact }` | Itens de `messages` no modo controlado. Mensagens `error` aparecem destacadas no transcript. |
199
+ | `ChatTranscriptMessage` | Os itens de `ChatTranscriptItem` que têm `content` | Argumento de `renderMessageActions`. |
200
+ | `ChatArtifact` | `{ kind, ref, title? }` | Algo produzido na conversa, como um link ou um relatório; `ref` é o endereço ou identificador. |
@@ -1,6 +1,11 @@
1
1
  ## Escolha booleana
2
2
 
3
- Sempre em par com Label (htmlFor↔id) — clicar no texto alterna a caixa. defaultChecked para o modo não controlado.
3
+ Use `Checkbox` quando a pessoa precisar marcar uma opção que só vale ao salvar um formulário, como
4
+ aceitar uma condição ou incluir uma etapa. Para uma preferência que entra em vigor no clique, use
5
+ [`Switch`](/ui/switch).
6
+
7
+ Associe cada caixa a um [`Label`](/ui/label) pelo par `htmlFor` e `id`: clicar no texto também
8
+ alterna a caixa. `defaultChecked` define o estado inicial no modo não controlado.
4
9
 
5
10
  ```tsx preview
6
11
  <div className="flex items-center gap-2">
@@ -11,7 +16,8 @@ Sempre em par com Label (htmlFor↔id) — clicar no texto alterna a caixa. defa
11
16
 
12
17
  ## Controlado
13
18
 
14
- onCheckedChange recebe boolean | 'indeterminate' — compare com true para guardar um boolean.
19
+ Use `checked` e `onCheckedChange` quando o estado pertencer ao consumidor. O callback recebe
20
+ `boolean` ou `'indeterminate'`; compare com `true` para guardar um booleano.
15
21
 
16
22
  ```tsx preview
17
23
  const [autoReview, setAutoReview] = useState(true)
@@ -30,7 +36,8 @@ render(
30
36
 
31
37
  ## Lista de opções
32
38
 
33
- Várias caixas, um estado: o conjunto marcado é a lista de valores — o padrão para anexar skills a um agente.
39
+ Quando várias caixas alimentam um único valor, guarde a lista dos itens marcados. É o caso de
40
+ escolher as habilidades de um agente.
34
41
 
35
42
  ```tsx preview col-start
36
43
  const [skills, setSkills] = useState(['clean-code', 'test'])
@@ -50,9 +57,53 @@ render(
50
57
  )
51
58
  ```
52
59
 
60
+ ## Seleção parcial
61
+
62
+ Quando uma caixa controla um grupo, como “Selecionar todas”, ela precisa mostrar também o caso em
63
+ que só parte do grupo está marcada. Passe `checked="indeterminate"` nesse caso: a caixa fica
64
+ preenchida e mostra um traço no lugar do visto, e leitores de tela a anunciam como parcialmente
65
+ marcada. Ao clicar em uma caixa indeterminada, `onCheckedChange` recebe `true`.
66
+
67
+ ```tsx preview col-start
68
+ const all = ['clean-code', 'code-style', 'ddd', 'test']
69
+ const [skills, setSkills] = useState(['clean-code', 'test'])
70
+ const allState = skills.length === all.length ? true : skills.length > 0 ? 'indeterminate' : false
71
+ const toggleSkill = (skill: string): void => {
72
+ setSkills((prev) => (prev.includes(skill) ? prev.filter((s) => s !== skill) : [...prev, skill]))
73
+ }
74
+
75
+ render(
76
+ <>
77
+ <div className="flex items-center gap-2">
78
+ <Checkbox
79
+ id="skills-all"
80
+ checked={allState}
81
+ onCheckedChange={(v) => setSkills(v === true ? all : [])}
82
+ />
83
+ <Label htmlFor="skills-all">Selecionar todas</Label>
84
+ </div>
85
+ <div className="flex flex-col gap-2 pl-6">
86
+ {all.map((skill) => (
87
+ <div key={skill} className="flex items-center gap-2">
88
+ <Checkbox
89
+ id={`partial-${skill}`}
90
+ checked={skills.includes(skill)}
91
+ onCheckedChange={() => toggleSkill(skill)}
92
+ />
93
+ <Label htmlFor={`partial-${skill}`}>{skill}</Label>
94
+ </div>
95
+ ))}
96
+ </div>
97
+ </>,
98
+ )
99
+ ```
100
+
101
+ Com o grupo inteiro marcado, o clique em “Selecionar todas” recebe `false` e limpa a lista.
102
+
53
103
  ## Desabilitado
54
104
 
55
- disabled esmaece a caixa e o rótulo em par (peer-disabled no Label) — marcado ou não.
105
+ `disabled` esmaece e bloqueia a caixa, marcada ou não. O `Label` escrito logo depois da caixa, no
106
+ mesmo contêiner, esmaece junto.
56
107
 
57
108
  ```tsx preview col-start
58
109
  <div className="flex items-center gap-2">
@@ -65,11 +116,26 @@ disabled esmaece a caixa e o rótulo em par (peer-disabled no Label) — marcado
65
116
  </div>
66
117
  ```
67
118
 
119
+ ## Estado inválido
120
+
121
+ Quando a marcação for obrigatória e estiver ausente, declare `aria-invalid` na caixa e explique o
122
+ que falta com `FieldError`. `data-invalid="true"` no [`Field`](/ui/field) leva o contexto de erro
123
+ também ao rótulo.
124
+
125
+ ```tsx preview col-start
126
+ <Field orientation="horizontal" data-invalid="true">
127
+ <Checkbox id="accept-terms" aria-invalid aria-describedby="accept-terms-error" />
128
+ <FieldLabel htmlFor="accept-terms">Li e aceito os termos de uso</FieldLabel>
129
+ </Field>
130
+ <FieldError id="accept-terms-error">Aceite os termos para continuar.</FieldError>
131
+ ```
132
+
68
133
  ## Propriedades de Checkbox
69
134
 
70
135
  | Propriedade | Tipo | Padrão | Descrição |
71
136
  |---|---|---|---|
72
- | `checked` | `boolean \| 'indeterminate'` | | O estado, no modo controlado — parear com onCheckedChange. |
73
- | `onCheckedChange` | `(checked: boolean \| 'indeterminate') => void` | | Chamado a cada alternância. Para guardar um boolean, compare com true. |
137
+ | `checked` | `boolean \| 'indeterminate'` | | Estado no modo controlado. Use com `onCheckedChange`. |
138
+ | `onCheckedChange` | `(checked: boolean \| 'indeterminate') => void` | | Chamado a cada alternância. Para guardar um booleano, compare com `true`. |
74
139
  | `defaultChecked` | `boolean` | `false` | Estado inicial no modo não controlado. |
75
- | `disabled` | `boolean` | `false` | Esmaece e bloqueia — o Label em par esmaece junto (peer-disabled). |
140
+ | `disabled` | `boolean` | `false` | Esmaece e bloqueia a caixa; o `Label` irmão posterior esmaece junto. |
141
+ | `aria-invalid` | `boolean` | | Aplica a borda e o anel de erro. |
@@ -4,14 +4,14 @@ title: CLI opus
4
4
 
5
5
  # CLI opus
6
6
 
7
- O Opus traz um CLI que cobre o ciclo: faz o bootstrap, gera artefatos a partir das declarações,
8
- valida as convenções e expõe o estado vivo para os agentes via MCP. `opus --help` lista os
9
- comandos e `opus <comando> --help` detalha cada um.
7
+ O CLI `opus` acompanha o projeto do início à entrega: cria o app, gera artefatos a partir das
8
+ declarações, valida as convenções e oferece a estrutura do projeto a agentes por MCP. `opus --help`
9
+ lista os comandos e `opus <comando> --help` detalha cada um.
10
10
 
11
11
  ## Gates
12
12
 
13
- > Use os gates no CI e antes de entregar uma mudança. Cada um informa o que precisa de ajuste e
14
- > encerra com sucesso (exit 0) quando não encontra violação.
13
+ Use os gates no CI e antes de entregar uma mudança. Cada um informa o que precisa de ajuste e
14
+ encerra com sucesso (exit 0) quando não encontra violação.
15
15
 
16
16
  ```bash
17
17
  opus check src # convenções das actions e da UI (exit ≠ 0 se violar)
@@ -20,33 +20,37 @@ opus db check # drift entidade ↔ banco (read-only)
20
20
  opus seed check # bindings, dependências, ciclos e scripts paralelos de seed
21
21
  ```
22
22
 
23
- `opus check` lê o source sem executar nada e aplica regras em três grupos: actions
24
- (`action-name`, `kind`, `field-order`, `export`, `requires-sem-authorize`), UI
25
- (`ui-structure`, `ui-semantic-api`, `removed-ui-token`, `unpaired-ui-surface`) e Produtos de Dados
26
- (`data-product-export`, `-id`, `-duplicate`, `-version`, `-interfaces`, `-interface`, `-entities`
27
- e `-entity`, todas com o prefixo `data-product`). Um projeto
28
- marcado com `opus.json` e ainda sem actions passa vacuamente; sem o marcador, zero actions
29
- falha, porque um gate vazio não é aprovação. `opus check --help` descreve cada regra.
23
+ `opus check` lê o código-fonte sem executá-lo e aplica regras em três grupos: actions
24
+ (`action-name`, `kind`, `field-order`, `export`, `requires-sem-authorize`, `ignored-by-kind`), UI
25
+ (`ui-structure`, `ui-structure-split`, `ui-semantic-api`, `removed-ui-token`, `unpaired-ui-surface`) e
26
+ [Produtos de Dados](/docs/data-products) (`data-product-export`, `-id`, `-duplicate`, `-version`,
27
+ `-interfaces`, `-interface`, `-entities` e `-entity`, todas com o prefixo `data-product`). Um projeto
28
+ marcado com `opus.json` e ainda sem actions passa sem achados; sem o marcador, nenhuma action
29
+ encontrada é falha, porque um gate que não verificou nada não é aprovação. `opus check --help` descreve cada regra.
30
30
 
31
31
  ### Hook de pré-push
32
32
 
33
- O hook Git que `opus setup` instala roda, antes de cada push: `opus copy --check`; depois
34
- `opus pre-push materialization` na raiz, para confirmar que os artefatos Opus materializados
35
- (skills, hooks, instruções) correspondem à versão adotada; e `opus check` em cada app com
36
- `opus.json`. Qualquer um com exit ≠ 0 bloqueia o push. Os mesmos comandos podem ser executados à
37
- mão para antecipar o resultado:
33
+ O hook Git que `opus setup` instala executa, antes de cada push:
34
+
35
+ 1. `opus copy --check`;
36
+ 2. `opus pre-push materialization` na raiz, que confirma se os artefatos Opus materializados
37
+ (skills, hooks e instruções) correspondem à versão adotada;
38
+ 3. `opus check` em cada app com `opus.json`.
39
+
40
+ Qualquer comando com exit ≠ 0 bloqueia o push. Para antecipar o resultado, execute os mesmos
41
+ comandos manualmente:
38
42
 
39
43
  ```bash
40
44
  opus pre-push # o mesmo opus check no diretório atual
41
45
  opus pre-push apps/portal # o check de um app específico
42
- opus pre-push materialization # só a freshness dos artefatos materializados
46
+ opus pre-push materialization # só confere se os artefatos materializados estão atualizados
43
47
  ```
44
48
 
45
49
  ## Seeds de desenvolvimento e teste
46
50
 
47
- > Seeds registrados no `opus.config.ts` têm perfis, métricas e escopos explícitos. Listagem e gate
48
- > não abrem o banco; planejamento, aplicação e verificação só conectam com
49
- > `NODE_ENV=development` ou `NODE_ENV=test`.
51
+ Seeds registrados no `opus.config.ts` têm perfis, métricas e escopos explícitos. Listagem e gate
52
+ não abrem o banco; planejamento, aplicação e verificação só conectam com `NODE_ENV=development` ou
53
+ `NODE_ENV=test`.
50
54
 
51
55
  ```bash
52
56
  opus seed list
@@ -58,7 +62,7 @@ NODE_ENV=development opus seed verify customers.scenarios --profile smoke --scop
58
62
  Com qualquer outro valor de `NODE_ENV`, inclusive ausente, esses três comandos falham antes de
59
63
  abrir a conexão. O escopo informado também precisa constar em `safety.scopes` do seed.
60
64
 
61
- `--profile` escolhe o perfil (default: o `defaultProfile` do seed) e `--scope` declara o escopo
65
+ `--profile` escolhe o perfil (padrão: o `defaultProfile` do seed) e `--scope` declara o escopo
62
66
  dos dados (a variável `OPUS_SEED_SCOPE` é a alternativa). `--json` devolve o resultado
63
67
  estruturado, inclusive em caso de erro. `apply` converge quando repetido; não há reset ou
64
68
  truncate no contrato. A skill `$create-opus-seed` estrutura um novo dataset, e
@@ -66,33 +70,47 @@ truncate no contrato. A skill `$create-opus-seed` estrutura um novo dataset, e
66
70
 
67
71
  ## Geração e introspecção
68
72
 
69
- > As declarações (`description` de entidades/actions) são a fonte; o `gen` as projeta. O manifest
70
- > é um lockfile versionado. A diferença do manifest torna a revisão objetiva.
73
+ As declarações, incluindo a `description` de entidades e actions, são a fonte; `opus gen` as
74
+ projeta. O manifest é versionado como um lockfile, e sua diferença mostra na revisão o que mudou
75
+ no contrato.
71
76
 
72
77
  ```bash
73
78
  opus gen # manifest / openapi / docs / stubs a partir do opus.config.ts
74
79
  opus gen --config ./apps/api/opus.config.ts # config fora do diretório atual
75
- opus gen --output ./custom/gen # pasta de saída (default: a do config, ou ./.gen)
80
+ opus gen --output ./custom/gen # pasta de saída (padrão: a do config, ou ./.gen)
76
81
  opus copy # inventário semântico de copy dos contratos
77
- opus introspect # modelo da estrutura (actions/reactions/schedules + wiring)
82
+ opus introspect # modelo da estrutura: actions, reactions, schedules e ligações
78
83
  opus introspect --json
79
84
  ```
80
85
 
81
86
  `opus copy` projeta os textos de `defineAction`/`defineContract` no inventário que a política de
82
- copy da `@softize/base` valida. O caminho vem de `base.json` (`copy.inventory`; default
87
+ copy da `@softize/base` valida. O caminho vem de `base.json` (`copy.inventory`; padrão
83
88
  `.base/copy-inventory.json` na raiz Git), a escrita é atômica e o arquivo gerado deve ser
84
89
  versionado. `--check` compara sem escrever e falha quando o inventário está ausente,
85
90
  desatualizado ou contém copy dinâmica não declarada.
86
91
 
92
+ `opus introspect` lê o código sem executá-lo e só reconhece `defineAction`, `defineReaction`,
93
+ `defineSchedule` e `defineDataProduct`. Actions do par `defineContract` e `bindAction`, reactions do
94
+ par `defineReactionContract` e `bindReaction` e schedules declarados sem `defineSchedule` ficam fora
95
+ do modelo. Para conferir as actions do split, use `opus check`.
96
+
87
97
  `--config` vale para `gen`, `db` e `seed` e sempre aponta para um caminho dentro do projeto. O
88
- `gen` aceita `--force` por compatibilidade, mas a flag não altera nada: a saída mora em um
98
+ `gen` aceita `--force` por compatibilidade, mas a flag não altera nada: a saída fica em um
89
99
  diretório dedicado e é sempre sobrescrita.
90
100
 
101
+ ### Variáveis de ambiente
102
+
103
+ `gen`, `db` e `seed` carregam o `.env` que está ao lado do `opus.config.ts` antes de importar o
104
+ config. Assim, a conexão aberta pelo config usa as mesmas variáveis do projeto. Uma variável já
105
+ definida no ambiente vence o arquivo: `DATABASE_URL=… opus db migrate` usa a URL informada no
106
+ comando. A ausência do `.env` não é erro, porque containers e CI costumam injetar as variáveis
107
+ diretamente.
108
+
91
109
  ## Banco
92
110
 
93
- > Os comandos `db` carregam o `opus.config.ts` e abrem conexão; por isso rodam num runner isolado
94
- > via `tsx`. O padrão é um schema idempotente: um script SQL evolutivo re-rodável, não migrations
95
- > versionadas.
111
+ Os comandos `db` carregam o `opus.config.ts` e abrem conexão; por isso rodam num processo separado,
112
+ com `tsx`. O padrão é um schema idempotente: um único script SQL que pode ser executado de novo a
113
+ cada mudança, em vez de migrations numeradas.
96
114
 
97
115
  ```bash
98
116
  opus db check # compara o schema do banco com as entidades (read-only)
@@ -100,36 +118,57 @@ opus db migrate # aplica o schema idempotente e roda o drift-check na sequên
100
118
  opus db scaffold # rascunho kysely a partir do diff, como referência para escrever o SQL
101
119
  ```
102
120
 
103
- `db scaffold` gera um rascunho para revisão à mão; a verdade continua sendo o script. `db migrate
104
- down` está aposentado e encerra com erro: sem histórico de migrations não há o que reverter —
105
- rollback é editar o schema e rodar `opus db migrate` de novo. O `opus.config.ts` precisa expor a
106
- factory lazy `database`, as entidades (ou domínios) e, opcionalmente, `schema` e `migrations`;
107
- `opus db --help` detalha.
121
+ `db scaffold` gera um rascunho para revisão manual; a fonte continua sendo o script. `db migrate
122
+ down` foi descontinuado e encerra com erro: sem histórico de migrations, não há o que reverter.
123
+ Para desfazer uma mudança, edite o schema e rode `opus db migrate` de novo.
124
+
125
+ O `opus.config.ts` precisa expor a factory `database`, chamada só quando um comando precisa do
126
+ banco, as entidades ou os domínios e, opcionalmente, `schema` e `migrations`; `opus db --help`
127
+ detalha cada campo.
108
128
 
109
129
  ## Bootstrap
110
130
 
111
- > `create` scaffolda um app novo com os pré-requisitos plugados; `setup` é per-app
112
- > (idempotente, nunca sobrescreve o seu).
131
+ `create` gera um app novo com os pré-requisitos configurados. `setup` roda em cada app, pode ser
132
+ repetido e não sobrescreve arquivos do projeto.
113
133
 
114
134
  ```bash
115
- opus create meu-app # app canônico do zero: protocolo + UI + preview + automação
116
- opus create meu-cliente --monorepo # a RAIZ de um workspace (apps/* + packages/*)
135
+ opus create meu-app # app novo: protocolo, UI, preview e automação
136
+ opus create meu-cliente --monorepo # a raiz de um workspace (apps/* + packages/*)
117
137
  opus create apps/portal # dentro de um workspace: só o app (modo detectado)
118
138
  opus setup # grava opus.json e materializa a camada específica do SDK
119
139
  ```
120
140
 
121
- O esqueleto do `create` versiona com o Opus (sai do mesmo pacote que o SDK que ele
122
- configura) e nasce com os gates verdes: domínio-exemplo canônico, teste, manifest e o dev
123
- server pronto para o preview do Maestro. O método geral, a memória e a revisão vêm da Base
124
- depois de `pnpm run setup`. Dois modos, por detecção:
125
- repo standalone (template inteiro) ou **app em monorepo** (dentro de um workspace pnpm:
126
- só os arquivos do app; o que a raiz precisa ter vira aviso, sem clobber).
127
-
128
- ## MCP — estado vivo para os agentes
129
-
130
- > O server MCP expõe introspecção, check e scaffold de action. É como um agente lê a estrutura e cria
131
- > action no formato canônico sem decorar convenção.
132
-
133
- `opus mcp` sobe o server (configurado em `.mcp.json`): o agente consulta a estrutura, roda o
134
- check e gera o esqueleto de uma action nova pela skill `$create-opus-action` — a mesma régua do
135
- `opus check`, por construção.
141
+ O esqueleto do `create` vem do mesmo pacote que o SDK e acompanha sua versão. O app nasce com os
142
+ gates aprovados: domínio de exemplo, teste, manifest e dev server que respeita a variável `PORT`,
143
+ para que um orquestrador de preview escolha a porta. O método geral, a memória e a revisão vêm da
144
+ Base depois de `pnpm run setup`.
145
+
146
+ O `create` detecta o modo. Fora de um workspace pnpm, gera o repositório inteiro. Dentro de um
147
+ workspace, gera só os arquivos do app e lista como aviso o que a raiz precisa ter, sem alterar os
148
+ arquivos dela.
149
+
150
+ ## MCP para agentes de desenvolvimento
151
+
152
+ `opus mcp` inicia, por stdio, um servidor MCP para o agente que trabalha no código do projeto. Com
153
+ ele, o agente consulta a estrutura, valida convenções, gera actions no formato esperado e
154
+ consulta o catálogo de componentes. Esse servidor não executa actions do app; para expor actions a
155
+ agentes em produção, use o [servidor MCP do runtime](/docs/mcp).
156
+
157
+ | Tool | O que faz |
158
+ | --- | --- |
159
+ | `opus_introspect` | Devolve o modelo de `opus introspect`, com os mesmos limites descritos acima. |
160
+ | `opus_check` | Executa as regras de `opus check` e devolve os achados. |
161
+ | `opus_create_action` | Devolve, sem gravar, o contrato e o binding iniciais de uma action a partir de recurso, verbo e kind. A skill `$create-opus-action` usa o mesmo gerador. |
162
+ | `opus_list_components` | Lista os componentes da UI, quando usar cada um e o que importar. |
163
+ | `opus_get_component` | Detalha um componente pelo nome. |
164
+
165
+ O setup não cria a configuração do cliente MCP. Em clientes que leem `.mcp.json` na raiz do
166
+ projeto, registre o servidor assim:
167
+
168
+ ```json
169
+ {
170
+ "mcpServers": {
171
+ "opus": { "command": "pnpm", "args": ["exec", "opus", "mcp"] }
172
+ }
173
+ }
174
+ ```