@softize/opus 8.6.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.
Files changed (286) hide show
  1. package/CHANGELOG.md +1616 -0
  2. package/LICENSE +21 -0
  3. package/README.md +113 -0
  4. package/bin/cli.mjs +528 -0
  5. package/bin/lib/check.mjs +307 -0
  6. package/bin/lib/components.mjs +151 -0
  7. package/bin/lib/create.mjs +208 -0
  8. package/bin/lib/db-check-runner.mjs +86 -0
  9. package/bin/lib/db-migrate-runner.mjs +89 -0
  10. package/bin/lib/db-scaffold-runner.mjs +84 -0
  11. package/bin/lib/db.mjs +261 -0
  12. package/bin/lib/docs-include.mjs +48 -0
  13. package/bin/lib/gen-dicts.mjs +134 -0
  14. package/bin/lib/gen-docs.mjs +288 -0
  15. package/bin/lib/gen-manifest.mjs +102 -0
  16. package/bin/lib/gen-openapi.mjs +195 -0
  17. package/bin/lib/gen-runner.mjs +472 -0
  18. package/bin/lib/gen-stubs.mjs +463 -0
  19. package/bin/lib/gen.mjs +311 -0
  20. package/bin/lib/init.mjs +514 -0
  21. package/bin/lib/introspect.mjs +107 -0
  22. package/bin/lib/mcp.mjs +85 -0
  23. package/bin/lib/postinstall.mjs +56 -0
  24. package/docs/chat-event-protocol.md +85 -0
  25. package/docs/code-style.md +16 -0
  26. package/docs/data-layer.md +246 -0
  27. package/docs/ownership-vs-shadcn-lock.md +102 -0
  28. package/docs/protocol.md +2053 -0
  29. package/docs/releasing.md +110 -0
  30. package/docs/shellnav.md +131 -0
  31. package/package.json +338 -0
  32. package/registry/hooks/hooks.json +26 -0
  33. package/registry/hooks/link-memory-on-start.mjs +46 -0
  34. package/registry/hooks/opus-check-on-stop.mjs +114 -0
  35. package/registry/skills/create-action/SKILL.md +49 -0
  36. package/registry/skills/create-action/scaffold.mjs +122 -0
  37. package/registry/templates/app/_gitignore +3 -0
  38. package/registry/templates/app/_npmrc +1 -0
  39. package/registry/templates/app/_opus/_gitignore +5 -0
  40. package/registry/templates/app/_prettierrc.json +6 -0
  41. package/registry/templates/app/index.html +13 -0
  42. package/registry/templates/app/opus.config.ts +16 -0
  43. package/registry/templates/app/package.json +43 -0
  44. package/registry/templates/app/pnpm-workspace.yaml +11 -0
  45. package/registry/templates/app/public/favicon.svg +4 -0
  46. package/registry/templates/app/src/App.tsx +37 -0
  47. package/registry/templates/app/src/domains/tasks/actions/list.test.ts +34 -0
  48. package/registry/templates/app/src/domains/tasks/actions/list.ts +33 -0
  49. package/registry/templates/app/src/domains/tasks/index.ts +13 -0
  50. package/registry/templates/app/src/index.css +18 -0
  51. package/registry/templates/app/src/main.tsx +25 -0
  52. package/registry/templates/app/tsconfig.json +20 -0
  53. package/registry/templates/app/vite.config.ts +46 -0
  54. package/registry/templates/monorepo/_gitignore +3 -0
  55. package/registry/templates/monorepo/_npmrc +1 -0
  56. package/registry/templates/monorepo/package.json +9 -0
  57. package/registry/templates/monorepo/pnpm-workspace.yaml +14 -0
  58. package/src/ai/ask.ts +64 -0
  59. package/src/ai/drivers/anthropic.ts +309 -0
  60. package/src/ai/index.ts +17 -0
  61. package/src/audit/drivers/console.ts +117 -0
  62. package/src/audit/drivers/pg.ts +172 -0
  63. package/src/audit/index.ts +51 -0
  64. package/src/auth/drivers/better-auth.ts +103 -0
  65. package/src/auth/drivers/jwt.ts +188 -0
  66. package/src/auth/index.ts +9 -0
  67. package/src/client/drivers/fetch.ts +202 -0
  68. package/src/client/index.ts +22 -0
  69. package/src/core/actions.ts +110 -0
  70. package/src/core/audit.ts +239 -0
  71. package/src/core/contracts.ts +137 -0
  72. package/src/core/domain.ts +310 -0
  73. package/src/core/errors.ts +181 -0
  74. package/src/core/index.ts +174 -0
  75. package/src/core/logical-type.ts +31 -0
  76. package/src/core/reactions.ts +81 -0
  77. package/src/core/runtime.ts +1167 -0
  78. package/src/core/schedules.ts +41 -0
  79. package/src/core/types.ts +1356 -0
  80. package/src/data/drivers/kysely.ts +389 -0
  81. package/src/data/index.ts +10 -0
  82. package/src/data/readonly-pool.ts +160 -0
  83. package/src/dsl/eval.ts +136 -0
  84. package/src/dsl/index.ts +29 -0
  85. package/src/dsl/kysely.ts +230 -0
  86. package/src/dsl/loads.ts +123 -0
  87. package/src/dsl/parser.ts +423 -0
  88. package/src/dsl/types.ts +113 -0
  89. package/src/events/drivers/mitt.ts +70 -0
  90. package/src/events/index.ts +9 -0
  91. package/src/log/drivers/pino.ts +57 -0
  92. package/src/log/index.ts +9 -0
  93. package/src/mcp/index.ts +62 -0
  94. package/src/queue/drivers/bullmq.ts +190 -0
  95. package/src/queue/index.ts +9 -0
  96. package/src/scheduler/drivers/node-cron.ts +93 -0
  97. package/src/scheduler/every.ts +45 -0
  98. package/src/scheduler/index.ts +9 -0
  99. package/src/schema/drivers/zod.ts +765 -0
  100. package/src/schema/entity.ts +439 -0
  101. package/src/schema/format/locale.ts +144 -0
  102. package/src/schema/index.ts +65 -0
  103. package/src/schema/openapi.ts +302 -0
  104. package/src/schema/scaffold.ts +160 -0
  105. package/src/server/drivers/fastify.ts +224 -0
  106. package/src/server/drivers/node.ts +386 -0
  107. package/src/server/index.ts +142 -0
  108. package/src/storage/drivers/fs.ts +90 -0
  109. package/src/storage/drivers/s3.ts +117 -0
  110. package/src/storage/index.ts +27 -0
  111. package/src/testing/fake.ts +298 -0
  112. package/src/testing/index.ts +324 -0
  113. package/src/ui/components/patterns/action-form-card.tsx +48 -0
  114. package/src/ui/components/patterns/action-list-dialog.tsx +93 -0
  115. package/src/ui/components/patterns/app-shell.tsx +227 -0
  116. package/src/ui/components/patterns/confirm.tsx +226 -0
  117. package/src/ui/components/patterns/data-state.tsx +75 -0
  118. package/src/ui/components/patterns/form-dialog.tsx +64 -0
  119. package/src/ui/components/patterns/form.tsx +584 -0
  120. package/src/ui/components/patterns/list.tsx +1488 -0
  121. package/src/ui/components/patterns/page.tsx +46 -0
  122. package/src/ui/components/patterns/section-shell.tsx +246 -0
  123. package/src/ui/components/patterns/shell-nav.tsx +150 -0
  124. package/src/ui/components/patterns/sidebar.tsx +89 -0
  125. package/src/ui/components/patterns/split.tsx +93 -0
  126. package/src/ui/components/patterns/trigger.tsx +196 -0
  127. package/src/ui/components/patterns/view.tsx +84 -0
  128. package/src/ui/components/primitives/accordion.tsx +64 -0
  129. package/src/ui/components/primitives/alert-dialog.tsx +190 -0
  130. package/src/ui/components/primitives/alert.tsx +116 -0
  131. package/src/ui/components/primitives/aspect-ratio.tsx +9 -0
  132. package/src/ui/components/primitives/avatar.tsx +107 -0
  133. package/src/ui/components/primitives/badge.tsx +37 -0
  134. package/src/ui/components/primitives/breadcrumb.tsx +109 -0
  135. package/src/ui/components/primitives/button-group.tsx +83 -0
  136. package/src/ui/components/primitives/button.tsx +102 -0
  137. package/src/ui/components/primitives/calendar.tsx +218 -0
  138. package/src/ui/components/primitives/card.tsx +56 -0
  139. package/src/ui/components/primitives/carousel.tsx +239 -0
  140. package/src/ui/components/primitives/chat.tsx +407 -0
  141. package/src/ui/components/primitives/checkbox.tsx +30 -0
  142. package/src/ui/components/primitives/collapsible.tsx +31 -0
  143. package/src/ui/components/primitives/command.tsx +182 -0
  144. package/src/ui/components/primitives/composer.tsx +121 -0
  145. package/src/ui/components/primitives/copyable.tsx +50 -0
  146. package/src/ui/components/primitives/dialog.tsx +147 -0
  147. package/src/ui/components/primitives/drawer.tsx +141 -0
  148. package/src/ui/components/primitives/empty.tsx +104 -0
  149. package/src/ui/components/primitives/field.tsx +246 -0
  150. package/src/ui/components/primitives/icon-picker.tsx +180 -0
  151. package/src/ui/components/primitives/input-group.tsx +168 -0
  152. package/src/ui/components/primitives/input-otp.tsx +75 -0
  153. package/src/ui/components/primitives/input.tsx +72 -0
  154. package/src/ui/components/primitives/item.tsx +193 -0
  155. package/src/ui/components/primitives/kbd.tsx +28 -0
  156. package/src/ui/components/primitives/label.tsx +22 -0
  157. package/src/ui/components/primitives/markdown.tsx +35 -0
  158. package/src/ui/components/primitives/menu.tsx +255 -0
  159. package/src/ui/components/primitives/pagination.tsx +127 -0
  160. package/src/ui/components/primitives/popover.tsx +87 -0
  161. package/src/ui/components/primitives/progress.tsx +29 -0
  162. package/src/ui/components/primitives/radio-group.tsx +43 -0
  163. package/src/ui/components/primitives/resizable.tsx +51 -0
  164. package/src/ui/components/primitives/scroll-area.tsx +56 -0
  165. package/src/ui/components/primitives/select.tsx +479 -0
  166. package/src/ui/components/primitives/separator.tsx +26 -0
  167. package/src/ui/components/primitives/skeleton.tsx +13 -0
  168. package/src/ui/components/primitives/slider.tsx +61 -0
  169. package/src/ui/components/primitives/sonner.tsx +46 -0
  170. package/src/ui/components/primitives/spinner.tsx +29 -0
  171. package/src/ui/components/primitives/switch.tsx +33 -0
  172. package/src/ui/components/primitives/table.tsx +114 -0
  173. package/src/ui/components/primitives/tabs.tsx +104 -0
  174. package/src/ui/components/primitives/textarea.tsx +18 -0
  175. package/src/ui/components/primitives/toggle-group.tsx +81 -0
  176. package/src/ui/components/primitives/toggle.tsx +45 -0
  177. package/src/ui/components/primitives/tooltip.tsx +55 -0
  178. package/src/ui/components/primitives/truncate.tsx +49 -0
  179. package/src/ui/docs/DocBrowser.tsx +90 -0
  180. package/src/ui/docs/changelog.tsx +80 -0
  181. package/src/ui/docs/content/accordion.md +86 -0
  182. package/src/ui/docs/content/action-form-card.md +24 -0
  183. package/src/ui/docs/content/action-form-dialog.md +30 -0
  184. package/src/ui/docs/content/action-form.md +125 -0
  185. package/src/ui/docs/content/action-list-dialog.md +68 -0
  186. package/src/ui/docs/content/action-list.md +194 -0
  187. package/src/ui/docs/content/action-trigger.md +72 -0
  188. package/src/ui/docs/content/action-view.md +47 -0
  189. package/src/ui/docs/content/actions.md +138 -0
  190. package/src/ui/docs/content/ai.md +112 -0
  191. package/src/ui/docs/content/alert-dialog.md +73 -0
  192. package/src/ui/docs/content/alert.md +69 -0
  193. package/src/ui/docs/content/app-shell.md +155 -0
  194. package/src/ui/docs/content/aspect-ratio.md +66 -0
  195. package/src/ui/docs/content/audit.md +84 -0
  196. package/src/ui/docs/content/auth.md +70 -0
  197. package/src/ui/docs/content/avatar.md +94 -0
  198. package/src/ui/docs/content/badge.md +48 -0
  199. package/src/ui/docs/content/breadcrumb.md +87 -0
  200. package/src/ui/docs/content/button-group.md +71 -0
  201. package/src/ui/docs/content/button.md +60 -0
  202. package/src/ui/docs/content/calendar.md +62 -0
  203. package/src/ui/docs/content/card.md +49 -0
  204. package/src/ui/docs/content/carousel.md +85 -0
  205. package/src/ui/docs/content/chat.md +69 -0
  206. package/src/ui/docs/content/checkbox.md +75 -0
  207. package/src/ui/docs/content/cli.md +58 -0
  208. package/src/ui/docs/content/collapsible.md +64 -0
  209. package/src/ui/docs/content/command.md +56 -0
  210. package/src/ui/docs/content/composer.md +50 -0
  211. package/src/ui/docs/content/confirm.md +120 -0
  212. package/src/ui/docs/content/copyable.md +30 -0
  213. package/src/ui/docs/content/customization.md +110 -0
  214. package/src/ui/docs/content/cycle.md +34 -0
  215. package/src/ui/docs/content/data-state.md +47 -0
  216. package/src/ui/docs/content/data.md +99 -0
  217. package/src/ui/docs/content/dialog.md +60 -0
  218. package/src/ui/docs/content/drawer.md +55 -0
  219. package/src/ui/docs/content/empty.md +66 -0
  220. package/src/ui/docs/content/events.md +61 -0
  221. package/src/ui/docs/content/field.md +58 -0
  222. package/src/ui/docs/content/getting-started.md +109 -0
  223. package/src/ui/docs/content/icon-picker.md +51 -0
  224. package/src/ui/docs/content/input-group.md +78 -0
  225. package/src/ui/docs/content/input-otp.md +72 -0
  226. package/src/ui/docs/content/input.md +78 -0
  227. package/src/ui/docs/content/item.md +84 -0
  228. package/src/ui/docs/content/kbd.md +62 -0
  229. package/src/ui/docs/content/label.md +32 -0
  230. package/src/ui/docs/content/log.md +55 -0
  231. package/src/ui/docs/content/markdown.md +41 -0
  232. package/src/ui/docs/content/mcp.md +44 -0
  233. package/src/ui/docs/content/menu.md +114 -0
  234. package/src/ui/docs/content/microcopy.md +83 -0
  235. package/src/ui/docs/content/page.md +34 -0
  236. package/src/ui/docs/content/pagination.md +99 -0
  237. package/src/ui/docs/content/popover.md +49 -0
  238. package/src/ui/docs/content/progress.md +69 -0
  239. package/src/ui/docs/content/queue.md +62 -0
  240. package/src/ui/docs/content/radio-group.md +77 -0
  241. package/src/ui/docs/content/resizable.md +86 -0
  242. package/src/ui/docs/content/router.md +56 -0
  243. package/src/ui/docs/content/runtime.md +77 -0
  244. package/src/ui/docs/content/scheduler.md +66 -0
  245. package/src/ui/docs/content/scroll-area.md +89 -0
  246. package/src/ui/docs/content/section-shell.md +121 -0
  247. package/src/ui/docs/content/select.md +342 -0
  248. package/src/ui/docs/content/separator.md +33 -0
  249. package/src/ui/docs/content/sidebar.md +38 -0
  250. package/src/ui/docs/content/skeleton.md +34 -0
  251. package/src/ui/docs/content/slider.md +64 -0
  252. package/src/ui/docs/content/spinner.md +37 -0
  253. package/src/ui/docs/content/split.md +33 -0
  254. package/src/ui/docs/content/storage.md +69 -0
  255. package/src/ui/docs/content/switch.md +69 -0
  256. package/src/ui/docs/content/table.md +102 -0
  257. package/src/ui/docs/content/tabs.md +94 -0
  258. package/src/ui/docs/content/testing.md +89 -0
  259. package/src/ui/docs/content/textarea.md +30 -0
  260. package/src/ui/docs/content/toast.md +67 -0
  261. package/src/ui/docs/content/toggle-group.md +81 -0
  262. package/src/ui/docs/content/toggle.md +72 -0
  263. package/src/ui/docs/content/tokens.md +171 -0
  264. package/src/ui/docs/content/tooltip.md +50 -0
  265. package/src/ui/docs/content/truncate.md +37 -0
  266. package/src/ui/docs/content/ui.md +40 -0
  267. package/src/ui/docs/content/upgrading.md +48 -0
  268. package/src/ui/docs/doc-client.tsx +214 -0
  269. package/src/ui/docs/doc.tsx +301 -0
  270. package/src/ui/docs/folder.tsx +149 -0
  271. package/src/ui/docs/index.ts +21 -0
  272. package/src/ui/docs/markdown.tsx +130 -0
  273. package/src/ui/docs/md-raw.d.ts +4 -0
  274. package/src/ui/docs/plugin.ts +104 -0
  275. package/src/ui/docs/registry.tsx +424 -0
  276. package/src/ui/docs/standalone.tsx +107 -0
  277. package/src/ui/drivers/react.tsx +627 -0
  278. package/src/ui/index.ts +92 -0
  279. package/src/ui/lib/cn.ts +10 -0
  280. package/src/ui/lib/zod-pt-br.ts +38 -0
  281. package/src/ui/meta.ts +412 -0
  282. package/src/ui/react.tsx +235 -0
  283. package/src/ui/router.ts +96 -0
  284. package/src/ui/theme.css +234 -0
  285. package/src/vite/design.ts +652 -0
  286. package/src/vite/index.ts +8 -0
