@softize/opus 12.8.1 → 12.9.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/CHANGELOG.md CHANGED
@@ -7,6 +7,13 @@ Depois de qualquer bump, rode os gates (`typecheck` · `test` · `opus check` ·
7
7
  `opus copy --check` · `base copy check` · `manifest:check`) — eles apontam o que a
8
8
  mudança cobra do seu código.
9
9
 
10
+ ## 12.9.0 — 2026-09-03
11
+
12
+ `Chat` aceita `empty`, um nó React para o estado sem mensagens: o app compõe o vazio com os
13
+ slots de `Empty` (marca, nome do agente, saudação administrável) e entrega no mesmo lugar do
14
+ `greeting`, que continua valendo como atalho de uma frase. `empty` vence quando os dois existem
15
+ e some quando a conversa começa; o slot ganha `data-variant="composed"`. Mudança aditiva.
16
+
10
17
  ## 12.8.1 — 2026-09-02
11
18
 
12
19
  Num monorepo pnpm em que o workspace do contrato e o da SPA resolvem cópias distintas do Opus
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softize/opus",
3
- "version": "12.8.1",
3
+ "version": "12.9.0",
4
4
  "description": "End-to-end action protocol for TypeScript. Single package with subpath exports (core + adapters).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -71,6 +71,11 @@ export interface ChatProps {
71
71
  /** Texto do estado vazio (centrado, some quando a conversa começa). Não entra no
72
72
  * transcript — é apresentação, não fala do assistente. */
73
73
  greeting?: string
74
+ /** Estado vazio composto pelo app (ex.: `EmptyHeader` com a marca, o nome e a saudação do
75
+ * agente). Ocupa o mesmo lugar do `greeting` e vence quando os dois existem; `null` ou
76
+ * `false` deixam o `greeting` valer; some quando a conversa começa. Prefira `greeting`
77
+ * quando bastar uma frase. */
78
+ empty?: React.ReactNode
74
79
  /** Histórico inicial do modo autogerenciado (reidratação). Troque a `key` do componente
75
80
  * ao trocar de conversa — o estado interno reinicia com estas mensagens. */
76
81
  initialMessages?: ChatMessage[]
@@ -107,6 +112,7 @@ interface ChatTranscriptProps {
107
112
  items: ChatTranscriptItem[]
108
113
  indicator: string | null | undefined
109
114
  greeting: string | undefined
115
+ empty: React.ReactNode | undefined
110
116
  renderArtifact: ChatProps['renderArtifact']
111
117
  }
112
118
 
@@ -120,11 +126,15 @@ const ChatTranscript = React.memo(function ChatTranscript({
120
126
  items,
121
127
  indicator,
122
128
  greeting,
129
+ empty,
123
130
  renderArtifact,
124
131
  }: ChatTranscriptProps): React.ReactElement {
125
132
  const scrollRef = React.useRef<HTMLDivElement>(null)
126
133
  const turns = React.useMemo(() => groupTurns(items), [items])
127
134
  const indicatorVisible = indicator !== undefined
135
+ // `null`/`false` em `empty` significam "sem nó" (o dado ainda não chegou): o greeting
136
+ // continua como fallback em vez de um wrapper composto vazio.
137
+ const composedEmpty = empty !== undefined && empty !== null && empty !== false
128
138
  React.useEffect(() => {
129
139
  scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight })
130
140
  }, [items, indicator])
