@danieltmn/tamnora 0.6.2 → 0.7.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 +49 -0
- package/SECURITY.md +6 -0
- package/harness/packages/cli/src/commands/repl.js +2 -0
- package/harness/packages/cli/src/commands/run.js +2 -0
- package/harness/packages/cli/src/commands/tui.js +2 -0
- package/harness/packages/cli/src/lib/runtime.js +4 -0
- package/harness/packages/cli/src/tui/app.js +4 -0
- package/harness/packages/core/src/agent/runAgent.js +131 -47
- package/harness/packages/core/src/config/index.js +3 -1
- package/harness/packages/core/src/config/secrets.js +36 -0
- package/harness/packages/core/src/eval/runTask.js +2 -11
- package/harness/packages/core/src/index.js +13 -1
- package/harness/packages/core/src/mcp/common.js +15 -11
- package/harness/packages/core/src/mcp/index.js +1 -1
- package/harness/packages/core/src/mcp/registry.js +1 -0
- package/harness/packages/core/src/mcp/stdio.js +41 -2
- package/harness/packages/core/src/redact.js +104 -0
- package/harness/packages/core/test/agent.test.js +93 -0
- package/harness/packages/core/test/mcp.test.js +67 -0
- package/harness/packages/core/test/redact.test.js +45 -0
- package/harness/packages/server/public/app.js +368 -41
- package/harness/packages/server/public/index.html +102 -26
- package/harness/packages/server/public/markdown.js +43 -0
- package/harness/packages/server/public/styles.css +171 -21
- package/harness/packages/server/src/lib/agentTools.js +9 -3
- package/harness/packages/server/src/lib/channels/telegram.js +11 -5
- package/harness/packages/server/src/routes/chat.js +8 -1
- package/harness/packages/server/src/routes/mcp.js +29 -5
- package/harness/packages/server/src/routes/providers.js +16 -15
- package/harness/packages/server/src/server.js +13 -0
- package/harness/packages/server/test/markdown.test.js +33 -0
- package/harness/packages/server/test/server.test.js +77 -1
- package/harness/tamnora.example.json +17 -1
- package/package.json +1 -1
|
@@ -58,6 +58,7 @@ const ICON_PATHS = {
|
|
|
58
58
|
file: ['M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z', 'M14 2v4a2 2 0 0 0 2 2h4'],
|
|
59
59
|
'refresh-cw': ['M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8', 'M21 3v5h-5', 'M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16', 'M8 16H3v5'],
|
|
60
60
|
trash: ['M3 6h18', 'M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6', 'M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2', 'M10 11v6', 'M14 11v6'],
|
|
61
|
+
'dots-vertical': ['M12 5h.01', 'M12 12h.01', 'M12 19h.01'],
|
|
61
62
|
};
|
|
62
63
|
|
|
63
64
|
function icon(name, size = 16, className = '') {
|
|
@@ -178,7 +179,7 @@ function activateTab(name) {
|
|
|
178
179
|
hideMoreMenu();
|
|
179
180
|
if (name === 'runs') refreshRuns();
|
|
180
181
|
if (name === 'providers') refreshProviders();
|
|
181
|
-
if (name === 'ajustes') { refreshMcp(); refreshSkills(); refreshChannels(); }
|
|
182
|
+
if (name === 'ajustes') { refreshMcp(); refreshSkills(); refreshChannels(); refreshLimits(); }
|
|
182
183
|
}
|
|
183
184
|
|
|
184
185
|
document.querySelectorAll('.tab[data-tab]').forEach((t) =>
|
|
@@ -203,27 +204,61 @@ document.addEventListener('keydown', (event) => {
|
|
|
203
204
|
/* --- settings modal (modo / proveedor / modelo / auto) -------------------- */
|
|
204
205
|
|
|
205
206
|
const settingsModal = $('#settings-modal');
|
|
207
|
+
const agentControls = $('#agent-settings-controls');
|
|
208
|
+
const agentControlsSlot = $('#agent-controls-slot');
|
|
209
|
+
const settingsControlsSlot = $('#settings-controls-slot');
|
|
210
|
+
|
|
211
|
+
/** Los controles modo/proveedor/modelo/auto se comparten entre el tab Agente
|
|
212
|
+
* y el modal: se mueven de un slot al otro para no duplicar IDs ni estado. */
|
|
213
|
+
function moveAgentControls(target) {
|
|
214
|
+
if (agentControls && target) target.appendChild(agentControls);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function openSettingsModal() {
|
|
218
|
+
if (!settingsModal) return;
|
|
219
|
+
moveAgentControls(settingsControlsSlot);
|
|
220
|
+
syncDefaultCwd();
|
|
221
|
+
settingsModal.showModal();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function closeSettingsModal() {
|
|
225
|
+
moveAgentControls(agentControlsSlot);
|
|
226
|
+
settingsModal?.close();
|
|
227
|
+
}
|
|
228
|
+
|
|
206
229
|
if (settingsModal) {
|
|
207
|
-
$('#settings-toggle')?.addEventListener('click',
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
});
|
|
211
|
-
$('#settings-close')?.addEventListener('click', () => settingsModal.close());
|
|
212
|
-
$('#settings-done')?.addEventListener('click', () => settingsModal.close());
|
|
230
|
+
$('#settings-toggle')?.addEventListener('click', openSettingsModal);
|
|
231
|
+
$('#settings-close')?.addEventListener('click', closeSettingsModal);
|
|
232
|
+
$('#settings-done')?.addEventListener('click', closeSettingsModal);
|
|
213
233
|
$('#settings-providers')?.addEventListener('click', () => {
|
|
214
|
-
|
|
234
|
+
closeSettingsModal();
|
|
215
235
|
activateTab('providers');
|
|
216
236
|
});
|
|
217
|
-
$('#
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
});
|
|
237
|
+
$('#agent-settings-open')?.addEventListener('click', openSettingsModal);
|
|
238
|
+
$('#agent-providers')?.addEventListener('click', () => activateTab('providers'));
|
|
239
|
+
$('#default-cwd-pick')?.addEventListener('click', () => openFolderPicker('default'));
|
|
221
240
|
$('#default-cwd-clear')?.addEventListener('click', () => {
|
|
222
241
|
writeDefaultCwd('');
|
|
223
242
|
syncDefaultCwd();
|
|
224
243
|
});
|
|
225
244
|
settingsModal.addEventListener('click', (event) => {
|
|
226
|
-
if (event.target === settingsModal)
|
|
245
|
+
if (event.target === settingsModal) closeSettingsModal();
|
|
246
|
+
});
|
|
247
|
+
settingsModal.addEventListener('close', () => moveAgentControls(agentControlsSlot));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// El footer (modo · proveedor · modelo · estado) abre el modal de ajustes.
|
|
251
|
+
const statusbarEl = $('#statusbar');
|
|
252
|
+
if (statusbarEl) {
|
|
253
|
+
statusbarEl.addEventListener('click', (event) => {
|
|
254
|
+
if (event.target.closest('#sb-context')) return;
|
|
255
|
+
openSettingsModal();
|
|
256
|
+
});
|
|
257
|
+
statusbarEl.addEventListener('keydown', (event) => {
|
|
258
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
259
|
+
event.preventDefault();
|
|
260
|
+
openSettingsModal();
|
|
261
|
+
}
|
|
227
262
|
});
|
|
228
263
|
}
|
|
229
264
|
|
|
@@ -235,7 +270,7 @@ const PALETTE_COMMANDS = [
|
|
|
235
270
|
{ id: 'compact', label: 'Compactar la conversación', hint: '/compact', run: () => runSlash('compact') },
|
|
236
271
|
{ id: 'clear', label: 'Limpiar la conversación', hint: '/clear', run: () => runSlash('clear') },
|
|
237
272
|
{ id: 'mode', label: 'Cambiar Plan / Build', hint: 'Ctrl+.', run: () => toggleMode() },
|
|
238
|
-
{ id: 'settings', label: 'Ajustes del agente (modelo, modo, auto)', run: () =>
|
|
273
|
+
{ id: 'settings', label: 'Ajustes del agente (modelo, modo, auto)', run: () => openSettingsModal() },
|
|
239
274
|
{ id: 'providers', label: 'Gestionar proveedores', run: () => activateTab('providers') },
|
|
240
275
|
{ id: 'ajustes', label: 'MCP y Skills (ajustes)', run: () => activateTab('ajustes') },
|
|
241
276
|
{ id: 'explorer', label: 'Mostrar u ocultar archivos', run: () => setExplorerOpen(!state.explorerOpen) },
|
|
@@ -489,11 +524,13 @@ function persistLayout() {
|
|
|
489
524
|
} catch {
|
|
490
525
|
// storage unavailable
|
|
491
526
|
}
|
|
527
|
+
scheduleScrollNav();
|
|
492
528
|
}
|
|
493
529
|
|
|
494
530
|
function applyPanelWidths() {
|
|
495
531
|
panels.sidebar.el.style.width = `${panels.sidebar.width}px`;
|
|
496
532
|
panels.explorer.el.style.width = `${panels.explorer.width}px`;
|
|
533
|
+
scheduleScrollNav();
|
|
497
534
|
}
|
|
498
535
|
|
|
499
536
|
function attachResizer(panel) {
|
|
@@ -575,6 +612,10 @@ function initLayout() {
|
|
|
575
612
|
mobileQuery.addEventListener('change', () => setSidebarOpen(isMobile() ? false : readSidebarPref()));
|
|
576
613
|
}
|
|
577
614
|
|
|
615
|
+
// `scrollNavFrame` debe existir antes de `initLayout()`: `applyPanelWidths`
|
|
616
|
+
// llama a `scheduleScrollNav()` durante el arranque (evita el TDZ).
|
|
617
|
+
let scrollNavFrame = 0;
|
|
618
|
+
|
|
578
619
|
initLayout();
|
|
579
620
|
|
|
580
621
|
function formatK(n) {
|
|
@@ -699,12 +740,50 @@ const controllers = new Map();
|
|
|
699
740
|
const sessionsEl = $('#sessions');
|
|
700
741
|
|
|
701
742
|
let activePane = blankPane;
|
|
743
|
+
let restorePreviewMax = null;
|
|
702
744
|
const paneOf = (message) => message?.bubble?.closest('.messages') ?? activePane;
|
|
703
745
|
|
|
704
746
|
const scrollMessages = (pane = activePane) => {
|
|
705
747
|
if (pane) pane.scrollTop = pane.scrollHeight;
|
|
706
748
|
};
|
|
707
749
|
|
|
750
|
+
const scrollNav = $('#scroll-nav');
|
|
751
|
+
const scrollTopBtn = $('#scroll-top');
|
|
752
|
+
const scrollDownBtn = $('#scroll-down');
|
|
753
|
+
|
|
754
|
+
/** Muestra/oculta los saltos de scroll según el pane activo. */
|
|
755
|
+
function updateScrollNav() {
|
|
756
|
+
const pane = activePane;
|
|
757
|
+
if (!pane) return;
|
|
758
|
+
const max = pane.scrollHeight - pane.clientHeight;
|
|
759
|
+
const visible = max > 40;
|
|
760
|
+
scrollNav.hidden = !visible;
|
|
761
|
+
if (!visible) return;
|
|
762
|
+
scrollTopBtn.disabled = pane.scrollTop < 40;
|
|
763
|
+
scrollDownBtn.disabled = max - pane.scrollTop < 40;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function scheduleScrollNav() {
|
|
767
|
+
if (scrollNavFrame) return;
|
|
768
|
+
scrollNavFrame = requestAnimationFrame(() => {
|
|
769
|
+
scrollNavFrame = 0;
|
|
770
|
+
updateScrollNav();
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
scrollTopBtn.addEventListener('click', () => activePane?.scrollTo({ top: 0, behavior: 'smooth' }));
|
|
775
|
+
scrollDownBtn.addEventListener('click', () =>
|
|
776
|
+
activePane?.scrollTo({ top: activePane.scrollHeight, behavior: 'smooth' })
|
|
777
|
+
);
|
|
778
|
+
// `scroll` no burbujea: se escucha en captura desde el contenedor de los panes.
|
|
779
|
+
panesEl.addEventListener('scroll', scheduleScrollNav, true);
|
|
780
|
+
window.addEventListener('resize', scheduleScrollNav);
|
|
781
|
+
new MutationObserver(scheduleScrollNav).observe(panesEl, {
|
|
782
|
+
childList: true,
|
|
783
|
+
characterData: true,
|
|
784
|
+
subtree: true,
|
|
785
|
+
});
|
|
786
|
+
|
|
708
787
|
function addMessage(role, text = '', pane = activePane) {
|
|
709
788
|
pane.querySelector('.empty')?.remove();
|
|
710
789
|
const wrap = el('div', `msg ${role}`);
|
|
@@ -721,12 +800,10 @@ function addMessage(role, text = '', pane = activePane) {
|
|
|
721
800
|
const bubble = el('div', 'bubble');
|
|
722
801
|
const textEl = el('div', 'text');
|
|
723
802
|
const tools = el('div', 'tools');
|
|
724
|
-
|
|
725
|
-
activity.hidden = true;
|
|
726
|
-
bubble.append(textEl, tools, activity);
|
|
803
|
+
bubble.append(textEl, tools);
|
|
727
804
|
wrap.append(label, bubble);
|
|
728
805
|
pane.appendChild(wrap);
|
|
729
|
-
const message = { role, wrap, bubble, label, modeEl, textEl, tools,
|
|
806
|
+
const message = { role, wrap, bubble, label, modeEl, textEl, tools, reasoningEl: null, id: null };
|
|
730
807
|
addMessageActions(message);
|
|
731
808
|
if (text) setText(message, text);
|
|
732
809
|
scrollMessages(pane);
|
|
@@ -741,18 +818,79 @@ function setMessageMode(message, modeId) {
|
|
|
741
818
|
message.modeEl.hidden = false;
|
|
742
819
|
}
|
|
743
820
|
|
|
821
|
+
let openMessageMenu = null;
|
|
822
|
+
|
|
823
|
+
/** El menu flota (position: fixed) para no quedar recortado por el scroll. */
|
|
824
|
+
function positionMessageMenu() {
|
|
825
|
+
if (!openMessageMenu) return;
|
|
826
|
+
const { menu, trigger } = openMessageMenu;
|
|
827
|
+
const rect = trigger.getBoundingClientRect();
|
|
828
|
+
const size = menu.getBoundingClientRect();
|
|
829
|
+
let top = rect.bottom + 4;
|
|
830
|
+
if (top + size.height > window.innerHeight - 8) top = Math.max(8, rect.top - size.height - 4);
|
|
831
|
+
let left = rect.left;
|
|
832
|
+
if (left + size.width > window.innerWidth - 8) left = Math.max(8, window.innerWidth - size.width - 8);
|
|
833
|
+
menu.style.top = `${top}px`;
|
|
834
|
+
menu.style.left = `${left}px`;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function closeMessageMenus() {
|
|
838
|
+
if (!openMessageMenu) return;
|
|
839
|
+
const current = openMessageMenu;
|
|
840
|
+
openMessageMenu = null;
|
|
841
|
+
current.menu.hidden = true;
|
|
842
|
+
current.trigger.setAttribute('aria-expanded', 'false');
|
|
843
|
+
current.actions?.appendChild(current.menu);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
function toggleMessageMenu(message) {
|
|
847
|
+
const wasOpen = openMessageMenu === message;
|
|
848
|
+
closeMessageMenus();
|
|
849
|
+
if (wasOpen) return;
|
|
850
|
+
document.body.appendChild(message.menu);
|
|
851
|
+
message.menu.hidden = false;
|
|
852
|
+
message.trigger.setAttribute('aria-expanded', 'true');
|
|
853
|
+
openMessageMenu = message;
|
|
854
|
+
positionMessageMenu();
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
document.addEventListener('click', (event) => {
|
|
858
|
+
if (!openMessageMenu) return;
|
|
859
|
+
if (event.target.closest('.msg-menu') || event.target.closest('.msg-menu-btn')) return;
|
|
860
|
+
closeMessageMenus();
|
|
861
|
+
});
|
|
862
|
+
document.addEventListener('keydown', (event) => {
|
|
863
|
+
if (event.key === 'Escape') closeMessageMenus();
|
|
864
|
+
});
|
|
865
|
+
window.addEventListener('resize', positionMessageMenu);
|
|
866
|
+
document.addEventListener('scroll', positionMessageMenu, true);
|
|
867
|
+
|
|
744
868
|
function addMessageActions(message) {
|
|
745
869
|
const actions = el('div', 'msg-actions');
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
870
|
+
const trigger = button('Opciones...', () => toggleMessageMenu(message), 'msg-menu-btn');
|
|
871
|
+
trigger.title = 'opciones';
|
|
872
|
+
trigger.setAttribute('aria-haspopup', 'menu');
|
|
873
|
+
trigger.setAttribute('aria-expanded', 'false');
|
|
874
|
+
trigger.setAttribute('aria-label', 'opciones del mensaje');
|
|
875
|
+
|
|
876
|
+
const menu = el('div', 'msg-menu');
|
|
877
|
+
menu.hidden = true;
|
|
878
|
+
const addItem = (label, run, cls = 'msg-act') => {
|
|
879
|
+
menu.appendChild(button(label, () => {
|
|
880
|
+
closeMessageMenus();
|
|
881
|
+
run();
|
|
882
|
+
}, cls));
|
|
883
|
+
};
|
|
884
|
+
addItem('copiar', () => copyMessage(message));
|
|
885
|
+
if (message.role === 'assistant') addItem('regenerar', () => regenerate(message));
|
|
886
|
+
if (message.role === 'user') addItem('editar', () => editMessage(message));
|
|
887
|
+
addItem('borrar desde acá', () => deleteFrom(message), 'msg-act danger');
|
|
888
|
+
|
|
889
|
+
actions.append(trigger, menu);
|
|
754
890
|
message.bubble.appendChild(actions);
|
|
755
891
|
message.actions = actions;
|
|
892
|
+
message.menu = menu;
|
|
893
|
+
message.trigger = trigger;
|
|
756
894
|
}
|
|
757
895
|
|
|
758
896
|
function messageId(message) {
|
|
@@ -823,17 +961,25 @@ const ACTIVITY = {
|
|
|
823
961
|
waiting: { cls: 'loader-thinking', label: 'esperando aprobación…' },
|
|
824
962
|
};
|
|
825
963
|
|
|
826
|
-
|
|
827
|
-
|
|
964
|
+
const footerActivityEl = $('#sb-activity');
|
|
965
|
+
|
|
966
|
+
/** El indicador de "pensando/trabajando" vive en el footer (una sola vez). */
|
|
967
|
+
function renderFooterActivity(kind) {
|
|
968
|
+
if (!footerActivityEl) return;
|
|
828
969
|
if (!kind) {
|
|
829
|
-
|
|
830
|
-
|
|
970
|
+
footerActivityEl.hidden = true;
|
|
971
|
+
footerActivityEl.replaceChildren();
|
|
831
972
|
return;
|
|
832
973
|
}
|
|
833
|
-
const def = ACTIVITY[kind];
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
974
|
+
const def = ACTIVITY[kind] ?? ACTIVITY.thinking;
|
|
975
|
+
footerActivityEl.hidden = false;
|
|
976
|
+
footerActivityEl.replaceChildren(el('span', `loader ${def.cls}`), el('span', 'activity-label', def.label));
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
function setActivity(message, kind) {
|
|
980
|
+
// Solo el turno de la sesion activa refleja su actividad en el footer.
|
|
981
|
+
if (paneOf(message) !== activePane) return;
|
|
982
|
+
renderFooterActivity(kind);
|
|
837
983
|
}
|
|
838
984
|
|
|
839
985
|
function setText(message, text) {
|
|
@@ -1302,6 +1448,8 @@ function showPane(id) {
|
|
|
1302
1448
|
for (const item of state.panes.values()) item.el.hidden = item.id !== id;
|
|
1303
1449
|
blankPane.hidden = Boolean(record);
|
|
1304
1450
|
activePane = record ? record.el : blankPane;
|
|
1451
|
+
renderFooterActivity(record?.working ? 'thinking' : null);
|
|
1452
|
+
scheduleScrollNav();
|
|
1305
1453
|
}
|
|
1306
1454
|
|
|
1307
1455
|
function renderTabs() {
|
|
@@ -1361,8 +1509,9 @@ function activateSession(id) {
|
|
|
1361
1509
|
|
|
1362
1510
|
function auditStatus(record) {
|
|
1363
1511
|
submitBtn.disabled = record.working;
|
|
1364
|
-
$('#sb-rounds').textContent = record.working ? '
|
|
1512
|
+
$('#sb-rounds').textContent = record.working ? '' : 'listo';
|
|
1365
1513
|
$('#sb-rounds').classList.toggle('running', record.working);
|
|
1514
|
+
renderFooterActivity(record.working ? 'thinking' : null);
|
|
1366
1515
|
}
|
|
1367
1516
|
|
|
1368
1517
|
function closeTab(id) {
|
|
@@ -1403,8 +1552,9 @@ function setWorking(id, working) {
|
|
|
1403
1552
|
if (id === state.sessionId) {
|
|
1404
1553
|
submitBtn.disabled = working;
|
|
1405
1554
|
if (stopBtn) stopBtn.hidden = !working;
|
|
1406
|
-
$('#sb-rounds').textContent = working ? '
|
|
1555
|
+
$('#sb-rounds').textContent = working ? '' : 'listo';
|
|
1407
1556
|
$('#sb-rounds').classList.toggle('running', working);
|
|
1557
|
+
renderFooterActivity(working ? 'thinking' : null);
|
|
1408
1558
|
}
|
|
1409
1559
|
}
|
|
1410
1560
|
|
|
@@ -1545,11 +1695,25 @@ function renderCompletion(message, data) {
|
|
|
1545
1695
|
const duration = formatDuration(data.durationMs);
|
|
1546
1696
|
if (duration) parts.push(duration);
|
|
1547
1697
|
if (data.tokens?.total_tokens) parts.push(`${Number(data.tokens.total_tokens).toLocaleString('es')} tokens`);
|
|
1548
|
-
const
|
|
1549
|
-
|
|
1698
|
+
const cut = ['max_rounds', 'no_progress', 'budget'].includes(data.stopReason);
|
|
1699
|
+
const label = cut ? (data.wrappedUp ? '⚠ corte de seguridad · resumen' : '⚠ corte de seguridad') : '✓ terminado';
|
|
1700
|
+
message.tools.appendChild(el('div', cut ? 'chip warn' : 'chip ok', `${label} · ${parts.join(' · ')}`));
|
|
1550
1701
|
scrollMessages(paneOf(message));
|
|
1551
1702
|
}
|
|
1552
1703
|
|
|
1704
|
+
/** Retoma un turno cortado por límite/bucle sin tener que escribir "continúa". */
|
|
1705
|
+
function addContinueButton(turn, data) {
|
|
1706
|
+
const btn = el('button', 'chip', data.wrappedUp ? '↻ Continuar el trabajo' : '↻ Continuar');
|
|
1707
|
+
btn.type = 'button';
|
|
1708
|
+
btn.addEventListener('click', () => {
|
|
1709
|
+
if (state.panes.get(turn.sessionKey)?.working) return;
|
|
1710
|
+
chatInput.value = 'Continuá donde quedaste.';
|
|
1711
|
+
sendMessage();
|
|
1712
|
+
});
|
|
1713
|
+
turn.assistant().tools.appendChild(btn);
|
|
1714
|
+
scrollMessages(paneOf(turn.assistant()));
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1553
1717
|
function handleChatEvent(event, data, turn) {
|
|
1554
1718
|
if (handleAgentEvent(event, data, turn)) return;
|
|
1555
1719
|
if (event === 'agent_start') {
|
|
@@ -1615,6 +1779,8 @@ function handleChatEvent(event, data, turn) {
|
|
|
1615
1779
|
if (!turn.sawText) setText(turn.assistant(), `↻ reintentando (${data.attempt}) en ${Math.round((data.delayMs || 0) / 1000)} s...`);
|
|
1616
1780
|
} else if (event === 'trim') {
|
|
1617
1781
|
turn.assistant().tools.appendChild(el('div', 'chip', `✂ contexto recortado (${data.trimmed} resultados, ${data.dropped} mensajes)`));
|
|
1782
|
+
} else if (event === 'repeat') {
|
|
1783
|
+
turn.assistant().tools.appendChild(el('div', 'chip warn', `↻ ${data.name} repetida sin cambios`));
|
|
1618
1784
|
} else if (event === 'warning') {
|
|
1619
1785
|
turn.assistant().tools.appendChild(el('div', 'chip', `⚠ ${data.message}`));
|
|
1620
1786
|
} else if (event === 'error') {
|
|
@@ -1625,12 +1791,16 @@ function handleChatEvent(event, data, turn) {
|
|
|
1625
1791
|
const finalText = (turn.active?.textEl?.textContent ?? '').trim();
|
|
1626
1792
|
const tools = Number(data.toolCalls) || 0;
|
|
1627
1793
|
if (!finalText) {
|
|
1628
|
-
if (data.stopReason === '
|
|
1794
|
+
if (data.stopReason === 'no_progress') {
|
|
1795
|
+
setText(turn.assistant(), '⚠ El agente se quedó repitiendo las mismas herramientas sin avanzar.');
|
|
1796
|
+
} else if (data.stopReason === 'budget') {
|
|
1797
|
+
setText(turn.assistant(), '⚠ El agente alcanzó el presupuesto de tokens/tiempo sin respuesta final.');
|
|
1798
|
+
} else if (data.stopReason === 'max_rounds') {
|
|
1629
1799
|
setText(
|
|
1630
1800
|
turn.assistant(),
|
|
1631
1801
|
`⚠ El agente alcanzó el máximo de rondas (${data.rounds ?? '?'}) sin dejar una respuesta final.${
|
|
1632
1802
|
tools ? ` Hizo ${tools} llamadas a herramientas.` : ''
|
|
1633
|
-
}
|
|
1803
|
+
}`
|
|
1634
1804
|
);
|
|
1635
1805
|
} else if (tools) {
|
|
1636
1806
|
setText(turn.assistant(), '✓ El agente terminó sus herramientas, pero no devolvió un mensaje final.');
|
|
@@ -1639,6 +1809,7 @@ function handleChatEvent(event, data, turn) {
|
|
|
1639
1809
|
}
|
|
1640
1810
|
}
|
|
1641
1811
|
renderCompletion(turn.active, data);
|
|
1812
|
+
if (data.continuable) addContinueButton(turn, data);
|
|
1642
1813
|
updateStatus({ rounds: data.rounds, stopReason: data.stopReason });
|
|
1643
1814
|
addTokens(data.tokens);
|
|
1644
1815
|
} else if (event === 'aborted') {
|
|
@@ -1679,6 +1850,7 @@ async function attachRun(sessionKey, pane) {
|
|
|
1679
1850
|
async function sendMessage() {
|
|
1680
1851
|
const current = state.panes.get(state.sessionId);
|
|
1681
1852
|
if (current?.working) return;
|
|
1853
|
+
hideSlashMenu();
|
|
1682
1854
|
let input = chatInput.value.trim();
|
|
1683
1855
|
const images = state.attachments.slice();
|
|
1684
1856
|
if (input.startsWith('/') && !images.length) {
|
|
@@ -1778,11 +1950,13 @@ chatForm.addEventListener('submit', (event) => {
|
|
|
1778
1950
|
sendMessage();
|
|
1779
1951
|
});
|
|
1780
1952
|
chatInput.addEventListener('keydown', (event) => {
|
|
1953
|
+
if (handleSlashKey(event)) return;
|
|
1781
1954
|
if (event.key === 'Enter' && !event.shiftKey) {
|
|
1782
1955
|
event.preventDefault();
|
|
1783
1956
|
sendMessage();
|
|
1784
1957
|
}
|
|
1785
1958
|
});
|
|
1959
|
+
chatInput.addEventListener('input', onChatInputChanged);
|
|
1786
1960
|
|
|
1787
1961
|
/* --- image attachments ---------------------------------------------------- */
|
|
1788
1962
|
|
|
@@ -1956,6 +2130,99 @@ async function runCommand(name, arg) {
|
|
|
1956
2130
|
}
|
|
1957
2131
|
}
|
|
1958
2132
|
|
|
2133
|
+
/* --- menú de comandos "/" ------------------------------------------------- */
|
|
2134
|
+
|
|
2135
|
+
const slashMenuEl = $('#slash-menu');
|
|
2136
|
+
let slashMatches = [];
|
|
2137
|
+
let slashIndex = 0;
|
|
2138
|
+
|
|
2139
|
+
function slashCommandEntries() {
|
|
2140
|
+
return COMMANDS.map(([cmd, desc]) => {
|
|
2141
|
+
const name = cmd.split(' ')[0];
|
|
2142
|
+
const takesArgs = cmd.includes(' ');
|
|
2143
|
+
return { name, desc, insert: takesArgs ? `${name} ` : name, takesArgs };
|
|
2144
|
+
});
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
function hideSlashMenu() {
|
|
2148
|
+
if (!slashMenuEl) return;
|
|
2149
|
+
slashMenuEl.hidden = true;
|
|
2150
|
+
slashMenuEl.replaceChildren();
|
|
2151
|
+
slashMatches = [];
|
|
2152
|
+
slashIndex = 0;
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
function renderSlashMenu() {
|
|
2156
|
+
if (!slashMenuEl) return;
|
|
2157
|
+
slashMenuEl.replaceChildren();
|
|
2158
|
+
slashMatches.forEach((entry, index) => {
|
|
2159
|
+
const item = el('button', `slash-item${index === slashIndex ? ' active' : ''}`);
|
|
2160
|
+
item.type = 'button';
|
|
2161
|
+
item.append(el('span', 'slash-cmd', entry.name), el('span', 'slash-desc', entry.desc));
|
|
2162
|
+
// mousedown (no click) para no perder el foco del textarea antes de elegir.
|
|
2163
|
+
item.addEventListener('mousedown', (event) => {
|
|
2164
|
+
event.preventDefault();
|
|
2165
|
+
chooseSlash(index);
|
|
2166
|
+
});
|
|
2167
|
+
slashMenuEl.appendChild(item);
|
|
2168
|
+
});
|
|
2169
|
+
slashMenuEl.hidden = false;
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
function updateSlashMenu() {
|
|
2173
|
+
if (!slashMenuEl) return;
|
|
2174
|
+
const value = chatInput.value;
|
|
2175
|
+
if (!value.startsWith('/') || /\s/.test(value)) return hideSlashMenu();
|
|
2176
|
+
const query = value.slice(1).toLowerCase();
|
|
2177
|
+
slashMatches = slashCommandEntries().filter((entry) => entry.name.slice(1).toLowerCase().startsWith(query));
|
|
2178
|
+
if (!slashMatches.length) return hideSlashMenu();
|
|
2179
|
+
if (slashIndex >= slashMatches.length) slashIndex = 0;
|
|
2180
|
+
renderSlashMenu();
|
|
2181
|
+
}
|
|
2182
|
+
|
|
2183
|
+
function chooseSlash(index) {
|
|
2184
|
+
const entry = slashMatches[index];
|
|
2185
|
+
if (!entry) return;
|
|
2186
|
+
chatInput.value = entry.insert;
|
|
2187
|
+
chatInput.focus();
|
|
2188
|
+
hideSlashMenu();
|
|
2189
|
+
if (!entry.takesArgs) sendMessage();
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2192
|
+
function handleSlashKey(event) {
|
|
2193
|
+
if (!slashMenuEl || slashMenuEl.hidden || !slashMatches.length) return false;
|
|
2194
|
+
if (event.key === 'ArrowDown') {
|
|
2195
|
+
event.preventDefault();
|
|
2196
|
+
slashIndex = (slashIndex + 1) % slashMatches.length;
|
|
2197
|
+
renderSlashMenu();
|
|
2198
|
+
return true;
|
|
2199
|
+
}
|
|
2200
|
+
if (event.key === 'ArrowUp') {
|
|
2201
|
+
event.preventDefault();
|
|
2202
|
+
slashIndex = (slashIndex - 1 + slashMatches.length) % slashMatches.length;
|
|
2203
|
+
renderSlashMenu();
|
|
2204
|
+
return true;
|
|
2205
|
+
}
|
|
2206
|
+
if (event.key === 'Escape') {
|
|
2207
|
+
event.preventDefault();
|
|
2208
|
+
hideSlashMenu();
|
|
2209
|
+
return true;
|
|
2210
|
+
}
|
|
2211
|
+
if (event.key === 'Enter' || event.key === 'Tab') {
|
|
2212
|
+
event.preventDefault();
|
|
2213
|
+
chooseSlash(slashIndex);
|
|
2214
|
+
return true;
|
|
2215
|
+
}
|
|
2216
|
+
return false;
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
function onChatInputChanged() {
|
|
2220
|
+
slashIndex = 0;
|
|
2221
|
+
updateSlashMenu();
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
chatInput.addEventListener('blur', () => setTimeout(hideSlashMenu, 120));
|
|
2225
|
+
|
|
1959
2226
|
/* --- folder picker -------------------------------------------------------- */
|
|
1960
2227
|
|
|
1961
2228
|
const folderModal = $('#folder-modal');
|
|
@@ -2402,6 +2669,50 @@ const tgTranscribeEl = $('#tg-transcribe');
|
|
|
2402
2669
|
const tgTranscribeProviderEl = $('#tg-transcribe-provider');
|
|
2403
2670
|
const tgStatusEl = $('#tg-status');
|
|
2404
2671
|
|
|
2672
|
+
/* --- ajustes: pestañas internas + límites del agente --------------------- */
|
|
2673
|
+
|
|
2674
|
+
const ajustesTabs = document.querySelectorAll('.ajustes-tab');
|
|
2675
|
+
|
|
2676
|
+
function activateAjustesPanel(name) {
|
|
2677
|
+
ajustesTabs.forEach((tab) => tab.classList.toggle('active', tab.dataset.panel === name));
|
|
2678
|
+
document
|
|
2679
|
+
.querySelectorAll('.ajustes-panel')
|
|
2680
|
+
.forEach((panel) => panel.classList.toggle('active', panel.dataset.panel === name));
|
|
2681
|
+
if (name === 'agente') moveAgentControls(agentControlsSlot);
|
|
2682
|
+
}
|
|
2683
|
+
|
|
2684
|
+
ajustesTabs.forEach((tab) =>
|
|
2685
|
+
tab.addEventListener('click', () => activateAjustesPanel(tab.dataset.panel))
|
|
2686
|
+
);
|
|
2687
|
+
|
|
2688
|
+
const limitsRoundsEl = $('#limits-rounds');
|
|
2689
|
+
const limitsTokensEl = $('#limits-tokens');
|
|
2690
|
+
const limitsDurationEl = $('#limits-duration');
|
|
2691
|
+
|
|
2692
|
+
async function refreshLimits() {
|
|
2693
|
+
const data = await api('/api/settings').catch(() => null);
|
|
2694
|
+
const settings = data?.settings;
|
|
2695
|
+
if (!settings) return;
|
|
2696
|
+
if (limitsRoundsEl) limitsRoundsEl.value = settings.maxRounds ?? 0;
|
|
2697
|
+
if (limitsTokensEl) limitsTokensEl.value = settings.maxTokens ?? 0;
|
|
2698
|
+
if (limitsDurationEl) limitsDurationEl.value = Math.round((settings.maxDurationMs ?? 0) / 1000);
|
|
2699
|
+
}
|
|
2700
|
+
|
|
2701
|
+
$('#limits-save')?.addEventListener('click', async () => {
|
|
2702
|
+
const body = {
|
|
2703
|
+
maxRounds: Math.max(0, Number(limitsRoundsEl?.value) || 0),
|
|
2704
|
+
maxTokens: Math.max(0, Number(limitsTokensEl?.value) || 0),
|
|
2705
|
+
maxDurationMs: Math.max(0, Math.round((Number(limitsDurationEl?.value) || 0) * 1000)),
|
|
2706
|
+
};
|
|
2707
|
+
try {
|
|
2708
|
+
await api('/api/settings', { method: 'PUT', body });
|
|
2709
|
+
refreshLimits();
|
|
2710
|
+
setStatus('#limits-msg', 'guardado', 'ok');
|
|
2711
|
+
} catch (error) {
|
|
2712
|
+
setStatus('#limits-msg', error.message, 'err');
|
|
2713
|
+
}
|
|
2714
|
+
});
|
|
2715
|
+
|
|
2405
2716
|
function setStatus(node, text, cls = '') {
|
|
2406
2717
|
const elNode = $(node);
|
|
2407
2718
|
if (!elNode) return;
|
|
@@ -2443,6 +2754,8 @@ function syncMcpFormFields() {
|
|
|
2443
2754
|
const http = $('#mf-transport').value === 'http';
|
|
2444
2755
|
$('#mf-command-field').hidden = http;
|
|
2445
2756
|
$('#mf-args-field').hidden = http;
|
|
2757
|
+
$('#mf-env-field').hidden = http;
|
|
2758
|
+
$('#mf-inherit-field').hidden = http;
|
|
2446
2759
|
$('#mf-url-field').hidden = !http;
|
|
2447
2760
|
}
|
|
2448
2761
|
|
|
@@ -2458,6 +2771,7 @@ function openMcpForm(server = null) {
|
|
|
2458
2771
|
$('#mf-env').value = Object.entries(server?.env ?? {})
|
|
2459
2772
|
.map(([key, value]) => `${key}=${value ?? ''}`)
|
|
2460
2773
|
.join('\n');
|
|
2774
|
+
$('#mf-inherit-env').checked = server?.inheritEnv === true;
|
|
2461
2775
|
$('#mf-enabled').checked = server ? server.enabled !== false : true;
|
|
2462
2776
|
syncMcpFormFields();
|
|
2463
2777
|
setStatus('#mf-status', '');
|
|
@@ -2517,6 +2831,7 @@ async function submitMcp(event) {
|
|
|
2517
2831
|
args: $('#mf-args').value.trim(),
|
|
2518
2832
|
url: $('#mf-url').value.trim(),
|
|
2519
2833
|
env: parseEnv($('#mf-env').value),
|
|
2834
|
+
inheritEnv: $('#mf-inherit-env').checked,
|
|
2520
2835
|
enabled: $('#mf-enabled').checked,
|
|
2521
2836
|
};
|
|
2522
2837
|
try {
|
|
@@ -2897,6 +3212,7 @@ function setExplorerOpen(open) {
|
|
|
2897
3212
|
state.explorerOpen = open;
|
|
2898
3213
|
explorer.hidden = !open;
|
|
2899
3214
|
panels.explorer.resizer.hidden = !open;
|
|
3215
|
+
if (!open) restorePreviewMax?.();
|
|
2900
3216
|
const handle = $('#explorer-handle');
|
|
2901
3217
|
if (handle) {
|
|
2902
3218
|
handle.hidden = open;
|
|
@@ -3164,14 +3480,23 @@ function setPreviewStatus(text, isError = false) {
|
|
|
3164
3480
|
}
|
|
3165
3481
|
|
|
3166
3482
|
/* Ancho por dispositivo + maximizar, compartido por preview y visor de archivos. */
|
|
3167
|
-
function setupPreviewTools({ container, stage, frame, select, maxButton }) {
|
|
3483
|
+
function setupPreviewTools({ container, stage, frame, select, maxButton, teleport = false }) {
|
|
3168
3484
|
const applyDevice = () => {
|
|
3169
3485
|
const value = select?.value || 'auto';
|
|
3170
3486
|
if (stage) stage.dataset.device = value;
|
|
3171
3487
|
frame.style.width = value === 'auto' ? '100%' : `${value}px`;
|
|
3172
3488
|
};
|
|
3173
3489
|
select?.addEventListener('change', applyDevice);
|
|
3490
|
+
// El panel del explorador usa `backdrop-filter`, que crea un containing block
|
|
3491
|
+
// y dejaria el `position: fixed` encerrado en la barra. Al maximizar movemos
|
|
3492
|
+
// el contenedor a `body` y lo devolvemos a su lugar al restaurar.
|
|
3493
|
+
const home = teleport ? container?.parentNode : null;
|
|
3494
|
+
const homeAfter = teleport ? container?.nextSibling : null;
|
|
3174
3495
|
const setMax = (on) => {
|
|
3496
|
+
if (teleport && container && home) {
|
|
3497
|
+
if (on) document.body.appendChild(container);
|
|
3498
|
+
else if (!home.contains(container)) home.insertBefore(container, homeAfter ?? null);
|
|
3499
|
+
}
|
|
3175
3500
|
container?.classList.toggle('preview-maximized', on);
|
|
3176
3501
|
maxButton?.setAttribute('aria-pressed', String(on));
|
|
3177
3502
|
if (maxButton) maxButton.title = on ? 'restaurar' : 'maximizar';
|
|
@@ -3189,7 +3514,9 @@ const previewTools = setupPreviewTools({
|
|
|
3189
3514
|
frame: previewFrame,
|
|
3190
3515
|
select: $('#preview-device'),
|
|
3191
3516
|
maxButton: $('#preview-max'),
|
|
3517
|
+
teleport: true,
|
|
3192
3518
|
});
|
|
3519
|
+
restorePreviewMax = () => previewTools.setMax(false);
|
|
3193
3520
|
previewTools.applyDevice();
|
|
3194
3521
|
|
|
3195
3522
|
function loadPreview(url) {
|