@softize/opus 9.0.7 → 9.0.9
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,22 @@ 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.9 — 2026-08-19
|
|
15
|
+
|
|
16
|
+
**O composer do `Chat` passa a navegar pelas mensagens já enviadas.** Com o campo vazio,
|
|
17
|
+
`↑` recupera a mensagem mais recente e continua pelas anteriores; `↓` avança e, depois da
|
|
18
|
+
mais nova, devolve o rascunho. Enquanto há texto sendo editado, as setas preservam a
|
|
19
|
+
navegação normal da textarea. O `Composer` expõe callbacks opcionais para shells que
|
|
20
|
+
queiram fornecer outro histórico, sem mudar o comportamento padrão do componente isolado.
|
|
21
|
+
|
|
22
|
+
## 9.0.8 — 2026-08-18
|
|
23
|
+
|
|
24
|
+
**Scrollbars nativas passam a usar o acabamento discreto como parte do tema base.** Todo
|
|
25
|
+
documento que importa `@softize/opus/ui/theme.css` recebe o indicador fino, revelado no
|
|
26
|
+
hover ou foco, inclusive em conteúdo renderizado por portal. A classe
|
|
27
|
+
`scrollbar-subtle` deixa de ser necessária; iframes continuam isolados e precisam importar
|
|
28
|
+
o tema no próprio documento.
|
|
29
|
+
|
|
14
30
|
## 9.0.7 — 2026-08-18
|
|
15
31
|
|
|
16
32
|
**Scrollbars nativas podem compartilhar um acabamento discreto e previsível.** A classe
|
package/package.json
CHANGED
|
@@ -133,7 +133,7 @@ const ChatTranscript = React.memo(function ChatTranscript({
|
|
|
133
133
|
<div
|
|
134
134
|
ref={scrollRef}
|
|
135
135
|
data-slot="chat-scroll"
|
|
136
|
-
className="
|
|
136
|
+
className="flex min-h-0 flex-1 flex-col gap-3 overflow-y-auto p-4"
|
|
137
137
|
>
|
|
138
138
|
{items.length === 0 && !indicatorVisible && greeting !== undefined && (
|
|
139
139
|
<div data-slot="chat-empty" className="m-auto max-w-[280px] text-center text-muted-foreground">
|
|
@@ -212,7 +212,8 @@ const ChatTranscript = React.memo(function ChatTranscript({
|
|
|
212
212
|
})
|
|
213
213
|
|
|
214
214
|
/**
|
|
215
|
-
* Chat da casa: lista de mensagens + composer (Enter envia / Shift+Enter quebra linha
|
|
215
|
+
* Chat da casa: lista de mensagens + composer (Enter envia / Shift+Enter quebra linha;
|
|
216
|
+
* com o campo vazio, ↑/↓ navegam pelas mensagens anteriores do usuário),
|
|
216
217
|
* transcript em turnos, fala do usuário em fundo discreto, assistente em Markdown direto
|
|
217
218
|
* no corpo, tool como indicador vivo
|
|
218
219
|
* (nunca mensagem) e artefatos via `renderArtifact`. Dois modos:
|
|
@@ -246,11 +247,52 @@ export function Chat({
|
|
|
246
247
|
const [input, setInput] = React.useState('')
|
|
247
248
|
const [ownBusy, setOwnBusy] = React.useState(false)
|
|
248
249
|
const [ownActivity, setOwnActivity] = React.useState<string | null>(null)
|
|
250
|
+
const historyCursor = React.useRef<number | null>(null)
|
|
251
|
+
const historyDraft = React.useRef('')
|
|
249
252
|
|
|
250
253
|
const items = controlled ? messages : ownItems
|
|
251
254
|
const busy = controlled ? (busyProp ?? false) : ownBusy
|
|
252
255
|
// Indicador: no controlado o app manda (undefined esconde); no autogerenciado segue o busy.
|
|
253
256
|
const indicator = controlled ? activityProp : ownBusy ? ownActivity : undefined
|
|
257
|
+
const inputHistory = React.useMemo(
|
|
258
|
+
() => items.filter((item): item is ChatMessage => item.role === 'user').map((item) => item.content),
|
|
259
|
+
[items],
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
function changeInput(value: string): void {
|
|
263
|
+
historyCursor.current = null
|
|
264
|
+
setInput(value)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function showPreviousInput(): boolean {
|
|
268
|
+
if (inputHistory.length === 0) return false
|
|
269
|
+
|
|
270
|
+
if (historyCursor.current === null) {
|
|
271
|
+
// Não rouba ↑ do cursor em mensagens novas ou multilinha. A navegação começa no
|
|
272
|
+
// composer vazio, como em shells; depois disso ↑/↓ pertencem ao histórico.
|
|
273
|
+
if (input !== '') return false
|
|
274
|
+
historyDraft.current = input
|
|
275
|
+
historyCursor.current = inputHistory.length - 1
|
|
276
|
+
} else if (historyCursor.current > 0) {
|
|
277
|
+
historyCursor.current -= 1
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
setInput(inputHistory[historyCursor.current])
|
|
281
|
+
return true
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function showNextInput(): boolean {
|
|
285
|
+
if (historyCursor.current === null) return false
|
|
286
|
+
|
|
287
|
+
if (historyCursor.current < inputHistory.length - 1) {
|
|
288
|
+
historyCursor.current += 1
|
|
289
|
+
setInput(inputHistory[historyCursor.current])
|
|
290
|
+
} else {
|
|
291
|
+
historyCursor.current = null
|
|
292
|
+
setInput(historyDraft.current)
|
|
293
|
+
}
|
|
294
|
+
return true
|
|
295
|
+
}
|
|
254
296
|
|
|
255
297
|
const kickoffRan = React.useRef(false)
|
|
256
298
|
React.useEffect(() => {
|
|
@@ -264,6 +306,8 @@ export function Chat({
|
|
|
264
306
|
async function submit(): Promise<void> {
|
|
265
307
|
const text = input.trim()
|
|
266
308
|
if (text === '' || busy) return
|
|
309
|
+
historyCursor.current = null
|
|
310
|
+
historyDraft.current = ''
|
|
267
311
|
setInput('')
|
|
268
312
|
|
|
269
313
|
if (controlled) {
|
|
@@ -339,8 +383,10 @@ export function Chat({
|
|
|
339
383
|
{notice}
|
|
340
384
|
<Composer
|
|
341
385
|
value={input}
|
|
342
|
-
onChange={
|
|
386
|
+
onChange={changeInput}
|
|
343
387
|
onSubmit={() => void submit()}
|
|
388
|
+
onHistoryPrevious={showPreviousInput}
|
|
389
|
+
onHistoryNext={showNextInput}
|
|
344
390
|
busy={busy}
|
|
345
391
|
placeholder={placeholder}
|
|
346
392
|
actions={composerActions}
|
|
@@ -12,6 +12,10 @@ export interface ComposerProps {
|
|
|
12
12
|
onChange: (value: string) => void
|
|
13
13
|
/** Enter (sem Shift) ou o botão enviar. Só dispara quando dá pra enviar (ver `submitDisabled`). */
|
|
14
14
|
onSubmit: () => void
|
|
15
|
+
/** Navegação opcional pelo histórico do dono do composer. Retorne `true` quando a tecla
|
|
16
|
+
* foi consumida; o `<Chat>` usa isso para ↑/↓ sem interferir no cursor normal. */
|
|
17
|
+
onHistoryPrevious?: () => boolean
|
|
18
|
+
onHistoryNext?: () => boolean
|
|
15
19
|
/** Trava o composer enquanto o turno/ação corre — o enviar vira spinner. */
|
|
16
20
|
busy?: boolean
|
|
17
21
|
/** Gate EXTRA de envio além de "vazio" e "busy" (ex.: falta escolher o app). Desabilita o
|
|
@@ -39,6 +43,8 @@ export function Composer({
|
|
|
39
43
|
value,
|
|
40
44
|
onChange,
|
|
41
45
|
onSubmit,
|
|
46
|
+
onHistoryPrevious,
|
|
47
|
+
onHistoryNext,
|
|
42
48
|
busy = false,
|
|
43
49
|
submitDisabled = false,
|
|
44
50
|
placeholder = 'Escreva uma mensagem…',
|
|
@@ -59,6 +65,14 @@ export function Composer({
|
|
|
59
65
|
value={value}
|
|
60
66
|
onChange={(e) => onChange(e.target.value)}
|
|
61
67
|
onKeyDown={(e) => {
|
|
68
|
+
if (e.key === 'ArrowUp' && onHistoryPrevious?.()) {
|
|
69
|
+
e.preventDefault()
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
if (e.key === 'ArrowDown' && onHistoryNext?.()) {
|
|
73
|
+
e.preventDefault()
|
|
74
|
+
return
|
|
75
|
+
}
|
|
62
76
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
63
77
|
e.preventDefault()
|
|
64
78
|
fire()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
## Básico
|
|
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`. O `greeting` é o estado vazio (centrado; some quando a conversa começa e NÃO entra no transcript). Dê altura ao container.
|
|
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.
|
|
4
4
|
|
|
5
5
|
```tsx preview
|
|
6
6
|
<div className="h-96 rounded-lg border">
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
## Básico
|
|
2
2
|
|
|
3
|
-
A caixa de escrever da casa: textarea numa pílula elevada (`rounded-card` + `border` + `shadow-sm`), **Enter** envia / **Shift+Enter** quebra linha, enviar dentro. É o composer do [Chat](/components/chat) extraído — use SOZINHO quando há entrada de texto mas não um chat (ex.: criar uma sessão). Controlado: o dono do texto é você.
|
|
3
|
+
A caixa de escrever da casa: textarea numa pílula elevada (`rounded-card` + `border` + `shadow-sm`), **Enter** envia / **Shift+Enter** quebra linha, enviar dentro. É o composer do [Chat](/components/chat) extraído — use SOZINHO quando há entrada de texto mas não um chat (ex.: criar uma sessão). Controlado: o dono do texto é você. Os callbacks opcionais `onHistoryPrevious` e `onHistoryNext` permitem que esse dono consuma **↑/↓**; sem eles, as setas mantêm o comportamento nativo da textarea.
|
|
4
4
|
|
|
5
5
|
```tsx preview col
|
|
6
6
|
const [text, setText] = React.useState('')
|
package/src/ui/theme.css
CHANGED
|
@@ -180,39 +180,39 @@
|
|
|
180
180
|
}
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
-
/*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
183
|
+
/* Scrollbar nativa da casa: todo scroller do documento compartilha o mesmo acabamento,
|
|
184
|
+
* inclusive conteúdo renderizado por portal. Isso elimina a variação do macOS entre raiz,
|
|
185
|
+
* elementos aninhados, mouse e trackpad sem trocar o mecanismo nativo nem adicionar trabalho
|
|
186
|
+
* em JS. Firefox usa scrollbar-color; Chromium/Safari usam os pseudos abaixo. Iframes
|
|
187
|
+
* continuam sendo documentos isolados e devem importar o tema no próprio documento. */
|
|
188
188
|
@media (hover: hover) {
|
|
189
|
-
|
|
190
|
-
|
|
189
|
+
:root,
|
|
190
|
+
:root * {
|
|
191
191
|
scrollbar-color: transparent transparent;
|
|
192
192
|
scrollbar-width: thin;
|
|
193
193
|
}
|
|
194
|
-
|
|
195
|
-
|
|
194
|
+
:root:is(:hover, :focus, :focus-within),
|
|
195
|
+
:root *:is(:hover, :focus, :focus-within) {
|
|
196
196
|
scrollbar-color: color-mix(in srgb, var(--muted-foreground) 35%, transparent) transparent;
|
|
197
197
|
}
|
|
198
|
-
|
|
199
|
-
|
|
198
|
+
:root::-webkit-scrollbar,
|
|
199
|
+
:root *::-webkit-scrollbar {
|
|
200
200
|
width: 0.5rem;
|
|
201
201
|
height: 0.5rem;
|
|
202
202
|
}
|
|
203
|
-
|
|
204
|
-
|
|
203
|
+
:root::-webkit-scrollbar-track,
|
|
204
|
+
:root *::-webkit-scrollbar-track {
|
|
205
205
|
background: transparent;
|
|
206
206
|
}
|
|
207
|
-
|
|
208
|
-
|
|
207
|
+
:root::-webkit-scrollbar-thumb,
|
|
208
|
+
:root *::-webkit-scrollbar-thumb {
|
|
209
209
|
border: 2px solid transparent;
|
|
210
210
|
border-radius: 9999px;
|
|
211
211
|
background-color: transparent;
|
|
212
212
|
background-clip: content-box;
|
|
213
213
|
}
|
|
214
|
-
|
|
215
|
-
|
|
214
|
+
:root:is(:hover, :focus, :focus-within)::-webkit-scrollbar-thumb,
|
|
215
|
+
:root *:is(:hover, :focus, :focus-within)::-webkit-scrollbar-thumb {
|
|
216
216
|
background-color: color-mix(in srgb, var(--muted-foreground) 35%, transparent);
|
|
217
217
|
}
|
|
218
218
|
}
|