@danieltmn/tamnora 0.25.0 → 0.26.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.
Files changed (110) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +13 -25
  3. package/SECURITY.md +3 -4
  4. package/harness/packages/cli/src/cli.js +0 -3
  5. package/harness/packages/cli/src/commands/bridge.js +0 -1
  6. package/harness/packages/cli/src/commands/repl.js +20 -4
  7. package/harness/packages/cli/src/commands/run.js +6 -2
  8. package/harness/packages/cli/src/commands/server.js +1 -1
  9. package/harness/packages/cli/src/commands/tui.js +6 -2
  10. package/harness/packages/cli/src/lib/runtime.js +2 -0
  11. package/harness/packages/cli/src/lib/util.js +2 -2
  12. package/harness/packages/cli/src/tui/app.js +24 -4
  13. package/harness/packages/cli/src/tui/layout.js +1 -1
  14. package/harness/packages/cli/test/tunnel.test.js +5 -32
  15. package/harness/packages/core/src/agent/runAgent.js +5 -2
  16. package/harness/packages/core/src/agents/index.js +113 -0
  17. package/harness/packages/core/src/config/app.js +0 -4
  18. package/harness/packages/core/src/config/home.js +1 -20
  19. package/harness/packages/core/src/config/index.js +6 -8
  20. package/harness/packages/core/src/extensions/load.js +22 -0
  21. package/harness/packages/core/src/frontmatter.js +32 -0
  22. package/harness/packages/core/src/hooks/index.js +2 -16
  23. package/harness/packages/core/src/index.js +12 -14
  24. package/harness/packages/core/src/plugins/index.js +2 -16
  25. package/harness/packages/core/src/procs/project.js +68 -0
  26. package/harness/packages/core/src/procs/static.js +17 -0
  27. package/harness/packages/core/src/providers/index.js +3 -1
  28. package/harness/packages/core/src/providers/ollama.js +4 -1
  29. package/harness/packages/core/src/providers/openaiCompatible.js +6 -1
  30. package/harness/packages/core/src/providers/openrouter.js +5 -1
  31. package/harness/packages/core/src/providers/presets.js +4 -3
  32. package/harness/packages/core/src/providers/reasoning.js +58 -0
  33. package/harness/packages/core/src/rules/index.js +8 -27
  34. package/harness/packages/core/src/skills/index.js +8 -19
  35. package/harness/packages/core/src/storage/sqlite.js +595 -690
  36. package/harness/packages/core/src/tunnel.js +9 -147
  37. package/harness/packages/core/test/agents.test.js +111 -0
  38. package/harness/packages/core/test/home.test.js +2 -35
  39. package/harness/packages/core/test/procs.test.js +3 -64
  40. package/harness/packages/core/test/reasoning.test.js +43 -0
  41. package/harness/packages/core/test/storage.test.js +1 -12
  42. package/harness/packages/server/public/app.js +39 -19
  43. package/harness/packages/server/public/appearance.js +2 -56
  44. package/harness/packages/server/public/boot.js +2 -2
  45. package/harness/packages/server/public/cards.js +2 -21
  46. package/harness/packages/server/public/chat.js +1 -16
  47. package/harness/packages/server/public/chatFormat.js +0 -11
  48. package/harness/packages/server/public/chrome.js +95 -5
  49. package/harness/packages/server/public/commands.js +1 -2
  50. package/harness/packages/server/public/core.js +1 -1
  51. package/harness/packages/server/public/explorer.js +7 -10
  52. package/harness/packages/server/public/icons (1).zip +0 -0
  53. package/harness/packages/server/public/index.html +1228 -1237
  54. package/harness/packages/server/public/landing/boot.js +2 -3
  55. package/harness/packages/server/public/landing/index.html +3 -367
  56. package/harness/packages/server/public/sessions.js +1 -0
  57. package/harness/packages/server/public/settings.js +1255 -1155
  58. package/harness/packages/server/public/settingsNav.js +1 -1
  59. package/harness/packages/server/public/styles.css +2341 -2867
  60. package/harness/packages/server/public/system.js +356 -495
  61. package/harness/packages/server/public/voice.js +6 -22
  62. package/harness/packages/server/src/auth.js +4 -15
  63. package/harness/packages/server/src/cli.js +1 -1
  64. package/harness/packages/server/src/lib/agents.js +60 -0
  65. package/harness/packages/server/src/lib/appearance.js +3 -22
  66. package/harness/packages/server/src/lib/channels/telegram/commands.js +1 -1
  67. package/harness/packages/server/src/lib/channels/telegram/run.js +1 -0
  68. package/harness/packages/server/src/lib/definitions.js +40 -0
  69. package/harness/packages/server/src/lib/prompt.js +11 -3
  70. package/harness/packages/server/src/lib/scheduler.js +4 -0
  71. package/harness/packages/server/src/lib/sessionView.js +1 -0
  72. package/harness/packages/server/src/lib/skills.js +27 -21
  73. package/harness/packages/server/src/lib/tasks.js +71 -23
  74. package/harness/packages/server/src/lib/ttsSettings.js +2 -2
  75. package/harness/packages/server/src/lib/workspace.js +9 -11
  76. package/harness/packages/server/src/routes/agents.js +88 -0
  77. package/harness/packages/server/src/routes/chat.js +9 -44
  78. package/harness/packages/server/src/routes/preview.js +5 -22
  79. package/harness/packages/server/src/routes/procs.js +2 -4
  80. package/harness/packages/server/src/routes/providers.js +7 -0
  81. package/harness/packages/server/src/routes/sessions.js +3 -1
  82. package/harness/packages/server/src/routes/tts.js +1 -1
  83. package/harness/packages/server/src/server.js +8 -5
  84. package/harness/packages/server/test/agents.test.js +194 -0
  85. package/harness/packages/server/test/appearance-route.test.js +3 -3
  86. package/harness/packages/server/test/appearance.test.js +12 -38
  87. package/harness/packages/server/test/chatFormat.test.js +0 -8
  88. package/harness/packages/server/test/consoleTabs.test.js +2 -2
  89. package/harness/packages/server/test/ops.test.js +8 -56
  90. package/harness/packages/server/test/preview.test.js +2 -42
  91. package/harness/packages/server/test/security.test.js +194 -370
  92. package/harness/packages/server/test/settingsNav.test.js +2 -2
  93. package/harness/packages/server/test/tts.test.js +4 -4
  94. package/harness/tamnora.example.json +2 -1
  95. package/package.json +2 -2
  96. package/harness/packages/cli/src/commands/eval.js +0 -186
  97. package/harness/packages/core/src/eval/dataset.js +0 -47
  98. package/harness/packages/core/src/eval/judge.js +0 -39
  99. package/harness/packages/core/src/eval/report.js +0 -48
  100. package/harness/packages/core/src/eval/runTask.js +0 -62
  101. package/harness/packages/core/src/eval/runner.js +0 -153
  102. package/harness/packages/core/src/eval/scorers.js +0 -217
  103. package/harness/packages/core/src/eval/task.js +0 -59
  104. package/harness/packages/core/src/procs/detect.js +0 -149
  105. package/harness/packages/core/test/eval.test.js +0 -144
  106. package/harness/packages/server/src/lib/advisor.js +0 -198
  107. package/harness/packages/server/src/routes/advisor.js +0 -79
  108. package/harness/packages/server/src/routes/eval.js +0 -100
  109. package/harness/packages/server/test/advisor.test.js +0 -249
  110. package/harness/scripts/example-dataset.jsonl +0 -4
