primocode 11.2.0 → 11.3.1

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.
@@ -6,7 +6,7 @@
6
6
  const fs = require('fs');
7
7
  const path = require('path');
8
8
 
9
- const ICONES = ['folder', 'film', 'palette', 'workflow', 'pen', 'home', 'shield', 'bell', 'sliders'];
9
+ const ICONES = ['folder', 'film', 'palette', 'workflow', 'pen', 'home', 'shield', 'bell', 'sliders', 'code', 'rocket', 'cake', 'store', 'clapperboard', 'megaphone', 'graduation-cap', 'heart', 'camera', 'music'];
10
10
  const COR_PADRAO = '#22d3ee';
11
11
  const hexOk = (h) => /^#[0-9a-f]{6}$/i.test(String(h || ''));
12
12
  const cfg = () => require('./config/config');
@@ -36,6 +36,21 @@ function ramoGit(dir) {
36
36
  } catch { return { git: false, ramo: '' }; }
37
37
  }
38
38
 
39
+ /* O "dono/repo" do remoto origin, lido do .git/config (sem rodar o git). Vazio se não há. */
40
+ function remotoGit(dir) {
41
+ try {
42
+ const cfg = fs.readFileSync(path.join(dir, '.git', 'config'), 'utf8');
43
+ const m = /\[remote "origin"\][^\[]*?url\s*=\s*(\S+)/.exec(cfg);
44
+ const r = m && /[:/]([\w.-]+\/[\w.-]+?)(?:\.git)?$/.exec(m[1]);
45
+ return r ? r[1] : '';
46
+ } catch { return ''; }
47
+ }
48
+
49
+ /* Quantos itens (arquivos e pastas, sem os ocultos) o projeto tem na raiz, e quando mexeram nele por último. */
50
+ function conteudo(dir) {
51
+ try { return { itens: fs.readdirSync(dir).filter((n) => !n.startsWith('.')).length, atualizado: fs.statSync(dir).mtimeMs }; } catch { return { itens: 0, atualizado: null }; }
52
+ }
53
+
39
54
  function resumo(id) {
40
55
  const dir = path.join(raiz(), id);
41
56
  const m = lerMeta(dir);
@@ -43,12 +58,14 @@ function resumo(id) {
43
58
  id, nome: m.nome || id, descricao: m.descricao || '',
44
59
  icone: ICONES.includes(m.icone) ? m.icone : 'folder', cor: hexOk(m.cor) ? m.cor : COR_PADRAO,
45
60
  caminho: dir, arquivado: !!m.arquivado, criado: m.criado || null,
46
- ...ramoGit(dir),
61
+ ...ramoGit(dir), remoto: remotoGit(dir), ...conteudo(dir),
47
62
  };
48
63
  }
49
64
 
50
65
  function listar({ arquivados = false } = {}) {
51
- return cfg().listProjects().map(resumo).filter((p) => arquivados || !p.arquivado);
66
+ /* O mais recente primeiro (como a lista da barra lateral); empate por nome. */
67
+ return cfg().listProjects().map(resumo).filter((p) => arquivados || !p.arquivado)
68
+ .sort((a, b) => String(b.criado || '').localeCompare(String(a.criado || '')) || a.nome.localeCompare(b.nome));
52
69
  }
53
70
 
54
71
  function limpar(c, { criando = false } = {}) {
@@ -246,6 +246,17 @@ function ident(profundidade) {
246
246
  * duas versões do texto, que o `limparParaOModelo` apaga antes de a conversa
247
247
  * seguir. Sem essa limpeza, cada edição mandaria o arquivo inteiro de volta
248
248
  * ao modelo — que acabou de escrevê-lo e não aprende nada relendo. */
249
+ /* +N −M de uma escrita ou edição: o diff de verdade (linhas que entraram e saíram). Arquivo
250
+ novo conta todas as linhas como entradas. Sem as duas versões nem a contagem, null. */
251
+ function contarMudanca(result) {
252
+ if (!result) return null;
253
+ if (typeof result._antes === 'string' && typeof result._depois === 'string') {
254
+ try { return diff.contar(diff.operacoes(result._antes, result._depois)); } catch { return null; }
255
+ }
256
+ if (result.created && Number.isFinite(result.linhas)) return { mais: result.linhas, menos: 0 };
257
+ return null;
258
+ }
259
+
249
260
  function mostrarDiff(result, profundidade) {
250
261
  if (!result || typeof result._antes !== 'string' || typeof result._depois !== 'string') return;
251
262
  const bloco = diff.desenhar(result._antes, result._depois, {
@@ -287,17 +298,24 @@ function logResult(kind, result, profundidade) {
287
298
  }
288
299
 
289
300
  switch (kind) {
290
- case 'write_file':
291
- console.log(p + c.muted(`${result.created ? 'criado' : 'atualizado'} · ${tools.fmtBytes(result.bytes)}`));
301
+ case 'write_file': {
302
+ const k = contarMudanca(result);
303
+ if (k) require('../../tui/ui').registrarArquivo(result.path, k.mais, k.menos);
304
+ console.log(p + c.muted(`${result.created ? 'criado' : 'atualizado'} · ${tools.fmtBytes(result.bytes)}`)
305
+ + (k ? c.muted(' · ') + c.ok(`+${k.mais}`) + ' ' + c.err(`−${k.menos}`) : ''));
292
306
  // Reescrever arquivo que já existia apaga o que estava lá. O
293
307
  // número de bytes não conta isso; o bloco embaixo conta.
294
308
  mostrarDiff(result, profundidade);
295
309
  return;
310
+ }
296
311
  case 'edit_file':
297
312
  case 'multi_edit': {
298
313
  const delta = result.linhasDepois - result.linhasAntes;
299
314
  const sinal = delta > 0 ? c.ok(`+${delta}`) : delta < 0 ? c.err(`${delta}`) : c.muted('±0');
300
- console.log(p + c.muted(`${result.substituicoes} troca${result.substituicoes > 1 ? 's' : ''} · `) + sinal + c.muted(' linhas'));
315
+ const k = contarMudanca(result);
316
+ if (k) require('../../tui/ui').registrarArquivo(result.path, k.mais, k.menos);
317
+ console.log(p + c.muted(`${result.substituicoes} troca${result.substituicoes > 1 ? 's' : ''} · `) + sinal + c.muted(' linhas')
318
+ + (k ? c.muted(' · ') + c.ok(`+${k.mais}`) + ' ' + c.err(`−${k.menos}`) : ''));
301
319
  mostrarDiff(result, profundidade);
302
320
  return;
303
321
  }
@@ -342,6 +360,7 @@ function logResult(kind, result, profundidade) {
342
360
  return;
343
361
  }
344
362
  case 'write_todos': {
363
+ require('../../tui/ui').estadoSessao.todos = result.todos || [];
345
364
  const marca = { completed: c.ok('✔'), in_progress: c.brand('▶'), pending: c.dim('○') };
346
365
  for (const t of result.todos || []) {
347
366
  const texto = t.status === 'completed' ? c.dim(t.content) : c.white(t.content);
@@ -2287,7 +2306,7 @@ async function runActInterno(userPrompt, opts) {
2287
2306
  // arquivo de verdade — a memória era descartada nesse caso, sempre.
2288
2307
  // A raiz de trabalho não é projeto: um memory.md solto em ~/primocode
2289
2308
  // era o que um pedido de vídeo (que trabalha na raiz) deixava para trás.
2290
- if (!subagent && temPasta && require('path').resolve(pastaAtual) !== require('path').resolve(cfg.ROOT_DIR)) {
2309
+ if (!subagent && temPasta && pastaAtual && require('path').resolve(pastaAtual) !== require('path').resolve(cfg.ROOT_DIR)) {
2291
2310
  cfg.appendProjectMemory(pastaAtual, passagemDaParada || `**Pedido:** ${userPrompt.slice(0, 300)}\n**Resultado:** ${finalText.slice(0, 500)}`);
2292
2311
  }
2293
2312
 
package/lib/tool/tools.js CHANGED
@@ -688,6 +688,7 @@ async function despachar(name, args, ctx) {
688
688
  ok: true, created,
689
689
  path: path.relative(projectDir, filePath).replace(/\\/g, '/'),
690
690
  bytes: content.length,
691
+ linhas: content.split('\n').length,
691
692
  ...(created ? {} : { _antes: antes, _depois: content }),
692
693
  };
693
694
  }
@@ -162,11 +162,11 @@ function quebrar(texto, w) {
162
162
  * @returns {{linhas: string[], cursorLinha: number, cursorColuna: number}}
163
163
  */
164
164
  function montar(e) {
165
- const colunas = Math.max(24, Math.min(e.colunas || 80, 200));
165
+ const colunas = Math.max(24, Math.min(e.colunas || 80, 500));
166
166
  /* Na ABERTURA (nada mandado ainda) a caixa é estreita e centrada embaixo
167
167
  do logo, como a home do OpenCode; depois do primeiro pedido ela ocupa a
168
168
  largura toda. `recuo` é a margem da esquerda, que entra em toda linha. */
169
- const largura = e.centro ? Math.min(colunas - 1, 80) : colunas - 1; // a última coluna
169
+ const largura = e.centro ? Math.min(colunas - 1, Math.max(80, Math.min(Math.round(colunas * 0.6), 160))) : colunas - 1; // a última coluna
170
170
  // fica livre: escrever nela faz o terminal quebrar a linha
171
171
  const recuo = e.centro ? Math.max(0, Math.floor((colunas - largura) / 2)) : 0;
172
172
  const w = Math.max(8, largura - PREFIXO);
@@ -237,15 +237,20 @@ function montar(e) {
237
237
  // ── a linha de baixo: pasta à esquerda, atalhos à direita ────────────
238
238
  if (st.linhas) {
239
239
  for (const linha of st.linhas) out.push(' ' + truncate(linha, largura - 2));
240
- } else {
240
+ } else if (!e.centro) {
241
241
  /* Uma linha vazia entre o painel e o rodapé. Colado, o rodapé parecia
242
242
  SOBREPOSTO ao painel em terminais de entrelinha apertada (o Orca):
243
243
  o fundo do painel encostava nas letras da pasta. */
244
244
  out.push('');
245
245
  const esq = st.esquerda || '';
246
- const dir = st.direita || '';
246
+ let dir = st.direita || '';
247
+ /* Sem espaço para tudo, ficam os ATALHOS (é o que a pessoa não sabe que existe) e a pasta
248
+ encolhe; antes disto a linha perdia os atalhos e ficava só a pasta. */
249
+ if (largura - 2 - visibleWidth(esq) - visibleWidth(dir) < 2 && st.atalhos) dir = st.atalhos;
247
250
  const sobra = largura - 2 - visibleWidth(esq) - visibleWidth(dir);
248
- out.push(' ' + (sobra >= 2 ? esq + ' '.repeat(sobra) + dir : truncate(esq, largura - 2)));
251
+ out.push(' ' + (sobra >= 2 ? esq + ' '.repeat(sobra) + dir
252
+ : visibleWidth(dir) + 14 <= largura - 2 ? truncate(esq, largura - 2 - visibleWidth(dir) - 2) + ' ' + dir
253
+ : truncate(esq, largura - 2)));
249
254
  }
250
255
 
251
256
  // ── o menu dos comandos ──────────────────────────────────────────────
@@ -296,7 +301,7 @@ function montar(e) {
296
301
 
297
302
  /** O pedido que foi mandado, como fica no histórico da tela: um painel. */
298
303
  function pedidoNaTela(texto, colunas, guardados = []) {
299
- const largura = Math.max(24, Math.min(colunas || 80, 200)) - 1;
304
+ const largura = Math.max(24, Math.min(colunas || 80, 500)) - 1;
300
305
  const barra = `${COR.barra}┃${COR.reset}`;
301
306
  const linhas = quebrar(texto, Math.max(8, largura - PREFIXO))
302
307
  .map((l) => painel(barra + ' ' + pintarEtiquetas(c.white(l.texto)), largura));
@@ -338,6 +343,12 @@ class Entrada extends EventEmitter {
338
343
  this.cheia = !!(op.telaCheia && this.tty);
339
344
  this.conversa = [];
340
345
  this.rolagem = 0;
346
+ // Diálogo aberto (paleta, modelo, agente, sessões, tema), o aviso do canto e a barra da direita.
347
+ this.dialogo = null;
348
+ this.toastAtual = null;
349
+ this.lateralOculta = false;
350
+ this._lider = false;
351
+ this._modoCor = tela.modoDeCor();
341
352
  }
342
353
 
343
354
  // ── ciclo de vida ────────────────────────────────────────────────────
@@ -360,7 +371,7 @@ class Entrada extends EventEmitter {
360
371
  this._aoTeclar = (ch, key) => this._tecla(ch, key);
361
372
  process.stdin.on('keypress', this._aoTeclar);
362
373
 
363
- this._aoRedimensionar = () => { if (this.cheia) { this._quadroAnterior = null; this._pintar(); } else if (this.desenhado) this._redesenhar(); };
374
+ this._aoRedimensionar = () => { if (this.cheia) { if (this._naTela) this.escreverCru('\x1b[2J'); this._quadroAnterior = null; this._pintar(); } else if (this.desenhado) this._redesenhar(); };
364
375
  process.stdout.on('resize', this._aoRedimensionar);
365
376
 
366
377
  /* Todo `console.log` do programa passa a apagar a caixa antes de
@@ -384,6 +395,7 @@ class Entrada extends EventEmitter {
384
395
  return true;
385
396
  }
386
397
  eu._apagar();
398
+ if (eu._modoCor === 'none') texto = tela.adaptarCor(texto, 'none');
387
399
  const r = eu._stdoutOriginal.call(this, texto, ...resto);
388
400
  /* ── O TEXTO QUE CHEGA EM PEDAÇOS ────────────────────────────
389
401
  A resposta do chat sai em fluxo: dezenas de `write` pequenos,
@@ -495,7 +507,7 @@ class Entrada extends EventEmitter {
495
507
  this._colunasReais = process.stdout.columns || 80;
496
508
  Object.defineProperty(process.stdout, 'columns', {
497
509
  configurable: true,
498
- get() { return eu._naTela ? tela.larguraDaConversa(eu._colunasReais) : eu._colunasReais; },
510
+ get() { return eu._naTela ? tela.larguraDaConversa(eu._colunasReais, eu.lateralOculta) : eu._colunasReais; },
499
511
  set(v) { eu._colunasReais = v; }, // o Node escreve aqui quando a janela muda
500
512
  });
501
513
  }
@@ -530,16 +542,15 @@ class Entrada extends EventEmitter {
530
542
  const quadro = Date.now() / 140;
531
543
  /* O topo da barra da direita: o logo do PrimoCode (que anima enquanto
532
544
  ele trabalha) e o ESTADO — trabalhando, há quanto tempo, no quê. */
533
- const topo = this.op.logoAnimado ? this.op.logoAnimado(quadro, trabalhando) : [];
534
- topo.push('');
545
+ const pe = this.op.logoAnimado ? this.op.logoAnimado(quadro, trabalhando) : [];
535
546
  if (trabalhando) {
536
547
  const giro = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'][Math.floor(quadro) % 10];
537
- topo.push(c.warn(giro + ' Trabalhando') + (this._desde ? c.dim(` · ${Math.floor((Date.now() - this._desde) / 1000)}s`) : ''));
548
+ pe.push(c.warn(giro + ' Trabalhando') + (this._desde ? c.dim(` · ${Math.floor((Date.now() - this._desde) / 1000)}s`) : ''));
538
549
  const agora = this.op.agora ? this.op.agora() : '';
539
- if (agora) topo.push(c.muted(agora));
540
- if (this.spinnerTexto) topo.push(c.dim(stripAnsiLocal(this.spinnerTexto).replace(/^[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏]\s*/, '')));
550
+ const detalhe = agora || (this.spinnerTexto ? stripAnsiLocal(this.spinnerTexto).replace(/^[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏]\s*/, '') : '');
551
+ if (detalhe) pe.push(c.dim(detalhe));
541
552
  } else {
542
- topo.push(c.brand('● ') + c.muted('Pronto') + c.dim(' · esperando você'));
553
+ pe.push(c.brand('● ') + c.muted('Pronto') + c.dim(' · esperando você'));
543
554
  }
544
555
  if (trabalhando && !this._animando) {
545
556
  // Enquanto trabalha, a barra se redesenha sozinha (o logo anima
@@ -554,10 +565,16 @@ class Entrada extends EventEmitter {
554
565
  rolagem: this.rolagem,
555
566
  home: !this.mandou && !conversa && !this.painelTexto,
556
567
  caixa: (colunas, centro) => montar({ ...this._estado(), colunas, centro, menuAcima: true }),
557
- lateral: { titulo: this.titulo, topo, ...(this.op.lateral ? this.op.lateral() : {}) },
568
+ lateral: { titulo: this.titulo, pe, ...(this.op.lateral ? this.op.lateral() : {}) },
569
+ lateralOculta: this.lateralOculta,
570
+ dialogo: this._dialogoParaTela(),
571
+ toast: this.toastAtual,
558
572
  painel: this.painelTexto,
559
573
  versao: this.op.versao || '',
574
+ plano: this.op.plano ? this.op.plano() : '',
575
+ comandosHome: this.op.comandosHome,
560
576
  logo: this.op.logo,
577
+ logoSimples: this.op.logoSimples,
561
578
  pasta: this.op.pasta ? this.op.pasta() : '',
562
579
  });
563
580
  }
@@ -575,7 +592,7 @@ class Entrada extends EventEmitter {
575
592
  let corpo = '';
576
593
  q.linhas.forEach((l, i) => {
577
594
  if (mesmoTamanho && antes.linhas[i] === l) return;
578
- corpo += `\x1b[${i + 1};1H${l}\x1b[0m\x1b[K`;
595
+ corpo += `\x1b[${i + 1};1H${tela.adaptarCor(l, this._modoCor)}\x1b[0m\x1b[K`;
579
596
  });
580
597
  const mesmoCursor = antes && antes.cursor.linha === q.cursor.linha && antes.cursor.coluna === q.cursor.coluna;
581
598
  this._quadroAnterior = { linhas: q.linhas, cursor: q.cursor };
@@ -816,6 +833,9 @@ class Entrada extends EventEmitter {
816
833
  return;
817
834
  }
818
835
 
836
+ if (this.dialogo) return this._teclaDialogo(ch, key);
837
+ if (this._lider) { this._lider = false; if (key && !key.ctrl && key.name !== 'escape') return this._atalhoDoLider(key); }
838
+
819
839
  if (!key) { if (ch) this.write(ch); return; }
820
840
 
821
841
  // Esc Esc e o resto das teclas de fora: quem escuta decide. A caixa
@@ -935,16 +955,121 @@ class Entrada extends EventEmitter {
935
955
  if (this.cheia) { this.conversa = []; this.rolagem = 0; return this._pintar(); }
936
956
  this._apagar(); this.escreverCru('\x1b[2J\x1b[H'); return this._desenhar();
937
957
  case 'j': return this._novaLinha();
938
- // Ctrl+P abre os comandos, como no OpenCode: o mesmo menu do "/".
958
+ // Ctrl+P abre a paleta de comandos, como no OpenCode (a lista com busca no meio da tela).
939
959
  case 'p':
960
+ if (this.cheia && this.op.paleta) return this.abrirDialogo({ titulo: 'Comandos', itens: this.op.paleta(), aoEscolher: (it) => this._rodarItem(it) });
940
961
  if (this.line && !this.line.startsWith('/')) return;
941
962
  if (!this.line) { this.line = '/'; this.cursor = 1; }
942
963
  this._filtrarMenu();
943
964
  return this._redesenhar();
965
+ // Ctrl+X é o "líder" do OpenCode: a tecla seguinte escolhe (n nova, m modelo, a agente, l sessões, t tema, b barra).
966
+ case 'x':
967
+ if (!this.cheia) return;
968
+ this._lider = true;
969
+ clearTimeout(this._liderTempo);
970
+ this._liderTempo = setTimeout(() => { this._lider = false; }, 2500);
971
+ if (this._liderTempo.unref) this._liderTempo.unref();
972
+ this.toast('ctrl+x n nova · m modo · a agentes · l sessões · t tema · b barra', 'info', 2400);
973
+ return;
944
974
  default: return;
945
975
  }
946
976
  }
947
977
 
978
+ // ── diálogos, paleta e avisos (o `dialog-*` e o toast do OpenCode) ────
979
+
980
+ /**
981
+ * Abre uma lista com busca no meio da tela.
982
+ * @param {{titulo:string, itens:Array<{nome,desc,tecla,secao}>|(()=>Array), aoEscolher:(item:object)=>void}} def
983
+ */
984
+ abrirDialogo(def) {
985
+ this.menu = null;
986
+ this.dialogo = { ...def, filtro: '', sel: 0 };
987
+ this._pintar();
988
+ }
989
+
990
+ /** Volta à home: conversa, título e tarefas zeradas (o /clear e o ctrl+x n). */
991
+ novaConversa() {
992
+ this.conversa = []; this.rolagem = 0; this.titulo = ''; this.mandou = false;
993
+ this._quadroAnterior = null;
994
+ if (this.cheia) this._pintar();
995
+ }
996
+
997
+ fecharDialogo() { this.dialogo = null; this._quadroAnterior = null; this._pintar(); }
998
+
999
+ _itensDoDialogo() {
1000
+ const d = this.dialogo;
1001
+ const todos = typeof d.itens === 'function' ? d.itens() : (d.itens || []);
1002
+ const sem = (t) => String(t || '').normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase();
1003
+ const q = sem(d.filtro).trim();
1004
+ if (!q) return todos;
1005
+ return todos.filter((it) => q.split(/\s+/).every((p) => sem(`${it.nome} ${stripAnsiLocal(it.desc || '')} ${it.secao || ''}`).includes(p)));
1006
+ }
1007
+
1008
+ _dialogoParaTela() {
1009
+ if (!this.dialogo) return null;
1010
+ const itens = this._itensDoDialogo();
1011
+ this.dialogo.sel = Math.max(0, Math.min(this.dialogo.sel, itens.length - 1));
1012
+ return { titulo: this.dialogo.titulo, filtro: this.dialogo.filtro, itens, sel: this.dialogo.sel, vazio: this.dialogo.vazio };
1013
+ }
1014
+
1015
+ _teclaDialogo(ch, key) {
1016
+ const d = this.dialogo;
1017
+ const seq = (key && key.sequence) || ch || '';
1018
+ const n = this._itensDoDialogo().length;
1019
+ const mover = (p) => { d.sel = Math.max(0, Math.min(n - 1, d.sel + p)); this._pintar(); };
1020
+ if (key && key.name === 'escape') return this.fecharDialogo();
1021
+ if (key && key.ctrl && key.name === 'c') return this.fecharDialogo();
1022
+ if (key && (key.name === 'up' || (key.ctrl && key.name === 'p'))) return mover(-1);
1023
+ if (key && (key.name === 'down' || (key.ctrl && key.name === 'n'))) return mover(1);
1024
+ if (key && key.name === 'pageup') return mover(-8);
1025
+ if (key && key.name === 'pagedown') return mover(8);
1026
+ if (key && (key.name === 'return' || key.name === 'enter')) {
1027
+ const item = this._itensDoDialogo()[d.sel];
1028
+ this.dialogo = null; this._quadroAnterior = null;
1029
+ this._pintar();
1030
+ if (item && d.aoEscolher) d.aoEscolher(item, this);
1031
+ return;
1032
+ }
1033
+ if (key && key.name === 'backspace') { d.filtro = d.filtro.slice(0, -1); d.sel = 0; return this._pintar(); }
1034
+ if (seq && !(key && (key.ctrl || key.meta)) && seq >= ' ' && seq !== '\x7f') { d.filtro += seq; d.sel = 0; this._pintar(); }
1035
+ }
1036
+
1037
+ /** Roda um item da paleta: comando de barra vai como se tivesse sido digitado; função é chamada. */
1038
+ _rodarItem(it) {
1039
+ if (!it) return;
1040
+ if (typeof it.acao === 'function') return it.acao(this);
1041
+ const cmd = it.cmd || it.nome;
1042
+ if (it.preencher) { this.line = cmd; this.cursor = cmd.length; return this._redesenhar(); }
1043
+ this.line = cmd; this.cursor = cmd.length;
1044
+ this._enviar();
1045
+ }
1046
+
1047
+ /** A tecla depois do ctrl+x. */
1048
+ _atalhoDoLider(key) {
1049
+ const d = this.op.dialogos || {};
1050
+ const abrir = (nome) => { if (d[nome]) Promise.resolve(d[nome](this)).then((def) => { if (def) this.abrirDialogo(def); }); };
1051
+ switch (key.name) {
1052
+ case 'n': return this._rodarItem({ cmd: '/clear' });
1053
+ case 'p': return this._rodarItem({ cmd: '/projects' });
1054
+ case 's': return this._rodarItem({ cmd: '/estudio' });
1055
+ case 'm': return abrir('modelo');
1056
+ case 'a': return abrir('agente');
1057
+ case 'l': return abrir('sessoes');
1058
+ case 't': return abrir('tema');
1059
+ case 'b': this.lateralOculta = !this.lateralOculta; this._quadroAnterior = null; return this._pintar();
1060
+ default: return;
1061
+ }
1062
+ }
1063
+
1064
+ /** Um aviso no canto de cima por alguns segundos. tipo: info · ok · aviso · erro. */
1065
+ toast(texto, tipo = 'info', ms = 3000) {
1066
+ this.toastAtual = { texto: String(texto || ''), tipo };
1067
+ clearTimeout(this._toastTempo);
1068
+ this._toastTempo = setTimeout(() => { this.toastAtual = null; this._quadroAnterior = null; this._agendar(); }, ms);
1069
+ if (this._toastTempo.unref) this._toastTempo.unref();
1070
+ if (this.cheia) this._agendar();
1071
+ }
1072
+
948
1073
  _novaLinha() {
949
1074
  this.line = this.line.slice(0, this.cursor) + '\n' + this.line.slice(this.cursor);
950
1075
  this.cursor++;
@@ -1010,7 +1135,7 @@ class Entrada extends EventEmitter {
1010
1135
  // painel da caixa, com a barra, e um respiro em cima e embaixo.
1011
1136
  if (this.cheia) {
1012
1137
  const W = this._colunasReais || 80;
1013
- const mainW = W >= tela.LATERAL_MIN ? W - tela.LATERAL_W : W;
1138
+ const mainW = W - tela.larguraLateral(W, this.lateralOculta);
1014
1139
  this.rolagem = 0;
1015
1140
  /* Uma linha em branco antes e outra depois do pedido: cada
1016
1141
  troca (pedido, resposta) é um bloco separado na tela. */