@@ -0,0 +1,407 @@
1
+ import * as React from 'react'
2
+ import type { ChatEvent } from '../../../core/index.ts'
3
+ import { cn } from '../../lib/cn.ts'
4
+ import { Composer } from './composer.tsx'
5
+ import { Markdown } from './markdown.tsx'
6
+
7
+ export interface ChatMessage {
8
+ role: 'user' | 'assistant'
9
+ content: string
10
+ }
11
+
12
+ /** Artefato produzido na conversa (evento `artifact` do protocolo). */
13
+ export interface ChatArtifact {
14
+ kind: string
15
+ ref: string
16
+ title?: string
17
+ }
18
+
19
+ /** Item do transcript no modo CONTROLADO (o app é o dono do estado — ex.: sala
20
+ * server-autoritativa com replay, como o ChatRail do Maestro). */
21
+ export type ChatTranscriptItem =
22
+ | { role: 'user' | 'assistant' | 'error'; content: string }
23
+ | { role: 'artifact'; artifact: ChatArtifact }
24
+
25
+ /** Feedback humano do tool em uso (default pt-BR; override por prop). */
26
+ function defaultHumanizeTool(name: string): string {
27
+ const n = name.toLowerCase()
28
+ if (/read|glob|grep|search|list|view|query/.test(n)) return 'Consultando…'
29
+ if (/edit|write|create|update/.test(n)) return 'Escrevendo…'
30
+ if (/fetch|web/.test(n)) return 'Pesquisando…'
31
+ return 'Trabalhando…'
32
+ }
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
+ */
52
+ 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
+ return (
100
+ <div data-slot="chat-user" className="break-words rounded-xl bg-card px-3 py-2 text-sm leading-snug ring-1 ring-edge shadow-card">
101
+ <div
102
+ ref={ref}
103
+ data-slot="chat-user-body"
104
+ // Teto NOS DOIS estados, e o do aberto não é zelo: soltar a altura recriava o
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
+ )}
132
+ </div>
133
+ )
134
+ }
135
+
136
+ export interface ChatProps {
137
+ /** Modo AUTOGERENCIADO: envia a conversa (o histórico, já com a nova mensagem) e devolve
138
+ * a resposta — `Promise<string>` (request/response) OU `AsyncIterable<ChatEvent>`
139
+ * (streaming; docs/chat-event-protocol.md). No Opus, o backend liga em
140
+ * `runtime.aiFor(base).run(...)` ou `.runStream(...)`. Ignorado no modo controlado. */
141
+ send?: (messages: ChatMessage[]) => Promise<string> | AsyncIterable<ChatEvent>
142
+ /** Modo CONTROLADO: o transcript vem daqui e o app é o dono do estado (transporte
143
+ * próprio — SSE com replay, etc.). Presente → `onSend`/`busy`/`activity` assumem. */
144
+ messages?: ChatTranscriptItem[]
145
+ /** Modo controlado: recebe o texto enviado. Retornar `false` devolve o texto ao
146
+ * composer (falha de envio — nada de perder o que o usuário digitou). */
147
+ onSend?: (text: string) => void | boolean | Promise<void | boolean>
148
+ /** Modo controlado: trava o composer enquanto o turno corre. */
149
+ busy?: boolean
150
+ /** Modo controlado: indicador vivo — `undefined` esconde, `null` mostra "Pensando…",
151
+ * string mostra o rótulo (já humanizado pelo app). */
152
+ activity?: string | null
153
+ /** Avisos do app acima do composer (credencial, agente desatualizado…). */
154
+ notice?: React.ReactNode
155
+ /** Seletores discretos na barra do composer (o `actions` do `<Composer>`): agente,
156
+ * app, escopo… O lugar deles é a barra — `notice` é pra aviso, não pra controle. */
157
+ composerActions?: React.ReactNode
158
+ /** Texto do estado vazio (centrado, some quando a conversa começa). Não entra no
159
+ * transcript — é apresentação, não fala do assistente. */
160
+ greeting?: string
161
+ /** Histórico inicial do modo autogerenciado (reidratação). Troque a `key` do componente
162
+ * ao trocar de conversa — o estado interno reinicia com estas mensagens. */
163
+ initialMessages?: ChatMessage[]
164
+ /** Modo autogerenciado: conversa que COMEÇA pelo assistente — roda uma vez no mount
165
+ * quando o transcript nasce vazio (agente proativo: relatório recém-criado, sessão
166
+ * nova…). Mesmo contrato de retorno do `send`, sem mensagem do usuário. */
167
+ kickoff?: () => Promise<string> | AsyncIterable<ChatEvent>
168
+ /** Render do evento `artifact` (card, iframe, preview…). Default: link com o título. */
169
+ renderArtifact?: (artifact: ChatArtifact) => React.ReactNode
170
+ /** Rótulo humano do tool em uso no indicador vivo (modo autogerenciado). */
171
+ humanizeTool?: (name: string, detail?: string) => string
172
+ placeholder?: string
173
+ className?: string
174
+ }
175
+
176
+ function isAsyncIterable(value: unknown): value is AsyncIterable<ChatEvent> {
177
+ return typeof value === 'object' && value !== null && Symbol.asyncIterator in value
178
+ }
179
+
180
+ /** Agrupa o transcript em turnos (mensagem do usuário + o que veio em resposta) —
181
+ * o cabeçalho do turno fica sticky enquanto o turno está em vista. */
182
+ function groupTurns(items: ChatTranscriptItem[]): Array<{ user: ChatTranscriptItem | null; rest: ChatTranscriptItem[] }> {
183
+ const turns: Array<{ user: ChatTranscriptItem | null; rest: ChatTranscriptItem[] }> = []
184
+ for (const item of items) {
185
+ if (item.role === 'user') turns.push({ user: item, rest: [] })
186
+ else {
187
+ if (turns.length === 0) turns.push({ user: null, rest: [] })
188
+ turns[turns.length - 1].rest.push(item)
189
+ }
190
+ }
191
+ return turns
192
+ }
193
+
194
+ /**
195
+ * Chat da casa: lista de mensagens + composer (Enter envia / Shift+Enter quebra linha),
196
+ * transcript em turnos com o cabeçalho sticky (a pergunta fica à vista enquanto a
197
+ * resposta rola), assistente em Markdown direto no corpo, tool como indicador vivo
198
+ * (nunca mensagem) e artefatos via `renderArtifact`. Dois modos:
199
+ *
200
+ * - **Autogerenciado** (`send`): o componente é dono da conversa; o `send` devolve
201
+ * `Promise<string>` ou `AsyncIterable<ChatEvent>` (streaming).
202
+ * - **Controlado** (`messages` + `onSend` + `busy`/`activity`): o app é dono do estado —
203
+ * o caso de transporte próprio (SSE server-autoritativo com replay, como o Maestro).
204
+ *
205
+ * Dê altura ao container (ex.: `h-full`).
206
+ */
207
+ export function Chat({
208
+ send,
209
+ messages,
210
+ onSend,
211
+ busy: busyProp,
212
+ activity: activityProp,
213
+ notice,
214
+ composerActions,
215
+ greeting,
216
+ initialMessages,
217
+ kickoff,
218
+ renderArtifact,
219
+ humanizeTool = defaultHumanizeTool,
220
+ placeholder = 'Escreva uma mensagem…',
221
+ className,
222
+ }: ChatProps): React.ReactElement {
223
+ const controlled = messages !== undefined
224
+ const [ownItems, setOwnItems] = React.useState<ChatTranscriptItem[]>(() => [...(initialMessages ?? [])])
225
+ const [input, setInput] = React.useState('')
226
+ const [ownBusy, setOwnBusy] = React.useState(false)
227
+ const [ownActivity, setOwnActivity] = React.useState<string | null>(null)
228
+ const scrollRef = React.useRef<HTMLDivElement>(null)
229
+
230
+ const items = controlled ? messages : ownItems
231
+ const busy = controlled ? (busyProp ?? false) : ownBusy
232
+ // Indicador: no controlado o app manda (undefined esconde); no autogerenciado segue o busy.
233
+ const indicator = controlled ? activityProp : ownBusy ? ownActivity : undefined
234
+
235
+ React.useEffect(() => {
236
+ scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight })
237
+ }, [items, indicator])
238
+
239
+ const kickoffRan = React.useRef(false)
240
+ React.useEffect(() => {
241
+ if (controlled || kickoff === undefined || kickoffRan.current) return
242
+ if (ownItems.length > 0) return
243
+ kickoffRan.current = true
244
+ void consume(kickoff)
245
+ // eslint-disable-next-line react-hooks/exhaustive-deps
246
+ }, [])
247
+
248
+ async function submit(): Promise<void> {
249
+ const text = input.trim()
250
+ if (text === '' || busy) return
251
+ setInput('')
252
+
253
+ if (controlled) {
254
+ const ok = await onSend?.(text)
255
+ if (ok === false) setInput(text)
256
+ return
257
+ }
258
+
259
+ const next: ChatTranscriptItem[] = [...ownItems, { role: 'user', content: text }]
260
+ setOwnItems(next)
261
+ const transcript = next.filter((i): i is ChatMessage => i.role === 'user' || i.role === 'assistant')
262
+ await consume(() => send!(transcript))
263
+ }
264
+
265
+ // Turno autogerenciado (send ou kickoff): consome string/stream pro transcript próprio.
266
+ async function consume(run: () => Promise<string> | AsyncIterable<ChatEvent>): Promise<void> {
267
+ setOwnBusy(true)
268
+ setOwnActivity(null)
269
+ try {
270
+ const result = run()
271
+ if (isAsyncIterable(result)) {
272
+ let streaming = false
273
+ for await (const event of result) {
274
+ if (event.type === 'text') {
275
+ setOwnActivity(null)
276
+ setOwnItems((list) => {
277
+ const last = list[list.length - 1]
278
+ if (streaming && last !== undefined && last.role === 'assistant') {
279
+ return [...list.slice(0, -1), { role: 'assistant', content: last.content + event.delta }]
280
+ }
281
+ streaming = true
282
+ return [...list, { role: 'assistant', content: event.delta }]
283
+ })
284
+ } else if (event.type === 'tool') {
285
+ streaming = false
286
+ setOwnActivity(humanizeTool(event.name, event.detail))
287
+ } else if (event.type === 'artifact') {
288
+ streaming = false
289
+ setOwnItems((list) => [
290
+ ...list,
291
+ { role: 'artifact', artifact: { kind: event.kind, ref: event.ref, title: event.title } },
292
+ ])
293
+ } else if (event.type === 'done') {
294
+ if (!event.ok) {
295
+ setOwnItems((list) => [...list, { role: 'error', content: event.error ?? 'Desculpe, algo falhou.' }])
296
+ }
297
+ }
298
+ }
299
+ } else {
300
+ const reply = await result
301
+ setOwnItems((list) => [...list, { role: 'assistant', content: reply }])
302
+ }
303
+ } catch {
304
+ setOwnItems((list) => [...list, { role: 'error', content: 'Desculpe, algo falhou.' }])
305
+ } finally {
306
+ setOwnBusy(false)
307
+ setOwnActivity(null)
308
+ }
309
+ }
310
+
311
+ const indicatorVisible = indicator !== undefined
312
+
313
+ return (
314
+ <div data-slot="chat" className={cn('flex h-full flex-col', className)}>
315
+ <div ref={scrollRef} data-slot="chat-scroll" className="flex min-h-0 flex-1 flex-col gap-3 overflow-y-auto p-4">
316
+ {items.length === 0 && !indicatorVisible && greeting !== undefined && (
317
+ <div data-slot="chat-empty" className="m-auto max-w-[280px] text-center text-muted-foreground">
318
+ <div className="mb-2 text-4xl">✦</div>
319
+ <p className="text-sm leading-relaxed">{greeting}</p>
320
+ </div>
321
+ )}
322
+ {groupTurns(items).map((turn, ti) => (
323
+ // Hierarquia do espaço: as falas de um mesmo turno são um raciocínio contínuo
324
+ // (o agente narra o que vai fazendo), então andam juntas; quem separa é o gap
325
+ // MAIOR entre turnos. Com o mesmo gap nos dois níveis, um turno de oito falas
326
+ // curtas virava uma parede uniforme, sem começo nem fim visíveis.
327
+ <div key={ti} className="flex flex-col gap-1">
328
+ {turn.user !== null && turn.user.role === 'user' && (
329
+ // Cabeçalho do turno: gruda no topo enquanto o turno está em vista (igual
330
+ // Claude) — a mensagem do usuário num card, não num balão preenchido.
331
+ <div className="sticky top-0 z-10 py-1.5">
332
+ <UserTurn content={turn.user.content} />
333
+ </div>
334
+ )}
335
+ {turn.rest.map((item, i) => {
336
+ if (item.role === 'artifact') {
337
+ return (
338
+ <div key={i} data-slot="chat-artifact" className="flex justify-start">
339
+ {renderArtifact !== undefined ? (
340
+ renderArtifact(item.artifact)
341
+ ) : (
342
+ <a
343
+ href={item.artifact.ref}
344
+ target="_blank"
345
+ rel="noreferrer"
346
+ className="rounded-xl border bg-card px-3.5 py-2 text-sm underline-offset-2 hover:underline"
347
+ >
348
+ {item.artifact.title ?? item.artifact.ref}
349
+ </a>
350
+ )}
351
+ </div>
352
+ )
353
+ }
354
+ if (item.role === 'error') {
355
+ return (
356
+ <div
357
+ key={i}
358
+ className="max-w-[88%] self-start rounded-xl border border-destructive/30 bg-destructive/10 px-3.5 py-2.5 text-sm text-destructive"
359
+ >
360
+ {item.content}
361
+ </div>
362
+ )
363
+ }
364
+ // O assistente responde direto no corpo, em Markdown (convenção da casa).
365
+ return (
366
+ <div
367
+ key={i}
368
+ className="break-words px-1 text-sm leading-snug"
369
+ >
370
+ <Markdown content={item.content} />
371
+ </div>
372
+ )
373
+ })}
374
+ </div>
375
+ ))}
376
+ {indicatorVisible && (
377
+ <div data-slot="chat-activity" className="flex items-center gap-2 py-1 text-xs text-muted-foreground">
378
+ <span className="flex gap-1">
379
+ {[0, 1, 2].map((i) => (
380
+ <span
381
+ key={i}
382
+ className="h-1.5 w-1.5 animate-bounce rounded-full bg-muted-foreground/50"
383
+ style={{ animationDelay: `${i * 150}ms` }}
384
+ />
385
+ ))}
386
+ </span>
387
+ <span>{indicator ?? 'Pensando…'}</span>
388
+ </div>
389
+ )}
390
+ </div>
391
+ {/* Composer da casa (pílula elevada, enviar dentro). Extraído no <Composer> — o Chat
392
+ liga texto/envio e repassa `composerActions` pros seletores da conversa (agente,
393
+ app…); o <Composer> sozinho segue sendo o caminho de quem não tem chat. */}
394
+ <div className="shrink-0 p-3">
395
+ {notice}
396
+ <Composer
397
+ value={input}
398
+ onChange={setInput}
399
+ onSubmit={() => void submit()}
400
+ busy={busy}
401
+ placeholder={placeholder}
402
+ actions={composerActions}
403
+ />
404
+ </div>
405
+ </div>
406
+ )
407
+ }
@@ -0,0 +1,30 @@
1
+ import * as React from "react"
2
+ import { CheckIcon } from "lucide-react"
3
+ import { Checkbox as CheckboxPrimitive } from "radix-ui"
4
+
5
+ import { cn } from '../../lib/cn.ts'
6
+
7
+ function Checkbox({
8
+ className,
9
+ ...props
10
+ }: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
11
+ return (
12
+ <CheckboxPrimitive.Root
13
+ data-slot="checkbox"
14
+ className={cn(
15
+ "peer size-4 shrink-0 rounded-[4px] border border-input transition-shadow outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:bg-input/30 dark:aria-invalid:ring-destructive/40 dark:data-[state=checked]:bg-primary",
16
+ className
17
+ )}
18
+ {...props}
19
+ >
20
+ <CheckboxPrimitive.Indicator
21
+ data-slot="checkbox-indicator"
22
+ className="grid place-content-center text-current transition-none"
23
+ >
24
+ <CheckIcon className="size-3.5" />
25
+ </CheckboxPrimitive.Indicator>
26
+ </CheckboxPrimitive.Root>
27
+ )
28
+ }
29
+
30
+ export { Checkbox }
@@ -0,0 +1,31 @@
1
+ import { Collapsible as CollapsiblePrimitive } from "radix-ui"
2
+
3
+ function Collapsible({
4
+ ...props
5
+ }: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
6
+ return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
7
+ }
8
+
9
+ function CollapsibleTrigger({
10
+ ...props
11
+ }: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
12
+ return (
13
+ <CollapsiblePrimitive.CollapsibleTrigger
14
+ data-slot="collapsible-trigger"
15
+ {...props}
16
+ />
17
+ )
18
+ }
19
+
20
+ function CollapsibleContent({
21
+ ...props
22
+ }: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
23
+ return (
24
+ <CollapsiblePrimitive.CollapsibleContent
25
+ data-slot="collapsible-content"
26
+ {...props}
27
+ />
28
+ )
29
+ }
30
+
31
+ export { Collapsible, CollapsibleTrigger, CollapsibleContent }
@@ -0,0 +1,182 @@
1
+ import * as React from "react"
2
+ import { Command as CommandPrimitive } from "cmdk"
3
+ import { SearchIcon } from "lucide-react"
4
+
5
+ import { cn } from '../../lib/cn.ts'
6
+ import {
7
+ Dialog,
8
+ DialogContent,
9
+ DialogDescription,
10
+ DialogHeader,
11
+ DialogTitle,
12
+ } from './dialog.tsx'
13
+
14
+ function Command({
15
+ className,
16
+ ...props
17
+ }: React.ComponentProps<typeof CommandPrimitive>) {
18
+ return (
19
+ <CommandPrimitive
20
+ data-slot="command"
21
+ className={cn(
22
+ "flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
23
+ className
24
+ )}
25
+ {...props}
26
+ />
27
+ )
28
+ }
29
+
30
+ function CommandDialog({
31
+ title = "Command Palette",
32
+ description = "Search for a command to run...",
33
+ children,
34
+ className,
35
+ showCloseButton = true,
36
+ ...props
37
+ }: React.ComponentProps<typeof Dialog> & {
38
+ title?: string
39
+ description?: string
40
+ className?: string
41
+ showCloseButton?: boolean
42
+ }) {
43
+ return (
44
+ <Dialog {...props}>
45
+ <DialogHeader className="sr-only">
46
+ <DialogTitle>{title}</DialogTitle>
47
+ <DialogDescription>{description}</DialogDescription>
48
+ </DialogHeader>
49
+ <DialogContent
50
+ className={cn("overflow-hidden p-0", className)}
51
+ showCloseButton={showCloseButton}
52
+ >
53
+ <Command className="**:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
54
+ {children}
55
+ </Command>
56
+ </DialogContent>
57
+ </Dialog>
58
+ )
59
+ }
60
+
61
+ function CommandInput({
62
+ className,
63
+ ...props
64
+ }: React.ComponentProps<typeof CommandPrimitive.Input>) {
65
+ return (
66
+ <div
67
+ data-slot="command-input-wrapper"
68
+ className="flex h-9 items-center gap-2 border-b px-3"
69
+ >
70
+ <SearchIcon className="size-4 shrink-0 opacity-50" />
71
+ <CommandPrimitive.Input
72
+ data-slot="command-input"
73
+ className={cn(
74
+ "flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
75
+ className
76
+ )}
77
+ {...props}
78
+ />
79
+ </div>
80
+ )
81
+ }
82
+
83
+ function CommandList({
84
+ className,
85
+ ...props
86
+ }: React.ComponentProps<typeof CommandPrimitive.List>) {
87
+ return (
88
+ <CommandPrimitive.List
89
+ data-slot="command-list"
90
+ className={cn(
91
+ "max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
92
+ className
93
+ )}
94
+ {...props}
95
+ />
96
+ )
97
+ }
98
+
99
+ function CommandEmpty({
100
+ ...props
101
+ }: React.ComponentProps<typeof CommandPrimitive.Empty>) {
102
+ return (
103
+ <CommandPrimitive.Empty
104
+ data-slot="command-empty"
105
+ className="py-6 text-center text-sm"
106
+ {...props}
107
+ />
108
+ )
109
+ }
110
+
111
+ function CommandGroup({
112
+ className,
113
+ ...props
114
+ }: React.ComponentProps<typeof CommandPrimitive.Group>) {
115
+ return (
116
+ <CommandPrimitive.Group
117
+ data-slot="command-group"
118
+ className={cn(
119
+ "overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
120
+ className
121
+ )}
122
+ {...props}
123
+ />
124
+ )
125
+ }
126
+
127
+ function CommandSeparator({
128
+ className,
129
+ ...props
130
+ }: React.ComponentProps<typeof CommandPrimitive.Separator>) {
131
+ return (
132
+ <CommandPrimitive.Separator
133
+ data-slot="command-separator"
134
+ className={cn("-mx-1 h-px bg-border", className)}
135
+ {...props}
136
+ />
137
+ )
138
+ }
139
+
140
+ function CommandItem({
141
+ className,
142
+ ...props
143
+ }: React.ComponentProps<typeof CommandPrimitive.Item>) {
144
+ return (
145
+ <CommandPrimitive.Item
146
+ data-slot="command-item"
147
+ className={cn(
148
+ "relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
149
+ className
150
+ )}
151
+ {...props}
152
+ />
153
+ )
154
+ }
155
+
156
+ function CommandShortcut({
157
+ className,
158
+ ...props
159
+ }: React.ComponentProps<"span">) {
160
+ return (
161
+ <span
162
+ data-slot="command-shortcut"
163
+ className={cn(
164
+ "ml-auto text-xs tracking-widest text-muted-foreground",
165
+ className
166
+ )}
167
+ {...props}
168
+ />
169
+ )
170
+ }
171
+
172
+ export {
173
+ Command,
174
+ CommandDialog,
175
+ CommandInput,
176
+ CommandList,
177
+ CommandEmpty,
178
+ CommandGroup,
179
+ CommandItem,
180
+ CommandShortcut,
181
+ CommandSeparator,
182
+ }