@danieltmn/tamnora 0.13.1 → 0.15.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 +38 -0
- package/harness/packages/core/src/config/index.js +2 -0
- package/harness/packages/core/src/hooks/index.js +42 -0
- package/harness/packages/core/src/index.js +3 -0
- package/harness/packages/core/src/plugins/index.js +36 -0
- package/harness/packages/core/src/rules/index.js +9 -63
- package/harness/packages/core/src/schedule/cron.js +124 -0
- package/harness/packages/core/src/storage/sqlite.js +96 -1
- package/harness/packages/core/src/tools/glob.js +64 -2
- package/harness/packages/core/test/cron.test.js +46 -0
- package/harness/packages/core/test/hooks.test.js +29 -0
- package/harness/packages/core/test/plugins.test.js +25 -0
- package/harness/packages/server/public/app.js +193 -3
- package/harness/packages/server/public/index.html +76 -2
- package/harness/packages/server/public/landing/index.html +5 -3
- package/harness/packages/server/public/styles.css +17 -10
- package/harness/packages/server/src/lib/advisor.js +4 -0
- package/harness/packages/server/src/lib/agentRun.js +24 -1
- package/harness/packages/server/src/lib/channels/telegram.js +7 -1
- package/harness/packages/server/src/lib/checkpoints.js +106 -0
- package/harness/packages/server/src/lib/compact.js +4 -0
- package/harness/packages/server/src/lib/hooks.js +45 -0
- package/harness/packages/server/src/lib/plugins.js +76 -0
- package/harness/packages/server/src/lib/scheduler.js +190 -0
- package/harness/packages/server/src/routes/advisor.js +4 -2
- package/harness/packages/server/src/routes/chat.js +32 -7
- package/harness/packages/server/src/routes/schedules.js +124 -0
- package/harness/packages/server/src/routes/sessions.js +18 -2
- package/harness/packages/server/src/server.js +24 -0
- package/harness/packages/server/test/server.test.js +201 -0
- package/harness/tamnora.example.json +2 -0
- package/package.json +1 -1
|
@@ -72,6 +72,7 @@ const ICON_PATHS = {
|
|
|
72
72
|
'folder-open': ['m6 14 1.45-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.55 6a2 2 0 0 1-1.94 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2'],
|
|
73
73
|
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'],
|
|
74
74
|
'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'],
|
|
75
|
+
undo: ['M3 7v6h6', 'M3 13a9 9 0 1 1 2.64 6.36'],
|
|
75
76
|
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'],
|
|
76
77
|
'dots-vertical': ['M12 5h.01', 'M12 12h.01', 'M12 19h.01'],
|
|
77
78
|
'dots-horizontal': ['M5 12h.01', 'M12 12h.01', 'M19 12h.01'],
|
|
@@ -195,7 +196,7 @@ function activateTab(name) {
|
|
|
195
196
|
hideMoreMenu();
|
|
196
197
|
if (name === 'runs') refreshRuns();
|
|
197
198
|
if (name === 'providers') refreshProviders();
|
|
198
|
-
if (name === 'ajustes') { refreshMcp(); refreshSkills(); refreshMemory(); refreshChannels(); refreshLimits(); refreshVoz(); refreshAsesor(); refreshTranscribe(); refreshProfile(); }
|
|
199
|
+
if (name === 'ajustes') { refreshMcp(); refreshSkills(); refreshMemory(); refreshSchedules(); refreshChannels(); refreshLimits(); refreshVoz(); refreshAsesor(); refreshTranscribe(); refreshProfile(); }
|
|
199
200
|
}
|
|
200
201
|
|
|
201
202
|
document.querySelectorAll('.tab[data-tab]').forEach((t) =>
|
|
@@ -980,6 +981,7 @@ function addMessageActions(message) {
|
|
|
980
981
|
};
|
|
981
982
|
addItem('copiar', () => copyMessage(message), 'copy');
|
|
982
983
|
if (message.role === 'assistant') addItem('regenerar', () => regenerate(message), 'refresh-cw');
|
|
984
|
+
if (message.role === 'assistant') addItem('deshacer cambios', () => undoTurn(message), 'undo');
|
|
983
985
|
if (message.role === 'user') addItem('editar', () => editMessage(message), 'pencil');
|
|
984
986
|
menu.appendChild(el('div', 'msg-menu-sep'));
|
|
985
987
|
addItem('borrar desde acá', () => deleteFrom(message), 'trash', 'msg-act danger');
|
|
@@ -1042,8 +1044,27 @@ async function regenerate(message) {
|
|
|
1042
1044
|
await sendMessage();
|
|
1043
1045
|
}
|
|
1044
1046
|
|
|
1045
|
-
function
|
|
1046
|
-
const id =
|
|
1047
|
+
async function undoTurn(message) {
|
|
1048
|
+
const id = message?.wrap?.closest('.messages')?.dataset.session || state.sessionId;
|
|
1049
|
+
if (!id || state.panes.get(id)?.working) return;
|
|
1050
|
+
const detail = await api(`/api/sessions/${id}`).catch(() => null);
|
|
1051
|
+
if (!detail) return;
|
|
1052
|
+
const messages = detail.messages || [];
|
|
1053
|
+
const at = messages.findIndex((m) => m.id === messageId(message));
|
|
1054
|
+
let turn = null;
|
|
1055
|
+
for (let i = at >= 0 ? at : messages.length - 1; i >= 0; i--) {
|
|
1056
|
+
if (messages[i].role === 'user') {
|
|
1057
|
+
turn = messages[i];
|
|
1058
|
+
break;
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
if (!turn) return;
|
|
1062
|
+
if (!window.confirm('¿Deshacer este turno? Se restauran los archivos a como estaban y se borra el turno del historial.')) return;
|
|
1063
|
+
await api(`/api/sessions/${id}/rewind`, { method: 'POST', body: { turn: turn.id } }).catch(() => {});
|
|
1064
|
+
await reloadPane(id);
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
function editMessage(message) { const id = messageId(message);
|
|
1047
1068
|
chatInput.value = message.textEl.textContent;
|
|
1048
1069
|
chatInput.focus();
|
|
1049
1070
|
state.pendingEdit = id ? { fromId: id } : null;
|
|
@@ -3705,6 +3726,9 @@ const memoryListEl = $('#memory-list');
|
|
|
3705
3726
|
const memoryIndexEl = $('#memory-index');
|
|
3706
3727
|
const memoryModal = $('#memory-modal');
|
|
3707
3728
|
const memoryForm = $('#memory-form');
|
|
3729
|
+
const scheduleListEl = $('#schedule-list');
|
|
3730
|
+
const scheduleModal = $('#schedule-modal');
|
|
3731
|
+
const scheduleForm = $('#schedule-form');
|
|
3708
3732
|
const tgTokenEl = $('#tg-token');
|
|
3709
3733
|
const tgChatsEl = $('#tg-chats');
|
|
3710
3734
|
const tgTranscribeEl = $('#tg-transcribe');
|
|
@@ -3730,6 +3754,8 @@ const asesorExplainEl = $('#asesor-explain');
|
|
|
3730
3754
|
const asesorAdviceEl = $('#asesor-advice');
|
|
3731
3755
|
const asesorWordsEl = $('#asesor-words');
|
|
3732
3756
|
const asesorNotesEl = $('#asesor-notes');
|
|
3757
|
+
const asesorProviderEl = $('#asesor-provider');
|
|
3758
|
+
const asesorModelEl = $('#asesor-model');
|
|
3733
3759
|
const trProviderEl = $('#tr-provider');
|
|
3734
3760
|
const trModelEl = $('#tr-model');
|
|
3735
3761
|
|
|
@@ -4075,6 +4101,157 @@ async function saveMemoryIndex() {
|
|
|
4075
4101
|
refreshMemory();
|
|
4076
4102
|
}
|
|
4077
4103
|
|
|
4104
|
+
/* --- ajustes: Tareas programadas ----------------------------------------- */
|
|
4105
|
+
|
|
4106
|
+
function fmtWhen(ms) {
|
|
4107
|
+
try {
|
|
4108
|
+
return new Date(ms).toLocaleString();
|
|
4109
|
+
} catch {
|
|
4110
|
+
return '';
|
|
4111
|
+
}
|
|
4112
|
+
}
|
|
4113
|
+
|
|
4114
|
+
function scheduleTypeFields() {
|
|
4115
|
+
const type = $('#sc-type').value;
|
|
4116
|
+
const agent = type === 'agent';
|
|
4117
|
+
$('#sc-agent-prompt-row').hidden = !agent;
|
|
4118
|
+
$('#sc-agent-mode-row').hidden = !agent;
|
|
4119
|
+
$('#sc-shell-cmd-row').hidden = agent;
|
|
4120
|
+
$('#sc-shell-args-row').hidden = agent;
|
|
4121
|
+
}
|
|
4122
|
+
|
|
4123
|
+
async function refreshSchedules() {
|
|
4124
|
+
if (!scheduleListEl) return;
|
|
4125
|
+
const data = await api('/api/schedules').catch(() => null);
|
|
4126
|
+
if (!data) return;
|
|
4127
|
+
clear(scheduleListEl);
|
|
4128
|
+
if (!data.schedules.length) {
|
|
4129
|
+
scheduleListEl.appendChild(el('li', 'muted', 'sin tareas todavía'));
|
|
4130
|
+
return;
|
|
4131
|
+
}
|
|
4132
|
+
for (const s of data.schedules) {
|
|
4133
|
+
const li = el('li', s.enabled ? 'active' : '');
|
|
4134
|
+
li.appendChild(el('span', `pdot${s.enabled ? ' on' : ''}`));
|
|
4135
|
+
const main = el('div', 'grow');
|
|
4136
|
+
const meta = [s.type, s.spec];
|
|
4137
|
+
if (s.nextRun) meta.push(`próxima ${fmtWhen(s.nextRun)}`);
|
|
4138
|
+
if (s.lastStatus) meta.push(s.lastStatus);
|
|
4139
|
+
main.append(el('div', '', s.name || s.id), el('div', 'pmeta', meta.join(' · ')));
|
|
4140
|
+
li.appendChild(main);
|
|
4141
|
+
const actions = el('div', 'pactions');
|
|
4142
|
+
actions.appendChild(button('correr', (event) => { event.stopPropagation(); runSchedule(s.id); }));
|
|
4143
|
+
actions.appendChild(button(s.enabled ? 'pausar' : 'activar', (event) => { event.stopPropagation(); toggleSchedule(s.id); }));
|
|
4144
|
+
actions.appendChild(button('editar', (event) => { event.stopPropagation(); openScheduleForm(s.id); }));
|
|
4145
|
+
actions.appendChild(button('borrar', (event) => { event.stopPropagation(); removeSchedule(s.id); }, 'btn danger'));
|
|
4146
|
+
li.appendChild(actions);
|
|
4147
|
+
li.addEventListener('click', () => openScheduleForm(s.id));
|
|
4148
|
+
scheduleListEl.appendChild(li);
|
|
4149
|
+
}
|
|
4150
|
+
}
|
|
4151
|
+
|
|
4152
|
+
async function openScheduleForm(id = null) {
|
|
4153
|
+
$('#schedule-title').textContent = id ? `editar ${id}` : 'nueva tarea';
|
|
4154
|
+
$('#sc-id').value = id ?? '';
|
|
4155
|
+
$('#sc-id').readOnly = Boolean(id);
|
|
4156
|
+
$('#sc-name').value = '';
|
|
4157
|
+
$('#sc-type').value = 'agent';
|
|
4158
|
+
$('#sc-spec').value = '';
|
|
4159
|
+
$('#sc-prompt').value = '';
|
|
4160
|
+
$('#sc-mode').value = 'agent';
|
|
4161
|
+
$('#sc-command').value = '';
|
|
4162
|
+
$('#sc-args').value = '';
|
|
4163
|
+
$('#sc-cwd').value = '';
|
|
4164
|
+
$('#sc-enabled').checked = true;
|
|
4165
|
+
setStatus('#sc-status', '');
|
|
4166
|
+
if (id) {
|
|
4167
|
+
const data = await api('/api/schedules').catch(() => null);
|
|
4168
|
+
const s = data?.schedules?.find((item) => item.id === id);
|
|
4169
|
+
if (s) {
|
|
4170
|
+
$('#sc-name').value = s.name ?? '';
|
|
4171
|
+
$('#sc-type').value = s.type ?? 'agent';
|
|
4172
|
+
$('#sc-spec').value = s.spec ?? '';
|
|
4173
|
+
$('#sc-cwd').value = s.cwd ?? '';
|
|
4174
|
+
$('#sc-enabled').checked = s.enabled !== false;
|
|
4175
|
+
const payload = s.payload ?? {};
|
|
4176
|
+
if (s.type === 'shell') {
|
|
4177
|
+
$('#sc-command').value = payload.command ?? '';
|
|
4178
|
+
$('#sc-args').value = Array.isArray(payload.args) ? payload.args.join(' ') : payload.args ?? '';
|
|
4179
|
+
} else {
|
|
4180
|
+
$('#sc-prompt').value = payload.prompt ?? '';
|
|
4181
|
+
$('#sc-mode').value = payload.mode ?? 'agent';
|
|
4182
|
+
}
|
|
4183
|
+
}
|
|
4184
|
+
}
|
|
4185
|
+
scheduleTypeFields();
|
|
4186
|
+
scheduleModal.showModal();
|
|
4187
|
+
if (!id) $('#sc-id').focus();
|
|
4188
|
+
}
|
|
4189
|
+
|
|
4190
|
+
function closeScheduleForm() {
|
|
4191
|
+
if (scheduleModal.open) scheduleModal.close();
|
|
4192
|
+
}
|
|
4193
|
+
|
|
4194
|
+
async function submitSchedule(event) {
|
|
4195
|
+
event.preventDefault();
|
|
4196
|
+
const id = $('#sc-id').value.trim();
|
|
4197
|
+
if (!id) return setStatus('#sc-status', 'Falta el id.', 'err');
|
|
4198
|
+
const type = $('#sc-type').value;
|
|
4199
|
+
const body = {
|
|
4200
|
+
id,
|
|
4201
|
+
name: $('#sc-name').value.trim() || id,
|
|
4202
|
+
type,
|
|
4203
|
+
spec: $('#sc-spec').value.trim(),
|
|
4204
|
+
cwd: $('#sc-cwd').value.trim() || undefined,
|
|
4205
|
+
enabled: $('#sc-enabled').checked,
|
|
4206
|
+
};
|
|
4207
|
+
if (type === 'shell') {
|
|
4208
|
+
body.command = $('#sc-command').value.trim();
|
|
4209
|
+
body.args = $('#sc-args').value.trim();
|
|
4210
|
+
} else {
|
|
4211
|
+
body.prompt = $('#sc-prompt').value.trim();
|
|
4212
|
+
body.mode = $('#sc-mode').value;
|
|
4213
|
+
}
|
|
4214
|
+
try {
|
|
4215
|
+
const exists = $('#sc-id').readOnly;
|
|
4216
|
+
await api(exists ? `/api/schedules/${encodeURIComponent(id)}` : '/api/schedules', {
|
|
4217
|
+
method: exists ? 'PUT' : 'POST',
|
|
4218
|
+
body,
|
|
4219
|
+
});
|
|
4220
|
+
} catch (error) {
|
|
4221
|
+
return setStatus('#sc-status', error.message, 'err');
|
|
4222
|
+
}
|
|
4223
|
+
closeScheduleForm();
|
|
4224
|
+
refreshSchedules();
|
|
4225
|
+
}
|
|
4226
|
+
|
|
4227
|
+
async function toggleSchedule(id) {
|
|
4228
|
+
await toggleEntity(`/api/schedules/${encodeURIComponent(id)}/toggle`, {
|
|
4229
|
+
method: 'POST',
|
|
4230
|
+
errorLabel: 'cambiar la tarea',
|
|
4231
|
+
refresh: refreshSchedules,
|
|
4232
|
+
});
|
|
4233
|
+
}
|
|
4234
|
+
|
|
4235
|
+
async function removeSchedule(id) {
|
|
4236
|
+
await deleteEntity(`/api/schedules/${encodeURIComponent(id)}`, {
|
|
4237
|
+
confirmText: `¿Eliminar la tarea "${id}"?`,
|
|
4238
|
+
refresh: refreshSchedules,
|
|
4239
|
+
});
|
|
4240
|
+
}
|
|
4241
|
+
|
|
4242
|
+
async function runSchedule(id) {
|
|
4243
|
+
setStatus('#schedule-status', `corriendo ${id}…`);
|
|
4244
|
+
try {
|
|
4245
|
+
await api(`/api/schedules/${encodeURIComponent(id)}/run`, { method: 'POST' });
|
|
4246
|
+
} catch (error) {
|
|
4247
|
+
setStatus('#schedule-status', error.message, 'err');
|
|
4248
|
+
refreshSchedules();
|
|
4249
|
+
return;
|
|
4250
|
+
}
|
|
4251
|
+
setStatus('#schedule-status', `${id}: listo`, 'ok');
|
|
4252
|
+
refreshSchedules();
|
|
4253
|
+
}
|
|
4254
|
+
|
|
4078
4255
|
async function refreshChannels() {
|
|
4079
4256
|
if (!tgStatusEl) return;
|
|
4080
4257
|
const data = await api('/api/channels').catch(() => null);
|
|
@@ -4559,6 +4736,8 @@ function aplicarAsesor(data) {
|
|
|
4559
4736
|
if (asesorAdviceEl) asesorAdviceEl.checked = data.advice !== false;
|
|
4560
4737
|
if (asesorWordsEl) asesorWordsEl.value = data.maxWords ?? 40;
|
|
4561
4738
|
if (asesorNotesEl) asesorNotesEl.value = data.notes ?? '';
|
|
4739
|
+
if (asesorProviderEl) asesorProviderEl.value = data.provider ?? '';
|
|
4740
|
+
if (asesorModelEl) asesorModelEl.value = data.model ?? '';
|
|
4562
4741
|
const estado = $('#asesor-status');
|
|
4563
4742
|
if (estado) {
|
|
4564
4743
|
estado.className = `summary${data.enabled ? ' ok' : ''}`;
|
|
@@ -4580,6 +4759,8 @@ function asesorBody() {
|
|
|
4580
4759
|
advice: asesorAdviceEl?.checked !== false,
|
|
4581
4760
|
maxWords: Number(asesorWordsEl?.value) || 40,
|
|
4582
4761
|
notes: asesorNotesEl?.value ?? '',
|
|
4762
|
+
provider: asesorProviderEl?.value ?? '',
|
|
4763
|
+
model: asesorModelEl?.value ?? '',
|
|
4583
4764
|
};
|
|
4584
4765
|
}
|
|
4585
4766
|
|
|
@@ -4760,6 +4941,15 @@ $('#asesor-test')?.addEventListener('click', testAsesor);
|
|
|
4760
4941
|
memoryModal?.addEventListener('click', (event) => {
|
|
4761
4942
|
if (event.target === memoryModal) closeMemoryForm();
|
|
4762
4943
|
});
|
|
4944
|
+
|
|
4945
|
+
$('#sc-type')?.addEventListener('change', scheduleTypeFields);
|
|
4946
|
+
$('#schedule-new')?.addEventListener('click', () => openScheduleForm());
|
|
4947
|
+
$('#schedule-close')?.addEventListener('click', closeScheduleForm);
|
|
4948
|
+
$('#sc-cancel')?.addEventListener('click', closeScheduleForm);
|
|
4949
|
+
scheduleForm?.addEventListener('submit', submitSchedule);
|
|
4950
|
+
scheduleModal?.addEventListener('click', (event) => {
|
|
4951
|
+
if (event.target === scheduleModal) closeScheduleForm();
|
|
4952
|
+
});
|
|
4763
4953
|
}
|
|
4764
4954
|
|
|
4765
4955
|
/* --- eval ---------------------------------------------------------------- */
|
|
@@ -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 id="topbar-section" class="topbar-section">chat</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.15.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>
|
|
@@ -462,6 +462,7 @@
|
|
|
462
462
|
<button type="button" class="ajustes-tab" data-panel="mcp" role="tab">MCP</button>
|
|
463
463
|
<button type="button" class="ajustes-tab" data-panel="skills" role="tab">Skills</button>
|
|
464
464
|
<button type="button" class="ajustes-tab" data-panel="memoria" role="tab">Memoria</button>
|
|
465
|
+
<button type="button" class="ajustes-tab" data-panel="tareas" role="tab">Tareas</button>
|
|
465
466
|
<button type="button" class="ajustes-tab" data-panel="telegram" role="tab">Telegram</button>
|
|
466
467
|
<button type="button" class="ajustes-tab" data-panel="voz" role="tab">Voz</button>
|
|
467
468
|
<button type="button" class="ajustes-tab" data-panel="asesor" role="tab">Asesor</button>
|
|
@@ -603,6 +604,19 @@
|
|
|
603
604
|
<ul id="memory-list" class="providers"></ul>
|
|
604
605
|
</div>
|
|
605
606
|
|
|
607
|
+
<div class="panel ajustes-panel" data-panel="tareas">
|
|
608
|
+
<div class="row between">
|
|
609
|
+
<h2>Tareas</h2>
|
|
610
|
+
<button id="schedule-new" class="btn">+ tarea</button>
|
|
611
|
+
</div>
|
|
612
|
+
<p class="muted verify-line">
|
|
613
|
+
Trabajos recurrentes que corren solos mientras el server está vivo. <code>agent</code> corre un turno
|
|
614
|
+
(con las tools y permisos configurados); <code>shell</code> ejecuta un comando permitido.
|
|
615
|
+
</p>
|
|
616
|
+
<div id="schedule-status" class="summary"></div>
|
|
617
|
+
<ul id="schedule-list" class="providers"></ul>
|
|
618
|
+
</div>
|
|
619
|
+
|
|
606
620
|
<div class="panel ajustes-panel" data-panel="telegram">
|
|
607
621
|
<div class="row between">
|
|
608
622
|
<h2>Telegram</h2>
|
|
@@ -723,8 +737,16 @@
|
|
|
723
737
|
<p class="muted verify-line">
|
|
724
738
|
Un asesor que observa lo que el agente hace, lo interpreta y solo habla cuando tiene
|
|
725
739
|
algo que aportar: te alerta de un problema en el código, te explica un proceso o te da
|
|
726
|
-
un consejo. No lee la respuesta del agente. Usa el proveedor y modelo activos.
|
|
740
|
+
un consejo. No lee la respuesta del agente. Usa el proveedor y modelo activos, o unos propios.
|
|
727
741
|
</p>
|
|
742
|
+
<div class="row">
|
|
743
|
+
<label class="field"><span>proveedor (opcional)</span>
|
|
744
|
+
<input id="asesor-provider" spellcheck="false" placeholder="por defecto: el activo" />
|
|
745
|
+
</label>
|
|
746
|
+
<label class="field grow"><span>modelo (opcional)</span>
|
|
747
|
+
<input id="asesor-model" spellcheck="false" placeholder="por defecto: el activo" />
|
|
748
|
+
</label>
|
|
749
|
+
</div>
|
|
728
750
|
<label class="check" title="tener un asesor que interprete y comente el turno">
|
|
729
751
|
<input id="asesor-enabled" type="checkbox" /> Habilitar el asesor
|
|
730
752
|
</label>
|
|
@@ -1001,6 +1023,58 @@
|
|
|
1001
1023
|
</form>
|
|
1002
1024
|
</dialog>
|
|
1003
1025
|
|
|
1026
|
+
<dialog id="schedule-modal" class="modal">
|
|
1027
|
+
<form id="schedule-form" class="modal-card">
|
|
1028
|
+
<div class="row between">
|
|
1029
|
+
<h3 id="schedule-title">nueva tarea</h3>
|
|
1030
|
+
<button type="button" id="schedule-close" class="btn icon" title="cerrar" aria-label="cerrar">
|
|
1031
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
1032
|
+
<path d="M18 6 6 18M6 6l12 12" />
|
|
1033
|
+
</svg>
|
|
1034
|
+
</button>
|
|
1035
|
+
</div>
|
|
1036
|
+
<div class="row">
|
|
1037
|
+
<label class="field"><span>id</span><input id="sc-id" spellcheck="false" placeholder="resumen-diario" /></label>
|
|
1038
|
+
<label class="field grow"><span>nombre</span><input id="sc-name" spellcheck="false" placeholder="Resumen diario" /></label>
|
|
1039
|
+
</div>
|
|
1040
|
+
<div class="row">
|
|
1041
|
+
<label class="field"><span>tipo</span>
|
|
1042
|
+
<select id="sc-type">
|
|
1043
|
+
<option value="agent">agent</option>
|
|
1044
|
+
<option value="shell">shell</option>
|
|
1045
|
+
</select>
|
|
1046
|
+
</label>
|
|
1047
|
+
<label class="field grow"><span>cuándo</span>
|
|
1048
|
+
<input id="sc-spec" spellcheck="false" placeholder="every 30 m · daily 09:00 · 0 6 * * 1" />
|
|
1049
|
+
</label>
|
|
1050
|
+
</div>
|
|
1051
|
+
<label class="field" id="sc-agent-prompt-row"><span>prompt (agent)</span>
|
|
1052
|
+
<textarea id="sc-prompt" rows="4" spellcheck="false" placeholder="Qué tiene que hacer el agente…"></textarea>
|
|
1053
|
+
</label>
|
|
1054
|
+
<label class="field" id="sc-agent-mode-row"><span>modo (agent)</span>
|
|
1055
|
+
<select id="sc-mode">
|
|
1056
|
+
<option value="agent">agent</option>
|
|
1057
|
+
<option value="plan">plan</option>
|
|
1058
|
+
</select>
|
|
1059
|
+
</label>
|
|
1060
|
+
<label class="field" id="sc-shell-cmd-row"><span>comando (shell)</span>
|
|
1061
|
+
<input id="sc-command" spellcheck="false" placeholder="node · npm · pnpm" />
|
|
1062
|
+
</label>
|
|
1063
|
+
<label class="field" id="sc-shell-args-row"><span>argumentos (shell)</span>
|
|
1064
|
+
<input id="sc-args" spellcheck="false" placeholder="run build" />
|
|
1065
|
+
</label>
|
|
1066
|
+
<label class="field"><span>carpeta (opcional)</span>
|
|
1067
|
+
<input id="sc-cwd" spellcheck="false" placeholder="por defecto: la carpeta del server" />
|
|
1068
|
+
</label>
|
|
1069
|
+
<label class="check"><input id="sc-enabled" type="checkbox" checked /> activa</label>
|
|
1070
|
+
<div class="row between settings-actions">
|
|
1071
|
+
<button type="button" id="sc-cancel" class="btn">cancelar</button>
|
|
1072
|
+
<button type="submit" class="btn primary">guardar</button>
|
|
1073
|
+
</div>
|
|
1074
|
+
<div id="sc-status" class="summary"></div>
|
|
1075
|
+
</form>
|
|
1076
|
+
</dialog>
|
|
1077
|
+
|
|
1004
1078
|
<dialog id="palette-modal" class="modal palette-modal">
|
|
1005
1079
|
<div class="palette">
|
|
1006
1080
|
<div class="palette-row">
|
|
@@ -183,9 +183,11 @@
|
|
|
183
183
|
}
|
|
184
184
|
.nav-icon:hover { color: var(--accent); }
|
|
185
185
|
.nav-icon svg { width: 17px; height: 17px; display: block; }
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
186
|
+
/* El icono muestra el tema al que se va a cambiar: claro -> luna, oscuro -> sol. */
|
|
187
|
+
#theme-toggle .ico-sun { display: none; }
|
|
188
|
+
#theme-toggle .ico-moon { display: block; }
|
|
189
|
+
html[data-theme='dark'] #theme-toggle .ico-sun { display: block; }
|
|
190
|
+
html[data-theme='dark'] #theme-toggle .ico-moon { display: none; }
|
|
189
191
|
|
|
190
192
|
.hero {
|
|
191
193
|
width: min(1180px, calc(100% - 48px));
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
--mono: "DM Mono", ui-monospace, "Cascadia Code", "JetBrains Mono", "SF Mono", Consolas, monospace;
|
|
23
23
|
--font: var(--sans);
|
|
24
24
|
--chat-font: 14.5px;
|
|
25
|
-
--page-bg:
|
|
25
|
+
--page-bg: #101107;
|
|
26
26
|
color-scheme: dark;
|
|
27
27
|
}
|
|
28
28
|
|
|
@@ -48,7 +48,7 @@ html[data-theme='paper'] {
|
|
|
48
48
|
--warn: #8a5a00;
|
|
49
49
|
--solid: #f7f2e6;
|
|
50
50
|
--primary-fg: #fbf6ec;
|
|
51
|
-
--page-bg:
|
|
51
|
+
--page-bg: #f5efe2;
|
|
52
52
|
}
|
|
53
53
|
html[data-theme='paper'] .tok-comment { color: #7a7669; }
|
|
54
54
|
html[data-theme='paper'] .tok-string { color: #7a4a2c; }
|
|
@@ -144,9 +144,11 @@ pre, code, kbd, samp { font-family: var(--mono); }
|
|
|
144
144
|
#more-menu button.menu-primary { display: none; }
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
147
|
+
/* El icono muestra el tema al que se va a cambiar: claro -> luna, oscuro -> sol. */
|
|
148
|
+
#theme-toggle .ico-sun { display: none; }
|
|
149
|
+
#theme-toggle .ico-moon { display: block; }
|
|
150
|
+
html[data-theme='dark'] #theme-toggle .ico-sun { display: block; }
|
|
151
|
+
html[data-theme='dark'] #theme-toggle .ico-moon { display: none; }
|
|
150
152
|
.menu-pop {
|
|
151
153
|
position: absolute;
|
|
152
154
|
top: calc(100% + 4px);
|
|
@@ -1384,9 +1386,12 @@ pre.code code { background: none; border: 0; padding: 0; font-size: 0.92em; }
|
|
|
1384
1386
|
background: var(--rail);
|
|
1385
1387
|
backdrop-filter: blur(14px);
|
|
1386
1388
|
}
|
|
1387
|
-
.explorer-head { display: flex; align-items: center; gap: 6px; padding: 9px 12px; border-bottom: 1px solid var(--border); }
|
|
1389
|
+
.explorer-head { display: flex; align-items: center; gap: 6px; padding: 9px 12px; border-bottom: 1px solid var(--border); min-width: 0; }
|
|
1388
1390
|
.explorer-title { font: 700 14px var(--serif); letter-spacing: -0.02em; }
|
|
1389
1391
|
.explorer-head .spacer { flex: 1; }
|
|
1392
|
+
/* Los botones de accion (refrescar/cerrar) nunca se recortan: las pestanas se
|
|
1393
|
+
angostan y hacen scroll horizontal en vez de empujarlos fuera del panel. */
|
|
1394
|
+
.explorer-head .btn.icon { flex: 0 0 auto; }
|
|
1390
1395
|
.explorer-cwd { padding: 9px 12px 0; color: var(--muted); font: 500 11px var(--mono); word-break: break-all; }
|
|
1391
1396
|
.explorer-cwd:empty { display: none; }
|
|
1392
1397
|
.explorer-filter { padding: 9px 12px; }
|
|
@@ -1407,8 +1412,9 @@ pre.code code { background: none; border: 0; padding: 0; font-size: 0.92em; }
|
|
|
1407
1412
|
.explorer-status { padding: 6px 12px; border-top: 1px solid var(--border); color: var(--muted); font: 500 11px var(--mono); min-height: 27px; }
|
|
1408
1413
|
.explorer-status.err { color: var(--danger); }
|
|
1409
1414
|
|
|
1410
|
-
.xtabs { display: flex; gap: 4px; }
|
|
1411
|
-
.
|
|
1415
|
+
.xtabs { display: flex; gap: 4px; min-width: 0; flex: 0 1 auto; overflow-x: auto; scrollbar-width: none; }
|
|
1416
|
+
.xtabs::-webkit-scrollbar { display: none; }
|
|
1417
|
+
.xtab { flex: 0 0 auto; border: 0; background: none; color: var(--muted); cursor: pointer; font: 600 12px var(--sans); padding: 4px 7px; border-radius: 7px; }
|
|
1412
1418
|
.xtab:hover { background: var(--theirs); color: var(--text); }
|
|
1413
1419
|
.xtab.is-active { background: var(--theirs); color: var(--text); }
|
|
1414
1420
|
.xp-pane { display: flex; flex-direction: column; flex: 1; min-height: 0; }
|
|
@@ -1566,8 +1572,9 @@ dialog.file-modal.preview-maximized {
|
|
|
1566
1572
|
}
|
|
1567
1573
|
|
|
1568
1574
|
/* panel heads + collapsed handles ----------------------------------------- */
|
|
1569
|
-
.panel-head { display: flex; align-items: center; gap: 8px; padding: 2px 2px 6px; }
|
|
1570
|
-
.panel-head-title { flex: 1; font: 700 13px var(--serif); letter-spacing: -0.02em; color: var(--muted); text-transform: lowercase; }
|
|
1575
|
+
.panel-head { display: flex; align-items: center; gap: 8px; padding: 2px 2px 6px; min-width: 0; }
|
|
1576
|
+
.panel-head-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font: 700 13px var(--serif); letter-spacing: -0.02em; color: var(--muted); text-transform: lowercase; }
|
|
1577
|
+
.panel-head .btn.icon { flex: 0 0 auto; }
|
|
1571
1578
|
.panel-handle {
|
|
1572
1579
|
position: absolute;
|
|
1573
1580
|
top: 50%;
|
|
@@ -15,6 +15,8 @@ export const DEFAULT_ADVISOR = {
|
|
|
15
15
|
advice: true,
|
|
16
16
|
notes: '',
|
|
17
17
|
maxWords: 40,
|
|
18
|
+
provider: '',
|
|
19
|
+
model: '',
|
|
18
20
|
};
|
|
19
21
|
|
|
20
22
|
const KINDS = new Set(['code', 'explain', 'advice']);
|
|
@@ -34,6 +36,8 @@ export function resolveAdvisor(app) {
|
|
|
34
36
|
maxWords: Number.isFinite(maxWords)
|
|
35
37
|
? Math.min(Math.max(Math.round(maxWords), 10), 120)
|
|
36
38
|
: DEFAULT_ADVISOR.maxWords,
|
|
39
|
+
provider: typeof saved.provider === 'string' ? saved.provider.trim().slice(0, 60) : '',
|
|
40
|
+
model: typeof saved.model === 'string' ? saved.model.trim().slice(0, 120) : '',
|
|
37
41
|
};
|
|
38
42
|
}
|
|
39
43
|
|
|
@@ -20,21 +20,44 @@ export function toolCallPayload(event) {
|
|
|
20
20
|
* Combina tools locales + admin + subagentes en un toolset unificado.
|
|
21
21
|
* `approve` es el callback de aprobacion de las tools de administraación.
|
|
22
22
|
*/
|
|
23
|
-
export function combineToolset({ local, admin = null, tasks = null, exposeAdmin = false, approve = null }) {
|
|
23
|
+
export function combineToolset({ local, admin = null, tasks = null, plugins = null, exposeAdmin = false, approve = null }) {
|
|
24
24
|
const adminAllowed = (name) => Boolean(admin?.handles(name)) && (exposeAdmin || admin.readOnly?.has(name));
|
|
25
25
|
const definitions = [
|
|
26
26
|
...local.definitions,
|
|
27
27
|
...(admin ? admin.definitions.filter((d) => adminAllowed(d.function.name)) : []),
|
|
28
28
|
...(tasks ? tasks.definitions : []),
|
|
29
|
+
...(plugins ? plugins.definitions : []),
|
|
29
30
|
];
|
|
30
31
|
const execute = (name, args, context = {}) => {
|
|
31
32
|
if (tasks?.handles(name)) return tasks.execute(name, args);
|
|
32
33
|
if (adminAllowed(name)) return admin.execute(name, args, { approve, ...context });
|
|
34
|
+
if (plugins?.handles(name)) return plugins.execute(name, args, { approve, ...context });
|
|
33
35
|
return local.execute(name, args, context);
|
|
34
36
|
};
|
|
35
37
|
return { definitions, execute };
|
|
36
38
|
}
|
|
37
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Combina varios handlers de `onLifecycle` en uno: los corre a todos y concatena
|
|
42
|
+
* los `inject` que devuelvan (reglas por path, más tarde hooks).
|
|
43
|
+
*/
|
|
44
|
+
export function combineLifecycle(handlers = []) {
|
|
45
|
+
const active = handlers.filter(Boolean);
|
|
46
|
+
if (!active.length) return null;
|
|
47
|
+
return async function onLifecycle(event) {
|
|
48
|
+
const injects = [];
|
|
49
|
+
for (const handler of active) {
|
|
50
|
+
const out = await Promise.resolve()
|
|
51
|
+
.then(() => handler(event))
|
|
52
|
+
.catch(() => undefined);
|
|
53
|
+
if (out && typeof out === 'object' && typeof out.inject === 'string' && out.inject.trim()) {
|
|
54
|
+
injects.push(out.inject);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return injects.length ? { inject: injects.join('\n\n') } : undefined;
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
38
61
|
/** Resuelve proveedor + config con la API key almacenada. */
|
|
39
62
|
export function resolveProviderFor(ctx, providerId, extra = {}) {
|
|
40
63
|
return ctx.resolveProvider({
|
|
@@ -477,7 +477,13 @@ export function startTelegramChannel(
|
|
|
477
477
|
});
|
|
478
478
|
const exposeAdmin = !mode.tools;
|
|
479
479
|
const admin = ctx.agentTools;
|
|
480
|
-
const { definitions, execute } = combineToolset({
|
|
480
|
+
const { definitions, execute } = combineToolset({
|
|
481
|
+
local,
|
|
482
|
+
admin,
|
|
483
|
+
plugins: exposeAdmin ? ctx.plugins : null,
|
|
484
|
+
exposeAdmin,
|
|
485
|
+
approve: onAsk,
|
|
486
|
+
});
|
|
481
487
|
const { provider, config } = resolveProviderFor(ctx, providerId);
|
|
482
488
|
const userMessage = buildUserMessage(input, images);
|
|
483
489
|
const messages = await composeAgentMessages({ ctx, cwd, mode, history, userMessage });
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Checkpoints: pre-imagen de los archivos que `write`/`edit` van a tocar, para
|
|
2
|
+
// poder "deshacer" un turno. Se guardan en la casa (`checkpoints/`), no en el
|
|
3
|
+
// workspace, asi no ensucian el repo. Indexados por `sessionId` + turno (id del
|
|
4
|
+
// mensaje del usuario que abrio el run).
|
|
5
|
+
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises';
|
|
8
|
+
import { homeDir, resolveInRoot } from './core.js';
|
|
9
|
+
|
|
10
|
+
const MAX_FILE_BYTES = 512 * 1024;
|
|
11
|
+
|
|
12
|
+
function checkpointsDir() {
|
|
13
|
+
return path.join(homeDir(), 'checkpoints');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function manifestPath(sessionId) {
|
|
17
|
+
const safe = String(sessionId).replace(/[^a-zA-Z0-9_-]/g, '-');
|
|
18
|
+
return path.join(checkpointsDir(), `${safe}.json`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function readManifest(sessionId) {
|
|
22
|
+
try {
|
|
23
|
+
return JSON.parse(await readFile(manifestPath(sessionId), 'utf8'));
|
|
24
|
+
} catch {
|
|
25
|
+
return {};
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function writeManifest(sessionId, manifest) {
|
|
30
|
+
await mkdir(checkpointsDir(), { recursive: true });
|
|
31
|
+
await writeFile(manifestPath(sessionId), JSON.stringify(manifest), 'utf8');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Handler de `onLifecycle` que, antes de un `write`/`edit`, guarda la version
|
|
36
|
+
* previa del archivo (o marca que no existia) para el turno `turnId`.
|
|
37
|
+
*/
|
|
38
|
+
export function createCheckpointer(ctx, { sessionId, cwd, turnId }) {
|
|
39
|
+
if (!sessionId || !turnId) return null;
|
|
40
|
+
return async function onLifecycle(event) {
|
|
41
|
+
if (event?.type !== 'tool.before') return undefined;
|
|
42
|
+
if (event.name !== 'write' && event.name !== 'edit') return undefined;
|
|
43
|
+
const rel = String(event.args?.path ?? '').trim();
|
|
44
|
+
if (!rel) return undefined;
|
|
45
|
+
let abs;
|
|
46
|
+
try {
|
|
47
|
+
abs = resolveInRoot(cwd, rel);
|
|
48
|
+
} catch {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
let entry;
|
|
52
|
+
try {
|
|
53
|
+
const st = await stat(abs);
|
|
54
|
+
if (!st.isFile() || st.size > MAX_FILE_BYTES) return undefined;
|
|
55
|
+
entry = { path: rel, existed: true, content: (await readFile(abs)).toString('base64') };
|
|
56
|
+
} catch {
|
|
57
|
+
entry = { path: rel, existed: false, content: null };
|
|
58
|
+
}
|
|
59
|
+
const manifest = await readManifest(sessionId);
|
|
60
|
+
const list = manifest[turnId] ?? (manifest[turnId] = []);
|
|
61
|
+
if (!list.some((item) => item.path === rel)) list.push(entry);
|
|
62
|
+
await writeManifest(sessionId, manifest);
|
|
63
|
+
return undefined;
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Turnos (ids de mensaje) que tienen checkpoint guardado. */
|
|
68
|
+
export async function checkpointTurns(sessionId) {
|
|
69
|
+
return Object.keys(await readManifest(sessionId));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Restaura los archivos del turno y trunca el historial desde ese mensaje. */
|
|
73
|
+
export async function rewindSession(ctx, sessionId, turnId) {
|
|
74
|
+
const session = ctx.store.getSession(sessionId);
|
|
75
|
+
const cwd = session?.cwd || ctx.cwd;
|
|
76
|
+
const manifest = await readManifest(sessionId);
|
|
77
|
+
const entries = manifest[turnId] ?? [];
|
|
78
|
+
let restored = 0;
|
|
79
|
+
const files = [];
|
|
80
|
+
for (const entry of entries) {
|
|
81
|
+
let abs;
|
|
82
|
+
try {
|
|
83
|
+
abs = resolveInRoot(cwd, entry.path);
|
|
84
|
+
} catch {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
if (entry.existed) {
|
|
89
|
+
await mkdir(path.dirname(abs), { recursive: true });
|
|
90
|
+
await writeFile(abs, Buffer.from(entry.content ?? '', 'base64'));
|
|
91
|
+
} else {
|
|
92
|
+
await rm(abs, { force: true });
|
|
93
|
+
}
|
|
94
|
+
restored += 1;
|
|
95
|
+
files.push(entry.path);
|
|
96
|
+
} catch {
|
|
97
|
+
// un archivo que no se pudo restaurar no frena el resto
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const removed = ctx.store.deleteMessagesFrom(sessionId, turnId);
|
|
101
|
+
if (manifest[turnId]) {
|
|
102
|
+
delete manifest[turnId];
|
|
103
|
+
await writeManifest(sessionId, manifest);
|
|
104
|
+
}
|
|
105
|
+
return { restored, removed, files, turnId };
|
|
106
|
+
}
|