@@ -4,22 +4,9 @@
4
4
 
5
5
  export const THEMES = [
6
6
  { id: 'paper', label: 'Papel', scheme: 'light', metaColor: '#f5efe2' },
7
- { id: 'dark', label: 'Oscuro', scheme: 'dark', metaColor: '#101107' },
8
- { id: 'midnight', label: 'Medianoche', scheme: 'dark', metaColor: '#0b1220' },
9
- { id: 'nord', label: 'Nord', scheme: 'dark', metaColor: '#2e3440' },
10
- { id: 'dracula', label: 'Dracula', scheme: 'dark', metaColor: '#282a36' },
11
- { id: 'solarized', label: 'Solarized', scheme: 'light', metaColor: '#fdf6e3' },
12
- { id: 'solarized-dark', label: 'Solarized oscuro', scheme: 'dark', metaColor: '#002b36' },
13
- { id: 'bosque', label: 'Bosque', scheme: 'light', metaColor: '#f2f4ec' },
14
- { id: 'vino', label: 'Vino', scheme: 'light', metaColor: '#f7eeee' },
15
- { id: 'arena', label: 'Arena', scheme: 'light', metaColor: '#f6efdd' },
16
- { id: 'hielo', label: 'Hielo', scheme: 'light', metaColor: '#edf3f7' },
17
- { id: 'vercel', label: 'Vercel', scheme: 'dark', metaColor: '#000000' },
18
- { id: 'dark-plus', label: 'Dark+', scheme: 'dark', metaColor: '#1e1e1e' },
19
7
  { id: 'github', label: 'GitHub', scheme: 'light', metaColor: '#ffffff' },
20
- { id: 'github-dark', label: 'GitHub oscuro', scheme: 'dark', metaColor: '#0d1117' },
8
+ { id: 'dark', label: 'Oscuro', scheme: 'dark', metaColor: '#101107' },
21
9
  { id: 'mocha', label: 'Mocha', scheme: 'dark', metaColor: '#211912' },
22
- { id: 'ebano', label: 'Ébano', scheme: 'dark', metaColor: '#14191f' },
23
10
  ];
