@danieltmn/tamnora 0.5.1 → 0.6.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/CHANGELOG.md +7 -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 +436 -110
- package/harness/packages/server/public/index.html +1 -1
- package/harness/packages/server/public/styles.css +95 -4
- package/harness/packages/server/src/lib/channels/index.js +43 -0
- package/harness/packages/server/src/lib/{telegram.js → channels/telegram.js} +157 -26
- 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/chat.js +161 -77
- package/harness/packages/server/src/routes/sessions.js +17 -0
- package/harness/packages/server/src/server.js +12 -16
- package/harness/packages/server/test/server.test.js +129 -1
- package/harness/tamnora.example.json +2 -1
- package/package.json +1 -1
|
@@ -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');
|
|
@@ -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,213 @@ $('#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 handleChatEvent(event, data, turn) {
|
|
1535
|
+
if (handleAgentEvent(event, data, turn)) return;
|
|
1536
|
+
if (event === 'agent_start') {
|
|
1537
|
+
turn.agents.set(data.agentId, addAgentCard(turn.assistant(), data));
|
|
1538
|
+
} else if (event === 'agent_end') {
|
|
1539
|
+
const agent = turn.agents.get(data.agentId);
|
|
1540
|
+
if (agent) {
|
|
1541
|
+
agent.card.classList.remove('running');
|
|
1542
|
+
agent.card.classList.add(data.status === 'error' ? 'failed' : 'done');
|
|
1543
|
+
agent.glyph.textContent = data.status === 'error' ? '✗' : '✓';
|
|
1544
|
+
agent.statusEl.textContent = data.status;
|
|
1545
|
+
if (data.content && !agent.sawText) agent.textEl.textContent = data.content;
|
|
1546
|
+
}
|
|
1547
|
+
} else if (event === 'start') {
|
|
1548
|
+
if (data.cwd) setCwd(data.cwd);
|
|
1549
|
+
if (data.mode) turn.turnMode = data.mode;
|
|
1550
|
+
if (turn.active) setMessageMode(turn.active, turn.turnMode);
|
|
1551
|
+
} else if (event === 'text') {
|
|
1552
|
+
turn.sawText = true;
|
|
1553
|
+
const target = turn.flowTarget();
|
|
1554
|
+
setText(target, data.full);
|
|
1555
|
+
setActivity(target, 'thinking');
|
|
1556
|
+
} else if (event === 'reasoning') {
|
|
1557
|
+
const target = turn.flowTarget();
|
|
1558
|
+
if (!turn.sawText) ensureReasoning(target).textContent = data.full;
|
|
1559
|
+
setActivity(target, 'thinking');
|
|
1560
|
+
} else if (event === 'tools') {
|
|
1561
|
+
turn.startNewOnText = true;
|
|
1562
|
+
} else if (event === 'tool_call') {
|
|
1563
|
+
const target = turn.assistant();
|
|
1564
|
+
setActivity(target, 'working');
|
|
1565
|
+
turn.lastTool = addToolCard(target, data.name, data.args, { callId: data.id });
|
|
1566
|
+
} else if (event === 'tool_result') {
|
|
1567
|
+
setActivity(turn.assistant(), 'thinking');
|
|
1568
|
+
if (turn.lastTool) {
|
|
1569
|
+
const failed = data.failed ?? /^Error|^Permiso denegado/i.test(String(data.preview ?? data.result));
|
|
1570
|
+
renderToolOut(turn.lastTool.out, data.preview ?? data.result);
|
|
1571
|
+
turn.lastTool.card.classList.remove('running');
|
|
1572
|
+
turn.lastTool.card.classList.toggle('failed', failed);
|
|
1573
|
+
if (!failed) turn.lastTool.card.classList.add('done');
|
|
1574
|
+
turn.lastTool.glyph.textContent = failed ? '✗' : '✓';
|
|
1575
|
+
if (data.truncated) {
|
|
1576
|
+
turn.lastTool.truncated = true;
|
|
1577
|
+
turn.lastTool.more.hidden = false;
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
} else if (event === 'permission') {
|
|
1581
|
+
setActivity(turn.assistant(), 'waiting');
|
|
1582
|
+
addPermissionCard(turn.assistant(), data);
|
|
1583
|
+
} else if (event === 'permission_resolved') {
|
|
1584
|
+
setActivity(turn.assistant(), 'thinking');
|
|
1585
|
+
const entry = state.permissionCards.get(data.askId);
|
|
1586
|
+
if (entry && entry.verdict.textContent === 'requiere aprobación') {
|
|
1587
|
+
entry.card.classList.add('collapsed', data.allow ? 'approved' : 'denied');
|
|
1588
|
+
entry.card.classList.remove('pending');
|
|
1589
|
+
entry.card.querySelector('.perm-icon').textContent = data.allow ? '✓' : '✗';
|
|
1590
|
+
entry.verdict.textContent = data.allow ? 'aprobado' : 'denegado (sin respuesta)';
|
|
1591
|
+
for (const btn of Object.values(entry.buttons)) btn.disabled = true;
|
|
1592
|
+
}
|
|
1593
|
+
} else if (event === 'stall') {
|
|
1594
|
+
if (!turn.sawText) setText(turn.assistant(), `⚠ ${data.seconds} s sin respuesta del modelo (proveedor caído o muy lento). Sigo esperando...`);
|
|
1595
|
+
} else if (event === 'retry') {
|
|
1596
|
+
if (!turn.sawText) setText(turn.assistant(), `↻ reintentando (${data.attempt}) en ${Math.round((data.delayMs || 0) / 1000)} s...`);
|
|
1597
|
+
} else if (event === 'trim') {
|
|
1598
|
+
turn.assistant().tools.appendChild(el('div', 'chip', `✂ contexto recortado (${data.trimmed} resultados, ${data.dropped} mensajes)`));
|
|
1599
|
+
} else if (event === 'warning') {
|
|
1600
|
+
turn.assistant().tools.appendChild(el('div', 'chip', `⚠ ${data.message}`));
|
|
1601
|
+
} else if (event === 'error') {
|
|
1602
|
+
setActivity(turn.active, null);
|
|
1603
|
+
turn.assistant().tools.appendChild(el('div', 'chip err', `✗ ${data.message}`));
|
|
1604
|
+
} else if (event === 'done') {
|
|
1605
|
+
setActivity(turn.active, null);
|
|
1606
|
+
if (!data.content && !turn.sawText) setText(turn.assistant(), '(sin texto)');
|
|
1607
|
+
updateStatus({ rounds: data.rounds, stopReason: data.stopReason });
|
|
1608
|
+
addTokens(data.tokens);
|
|
1609
|
+
} else if (event === 'aborted') {
|
|
1610
|
+
setActivity(turn.active, null);
|
|
1611
|
+
setText(turn.assistant(), '⏹ cancelado.');
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
/**
|
|
1616
|
+
* Se engancha a un run en curso: re-renderiza el historial persistido y
|
|
1617
|
+
* reproduce los eventos del turno actual (asi el progreso no se pierde si el
|
|
1618
|
+
* telefono bloqueo la pantalla o se corto la red).
|
|
1619
|
+
*/
|
|
1620
|
+
async function attachRun(sessionKey, pane) {
|
|
1621
|
+
const record = state.panes.get(sessionKey);
|
|
1622
|
+
if (record?.working) return;
|
|
1623
|
+
await renderPane(sessionKey);
|
|
1624
|
+
const controller = new AbortController();
|
|
1625
|
+
controllers.set(sessionKey, controller);
|
|
1626
|
+
if (record) setWorking(sessionKey, true);
|
|
1627
|
+
const turn = createChatTurn(pane, sessionKey);
|
|
1628
|
+
try {
|
|
1629
|
+
await streamSse(
|
|
1630
|
+
`/api/sessions/${sessionKey}/stream?from=0`,
|
|
1631
|
+
{ signal: controller.signal },
|
|
1632
|
+
(event, data) => handleChatEvent(event, data, turn)
|
|
1633
|
+
);
|
|
1634
|
+
} catch {
|
|
1635
|
+
// si falla la reconexion, el historial persistido queda visible
|
|
1636
|
+
} finally {
|
|
1637
|
+
controllers.delete(sessionKey);
|
|
1638
|
+
if (state.panes.get(sessionKey) === record) setWorking(sessionKey, false);
|
|
1639
|
+
setActivity(turn.active, null);
|
|
1640
|
+
refreshSessions();
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1262
1644
|
async function sendMessage() {
|
|
1263
1645
|
const current = state.panes.get(state.sessionId);
|
|
1264
1646
|
if (current?.working) return;
|
|
@@ -1297,24 +1679,13 @@ async function sendMessage() {
|
|
|
1297
1679
|
chatInput.value = '';
|
|
1298
1680
|
state.attachments = [];
|
|
1299
1681
|
renderAttachments();
|
|
1300
|
-
|
|
1682
|
+
|
|
1683
|
+
const turn = createChatTurn(pane, sessionKey);
|
|
1684
|
+
turn.active = addMessage('assistant', '', pane);
|
|
1685
|
+
setMessageMode(turn.active, turn.turnMode);
|
|
1301
1686
|
const controller = new AbortController();
|
|
1302
1687
|
controllers.set(sessionKey, controller);
|
|
1303
1688
|
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
1689
|
|
|
1319
1690
|
try {
|
|
1320
1691
|
await streamPost(
|
|
@@ -1332,74 +1703,27 @@ async function sendMessage() {
|
|
|
1332
1703
|
return context ? Math.floor(context * 0.8) : undefined;
|
|
1333
1704
|
})(),
|
|
1334
1705
|
},
|
|
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
|
-
},
|
|
1706
|
+
(event, data) => handleChatEvent(event, data, turn),
|
|
1394
1707
|
controller.signal
|
|
1395
1708
|
);
|
|
1396
1709
|
} catch (error) {
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1710
|
+
if (error.name === 'AbortError') {
|
|
1711
|
+
setActivity(turn.active, null);
|
|
1712
|
+
setText(turn.assistant(), '⏹ cancelado.');
|
|
1713
|
+
} else {
|
|
1714
|
+
const detail = await api(`/api/sessions/${sessionKey}`).catch(() => null);
|
|
1715
|
+
if (detail?.running) {
|
|
1716
|
+
controllers.delete(sessionKey);
|
|
1717
|
+
setWorking(sessionKey, false);
|
|
1718
|
+
await attachRun(sessionKey, pane);
|
|
1719
|
+
} else {
|
|
1720
|
+
setActivity(turn.active, null);
|
|
1721
|
+
setText(turn.assistant(), `Error: ${error.message}`);
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1400
1724
|
} finally {
|
|
1401
1725
|
controllers.delete(sessionKey);
|
|
1402
|
-
setActivity(active, null);
|
|
1726
|
+
setActivity(turn.active, null);
|
|
1403
1727
|
setWorking(sessionKey, false);
|
|
1404
1728
|
refreshSessions();
|
|
1405
1729
|
}
|
|
@@ -1407,7 +1731,9 @@ async function sendMessage() {
|
|
|
1407
1731
|
|
|
1408
1732
|
if (stopBtn) {
|
|
1409
1733
|
stopBtn.addEventListener('click', () => {
|
|
1410
|
-
const
|
|
1734
|
+
const id = state.sessionId;
|
|
1735
|
+
if (id) api(`/api/sessions/${id}/stop`, { method: 'POST' }).catch(() => {});
|
|
1736
|
+
const controller = controllers.get(id);
|
|
1411
1737
|
if (controller) controller.abort();
|
|
1412
1738
|
});
|
|
1413
1739
|
}
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
</head>
|
|
34
34
|
<body>
|
|
35
35
|
<header class="topbar">
|
|
36
|
-
<div class="brand"><span class="logo"><span class="logo-t">T</span><span class="logo-h">H</span></span> <span class="ver">v0.
|
|
36
|
+
<div class="brand"><span class="logo"><span class="logo-t">T</span><span class="logo-h">H</span></span> <span class="ver">v0.6.0</span></div>
|
|
37
37
|
<nav class="tabs">
|
|
38
38
|
<button class="tab active" data-tab="chat">chat</button>
|
|
39
39
|
<button class="tab" data-tab="providers">proveedores</button>
|