primocode 10.15.2 → 10.17.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.
Files changed (245) hide show
  1. package/LICENSES/edit-ia-pro.txt +32 -0
  2. package/LICENSES/fontes-OFL.txt +476 -0
  3. package/LICENSES/hyperframes-Apache-2.0.txt +205 -0
  4. package/LICENSES/permanent-marker-Apache-2.0.txt +204 -0
  5. package/LICENSES/selfie-multiclass-Apache-2.0.txt +227 -0
  6. package/LICENSES/three-MIT.txt +23 -0
  7. package/LICENSES/video-use-MIT.txt +24 -0
  8. package/LICENSES/woven-sfx-CC0.txt +16 -0
  9. package/LICENSES/yunet-MIT.txt +21 -0
  10. package/README.md +2 -2
  11. package/bin/primocode.js +74 -9
  12. package/editor/LICENSE.md +11 -0
  13. package/editor/README.md +321 -0
  14. package/editor/SKILL.md +307 -0
  15. package/editor/THIRD_PARTY_NOTICES.md +228 -0
  16. package/editor/assets/baixar.json +591 -0
  17. package/editor/assets/fonts/anton/OFL.txt +93 -0
  18. package/editor/assets/fonts/caveat/OFL.txt +93 -0
  19. package/editor/assets/fonts/figtree/OFL.txt +93 -0
  20. package/editor/assets/fonts/fonts.json +10 -0
  21. package/editor/assets/fonts/montserrat/OFL.txt +93 -0
  22. package/editor/assets/fonts/permanentmarker/LICENSE.txt +202 -0
  23. package/editor/assets/models/SELFIE_MULTICLASS_LICENSE.txt +227 -0
  24. package/editor/assets/models/YUNET_LICENSE.txt +21 -0
  25. package/editor/assets/sfx/licencas/bass_drop.txt +32 -0
  26. package/editor/assets/sfx/licencas/cash.txt +76 -0
  27. package/editor/assets/sfx/licencas/charge.txt +31 -0
  28. package/editor/assets/sfx/licencas/click.txt +46 -0
  29. package/editor/assets/sfx/licencas/coin.txt +50 -0
  30. package/editor/assets/sfx/licencas/count.txt +47 -0
  31. package/editor/assets/sfx/licencas/data.txt +31 -0
  32. package/editor/assets/sfx/licencas/error.txt +32 -0
  33. package/editor/assets/sfx/licencas/flash.txt +31 -0
  34. package/editor/assets/sfx/licencas/gears.txt +82 -0
  35. package/editor/assets/sfx/licencas/keyboard.txt +48 -0
  36. package/editor/assets/sfx/licencas/metal_hit.txt +62 -0
  37. package/editor/assets/sfx/licencas/metal_riser.txt +61 -0
  38. package/editor/assets/sfx/licencas/mouse_click.txt +50 -0
  39. package/editor/assets/sfx/licencas/notification.txt +54 -0
  40. package/editor/assets/sfx/licencas/pop.txt +38 -0
  41. package/editor/assets/sfx/licencas/rewind.txt +32 -0
  42. package/editor/assets/sfx/licencas/rise.txt +46 -0
  43. package/editor/assets/sfx/licencas/sinteticos.txt +21 -0
  44. package/editor/assets/sfx/licencas/snap.txt +50 -0
  45. package/editor/assets/sfx/licencas/success.txt +40 -0
  46. package/editor/assets/sfx/licencas/synth_hit.txt +32 -0
  47. package/editor/assets/sfx/licencas/tick.txt +14 -0
  48. package/editor/assets/sfx/licencas/whoosh.txt +47 -0
  49. package/editor/assets/sfx/licencas/wind.txt +31 -0
  50. package/editor/assets/sfx/sfx.json +832 -0
  51. package/editor/assets/sfx/sfx_sinteticos.json +317 -0
  52. package/editor/assets/sfx/sfx_woven.json +387 -0
  53. package/editor/assets/sfx/woven/LICENCA.md +16 -0
  54. package/editor/fichas/briefing-do-video.md +45 -0
  55. package/editor/fichas/conferencia-da-amostra.md +56 -0
  56. package/editor/fichas/matriz-de-ganchos.csv +3 -0
  57. package/editor/fichas/padrao.json +16 -0
  58. package/editor/fichas/planilha-de-cenas.csv +4 -0
  59. package/editor/fichas/planilha-de-versoes.csv +4 -0
  60. package/editor/fichas/plano-em-blocos.md +32 -0
  61. package/editor/fichas/registro-de-materiais.json +27 -0
  62. package/editor/helpers/assets_ocr/ocr_vision.swift +41 -0
  63. package/editor/helpers/bordas_precisas.py +272 -0
  64. package/editor/helpers/camera_viva.py +426 -0
  65. package/editor/helpers/captions.py +601 -0
  66. package/editor/helpers/common.py +308 -0
  67. package/editor/helpers/compose_final.py +3555 -0
  68. package/editor/helpers/conferir_corte.py +234 -0
  69. package/editor/helpers/conferir_dados.py +405 -0
  70. package/editor/helpers/contact_sheet.py +282 -0
  71. package/editor/helpers/corte_por_onda.py +433 -0
  72. package/editor/helpers/corte_seco.py +595 -0
  73. package/editor/helpers/denoise.py +971 -0
  74. package/editor/helpers/efeitos.py +408 -0
  75. package/editor/helpers/efeitos_gerar.py +159 -0
  76. package/editor/helpers/elevenlabs_api.py +442 -0
  77. package/editor/helpers/face_layout.py +570 -0
  78. package/editor/helpers/fetch_images.py +423 -0
  79. package/editor/helpers/ganho_por_trecho.py +214 -0
  80. package/editor/helpers/gen_image.py +312 -0
  81. package/editor/helpers/gen_music.py +316 -0
  82. package/editor/helpers/gen_sfx.py +148 -0
  83. package/editor/helpers/grade.py +307 -0
  84. package/editor/helpers/icloud.py +119 -0
  85. package/editor/helpers/make_sfx.py +940 -0
  86. package/editor/helpers/matte.py +1342 -0
  87. package/editor/helpers/medir_referencia.py +2232 -0
  88. package/editor/helpers/mixar_trilha.py +354 -0
  89. package/editor/helpers/montar_corte.py +295 -0
  90. package/editor/helpers/motion/__init__.py +13 -0
  91. package/editor/helpers/motion/engine.py +1915 -0
  92. package/editor/helpers/motion/registry.py +510 -0
  93. package/editor/helpers/motion/render.py +665 -0
  94. package/editor/helpers/motion/templates/barra_progresso.py +370 -0
  95. package/editor/helpers/motion/templates/carimbo.py +132 -0
  96. package/editor/helpers/motion/templates/checklist.py +271 -0
  97. package/editor/helpers/motion/templates/comentario_cta.py +380 -0
  98. package/editor/helpers/motion/templates/contador.py +222 -0
  99. package/editor/helpers/motion/templates/destaque.py +263 -0
  100. package/editor/helpers/motion/templates/etiqueta_preco.py +461 -0
  101. package/editor/helpers/motion/templates/grafico_barras.py +510 -0
  102. package/editor/helpers/motion/templates/grafico_linha.py +462 -0
  103. package/editor/helpers/motion/templates/notificacao.py +266 -0
  104. package/editor/helpers/motion/templates/palavra_impacto.py +410 -0
  105. package/editor/helpers/motion/templates/passos.py +321 -0
  106. package/editor/helpers/motion/templates/riscado.py +298 -0
  107. package/editor/helpers/motion/templates/selo_garantia.py +150 -0
  108. package/editor/helpers/motion_render.py +241 -0
  109. package/editor/helpers/motion_sugerir.py +961 -0
  110. package/editor/helpers/noise_check.py +134 -0
  111. package/editor/helpers/pack_transcripts.py +223 -0
  112. package/editor/helpers/palavras_na_onda.py +176 -0
  113. package/editor/helpers/plano_sfx.py +839 -0
  114. package/editor/helpers/portao.py +427 -0
  115. package/editor/helpers/preview_server.py +1177 -0
  116. package/editor/helpers/rastrear_rosto.py +298 -0
  117. package/editor/helpers/render_cut.py +1256 -0
  118. package/editor/helpers/respiros.py +89 -0
  119. package/editor/helpers/sincronizar_cameras.py +571 -0
  120. package/editor/helpers/speech_regions.py +423 -0
  121. package/editor/helpers/style_catalog.json +231 -0
  122. package/editor/helpers/style_samples.py +246 -0
  123. package/editor/helpers/tela_verde.py +2113 -0
  124. package/editor/helpers/text_render.py +2059 -0
  125. package/editor/helpers/texto_na_tela.py +366 -0
  126. package/editor/helpers/timeline_view.py +292 -0
  127. package/editor/helpers/tira_de_quadros.py +113 -0
  128. package/editor/helpers/transcribe.py +734 -0
  129. package/editor/helpers/transcribe_batch.py +129 -0
  130. package/editor/helpers/trilhas.py +1146 -0
  131. package/editor/helpers/trilhas_humores.json +66 -0
  132. package/editor/helpers/valores.py +187 -0
  133. package/editor/helpers/verify_cut.py +820 -0
  134. package/editor/helpers/voice_levels.py +233 -0
  135. package/editor/helpers/voz_check.py +386 -0
  136. package/editor/helpers/voz_limpa.py +81 -0
  137. package/editor/helpers/watch_preview.py +372 -0
  138. package/editor/modelo-projeto/LEIA.md +402 -0
  139. package/editor/modelo-projeto/assets/fontes/OFL-Caveat.txt +93 -0
  140. package/editor/modelo-projeto/assets/fontes/OFL-JetBrainsMono.txt +93 -0
  141. package/editor/modelo-projeto/assets/fontes/OFL-Montserrat.txt +93 -0
  142. package/editor/modelo-projeto/assets/fontes/larguras.json +1 -0
  143. package/editor/modelo-projeto/assets/three/BufferGeometryUtils.js +1438 -0
  144. package/editor/modelo-projeto/assets/three/GLTFLoader.js +4895 -0
  145. package/editor/modelo-projeto/assets/three/LICENSE +21 -0
  146. package/editor/modelo-projeto/assets/three/ORIGEM.txt +17 -0
  147. package/editor/modelo-projeto/assets/three/RoomEnvironment.js +185 -0
  148. package/editor/modelo-projeto/assets/three/three.core.min.js +6 -0
  149. package/editor/modelo-projeto/assets/three/three.module.min.js +6 -0
  150. package/editor/modelo-projeto/build.py +4828 -0
  151. package/editor/modelo-projeto/compositions/cenas/2d/k2d_antes_depois.html +199 -0
  152. package/editor/modelo-projeto/compositions/cenas/2d/k2d_arquivo.html +232 -0
  153. package/editor/modelo-projeto/compositions/cenas/2d/k2d_busca.html +315 -0
  154. package/editor/modelo-projeto/compositions/cenas/2d/k2d_comando.html +304 -0
  155. package/editor/modelo-projeto/compositions/cenas/2d/k2d_conversa.html +217 -0
  156. package/editor/modelo-projeto/compositions/cenas/2d/k2d_esquema.html +289 -0
  157. package/editor/modelo-projeto/compositions/cenas/2d/k2d_medidor.html +249 -0
  158. package/editor/modelo-projeto/compositions/cenas/2d/k2d_notas.html +233 -0
  159. package/editor/modelo-projeto/compositions/cenas/3d/k3d_caminho.html +197 -0
  160. package/editor/modelo-projeto/compositions/cenas/3d/k3d_explodida.html +152 -0
  161. package/editor/modelo-projeto/compositions/cenas/3d/k3d_tela.html +167 -0
  162. package/editor/modelo-projeto/compositions/cenas/3d/k3d_volume.html +124 -0
  163. package/editor/modelo-projeto/compositions/cenas/3d/motor-three.js +728 -0
  164. package/editor/modelo-projeto/compositions/cenas/LEIA.md +177 -0
  165. package/editor/modelo-projeto/ferramentas/medir_fontes.py +65 -0
  166. package/editor/modelo-projeto/ferramentas/pessoa_recortada.py +176 -0
  167. package/editor/modelo-projeto/ferramentas/rastrear_maos.py +189 -0
  168. package/editor/modelo-projeto/hyperframes.json +12 -0
  169. package/editor/modelo-projeto/package.json +11 -0
  170. package/editor/modelo-projeto/projeto.json +16 -0
  171. package/editor/preview/app.css +1009 -0
  172. package/editor/preview/app.js +2122 -0
  173. package/editor/preview/index.html +307 -0
  174. package/editor/pyproject.toml +23 -0
  175. package/editor/references/3d.md +236 -0
  176. package/editor/references/animacao-catalogo.md +672 -0
  177. package/editor/references/animacao.md +498 -0
  178. package/editor/references/comandos.md +92 -0
  179. package/editor/references/corte-seco-como-funciona.md +50 -0
  180. package/editor/references/corte-seco.md +88 -0
  181. package/editor/references/corte.md +290 -0
  182. package/editor/references/direcao-por-blocos.md +218 -0
  183. package/editor/references/enquadramento.md +654 -0
  184. package/editor/references/estilos-prontos.md +355 -0
  185. package/editor/references/estudio.md +100 -0
  186. package/editor/references/exemplo-eventos.json +67 -0
  187. package/editor/references/lettering.md +252 -0
  188. package/editor/references/modelos.md +494 -0
  189. package/editor/references/phase2.md +1007 -0
  190. package/editor/references/referencia-aplicar.md +203 -0
  191. package/editor/references/referencia-medir.md +375 -0
  192. package/editor/references/revisao.md +335 -0
  193. package/editor/references/som-e-trilha.md +146 -0
  194. package/editor/references/tabela-de-sons.md +130 -0
  195. package/editor/references/variacoes.md +234 -0
  196. package/editor/uv.lock +1141 -0
  197. package/lib/act.js +38 -8
  198. package/lib/app-chat.js +97 -0
  199. package/lib/catalogo.js +7 -1
  200. package/lib/chamada-torta.js +81 -0
  201. package/lib/editor-pro.js +786 -0
  202. package/lib/entrega.js +58 -0
  203. package/lib/estudio/conferencia-visual.js +76 -0
  204. package/lib/estudio/diretor.js +6 -1
  205. package/lib/estudio/exportar.js +3 -1
  206. package/lib/estudio/gosto/tela.html +1349 -0
  207. package/lib/estudio/gosto.js +305 -0
  208. package/lib/estudio/index.js +12 -1
  209. package/lib/estudio/kit-ae/base.jsx +119 -13
  210. package/lib/estudio/kit-ae/cenas.jsx +47 -15
  211. package/lib/estudio/kit-ae/guarda.jsx +274 -0
  212. package/lib/estudio/kit-ae/primitivas.jsx +180 -24
  213. package/lib/estudio/maquina-diretor.js +70 -12
  214. package/lib/estudio/maquina-foto.js +177 -0
  215. package/lib/estudio/maquina-guarda.js +207 -0
  216. package/lib/estudio/maquina-layout.js +22 -1
  217. package/lib/estudio/maquina.js +64 -11
  218. package/lib/estudio/referencias.js +1 -1
  219. package/lib/estudio/visao/visao.swift +97 -0
  220. package/lib/estudio/visao.js +60 -0
  221. package/lib/headless.js +15 -3
  222. package/lib/local/cerebro.json +285 -7
  223. package/lib/local/motor.js +5 -2
  224. package/lib/memoria-usuario.js +164 -0
  225. package/lib/nuvem.js +33 -2
  226. package/lib/passagem.js +50 -0
  227. package/lib/perigo.js +19 -11
  228. package/lib/protecao.js +134 -0
  229. package/lib/repeticao.js +1 -1
  230. package/lib/sandbox.js +57 -9
  231. package/lib/titulo-conversa.js +46 -0
  232. package/lib/tools.js +23 -4
  233. package/lib/ui.js +2 -2
  234. package/package.json +3 -2
  235. package/skills/editar-meu-video/SKILL.md +6 -2
  236. package/skills/editar-video/SKILL.md +113 -0
  237. package/studio/pessoa.py +52 -0
  238. package/voz/primo_voz/__main__.py +55 -40
  239. package/voz/primo_voz/app.py +500 -4
  240. package/voz/primo_voz/chat.html +1773 -0
  241. package/voz/primo_voz/conversa.py +0 -26
  242. package/voz/primo_voz/hud.html +8 -2
  243. package/voz/primo_voz/hud.py +129 -5
  244. package/voz/primo_voz/identidade.py +1 -1
  245. package/voz/primo_voz/painel_dados.py +134 -0
