brito-cli 0.6.0 → 0.6.3
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/README.md +16 -2
- package/README.pt-BR.md +16 -2
- package/dist/graph/store.js +2 -2
- package/dist/permissions.js +2 -2
- package/dist/repl.js +11 -8
- package/dist/ui/table.js +102 -0
- package/dist/ui/ui.js +89 -18
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ Brito is an agent for software development. Use it in the terminal (`brito-cli`
|
|
|
12
12
|
|
|
13
13
|
## Highlights
|
|
14
14
|
|
|
15
|
-
> **Upgrading to 0.6:** the graph engine changed from Kùzu (discontinued) to LadybugDB. The graph is rebuilt automatically on first use; your old `.brito/graph.kuzu` is kept. Brito asks once whether to copy your notes (memory) to the new graph, or run `brito migrar-notas` any time.
|
|
15
|
+
> **Upgrading to 0.6:** the graph engine changed from Kùzu (discontinued) to LadybugDB. The graph is rebuilt automatically on first use; your old `.brito/graph.kuzu` is kept. Brito asks once whether to copy your notes (memory) to the new graph, or run `brito migrar-notas` any time. Tests run in CI on macOS, Linux and Windows (use 0.6.1 or later on Windows/Linux). See the [CHANGELOG](CHANGELOG.md).
|
|
16
16
|
|
|
17
17
|
- **Graph-first exploration.** `graph_search`, `graph_context`, `graph_callers`, `graph_callees` and `graph_impact` answer "where is it, who uses it, what breaks" without reading whole files. The graph is embedded ([LadybugDB](https://github.com/LadybugDB/ladybug), fork mantido do Kùzu), local and incremental.
|
|
18
18
|
- **Wide coverage.** TypeScript/JavaScript, Python and the script blocks of `.svelte`, `.vue` and `.astro` components. Import aliases (`tsconfig` paths, `package.json` imports, SvelteKit's `$lib`) become real edges. Files ignored by `.gitignore` stay out.
|
|
@@ -27,6 +27,10 @@ Brito is an agent for software development. Use it in the terminal (`brito-cli`
|
|
|
27
27
|
|
|
28
28
|
## How it works
|
|
29
29
|
|
|
30
|
+

|
|
31
|
+
|
|
32
|
+
*The graph is built once and updated incrementally; a question like "what breaks if I change this?" becomes one or two graph calls.*
|
|
33
|
+
|
|
30
34
|
```mermaid
|
|
31
35
|
flowchart LR
|
|
32
36
|
code["Your project<br/>.ts · .js · .py · .java · .kt · .go · .rb · .php · .cs · .rs · .svelte · .vue · .astro"] -->|"tree-sitter<br/>incremental"| cg[("Code graph<br/>symbols · calls · imports")]
|
|
@@ -47,8 +51,18 @@ flowchart LR
|
|
|
47
51
|
- **Skills**: step-by-step procedures for *your* project (commands, order, traps, how to verify). The agent loads one with `skill_load` before a task it covers.
|
|
48
52
|
- **Journal**: Brito remembers across sessions which tasks repeated, which is the best hint that a procedure is general and worth saving.
|
|
49
53
|
|
|
54
|
+
### Architecture at a glance
|
|
55
|
+
|
|
56
|
+

|
|
57
|
+
|
|
58
|
+
*Everything the model sees or can do is one of eight components, grouped by role. The mono-spaced text is the real file or command; the pill says when it fires.*
|
|
59
|
+
|
|
50
60
|
## Skills that earn their place
|
|
51
61
|
|
|
62
|
+

|
|
63
|
+
|
|
64
|
+
*The two paths a skill can take. Safe ones prove themselves in trial; risky ones wait for your approval.*
|
|
65
|
+
|
|
52
66
|

|
|
53
67
|
|
|
54
68
|
0. **Autonomous path (default).** The reflection saves a new skill as *trial*, marked "(em teste)" in the index. Each turn that loads it counts as a win (no error-then-fix, no user correction) or a loss. 3 wins (and at most 1 loss per 3 wins) promote it; 2 losses without more wins, or 21 days unused, archive it to `.brito/skills/.trash`. Brito prints one line per promotion or archive. Limits: at most 3 new automatic skills a day, project scope only, and a weekly automatic consolidation that only touches automatic skills.
|
|
@@ -133,7 +147,7 @@ OK: 20 arquivos, 66 símbolos, 54 chamadas, 17 imports | +0 ~0 -0 =20 (28ms)
|
|
|
133
147
|
|---|---|
|
|
134
148
|
| `/help` | list commands |
|
|
135
149
|
| `/model`, `/effort`, `/reasoning` | choose model, reasoning effort and thinking (session, global or once) |
|
|
136
|
-
| `/modo` or `Shift+Tab` | switch permission mode: manual → auto-edits → plan |
|
|
150
|
+
| `/modo` or `Shift+Tab` | switch permission mode: manual → auto-edits → plan → bypass ("everything allowed") |
|
|
137
151
|
| `/skills` | list skills · `pending` · `approve <name>` · `reject <name>` · `consolidate` · `rm <name>` · `promote <name>` |
|
|
138
152
|
| `/learn` | extract skills from the last turn now |
|
|
139
153
|
| `/agents`, `/tools`, `/index`, `/context` | inspect subagents, tools, the index and the context window |
|
package/README.pt-BR.md
CHANGED
|
@@ -12,7 +12,7 @@ O Brito é um agente para desenvolvimento de software. Use no terminal (`brito-c
|
|
|
12
12
|
|
|
13
13
|
## Destaques
|
|
14
14
|
|
|
15
|
-
> **Atualizando para a 0.6:** o motor do grafo mudou do Kùzu (descontinuado) para o LadybugDB. O grafo é reconstruído sozinho no primeiro uso; o seu `.brito/graph.kuzu` antigo é mantido. O Brito pergunta uma vez se copia as suas notas (memória) para o grafo novo, ou rode `brito migrar-notas` quando quiser.
|
|
15
|
+
> **Atualizando para a 0.6:** o motor do grafo mudou do Kùzu (descontinuado) para o LadybugDB. O grafo é reconstruído sozinho no primeiro uso; o seu `.brito/graph.kuzu` antigo é mantido. O Brito pergunta uma vez se copia as suas notas (memória) para o grafo novo, ou rode `brito migrar-notas` quando quiser. Os testes rodam no CI em macOS, Linux e Windows (use a 0.6.1 ou posterior no Windows/Linux). Veja o [CHANGELOG](CHANGELOG.md).
|
|
16
16
|
|
|
17
17
|
- **Exploração guiada pelo grafo.** `graph_search`, `graph_context`, `graph_callers`, `graph_callees` e `graph_impact` respondem "onde está, quem usa, o que quebra" sem ler arquivos inteiros. O grafo é embutido ([LadybugDB](https://github.com/LadybugDB/ladybug), fork mantido do Kùzu), local e incremental.
|
|
18
18
|
- **Cobertura ampla.** TypeScript/JavaScript, Python e os blocos de script de componentes `.svelte`, `.vue` e `.astro`. Aliases de import (`paths` do tsconfig, `imports` do package.json, `$lib` do SvelteKit) viram arestas de verdade. O que o `.gitignore` ignora fica de fora.
|
|
@@ -27,6 +27,10 @@ O Brito é um agente para desenvolvimento de software. Use no terminal (`brito-c
|
|
|
27
27
|
|
|
28
28
|
## Como funciona
|
|
29
29
|
|
|
30
|
+

|
|
31
|
+
|
|
32
|
+
*O grafo é construído uma vez e atualizado de forma incremental; uma pergunta como "o que quebra se eu mudar isto?" vira uma ou duas chamadas ao grafo.*
|
|
33
|
+
|
|
30
34
|
```mermaid
|
|
31
35
|
flowchart LR
|
|
32
36
|
code["Seu projeto<br/>.ts · .js · .py · .java · .kt · .go · .rb · .php · .cs · .rs · .svelte · .vue · .astro"] -->|"tree-sitter<br/>incremental"| cg[("Grafo de código<br/>símbolos · chamadas · imports")]
|
|
@@ -43,8 +47,18 @@ flowchart LR
|
|
|
43
47
|
- **Skills**: procedimentos passo a passo para o *seu* projeto (comandos, ordem, armadilhas, como verificar). O agente carrega uma com `skill_load` antes de uma tarefa que ela cobre.
|
|
44
48
|
- **Journal**: o Brito lembra entre sessões quais tarefas se repetiram, o melhor indício de que um procedimento é geral e vale guardar.
|
|
45
49
|
|
|
50
|
+
### Arquitetura em uma imagem
|
|
51
|
+
|
|
52
|
+

|
|
53
|
+
|
|
54
|
+
*Tudo o que o modelo vê ou pode fazer é um dos oito componentes, agrupados por papel. O texto em monoespaçado é o arquivo ou comando real; a etiqueta diz quando ele entra em ação.*
|
|
55
|
+
|
|
46
56
|
## Skills que merecem o lugar
|
|
47
57
|
|
|
58
|
+

|
|
59
|
+
|
|
60
|
+
*Os dois caminhos de uma skill. As seguras se provam em teste; as arriscadas esperam a sua aprovação.*
|
|
61
|
+
|
|
48
62
|

|
|
49
63
|
|
|
50
64
|
0. **Caminho autônomo (padrão).** A reflexão grava a skill nova como *em teste*, marcada "(em teste)" no índice. Cada turno que a carrega conta como vitória (sem erro→conserto nem correção do usuário) ou falha. 3 vitórias (e no máximo 1 falha a cada 3 vitórias) a promovem; 2 falhas sem mais vitórias, ou 21 dias sem uso, a arquivam em `.brito/skills/.trash`. O Brito imprime uma linha a cada promoção ou arquivamento. Limites: no máximo 3 skills automáticas novas por dia, só no escopo do projeto, e uma consolidação semanal automática que só mexe em skills automáticas.
|
|
@@ -129,7 +143,7 @@ OK: 20 arquivos, 66 símbolos, 54 chamadas, 17 imports | +0 ~0 -0 =20 (28ms)
|
|
|
129
143
|
|---|---|
|
|
130
144
|
| `/help` | lista os comandos |
|
|
131
145
|
| `/model`, `/effort`, `/reasoning` | escolhem modelo, esforço de raciocínio e thinking (sessão, global ou uma vez) |
|
|
132
|
-
| `/modo` ou `Shift+Tab` | troca o modo de permissão: manual → auto-edições → plano |
|
|
146
|
+
| `/modo` ou `Shift+Tab` | troca o modo de permissão: manual → auto-edições → plano → tudo liberado |
|
|
133
147
|
| `/skills` | lista skills · `pending` · `approve <nome>` · `reject <nome>` · `consolidate` · `rm <nome>` · `promote <nome>` |
|
|
134
148
|
| `/learn` | extrai skills do último turno agora |
|
|
135
149
|
| `/agents`, `/tools`, `/index`, `/context` | inspecionam subagentes, tools, o índice e a janela de contexto |
|
package/dist/graph/store.js
CHANGED
|
@@ -21,8 +21,8 @@ export class GraphLockedError extends Error {
|
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
const isLockError = (e) => /could not set lock/i.test(String(e?.message ?? e));
|
|
24
|
-
/** Outro processo está criando o banco
|
|
25
|
-
const isCreatingError = (e) => /shadow file
|
|
24
|
+
/** Outro processo está criando o banco ou fazendo checkpoint: o .shadow/.wal aparece, some ou fica negado por instantes (a mensagem muda por SO). Passa sozinho. */
|
|
25
|
+
const isCreatingError = (e) => /shadow file|\.(shadow|wal)\b|checkpoint is in progress/i.test(String(e?.message ?? e));
|
|
26
26
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
27
27
|
/** Quanto esperar o banco liberar antes de desistir: cobre sobreposições curtas (a reflexão de skills e o turno, por exemplo). */
|
|
28
28
|
const LOCK_WAIT_MS = 15_000;
|
package/dist/permissions.js
CHANGED
|
@@ -6,8 +6,8 @@ export const perms = { mode: "manual" };
|
|
|
6
6
|
*/
|
|
7
7
|
export const MODE_ENV = "BRITO_PERMS_MODE";
|
|
8
8
|
export const modeFromEnv = (v) => ["manual", "auto", "plan", "bypass"].find((m) => m === v);
|
|
9
|
-
const CYCLE = ["manual", "auto", "plan"];
|
|
10
|
-
/** Próximo modo do ciclo; `bypass` (
|
|
9
|
+
export const CYCLE = ["manual", "auto", "plan", "bypass"];
|
|
10
|
+
/** Próximo modo do ciclo; depois de `bypass` (tudo liberado) volta para manual. */
|
|
11
11
|
export function cycleMode() {
|
|
12
12
|
perms.mode = CYCLE[(CYCLE.indexOf(perms.mode) + 1) % CYCLE.length];
|
|
13
13
|
return perms.mode;
|
package/dist/repl.js
CHANGED
|
@@ -39,11 +39,14 @@ export async function runRepl(b, o) {
|
|
|
39
39
|
const modeColor = { manual: c.muted, auto: c.ok, plan: c.warn, bypass: c.err };
|
|
40
40
|
const modeChip = () => { const m = MODE_LABEL[perms.mode]; return modeColor[perms.mode](`${m.icon} ${m.name}`); };
|
|
41
41
|
const status = () => ui.statusLine({
|
|
42
|
+
project: path.basename(o.root),
|
|
42
43
|
mode: modeChip(),
|
|
43
44
|
model: b.model(), ctxTokens: b.contextTokens(), ctxWindow: b.ctxWindow,
|
|
44
45
|
input: b.usage().input, output: b.usage().output, cacheRead: b.usage().cacheRead, startedAt: o.startedAt, cost: b.cost?.(),
|
|
45
46
|
extra: `${skillCount} ${skillCount === 1 ? "skill" : "skills"}`,
|
|
46
47
|
});
|
|
48
|
+
if (interactive && process.stdout.isTTY)
|
|
49
|
+
process.stdout.write(`\x1b]0;brito · ${path.basename(o.root)}\x07`);
|
|
47
50
|
const dock = interactive ? new Dock({ status, commands: menu, historyFile: path.join(globalHome(), "history") }) : null;
|
|
48
51
|
if (dock) {
|
|
49
52
|
await dock.start();
|
|
@@ -90,7 +93,7 @@ export async function runRepl(b, o) {
|
|
|
90
93
|
"/help": () => {
|
|
91
94
|
for (const m of menu)
|
|
92
95
|
ui.print(` ${c.soft(m.name.padEnd(12))} ${c.muted(m.desc)}`);
|
|
93
|
-
ui.print(` ${c.soft("Shift+Tab".padEnd(12))} ${c.muted("alterna modo: manual → auto-edições → plano")}`);
|
|
96
|
+
ui.print(` ${c.soft("Shift+Tab".padEnd(12))} ${c.muted("alterna modo: manual → auto-edições → plano → tudo liberado")}`);
|
|
94
97
|
ui.print(` ${c.soft("!comando".padEnd(12))} ${c.muted("roda no shell, sem chamar o modelo")}`);
|
|
95
98
|
ui.print(c.dim(" Enter envia · Alt+Enter nova linha · ↑↓ histórico · Tab completa · Ctrl+C interrompe"));
|
|
96
99
|
},
|
|
@@ -106,13 +109,13 @@ export async function runRepl(b, o) {
|
|
|
106
109
|
"/indicadores": () => { for (const l of ui.statusLegend())
|
|
107
110
|
ui.print(l); },
|
|
108
111
|
"/modo": ([m]) => {
|
|
109
|
-
const pick = { manual: "manual", auto: "auto", plano: "plan", plan: "plan" };
|
|
112
|
+
const pick = { manual: "manual", auto: "auto", plano: "plan", plan: "plan", tudo: "bypass", bypass: "bypass" };
|
|
110
113
|
if (m && pick[m]) {
|
|
111
114
|
perms.mode = pick[m];
|
|
112
115
|
dock?.repaint();
|
|
113
116
|
}
|
|
114
117
|
else if (m) {
|
|
115
|
-
ui.warn("use: /modo manual | auto | plano");
|
|
118
|
+
ui.warn("use: /modo manual | auto | plano | tudo");
|
|
116
119
|
return;
|
|
117
120
|
}
|
|
118
121
|
const i = MODE_LABEL[perms.mode];
|
|
@@ -150,13 +153,13 @@ export async function runRepl(b, o) {
|
|
|
150
153
|
const echo = (text) => {
|
|
151
154
|
const lines = text.split("\n");
|
|
152
155
|
const shown = lines.length > 4 ? [...lines.slice(0, 3), c.dim(`… (+${lines.length - 3} linhas)`)] : lines;
|
|
153
|
-
// balão do usuário: mesmo formato da caixa do Brito, em vermelho e com o nome de usuário da máquina
|
|
156
|
+
// balão do usuário (sem borda lateral, para copiar limpo): mesmo formato da caixa do Brito, em vermelho e com o nome de usuário da máquina
|
|
154
157
|
const label = ` ${userName()} `;
|
|
155
|
-
const room = Math.max(10, cols() -
|
|
158
|
+
const room = Math.max(10, cols() - 1);
|
|
156
159
|
const body = shown.flatMap((l) => (l.length <= room ? [l] : l.match(new RegExp(`.{1,${room}}`, "gs")) ?? [l]));
|
|
157
|
-
ui.print("\n" + c.err("
|
|
158
|
-
body.map((l) => c.
|
|
159
|
-
c.err("
|
|
160
|
+
ui.print("\n" + c.err("──") + c.bold(c.err(label)) + c.err("─".repeat(Math.max(2, cols() - label.length - 2))) + "\n" +
|
|
161
|
+
body.map((l) => c.bold(c.err(l))).join("\n") + "\n" +
|
|
162
|
+
c.err("─".repeat(cols())));
|
|
160
163
|
};
|
|
161
164
|
const shutdown = async () => {
|
|
162
165
|
if (closing)
|
package/dist/ui/table.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import stringWidth from "string-width";
|
|
2
|
+
import { c } from "./theme.js";
|
|
3
|
+
/** Separa as células de uma linha `| a | b |` (aceita `\|` dentro da célula). */
|
|
4
|
+
function cells(line) {
|
|
5
|
+
let s = line.trim();
|
|
6
|
+
if (s.startsWith("|"))
|
|
7
|
+
s = s.slice(1);
|
|
8
|
+
if (s.endsWith("|") && !s.endsWith("\\|"))
|
|
9
|
+
s = s.slice(0, -1);
|
|
10
|
+
return s.split(/(?<!\\)\|/).map((x) => x.replace(/\\\|/g, "|").trim());
|
|
11
|
+
}
|
|
12
|
+
const isSep = (row) => row.length > 0 && row.every((x) => /^:?-{1,}:?$/.test(x.replace(/\s/g, "")));
|
|
13
|
+
/** Tira a marcação inline (**negrito**, *itálico*, `código`) — a tabela é desenhada sem ela. */
|
|
14
|
+
const plain = (s) => s.replace(/\*\*(.+?)\*\*/g, "$1").replace(/__(.+?)__/g, "$1").replace(/(^|[^\w*])\*(?!\s)(.+?)\*(?!\w)/g, "$1$2").replace(/`([^`]*)`/g, "$1").replace(/<br\s*\/?>/gi, " ");
|
|
15
|
+
/** Quebra o texto em linhas de no máximo `w` colunas (palavra longa demais é cortada). */
|
|
16
|
+
function wrap(text, w) {
|
|
17
|
+
const out = [];
|
|
18
|
+
let cur = "";
|
|
19
|
+
const push = (word) => {
|
|
20
|
+
while (stringWidth(word) > w) {
|
|
21
|
+
if (cur) {
|
|
22
|
+
out.push(cur);
|
|
23
|
+
cur = "";
|
|
24
|
+
}
|
|
25
|
+
let piece = "";
|
|
26
|
+
for (const ch of word) {
|
|
27
|
+
if (stringWidth(piece + ch) > w)
|
|
28
|
+
break;
|
|
29
|
+
piece += ch;
|
|
30
|
+
}
|
|
31
|
+
out.push(piece);
|
|
32
|
+
word = word.slice(piece.length);
|
|
33
|
+
}
|
|
34
|
+
if (!cur)
|
|
35
|
+
cur = word;
|
|
36
|
+
else if (stringWidth(cur) + 1 + stringWidth(word) <= w)
|
|
37
|
+
cur += " " + word;
|
|
38
|
+
else {
|
|
39
|
+
out.push(cur);
|
|
40
|
+
cur = word;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
for (const word of text.split(/\s+/).filter(Boolean))
|
|
44
|
+
push(word);
|
|
45
|
+
if (cur || !out.length)
|
|
46
|
+
out.push(cur);
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
const pad = (s, w, align) => {
|
|
50
|
+
const gap = Math.max(0, w - stringWidth(s));
|
|
51
|
+
if (align === "r")
|
|
52
|
+
return " ".repeat(gap) + s;
|
|
53
|
+
if (align === "c")
|
|
54
|
+
return " ".repeat(Math.floor(gap / 2)) + s + " ".repeat(Math.ceil(gap / 2));
|
|
55
|
+
return s + " ".repeat(gap);
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Desenha uma tabela markdown como grade com bordas (┌─┬─┐ / │ / ├─┼─┤ / └─┴─┘), como o Claude Code.
|
|
59
|
+
* Cabe em `width` colunas: as colunas mais largas encolhem e o texto quebra dentro delas.
|
|
60
|
+
* Devolve null se as linhas não formam uma tabela (falta a linha `|---|---|`).
|
|
61
|
+
*/
|
|
62
|
+
export function renderTable(lines, width) {
|
|
63
|
+
if (lines.length < 2)
|
|
64
|
+
return null;
|
|
65
|
+
const head = cells(lines[0]);
|
|
66
|
+
const sepRow = cells(lines[1]);
|
|
67
|
+
if (!isSep(sepRow))
|
|
68
|
+
return null;
|
|
69
|
+
const n = head.length;
|
|
70
|
+
const align = Array.from({ length: n }, (_, i) => {
|
|
71
|
+
const s = (sepRow[i] ?? "").replace(/\s/g, "");
|
|
72
|
+
return s.startsWith(":") && s.endsWith(":") ? "c" : s.endsWith(":") ? "r" : "l";
|
|
73
|
+
});
|
|
74
|
+
const norm = (r) => Array.from({ length: n }, (_, i) => plain(r[i] ?? ""));
|
|
75
|
+
const rows = [norm(head), ...lines.slice(2).map(cells).map(norm)];
|
|
76
|
+
const w = Array.from({ length: n }, (_, i) => Math.max(1, ...rows.map((r) => stringWidth(r[i]))));
|
|
77
|
+
const room = Math.max(n, width - (3 * n + 1));
|
|
78
|
+
// encolhe sempre a coluna mais larga até caber (mínimo de 8 colunas, ou menos se não houver espaço)
|
|
79
|
+
const min = Math.max(1, Math.min(8, Math.floor(room / n)));
|
|
80
|
+
while (w.reduce((a, b) => a + b, 0) > room) {
|
|
81
|
+
const i = w.indexOf(Math.max(...w));
|
|
82
|
+
if (w[i] <= min)
|
|
83
|
+
break;
|
|
84
|
+
w[i]--;
|
|
85
|
+
}
|
|
86
|
+
const border = (l, m, r) => c.accent(l + w.map((x) => "─".repeat(x + 2)).join(m) + r);
|
|
87
|
+
const bar = c.accent("│");
|
|
88
|
+
const out = [border("┌", "┬", "┐")];
|
|
89
|
+
rows.forEach((r, ri) => {
|
|
90
|
+
const wrapped = r.map((cell, i) => wrap(cell, w[i]));
|
|
91
|
+
const h = Math.max(...wrapped.map((x) => x.length));
|
|
92
|
+
for (let k = 0; k < h; k++) {
|
|
93
|
+
const body = wrapped.map((x, i) => {
|
|
94
|
+
const cell = pad(x[k] ?? "", w[i], ri === 0 ? "c" : align[i]);
|
|
95
|
+
return " " + (ri === 0 ? c.bold(cell) : cell) + " ";
|
|
96
|
+
});
|
|
97
|
+
out.push(bar + body.join(bar) + bar);
|
|
98
|
+
}
|
|
99
|
+
out.push(ri === rows.length - 1 ? border("└", "┴", "┘") : border("├", "┼", "┤"));
|
|
100
|
+
});
|
|
101
|
+
return out;
|
|
102
|
+
}
|
package/dist/ui/ui.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { BOLD, CODE, ITALIC, OFF, c, cols, palette, useColor } from "./theme.js";
|
|
2
2
|
import { renderBanner } from "./banner.js";
|
|
3
|
+
import { renderTable } from "./table.js";
|
|
3
4
|
import stringWidth from "string-width";
|
|
4
5
|
export const NAME = "Brito";
|
|
5
6
|
const isTTY = !!process.stdout.isTTY;
|
|
@@ -159,6 +160,70 @@ class UI {
|
|
|
159
160
|
skip = false; // descarta o resto da linha do ``` (nome da linguagem)
|
|
160
161
|
bol = true; // só espaços até agora nesta linha
|
|
161
162
|
prev = " "; // último caractere resolvido
|
|
163
|
+
// tabelas markdown: linhas que começam com `|` são guardadas inteiras e desenhadas alinhadas no fim da tabela
|
|
164
|
+
rawBol = true; // início de linha no texto bruto do modelo
|
|
165
|
+
lead = ""; // espaços no início da linha, antes de decidir se é tabela
|
|
166
|
+
tblLine = null; // linha de tabela em andamento
|
|
167
|
+
tbl = []; // linhas completas da tabela atual
|
|
168
|
+
/** Primeira etapa do streaming: separa as linhas de tabela; o resto segue para o markdown. */
|
|
169
|
+
raw(ch) {
|
|
170
|
+
if (this.tblLine !== null) {
|
|
171
|
+
if (ch === "\n") {
|
|
172
|
+
this.tbl.push(this.tblLine);
|
|
173
|
+
this.tblLine = null;
|
|
174
|
+
this.rawBol = true;
|
|
175
|
+
}
|
|
176
|
+
else
|
|
177
|
+
this.tblLine += ch;
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (this.rawBol && !this.st.fence) {
|
|
181
|
+
if (ch === " " || ch === "\t") {
|
|
182
|
+
this.lead += ch;
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (ch === "|") {
|
|
186
|
+
this.tblLine = "|";
|
|
187
|
+
this.lead = "";
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
this.flushTable();
|
|
192
|
+
if (this.lead) {
|
|
193
|
+
for (const x of this.lead)
|
|
194
|
+
this.feed(x);
|
|
195
|
+
this.lead = "";
|
|
196
|
+
}
|
|
197
|
+
this.rawBol = ch === "\n";
|
|
198
|
+
this.feed(ch);
|
|
199
|
+
}
|
|
200
|
+
/** Fecha a tabela pendente: desenha alinhada ou, se não for tabela de verdade, devolve as linhas como texto. */
|
|
201
|
+
flushTable() {
|
|
202
|
+
if (this.tblLine !== null) {
|
|
203
|
+
this.tbl.push(this.tblLine);
|
|
204
|
+
this.tblLine = null;
|
|
205
|
+
}
|
|
206
|
+
if (!this.tbl.length)
|
|
207
|
+
return;
|
|
208
|
+
const lines = this.tbl;
|
|
209
|
+
this.tbl = [];
|
|
210
|
+
const drawn = renderTable(lines, cols() - 1);
|
|
211
|
+
if (!drawn) {
|
|
212
|
+
for (const l of lines)
|
|
213
|
+
for (const x of l + "\n")
|
|
214
|
+
this.feed(x);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
this.emitWord();
|
|
218
|
+
if (!this.lineStart)
|
|
219
|
+
this.out("\n");
|
|
220
|
+
for (const l of drawn)
|
|
221
|
+
this.out(l + "\n");
|
|
222
|
+
this.lineStart = true;
|
|
223
|
+
this.lineW = 0;
|
|
224
|
+
this.prev = "\n";
|
|
225
|
+
this.bol = true;
|
|
226
|
+
}
|
|
162
227
|
seq() {
|
|
163
228
|
const { b, i, k, fence } = this.st;
|
|
164
229
|
return k || fence ? CODE : (i ? ITALIC : "") + (b ? BOLD : "");
|
|
@@ -178,6 +243,10 @@ class UI {
|
|
|
178
243
|
this.skip = false;
|
|
179
244
|
this.bol = true;
|
|
180
245
|
this.prev = " ";
|
|
246
|
+
this.rawBol = true;
|
|
247
|
+
this.lead = "";
|
|
248
|
+
this.tblLine = null;
|
|
249
|
+
this.tbl = [];
|
|
181
250
|
}
|
|
182
251
|
/** Descarrega a palavra em andamento, quebrando a linha antes dela se não couber. */
|
|
183
252
|
emitWord() {
|
|
@@ -190,11 +259,8 @@ class UI {
|
|
|
190
259
|
this.lineStart = true;
|
|
191
260
|
this.lineW = 0;
|
|
192
261
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
this.lineStart = false;
|
|
196
|
-
}
|
|
197
|
-
// o estilo é reaplicado por palavra: a borda e os espaços não herdam a cor
|
|
262
|
+
this.lineStart = false; // sem borda lateral: copiar o texto do terminal sai limpo
|
|
263
|
+
// o estilo é reaplicado por palavra: os espaços não herdam a cor
|
|
198
264
|
this.out(this.wordStart + this.word + (this.active() ? OFF : ""));
|
|
199
265
|
this.lineW += w;
|
|
200
266
|
this.word = "";
|
|
@@ -269,16 +335,13 @@ class UI {
|
|
|
269
335
|
this.st.i = false;
|
|
270
336
|
} // linha em branco encerra ênfase aberta
|
|
271
337
|
this.wordStart = this.seq();
|
|
272
|
-
this.out(
|
|
338
|
+
this.out("\n");
|
|
273
339
|
this.lineStart = true;
|
|
274
340
|
this.lineW = 0;
|
|
275
341
|
}
|
|
276
342
|
else if (ch === " ") {
|
|
277
343
|
this.emitWord();
|
|
278
|
-
|
|
279
|
-
this.out(c.accent("│ "));
|
|
280
|
-
this.lineStart = false;
|
|
281
|
-
}
|
|
344
|
+
this.lineStart = false;
|
|
282
345
|
if (this.lineW + 1 > cols() - 2) {
|
|
283
346
|
this.out("\n");
|
|
284
347
|
this.lineStart = true;
|
|
@@ -299,7 +362,7 @@ class UI {
|
|
|
299
362
|
if (!this.textOpen) {
|
|
300
363
|
this.clear();
|
|
301
364
|
const label = ` ${NAME} `;
|
|
302
|
-
this.out(c.accent("
|
|
365
|
+
this.out(c.accent("──") + c.bold(c.light(label)) + c.accent("─".repeat(Math.max(2, cols() - label.length - 2))) + "\n");
|
|
303
366
|
this.textOpen = true;
|
|
304
367
|
this.lineStart = true;
|
|
305
368
|
this.lineW = 0;
|
|
@@ -307,18 +370,24 @@ class UI {
|
|
|
307
370
|
this.resetStyle();
|
|
308
371
|
}
|
|
309
372
|
for (const ch of chunk)
|
|
310
|
-
this.
|
|
373
|
+
this.raw(ch);
|
|
311
374
|
}
|
|
312
375
|
textEnd() {
|
|
313
376
|
if (!this.textOpen)
|
|
314
377
|
return;
|
|
378
|
+
this.flushTable();
|
|
379
|
+
if (this.lead) {
|
|
380
|
+
for (const x of this.lead)
|
|
381
|
+
this.feed(x);
|
|
382
|
+
this.lead = "";
|
|
383
|
+
}
|
|
315
384
|
if (this.run.n)
|
|
316
385
|
this.resolve("\n"); // marcador no fim da resposta: fecha se havia abertura, senão fica literal
|
|
317
386
|
this.emitWord();
|
|
318
387
|
this.resetStyle();
|
|
319
388
|
if (!this.lineStart)
|
|
320
389
|
this.out("\n");
|
|
321
|
-
this.out(c.accent("
|
|
390
|
+
this.out(c.accent("─".repeat(cols())) + "\n");
|
|
322
391
|
this.textOpen = false;
|
|
323
392
|
this.draw();
|
|
324
393
|
}
|
|
@@ -355,17 +424,17 @@ class UI {
|
|
|
355
424
|
return;
|
|
356
425
|
this.textEnd();
|
|
357
426
|
const w = cols();
|
|
358
|
-
const wrapped = body.split("\n").flatMap((l) => wrapLine(l.trimEnd(), w -
|
|
427
|
+
const wrapped = body.split("\n").flatMap((l) => wrapLine(l.trimEnd(), w - 1));
|
|
359
428
|
const MAX = 10;
|
|
360
429
|
const cut = !this.reasoning.full && wrapped.length > MAX;
|
|
361
430
|
const lines = cut ? wrapped.slice(0, MAX) : wrapped;
|
|
362
431
|
const label = " raciocínio ";
|
|
363
|
-
this.print(c.dim(
|
|
432
|
+
this.print(c.dim(`──${label}${"─".repeat(Math.max(0, w - 2 - label.length))}`));
|
|
364
433
|
for (const l of lines)
|
|
365
|
-
this.print(c.dim(
|
|
434
|
+
this.print(c.dim(l));
|
|
366
435
|
if (cut)
|
|
367
|
-
this.print(c.dim(
|
|
368
|
-
this.print(c.dim(
|
|
436
|
+
this.print(c.dim(`… +${wrapped.length - MAX} linhas (/reasoning full mostra tudo)`));
|
|
437
|
+
this.print(c.dim("─".repeat(w)));
|
|
369
438
|
}
|
|
370
439
|
info(msg) { this.textEnd(); this.print(c.muted(msg)); }
|
|
371
440
|
warn(msg) { this.textEnd(); this.print(c.rgb(palette.warn)(msg)); }
|
|
@@ -389,6 +458,7 @@ class UI {
|
|
|
389
458
|
const hitColour = hit === undefined ? c.muted : hit >= 70 ? c.ok : hit >= 40 ? c.warn : c.err;
|
|
390
459
|
// [prioridade (maior = descarta primeiro), texto]
|
|
391
460
|
const segs = [
|
|
461
|
+
...(s.project ? [[0, c.light("📁 ") + c.bold(c.soft(s.project))]] : []),
|
|
392
462
|
...(s.mode ? [[0, s.mode]] : []),
|
|
393
463
|
[0, c.light("⬢ ") + c.bold(c.soft(s.model))],
|
|
394
464
|
[0, label("contexto") + c.muted(`${fmtTokens(s.ctxTokens)}/${fmtTokens(s.ctxWindow)}`) + ` [${bar}] ${colour(`${pct}%`)}`],
|
|
@@ -417,6 +487,7 @@ class UI {
|
|
|
417
487
|
["tokens", "total da sessão: ↑ enviados ao modelo (inclui cache) · ↓ gerados por ele."],
|
|
418
488
|
["custo", "gasto estimado da sessão em dólares."],
|
|
419
489
|
["tempo", "há quanto tempo a sessão está aberta · (ativo) soma do tempo gasto em todos os turnos."],
|
|
490
|
+
["📁", "projeto (pasta) em que o Brito está trabalhando — também aparece no título da janela do terminal."],
|
|
420
491
|
["skill", "skill carregada (skill_load) no turno em andamento."],
|
|
421
492
|
].map(([k, v]) => ` ${c.soft(k.padEnd(9))} ${c.muted(v)}`);
|
|
422
493
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brito-cli",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.3",
|
|
4
4
|
"description": "Coding-agent CLI with a code knowledge graph, memory and reviewable skills / CLI de agente de código com grafo de conhecimento, memória e skills revisáveis",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|