@@ -135,11 +145,19 @@ const ChatTranscript = React.memo(function ChatTranscript({
135
145
  data-slot="chat-scroll"
136
146
  className="flex min-h-0 flex-1 flex-col gap-3 overflow-y-auto p-4"
137
147
  >
138
- {items.length === 0 && !indicatorVisible && greeting !== undefined && (
139
- <div data-slot="chat-empty" className="m-auto max-w-[17.5rem] text-center text-muted-foreground">
140
- <div className="mb-2 text-4xl">✦</div>
141
- <p className="text-sm leading-relaxed">{greeting}</p>
148
+ {items.length === 0 && !indicatorVisible && composedEmpty ? (
149
+ <div data-slot="chat-empty" data-variant="composed" className="m-auto flex justify-center text-center">
150
+ {empty}
142
151
  </div>
152
+ ) : (
153
+ items.length === 0 &&
154
+ !indicatorVisible &&
155
+ greeting !== undefined && (
156
+ <div data-slot="chat-empty" className="m-auto max-w-[17.5rem] text-center text-muted-foreground">
157
+ <div className="mb-2 text-4xl">✦</div>
158
+ <p className="text-sm leading-relaxed">{greeting}</p>
159
+ </div>
160
+ )
143
161
  )}
144
162
  {turns.map((turn, ti) => (
145
163
  // Hierarquia do espaço: as falas de um mesmo turno são um raciocínio contínuo
@@ -235,6 +253,7 @@ export function Chat({
235
253
  composerActions,
236
254
  composerClassName,
237
255
  greeting,
256
+ empty,
238
257
  initialMessages,
239
258
  kickoff,
240
259
  renderArtifact,
@@ -374,6 +393,7 @@ export function Chat({
374
393
  items={items}
375
394
  indicator={indicator}
376
395
  greeting={greeting}
396
+ empty={empty}
377
397
  renderArtifact={renderArtifact}
378
398
  />
379
399
  {/* Composer da casa (pílula elevada, enviar dentro). Extraído no <Composer> — o Chat
@@ -15,6 +15,30 @@ Um chat mínimo: lista de mensagens + composer. A conversa é gerenciada por den
15
15
  </div>
16
16
  ```
17
17
 
18
+ ## Estado vazio composto
19
+
20
+ Quando uma frase não basta — marca, nome do agente e saudação administrável, por exemplo — o
21
+ 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.
24
+
25
+ ```tsx preview
26
+ <div className="h-72 rounded-lg border">
27
+ <Chat
28
+ empty={
29
+ <EmptyHeader>
30
+ <EmptyMedia variant="icon">
31
+ <Sparkles />
32
+ </EmptyMedia>
33
+ <EmptyTitle>Copilot</EmptyTitle>
34
+ <EmptyDescription>Pergunte sobre vendas, estoque, oficina ou pessoas.</EmptyDescription>
35
+ </EmptyHeader>
36
+ }
37
+ send={async () => 'Pronto.'}
38
+ />
39
+ </div>
40
+ ```
41
+
18
42
  ## O backend (o agente)
19
43
 
20
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**:
package/src/ui/meta.ts CHANGED
@@ -75,7 +75,7 @@ export const componentMeta = {
75
75
  name: 'chat',
76
76
  ancestry: 'opus',
77
77
  whenToUse:
78
- 'Chat da casa (lista de mensagens + composer) que gerencia a conversa por dentro (estado/loading/auto-scroll; Enter envia, Shift+Enter quebra linha). A inteligência vem da prop `send` — resposta inteira (Promise<string>) OU streaming (AsyncIterable<ChatEvent>: texto incremental, indicador vivo do tool, artefato via renderArtifact). No Opus, o backend liga em `runtime.aiFor(base).run(...)` ou `.runStream(...)`. Dê altura ao container (ex.: `h-full`).',
78
+ 'Chat da casa (lista de mensagens + composer) que gerencia a conversa por dentro (estado/loading/auto-scroll; Enter envia, Shift+Enter quebra linha). A inteligência vem da prop `send` — resposta inteira (Promise<string>) OU streaming (AsyncIterable<ChatEvent>: texto incremental, indicador vivo do tool, artefato via renderArtifact). No Opus, o backend liga em `runtime.aiFor(base).run(...)` ou `.runStream(...)`. Estado vazio por `greeting` (frase) ou `empty` (nó composto com os slots de Empty). Dê altura ao container (ex.: `h-full`).',
79
79
  },
80
80
  'checkbox': {
81
81
  name: 'checkbox',