@@ -86,6 +86,33 @@ def achar_projeto(nome: str) -> "Path | None":
86
86
 
87
87
  PEDIDO_PASTA = re.compile(r"^(?:(?:abre|abrir|abra|troca|trocar|muda|mudar|vai|ir)\s+(?:para|pra|pro|no|na|o|a)?\s*(?:o\s+|a\s+)?(?:projeto|pasta)\s+|trabalha(?:r)?\s+(?:no|na|em)\s+(?:o\s+|a\s+)?(?:projeto\s+|pasta\s+)?)(.+?)[.!?]*$", re.I)
88
88
  VERBO_ACAO = re.compile(r"^(?:e\s+|ent[aã]o\s+|agora\s+)?(?:cria|crie|criar|faz|fa[cç]a|fazer|monta|monte|gera|gere|escreve|escreva|edita|edite|muda|mude|corrige|corrija|conserta|conserte|instala|instale|roda|rode|executa|execute|abre|abra|apaga|apague|deleta|enxuga|enxugue|atualiza|atualize|adiciona|adicione|remove|remova|refatora|publica|publique|coloca|ponha|p[oõ]e|troca|continua|continue|pode ser|sim|isso|ok|beleza|manda|bora|vai)\b", re.I)
89
+ # O 1º motion sem gosto salvo: o motor (em -p, PRIMOCODE_APP=1) escreve no stderr `primocode:abrir-gosto <url>` e espera.
90
+ # O app bloqueia navegador por padrão (PRIMOCODE_SEM_NAVEGADOR=1); ESTA é a exceção explícita: a tela local do gosto abre
91
+ # no navegador padrão. Só a tela local (127.0.0.1 + token hex) passa; qualquer outra coisa na linha é recusada.
92
+ GOSTO_LINHA = re.compile(r"^primocode:abrir-gosto (http://127\.0\.0\.1:\d{2,5}/\?t=[0-9a-f]{32})$")
93
+
94
+
95
+ def url_gosto(linha: str) -> "str | None":
96
+ m = GOSTO_LINHA.match(str(linha or "").strip())
97
+ return m.group(1) if m else None
98
+
99
+
100
+ def abrir_gosto(linha: str, abrir=None) -> bool:
101
+ """True se a linha era do gosto (aberta ou recusada); False se é outra linha qualquer."""
102
+ if not str(linha or "").startswith("primocode:abrir-gosto"):
103
+ return False
104
+ url = url_gosto(linha)
105
+ if not url:
106
+ log("gosto: linha recusada")
107
+ return True
108
+ log("gosto: abrindo a tela do gosto no navegador padrão")
109
+ try:
110
+ (abrir or (lambda u: subprocess.Popen(["open", u], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)))(url)
111
+ except Exception as e:
112
+ log(f"gosto: não abriu: {e}")
113
+ return True
114
+
115
+
89
116
  PEDE_NAVEGADOR = re.compile(r"\babr(?:e|a|ir)\b|\bmostr(?:a|e|ar)\b|navegador|visualiz|preview|pr[eé]-?visualiza", re.I)
