primocode 9.8.0-beta.2 → 9.8.0-beta.21

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 (74) hide show
  1. package/README.md +33 -6
  2. package/app/ansi.js +55 -0
  3. package/app/app.css +482 -281
  4. package/app/app.js +888 -315
  5. package/app/index.html +176 -101
  6. package/app/primo-marca.js +58 -1
  7. package/bin/primocode.js +697 -118
  8. package/comandos/explicar.md +14 -0
  9. package/comandos/publicar.md +10 -0
  10. package/comandos/revisar.md +13 -0
  11. package/lib/act.js +130 -26
  12. package/lib/api.js +8 -1
  13. package/lib/app.js +317 -74
  14. package/lib/browser.js +79 -3
  15. package/lib/catalogo.js +11 -3
  16. package/lib/checkpoint-git.js +218 -0
  17. package/lib/checkpoint.js +249 -0
  18. package/lib/claude-engine.js +25 -3
  19. package/lib/comandos-extra.js +481 -0
  20. package/lib/comandos.js +104 -0
  21. package/lib/config.js +21 -4
  22. package/lib/estudio/acervo-site.js +157 -0
  23. package/lib/estudio/assets.js +77 -3
  24. package/lib/estudio/cinema.js +354 -0
  25. package/lib/estudio/composicao.js +470 -0
  26. package/lib/estudio/edicao.js +450 -0
  27. package/lib/estudio/impacto.js +263 -0
  28. package/lib/estudio/index.js +769 -5
  29. package/lib/estudio/plano.js +5 -2
  30. package/lib/estudio/remotion.js +228 -23
  31. package/lib/estudio/slides.js +34 -30
  32. package/lib/fala.js +95 -1
  33. package/lib/local/cerebro.json +361 -180
  34. package/lib/local/entrada.js +18 -8
  35. package/lib/local/motor-cli.js +1 -1
  36. package/lib/mencoes.js +98 -0
  37. package/lib/offline.js +168 -0
  38. package/lib/painel.js +291 -0
  39. package/lib/perigo.js +256 -0
  40. package/lib/pre-leitura.js +118 -0
  41. package/lib/repeticao.js +4 -2
  42. package/lib/skills.js +6 -1
  43. package/lib/tempos.js +62 -0
  44. package/lib/ui.js +171 -17
  45. package/package.json +3 -2
  46. package/skills/apresentacao-pptx/SKILL.md +52 -0
  47. package/skills/cartaz-de-arte/LICENSE.txt +202 -0
  48. package/skills/cartaz-de-arte/SKILL.md +47 -0
  49. package/skills/criar-skill/LICENSE.txt +202 -0
  50. package/skills/criar-skill/SKILL.md +49 -0
  51. package/skills/depurar-de-verdade/LICENSE.txt +21 -0
  52. package/skills/depurar-de-verdade/SKILL.md +14 -5
  53. package/skills/design-de-verdade/LICENSE.txt +177 -0
  54. package/skills/design-de-verdade/SKILL.md +42 -0
  55. package/skills/documento-word/SKILL.md +57 -0
  56. package/skills/interface-revisada/SKILL.md +47 -0
  57. package/skills/me-ensina/LICENSE.txt +21 -0
  58. package/skills/me-ensina/SKILL.md +27 -0
  59. package/skills/me-entrevista/LICENSE.txt +21 -0
  60. package/skills/me-entrevista/SKILL.md +27 -0
  61. package/skills/motion-de-primeira/SKILL.md +1 -1
  62. package/skills/pdf-bonito/SKILL.md +48 -0
  63. package/skills/planilha-excel/SKILL.md +47 -0
  64. package/skills/react-sem-dor/SKILL.md +48 -0
  65. package/skills/revisao-antes-de-entregar/LICENSE.txt +21 -0
  66. package/skills/revisao-antes-de-entregar/SKILL.md +6 -1
  67. package/skills/site-com-motion/SKILL.md +39 -0
  68. package/skills/temas-prontos/LICENSE.txt +202 -0
  69. package/skills/temas-prontos/SKILL.md +50 -0
  70. package/skills/testar-o-site/LICENSE.txt +202 -0
  71. package/skills/testar-o-site/SKILL.md +46 -0
  72. package/skills/video-no-remotion/SKILL.md +48 -0
  73. package/studio/narrar.py +111 -10
  74. package/studio/rostos.py +52 -0
package/bin/primocode.js CHANGED
@@ -31,13 +31,29 @@ const effort = require('../lib/effort');
31
31
  const memoria = require('../lib/memoria-conversa');
32
32
  const registro = require('../lib/registro');
33
33
  const permissao = require('../lib/permissao');
34
+ const perigo = require('../lib/perigo');
34
35
  const skills = require('../lib/skills');
36
+ const checkpoint = require('../lib/checkpoint');
37
+ const comandosLib = require('../lib/comandos');
35
38
  const marketplace = require('../lib/marketplace');
36
39
  const vozModo = require('../lib/voz');
37
40
  const appLib = require('../lib/app');
38
41
  const conta = require('../lib/conta');
39
42
  const nuvem = require('../lib/nuvem');
40
43
  const compat = require('../lib/compatibilidade');
44
+ const mencoesLib = require('../lib/mencoes');
45
+ const extra = require('../lib/comandos-extra');
46
+ const offline = require('../lib/offline');
47
+
48
+ /* O /custo: toda conversa com o modelo passa pelo lib/api.js, e o contador
49
+ embrulha as funções dele uma vez, aqui. Quem atende é o provedor do /api
50
+ quando há um, senão o servidor — é por eles que o /custo separa. */
51
+ const contadorDaSessao = extra.novoContador();
52
+ extra.instalarContador(api, contadorDaSessao, () => {
53
+ const p = provedorLocal.atual();
54
+ return p ? p.nome : 'Servidor PrimoCode';
55
+ });
56
+ const tempos = require('../lib/tempos');
41
57
 
42
58
  // Rede de segurança de último recurso: sem isto, uma exceção fora de um
43
59
  // try/catch (ex.: dentro de um callback de socket, como o parser de SSE) não
