@danieltmn/tamnora 0.5.1 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +30 -0
- package/harness/packages/core/src/config/index.js +1 -0
- package/harness/packages/core/src/index.js +1 -0
- package/harness/packages/core/src/storage/sqlite.js +108 -2
- package/harness/packages/core/src/tools/labels.js +83 -0
- package/harness/packages/core/test/labels.test.js +34 -0
- package/harness/packages/server/public/app.js +527 -111
- package/harness/packages/server/public/index.html +36 -1
- package/harness/packages/server/public/styles.css +97 -4
- package/harness/packages/server/src/lib/channels/index.js +89 -0
- package/harness/packages/server/src/lib/channels/telegram.js +532 -0
- package/harness/packages/server/src/lib/http.js +3 -2
- package/harness/packages/server/src/lib/runManager.js +102 -0
- package/harness/packages/server/src/lib/tasks.js +126 -0
- package/harness/packages/server/src/routes/channels.js +47 -0
- package/harness/packages/server/src/routes/chat.js +166 -77
- package/harness/packages/server/src/routes/sessions.js +17 -0
- package/harness/packages/server/src/server.js +15 -16
- package/harness/packages/server/test/server.test.js +213 -1
- package/harness/packages/server/test/telegram.test.js +94 -0
- package/harness/tamnora.example.json +2 -1
- package/package.json +1 -1
- package/harness/packages/server/src/lib/telegram.js +0 -267
|
@@ -102,13 +102,14 @@ async function api(path, { method = 'GET', body } = {}) {
|
|
|
102
102
|
return res.json();
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
async function
|
|
106
|
-
const headers = {
|
|
107
|
-
if (
|
|
105
|
+
async function streamSse(url, { method = 'GET', body, signal } = {}, onEvent) {
|
|
106
|
+
const headers = {};
|
|
107
|
+
if (body) headers['Content-Type'] = 'application/json';
|
|
108
|
+
if (method !== 'GET' && csrfToken) headers['x-csrf'] = csrfToken;
|
|
108
109
|
const res = await fetch(url, {
|
|
109
|
-
method
|
|
110
|
+
method,
|
|
110
111
|
headers,
|
|
111
|
-
body: JSON.stringify(body),
|
|
112
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
112
113
|
signal,
|
|
113
114
|
});
|
|
114
115
|
if (!res.ok) {
|
|
@@ -133,9 +134,11 @@ async function streamPost(url, body, onEvent, signal) {
|
|
|
133
134
|
buffer = buffer.slice(index + 2);
|
|
134
135
|
let event = 'message';
|
|
135
136
|
let data = '';
|
|
137
|
+
let id = null;
|
|
136
138
|
for (const line of frame.split('\n')) {
|
|
137
139
|
if (line.startsWith('event:')) event = line.slice(6).trim();
|
|
138
140
|
else if (line.startsWith('data:')) data += line.slice(5).trim();
|
|
141
|
+
else if (line.startsWith('id:')) id = line.slice(3).trim();
|
|
139
142
|
}
|
|
140
143
|
let parsed = {};
|
|
141
144
|
if (data) {
|
|
@@ -145,11 +148,13 @@ async function streamPost(url, body, onEvent, signal) {
|
|
|
145
148
|
parsed = { raw: data };
|
|
146
149
|
}
|
|
147
150
|
}
|
|
148
|
-
onEvent(event, parsed);
|
|
151
|
+
onEvent(event, parsed, id);
|
|
149
152
|
}
|
|
150
153
|
}
|
|
151
154
|
}
|
|
152
155
|
|
|
156
|
+
const streamPost = (url, body, onEvent, signal) => streamSse(url, { method: 'POST', body, signal }, onEvent);
|
|
157
|
+
|
|
153
158
|
/* --- tabs ---------------------------------------------------------------- */
|
|
154
159
|
|
|
155
160
|
const moreTab = $('#more-tab');
|
|
@@ -173,7 +178,7 @@ function activateTab(name) {
|
|
|
173
178
|
hideMoreMenu();
|
|
174
179
|
if (name === 'runs') refreshRuns();
|
|
175
180
|
if (name === 'providers') refreshProviders();
|
|
176
|
-
if (name === 'ajustes') { refreshMcp(); refreshSkills(); }
|
|
181
|
+
if (name === 'ajustes') { refreshMcp(); refreshSkills(); refreshChannels(); }
|
|
177
182
|
}
|
|
178
183
|
|
|
179
184
|
document.querySelectorAll('.tab[data-tab]').forEach((t) =>
|
|
@@ -760,14 +765,21 @@ function copyMessage(message) {
|
|
|
760
765
|
if (write?.catch) write.catch(() => {});
|
|
761
766
|
}
|
|
762
767
|
|
|
763
|
-
async function
|
|
768
|
+
async function renderPane(id) {
|
|
764
769
|
const record = state.panes.get(id);
|
|
765
|
-
if (!record) return;
|
|
770
|
+
if (!record) return null;
|
|
766
771
|
const detail = await api(`/api/sessions/${id}`).catch(() => null);
|
|
767
|
-
if (!detail) return;
|
|
772
|
+
if (!detail) return null;
|
|
768
773
|
record.el.replaceChildren();
|
|
769
|
-
|
|
774
|
+
renderStoredMessages(detail.messages, record.el);
|
|
770
775
|
if (!detail.messages.length) record.el.appendChild(el('div', 'empty', 'Sesión vacía.'));
|
|
776
|
+
return detail;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
async function reloadPane(id) {
|
|
780
|
+
const detail = await renderPane(id);
|
|
781
|
+
const record = state.panes.get(id);
|
|
782
|
+
if (detail?.running && record && !record.working) await attachRun(id, record.el);
|
|
771
783
|
}
|
|
772
784
|
|
|
773
785
|
async function regenerate(message) {
|
|
@@ -887,19 +899,165 @@ function renderToolOut(node, text) {
|
|
|
887
899
|
}
|
|
888
900
|
}
|
|
889
901
|
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
902
|
+
const READ_ONLY_TOOLS = new Set([
|
|
903
|
+
'read',
|
|
904
|
+
'list',
|
|
905
|
+
'glob',
|
|
906
|
+
'grep',
|
|
907
|
+
'fetch_url',
|
|
908
|
+
'git_status',
|
|
909
|
+
'git_diff',
|
|
910
|
+
'git_log',
|
|
911
|
+
'task',
|
|
912
|
+
'spawn_agent',
|
|
913
|
+
]);
|
|
914
|
+
const toolCardIndex = new Map();
|
|
915
|
+
|
|
916
|
+
function isReadOnlyTool(name) {
|
|
917
|
+
const value = String(name ?? '');
|
|
918
|
+
if (READ_ONLY_TOOLS.has(value)) return true;
|
|
919
|
+
return /^mcp__.+__(read|list|search|get|fetch|query)/i.test(value);
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
function shortText(value, max = 80) {
|
|
923
|
+
const raw = String(value ?? '').replace(/\s+/g, ' ').trim();
|
|
924
|
+
return raw.length > max ? `${raw.slice(0, max - 1)}…` : raw;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
function toolSummary(name, args) {
|
|
928
|
+
const a = args && typeof args === 'object' ? args : {};
|
|
929
|
+
switch (name) {
|
|
930
|
+
case 'read': {
|
|
931
|
+
const base = a.path ? shortText(a.path, 60) : '.';
|
|
932
|
+
const offset = Number(a.offset) || 0;
|
|
933
|
+
const limit = Number(a.limit) || 0;
|
|
934
|
+
if (offset && limit) return `${base}:${offset}-${offset + limit - 1}`;
|
|
935
|
+
if (offset) return `${base}:${offset}+`;
|
|
936
|
+
return base;
|
|
937
|
+
}
|
|
938
|
+
case 'write':
|
|
939
|
+
case 'edit':
|
|
940
|
+
return a.path ? shortText(a.path, 60) : 'archivo';
|
|
941
|
+
case 'list':
|
|
942
|
+
return a.path ? shortText(a.path, 60) : '.';
|
|
943
|
+
case 'glob':
|
|
944
|
+
return [a.pattern ? `"${shortText(a.pattern, 50)}"` : '', a.path ? `en ${shortText(a.path, 40)}` : '']
|
|
945
|
+
.filter(Boolean)
|
|
946
|
+
.join(' ');
|
|
947
|
+
case 'grep':
|
|
948
|
+
return [
|
|
949
|
+
a.pattern ? `"${shortText(a.pattern, 50)}"` : '',
|
|
950
|
+
a.include ? shortText(a.include, 30) : '',
|
|
951
|
+
a.path ? `en ${shortText(a.path, 40)}` : '',
|
|
952
|
+
]
|
|
953
|
+
.filter(Boolean)
|
|
954
|
+
.join(' ');
|
|
955
|
+
case 'bash':
|
|
956
|
+
return shortText(String(a.command ?? '').split('\n')[0], 90);
|
|
957
|
+
case 'fetch_url':
|
|
958
|
+
return shortText(a.url, 90);
|
|
959
|
+
case 'git_diff':
|
|
960
|
+
return [a.ref ? `vs ${shortText(a.ref, 30)}` : a.staged ? 'staged' : 'unstaged', a.path ? shortText(a.path, 40) : '']
|
|
961
|
+
.filter(Boolean)
|
|
962
|
+
.join(' ');
|
|
963
|
+
case 'git_log':
|
|
964
|
+
return a.limit ? `${a.limit} commits` : '';
|
|
965
|
+
case 'git_commit':
|
|
966
|
+
return shortText(a.message, 70);
|
|
967
|
+
case 'task':
|
|
968
|
+
case 'spawn_agent':
|
|
969
|
+
return shortText(a.description || a.prompt, 80);
|
|
970
|
+
default:
|
|
971
|
+
return '';
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
function parseToolArgs(call) {
|
|
976
|
+
const raw = call?.function?.arguments ?? call?.arguments;
|
|
977
|
+
if (raw && typeof raw === 'object') return raw;
|
|
978
|
+
if (typeof raw !== 'string') return {};
|
|
979
|
+
try {
|
|
980
|
+
return JSON.parse(raw);
|
|
981
|
+
} catch {
|
|
982
|
+
return {};
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
function previewText(value, maxLines = 24, maxChars = 4000) {
|
|
987
|
+
const content = typeof value === 'string' ? value : JSON.stringify(value ?? '', null, 2) ?? '';
|
|
988
|
+
const lines = content.split('\n');
|
|
989
|
+
let preview = lines.slice(0, maxLines).join('\n');
|
|
990
|
+
let truncated = lines.length > maxLines;
|
|
991
|
+
if (preview.length > maxChars) {
|
|
992
|
+
preview = preview.slice(0, maxChars);
|
|
993
|
+
truncated = true;
|
|
994
|
+
}
|
|
995
|
+
return { preview, truncated };
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
function ensureToolsToggle(message) {
|
|
999
|
+
if (!message.toolsToggle) {
|
|
1000
|
+
const bar = el('div', 'tools-actions');
|
|
1001
|
+
const label = el('span', 'tools-count');
|
|
1002
|
+
const toggle = button('colapsar todo', () => {
|
|
1003
|
+
const details = [...message.tools.querySelectorAll('details.tool-card')];
|
|
1004
|
+
const anyOpen = details.some((node) => node.open);
|
|
1005
|
+
for (const node of details) node.open = !anyOpen;
|
|
1006
|
+
toggle.textContent = anyOpen ? 'expandir todo' : 'colapsar todo';
|
|
1007
|
+
}, 'tools-toggle');
|
|
1008
|
+
bar.append(label, toggle);
|
|
1009
|
+
message.tools.prepend(bar);
|
|
1010
|
+
message.toolsToggle = bar;
|
|
1011
|
+
message.toolsCount = label;
|
|
1012
|
+
message.toolsToggleBtn = toggle;
|
|
1013
|
+
}
|
|
1014
|
+
const total = message.tools.querySelectorAll('details.tool-card').length;
|
|
1015
|
+
const groups = new Set([...message.tools.querySelectorAll('details.tool-card')].map((n) => n.dataset.round ?? ''));
|
|
1016
|
+
message.toolsToggle.hidden = total < 2;
|
|
1017
|
+
message.toolsCount.textContent = groups.size > 1 ? `${total} pasos` : `${total} herramientas`;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
async function loadFullToolResult(entry, message) {
|
|
1021
|
+
if (entry.full || !entry.callId) return;
|
|
1022
|
+
const sessionId = paneOf(message)?.dataset.session;
|
|
1023
|
+
if (!sessionId) return;
|
|
1024
|
+
entry.more.disabled = true;
|
|
1025
|
+
entry.more.textContent = 'cargando…';
|
|
1026
|
+
const data = await api(
|
|
1027
|
+
`/api/sessions/${sessionId}/tool-result?callId=${encodeURIComponent(entry.callId)}`
|
|
1028
|
+
).catch(() => null);
|
|
1029
|
+
entry.more.disabled = false;
|
|
1030
|
+
if (data?.content != null) {
|
|
1031
|
+
renderToolOut(entry.out, data.content);
|
|
1032
|
+
entry.full = true;
|
|
1033
|
+
entry.more.hidden = true;
|
|
1034
|
+
} else {
|
|
1035
|
+
entry.more.textContent = 'ver completo';
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
function addToolCard(message, name, args, { callId = null, open = null, round = null } = {}) {
|
|
1040
|
+
const collapsed = open === null ? isReadOnlyTool(name) : !open;
|
|
1041
|
+
const card = el('details', 'tool-card running');
|
|
1042
|
+
card.open = !collapsed;
|
|
1043
|
+
if (round != null) card.dataset.round = String(round);
|
|
1044
|
+
const head = el('summary', 'tool-head');
|
|
1045
|
+
const glyph = el('span', 'tool-glyph', '⏺');
|
|
1046
|
+
const argsEl = el('span', 'tool-args', toolSummary(name, args));
|
|
1047
|
+
head.append(glyph, el('span', 'tool-name', name), argsEl);
|
|
898
1048
|
const out = el('div', 'tool-out');
|
|
899
|
-
|
|
1049
|
+
const more = button('ver completo', () => loadFullToolResult(entry, message), 'tool-more');
|
|
1050
|
+
more.hidden = true;
|
|
1051
|
+
card.append(head, out, more);
|
|
900
1052
|
message.tools.appendChild(card);
|
|
1053
|
+
ensureToolsToggle(message);
|
|
901
1054
|
scrollMessages(paneOf(message));
|
|
902
|
-
|
|
1055
|
+
|
|
1056
|
+
const entry = { card, out, glyph, argsEl, more, callId, full: false, truncated: false };
|
|
1057
|
+
card.addEventListener('toggle', () => {
|
|
1058
|
+
if (card.open && entry.truncated && !entry.full) loadFullToolResult(entry, message);
|
|
1059
|
+
});
|
|
1060
|
+
return entry;
|
|
903
1061
|
}
|
|
904
1062
|
|
|
905
1063
|
function addPermissionCard(message, data) {
|
|
@@ -957,6 +1115,7 @@ function userImages(content) {
|
|
|
957
1115
|
}
|
|
958
1116
|
|
|
959
1117
|
function renderStored(message, pane) {
|
|
1118
|
+
if (message.agent_id) return;
|
|
960
1119
|
if (message.role === 'user') {
|
|
961
1120
|
const bubble = addMessage('user', userText(message.content), pane);
|
|
962
1121
|
if (message.id) {
|
|
@@ -972,16 +1131,31 @@ function renderStored(message, pane) {
|
|
|
972
1131
|
bubble.wrap.dataset.messageId = message.id;
|
|
973
1132
|
}
|
|
974
1133
|
for (const call of message.tool_calls ?? []) {
|
|
975
|
-
const
|
|
976
|
-
|
|
977
|
-
call.function?.name ?? call.name,
|
|
978
|
-
call.function?.arguments ?? call.arguments ?? ''
|
|
979
|
-
);
|
|
1134
|
+
const name = call.function?.name ?? call.name;
|
|
1135
|
+
const tool = addToolCard(bubble, name, parseToolArgs(call), { callId: call.id, open: false });
|
|
980
1136
|
tool.card.classList.remove('running');
|
|
981
1137
|
tool.card.classList.add('done');
|
|
982
|
-
tool.
|
|
1138
|
+
tool.glyph.textContent = '✓';
|
|
1139
|
+
if (call.id) toolCardIndex.set(call.id, tool);
|
|
983
1140
|
}
|
|
984
|
-
}
|
|
1141
|
+
} else if (message.role === 'tool') {
|
|
1142
|
+
const tool = toolCardIndex.get(message.tool_call_id);
|
|
1143
|
+
if (!tool) return;
|
|
1144
|
+
const failed = /^Error|^Permiso denegado/i.test(String(message.content));
|
|
1145
|
+
const { preview, truncated } = previewText(message.content);
|
|
1146
|
+
renderToolOut(tool.out, preview);
|
|
1147
|
+
tool.card.classList.remove('running');
|
|
1148
|
+
tool.card.classList.toggle('failed', failed);
|
|
1149
|
+
if (!failed) tool.card.classList.add('done');
|
|
1150
|
+
tool.glyph.textContent = failed ? '✗' : '✓';
|
|
1151
|
+
tool.truncated = truncated;
|
|
1152
|
+
if (truncated) tool.more.hidden = false;
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
function renderStoredMessages(messages, pane) {
|
|
1157
|
+
toolCardIndex.clear();
|
|
1158
|
+
for (const message of messages) renderStored(message, pane);
|
|
985
1159
|
}
|
|
986
1160
|
|
|
987
1161
|
function basename(p) {
|
|
@@ -1097,7 +1271,7 @@ async function removeProject(cwd, count) {
|
|
|
1097
1271
|
|
|
1098
1272
|
/* --- session tabs --------------------------------------------------------- */
|
|
1099
1273
|
|
|
1100
|
-
function registerSession(session, messages = [], tokens = { in: 0, out: 0 }) {
|
|
1274
|
+
function registerSession(session, messages = [], tokens = { in: 0, out: 0 }, running = false) {
|
|
1101
1275
|
const id = session.id;
|
|
1102
1276
|
if (state.panes.has(id)) return state.panes.get(id);
|
|
1103
1277
|
const pane = document.createElement('div');
|
|
@@ -1115,10 +1289,11 @@ function registerSession(session, messages = [], tokens = { in: 0, out: 0 }) {
|
|
|
1115
1289
|
};
|
|
1116
1290
|
state.panes.set(id, record);
|
|
1117
1291
|
state.tabOrder.push(id);
|
|
1118
|
-
|
|
1292
|
+
renderStoredMessages(messages, pane);
|
|
1119
1293
|
if (!messages.length) {
|
|
1120
1294
|
pane.appendChild(el('div', 'empty', `Sesión vacía${session.cwd ? ` en ${basename(session.cwd)}` : ''}.`));
|
|
1121
1295
|
}
|
|
1296
|
+
if (running) attachRun(id, pane);
|
|
1122
1297
|
return record;
|
|
1123
1298
|
}
|
|
1124
1299
|
|
|
@@ -1163,7 +1338,7 @@ async function openTab(id) {
|
|
|
1163
1338
|
return null;
|
|
1164
1339
|
});
|
|
1165
1340
|
if (!data) return;
|
|
1166
|
-
registerSession(data.session, data.messages, data.tokens);
|
|
1341
|
+
registerSession(data.session, data.messages, data.tokens, data.running);
|
|
1167
1342
|
activateSession(id);
|
|
1168
1343
|
}
|
|
1169
1344
|
|
|
@@ -1244,7 +1419,7 @@ async function restoreTabs() {
|
|
|
1244
1419
|
for (const id of ids) {
|
|
1245
1420
|
if (state.panes.has(id)) continue;
|
|
1246
1421
|
const data = await api(`/api/sessions/${id}`).catch(() => null);
|
|
1247
|
-
if (data) registerSession(data.session, data.messages, data.tokens);
|
|
1422
|
+
if (data) registerSession(data.session, data.messages, data.tokens, data.running);
|
|
1248
1423
|
}
|
|
1249
1424
|
const active = saved?.active && state.panes.has(saved.active) ? saved.active : state.tabOrder[state.tabOrder.length - 1];
|
|
1250
1425
|
if (active) activateSession(active);
|
|
@@ -1259,6 +1434,248 @@ $('#new-session').addEventListener('click', () => {
|
|
|
1259
1434
|
const INIT_PROMPT =
|
|
1260
1435
|
'Analizá este proyecto (estructura, stack, convenciones, comandos de build/test) y creá o actualizá un archivo AGENTS.md en la raíz con instrucciones claras para futuros agentes. Inspeccioná archivos reales antes de escribir.';
|
|
1261
1436
|
|
|
1437
|
+
function createChatTurn(pane, sessionKey) {
|
|
1438
|
+
const turn = {
|
|
1439
|
+
pane,
|
|
1440
|
+
sessionKey,
|
|
1441
|
+
active: null,
|
|
1442
|
+
sawText: false,
|
|
1443
|
+
lastTool: null,
|
|
1444
|
+
startNewOnText: false,
|
|
1445
|
+
agents: new Map(),
|
|
1446
|
+
turnMode: modeSelect.value || 'agent',
|
|
1447
|
+
assistant() {
|
|
1448
|
+
if (!turn.active) {
|
|
1449
|
+
turn.active = addMessage('assistant', '', turn.pane);
|
|
1450
|
+
setMessageMode(turn.active, turn.turnMode);
|
|
1451
|
+
}
|
|
1452
|
+
return turn.active;
|
|
1453
|
+
},
|
|
1454
|
+
flowTarget() {
|
|
1455
|
+
if (turn.startNewOnText) {
|
|
1456
|
+
turn.active = addMessage('assistant', '', turn.pane);
|
|
1457
|
+
setMessageMode(turn.active, turn.turnMode);
|
|
1458
|
+
turn.startNewOnText = false;
|
|
1459
|
+
setActivity(turn.active, 'thinking');
|
|
1460
|
+
}
|
|
1461
|
+
return turn.assistant();
|
|
1462
|
+
},
|
|
1463
|
+
};
|
|
1464
|
+
return turn;
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
function addAgentCard(message, data) {
|
|
1468
|
+
const card = el('details', 'agent-card running');
|
|
1469
|
+
const head = el('summary', 'agent-head');
|
|
1470
|
+
const glyph = el('span', 'agent-glyph', '⏺');
|
|
1471
|
+
const title = el('span', 'agent-title', `subagente · ${data.description ?? 'tarea'}`);
|
|
1472
|
+
const status = el('span', 'agent-status', data.mode === 'plan' ? 'solo lectura' : 'agente');
|
|
1473
|
+
head.append(glyph, title, status);
|
|
1474
|
+
const body = el('div', 'agent-body');
|
|
1475
|
+
const reasoning = el('details', 'reasoning');
|
|
1476
|
+
reasoning.appendChild(el('summary', '', 'razonamiento'));
|
|
1477
|
+
const reasoningEl = el('div', 'body');
|
|
1478
|
+
reasoning.appendChild(reasoningEl);
|
|
1479
|
+
reasoning.hidden = true;
|
|
1480
|
+
const textEl = el('div', 'agent-text');
|
|
1481
|
+
const toolsEl = el('div', 'agent-tools');
|
|
1482
|
+
body.append(reasoning, textEl, toolsEl);
|
|
1483
|
+
card.append(head, body);
|
|
1484
|
+
message.tools.appendChild(card);
|
|
1485
|
+
ensureToolsToggle(message);
|
|
1486
|
+
scrollMessages(paneOf(message));
|
|
1487
|
+
const pseudo = { tools: toolsEl, bubble: card, toolsToggle: null };
|
|
1488
|
+
return {
|
|
1489
|
+
card,
|
|
1490
|
+
glyph,
|
|
1491
|
+
statusEl: status,
|
|
1492
|
+
textEl,
|
|
1493
|
+
reasoning,
|
|
1494
|
+
reasoningEl,
|
|
1495
|
+
toolsEl,
|
|
1496
|
+
message: pseudo,
|
|
1497
|
+
lastTool: null,
|
|
1498
|
+
sawText: false,
|
|
1499
|
+
};
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
function handleAgentEvent(event, data, turn) {
|
|
1503
|
+
if (!data?.agentId) return false;
|
|
1504
|
+
const agent = turn.agents.get(data.agentId);
|
|
1505
|
+
if (!agent) return false;
|
|
1506
|
+
if (event === 'text') {
|
|
1507
|
+
agent.sawText = true;
|
|
1508
|
+
agent.textEl.innerHTML = renderMarkdown(data.full);
|
|
1509
|
+
scrollMessages(paneOf(agent.message));
|
|
1510
|
+
} else if (event === 'reasoning') {
|
|
1511
|
+
agent.reasoning.hidden = false;
|
|
1512
|
+
if (!agent.sawText) agent.reasoningEl.textContent = data.full;
|
|
1513
|
+
} else if (event === 'tool_call') {
|
|
1514
|
+
agent.lastTool = addToolCard(agent.message, data.name, data.args, { callId: data.id });
|
|
1515
|
+
} else if (event === 'tool_result') {
|
|
1516
|
+
if (agent.lastTool) {
|
|
1517
|
+
const failed = data.failed ?? /^Error|^Permiso denegado/i.test(String(data.preview ?? data.result));
|
|
1518
|
+
renderToolOut(agent.lastTool.out, data.preview ?? data.result);
|
|
1519
|
+
agent.lastTool.card.classList.remove('running');
|
|
1520
|
+
agent.lastTool.card.classList.toggle('failed', failed);
|
|
1521
|
+
if (!failed) agent.lastTool.card.classList.add('done');
|
|
1522
|
+
agent.lastTool.glyph.textContent = failed ? '✗' : '✓';
|
|
1523
|
+
if (data.truncated) {
|
|
1524
|
+
agent.lastTool.truncated = true;
|
|
1525
|
+
agent.lastTool.more.hidden = false;
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
} else {
|
|
1529
|
+
return false;
|
|
1530
|
+
}
|
|
1531
|
+
return true;
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
function formatDuration(ms) {
|
|
1535
|
+
const value = Number(ms) || 0;
|
|
1536
|
+
if (value <= 0) return '';
|
|
1537
|
+
return value >= 1000 ? `${(value / 1000).toFixed(1)}s` : `${value}ms`;
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
/** Cierre del turno: siempre deja un resumen visible (rondas, tools, tiempo). */
|
|
1541
|
+
function renderCompletion(message, data) {
|
|
1542
|
+
const tools = Number(data.toolCalls) || 0;
|
|
1543
|
+
const parts = [`${data.rounds ?? '?'} ronda${data.rounds === 1 ? '' : 's'}`];
|
|
1544
|
+
if (tools) parts.push(`${tools} herramienta${tools === 1 ? '' : 's'}`);
|
|
1545
|
+
const duration = formatDuration(data.durationMs);
|
|
1546
|
+
if (duration) parts.push(duration);
|
|
1547
|
+
if (data.tokens?.total_tokens) parts.push(`${Number(data.tokens.total_tokens).toLocaleString('es')} tokens`);
|
|
1548
|
+
const maxed = data.stopReason === 'max_rounds';
|
|
1549
|
+
message.tools.appendChild(el('div', maxed ? 'chip warn' : 'chip ok', `${maxed ? '⚠ máximo de rondas' : '✓ terminado'} · ${parts.join(' · ')}`));
|
|
1550
|
+
scrollMessages(paneOf(message));
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
function handleChatEvent(event, data, turn) {
|
|
1554
|
+
if (handleAgentEvent(event, data, turn)) return;
|
|
1555
|
+
if (event === 'agent_start') {
|
|
1556
|
+
turn.agents.set(data.agentId, addAgentCard(turn.assistant(), data));
|
|
1557
|
+
} else if (event === 'agent_end') {
|
|
1558
|
+
const agent = turn.agents.get(data.agentId);
|
|
1559
|
+
if (agent) {
|
|
1560
|
+
agent.card.classList.remove('running');
|
|
1561
|
+
agent.card.classList.add(data.status === 'error' ? 'failed' : 'done');
|
|
1562
|
+
agent.glyph.textContent = data.status === 'error' ? '✗' : '✓';
|
|
1563
|
+
agent.statusEl.textContent = data.status;
|
|
1564
|
+
if (data.content && !agent.sawText) agent.textEl.textContent = data.content;
|
|
1565
|
+
}
|
|
1566
|
+
} else if (event === 'start') {
|
|
1567
|
+
if (data.cwd) setCwd(data.cwd);
|
|
1568
|
+
if (data.mode) turn.turnMode = data.mode;
|
|
1569
|
+
if (turn.active) setMessageMode(turn.active, turn.turnMode);
|
|
1570
|
+
} else if (event === 'text') {
|
|
1571
|
+
turn.sawText = true;
|
|
1572
|
+
const target = turn.flowTarget();
|
|
1573
|
+
setText(target, data.full);
|
|
1574
|
+
setActivity(target, 'thinking');
|
|
1575
|
+
} else if (event === 'reasoning') {
|
|
1576
|
+
const target = turn.flowTarget();
|
|
1577
|
+
if (!turn.sawText) ensureReasoning(target).textContent = data.full;
|
|
1578
|
+
setActivity(target, 'thinking');
|
|
1579
|
+
} else if (event === 'tools') {
|
|
1580
|
+
turn.startNewOnText = true;
|
|
1581
|
+
} else if (event === 'tool_call') {
|
|
1582
|
+
const target = turn.assistant();
|
|
1583
|
+
setActivity(target, 'working');
|
|
1584
|
+
turn.lastTool = addToolCard(target, data.name, data.args, { callId: data.id });
|
|
1585
|
+
} else if (event === 'tool_result') {
|
|
1586
|
+
setActivity(turn.assistant(), 'thinking');
|
|
1587
|
+
if (turn.lastTool) {
|
|
1588
|
+
const failed = data.failed ?? /^Error|^Permiso denegado/i.test(String(data.preview ?? data.result));
|
|
1589
|
+
renderToolOut(turn.lastTool.out, data.preview ?? data.result);
|
|
1590
|
+
turn.lastTool.card.classList.remove('running');
|
|
1591
|
+
turn.lastTool.card.classList.toggle('failed', failed);
|
|
1592
|
+
if (!failed) turn.lastTool.card.classList.add('done');
|
|
1593
|
+
turn.lastTool.glyph.textContent = failed ? '✗' : '✓';
|
|
1594
|
+
if (data.truncated) {
|
|
1595
|
+
turn.lastTool.truncated = true;
|
|
1596
|
+
turn.lastTool.more.hidden = false;
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
} else if (event === 'permission') {
|
|
1600
|
+
setActivity(turn.assistant(), 'waiting');
|
|
1601
|
+
addPermissionCard(turn.assistant(), data);
|
|
1602
|
+
} else if (event === 'permission_resolved') {
|
|
1603
|
+
setActivity(turn.assistant(), 'thinking');
|
|
1604
|
+
const entry = state.permissionCards.get(data.askId);
|
|
1605
|
+
if (entry && entry.verdict.textContent === 'requiere aprobación') {
|
|
1606
|
+
entry.card.classList.add('collapsed', data.allow ? 'approved' : 'denied');
|
|
1607
|
+
entry.card.classList.remove('pending');
|
|
1608
|
+
entry.card.querySelector('.perm-icon').textContent = data.allow ? '✓' : '✗';
|
|
1609
|
+
entry.verdict.textContent = data.allow ? 'aprobado' : 'denegado (sin respuesta)';
|
|
1610
|
+
for (const btn of Object.values(entry.buttons)) btn.disabled = true;
|
|
1611
|
+
}
|
|
1612
|
+
} else if (event === 'stall') {
|
|
1613
|
+
if (!turn.sawText) setText(turn.assistant(), `⚠ ${data.seconds} s sin respuesta del modelo (proveedor caído o muy lento). Sigo esperando...`);
|
|
1614
|
+
} else if (event === 'retry') {
|
|
1615
|
+
if (!turn.sawText) setText(turn.assistant(), `↻ reintentando (${data.attempt}) en ${Math.round((data.delayMs || 0) / 1000)} s...`);
|
|
1616
|
+
} else if (event === 'trim') {
|
|
1617
|
+
turn.assistant().tools.appendChild(el('div', 'chip', `✂ contexto recortado (${data.trimmed} resultados, ${data.dropped} mensajes)`));
|
|
1618
|
+
} else if (event === 'warning') {
|
|
1619
|
+
turn.assistant().tools.appendChild(el('div', 'chip', `⚠ ${data.message}`));
|
|
1620
|
+
} else if (event === 'error') {
|
|
1621
|
+
setActivity(turn.active, null);
|
|
1622
|
+
turn.assistant().tools.appendChild(el('div', 'chip err', `✗ ${data.message}`));
|
|
1623
|
+
} else if (event === 'done') {
|
|
1624
|
+
setActivity(turn.active, null);
|
|
1625
|
+
const finalText = (turn.active?.textEl?.textContent ?? '').trim();
|
|
1626
|
+
const tools = Number(data.toolCalls) || 0;
|
|
1627
|
+
if (!finalText) {
|
|
1628
|
+
if (data.stopReason === 'max_rounds') {
|
|
1629
|
+
setText(
|
|
1630
|
+
turn.assistant(),
|
|
1631
|
+
`⚠ El agente alcanzó el máximo de rondas (${data.rounds ?? '?'}) sin dejar una respuesta final.${
|
|
1632
|
+
tools ? ` Hizo ${tools} llamadas a herramientas.` : ''
|
|
1633
|
+
} Pedile que continúe o subí \`maxRounds\`.`
|
|
1634
|
+
);
|
|
1635
|
+
} else if (tools) {
|
|
1636
|
+
setText(turn.assistant(), '✓ El agente terminó sus herramientas, pero no devolvió un mensaje final.');
|
|
1637
|
+
} else {
|
|
1638
|
+
setText(turn.assistant(), '(sin texto)');
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
renderCompletion(turn.active, data);
|
|
1642
|
+
updateStatus({ rounds: data.rounds, stopReason: data.stopReason });
|
|
1643
|
+
addTokens(data.tokens);
|
|
1644
|
+
} else if (event === 'aborted') {
|
|
1645
|
+
setActivity(turn.active, null);
|
|
1646
|
+
setText(turn.assistant(), '⏹ cancelado.');
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
/**
|
|
1651
|
+
* Se engancha a un run en curso: re-renderiza el historial persistido y
|
|
1652
|
+
* reproduce los eventos del turno actual (asi el progreso no se pierde si el
|
|
1653
|
+
* telefono bloqueo la pantalla o se corto la red).
|
|
1654
|
+
*/
|
|
1655
|
+
async function attachRun(sessionKey, pane) {
|
|
1656
|
+
const record = state.panes.get(sessionKey);
|
|
1657
|
+
if (record?.working) return;
|
|
1658
|
+
await renderPane(sessionKey);
|
|
1659
|
+
const controller = new AbortController();
|
|
1660
|
+
controllers.set(sessionKey, controller);
|
|
1661
|
+
if (record) setWorking(sessionKey, true);
|
|
1662
|
+
const turn = createChatTurn(pane, sessionKey);
|
|
1663
|
+
try {
|
|
1664
|
+
await streamSse(
|
|
1665
|
+
`/api/sessions/${sessionKey}/stream?from=0`,
|
|
1666
|
+
{ signal: controller.signal },
|
|
1667
|
+
(event, data) => handleChatEvent(event, data, turn)
|
|
1668
|
+
);
|
|
1669
|
+
} catch {
|
|
1670
|
+
// si falla la reconexion, el historial persistido queda visible
|
|
1671
|
+
} finally {
|
|
1672
|
+
controllers.delete(sessionKey);
|
|
1673
|
+
if (state.panes.get(sessionKey) === record) setWorking(sessionKey, false);
|
|
1674
|
+
setActivity(turn.active, null);
|
|
1675
|
+
refreshSessions();
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1262
1679
|
async function sendMessage() {
|
|
1263
1680
|
const current = state.panes.get(state.sessionId);
|
|
1264
1681
|
if (current?.working) return;
|
|
@@ -1297,24 +1714,13 @@ async function sendMessage() {
|
|
|
1297
1714
|
chatInput.value = '';
|
|
1298
1715
|
state.attachments = [];
|
|
1299
1716
|
renderAttachments();
|
|
1300
|
-
|
|
1717
|
+
|
|
1718
|
+
const turn = createChatTurn(pane, sessionKey);
|
|
1719
|
+
turn.active = addMessage('assistant', '', pane);
|
|
1720
|
+
setMessageMode(turn.active, turn.turnMode);
|
|
1301
1721
|
const controller = new AbortController();
|
|
1302
1722
|
controllers.set(sessionKey, controller);
|
|
1303
1723
|
setWorking(sessionKey, true);
|
|
1304
|
-
let sawText = false;
|
|
1305
|
-
let lastTool = null;
|
|
1306
|
-
let startNewOnText = false;
|
|
1307
|
-
let turnMode = modeSelect.value || 'agent';
|
|
1308
|
-
|
|
1309
|
-
const flowTarget = () => {
|
|
1310
|
-
if (startNewOnText) {
|
|
1311
|
-
active = addMessage('assistant', '', pane);
|
|
1312
|
-
setMessageMode(active, turnMode);
|
|
1313
|
-
startNewOnText = false;
|
|
1314
|
-
setActivity(active, 'thinking');
|
|
1315
|
-
}
|
|
1316
|
-
return active;
|
|
1317
|
-
};
|
|
1318
1724
|
|
|
1319
1725
|
try {
|
|
1320
1726
|
await streamPost(
|
|
@@ -1332,74 +1738,27 @@ async function sendMessage() {
|
|
|
1332
1738
|
return context ? Math.floor(context * 0.8) : undefined;
|
|
1333
1739
|
})(),
|
|
1334
1740
|
},
|
|
1335
|
-
(event, data) =>
|
|
1336
|
-
if (event === 'start') {
|
|
1337
|
-
if (data.cwd) setCwd(data.cwd);
|
|
1338
|
-
if (data.mode) turnMode = data.mode;
|
|
1339
|
-
setMessageMode(active, turnMode);
|
|
1340
|
-
} else if (event === 'text') {
|
|
1341
|
-
sawText = true;
|
|
1342
|
-
const target = flowTarget();
|
|
1343
|
-
setText(target, data.full);
|
|
1344
|
-
setActivity(target, 'thinking');
|
|
1345
|
-
} else if (event === 'reasoning') {
|
|
1346
|
-
const target = flowTarget();
|
|
1347
|
-
if (!sawText) ensureReasoning(target).textContent = data.full;
|
|
1348
|
-
setActivity(target, 'thinking');
|
|
1349
|
-
} else if (event === 'tools') {
|
|
1350
|
-
startNewOnText = true;
|
|
1351
|
-
} else if (event === 'tool_call') {
|
|
1352
|
-
setActivity(active, 'working');
|
|
1353
|
-
lastTool = addToolCard(active, data.name, JSON.stringify(data.args));
|
|
1354
|
-
} else if (event === 'tool_result') {
|
|
1355
|
-
setActivity(active, 'thinking');
|
|
1356
|
-
if (lastTool) {
|
|
1357
|
-
const failed = /^Error|^Permiso denegado/i.test(String(data.result));
|
|
1358
|
-
renderToolOut(lastTool.out, truncate(data.result));
|
|
1359
|
-
lastTool.card.classList.remove('running');
|
|
1360
|
-
lastTool.card.classList.add(failed ? 'failed' : 'done');
|
|
1361
|
-
lastTool.card.querySelector('.tool-glyph').textContent = failed ? '✗' : '✓';
|
|
1362
|
-
}
|
|
1363
|
-
} else if (event === 'permission') {
|
|
1364
|
-
setActivity(active, 'waiting');
|
|
1365
|
-
addPermissionCard(active, data);
|
|
1366
|
-
} else if (event === 'permission_resolved') {
|
|
1367
|
-
setActivity(active, 'thinking');
|
|
1368
|
-
const entry = state.permissionCards.get(data.askId);
|
|
1369
|
-
if (entry && entry.verdict.textContent === 'requiere aprobación') {
|
|
1370
|
-
entry.card.classList.add('collapsed', data.allow ? 'approved' : 'denied');
|
|
1371
|
-
entry.card.classList.remove('pending');
|
|
1372
|
-
entry.card.querySelector('.perm-icon').textContent = data.allow ? '✓' : '✗';
|
|
1373
|
-
entry.verdict.textContent = data.allow ? 'aprobado' : 'denegado (sin respuesta)';
|
|
1374
|
-
for (const btn of Object.values(entry.buttons)) btn.disabled = true;
|
|
1375
|
-
}
|
|
1376
|
-
} else if (event === 'stall') {
|
|
1377
|
-
if (!sawText) setText(active, `⚠ ${data.seconds} s sin respuesta del modelo (proveedor caído o muy lento). Sigo esperando...`);
|
|
1378
|
-
} else if (event === 'retry') {
|
|
1379
|
-
if (!sawText) setText(active, `↻ reintentando (${data.attempt}) en ${Math.round((data.delayMs || 0) / 1000)} s...`);
|
|
1380
|
-
} else if (event === 'trim') {
|
|
1381
|
-
active.tools.appendChild(el('div', 'chip', `✂ contexto recortado (${data.trimmed} resultados, ${data.dropped} mensajes)`));
|
|
1382
|
-
} else if (event === 'warning') {
|
|
1383
|
-
active.tools.appendChild(el('div', 'chip', `⚠ ${data.message}`));
|
|
1384
|
-
} else if (event === 'error') {
|
|
1385
|
-
setActivity(active, null);
|
|
1386
|
-
active.tools.appendChild(el('div', 'chip err', `✗ ${data.message}`));
|
|
1387
|
-
} else if (event === 'done') {
|
|
1388
|
-
setActivity(active, null);
|
|
1389
|
-
if (!data.content && !sawText) setText(active, '(sin texto)');
|
|
1390
|
-
updateStatus({ rounds: data.rounds, stopReason: data.stopReason });
|
|
1391
|
-
addTokens(data.tokens);
|
|
1392
|
-
}
|
|
1393
|
-
},
|
|
1741
|
+
(event, data) => handleChatEvent(event, data, turn),
|
|
1394
1742
|
controller.signal
|
|
1395
1743
|
);
|
|
1396
1744
|
} catch (error) {
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1745
|
+
if (error.name === 'AbortError') {
|
|
1746
|
+
setActivity(turn.active, null);
|
|
1747
|
+
setText(turn.assistant(), '⏹ cancelado.');
|
|
1748
|
+
} else {
|
|
1749
|
+
const detail = await api(`/api/sessions/${sessionKey}`).catch(() => null);
|
|
1750
|
+
if (detail?.running) {
|
|
1751
|
+
controllers.delete(sessionKey);
|
|
1752
|
+
setWorking(sessionKey, false);
|
|
1753
|
+
await attachRun(sessionKey, pane);
|
|
1754
|
+
} else {
|
|
1755
|
+
setActivity(turn.active, null);
|
|
1756
|
+
setText(turn.assistant(), `Error: ${error.message}`);
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1400
1759
|
} finally {
|
|
1401
1760
|
controllers.delete(sessionKey);
|
|
1402
|
-
setActivity(active, null);
|
|
1761
|
+
setActivity(turn.active, null);
|
|
1403
1762
|
setWorking(sessionKey, false);
|
|
1404
1763
|
refreshSessions();
|
|
1405
1764
|
}
|
|
@@ -1407,7 +1766,9 @@ async function sendMessage() {
|
|
|
1407
1766
|
|
|
1408
1767
|
if (stopBtn) {
|
|
1409
1768
|
stopBtn.addEventListener('click', () => {
|
|
1410
|
-
const
|
|
1769
|
+
const id = state.sessionId;
|
|
1770
|
+
if (id) api(`/api/sessions/${id}/stop`, { method: 'POST' }).catch(() => {});
|
|
1771
|
+
const controller = controllers.get(id);
|
|
1411
1772
|
if (controller) controller.abort();
|
|
1412
1773
|
});
|
|
1413
1774
|
}
|
|
@@ -2035,6 +2396,10 @@ const mcpForm = $('#mcp-form');
|
|
|
2035
2396
|
const skillListEl = $('#skill-list');
|
|
2036
2397
|
const skillModal = $('#skill-modal');
|
|
2037
2398
|
const skillForm = $('#skill-form');
|
|
2399
|
+
const tgTokenEl = $('#tg-token');
|
|
2400
|
+
const tgChatsEl = $('#tg-chats');
|
|
2401
|
+
const tgTranscribeEl = $('#tg-transcribe');
|
|
2402
|
+
const tgStatusEl = $('#tg-status');
|
|
2038
2403
|
|
|
2039
2404
|
function setStatus(node, text, cls = '') {
|
|
2040
2405
|
const elNode = $(node);
|
|
@@ -2249,6 +2614,55 @@ async function submitSkill(event) {
|
|
|
2249
2614
|
refreshSkills();
|
|
2250
2615
|
}
|
|
2251
2616
|
|
|
2617
|
+
async function refreshChannels() {
|
|
2618
|
+
if (!tgStatusEl) return;
|
|
2619
|
+
const data = await api('/api/channels').catch(() => null);
|
|
2620
|
+
if (!data?.telegram) return;
|
|
2621
|
+
const tg = data.telegram;
|
|
2622
|
+
tgStatusEl.className = `summary${tg.running ? ' ok' : ''}`;
|
|
2623
|
+
tgStatusEl.textContent = tg.running
|
|
2624
|
+
? `activo${tg.tokenHint ? ` · ${tg.tokenHint}` : ''}`
|
|
2625
|
+
: tg.tokenSet
|
|
2626
|
+
? 'token guardado (bot detenido)'
|
|
2627
|
+
: 'sin configurar';
|
|
2628
|
+
if (tgChatsEl && document.activeElement !== tgChatsEl) {
|
|
2629
|
+
tgChatsEl.value = (tg.allowedChatIds ?? []).join(', ');
|
|
2630
|
+
}
|
|
2631
|
+
if (tgTranscribeEl && document.activeElement !== tgTranscribeEl) {
|
|
2632
|
+
tgTranscribeEl.value = tg.transcribeModel ?? 'whisper-1';
|
|
2633
|
+
}
|
|
2634
|
+
if (tgTokenEl && document.activeElement !== tgTokenEl) {
|
|
2635
|
+
tgTokenEl.value = '';
|
|
2636
|
+
tgTokenEl.placeholder = tg.tokenHint ? `${tg.tokenHint} (dejar vacío para no cambiar)` : '123456:ABC…';
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2639
|
+
|
|
2640
|
+
async function submitChannel() {
|
|
2641
|
+
if (!tgTokenEl) return;
|
|
2642
|
+
setStatus('#tg-msg', 'guardando…');
|
|
2643
|
+
const body = {
|
|
2644
|
+
allowedChatIds: tgChatsEl?.value ?? '',
|
|
2645
|
+
transcribeModel: tgTranscribeEl?.value.trim() || 'whisper-1',
|
|
2646
|
+
};
|
|
2647
|
+
const token = tgTokenEl.value.trim();
|
|
2648
|
+
if (token) body.token = token;
|
|
2649
|
+
const data = await api('/api/channels/telegram', { method: 'PUT', body }).catch((e) => ({ error: e.message }));
|
|
2650
|
+
if (data?.error) return setStatus('#tg-msg', data.error, 'err');
|
|
2651
|
+
setStatus('#tg-msg', data.telegram?.running ? 'guardado · bot activo' : 'guardado', 'ok');
|
|
2652
|
+
refreshChannels();
|
|
2653
|
+
}
|
|
2654
|
+
|
|
2655
|
+
async function testChannel() {
|
|
2656
|
+
if (!tgTokenEl) return;
|
|
2657
|
+
setStatus('#tg-msg', 'probando…');
|
|
2658
|
+
const data = await api('/api/channels/telegram/test', {
|
|
2659
|
+
method: 'POST',
|
|
2660
|
+
body: { token: tgTokenEl.value.trim() },
|
|
2661
|
+
}).catch((e) => ({ ok: false, error: e.message }));
|
|
2662
|
+
if (data?.ok) setStatus('#tg-msg', `token OK${data.username ? ` · @${data.username}` : ''}`, 'ok');
|
|
2663
|
+
else setStatus('#tg-msg', data?.error || 'token inválido', 'err');
|
|
2664
|
+
}
|
|
2665
|
+
|
|
2252
2666
|
async function importMcp() {
|
|
2253
2667
|
const input = $('#mcp-import');
|
|
2254
2668
|
const value = input.value.trim();
|
|
@@ -2296,6 +2710,8 @@ if (mcpListEl) {
|
|
|
2296
2710
|
}
|
|
2297
2711
|
});
|
|
2298
2712
|
$('#mcp-reload').addEventListener('click', () => refreshMcp({ connect: true }));
|
|
2713
|
+
$('#tg-save')?.addEventListener('click', submitChannel);
|
|
2714
|
+
$('#tg-test')?.addEventListener('click', testChannel);
|
|
2299
2715
|
$('#mcp-close').addEventListener('click', closeMcpForm);
|
|
2300
2716
|
$('#mf-cancel').addEventListener('click', closeMcpForm);
|
|
2301
2717
|
$('#mf-transport').addEventListener('change', syncMcpFormFields);
|