24
11
 
25
12
  export const FONT_PRESETS = [
@@ -39,46 +26,6 @@ export const FONT_PRESETS = [
39
26
  mono: 'ui-monospace, "Cascadia Code", "SF Mono", Consolas, monospace',
40
27
  google: null,
41
28
  },
42
- {
43
- id: 'plex',
44
- label: 'IBM Plex · Sans / Serif / Mono',
45
- sans: '"IBM Plex Sans", system-ui, "Segoe UI", Arial, sans-serif',
46
- serif: '"IBM Plex Serif", Georgia, "Times New Roman", serif',
47
- mono: '"IBM Plex Mono", ui-monospace, Consolas, monospace',
48
- google: 'family=IBM+Plex+Mono:wght@400;500&family=IBM+Plex+Sans:wght@400;500;600;700&family=IBM+Plex+Serif:wght@400;600',
49
- },
50
- {
51
- id: 'inter',
52
- label: 'Inter · Inter / Newsreader / JetBrains Mono',
53
- sans: 'Inter, system-ui, "Segoe UI", Arial, sans-serif',
54
- serif: 'Newsreader, Georgia, "Times New Roman", serif',
55
- mono: '"JetBrains Mono", ui-monospace, Consolas, monospace',
56
- google: 'family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&family=Newsreader:wght@400;600',
57
- },
58
- {
59
- id: 'jetbrains',
60
- label: 'JetBrains Mono · código',
61
- sans: '"JetBrains Mono", ui-monospace, "Cascadia Code", Consolas, monospace',
62
- serif: 'Georgia, "Times New Roman", serif',
63
- mono: '"JetBrains Mono", ui-monospace, "Cascadia Code", Consolas, monospace',
64
- google: 'family=JetBrains+Mono:wght@400;500;700',
65
- },
66
- {
67
- id: 'fira',
68
- label: 'Fira Code · código',
69
- sans: '"Fira Code", ui-monospace, "Cascadia Code", Consolas, monospace',
70
- serif: 'Georgia, "Times New Roman", serif',
71
- mono: '"Fira Code", ui-monospace, "Cascadia Code", Consolas, monospace',
72
- google: 'family=Fira+Code:wght@400;500;600;700',
73
- },
74
- {
75
- id: 'source',
76
- label: 'Source Code Pro · código',
77
- sans: '"Source Code Pro", ui-monospace, "Cascadia Code", Consolas, monospace',
78
- serif: 'Georgia, "Times New Roman", serif',
79
- mono: '"Source Code Pro", ui-monospace, "Cascadia Code", Consolas, monospace',
80
- google: 'family=Source+Code+Pro:wght@400;500;600;700',
81
- },
82
29
  ];