@@ -92,27 +108,90 @@ const state = {
92
108
  // existe para a frente em vez de subir outro servidor ao lado.
93
109
  let janelaAberta = null;
94
110
 
111
+ /* O que o /rc, o /ide e o `primo --app` passam ao lib/app.js. Uma função
112
+ só porque os três sobem a MESMA janela: se cada um montasse a sua, o /ide
113
+ aberto depois do /rc acharia um servidor sem a pasta do projeto — e o
114
+ painel da prévia nasceria vazio. */
115
+ function opcoesDaJanela(extra) {
116
+ return Object.assign({
117
+ versao: VERSION,
118
+ enviar: (t) => sendPrompt(t),
119
+ barra: (t) => handleSlash(t),
120
+ interromper,
121
+ estado: () => ({
122
+ pasta: state.workDir ? shortPath(state.workDir) : null,
123
+ projeto: state.currentProject,
124
+ modo: state.mode,
125
+ esforco: state.config.effort,
126
+ permissoes: perigo.normalizarModo(state.config.permissoes),
127
+ voz: !!vozEmEspera,
128
+ usuario: state.config.userName || null,
129
+ mascote: state.config.mascot || 'primo',
130
+ anexos: state.attachedFiles.map((f) => f.path),
131
+ }),
132
+ // Lida na hora, não guardada: o /dir e o /project trocam a pasta
133
+ // com a janela aberta, e o painel tem de acompanhar.
134
+ pastaProjeto: () => projectDir(),
135
+ /* As ações da janela passam pelos MESMOS caminhos dos comandos — o
136
+ /projetos, o /clear, o /permissoes, o /effort, o /attach — para a
137
+ janela e o terminal nunca discordarem sobre onde se está. */
138
+ entrarProjeto: (dir) => {
139
+ entrarNoProjeto(dir, 'projeto');
140
+ console.log(c.ok(` Trabalhando em ${state.currentProject}`) + c.muted(` (${shortPath(dir)})`));
141
+ refreshPrompt();
142
+ },
143
+ novoChat: () => handleSlash('/clear'),
144
+ mudarPermissoes: (m) => handleSlash('/permissoes ' + m),
145
+ mudarEsforco: (n) => aplicarEffort(n),
146
+ anexar: (f) => {
147
+ state.attachedFiles.push(f);
148
+ console.log(c.ok(`Anexado: ${f.path} (${tools.fmtBytes(f.content.length)})`) + c.muted(' pela janela'));
149
+ refreshPrompt();
150
+ },
151
+ desanexar: (nome) => {
152
+ state.attachedFiles = nome ? state.attachedFiles.filter((f) => f.path !== nome) : [];
153
+ refreshPrompt();
154
+ },
155
+ voz: (ligar) => handleSlash(ligar ? '/voice' : '/voice parar'),
156
+ continuarConversa: (alvo) => abrirConversa(alvo),
157
+ }, extra || {});
158
+ }
159
+
95
160
  const HELP = `
96
161
  ${c.bold('Comandos')}
97
162
  ${c.brand('/dir')} <caminho> trabalha numa pasta que já existe (onde você quiser)
98
163
  ${c.brand('/project')} lista projetos ou cria em ~/primocode: /project <nome>
164
+ ${c.brand('/projetos')} [n] projetos do mais recente, com tamanho · /projetos 2 entra
165
+ ${c.brand('/novo')} <descrição> começa um projeto NOVO do zero, em pasta própria
99
166
  ${c.brand('/mode')} /mode primocode|chat|auto
100
167
  ${c.brand('/effort')} esforço do agente — do rápido ao mais esperto
168
+ ${c.brand('/permissoes')} quando ele pergunta antes de agir: auto|perguntar|livre
101
169
  ${c.brand('/compact')} resume a conversa e devolve o contexto
102
- ${c.brand('/attach')} <arquivo> anexa arquivo ao próximo pedido
170
+ ${c.brand('/init')} estuda o projeto e escreve o PRIMO.md
171
+ ${c.brand('/attach')} <arquivo> anexa arquivo ao próximo pedido · ou @arquivo no pedido
103
172
  ${c.brand('/files')} lista anexos ${c.brand('/detach')} remove
104
173
  ${c.brand('/all')} lista tudo que ele sabe fazer
174
+ ${c.brand('/desfazer')} [lista|N] desfaz o que o último pedido (ou os N últimos) mudou
105
175
  ${c.brand('/tree')} [n] árvore do projeto
106
176
  ${c.brand('/read')} <arquivo> mostra um arquivo
107
177
  ${c.brand('/run')} <comando> executa no shell do projeto
108
178
  ${c.brand('/open')} <arq|url> abre no navegador
179
+ ${c.brand('/abrir')} abre o projeto: o servidor de pé, ou a index.html
180
+ ${c.brand('/copiar')} copia a última resposta
181
+ ${c.brand('/historico')} [termo] procura nos seus pedidos antigos
109
182
  ${c.brand('/rc')} abre a janela do PrimoCode (chat, sem terminal)
183
+ ${c.brand('/ide')} abre a janela com a prévia do site e os arquivos ao lado
110
184
  ${c.brand('/login')} entra na sua conta · ${c.brand('/account')} mostra o plano
111
185
  ${c.brand('/chats')} [n] suas conversas — as mesmas do app e do celular
112
186
  ${c.brand('/logout')} desconecta este computador da conta
187
+ ${c.brand('/comandos')} seus comandos (/revisar, /explicar…) · /comandos novo <nome>
113
188
  ${c.brand('/status')} checa o servidor
189
+ ${c.brand('/offline')} roda o servidor no seu computador (temporário) · /offline off volta
190
+ ${c.brand('/doctor')} confere tudo (servidor, /api, Chrome, disco…) e diz como consertar
191
+ ${c.brand('/custo')} tokens e pedidos desta sessão, por provedor
192
+ ${c.brand('/atalhos')} as teclas que ajudam
114
193
  ${c.brand('/name')} <nome> como o Primo te chama
115
- ${c.brand('/mascot')} <estilo> mascote: primo|fox|cat
194
+ ${c.brand('/mascot')} [nome|n] galeria dos mascotes · /mascot reacoes on|off
116
195
  ${c.brand('/config')} mostra suas preferências
117
196
  ${c.brand('/clear')} limpa a conversa ${c.brand('/cls')} limpa a tela
118
197
  ${c.brand('/uninstall')} remove o PrimoCode
@@ -138,19 +217,28 @@ ${c.bold('Onde ele trabalha')}
138
217
  const SLASH_COMANDOS = [
139
218
  ['/help', 'todos os comandos'],
140
219
  ['/project', 'lista/cria projetos em ~/primocode'],
220
+ ['/projetos', 'projetos do mais recente, com tamanho · /projetos 2 entra'],
221
+ ['/novo', 'começa um projeto novo do zero: /novo site da padaria'],
141
222
  ['/dir', 'trabalhar numa pasta sua'],
142
223
  ['/mode', 'primocode | chat | auto'],
143
224
  ['/effort', 'esforço: low → ultracode (←/→ para ajustar)'],
225
+ ['/permissoes', 'auto | perguntar | livre — quando ele pede licença'],
144
226
  ['/compact', 'resume a conversa e devolve o contexto'],
227
+ ['/init', 'estuda o projeto e escreve o PRIMO.md (as instruções dele)'],
145
228
  ['/attach', 'anexa arquivo ao pedido'],
146
229
  ['/detach', 'remove os anexos'],
147
230
  ['/files', 'lista os anexos'],
148
231
  ['/all', 'tudo que ele sabe fazer'],
232
+ ['/desfazer', 'desfaz o que o último pedido mudou nos arquivos (lista · N)'],
149
233
  ['/tree', 'árvore do projeto'],
150
234
  ['/read', 'mostra um arquivo'],
151
235
  ['/run', 'roda um comando'],
152
236
  ['/open', 'abre no navegador'],
237
+ ['/abrir', 'abre o projeto atual: o servidor de pé, ou a index.html'],
238
+ ['/copiar', 'copia a última resposta'],
239
+ ['/historico', 'procura nos seus pedidos antigos: /historico login'],
153
240
  ['/rc', 'abre a janela do PrimoCode'],
241
+ ['/ide', 'abre a janela com a prévia do site e os arquivos ao lado'],
154
242
  ['/login', 'entra na sua conta Conecta Primo'],
155
243
  ['/account', 'sua conta, plano e computadores conectados'],
156
244
  ['/chats', 'suas conversas salvas, as mesmas do app e do celular'],
@@ -161,11 +249,16 @@ const SLASH_COMANDOS = [
161
249
  ['/skills', 'o que ele sabe fazer bem — ligue e desligue'],
162
250
  ['/marketplace', 'a tela das skills: navegue, ligue, crie'],
163
251
  ['/install-skill', 'instala uma skill de um arquivo'],
252
+ ['/comandos', 'seus comandos: um .md vira /nome · /comandos novo <nome>'],
164
253
  ['/api', 'usa a SUA chave (Gemini, ChatGPT, Claude…) direto daqui, sem o limite do servidor'],
165
254
  ['/status', 'checa o servidor'],
255
+ ['/offline', 'roda o servidor aqui no seu computador, com as chaves do Render'],
256
+ ['/doctor', 'confere se está tudo certo e diz como consertar'],
257
+ ['/custo', 'tokens e pedidos desta sessão, por provedor'],
258
+ ['/atalhos', 'as teclas que ajudam'],
166
259
  ['/connectors', 'quem está atendendo os seus pedidos'],
167
260
  ['/name', 'como ele te chama'],
168
- ['/mascot', 'primo | fox | cat'],
261
+ ['/mascot', 'galeria dos mascotes · escolha por nome ou número'],
169
262
  ['/config', 'suas preferências'],
170
263
  ['/clear', 'limpa a conversa'],
171
264
  ['/cls', 'limpa a tela'],
@@ -312,9 +405,34 @@ function aplicarEffort(nome) {
312
405
  // Enquanto o slider está no ar, o fantasma do autocompletar não desenha.
313
406
  let sliderAtivo = false;
314
407
 
408
+ /* Os comandos da pessoa (lib/comandos.js), com o do projeto atual por cima.
409
+ A caixa de digitar pede a lista a cada tecla para desenhar o fantasma; um
410
+ cache de dois segundos poupa ler três pastas por letra digitada, e ainda
411
+ pega na hora um arquivo que a pessoa acabou de salvar. */
412
+ let cacheComandos = { em: 0, dir: null, lista: [] };
413
+ function comandosDaPessoa() {
414
+ const agora = Date.now();
415
+ if (agora - cacheComandos.em > 2000 || cacheComandos.dir !== state.workDir) {
416
+ let lista = [];
417
+ try { lista = comandosLib.carregar({ projectDir: state.workDir }); } catch { /* disco estranho não trava a caixa */ }
418
+ cacheComandos = { em: agora, dir: state.workDir, lista };
419
+ }
420
+ return cacheComandos.lista;
421
+ }
422
+
423
+ /* O menu e o fantasma enxergam os dois tipos. O embutido vem primeiro e,
424
+ com o mesmo nome, o da pessoa nem aparece: quem ganha é sempre o embutido. */
425
+ function comandosDoMenu() {
426
+ const embutidos = new Set(SLASH_COMANDOS.map(([n]) => n));
427
+ const deles = comandosDaPessoa()
428
+ .filter((x) => !embutidos.has('/' + x.nome))
429
+ .map((x) => ['/' + x.nome, x.descricao || `seu comando (${x.origem})`]);
430
+ return SLASH_COMANDOS.concat(deles);
431
+ }
432
+
315
433
  function mostrarMenuSlash() {
316
434
  console.log('');
317
- for (const [nome, desc] of SLASH_COMANDOS) {
435
+ for (const [nome, desc] of comandosDoMenu()) {
318
436
  console.log(` ${c.brand(ui.padRight(nome, 12))}${c.muted(desc)}`);
319
437
  }
320
438
  console.log(c.dim('\n Tab completa · metade do comando já serve: /eff vira /effort'));
@@ -554,14 +672,18 @@ const RE_PROJETO_NOVO = /\b(novo projeto|projeto novo|outro projeto|do zero|come
554
672
  const RE_CRIAR_UM = /\b(crie|cria|criar|fa[cç]a|faz|fazer|monte|monta|montar|gere|gera|desenvolva|construa|programe|quero|preciso de)\s+(um|uma|outro|outra|novo|nova)\b/;
555
673
  const RE_REFERE_ATUAL = /\b(isso|isto|esse|essa|este|esta|nele|nela|dele|dela|no projeto|nesse|nessa|neste|nesta|continua|continue|edita|edite|muda|mude|melhora|melhore|adiciona|adicione|coloca|coloque|p[oõ]e|corrige|corrija|arruma|arrume|ajusta|ajuste)\b/;
556
674
 
675
+ /* "crie uma nova seção", "cria um botão", "faz uma tela de login": é um
676
+ PEDAÇO do projeto em que se está, não outro projeto. */
677
+ const RE_PEDACO = /\b(um|uma|outro|outra|novo|nova)\s+(nov[ao]\s+)?(secao|secoes|parte|botao|botoes|tela|aba|menu|rodape|cabecalho|formulario|fase|funcao|funcionalidade|campo|imagem|animacao|efeito|cor|fonte|pagina interna|area|bloco|card|modal|link)\b/;
678
+
557
679
  function pedeProjetoNovo(texto) {
558
680
  const t = semAcento(texto);
559
681
  if (RE_PROJETO_NOVO.test(t)) return true;
560
- if (!RE_CRIAR_UM.test(t) || RE_REFERE_ATUAL.test(t)) return false;
682
+ if (!RE_CRIAR_UM.test(t) || RE_REFERE_ATUAL.test(t) || RE_PEDACO.test(t)) return false;
561
683
  // Mesmo assunto do projeto atual ("crie uma fase nova pro jogo de carro")
562
684
  // é continuação, não projeto novo.
563
- const atual = new Set(cfg.slugify(state.currentProject || '').split('-').filter((x) => x.length > 2));
564
- const pedido = cfg.slugify(deriveProjectName(texto)).split('-').filter((x) => x.length > 2);
685
+ const atual = new Set(cfg.tokensDoAssunto(state.currentProject || ''));
686
+ const pedido = cfg.tokensDoAssunto(deriveProjectName(texto));
565
687
  return !pedido.some((x) => atual.has(x));
566
688
  }
567
689
 
@@ -612,6 +734,8 @@ function ehSoEstudio(texto) {
612
734
  }
613
735
 
614
736
  function planWorkDir(userText) {
737
+ // O /novo já criou a pasta e entrou nela: nenhuma heurística decide outra.
738
+ if (state.projetoFixo) return { dir: state.workDir, pending: false, name: state.currentProject };
615
739
  const escolhida = ['/dir', 'detectada', 'projeto'].includes(state.workDirOrigem);
616
740
  // Pasta escolhida a dedo (/dir, ou dita no pedido) manda sempre. Projeto
617
741
  // em andamento manda também — a menos que o pedido abra outra coisa.
@@ -633,16 +757,17 @@ function planWorkDir(userText) {
633
757
  paralelo com o trabalho: a pasta só nasce na primeira escrita, e até lá a
634
758
  resposta já chegou. Só pergunta quando o pedido é de CRIAR algo — pedir
635
759
  para abrir o Chrome não gasta chamada com nome de pasta. */
760
+ async function perguntarNome(texto) {
761
+ let resposta = '';
762
+ await api.requestChat(state.config.server, {
763
+ prompt: texto, history: [], model: state.config.model, token: state.config.token,
764
+ }, (evt) => { if (evt.delta) resposta += evt.delta; });
765
+ return resposta;
766
+ }
767
+
636
768
  function batizarDepois(plan, userText) {
637
769
  if (!plan.pending || !(RE_CRIAR_UM.test(semAcento(userText)) || PEDIDO_DE_CODIGO.test(userText))) return;
638
- const perguntar = async (texto) => {
639
- let resposta = '';
640
- await api.requestChat(state.config.server, {
641
- prompt: texto, history: [], model: state.config.model, token: state.config.token,
642
- }, (evt) => { if (evt.delta) resposta += evt.delta; });
643
- return resposta;
644
- };
645
- nomeProjeto.escolher({ pedido: userText, nomes: cfg.listProjects(), perguntar })
770
+ plan.batizando = nomeProjeto.escolher({ pedido: userText, nomes: cfg.listProjects(), perguntar: perguntarNome })
646
771
  .then((nome) => { if (nome) plan.nomeEscolhido = nome; });
647
772
  }
648
773
 
@@ -726,8 +851,15 @@ function prepararPasta(userText) {
726
851
  * Enter sozinho é NÃO. O padrão de quem apertou Enter sem ler não pode ser
727
852
  * entregar o mouse e o teclado.
728
853
  */
729
- async function autorizarControle(oQueVaiFazer, { primeira } = {}) {
730
- if (primeira) {
854
+ async function autorizarControle(oQueVaiFazer, { primeira, tipo } = {}) {
855
+ /* O mesmo portão serve ao /permissoes (lib/perigo.js): a pergunta muda de
856
+ cabeçalho, as três respostas são as mesmas. */
857
+ if (tipo) {
858
+ console.log('');
859
+ console.log(' ' + c.warn('▸▸') + ' ' + c.bold(c.white(tipo === 'perigo'
860
+ ? 'Isto não tem volta. O Primo quer:'
861
+ : 'O Primo quer (você escolheu /permissoes perguntar):')));
862
+ } else if (primeira) {
731
863
  console.log('');
732
864
  console.log(' ' + c.warn('▸▸') + ' ' + c.bold(c.white('O Primo quer usar o seu computador.')));
733
865
  console.log(' ' + c.muted('Ele vai mexer no mouse e no teclado de verdade, no programa que'));
@@ -758,7 +890,15 @@ async function autorizarControle(oQueVaiFazer, { primeira } = {}) {
758
890
  * simplesmente travava. Abrir o /voice e PEDIR em voz para ele mexer no
759
891
  * computador já é a autorização — é o produto funcionando como o Jarvis. Fica
760
892
  * dito no terminal, a cada ação, o que ele fez; e fechar o PrimoCode esquece. */
761
- async function autorizarPorVoz(oQueVaiFazer, { primeira } = {}) {
893
+ async function autorizarPorVoz(oQueVaiFazer, { primeira, tipo } = {}) {
894
+ /* Pedir em voz é autorizar o que foi pedido — mas não o que não tem volta.
895
+ Ninguém está lendo o terminal para ver um `rm -rf` passar: o perigoso
896
+ é recusado, e o agente conta em voz o que ficou por fazer. */
897
+ if (tipo === 'perigo') {
898
+ console.log(' ' + c.warn('▸▸ recusado no modo voz: ') + c.muted(oQueVaiFazer));
899
+ return 'nao';
900
+ }
901
+ if (tipo) return 'agora';
762
902
  if (primeira) {
763
903
  console.log(' ' + c.warn('▸▸') + ' ' + c.muted('Modo voz: o Primo vai usar o mouse e o teclado. Você vê o cursor dele na tela.'));
764
904
  }
@@ -935,9 +1075,23 @@ async function sendPrompt(userText) {
935
1075
  return;
936
1076
  }
937
1077
 
1078
+ // PRIMOCODE_TEMPOS=1: uma linha no fim com o custo de cada fase.
1079
+ tempos.comecar();
1080
+ try { await enviarPedido(userText); }
1081
+ finally {
1082
+ const linha = tempos.relatorio();
1083
+ if (linha) console.log(c.dim(` ⏱ ${linha}`));
1084
+ }
1085
+ }
1086
+
1087
+ async function enviarPedido(userText) {
938
1088
  let prompt = userText;
939
- if (state.attachedFiles.length) {
940
- const parts = state.attachedFiles.map((f) => `Arquivo: ${f.path}\n\`\`\`\n${f.content}\n\`\`\``);
1089
+ // "@index.html" no pedido é um /attach só deste pedido: vai junto dos
1090
+ // anexos, mas não fica em state.attachedFiles para o próximo.
1091
+ const citados = mencoesDoPedido(userText);
1092
+ const anexos = [...state.attachedFiles, ...citados];
1093
+ if (anexos.length) {
1094
+ const parts = anexos.map((f) => `Arquivo: ${f.path}\n\`\`\`\n${f.content}\n\`\`\``);
941
1095
  prompt = parts.join('\n\n') + '\n\n' + userText;
942
1096
  }
