kafox-cli 0.6.0 → 0.8.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/README.md +61 -5
- package/bin/kafox.js +85 -2
- package/package.json +1 -1
- package/src/agent.js +204 -9
- package/src/auth.js +77 -30
- package/src/cli.js +32 -2
- package/src/gateway.js +103 -9
- package/src/login-screen.js +203 -0
- package/src/models.js +1 -4
- package/src/runner.js +98 -0
- package/src/sessions.js +172 -0
- package/src/storage.js +2 -1
- package/src/subagents.js +156 -0
- package/src/tools.js +12 -0
- package/src/tui.js +216 -44
package/src/tui.js
CHANGED
|
@@ -23,6 +23,7 @@ const { logoLines, LOGO_WIDTH, LOGO_HEIGHT } = require("./logo");
|
|
|
23
23
|
const { diffBlocks } = require("./diff");
|
|
24
24
|
const { toolMeta, parseCommandOutput } = require("./tools");
|
|
25
25
|
const { McpHub } = require("./mcp");
|
|
26
|
+
const sessions = require("./sessions");
|
|
26
27
|
|
|
27
28
|
const ANSI = /\x1b\[[0-9;]*m/g;
|
|
28
29
|
const stripAnsi = (value) => String(value).replace(ANSI, "");
|
|
@@ -508,6 +509,9 @@ function initialState() {
|
|
|
508
509
|
scroll: 0,
|
|
509
510
|
pageSize: 10,
|
|
510
511
|
mouse: true,
|
|
512
|
+
retry: null,
|
|
513
|
+
sessionId: null,
|
|
514
|
+
sessionStartedAt: null,
|
|
511
515
|
model: cfg.model || null,
|
|
512
516
|
theme: cfg.theme || "default",
|
|
513
517
|
suspended: false,
|
|
@@ -851,7 +855,7 @@ function messageLines(state, width) {
|
|
|
851
855
|
|
|
852
856
|
if (state.busy && !streaming?.text) {
|
|
853
857
|
lines.push("");
|
|
854
|
-
lines.push(indent(2) + chalk.dim("
|
|
858
|
+
lines.push(indent(2) + chalk.dim("trabalhando" + ".".repeat(state.frame % 4)));
|
|
855
859
|
}
|
|
856
860
|
return lines;
|
|
857
861
|
}
|
|
@@ -962,6 +966,23 @@ function queueLines(state, width) {
|
|
|
962
966
|
return rows;
|
|
963
967
|
}
|
|
964
968
|
|
|
969
|
+
function modelLine(state, width) {
|
|
970
|
+
const model = state.model || "auto";
|
|
971
|
+
// O aviso do momento tem prioridade sobre a versao: com nome de modelo longo
|
|
972
|
+
// a linha trunca e a mensagem util precisa aparecer primeiro.
|
|
973
|
+
let label = `${model} · kafox v${state.version}`;
|
|
974
|
+
if (state.escArmed) {
|
|
975
|
+
label = `${model} · Esc de novo para parar`;
|
|
976
|
+
} else if (state.retry) {
|
|
977
|
+
const left = Math.max(1, Math.ceil(((state.retry.endsAt || 0) - Date.now()) / 1000));
|
|
978
|
+
label = `${model} · reconectando em ${left}s (tentativa ${state.retry.attempt}/${state.retry.maxAttempts})`;
|
|
979
|
+
}
|
|
980
|
+
return paintPanel(
|
|
981
|
+
state.theme,
|
|
982
|
+
padRight(indent(2) + chalk.dim(truncate(label, width - 2)), width)
|
|
983
|
+
);
|
|
984
|
+
}
|
|
985
|
+
|
|
965
986
|
function inputBlock(state, width, { hero = false, tight = false } = {}) {
|
|
966
987
|
const available = width - 4;
|
|
967
988
|
const start =
|
|
@@ -975,36 +996,23 @@ function inputBlock(state, width, { hero = false, tight = false } = {}) {
|
|
|
975
996
|
if (state.input) {
|
|
976
997
|
body = prompt + shown;
|
|
977
998
|
} else {
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
: "Pergunte alguma coisa...";
|
|
999
|
+
// Enquanto a IA trabalha os pontinhos correm; parada, a caixa fica limpa.
|
|
1000
|
+
const placeholder = state.busy ? `trabalhando${".".repeat(state.frame % 4)}` : "";
|
|
981
1001
|
body = prompt + chalk.dim(truncate(placeholder, available));
|
|
982
1002
|
}
|
|
983
1003
|
|
|
984
|
-
const lines = [
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
const status = state.escArmed
|
|
990
|
-
? " Esc de novo para parar a IA"
|
|
991
|
-
: state.busy
|
|
992
|
-
? ` pensando${".".repeat(state.frame % 4)} · Esc 2× para parar`
|
|
993
|
-
: state.autoApprove
|
|
994
|
-
? " rodando tudo"
|
|
995
|
-
: "";
|
|
996
|
-
// Com o Esc armado a versao sai de cena: o aviso precisa caber na linha.
|
|
997
|
-
const label = state.escArmed
|
|
998
|
-
? `${model}${status}`
|
|
999
|
-
: `${model} · kafox v${state.version}${status}`;
|
|
1000
|
-
lines.push(paintPanel(state.theme, padRight(indent(2) + chalk.dim(truncate(label, width - 2)), width)));
|
|
1001
|
-
lines.push(rule(state, width));
|
|
1002
|
-
}
|
|
1004
|
+
const lines = [
|
|
1005
|
+
rule(state, width),
|
|
1006
|
+
paintPanel(state.theme, padRight(body, width)),
|
|
1007
|
+
rule(state, width),
|
|
1008
|
+
];
|
|
1003
1009
|
|
|
1004
1010
|
return {
|
|
1005
1011
|
lines,
|
|
1006
|
-
cursorRow,
|
|
1012
|
+
cursorRow: 1,
|
|
1007
1013
|
cursorCol: 4 + (state.cursor - start),
|
|
1014
|
+
// O modelo fica fora da caixa de texto, logo abaixo dela.
|
|
1015
|
+
footer: tight ? null : modelLine(state, width),
|
|
1008
1016
|
};
|
|
1009
1017
|
}
|
|
1010
1018
|
|
|
@@ -1132,13 +1140,20 @@ function buildOverlay(state, cols, rows) {
|
|
|
1132
1140
|
} else if (state.picker) {
|
|
1133
1141
|
const picker = state.picker;
|
|
1134
1142
|
const themePicker = picker.kind === "theme";
|
|
1135
|
-
|
|
1136
|
-
|
|
1143
|
+
const chatPicker = picker.kind === "chat";
|
|
1144
|
+
title = themePicker
|
|
1145
|
+
? `Tema (${picker.index + 1}/${picker.items.length})`
|
|
1146
|
+
: chatPicker
|
|
1147
|
+
? `Conversas (${picker.index + 1}/${picker.items.length})`
|
|
1148
|
+
: "Escolha o modelo";
|
|
1149
|
+
hint = chatPicker
|
|
1150
|
+
? "↑/↓ ou clique · Enter retoma · Esc cancela"
|
|
1151
|
+
: "↑/↓ ou clique · Enter aplica · Esc cancela";
|
|
1137
1152
|
|
|
1138
1153
|
if (picker.loading) {
|
|
1139
1154
|
body = [chalk.dim("carregando modelos...")];
|
|
1140
1155
|
} else {
|
|
1141
|
-
const height = Math.min(themePicker ? 9 : 8, picker.items.length);
|
|
1156
|
+
const height = Math.min(chatPicker ? 10 : themePicker ? 9 : 8, picker.items.length);
|
|
1142
1157
|
const start = clamp(
|
|
1143
1158
|
picker.index - Math.floor(height / 2),
|
|
1144
1159
|
0,
|
|
@@ -1152,10 +1167,18 @@ function buildOverlay(state, cols, rows) {
|
|
|
1152
1167
|
let line;
|
|
1153
1168
|
if (themePicker) {
|
|
1154
1169
|
line = padRight(` ${caret} ${padRight(item.id, 15)} ` + themeSwatch(item.id), width - 4);
|
|
1170
|
+
} else if (chatPicker) {
|
|
1171
|
+
// Formato do seletor: "titulo do chat - 28/10", com o codigo do
|
|
1172
|
+
// resume discreto na direita para quem quiser copiar.
|
|
1173
|
+
const right = chalk.dim(item.id);
|
|
1174
|
+
const space = width - 6;
|
|
1175
|
+
const room = Math.max(12, space - visible(right) - 12);
|
|
1176
|
+
const when = sessions.dayMonth(item.updatedAt);
|
|
1177
|
+
const left = ` ${caret} ${truncate(item.title || item.id, room)}${when ? ` - ${when}` : ""}`;
|
|
1178
|
+
const gap = Math.max(1, space - visible(left) - visible(right));
|
|
1179
|
+
line = padRight(left + " ".repeat(gap) + right, width - 4);
|
|
1155
1180
|
} else {
|
|
1156
|
-
|
|
1157
|
-
const label = padRight(` ${caret} ${item.id}`, Math.max(20, width - 28));
|
|
1158
|
-
line = padRight(label + price, width - 4);
|
|
1181
|
+
line = padRight(` ${caret} ${item.id} `, width - 4);
|
|
1159
1182
|
}
|
|
1160
1183
|
body.push(real === picker.index ? chalk.inverse(line) : line);
|
|
1161
1184
|
}
|
|
@@ -1231,7 +1254,14 @@ function buildFrame(state, cols, rows, commands = {}) {
|
|
|
1231
1254
|
const input = inputBlock(state, inner, { hero, tight });
|
|
1232
1255
|
const status = statusLines(state, inner, { bare });
|
|
1233
1256
|
const hints = hintsLines(state, inner, { bare });
|
|
1234
|
-
const chrome = [
|
|
1257
|
+
const chrome = [
|
|
1258
|
+
...queue,
|
|
1259
|
+
...menu,
|
|
1260
|
+
...input.lines,
|
|
1261
|
+
...(input.footer ? [input.footer] : []),
|
|
1262
|
+
...status,
|
|
1263
|
+
...hints,
|
|
1264
|
+
];
|
|
1235
1265
|
|
|
1236
1266
|
const head = hero ? heroLines(state, inner, rows - chrome.length) : [];
|
|
1237
1267
|
|
|
@@ -1297,15 +1327,45 @@ async function runTUI({
|
|
|
1297
1327
|
onCommand,
|
|
1298
1328
|
interactiveCommands = new Set(),
|
|
1299
1329
|
autoApprove = false,
|
|
1330
|
+
resume = null,
|
|
1300
1331
|
} = {}) {
|
|
1301
1332
|
const stdin = process.stdin;
|
|
1302
1333
|
const stdout = process.stdout;
|
|
1303
1334
|
const state = initialState();
|
|
1335
|
+
|
|
1336
|
+
// resume: "pick" abre o seletor de conversas, "last" pega a mais recente e
|
|
1337
|
+
// qualquer outro valor e o codigo do chat que o usuario fechou no Ctrl+C.
|
|
1338
|
+
let resumeMissing = null;
|
|
1339
|
+
let resumePick = false;
|
|
1340
|
+
if (resume) {
|
|
1341
|
+
if (resume === "pick") {
|
|
1342
|
+
resumePick = true;
|
|
1343
|
+
} else {
|
|
1344
|
+
const id = resume === "last" || resume === true ? sessions.latest()?.id : String(resume);
|
|
1345
|
+
const saved = resume === "last" || resume === true ? sessions.load(id) : sessions.find(id);
|
|
1346
|
+
if (saved) {
|
|
1347
|
+
state.sessionId = saved.id;
|
|
1348
|
+
state.sessionStartedAt = saved.startedAt;
|
|
1349
|
+
state.messages = Array.isArray(saved.messages) ? saved.messages : [];
|
|
1350
|
+
state.context = Array.isArray(saved.context) ? saved.context : [];
|
|
1351
|
+
if (saved.model) state.model = saved.model;
|
|
1352
|
+
if (saved.theme) state.theme = saved.theme;
|
|
1353
|
+
state.tokens = estimateTokens(state);
|
|
1354
|
+
} else {
|
|
1355
|
+
resumeMissing = String(id || resume);
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
if (!state.sessionId) {
|
|
1360
|
+
state.sessionId = sessions.newId();
|
|
1361
|
+
state.sessionStartedAt = new Date().toISOString();
|
|
1362
|
+
}
|
|
1304
1363
|
// `kafox --allow-all` (ou KAFOX_ALLOW_ALL=1) ja comeca sem pedir aprovacao.
|
|
1305
1364
|
state.autoApprove = Boolean(autoApprove) || process.env.KAFOX_ALLOW_ALL === "1";
|
|
1306
1365
|
let mcp = new McpHub({ cwd: process.cwd() });
|
|
1307
1366
|
let spinner = null;
|
|
1308
1367
|
let escTimer = null;
|
|
1368
|
+
let saveTimer = null;
|
|
1309
1369
|
let closed = false;
|
|
1310
1370
|
let previousLines = [];
|
|
1311
1371
|
|
|
@@ -1365,6 +1425,34 @@ async function runTUI({
|
|
|
1365
1425
|
render();
|
|
1366
1426
|
}
|
|
1367
1427
|
|
|
1428
|
+
/** Grava a conversa em ~/.kafox/chats para o --resume achar depois. */
|
|
1429
|
+
function persist() {
|
|
1430
|
+
if (!state.sessionId || closed) return;
|
|
1431
|
+
if (saveTimer) clearTimeout(saveTimer);
|
|
1432
|
+
saveTimer = setTimeout(() => {
|
|
1433
|
+
saveTimer = null;
|
|
1434
|
+
saveNow();
|
|
1435
|
+
}, 400);
|
|
1436
|
+
saveTimer.unref?.();
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
function saveNow() {
|
|
1440
|
+
if (!state.sessionId) return null;
|
|
1441
|
+
try {
|
|
1442
|
+
return sessions.save({
|
|
1443
|
+
id: state.sessionId,
|
|
1444
|
+
cwd: process.cwd(),
|
|
1445
|
+
model: state.model,
|
|
1446
|
+
theme: state.theme,
|
|
1447
|
+
messages: state.messages,
|
|
1448
|
+
context: state.context,
|
|
1449
|
+
startedAt: state.sessionStartedAt,
|
|
1450
|
+
});
|
|
1451
|
+
} catch {
|
|
1452
|
+
return null;
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1368
1456
|
function render() {
|
|
1369
1457
|
if (closed || state.suspended) return;
|
|
1370
1458
|
const cols = Math.max(30, stdout.columns || 80);
|
|
@@ -1406,11 +1494,7 @@ async function runTUI({
|
|
|
1406
1494
|
const pricing = await fetchApi("/api/public/pricing")
|
|
1407
1495
|
.then((res) => res.json())
|
|
1408
1496
|
.catch(() => null);
|
|
1409
|
-
state.models = (pricing?.pricing || []).map((row) => ({
|
|
1410
|
-
id: row.model,
|
|
1411
|
-
input: row.input_micros_per_1m,
|
|
1412
|
-
output: row.output_micros_per_1m,
|
|
1413
|
-
}));
|
|
1497
|
+
state.models = (pricing?.pricing || []).map((row) => ({ id: row.model }));
|
|
1414
1498
|
return state.models;
|
|
1415
1499
|
}
|
|
1416
1500
|
|
|
@@ -1449,6 +1533,41 @@ async function runTUI({
|
|
|
1449
1533
|
render();
|
|
1450
1534
|
}
|
|
1451
1535
|
|
|
1536
|
+
/** Modal com as conversas salvas (setas/mouse + Enter), igual ao de modelos. */
|
|
1537
|
+
function openChatPicker() {
|
|
1538
|
+
const items = sessions.list();
|
|
1539
|
+
if (!items.length) {
|
|
1540
|
+
state.modal = {
|
|
1541
|
+
title: "Conversas",
|
|
1542
|
+
text: `Nenhuma conversa salva ainda.\nFicam em ${sessions.chatsDir()}.`,
|
|
1543
|
+
};
|
|
1544
|
+
return render();
|
|
1545
|
+
}
|
|
1546
|
+
state.menuDismissed = true;
|
|
1547
|
+
state.picker = { kind: "chat", items, index: 0, loading: false };
|
|
1548
|
+
render();
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
/** Troca a sessao atual pela conversa escolhida no seletor. */
|
|
1552
|
+
function resumeChat(id) {
|
|
1553
|
+
const saved = sessions.find(id);
|
|
1554
|
+
state.picker = null;
|
|
1555
|
+
if (!saved) {
|
|
1556
|
+
state.modal = { title: "Resume", text: "Nao consegui abrir essa conversa." };
|
|
1557
|
+
return render();
|
|
1558
|
+
}
|
|
1559
|
+
state.sessionId = saved.id;
|
|
1560
|
+
state.sessionStartedAt = saved.startedAt;
|
|
1561
|
+
state.messages = Array.isArray(saved.messages) ? saved.messages : [];
|
|
1562
|
+
state.context = Array.isArray(saved.context) ? saved.context : [];
|
|
1563
|
+
if (saved.model) state.model = saved.model;
|
|
1564
|
+
if (saved.theme) state.theme = saved.theme;
|
|
1565
|
+
state.files = [];
|
|
1566
|
+
state.tokens = estimateTokens(state);
|
|
1567
|
+
state.scroll = 0;
|
|
1568
|
+
return render();
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1452
1571
|
async function openModelPicker() {
|
|
1453
1572
|
state.menuDismissed = true;
|
|
1454
1573
|
state.picker = { kind: "model", items: state.models, index: 0, loading: true };
|
|
@@ -1525,7 +1644,8 @@ async function runTUI({
|
|
|
1525
1644
|
const name = parts[0];
|
|
1526
1645
|
if (name === "/exit" || name === "/quit") return quit();
|
|
1527
1646
|
|
|
1528
|
-
|
|
1647
|
+
// /models e /model sem argumento abrem o mesmo seletor navegavel.
|
|
1648
|
+
if ((name === "/model" || name === "/models") && !parts[1]) return openModelPicker();
|
|
1529
1649
|
if (name === "/theme") return openThemePicker();
|
|
1530
1650
|
if (name === "/allow") {
|
|
1531
1651
|
const argument = (parts[1] || "on").toLowerCase();
|
|
@@ -1665,15 +1785,17 @@ async function runTUI({
|
|
|
1665
1785
|
state.streaming.reasoning += chunk;
|
|
1666
1786
|
scheduleRender();
|
|
1667
1787
|
},
|
|
1668
|
-
onToolStart: ({ name, args, label, change, fromMcp }) => {
|
|
1788
|
+
onToolStart: ({ id, name, args, label, change, fromMcp, subagent }) => {
|
|
1669
1789
|
finishStreaming();
|
|
1670
1790
|
state.messages.push({
|
|
1671
1791
|
role: "tool",
|
|
1672
1792
|
tool: {
|
|
1793
|
+
id,
|
|
1673
1794
|
name,
|
|
1674
1795
|
args,
|
|
1675
1796
|
label,
|
|
1676
1797
|
change,
|
|
1798
|
+
subagent,
|
|
1677
1799
|
meta: toolMeta(name, fromMcp ? mcp : null),
|
|
1678
1800
|
status: "running",
|
|
1679
1801
|
startedAt: Date.now(),
|
|
@@ -1682,10 +1804,34 @@ async function runTUI({
|
|
|
1682
1804
|
state.scroll = 0;
|
|
1683
1805
|
render();
|
|
1684
1806
|
},
|
|
1685
|
-
|
|
1807
|
+
onRetry: (info) => {
|
|
1808
|
+
state.retry = { ...info, endsAt: Date.now() + info.delay };
|
|
1809
|
+
scheduleRender();
|
|
1810
|
+
},
|
|
1811
|
+
onSubagentEvent: (event) => {
|
|
1812
|
+
const card = [...state.messages].reverse().find((message) => {
|
|
1813
|
+
if (message.role !== "tool" || !message.tool?.id) return false;
|
|
1814
|
+
return message.tool.id === event.id;
|
|
1815
|
+
});
|
|
1816
|
+
if (!card) return;
|
|
1817
|
+
const prefix = `subagente ${event.index}/${event.total}`;
|
|
1818
|
+
if (event.phase === "start") card.tool.summary = `${prefix}: ${event.objective}`;
|
|
1819
|
+
else if (event.phase === "tool") card.tool.summary = `${prefix}: ${event.tool} · ${event.text || ""}`;
|
|
1820
|
+
else if (event.phase === "tool-done") card.tool.summary = `${prefix}: ${event.tool} ${event.ok ? "ok" : "falhou"}`;
|
|
1821
|
+
else if (event.phase === "done")
|
|
1822
|
+
card.tool.summary = `${prefix} ${event.ok ? "concluído" : "falhou"} em ${event.ms}ms`;
|
|
1823
|
+
state.scroll = 0;
|
|
1824
|
+
scheduleRender();
|
|
1825
|
+
},
|
|
1826
|
+
onToolResult: ({ id, name, output, change, ok, skipped, summary, ms }) => {
|
|
1686
1827
|
const card = [...state.messages]
|
|
1687
1828
|
.reverse()
|
|
1688
|
-
.find(
|
|
1829
|
+
.find(
|
|
1830
|
+
(message) =>
|
|
1831
|
+
message.role === "tool" &&
|
|
1832
|
+
message.tool.status === "running" &&
|
|
1833
|
+
(id ? message.tool.id === id : message.tool.name === name)
|
|
1834
|
+
);
|
|
1689
1835
|
if (card) {
|
|
1690
1836
|
card.tool.status = "done";
|
|
1691
1837
|
card.tool.ok = ok;
|
|
@@ -1742,8 +1888,10 @@ async function runTUI({
|
|
|
1742
1888
|
error: true,
|
|
1743
1889
|
});
|
|
1744
1890
|
}
|
|
1891
|
+
state.retry = null;
|
|
1745
1892
|
state.tokens = estimateTokens(state);
|
|
1746
1893
|
state.scroll = 0;
|
|
1894
|
+
persist();
|
|
1747
1895
|
render();
|
|
1748
1896
|
}
|
|
1749
1897
|
|
|
@@ -1873,12 +2021,12 @@ async function runTUI({
|
|
|
1873
2021
|
if (!state.picker) return;
|
|
1874
2022
|
const index = overlay.hit.start + offset;
|
|
1875
2023
|
if (!state.picker.items[index]) return;
|
|
1876
|
-
state.picker.index = index;
|
|
1877
|
-
|
|
1878
|
-
if (state.picker.kind === "theme") {
|
|
2024
|
+
state.picker.index = index; if (state.picker.kind === "theme") {
|
|
1879
2025
|
previewTheme(index);
|
|
1880
2026
|
return applyTheme();
|
|
1881
2027
|
}
|
|
2028
|
+
if (state.picker.kind === "chat") return resumeChat(state.picker.items[index]?.id);
|
|
2029
|
+
|
|
1882
2030
|
return selectModel();
|
|
1883
2031
|
}
|
|
1884
2032
|
|
|
@@ -1970,7 +2118,11 @@ async function runTUI({
|
|
|
1970
2118
|
if (themePicker) previewTheme(picker.index);
|
|
1971
2119
|
return render();
|
|
1972
2120
|
}
|
|
1973
|
-
if (key.name === "return")
|
|
2121
|
+
if (key.name === "return") {
|
|
2122
|
+
if (themePicker) return applyTheme();
|
|
2123
|
+
if (picker.kind === "chat") return resumeChat(picker.items[picker.index]?.id);
|
|
2124
|
+
return selectModel();
|
|
2125
|
+
}
|
|
1974
2126
|
if (key.name === "escape") {
|
|
1975
2127
|
if (themePicker) return cancelTheme();
|
|
1976
2128
|
state.picker = null;
|
|
@@ -2077,14 +2229,27 @@ async function runTUI({
|
|
|
2077
2229
|
if (spinner) clearInterval(spinner);
|
|
2078
2230
|
if (renderTimer) clearTimeout(renderTimer);
|
|
2079
2231
|
if (escTimer) clearTimeout(escTimer);
|
|
2232
|
+
if (saveTimer) clearTimeout(saveTimer);
|
|
2080
2233
|
stdin.removeListener("data", onData);
|
|
2081
2234
|
stdout.removeListener("resize", onResize);
|
|
2082
2235
|
try {
|
|
2083
2236
|
stdin.setRawMode(false);
|
|
2084
2237
|
} catch {}
|
|
2238
|
+
// Fechou no meio da conversa: guarda e mostra como voltar para ela.
|
|
2239
|
+
const hasChat = state.messages.some(
|
|
2240
|
+
(message) => message.role === "user" || message.role === "assistant"
|
|
2241
|
+
);
|
|
2242
|
+
if (hasChat) saveNow();
|
|
2085
2243
|
mcp.close();
|
|
2086
2244
|
leaveScreen();
|
|
2087
2245
|
write("\n");
|
|
2246
|
+
if (hasChat) {
|
|
2247
|
+
write(
|
|
2248
|
+
`${chalk.cyan(" Retome esta conversa:")} ${chalk.bold(
|
|
2249
|
+
`kafox --resume ${state.sessionId}`
|
|
2250
|
+
)}\n\n`
|
|
2251
|
+
);
|
|
2252
|
+
}
|
|
2088
2253
|
process.exit(0);
|
|
2089
2254
|
}
|
|
2090
2255
|
|
|
@@ -2103,7 +2268,14 @@ async function runTUI({
|
|
|
2103
2268
|
stdout.on("resize", onResize);
|
|
2104
2269
|
|
|
2105
2270
|
await refreshAccount();
|
|
2271
|
+
if (resumeMissing) {
|
|
2272
|
+
state.modal = {
|
|
2273
|
+
title: "Resume",
|
|
2274
|
+
text: `Nao achei a conversa "${resumeMissing}".\nVeja as salvas em ${sessions.chatsDir()}.`,
|
|
2275
|
+
};
|
|
2276
|
+
}
|
|
2106
2277
|
render();
|
|
2278
|
+
if (resumePick) openChatPicker();
|
|
2107
2279
|
connectMcp().catch(() => {});
|
|
2108
2280
|
|
|
2109
2281
|
spinner = setInterval(() => {
|