kafox-cli 0.10.0 → 0.10.2
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/package.json +1 -1
- package/src/auth.js +4 -2
- package/src/compact.js +40 -0
- package/src/gateway.js +178 -25
- package/src/mobile/auth.js +122 -0
- package/src/mobile/bridge.js +12 -1
- package/src/mobile/captcha.js +96 -0
- package/src/mobile/history.js +71 -0
- package/src/mobile/index.js +18 -0
- package/src/mobile/ratelimit.js +79 -0
- package/src/mobile/server.js +105 -3
- package/src/mobile/tui-bridge.js +1 -0
- package/src/mobile/view.js +223 -14
- package/src/tui.js +225 -45
package/src/tui.js
CHANGED
|
@@ -16,7 +16,7 @@ const os = require("os");
|
|
|
16
16
|
const path = require("path");
|
|
17
17
|
const chalk = require("chalk");
|
|
18
18
|
const storage = require("./storage");
|
|
19
|
-
const { estimateTokens: countContextTokens, compactIfNeeded } = require("./compact");
|
|
19
|
+
const { estimateTokens: countContextTokens, compactIfNeeded, trimHistory } = require("./compact");
|
|
20
20
|
const { ensureSession, fetchApi } = require("./auth");
|
|
21
21
|
const { runAgent } = require("./agent");
|
|
22
22
|
const { THEMES, themeColors, paintLine, paintPanel, borderColor } = require("./theme");
|
|
@@ -316,8 +316,26 @@ function tableLines(header, aligns = [], body = [], width = 80, theme = "default
|
|
|
316
316
|
return lines;
|
|
317
317
|
}
|
|
318
318
|
|
|
319
|
-
|
|
319
|
+
// Cache do markdown renderizado por {texto, largura, tema}. Sem isso, cada
|
|
320
|
+
// frame (inclusive a cada tecla) reparseava o markdown de TODAS as mensagens
|
|
321
|
+
// da conversa, o que domina o tempo de render em chats longos.
|
|
322
|
+
const MARKDOWN_CACHE = new Map();
|
|
323
|
+
const MARKDOWN_CACHE_MAX = 400;
|
|
324
|
+
/** Converte Markdown simples em linhas prontas para o terminal (com cache). */
|
|
320
325
|
function markdownLines(text, width, theme = "default") {
|
|
326
|
+
const key = `${width}\u0000${theme}\u0000${text}`;
|
|
327
|
+
const hit = MARKDOWN_CACHE.get(key);
|
|
328
|
+
if (hit) return hit;
|
|
329
|
+
const out = markdownLinesUncached(text, width, theme);
|
|
330
|
+
if (MARKDOWN_CACHE.size >= MARKDOWN_CACHE_MAX) {
|
|
331
|
+
// Despeja o mais antigo (ordem de insercao do Map).
|
|
332
|
+
MARKDOWN_CACHE.delete(MARKDOWN_CACHE.keys().next().value);
|
|
333
|
+
}
|
|
334
|
+
MARKDOWN_CACHE.set(key, out);
|
|
335
|
+
return out;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function markdownLinesUncached(text, width, theme = "default") {
|
|
321
339
|
const accent = accentFor(theme);
|
|
322
340
|
const out = [];
|
|
323
341
|
const rows = String(text).replace(/\r/g, "").split("\n");
|
|
@@ -481,6 +499,8 @@ const CONTROL_KEYS = {
|
|
|
481
499
|
12: { name: "l", ctrl: true },
|
|
482
500
|
13: { name: "return" },
|
|
483
501
|
21: { name: "u", ctrl: true },
|
|
502
|
+
// Ctrl+R: atualiza saldo da conta e dos provedores externos agora.
|
|
503
|
+
18: { name: "r", ctrl: true },
|
|
484
504
|
// Ctrl+Q: desiste das mensagens que estao na fila (nao cancela a atual).
|
|
485
505
|
17: { name: "q", ctrl: true },
|
|
486
506
|
23: { name: "w", ctrl: true },
|
|
@@ -684,6 +704,7 @@ function initialState() {
|
|
|
684
704
|
modal: null,
|
|
685
705
|
models: [],
|
|
686
706
|
balance: null,
|
|
707
|
+
providerBalances: [],
|
|
687
708
|
tokens: 0,
|
|
688
709
|
files: [],
|
|
689
710
|
mcp: { loading: false, statuses: [], tools: 0 },
|
|
@@ -754,6 +775,8 @@ function heroLines(state, width, available) {
|
|
|
754
775
|
" envia " +
|
|
755
776
|
chalk.dim("/") +
|
|
756
777
|
" menu " +
|
|
778
|
+
chalk.dim("ctrl+r") +
|
|
779
|
+
" saldo " +
|
|
757
780
|
chalk.dim("ctrl+c") +
|
|
758
781
|
" sai",
|
|
759
782
|
width
|
|
@@ -1082,40 +1105,81 @@ function compactionLines(message, width, state) {
|
|
|
1082
1105
|
return lines;
|
|
1083
1106
|
}
|
|
1084
1107
|
|
|
1085
|
-
|
|
1108
|
+
/**
|
|
1109
|
+
* Virtualizacao de mensagens.
|
|
1110
|
+
*
|
|
1111
|
+
* Sem isso, cada frame (inclusive a cada tecla) reconstruia linha a linha TODO
|
|
1112
|
+
* o historico da conversa; em chats longos isso domina o tempo de render e
|
|
1113
|
+
* trava a digitacao. Aqui cada mensagem tem suas linhas memorizadas (chaveadas
|
|
1114
|
+
* por texto/largura/tema + o estado que muda o desenho) e so as mensagens que
|
|
1115
|
+
* caem no viewport sao realmente remontadas.
|
|
1116
|
+
*/
|
|
1117
|
+
const MESSAGE_CACHE = new Map();
|
|
1118
|
+
const MESSAGE_CACHE_MAX = 600;
|
|
1119
|
+
|
|
1120
|
+
/** Chave de cache de uma mensagem: tudo que muda o desenho dela. */
|
|
1121
|
+
function messageCacheKey(message, width, state) {
|
|
1122
|
+
const streaming = state.streaming;
|
|
1123
|
+
const extra =
|
|
1124
|
+
message.role === "tool"
|
|
1125
|
+
? `${message.status || ""}\u0001${message.duration || ""}\u0001${state.toolCompact ? 1 : 0}`
|
|
1126
|
+
: message.role === "thinking"
|
|
1127
|
+
? `${message.done ? 1 : 0}`
|
|
1128
|
+
: "";
|
|
1129
|
+
return `${width}\u0000${state.theme}\u0000${message.role}\u0000${extra}\u0000${message.text}`;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
/** Linhas de UMA mensagem (com cache). */
|
|
1133
|
+
function messageBlock(message, width, state) {
|
|
1134
|
+
const key = messageCacheKey(message, width, state);
|
|
1135
|
+
const hit = MESSAGE_CACHE.get(key);
|
|
1136
|
+
if (hit) return hit;
|
|
1137
|
+
const out = messageBlockUncached(message, width, state);
|
|
1138
|
+
if (MESSAGE_CACHE.size >= MESSAGE_CACHE_MAX) {
|
|
1139
|
+
MESSAGE_CACHE.delete(MESSAGE_CACHE.keys().next().value);
|
|
1140
|
+
}
|
|
1141
|
+
MESSAGE_CACHE.set(key, out);
|
|
1142
|
+
return out;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
function messageBlockUncached(message, width, state) {
|
|
1086
1146
|
const lines = [];
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
const mark = message.interrupted ? "⏹" : "!";
|
|
1114
|
-
for (const line of wrap(message.text, width - 4)) lines.push(color(indent(2) + mark + " " + line));
|
|
1115
|
-
}
|
|
1147
|
+
if (message.role === "user") {
|
|
1148
|
+
lines.push("");
|
|
1149
|
+
const body = wrap(message.text, width - 2);
|
|
1150
|
+
lines.push(indent(2) + chalk.cyan("▌ ") + chalk.bold(body[0] || ""));
|
|
1151
|
+
for (let index = 1; index < body.length; index += 1) {
|
|
1152
|
+
lines.push(indent(4) + chalk.bold(body[index]));
|
|
1153
|
+
}
|
|
1154
|
+
} else if (message.role === "compaction") {
|
|
1155
|
+
lines.push(...compactionLines(message, width, state));
|
|
1156
|
+
} else if (message.role === "assistant") {
|
|
1157
|
+
lines.push("");
|
|
1158
|
+
lines.push(...markdownLines(message.text, width - 2, state.theme));
|
|
1159
|
+
const meta = [];
|
|
1160
|
+
if (message.interrupted) meta.push("interrompido");
|
|
1161
|
+
if (message.ms) meta.push(`${(message.ms / 1000).toFixed(1)}s`);
|
|
1162
|
+
if (meta.length) lines.push("");
|
|
1163
|
+
if (meta.length) lines.push(indent(2) + chalk.dim(meta.join(" · ")));
|
|
1164
|
+
} else if (message.role === "thinking") {
|
|
1165
|
+
lines.push(...thinkingLines(message, width));
|
|
1166
|
+
} else if (message.role === "tool") {
|
|
1167
|
+
lines.push(...toolCardLines(message, width, state));
|
|
1168
|
+
} else {
|
|
1169
|
+
lines.push("");
|
|
1170
|
+
const color = message.error ? chalk.red : message.interrupted ? chalk.yellow : chalk.dim;
|
|
1171
|
+
const mark = message.interrupted ? "⏹" : "!";
|
|
1172
|
+
for (const line of wrap(message.text, width - 4)) lines.push(color(indent(2) + mark + " " + line));
|
|
1116
1173
|
}
|
|
1174
|
+
return lines;
|
|
1175
|
+
}
|
|
1117
1176
|
|
|
1118
|
-
|
|
1177
|
+
/**
|
|
1178
|
+
* "Cauda" da conversa: bloco de streaming, indicador de "trabalhando" etc.
|
|
1179
|
+
* Fica separado do historico porque muda a cada token e nao vale cachear.
|
|
1180
|
+
*/
|
|
1181
|
+
function messageTailLines(state, width) {
|
|
1182
|
+
const lines = [];
|
|
1119
1183
|
const streaming = state.streaming;
|
|
1120
1184
|
if (streaming && (streaming.reasoning || streaming.text)) {
|
|
1121
1185
|
if (streaming.reasoning) {
|
|
@@ -1134,7 +1198,6 @@ function messageLines(state, width) {
|
|
|
1134
1198
|
lines.push(...markdownLines(streaming.text, width - 2, state.theme));
|
|
1135
1199
|
}
|
|
1136
1200
|
}
|
|
1137
|
-
|
|
1138
1201
|
if (state.busy && !streaming?.text) {
|
|
1139
1202
|
lines.push("");
|
|
1140
1203
|
lines.push(indent(2) + chalk.dim("trabalhando" + ".".repeat(state.frame % 4)));
|
|
@@ -1142,6 +1205,47 @@ function messageLines(state, width) {
|
|
|
1142
1205
|
return lines;
|
|
1143
1206
|
}
|
|
1144
1207
|
|
|
1208
|
+
/**
|
|
1209
|
+
* Monta o corpo da conversa gastando o minimo: processa mensagens de tras pra
|
|
1210
|
+
* frente ate cobrir o viewport pedido (ou ate acabar o historico). Fora do
|
|
1211
|
+
* viewport nem tocamos no markdown. Devolve o array ja "padded" no topo para
|
|
1212
|
+
* preencher a altura do terminal.
|
|
1213
|
+
*/
|
|
1214
|
+
function messageLines(state, width, budget = Infinity) {
|
|
1215
|
+
const need = budget === Infinity ? Infinity : Math.max(0, budget);
|
|
1216
|
+
const blocks = [];
|
|
1217
|
+
let total = 0;
|
|
1218
|
+
|
|
1219
|
+
const tail = messageTailLines(state, width);
|
|
1220
|
+
if (tail.length) {
|
|
1221
|
+
blocks.push({ start: 0, lines: tail });
|
|
1222
|
+
total += tail.length;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
for (let index = state.messages.length - 1; index >= 0; index -= 1) {
|
|
1226
|
+
const message = state.messages[index];
|
|
1227
|
+
const lines = messageBlock(message, width, state);
|
|
1228
|
+
blocks.push({ lines });
|
|
1229
|
+
total += lines.length;
|
|
1230
|
+
// Ja cobrimos o viewport (com uma folga para o padding do topo)? Paramos.
|
|
1231
|
+
if (total >= need + 4) break;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
const lines = [];
|
|
1235
|
+
for (let index = blocks.length - 1; index >= 0; index -= 1) {
|
|
1236
|
+
lines.push(...blocks[index].lines);
|
|
1237
|
+
}
|
|
1238
|
+
return lines;
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
function messageLinesFull(state, width) {
|
|
1242
|
+
const lines = [];
|
|
1243
|
+
for (const message of state.messages) lines.push(...messageBlock(message, width, state));
|
|
1244
|
+
lines.push(...messageTailLines(state, width));
|
|
1245
|
+
return lines;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
|
|
1145
1249
|
function filteredCommands(state, commands) {
|
|
1146
1250
|
if (state.menuDismissed) return [];
|
|
1147
1251
|
if (!state.input.startsWith("/") || state.input.includes(" ")) return [];
|
|
@@ -1228,6 +1332,22 @@ function sidebarLines(state, width) {
|
|
|
1228
1332
|
lines.push(chalk.dim(`${formatTokens(tokens)} tokens · ${messages} msg${messages === 1 ? "" : "s"}`));
|
|
1229
1333
|
lines.push(chalk.dim(state.balance == null ? "saldo --" : `R$ ${(state.balance / 1_000_000).toFixed(2)}`));
|
|
1230
1334
|
|
|
1335
|
+
// Saldo dos provedores externos (unlid etc.): so aparece quando o backend
|
|
1336
|
+
// consegue ler. Moeda vem do provedor; "sem consulta" quando falha.
|
|
1337
|
+
if (state.balanceLoading) lines.push(chalk.dim("atualizando…"));
|
|
1338
|
+
for (const item of state.providerBalances || []) {
|
|
1339
|
+
const label = truncate(item.label || item.id || "provider", Math.max(6, width - 12));
|
|
1340
|
+
if (item.ok && item.balance != null) {
|
|
1341
|
+
const money =
|
|
1342
|
+
item.currency === "BRL"
|
|
1343
|
+
? `R$ ${Number(item.balance).toFixed(2)}`
|
|
1344
|
+
: `${Number(item.balance).toFixed(2)} ${item.currency || "USD"}`;
|
|
1345
|
+
lines.push(chalk.dim(`${label} `) + chalk.cyan(money));
|
|
1346
|
+
} else {
|
|
1347
|
+
lines.push(chalk.dim(`${label} `) + chalk.dim("--"));
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1231
1351
|
lines.push("");
|
|
1232
1352
|
lines.push(border(state, "MCP"));
|
|
1233
1353
|
if (state.mcp.loading) {
|
|
@@ -1408,6 +1528,29 @@ function box(state, { title, body, hint, width }) {
|
|
|
1408
1528
|
|
|
1409
1529
|
function approvalBody(state, width) {
|
|
1410
1530
|
const { name, args, change } = state.approval;
|
|
1531
|
+
|
|
1532
|
+
// Pedido de pareamento do celular: mesmo widget de opcoes, conteudo proprio.
|
|
1533
|
+
if (name === "__pair__") {
|
|
1534
|
+
const body = [];
|
|
1535
|
+
const who = change?.device || "celular";
|
|
1536
|
+
body.push(chalk.bold("Um celular quer entrar nesta conversa."));
|
|
1537
|
+
body.push("");
|
|
1538
|
+
if (change?.title) body.push(truncate("conversa: " + change.title, width - 4));
|
|
1539
|
+
if (change?.base) body.push(chalk.dim(truncate(change.base, width - 4)));
|
|
1540
|
+
body.push(chalk.dim(truncate("dispositivo: " + who, width - 4)));
|
|
1541
|
+
body.push("");
|
|
1542
|
+
const optionsStart = body.length;
|
|
1543
|
+
const opts = state.approval.options || APPROVAL_OPTIONS;
|
|
1544
|
+
for (const [index, option] of opts.entries()) {
|
|
1545
|
+
const selected = index === state.approval.index;
|
|
1546
|
+
const line = padRight(`${selected ? "▸ " : " "}${option.label}`, width - 6);
|
|
1547
|
+
if (selected) body.push(chalk.inverse(chalk.bold(line)));
|
|
1548
|
+
else if (option.id === "deny") body.push(chalk.dim(line));
|
|
1549
|
+
else body.push(line);
|
|
1550
|
+
}
|
|
1551
|
+
return { title: "Pareamento do celular", body, optionsStart };
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1411
1554
|
const meta = toolMeta(name);
|
|
1412
1555
|
const body = [];
|
|
1413
1556
|
|
|
@@ -1613,7 +1756,7 @@ function buildFrame(state, cols, rows, commands = {}) {
|
|
|
1613
1756
|
const bodyHeight = Math.max(0, rows - head.length - chrome.length);
|
|
1614
1757
|
state.pageSize = Math.max(1, bodyHeight);
|
|
1615
1758
|
|
|
1616
|
-
let body = hero ? [] : messageLines(state, inner);
|
|
1759
|
+
let body = hero ? [] : messageLines(state, inner, bodyHeight + state.scroll);
|
|
1617
1760
|
const maxScroll = Math.max(0, body.length - bodyHeight);
|
|
1618
1761
|
state.scroll = clamp(state.scroll, 0, maxScroll);
|
|
1619
1762
|
const end = body.length - state.scroll;
|
|
@@ -1732,19 +1875,35 @@ async function runTUI({
|
|
|
1732
1875
|
renderTimer.unref?.();
|
|
1733
1876
|
}
|
|
1734
1877
|
|
|
1878
|
+
// Digitar/apagar dispara uma tecla por evento: render() sincrono a cada tecla
|
|
1879
|
+
// reconstroi a conversa inteira e trava o input em chats longos. Este
|
|
1880
|
+
// agendador curto (~60fps) mantem a resposta visual imediata e coalesce
|
|
1881
|
+
// rajadas (segurar backspace/colar) num unico frame.
|
|
1882
|
+
let inputTimer = null;
|
|
1883
|
+
function scheduleInputRender() {
|
|
1884
|
+
if (inputTimer || closed) return;
|
|
1885
|
+
inputTimer = setTimeout(() => {
|
|
1886
|
+
inputTimer = null;
|
|
1887
|
+
render();
|
|
1888
|
+
}, 16);
|
|
1889
|
+
inputTimer.unref?.();
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1735
1892
|
const mouseMode = () => (state.mouse ? "\x1b[?1000h\x1b[?1006h" : "\x1b[?1000l\x1b[?1006l");
|
|
1736
1893
|
const enterScreen = () =>
|
|
1737
1894
|
write(`\x1b[?1049h\x1b[?25l\x1b[H\x1b[2J${mouseMode()}\x1b[?2004h`);
|
|
1738
1895
|
const leaveScreen = () =>
|
|
1739
1896
|
write("\x1b[?2004l\x1b[?1000l\x1b[?1006l\x1b[?25h\x1b[?1049l");
|
|
1740
1897
|
|
|
1741
|
-
async function refreshAccount() {
|
|
1898
|
+
async function refreshAccount({ fresh = false } = {}) {
|
|
1742
1899
|
const token = await ensureSession();
|
|
1743
1900
|
if (!token) return;
|
|
1744
|
-
const response = await fetchApi(
|
|
1901
|
+
const response = await fetchApi(`/api/bootstrap${fresh ? "?fresh=1" : ""}`).catch(() => null);
|
|
1745
1902
|
if (!response || !response.ok) return;
|
|
1746
1903
|
const data = await response.json().catch(() => null);
|
|
1747
1904
|
if (data?.user) state.balance = data.user.balance_micros;
|
|
1905
|
+
// Saldo (credits) dos provedores externos, ex.: unlid/uncensored.
|
|
1906
|
+
state.providerBalances = Array.isArray(data?.provider_balances) ? data.provider_balances : [];
|
|
1748
1907
|
|
|
1749
1908
|
if (!state.model) {
|
|
1750
1909
|
const pricing = await fetchApi("/api/public/pricing")
|
|
@@ -1754,6 +1913,19 @@ async function runTUI({
|
|
|
1754
1913
|
}
|
|
1755
1914
|
}
|
|
1756
1915
|
|
|
1916
|
+
/** Atalho/botão de atualizar: re-busca saldo da conta e dos provedores. */
|
|
1917
|
+
async function refreshBalance() {
|
|
1918
|
+
if (state.balanceLoading) return;
|
|
1919
|
+
state.balanceLoading = true;
|
|
1920
|
+
render();
|
|
1921
|
+
try {
|
|
1922
|
+
await refreshAccount({ fresh: true });
|
|
1923
|
+
} finally {
|
|
1924
|
+
state.balanceLoading = false;
|
|
1925
|
+
render();
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1757
1929
|
function applyMcp() {
|
|
1758
1930
|
state.mcp.statuses = mcp.statuses();
|
|
1759
1931
|
state.mcp.tools = state.mcp.statuses.reduce((total, item) => total + item.tools, 0);
|
|
@@ -2420,14 +2592,14 @@ async function runTUI({
|
|
|
2420
2592
|
text: "Interrompido por você (Esc).",
|
|
2421
2593
|
interrupted: true,
|
|
2422
2594
|
});
|
|
2423
|
-
if (result.messages) state.context = result.messages;
|
|
2595
|
+
if (result.messages) state.context = trimHistory(result.messages);
|
|
2424
2596
|
} else if (result.ok) {
|
|
2425
2597
|
const text = String(result.text || "").trim();
|
|
2426
2598
|
// O stream ja foi commitado em finishStreaming(): so empurra aqui se for
|
|
2427
2599
|
// texto novo (ou se nao houve streaming, ex.: resposta sem onText).
|
|
2428
2600
|
if (text) commitFinalText(state, text, result.ms);
|
|
2429
2601
|
state.streamedText = "";
|
|
2430
|
-
if (result.messages) state.context = result.messages;
|
|
2602
|
+
if (result.messages) state.context = trimHistory(result.messages);
|
|
2431
2603
|
// Reconectou de verdade: limpa o alerta e os erros de rede pendentes.
|
|
2432
2604
|
clearRetry();
|
|
2433
2605
|
} else {
|
|
@@ -2510,10 +2682,15 @@ async function runTUI({
|
|
|
2510
2682
|
const pending = state.approval;
|
|
2511
2683
|
if (!pending) return;
|
|
2512
2684
|
state.approval = null;
|
|
2513
|
-
|
|
2514
|
-
|
|
2685
|
+
// O pedido de pareamento NAO mexe no autoApprove de ferramentas: "lembrar"
|
|
2686
|
+
// ali significa confiar naquele navegador, tratado por quem criou o pedido.
|
|
2687
|
+
const isPair = pending.name === "__pair__";
|
|
2688
|
+
if (decision === "always" && !isPair) state.autoApprove = true;
|
|
2689
|
+
if (decision === "always" && !isPair) persist();
|
|
2515
2690
|
render();
|
|
2516
|
-
|
|
2691
|
+
// Pareamento precisa do texto da decisao (once/always/deny); ferramentas
|
|
2692
|
+
// so querem o booleano "aprovado?".
|
|
2693
|
+
pending.resolve(isPair ? decision : decision !== "deny");
|
|
2517
2694
|
}
|
|
2518
2695
|
|
|
2519
2696
|
// Com captura de mouse ligada o terminal entrega a roda para a TUI, mas em
|
|
@@ -2589,11 +2766,12 @@ async function runTUI({
|
|
|
2589
2766
|
state.cursor = cursor == null ? value.length : clamp(cursor, 0, value.length);
|
|
2590
2767
|
state.menuIndex = 0;
|
|
2591
2768
|
state.menuDismissed = false;
|
|
2592
|
-
|
|
2769
|
+
scheduleInputRender();
|
|
2593
2770
|
}
|
|
2594
2771
|
|
|
2595
2772
|
function moveApproval(delta) {
|
|
2596
|
-
const
|
|
2773
|
+
const opts = state.approval.options || APPROVAL_OPTIONS;
|
|
2774
|
+
const count = opts.length;
|
|
2597
2775
|
state.approval.index = (state.approval.index + delta + count) % count;
|
|
2598
2776
|
render();
|
|
2599
2777
|
}
|
|
@@ -2610,10 +2788,11 @@ async function runTUI({
|
|
|
2610
2788
|
|
|
2611
2789
|
// Aprovacao de ferramenta: navega e confirma.
|
|
2612
2790
|
if (state.approval) {
|
|
2791
|
+
const opts = state.approval.options || APPROVAL_OPTIONS;
|
|
2613
2792
|
if (key.name === "up" || key.name === "down" || key.name === "tab") {
|
|
2614
2793
|
return moveApproval(key.name === "up" ? -1 : 1);
|
|
2615
2794
|
}
|
|
2616
|
-
if (key.name === "return") return settleApproval(
|
|
2795
|
+
if (key.name === "return") return settleApproval(opts[state.approval.index].id);
|
|
2617
2796
|
if (key.name === "escape") return settleApproval("deny");
|
|
2618
2797
|
if (key.name === "a") return settleApproval("always");
|
|
2619
2798
|
if (key.name === "y") return settleApproval("once");
|
|
@@ -2652,6 +2831,7 @@ async function runTUI({
|
|
|
2652
2831
|
|
|
2653
2832
|
if (key.name === "f2") return toggleMouse();
|
|
2654
2833
|
if (key.ctrl && key.name === "y") return copyLastAnswer();
|
|
2834
|
+
if (key.ctrl && key.name === "r") return refreshBalance();
|
|
2655
2835
|
|
|
2656
2836
|
// Modal de texto: qualquer tecla fecha — e se for texto, ele vai para o
|
|
2657
2837
|
// input em vez de ser engolido (quem digita nao perde o que escreveu).
|