@softize/opus 9.0.4 → 9.0.5
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
|
@@ -11,6 +11,12 @@ 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.5 — 2026-08-18
|
|
15
|
+
|
|
16
|
+
**Cards de usuário no `Chat` ficam planos enquanto percorrem o histórico.** A sombra passa
|
|
17
|
+
a indicar somente o turno efetivamente pinado no topo durante o scroll, em vez de elevar
|
|
18
|
+
todas as mensagens que apenas têm comportamento `sticky` disponível.
|
|
19
|
+
|
|
14
20
|
## 9.0.4 — 2026-08-18
|
|
15
21
|
|
|
16
22
|
**Digitar no `Chat` deixa de reprocessar o histórico inteiro.** O transcript agora é uma
|
package/package.json
CHANGED
|
@@ -49,7 +49,13 @@ function defaultHumanizeTool(name: string): string {
|
|
|
49
49
|
* dela sugere). A medida só vale COLAPSADO: aberto, `scrollHeight === clientHeight` e a
|
|
50
50
|
* pergunta "transborda?" passaria a responder não, sumindo com o botão de fechar.
|
|
51
51
|
*/
|
|
52
|
-
const UserTurn = React.memo(function UserTurn({
|
|
52
|
+
const UserTurn = React.memo(function UserTurn({
|
|
53
|
+
content,
|
|
54
|
+
pinned,
|
|
55
|
+
}: {
|
|
56
|
+
content: string
|
|
57
|
+
pinned: boolean
|
|
58
|
+
}): React.ReactElement {
|
|
53
59
|
const ref = React.useRef<HTMLDivElement>(null)
|
|
54
60
|
const [overflows, setOverflows] = React.useState(false)
|
|
55
61
|
const [expanded, setExpanded] = React.useState(false)
|
|
@@ -97,7 +103,13 @@ const UserTurn = React.memo(function UserTurn({ content }: { content: string }):
|
|
|
97
103
|
}, [content, expanded])
|
|
98
104
|
|
|
99
105
|
return (
|
|
100
|
-
<div
|
|
106
|
+
<div
|
|
107
|
+
data-slot="chat-user"
|
|
108
|
+
className={cn(
|
|
109
|
+
'break-words rounded-xl border border-border bg-card px-3 py-2 text-sm leading-snug',
|
|
110
|
+
pinned && 'shadow-sm',
|
|
111
|
+
)}
|
|
112
|
+
>
|
|
101
113
|
<div
|
|
102
114
|
ref={ref}
|
|
103
115
|
data-slot="chat-user-body"
|
|
@@ -216,10 +228,59 @@ const ChatTranscript = React.memo(function ChatTranscript({
|
|
|
216
228
|
const scrollRef = React.useRef<HTMLDivElement>(null)
|
|
217
229
|
const turns = React.useMemo(() => groupTurns(items), [items])
|
|
218
230
|
const indicatorVisible = indicator !== undefined
|
|
231
|
+
const [pinnedTurn, setPinnedTurn] = React.useState<number | null>(null)
|
|
232
|
+
const pinFrameRef = React.useRef<number | undefined>(undefined)
|
|
233
|
+
|
|
234
|
+
const measurePinnedTurn = React.useCallback((): void => {
|
|
235
|
+
const scroll = scrollRef.current
|
|
236
|
+
if (scroll === null) return
|
|
237
|
+
|
|
238
|
+
const rect = scroll.getBoundingClientRect()
|
|
239
|
+
const hit = document.elementFromPoint(rect.left + rect.width / 2, rect.top + 1)
|
|
240
|
+
const header = hit?.closest<HTMLElement>('[data-slot="chat-turn-user"]') ?? null
|
|
241
|
+
const index = Number(header?.dataset['turn'])
|
|
242
|
+
const next = header !== null && Number.isInteger(index) ? index : null
|
|
243
|
+
setPinnedTurn((current) => current === next ? current : next)
|
|
244
|
+
}, [])
|
|
245
|
+
|
|
246
|
+
const schedulePinnedTurnMeasure = React.useCallback((): void => {
|
|
247
|
+
if (pinFrameRef.current !== undefined) return
|
|
248
|
+
pinFrameRef.current = requestAnimationFrame(() => {
|
|
249
|
+
pinFrameRef.current = undefined
|
|
250
|
+
measurePinnedTurn()
|
|
251
|
+
})
|
|
252
|
+
}, [measurePinnedTurn])
|
|
253
|
+
|
|
254
|
+
React.useLayoutEffect(() => {
|
|
255
|
+
const scroll = scrollRef.current
|
|
256
|
+
if (scroll === null) return
|
|
257
|
+
measurePinnedTurn()
|
|
258
|
+
scroll.addEventListener('scroll', schedulePinnedTurnMeasure, { passive: true })
|
|
259
|
+
if (typeof ResizeObserver === 'undefined') {
|
|
260
|
+
return () => {
|
|
261
|
+
if (pinFrameRef.current !== undefined) {
|
|
262
|
+
cancelAnimationFrame(pinFrameRef.current)
|
|
263
|
+
pinFrameRef.current = undefined
|
|
264
|
+
}
|
|
265
|
+
scroll.removeEventListener('scroll', schedulePinnedTurnMeasure)
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
const observer = new ResizeObserver(measurePinnedTurn)
|
|
269
|
+
observer.observe(scroll)
|
|
270
|
+
return () => {
|
|
271
|
+
if (pinFrameRef.current !== undefined) {
|
|
272
|
+
cancelAnimationFrame(pinFrameRef.current)
|
|
273
|
+
pinFrameRef.current = undefined
|
|
274
|
+
}
|
|
275
|
+
observer.disconnect()
|
|
276
|
+
scroll.removeEventListener('scroll', schedulePinnedTurnMeasure)
|
|
277
|
+
}
|
|
278
|
+
}, [turns.length, measurePinnedTurn, schedulePinnedTurnMeasure])
|
|
219
279
|
|
|
220
280
|
React.useEffect(() => {
|
|
221
281
|
scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight })
|
|
222
|
-
|
|
282
|
+
measurePinnedTurn()
|
|
283
|
+
}, [items, indicator, measurePinnedTurn])
|
|
223
284
|
|
|
224
285
|
return (
|
|
225
286
|
<div
|
|
@@ -242,8 +303,13 @@ const ChatTranscript = React.memo(function ChatTranscript({
|
|
|
242
303
|
{turn.user !== null && turn.user.role === 'user' && (
|
|
243
304
|
// Cabeçalho do turno: gruda no topo enquanto o turno está em vista (igual
|
|
244
305
|
// Claude) — a mensagem do usuário num card, não num balão preenchido.
|
|
245
|
-
<div
|
|
246
|
-
|
|
306
|
+
<div
|
|
307
|
+
data-slot="chat-turn-user"
|
|
308
|
+
data-turn={ti}
|
|
309
|
+
data-pinned={pinnedTurn === ti ? 'true' : undefined}
|
|
310
|
+
className="sticky top-0 z-10 py-1.5"
|
|
311
|
+
>
|
|
312
|
+
<UserTurn content={turn.user.content} pinned={pinnedTurn === ti} />
|
|
247
313
|
</div>
|
|
248
314
|
)}
|
|
249
315
|
{turn.rest.map((item, i) => {
|