@danieltmn/tamnora 0.11.2 → 0.12.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 +31 -0
- package/README.md +7 -1
- package/harness/packages/cli/src/commands/repl.js +18 -5
- package/harness/packages/cli/src/commands/run.js +11 -2
- package/harness/packages/cli/src/commands/tui.js +13 -3
- package/harness/packages/cli/src/lib/runtime.js +2 -0
- package/harness/packages/cli/src/tui/app.js +22 -0
- package/harness/packages/core/src/agent/compact.js +38 -0
- package/harness/packages/core/src/agent/context.js +13 -8
- package/harness/packages/core/src/agent/runAgent.js +15 -2
- package/harness/packages/core/src/agent/scope.js +15 -0
- package/harness/packages/core/src/config/index.js +4 -0
- package/harness/packages/core/src/index.js +10 -1
- package/harness/packages/core/src/providers/cache.js +29 -0
- package/harness/packages/core/src/providers/ollama.js +3 -2
- package/harness/packages/core/src/providers/openaiCompatible.js +9 -2
- package/harness/packages/core/src/providers/openrouter.js +6 -1
- package/harness/packages/core/src/providers/presets.js +7 -4
- package/harness/packages/core/src/providers/stream.js +45 -4
- package/harness/packages/core/src/skills/index.js +21 -0
- package/harness/packages/core/src/tools/executors.js +21 -13
- package/harness/packages/core/src/tools/index.js +2 -2
- package/harness/packages/core/src/tools/registry.js +3 -2
- package/harness/packages/core/test/agent.test.js +38 -0
- package/harness/packages/core/test/compact.test.js +33 -0
- package/harness/packages/core/test/context.test.js +14 -0
- package/harness/packages/core/test/providers.test.js +66 -0
- package/harness/packages/core/test/skills.test.js +10 -1
- package/harness/packages/core/test/stream.test.js +20 -0
- package/harness/packages/core/test/tools.test.js +18 -0
- package/harness/packages/server/public/app.js +93 -38
- package/harness/packages/server/public/index.html +8 -2
- package/harness/packages/server/public/markdown.js +57 -3
- package/harness/packages/server/public/styles.css +117 -13
- package/harness/packages/server/src/lib/agentRun.js +7 -7
- package/harness/packages/server/src/lib/agentTools.js +18 -3
- package/harness/packages/server/src/lib/approvals.js +7 -0
- package/harness/packages/server/src/lib/channels/telegram.js +9 -1
- package/harness/packages/server/src/lib/compact.js +77 -0
- package/harness/packages/server/src/lib/prompt.js +4 -1
- package/harness/packages/server/src/lib/skills.js +12 -1
- package/harness/packages/server/src/lib/tasks.js +2 -1
- package/harness/packages/server/src/routes/chat.js +21 -2
- package/harness/packages/server/src/routes/providers.js +4 -1
- package/harness/packages/server/src/routes/sessions.js +4 -50
- package/harness/packages/server/src/server.js +8 -2
- package/harness/packages/server/test/markdown.test.js +19 -2
- package/harness/packages/server/test/scope.test.js +68 -0
- package/harness/packages/server/test/server.test.js +130 -49
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Tamnora console — dependency-free client for the harness server (terminal theme).
|
|
2
2
|
|
|
3
|
-
import { renderMarkdown, speechText } from './markdown.js';
|
|
3
|
+
import { renderMarkdown, speechText, highlight } from './markdown.js';
|
|
4
4
|
import { streamSse, followRun } from './runStream.js';
|
|
5
5
|
import { diffState, pollDelayMs, localState, summarizeArgs } from './reconcile.js';
|
|
6
6
|
|
|
@@ -21,8 +21,10 @@ const state = {
|
|
|
21
21
|
panes: new Map(),
|
|
22
22
|
tabOrder: [],
|
|
23
23
|
activeTab: null,
|
|
24
|
-
collapsedGroups: new Set(),
|
|
25
24
|
queues: new Map(),
|
|
25
|
+
// Grupos de sesiones abiertos a mano. Arranca vacio y NO se persiste: el
|
|
26
|
+
// sidebar siempre empieza con todas las carpetas sin expandir.
|
|
27
|
+
expandedGroups: new Set(),
|
|
26
28
|
// Sesiones con un run vivo segun el canal global de eventos. El sidebar lo
|
|
27
29
|
// usa para el punto de actividad antes de abrir la sesion.
|
|
28
30
|
remoteActive: new Set(),
|
|
@@ -173,6 +175,7 @@ const storage = {
|
|
|
173
175
|
const moreTab = $('#more-tab');
|
|
174
176
|
const moreMenu = $('#more-menu');
|
|
175
177
|
const moreTabLabel = $('#more-tab-label');
|
|
178
|
+
const topbarSection = $('#topbar-section');
|
|
176
179
|
|
|
177
180
|
const TAB_LABELS = { chat: 'chat', providers: 'proveedores', ajustes: 'ajustes', eval: 'eval', runs: 'runs' };
|
|
178
181
|
|
|
@@ -186,6 +189,7 @@ function activateTab(name) {
|
|
|
186
189
|
.querySelectorAll('.tab[data-tab], #more-menu button[data-tab]')
|
|
187
190
|
.forEach((t) => t.classList.toggle('active', t.dataset.tab === name));
|
|
188
191
|
if (moreTabLabel) moreTabLabel.textContent = TAB_LABELS[name] || name;
|
|
192
|
+
if (topbarSection) topbarSection.textContent = TAB_LABELS[name] || name;
|
|
189
193
|
moreTab.classList.toggle('active', name === 'eval' || name === 'runs');
|
|
190
194
|
document.querySelectorAll('.view').forEach((v) => v.classList.toggle('active', v.id === `view-${name}`));
|
|
191
195
|
hideMoreMenu();
|
|
@@ -427,6 +431,7 @@ const providerSelect = $('#provider');
|
|
|
427
431
|
const modelSelect = $('#model');
|
|
428
432
|
const modeSelect = $('#mode');
|
|
429
433
|
const yesInput = $('#yes');
|
|
434
|
+
const strictInput = $('#strict-scope');
|
|
430
435
|
|
|
431
436
|
async function loadModes() {
|
|
432
437
|
const data = await api('/api/modes').catch(() => ({ modes: [] }));
|
|
@@ -441,6 +446,7 @@ async function loadModes() {
|
|
|
441
446
|
const settings = await api('/api/settings').catch(() => null);
|
|
442
447
|
const active = settings?.settings?.mode || 'agent';
|
|
443
448
|
if ([...modeSelect.options].some((o) => o.value === active)) modeSelect.value = active;
|
|
449
|
+
if (strictInput) strictInput.checked = settings?.settings?.strictScope === true;
|
|
444
450
|
defaultCwdCache = settings?.settings?.defaultCwd || '';
|
|
445
451
|
if (!defaultCwdCache) {
|
|
446
452
|
// Migración: la carpeta predeterminada vivía en localStorage; la subimos al
|
|
@@ -545,6 +551,9 @@ document.addEventListener('keydown', (event) => {
|
|
|
545
551
|
});
|
|
546
552
|
modelPill?.addEventListener('click', () => openSettingsModal());
|
|
547
553
|
yesInput?.addEventListener('change', renderModePill);
|
|
554
|
+
strictInput?.addEventListener('change', () =>
|
|
555
|
+
api('/api/settings', { method: 'PUT', body: { strictScope: strictInput.checked } }).catch(() => null)
|
|
556
|
+
);
|
|
548
557
|
|
|
549
558
|
function updateStatus(extra = {}) {
|
|
550
559
|
renderModePill();
|
|
@@ -663,7 +672,6 @@ function initLayout() {
|
|
|
663
672
|
const widths = storage.getJson('tamnora-panel-widths', {}) || {};
|
|
664
673
|
if (Number(widths.sidebar)) panels.sidebar.width = Math.min(panels.sidebar.max, Math.max(panels.sidebar.min, widths.sidebar));
|
|
665
674
|
if (Number(widths.explorer)) panels.explorer.width = Math.min(panels.explorer.max, Math.max(panels.explorer.min, widths.explorer));
|
|
666
|
-
state.collapsedGroups = new Set(storage.getJson('tamnora-collapsed-groups', []) || []);
|
|
667
675
|
// En movil el panel arranca cerrado (drawer); en desktop, la preferencia guardada.
|
|
668
676
|
applyPanelWidths();
|
|
669
677
|
attachResizer(panels.sidebar);
|
|
@@ -1053,6 +1061,7 @@ const ACTIVITY = {
|
|
|
1053
1061
|
thinking: { cls: 'loader-thinking', label: 'pensando…' },
|
|
1054
1062
|
working: { cls: 'loader-working', label: 'trabajando…' },
|
|
1055
1063
|
reconnecting: { cls: 'loader-working', label: 'reconectando…' },
|
|
1064
|
+
stopping: { cls: 'loader-working', label: 'deteniendo…' },
|
|
1056
1065
|
};
|
|
1057
1066
|
|
|
1058
1067
|
const footerActivityEl = $('#sb-activity');
|
|
@@ -1573,10 +1582,6 @@ function basename(p) {
|
|
|
1573
1582
|
return text.split(/[\\/]/).pop() || text;
|
|
1574
1583
|
}
|
|
1575
1584
|
|
|
1576
|
-
function persistCollapsedGroups() {
|
|
1577
|
-
storage.setJson('tamnora-collapsed-groups', [...state.collapsedGroups]);
|
|
1578
|
-
}
|
|
1579
|
-
|
|
1580
1585
|
// Pestañas cerradas desde el último reporte: para que el server las olvide ya,
|
|
1581
1586
|
// sin esperar el TTL de presencia.
|
|
1582
1587
|
const presenceClosed = new Set();
|
|
@@ -1611,9 +1616,10 @@ function renderSessions(sessions) {
|
|
|
1611
1616
|
for (const [key, items] of groups) {
|
|
1612
1617
|
const groupLi = el('li', 'session-group');
|
|
1613
1618
|
const head = el('div', 'group-head');
|
|
1614
|
-
const
|
|
1619
|
+
const open = state.expandedGroups.has(key);
|
|
1620
|
+
head.classList.toggle('open', open);
|
|
1615
1621
|
const twisty = el('span', 'tw');
|
|
1616
|
-
twisty.appendChild(icon(
|
|
1622
|
+
twisty.appendChild(icon(open ? 'chevron-down' : 'chevron-right', 14));
|
|
1617
1623
|
head.append(twisty, icon('folder', 15, 'fi'), el('span', 'gname', key ? basename(key) : 'sin carpeta'), el('span', 'gcount', String(items.length)));
|
|
1618
1624
|
head.title = key || 'sesiones sin carpeta';
|
|
1619
1625
|
const delGroup = el('button', 'group-del');
|
|
@@ -1627,19 +1633,23 @@ function renderSessions(sessions) {
|
|
|
1627
1633
|
});
|
|
1628
1634
|
head.appendChild(delGroup);
|
|
1629
1635
|
const list = el('ul', 'group-sessions');
|
|
1630
|
-
list.hidden =
|
|
1636
|
+
list.hidden = !open;
|
|
1631
1637
|
head.addEventListener('click', () => {
|
|
1632
|
-
const
|
|
1633
|
-
if (
|
|
1634
|
-
else state.
|
|
1635
|
-
|
|
1636
|
-
list.hidden =
|
|
1638
|
+
const nowOpen = !state.expandedGroups.has(key);
|
|
1639
|
+
if (nowOpen) state.expandedGroups.add(key);
|
|
1640
|
+
else state.expandedGroups.delete(key);
|
|
1641
|
+
head.classList.toggle('open', nowOpen);
|
|
1642
|
+
list.hidden = !nowOpen;
|
|
1637
1643
|
clear(twisty);
|
|
1638
|
-
twisty.appendChild(icon(
|
|
1644
|
+
twisty.appendChild(icon(nowOpen ? 'chevron-down' : 'chevron-right', 14));
|
|
1639
1645
|
});
|
|
1640
1646
|
for (const session of items) {
|
|
1641
1647
|
const li = el('li', `session-item${session.id === state.sessionId ? ' active' : ''}`);
|
|
1642
|
-
|
|
1648
|
+
const main = el('div', 'session-main');
|
|
1649
|
+
main.appendChild(el('span', 'session-title', session.title || '(sin título)'));
|
|
1650
|
+
const meta = [session.provider, session.model].filter(Boolean).join(' / ');
|
|
1651
|
+
if (meta) main.appendChild(el('span', 'session-sub', meta));
|
|
1652
|
+
li.appendChild(main);
|
|
1643
1653
|
// Punto de actividad: el server lo dice en el listado, y el reconciliador
|
|
1644
1654
|
// lo mantiene al dia si el run arranca despues de este render. El estado
|
|
1645
1655
|
// "esperandote a vos" tiene su propio color: un run trabajando en otra
|
|
@@ -2522,11 +2532,21 @@ async function sendMessage() {
|
|
|
2522
2532
|
}
|
|
2523
2533
|
|
|
2524
2534
|
if (stopBtn) {
|
|
2525
|
-
stopBtn.addEventListener('click', () => {
|
|
2535
|
+
stopBtn.addEventListener('click', async () => {
|
|
2526
2536
|
const id = state.sessionId;
|
|
2527
|
-
if (id
|
|
2528
|
-
|
|
2529
|
-
|
|
2537
|
+
if (!id || stopBtn.disabled) return;
|
|
2538
|
+
stopBtn.disabled = true;
|
|
2539
|
+
renderFooterActivity('stopping');
|
|
2540
|
+
// Abortamos el stream local (corta el seguimiento ya) y pedimos al server que
|
|
2541
|
+
// aborte el run: sin esto el run es durable y seguiria generando.
|
|
2542
|
+
controllers.get(id)?.abort();
|
|
2543
|
+
try {
|
|
2544
|
+
await api(`/api/sessions/${id}/stop`, { method: 'POST' });
|
|
2545
|
+
} catch (error) {
|
|
2546
|
+
toast(`No se pudo detener el run: ${error.message}`);
|
|
2547
|
+
} finally {
|
|
2548
|
+
stopBtn.disabled = false;
|
|
2549
|
+
}
|
|
2530
2550
|
});
|
|
2531
2551
|
}
|
|
2532
2552
|
|
|
@@ -4931,7 +4951,7 @@ function refreshTwisty(li, open) {
|
|
|
4931
4951
|
twisty.appendChild(icon(open ? 'chevron-down' : 'chevron-right', 14));
|
|
4932
4952
|
}
|
|
4933
4953
|
|
|
4934
|
-
const filePreviewState = { current: null, kind: null, loaded: false, open: false, content: '' };
|
|
4954
|
+
const filePreviewState = { current: null, kind: null, lang: null, loaded: false, open: false, content: '' };
|
|
4935
4955
|
|
|
4936
4956
|
const filePreviewTools = setupPreviewTools({
|
|
4937
4957
|
container: $('#file-modal'),
|
|
@@ -4947,6 +4967,36 @@ function filePreviewKind(name) {
|
|
|
4947
4967
|
return null;
|
|
4948
4968
|
}
|
|
4949
4969
|
|
|
4970
|
+
const CODE_LANGS = {
|
|
4971
|
+
js: 'js', mjs: 'js', cjs: 'js', jsx: 'js',
|
|
4972
|
+
ts: 'ts', tsx: 'ts',
|
|
4973
|
+
json: 'json',
|
|
4974
|
+
css: 'css',
|
|
4975
|
+
html: 'html', htm: 'html',
|
|
4976
|
+
xml: 'xml', svg: 'xml',
|
|
4977
|
+
yml: 'yaml', yaml: 'yaml',
|
|
4978
|
+
sh: 'bash', bash: 'bash',
|
|
4979
|
+
py: 'python',
|
|
4980
|
+
};
|
|
4981
|
+
|
|
4982
|
+
function filePreviewLang(name) {
|
|
4983
|
+
const ext = String(name ?? '').split('.').pop().toLowerCase();
|
|
4984
|
+
return CODE_LANGS[ext] || null;
|
|
4985
|
+
}
|
|
4986
|
+
|
|
4987
|
+
function renderCodeView(content, lang) {
|
|
4988
|
+
const gutter = $('#file-gutter');
|
|
4989
|
+
const node = $('#file-content');
|
|
4990
|
+
if (!lang) {
|
|
4991
|
+
node.textContent = content;
|
|
4992
|
+
if (gutter) gutter.textContent = '';
|
|
4993
|
+
return;
|
|
4994
|
+
}
|
|
4995
|
+
const lines = content.replace(/\r\n?/g, '\n').split('\n');
|
|
4996
|
+
if (gutter) gutter.textContent = lines.map((_, i) => i + 1).join('\n');
|
|
4997
|
+
node.innerHTML = highlight(content, lang);
|
|
4998
|
+
}
|
|
4999
|
+
|
|
4950
5000
|
async function openFilePreview(relPath, name) {
|
|
4951
5001
|
const params = new URLSearchParams({ root: state.cwd, path: relPath });
|
|
4952
5002
|
const data = await api(`/api/files/content?${params}`).catch((error) => {
|
|
@@ -4954,31 +5004,36 @@ async function openFilePreview(relPath, name) {
|
|
|
4954
5004
|
return null;
|
|
4955
5005
|
});
|
|
4956
5006
|
if (!data) return;
|
|
5007
|
+
const code = $('#file-code');
|
|
5008
|
+
const lang = filePreviewLang(name);
|
|
5009
|
+
filePreviewState.current = { relPath, name };
|
|
5010
|
+
filePreviewState.kind = filePreviewKind(name);
|
|
5011
|
+
filePreviewState.lang = lang;
|
|
5012
|
+
filePreviewState.loaded = false;
|
|
5013
|
+
filePreviewState.open = false;
|
|
5014
|
+
filePreviewState.content = data.content;
|
|
5015
|
+
|
|
4957
5016
|
$('#file-title').textContent = name;
|
|
4958
|
-
$('#file-meta').textContent = `${data.size} bytes`;
|
|
4959
|
-
|
|
5017
|
+
$('#file-meta').textContent = lang ? `${data.size} bytes · ${lang}` : `${data.size} bytes`;
|
|
5018
|
+
renderCodeView(data.content, lang);
|
|
5019
|
+
code.classList.toggle('has-lang', Boolean(lang));
|
|
4960
5020
|
|
|
4961
5021
|
const toggle = $('#file-preview-toggle');
|
|
4962
5022
|
const device = $('#file-device');
|
|
4963
5023
|
const maxButton = $('#file-max');
|
|
4964
5024
|
const modal = $('#file-modal');
|
|
4965
|
-
filePreviewState.current = { relPath, name };
|
|
4966
|
-
filePreviewState.kind = filePreviewKind(name);
|
|
4967
|
-
filePreviewState.loaded = false;
|
|
4968
|
-
filePreviewState.open = false;
|
|
4969
|
-
filePreviewState.content = data.content;
|
|
4970
5025
|
|
|
4971
5026
|
toggle.hidden = !filePreviewState.kind;
|
|
4972
5027
|
toggle.textContent = 'vista previa';
|
|
4973
5028
|
device.hidden = true;
|
|
4974
5029
|
device.value = 'auto';
|
|
4975
|
-
maxButton.hidden =
|
|
5030
|
+
maxButton.hidden = false;
|
|
4976
5031
|
$('#file-frame').src = 'about:blank';
|
|
4977
5032
|
$('#file-stage').hidden = true;
|
|
4978
5033
|
$('#file-render').hidden = true;
|
|
4979
5034
|
$('#file-render').innerHTML = '';
|
|
4980
|
-
|
|
4981
|
-
|
|
5035
|
+
code.hidden = false;
|
|
5036
|
+
filePreviewTools.setMax(false);
|
|
4982
5037
|
filePreviewTools.applyDevice();
|
|
4983
5038
|
|
|
4984
5039
|
modal.showModal();
|
|
@@ -4986,7 +5041,7 @@ async function openFilePreview(relPath, name) {
|
|
|
4986
5041
|
|
|
4987
5042
|
$('#file-preview-toggle')?.addEventListener('click', async () => {
|
|
4988
5043
|
const toggle = $('#file-preview-toggle');
|
|
4989
|
-
const
|
|
5044
|
+
const code = $('#file-code');
|
|
4990
5045
|
const stage = $('#file-stage');
|
|
4991
5046
|
const frame = $('#file-frame');
|
|
4992
5047
|
const render = $('#file-render');
|
|
@@ -4998,22 +5053,22 @@ $('#file-preview-toggle')?.addEventListener('click', async () => {
|
|
|
4998
5053
|
filePreviewState.open = !filePreviewState.open;
|
|
4999
5054
|
const off = () => {
|
|
5000
5055
|
toggle.textContent = 'vista previa';
|
|
5001
|
-
|
|
5056
|
+
code.hidden = false;
|
|
5002
5057
|
stage.hidden = true;
|
|
5003
5058
|
render.hidden = true;
|
|
5004
5059
|
device.hidden = true;
|
|
5005
|
-
maxButton.hidden =
|
|
5060
|
+
maxButton.hidden = false;
|
|
5006
5061
|
$('#file-modal').classList.remove('preview-maximized');
|
|
5007
5062
|
};
|
|
5008
5063
|
if (!filePreviewState.open) return off();
|
|
5009
5064
|
|
|
5010
5065
|
if (filePreviewState.kind === 'markdown') {
|
|
5011
5066
|
render.innerHTML = renderMarkdown(filePreviewState.content);
|
|
5012
|
-
|
|
5067
|
+
code.hidden = true;
|
|
5013
5068
|
stage.hidden = true;
|
|
5014
5069
|
render.hidden = false;
|
|
5015
5070
|
device.hidden = true;
|
|
5016
|
-
maxButton.hidden =
|
|
5071
|
+
maxButton.hidden = false;
|
|
5017
5072
|
toggle.textContent = 'ver código';
|
|
5018
5073
|
return;
|
|
5019
5074
|
}
|
|
@@ -5035,7 +5090,7 @@ $('#file-preview-toggle')?.addEventListener('click', async () => {
|
|
|
5035
5090
|
filePreviewTools.applyDevice();
|
|
5036
5091
|
}
|
|
5037
5092
|
toggle.textContent = 'ver código';
|
|
5038
|
-
|
|
5093
|
+
code.hidden = true;
|
|
5039
5094
|
render.hidden = true;
|
|
5040
5095
|
stage.hidden = false;
|
|
5041
5096
|
});
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
</head>
|
|
38
38
|
<body>
|
|
39
39
|
<header class="topbar">
|
|
40
|
-
<div class="brand"><span class="logo"><img src="/icons/logo.png" alt="Tamnora" /></span> <span class="ver">v0.
|
|
40
|
+
<div class="brand"><span class="logo"><img src="/icons/logo.png" alt="Tamnora" /></span> <span id="topbar-section" class="topbar-section">chat</span> <span class="ver">v0.12.0</span></div>
|
|
41
41
|
<nav class="tabs">
|
|
42
42
|
<button class="tab active" data-tab="chat">chat</button>
|
|
43
43
|
<button class="tab" data-tab="providers">proveedores</button>
|
|
@@ -350,7 +350,10 @@
|
|
|
350
350
|
</button>
|
|
351
351
|
</div>
|
|
352
352
|
</div>
|
|
353
|
-
<
|
|
353
|
+
<div id="file-code" class="code-view">
|
|
354
|
+
<pre id="file-gutter" class="code-gutter" aria-hidden="true"></pre>
|
|
355
|
+
<pre id="file-content" class="file-preview"></pre>
|
|
356
|
+
</div>
|
|
354
357
|
<div id="file-render" class="file-render text" hidden></div>
|
|
355
358
|
<div id="file-stage" class="preview-stage" data-device="auto" hidden>
|
|
356
359
|
<iframe id="file-frame" class="file-frame" title="vista previa del archivo"></iframe>
|
|
@@ -483,6 +486,9 @@
|
|
|
483
486
|
<label class="check" title="aprobar automáticamente tools con permiso ask">
|
|
484
487
|
<input id="yes" type="checkbox" /> Aprobar automáticamente las tools (auto)
|
|
485
488
|
</label>
|
|
489
|
+
<label class="check" title="el agente hace exactamente lo pedido, sin mejoras ni cambios fuera del pedido">
|
|
490
|
+
<input id="strict-scope" type="checkbox" /> Hacer solo lo pedido (estricto)
|
|
491
|
+
</label>
|
|
486
492
|
</div>
|
|
487
493
|
</div>
|
|
488
494
|
<div class="row">
|
|
@@ -25,6 +25,9 @@ function normalizeLang(lang) {
|
|
|
25
25
|
if (value === 'javascript' || value === 'mjs' || value === 'cjs') return 'js';
|
|
26
26
|
if (value === 'shell' || value === 'zsh' || value === 'console') return 'bash';
|
|
27
27
|
if (value === 'yml' || value === 'yaml') return 'yaml';
|
|
28
|
+
if (value === 'htm') return 'html';
|
|
29
|
+
if (value === 'svg') return 'xml';
|
|
30
|
+
if (value === 'scss' || value === 'less') return 'css';
|
|
28
31
|
return value;
|
|
29
32
|
}
|
|
30
33
|
|
|
@@ -32,9 +35,8 @@ function wrap(cls, text) {
|
|
|
32
35
|
return `<span class="${cls}">${escapeHtml(text)}</span>`;
|
|
33
36
|
}
|
|
34
37
|
|
|
35
|
-
/**
|
|
36
|
-
|
|
37
|
-
const language = normalizeLang(lang);
|
|
38
|
+
/** Tokenizer generico: comentarios, strings, numeros y keywords. Best effort. */
|
|
39
|
+
function highlightCode(code, language) {
|
|
38
40
|
const keywords = new Set((KEYWORDS[language] ?? '').split(/\s+/).filter(Boolean));
|
|
39
41
|
const pattern =
|
|
40
42
|
/(\/\/[^\n]*|#[^\n]*|\/\*[\s\S]*?\*\/)|("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`)|(\b\d[\d._]*\b)|([A-Za-z_$][\w$]*)/g;
|
|
@@ -55,6 +57,58 @@ export function highlight(code, lang) {
|
|
|
55
57
|
return out + escapeHtml(code.slice(last));
|
|
56
58
|
}
|
|
57
59
|
|
|
60
|
+
/** Resaltado de HTML/XML: comentarios, tags, atributos y strings. */
|
|
61
|
+
function highlightMarkup(code) {
|
|
62
|
+
const pattern =
|
|
63
|
+
/(<!--[\s\S]*?-->)|("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')|(<\/?)([A-Za-z][\w:.-]*)|([A-Za-z_:][-A-Za-z0-9_:.]*)(?=\s*=)|(\/?>)/g;
|
|
64
|
+
let out = '';
|
|
65
|
+
let last = 0;
|
|
66
|
+
let match;
|
|
67
|
+
while ((match = pattern.exec(code))) {
|
|
68
|
+
out += escapeHtml(code.slice(last, match.index));
|
|
69
|
+
const [full, comment, str, bracket, tag, attr, close] = match;
|
|
70
|
+
if (comment) out += wrap('tok-comment', comment);
|
|
71
|
+
else if (str) out += wrap('tok-string', str);
|
|
72
|
+
else if (bracket) out += wrap('tok-punct', bracket) + (tag ? wrap('tok-tag', tag) : '');
|
|
73
|
+
else if (attr) out += wrap('tok-attr', attr);
|
|
74
|
+
else if (close) out += wrap('tok-punct', close);
|
|
75
|
+
else out += escapeHtml(full);
|
|
76
|
+
last = match.index + full.length;
|
|
77
|
+
}
|
|
78
|
+
return out + escapeHtml(code.slice(last));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Resaltado de CSS: comentarios, selectores, propiedades, at-rules y valores. */
|
|
82
|
+
function highlightCss(code) {
|
|
83
|
+
const pattern =
|
|
84
|
+
/(\/\*[\s\S]*?\*\/)|("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')|(@[A-Za-z-]+)|(#[0-9a-fA-F]{3,8}\b)|(\b-?\d[\d.]*(?:px|em|rem|%|vh|vw|vmin|vmax|s|ms|fr|deg|ch|pt|ex)?\b)|([.#][A-Za-z_][\w-]*)|([-\w]+)(?=\s*:)/g;
|
|
85
|
+
let out = '';
|
|
86
|
+
let last = 0;
|
|
87
|
+
let match;
|
|
88
|
+
while ((match = pattern.exec(code))) {
|
|
89
|
+
out += escapeHtml(code.slice(last, match.index));
|
|
90
|
+
const [full, comment, str, at, color, num, selector, prop] = match;
|
|
91
|
+
if (comment) out += wrap('tok-comment', full);
|
|
92
|
+
else if (str) out += wrap('tok-string', full);
|
|
93
|
+
else if (at) out += wrap('tok-tag', full);
|
|
94
|
+
else if (color) out += wrap('tok-number', full);
|
|
95
|
+
else if (num) out += wrap('tok-number', full);
|
|
96
|
+
else if (selector) out += wrap('tok-selector', full);
|
|
97
|
+
else if (prop) out += wrap('tok-attr', full);
|
|
98
|
+
else out += escapeHtml(full);
|
|
99
|
+
last = match.index + full.length;
|
|
100
|
+
}
|
|
101
|
+
return out + escapeHtml(code.slice(last));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Minimal highlighter: best effort, always HTML-escaped. */
|
|
105
|
+
export function highlight(code, lang) {
|
|
106
|
+
const language = normalizeLang(lang);
|
|
107
|
+
if (language === 'html' || language === 'xml') return highlightMarkup(code);
|
|
108
|
+
if (language === 'css') return highlightCss(code);
|
|
109
|
+
return highlightCode(code, language);
|
|
110
|
+
}
|
|
111
|
+
|
|
58
112
|
function inline(text) {
|
|
59
113
|
let out = escapeHtml(text);
|
|
60
114
|
const codes = [];
|
|
@@ -54,6 +54,10 @@ html[data-theme='paper'] .tok-comment { color: #7a7669; }
|
|
|
54
54
|
html[data-theme='paper'] .tok-string { color: #7a4a2c; }
|
|
55
55
|
html[data-theme='paper'] .tok-number { color: #8a5a00; }
|
|
56
56
|
html[data-theme='paper'] .tok-keyword { color: #a3341f; }
|
|
57
|
+
html[data-theme='paper'] .tok-tag { color: #a3341f; }
|
|
58
|
+
html[data-theme='paper'] .tok-attr { color: #4f6b46; }
|
|
59
|
+
html[data-theme='paper'] .tok-selector { color: #8a5a00; }
|
|
60
|
+
html[data-theme='paper'] .tok-punct { color: #7a7669; }
|
|
57
61
|
html[data-theme='paper'] .btn.primary { color: #fbf6ec; }
|
|
58
62
|
|
|
59
63
|
* { box-sizing: border-box; }
|
|
@@ -118,6 +122,7 @@ pre, code, kbd, samp { font-family: var(--mono); }
|
|
|
118
122
|
}
|
|
119
123
|
.brand .logo img { width: 100%; height: 100%; display: block; object-fit: cover; }
|
|
120
124
|
.brand .ver { color: var(--muted); font-family: var(--mono); font-weight: 400; font-size: 10px; letter-spacing: 0.04em; }
|
|
125
|
+
.brand .topbar-section { display: none; }
|
|
121
126
|
.tabs { grid-column: 2; justify-self: center; display: flex; gap: 2px; }
|
|
122
127
|
.tab {
|
|
123
128
|
background: transparent;
|
|
@@ -245,13 +250,16 @@ main { flex: 1; min-height: 0; min-width: 0; display: flex; }
|
|
|
245
250
|
display: flex;
|
|
246
251
|
align-items: center;
|
|
247
252
|
gap: 7px;
|
|
248
|
-
padding:
|
|
249
|
-
border-radius:
|
|
253
|
+
padding: 7px 9px;
|
|
254
|
+
border-radius: 8px;
|
|
250
255
|
cursor: pointer;
|
|
251
|
-
color: var(--
|
|
256
|
+
color: var(--text);
|
|
257
|
+
border: 1px solid transparent;
|
|
258
|
+
transition: background 0.12s ease, border-color 0.12s ease;
|
|
252
259
|
}
|
|
253
|
-
.group-head:hover { background: var(--panel); color: var(--
|
|
254
|
-
.group-head .tw { width: 14px; height: 14px; flex: 0 0 auto; display: grid; place-items: center; }
|
|
260
|
+
.group-head:hover { background: var(--panel); border-color: var(--border); }
|
|
261
|
+
.group-head .tw { width: 14px; height: 14px; flex: 0 0 auto; display: grid; place-items: center; color: var(--muted); }
|
|
262
|
+
.group-head .tw svg { transition: transform 0.15s ease; }
|
|
255
263
|
.group-head .fi { width: 15px; height: 15px; flex: 0 0 auto; color: var(--accent); }
|
|
256
264
|
.group-head .gname {
|
|
257
265
|
flex: 1;
|
|
@@ -259,27 +267,65 @@ main { flex: 1; min-height: 0; min-width: 0; display: flex; }
|
|
|
259
267
|
overflow: hidden;
|
|
260
268
|
text-overflow: ellipsis;
|
|
261
269
|
white-space: nowrap;
|
|
262
|
-
font: 600
|
|
270
|
+
font: 600 11.5px var(--sans);
|
|
263
271
|
letter-spacing: 0.02em;
|
|
264
272
|
}
|
|
265
|
-
.group-head .gcount {
|
|
266
|
-
|
|
273
|
+
.group-head .gcount {
|
|
274
|
+
flex: 0 0 auto;
|
|
275
|
+
min-width: 18px;
|
|
276
|
+
padding: 1px 6px;
|
|
277
|
+
text-align: center;
|
|
278
|
+
border-radius: 999px;
|
|
279
|
+
background: var(--theirs);
|
|
280
|
+
border: 1px solid var(--border);
|
|
281
|
+
font: 600 10px var(--mono);
|
|
282
|
+
color: var(--muted);
|
|
283
|
+
}
|
|
284
|
+
.group-sessions {
|
|
285
|
+
list-style: none;
|
|
286
|
+
margin: 3px 0 5px 9px;
|
|
287
|
+
padding: 0 0 0 11px;
|
|
288
|
+
border-left: 1px solid var(--border);
|
|
289
|
+
display: flex;
|
|
290
|
+
flex-direction: column;
|
|
291
|
+
gap: 2px;
|
|
292
|
+
}
|
|
267
293
|
.session-item {
|
|
294
|
+
position: relative;
|
|
268
295
|
display: flex;
|
|
269
296
|
align-items: center;
|
|
270
297
|
gap: 6px;
|
|
271
|
-
padding:
|
|
272
|
-
border-radius:
|
|
298
|
+
padding: 7px 9px;
|
|
299
|
+
border-radius: 7px;
|
|
273
300
|
cursor: pointer;
|
|
274
301
|
color: var(--muted);
|
|
275
302
|
white-space: nowrap;
|
|
276
303
|
overflow: hidden;
|
|
277
304
|
border: 1px solid transparent;
|
|
305
|
+
transition: background 0.12s ease, color 0.12s ease;
|
|
278
306
|
}
|
|
279
307
|
.session-item:hover { background: var(--panel); color: var(--text); }
|
|
280
308
|
.session-item.active { background: var(--theirs); color: var(--text); border-color: var(--border); }
|
|
309
|
+
.session-item.active::before {
|
|
310
|
+
content: '';
|
|
311
|
+
position: absolute;
|
|
312
|
+
left: 0;
|
|
313
|
+
top: 7px;
|
|
314
|
+
bottom: 7px;
|
|
315
|
+
width: 2px;
|
|
316
|
+
border-radius: 2px;
|
|
317
|
+
background: var(--accent);
|
|
318
|
+
}
|
|
281
319
|
.session-item .sub { color: var(--muted); font-size: 11px; overflow: hidden; text-overflow: ellipsis; }
|
|
282
|
-
.session-
|
|
320
|
+
.session-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
|
|
321
|
+
.session-item .session-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
322
|
+
.session-item .session-sub {
|
|
323
|
+
color: var(--muted);
|
|
324
|
+
font: 500 10.5px var(--mono);
|
|
325
|
+
overflow: hidden;
|
|
326
|
+
text-overflow: ellipsis;
|
|
327
|
+
opacity: 0.75;
|
|
328
|
+
}
|
|
283
329
|
.session-live {
|
|
284
330
|
flex: none;
|
|
285
331
|
width: 6px;
|
|
@@ -1108,6 +1154,10 @@ pre.code code { background: none; border: 0; padding: 0; font-size: 0.92em; }
|
|
|
1108
1154
|
.tok-string { color: #c2a878; }
|
|
1109
1155
|
.tok-number { color: #e0a05c; }
|
|
1110
1156
|
.tok-keyword { color: #e0794f; }
|
|
1157
|
+
.tok-tag { color: #e0794f; }
|
|
1158
|
+
.tok-attr { color: #8fae7f; }
|
|
1159
|
+
.tok-selector { color: #d9a441; }
|
|
1160
|
+
.tok-punct { color: #9c9789; }
|
|
1111
1161
|
|
|
1112
1162
|
.composer-status {
|
|
1113
1163
|
display: flex;
|
|
@@ -1343,10 +1393,14 @@ pre.code code { background: none; border: 0; padding: 0; font-size: 0.92em; }
|
|
|
1343
1393
|
.explorer-filter input { width: 100%; }
|
|
1344
1394
|
|
|
1345
1395
|
.tree { list-style: none; margin: 0; padding: 2px 8px 12px; overflow-y: auto; flex: 1; min-height: 0; }
|
|
1346
|
-
|
|
1396
|
+
/* El subárbol queda oculto salvo que su carpeta esté abierta (clase .open que
|
|
1397
|
+
mantiene el JS). Sin esto, expandir no tenía vuelta atrás. */
|
|
1398
|
+
.tree .tree { padding: 0 0 0 13px; overflow: visible; display: none; }
|
|
1399
|
+
.tree li.open > .tree { display: block; }
|
|
1347
1400
|
.tree-row { display: flex; align-items: center; gap: 6px; padding: 3px 6px; border-radius: 7px; cursor: pointer; white-space: nowrap; }
|
|
1348
1401
|
.tree-row:hover { background: var(--theirs); }
|
|
1349
1402
|
.tree-row .tw { width: 14px; height: 14px; flex: 0 0 auto; display: grid; place-items: center; color: var(--muted); }
|
|
1403
|
+
.tree-row .tw svg { transition: transform 0.15s ease; }
|
|
1350
1404
|
.tree-row .fi { width: 15px; height: 15px; flex: 0 0 auto; color: var(--muted); }
|
|
1351
1405
|
.tree-row.dir .fi { color: var(--accent); }
|
|
1352
1406
|
.tree-name { overflow: hidden; text-overflow: ellipsis; min-width: 0; }
|
|
@@ -1431,6 +1485,42 @@ pre.code code { background: none; border: 0; padding: 0; font-size: 0.92em; }
|
|
|
1431
1485
|
white-space: pre;
|
|
1432
1486
|
}
|
|
1433
1487
|
|
|
1488
|
+
/* Visor de codigo: gutter con numeros de linea + resaltado (estilo VSCode). */
|
|
1489
|
+
.code-view {
|
|
1490
|
+
display: flex;
|
|
1491
|
+
align-items: stretch;
|
|
1492
|
+
max-height: 62vh;
|
|
1493
|
+
overflow: auto;
|
|
1494
|
+
background: var(--theirs);
|
|
1495
|
+
border: 1px solid var(--border);
|
|
1496
|
+
border-radius: 10px;
|
|
1497
|
+
}
|
|
1498
|
+
.code-view .file-preview {
|
|
1499
|
+
flex: 1;
|
|
1500
|
+
min-width: 0;
|
|
1501
|
+
max-height: none;
|
|
1502
|
+
overflow: visible;
|
|
1503
|
+
border: 0;
|
|
1504
|
+
border-radius: 0;
|
|
1505
|
+
background: transparent;
|
|
1506
|
+
}
|
|
1507
|
+
.code-gutter {
|
|
1508
|
+
position: sticky;
|
|
1509
|
+
left: 0;
|
|
1510
|
+
z-index: 1;
|
|
1511
|
+
flex: 0 0 auto;
|
|
1512
|
+
margin: 0;
|
|
1513
|
+
padding: 12px 8px 12px 10px;
|
|
1514
|
+
text-align: right;
|
|
1515
|
+
color: var(--muted);
|
|
1516
|
+
background: var(--panel);
|
|
1517
|
+
border-right: 1px solid var(--border);
|
|
1518
|
+
font: 12px/1.55 var(--mono);
|
|
1519
|
+
white-space: pre;
|
|
1520
|
+
user-select: none;
|
|
1521
|
+
}
|
|
1522
|
+
.code-view:not(.has-lang) .code-gutter { display: none; }
|
|
1523
|
+
|
|
1434
1524
|
.file-render {
|
|
1435
1525
|
margin: 0;
|
|
1436
1526
|
padding: 14px 16px;
|
|
@@ -1467,10 +1557,12 @@ dialog.file-modal.preview-maximized {
|
|
|
1467
1557
|
.file-modal.preview-maximized .modal-card { height: 100vh; }
|
|
1468
1558
|
.file-modal.preview-maximized .file-stage,
|
|
1469
1559
|
.file-modal.preview-maximized .file-render,
|
|
1560
|
+
.file-modal.preview-maximized .code-view,
|
|
1470
1561
|
.file-modal.preview-maximized .file-preview {
|
|
1471
1562
|
flex: 1;
|
|
1472
1563
|
height: auto;
|
|
1473
1564
|
max-height: none;
|
|
1565
|
+
min-height: 0;
|
|
1474
1566
|
}
|
|
1475
1567
|
|
|
1476
1568
|
/* panel heads + collapsed handles ----------------------------------------- */
|
|
@@ -1541,6 +1633,18 @@ dialog.file-modal.preview-maximized {
|
|
|
1541
1633
|
.brand .ver {
|
|
1542
1634
|
display: none;
|
|
1543
1635
|
}
|
|
1636
|
+
.brand .topbar-section {
|
|
1637
|
+
display: inline-block;
|
|
1638
|
+
padding: 2px 8px;
|
|
1639
|
+
border-radius: 999px;
|
|
1640
|
+
border: 1px solid var(--border);
|
|
1641
|
+
background: var(--accent-soft);
|
|
1642
|
+
color: var(--accent);
|
|
1643
|
+
font: 700 10px var(--sans);
|
|
1644
|
+
text-transform: uppercase;
|
|
1645
|
+
letter-spacing: 0.08em;
|
|
1646
|
+
white-space: nowrap;
|
|
1647
|
+
}
|
|
1544
1648
|
.tabs {
|
|
1545
1649
|
margin-left: auto;
|
|
1546
1650
|
max-width: 100%;
|
|
@@ -1549,7 +1653,7 @@ dialog.file-modal.preview-maximized {
|
|
|
1549
1653
|
display: none;
|
|
1550
1654
|
}
|
|
1551
1655
|
#more-tab-label {
|
|
1552
|
-
display:
|
|
1656
|
+
display: none;
|
|
1553
1657
|
}
|
|
1554
1658
|
.menu-pop {
|
|
1555
1659
|
top: calc(100% + 2px);
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// subagentes). La creacion de tools y el onEvent de cada canal siguen siendo
|
|
3
3
|
// propios (los canales de salida son distintos); lo combinable vive aca.
|
|
4
4
|
|
|
5
|
-
import { summarizeToolCall, previewToolResult } from './core.js';
|
|
5
|
+
import { summarizeToolCall, previewToolResult, liveHistory } from './core.js';
|
|
6
6
|
|
|
7
7
|
const FAILED_RE = /^Error|^Permiso denegado/i;
|
|
8
8
|
|
|
@@ -21,16 +21,15 @@ export function toolCallPayload(event) {
|
|
|
21
21
|
* `approve` es el callback de aprobacion de las tools de administraación.
|
|
22
22
|
*/
|
|
23
23
|
export function combineToolset({ local, admin = null, tasks = null, exposeAdmin = false, approve = null }) {
|
|
24
|
+
const adminAllowed = (name) => Boolean(admin?.handles(name)) && (exposeAdmin || admin.readOnly?.has(name));
|
|
24
25
|
const definitions = [
|
|
25
26
|
...local.definitions,
|
|
26
|
-
...(
|
|
27
|
+
...(admin ? admin.definitions.filter((d) => adminAllowed(d.function.name)) : []),
|
|
27
28
|
...(tasks ? tasks.definitions : []),
|
|
28
29
|
];
|
|
29
30
|
const execute = (name, args, context) => {
|
|
30
31
|
if (tasks?.handles(name)) return tasks.execute(name, args);
|
|
31
|
-
if (
|
|
32
|
-
return admin.execute(name, args, approve ? { approve } : undefined);
|
|
33
|
-
}
|
|
32
|
+
if (adminAllowed(name)) return admin.execute(name, args, approve ? { approve } : undefined);
|
|
34
33
|
return local.execute(name, args, context);
|
|
35
34
|
};
|
|
36
35
|
return { definitions, execute };
|
|
@@ -67,12 +66,13 @@ export async function composeAgentMessages({ ctx, cwd, mode, history = [], userM
|
|
|
67
66
|
const basePrompt = await ctx.systemPromptFor(cwd);
|
|
68
67
|
const goal = sessionRecord?.meta?.goal;
|
|
69
68
|
const summary = sessionRecord?.meta?.summary;
|
|
69
|
+
// Historial vigente: sin el asesor y sin lo ya resumido (si hay cursor).
|
|
70
|
+
const live = liveHistory(history, sessionRecord?.meta?.summaryCursor ?? null);
|
|
70
71
|
return [
|
|
71
72
|
{ role: 'system', content: mode.systemPrompt ? `${basePrompt}\n\n${mode.systemPrompt}` : basePrompt },
|
|
72
73
|
...(goal ? [{ role: 'system', content: `Objetivo de la sesión: ${goal}` }] : []),
|
|
73
74
|
...(summary ? [{ role: 'system', content: `Resumen de la conversación anterior:\n${summary}` }] : []),
|
|
74
|
-
|
|
75
|
-
...history.filter((message) => message.name !== 'advisor'),
|
|
75
|
+
...live,
|
|
76
76
|
userMessage,
|
|
77
77
|
];
|
|
78
78
|
}
|