@softize/opus 9.0.4 → 9.0.6
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 +12 -0
- package/package.json +1 -1
- package/src/ui/components/primitives/chat.tsx +10 -104
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,18 @@ Depois de qualquer bump, rode os gates (`typecheck` · `test` · `opus check` ·
|
|
|
11
11
|
> tinha ficado sem registro nenhum, o que é exatamente o caso que este arquivo existe
|
|
12
12
|
> pra cobrir.
|
|
13
13
|
|
|
14
|
+
## 9.0.6 — 2026-08-18
|
|
15
|
+
|
|
16
|
+
**As falas do usuário no `Chat` voltam ao fluxo normal da conversa.** Elas usam fundo
|
|
17
|
+
discreto, abraçam o conteúdo até 88% da largura e deixam de ter borda, sombra, pin no topo
|
|
18
|
+
e truncamento. O scroll não precisa mais medir qual turno está pinado.
|
|
19
|
+
|
|
20
|
+
## 9.0.5 — 2026-08-18
|
|
21
|
+
|
|
22
|
+
**Cards de usuário no `Chat` ficam planos enquanto percorrem o histórico.** A sombra passa
|
|
23
|
+
a indicar somente o turno efetivamente pinado no topo durante o scroll, em vez de elevar
|
|
24
|
+
todas as mensagens que apenas têm comportamento `sticky` disponível.
|
|
25
|
+
|
|
14
26
|
## 9.0.4 — 2026-08-18
|
|
15
27
|
|
|
16
28
|
**Digitar no `Chat` deixa de reprocessar o histórico inteiro.** O transcript agora é uma
|
package/package.json
CHANGED
|
@@ -31,104 +31,14 @@ function defaultHumanizeTool(name: string): string {
|
|
|
31
31
|
return 'Trabalhando…'
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
/**
|
|
35
|
-
* A fala do usuário no topo do turno — o card que GRUDA (sticky) enquanto o turno está
|
|
36
|
-
* em vista.
|
|
37
|
-
*
|
|
38
|
-
* Ela nasce com TETO de altura, e o motivo é concreto: um brief longo (o caso normal na
|
|
39
|
-
* cabine do Maestro, onde a primeira mensagem é o enunciado inteiro da tarefa) ocupava a
|
|
40
|
-
* sala toda — e, por ser sticky, ficava lá, cobrindo a resposta que a pessoa está
|
|
41
|
-
* esperando ler. Quanto mais caprichado o enunciado, menos se via do trabalho.
|
|
42
|
-
*
|
|
43
|
-
* Truncar sozinho não serve: esta é a ÚNICA cópia do texto na tela, então o corte precisa
|
|
44
|
-
* vir com o gesto de abrir. Colapsado mostra o começo com um esmaecido no pé; aberto
|
|
45
|
-
* mostra tudo e continua sticky.
|
|
46
|
-
*
|
|
47
|
-
* O botão só aparece quando há de fato o que revelar — medido no DOM, não adivinhado por
|
|
48
|
-
* contagem de caracteres, que erra com markdown (uma tabela ocupa muito mais que o texto
|
|
49
|
-
* dela sugere). A medida só vale COLAPSADO: aberto, `scrollHeight === clientHeight` e a
|
|
50
|
-
* pergunta "transborda?" passaria a responder não, sumindo com o botão de fechar.
|
|
51
|
-
*/
|
|
34
|
+
/** A fala do usuário: fundo discreto, largura pelo conteúdo e sem se sobrepor ao histórico. */
|
|
52
35
|
const UserTurn = React.memo(function UserTurn({ content }: { content: string }): React.ReactElement {
|
|
53
|
-
const ref = React.useRef<HTMLDivElement>(null)
|
|
54
|
-
const [overflows, setOverflows] = React.useState(false)
|
|
55
|
-
const [expanded, setExpanded] = React.useState(false)
|
|
56
|
-
const [cap, setCap] = React.useState<number>()
|
|
57
|
-
|
|
58
|
-
// Teto do EXPANDIDO, em px, medido no scroll container — não em `vh`.
|
|
59
|
-
//
|
|
60
|
-
// A primeira tentativa usou `60vh`, e a revisão mostrou por que não serve: o bloco que
|
|
61
|
-
// contém o sticky é o scroll container, cuja altura vem do CONSUMIDOR (`h-full`), não da
|
|
62
|
-
// tela. Num contêiner mais baixo que 60vh — um `<Chat>` embutido num painel de 400px numa
|
|
63
|
-
// tela de 1080, por exemplo — o card pinado fica mais alto que o próprio scroll port, e o
|
|
64
|
-
// botão de fechar sai da região visível e NÃO volta enquanto o turno estiver em vista.
|
|
65
|
-
// Era o mesmo defeito, alcançado por outra porta.
|
|
66
|
-
//
|
|
67
|
-
// CONTRATO que isto passa a cobrar: o `<Chat>` precisa de altura LIMITADA pelo pai. Já
|
|
68
|
-
// era assim de fato (sem isso o `overflow-y-auto` e o `sticky` não significam nada), mas
|
|
69
|
-
// agora tem dente: com altura auto, o container é dimensionado pelo CONTEÚDO, e aí capar
|
|
70
|
-
// encolhe o conteúdo, que baixa o teto, que encolhe de novo — espiral, e provável
|
|
71
|
-
// "ResizeObserver loop completed with undelivered notifications". Com `vh` não existia,
|
|
72
|
-
// porque a viewport não depende do conteúdo.
|
|
73
|
-
React.useLayoutEffect(() => {
|
|
74
|
-
const scroll = ref.current?.closest('[data-slot="chat-scroll"]')
|
|
75
|
-
if (!(scroll instanceof HTMLElement)) return
|
|
76
|
-
const apply = (): void => setCap(Math.round(scroll.clientHeight * 0.6))
|
|
77
|
-
apply()
|
|
78
|
-
if (typeof ResizeObserver === 'undefined') return
|
|
79
|
-
const ro = new ResizeObserver(apply)
|
|
80
|
-
ro.observe(scroll)
|
|
81
|
-
return () => ro.disconnect()
|
|
82
|
-
}, [])
|
|
83
|
-
|
|
84
|
-
React.useLayoutEffect(() => {
|
|
85
|
-
if (expanded) return
|
|
86
|
-
const el = ref.current
|
|
87
|
-
if (el === null) return
|
|
88
|
-
const measure = (): void => setOverflows(el.scrollHeight > el.clientHeight + 1)
|
|
89
|
-
measure()
|
|
90
|
-
// A largura muda o quebra-linha, e com ela a resposta: um texto que cabe no rail
|
|
91
|
-
// aberto transborda com ele estreito. Sem observar, o botão some ou sobra na hora
|
|
92
|
-
// errada — e o rail do Maestro é redimensionável.
|
|
93
|
-
if (typeof ResizeObserver === 'undefined') return
|
|
94
|
-
const ro = new ResizeObserver(measure)
|
|
95
|
-
ro.observe(el)
|
|
96
|
-
return () => ro.disconnect()
|
|
97
|
-
}, [content, expanded])
|
|
98
|
-
|
|
99
36
|
return (
|
|
100
|
-
<div
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
// defeito que este componente veio matar, agora atrás de um clique — o card segue
|
|
106
|
-
// `sticky` com fundo opaco e volta a pintar por cima das falas seguintes. Pior, o
|
|
107
|
-
// botão de fechar é o último filho: com card mais alto que a viewport ele fica
|
|
108
|
-
// abaixo da dobra, e o sticky só consegue subir a altura da RESPOSTA (centenas de
|
|
109
|
-
// px contra milhares de card, no caso-alvo de brief enorme + resposta curta). A
|
|
110
|
-
// pessoa abria o brief e ficava sem como fechar. Com teto, o card inteiro cabe e o
|
|
111
|
-
// botão está sempre alcançável; o brief se lê rolando DENTRO dele.
|
|
112
|
-
className={cn(expanded ? 'overflow-y-auto' : 'max-h-40 overflow-hidden')}
|
|
113
|
-
style={expanded ? { maxHeight: cap } : undefined}
|
|
114
|
-
>
|
|
115
|
-
<Markdown content={content} />
|
|
116
|
-
{/* Esmaecido DENTRO do body, colado no fim do texto: como irmão do botão ele caía
|
|
117
|
-
~28px abaixo do corte (mt-1 + linha do botão + py-2), e a maior parte dos 40px
|
|
118
|
-
pintava a faixa do botão em vez do texto truncado. */}
|
|
119
|
-
{overflows && !expanded && (
|
|
120
|
-
<div className="pointer-events-none sticky bottom-0 -mt-10 h-10 bg-gradient-to-t from-card to-transparent" />
|
|
121
|
-
)}
|
|
122
|
-
</div>
|
|
123
|
-
{overflows && (
|
|
124
|
-
<button
|
|
125
|
-
type="button"
|
|
126
|
-
onClick={() => setExpanded((v) => !v)}
|
|
127
|
-
className="mt-1 text-xs text-muted-foreground underline-offset-2 hover:underline"
|
|
128
|
-
>
|
|
129
|
-
{expanded ? 'Mostrar menos' : 'Mostrar mais'}
|
|
130
|
-
</button>
|
|
131
|
-
)}
|
|
37
|
+
<div
|
|
38
|
+
data-slot="chat-user"
|
|
39
|
+
className="w-fit max-w-[88%] self-end break-words rounded-xl bg-muted px-3 py-2 text-sm leading-snug"
|
|
40
|
+
>
|
|
41
|
+
<Markdown content={content} />
|
|
132
42
|
</div>
|
|
133
43
|
)
|
|
134
44
|
})
|
|
@@ -180,8 +90,7 @@ function isAsyncIterable(value: unknown): value is AsyncIterable<ChatEvent> {
|
|
|
180
90
|
return typeof value === 'object' && value !== null && Symbol.asyncIterator in value
|
|
181
91
|
}
|
|
182
92
|
|
|
183
|
-
/** Agrupa o transcript em turnos (mensagem do usuário + o que veio em resposta)
|
|
184
|
-
* o cabeçalho do turno fica sticky enquanto o turno está em vista. */
|
|
93
|
+
/** Agrupa o transcript em turnos (mensagem do usuário + o que veio em resposta). */
|
|
185
94
|
function groupTurns(items: ChatTranscriptItem[]): Array<{ user: ChatTranscriptItem | null; rest: ChatTranscriptItem[] }> {
|
|
186
95
|
const turns: Array<{ user: ChatTranscriptItem | null; rest: ChatTranscriptItem[] }> = []
|
|
187
96
|
for (const item of items) {
|
|
@@ -216,7 +125,6 @@ const ChatTranscript = React.memo(function ChatTranscript({
|
|
|
216
125
|
const scrollRef = React.useRef<HTMLDivElement>(null)
|
|
217
126
|
const turns = React.useMemo(() => groupTurns(items), [items])
|
|
218
127
|
const indicatorVisible = indicator !== undefined
|
|
219
|
-
|
|
220
128
|
React.useEffect(() => {
|
|
221
129
|
scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight })
|
|
222
130
|
}, [items, indicator])
|
|
@@ -240,9 +148,7 @@ const ChatTranscript = React.memo(function ChatTranscript({
|
|
|
240
148
|
// curtas virava uma parede uniforme, sem começo nem fim visíveis.
|
|
241
149
|
<div key={ti} className="flex flex-col gap-1">
|
|
242
150
|
{turn.user !== null && turn.user.role === 'user' && (
|
|
243
|
-
|
|
244
|
-
// Claude) — a mensagem do usuário num card, não num balão preenchido.
|
|
245
|
-
<div className="sticky top-0 z-10 py-1.5">
|
|
151
|
+
<div data-slot="chat-turn-user" className="flex justify-end py-1.5">
|
|
246
152
|
<UserTurn content={turn.user.content} />
|
|
247
153
|
</div>
|
|
248
154
|
)}
|
|
@@ -307,8 +213,8 @@ const ChatTranscript = React.memo(function ChatTranscript({
|
|
|
307
213
|
|
|
308
214
|
/**
|
|
309
215
|
* Chat da casa: lista de mensagens + composer (Enter envia / Shift+Enter quebra linha),
|
|
310
|
-
* transcript em turnos
|
|
311
|
-
*
|
|
216
|
+
* transcript em turnos, fala do usuário em fundo discreto, assistente em Markdown direto
|
|
217
|
+
* no corpo, tool como indicador vivo
|
|
312
218
|
* (nunca mensagem) e artefatos via `renderArtifact`. Dois modos:
|
|
313
219
|
*
|
|
314
220
|
* - **Autogerenciado** (`send`): o componente é dono da conversa; o `send` devolve
|