90
117
 
91
118
 
@@ -147,6 +174,8 @@ class App:
147
174
  self.vivo = True
148
175
  self.trava = threading.Lock()
149
176
  self.cache_clima: dict = {}
177
+ self.ao_ouvir = lambda: None
178
+ self.desligando = False # a voz foi fechada de propósito: o motor sair não derruba o app
150
179
 
151
180
  # ── entrada do painel (iterável de linhas JSON) ──
152
181
  def __iter__(self):
@@ -182,6 +211,13 @@ class App:
182
211
  self.painel({"login": logado()})
183
212
  threading.Thread(target=trabalho, daemon=True).start()
184
213
 
214
+ def entrar_e_esperar(self, limite: float = 600) -> None:
215
+ """O chat espera o login terminar para mostrar a conta e as conversas."""
216
+ self.entrar()
217
+ fim = time.time() + limite
218
+ while time.time() < fim and not logado():
219
+ time.sleep(1)
220
+
185
221
  def ambiente(self) -> dict:
186
222
  env = dict(os.environ)
187
223
  extra = [str(Path(self.node).parent)] if self.node else []
@@ -230,10 +266,14 @@ class App:
230
266
  elif t == "parar":
231
267
  self.matar_cerebro()
232
268
  elif t == "ouvi" and str(m.get("texto") or "").strip():
