@danieltmn/tamnora 0.25.0 → 0.25.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/harness/packages/cli/src/commands/repl.js +18 -2
- package/harness/packages/cli/src/commands/run.js +5 -1
- package/harness/packages/cli/src/commands/tui.js +5 -1
- package/harness/packages/cli/src/lib/runtime.js +2 -0
- package/harness/packages/cli/src/tui/app.js +22 -2
- package/harness/packages/cli/src/tui/layout.js +1 -1
- package/harness/packages/core/src/agent/runAgent.js +5 -2
- package/harness/packages/core/src/agents/index.js +141 -0
- package/harness/packages/core/src/config/index.js +1 -0
- package/harness/packages/core/src/index.js +10 -0
- package/harness/packages/core/src/providers/index.js +3 -1
- package/harness/packages/core/src/providers/ollama.js +4 -1
- package/harness/packages/core/src/providers/openaiCompatible.js +6 -1
- package/harness/packages/core/src/providers/openrouter.js +5 -1
- package/harness/packages/core/src/providers/presets.js +4 -3
- package/harness/packages/core/src/providers/reasoning.js +58 -0
- package/harness/packages/core/test/agents.test.js +111 -0
- package/harness/packages/core/test/reasoning.test.js +43 -0
- package/harness/packages/server/public/app.js +37 -0
- package/harness/packages/server/public/chat.js +1 -0
- package/harness/packages/server/public/chrome.js +94 -1
- package/harness/packages/server/public/core.js +1 -1
- package/harness/packages/server/public/icons (1).zip +0 -0
- package/harness/packages/server/public/index.html +90 -1
- package/harness/packages/server/public/sessions.js +1 -0
- package/harness/packages/server/public/settings.js +167 -0
- package/harness/packages/server/public/settingsNav.js +1 -0
- package/harness/packages/server/src/lib/agents.js +74 -0
- package/harness/packages/server/src/lib/channels/telegram/run.js +1 -0
- package/harness/packages/server/src/lib/prompt.js +11 -3
- package/harness/packages/server/src/lib/scheduler.js +4 -0
- package/harness/packages/server/src/lib/sessionView.js +1 -0
- package/harness/packages/server/src/lib/skills.js +13 -0
- package/harness/packages/server/src/lib/tasks.js +71 -23
- package/harness/packages/server/src/routes/agents.js +88 -0
- package/harness/packages/server/src/routes/chat.js +9 -1
- package/harness/packages/server/src/routes/providers.js +7 -0
- package/harness/packages/server/src/routes/sessions.js +3 -1
- package/harness/packages/server/src/server.js +7 -0
- package/harness/packages/server/test/agents.test.js +194 -0
- package/harness/packages/server/test/consoleTabs.test.js +2 -2
- package/harness/packages/server/test/settingsNav.test.js +2 -2
- package/harness/tamnora.example.json +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdtemp, mkdir, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { readFile } from 'node:fs/promises';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import {
|
|
8
|
+
AGENT_FILE,
|
|
9
|
+
parseAgentMarkdown,
|
|
10
|
+
loadAgents,
|
|
11
|
+
mergeAgents,
|
|
12
|
+
buildAgentsIndex,
|
|
13
|
+
buildSubagentPrompt,
|
|
14
|
+
writeAgentFile,
|
|
15
|
+
} from '../src/agents/index.js';
|
|
16
|
+
|
|
17
|
+
test('parseAgentMarkdown reads frontmatter, lists and the body', () => {
|
|
18
|
+
const parsed = parseAgentMarkdown(
|
|
19
|
+
'---\nname: Revisor\ndescription: Busca bugs\nmodel: claude\nprovider: anthropic\nmode: plan\neffort: high\nmaxRounds: 4\ntools: read, grep\nskills: [code-review, tests]\n---\n\n# Rol\nMirá el diff.',
|
|
20
|
+
'revisor'
|
|
21
|
+
);
|
|
22
|
+
assert.equal(parsed.id, 'revisor');
|
|
23
|
+
assert.equal(parsed.name, 'Revisor');
|
|
24
|
+
assert.equal(parsed.description, 'Busca bugs');
|
|
25
|
+
assert.equal(parsed.model, 'claude');
|
|
26
|
+
assert.equal(parsed.provider, 'anthropic');
|
|
27
|
+
assert.equal(parsed.mode, 'plan');
|
|
28
|
+
assert.equal(parsed.effort, 'high');
|
|
29
|
+
assert.equal(parsed.maxRounds, 4);
|
|
30
|
+
assert.deepEqual(parsed.tools, ['read', 'grep']);
|
|
31
|
+
assert.deepEqual(parsed.skills, ['code-review', 'tests']);
|
|
32
|
+
assert.match(parsed.body, /Mirá el diff/);
|
|
33
|
+
|
|
34
|
+
const plain = parseAgentMarkdown('Solo cuerpo.', 'x');
|
|
35
|
+
assert.equal(plain.name, 'x');
|
|
36
|
+
assert.equal(plain.tools, null);
|
|
37
|
+
assert.equal(plain.skills, null);
|
|
38
|
+
assert.equal(plain.body, 'Solo cuerpo.');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('loadAgents scans directories and standalone .md files', async () => {
|
|
42
|
+
const root = await mkdtemp(path.join(tmpdir(), 'tamnora-agents-'));
|
|
43
|
+
await mkdir(path.join(root, 'revisor'));
|
|
44
|
+
await writeFile(path.join(root, 'revisor', AGENT_FILE), '---\nname: Revisor\n---\nCuerpo.');
|
|
45
|
+
await writeFile(path.join(root, 'tests.md'), '---\nname: Tests\n---\nCuerpo tests.');
|
|
46
|
+
await writeFile(path.join(root, 'ignorado.txt'), 'no es un agente');
|
|
47
|
+
|
|
48
|
+
const agents = loadAgents(root);
|
|
49
|
+
assert.deepEqual(
|
|
50
|
+
agents.map((agent) => agent.id),
|
|
51
|
+
['revisor', 'tests']
|
|
52
|
+
);
|
|
53
|
+
assert.equal(agents.find((agent) => agent.id === 'revisor').name, 'Revisor');
|
|
54
|
+
assert.equal(loadAgents(path.join(root, 'nope')).length, 0);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('mergeAgents lets the workspace override the casa agent', async () => {
|
|
58
|
+
const home = await mkdtemp(path.join(tmpdir(), 'tamnora-agents-home-'));
|
|
59
|
+
const workspace = await mkdtemp(path.join(tmpdir(), 'tamnora-agents-ws-'));
|
|
60
|
+
await mkdir(path.join(home, 'demo'));
|
|
61
|
+
await writeFile(path.join(home, 'demo', AGENT_FILE), '---\nname: Demo casa\n---\nCasa.');
|
|
62
|
+
await mkdir(path.join(workspace, 'demo'));
|
|
63
|
+
await writeFile(path.join(workspace, 'demo', AGENT_FILE), '---\nname: Demo workspace\n---\nWorkspace.');
|
|
64
|
+
|
|
65
|
+
const merged = mergeAgents([loadAgents(home), loadAgents(workspace)]);
|
|
66
|
+
assert.equal(merged.length, 1);
|
|
67
|
+
assert.equal(merged[0].name, 'Demo workspace');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('buildAgentsIndex catalogs subagents and buildSubagentPrompt includes instructions', () => {
|
|
71
|
+
assert.equal(buildAgentsIndex([]), '');
|
|
72
|
+
const index = buildAgentsIndex([
|
|
73
|
+
{ id: 'revisor', name: 'Revisor', description: 'Busca bugs' },
|
|
74
|
+
{ id: 'docs', name: 'Docs' },
|
|
75
|
+
]);
|
|
76
|
+
assert.match(index, /task\(\{ agent: "<id>"/);
|
|
77
|
+
assert.match(index, /revisor: Revisor — Busca bugs/);
|
|
78
|
+
assert.match(index, /docs: Docs/);
|
|
79
|
+
|
|
80
|
+
const prompt = buildSubagentPrompt({ id: 'revisor', name: 'Revisor', description: 'Busca bugs', body: 'Mirá el diff.' }, { basePrompt: 'BASE' });
|
|
81
|
+
assert.match(prompt, /BASE/);
|
|
82
|
+
assert.match(prompt, /subagente "Revisor"/);
|
|
83
|
+
assert.match(prompt, /Mirá el diff\./);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('writeAgentFile round-trips through parseAgentMarkdown', async () => {
|
|
87
|
+
const root = await mkdtemp(path.join(tmpdir(), 'tamnora-agents-write-'));
|
|
88
|
+
await writeAgentFile(path.join(root, 'revisor'), {
|
|
89
|
+
id: 'revisor',
|
|
90
|
+
name: 'Revisor',
|
|
91
|
+
description: 'Busca bugs',
|
|
92
|
+
model: 'gpt',
|
|
93
|
+
provider: 'openai',
|
|
94
|
+
mode: 'plan',
|
|
95
|
+
effort: 'low',
|
|
96
|
+
maxRounds: 3,
|
|
97
|
+
tools: ['read'],
|
|
98
|
+
skills: null,
|
|
99
|
+
body: '# Rol\nMirá el diff.',
|
|
100
|
+
});
|
|
101
|
+
const text = await readFile(path.join(root, 'revisor', AGENT_FILE), 'utf8');
|
|
102
|
+
const parsed = parseAgentMarkdown(text, 'revisor');
|
|
103
|
+
assert.equal(parsed.name, 'Revisor');
|
|
104
|
+
assert.equal(parsed.effort, 'low');
|
|
105
|
+
assert.equal(parsed.maxRounds, 3);
|
|
106
|
+
assert.deepEqual(parsed.tools, ['read']);
|
|
107
|
+
assert.equal(parsed.skills, null);
|
|
108
|
+
assert.match(parsed.body, /Mirá el diff/);
|
|
109
|
+
|
|
110
|
+
await assert.rejects(() => writeAgentFile(path.join(root, '..'), { id: '../escape', body: 'x' }), /inválido/);
|
|
111
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { normalizeEffort, applyReasoningEffort, EFFORT_LEVELS } from '../src/providers/reasoning.js';
|
|
4
|
+
|
|
5
|
+
test('normalizeEffort accepts the common vocabulary and aliases', () => {
|
|
6
|
+
for (const level of EFFORT_LEVELS) assert.equal(normalizeEffort(level), level);
|
|
7
|
+
assert.equal(normalizeEffort(' HIGH '), 'high');
|
|
8
|
+
assert.equal(normalizeEffort('none'), 'off');
|
|
9
|
+
assert.equal(normalizeEffort('minimal'), 'low');
|
|
10
|
+
assert.equal(normalizeEffort('xhigh'), 'max');
|
|
11
|
+
assert.equal(normalizeEffort('ultra'), 'max');
|
|
12
|
+
assert.equal(normalizeEffort('auto'), '');
|
|
13
|
+
assert.equal(normalizeEffort(''), '');
|
|
14
|
+
assert.equal(normalizeEffort(null), '');
|
|
15
|
+
assert.equal(normalizeEffort('inventado'), '');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('applyReasoningEffort maps the level to each provider shape', () => {
|
|
19
|
+
const deepseek = applyReasoningEffort({}, 'high', { reasoning: 'deepseek' });
|
|
20
|
+
assert.equal(deepseek.reasoning_effort, 'high');
|
|
21
|
+
assert.deepEqual(deepseek.thinking, { type: 'enabled' });
|
|
22
|
+
|
|
23
|
+
const off = applyReasoningEffort({}, 'off', { reasoning: 'deepseek' });
|
|
24
|
+
assert.equal(off.reasoning_effort, 'none');
|
|
25
|
+
assert.deepEqual(off.thinking, { type: 'disabled' });
|
|
26
|
+
|
|
27
|
+
const openai = applyReasoningEffort({}, 'max', { reasoning: 'openai' });
|
|
28
|
+
assert.equal(openai.reasoning_effort, 'high', 'OpenAI no llega a max');
|
|
29
|
+
|
|
30
|
+
const openrouter = applyReasoningEffort({}, 'max', { reasoning: 'effort' });
|
|
31
|
+
assert.equal(openrouter.reasoning_effort, 'max');
|
|
32
|
+
|
|
33
|
+
const ollama = applyReasoningEffort({}, 'low', { reasoning: 'think' });
|
|
34
|
+
assert.equal(ollama.think, 'low');
|
|
35
|
+
const ollamaOff = applyReasoningEffort({}, 'off', { reasoning: 'think' });
|
|
36
|
+
assert.equal(ollamaOff.think, false);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('applyReasoningEffort leaves unsupported providers untouched', () => {
|
|
40
|
+
assert.deepEqual(applyReasoningEffort({ model: 'x' }, 'high', { reasoning: 'none' }), { model: 'x' });
|
|
41
|
+
assert.deepEqual(applyReasoningEffort({ model: 'x' }, 'high', {}), { model: 'x' });
|
|
42
|
+
assert.deepEqual(applyReasoningEffort({ model: 'x' }, '', { reasoning: 'effort' }), { model: 'x' });
|
|
43
|
+
});
|
|
@@ -26,12 +26,16 @@ import {
|
|
|
26
26
|
agentControlsSlot,
|
|
27
27
|
buildAppearanceControls,
|
|
28
28
|
closeCreateFolder,
|
|
29
|
+
closeEffortMenu,
|
|
29
30
|
closeFolderPicker,
|
|
30
31
|
closeModeMenu,
|
|
31
32
|
closeSettingsModal,
|
|
32
33
|
createFolder,
|
|
33
34
|
cwdPill,
|
|
34
35
|
cycleTheme,
|
|
36
|
+
effortMenu,
|
|
37
|
+
effortPill,
|
|
38
|
+
effortSelect,
|
|
35
39
|
filterFolders,
|
|
36
40
|
folderGoto,
|
|
37
41
|
folderList,
|
|
@@ -60,6 +64,7 @@ import {
|
|
|
60
64
|
providerSelect,
|
|
61
65
|
renderModePill,
|
|
62
66
|
setCwd,
|
|
67
|
+
setEffort,
|
|
63
68
|
setMode,
|
|
64
69
|
settingsModal,
|
|
65
70
|
statusbarEl,
|
|
@@ -114,6 +119,9 @@ import { wireModelPicker } from './models.js';
|
|
|
114
119
|
import { restoreTabs, startNewSession } from './sessions.js';
|
|
115
120
|
import {
|
|
116
121
|
addPerfilFieldRow,
|
|
122
|
+
agentForm,
|
|
123
|
+
agentModal,
|
|
124
|
+
closeAgentForm,
|
|
117
125
|
closeMcpForm,
|
|
118
126
|
closeMemoryForm,
|
|
119
127
|
closeProviderForm,
|
|
@@ -133,6 +141,7 @@ import {
|
|
|
133
141
|
mcpPinnedEl,
|
|
134
142
|
memoryForm,
|
|
135
143
|
memoryModal,
|
|
144
|
+
openAgentForm,
|
|
136
145
|
openMcpForm,
|
|
137
146
|
openMemoryForm,
|
|
138
147
|
openProviderForm,
|
|
@@ -155,6 +164,7 @@ import {
|
|
|
155
164
|
setStatus,
|
|
156
165
|
skillForm,
|
|
157
166
|
skillModal,
|
|
167
|
+
submitAgent,
|
|
158
168
|
submitAsesor,
|
|
159
169
|
submitChannel,
|
|
160
170
|
submitMcp,
|
|
@@ -335,6 +345,25 @@ document.addEventListener('keydown', (event) => {
|
|
|
335
345
|
if (event.key === 'Escape') closeModeMenu();
|
|
336
346
|
});
|
|
337
347
|
modelPill?.addEventListener('click', () => openSettingsModal());
|
|
348
|
+
effortPill?.addEventListener('click', (event) => {
|
|
349
|
+
event.stopPropagation();
|
|
350
|
+
if (!effortMenu) return;
|
|
351
|
+
effortMenu.hidden = !effortMenu.hidden;
|
|
352
|
+
effortPill.setAttribute('aria-expanded', effortMenu.hidden ? 'false' : 'true');
|
|
353
|
+
});
|
|
354
|
+
effortMenu?.addEventListener('click', async (event) => {
|
|
355
|
+
const item = event.target.closest('.ctrl-menu-item');
|
|
356
|
+
if (!item) return;
|
|
357
|
+
closeEffortMenu();
|
|
358
|
+
await setEffort(item.dataset.effort || '');
|
|
359
|
+
});
|
|
360
|
+
effortSelect?.addEventListener('change', () => setEffort(effortSelect.value));
|
|
361
|
+
document.addEventListener('click', (event) => {
|
|
362
|
+
if (effortMenu && !effortMenu.hidden && !event.target.closest('.ctrl-wrap')) closeEffortMenu();
|
|
363
|
+
});
|
|
364
|
+
document.addEventListener('keydown', (event) => {
|
|
365
|
+
if (event.key === 'Escape') closeEffortMenu();
|
|
366
|
+
});
|
|
338
367
|
yesInput?.addEventListener('change', renderModePill);
|
|
339
368
|
strictInput?.addEventListener('change', () =>
|
|
340
369
|
api('/api/settings', { method: 'PUT', body: { strictScope: strictInput.checked } }).catch(() => null)
|
|
@@ -637,6 +666,14 @@ $('#asesor-test')?.addEventListener('click', testAsesor);
|
|
|
637
666
|
if (event.target === skillModal) closeSkillForm();
|
|
638
667
|
});
|
|
639
668
|
|
|
669
|
+
$('#agent-new')?.addEventListener('click', () => openAgentForm());
|
|
670
|
+
$('#agent-close')?.addEventListener('click', closeAgentForm);
|
|
671
|
+
$('#af-cancel')?.addEventListener('click', closeAgentForm);
|
|
672
|
+
agentForm?.addEventListener('submit', submitAgent);
|
|
673
|
+
agentModal?.addEventListener('click', (event) => {
|
|
674
|
+
if (event.target === agentModal) closeAgentForm();
|
|
675
|
+
});
|
|
676
|
+
|
|
640
677
|
$('#memory-new')?.addEventListener('click', () => openMemoryForm());
|
|
641
678
|
$('#memory-close')?.addEventListener('click', closeMemoryForm);
|
|
642
679
|
$('#mem-cancel')?.addEventListener('click', closeMemoryForm);
|
|
@@ -1080,6 +1080,7 @@ export async function startTurn(sessionKey, pane, input, images, opts = {}) {
|
|
|
1080
1080
|
provider: owner?.provider || providerSelect.value || undefined,
|
|
1081
1081
|
model: owner?.model || modelSelect.value || undefined,
|
|
1082
1082
|
mode: owner?.mode || modeSelect.value || undefined,
|
|
1083
|
+
effort: owner?.effort ?? undefined,
|
|
1083
1084
|
yes: yesInput.checked ? true : undefined,
|
|
1084
1085
|
contextTokens: contextTokensForRequest(),
|
|
1085
1086
|
interrupt: opts.interrupt ? true : undefined,
|
|
@@ -16,6 +16,7 @@ import { pickVisible } from './modelsQuery.js';
|
|
|
16
16
|
import { activateSession, registerSession } from './sessions.js';
|
|
17
17
|
import {
|
|
18
18
|
activateAjustesPanel,
|
|
19
|
+
refreshAgents,
|
|
19
20
|
refreshAsesor,
|
|
20
21
|
refreshChannels,
|
|
21
22
|
refreshLimits,
|
|
@@ -69,7 +70,7 @@ export function activateTab(name) {
|
|
|
69
70
|
hideMoreMenu();
|
|
70
71
|
if (name === 'runs') refreshRuns();
|
|
71
72
|
if (name === 'providers') refreshProviders();
|
|
72
|
-
if (name === 'ajustes') { setAjustesLevel('list'); refreshMcp(); refreshMcpCatalog(); refreshSkills(); refreshSkillCatalog(); refreshMemory(); refreshSchedules(); refreshChannels(); refreshLimits(); refreshVoz(); refreshAsesor(); refreshTranscribe(); refreshProfile(); }
|
|
73
|
+
if (name === 'ajustes') { setAjustesLevel('list'); refreshMcp(); refreshMcpCatalog(); refreshSkills(); refreshSkillCatalog(); refreshAgents(); refreshMemory(); refreshSchedules(); refreshChannels(); refreshLimits(); refreshVoz(); refreshAsesor(); refreshTranscribe(); refreshProfile(); }
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
/* --- settings modal (modo / proveedor / modelo / auto) -------------------- */
|
|
@@ -122,6 +123,8 @@ export async function loadModes() {
|
|
|
122
123
|
const active = settings?.settings?.mode || 'agent';
|
|
123
124
|
if ([...modeSelect.options].some((o) => o.value === active)) modeSelect.value = active;
|
|
124
125
|
if (strictInput) strictInput.checked = settings?.settings?.strictScope === true;
|
|
126
|
+
state.active.effort = settings?.settings?.effort ?? '';
|
|
127
|
+
renderEffortPill();
|
|
125
128
|
defaultCwdCache = settings?.settings?.defaultCwd || '';
|
|
126
129
|
if (!defaultCwdCache) {
|
|
127
130
|
// Migración: la carpeta predeterminada vivía en localStorage; la subimos al
|
|
@@ -161,6 +164,19 @@ export const modePillLabel = $('#mode-pill-label');
|
|
|
161
164
|
export const modeMenu = $('#mode-menu');
|
|
162
165
|
export const modelPill = $('#model-pill');
|
|
163
166
|
export const modelPillLabel = $('#model-pill-label');
|
|
167
|
+
export const effortPill = $('#effort-pill');
|
|
168
|
+
export const effortPillLabel = $('#effort-pill-label');
|
|
169
|
+
export const effortMenu = $('#effort-menu');
|
|
170
|
+
export const effortSelect = $('#effort');
|
|
171
|
+
|
|
172
|
+
const EFFORT_CHOICES = [
|
|
173
|
+
{ value: '', label: 'auto' },
|
|
174
|
+
{ value: 'off', label: 'off' },
|
|
175
|
+
{ value: 'low', label: 'low' },
|
|
176
|
+
{ value: 'medium', label: 'medium' },
|
|
177
|
+
{ value: 'high', label: 'high' },
|
|
178
|
+
{ value: 'max', label: 'max' },
|
|
179
|
+
];
|
|
164
180
|
|
|
165
181
|
export function renderCwdPill() {
|
|
166
182
|
if (!cwdPillLabel) return;
|
|
@@ -208,8 +224,82 @@ export function closeModeMenu() {
|
|
|
208
224
|
modePill?.setAttribute('aria-expanded', 'false');
|
|
209
225
|
}
|
|
210
226
|
|
|
227
|
+
/* --- esfuerzo de razonamiento -------------------------------------------- */
|
|
228
|
+
|
|
229
|
+
/** Esfuerzo de la sesión abierta; si no eligió, el default global. */
|
|
230
|
+
export function effortValue(record = state.panes.get(state.sessionId)) {
|
|
231
|
+
if (record && record.effort !== undefined) return record.effort;
|
|
232
|
+
return state.active.effort ?? '';
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function checkIcon() {
|
|
236
|
+
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
237
|
+
svg.setAttribute('class', 'ctrl-check');
|
|
238
|
+
svg.setAttribute('viewBox', '0 0 24 24');
|
|
239
|
+
svg.setAttribute('fill', 'none');
|
|
240
|
+
svg.setAttribute('stroke', 'currentColor');
|
|
241
|
+
svg.setAttribute('stroke-width', '2.2');
|
|
242
|
+
svg.setAttribute('stroke-linecap', 'round');
|
|
243
|
+
svg.setAttribute('stroke-linejoin', 'round');
|
|
244
|
+
svg.setAttribute('aria-hidden', 'true');
|
|
245
|
+
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
246
|
+
path.setAttribute('d', 'M20 6 9 17l-5-5');
|
|
247
|
+
svg.appendChild(path);
|
|
248
|
+
return svg;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function buildEffortMenu() {
|
|
252
|
+
if (!effortMenu) return;
|
|
253
|
+
clear(effortMenu);
|
|
254
|
+
for (const choice of EFFORT_CHOICES) {
|
|
255
|
+
const item = document.createElement('button');
|
|
256
|
+
item.type = 'button';
|
|
257
|
+
item.className = 'ctrl-menu-item';
|
|
258
|
+
item.dataset.effort = choice.value;
|
|
259
|
+
item.setAttribute('role', 'menuitemradio');
|
|
260
|
+
item.setAttribute('aria-checked', 'false');
|
|
261
|
+
item.appendChild(el('span', '', choice.label));
|
|
262
|
+
item.appendChild(checkIcon());
|
|
263
|
+
effortMenu.appendChild(item);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
buildEffortMenu();
|
|
267
|
+
|
|
268
|
+
export function renderEffortPill() {
|
|
269
|
+
const value = effortValue();
|
|
270
|
+
if (effortPillLabel) effortPillLabel.textContent = value || 'auto';
|
|
271
|
+
if (effortPill) {
|
|
272
|
+
effortPill.title = value
|
|
273
|
+
? `esfuerzo de razonamiento: ${value}`
|
|
274
|
+
: 'esfuerzo de razonamiento: auto (default del modelo)';
|
|
275
|
+
}
|
|
276
|
+
if (effortSelect && effortSelect.value !== value) effortSelect.value = value;
|
|
277
|
+
if (!effortMenu) return;
|
|
278
|
+
for (const item of effortMenu.querySelectorAll('.ctrl-menu-item')) {
|
|
279
|
+
const active = (item.dataset.effort || '') === value;
|
|
280
|
+
item.classList.toggle('active', active);
|
|
281
|
+
item.setAttribute('aria-checked', active ? 'true' : 'false');
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function closeEffortMenu() {
|
|
286
|
+
if (!effortMenu || effortMenu.hidden) return;
|
|
287
|
+
effortMenu.hidden = true;
|
|
288
|
+
effortPill?.setAttribute('aria-expanded', 'false');
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export async function setEffort(value, { persist = true } = {}) {
|
|
292
|
+
const next = EFFORT_CHOICES.some((choice) => choice.value === value) ? value : '';
|
|
293
|
+
const record = state.panes.get(state.sessionId);
|
|
294
|
+
if (record) record.effort = next;
|
|
295
|
+
state.active.effort = next;
|
|
296
|
+
renderEffortPill();
|
|
297
|
+
if (persist) await persistAgent({ effort: next });
|
|
298
|
+
}
|
|
299
|
+
|
|
211
300
|
export function updateStatus(extra = {}) {
|
|
212
301
|
renderModePill();
|
|
302
|
+
renderEffortPill();
|
|
213
303
|
renderModelPill();
|
|
214
304
|
if (extra.rounds !== undefined) $('#sb-rounds').textContent = `${extra.rounds} rondas`;
|
|
215
305
|
if (extra.stopReason) $('#sb-rounds').textContent = extra.stopReason;
|
|
@@ -492,6 +582,7 @@ export async function saveSettings(patch) {
|
|
|
492
582
|
if (out?.settings) {
|
|
493
583
|
state.active.provider = out.settings.provider;
|
|
494
584
|
state.active.model = out.settings.model;
|
|
585
|
+
if ('effort' in patch) state.active.effort = patch.effort ?? '';
|
|
495
586
|
}
|
|
496
587
|
if (patch.provider) providerSelect.value = patch.provider;
|
|
497
588
|
updateStatus();
|
|
@@ -508,6 +599,7 @@ export async function persistAgent(patch) {
|
|
|
508
599
|
if (patch.provider) record.provider = patch.provider;
|
|
509
600
|
if (patch.model) record.model = patch.model;
|
|
510
601
|
if (patch.mode) record.mode = patch.mode;
|
|
602
|
+
if (patch.effort !== undefined) record.effort = patch.effort;
|
|
511
603
|
api(`/api/sessions/${record.id}`, { method: 'PUT', body: patch }).catch(() => null);
|
|
512
604
|
}
|
|
513
605
|
await saveSettings(patch);
|
|
@@ -532,6 +624,7 @@ export async function applySessionAgent(record) {
|
|
|
532
624
|
if (mode !== 'voice') setHandsFree(false);
|
|
533
625
|
updateStatus();
|
|
534
626
|
renderModePill();
|
|
627
|
+
renderEffortPill();
|
|
535
628
|
renderModelPill();
|
|
536
629
|
}
|
|
537
630
|
|
|
@@ -18,7 +18,7 @@ export const state = {
|
|
|
18
18
|
sessionId: null,
|
|
19
19
|
streaming: false,
|
|
20
20
|
providers: [],
|
|
21
|
-
active: { provider: '', model: '' },
|
|
21
|
+
active: { provider: '', model: '', effort: '' },
|
|
22
22
|
// Catalogo de modelos del proveedor y su seleccion. Se cachea porque el
|
|
23
23
|
// catalogo son cientos de modelos (~591 KB) y `loadModels` corre en cada
|
|
24
24
|
// cambio de pestana: refetchearlo hacia parpadear la consola.
|
|
Binary file
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
</head>
|
|
21
21
|
<body>
|
|
22
22
|
<header class="topbar">
|
|
23
|
-
<div class="brand"><span class="logo"><img src="/icons/logo.png" alt="Tamnora" /></span> <button id="back-chat" type="button" class="back-chat" title="volver al chat" aria-label="volver al chat" hidden><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m15 18-6-6 6-6" /></svg><span>chat</span></button> <span id="topbar-section" class="topbar-section">chat</span> <span class="ver">v0.25.
|
|
23
|
+
<div class="brand"><span class="logo"><img src="/icons/logo.png" alt="Tamnora" /></span> <button id="back-chat" type="button" class="back-chat" title="volver al chat" aria-label="volver al chat" hidden><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m15 18-6-6 6-6" /></svg><span>chat</span></button> <span id="topbar-section" class="topbar-section">chat</span> <span class="ver">v0.25.1</span></div>
|
|
24
24
|
<div class="settings">
|
|
25
25
|
<button id="new-session-top" type="button" class="btn topbar-new" title="nueva sesión" aria-label="nueva sesión">
|
|
26
26
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true">
|
|
@@ -194,6 +194,14 @@
|
|
|
194
194
|
</button>
|
|
195
195
|
<input id="image-input" type="file" accept="image/*" multiple hidden />
|
|
196
196
|
<textarea id="chat-input" rows="1" placeholder="escribí un mensaje…" title="Enter envía · Shift+Enter salto" aria-label="mensaje para el asistente" spellcheck="false"></textarea>
|
|
197
|
+
<div class="ctrl-wrap">
|
|
198
|
+
<button type="button" id="effort-pill" class="ctrl-pill" aria-haspopup="menu" aria-expanded="false" title="esfuerzo de razonamiento">
|
|
199
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M13 2 3 14h9l-1 8 10-12h-9l1-8z" /></svg>
|
|
200
|
+
<span id="effort-pill-label">auto</span>
|
|
201
|
+
<svg class="chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6" /></svg>
|
|
202
|
+
</button>
|
|
203
|
+
<div id="effort-menu" class="ctrl-menu" role="menu" hidden></div>
|
|
204
|
+
</div>
|
|
197
205
|
<button type="button" id="model-pill" class="ctrl-pill model-pill" title="proveedor y modelo activos">
|
|
198
206
|
<span id="model-pill-label">—</span>
|
|
199
207
|
<svg class="chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6" /></svg>
|
|
@@ -472,6 +480,7 @@
|
|
|
472
480
|
<button type="button" class="ajustes-tab" id="ajustes-tab-perfil" data-panel="perfil" role="tab" aria-selected="false" aria-controls="ajustes-panel-perfil">Perfil</button>
|
|
473
481
|
<button type="button" class="ajustes-tab" id="ajustes-tab-memoria" data-panel="memoria" role="tab" aria-selected="false" aria-controls="ajustes-panel-memoria">Memoria</button>
|
|
474
482
|
<button type="button" class="ajustes-tab" id="ajustes-tab-skills" data-panel="skills" role="tab" aria-selected="false" aria-controls="ajustes-panel-skills">Skills</button>
|
|
483
|
+
<button type="button" class="ajustes-tab" id="ajustes-tab-agentes" data-panel="agentes" role="tab" aria-selected="false" aria-controls="ajustes-panel-agentes">Subagentes</button>
|
|
475
484
|
<button type="button" class="ajustes-tab" id="ajustes-tab-mcp" data-panel="mcp" role="tab" aria-selected="false" aria-controls="ajustes-panel-mcp">MCP</button>
|
|
476
485
|
</div>
|
|
477
486
|
<div class="ajustes-group" data-group="canales" role="presentation">
|
|
@@ -512,6 +521,15 @@
|
|
|
512
521
|
<select id="model" title="modelo"></select>
|
|
513
522
|
<button type="button" id="model-pick" class="btn" title="elegir qué modelos mostrar">elegir</button>
|
|
514
523
|
</div>
|
|
524
|
+
<label for="effort">Esfuerzo de razonamiento</label>
|
|
525
|
+
<select id="effort" title="cuánto piensa el modelo antes de responder">
|
|
526
|
+
<option value="">auto (default del modelo)</option>
|
|
527
|
+
<option value="off">off · sin razonamiento</option>
|
|
528
|
+
<option value="low">low · bajo</option>
|
|
529
|
+
<option value="medium">medium · medio</option>
|
|
530
|
+
<option value="high">high · alto</option>
|
|
531
|
+
<option value="max">max · máximo</option>
|
|
532
|
+
</select>
|
|
515
533
|
<label class="check" title="aprobar automáticamente tools con permiso ask">
|
|
516
534
|
<input id="yes" type="checkbox" /> Aprobar automáticamente las tools (auto)
|
|
517
535
|
</label>
|
|
@@ -646,6 +664,20 @@
|
|
|
646
664
|
</details>
|
|
647
665
|
</div>
|
|
648
666
|
|
|
667
|
+
<div class="panel ajustes-panel" id="ajustes-panel-agentes" data-panel="agentes" role="tabpanel" aria-labelledby="ajustes-tab-agentes">
|
|
668
|
+
<div class="row between">
|
|
669
|
+
<h2>Subagentes</h2>
|
|
670
|
+
<button id="agent-new" class="btn">+ nuevo</button>
|
|
671
|
+
</div>
|
|
672
|
+
<p class="muted verify-line">
|
|
673
|
+
Subagentes definidos en <code>agents/</code> (la casa) o <code>.tamnora/agents/</code> (el proyecto).
|
|
674
|
+
El agente principal los ve como catálogo y delega con <code>task</code>; cada uno aporta su
|
|
675
|
+
modelo, modo, tools, skills y esfuerzo.
|
|
676
|
+
</p>
|
|
677
|
+
<div id="agent-status" class="summary"></div>
|
|
678
|
+
<ul id="agent-list" class="providers"></ul>
|
|
679
|
+
</div>
|
|
680
|
+
|
|
649
681
|
<div class="panel ajustes-panel" id="ajustes-panel-memoria" data-panel="memoria" role="tabpanel" aria-labelledby="ajustes-tab-memoria">
|
|
650
682
|
<div class="row between">
|
|
651
683
|
<h2>Memoria</h2>
|
|
@@ -1111,6 +1143,63 @@
|
|
|
1111
1143
|
</form>
|
|
1112
1144
|
</dialog>
|
|
1113
1145
|
|
|
1146
|
+
<dialog id="agent-modal" class="modal">
|
|
1147
|
+
<form id="agent-form" class="modal-card">
|
|
1148
|
+
<div class="row between">
|
|
1149
|
+
<h3 id="agent-title">nuevo subagente</h3>
|
|
1150
|
+
<button type="button" id="agent-close" class="btn icon" title="cerrar" aria-label="cerrar">
|
|
1151
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
1152
|
+
<path d="M18 6 6 18M6 6l12 12" />
|
|
1153
|
+
</svg>
|
|
1154
|
+
</button>
|
|
1155
|
+
</div>
|
|
1156
|
+
<div class="row">
|
|
1157
|
+
<label class="field"><span>id</span><input id="af-id" spellcheck="false" placeholder="revisor" /></label>
|
|
1158
|
+
<label class="field grow"><span>nombre</span><input id="af-name" spellcheck="false" placeholder="Revisor" /></label>
|
|
1159
|
+
</div>
|
|
1160
|
+
<label class="field"><span>descripción (cuándo delegarle)</span><input id="af-description" spellcheck="false" placeholder="Revisa cambios y busca bugs" /></label>
|
|
1161
|
+
<div class="row">
|
|
1162
|
+
<label class="field"><span>proveedor (opcional)</span>
|
|
1163
|
+
<select id="af-provider"><option value="">heredar</option></select>
|
|
1164
|
+
</label>
|
|
1165
|
+
<label class="field grow"><span>modelo (opcional)</span><input id="af-model" spellcheck="false" placeholder="heredar" /></label>
|
|
1166
|
+
</div>
|
|
1167
|
+
<div class="row">
|
|
1168
|
+
<label class="field"><span>modo</span>
|
|
1169
|
+
<select id="af-mode">
|
|
1170
|
+
<option value="">heredar (plan)</option>
|
|
1171
|
+
<option value="plan">plan (solo lectura)</option>
|
|
1172
|
+
<option value="agent">agent (escritura)</option>
|
|
1173
|
+
<option value="voice">voice (.md / .json)</option>
|
|
1174
|
+
</select>
|
|
1175
|
+
</label>
|
|
1176
|
+
<label class="field"><span>esfuerzo</span>
|
|
1177
|
+
<select id="af-effort">
|
|
1178
|
+
<option value="">heredar</option>
|
|
1179
|
+
<option value="off">off</option>
|
|
1180
|
+
<option value="low">low</option>
|
|
1181
|
+
<option value="medium">medium</option>
|
|
1182
|
+
<option value="high">high</option>
|
|
1183
|
+
<option value="max">max</option>
|
|
1184
|
+
</select>
|
|
1185
|
+
</label>
|
|
1186
|
+
<label class="field"><span>max rondas</span><input id="af-rounds" type="number" min="0" placeholder="6" /></label>
|
|
1187
|
+
</div>
|
|
1188
|
+
<label class="field"><span>tools (coma · vacío = las del modo)</span><input id="af-tools" spellcheck="false" placeholder="read, grep, glob" /></label>
|
|
1189
|
+
<label class="field"><span>skills (Ctrl/Cmd para elegir varias · vacío = todas)</span>
|
|
1190
|
+
<select id="af-skills" multiple size="5"></select>
|
|
1191
|
+
</label>
|
|
1192
|
+
<label class="field"><span>instrucciones del subagente (markdown)</span>
|
|
1193
|
+
<textarea id="af-body" rows="8" spellcheck="false" placeholder="Cómo encarar las tareas que le deleguen…"></textarea>
|
|
1194
|
+
</label>
|
|
1195
|
+
<div class="row between settings-actions">
|
|
1196
|
+
<button type="button" id="af-cancel" class="btn">cancelar</button>
|
|
1197
|
+
<button type="submit" class="btn primary">guardar</button>
|
|
1198
|
+
</div>
|
|
1199
|
+
<div id="af-status" class="summary"></div>
|
|
1200
|
+
</form>
|
|
1201
|
+
</dialog>
|
|
1202
|
+
|
|
1114
1203
|
<dialog id="memory-modal" class="modal">
|
|
1115
1204
|
<form id="memory-form" class="modal-card">
|
|
1116
1205
|
<div class="row between">
|
|
@@ -52,6 +52,7 @@ export function registerSession(session, messages = [], tokens = { in: 0, out: 0
|
|
|
52
52
|
provider: session.provider || '',
|
|
53
53
|
model: session.model || '',
|
|
54
54
|
mode: session.mode || '',
|
|
55
|
+
effort: session.effort || '',
|
|
55
56
|
working: false,
|
|
56
57
|
tokens,
|
|
57
58
|
lastTurn: { prompt: 0, completion: 0 },
|