primocode 11.3.2 → 11.4.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Vanilagy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # PrimoCode v11.3.2
1
+ # PrimoCode v11.4.0
2
2
 
3
3
  Agente de engenharia brasileiro para o terminal, no estilo Claude Code.
4
4
  Cria arquivos de verdade, roda comandos, **controla o navegador e o desktop** —
@@ -9,7 +9,7 @@ OpenRouter, e cada apelido (`top`/`main`/`fast`) é uma **corrente**: se um mode
9
9
  está no limite de pedidos, o próximo atende. Você não vê o erro, e não paga nada.
10
10
 
11
11
  ```
12
- ▐▛███▜▌ Primo Code v11.3.2
12
+ ▐▛███▜▌ Primo Code v11.4.0
13
13
  ▝▜█████▛▘ Bem-vindo de volta, Joel
14
14
  ▘▘ ▝▝ ~/primocode · /dir <pasta> muda
15
15
  ```
package/lib/cli/sessao.js CHANGED
@@ -143,6 +143,27 @@ function linhasDosGrupos() {
143
143
  return saida;
144
144
  }
145
145
 
146
+ /* O `--help` de fora: só o essencial. Os grupos completos moram no /help (e no "/") dentro da sessão. */
147
+ const HELP_ENXUTO = ['new', 'projects', 'undo', 'check', 'open', 'studio', 'help', 'exit'];
148
+ /* PRIMO_DEBUG=1: o que os catch silenciosos engolem vai pro stderr. Sem a variável, nada muda. */
149
+ function depurar(onde, e) {
150
+ if (process.env.PRIMO_DEBUG !== '1') return;
151
+ try { process.stderr.write(`[PRIMO_DEBUG] ${onde}: ${e && e.stack ? e.stack : e}\n`); } catch { /* stderr fechado */ }
152
+ }
153
+
154
+ function montarHelpEnxuto() {
155
+ const desc = new Map(comandosDoMenu().map(([n, d]) => [n.slice(1), d]));
156
+ const linhas = HELP_ENXUTO.map((n) => { const d = desc.get(n); return d ? ` ${c.brand(ui.padRight('/' + n, 16))}${c.muted(d)}` : ` ${c.brand('/' + n)}`; });
157
+ return `
158
+ ${c.bold('Comandos')}
159
+
160
+ ${linhas.join('\n')}
161
+ ${c.brand('-p')} "pedido" modo script: sem tela, aceita stdin e --output-format json
162
+
163
+ ${c.dim('digite / pra ver todos')}
164
+ `;
165
+ }
166
+
146
167
  function montarHelp() {
147
168
  return `
148
169
  ${c.bold('Comandos')} ${c.dim('· os nomes em português continuam valendo (/desfazer = /undo)')}
@@ -218,10 +239,6 @@ const SLASH_COMANDOS = [
218
239
  ['/chats', 'suas conversas salvas, as mesmas do app e do celular'],
219
240
  ['/logout', 'desconecta este computador da conta'],
220
241
  ['!comando', 'roda direto no shell, sem passar pelo agente (o campo fica âmbar)'],
221
- ['/voice', 'liga o Primo: clique nele e fale; ele faz e responde em voz alta, com a tela dele (robô, agenda, clima, música)'],
222
- ['/voice painel', 'só o painel do Primo (ESC esconde)'],
223
- ['/voice nome <Nome>', 'dá outro nome ao assistente (só o nome; o modo voz abre no clique)'],
224
- ['/voice parar', 'desliga o modo voz e fecha o microfone'],
225
242
  ['/skills', 'o que ele sabe fazer bem · /skills store: a loja · /skills add <arquivo>: instala'],
226
243
  ['/commands', 'seus comandos: um .md vira /nome · /commands novo <nome>'],
227
244
  ['/styles', 'os estilos de motion (kits): a lista e a galeria · /styles <estilo>: o preview dele'],
@@ -234,6 +251,7 @@ const SLASH_COMANDOS = [
234
251
  ['/keys', 'as teclas que ajudam'],
235
252
  ['/connectors', 'quem está atendendo os seus pedidos'],
236
253
  ['/name', 'como ele te chama · /name voz <Nome>: o nome do assistente de voz'],
254
+ ['/studio', 'escolhe o que criar: slides, vídeo, motion, anúncio, abertura'],
237
255
  ['/brand', 'kit de marca: cores, fontes e logo'],
238
256
  ['/mascot', 'galeria dos mascotes · escolha por nome ou número'],
239
257
  ['/config', 'suas preferências'],
@@ -243,7 +261,7 @@ const SLASH_COMANDOS = [
243
261
  ['/version', 'versão'],
244
262
  ['/exit', 'sair'],
245
263
  ];
246
- // A CLI nova (lib/cli-nova.js): /estudio /agentes /piloto /avisos /buscar entram no menu e no autopreenchimento.
264
+ // A CLI nova (lib/cli-nova.js): /studio /agentes /piloto /avisos /buscar entram no menu e no autopreenchimento.
247
265
  SLASH_COMANDOS.splice(Math.max(0, SLASH_COMANDOS.findIndex(([n]) => n === '/marca')), 0, ...require('../cli-nova.js').SLASH_NOVOS);
248
266
 
249
267
  /* /api: a chave da própria pessoa — lib/cli/comando-api.js. */
@@ -540,7 +558,7 @@ function barraLateral() {
540
558
  * O desenho é do OpenCode: uma lista com busca no meio da tela. O conteúdo é
541
559
  * o que já existe aqui: os comandos de barra, os modos, os agentes da pessoa,
542
560
  * as conversas salvas e os temas. Escolher um item roda o comando de sempre. */
543
- const ATALHOS_DO_LIDER = { '/clear': 'ctrl+x n', '/projects': 'ctrl+x p', '/estudio': 'ctrl+x s' };
561
+ const ATALHOS_DO_LIDER = { '/clear': 'ctrl+x n', '/projects': 'ctrl+x p', '/studio': 'ctrl+x s' };
544
562
 
545
563
  function paletaDeComandos() {
546
564
  const itens = [
@@ -552,7 +570,7 @@ function paletaDeComandos() {
552
570
  { secao: 'Sugeridos', nome: 'Mostrar ou esconder a barra da direita', tecla: 'ctrl+x b', acao: (r) => { r.lateralOculta = !r.lateralOculta; r._quadroAnterior = null; r._pintar(); } },
553
571
  ];
554
572
  for (const [nome, desc] of comandosDoMenu()) {
555
- if (/^(\/uninstall|!comando|\/voice )/.test(nome) || nome.includes(' ') || nome === '/clear') continue;
573
+ if (/^(\/uninstall|!comando)/.test(nome) || nome.includes(' ') || nome === '/clear') continue;
556
574
  itens.push({ secao: 'Comandos', nome, desc, cmd: nome, tecla: ATALHOS_DO_LIDER[nome] || '', preencher: /\s<|<.*>/.test(desc) && /^\/(dir|worktree|rule|lock|attach|read|run|open|history|howto)$/.test(nome) });
557
575
  }
558
576
  return itens;
@@ -1747,7 +1765,8 @@ async function baixarCriacaoDireto(texto) {
1747
1765
  console.log(c.muted(' ') + c.white(ultimo.arquivo));
1748
1766
  console.log(c.dim(` ${(ultimo.bytes / 1048576).toFixed(1)} MB · o arquivo já estava pronto, é só copiar daí.`));
1749
1767
  return true;
1750
- } catch {
1768
+ } catch (e) {
1769
+ depurar('projeto renderizado', e);
1751
1770
  return false; // sem projeto renderizado: segue o caminho normal
1752
1771
  }
1753
1772
  }
@@ -1790,7 +1809,8 @@ async function dividirTarefa(pedido) {
1790
1809
  const passos = (r && Array.isArray(r.passos) && r.passos.length) ? r.passos : inteiro;
1791
1810
  if (passos.length > 1) console.log(c.dim(` ⎿ ${passos[0].titulo}`));
1792
1811
  return passos;
1793
- } catch {
1812
+ } catch (e) {
1813
+ depurar('preparar o pedido', e);
1794
1814
  sp.stop(); // preparar o pedido é melhoria, não requisito
1795
1815
  return inteiro;
1796
1816
  }
@@ -1832,7 +1852,8 @@ async function comprimirConversa(forcado = false, { silencioso = false } = {}) {
1832
1852
  gravarMemoriaDaPasta(dir);
1833
1853
  const e = memoria.economia(state.memoria, state.history);
1834
1854
  if (!silencioso && e.poupado > 0) console.log(c.dim(` ⎿ conversa resumida · ${e.cheio} → ${e.enxuto} tokens por pedido`));
1835
- } catch {
1855
+ } catch (e) {
1856
+ depurar('resumo da conversa', e);
1836
1857
  sp.stop(); // resumo é economia, não requisito: falhou, segue a vida
1837
1858
  }
1838
1859
  }
@@ -2269,8 +2290,8 @@ async function handleSlash(text) {
2269
2290
  listava os nomes, em ordem alfabética, e quem procurava "o de ontem"
2270
2291
  tinha de abrir pasta por pasta. Agora vem do mais recente, com
2271
2292
  quando mexeu e o tamanho, e um número para entrar. */
2272
- case 'estudio': case 'agentes': case 'piloto': case 'avisos': case 'buscar': {
2273
- const r = require('../cli-nova.js').COMANDOS[nome](rest, { c, dir: process.env.PRIMOCODE_AGENTES });
2293
+ case 'studio': case 'estudio': case 'agentes': case 'piloto': case 'avisos': case 'buscar': {
2294
+ const r = require('../cli-nova.js').COMANDOS[nome === 'studio' ? 'estudio' : nome](rest, { c, dir: process.env.PRIMOCODE_AGENTES });
2274
2295
  for (const l of r.linhas) console.log(l);
2275
2296
  if (r.pedido) await sendPrompt(`${r.instrucao}\n\nPEDIDO: ${r.pedido}`);
2276
2297
  break;
@@ -3063,7 +3084,7 @@ async function handleSlash(text) {
3063
3084
  const nomeVoz = rest.match(/^(?:voz|voice|assistente)\s+(.{2,20})$/i);
3064
3085
  if (nomeVoz) {
3065
3086
  const salvo = vozModo.definirNome(nomeVoz[1]);
3066
- console.log(salvo ? c.ok(` Fechado: o assistente agora se chama ${salvo}. Vale na próxima vez que ligar o /voice.`) : c.err(' Nome inválido (1 ou 2 palavras, até 20 letras).'));
3087
+ console.log(salvo ? c.ok(` Fechado: o assistente agora se chama ${salvo}. Vale na próxima vez que o modo voz abrir.`) : c.err(' Nome inválido (1 ou 2 palavras, até 20 letras).'));
3067
3088
  break;
3068
3089
  }
3069
3090
  if (!rest) { console.log(c.muted(` Nome atual: ${state.config.userName || '(nenhum)'}`)); break; }
@@ -3344,86 +3365,6 @@ async function handleSlash(text) {
3344
3365
  }
3345
3366
 
3346
3367
 
3347
- /* ── /voice ──────────────────────────────────────────────────────
3348
- * O ouvido e a boca são Python (voz/primo_voz); o cérebro é este
3349
- * terminal. Ver entradaDeVoz no main() e lib/plugin/voz.js.
3350
- */
3351
- case 'voice': {
3352
- /* ── O MODO VOZ É UMA FORMA DE DIGITAR ─────────────────────────
3353
- Você fala; o que foi dito entra aqui como se tivesse sido
3354
- digitado — a mesma fila, o mesmo recado no meio da tarefa, o
3355
- mesmo roteador de conversa — e a resposta final sai em voz alta.
3356
- O terminal continua seu: dá para falar e digitar ao mesmo tempo.
3357
-
3358
- Desligar é o primeiro caso de propósito: quem quer fechar o
3359
- microfone quer AGORA, sem esbarrar em checagem de dependência. */
3360
- const argVoz = (rest || '').trim();
3361
- // Só o painel (HUD em tela cheia no Mac; quadro no terminal fora dele).
3362
- if (/^painel$/i.test(argVoz)) {
3363
- const py = vozModo.python();
3364
- if (!py) { console.log(c.err(' O painel precisa do Python 3.')); break; }
3365
- if (vozPainel) { try { vozPainel.processo.kill(); } catch {} vozPainel = null; }
3366
- vozPainel = vozModo.abrirPainel({ python: py });
3367
- console.log(c.muted(vozPainel.tela ? ' Tela do Primo aberta — ESC esconde.' : ' Sem o painel gráfico aqui: rode `primocode voz painel` num terminal separado.'));
3368
- break;
3369
- }
3370
- // /voice nome <Nome>: a pessoa dá o nome ao assistente (o nome não ativa nada: o modo voz só abre no clique).
3371
- const mNome = argVoz.match(/^nome\s+(.{2,20})$/i);
3372
- if (mNome) {
3373
- const salvo = vozModo.definirNome(mNome[1]);
3374
- console.log(salvo ? c.ok(` Fechado: o assistente agora se chama ${salvo}. Vale na próxima vez que ligar o /voice.`) : c.err(' Nome inválido (1 ou 2 palavras, até 20 letras).'));
3375
- break;
3376
- }
3377
- if (/^(parar|stop|off|sair)$/i.test(argVoz)) {
3378
- if (vozPainel) { try { vozPainel.processo.kill(); } catch {} vozPainel = null; }
3379
- if (!vozEmEspera) { console.log(c.muted(vozPainel === null ? ' Modo voz e painel fechados.' : ' O modo voz não está ligado.')); break; }
3380
- try { vozEmEspera.processo.kill(); } catch {}
3381
- vozEmEspera = null;
3382
- console.log(c.muted(' Modo voz desligado. O microfone fechou.'));
3383
- break;
3384
- }
3385
- if (vozEmEspera) {
3386
- console.log(c.muted(' O modo voz já está ouvindo — é só falar.') + c.dim(' /voice parar desliga.'));
3387
- break;
3388
- }
3389
-
3390
- // "Instale tudo que precisa, rode tudo que precisa." Da segunda vez
3391
- // em diante tudo já está lá, e isto leva um segundo.
3392
- const sp = new Spinner('preparando o modo voz').start();
3393
- const prep = await vozModo.preparar((msg) => { sp.text = msg; });
3394
- sp.stop();
3395
- if (!prep.ok) {
3396
- console.log(c.err(` ${prep.error}`));
3397
- if (prep.comoResolver) console.log(c.muted(` ${prep.comoResolver}`));
3398
- for (const a of prep.avisos) console.log(c.dim(` ${a}`));
3399
- break;
3400
- }
3401
- for (const a of prep.avisos) console.log(c.dim(` ${a}`));
3402
- if (process.platform === 'darwin') { try { vozModo.instalarApp({ python: prep.python }); } catch { /* o app é bônus */ } }
3403
-
3404
- const sessao = vozModo.abrir({
3405
- python: prep.python,
3406
- servidor: state.config.server,
3407
- token: state.config.token,
3408
- voz: state.config.voz || 'antonio',
3409
- aoPronto: () => {
3410
- console.log(' ' + c.bold(c.brand('🎙 Modo voz ligado')) + c.muted(' — é só falar. A resposta sai em voz alta.'));
3411
- console.log(' ' + c.dim('Falar por cima corta a voz · digitar continua valendo · /voice parar desliga'));
3412
- },
3413
- aoParar: () => { try { require('../session/runner/act.js').interromper(); } catch { /* nada rodando */ } },
3414
- aoOuvir: (texto) => { if (entradaDeVoz) entradaDeVoz(texto); },
3415
- mostrar: (texto) => console.log(' ' + c.dim(texto)),
3416
- });
3417
- vozEmEspera = sessao;
3418
- require('../plugin/voz-ponte').escutar((nome, args) => sessao.ferramenta(nome, args));
3419
- sessao.pronto.then((r) => {
3420
- if (vozEmEspera === sessao) vozEmEspera = null;
3421
- if (r && !r.ok) console.log(c.err(` o modo voz fechou: ${r.error}`));
3422
- });
3423
- break;
3424
- }
3425
-
3426
-
3427
3368
  /* ── /connectors ─────────────────────────────────────────────────
3428
3369
  * Só mostra. Não há o que configurar — os conectores vivem no
3429
3370
  * servidor, com as chaves dele, e é assim de propósito: a promessa do
@@ -3923,12 +3864,12 @@ async function main() {
3923
3864
  }
3924
3865
  if (sub === 'instalar-app') {
3925
3866
  const py = vz.python();
3926
- if (!py) { console.log('O app precisa do Python 3 (rode /voice uma vez para preparar o ambiente).'); process.exit(1); }
3867
+ if (!py) { console.log('O app precisa do Python 3 (instale o Python 3).'); process.exit(1); }
3927
3868
  const r = vz.instalarApp({ python: py, dock: resto.includes('--dock') });
3928
3869
  console.log(r.ok ? `Primo.app instalado em ${r.app}. Abra pelo Launchpad, Spotlight ou Finder.` : `Não deu: ${r.erro || 'veja o log'}`);
3929
3870
  process.exit(r.ok ? 0 : 1);
3930
3871
  }
3931
- console.log('Uso: primocode voz nome <Nome> | primocode voz painel | primocode voz instalar-app [--dock] (o resto é /voice dentro da sessão)');
3872
+ console.log('Uso: primocode voz nome <Nome> | primocode voz painel | primocode voz instalar-app [--dock]');
3932
3873
  process.exit(0);
3933
3874
  }
3934
3875
  if (['doutor', 'doctor', 'preparar'].includes(process.argv[2])) {
@@ -4003,8 +3944,8 @@ async function main() {
4003
3944
  process.exit(0);
4004
3945
  }
4005
3946
  if (args[0] === '--help' || args[0] === '-h') {
4006
- console.log(welcomeBox(VERSION, null, { mascot: state.config.mascot, userName: state.config.userName, modo: state.mode }));
4007
- console.log(montarHelp());
3947
+ if (process.stdout.isTTY && !(process.env.NO_COLOR !== undefined && process.env.NO_COLOR !== '')) console.log(welcomeBox(VERSION, null, { mascot: state.config.mascot, userName: state.config.userName, modo: state.mode }));
3948
+ console.log(montarHelpEnxuto());
4008
3949
  process.exit(0);
4009
3950
  }
4010
3951
 
@@ -4150,7 +4091,7 @@ async function main() {
4150
4091
  if (agora - ultimoEsc < 2000) {
4151
4092
  ultimoEsc = 0;
4152
4093
  interromper();
4153
- if (chatEmVoo) { try { chatEmVoo.abort(); } catch {} }
4094
+ if (chatEmVoo) { try { chatEmVoo.abort(); } catch (e) { depurar('abortar o chat', e); } }
4154
4095
  console.log(c.warn(' ⏹ parando o agente…'));
4155
4096
  } else {
4156
4097
  ultimoEsc = agora;
package/lib/cli-nova.js CHANGED
@@ -98,13 +98,13 @@ function estudio(rest, { c } = {}) {
98
98
  if (!t0) {
99
99
  linhas.push(cor(c, 'white')('O que vamos criar?'));
100
100
  for (const [n, d] of TIPOS_ESTUDIO) linhas.push(` ${cor(c, 'brand')(n.padEnd(10))}${cor(c, 'muted')(d)}`);
101
- linhas.push(cor(c, 'dim')(' /estudio <tipo> [o que criar]'));
101
+ linhas.push(cor(c, 'dim')(' /studio <tipo> [o que criar]'));
102
102
  return { linhas };
103
103
  }
104
104
  if (!TIPOS_ESTUDIO.some(([n]) => n === tipo) && !QUADRADOS[tipo]) return { linhas: [cor(c, 'err')(`Tipo "${t0}" não existe. Use: ${TIPOS_ESTUDIO.map(([n]) => n).join(', ')}.`)], erro: true };
105
105
  if (tipo === 'carrossel') return { linhas: [cor(c, 'warn')('Carrossel ainda não tem receita no Estúdio. Descreva como slides ou peça direto na conversa.')], erro: true };
106
106
  const briefing = r.join(' ');
107
- if (!briefing) return { linhas: [cor(c, 'white')(`O que vamos criar em ${tipo}?`), cor(c, 'dim')(` /estudio ${tipo} <descrição do que criar>`)], tipo };
107
+ if (!briefing) return { linhas: [cor(c, 'white')(`O que vamos criar em ${tipo}?`), cor(c, 'dim')(` /studio ${tipo} <descrição do que criar>`)], tipo };
108
108
  return { linhas: [], tipo, pedido: briefing, instrucao: instrucaoDoTipo(tipo) };
109
109
  }
110
110
 
@@ -205,7 +205,6 @@ function buscar(rest, { c, dir } = {}) {
205
205
  }
206
206
 
207
207
  const SLASH_NOVOS = [
208
- ['/estudio', 'escolhe o que criar: slides, vídeo, motion, anúncio, abertura'],
209
208
  ['/agentes', 'seus agentes: lista · ligar · desligar · criar · apagar'],
210
209
  ['/piloto', 'piloto automático: on | off [grupo]'],
211
210
  ['/avisos', 'fila de aprovação e avisos · a <n> aprova · r <n> recusa'],
package/lib/nomes.js CHANGED
@@ -91,7 +91,7 @@ const COMANDOS = {
91
91
  taste: { grupo: 'video', antigos: ['gosto', 'referencias'] },
92
92
  brand: { grupo: 'video', antigos: ['marca'] },
93
93
  mascot: { grupo: 'video', antigos: ['mascote'] },
94
- voice: { grupo: 'video', antigos: ['voz'] },
94
+ studio: { grupo: 'video', antigos: ['estudio', 'estúdio'] },
95
95
  styles: { grupo: 'video', antigos: ['estilos', 'kits'] },
96
96
  // 9. Conta e sistema
97
97
  login: { grupo: 'conta', antigos: ['entrar', 'conectar'] },
@@ -167,7 +167,7 @@ const QUANDO = {
167
167
  taste: 'quer ver, refazer ou ensinar o gosto de motion (`/taste referencias` com vídeos que curtiu)',
168
168
  brand: 'quer cores, fontes e logo da marca em todo vídeo',
169
169
  mascot: 'quer escolher ou ver os mascotes',
170
- voice: 'quer falar com o Primo por voz',
170
+ studio: 'quer criar um vídeo, slides, motion, anúncio ou abertura (o Estúdio)',
171
171
  styles: 'quer ver os estilos de motion prontos (galeria com capa e preview; `/styles <estilo>`: um)',
172
172
  login: 'quer entrar na conta',
173
173
  logout: 'quer sair da conta neste computador',
@@ -30,7 +30,7 @@ const trava = require('../trava');
30
30
 
31
31
  const MODOS = ['auto', 'perguntar', 'livre', 'plano'];
32
32
  const COMANDOS = new Set(['run_shell', 'run_in_new_terminal', 'run_background']);
33
- const ESCRITAS = new Set(['write_file', 'edit_file', 'multi_edit']);
33
+ const ESCRITAS = new Set(['write_file', 'edit_file', 'multi_edit', 'notebook_edit']);
34
34
 
35
35
  function normalizarModo(m) {
36
36
  const s = String(m || '').trim().toLowerCase();
@@ -103,7 +103,7 @@ function acharNavegador(env = process.env, existe = require('fs').existsSync) {
103
103
  function htmlParaPdf(htmlPath, pdfPath, { navegador = acharNavegador() } = {}) {
104
104
  if (!navegador) return { ok: false, erro: 'Chrome/Chromium não encontrado (defina PRIMO_CHROME)' };
105
105
  const { spawnSync } = require('child_process');
106
- const r = spawnSync(navegador, ['--headless=new', '--use-mock-keychain', '--password-store=basic', '--disable-gpu', '--no-pdf-header-footer',
106
+ const r = spawnSync(navegador, ['--headless=new', '--use-mock-keychain', '--password-store=basic', '--use-mock-keychain', '--password-store=basic', '--use-mock-keychain', '--password-store=basic', '--use-mock-keychain', '--password-store=basic', '--use-mock-keychain', '--password-store=basic', '--disable-gpu', '--no-pdf-header-footer',
107
107
  `--print-to-pdf=${pdfPath}`, 'file://' + htmlPath], { encoding: 'utf8', timeout: 90000 });
108
108
  return require('fs').existsSync(pdfPath) ? { ok: true } : { ok: false, erro: (r.stderr || '').slice(-300) || 'PDF não saiu' };
109
109
  }
@@ -180,7 +180,7 @@ function htmlParaPngs(htmlPath, baseSemExt, n, w, h, { escala = 1, navegador = a
180
180
  const tmp = `${baseSemExt}.tmp-${i}.html`;
181
181
  const alvo = `${baseSemExt}-${String(i).padStart(2, '0')}.png`;
182
182
  fs.writeFileSync(tmp, html.replace('</head>', `<style>section:not(:nth-of-type(${i})){display:none!important}html,body{height:${h}px;overflow:hidden}</style></head>`));
183
- spawnSync(navegador, ['--headless=new', '--use-mock-keychain', '--password-store=basic', '--disable-gpu', '--hide-scrollbars', `--force-device-scale-factor=${escala}`,
183
+ spawnSync(navegador, ['--headless=new', '--use-mock-keychain', '--password-store=basic', '--use-mock-keychain', '--password-store=basic', '--use-mock-keychain', '--password-store=basic', '--use-mock-keychain', '--password-store=basic', '--use-mock-keychain', '--password-store=basic', '--disable-gpu', '--hide-scrollbars', `--force-device-scale-factor=${escala}`,
184
184
  `--window-size=${w},${h}`, `--screenshot=${alvo}`, 'file://' + tmp], { encoding: 'utf8', timeout: 90000 });
185
185
  try { fs.unlinkSync(tmp); } catch {}
186
186
  if (fs.existsSync(alvo)) arquivos.push(alvo); else erro = `PNG ${i} não saiu`;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "origem": "primocode-server 8.20.0 46ada83",
3
- "SYSTEM_NUCLEO": "Você é o PrimoCode, agente de engenharia da ConectaPrimoAI.\n\nCONVERSA OU TRABALHO: cumprimento, agradecimento, pergunta sobre você ou papo\n(\"oi\", \"o que você faz?\") → responda em texto, 1 ou 2 frases, sem tool nenhuma,\ne não invente tarefa que ninguém pediu. Pedido de criar, alterar, ler, rodar ou\nconsertar → EXECUTE, até o fim, sem conversar no meio.\n\nREGRAS ABSOLUTAS (executando):\n1. NUNCA escreva código na resposta. Código vai SEMPRE dentro de uma tool.\n2. NUNCA pergunte ao usuário. Decida e execute.\n3. NUNCA invente ferramenta, serviço, porta ou API: as suas são EXATAMENTE as\n desta lista. E nunca diga que algo \"não está rodando\" para se esquivar — o\n que precisa subir, sobe sozinho quando você chama a tool. Prometer o que\n você não tem, ou culpar um serviço, é o pior erro possível.\n4. Caminhos RELATIVOS (\"index.html\", \"src/game.js\"). Você já está na pasta do\n projeto: nunca caminho absoluto, nunca \"../\".\n5. Termine SEMPRE com finish(\"frase curta do que foi feito\").\n6. Resposta em texto: no máximo 1 frase, sem markdown.\n7. NUNCA repita uma chamada que já fez. Reformular a mesma busca traz as mesmas\n fontes; spec e manual são FIXOS — leu uma vez, está lido. Pesquisou e leu a\n spec? O passo seguinte é CRIAR com o que já tem.\n\nESCOLHENDO A TOOL: arquivo novo → write_file; mudar parte → edit_file (nunca\nreescreva o arquivo inteiro para trocar poucas linhas); várias mudanças no mesmo\narquivo → multi_edit, numa chamada; não sabe onde está → grep (conteúdo) ou glob\n(nome), nunca list_dir (o mapa já está aqui); servidor de pé → run_background.\n\nANTES DE EDITAR, LEIA SÓ O ARQUIVO QUE VAI MUDAR (edit_file exige o texto\nliteral), inteiro e uma vez. Nada de ler outros \"para entender\".\n\nQUANDO FALHA: leia o erro e conserte. Se edit_file disser \"trecho não\nencontrado\", ele devolve \"trechosParecidos\" com as linhas reais — copie de lá.\nNUNCA chame finish dizendo que fez algo que falhou: \"não consegui alterar X\nporque Y\" é melhor que um \"pronto!\" falso.\n\nPLANO: tarefa de 3+ passos começa com write_todos e vai atualizando o status (só\nUMA \"in_progress\" por vez). Tarefa curta não precisa.\n\nCOMO VOCÊ FALA ENQUANTO TRABALHA: antes de uma tool que muda o rumo (conserto,\nconversão, desvio), UMA frase em primeira pessoa com o quê e o porquê (\"o vídeo\nveio em .avi, vou converter para mp4\"); resolvido, \"feito, continuando\". Passo\nóbvio não precisa. NUNCA cite limite, cota, token, modelo, provedor, tentativa\nou erro interno: quem lê vê o trabalho, não o encanamento. Contornou uma\nfalha? Conte o contorno.\n\nMEMÓRIA: você recebe memory-geral.md (o usuário) e o memory.md do projeto.\nRespeite as preferências e decisões que estão lá, e não recomece do zero um\nprojeto que já tem arquivos — veja o mapa antes de escrever. O memory.md do projeto se\ngrava sozinho. Já o memory-geral.md só guarda o que você mandar com\nremember_general: nome, preferência que vale em qualquer projeto, decisão de\nlongo prazo. Sem essa chamada, nada disso sobrevive para a próxima vez.\n\nPROCESSO: ache o alvo (mapa ou grep) → leia só ele → faça → confira → finish.\nCriar do zero: escreva direto, sem ler nada.",
3
+ "SYSTEM_NUCLEO": "Você é o PrimoCode, agente de engenharia da ConectaPrimoAI.\n\nCONVERSA OU TRABALHO: cumprimento, agradecimento, pergunta sobre você ou papo\n(\"oi\", \"o que você faz?\") → responda em texto, 1 ou 2 frases, sem tool nenhuma,\ne não invente tarefa que ninguém pediu. Pedido de criar, alterar, ler, rodar ou\nconsertar → EXECUTE, até o fim, sem conversar no meio.\n\nREGRAS ABSOLUTAS (executando):\n1. NUNCA escreva código na resposta. Código vai SEMPRE dentro de uma tool.\n2. NUNCA pergunte ao usuário. Decida e execute.\n3. NUNCA invente ferramenta, serviço, porta ou API: as suas são EXATAMENTE as\n desta lista. E nunca diga que algo \"não está rodando\" para se esquivar — o\n que precisa subir, sobe sozinho quando você chama a tool. Prometer o que\n você não tem, ou culpar um serviço, é o pior erro possível.\n4. Caminhos RELATIVOS (\"index.html\", \"src/game.js\"). Você já está na pasta do\n projeto: nunca caminho absoluto, nunca \"../\".\n5. Termine SEMPRE com finish(\"frase curta do que foi feito\").\n6. Resposta em texto: no máximo 1 frase, sem markdown.\n7. NUNCA repita uma chamada que já fez. Reformular a mesma busca traz as mesmas\n fontes; spec e manual são FIXOS — leu uma vez, está lido. Pesquisou e leu a\n spec? O passo seguinte é CRIAR com o que já tem.\n\nESCOLHENDO A TOOL: arquivo novo → write_file; mudar parte → edit_file (nunca\nreescreva o arquivo inteiro para trocar poucas linhas); várias mudanças no mesmo\narquivo → multi_edit, numa chamada; não sabe onde está → grep (conteúdo) ou glob\n(nome), nunca list_dir (o mapa já está aqui); servidor de pé → run_background.\n\nANTES DE EDITAR, LEIA SÓ O ARQUIVO QUE VAI MUDAR (edit_file exige o texto\nliteral), inteiro e uma vez. Nada de ler outros \"para entender\".\n\nQUANDO FALHA: leia o erro e conserte. Se edit_file disser \"trecho não\nencontrado\", ele devolve \"trechosParecidos\" com as linhas reais — copie de lá.\nNUNCA chame finish dizendo que fez algo que falhou: \"não consegui alterar X\nporque Y\" é melhor que um \"pronto!\" falso.\n\nPLANO: tarefa de 3+ passos começa com write_todos e vai atualizando o status (só\nUMA \"in_progress\" por vez). Tarefa curta não precisa.\n\nCOMO VOCÊ FALA ENQUANTO TRABALHA: antes de uma tool que muda o rumo (conserto,\nconversão, desvio), UMA frase em primeira pessoa com o quê e o porquê (\"o vídeo\nveio em .avi, vou converter para mp4\"); resolvido, \"feito, continuando\". Passo\nóbvio não precisa. NUNCA cite limite, cota, token, modelo, provedor, tentativa\nou erro interno: quem lê vê o trabalho, não o encanamento. Contornou uma\nfalha? Conte o contorno.\n\nTOM (como um engenheiro sênior): seguro e direto. Português do Brasil, frases curtas, uma ideia por frase.\nDê o dado concreto: arquivo, linha, comando, número. Diga o que fez e o que conferiu (\"rodei npm test: 42 ok\").\nNão sabe ou não conferiu? Diga com clareza (\"não rodei os testes\"). Proibido: \"Claro!\", \"Ótima pergunta\",\nelogio, desculpa em excesso, enrolação e emoji. Pouco jargão: se usar termo técnico, ele tem que servir ao leitor.\n\nMEMÓRIA: você recebe memory-geral.md (o usuário) e o memory.md do projeto.\nRespeite as preferências e decisões que estão lá, e não recomece do zero um\nprojeto que já tem arquivos — veja o mapa antes de escrever. O memory.md do projeto se\ngrava sozinho. Já o memory-geral.md só guarda o que você mandar com\nremember_general: nome, preferência que vale em qualquer projeto, decisão de\nlongo prazo. Sem essa chamada, nada disso sobrevive para a próxima vez.\n\nPROCESSO: ache o alvo (mapa ou grep) → leia só ele → faça → confira → finish.\nCriar do zero: escreva direto, sem ler nada.",
4
4
  "SECOES": {
5
5
  "pesquisa": "PEDIDO QUE DEPENDE DE FATO DO MUNDO — PESQUISE PRIMEIRO:\nSe o que o usuário pediu envolve algo real (uma empresa, um produto, uma\npessoa, um mercado, um número, um acontecimento), chame pesquisar_referencia\nANTES de criar qualquer coisa, em qualquer esforço. \"Pesquise sobre a empresa X\ne faça slides\" é UM pedido só: pesquisa e depois cria, sem parar para perguntar\ne sem entregar o deck com dado inventado.\n\nUm deck bonito cheio de número inventado é pior que nenhum deck — o usuário\nleva aquilo para uma reunião. Se depois da pesquisa faltar um dado, escreva na\npeça o que você sabe e deixe o buraco explícito (\"faturamento: não divulgado\"),\nnunca um número plausível de sua cabeça.",
6
6
  "estudio": "O ESTÚDIO — VÍDEO, MOTION, SLIDES E ARTE, FEITOS AQUI:\nCada peça é uma PASTA no computador do usuário, e o arquivo final sai pelo\nRemotion (.mp4 no vídeo, .pdf em slides e arte). Nada de link para abrir num\nsite: no fim, o arquivo abre sozinho para o usuário.\n\nFLUXO (vídeo, motion, slides ou arte feitos do zero):\n1. estudio_projeto acao:\"criar\" (titulo, pedido, formato: video | slides | arte).\n2. Empresa ou marca de verdade → estudio_site ANTES, e use as cores, a fonte\n e a logo que ele devolve em roteiro.marca.\n3. estudio_plano com ROTEIRO: 6 a 12 partes, cada uma com o tipo certo\n (abertura, numero, lista, passos, contraste, citacao, imagem, grafico,\n titulo, cta) e a \"fala\" — a frase dita, que não repete a tela. O PrimoCode\n faz o layout, a voz com entonação, os ícones, a música e os sons. NÃO\n escreva cenas nem posições à mão.\n4. estudio_render. Para ajustar, mande o roteiro de novo com a mudança.\n\nGRAVAÇÃO DO USUÁRIO FALANDO (\"edita meu vídeo\", \"faz um reels disso\", \"coloca\nlegenda e efeitos\", um .mp4/.mov dele): a edição completa é do editor_pro (o\nmotor do EDIT IA PRO). acao:\"cortar\" com o video → leia a fala que volta (com os\ntempos) → acao:\"montar\" com a pasta e o plano (gancho, pecas, broll) → entregue\n<pasta>/final.mp4 só com o portão PASSOU. Atalho: acao:\"editar\" faz tudo com o\nplano automático. Operações soltas na gravação (só cortar um trecho, juntar,\nexportar) seguem no estudio_video. NUNCA escreva .mp4/.mp3 com write_file e\nnunca monte vídeo com ffmpeg na mão.\n\nO ROTEIRO BOM: a primeira parte prende (abertura ou contraste), o miolo prova\n(numero, grafico, lista, imagem) e a última pede ação (cta). Varie os tipos —\ntrês \"titulo\" seguidos parecem slide parado. Título que diz algo (\"Receita\ntriplicou em 18 meses\"), não rótulo (\"Resultados\"). Número de verdade ou\nnenhum. A fala não repete a tela: a tela dá o título, a voz dá o argumento.\n\nSE O USUÁRIO CITAR UM APP (Canva, PowerPoint, CapCut, Premiere…): faça a peça\ncom as ferramentas do Estúdio e avise em uma linha. Se ele insistir no app,\nprocure um MCP dele (mcp_procurar) antes de abrir com o mouse.\n\nDocumento, planilha e PDF não são do Estúdio: faça o arquivo (.docx, .xlsx,\n.pdf) com código, na pasta do projeto.",
@@ -163,6 +163,16 @@ const GRUPOS = [
163
163
  tools: ['spawn_agent'],
164
164
  gatilho: /\b(v[áa]rios|v[áa]rias|todos os|todas as|em paralelo|projeto inteiro|refator\w*|migra[çc][ãa]o|auditor\w*|e tamb[ée]m|e depois|al[ée]m disso|sistema|plataforma|complet[oa]|backend|frontend|front e back|full.?stack|testes|telas|m[óo]dulos|camadas|arquivos|crud|api e)\b/i, // sem "site", "app", "aplicativo": casavam com quase todo pedido (13/10)
165
165
  },
166
+ {
167
+ nome: 'notebook',
168
+ tools: ['notebook_edit'],
169
+ gatilho: /\b(ipynb|notebook|jupyter|c[ée]lula)\b/i,
170
+ },
171
+ {
172
+ nome: 'skill',
173
+ tools: ['skill'],
174
+ gatilho: /\bskills?\b/i,
175
+ },
166
176
  {
167
177
  nome: 'terminal',
168
178
  tools: ['run_in_new_terminal', 'run_background', 'background_output', 'stop_background'],
@@ -1489,7 +1489,7 @@ async function runActInterno(userPrompt, opts) {
1489
1489
  emVoo = null;
1490
1490
  if (!subagent && !avisouReserva && modeloDaVolta && modeloDaVolta.reserva) {
1491
1491
  avisouReserva = true;
1492
- ev('linha', { texto: ` ⎿ ${linhaDaReserva(modeloDaVolta.modelo)}`, nivel: 'dim' });
1492
+ ev('linha', { texto: ` ⎿ ${linhaDaReserva(modeloDaVolta.modelo)}`, nivel: 'aviso' });
1493
1493
  }
1494
1494
  traco(`volta ${voltas} voltou em ${Math.round((Date.now() - tRodada) / 1000)}s · ${toolCalls.map((t) => t.name).join(', ') || 'texto'}${streamError ? ' · erro: ' + String(streamError).slice(0, 160) : ''}`);
1495
1495
  tempos.somar(subagent ? 'rodada do subagente' : 'rodada do modelo', Date.now() - tRodada);
@@ -26,6 +26,8 @@ const GRUPOS = [
26
26
  args: 'path. old_string: trecho exato a substituir, precisa ser único no arquivo. new_string: texto que entra no lugar. replace_all (opcional, bool, padrão false): troca todas as ocorrências.' },
27
27
  { tool: 'multi_edit', escreve: true, faz: 'várias trocas no mesmo arquivo, tudo ou nada',
28
28
  args: 'path. edits: lista de {old_string, new_string, replace_all (opcional)}, aplicadas em ordem.' },
29
+ { tool: 'notebook_edit', escreve: true, faz: 'muda uma célula de um notebook .ipynb pelo índice: troca, insere ou apaga',
30
+ args: 'path: o .ipynb. cell_number: índice da célula (começa em 0). edit_mode (opcional): replace (padrão) | insert | delete. new_source: texto novo (replace e insert). cell_type (opcional): code | markdown (obrigatório no insert).' },
29
31
  { tool: 'read_file', escreve: false, faz: 'lê arquivo, inteiro ou por pedaço',
30
32
  args: 'path. offset (opcional, número): primeira linha a ler. limit (opcional, número): quantas linhas.' },
31
33
  { tool: 'list_dir', escreve: false, faz: 'mostra a árvore de pastas',
@@ -262,6 +264,8 @@ const GRUPOS = [
262
264
  args: 'consulta: poucas palavras. limite (opcional, número, padrão 6).' },
263
265
  { tool: 'ler_url', escreve: false, faz: 'abre um link e devolve o texto da página em markdown',
264
266
  args: 'url: link completo. max_chars (opcional), desde (opcional, número: continua uma leitura cortada).' },
267
+ { tool: 'skill', escreve: false, faz: 'carrega o SKILL.md de uma skill instalada pelo nome, para seguir as instruções dela',
268
+ args: 'nome: o nome da skill (ex. "editar-video"). Sem argumentos: lista as instaladas.' },
265
269
  { tool: 'agentes_buscar', escreve: false, faz: 'acha o especialista certo no catálogo de 100+ agentes (ou traz o briefing de um)',
266
270
  args: 'consulta: o que você precisa, em poucas palavras. ver (opcional): id de um agente para ler o briefing completo. Sem argumentos: lista as áreas.' },
267
271
  { tool: 'capacidade_buscar', escreve: false, faz: 'acha uma receita pronta entre 1000+ (22 grupos: git, shell, depuração, código, frontend, backend, banco, devops, testes, mobile, ia-llm, design, video-motion, marketing, produto-negocio, windows-linux, web, segurança…)',
@@ -18,6 +18,7 @@ const SCHEMAS = {
18
18
  read_file: { offset: 'number', limit: 'number' },
19
19
  edit_file: { replace_all: 'boolean' },
20
20
  multi_edit: { edits: 'array' },
21
+ notebook_edit: { cell_number: 'number' },
21
22
  write_todos: { todos: 'array' },
22
23
  glob: { limit: 'number' },
23
24
  run_shell: { timeout_ms: 'number', em_segundo_plano: 'boolean' },
package/lib/tool/tools.js CHANGED
@@ -604,6 +604,55 @@ function writeTodos({ todos }) {
604
604
  }
605
605
  function getTodos() { return TODOS; }
606
606
 
607
+ /** Edita UMA célula de um .ipynb pelo índice: replace, insert ou delete. Só mexe na célula; o resto do JSON fica como estava. */
608
+ function notebookEdit({ path: rawPath, cell_number, edit_mode, new_source, cell_type }, projectDir) {
609
+ const filePath = resolveInProject(rawPath, projectDir);
610
+ if (!/\.ipynb$/i.test(filePath)) return { ok: false, error: 'notebook_edit só edita arquivo .ipynb' };
611
+ if (!fs.existsSync(filePath)) return { ok: false, error: `Arquivo não existe: ${rawPath}` };
612
+ let nb;
613
+ try { nb = JSON.parse(fs.readFileSync(filePath, 'utf-8')); } catch (e) { return { ok: false, error: `notebook inválido (JSON): ${e.message}` }; }
614
+ if (!nb || !Array.isArray(nb.cells)) return { ok: false, error: 'notebook sem lista "cells"' };
615
+ const modo = edit_mode || 'replace';
616
+ if (!['replace', 'insert', 'delete'].includes(modo)) return { ok: false, error: 'edit_mode precisa ser replace, insert ou delete' };
617
+ const n = Number(cell_number);
618
+ const max = modo === 'insert' ? nb.cells.length : nb.cells.length - 1;
619
+ if (!Number.isInteger(n) || n < 0 || n > max) return { ok: false, error: `cell_number ${cell_number} fora do intervalo (0 a ${max}; o notebook tem ${nb.cells.length} células)` };
620
+ if (modo !== 'delete' && typeof new_source !== 'string') return { ok: false, error: 'new_source é obrigatório em replace e insert' };
621
+ // .ipynb guarda o código como lista de linhas, cada uma terminando em \n menos a última
622
+ const fonte = (t) => t.split('\n').map((l, i, a) => (i < a.length - 1 ? l + '\n' : l)).filter((l, i, a) => l !== '' || a.length === 1);
623
+ if (modo === 'delete') nb.cells.splice(n, 1);
624
+ else if (modo === 'insert') {
625
+ if (!['code', 'markdown'].includes(cell_type)) return { ok: false, error: 'insert exige cell_type: code ou markdown' };
626
+ const cel = { cell_type, metadata: {}, source: fonte(new_source) };
627
+ if (cell_type === 'code') { cel.execution_count = null; cel.outputs = []; }
628
+ nb.cells.splice(n, 0, cel);
629
+ } else {
630
+ const cel = nb.cells[n];
631
+ cel.source = fonte(new_source);
632
+ if (cell_type && cell_type !== cel.cell_type) {
633
+ if (!['code', 'markdown'].includes(cell_type)) return { ok: false, error: 'cell_type precisa ser code ou markdown' };
634
+ cel.cell_type = cell_type;
635
+ if (cell_type === 'code') { cel.execution_count = null; cel.outputs = []; } else { delete cel.execution_count; delete cel.outputs; }
636
+ } else if (cel.cell_type === 'code') { cel.execution_count = null; cel.outputs = []; }
637
+ }
638
+ fs.writeFileSync(filePath, JSON.stringify(nb, null, (nb.nbformat && nb.nbformat >= 4 ? 1 : 2)) + '\n', 'utf-8');
639
+ return { ok: true, path: rawPath, modo, cell_number: n, celulas: nb.cells.length };
640
+ }
641
+
642
+ /** Carrega o SKILL.md de uma skill instalada. Sem nome, lista as que existem. */
643
+ function skillTool({ nome, name } = {}) {
644
+ const skills = require('../skill/skills');
645
+ const pedido = String(nome || name || '').trim();
646
+ if (!pedido) {
647
+ const lista = skills.listar().filter((s) => s.ligada).map((s) => ({ nome: s.nome, descricao: s.descricao }));
648
+ return { ok: true, skills: lista, comoUsar: 'chame skill com nome:"<uma delas>" para ler as instruções.' };
649
+ }
650
+ const s = skills.ler(skills.apelidar(pedido));
651
+ if (!s) return { ok: false, error: `não há skill "${pedido}"`, disponiveis: skills.listar().map((x) => x.nome) };
652
+ if (!s.ligada) return { ok: false, error: `a skill "${s.nome}" está desligada` };
653
+ return { ok: true, nome: s.nome, titulo: s.titulo, descricao: s.descricao, instrucoes: s.corpo };
654
+ }
655
+
607
656
  /**
608
657
  * Executa uma ferramenta e NUNCA lança.
609
658
  *
@@ -778,6 +827,10 @@ async function despachar(name, args, ctx) {
778
827
  return multiEdit(args, projectDir);
779
828
  case 'write_todos':
780
829
  return writeTodos(args);
830
+ case 'notebook_edit':
831
+ return notebookEdit(args, projectDir);
832
+ case 'skill':
833
+ return skillTool(args);
781
834
  case 'agentes_buscar':
782
835
  return require('../agentes').ferramentaBuscar(args || {});
783
836
  case 'capacidade_buscar':
package/lib/tui/ui.js CHANGED
@@ -36,9 +36,11 @@ const PALETTE = {
36
36
  reset: '\x1b[0m',
37
37
  };
38
38
 
39
- /* NO_COLOR (https://no-color.org): sem nenhuma cor. Negrito e reset ficam. */
40
- if (process.env.NO_COLOR !== undefined && process.env.NO_COLOR !== '') {
41
- for (const k of Object.keys(PALETTE)) if (k !== 'bold' && k !== 'reset') PALETTE[k] = '';
39
+ /* Sem cor: NO_COLOR (https://no-color.org) ou a saída não é um terminal (pipe, arquivo, CI), salvo FORCE_COLOR.
40
+ Nenhum código ANSI sai, nem negrito. */
41
+ const SEM_COR = (process.env.NO_COLOR !== undefined && process.env.NO_COLOR !== '') || (!process.stdout.isTTY && !process.env.FORCE_COLOR);
42
+ if (SEM_COR) {
43
+ for (const k of Object.keys(PALETTE)) PALETTE[k] = '';
42
44
  }
43
45
 
44
46
  /* O que a barra da direita da tela cheia mostra e que vem do trabalho do agente:
@@ -418,6 +420,7 @@ let temaAtual = 'marca';
418
420
  function definirTema(id) {
419
421
  const t = TEMAS[id];
420
422
  if (!t) return false;
423
+ if (SEM_COR) { temaAtual = id; return true; }
421
424
  PALETTE.brand = `\x1b[38;5;${t.brand}m`; PALETTE.brandDim = `\x1b[38;5;${t.brandDim}m`;
422
425
  PALETTE.ok = PALETTE.brand; PALETTE.cyan = PALETTE.brand;
423
426
  temaAtual = id;
@@ -618,7 +621,7 @@ const agora = { texto: '', desde: 0 };
618
621
 
619
622
  // O rótulo de cada tool na tela — o que a pessoa lê no lugar do nome interno.
620
623
  const ROTULOS = {
621
- read_file:'Read', write_file:'Write', edit_file:'Edit', multi_edit:'MultiEdit',
624
+ read_file:'Read', write_file:'Write', edit_file:'Edit', multi_edit:'MultiEdit', notebook_edit:'Notebook', skill:'Skill',
622
625
  list_dir:'List', glob:'Glob', grep:'Grep', run_shell:'Bash',
623
626
  run_in_new_terminal:'Terminal', run_background:'Fundo', background_output:'Fundo·log', stop_background:'Fundo·stop', open_in_browser:'Open', spawn_agent:'Agent',
624
627
  write_todos:'Plano', perguntar:'Pergunta', finish:'Finish', pesquisar_referencia:'Pesquisa',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "primocode",
3
- "version": "11.3.2",
3
+ "version": "11.4.0",
4
4
  "description": "PrimoCode — agente de engenharia com IA e cursor próprio. Requer conta Conecta Primo AI (Premium ou Super). Cria arquivos, roda comandos, controla navegador e desktop: abre apps, clica em botões e ícones pelo nome, digita e usa atalhos.",
5
5
  "main": "bin/primocode.js",
6
6
  "bin": {
@@ -11,7 +11,7 @@
11
11
  "postinstall": "node lib/preparar.js || true",
12
12
  "test:server": "cd server && npm test",
13
13
  "test": "node scripts/testes.js",
14
- "test:app": "node test/app-fluxo.test.mjs && node test/app-primeira.test.mjs && node test/app-entrar.test.mjs && node test/app-tela-estudio.test.mjs && node test/app-celular-telas.test.mjs && node test/app-compositor-voz.test.mjs",
14
+ "test:app": "node test/app-fluxo.test.mjs && node test/app-primeira.test.mjs && node test/app-entrar.test.mjs && node test/app-tela-estudio.test.mjs && node test/app-celular-telas.test.mjs && node test/app-render-celular.test.mjs && node test/app-nuvem-estudio.test.mjs",
15
15
  "app:build": "cd app && npm run dist",
16
16
  "app:dev": "cd app && npm start",
17
17
  "mobile:build": "cd mobile && npm run build"
@@ -117,11 +117,10 @@ a{color:#67e8f9}a:hover{color:#22d3ee}
117
117
  .enviar:hover{transform:scale(1.08);background:#4a7de0}
118
118
  .enviar:active{transform:scale(.92)}
119
119
  .enviar svg{position:absolute;transition:opacity .22s,transform .45s cubic-bezier(.3,1.7,.5,1)}
120
- .enviar .i-voz,.enviar .i-seta,.enviar .i-stop{opacity:0;transform:scale(.3) rotate(-90deg)}
121
- .enviar.vazio .i-voz{opacity:1;transform:none}
120
+ .enviar .i-seta,.enviar .i-stop{opacity:0;transform:scale(.3) rotate(-90deg)}
122
121
  .enviar.cheio .i-seta{opacity:1;transform:none}
123
122
  .enviar.parar .i-stop{opacity:1;transform:none}
124
- .enviar.semvoz{visibility:hidden;pointer-events:none}
123
+ .enviar.vazio{visibility:hidden;pointer-events:none}
125
124
  /* mascote e título */
126
125
  .mascote{transition:transform .35s cubic-bezier(.3,1.5,.5,1);filter:drop-shadow(0 0 26px rgba(34,211,238,.35))}
127
126
  .pula-entra{animation:cai .9s cubic-bezier(.3,1.6,.5,1) both}
@@ -313,6 +312,30 @@ a{color:#67e8f9}a:hover{color:#22d3ee}
313
312
  @keyframes efhalo{0%,100%{box-shadow:0 1px 5px rgba(0,0,0,.4),0 0 5px 1px rgba(61,139,255,.5)}50%{box-shadow:0 1px 5px rgba(0,0,0,.4),0 0 9px 2px rgba(61,139,255,.7)}}
314
313
  .efs-rec{position:relative;height:14px;margin-top:5px}
315
314
  .efs-rec span{position:absolute;top:0;left:calc(var(--pad) + var(--q)*(100% - var(--pad)*2));transform:translateX(-50%);font-size:11px;color:#8a8983;white-space:nowrap}
315
+ /* Vidro nativo (app desktop no macOS, S5): o main.js liga a janela com vibrancy e põe a classe .vidro-nativo.
316
+ Vidro só na lateral, na barra de cima e no botão de enviar; a conversa continua sólida. Sem transparência: cores sólidas de antes. */
317
+ html.vidro-nativo,html.vidro-nativo body{background:transparent}
318
+ html.vidro-nativo .app{background:transparent}
319
+ html.vidro-nativo #main{background:#131312}
320
+ html.vidro-nativo #lado{background:rgba(15,15,14,.42);border-right-color:rgba(255,255,255,.07);box-shadow:inset -1px 0 0 rgba(255,255,255,.04)}
321
+ html.vidro-nativo #main>header{background:linear-gradient(180deg,rgba(255,255,255,.06),transparent 70%),rgba(19,19,18,.55);-webkit-backdrop-filter:blur(22px) saturate(1.6);backdrop-filter:blur(22px) saturate(1.6);border-bottom-color:rgba(255,255,255,.08)}
322
+ html.vidro-nativo .enviar.cheio{background:linear-gradient(180deg,rgba(255,255,255,.28),transparent 55%),#3b6fd4;box-shadow:inset 0 1px 0 rgba(255,255,255,.45),0 4px 14px rgba(59,111,212,.35)}
323
+ html.vidro-nativo .menu{transition-timing-function:cubic-bezier(.32,.72,0,1)}
324
+ /* Mola e folha (S5, kit apple.js inlinado): menus abrem com mola (linear() do CSS) e fecham arrastando para baixo. */
325
+ :root{--ap-mola:cubic-bezier(.32,.72,0,1)}
326
+ .menu:not(.pal){animation-duration:.5s;animation-timing-function:var(--ap-mola)}
327
+ .menu.ap-solta{transition:transform .5s var(--ap-mola),opacity .25s;animation:none}
328
+ .menu.ap-arrasta{transition:none;animation:none;cursor:grabbing}
329
+ /* Popover do esforço: abre com a mesma mola do menu, a partir do chip (sem arrasto, porque tem slider). */
330
+ .menu.efs-pop{animation:abre .5s var(--ap-mola) both;transform-origin:bottom left}
331
+ @media (prefers-reduced-motion:reduce){.menu.efs-pop{animation:none}}
332
+ @media (prefers-reduced-motion:reduce){.menu.ap-solta{transition:none}}
333
+ @media (prefers-reduced-transparency:reduce){
334
+ html.vidro-nativo #lado{background:#0f0f0e}
335
+ html.vidro-nativo #main>header{background:#131312;-webkit-backdrop-filter:none;backdrop-filter:none}
336
+ html.vidro-nativo .app{background:#131312}
337
+ html.vidro-nativo .enviar.cheio{background:#3b6fd4;box-shadow:none}
338
+ }
316
339
  @media (prefers-reduced-motion:reduce){.app *{animation:none !important;transition:none !important}}
317
340
  </style>
318
341
  </head>
@@ -354,8 +377,7 @@ a{color:#67e8f9}a:hover{color:#22d3ee}
354
377
  <button class="chip" id="chipModo" data-a="menuModo"></button>
355
378
  <button class="chip" id="chipEf" data-a="menuEf" hidden></button>
356
379
  </div>
357
- <button class="enviar vazio" id="enviar" data-a="enviar" aria-label="Abrir a voz do Primo">
358
- <svg class="i-voz" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" aria-hidden="true"><path d="M5 10v4M9.5 7v10M14 4.5v15M18.5 8v8"></path></svg>
380
+ <button class="enviar vazio" id="enviar" data-a="enviar" aria-label="Enviar">
359
381
  <svg class="i-seta" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 19V5M5 12l7-7 7 7"></path></svg>
360
382
  <svg class="i-stop" width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><rect x="6" y="6" width="12" height="12" rx="2.5"></rect></svg>
361
383
  </button>
@@ -413,7 +435,6 @@ var ICO={
413
435
  var ABAS=[
414
436
  {id:'geral',t:'Geral',cor:'#6b6b66',ico:'<path d="M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6ZM12 2v3M12 19v3M2 12h3M19 12h3M4.9 4.9l2.1 2.1M17 17l2.1 2.1M4.9 19.1 7 17M17 7l2.1-2.1"></path>'},
415
437
  {id:'modelos',t:'Modelos',cor:'#7c5cff',ico:'<path d="M12 3l2.2 5.8L20 11l-5.8 2.2L12 19l-2.2-5.8L4 11l5.8-2.2Z"></path>'},
416
- {id:'voz',t:'Voz',cor:'#f0597a',ico:'<path d="M5 10v4M9.5 7v10M14 4.5v15M18.5 8v8"></path>'},
417
438
  {id:'conta',t:'Conta',cor:'#0ea5b7',ico:'<path d="M12 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8ZM4 21a8 8 0 0 1 16 0"></path>'}
418
439
  ];
419
440
  var MODOS=[
@@ -667,7 +688,7 @@ var estado={
667
688
  anexos:[],menu:false,aviso:null,
668
689
  skills:null,ajustes:null,busca:'',
669
690
  comandos:[],pal:{aberta:false,itens:[],sel:0,q:null},abertas:{},confirma:null,provSel:'',chaveDraft:'',chaveSalva:false,
670
- semVoz:false,arqBase:'' /* o app desktop manda semVoz (sem motor de voz) e arqBase (o protocolo que serve os arquivos); no Mac ficam vazios */
691
+ arqBase:'' /* o app desktop manda arqBase (o protocolo que serve os arquivos); no Mac fica vazio */
671
692
  };
672
693
  var animar=false;
673
694
  var mx=0,my=0;
@@ -841,11 +862,10 @@ function htmlSkills(anim){
841
862
  /* ajustes */
842
863
  function tg(a,on,rot,extra){return '<button class="tg'+(on?' on':'')+'" data-a="'+a+'" aria-label="'+esc(rot)+'"'+(extra||'')+'></button>';}
843
864
  function htmlConfig(anim){
844
- var abas=ABAS.filter(function(a){return !(estado.semVoz&&a.id==='voz');}).map(function(a){
865
+ var abas=ABAS.map(function(a){
845
866
  return '<button class="lado-cfg'+(estado.aba===a.id?' on':'')+'" data-a="aba" data-id="'+a.id+'"><span class="tile" style="background:'+a.cor+'">'+ic(a.ico,14,2.2)+'</span>'+a.t+'</button>';
846
867
  }).join('');
847
- if(estado.semVoz&&estado.aba==='voz')estado.aba='modelos';
848
- var corpo={geral:cfgGeral,modelos:cfgModelos,voz:cfgVoz,conta:cfgConta}[estado.aba]();
868
+ var corpo={geral:cfgGeral,modelos:cfgModelos,conta:cfgConta}[estado.aba]();
849
869
  return '<div class="'+(anim?'tela-entra':'')+'" style="width:100%;max-width:860px;align-self:center;margin-bottom:auto;display:flex;flex-wrap:wrap;gap:32px">'+
850
870
  '<nav style="flex:1 1 170px;max-width:210px;display:flex;flex-direction:column;gap:2px"><h2 style="margin:0 0 14px 8px;font-family:\'Instrument Serif\',serif;font-weight:400;font-size:34px">Ajustes</h2>'+abas+'</nav>'+
851
871
  '<div style="flex:999 1 440px;min-width:0">'+corpo+'</div></div>';
@@ -882,12 +902,6 @@ function cfgModelos(){
882
902
  }
883
903
  return h;
884
904
  }
885
- function cfgVoz(){
886
- var A=estado.ajustes;
887
- if(!A||!A.voz)return h3('Voz')+'<p style="color:#8a8983;font-size:13px;margin:16px 4px">Carregando os ajustes.</p>';
888
- return h3('Voz')+'<div class="grupo"><div class="lin"><label for="vozNome" style="min-width:0">Nome do assistente<small>é a palavra que chama o Primo na voz</small></label><input id="vozNome" class="campo campo-s" data-in="voz_nome" autocomplete="off" value="'+esc(A.voz.nome||'')+'"></div>'+
889
- '<div class="lin"><span>Sons</span>'+tg('sons',!!A.voz.sons,'Sons')+'</div></div>';
890
- }
891
905
  function cfgConta(){
892
906
  if(!estado.logado)return h3('Conta')+'<div class="grupo"><div class="lin"><span>Entre pra salvar as conversas e usar o Primo.</span><button class="entrar" data-a="entrar">Entrar</button></div></div>';
893
907
  var ini=(String(estado.email||'?').trim().charAt(0)||'?').toUpperCase();
@@ -908,6 +922,19 @@ function renderAnexos(){
908
922
  }
909
923
  var GAUGE='<path d="M4 17a8 8 0 1 1 16 0"></path><path d="m12 17 4-5"></path>';
910
924
  function efNiveis(){var e=estado.ajustes&&estado.ajustes.effort;return e&&Array.isArray(e.niveis)?e.niveis.map(function(x){return typeof x==='string'?{nome:x,resumo:''}:x;}):[];}
925
+ /* Kit apple.js (S5), parte mínima inlinada: mola crítica/amortecida como easing CSS e arrasto-para-fechar no menu. */
926
+ function apMola(t,am,rig){if(t<=0)return 0;if(am>=1)return 1-(1+rig*t)*Math.exp(-rig*t);var w=rig*Math.sqrt(1-am*am);return 1-Math.exp(-am*rig*t)*(Math.cos(w*t)+(am*rig/w)*Math.sin(w*t));}
927
+ (function(){try{var n=40,fim=6/(0.8*24),p=[];for(var i=0;i<=n;i++)p.push(+apMola(i/n*fim,0.8,24).toFixed(4));p[n]=1;
928
+ if(window.CSS&&CSS.supports&&CSS.supports('transition-timing-function','linear(0,1)'))document.documentElement.style.setProperty('--ap-mola','linear('+p.join(',')+')');}catch(e){}})();
929
+ function apLigaMenu(){
930
+ var m=document.querySelector('#menu>.menu');if(!m||m.dataset.ap||m.classList.contains('efs-pop'))return;m.dataset.ap='1';
931
+ var y0=0,t0=0,y1=0,t1=0,on=false;
932
+ m.addEventListener('pointerdown',function(e){if(e.target.closest('button,input,.efs'))return;on=true;y0=y1=e.clientY;t0=t1=performance.now();m.classList.add('ap-arrasta');m.setPointerCapture&&m.setPointerCapture(e.pointerId);});
933
+ m.addEventListener('pointermove',function(e){if(!on)return;y1=e.clientY;t1=performance.now();var d=y1-y0;m.style.transform='translateY('+(d>0?d:-60*(1-Math.exp(d/60)))+'px)';});
934
+ function fim(){if(!on)return;on=false;var d=y1-y0,v=d/Math.max(1,t1-t0);m.classList.remove('ap-arrasta');m.classList.add('ap-solta');
935
+ if(d+v*200>70){m.style.transform='translateY(140%)';m.style.opacity='0';setTimeout(function(){estado.menu=false;renderMenu();},260);}else m.style.transform='';}
936
+ m.addEventListener('pointerup',fim);m.addEventListener('pointercancel',fim);
937
+ }
911
938
  function renderMenu(){
912
939
  var chip=$('chipModo'),ce=$('chipEf');
913
940
  var atual=MODOS.filter(function(o){return o.id===estado.modo;})[0]||MODOS[0];
@@ -930,6 +957,8 @@ function renderMenu(){
930
957
  MODOS.map(function(o){return '<button class="opcao" data-a="modo" data-v="'+o.id+'"><span>'+o.t+'<small>'+o.d+'</small></span>'+(o.id===estado.modo?ic(ICO.ok,16,2.6).replace('<svg ','<svg class="ok" '):'')+'</button>';}).join('')+
931
958
  '<div style="height:1px;background:#2e2e2b;margin:6px 8px"></div>'+
932
959
  '<button class="opcao" data-a="cfgmodelos"><span style="color:#a3a29b;display:inline-flex">'+ic(GEAR,16)+'</span><span>Configurar outros modelos<small>Claude, Codex ou a sua chave</small></span></button></div>';
960
+
961
+ apLigaMenu();
933
962
  }
934
963
  var EF_NOME={low:'Baixo',medium:'Médio',high:'Alto',xhigh:'Extra alto',max:'Máximo',ultracode:'Ultracode'};
935
964
  function efNome(id){return EF_NOME[id]||id;}
@@ -1161,14 +1190,13 @@ function renderAvisos(){
1161
1190
  var a=estado.aviso,el=$('avisos');
1162
1191
  if(!a){el.innerHTML='';return;}
1163
1192
  if(a.tipo==='login')el.innerHTML='<div class="cartao-av"><span>Entre na sua conta do PrimoCode pra conversar.</span><span style="display:flex;align-items:center;gap:6px"><button class="entrar" data-a="entrar">Entrar</button><button class="x" data-a="fechaAviso" aria-label="Fechar">'+ic(ICO.x,12,2.4)+'</button></span></div>';
1164
- else if(a.tipo==='voz')el.innerHTML='<div class="cartao-av"><span style="min-width:0;overflow-wrap:anywhere">'+esc(a.texto)+'</span><span style="display:flex;align-items:center;gap:6px">'+(a.instalar?'<button class="entrar" data-a="vozInstalar">Instalar a voz</button>':'')+(a.instalando?'':'<button class="x" data-a="fechaAviso" aria-label="Fechar">'+ic(ICO.x,12,2.4)+'</button>')+'</span></div>';
1165
1193
  else if(a.tipo==='sair')el.innerHTML='<div class="cartao-av"><span>Sair da conta?</span><span style="display:flex;align-items:center;gap:6px"><button class="mc-b" data-a="fechaAviso">Cancelar</button><button class="entrar" style="background:#b8473d" data-a="sairConfirma">Sair</button></span></div>';
1166
1194
  else el.innerHTML='<div class="cartao-av erro"><span style="min-width:0;overflow-wrap:anywhere">'+esc(a.texto)+'</span><button class="x" data-a="fechaAviso" aria-label="Fechar">'+ic(ICO.x,12,2.4)+'</button></div>';
1167
1195
  }
1168
1196
  function atualizaBotao(){
1169
1197
  var b=$('enviar'),vazio=!ta().value.trim()&&!estado.anexos.length;
1170
1198
  if(estado.esperando){b.className='enviar parar';b.setAttribute('aria-label','Parar');}
1171
- else if(vazio){b.className='enviar vazio'+(estado.semVoz?' semvoz':'');b.setAttribute('aria-label','Abrir a voz do Primo');}
1199
+ else if(vazio){b.className='enviar vazio';b.setAttribute('aria-label','Enviar');}
1172
1200
  else{b.className='enviar cheio';b.setAttribute('aria-label','Enviar');}
1173
1201
  }
1174
1202
  function cresce(){var t=ta();t.style.height='auto';t.style.height=Math.min(t.scrollHeight,160)+'px';}
@@ -1307,13 +1335,6 @@ function appCmd(c,args){
1307
1335
  var u=ultimaResposta();
1308
1336
  if(u){copiarTexto(u,{textContent:'Copiar'});toast('Copiado');}else toast('Nada pra copiar');
1309
1337
  break;
1310
- case 'voz':if(!estado.semVoz)post({acao:'voz'});break;
1311
- case 'voz_nome':
1312
- estado.aba='voz';post({acao:'ajustes'});
1313
- if(args){post({acao:'ajuste',chave:'voz_nome',valor:String(args)});if(estado.ajustes&&estado.ajustes.voz)estado.ajustes.voz.nome=String(args);}
1314
- navegar('config');
1315
- setTimeout(function(){var e=$('vozNome');if(e)e.focus();},80);
1316
- break;
1317
1338
  case 'effort':
1318
1339
  var efc=estado.ajustes&&estado.ajustes.effort,nvs=efc?efNiveis().map(function(x){return x.nome;}):['low','medium','high','xhigh','max','ultracode'];
1319
1340
  if(al){
@@ -1351,7 +1372,7 @@ function enviar(pular){
1351
1372
  }
1352
1373
  function apertar(){
1353
1374
  if(estado.esperando){post({acao:'parar'});estado.esperando=false;estado.passos=[];renderEspera();atualizaBotao();return;}
1354
- if(!ta().value.trim()&&!estado.anexos.length){if(!estado.semVoz)post({acao:'voz'});return;}
1375
+ if(!ta().value.trim()&&!estado.anexos.length)return;
1355
1376
  enviar();
1356
1377
  }
1357
1378
  function setModo(v){
@@ -1453,7 +1474,6 @@ document.addEventListener('click',function(e){
1453
1474
  case 'entrar':post({acao:'entrar'});break;
1454
1475
  case 'sair':post({acao:'sair_conta'});break;
1455
1476
  case 'fechaAviso':estado.aviso=null;renderAvisos();break;
1456
- case 'vozInstalar':estado.aviso={tipo:'voz',texto:'Preparando a voz…',instalando:true};renderAvisos();post({acao:'voz_instalar'});break;
1457
1477
  case 'link':e.preventDefault();post({acao:'abrir_link',url:d.url});break;
1458
1478
  case 'skill':
1459
1479
  var k=estado.skills&&estado.skills[+d.i];
@@ -1480,13 +1500,6 @@ document.addEventListener('click',function(e){
1480
1500
  estado.chaveDraft='';estado.chaveSalva=true;renderPrincipal();
1481
1501
  break;
1482
1502
  case 'pasta':post({acao:'escolher_pasta'});break;
1483
- case 'sons':
1484
- if(estado.ajustes&&estado.ajustes.voz){
1485
- estado.ajustes.voz.sons=!estado.ajustes.voz.sons;
1486
- post({acao:'ajuste',chave:'voz_sons',valor:!!estado.ajustes.voz.sons});
1487
- renderPrincipal();
1488
- }
1489
- break;
1490
1503
  }
1491
1504
  });
1492
1505
  $('lb').addEventListener('click',function(){$('lb').hidden=true;});
@@ -1526,13 +1539,6 @@ document.addEventListener('input',function(e){
1526
1539
  document.addEventListener('change',function(e){
1527
1540
  var t=e.target;
1528
1541
  if(t.id==='provSel'){estado.provSel=t.value;estado.chaveSalva=false;renderPrincipal();}
1529
- else if(t.getAttribute&&t.getAttribute('data-in')==='voz_nome'){
1530
- var v=t.value.trim();
1531
- if(estado.ajustes&&estado.ajustes.voz&&v!==estado.ajustes.voz.nome){
1532
- estado.ajustes.voz.nome=v;
1533
- post({acao:'ajuste',chave:'voz_nome',valor:v});
1534
- }
1535
- }
1536
1542
  });
1537
1543
  document.addEventListener('keydown',function(e){
1538
1544
  if(e.key==='Escape'&&!$('lb').hidden){$('lb').hidden=true;return;}
@@ -1553,7 +1559,6 @@ document.addEventListener('keydown',function(e){
1553
1559
  if(e.key==='Escape'&&estado.menu){estado.menu=false;renderMenu();}
1554
1560
  if(e.target.id==='pergunta'&&e.key==='Enter'&&!e.shiftKey&&!e.isComposing){e.preventDefault();enviar();}
1555
1561
  if(e.target.id==='chaveApi'&&e.key==='Enter'){e.preventDefault();var b=$('chaveBtn');if(b)b.click();}
1556
- if(e.target.id==='vozNome'&&e.key==='Enter'){e.preventDefault();e.target.blur();}
1557
1562
  });
1558
1563
 
1559
1564
  /* arrastar e soltar */
@@ -1604,7 +1609,7 @@ var tratar={
1604
1609
  sincConta(v);
1605
1610
  if(v.pasta!=null)estado.pasta=v.pasta;
1606
1611
  if(v.modo==='chat'||v.modo==='agent')estado.modo=v.modo;
1607
- estado.semVoz=!!v.semVoz;estado.arqBase=String(v.arqBase||'');atualizaBotao();
1612
+ estado.arqBase=String(v.arqBase||'');atualizaBotao();
1608
1613
  renderConta();renderMenu();renderAvisos();
1609
1614
  if(estado.tela==='config')renderPrincipal();
1610
1615
  if(estado.logado)pedirConversas();
@@ -1662,10 +1667,6 @@ var tratar={
1662
1667
  else{estado.aviso={tipo:'erro',texto:txt};renderAvisos();}
1663
1668
  }else{estado.aviso={tipo:'erro',texto:txt};renderAvisos();}
1664
1669
  },
1665
- vozAviso:function(v){ /* o app desktop: a voz não está instalada, está instalando ou terminou */
1666
- if(!v)return;
1667
- estado.aviso={tipo:'voz',texto:String(v.texto||''),instalar:!!v.instalar,instalando:!!v.instalando};renderAvisos();
1668
- },
1669
1670
  anexo:function(v){
1670
1671
  if(!v||!v.caminho)return;
1671
1672
  if(estado.anexos.some(function(a){return a.caminho===v.caminho;}))return;
@@ -1730,7 +1731,7 @@ if(location.search.indexOf('demo')>=0&&!(window.webkit&&window.webkit.messageHan
1730
1731
  d1:{id:'d1',titulo:'Botão do WhatsApp no topo',mensagens:[{papel:'voce',texto:'Põe um botão do WhatsApp fixo no topo'},{papel:'primo',texto:'Pronto: o botão fica **fixo no topo**.\n\n- abre a conversa\n- mensagem já escrita\n\nVeja `index.html`:\n```html\n<a class="zap">Fale comigo</a>\n```\nMais em [o site](https://example.com).'}]},
1731
1732
  d2:{id:'d2',titulo:'Cardápio com fotos',mensagens:[{papel:'voce',texto:'Monta o cardápio'},{papel:'primo',texto:'Montei o cardápio em 3 seções.'}]}
1732
1733
  };
1733
- var aj={effort:{atual:'medium',niveis:[{nome:'low',resumo:'rápido — tarefa simples, resposta curta'},{nome:'medium',resumo:'equilibrado — o padrão'},{nome:'high',resumo:'pensa mais antes de agir'},{nome:'xhigh',resumo:'bem fundo — tarefas difíceis'},{nome:'max',resumo:'o máximo de raciocínio'},{nome:'ultracode',resumo:'esforço total em código, sem pressa'}]},permissoes:{atual:'auto',opcoes:[{nome:'auto',resumo:'o Primo decide quando pedir licença'},{nome:'livre',resumo:'faz tudo sem perguntar'},{nome:'plano',resumo:'só planeja, não mexe em nada'}]},modo:'agent',provedor:{id:'primo',nome:'Primo',chave_mascarada:''},provedores_chave:[{id:'anthropic',nome:'Anthropic'},{id:'openai',nome:'OpenAI'},{id:'openrouter',nome:'OpenRouter'}],pasta:'~/Primo',voz:{nome:'Primo',sons:true},conta:{logado:!fora,email:'joel@exemplo.com',plano:'Pro',planoAte:'2026-12-31'}};
1734
+ var aj={effort:{atual:'medium',niveis:[{nome:'low',resumo:'rápido — tarefa simples, resposta curta'},{nome:'medium',resumo:'equilibrado — o padrão'},{nome:'high',resumo:'pensa mais antes de agir'},{nome:'xhigh',resumo:'bem fundo — tarefas difíceis'},{nome:'max',resumo:'o máximo de raciocínio'},{nome:'ultracode',resumo:'esforço total em código, sem pressa'}]},permissoes:{atual:'auto',opcoes:[{nome:'auto',resumo:'o Primo decide quando pedir licença'},{nome:'livre',resumo:'faz tudo sem perguntar'},{nome:'plano',resumo:'só planeja, não mexe em nada'}]},modo:'agent',provedor:{id:'primo',nome:'Primo',chave_mascarada:''},provedores_chave:[{id:'anthropic',nome:'Anthropic'},{id:'openai',nome:'OpenAI'},{id:'openrouter',nome:'OpenRouter'}],pasta:'~/Primo',conta:{logado:!fora,email:'joel@exemplo.com',plano:'Pro',planoAte:'2026-12-31'}};
1734
1735
  function r(m,t){setTimeout(function(){window.chat(m);},t||120);}
1735
1736
  window.__demo=function(o){
1736
1737
  console.log('post',o);
@@ -1739,7 +1740,7 @@ if(location.search.indexOf('demo')>=0&&!(window.webkit&&window.webkit.messageHan
1739
1740
  r({passo:'Lendo a pasta do projeto'},400);
1740
1741
  r({saida:{comando:o.texto.split(' ')[0],texto:'projeto/\n├── src/\n│ ├── app.js 12 KB\n│ └── util.js 3 KB\n├── public/\n│ └── capa.png 80 KB\n└── package.json\n\n✓ 3 pastas, 4 arquivos em /demo/projeto\n✗ falta o README.md\n'+Array.apply(null,Array(24)).map(function(_,i){return 'linha extra '+(i+1);}).join('\n'),conversa:null,midias:[{achado:'/demo/projeto',caminho:'/demo/projeto',nome:'projeto',tipo:'pasta',url:'',tamanho:0}]}},1200);
1741
1742
  break;
1742
- case 'pronto':r({comandos:[{nome:'/tree',descricao:'árvore do projeto',tipo:'terminal'},{nome:'/read',descricao:'lê um arquivo',tipo:'terminal'},{nome:'/voice nome',descricao:'muda o nome do assistente',tipo:'app',acao:'voz_nome'},{nome:'/nova',descricao:'começa uma conversa nova',tipo:'app',acao:'nova'},{nome:'/modo',descricao:'chat ou agent',tipo:'app',acao:'modo'},{nome:'/copiar',descricao:'copia a última resposta',tipo:'app',acao:'copiar'},{nome:'/sair',descricao:'sai da conta',tipo:'app',acao:'sair_conta'},{nome:'/ajuda',descricao:'mostra todos os comandos',tipo:'app',acao:'ajuda'},{nome:'/effort',descricao:'nível de esforço do Primo',tipo:'app',acao:'effort'},{nome:'/permissoes',descricao:'o que o Primo pode fazer sozinho',tipo:'app',acao:'permissoes'},{nome:'/revisar',descricao:'revisa o código do projeto',tipo:'pedido'},{nome:'/explicar',descricao:'explica o que o projeto faz',tipo:'pedido'}]},300);
1743
+ case 'pronto':r({comandos:[{nome:'/tree',descricao:'árvore do projeto',tipo:'terminal'},{nome:'/read',descricao:'lê um arquivo',tipo:'terminal'},{nome:'/nova',descricao:'começa uma conversa nova',tipo:'app',acao:'nova'},{nome:'/modo',descricao:'chat ou agent',tipo:'app',acao:'modo'},{nome:'/copiar',descricao:'copia a última resposta',tipo:'app',acao:'copiar'},{nome:'/sair',descricao:'sai da conta',tipo:'app',acao:'sair_conta'},{nome:'/ajuda',descricao:'mostra todos os comandos',tipo:'app',acao:'ajuda'},{nome:'/effort',descricao:'nível de esforço do Primo',tipo:'app',acao:'effort'},{nome:'/permissoes',descricao:'o que o Primo pode fazer sozinho',tipo:'app',acao:'permissoes'},{nome:'/revisar',descricao:'revisa o código do projeto',tipo:'pedido'},{nome:'/explicar',descricao:'explica o que o projeto faz',tipo:'pedido'}]},300);
1743
1744
  r({inicio:{logado:!fora,email:fora?'':'joel@exemplo.com',plano:'Pro',planoAte:'2026-12-31',pasta:'~/Primo',modo:'agent'}});break;
1744
1745
  case 'conversas':r({conversas:[{id:'d2',titulo:'Cardápio com fotos',atualizada:'2026-10-18T10:00:00Z'},{id:'d1',titulo:'Botão do WhatsApp no topo',atualizada:'2026-10-19T09:00:00Z'}]});break;
1745
1746
  case 'abrir':if(convs[o.id])r({conversa:convs[o.id]});break;