269
+ self.ao_ouvir() # pedido falado: a tela de voz vem pra frente
233
270
  threading.Thread(target=self.pensar, args=(str(m["texto"]).strip(),), daemon=True).start()
234
271
  elif t == "fim":
235
272
  break
236
273
  log("motor de voz: saiu")
274
+ if self.desligando:
275
+ self.desligando = False
276
+ return
237
277
  if self.vivo:
238
278
  self.vivo = False
239
279
  os._exit(0)
@@ -356,7 +396,19 @@ class App:
356
396
  self.cerebro = subprocess.Popen(args, cwd=str(self.pasta), env=env, text=True,
357
397
  stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
358
398
  start_new_session=True) # grupo próprio: nada do que o agente faça com processos atinge o app
359
- out, err = self.cerebro.communicate(timeout=900)
399
+ linhas_err: "list[str]" = []
400
+ cer = self.cerebro
401
+
402
+ def ler_err(): # ao vivo: a linha do gosto tem que abrir a tela ENQUANTO o motor espera
403
+ for l in cer.stderr:
404
+ if not abrir_gosto(l):
405
+ linhas_err.append(l)
406
+ th_err = threading.Thread(target=ler_err, daemon=True)
407
+ th_err.start()
408
+ out = cer.stdout.read()
409
+ cer.wait(timeout=900)
410
+ th_err.join(timeout=2)
411
+ err = "".join(linhas_err)
360
412
  log(f"cérebro: saiu com código {self.cerebro.returncode}")
361
413
  timer.cancel()
362
414
  except Exception as e:
@@ -423,12 +475,456 @@ class App:
423
475
  self.motor.kill()
424
476
  log("app: encerrado")
425
477
 
426
- def iniciar(self, js, p) -> None:
478
+ # ── a voz só liga pelo botão: com o chat na frente, o microfone fica fechado ──
479
+ def ligar_voz(self) -> None:
480
+ with self.trava:
481
+ if self.motor and self.motor.poll() is None:
482
+ return
483
+ self.desligando = False
484
+ self.subir_motor()
485
+
486
+ def desligar_voz(self) -> None:
487
+ with self.trava:
488
+ if not self.motor or self.motor.poll() is not None:
489
+ return
490
+ self.desligando = True
491
+ m, self.motor = self.motor, None
492
+ log("motor de voz: desligando (voltou pro chat)")
493
+ try:
494
+ m.stdin.write(json.dumps({"tipo": "fim"}) + "\n")
495
+ m.stdin.flush()
496
+ m.wait(timeout=2)
497
+ except Exception:
498
+ m.kill()
499
+
500
+ def iniciar(self, js, p, com_chat: bool = True) -> None:
427
501
  self.js = js
428
- threading.Timer(0.5, self.subir_motor).start()
502
+ if not com_chat: # só o modo voz puro (sem chat) abre ouvindo
503
+ threading.Timer(0.5, self.subir_motor).start()
429
504
  threading.Timer(2.0, lambda: self.painel({"pasta": self.rotulo(), "login": logado()})).start()
430
505
 
431
506
 