83
30
 
84
31
  export const DEFAULTS = { theme: 'paper', font: 'clasico', chatFont: 14.5, scale: 1 };
@@ -116,8 +63,7 @@ export function fontIds() {
116
63
  }
117
64
 
118
65
  export function resolveTheme(id) {
119
- const legacy = id === 'light' ? 'paper' : id;
120
- return THEMES.some((t) => t.id === legacy) ? legacy : DEFAULTS.theme;
66
+ return THEMES.some((t) => t.id === id) ? id : DEFAULTS.theme;
121
67
  }
122
68
 
123
69
  export function resolveFont(id) {
@@ -1,7 +1,7 @@
1
1
  (function () {
2
2
  var root = document.documentElement;
3
- var THEMES = ['paper', 'dark', 'midnight', 'nord', 'dracula', 'solarized', 'solarized-dark', 'bosque', 'vino', 'arena', 'hielo', 'vercel', 'dark-plus', 'github', 'github-dark', 'mocha', 'ebano'];
4
- var DARK = ['dark', 'midnight', 'nord', 'dracula', 'solarized-dark', 'vercel', 'dark-plus', 'github-dark', 'mocha', 'ebano'];
3
+ var THEMES = ['paper', 'github', 'dark', 'mocha'];
4
+ var DARK = ['dark', 'mocha'];
5
5
  var appearance = null;
6
6
  try {
7
7
  appearance = JSON.parse(localStorage.getItem('tamnora-console-appearance') || 'null');
@@ -1,10 +1,10 @@
1
1
  // Tarjetas de la consola: las que dibujan una herramienta, un permiso, una
2
- // pregunta, un plan o el comentario del asesor.
2
+ // pregunta o un plan.
3
3
 
4
4
  import { api, button, el, icon, state } from './core.js';
5
5
  import { toast } from './commands.js';
6
6
  import { addMessage, chatInput, paneOf, scrollMessages, sendMessage } from './chat.js';
7
- import { ADVISOR_KINDS, advisorMeta, isFailedToolText, isReadOnlyTool, screenshotName, toolSummary } from './chatFormat.js';
7
+ import { isFailedToolText, isReadOnlyTool, screenshotName, toolSummary } from './chatFormat.js';
8
8
  import { renderMarkdown } from './markdown.js';
9
9
  import { summarizeArgs } from './reconcile.js';
10
10
  import { yesInput } from './chrome.js';
@@ -409,25 +409,6 @@ export function addPlanCard(container, { title, steps } = {}) {
409
409
  return card;
410
410
  }
411
411
 
412
- export function addAdvisorCard(container, { text, kind } = {}) {
413
- const clean = String(text ?? '').trim();
414
- if (!clean || !container) return null;
415
- const exists = [...container.querySelectorAll('.advisor-text')].some((node) => node.textContent === clean);
416
- if (exists) return null;
417
- const meta = advisorMeta(kind);
418
- const klass = ADVISOR_KINDS[kind] ? ` advisor-${kind}` : '';
419
- const card = el('div', `advisor-card${klass}`);
420
- const head = el('div', 'advisor-head');
421
- head.append(
422
- el('span', 'advisor-glyph', meta.glyph),
423
- el('span', 'advisor-title', 'asesor'),
424
- el('span', `advisor-badge advisor-badge-${kind || 'advice'}`, meta.label)
425
- );
426
- card.append(head, el('div', 'advisor-text', clean));
427
- container.appendChild(card);
428
- return card;
429
- }
430
-
431
412
  /**
432
413
  * Marca del punto de compactación: una línea que corta el historial y, debajo,
433
414
  * el resumen que pasa a ser el contexto del modelo. Lo de arriba sigue visible
@@ -13,9 +13,7 @@ import {
13
13
  } from './chrome.js';
14
14
  import { hideSlashMenu, notify, onChatInputChanged, runCommand, toast } from './commands.js';
15
15
  export {
16
- ADVISOR_KINDS,
17
16
  READ_ONLY_TOOLS,
18
- advisorMeta,
19
17
  basename,
20
18
  formatDuration,
21
19
  isFailedToolText,
@@ -53,7 +51,6 @@ import { setStatus } from './settings.js';
53
51
  import { reconcile, renderAlertSignals } from './system.js';
54
52
  import { narrar, reproducirVoz } from './voice.js';
55
53
  import {
56
- addAdvisorCard,
57
54
  addChoiceCard,
58
55
  addCompactCard,
59
56
  addPermissionCard,
@@ -69,7 +66,6 @@ import {
69
66
  toolCardIndex,} from './cards.js';
70
67
  export {
71
68
  PLAN_MARKS,
72
- addAdvisorCard,
73
69
  addChoiceCard,
74
70
  addCompactCard,
75
71
  addPermissionCard,
@@ -427,13 +423,6 @@ export function renderStored(message, pane) {
427
423
  addCompactCard(pane, { text: typeof message.content === 'string' ? message.content : '' });
428
424
  return;
429
425
  }
430
- if (message.role === 'assistant' && message.name === 'advisor') {
431
- addAdvisorCard(pane, {
432
- text: typeof message.content === 'string' ? message.content : '',
433
- kind: message.mode,
434
- });
435
- return;
436
- }
437
426
  if (message.role === 'user') {
438
427
  const bubble = addMessage('user', userText(message.content), pane);
439
428
  if (message.id) {
@@ -775,11 +764,6 @@ export function addContinueButton(turn, data) {
775
764
  export function handleChatEvent(event, data, turn) {
776
765
  if (handleAgentEvent(event, data, turn)) return;
777
766
  narrar(event, data, turn);
778
- if (event === 'advice') {
779
- addAdvisorCard(turn.pane, { text: data?.text, kind: data?.kind });
780
- scrollMessages(turn.pane);
781
- return;
782
- }
783
767
  if (event === 'agent_start') {
784
768
  turn.agents.set(data.agentId, addAgentCard(turn.assistant(), data));
785
769
  } else if (event === 'agent_end') {
@@ -1080,6 +1064,7 @@ export async function startTurn(sessionKey, pane, input, images, opts = {}) {
1080
1064
  provider: owner?.provider || providerSelect.value || undefined,
1081
1065
  model: owner?.model || modelSelect.value || undefined,
1082
1066
  mode: owner?.mode || modeSelect.value || undefined,
1067
+ effort: owner?.effort ?? undefined,
1083
1068
  yes: yesInput.checked ? true : undefined,
1084
1069
  contextTokens: contextTokensForRequest(),
1085
1070
  interrupt: opts.interrupt ? true : undefined,
@@ -131,17 +131,6 @@ export function userImages(content) {
131
131
  .filter(Boolean);
132
132
  }
133
133
 
134
- export const ADVISOR_KINDS = {
135
- code: { label: 'Cuidado!', glyph: '⚠' },
136
- explain: { label: 'Importante!', glyph: 'ℹ' },
137
- advice: { label: 'Consejo', glyph: '✦' },
138
- summary: { label: 'Resumen', glyph: '✎' },
139
- };
140
-
141
- export function advisorMeta(kind) {
142
- return ADVISOR_KINDS[kind] || { label: 'Asesor', glyph: '✦' };
143
- }
144
-
145
134
  /** Ultimo segmento de un path (sin separadores finales). */
146
135
  export function basename(p) {
147
136
  const text = String(p ?? '').replace(/[\\/]+$/, '');
@@ -16,7 +16,7 @@ import { pickVisible } from './modelsQuery.js';
16
16
  import { activateSession, registerSession } from './sessions.js';
17
17
  import {
18
18
  activateAjustesPanel,
19
- refreshAsesor,
19
+ refreshAgents,
20
20
  refreshChannels,
21
21
  refreshLimits,
22
22
  refreshMcp,
@@ -30,7 +30,6 @@ import {
30
30
  setAjustesLevel,
31
31
  setStatus,
32
32
  } from './settings.js';
33
- import { refreshRuns } from './system.js';
34
33
  import { refreshTranscribe, refreshVoz, setHandsFree } from './voice.js';
35
34
 
36
35
  export function setCwd(value) {
@@ -48,7 +47,7 @@ export const moreMenu = $('#more-menu');
48
47
  export const topbarSection = $('#topbar-section');
49
48
  export const backChat = $('#back-chat');
50
49
 
51
- export const TAB_LABELS = { chat: 'chat', providers: 'proveedores', ajustes: 'ajustes', eval: 'eval', runs: 'runs' };
50
+ export const TAB_LABELS = { chat: 'chat', providers: 'proveedores', ajustes: 'ajustes' };
52
51
 
53
52
  export function hideMoreMenu() {
54
53
  if (moreMenu.hidden) return;
@@ -67,9 +66,8 @@ export function activateTab(name) {
67
66
  moreTab.classList.toggle('active', name !== 'chat');
68
67
  document.querySelectorAll('.view').forEach((v) => v.classList.toggle('active', v.id === `view-${name}`));
69
68
  hideMoreMenu();
70
- if (name === 'runs') refreshRuns();
71
69
  if (name === 'providers') refreshProviders();
72
- if (name === 'ajustes') { setAjustesLevel('list'); refreshMcp(); refreshMcpCatalog(); refreshSkills(); refreshSkillCatalog(); refreshMemory(); refreshSchedules(); refreshChannels(); refreshLimits(); refreshVoz(); refreshAsesor(); refreshTranscribe(); refreshProfile(); }
70
+ if (name === 'ajustes') { setAjustesLevel('list'); refreshMcp(); refreshMcpCatalog(); refreshSkills(); refreshSkillCatalog(); refreshAgents(); refreshMemory(); refreshSchedules(); refreshChannels(); refreshLimits(); refreshVoz(); refreshTranscribe(); refreshProfile(); }
73
71
  }
74
72
 
75
73
  /* --- settings modal (modo / proveedor / modelo / auto) -------------------- */
@@ -122,6 +120,8 @@ export async function loadModes() {
122
120
  const active = settings?.settings?.mode || 'agent';
123
121
  if ([...modeSelect.options].some((o) => o.value === active)) modeSelect.value = active;
124
122
  if (strictInput) strictInput.checked = settings?.settings?.strictScope === true;
123
+ state.active.effort = settings?.settings?.effort ?? '';
124
+ renderEffortPill();
125
125
  defaultCwdCache = settings?.settings?.defaultCwd || '';
126
126
  if (!defaultCwdCache) {
127
127
  // Migración: la carpeta predeterminada vivía en localStorage; la subimos al
@@ -161,6 +161,19 @@ export const modePillLabel = $('#mode-pill-label');
161
161
  export const modeMenu = $('#mode-menu');
162
162
  export const modelPill = $('#model-pill');
163
163
  export const modelPillLabel = $('#model-pill-label');
164
+ export const effortPill = $('#effort-pill');
165
+ export const effortPillLabel = $('#effort-pill-label');
166
+ export const effortMenu = $('#effort-menu');
167
+ export const effortSelect = $('#effort');
168
+
169
+ const EFFORT_CHOICES = [
170
+ { value: '', label: 'auto' },
171
+ { value: 'off', label: 'off' },
172
+ { value: 'low', label: 'low' },
173
+ { value: 'medium', label: 'medium' },
174
+ { value: 'high', label: 'high' },
175
+ { value: 'max', label: 'max' },
176
+ ];
164
177
 
165
178
  export function renderCwdPill() {
166
179
  if (!cwdPillLabel) return;
@@ -208,8 +221,82 @@ export function closeModeMenu() {
208
221
  modePill?.setAttribute('aria-expanded', 'false');
209
222
  }
210
223
 
224
+ /* --- esfuerzo de razonamiento -------------------------------------------- */
225
+
226
+ /** Esfuerzo de la sesión abierta; si no eligió, el default global. */
227
+ export function effortValue(record = state.panes.get(state.sessionId)) {
228
+ if (record && record.effort !== undefined) return record.effort;
229
+ return state.active.effort ?? '';
230
+ }
231
+
232
+ function checkIcon() {
233
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
234
+ svg.setAttribute('class', 'ctrl-check');
235
+ svg.setAttribute('viewBox', '0 0 24 24');
236
+ svg.setAttribute('fill', 'none');
237
+ svg.setAttribute('stroke', 'currentColor');
238
+ svg.setAttribute('stroke-width', '2.2');
239
+ svg.setAttribute('stroke-linecap', 'round');
240
+ svg.setAttribute('stroke-linejoin', 'round');
241
+ svg.setAttribute('aria-hidden', 'true');
242
+ const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
243
+ path.setAttribute('d', 'M20 6 9 17l-5-5');
244
+ svg.appendChild(path);
245
+ return svg;
246
+ }
247
+
248
+ function buildEffortMenu() {
249
+ if (!effortMenu) return;
250
+ clear(effortMenu);
251
+ for (const choice of EFFORT_CHOICES) {
252
+ const item = document.createElement('button');
253
+ item.type = 'button';
254
+ item.className = 'ctrl-menu-item';
255
+ item.dataset.effort = choice.value;
256
+ item.setAttribute('role', 'menuitemradio');
257
+ item.setAttribute('aria-checked', 'false');
258
+ item.appendChild(el('span', '', choice.label));
259
+ item.appendChild(checkIcon());
260
+ effortMenu.appendChild(item);
261
+ }
262
+ }
263
+ buildEffortMenu();
264
+
265
+ export function renderEffortPill() {
266
+ const value = effortValue();
267
+ if (effortPillLabel) effortPillLabel.textContent = value || 'auto';
268
+ if (effortPill) {
269
+ effortPill.title = value
270
+ ? `esfuerzo de razonamiento: ${value}`
271
+ : 'esfuerzo de razonamiento: auto (default del modelo)';
272
+ }
273
+ if (effortSelect && effortSelect.value !== value) effortSelect.value = value;
274
+ if (!effortMenu) return;
275
+ for (const item of effortMenu.querySelectorAll('.ctrl-menu-item')) {
276
+ const active = (item.dataset.effort || '') === value;
277
+ item.classList.toggle('active', active);
278
+ item.setAttribute('aria-checked', active ? 'true' : 'false');
279
+ }
280
+ }
281
+
282
+ export function closeEffortMenu() {
283
+ if (!effortMenu || effortMenu.hidden) return;
284
+ effortMenu.hidden = true;
285
+ effortPill?.setAttribute('aria-expanded', 'false');
286
+ }
287
+
288
+ export async function setEffort(value, { persist = true } = {}) {
289
+ const next = EFFORT_CHOICES.some((choice) => choice.value === value) ? value : '';
290
+ const record = state.panes.get(state.sessionId);
291
+ if (record) record.effort = next;
292
+ state.active.effort = next;
293
+ renderEffortPill();
294
+ if (persist) await persistAgent({ effort: next });
295
+ }
296
+
211
297
  export function updateStatus(extra = {}) {
212
298
  renderModePill();
299
+ renderEffortPill();
213
300
  renderModelPill();
214
301
  if (extra.rounds !== undefined) $('#sb-rounds').textContent = `${extra.rounds} rondas`;
215
302
  if (extra.stopReason) $('#sb-rounds').textContent = extra.stopReason;
@@ -492,6 +579,7 @@ export async function saveSettings(patch) {
492
579
  if (out?.settings) {
493
580
  state.active.provider = out.settings.provider;
494
581
  state.active.model = out.settings.model;
582
+ if ('effort' in patch) state.active.effort = patch.effort ?? '';
495
583
  }
496
584
  if (patch.provider) providerSelect.value = patch.provider;
497
585
  updateStatus();
@@ -508,6 +596,7 @@ export async function persistAgent(patch) {
508
596
  if (patch.provider) record.provider = patch.provider;
509
597
  if (patch.model) record.model = patch.model;
510
598
  if (patch.mode) record.mode = patch.mode;
599
+ if (patch.effort !== undefined) record.effort = patch.effort;
511
600
  api(`/api/sessions/${record.id}`, { method: 'PUT', body: patch }).catch(() => null);
512
601
  }
513
602
  await saveSettings(patch);
@@ -532,6 +621,7 @@ export async function applySessionAgent(record) {
532
621
  if (mode !== 'voice') setHandsFree(false);
533
622
  updateStatus();
534
623
  renderModePill();
624
+ renderEffortPill();
535
625
  renderModelPill();
536
626
  }
537
627
 
@@ -218,8 +218,7 @@ export async function runCommand(name, arg) {
218
218
  notify(`Carpeta de trabajo: \`${cwd}\``);
219
219
  return { handled: true };
220
220
  }
221
- case 'portkill':
222
- case 'kill': {
221
+ case 'portkill': {
223
222
  const port = Number(arg);
224
223
  if (!Number.isInteger(port) || port < 1 || port > 65535) {
225
224
  notify('Uso: `/portkill <puerto>`, por ejemplo `/portkill 3000`.', 'notice err');
@@ -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.
@@ -245,7 +245,7 @@ export function setExplorerMode(mode) {
245
245
  export function refreshExplorerMode() {
246
246
  if (!state.explorerOpen) return;
247
247
  if (explorerMode === 'files') refreshExplorer();
248
- else if (explorerMode === 'procs') detectProcs();
248
+ else if (explorerMode === 'procs') loadProcChips();
249
249
  else if (explorerMode === 'git') refreshGit();
250
250
  else if (explorerMode === 'preview') syncPreviewTunnel();
251
251
  }
@@ -349,9 +349,9 @@ export function loadPreview(url) {
349
349
  setPreviewStatus(`cargando ${value} …`);
350
350
  }
351
351
 
352
- /** Abre acceso remoto al puerto (tailscale privado o tunel publico). */
352
+ /** Abre acceso remoto al puerto en el tailnet (privado). */
353
353
  export async function openRemoteTunnel(port) {
354
- const provider = $('#preview-provider')?.value || 'tailscale';
354
+ const provider = 'tailscale';
355
355
  setPreviewStatus(`abriendo acceso remoto (${provider})…`);
356
356
  const data = await api('/api/preview/tunnel', { method: 'POST', body: { port, provider } }).catch((e) => ({ error: e.message }));
357
357
  if (data?.error) {
@@ -366,7 +366,7 @@ export async function openRemoteTunnel(port) {
366
366
  loadPreview(data.tunnel.url);
367
367
  }
368
368
 
369
- /* procs: deteccion y dev servers */
369
+ /* procs: dev servers declarados por el proyecto */
370
370
 
371
371
  export const procsListEl = $('#procs-list');
372
372
  export const procsChipsEl = $('#procs-chips');
@@ -374,7 +374,7 @@ export const procsCwdEl = $('#procs-cwd');
374
374
  export const procsLogEl = $('#procs-log');
375
375
  export let selectedProc = null;
376
376
 
377
- export async function detectProcs() {
377
+ export async function loadProcChips() {
378
378
  clear(procsChipsEl);
379
379
  const cwd = state.cwd || '';
380
380
  procsCwdEl.textContent = cwd ? `cwd: ${cwd}` : 'sin carpeta';
@@ -382,17 +382,14 @@ export async function detectProcs() {
382
382
  if (!data) return;
383
383
  for (const chip of data.chips) {
384
384
  const node = button(chip.label, () => startProc(chip), 'xchip');
385
- if (chip.source === 'project') node.title = 'definido en .tamnora/procs.json';
385
+ node.title = 'definido en .tamnora/procs.json';
386
386
  if (chip.allowed === false) {
387
387
  node.disabled = true;
388
388
  node.title = `"${chip.command}" no está en processes.allow`;
389
389
  }
390
390
  procsChipsEl.appendChild(node);
391
391
  }
392
- if (!data.chips.length) procsChipsEl.appendChild(el('span', 'muted', 'no detecté comandos de dev'));
393
- else if (data.chips.some((c) => c.source === 'project')) {
394
- procsChipsEl.appendChild(el('span', 'muted', '· desde .tamnora/procs.json'));
395
- }
392
+ if (!data.chips.length) procsChipsEl.appendChild(el('span', 'muted', 'no hay procesos definidos en .tamnora/procs.json'));
396
393
  await refreshProcs();
397
394
  }
398
395