943
1097
 
@@ -967,15 +1121,22 @@ async function sendPrompt(userText) {
967
1121
 
968
1122
  // O resumo da rodada anterior é feito em segundo plano logo depois
969
1123
  // dela; aqui só se espera ele terminar, para não sair com memória velha.
970
- await esperarResumo();
971
-
1124
+ //
972
1125
  // O pedido passa por uma cabeça antes de virar trabalho: conserta o
973
1126
  // que está torto, diz qual é o entregável e o que conta como pronto.
974
1127
  // UMA chamada faz as duas coisas: arruma o pedido torto e, se fizer
975
1128
  // sentido, divide em passos. Eram duas idas ao modelo por pedido, e
976
1129
  // num plano limitado por REQUISIÇÕES (o grátis da Groq são 30 por
977
1130
  // minuto) cada chamada extra custa tanto quanto token.
978
- const passos = await dividirTarefa(prompt);
1131
+ //
1132
+ // As duas esperas correm JUNTAS. Uma não lê o que a outra escreve: o
1133
+ // plano só olha o texto do pedido, e o resumo só a conversa de antes.
1134
+ // Em fila, o pedido de duas fases pagava o resumo e DEPOIS o plano —
1135
+ // duas idas ao modelo em série antes da primeira rodada.
1136
+ const [, passos] = await Promise.all([
1137
+ tempos.medir('espera do resumo', () => esperarResumo()),
1138
+ tempos.medir('plano', () => dividirTarefa(prompt)),
1139
+ ]);
979
1140
 
980
1141
  let r = null;
981
1142
  let jaFeito = '';
@@ -984,6 +1145,10 @@ async function sendPrompt(userText) {
984
1145
  interessa é o do pedido inteiro: quem pediu um vídeo não quer três
985
1146
  balanços, quer saber o que existe no disco quando ele terminar. */
986
1147
  const livro = registro.novo();
1148
+ // Um checkpoint por PEDIDO (não por passo): é o que o /desfazer volta.
1149
+ // Com a pasta: a foto do projeto inteiro sai AGORA, antes de qualquer
1150
+ // tool — é ela que dá volta ao `rm`/`mv` que o run_shell fizer.
1151
+ checkpoint.iniciar(userText, projectDir());
987
1152
  for (let i = 0; i < passos.length; i++) {
988
1153
  if (i > 0) console.log(c.dim(` ⎿ ${passos[i].titulo}`));
989
1154
  // Só fato de passo CONCLUÍDO entra aqui. Num pedido real o passo de
@@ -1013,8 +1178,11 @@ async function sendPrompt(userText) {
1013
1178
  ensureWorkDir: plan.pending ? () => createPendingProject(plan) : null,
1014
1179
  // O portão do computador: quem pergunta é o terminal, quem
1015
1180
  // lembra da resposta é a sessão.
1016
- autorizar: autorizarControle,
1181
+ // No pedido único (`primo "…"`) não há ninguém para responder:
1182
+ // sem `autorizar`, o que pede licença é recusado (lib/perigo.js).
1183
+ autorizar: state.semTerminal ? undefined : autorizarControle,
1017
1184
  controle: state.controle,
1185
+ permissoes: state.config.permissoes,
1018
1186
  // Pedido de mexer no computador leva a dica que libera as
1019
1187
  // ferramentas desktop_* no servidor (ver DICA_COMPUTADOR).
1020
1188
  instrucao: i === 0 && querComputador(userText) ? vozModo.DICA_COMPUTADOR : '',
@@ -1058,12 +1226,19 @@ async function sendPrompt(userText) {
1058
1226
  : '';
1059
1227
  if (r.parado) break; // Esc Esc no meio do plano para o plano inteiro
1060
1228
  }
1229
+ checkpoint.encerrar();
1061
1230
 
1062
1231
  /* O balanço, depois da resposta. Depois, e não antes, de propósito: o
1063
1232
  que a pessoa quer ler primeiro é o que ele TEM A DIZER; a lista de
1064
1233
  arquivos é a conferência, e conferência se lê por último. */
1065
1234
  const balanco = registro.resumo(livro, { colunas: process.stdout.columns || 80 });
1066
1235
  if (balanco) console.log(balanco);
1236
+ // O rostinho do mascote, por último e numa linha só. O humor sai do
1237
+ // que aconteceu, não de adivinhar pelo texto da resposta.
1238
+ if (state.config.reacoes !== false && r) {
1239
+ const humor = r.parado ? 'parado' : r.concluiu === false ? 'preocupado' : 'feliz';
1240
+ console.log(ui.reacao(state.config.mascot, humor));
1241
+ }
1067
1242
  state.history.push({ role: 'user', content: userText });
1068
1243
  state.history.push({ role: 'assistant', content: r.assistantMessage.content });
1069
1244
  resumirDepois();
@@ -1074,8 +1249,8 @@ async function sendPrompt(userText) {
1074
1249
  state.gravador.turno(userText, r.assistantMessage.content,
1075
1250
  { modo: 'primocode', projeto: state.currentProject || null });
1076
1251
  } else {
1077
- await esperarResumo();
1078
- await runChat(prompt, userText);
1252
+ await tempos.medir('espera do resumo', () => esperarResumo());
1253
+ await tempos.medir('chat', () => runChat(prompt, userText));
1079
1254
  }
1080
1255
 
1081
1256
  state.attachedFiles = [];
@@ -1251,6 +1426,31 @@ async function esperarResumo(tetoMs = 15000) {
1251
1426
  /* A linha que vai junto do resumo: em que projeto se está. É o que faz
1252
1427
  "continua o jogo" e "muda a cor" caírem no mesmo projeto em vez de inventar
1253
1428
  outro. */
1429
+ function mencoesDoPedido(userText) {
1430
+ const { arquivos, ignorados } = mencoesLib.mencoes(userText, projectDir());
1431
+ const lidos = [];
1432
+ for (const a of arquivos) {
1433
+ try {
1434
+ lidos.push({ path: a.rel, content: fs.readFileSync(a.abs, 'utf-8') });
1435
+ console.log(c.dim(` ⎿ anexado: ${a.rel} (${tools.fmtBytes(a.bytes)})`));
1436
+ } catch { console.log(c.dim(` ⎿ não consegui ler: ${a.rel}`)); }
1437
+ }
1438
+ for (const i of ignorados) console.log(c.dim(` ⎿ não anexei ${i.rel}: ${i.motivo}`));
1439
+ return lidos;
1440
+ }
1441
+
1442
+ /* O pedido do /init. Passa pelo MESMO caminho de um pedido digitado
1443
+ (sendPrompt → runAct), então usa as ferramentas e o provedor de agora — um
1444
+ /init que falasse direto com o modelo não enxergaria os arquivos. */
1445
+ const PEDIDO_INIT = 'Estude este projeto (a pasta atual) e escreva um arquivo PRIMO.md na raiz dela. '
1446
+ + 'Primeiro olhe de verdade: liste os arquivos, leia o README, o package.json (ou o equivalente) e os arquivos principais. '
1447
+ + 'Depois escreva o PRIMO.md em português, curto (menos de 60 linhas), com estas seções: '
1448
+ + 'o que é o projeto (2 ou 3 frases); como rodar (os comandos exatos que você viu, de instalar, rodar e testar); '
1449
+ + 'a stack (linguagem, frameworks, banco); as convenções que o código segue (nomes, estilo, onde ficam os testes); '
1450
+ + 'e onde está cada coisa (as pastas e arquivos que importam, uma linha cada). '
1451
+ + 'Só escreva o que você conferiu nos arquivos — nada de inventar comando ou dependência. '
1452
+ + 'Esse arquivo vai ser lido no começo de todo pedido neste projeto, então cada linha precisa valer o espaço.';
1453
+
1254
1454
  function contextoDoProjeto() {
1255
1455
  if (!state.currentProject) return '';
1256
1456
  return `Projeto atual: ${state.currentProject} (pasta ${shortPath(projectDir())}). `
@@ -1258,6 +1458,14 @@ function contextoDoProjeto() {
1258
1458
  + 'trabalhe nos arquivos que já existem lá, não crie outro projeto.';
1259
1459
  }
1260
1460
 
1461
+ /* O chat não passa pelo runAct, e é no runAct (buildMemoryContext) que as
1462
+ instruções do projeto entram. Sem isto o PRIMO.md valeria para o agente e
1463
+ seria ignorado quando o modo auto manda o pedido para a conversa. */
1464
+ function comInstrucoes(history) {
1465
+ const texto = mencoesLib.instrucoesDoProjeto(state.currentProject || state.workDir ? projectDir() : null);
1466
+ return texto ? [{ role: 'system', content: texto }, ...history] : history;
1467
+ }
1468
+
1261
1469
  // O chat também precisa parar no Esc Esc. Sem isto o texto continuava saindo
1262
1470
  // na tela depois de o usuário ter mandado parar.
1263
1471
  let chatEmVoo = null;
@@ -1284,7 +1492,7 @@ async function runChat(promptForModel, userTextForMemory) {
1284
1492
  try {
1285
1493
  await api.requestChat(state.config.server, {
1286
1494
  prompt: promptForModel,
1287
- history: memoria.paraEnviar(state.memoria, state.history, contextoDoProjeto()),
1495
+ history: comInstrucoes(memoria.paraEnviar(state.memoria, state.history, contextoDoProjeto())),
1288
1496
  model: state.config.model,
1289
1497
  token: state.config.token,
1290
1498
  sinal: chatEmVoo.signal,
@@ -1404,13 +1612,59 @@ const RENOMEADOS = {
1404
1612
  conta: 'account',
1405
1613
  sair: 'exit', // sair é SAIR; desconectar a conta é /logout
1406
1614
  // o histórico
1407
- conversas: 'chats', historico: 'chats', histórico: 'chats',
1615
+ // /historico deixou de ser apelido do /chats: agora procura nos pedidos
1616
+ // guardados nesta máquina (lib/comandos-extra.js). /chats segue sendo a
1617
+ // conversa inteira, da conta.
1618
+ conversas: 'chats',
1408
1619
  // o resto
1409
1620
  pasta: 'dir',
1410
1621
  esforco: 'effort', esforço: 'effort',
1411
1622
  tudo: 'all', capacidades: 'all',
1412
1623
  };
1413
1624
 
1625
+ /* Abrir uma conversa salva e seguir dela. Uma função só, para o /chats n e o
1626
+ `primo -c` carregarem a conversa do MESMO jeito: dois caminhos divergiriam
1627
+ no primeiro detalhe (a memória, o gravador) e um deles gravaria errado. */
1628
+ async function abrirConversa(alvo) {
1629
+ const sp = new Spinner('abrindo').start();
1630
+ const conv = await nuvem.lerConversa(alvo.id);
1631
+ sp.stop();
1632
+ if (!conv.ok) return conv;
1633
+ // A memória entra ZERADA: o resumo antigo não veio junto, e um
1634
+ // resumo de outra conversa colado aqui faria o modelo responder
1635
+ // sobre coisa que não foi dita nesta.
1636
+ state.history = conv.mensagens.map((m) => ({
1637
+ role: m.papel, content: m.texto,
1638
+ }));
1639
+ state.memoria = memoria.nova();
1640
+ state.gravador.retomar(alvo.id, alvo.titulo, conv.mensagens.length);
1641
+ return conv;
1642
+ }
1643
+
1644
+ /* `primo --continue` / `primo -c`: volta na última conversa, sem passar pelo
1645
+ /chats. A lista do servidor já vem da mais recente para a mais antiga, então
1646
+ a última é a primeira. Sem conta ou sem conversa, começa do zero e diz. */
1647
+ async function continuarUltima() {
1648
+ if (!conta.atual()) {
1649
+ console.log(c.dim(' ⎿ sem conta conectada, não há conversa para continuar — começando do zero'));
1650
+ return false;
1651
+ }
1652
+ const l = await nuvem.listarConversas({ limite: 1 });
1653
+ const alvo = l.ok && l.conversas[0];
1654
+ if (!alvo) {
1655
+ console.log(c.dim(' ⎿ nenhuma conversa salva ainda — começando do zero'));
1656
+ return false;
1657
+ }
1658
+ const conv = await abrirConversa(alvo);
1659
+ if (!conv.ok) {
1660
+ console.log(c.dim(` ⎿ não consegui abrir a última conversa (${conv.error}) — começando do zero`));
1661
+ return false;
1662
+ }
1663
+ const quando = alvo.atualizada ? new Date(alvo.atualizada).toLocaleString('pt-BR') : 'data desconhecida';
1664
+ console.log(c.dim(` ⎿ continuando: ${alvo.titulo.slice(0, 60)} (${quando})`));
1665
+ return true;
1666
+ }
1667
+
1414
1668
  async function handleSlash(text) {
1415
1669
  const [cmd] = text.trim().slice(1).split(/\s+/);
1416
1670
  const rest = text.trim().slice(1 + cmd.length).trim();
@@ -1425,16 +1679,33 @@ async function handleSlash(text) {
1425
1679
  switch (nome) {
1426
1680
  case 'help': console.log(HELP); break;
1427
1681
 
1428
- case 'project': {
1429
- if (!rest) {
1430
- const projects = cfg.listProjects();
1431
- if (!projects.length) { console.log(c.muted(' Nenhum projeto ainda. Peça algo e eu crio o primeiro.')); break; }
1432
- console.log('');
1433
- for (const p of projects) {
1434
- const mark = p === state.currentProject ? c.brand(' ●') : ' ';
1435
- console.log(`${mark} ${p === state.currentProject ? c.brand(p) : c.white(p)}`);
1682
+ /* /projetos e /project sem nome são a MESMA lista: antes o /project só
1683
+ listava os nomes, em ordem alfabética, e quem procurava "o de ontem"
1684
+ tinha de abrir pasta por pasta. Agora vem do mais recente, com
1685
+ quando mexeu e o tamanho, e um número para entrar. */
1686
+ case 'projetos': case 'project': {
1687
+ const lista = extra.listarProjetos(cfg.ROOT_DIR);
1688
+ const numero = /^\d+$/.test(rest) ? Number(rest) : 0;
1689
+ if (!rest || (numero && nome === 'projetos') || (numero && numero <= lista.length)) {
1690
+ if (!lista.length) { console.log(c.muted(' Nenhum projeto ainda. Peça algo e eu crio o primeiro.')); break; }
1691
+ if (numero) {
1692
+ const alvo = lista[numero - 1];
1693
+ if (!alvo) { console.log(c.err(` Não há projeto ${numero}. São ${lista.length}.`)); break; }
1694
+ entrarNoProjeto(alvo.dir, 'projeto');
1695
+ console.log(c.ok(` Trabalhando em ${alvo.nome}`) + c.muted(` (${shortPath(alvo.dir)})`));
1696
+ refreshPrompt();
1697
+ break;
1436
1698
  }
1437
- console.log(c.muted(`\n ${cfg.ROOT_DIR}`));
1699
+ console.log('');
1700
+ const largura = Math.min(34, Math.max(...lista.map((x) => x.nome.length)));
1701
+ lista.forEach((x, i) => {
1702
+ const aqui = x.nome === state.currentProject;
1703
+ const nomeCol = ui.padRight(ui.truncate(aqui ? c.brand(x.nome) : c.white(x.nome), largura), largura);
1704
+ console.log(` ${aqui ? c.brand('●') : ' '} ${c.dim(String(i + 1).padStart(2))} ${nomeCol} `
1705
+ + c.muted(ui.padRight(extra.quandoFoi(x.modificado), 11))
1706
+ + c.dim(`${extra.fmtTamanho(x.bytes)}${x.cortado ? '+' : ''} · ${x.arquivos} arq.`));
1707
+ });
1708
+ console.log(c.dim(`\n ${shortPath(cfg.ROOT_DIR)} · /projetos 1 entra no primeiro · /novo <descrição> começa outro`));
1438
1709
  break;
1439
1710
  }
1440
1711
  const dir = cfg.createProjectDir(rest);
@@ -1496,6 +1767,38 @@ async function handleSlash(text) {
1496
1767
  break;
1497
1768
  }
1498
1769
 
1770
+ /* Quando ele pergunta antes de agir. Os três modos e o porquê de cada
1771
+ um estão em lib/perigo.js. */
1772
+ case 'permissoes':
1773
+ case 'permissions': {
1774
+ const DIZ = {
1775
+ auto: 'roda tudo, mas pergunta antes do que não tem volta (apagar fora do projeto, push --force, sudo…)',
1776
+ perguntar: 'pergunta antes de todo comando e de toda escrita de arquivo',
1777
+ livre: 'nunca pergunta — nem antes do que não tem volta',
1778
+ };
1779
+ const atual = perigo.normalizarModo(state.config.permissoes);
1780
+ if (!rest) {
1781
+ console.log(c.muted(' Agora: ') + c.brand(atual) + c.muted(' — ' + DIZ[atual]));
1782
+ for (const m of perigo.MODOS) console.log(` ${m === atual ? c.brand('● ' + m) : c.muted('○ ' + m)} ${c.dim(DIZ[m])}`);
1783
+ console.log(c.dim(' Use /permissoes auto|perguntar|livre'));
1784
+ break;
1785
+ }
1786
+ const pedido = rest.toLowerCase();
1787
+ if (!perigo.MODOS.includes(perigo.normalizarModo(pedido)) || (perigo.normalizarModo(pedido) === 'auto' && pedido !== 'auto')) {
1788
+ console.log(c.err('Uso: /permissoes auto|perguntar|livre'));
1789
+ break;
1790
+ }
1791
+ const m = perigo.normalizarModo(pedido);
1792
+ state.config.permissoes = m;
1793
+ cfg.saveConfig({ permissoes: m });
1794
+ console.log(c.ok(` Permissões: ${m}`) + c.muted(' — ' + DIZ[m]));
1795
+ if (m === 'livre') {
1796
+ console.log(' ' + c.warn('▸▸ Atenção: ele vai apagar, forçar push e usar sudo sem te perguntar.'));
1797
+ console.log(' ' + c.warn(' O que der errado pode não ter volta. /permissoes auto desfaz.'));
1798
+ }
1799
+ break;
1800
+ }
1801
+
1499
1802
  case 'mode': {
1500
1803
  const m = rest.toLowerCase();
1501
1804
  if (['primocode', 'chat', 'auto'].includes(m)) {
@@ -1556,6 +1859,20 @@ async function handleSlash(text) {
1556
1859
  break;
1557
1860
  }
1558
1861
 
1862
+ case 'init': {
1863
+ const alvo = path.join(projectDir(), 'PRIMO.md');
1864
+ if (fs.existsSync(alvo) && !(await confirm(`Já existe um PRIMO.md em ${shortPath(projectDir())}. Escrever outro por cima?`))) {
1865
+ console.log(c.muted(' Deixei o PRIMO.md como estava.'));
1866
+ break;
1867
+ }
1868
+ // Escrever arquivo pede o agente: no modo chat (ou num auto que
1869
+ // achasse que é conversa) o /init só responderia um texto.
1870
+ const modoAntes = state.mode;
1871
+ state.mode = 'primocode';
1872
+ try { await sendPrompt(PEDIDO_INIT); } finally { state.mode = modoAntes; }
1873
+ break;
1874
+ }
1875
+
1559
1876
  case 'detach': state.attachedFiles = []; console.log(c.ok('Anexos removidos.')); refreshPrompt(); break;
1560
1877
 
1561
1878
  case 'files':
@@ -1581,6 +1898,41 @@ async function handleSlash(text) {
1581
1898
  break;
1582
1899
  }
1583
1900
 
1901
+ /* ── /desfazer ────────────────────────────────────────────────
1902
+ * Quem não sabe git não tem como voltar uma edição ruim. Aqui volta:
1903
+ * cada pedido tirou uma foto do projeto (com git) ou guardou o
1904
+ * original dos arquivos que mexeu (sem git). */
1905
+ case 'desfazer': case 'undo': case 'rewind': {
1906
+ const cps = checkpoint.lista(projectDir());
1907
+ if (!cps.length) { console.log(c.muted(' Nada para desfazer nesta pasta.')); break; }
1908
+ const quando = (cp) => new Date(cp.quando).toLocaleString('pt-BR', { dateStyle: 'short', timeStyle: 'short' });
1909
+ if (/^(lista|list|ls)$/i.test(rest)) {
1910
+ cps.forEach((cp, i) => console.log(` ${c.brand(String(i + 1).padStart(2))} ${c.dim(quando(cp))} `
1911
+ + `${c.white(cp.pedido.slice(0, 50) + (cp.pedido.length > 50 ? '…' : ''))} `
1912
+ + c.muted(`${cp.mudou.length} ${cp.mudou.length === 1 ? 'arquivo mudou' : 'arquivos mudaram'}`)));
1913
+ console.log(c.muted('\n /desfazer N volta os N últimos pedidos.'));
1914
+ if (checkpoint.modo(projectDir()) === 'arquivo') {
1915
+ console.log(c.muted(' Sem git aqui: só volta o que o agente escreveu com as ferramentas de arquivo (não o que um comando apagou).'));
1916
+ }
1917
+ break;
1918
+ }
1919
+ const n = rest ? parseInt(rest, 10) : 1;
1920
+ if (!(n >= 1)) { console.log(c.err('Uso: /desfazer · /desfazer lista · /desfazer N')); break; }
1921
+ const alvo = cps.slice(0, n);
1922
+ console.log(c.muted(`\n Volta ${alvo.length === 1 ? 'o último pedido' : `os ${alvo.length} últimos pedidos`}:`));
1923
+ for (const cp of alvo) console.log(` ${c.dim(quando(cp))} ${c.white(cp.pedido.slice(0, 60))}`);
1924
+ const previa = checkpoint.previa(projectDir(), n);
1925
+ if (!previa.length) console.log(c.muted(' (o disco já está como estava antes)'));
1926
+ for (const a of previa.slice(0, 40)) console.log(c.muted(` ${a.acao} `) + shortPath(a.caminho));
1927
+ if (previa.length > 40) console.log(c.muted(` … e mais ${previa.length - 40}`));
1928
+ if (!(await confirm('Desfazer?'))) { console.log(c.muted('Cancelado.')); break; }
1929
+ for (const f of checkpoint.desfazer(projectDir(), n)) {
1930
+ const rotulo = f.acao === 'falhou' ? c.err(` falhou: ${f.erro} `) : c.ok(` ${f.acao} `);
1931
+ console.log(rotulo + shortPath(f.caminho));
1932
+ }
1933
+ break;
1934
+ }
1935
+
1584
1936
  case 'tree': console.log(tools.treeDir(projectDir(), parseInt(rest, 10) || 2)); break;
1585
1937
 
1586
1938
  case 'read': {
@@ -1603,8 +1955,57 @@ async function handleSlash(text) {
1603
1955
  break;
1604
1956
  }
1605
1957
 
1606
- case 'open': {
1607
- if (!rest) { console.log(c.err('Uso: /open <arquivo|url>')); break; }
1958
+ /* /novo: projeto novo por ORDEM da pessoa. Sem ele, "crie um site de
1959
+ receitas" pode cair dentro de "site-receitas-velho" porque o nome
1960
+ parece (findSimilarProject) — e quem quer começar do zero não tem
1961
+ como dizer isso. Aqui a pasta nasce antes, com nome que não colide,
1962
+ e o pedido roda preso nela. */
1963
+ case 'novo': case 'new': {
1964
+ if (!rest) { console.log(c.err('Uso: /novo <o que você quer criar>') + c.dim(' ex.: /novo site da padaria')); break; }
1965
+ const nomes = cfg.listProjects();
1966
+ const sp = new Spinner('escolhendo o nome da pasta').start();
1967
+ const sugerido = await nomeProjeto.escolher({ pedido: rest, nomes, perguntar: perguntarNome }).catch(() => null);
1968
+ sp.stop();
1969
+ const nomeNovo = extra.nomeDoNovo({
1970
+ descricao: rest, nomes, sugerido, derivar: deriveProjectName,
1971
+ slugify: cfg.slugify, semColisao: nomeProjeto.semColisao,
1972
+ });
1973
+ const dir = cfg.createProjectDir(nomeNovo);
1974
+ entrarNoProjeto(dir, 'projeto');
1975
+ // Conversa nova junto: o que se falou do projeto anterior puxaria o
1976
+ // agente de volta para ele.
1977
+ state.history = []; state.memoria = memoria.nova(); state.gravador.zerar();
1978
+ console.log(c.ok(` Projeto novo: ${state.currentProject}`) + c.muted(` (${shortPath(dir)})`));
1979
+ refreshPrompt();
1980
+ const modoAntes = state.mode;
1981
+ state.mode = 'primocode';
1982
+ state.projetoFixo = true;
1983
+ try { await sendPrompt(extra.pedidoDoNovo(rest, state.currentProject)); }
1984
+ finally { state.mode = modoAntes; state.projetoFixo = false; }
1985
+ break;
1986
+ }
1987
+
1988
+ /* /open sem nada (e /abrir) acha sozinho o que abrir: o servidor do
1989
+ projeto de pé, ou a index.html. Antes pedia o caminho, e quem não
1990
+ sabe que a página se chama index.html ficava parado aqui. */
1991
+ case 'abrir': case 'open': {
1992
+ if (!rest) {
1993
+ const pastaAtual = projectDir();
1994
+ if (path.resolve(pastaAtual) === path.resolve(cfg.ROOT_DIR)) {
1995
+ console.log(c.muted(' Nenhum projeto aberto.') + c.dim(' /projetos para escolher um'));
1996
+ break;
1997
+ }
1998
+ const alvo = await extra.acharAlvoAbrir(pastaAtual);
1999
+ if (!alvo) {
2000
+ console.log(c.muted(` Não achei página nem servidor em ${shortPath(pastaAtual)}.`));
2001
+ console.log(c.dim(' Peça "roda o projeto" ao Primo, ou /open <arquivo>.'));
2002
+ break;
2003
+ }
2004
+ const r = await tools.openInBrowser(alvo.url || alvo.caminho);
2005
+ if (r && r.ok === false) { console.log(c.err(` Não consegui abrir: ${r.error}`)); break; }
2006
+ console.log(c.ok(` Aberto: ${alvo.url || shortPath(alvo.caminho)}`) + c.dim(` (${alvo.porque})`));
2007
+ break;
2008
+ }
1608
2009
  const target = /^https?:\/\//i.test(rest) ? rest : path.resolve(projectDir(), rest);
1609
2010
  await tools.openInBrowser(target);
1610
2011
  console.log(c.ok(`Aberto: ${rest}`));
@@ -1619,32 +2020,47 @@ async function handleSlash(text) {
1619
2020
  break;
1620
2021
  }
1621
2022
 
1622
- case 'mascot': {
1623
- const estilos = ['primo', 'fox', 'cat'];
1624
- const m = rest.toLowerCase();
1625
- if (!m) {
2023
+ /* A galeria: todos lado a lado, número e nome em cima. Escolhe por
2024
+ nome ou número — na hora, se há teclado; senão pelo /mascot <n>.
2025
+ Num pipe não pergunta: a "resposta" seria o próximo comando. */
2026
+ case 'mascot': case 'mascote': {
2027
+ const reac = rest.match(/^(reac(?:oes|ões)?|rea[cç][aã]o|reactions?)\s*(on|off|liga[r]?|desliga[r]?|sim|n[aã]o)?$/i);
2028
+ if (reac) {
2029
+ const quer = reac[2] ? /^(on|liga|ligar|sim)$/i.test(reac[2]) : state.config.reacoes === false;
2030
+ state.config.reacoes = quer;
2031
+ cfg.saveConfig({ reacoes: quer });
2032
+ console.log(c.ok(` Reações do mascote: ${quer ? 'ligadas' : 'desligadas'}`)
2033
+ + (quer ? ' ' + ui.reacao(state.config.mascot, 'feliz').trim() : ''));
2034
+ break;
2035
+ }
2036
+ let pedido = rest;
2037
+ if (!pedido) {
1626
2038
  console.log('');
1627
- for (const e of estilos) {
1628
- console.log(` ${e === state.config.mascot ? c.brand('● ' + e) : c.muted('○ ' + e)}`);
1629
- ui.mascot(e).forEach((l) => console.log(' ' + l));
1630
- console.log('');
2039
+ console.log(ui.galeriaMascotes(state.config.mascot || 'primo', process.stdout.columns || 80));
2040
+ if (!process.stdin.isTTY || state.semTerminal) {
2041
+ console.log(c.muted(' Escolha: /mascot <nome ou número>') + c.dim(' · /mascot reacoes off desliga o rostinho'));
2042
+ break;
1631
2043
  }
1632
- console.log(c.muted(' Escolha: /mascot <estilo>'));
1633
- break;
2044
+ pedido = String(await ask(` ${c.brand('?')} Nome ou número ${c.dim('(Enter mantém)')}: `)).trim();
2045
+ if (!pedido) { console.log(c.muted(' Nada mudou.')); break; }
1634
2046
  }
1635
- if (!estilos.includes(m)) { console.log(c.err(`Uso: /mascot ${estilos.join('|')}`)); break; }
2047
+ const m = ui.resolverMascote(pedido);
2048
+ if (!m) { console.log(c.err(` Não conheço "${pedido}".`) + c.dim(` São: ${ui.MASCOTES.join(', ')}`)); break; }
1636
2049
  state.config.mascot = m;
1637
2050
  cfg.saveConfig({ mascot: m });
2051
+ const info = ui.mascoteInfo(m);
1638
2052
  console.log('');
1639
- ui.mascot(m).forEach((l) => console.log(' ' + l));
1640
- console.log(c.ok(`\nMascote: ${m}`));
2053
+ const arte = ui.mascot(m);
2054
+ arte.forEach((l, i) => console.log(' ' + l + (i === 0 ? ' ' + c.bold(c.white(info.nome))
2055
+ : i === 1 ? ' ' + c.dim(`"${info.frase}"`) : '')));
2056
+ console.log(c.ok(`\n Mascote: ${m}`));
1641
2057
  break;
1642
2058
  }
1643
2059
 
1644
2060
  case 'config': {
1645
2061
  console.log('');
1646
2062
  console.log(` ${c.muted('nome')} ${c.white(state.config.userName || '(nenhum)')}`);
1647
- console.log(` ${c.muted('mascote')} ${c.white(state.config.mascot || 'primo')}`);
2063
+ console.log(` ${c.muted('mascote')} ${c.white(state.config.mascot || 'primo')}${c.dim(state.config.reacoes === false ? ' · sem reações' : ' · com reações')}`);
1648
2064
  console.log(` ${c.muted('esforço')} ${c.white(state.config.effort)}${c.dim(' · /effort muda')}`);
1649
2065
  console.log(` ${c.muted('modelo')} ${c.white(state.config.model)}${c.dim(' (vem do esforço)')}`);
1650
2066
  console.log(` ${c.muted('modo')} ${c.white(state.mode)}`);
@@ -1683,6 +2099,78 @@ async function handleSlash(text) {
1683
2099
  state.gravador.zerar(); console.log(c.ok('Conversa limpa.')); break;
1684
2100
  case 'cls': process.stdout.write('\x1Bc'); break;
1685
2101
 
2102
+ /* O /status olha só o servidor. O /doctor olha tudo o que costuma
2103
+ ser o motivo de "não funciona" — e cada ✗ diz como consertar. */
2104
+ case 'doctor': case 'medico': case 'médico': {
2105
+ const sp = new Spinner('conferindo').start();
2106
+ const linhas = await extra.diagnosticar({
2107
+ saude: () => api.getHealth(state.config.server, state.config.token),
2108
+ provedor: () => provedorLocal.atual(),
2109
+ conferirChave: (id, chave) => provedorLocal.conferir(id, chave),
2110
+ cliInstalado: (id) => require('../lib/local/motor-cli').instalado(id),
2111
+ acharChrome: () => require('../lib/browser').findChrome(),
2112
+ raiz: cfg.ROOT_DIR,
2113
+ arquivoConfig: cfg.CONFIG_FILE,
2114
+ });
2115
+ sp.stop();
2116
+ console.log('');
2117
+ for (const l of linhas) {
2118
+ const marca = l.ok === true ? c.ok('✓') : l.ok === false ? c.err('✗') : c.dim('·');
2119
+ console.log(` ${marca} ${ui.padRight(c.white(l.item), 18)}${ui.truncate(c.muted(l.detalhe || ''), Math.max(20, (process.stdout.columns || 80) - 23))}`);
2120
+ if (l.conserto) console.log(` ${c.dim('→ ' + l.conserto)}`);
2121
+ }
2122
+ const ruins = linhas.filter((l) => l.ok === false).length;
2123
+ console.log(ruins ? c.warn(`\n ${ruins} ${ruins === 1 ? 'coisa para arrumar' : 'coisas para arrumar'}.`) : c.ok('\n Tudo certo.'));
2124
+ break;
2125
+ }
2126
+
2127
+ case 'custo': case 'cost': case 'tokens': {
2128
+ const { linhas, total } = extra.resumoCusto(contadorDaSessao);
2129
+ if (!linhas.length) { console.log(c.muted(' Nenhum pedido ao modelo nesta sessão ainda.')); break; }
2130
+ console.log('');
2131
+ for (const l of linhas) {
2132
+ console.log(` ${c.white(ui.padRight(l.provedor, 22))}${c.muted(ui.padRight(l.pedidos + (l.pedidos === 1 ? ' pedido' : ' pedidos'), 12))}`
2133
+ + `${c.dim('enviou ')}${c.white(extra.fmtNumero(l.enviados))}${c.dim(' · recebeu ')}${c.white(extra.fmtNumero(l.recebidos))}${c.dim(' tokens' + (l.exato ? '' : ' (estimado)'))}`);
2134
+ }
2135
+ if (linhas.length > 1) {
2136
+ console.log(` ${c.white(ui.padRight('Total', 22))}${c.muted(ui.padRight(total.pedidos + ' pedidos', 12))}`
2137
+ + `${c.dim('enviou ')}${c.white(extra.fmtNumero(total.enviados))}${c.dim(' · recebeu ')}${c.white(extra.fmtNumero(total.recebidos))}`);
2138
+ }
2139
+ if (linhas.some((l) => !l.exato)) console.log(c.dim('\n Estimado = caracteres ÷ 4. Conta cada pedido inteiro, com a conversa que vai junto.'));
2140
+ break;
2141
+ }
2142
+
2143
+ case 'copiar': case 'copy': {
2144
+ const r = extra.copiar(state.lastResponse);
2145
+ if (r.ok) console.log(c.ok(' Copiado.') + c.dim(` ${state.lastResponse.length} caracteres · é só colar`));
2146
+ else console.log(c.muted(` Não copiei: ${r.error}.`));
2147
+ break;
2148
+ }
2149
+
2150
+ case 'historico': case 'histórico': {
2151
+ const achados = extra.buscarHistorico(cfg.HISTORY_FILE, rest, 20);
2152
+ if (!achados.length) {
2153
+ console.log(c.muted(rest ? ` Nada com "${rest}" nos seus pedidos.` : ' Nenhum pedido guardado ainda.'));
2154
+ console.log(c.dim(' /chats mostra as conversas inteiras da sua conta.'));
2155
+ break;
2156
+ }
2157
+ console.log('');
2158
+ const largura = Math.max(30, (process.stdout.columns || 80) - 34);
2159
+ for (const e of achados) {
2160
+ const quando = new Date(e.ts).toLocaleString('pt-BR', { dateStyle: 'short', timeStyle: 'short' });
2161
+ console.log(` ${c.dim(quando)} ${c.muted(ui.padRight(ui.truncate(e.project || e.mode || '', 14), 14))} `
2162
+ + ui.truncate(c.white(String(e.prompt).replace(/\s+/g, ' ')), largura));
2163
+ }
2164
+ console.log(c.dim(`\n ${achados.length === 20 ? 'Os 20 mais recentes' : achados.length + ' pedido(s)'}${rest ? ` com "${rest}"` : ''} · /historico <termo> filtra · /chats abre conversas`));
2165
+ break;
2166
+ }
2167
+
2168
+ case 'atalhos': case 'shortcuts': case 'teclas': {
2169
+ console.log('');
2170
+ for (const [tecla, faz] of extra.ATALHOS) console.log(` ${c.brand(ui.padRight(tecla, 20))}${c.muted(faz)}`);
2171
+ break;
2172
+ }
2173
+
1686
2174
  case 'status': {
1687
2175
  const sp = new Spinner('verificando').start();
1688
2176
  try {
@@ -1853,6 +2341,8 @@ async function handleSlash(text) {
1853
2341
  return resposta;
1854
2342
  }
1855
2343
  const plan = prepararPasta(texto);
2344
+ // Pasta pendente (projeto que ainda vai nascer) é o ~/primocode: sem foto.
2345
+ checkpoint.iniciar(texto, plan.pending ? null : plan.dir);
1856
2346
  const resultado = await runAct(texto, {
1857
2347
  instrucao: vozModo.COMO_FALAR,
1858
2348
  server: state.config.server,
@@ -1865,7 +2355,9 @@ async function handleSlash(text) {
1865
2355
  ensureWorkDir: plan.pending ? () => createPendingProject(plan) : null,
1866
2356
  autorizar: autorizarPorVoz,
1867
2357
  controle: state.controle,
2358
+ permissoes: state.config.permissoes,
1868
2359
  });
2360
+ checkpoint.encerrar();
1869
2361
  const resposta = (resultado.assistantMessage && resultado.assistantMessage.content) || '';
1870
2362
  // A conversa é a MESMA do terminal: sair do modo voz e
1871
2363
  // continuar digitando não pode começar do zero.
@@ -1975,18 +2467,7 @@ async function handleSlash(text) {
1975
2467
  }
1976
2468
  const sp = new Spinner('abrindo a janela').start();
1977
2469
  try {
1978
- janelaAberta = await appLib.abrir({
1979
- versao: VERSION,
1980
- enviar: (t) => sendPrompt(t),
1981
- barra: (t) => handleSlash(t),
1982
- interromper,
1983
- estado: () => ({
1984
- pasta: state.workDir ? shortPath(state.workDir) : null,
1985
- projeto: state.currentProject,
1986
- modo: state.mode,
1987
- esforco: state.config.effort,
1988
- }),
1989
- });
2470
+ janelaAberta = await appLib.abrir(opcoesDaJanela());
1990
2471
  sp.stop();
1991
2472
  const j = janelaAberta.janela || {};
1992
2473
  console.log(c.ok(' Janela aberta.')
@@ -2010,6 +2491,37 @@ async function handleSlash(text) {
2010
2491
  break;
2011
2492
  }
2012
2493
 
2494
+ /* O /ide virou a MESMA janela do /rc, abrindo com a prévia do site
2495
+ à direita. O editor de código saiu ("remove a IDE ou deixa
2496
+ idêntico ao Codex"): quem não programa precisa ver o site
2497
+ nascendo, não de um editor para quebrar o que o agente fez. O
2498
+ comando continua existindo para o dedo viciado não bater em nada. */
2499
+ case 'ide': {
2500
+ if (janelaAberta) {
2501
+ await appLib.abrirJanela(janelaAberta.urlPainel || janelaAberta.url);
2502
+ console.log(c.ok(' Janela com a prévia aberta.') + c.muted(' (no mesmo servidor que já estava de pé)'));
2503
+ break;
2504
+ }
2505
+ const sp = new Spinner('abrindo a janela').start();
2506
+ try {
2507
+ janelaAberta = await appLib.abrir(opcoesDaJanela({ abrirEm: 'ide' }));
2508
+ sp.stop();
2509
+ const j = janelaAberta.janela || {};
2510
+ console.log(c.ok(' Janela aberta, com a prévia do site ao lado.')
2511
+ + c.muted(j.modo === 'janela' ? ` (${j.navegador})`
2512
+ : j.modo === 'aba' ? ' (aba do navegador padrão)' : ''));
2513
+ console.log(c.dim(` ${janelaAberta.urlPainel}`));
2514
+ if (j.modo === 'nenhum') {
2515
+ console.log(c.warn(' Não consegui abrir sozinho — copie o endereço acima no navegador.'));
2516
+ }
2517
+ console.log(c.muted(` Pasta: ${shortPath(projectDir())} · o terminal continua funcionando, os dois mostram a mesma coisa.`));
2518
+ } catch (e) {
2519
+ sp.stop();
2520
+ console.log(c.err(` Não subiu: ${e.message}`));
2521
+ }
2522
+ break;
2523
+ }
2524
+
2013
2525
  /* A conta do Conecta Primo, sem navegador: e-mail e senha aqui
2014
2526
  mesmo. O login inteiro mora em lib/conta.js; aqui é a tela dele. */
2015
2527
  case 'login': {
@@ -2067,18 +2579,8 @@ async function handleSlash(text) {
2067
2579
  const n = Number(rest);
2068
2580
  const alvo = l.conversas[n - 1];
2069
2581
  if (!alvo) { console.log(c.err(` Não achei a conversa ${rest}.`)); break; }
2070
- const sp2 = new Spinner('abrindo').start();
2071
- const conv = await nuvem.lerConversa(alvo.id);
2072
- sp2.stop();
2582
+ const conv = await abrirConversa(alvo);
2073
2583
  if (!conv.ok) { console.log(c.err(` ${conv.error}`)); break; }
2074
- // A memória entra ZERADA: o resumo antigo não veio junto, e um
2075
- // resumo de outra conversa colado aqui faria o modelo responder
2076
- // sobre coisa que não foi dita nesta.
2077
- state.history = conv.mensagens.map((m) => ({
2078
- role: m.papel, content: m.texto,
2079
- }));
2080
- state.memoria = memoria.nova();
2081
- state.gravador.retomar(alvo.id, alvo.titulo, conv.mensagens.length);
2082
2584
  console.log(c.ok(` ${alvo.titulo}`)
2083
2585
  + c.muted(` · ${conv.mensagens.length} mensagens carregadas`));
2084
2586
  console.log(c.dim(' Pode continuar daqui — o que você disser volta pro app e pro celular.'));
@@ -2154,6 +2656,68 @@ async function handleSlash(text) {
2154
2656
  break;
2155
2657
  }
2156
2658
 
2659
+ /* O servidor aqui, no computador da pessoa, até a sessão acabar
2660
+ (lib/offline.js). "Enquanto não tenho acesso ao Railway, eu testo
2661
+ com o projeto rodando localmente." (24/09/2026) */
2662
+ case 'offline': case 'local': {
2663
+ if (/^(off|sair|desligar|parar)$/i.test(rest)) {
2664
+ if (!state.servidorAntes) { console.log(c.muted(' O /offline não está ligado.')); break; }
2665
+ offline.desligar();
2666
+ state.config.server = state.servidorAntes;
2667
+ state.servidorAntes = null;
2668
+ console.log(c.ok(' Voltei ao servidor de antes: ') + c.dim(state.config.server));
2669
+ break;
2670
+ }
2671
+ if (state.servidorAntes) { console.log(c.muted(' Já está no servidor local: ') + c.dim(state.config.server) + c.muted(' · /offline off volta')); break; }
2672
+ const chaves = offline.prepararEnv();
2673
+ if (chaves.criado || !chaves.temChave) {
2674
+ console.log(c.warn(` ${chaves.criado ? 'Criei' : 'Falta chave em'} ${chaves.arquivo}`));
2675
+ console.log(c.muted(' Cole nele as chaves do Render (primocode-server → Environment), uma por linha:'));
2676
+ console.log(c.dim(' GROQ_API_KEY=… OPENROUTER_API_KEY=… GEMINI_API_KEY=…'));
2677
+ console.log(c.muted(' Depois rode ') + c.brand('/offline') + c.muted(' de novo.'));
2678
+ break;
2679
+ }
2680
+ const dir = offline.acharServidor({ pedido: rest });
2681
+ const sp = new Spinner('subindo o servidor aqui').start();
2682
+ const r = await offline.ligar({ dir, env: chaves.env });
2683
+ sp.stop();
2684
+ if (!r.ok) {
2685
+ console.log(c.err(' ' + r.error));
2686
+ if (r.comoResolver) console.log(c.dim(' → ' + r.comoResolver));
2687
+ break;
2688
+ }
2689
+ state.servidorAntes = state.config.server;
2690
+ state.config.server = r.url;
2691
+ console.log(c.ok(` Servidor ${r.versao || ''} ${r.reaproveitado ? 'que já estava de pé' : 'rodando aqui'}: `) + c.dim(r.url));
2692
+ console.log(c.dim(` ${dir || ''}${dir ? ' · ' : ''}chaves: ${chaves.nomes.join(', ')}${r.reaproveitado ? '' : ' · log em ' + offline.ARQUIVO_LOG}`));
2693
+ if (provedorLocal.atual()) console.log(c.warn(' Atenção: o /api está ligado e fala direto com a IA. Para usar o servidor local, /api off.'));
2694
+ console.log(c.muted(' Vale até fechar o PrimoCode. ') + c.brand('/offline off') + c.muted(' volta antes.'));
2695
+ break;
2696
+ }
2697
+
2698
+ /* Os comandos da pessoa: lista, e explica como fazer o dela. */
2699
+ case 'comandos': {
2700
+ const novo = rest.match(/^(novo|new|criar)\s+(\S+)$/i);
2701
+ if (novo) {
2702
+ const r = comandosLib.criar(novo[2]);
2703
+ if (!r.ok) { console.log(c.err(` ${r.error}`)); break; }
2704
+ cacheComandos.em = 0;
2705
+ console.log(c.ok(` Criado: /${path.basename(r.arquivo, '.md')}`) + c.muted(' abra e escreva o pedido:'));
2706
+ console.log(' ' + c.white(r.arquivo));
2707
+ break;
2708
+ }
2709
+ const lista = comandosDaPessoa();
2710
+ console.log(c.white('\n Seus comandos'));
2711
+ for (const x of lista) {
2712
+ console.log(` ${c.brand(ui.padRight('/' + x.nome, 14))}${c.dim(ui.padRight(x.origem, 11))}${c.muted(x.descricao)}`);
2713
+ }
2714
+ console.log(c.dim('\n Um arquivo .md vira um comando: revisar.md é /revisar.'));
2715
+ console.log(c.dim(' ~/.primocode/comandos/ vale em todo projeto · <projeto>/.primocode/comandos/ só naquele.'));
2716
+ console.log(c.dim(' $ARGUMENTOS no texto recebe o que você digitar depois: /revisar o login.'));
2717
+ console.log(c.dim(' /comandos novo <nome> cria um para você editar.'));
2718
+ break;
2719
+ }
2720
+
2157
2721
  case 'claude': case 'gpt': case 'codex': case 'key': case 'keys': {
2158
2722
  console.log(c.muted(' Não existe mais provedor para configurar.'));
2159
2723
  console.log(c.muted(' A API já vem com o PrimoCode: entrou na conta, pode usar.'));
@@ -2165,7 +2729,18 @@ async function handleSlash(text) {
2165
2729
  // "/" sozinho abre o menu; um prefixo único autopreenche e executa
2166
2730
  // ("/eff" → /effort); prefixo ambíguo mostra as opções.
2167
2731
  if (!cmd) { mostrarMenuSlash(); break; }
2168
- const casam = SLASH_COMANDOS.map(([nome]) => nome.slice(1)).filter((n) => n.startsWith(cmd.toLowerCase()));
2732
+ // Chegou aqui, não é embutido: o switch já atendeu todos eles.
2733
+ // Por isso um /help.md da pessoa nunca troca o /help de verdade.
2734
+ const proprio = comandosDaPessoa().find((x) => x.nome === nome);
2735
+ if (proprio) {
2736
+ console.log(c.dim(` ⎿ /${proprio.nome} · ${proprio.origem}`));
2737
+ // O MESMO caminho de um pedido digitado: modo, pasta, plano,
2738
+ // tudo. Um atalho que pulasse o sendPrompt pularia junto o
2739
+ // portão do plano e a escolha da pasta.
2740
+ await sendPrompt(comandosLib.expandir(proprio, rest));
2741
+ break;
2742
+ }
2743
+ const casam = comandosDoMenu().map(([nome]) => nome.slice(1)).filter((n) => n.startsWith(cmd.toLowerCase()));
2169
2744
  if (casam.length === 1) {
2170
2745
  console.log(c.dim(` → /${casam[0]}`));
2171
2746
  await handleSlash('/' + casam[0] + (rest ? ' ' + rest : ''));
@@ -2174,7 +2749,7 @@ async function handleSlash(text) {
2174
2749
  if (casam.length > 1) {
2175
2750
  console.log(c.muted(` /${cmd}… pode ser:`));
2176
2751
  for (const n of casam) {
2177
- const [, desc] = SLASH_COMANDOS.find(([nome]) => nome === '/' + n);
2752
+ const [, desc] = comandosDoMenu().find(([nome]) => nome === '/' + n);
2178
2753
  console.log(` ${c.brand(ui.padRight('/' + n, 12))}${c.dim(desc)}`);
2179
2754
  }
2180
2755
  break;
@@ -2247,7 +2822,11 @@ async function main() {
2247
2822
  // mesmo que uma refatoração.
2248
2823
  state.mode = ['primocode', 'chat', 'auto'].includes(state.config.mode) ? state.config.mode : 'auto';
2249
2824
 
2250
- const args = process.argv.slice(2);
2825
+ // `-c` / `--continue` pode vir em qualquer posição e sai da lista: o
2826
+ // resto (one-shot, -f) segue sendo lido do jeito de sempre.
2827
+ const todos = process.argv.slice(2);
2828
+ const continuar = todos.some((a) => a === '-c' || a === '--continue');
2829
+ const args = todos.filter((a) => a !== '-c' && a !== '--continue');
2251
2830
 
2252
2831
  // Flags padrão de CLI — funcionam em qualquer máquina, úteis em scripts/CI.
2253
2832
  if (args[0] === '--version' || args[0] === '-v' || args[0] === '-V') {
@@ -2285,18 +2864,7 @@ async function main() {
2285
2864
  process.exit(r.ok ? 0 : 1);
2286
2865
  }
2287
2866
 
2288
- const janela = await appLib.abrir({
2289
- versao: VERSION,
2290
- enviar: (t) => sendPrompt(t),
2291
- barra: (t) => handleSlash(t),
2292
- interromper,
2293
- estado: () => ({
2294
- pasta: state.workDir ? shortPath(state.workDir) : null,
2295
- projeto: state.currentProject,
2296
- modo: state.mode,
2297
- esforco: state.config.effort,
2298
- }),
2299
- });
2867
+ const janela = await appLib.abrir(opcoesDaJanela());
2300
2868
  janelaAberta = janela;
2301
2869
  console.log(c.brand(` PrimoCode`) + c.muted(` ${janela.url.split('?')[0]}`));
2302
2870
  // Sem readline não há nada segurando o laço de eventos além do
@@ -2307,33 +2875,31 @@ async function main() {
2307
2875
  return;
2308
2876
  }
2309
2877
 
2878
+ /* `primo ide`: o terminal de sempre, com a janela (e a prévia) já aberta.
2879
+ Só a palavra SOZINHA: `primo ide para gatos` continua sendo um pedido
2880
+ ao agente (modo um-tiro), como sempre foi. */
2881
+ const abrirIde = args.length === 1 && args[0].toLowerCase() === 'ide';
2882
+
2310
2883
  // Modo one-shot: primocode "crie um jogo de carro"
2311
- if (args.length && !args[0].startsWith('-')) {
2884
+ if (args.length && !args[0].startsWith('-') && !abrirIde) {
2312
2885
  const fileFlag = args.indexOf('-f');
2313
2886
  const words = fileFlag >= 0 ? args.slice(0, fileFlag) : args;
2314
2887
  const promptText = words.join(' ');
2315
- let fullPrompt = promptText;
2316
2888
 
2317
2889
  if (fileFlag >= 0 && args[fileFlag + 1]) {
2318
2890
  try {
2319
2891
  const fc = fs.readFileSync(path.resolve(args[fileFlag + 1]), 'utf-8');
2320
- fullPrompt = `Arquivo: ${args[fileFlag + 1]}\n\`\`\`\n${fc}\n\`\`\`\n\n${promptText}`;
2892
+ state.attachedFiles.push({ path: args[fileFlag + 1], content: fc });
2321
2893
  } catch { console.log(c.err(`Não consegui ler: ${args[fileFlag + 1]}`)); process.exit(1); }
2322
2894
  }
2323
2895
 
2324
- // Mesma função do modo interativo: detecta a pasta, adota projeto que já
2325
- // existe, e só então decide se há projeto novo pendente.
2326
- const plan = prepararPasta(promptText);
2327
- await runAct(fullPrompt, {
2328
- server: state.config.server,
2329
- model: state.config.model,
2330
- effort: state.config.effort,
2331
- token: state.config.token,
2332
- history: [],
2333
- projectDir: plan.dir,
2334
- projectName: plan.name || state.currentProject,
2335
- ensureWorkDir: plan.pending ? () => createPendingProject(plan) : null,
2336
- });
2896
+ // Com -c, o pedido continua a última conversa em vez de chegar sozinho.
2897
+ if (continuar) await continuarUltima();
2898
+ /* O MESMO caminho do pedido digitado (sendPrompt). Este modo tinha um
2899
+ runAct próprio, e por isso ficava sem tudo o que vive lá: o plano em
2900
+ passos, a retomada automática, o @arquivo, o checkpoint do /desfazer. */
2901
+ state.semTerminal = true;
2902
+ await sendPrompt(promptText);
2337
2903
  process.exit(0);
2338
2904
  }
2339
2905
 
@@ -2345,7 +2911,7 @@ async function main() {
2345
2911
  }));
2346
2912
  rl = entrada.criar({
2347
2913
  status: barraDeStatus,
2348
- comandos: () => SLASH_COMANDOS,
2914
+ comandos: comandosDoMenu,
2349
2915
  vazio: 'peça alguma coisa, ou / para os comandos',
2350
2916
  });
2351
2917
 
@@ -2427,7 +2993,13 @@ async function main() {
2427
2993
  try {
2428
2994
  if (ehBash(text)) await rodarBash(text);
2429
2995
  else if (isSlash(text)) await handleSlash(text);
2430
- else await sendPrompt(text);
2996
+ else {
2997
+ // Com a janela aberta, o pedido digitado AQUI também vira
2998
+ // balão lá — senão a janela mostraria a resposta de uma
2999
+ // pergunta que ninguém fez.
3000
+ if (janelaAberta && janelaAberta.pedidoDoTerminal) janelaAberta.pedidoDoTerminal(text);
3001
+ await sendPrompt(text);
3002
+ }
2431
3003
  } catch (e) {
2432
3004
  fala.problema(e);
2433
3005
  }
@@ -2484,15 +3056,22 @@ async function main() {
2484
3056
  // o primocode-server, que tem a chave do provedor.
2485
3057
  state.liberado = !r || r.liberado !== false;
2486
3058
  state.motivoBloqueio = r && r.motivo;
2487
- }).catch(() => { state.liberado = true; }).then(() => {
2488
- // O que sobrou de adiantado não era resposta de nada: vira comando,
2489
- // na ordem em que foi digitado. Nada se perde, só esperou a vez.
2490
- segurando = false;
2491
- while (adiantadas.length) queue.push(adiantadas.shift());
2492
- if (queue.length) drain();
2493
- else if (!closed) rl.prompt();
2494
- conferirServidor();
2495
- });
3059
+ }).catch(() => { state.liberado = true; })
3060
+ // Depois da abertura, porque é nela que a pessoa entra na conta — e
3061
+ // sem conta não há conversa para achar.
3062
+ .then(() => (continuar ? continuarUltima().catch(() => false) : null))
3063
+ .then(() => {
3064
+ // O que sobrou de adiantado não era resposta de nada: vira comando,
3065
+ // na ordem em que foi digitado. Nada se perde, só esperou a vez.
3066
+ segurando = false;
3067
+ // Depois da abertura (e do login dela): a janela sem conta abriria
3068
+ // mandando pedidos que o servidor recusa.
3069
+ if (abrirIde) queue.push('/ide');
3070
+ while (adiantadas.length) queue.push(adiantadas.shift());
3071
+ if (queue.length) drain();
3072
+ else if (!closed) rl.prompt();
3073
+ conferirServidor();
3074
+ });
2496
3075
  }
2497
3076
 
2498
3077
  main().catch((e) => { console.error(c.err(e.message || e)); process.exit(1); });