primocode 7.15.0 → 7.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/primocode.js +139 -51
- package/lib/act.js +10 -15
- package/lib/catalogo.js +1 -0
- package/lib/config.js +5 -2
- package/lib/desktop.js +4 -4
- package/lib/effort.js +51 -0
- package/lib/referencia.js +287 -0
- package/lib/tools.js +10 -0
- package/lib/ui.js +6 -6
- package/package.json +2 -2
- package/lib/streaming.js +0 -661
package/bin/primocode.js
CHANGED
|
@@ -22,7 +22,7 @@ const cfg = require('../lib/config');
|
|
|
22
22
|
const { runAct, interromper } = require('../lib/act');
|
|
23
23
|
const pasta = require('../lib/pasta');
|
|
24
24
|
const catalogo = require('../lib/catalogo');
|
|
25
|
-
const
|
|
25
|
+
const effort = require('../lib/effort');
|
|
26
26
|
|
|
27
27
|
// A pasta de onde o comando foi dado. Não é mais onde ele trabalha (o padrão é
|
|
28
28
|
// ~/primocode), mas continua servindo de ponto de partida para achar pasta que
|
|
@@ -48,7 +48,7 @@ ${c.bold('Comandos')}
|
|
|
48
48
|
${c.brand('/dir')} <caminho> trabalha numa pasta que já existe (onde você quiser)
|
|
49
49
|
${c.brand('/project')} lista projetos ou cria em ~/primocode: /project <nome>
|
|
50
50
|
${c.brand('/mode')} /mode primocode|chat|auto
|
|
51
|
-
${c.brand('/
|
|
51
|
+
${c.brand('/effort')} esforço do agente — do rápido ao mais esperto
|
|
52
52
|
${c.brand('/attach')} <arquivo> anexa arquivo ao próximo pedido
|
|
53
53
|
${c.brand('/files')} lista anexos ${c.brand('/detach')} remove
|
|
54
54
|
${c.brand('/tudo')} lista tudo que ele sabe fazer
|
|
@@ -56,7 +56,6 @@ ${c.bold('Comandos')}
|
|
|
56
56
|
${c.brand('/read')} <arquivo> mostra um arquivo
|
|
57
57
|
${c.brand('/run')} <comando> executa no shell do projeto
|
|
58
58
|
${c.brand('/open')} <arquivo|url> abre no navegador
|
|
59
|
-
${c.brand('/streaming')} painel ao vivo — veja o agente mexendo no computador
|
|
60
59
|
${c.brand('/status')} checa o servidor
|
|
61
60
|
${c.brand('/name')} <nome> como o Primo te chama
|
|
62
61
|
${c.brand('/mascot')} <estilo> mascote: primo|fox|cat
|
|
@@ -80,14 +79,14 @@ ${c.bold('Onde ele trabalha')}
|
|
|
80
79
|
|
|
81
80
|
// ── Autopreenchimento dos comandos (estilo Claude Code) ────────────────
|
|
82
81
|
// Metade do comando já serve: Tab completa, e um prefixo único executa
|
|
83
|
-
// direto — "/
|
|
82
|
+
// direto — "/eff" vira "/effort" sozinho. A lista é só a dos nomes
|
|
84
83
|
// canônicos; os apelidos (/cd, /stream…) continuam valendo por extenso.
|
|
85
84
|
const SLASH_COMANDOS = [
|
|
86
85
|
['/help', 'todos os comandos'],
|
|
87
86
|
['/project', 'lista/cria projetos em ~/primocode'],
|
|
88
87
|
['/dir', 'trabalhar numa pasta sua'],
|
|
89
88
|
['/mode', 'primocode | chat | auto'],
|
|
90
|
-
['/
|
|
89
|
+
['/effort', 'esforço: low → ultracode (←/→ para ajustar)'],
|
|
91
90
|
['/attach', 'anexa arquivo ao pedido'],
|
|
92
91
|
['/detach', 'remove os anexos'],
|
|
93
92
|
['/files', 'lista os anexos'],
|
|
@@ -96,7 +95,6 @@ const SLASH_COMANDOS = [
|
|
|
96
95
|
['/read', 'mostra um arquivo'],
|
|
97
96
|
['/run', 'roda um comando'],
|
|
98
97
|
['/open', 'abre no navegador'],
|
|
99
|
-
['/streaming', 'painel ao vivo — vídeo dele trabalhando'],
|
|
100
98
|
['/status', 'checa o servidor'],
|
|
101
99
|
['/name', 'como ele te chama'],
|
|
102
100
|
['/mascot', 'primo | fox | cat'],
|
|
@@ -117,12 +115,111 @@ function completarSlash(line) {
|
|
|
117
115
|
return [acertos, line];
|
|
118
116
|
}
|
|
119
117
|
|
|
118
|
+
// ── /effort: o slider de esforço (estilo Claude Code) ──────────────────
|
|
119
|
+
// Régua com os níveis, o marcador ▲ embaixo do escolhido, "Faster" à
|
|
120
|
+
// esquerda e "Smarter" à direita. ←/→ move, Enter confirma, Esc cancela.
|
|
121
|
+
|
|
122
|
+
function desenharEffort(idx) {
|
|
123
|
+
const cols = Math.max(52, Math.min(process.stdout.columns || 100, 104));
|
|
124
|
+
const regua = '▔'.repeat(cols);
|
|
125
|
+
const linhas = [];
|
|
126
|
+
|
|
127
|
+
// Cada nível ocupa uma coluna própria; o ultracode fica destacado no fim.
|
|
128
|
+
const rotulos = effort.NOMES;
|
|
129
|
+
const larguras = rotulos.map((n) => Math.max(n.length + 3, 9));
|
|
130
|
+
const posicoes = [];
|
|
131
|
+
let x = 23; // recuo da régua, igual ao do mockup
|
|
132
|
+
for (let i = 0; i < rotulos.length; i++) {
|
|
133
|
+
posicoes.push(x + Math.floor(larguras[i] / 2));
|
|
134
|
+
x += larguras[i];
|
|
135
|
+
}
|
|
136
|
+
const fimRegua = x;
|
|
137
|
+
|
|
138
|
+
linhas.push(c.dim(regua));
|
|
139
|
+
linhas.push(' ' + c.bold(c.white('Effort')));
|
|
140
|
+
linhas.push('');
|
|
141
|
+
|
|
142
|
+
// Faster ....▲.... Smarter — a trilha, com o marcador na posição atual.
|
|
143
|
+
const marca = posicoes[idx];
|
|
144
|
+
let trilha = '';
|
|
145
|
+
for (let k = 23; k < fimRegua; k++) {
|
|
146
|
+
if (k === marca) trilha += c.brand('▲');
|
|
147
|
+
else if (k === fimRegua - 12) trilha += c.dim('┆'); // ultracode é um degrau à parte
|
|
148
|
+
else trilha += c.dim(k < marca ? '─' : '─');
|
|
149
|
+
}
|
|
150
|
+
linhas.push(' ' + c.muted('Faster') + ' '.repeat(Math.max(1, fimRegua - 23 - 6 - 7)) + c.muted('Smarter'));
|
|
151
|
+
linhas.push(' ' + trilha);
|
|
152
|
+
|
|
153
|
+
// Os nomes, o escolhido aceso.
|
|
154
|
+
let linhaNomes = ' '.repeat(23);
|
|
155
|
+
for (let i = 0; i < rotulos.length; i++) {
|
|
156
|
+
const nome = rotulos[i];
|
|
157
|
+
const espaco = larguras[i];
|
|
158
|
+
const pad = Math.floor((espaco - nome.length) / 2);
|
|
159
|
+
const texto = i === idx ? c.bold(c.brand(nome)) : c.muted(nome);
|
|
160
|
+
linhaNomes += ' '.repeat(pad) + texto + ' '.repeat(espaco - nome.length - pad);
|
|
161
|
+
}
|
|
162
|
+
linhas.push(linhaNomes);
|
|
163
|
+
|
|
164
|
+
const nivel = effort.nivelDe(rotulos[idx]);
|
|
165
|
+
linhas.push(' ' + c.dim(nivel.resumo));
|
|
166
|
+
linhas.push('');
|
|
167
|
+
linhas.push(' ' + c.muted('←/→ ajusta · Enter confirma · Esc cancela'));
|
|
168
|
+
return linhas.join('\n');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Desenha, escuta as setas, e devolve o nome escolhido (ou null se cancelou).
|
|
172
|
+
function sliderEffort(atual) {
|
|
173
|
+
return new Promise((resolve) => {
|
|
174
|
+
if (!process.stdin.isTTY) { // sem terminal (pipe, CI) não há slider
|
|
175
|
+
console.log(c.muted(` Esforço atual: ${effort.normalizar(atual)}. Use /effort <${effort.NOMES.join('|')}>.`));
|
|
176
|
+
return resolve(null);
|
|
177
|
+
}
|
|
178
|
+
let idx = effort.indiceDe(atual);
|
|
179
|
+
const ALTURA = 9; // linhas que o desenho ocupa — para redesenhar por cima
|
|
180
|
+
|
|
181
|
+
const pintar = (primeira) => {
|
|
182
|
+
if (!primeira) process.stdout.write(`\x1b[${ALTURA}A\r`);
|
|
183
|
+
process.stdout.write(desenharEffort(idx).split('\n').map((l) => '\x1b[2K' + l).join('\n') + '\n');
|
|
184
|
+
};
|
|
185
|
+
pintar(true);
|
|
186
|
+
|
|
187
|
+
const aoTeclar = (ch, key) => {
|
|
188
|
+
if (!key) return;
|
|
189
|
+
if (key.name === 'right' || key.name === 'l') { idx = Math.min(effort.NOMES.length - 1, idx + 1); pintar(false); return; }
|
|
190
|
+
if (key.name === 'left' || key.name === 'h') { idx = Math.max(0, idx - 1); pintar(false); return; }
|
|
191
|
+
if (key.name === 'return' || key.name === 'enter') { encerrar(effort.NOMES[idx]); return; }
|
|
192
|
+
if (key.name === 'escape' || (key.ctrl && key.name === 'c')) { encerrar(null); return; }
|
|
193
|
+
};
|
|
194
|
+
const encerrar = (valor) => {
|
|
195
|
+
process.stdin.removeListener('keypress', aoTeclar);
|
|
196
|
+
sliderAtivo = false;
|
|
197
|
+
resolve(valor);
|
|
198
|
+
};
|
|
199
|
+
sliderAtivo = true;
|
|
200
|
+
process.stdin.on('keypress', aoTeclar);
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function aplicarEffort(nome) {
|
|
205
|
+
const nivel = effort.nivelDe(nome);
|
|
206
|
+
state.config.effort = nivel.nome;
|
|
207
|
+
state.config.model = nivel.model;
|
|
208
|
+
cfg.saveConfig({ effort: nivel.nome, model: nivel.model });
|
|
209
|
+
console.log(c.ok(`Esforço: ${nivel.nome}`) + c.muted(` ${nivel.resumo}`));
|
|
210
|
+
if (nivel.pesquisa) console.log(c.dim(' Pesquisa de referência ligada: ele consulta docs e exemplos antes de escrever.'));
|
|
211
|
+
refreshPrompt();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Enquanto o slider está no ar, o fantasma do autocompletar não desenha.
|
|
215
|
+
let sliderAtivo = false;
|
|
216
|
+
|
|
120
217
|
function mostrarMenuSlash() {
|
|
121
218
|
console.log('');
|
|
122
219
|
for (const [nome, desc] of SLASH_COMANDOS) {
|
|
123
220
|
console.log(` ${c.brand(ui.padRight(nome, 12))}${c.muted(desc)}`);
|
|
124
221
|
}
|
|
125
|
-
console.log(c.dim('\n Tab completa · metade do comando já serve: /
|
|
222
|
+
console.log(c.dim('\n Tab completa · metade do comando já serve: /eff vira /effort'));
|
|
126
223
|
}
|
|
127
224
|
|
|
128
225
|
let rl = null;
|
|
@@ -309,6 +406,7 @@ async function sendPrompt(userText) {
|
|
|
309
406
|
const r = await runAct(prompt, {
|
|
310
407
|
server: state.config.server,
|
|
311
408
|
model: state.config.model,
|
|
409
|
+
effort: state.config.effort,
|
|
312
410
|
token: state.config.token,
|
|
313
411
|
history: state.history,
|
|
314
412
|
projectDir: plan.dir,
|
|
@@ -464,24 +562,38 @@ async function handleSlash(text) {
|
|
|
464
562
|
break;
|
|
465
563
|
}
|
|
466
564
|
|
|
565
|
+
// O /model virou /effort: você escolhe o ESFORÇO num slider (←/→,
|
|
566
|
+
// Enter confirma, Esc cancela) e o modelo sai daí. Com argumento
|
|
567
|
+
// ("/effort high") aplica direto, para quem já sabe o que quer.
|
|
568
|
+
case 'effort': case 'esforco': case 'esforço': {
|
|
569
|
+
const pedido = rest.toLowerCase().trim();
|
|
570
|
+
if (pedido) {
|
|
571
|
+
if (!effort.NOMES.includes(pedido)) {
|
|
572
|
+
console.log(c.err(`Esforço inválido. Use: ${effort.NOMES.join(' | ')}`));
|
|
573
|
+
break;
|
|
574
|
+
}
|
|
575
|
+
aplicarEffort(pedido);
|
|
576
|
+
break;
|
|
577
|
+
}
|
|
578
|
+
const escolhido = await sliderEffort(state.config.effort);
|
|
579
|
+
if (escolhido) aplicarEffort(escolhido);
|
|
580
|
+
else console.log(c.muted(' Nada mudou.'));
|
|
581
|
+
break;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Continua existindo para quem fixa um ID do OpenRouter na unha; o
|
|
585
|
+
// caminho normal é o /effort.
|
|
467
586
|
case 'model': {
|
|
468
587
|
const m = rest.toLowerCase();
|
|
469
|
-
|
|
470
|
-
const nomes = {
|
|
471
|
-
top: 'o mais capaz — 550B, 1 milhão de tokens de contexto',
|
|
472
|
-
main: 'o padrão — agente de código, o melhor em tarefa de terminal',
|
|
473
|
-
fast: 'o mais rápido — resposta curta, alta vazão',
|
|
474
|
-
};
|
|
475
|
-
if (nomes[m]) {
|
|
476
|
-
state.config.model = m;
|
|
477
|
-
cfg.saveConfig({ model: m });
|
|
478
|
-
console.log(c.ok(`Modelo: ${m}`) + c.muted(` ${nomes[m]}`) + c.dim(' · grátis'));
|
|
479
|
-
} else if (m.includes('/')) {
|
|
480
|
-
// ID literal do OpenRouter, para quem quer sair dos três.
|
|
588
|
+
if (m.includes('/')) {
|
|
481
589
|
state.config.model = m;
|
|
482
590
|
cfg.saveConfig({ model: m });
|
|
483
|
-
console.log(c.ok(`Modelo: ${m}`) + c.muted(' precisa aceitar tools
|
|
484
|
-
|
|
591
|
+
console.log(c.ok(`Modelo fixo: ${m}`) + c.muted(' precisa aceitar tools no modo primocode'));
|
|
592
|
+
console.log(c.dim(' Para voltar ao normal, use /effort.'));
|
|
593
|
+
} else {
|
|
594
|
+
console.log(c.muted(' O modelo agora vem do esforço. Use ') + c.brand('/effort') + c.muted('.'));
|
|
595
|
+
console.log(c.dim(` Agora: ${state.config.effort} → modelo ${effort.modeloDe(state.config.effort)}`));
|
|
596
|
+
}
|
|
485
597
|
break;
|
|
486
598
|
}
|
|
487
599
|
|
|
@@ -552,33 +664,6 @@ async function handleSlash(text) {
|
|
|
552
664
|
break;
|
|
553
665
|
}
|
|
554
666
|
|
|
555
|
-
// Painel ao vivo: um servidorzinho local mostra cada ação do agente na
|
|
556
|
-
// tela + a foto tirada logo depois dela. Liga/desliga; /streaming on|off
|
|
557
|
-
// também funciona.
|
|
558
|
-
case 'streaming': case 'stream': case 'live': {
|
|
559
|
-
const pedido = rest.toLowerCase();
|
|
560
|
-
const ligar = pedido === 'on' ? true : pedido === 'off' ? false : !streaming.isOn();
|
|
561
|
-
|
|
562
|
-
if (!ligar) {
|
|
563
|
-
if (streaming.isOn()) { streaming.stop(); console.log(c.ok('Streaming desligado.')); }
|
|
564
|
-
else console.log(c.muted('O streaming já estava desligado.'));
|
|
565
|
-
break;
|
|
566
|
-
}
|
|
567
|
-
if (streaming.isOn()) {
|
|
568
|
-
console.log(c.muted(`Já está ligado: ${streaming.url()}`));
|
|
569
|
-
break;
|
|
570
|
-
}
|
|
571
|
-
try {
|
|
572
|
-
const url = await streaming.start();
|
|
573
|
-
console.log(c.ok('Streaming ligado: ') + c.white(url));
|
|
574
|
-
console.log(c.muted(' Dê uma instrução e veja cada passo dele aí, ao vivo. /streaming de novo desliga.'));
|
|
575
|
-
await tools.openInBrowser(url);
|
|
576
|
-
} catch (e) {
|
|
577
|
-
console.log(c.err(`Não consegui ligar o streaming: ${e.message}`));
|
|
578
|
-
}
|
|
579
|
-
break;
|
|
580
|
-
}
|
|
581
|
-
|
|
582
667
|
case 'name': {
|
|
583
668
|
if (!rest) { console.log(c.muted(` Nome atual: ${state.config.userName || '(nenhum)'}`)); break; }
|
|
584
669
|
state.config.userName = rest;
|
|
@@ -613,7 +698,8 @@ async function handleSlash(text) {
|
|
|
613
698
|
console.log('');
|
|
614
699
|
console.log(` ${c.muted('nome')} ${c.white(state.config.userName || '(nenhum)')}`);
|
|
615
700
|
console.log(` ${c.muted('mascote')} ${c.white(state.config.mascot || 'primo')}`);
|
|
616
|
-
console.log(` ${c.muted('
|
|
701
|
+
console.log(` ${c.muted('esforço')} ${c.white(state.config.effort)}${c.dim(' · /effort muda')}`);
|
|
702
|
+
console.log(` ${c.muted('modelo')} ${c.white(state.config.model)}${c.dim(' (vem do esforço)')}`);
|
|
617
703
|
console.log(` ${c.muted('modo')} ${c.white(state.mode)}`);
|
|
618
704
|
console.log(` ${c.muted('servidor')} ${c.dim(state.config.server)}`);
|
|
619
705
|
console.log(c.muted(`\n ${cfg.CONFIG_FILE}`));
|
|
@@ -654,7 +740,7 @@ async function handleSlash(text) {
|
|
|
654
740
|
case 'exit': case 'quit': if (rl) rl.close(); break;
|
|
655
741
|
default: {
|
|
656
742
|
// "/" sozinho abre o menu; um prefixo único autopreenche e executa
|
|
657
|
-
// ("/
|
|
743
|
+
// ("/eff" → /effort); prefixo ambíguo mostra as opções.
|
|
658
744
|
if (!cmd) { mostrarMenuSlash(); break; }
|
|
659
745
|
const casam = SLASH_COMANDOS.map(([nome]) => nome.slice(1)).filter((n) => n.startsWith(cmd.toLowerCase()));
|
|
660
746
|
if (casam.length === 1) {
|
|
@@ -733,6 +819,7 @@ async function main() {
|
|
|
733
819
|
await runAct(fullPrompt, {
|
|
734
820
|
server: state.config.server,
|
|
735
821
|
model: state.config.model,
|
|
822
|
+
effort: state.config.effort,
|
|
736
823
|
token: state.config.token,
|
|
737
824
|
history: [],
|
|
738
825
|
projectDir: plan.dir,
|
|
@@ -766,7 +853,7 @@ async function main() {
|
|
|
766
853
|
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
767
854
|
let fantasma = '';
|
|
768
855
|
const desenharFantasma = () => {
|
|
769
|
-
if (busy || pendingAsk || !rl) return;
|
|
856
|
+
if (busy || pendingAsk || sliderAtivo || !rl) return;
|
|
770
857
|
const linha = rl.line;
|
|
771
858
|
const noFim = rl.cursor === linha.length;
|
|
772
859
|
fantasma = '';
|
|
@@ -785,6 +872,7 @@ async function main() {
|
|
|
785
872
|
// sequence "\x1b\x1b" — vale como os dois.
|
|
786
873
|
let ultimoEsc = 0;
|
|
787
874
|
process.stdin.on('keypress', (ch, key) => {
|
|
875
|
+
if (sliderAtivo) return; // o slider é dono do teclado enquanto está no ar
|
|
788
876
|
if (key && (key.name === 'escape' || key.sequence === '\x1b\x1b')) {
|
|
789
877
|
if (!busy) { ultimoEsc = 0; return; } // sem agente rodando, Esc é só Esc
|
|
790
878
|
const agora = Date.now();
|
package/lib/act.js
CHANGED
|
@@ -15,10 +15,6 @@ const tools = require('./tools');
|
|
|
15
15
|
const api = require('./api');
|
|
16
16
|
const cfg = require('./config');
|
|
17
17
|
const catalogo = require('./catalogo');
|
|
18
|
-
// "painel", não "streaming": dentro do runAct existe um `let streaming` (a
|
|
19
|
-
// flag de texto chegando em stream) que SOMBREARIA este módulo — e o step()
|
|
20
|
-
// viraria TypeError engolido pelo catch. Foi um bug real, pego em teste.
|
|
21
|
-
const painel = require('./streaming');
|
|
22
18
|
|
|
23
19
|
const MAX_ITERATIONS = 15;
|
|
24
20
|
const MAX_PROFUNDIDADE = 2; // principal + 1 nível de subagente
|
|
@@ -105,6 +101,14 @@ function logResult(kind, result, profundidade) {
|
|
|
105
101
|
case 'browser_read':
|
|
106
102
|
console.log(p + c.muted(`${(result.text || '').length} caracteres lidos da página`));
|
|
107
103
|
return;
|
|
104
|
+
case 'pesquisar_referencia': {
|
|
105
|
+
console.log(p + c.muted(`${result.fontes} fonte${result.fontes === 1 ? '' : 's'} lida${result.fontes === 1 ? '' : 's'} sobre "${result.consulta}"`));
|
|
106
|
+
for (const r of (result.referencias || []).slice(0, 4)) {
|
|
107
|
+
console.log(p + c.dim(`${r.titulo}`.slice(0, 100)));
|
|
108
|
+
console.log(p + c.dim(` ${r.url}`.slice(0, 110)));
|
|
109
|
+
}
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
108
112
|
case 'desktop_windows': {
|
|
109
113
|
console.log(p + c.muted(`${result.total} janela${result.total === 1 ? '' : 's'}`));
|
|
110
114
|
for (const j of (result.janelas || []).slice(0, 5)) console.log(p + c.dim(`${j.app ? j.app + ' · ' : ''}${j.titulo}`.slice(0, 120)));
|
|
@@ -168,7 +172,7 @@ function buildMemoryContext(projectDir, projectName) {
|
|
|
168
172
|
async function runAct(userPrompt, opts) {
|
|
169
173
|
const {
|
|
170
174
|
server, model, token, history, projectDir, projectName,
|
|
171
|
-
ensureWorkDir, profundidade = 0, subagent = false,
|
|
175
|
+
ensureWorkDir, profundidade = 0, subagent = false, effort,
|
|
172
176
|
} = opts || {};
|
|
173
177
|
|
|
174
178
|
// A pasta de projeto só nasce quando o agente for mesmo MEXER EM ARQUIVO.
|
|
@@ -193,11 +197,6 @@ async function runAct(userPrompt, opts) {
|
|
|
193
197
|
|
|
194
198
|
let finalText = '';
|
|
195
199
|
|
|
196
|
-
// O pedido do usuário aparece na barra de comando do painel, digitado
|
|
197
|
-
// letra a letra — só o pedido principal; sub-tarefa de agente não é
|
|
198
|
-
// instrução de gente.
|
|
199
|
-
if (!subagent) { try { painel.instrucao(userPrompt); } catch {} }
|
|
200
|
-
|
|
201
200
|
// Pedido novo zera o pedido de parada anterior.
|
|
202
201
|
if (!subagent) PARAR = false;
|
|
203
202
|
|
|
@@ -210,7 +209,7 @@ async function runAct(userPrompt, opts) {
|
|
|
210
209
|
let streaming = false;
|
|
211
210
|
|
|
212
211
|
try {
|
|
213
|
-
await api.requestAct(server, { messages, model, token, projectDir, projectName, subagent }, (evt) => {
|
|
212
|
+
await api.requestAct(server, { messages, model, effort, token, projectDir, projectName, subagent }, (evt) => {
|
|
214
213
|
if (evt.erro) { streamError = evt.erro; return; }
|
|
215
214
|
if (evt.tool_call) { toolCalls.push(evt.tool_call); return; }
|
|
216
215
|
if (evt.delta) {
|
|
@@ -300,10 +299,6 @@ async function runAct(userPrompt, opts) {
|
|
|
300
299
|
const result = await tools.execute(tc.name, tc.arguments, { projectDir: pastaAtual });
|
|
301
300
|
logResult(tc.name, result, profundidade);
|
|
302
301
|
|
|
303
|
-
// Painel ao vivo (/streaming): registra o passo e fotografa a tela.
|
|
304
|
-
// Nunca pode derrubar o loop — falha de foto é problema do painel.
|
|
305
|
-
try { painel.step(tc.name, tc.arguments, result); } catch {}
|
|
306
|
-
|
|
307
302
|
// Uma escrita bem-sucedida limpa a dívida: o agente se corrigiu.
|
|
308
303
|
if (result && result.ok === false) falhasPendentes.push(`${tc.name}(${tc.arguments?.path || ''}): ${result.error}`);
|
|
309
304
|
else if (['write_file', 'edit_file'].includes(tc.name)) falhasPendentes.length = 0;
|
package/lib/catalogo.js
CHANGED
|
@@ -78,6 +78,7 @@ const GRUPOS = [
|
|
|
78
78
|
resumo: 'como ele se organiza numa tarefa grande',
|
|
79
79
|
itens: [
|
|
80
80
|
{ tool: 'write_todos', escreve: false, faz: 'monta o plano e vai marcando o que terminou' },
|
|
81
|
+
{ tool: 'pesquisar_referencia', escreve: false, faz: 'lê docs e exemplos antes de escrever — sobe a qualidade' },
|
|
81
82
|
// Estas duas o loop do agente atende (lib/act.js), não o tools.execute.
|
|
82
83
|
{ tool: 'spawn_agent', escreve: false, faz: 'chama um subagente para uma parte da tarefa', noLoop: true },
|
|
83
84
|
{ tool: 'finish', escreve: false, faz: 'encerra dizendo o que fez', noLoop: true },
|
package/lib/config.js
CHANGED
|
@@ -160,6 +160,9 @@ function loadConfig() {
|
|
|
160
160
|
return {
|
|
161
161
|
server: process.env.PRIMOCODE_SERVER || file.server || 'https://primocode-server.onrender.com',
|
|
162
162
|
model: process.env.PRIMOCODE_MODEL || file.model || 'main',
|
|
163
|
+
// Esforço, estilo Claude Code (/effort). É o que o usuário escolhe; o
|
|
164
|
+
// modelo é derivado dele. Guardado para durar entre sessões.
|
|
165
|
+
effort: process.env.PRIMOCODE_EFFORT || file.effort || 'medium',
|
|
163
166
|
token: process.env.PRIMOCODE_TOKEN || file.token || null,
|
|
164
167
|
autoApply: file.autoApply === true,
|
|
165
168
|
currentProject: file.currentProject || null,
|
|
@@ -177,8 +180,8 @@ function saveConfig(partial) {
|
|
|
177
180
|
ensureDir(CONFIG_DIR);
|
|
178
181
|
const cur = (() => { try { return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')); } catch { return {}; } })();
|
|
179
182
|
// Só persiste o que faz sentido guardar — nada de campos derivados.
|
|
180
|
-
const { server, model, token, autoApply, currentProject, userName, mascot, theme, mode } = { ...cur, ...partial };
|
|
181
|
-
const next = { server, model, token, autoApply, currentProject, userName, mascot, theme, mode };
|
|
183
|
+
const { server, model, effort, token, autoApply, currentProject, userName, mascot, theme, mode } = { ...cur, ...partial };
|
|
184
|
+
const next = { server, model, effort, token, autoApply, currentProject, userName, mascot, theme, mode };
|
|
182
185
|
fs.writeFileSync(CONFIG_FILE, JSON.stringify(next, null, 2), 'utf8');
|
|
183
186
|
return next;
|
|
184
187
|
}
|
package/lib/desktop.js
CHANGED
|
@@ -659,8 +659,8 @@ function clickElement(nome, opts = {}) {
|
|
|
659
659
|
}
|
|
660
660
|
|
|
661
661
|
// ── Screenshot ────────────────────────────────────────
|
|
662
|
-
// A ferramenta do Linux é resolvida UMA vez:
|
|
663
|
-
//
|
|
662
|
+
// A ferramenta do Linux é resolvida UMA vez: um `which` a cada screenshot
|
|
663
|
+
// seria desperdício.
|
|
664
664
|
let LINUX_SHOT;
|
|
665
665
|
function linuxShot() {
|
|
666
666
|
if (LINUX_SHOT === undefined) LINUX_SHOT = firstCmd(['scrot', 'maim', 'gnome-screenshot', 'spectacle', 'import']);
|
|
@@ -669,8 +669,8 @@ function linuxShot() {
|
|
|
669
669
|
|
|
670
670
|
/**
|
|
671
671
|
* O comando de shell que fotografa a tela para `filename` — ou null quando a
|
|
672
|
-
* plataforma não tem como. Existe separado do screenshot() para
|
|
673
|
-
*
|
|
672
|
+
* plataforma não tem como. Existe separado do screenshot() para quem precisa
|
|
673
|
+
* capturar de forma assíncrona, sem travar o processo.
|
|
674
674
|
*/
|
|
675
675
|
function screenshotCommand(filename) {
|
|
676
676
|
if (PLATFORM === 'darwin') return `screencapture -x "${filename}"`;
|
package/lib/effort.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* effort.js — o "esforço" do agente, no espírito do /effort do Claude Code.
|
|
3
|
+
*
|
|
4
|
+
* O usuário não escolhe mais um modelo por nome (/model): escolhe um ESFORÇO
|
|
5
|
+
* num controle deslizante, do mais rápido ao mais esperto. O modelo é derivado
|
|
6
|
+
* daqui. Fica tudo num lugar só, puro (sem I/O), para o teste bater sem rede.
|
|
7
|
+
*
|
|
8
|
+
* A pesquisa de referência (abrir docs/exemplos e aprender com eles antes de
|
|
9
|
+
* escrever) liga a partir do "high": esforço maior = ele pesquisa antes de
|
|
10
|
+
* codar, o que melhora a qualidade. É VISÍVEL no painel — nada escondido.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
// Ordem = ordem do slider, do mais rápido ao mais esperto.
|
|
16
|
+
const NIVEIS = [
|
|
17
|
+
{ nome: 'low', model: 'fast', pesquisa: false, resumo: 'rápido — tarefa simples, resposta curta' },
|
|
18
|
+
{ nome: 'medium', model: 'main', pesquisa: false, resumo: 'equilíbrio — o padrão do dia a dia' },
|
|
19
|
+
{ nome: 'high', model: 'main', pesquisa: true, resumo: 'capricha no código e pesquisa referências' },
|
|
20
|
+
{ nome: 'xhigh', model: 'top', pesquisa: true, resumo: 'o mais capaz — 1M de contexto, pesquisa' },
|
|
21
|
+
{ nome: 'max', model: 'top', pesquisa: true, resumo: 'esforço máximo — pesquisa a fundo' },
|
|
22
|
+
{ nome: 'ultracode', model: 'top', pesquisa: true, resumo: 'tudo no talo — pesquisa e cruza referências' },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const NOMES = NIVEIS.map((n) => n.nome);
|
|
26
|
+
|
|
27
|
+
function normalizar(nome) {
|
|
28
|
+
const n = String(nome || '').toLowerCase().trim();
|
|
29
|
+
return NOMES.includes(n) ? n : 'medium';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function nivelDe(nome) {
|
|
33
|
+
return NIVEIS.find((n) => n.nome === normalizar(nome)) || NIVEIS[1];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function indiceDe(nome) {
|
|
37
|
+
const i = NOMES.indexOf(normalizar(nome));
|
|
38
|
+
return i < 0 ? 1 : i;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** O apelido de modelo que este esforço usa. */
|
|
42
|
+
function modeloDe(nome) {
|
|
43
|
+
return nivelDe(nome).model;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Este esforço pesquisa referências antes de codar? */
|
|
47
|
+
function pesquisaDe(nome) {
|
|
48
|
+
return nivelDe(nome).pesquisa;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = { NIVEIS, NOMES, normalizar, nivelDe, indiceDe, modeloDe, pesquisaDe };
|