@danieltmn/tamnora 0.6.1 → 0.7.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 +29 -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 +222 -18
- package/harness/packages/server/public/index.html +93 -26
- package/harness/packages/server/public/styles.css +61 -1
- package/harness/packages/server/src/lib/agentTools.js +9 -3
- package/harness/packages/server/src/lib/channels/index.js +11 -4
- package/harness/packages/server/src/lib/channels/telegram.js +28 -10
- package/harness/packages/server/src/routes/channels.js +1 -0
- 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/server.test.js +78 -2
- package/harness/tamnora.example.json +17 -1
- package/package.json +1 -1
|
@@ -178,7 +178,7 @@ function activateTab(name) {
|
|
|
178
178
|
hideMoreMenu();
|
|
179
179
|
if (name === 'runs') refreshRuns();
|
|
180
180
|
if (name === 'providers') refreshProviders();
|
|
181
|
-
if (name === 'ajustes') { refreshMcp(); refreshSkills(); refreshChannels(); }
|
|
181
|
+
if (name === 'ajustes') { refreshMcp(); refreshSkills(); refreshChannels(); refreshLimits(); }
|
|
182
182
|
}
|
|
183
183
|
|
|
184
184
|
document.querySelectorAll('.tab[data-tab]').forEach((t) =>
|
|
@@ -203,27 +203,61 @@ document.addEventListener('keydown', (event) => {
|
|
|
203
203
|
/* --- settings modal (modo / proveedor / modelo / auto) -------------------- */
|
|
204
204
|
|
|
205
205
|
const settingsModal = $('#settings-modal');
|
|
206
|
+
const agentControls = $('#agent-settings-controls');
|
|
207
|
+
const agentControlsSlot = $('#agent-controls-slot');
|
|
208
|
+
const settingsControlsSlot = $('#settings-controls-slot');
|
|
209
|
+
|
|
210
|
+
/** Los controles modo/proveedor/modelo/auto se comparten entre el tab Agente
|
|
211
|
+
* y el modal: se mueven de un slot al otro para no duplicar IDs ni estado. */
|
|
212
|
+
function moveAgentControls(target) {
|
|
213
|
+
if (agentControls && target) target.appendChild(agentControls);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function openSettingsModal() {
|
|
217
|
+
if (!settingsModal) return;
|
|
218
|
+
moveAgentControls(settingsControlsSlot);
|
|
219
|
+
syncDefaultCwd();
|
|
220
|
+
settingsModal.showModal();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function closeSettingsModal() {
|
|
224
|
+
moveAgentControls(agentControlsSlot);
|
|
225
|
+
settingsModal?.close();
|
|
226
|
+
}
|
|
227
|
+
|
|
206
228
|
if (settingsModal) {
|
|
207
|
-
$('#settings-toggle')?.addEventListener('click',
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
});
|
|
211
|
-
$('#settings-close')?.addEventListener('click', () => settingsModal.close());
|
|
212
|
-
$('#settings-done')?.addEventListener('click', () => settingsModal.close());
|
|
229
|
+
$('#settings-toggle')?.addEventListener('click', openSettingsModal);
|
|
230
|
+
$('#settings-close')?.addEventListener('click', closeSettingsModal);
|
|
231
|
+
$('#settings-done')?.addEventListener('click', closeSettingsModal);
|
|
213
232
|
$('#settings-providers')?.addEventListener('click', () => {
|
|
214
|
-
|
|
233
|
+
closeSettingsModal();
|
|
215
234
|
activateTab('providers');
|
|
216
235
|
});
|
|
217
|
-
$('#
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
});
|
|
236
|
+
$('#agent-settings-open')?.addEventListener('click', openSettingsModal);
|
|
237
|
+
$('#agent-providers')?.addEventListener('click', () => activateTab('providers'));
|
|
238
|
+
$('#default-cwd-pick')?.addEventListener('click', () => openFolderPicker('default'));
|
|
221
239
|
$('#default-cwd-clear')?.addEventListener('click', () => {
|
|
222
240
|
writeDefaultCwd('');
|
|
223
241
|
syncDefaultCwd();
|
|
224
242
|
});
|
|
225
243
|
settingsModal.addEventListener('click', (event) => {
|
|
226
|
-
if (event.target === settingsModal)
|
|
244
|
+
if (event.target === settingsModal) closeSettingsModal();
|
|
245
|
+
});
|
|
246
|
+
settingsModal.addEventListener('close', () => moveAgentControls(agentControlsSlot));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// El footer (modo · proveedor · modelo · estado) abre el modal de ajustes.
|
|
250
|
+
const statusbarEl = $('#statusbar');
|
|
251
|
+
if (statusbarEl) {
|
|
252
|
+
statusbarEl.addEventListener('click', (event) => {
|
|
253
|
+
if (event.target.closest('#sb-context')) return;
|
|
254
|
+
openSettingsModal();
|
|
255
|
+
});
|
|
256
|
+
statusbarEl.addEventListener('keydown', (event) => {
|
|
257
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
258
|
+
event.preventDefault();
|
|
259
|
+
openSettingsModal();
|
|
260
|
+
}
|
|
227
261
|
});
|
|
228
262
|
}
|
|
229
263
|
|
|
@@ -235,7 +269,7 @@ const PALETTE_COMMANDS = [
|
|
|
235
269
|
{ id: 'compact', label: 'Compactar la conversación', hint: '/compact', run: () => runSlash('compact') },
|
|
236
270
|
{ id: 'clear', label: 'Limpiar la conversación', hint: '/clear', run: () => runSlash('clear') },
|
|
237
271
|
{ id: 'mode', label: 'Cambiar Plan / Build', hint: 'Ctrl+.', run: () => toggleMode() },
|
|
238
|
-
{ id: 'settings', label: 'Ajustes del agente (modelo, modo, auto)', run: () =>
|
|
272
|
+
{ id: 'settings', label: 'Ajustes del agente (modelo, modo, auto)', run: () => openSettingsModal() },
|
|
239
273
|
{ id: 'providers', label: 'Gestionar proveedores', run: () => activateTab('providers') },
|
|
240
274
|
{ id: 'ajustes', label: 'MCP y Skills (ajustes)', run: () => activateTab('ajustes') },
|
|
241
275
|
{ id: 'explorer', label: 'Mostrar u ocultar archivos', run: () => setExplorerOpen(!state.explorerOpen) },
|
|
@@ -1545,11 +1579,25 @@ function renderCompletion(message, data) {
|
|
|
1545
1579
|
const duration = formatDuration(data.durationMs);
|
|
1546
1580
|
if (duration) parts.push(duration);
|
|
1547
1581
|
if (data.tokens?.total_tokens) parts.push(`${Number(data.tokens.total_tokens).toLocaleString('es')} tokens`);
|
|
1548
|
-
const
|
|
1549
|
-
|
|
1582
|
+
const cut = ['max_rounds', 'no_progress', 'budget'].includes(data.stopReason);
|
|
1583
|
+
const label = cut ? (data.wrappedUp ? '⚠ corte de seguridad · resumen' : '⚠ corte de seguridad') : '✓ terminado';
|
|
1584
|
+
message.tools.appendChild(el('div', cut ? 'chip warn' : 'chip ok', `${label} · ${parts.join(' · ')}`));
|
|
1550
1585
|
scrollMessages(paneOf(message));
|
|
1551
1586
|
}
|
|
1552
1587
|
|
|
1588
|
+
/** Retoma un turno cortado por límite/bucle sin tener que escribir "continúa". */
|
|
1589
|
+
function addContinueButton(turn, data) {
|
|
1590
|
+
const btn = el('button', 'chip', data.wrappedUp ? '↻ Continuar el trabajo' : '↻ Continuar');
|
|
1591
|
+
btn.type = 'button';
|
|
1592
|
+
btn.addEventListener('click', () => {
|
|
1593
|
+
if (state.panes.get(turn.sessionKey)?.working) return;
|
|
1594
|
+
chatInput.value = 'Continuá donde quedaste.';
|
|
1595
|
+
sendMessage();
|
|
1596
|
+
});
|
|
1597
|
+
turn.assistant().tools.appendChild(btn);
|
|
1598
|
+
scrollMessages(paneOf(turn.assistant()));
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1553
1601
|
function handleChatEvent(event, data, turn) {
|
|
1554
1602
|
if (handleAgentEvent(event, data, turn)) return;
|
|
1555
1603
|
if (event === 'agent_start') {
|
|
@@ -1615,6 +1663,8 @@ function handleChatEvent(event, data, turn) {
|
|
|
1615
1663
|
if (!turn.sawText) setText(turn.assistant(), `↻ reintentando (${data.attempt}) en ${Math.round((data.delayMs || 0) / 1000)} s...`);
|
|
1616
1664
|
} else if (event === 'trim') {
|
|
1617
1665
|
turn.assistant().tools.appendChild(el('div', 'chip', `✂ contexto recortado (${data.trimmed} resultados, ${data.dropped} mensajes)`));
|
|
1666
|
+
} else if (event === 'repeat') {
|
|
1667
|
+
turn.assistant().tools.appendChild(el('div', 'chip warn', `↻ ${data.name} repetida sin cambios`));
|
|
1618
1668
|
} else if (event === 'warning') {
|
|
1619
1669
|
turn.assistant().tools.appendChild(el('div', 'chip', `⚠ ${data.message}`));
|
|
1620
1670
|
} else if (event === 'error') {
|
|
@@ -1625,12 +1675,16 @@ function handleChatEvent(event, data, turn) {
|
|
|
1625
1675
|
const finalText = (turn.active?.textEl?.textContent ?? '').trim();
|
|
1626
1676
|
const tools = Number(data.toolCalls) || 0;
|
|
1627
1677
|
if (!finalText) {
|
|
1628
|
-
if (data.stopReason === '
|
|
1678
|
+
if (data.stopReason === 'no_progress') {
|
|
1679
|
+
setText(turn.assistant(), '⚠ El agente se quedó repitiendo las mismas herramientas sin avanzar.');
|
|
1680
|
+
} else if (data.stopReason === 'budget') {
|
|
1681
|
+
setText(turn.assistant(), '⚠ El agente alcanzó el presupuesto de tokens/tiempo sin respuesta final.');
|
|
1682
|
+
} else if (data.stopReason === 'max_rounds') {
|
|
1629
1683
|
setText(
|
|
1630
1684
|
turn.assistant(),
|
|
1631
1685
|
`⚠ El agente alcanzó el máximo de rondas (${data.rounds ?? '?'}) sin dejar una respuesta final.${
|
|
1632
1686
|
tools ? ` Hizo ${tools} llamadas a herramientas.` : ''
|
|
1633
|
-
}
|
|
1687
|
+
}`
|
|
1634
1688
|
);
|
|
1635
1689
|
} else if (tools) {
|
|
1636
1690
|
setText(turn.assistant(), '✓ El agente terminó sus herramientas, pero no devolvió un mensaje final.');
|
|
@@ -1639,6 +1693,7 @@ function handleChatEvent(event, data, turn) {
|
|
|
1639
1693
|
}
|
|
1640
1694
|
}
|
|
1641
1695
|
renderCompletion(turn.active, data);
|
|
1696
|
+
if (data.continuable) addContinueButton(turn, data);
|
|
1642
1697
|
updateStatus({ rounds: data.rounds, stopReason: data.stopReason });
|
|
1643
1698
|
addTokens(data.tokens);
|
|
1644
1699
|
} else if (event === 'aborted') {
|
|
@@ -1679,6 +1734,7 @@ async function attachRun(sessionKey, pane) {
|
|
|
1679
1734
|
async function sendMessage() {
|
|
1680
1735
|
const current = state.panes.get(state.sessionId);
|
|
1681
1736
|
if (current?.working) return;
|
|
1737
|
+
hideSlashMenu();
|
|
1682
1738
|
let input = chatInput.value.trim();
|
|
1683
1739
|
const images = state.attachments.slice();
|
|
1684
1740
|
if (input.startsWith('/') && !images.length) {
|
|
@@ -1778,11 +1834,13 @@ chatForm.addEventListener('submit', (event) => {
|
|
|
1778
1834
|
sendMessage();
|
|
1779
1835
|
});
|
|
1780
1836
|
chatInput.addEventListener('keydown', (event) => {
|
|
1837
|
+
if (handleSlashKey(event)) return;
|
|
1781
1838
|
if (event.key === 'Enter' && !event.shiftKey) {
|
|
1782
1839
|
event.preventDefault();
|
|
1783
1840
|
sendMessage();
|
|
1784
1841
|
}
|
|
1785
1842
|
});
|
|
1843
|
+
chatInput.addEventListener('input', onChatInputChanged);
|
|
1786
1844
|
|
|
1787
1845
|
/* --- image attachments ---------------------------------------------------- */
|
|
1788
1846
|
|
|
@@ -1956,6 +2014,99 @@ async function runCommand(name, arg) {
|
|
|
1956
2014
|
}
|
|
1957
2015
|
}
|
|
1958
2016
|
|
|
2017
|
+
/* --- menú de comandos "/" ------------------------------------------------- */
|
|
2018
|
+
|
|
2019
|
+
const slashMenuEl = $('#slash-menu');
|
|
2020
|
+
let slashMatches = [];
|
|
2021
|
+
let slashIndex = 0;
|
|
2022
|
+
|
|
2023
|
+
function slashCommandEntries() {
|
|
2024
|
+
return COMMANDS.map(([cmd, desc]) => {
|
|
2025
|
+
const name = cmd.split(' ')[0];
|
|
2026
|
+
const takesArgs = cmd.includes(' ');
|
|
2027
|
+
return { name, desc, insert: takesArgs ? `${name} ` : name, takesArgs };
|
|
2028
|
+
});
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
function hideSlashMenu() {
|
|
2032
|
+
if (!slashMenuEl) return;
|
|
2033
|
+
slashMenuEl.hidden = true;
|
|
2034
|
+
slashMenuEl.replaceChildren();
|
|
2035
|
+
slashMatches = [];
|
|
2036
|
+
slashIndex = 0;
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
function renderSlashMenu() {
|
|
2040
|
+
if (!slashMenuEl) return;
|
|
2041
|
+
slashMenuEl.replaceChildren();
|
|
2042
|
+
slashMatches.forEach((entry, index) => {
|
|
2043
|
+
const item = el('button', `slash-item${index === slashIndex ? ' active' : ''}`);
|
|
2044
|
+
item.type = 'button';
|
|
2045
|
+
item.append(el('span', 'slash-cmd', entry.name), el('span', 'slash-desc', entry.desc));
|
|
2046
|
+
// mousedown (no click) para no perder el foco del textarea antes de elegir.
|
|
2047
|
+
item.addEventListener('mousedown', (event) => {
|
|
2048
|
+
event.preventDefault();
|
|
2049
|
+
chooseSlash(index);
|
|
2050
|
+
});
|
|
2051
|
+
slashMenuEl.appendChild(item);
|
|
2052
|
+
});
|
|
2053
|
+
slashMenuEl.hidden = false;
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
function updateSlashMenu() {
|
|
2057
|
+
if (!slashMenuEl) return;
|
|
2058
|
+
const value = chatInput.value;
|
|
2059
|
+
if (!value.startsWith('/') || /\s/.test(value)) return hideSlashMenu();
|
|
2060
|
+
const query = value.slice(1).toLowerCase();
|
|
2061
|
+
slashMatches = slashCommandEntries().filter((entry) => entry.name.slice(1).toLowerCase().startsWith(query));
|
|
2062
|
+
if (!slashMatches.length) return hideSlashMenu();
|
|
2063
|
+
if (slashIndex >= slashMatches.length) slashIndex = 0;
|
|
2064
|
+
renderSlashMenu();
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
function chooseSlash(index) {
|
|
2068
|
+
const entry = slashMatches[index];
|
|
2069
|
+
if (!entry) return;
|
|
2070
|
+
chatInput.value = entry.insert;
|
|
2071
|
+
chatInput.focus();
|
|
2072
|
+
hideSlashMenu();
|
|
2073
|
+
if (!entry.takesArgs) sendMessage();
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
function handleSlashKey(event) {
|
|
2077
|
+
if (!slashMenuEl || slashMenuEl.hidden || !slashMatches.length) return false;
|
|
2078
|
+
if (event.key === 'ArrowDown') {
|
|
2079
|
+
event.preventDefault();
|
|
2080
|
+
slashIndex = (slashIndex + 1) % slashMatches.length;
|
|
2081
|
+
renderSlashMenu();
|
|
2082
|
+
return true;
|
|
2083
|
+
}
|
|
2084
|
+
if (event.key === 'ArrowUp') {
|
|
2085
|
+
event.preventDefault();
|
|
2086
|
+
slashIndex = (slashIndex - 1 + slashMatches.length) % slashMatches.length;
|
|
2087
|
+
renderSlashMenu();
|
|
2088
|
+
return true;
|
|
2089
|
+
}
|
|
2090
|
+
if (event.key === 'Escape') {
|
|
2091
|
+
event.preventDefault();
|
|
2092
|
+
hideSlashMenu();
|
|
2093
|
+
return true;
|
|
2094
|
+
}
|
|
2095
|
+
if (event.key === 'Enter' || event.key === 'Tab') {
|
|
2096
|
+
event.preventDefault();
|
|
2097
|
+
chooseSlash(slashIndex);
|
|
2098
|
+
return true;
|
|
2099
|
+
}
|
|
2100
|
+
return false;
|
|
2101
|
+
}
|
|
2102
|
+
|
|
2103
|
+
function onChatInputChanged() {
|
|
2104
|
+
slashIndex = 0;
|
|
2105
|
+
updateSlashMenu();
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
chatInput.addEventListener('blur', () => setTimeout(hideSlashMenu, 120));
|
|
2109
|
+
|
|
1959
2110
|
/* --- folder picker -------------------------------------------------------- */
|
|
1960
2111
|
|
|
1961
2112
|
const folderModal = $('#folder-modal');
|
|
@@ -2399,8 +2550,53 @@ const skillForm = $('#skill-form');
|
|
|
2399
2550
|
const tgTokenEl = $('#tg-token');
|
|
2400
2551
|
const tgChatsEl = $('#tg-chats');
|
|
2401
2552
|
const tgTranscribeEl = $('#tg-transcribe');
|
|
2553
|
+
const tgTranscribeProviderEl = $('#tg-transcribe-provider');
|
|
2402
2554
|
const tgStatusEl = $('#tg-status');
|
|
2403
2555
|
|
|
2556
|
+
/* --- ajustes: pestañas internas + límites del agente --------------------- */
|
|
2557
|
+
|
|
2558
|
+
const ajustesTabs = document.querySelectorAll('.ajustes-tab');
|
|
2559
|
+
|
|
2560
|
+
function activateAjustesPanel(name) {
|
|
2561
|
+
ajustesTabs.forEach((tab) => tab.classList.toggle('active', tab.dataset.panel === name));
|
|
2562
|
+
document
|
|
2563
|
+
.querySelectorAll('.ajustes-panel')
|
|
2564
|
+
.forEach((panel) => panel.classList.toggle('active', panel.dataset.panel === name));
|
|
2565
|
+
if (name === 'agente') moveAgentControls(agentControlsSlot);
|
|
2566
|
+
}
|
|
2567
|
+
|
|
2568
|
+
ajustesTabs.forEach((tab) =>
|
|
2569
|
+
tab.addEventListener('click', () => activateAjustesPanel(tab.dataset.panel))
|
|
2570
|
+
);
|
|
2571
|
+
|
|
2572
|
+
const limitsRoundsEl = $('#limits-rounds');
|
|
2573
|
+
const limitsTokensEl = $('#limits-tokens');
|
|
2574
|
+
const limitsDurationEl = $('#limits-duration');
|
|
2575
|
+
|
|
2576
|
+
async function refreshLimits() {
|
|
2577
|
+
const data = await api('/api/settings').catch(() => null);
|
|
2578
|
+
const settings = data?.settings;
|
|
2579
|
+
if (!settings) return;
|
|
2580
|
+
if (limitsRoundsEl) limitsRoundsEl.value = settings.maxRounds ?? 0;
|
|
2581
|
+
if (limitsTokensEl) limitsTokensEl.value = settings.maxTokens ?? 0;
|
|
2582
|
+
if (limitsDurationEl) limitsDurationEl.value = Math.round((settings.maxDurationMs ?? 0) / 1000);
|
|
2583
|
+
}
|
|
2584
|
+
|
|
2585
|
+
$('#limits-save')?.addEventListener('click', async () => {
|
|
2586
|
+
const body = {
|
|
2587
|
+
maxRounds: Math.max(0, Number(limitsRoundsEl?.value) || 0),
|
|
2588
|
+
maxTokens: Math.max(0, Number(limitsTokensEl?.value) || 0),
|
|
2589
|
+
maxDurationMs: Math.max(0, Math.round((Number(limitsDurationEl?.value) || 0) * 1000)),
|
|
2590
|
+
};
|
|
2591
|
+
try {
|
|
2592
|
+
await api('/api/settings', { method: 'PUT', body });
|
|
2593
|
+
refreshLimits();
|
|
2594
|
+
setStatus('#limits-msg', 'guardado', 'ok');
|
|
2595
|
+
} catch (error) {
|
|
2596
|
+
setStatus('#limits-msg', error.message, 'err');
|
|
2597
|
+
}
|
|
2598
|
+
});
|
|
2599
|
+
|
|
2404
2600
|
function setStatus(node, text, cls = '') {
|
|
2405
2601
|
const elNode = $(node);
|
|
2406
2602
|
if (!elNode) return;
|
|
@@ -2442,6 +2638,8 @@ function syncMcpFormFields() {
|
|
|
2442
2638
|
const http = $('#mf-transport').value === 'http';
|
|
2443
2639
|
$('#mf-command-field').hidden = http;
|
|
2444
2640
|
$('#mf-args-field').hidden = http;
|
|
2641
|
+
$('#mf-env-field').hidden = http;
|
|
2642
|
+
$('#mf-inherit-field').hidden = http;
|
|
2445
2643
|
$('#mf-url-field').hidden = !http;
|
|
2446
2644
|
}
|
|
2447
2645
|
|
|
@@ -2457,6 +2655,7 @@ function openMcpForm(server = null) {
|
|
|
2457
2655
|
$('#mf-env').value = Object.entries(server?.env ?? {})
|
|
2458
2656
|
.map(([key, value]) => `${key}=${value ?? ''}`)
|
|
2459
2657
|
.join('\n');
|
|
2658
|
+
$('#mf-inherit-env').checked = server?.inheritEnv === true;
|
|
2460
2659
|
$('#mf-enabled').checked = server ? server.enabled !== false : true;
|
|
2461
2660
|
syncMcpFormFields();
|
|
2462
2661
|
setStatus('#mf-status', '');
|
|
@@ -2516,6 +2715,7 @@ async function submitMcp(event) {
|
|
|
2516
2715
|
args: $('#mf-args').value.trim(),
|
|
2517
2716
|
url: $('#mf-url').value.trim(),
|
|
2518
2717
|
env: parseEnv($('#mf-env').value),
|
|
2718
|
+
inheritEnv: $('#mf-inherit-env').checked,
|
|
2519
2719
|
enabled: $('#mf-enabled').checked,
|
|
2520
2720
|
};
|
|
2521
2721
|
try {
|
|
@@ -2631,6 +2831,9 @@ async function refreshChannels() {
|
|
|
2631
2831
|
if (tgTranscribeEl && document.activeElement !== tgTranscribeEl) {
|
|
2632
2832
|
tgTranscribeEl.value = tg.transcribeModel ?? 'whisper-1';
|
|
2633
2833
|
}
|
|
2834
|
+
if (tgTranscribeProviderEl && document.activeElement !== tgTranscribeProviderEl) {
|
|
2835
|
+
tgTranscribeProviderEl.value = tg.transcribeProvider ?? '';
|
|
2836
|
+
}
|
|
2634
2837
|
if (tgTokenEl && document.activeElement !== tgTokenEl) {
|
|
2635
2838
|
tgTokenEl.value = '';
|
|
2636
2839
|
tgTokenEl.placeholder = tg.tokenHint ? `${tg.tokenHint} (dejar vacío para no cambiar)` : '123456:ABC…';
|
|
@@ -2643,6 +2846,7 @@ async function submitChannel() {
|
|
|
2643
2846
|
const body = {
|
|
2644
2847
|
allowedChatIds: tgChatsEl?.value ?? '',
|
|
2645
2848
|
transcribeModel: tgTranscribeEl?.value.trim() || 'whisper-1',
|
|
2849
|
+
transcribeProvider: tgTranscribeProviderEl?.value.trim() || '',
|
|
2646
2850
|
};
|
|
2647
2851
|
const token = tgTokenEl.value.trim();
|
|
2648
2852
|
if (token) body.token = token;
|
|
@@ -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.7.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>
|
|
@@ -118,6 +118,7 @@
|
|
|
118
118
|
</div>
|
|
119
119
|
</div>
|
|
120
120
|
<form id="chat-form" class="composer">
|
|
121
|
+
<div id="slash-menu" class="slash-menu" hidden></div>
|
|
121
122
|
<div id="attach-preview" class="attach-preview" hidden></div>
|
|
122
123
|
<div class="composer-row">
|
|
123
124
|
<span class="prompt-mark">❯</span>
|
|
@@ -134,7 +135,7 @@
|
|
|
134
135
|
<button class="btn" type="button" id="stop-btn" title="Detener la respuesta" hidden>parar</button>
|
|
135
136
|
</div>
|
|
136
137
|
</form>
|
|
137
|
-
<div class="statusbar">
|
|
138
|
+
<div id="statusbar" class="statusbar" title="cambiar modo, proveedor y modelo" role="button" tabindex="0">
|
|
138
139
|
<span id="sb-mode">agent</span>
|
|
139
140
|
<span class="dot">·</span>
|
|
140
141
|
<span id="sb-provider">—</span>
|
|
@@ -343,7 +344,62 @@
|
|
|
343
344
|
</section>
|
|
344
345
|
|
|
345
346
|
<section id="view-ajustes" class="view">
|
|
346
|
-
<div class="
|
|
347
|
+
<div class="ajustes-wrap">
|
|
348
|
+
<nav class="ajustes-tabs" role="tablist" aria-label="secciones de ajustes">
|
|
349
|
+
<button type="button" class="ajustes-tab active" data-panel="agente" role="tab">Agente</button>
|
|
350
|
+
<button type="button" class="ajustes-tab" data-panel="sesiones" role="tab">Sesiones</button>
|
|
351
|
+
<button type="button" class="ajustes-tab" data-panel="mcp" role="tab">MCP</button>
|
|
352
|
+
<button type="button" class="ajustes-tab" data-panel="skills" role="tab">Skills</button>
|
|
353
|
+
<button type="button" class="ajustes-tab" data-panel="telegram" role="tab">Telegram</button>
|
|
354
|
+
<button type="button" class="ajustes-tab" data-panel="limites" role="tab">Límites</button>
|
|
355
|
+
</nav>
|
|
356
|
+
|
|
357
|
+
<div class="panel ajustes-panel active" data-panel="agente">
|
|
358
|
+
<div class="row between">
|
|
359
|
+
<h2>Agente</h2>
|
|
360
|
+
</div>
|
|
361
|
+
<p class="muted verify-line">
|
|
362
|
+
Modo, proveedor, modelo y aprobación automática del agente.
|
|
363
|
+
</p>
|
|
364
|
+
<div id="agent-controls-slot">
|
|
365
|
+
<div id="agent-settings-controls">
|
|
366
|
+
<label for="mode">Modo</label>
|
|
367
|
+
<select id="mode" title="modo de agente"></select>
|
|
368
|
+
<label for="provider">Proveedor</label>
|
|
369
|
+
<select id="provider" title="proveedor"></select>
|
|
370
|
+
<label for="model">Modelo</label>
|
|
371
|
+
<select id="model" title="modelo"></select>
|
|
372
|
+
<label class="check" title="aprobar automáticamente tools con permiso ask">
|
|
373
|
+
<input id="yes" type="checkbox" /> Aprobar automáticamente las tools (auto)
|
|
374
|
+
</label>
|
|
375
|
+
</div>
|
|
376
|
+
</div>
|
|
377
|
+
<div class="row">
|
|
378
|
+
<button type="button" id="agent-settings-open" class="btn">abrir en ventana</button>
|
|
379
|
+
<button type="button" id="agent-providers" class="btn">gestionar proveedores</button>
|
|
380
|
+
</div>
|
|
381
|
+
</div>
|
|
382
|
+
|
|
383
|
+
<div class="panel ajustes-panel" data-panel="sesiones">
|
|
384
|
+
<div class="row between">
|
|
385
|
+
<h2>Sesiones</h2>
|
|
386
|
+
</div>
|
|
387
|
+
<p class="muted verify-line">
|
|
388
|
+
Carpeta donde se abre el selector de carpetas y <code>/new</code> cuando no hay sesión.
|
|
389
|
+
</p>
|
|
390
|
+
<label for="default-cwd">Carpeta predeterminada para nuevas sesiones</label>
|
|
391
|
+
<div class="row default-cwd-row">
|
|
392
|
+
<input id="default-cwd" type="text" readonly placeholder="(la casa del usuario)" title="carpeta donde se abre el selector y /new sin sesión" />
|
|
393
|
+
<button type="button" id="default-cwd-pick" class="btn">elegir</button>
|
|
394
|
+
<button type="button" id="default-cwd-clear" class="btn icon" title="quitar la carpeta predeterminada" aria-label="quitar la carpeta predeterminada">
|
|
395
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
396
|
+
<path d="M18 6 6 18M6 6l12 12" />
|
|
397
|
+
</svg>
|
|
398
|
+
</button>
|
|
399
|
+
</div>
|
|
400
|
+
</div>
|
|
401
|
+
|
|
402
|
+
<div class="panel ajustes-panel" data-panel="mcp">
|
|
347
403
|
<div class="row between">
|
|
348
404
|
<h2>Servidores MCP</h2>
|
|
349
405
|
<div class="row">
|
|
@@ -362,7 +418,7 @@
|
|
|
362
418
|
<ul id="mcp-list" class="providers"></ul>
|
|
363
419
|
</div>
|
|
364
420
|
|
|
365
|
-
<div class="panel">
|
|
421
|
+
<div class="panel ajustes-panel" data-panel="skills">
|
|
366
422
|
<div class="row between">
|
|
367
423
|
<h2>Skills</h2>
|
|
368
424
|
<button id="skill-new" class="btn">+ nueva</button>
|
|
@@ -378,7 +434,7 @@
|
|
|
378
434
|
<ul id="skill-list" class="providers"></ul>
|
|
379
435
|
</div>
|
|
380
436
|
|
|
381
|
-
<div class="panel">
|
|
437
|
+
<div class="panel ajustes-panel" data-panel="telegram">
|
|
382
438
|
<div class="row between">
|
|
383
439
|
<h2>Telegram</h2>
|
|
384
440
|
<span id="tg-status" class="summary">cargando…</span>
|
|
@@ -394,6 +450,10 @@
|
|
|
394
450
|
<label class="field"><span>chats autorizados (ids separados por coma)</span>
|
|
395
451
|
<input id="tg-chats" spellcheck="false" placeholder="123456789, -1001234567890" />
|
|
396
452
|
</label>
|
|
453
|
+
<label class="field"><span>proveedor de transcripción (voz, opcional)</span>
|
|
454
|
+
<input id="tg-transcribe-provider" spellcheck="false"
|
|
455
|
+
placeholder="openai · groq — vacío = proveedor activo" />
|
|
456
|
+
</label>
|
|
397
457
|
<label class="field"><span>modelo de transcripción (voz)</span>
|
|
398
458
|
<input id="tg-transcribe" spellcheck="false" placeholder="whisper-1" />
|
|
399
459
|
</label>
|
|
@@ -412,6 +472,30 @@
|
|
|
412
472
|
<span id="tg-msg" class="summary"></span>
|
|
413
473
|
</div>
|
|
414
474
|
</div>
|
|
475
|
+
|
|
476
|
+
<div class="panel ajustes-panel" data-panel="limites">
|
|
477
|
+
<div class="row between">
|
|
478
|
+
<h2>Límites del agente</h2>
|
|
479
|
+
</div>
|
|
480
|
+
<p class="muted verify-line">
|
|
481
|
+
El agente trabaja hasta terminar solo (<code>end_turn</code>). Estos topes son
|
|
482
|
+
opcionales: <code>0</code> = sin límite. Al cortarse, cierra con un resumen.
|
|
483
|
+
</p>
|
|
484
|
+
<label class="field"><span>máximo de rondas de herramientas (0 = sin límite)</span>
|
|
485
|
+
<input id="limits-rounds" type="number" min="0" step="1" placeholder="0" />
|
|
486
|
+
</label>
|
|
487
|
+
<label class="field"><span>presupuesto de tokens (0 = sin límite)</span>
|
|
488
|
+
<input id="limits-tokens" type="number" min="0" step="1000" placeholder="0" />
|
|
489
|
+
</label>
|
|
490
|
+
<label class="field"><span>presupuesto de tiempo en segundos (0 = sin límite)</span>
|
|
491
|
+
<input id="limits-duration" type="number" min="0" step="1" placeholder="0" />
|
|
492
|
+
</label>
|
|
493
|
+
<div class="row">
|
|
494
|
+
<button id="limits-save" class="btn primary" type="button">guardar</button>
|
|
495
|
+
<span id="limits-msg" class="summary"></span>
|
|
496
|
+
</div>
|
|
497
|
+
</div>
|
|
498
|
+
</div>
|
|
415
499
|
</section>
|
|
416
500
|
|
|
417
501
|
<dialog id="folder-modal" class="modal folder-modal">
|
|
@@ -481,25 +565,7 @@
|
|
|
481
565
|
</svg>
|
|
482
566
|
</button>
|
|
483
567
|
</div>
|
|
484
|
-
<
|
|
485
|
-
<select id="mode" title="modo de agente"></select>
|
|
486
|
-
<label for="provider">Proveedor</label>
|
|
487
|
-
<select id="provider" title="proveedor"></select>
|
|
488
|
-
<label for="model">Modelo</label>
|
|
489
|
-
<select id="model" title="modelo"></select>
|
|
490
|
-
<label for="default-cwd">Carpeta predeterminada para nuevas sesiones</label>
|
|
491
|
-
<div class="row default-cwd-row">
|
|
492
|
-
<input id="default-cwd" type="text" readonly placeholder="(la casa del usuario)" title="carpeta donde se abre el selector y /new sin sesión" />
|
|
493
|
-
<button type="button" id="default-cwd-pick" class="btn">elegir</button>
|
|
494
|
-
<button type="button" id="default-cwd-clear" class="btn icon" title="quitar la carpeta predeterminada" aria-label="quitar la carpeta predeterminada">
|
|
495
|
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
496
|
-
<path d="M18 6 6 18M6 6l12 12" />
|
|
497
|
-
</svg>
|
|
498
|
-
</button>
|
|
499
|
-
</div>
|
|
500
|
-
<label class="check" title="aprobar automáticamente tools con permiso ask">
|
|
501
|
-
<input id="yes" type="checkbox" /> Aprobar automáticamente las tools (auto)
|
|
502
|
-
</label>
|
|
568
|
+
<div id="settings-controls-slot"></div>
|
|
503
569
|
<div class="row between settings-actions">
|
|
504
570
|
<button type="button" id="settings-providers" class="btn">gestionar proveedores</button>
|
|
505
571
|
<button type="button" id="settings-done" class="btn primary">listo</button>
|
|
@@ -534,9 +600,10 @@
|
|
|
534
600
|
<div class="row">
|
|
535
601
|
<label class="field grow" id="mf-args-field"><span>args (separados por espacio)</span><input id="mf-args" spellcheck="false" placeholder="harness/scripts/mcp-echo-server.mjs" /></label>
|
|
536
602
|
</div>
|
|
537
|
-
<label class="field"><span>env (KEY=valor por línea)</span>
|
|
538
|
-
<textarea id="mf-env" rows="3" spellcheck="false" placeholder="API_KEY
|
|
603
|
+
<label class="field" id="mf-env-field"><span>env (KEY=valor por línea · usá <code>env:VAR</code> para no hardcodear secretos)</span>
|
|
604
|
+
<textarea id="mf-env" rows="3" spellcheck="false" placeholder="API_KEY=env:MY_API_KEY"></textarea>
|
|
539
605
|
</label>
|
|
606
|
+
<label class="check" id="mf-inherit-field"><input id="mf-inherit-env" type="checkbox" /> heredar todo el entorno (<code>inheritEnv</code>, solo MCPs de confianza)</label>
|
|
540
607
|
<label class="check"><input id="mf-enabled" type="checkbox" checked /> habilitado</label>
|
|
541
608
|
<div class="row between settings-actions">
|
|
542
609
|
<button type="button" id="mf-cancel" class="btn">cancelar</button>
|
|
@@ -621,7 +621,7 @@ details.reasoning .body { color: var(--muted); white-space: pre-wrap; margin-top
|
|
|
621
621
|
|
|
622
622
|
@keyframes pulse { 50% { opacity: 0.35; } }
|
|
623
623
|
|
|
624
|
-
.composer { display: flex; flex-direction: column; gap: 8px; padding: 12px 14px; border-top: 1px solid var(--border); background: var(--panel); backdrop-filter: blur(14px); }
|
|
624
|
+
.composer { position: relative; display: flex; flex-direction: column; gap: 8px; padding: 12px 14px; border-top: 1px solid var(--border); background: var(--panel); backdrop-filter: blur(14px); }
|
|
625
625
|
.composer.dragging { outline: 1px dashed var(--accent); outline-offset: -4px; }
|
|
626
626
|
.composer-row { display: flex; align-items: flex-end; gap: 8px; }
|
|
627
627
|
.prompt-mark { color: var(--mine); font-weight: 700; padding-bottom: 8px; }
|
|
@@ -707,6 +707,46 @@ pre.code code { background: none; border: 0; padding: 0; font-size: 12.5px; }
|
|
|
707
707
|
}
|
|
708
708
|
.statusbar .dot { opacity: 0.5; }
|
|
709
709
|
.statusbar .spacer { flex: 1; }
|
|
710
|
+
.statusbar[role="button"] { cursor: pointer; }
|
|
711
|
+
.statusbar[role="button"]:hover { color: var(--fg); }
|
|
712
|
+
|
|
713
|
+
/* menú de comandos "/" en el composer */
|
|
714
|
+
.slash-menu {
|
|
715
|
+
position: absolute;
|
|
716
|
+
left: 14px;
|
|
717
|
+
right: 14px;
|
|
718
|
+
bottom: calc(100% - 6px);
|
|
719
|
+
max-height: 260px;
|
|
720
|
+
overflow-y: auto;
|
|
721
|
+
display: flex;
|
|
722
|
+
flex-direction: column;
|
|
723
|
+
gap: 2px;
|
|
724
|
+
padding: 6px;
|
|
725
|
+
border: 1px solid var(--border);
|
|
726
|
+
border-radius: 12px;
|
|
727
|
+
background: var(--panel);
|
|
728
|
+
box-shadow: 0 12px 32px rgb(0 0 0 / 0.22);
|
|
729
|
+
z-index: 30;
|
|
730
|
+
}
|
|
731
|
+
.slash-menu[hidden] { display: none; }
|
|
732
|
+
.slash-item {
|
|
733
|
+
display: flex;
|
|
734
|
+
align-items: baseline;
|
|
735
|
+
gap: 10px;
|
|
736
|
+
width: 100%;
|
|
737
|
+
padding: 7px 9px;
|
|
738
|
+
border: 0;
|
|
739
|
+
border-radius: 8px;
|
|
740
|
+
background: none;
|
|
741
|
+
color: var(--fg);
|
|
742
|
+
text-align: left;
|
|
743
|
+
cursor: pointer;
|
|
744
|
+
font: 500 13px var(--mono);
|
|
745
|
+
}
|
|
746
|
+
.slash-item.active,
|
|
747
|
+
.slash-item:hover { background: var(--theirs); }
|
|
748
|
+
.slash-cmd { color: var(--accent); font-weight: 700; }
|
|
749
|
+
.slash-desc { color: var(--muted); font: 400 12px var(--serif); }
|
|
710
750
|
.ctx { display: inline-flex; align-items: center; gap: 8px; }
|
|
711
751
|
.ctx-bar { width: 76px; height: 6px; border-radius: 999px; background: var(--border); overflow: hidden; }
|
|
712
752
|
.ctx-fill { display: block; height: 100%; width: 0; border-radius: 999px; background: currentColor; transition: width 0.35s ease; }
|
|
@@ -724,6 +764,26 @@ pre.code code { background: none; border: 0; padding: 0; font-size: 12.5px; }
|
|
|
724
764
|
|
|
725
765
|
.card { border: 1px solid var(--border); background: var(--panel); backdrop-filter: blur(14px); border-radius: 14px; padding: 14px; display: flex; flex-direction: column; gap: 10px; }
|
|
726
766
|
|
|
767
|
+
/* ajustes: pestañas internas ---------------------------------------------- */
|
|
768
|
+
#view-ajustes.active { display: block; overflow-y: auto; }
|
|
769
|
+
.ajustes-wrap { width: 100%; max-width: 780px; margin: 0 auto; padding: 18px; display: flex; flex-direction: column; gap: 12px; min-height: 100%; }
|
|
770
|
+
.ajustes-tabs { display: flex; gap: 2px; border-bottom: 1px solid var(--border); flex-wrap: wrap; }
|
|
771
|
+
.ajustes-tab {
|
|
772
|
+
appearance: none; background: none; border: 0; border-bottom: 2px solid transparent;
|
|
773
|
+
padding: 8px 14px; margin-bottom: -1px; color: var(--muted); cursor: pointer;
|
|
774
|
+
font: 600 13px var(--serif); letter-spacing: 0.01em;
|
|
775
|
+
}
|
|
776
|
+
.ajustes-tab:hover { color: var(--fg); }
|
|
777
|
+
.ajustes-tab.active { color: var(--fg); border-bottom-color: var(--accent); }
|
|
778
|
+
.ajustes-panel { display: none; }
|
|
779
|
+
.ajustes-panel.active { display: flex; }
|
|
780
|
+
.ajustes-panel.panel { flex: none; max-width: none; overflow: visible; padding: 6px 2px 18px; }
|
|
781
|
+
.ajustes-panel .verify-line code { font-size: 11px; }
|
|
782
|
+
#agent-settings-controls { display: flex; flex-direction: column; gap: 7px; }
|
|
783
|
+
#agent-settings-controls label { color: var(--muted); font-size: 12px; }
|
|
784
|
+
#agent-settings-controls select { width: 100%; }
|
|
785
|
+
#agent-controls-slot:empty { display: none; }
|
|
786
|
+
|
|
727
787
|
.providers { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }
|
|
728
788
|
.providers li {
|
|
729
789
|
display: flex;
|