507
+ # ── o que aparece embaixo de uma mensagem: os arquivos citados, mostrados ali mesmo ──
508
+ TIPOS = {"video": {".mp4", ".mov", ".webm", ".m4v"}, "imagem": {".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".avif"},
509
+ "audio": {".mp3", ".wav", ".m4a", ".aac", ".ogg", ".flac"}, "pdf": {".pdf"}, "html": {".html", ".htm"},
510
+ "codigo": {".js", ".jsx", ".ts", ".tsx", ".py", ".css", ".json", ".md", ".txt", ".sh", ".yml", ".yaml", ".toml",
511
+ ".rs", ".go", ".java", ".kt", ".swift", ".rb", ".php", ".sql", ".csv", ".xml", ".env.example"}}
512
+ CAMINHO_ABS = re.compile(r"(?<![\w/])(?:~|/(?:Users|Volumes|tmp|private|opt|var|Applications))(?:/[\w.@+\-()À-ÿ]+)+/?")
513
+ CAMINHO_REL = re.compile(r"(?<![\w/~.])(?:[\w.@+\-À-ÿ]+/)+[\w.@+\-À-ÿ]+\.\w{1,5}\b")
514
+ CRASE = re.compile(r"`([^`\n]{2,300})`")
515
+
516
+
517
+ def tipo_de(c: Path) -> str:
518
+ if c.is_dir():
519
+ return "pasta"
520
+ ext = c.suffix.lower()
521
+ return next((t for t, exts in TIPOS.items() if ext in exts), "arquivo")
522
+
523
+
524
+ def midias_do_texto(texto: str, pasta: Path, limite: int = 8) -> "list[dict]":
525
+ """Os arquivos e pastas que a mensagem cita e que EXISTEM: caminho absoluto, ~/..., ou relativo à pasta do
526
+ projeto (entre crases ou com barra). `achado` é o texto como apareceu, para a tela virar ele num botão."""
527
+ vistos, fora = set(), []
528
+ candidatos = [m.group(1).strip() for m in CRASE.finditer(texto or "")]
529
+ candidatos += [m.group(0) for m in CAMINHO_ABS.finditer(texto or "")] + [m.group(0) for m in CAMINHO_REL.finditer(texto or "")]
530
+ for achado in candidatos:
531
+ bruto = achado.rstrip(".,;:)!?").strip()
532
+ if not bruto or "\n" in bruto or bruto.startswith(("http://", "https://")):
533
+ continue
534
+ c = Path(os.path.expanduser(bruto))
535
+ if not c.is_absolute():
536
+ if "/" not in bruto and "." not in bruto:
537
+ continue
538
+ c = pasta / c
539
+ try:
540
+ c = c.resolve()
541
+ existe = c.exists()
542
+ except (OSError, RuntimeError):
543
+ continue
544
+ if not existe or str(c) in vistos or c == HOME or c == Path("/"):
545
+ continue
546
+ vistos.add(str(c))
547
+ tipo = tipo_de(c)
548
+ item = {"achado": bruto, "caminho": str(c), "nome": c.name, "tipo": tipo, "url": c.as_uri(),
549
+ "tamanho": c.stat().st_size if c.is_file() else 0}
550
+ if tipo == "codigo" and item["tamanho"] <= 200_000:
551
+ try:
552
+ item["trecho"] = "\n".join(c.read_text("utf-8", errors="replace").splitlines()[:40])[:4000]
553
+ except OSError:
554
+ pass
555
+ fora.append(item)
556
+ if len(fora) >= limite:
557
+ break
558
+ return fora
559
+
560
+
561
+ # Os comandos de / no app. Os do terminal que viram ação da própria janela, e os que não se aplicam a ela.
562
+ COMANDOS_DO_APP = {"/clear": "nova", "/chats": "buscar", "/login": "entrar", "/logout": "sair_conta", "/account": "conta",
563
+ "/mode": "modo", "/api": "modelos", "/config": "ajustes", "/skills": "skills", "/marketplace": "skills",
564
+ "/attach": "anexar", "/detach": "desanexar", "/copiar": "copiar", "/voice": "voz", "/voice nome": "voz_nome",
565
+ "/dir": "pasta", "/help": "ajuda", "/effort": "effort", "/permissoes": "permissoes"}
566
+ # "perguntar" precisa de alguém no terminal para responder: no app (sem terminal) não se aplica.
567
+ PERMISSOES_APP = [("auto", "roda tudo, mas pergunta antes do que não tem volta"), ("livre", "faz tudo sem perguntar"),
568
+ ("plano", "só lê e planeja, não muda nada")]
569
+ COMANDOS_FORA = {"/cls", "/exit", "/uninstall", "/atalhos", "/mascot", "/offline", "/files", "/voice painel", "/voice parar", "!comando"}
570
+
571
+
572
+ def comandos_para_o_app(lista: list) -> list:
573
+ """A lista do terminal (`menu-comandos`) com o tipo de cada um: app (a janela faz), terminal (roda e mostra a
574
+ saída) ou pedido (comando da pessoa: vira um pedido ao agente)."""
575
+ fora = []
576
+ for c in lista:
577
+ nome = str(c.get("nome") or "")
578
+ if not nome or nome in COMANDOS_FORA:
579
+ continue
580
+ item = {"nome": nome, "descricao": str(c.get("descricao") or "")}
581
+ if nome in COMANDOS_DO_APP:
582
+ item.update(tipo="app", acao=COMANDOS_DO_APP[nome])
583
+ else:
584
+ item["tipo"] = "terminal" if c.get("embutido") else "pedido"
585
+ fora.append(item)
586
+ return fora
587
+
588
+
589
+ class Chat:
590
+ """A tela do chat (chat.html): manda e recebe pela ponte `chat` do hud.py. O cérebro é o mesmo da voz
591
+ (`primocode -p ... --output-format json`), com `--resume <id>` para seguir a conversa da nuvem."""
592
+
593
+ MAX_ANEXO = 25 * 1024 * 1024
594
+
595
+ def __init__(self, app: App) -> None:
596
+ self.app = app
597
+ self.js = lambda m: None
598
+ self.mostrar_voz = lambda: None
599
+ self.parado = False
600
+ app.ao_ouvir = lambda: self.mostrar_voz()
601
+
602
+ def raiz(self) -> "Path | None":
603
+ r = Path(self.app.primocode).resolve().parent.parent if self.app.primocode else None
604
+ return r if r and (r / "lib").is_dir() else None
605
+
606
+ def node(self, codigo: str, timeout: int = 60) -> dict:
607
+ """Roda um pedaço de JS na raiz do PrimoCode e lê a última linha de JSON que ele imprimir."""
608
+ r = self.raiz()
609
+ if not (self.app.node and r):
610
+ return {"ok": False, "erro": "Não achei o PrimoCode instalado. Rode primocode voz instalar-app no terminal."}
611
+ try:
612
+ p = subprocess.run([self.app.node, "-e", codigo], cwd=str(r), env=self.app.ambiente(), capture_output=True, text=True, timeout=timeout)
613
+ except Exception as e:
614
+ return {"ok": False, "erro": str(e)}
615
+ for l in reversed(p.stdout.strip().splitlines()):
616
+ try:
617
+ return json.loads(l)
618
+ except ValueError:
619
+ continue
620
+ log("chat node: " + (p.stderr or "")[-300:])
621
+ return {"ok": False, "erro": "O PrimoCode não respondeu."}
622
+
623
+ def cli(self, *args: str, timeout: int = 60) -> dict:
624
+ if not (self.app.node and self.app.primocode):
625
+ return {"ok": False, "erro": "Não achei o PrimoCode instalado."}
626
+ try:
627
+ p = subprocess.run([self.app.node, self.app.primocode, *args], cwd=str(self.app.pasta), env=self.app.ambiente(),
628
+ capture_output=True, text=True, timeout=timeout)
629
+ except Exception as e:
630
+ return {"ok": False, "erro": str(e)}
631
+ for l in reversed(p.stdout.strip().splitlines()):
632
+ try:
633
+ return json.loads(l)
634
+ except ValueError:
635
+ continue
636
+ return {"ok": False, "erro": "O PrimoCode não respondeu."}
637
+
638
+ # ── o que a tela mostra ──
639
+ def conta(self) -> dict:
640
+ c = _json(PASTA_PC / "conta.json")
641
+ ate = c.get("planoAte")
642
+ if isinstance(ate, (int, float)):
643
+ ate = time.strftime("%d/%m/%Y", time.localtime(ate / 1000 if ate > 1e11 else ate))
644
+ return {"logado": logado(), "email": c.get("email") or "", "plano": c.get("plano") or "", "planoAte": ate or ""}
645
+
646
+ def modo(self) -> str:
647
+ return "chat" if _json(PASTA_PC / "config.json").get("mode") == "chat" else "agent"
648
+
649
+ def ajustes(self) -> dict:
650
+ r = self.node("const P=require('./lib/local/provedor.js');const a=P.atual();"
651
+ "console.log(JSON.stringify({ok:true,atual:a?{id:a.id,nome:a.nome,cli:a.cli,chave:a.chave?P.mascarar(a.chave):''}:null,"
652
+ "lista:Object.entries(P.PROVEDORES).filter(([,p])=>!p.cli).map(([id,p])=>({id,nome:p.nome}))}))")
653
+ atual = r.get("atual") if r.get("ok") else None
654
+ voz = _json(PASTA_PC / "voz.json")
655
+ cfg = _json(PASTA_PC / "config.json")
656
+ ef = self.node("const E=require('./lib/effort.js');console.log(JSON.stringify({ok:true,niveis:E.NIVEIS.map(n=>({nome:n.nome,resumo:n.resumo}))}))")
657
+ perm = cfg.get("permissoes") if cfg.get("permissoes") in dict(PERMISSOES_APP) else "auto"
658
+ return {"modo": self.modo(),
659
+ "effort": {"atual": str(cfg.get("effort") or "medium"), "niveis": ef.get("niveis") or []},
660
+ "permissoes": {"atual": perm, "opcoes": [{"nome": n, "resumo": r} for n, r in PERMISSOES_APP]},
661
+ "provedor": {"id": atual["id"], "nome": atual["nome"], "chave_mascarada": atual.get("chave") or ""} if atual else {"id": "primo", "nome": "Primo", "chave_mascarada": ""},
662
+ "provedores_chave": r.get("lista") or [], "pasta": str(self.app.pasta), "voz": {"nome": voz.get("nome") or "Primo", "sons": voz.get("sons", True) is not False},
663
+ "conta": self.conta()}
664
+
665
+ def conversas(self) -> None:
666
+ r = self.cli("conversas", "--json", "--limite", "40")
667
+ self.js({"conversas": r.get("conversas") or []} if r.get("ok") else {"conversas_erro": r.get("erro") or "Não deu pra carregar as conversas."})
668
+
669
+ def skills(self) -> None:
670
+ r = self.node("const S=require('./lib/skills.js');" # listar() já diz se cada skill está ligada
671
+ "console.log(JSON.stringify({ok:true,skills:S.listar().map(s=>({nome:s.nome,descricao:s.descricao||'',ligada:s.ligada!==false}))}))")
672
+ self.js({"skills": r.get("skills") or []} if r.get("ok") else {"erro": r.get("erro")})
673
+
674
+ # ── a ponte ──
675
+ def receber(self, m: dict) -> None:
676
+ a = m.get("acao")
677
+ trab = None
678
+ if a == "pronto":
679
+ def trab():
680
+ self.js({"inicio": {**self.conta(), "pasta": str(self.app.pasta), "modo": self.modo()}})
681
+ r = self.cli("menu-comandos")
682
+ if r.get("ok"):
683
+ self.js({"comandos": comandos_para_o_app(r.get("comandos") or [])})
684
+ if logado():
685
+ self.conversas()
686
+ elif a == "enviar":
687
+ self.parado = False
688
+ trab = lambda: self.pensar(str(m.get("texto") or ""), m.get("conversa"), m.get("modo") or self.modo(), m.get("anexos") or [])
689
+ elif a == "comando" and str(m.get("texto") or "").startswith("/"):
690
+ self.parado = False
691
+ trab = lambda: self.pensar(str(m["texto"]), m.get("conversa"), self.modo(), [], comando=True)
692
+ elif a == "parar":
693
+ self.parado = True
694
+ self.app.matar_cerebro()
695
+ elif a == "conversas":
696
+ trab = self.conversas
697
+ elif a == "abrir" and m.get("id"):
698
+ def trab():
699
+ r = self.cli("conversa", str(m["id"]), "--json")
700
+ if not r.get("ok"):
701
+ self.js({"erro": r.get("erro") or "Não abriu a conversa."})
702
+ return
703
+ msgs = [{**x, "midias": midias_do_texto(x.get("texto", ""), self.app.pasta)} for x in (r.get("mensagens") or [])]
704
+ self.js({"conversa": {"id": r.get("id"), "titulo": r.get("titulo"), "mensagens": msgs}})
705
+ elif a == "entrar":
706
+ def trab():
707
+ self.app.entrar_e_esperar()
708
+ self.js({"login": self.conta()})
709
+ if logado():
710
+ self.conversas()
711
+ elif a == "sair_conta":
712
+ def trab():
713
+ self.node("require('./lib/conta.js').sair();console.log('{\"ok\":true}')")
714
+ self.js({"login": self.conta()})
715
+ self.js({"conversas": []})
716
+ elif a == "excluir" and m.get("id"):
717
+ def trab():
718
+ r = self.cli("conversa", str(m["id"]), "--apagar", "--json", timeout=120)
719
+ if not r.get("ok"):
720
+ self.js({"erro": "Não apaguei a conversa: " + str(r.get("erro") or "sem resposta")})
721
+ self.conversas()
722
+ elif a == "excluir_todas":
723
+ def trab():
724
+ r = self.cli("conversas", "--apagar-todas", "--json", timeout=600)
725
+ if not r.get("ok"):
726
+ self.js({"erro": "Não apaguei todas: " + str(r.get("erro") or "sem resposta")})
727
+ self.conversas()
728
+ elif a == "skills":
729
+ trab = self.skills
730
+ elif a == "skill" and m.get("nome"):
731
+ def trab():
732
+ self.node(f"require('./lib/skills.js').ligar({json.dumps(str(m['nome']))},{'true' if m.get('ligada') else 'false'});console.log('{{\"ok\":true}}')")
733
+ self.skills()
734
+ elif a == "ajustes":
735
+ trab = lambda: self.js({"ajustes": self.ajustes()})
736
+ elif a == "ajuste":
737
+ trab = lambda: self.ajuste(str(m.get("chave") or ""), m.get("valor"))
738
+ elif a == "soltar":
739
+ trab = lambda: self.soltar(str(m.get("nome") or "arquivo"), str(m.get("base64") or ""))
740
+ if trab:
741
+ threading.Thread(target=trab, daemon=True).start()
742
+
743
+ def ajuste(self, chave: str, valor) -> None:
744
+ if chave in ("modo", "effort", "permissoes"):
745
+ cfg = _json(PASTA_PC / "config.json")
746
+ if chave == "modo":
747
+ cfg["mode"] = "chat" if valor == "chat" else "primocode"
748
+ elif chave == "effort" and str(valor) in ("low", "medium", "high", "xhigh", "max", "ultracode"):
749
+ cfg["effort"] = str(valor)
750
+ elif chave == "permissoes" and str(valor) in dict(PERMISSOES_APP):
751
+ cfg["permissoes"] = str(valor)
752
+ else:
753
+ self.js({"erro": f"Valor inválido para {chave}: {valor}"})
754
+ (PASTA_PC / "config.json").write_text(json.dumps(cfg, ensure_ascii=False, indent=2), "utf-8")
755
+ elif chave == "provedor" and isinstance(valor, dict):
756
+ pid = str(valor.get("provedor") or "")
757
+ if pid == "primo":
758
+ r = self.node("require('./lib/local/provedor.js').desligar();console.log('{\"ok\":true}')")
759
+ else:
760
+ r = self.node("const P=require('./lib/local/provedor.js');const p=P.PROVEDORES[" + json.dumps(pid) + "];"
761
+ "if(!p){console.log(JSON.stringify({ok:false,erro:'Provedor desconhecido.'}));}else{"
762
+ "P.salvar({provedor:" + json.dumps(pid) + ",chave:" + json.dumps(str(valor.get("chave") or "")) + ",modelo:p.modelos[0]});"
763
+ "console.log(JSON.stringify({ok:true}))}")
764
+ if not r.get("ok"):
765
+ self.js({"erro": r.get("erro") or "Não deu pra trocar o modelo."})
766
+ elif chave == "voz_nome":
767
+ from primo_voz import identidade
768
+ nome = identidade._nome_valido(str(valor or ""))
769
+ if not nome:
770
+ self.js({"erro": "O nome precisa ter 1 ou 2 palavras, até 20 letras."})
771
+ else:
772
+ self.gravar_voz(nome=nome)
773
+ self.app.js({"nome": nome})
774
+ elif chave == "voz_sons":
775
+ self.gravar_voz(sons=bool(valor))
776
+ self.js({"ajustes": self.ajustes()})
777
+
778
+ def gravar_voz(self, **campos) -> None:
779
+ arq = PASTA_PC / "voz.json"
780
+ d = _json(arq)
781
+ d.update(campos)
782
+ arq.write_text(json.dumps(d, ensure_ascii=False, indent=2), "utf-8")
783
+
784
+ def escolher_pasta(self, caminho: str) -> None:
785
+ d = Path(caminho)
786
+ if d.is_dir():
787
+ self.app.pasta, self.app.pasta_modo = d, "/dir"
788
+ gravar_pasta(d)
789
+ self.app.painel({"pasta": self.app.rotulo()})
790
+ self.js({"pasta": str(d)})
791
+
792
+ # ── anexos: tudo vai para <pasta>/anexos e o caminho entra no pedido ──
793
+ def destino(self, nome: str) -> Path:
794
+ pasta = self.app.pasta / "anexos"
795
+ pasta.mkdir(parents=True, exist_ok=True)
796
+ base = re.sub(r"[^\w.\- ]", "_", Path(nome).name).strip() or "arquivo"
797
+ alvo, k = pasta / base, 1
798
+ while alvo.exists():
799
+ alvo = pasta / f"{Path(base).stem}-{k}{Path(base).suffix}"
800
+ k += 1
801
+ return alvo
802
+
803
+ def anexar_caminhos(self, caminhos: "list[str]") -> None:
804
+ import shutil
805
+ for c in caminhos:
806
+ o = Path(c)
807
+ if not o.is_file():
808
+ continue
809
+ if o.stat().st_size > self.MAX_ANEXO:
810
+ self.js({"erro": f"{o.name} passa de 25 MB."})
811
+ continue
812
+ alvo = self.destino(o.name)
813
+ shutil.copy2(o, alvo)
814
+ self.js({"anexo": {"nome": o.name, "caminho": str(alvo)}})
815
+
816
+ def soltar(self, nome: str, b64: str) -> None:
817
+ import base64
818
+ try:
819
+ dados = base64.b64decode(b64, validate=False)
820
+ except Exception:
821
+ self.js({"erro": f"Não li {nome}."})
822
+ return
823
+ if len(dados) > self.MAX_ANEXO:
824
+ self.js({"erro": f"{nome} passa de 25 MB."})
825
+ return
826
+ alvo = self.destino(nome)
827
+ alvo.write_bytes(dados)
828
+ self.js({"anexo": {"nome": nome, "caminho": str(alvo)}})
829
+
830
+ # ── o cérebro ──
831
+ def pensar(self, texto: str, conversa, modo: str, anexos: list, comando: bool = False) -> None:
832
+ """comando=True: `texto` é um /comando do terminal; a saída volta como {saida} (texto de terminal), não como resposta."""
833
+ texto = texto.strip()
834
+ if anexos:
835
+ texto = (texto or "Olha esses arquivos.") + "\n\nAnexos:\n" + "\n".join(f"- {a}" for a in anexos)
836
+ if not texto:
837
+ return
838
+ if not comando and not logado(): # /help, /doctor, /version… funcionam sem conta
839
+ self.js({"login": self.conta()})
840
+ self.js({"erro": "Entre na sua conta do PrimoCode pra conversar."})
841
+ return
842
+ if not (self.app.node and self.app.primocode and Path(self.app.primocode).exists()):
843
+ self.js({"erro": "Não achei o PrimoCode instalado. Rode primocode voz instalar-app no terminal."})
844
+ return
845
+ with self.app.trava:
846
+ env = self.app.ambiente()
847
+ env.update({"PRIMOCODE_APP": "1", "PRIMOCODE_RAIZ": str(RAIZ), "PRIMOCODE_LOG_FERRAMENTAS": "1",
848
+ "PRIMOCODE_MODO": "chat" if modo == "chat" else "primocode"})
849
+ if comando and re.match(r"/(open|abrir)\b", texto):
850
+ env["PRIMOCODE_SEM_NAVEGADOR"] = "0" # /open e /abrir existem para abrir o navegador
851
+ if self.app.pasta != RAIZ:
852
+ env["PRIMOCODE_PASTA"], env["PRIMOCODE_PASTA_ORIGEM"] = str(self.app.pasta), ("/dir" if self.app.pasta_modo == "/dir" else "projeto")
853
+ args = [self.app.node, self.app.primocode, "-p", texto, "--output-format", "json"] + (["--resume", str(conversa)] if conversa else [])
854
+ log(f"chat: modo={modo} cwd={self.app.pasta} conversa={conversa or 'nova'}")
855
+ t0 = time.time()
856
+ try:
857
+ p = subprocess.Popen(args, cwd=str(self.app.pasta), env=env, text=True, stdin=subprocess.DEVNULL,
858
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE, start_new_session=True)
859
+ self.app.cerebro = p
860
+ resto: "list[str]" = []
861
+
862
+ def passos(): # cada ferramenta que o agente usa vira uma linha ao vivo no chat
863
+ for l in p.stderr:
864
+ if l.startswith("ferramenta:"):
865
+ self.js({"passo": passo_legivel(l)})
866
+ elif not abrir_gosto(l):
867
+ resto.append(l)
868
+ th = threading.Thread(target=passos, daemon=True)
869
+ th.start()
870
+ out = p.stdout.read()
871
+ p.wait(timeout=900)
872
+ th.join(timeout=2)
873
+ except Exception as e:
874
+ log(f"chat: cérebro falhou: {e}")
875
+ self.js({"erro": "O PrimoCode não respondeu."})
876
+ return
877
+ if self.parado:
878
+ self.js({"erro": "Parei."})
879
+ return
880
+ res = {}
881
+ for l in reversed((out or "").strip().splitlines()):
882
+ try:
883
+ res = json.loads(l)
884
+ break
885
+ except ValueError:
886
+ continue
887
+ if comando:
888
+ saida = str(res.get("result") or "").strip()
889
+ if res.get("is_error"):
890
+ self.js({"erro": saida or "O comando falhou."})
891
+ else:
892
+ self.js({"saida": {"comando": texto.split()[0], "texto": saida or "(sem saída)", "conversa": conversa, "midias": midias_do_texto(saida, self.app.pasta)}})
893
+ return
894
+ # o markdown cru da resposta (o -p json manda); sem ele, o texto do terminal sem os enfeites
895
+ txt = str(res.get("markdown") or "").strip() or \
896
+ "\n".join(l for l in str(res.get("result") or "").splitlines() if not l.lstrip().startswith("⎿")).replace("●", "").strip()
897
+ log(f"chat: {time.time() - t0:.1f}s, {len(txt)} caracteres, erro={bool(res.get('is_error'))}")
898
+ if res.get("is_error") or not txt:
899
+ self.js({"erro": txt or ("".join(resto)[-300:].strip() or "Não consegui uma resposta agora.")})
900
+ return
901
+ self.js({"resposta": {"texto": txt, "conversa": res.get("conversa") or conversa, "midias": midias_do_texto(txt, self.app.pasta)}})
902
+
903
+
904
+ FERRAMENTA_LEGIVEL = {"read_file": "Lendo", "write_file": "Escrevendo", "edit_file": "Editando", "multi_edit": "Editando",
905
+ "run_shell": "Rodando", "run_in_new_terminal": "Abrindo um terminal:", "list_dir": "Olhando a pasta",
906
+ "grep": "Procurando", "glob": "Procurando arquivos", "open_in_browser": "Abrindo no navegador",
907
+ "pesquisar_referencia": "Pesquisando referências", "write_todos": "Organizando as etapas",
908
+ "estudio_video": "Montando o vídeo", "estudio_motion": "Animando", "estudio_render": "Renderizando",
909
+ "estudio_editar": "Editando o vídeo", "estudio_narrar": "Gravando a narração", "estudio_site": "Fazendo o site",
910
+ "estudio_ae": "Animando no estilo After Effects", "estudio_midia": "Buscando mídia", "estudio_plano": "Planejando",
911
+ "estudio_projeto": "Abrindo o projeto"}
912
+
913
+
914
+ def passo_legivel(linha: str) -> str:
915
+ """`ferramenta: read_file {"path":"src/a.js"}` → `Lendo src/a.js`. O que não reconhece, mostra o nome."""
916
+ m = re.match(r"ferramenta:\s*(\S+)\s*(.*)", linha.strip())
917
+ if not m:
918
+ return linha.strip()[:120]
919
+ nome, resto = m.group(1), m.group(2)
920
+ try:
921
+ a = json.loads(resto) if resto else {}
922
+ except ValueError:
923
+ a = {}
924
+ alvo = next((str(a[k]) for k in ("path", "file_path", "file", "command", "cmd", "pattern", "query", "url", "dir") if isinstance(a, dict) and a.get(k)), "")
925
+ return (FERRAMENTA_LEGIVEL.get(nome, nome) + (" " + alvo if alvo else "")).strip()[:120]
926
+
927
+
432
928
  def main() -> int:
433
929
  import fcntl
434
930
  import atexit
@@ -444,7 +940,7 @@ def main() -> int:
444
940
  app = App()
445
941
  atexit.register(app.sair)
446
942
  from primo_voz import hud
447
- return hud.main(entrada=iter(app), saida=app.saida_do_painel, ao_iniciar=app.iniciar, ao_sair=app.sair)
943
+ return hud.main(entrada=iter(app), saida=app.saida_do_painel, ao_iniciar=app.iniciar, ao_sair=app.sair, chat=Chat(app))
448
944
 
449
945
 
450
946
  if __name__ == "__main__":