@danieltmn/tamnora 0.24.2 → 0.25.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 (49) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/harness/packages/server/public/app.js +831 -834
  3. package/harness/packages/server/public/appearance.js +4 -0
  4. package/harness/packages/server/public/boot.js +49 -0
  5. package/harness/packages/server/public/chrome.js +853 -852
  6. package/harness/packages/server/public/commands.js +448 -405
  7. package/harness/packages/server/public/core.js +229 -216
  8. package/harness/packages/server/public/favicon.ico +0 -0
  9. package/harness/packages/server/public/icons/icon-128x128.png +0 -0
  10. package/harness/packages/server/public/icons/icon-144x144.png +0 -0
  11. package/harness/packages/server/public/icons/icon-152x152.png +0 -0
  12. package/harness/packages/server/public/icons/icon-192x192.png +0 -0
  13. package/harness/packages/server/public/icons/icon-384x384.png +0 -0
  14. package/harness/packages/server/public/icons/icon-512x512.png +0 -0
  15. package/harness/packages/server/public/icons/icon-72x72.png +0 -0
  16. package/harness/packages/server/public/icons/icon-96x96.png +0 -0
  17. package/harness/packages/server/public/icons/logo.png +0 -0
  18. package/harness/packages/server/public/icons/logo_dark.png +0 -0
  19. package/harness/packages/server/public/icons/logo_light.png +0 -0
  20. package/harness/packages/server/public/icons/tamnora.ico +0 -0
  21. package/harness/packages/server/public/index.html +72 -101
  22. package/harness/packages/server/public/landing/boot.js +16 -0
  23. package/harness/packages/server/public/landing/index.html +123 -59
  24. package/harness/packages/server/public/landing/landing.js +11 -9
  25. package/harness/packages/server/public/logo.png +0 -0
  26. package/harness/packages/server/public/markdown.js +1 -1
  27. package/harness/packages/server/public/settings.js +1155 -1080
  28. package/harness/packages/server/public/settingsNav.js +81 -0
  29. package/harness/packages/server/public/styles.css +2807 -2525
  30. package/harness/packages/server/public/system.js +2 -2
  31. package/harness/packages/server/src/lib/appearance.js +4 -0
  32. package/harness/packages/server/src/lib/restart.js +20 -0
  33. package/harness/packages/server/src/lib/securityHeaders.js +53 -0
  34. package/harness/packages/server/src/routes/reload.js +46 -0
  35. package/harness/packages/server/src/server.js +6 -1
  36. package/harness/packages/server/test/accessibility.test.js +72 -0
  37. package/harness/packages/server/test/appearance.test.js +4 -0
  38. package/harness/packages/server/test/consoleModules.test.js +133 -57
  39. package/harness/packages/server/test/consoleTabs.test.js +228 -0
  40. package/harness/packages/server/test/docsSync.test.js +73 -0
  41. package/harness/packages/server/test/landingLogin.test.js +89 -0
  42. package/harness/packages/server/test/markdownSecurity.test.js +80 -0
  43. package/harness/packages/server/test/reload.test.js +95 -0
  44. package/harness/packages/server/test/securityHeaders.test.js +109 -0
  45. package/harness/packages/server/test/settingsNav.test.js +90 -0
  46. package/harness/packages/server/test/themeCss.js +110 -0
  47. package/harness/packages/server/test/themes.test.js +72 -0
  48. package/harness/scripts/restart.mjs +101 -0
  49. package/package.json +1 -1
@@ -1,838 +1,835 @@
1
- // Tamnora console — dependency-free client for the harness server (terminal theme).
2
-
3
- import {
4
- activePane,
5
- addImageFiles,
6
- attachInput,
7
- chatForm,
8
- chatInput,
9
- closeMessageMenus,
10
- controllers,
11
- interruptBtn,
12
- openMessageMenu,
13
- panesEl,
14
- positionMessageMenu,
15
- renderFooterActivity,
16
- scheduleScrollNav,
17
- scrollDownBtn,
18
- scrollTopBtn,
19
- sendInterrupt,
20
- sendMessage,
21
- sendPresence,
22
- stopBtn,
23
- } from './chat.js';
24
- import {
25
- activateTab,
26
- agentControlsSlot,
27
- buildAppearanceControls,
28
- closeCreateFolder,
29
- closeFolderPicker,
30
- closeModeMenu,
31
- closeSettingsModal,
32
- createFolder,
33
- cwdPill,
34
- cycleTheme,
35
- filterFolders,
36
- folderGoto,
37
- folderList,
38
- folderModal,
39
- folderNewName,
40
- folderParent,
41
- gotoFolder,
42
- hideMoreMenu,
43
- initLayout,
44
- loadFolders,
45
- loadModels,
46
- loadModes,
47
- logout,
48
- modeMenu,
49
- modePill,
50
- modeSelect,
51
- modelPill,
52
- modelSelect,
53
- moreMenu,
54
- moreTab,
55
- moveAgentControls,
56
- openCreateFolder,
57
- openFolderPicker,
58
- openSettingsModal,
59
- persistAgent,
60
- providerSelect,
61
- renderModePill,
62
- setCwd,
63
- setMode,
64
- settingsModal,
65
- statusbarEl,
66
- strictInput,
67
- syncDefaultCwd,
68
- themeToggle,
69
- toggleMode,
70
- useFolder,
71
- writeDefaultCwd,
72
- yesInput,
73
- } from './chrome.js';
74
- import {
75
- handleSlashKey,
76
- hideSlashMenu,
77
- onChatInputChanged,
78
- openPalette,
79
- palette,
80
- renderPalette,
81
- runPaletteItem,
82
- setPaletteIndex,
83
- toast,
84
- } from './commands.js';
85
- import { $, api, setCsrfToken, state, storage } from './core.js';
86
- import {
87
- PREVIEW_DEVICE_KEY,
88
- PREVIEW_URL_KEY,
89
- applyTreeFilter,
90
- detectProcs,
91
- explorerFilter,
92
- explorerMode,
93
- filePreviewState,
94
- filePreviewTools,
95
- gitEntries,
96
- gitFilterEl,
97
- gitModalEl,
98
- loadPreview,
99
- openRemoteTunnel,
100
- previewFrame,
101
- previewTools,
102
- previewUrlInput,
103
- refreshExplorerMode,
104
- renderGitList,
105
- setExplorerMode,
106
- setExplorerOpen,
107
- setExplorerStatus,
108
- setPreviewStatus,
109
- setTunnelUi,
110
- syncPreviewTunnel,
111
- } from './explorer.js';
1
+ // Tamnora console — dependency-free client for the harness server (terminal theme).
2
+
3
+ import {
4
+ activePane,
5
+ addImageFiles,
6
+ attachInput,
7
+ chatForm,
8
+ chatInput,
9
+ closeMessageMenus,
10
+ controllers,
11
+ interruptBtn,
12
+ openMessageMenu,
13
+ panesEl,
14
+ positionMessageMenu,
15
+ renderFooterActivity,
16
+ scheduleScrollNav,
17
+ scrollDownBtn,
18
+ scrollTopBtn,
19
+ sendInterrupt,
20
+ sendMessage,
21
+ sendPresence,
22
+ stopBtn,
23
+ } from './chat.js';
24
+ import {
25
+ activateTab,
26
+ agentControlsSlot,
27
+ buildAppearanceControls,
28
+ closeCreateFolder,
29
+ closeFolderPicker,
30
+ closeModeMenu,
31
+ closeSettingsModal,
32
+ createFolder,
33
+ cwdPill,
34
+ cycleTheme,
35
+ filterFolders,
36
+ folderGoto,
37
+ folderList,
38
+ folderModal,
39
+ folderNewName,
40
+ folderParent,
41
+ gotoFolder,
42
+ hideMoreMenu,
43
+ initLayout,
44
+ loadFolders,
45
+ loadModels,
46
+ loadModes,
47
+ logout,
48
+ modeMenu,
49
+ modePill,
50
+ modeSelect,
51
+ modelPill,
52
+ modelSelect,
53
+ moreMenu,
54
+ moreTab,
55
+ moveAgentControls,
56
+ openCreateFolder,
57
+ openFolderPicker,
58
+ openSettingsModal,
59
+ persistAgent,
60
+ providerSelect,
61
+ renderModePill,
62
+ setCwd,
63
+ setMode,
64
+ settingsModal,
65
+ statusbarEl,
66
+ strictInput,
67
+ syncDefaultCwd,
68
+ themeToggle,
69
+ toggleMode,
70
+ useFolder,
71
+ writeDefaultCwd,
72
+ yesInput,
73
+ } from './chrome.js';
74
+ import {
75
+ handleSlashKey,
76
+ hideSlashMenu,
77
+ onChatInputChanged,
78
+ openPalette,
79
+ palette,
80
+ renderPalette,
81
+ runPaletteItem,
82
+ setPaletteIndex,
83
+ toast,
84
+ } from './commands.js';
85
+ import { $, api, scrollBehavior, setCsrfToken, state, storage } from './core.js';
86
+ import {
87
+ PREVIEW_DEVICE_KEY,
88
+ PREVIEW_URL_KEY,
89
+ applyTreeFilter,
90
+ detectProcs,
91
+ explorerFilter,
92
+ explorerMode,
93
+ filePreviewState,
94
+ filePreviewTools,
95
+ gitEntries,
96
+ gitFilterEl,
97
+ gitModalEl,
98
+ loadPreview,
99
+ openRemoteTunnel,
100
+ previewFrame,
101
+ previewTools,
102
+ previewUrlInput,
103
+ refreshExplorerMode,
104
+ renderGitList,
105
+ setExplorerMode,
106
+ setExplorerOpen,
107
+ setExplorerStatus,
108
+ setPreviewStatus,
109
+ setTunnelUi,
110
+ syncPreviewTunnel,
111
+ } from './explorer.js';
112
112
  import { renderMarkdown } from './markdown.js';
113
- import { wireModelPicker } from './models.js';
114
- import { restoreTabs, startNewSession } from './sessions.js';
115
- import {
116
- activateAjustesPanel,
117
- addPerfilFieldRow,
118
- ajustesTabs,
119
- closeMcpForm,
120
- closeMemoryForm,
121
- closeProviderForm,
122
- closeScheduleForm,
123
- closeSkillForm,
124
- importMcp,
125
- importSkill,
126
- limitsCachingEl,
127
- limitsDurationEl,
128
- limitsOutputEl,
129
- limitsRoundsEl,
130
- limitsTokensEl,
131
- mcpForm,
132
- mcpListEl,
133
- mcpModal,
134
- mcpModeEl,
135
- mcpPinnedEl,
136
- memoryForm,
137
- memoryModal,
138
- openMcpForm,
139
- openMemoryForm,
140
- openProviderForm,
141
- openScheduleForm,
142
- openSkillForm,
143
- perfil,
144
- providerForm,
145
- providerModal,
146
- refreshLimits,
147
- refreshMcp,
148
- refreshMcpCatalog,
149
- refreshProviders,
150
- refreshSkillCatalog,
151
- saveMemoryIndex,
152
- scheduleForm,
153
- scheduleModal,
154
- scheduleTypeFields,
155
- searchMcp,
156
- searchSkills,
157
- setStatus,
158
- skillForm,
159
- skillModal,
160
- submitAsesor,
161
- submitChannel,
162
- submitMcp,
163
- submitMemory,
164
- submitProfile,
165
- submitProvider,
166
- submitSchedule,
167
- submitSkill,
168
- syncMcpFormFields,
169
- testAsesor,
170
- testChannel,
171
- testProvider,
172
- vozPitchEl,
173
- vozRateEl,
174
- } from './settings.js';
175
- import {
176
- brandVerEl,
177
- dismissUpdate,
178
- evalRunBtn,
179
- evalTasks,
180
- followEvents,
181
- forceUpdateCheck,
182
- reconcile,
183
- refreshRuns,
184
- refreshUpdate,
185
- renderAlertSignals,
186
- runEval,
187
- runUpdate,
188
- watchLifecycle,
189
- } from './system.js';
190
- import {
191
- handsFreeBtn,
192
- micBtn,
193
- refreshTranscribe,
194
- refreshVoz,
195
- renderVozSliders,
196
- submitTranscribe,
197
- submitVoz,
198
- syncTranscribeMode,
199
- testVoz,
200
- toggleHandsFree,
201
- toggleMic,
202
- toggleTrTest,
203
- trModeEl,
204
- trTestBtn,
205
- } from './voice.js';
206
-
207
- moreTab.addEventListener('click', (event) => {
208
- event.stopPropagation();
209
- const open = moreMenu.hidden;
210
- moreMenu.hidden = !open;
211
- moreTab.setAttribute('aria-expanded', String(open));
212
- });
213
- document.querySelectorAll('#more-menu button').forEach((item) =>
214
- item.addEventListener('click', () => activateTab(item.dataset.tab))
215
- );
216
- document.addEventListener('click', (event) => {
217
- if (!moreMenu.hidden && !event.target.closest('.menu')) hideMoreMenu();
218
- });
219
- document.addEventListener('keydown', (event) => {
220
- if (event.key === 'Escape' && !moreMenu.hidden) hideMoreMenu();
221
- });
222
-
113
+ import { wireModelPicker } from './models.js';
114
+ import { restoreTabs, startNewSession } from './sessions.js';
115
+ import {
116
+ addPerfilFieldRow,
117
+ closeMcpForm,
118
+ closeMemoryForm,
119
+ closeProviderForm,
120
+ closeScheduleForm,
121
+ closeSkillForm,
122
+ importMcp,
123
+ importSkill,
124
+ limitsCachingEl,
125
+ limitsDurationEl,
126
+ limitsOutputEl,
127
+ limitsRoundsEl,
128
+ limitsTokensEl,
129
+ mcpForm,
130
+ mcpListEl,
131
+ mcpModal,
132
+ mcpModeEl,
133
+ mcpPinnedEl,
134
+ memoryForm,
135
+ memoryModal,
136
+ openMcpForm,
137
+ openMemoryForm,
138
+ openProviderForm,
139
+ openScheduleForm,
140
+ openSkillForm,
141
+ perfil,
142
+ providerForm,
143
+ providerModal,
144
+ refreshLimits,
145
+ refreshMcp,
146
+ refreshMcpCatalog,
147
+ refreshProviders,
148
+ refreshSkillCatalog,
149
+ saveMemoryIndex,
150
+ scheduleForm,
151
+ scheduleModal,
152
+ scheduleTypeFields,
153
+ searchMcp,
154
+ searchSkills,
155
+ setStatus,
156
+ skillForm,
157
+ skillModal,
158
+ submitAsesor,
159
+ submitChannel,
160
+ submitMcp,
161
+ submitMemory,
162
+ submitProfile,
163
+ submitProvider,
164
+ submitSchedule,
165
+ submitSkill,
166
+ syncMcpFormFields,
167
+ testAsesor,
168
+ testChannel,
169
+ testProvider,
170
+ vozPitchEl,
171
+ vozRateEl,
172
+ wireAjustesNav,
173
+ } from './settings.js';
174
+ import {
175
+ brandVerEl,
176
+ dismissUpdate,
177
+ evalRunBtn,
178
+ evalTasks,
179
+ followEvents,
180
+ forceUpdateCheck,
181
+ reconcile,
182
+ refreshRuns,
183
+ refreshUpdate,
184
+ renderAlertSignals,
185
+ runEval,
186
+ runUpdate,
187
+ watchLifecycle,
188
+ } from './system.js';
189
+ import {
190
+ handsFreeBtn,
191
+ micBtn,
192
+ refreshTranscribe,
193
+ refreshVoz,
194
+ renderVozSliders,
195
+ submitTranscribe,
196
+ submitVoz,
197
+ syncTranscribeMode,
198
+ testVoz,
199
+ toggleHandsFree,
200
+ toggleMic,
201
+ toggleTrTest,
202
+ trModeEl,
203
+ trTestBtn,
204
+ } from './voice.js';
205
+
206
+ moreTab.addEventListener('click', (event) => {
207
+ event.stopPropagation();
208
+ const open = moreMenu.hidden;
209
+ moreMenu.hidden = !open;
210
+ moreTab.setAttribute('aria-expanded', String(open));
211
+ });
212
+ document.querySelectorAll('#more-menu button').forEach((item) =>
213
+ item.addEventListener('click', () => activateTab(item.dataset.tab))
214
+ );
215
+ document.addEventListener('click', (event) => {
216
+ if (!moreMenu.hidden && !event.target.closest('.menu')) hideMoreMenu();
217
+ });
218
+ document.addEventListener('keydown', (event) => {
219
+ if (event.key === 'Escape' && !moreMenu.hidden) hideMoreMenu();
220
+ });
221
+
223
222
  $('#back-chat')?.addEventListener('click', () => activateTab('chat'));
224
223
 
225
- if (settingsModal) {
226
- $('#settings-toggle')?.addEventListener('click', openSettingsModal);
227
- $('#settings-close')?.addEventListener('click', closeSettingsModal);
228
- $('#settings-done')?.addEventListener('click', closeSettingsModal);
229
- $('#settings-providers')?.addEventListener('click', () => {
230
- closeSettingsModal();
231
- activateTab('providers');
232
- });
233
- $('#agent-settings-open')?.addEventListener('click', openSettingsModal);
234
- $('#agent-providers')?.addEventListener('click', () => activateTab('providers'));
235
- $('#default-cwd-pick')?.addEventListener('click', () => openFolderPicker('default'));
236
- $('#default-cwd-clear')?.addEventListener('click', () => {
237
- writeDefaultCwd('');
238
- syncDefaultCwd();
239
- });
240
- settingsModal.addEventListener('click', (event) => {
241
- if (event.target === settingsModal) closeSettingsModal();
242
- });
243
- settingsModal.addEventListener('close', () => moveAgentControls(agentControlsSlot));
244
- }
245
-
246
- if (statusbarEl) {
247
- statusbarEl.addEventListener('click', (event) => {
248
- if (event.target.closest('#sb-context')) return;
249
- openSettingsModal();
250
- });
251
- statusbarEl.addEventListener('keydown', (event) => {
252
- if (event.key === 'Enter' || event.key === ' ') {
253
- event.preventDefault();
254
- openSettingsModal();
255
- }
256
- });
257
- }
258
-
259
- if (palette.modal) {
260
- palette.input.addEventListener('input', () => {
261
- const q = palette.input.value;
262
- renderPalette(q);
263
- clearTimeout(palette.timer);
264
- palette.timer = setTimeout(async () => {
265
- if (q.trim().length < 2) {
266
- palette.results = { sessions: [], messages: [] };
267
- renderPalette(q);
268
- return;
269
- }
270
- const data = await api(`/api/search?q=${encodeURIComponent(q)}&limit=15`).catch(() => null);
271
- if (data) {
272
- palette.results = { sessions: data.sessions || [], messages: data.messages || [] };
273
- renderPalette(q);
274
- }
275
- }, 180);
276
- });
277
- palette.input.addEventListener('keydown', (event) => {
278
- if (event.key === 'ArrowDown') {
279
- event.preventDefault();
280
- setPaletteIndex(palette.index + 1);
281
- } else if (event.key === 'ArrowUp') {
282
- event.preventDefault();
283
- setPaletteIndex(palette.index - 1);
284
- } else if (event.key === 'Enter') {
285
- event.preventDefault();
286
- const item = palette.items[palette.index];
287
- if (item) runPaletteItem(item);
288
- }
289
- });
290
- palette.modal.addEventListener('click', (event) => {
291
- if (event.target === palette.modal) palette.modal.close();
292
- });
293
- $('#search-toggle')?.addEventListener('click', openPalette);
294
- document.addEventListener('keydown', (event) => {
295
- if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'k') {
296
- event.preventDefault();
297
- openPalette();
298
- }
299
- });
300
- }
301
-
302
- modeSelect.addEventListener('change', () => setMode(modeSelect.value));
303
-
304
- document.addEventListener('keydown', (event) => {
305
- if ((event.ctrlKey || event.metaKey) && event.key === '.') {
306
- event.preventDefault();
307
- toggleMode();
308
- }
309
- });
310
-
311
- cwdPill?.addEventListener('click', () => openFolderPicker('session'));
312
- modePill?.addEventListener('click', (event) => {
313
- event.stopPropagation();
314
- if (!modeMenu) return;
315
- modeMenu.hidden = !modeMenu.hidden;
316
- modePill.setAttribute('aria-expanded', modeMenu.hidden ? 'false' : 'true');
317
- });
318
- modeMenu?.addEventListener('click', async (event) => {
319
- const item = event.target.closest('.ctrl-menu-item');
320
- if (!item) return;
321
- const kind = item.dataset.kind;
322
- closeModeMenu();
323
- if (kind === 'full') {
324
- await setMode('agent');
325
- if (yesInput) yesInput.checked = true;
326
- } else {
327
- if (yesInput) yesInput.checked = false;
328
- await setMode(kind === 'read' ? 'plan' : kind === 'voice' ? 'voice' : 'agent');
329
- }
330
- renderModePill();
331
- });
332
- document.addEventListener('click', (event) => {
333
- if (modeMenu && !modeMenu.hidden && !event.target.closest('.ctrl-wrap')) closeModeMenu();
334
- });
335
- document.addEventListener('keydown', (event) => {
336
- if (event.key === 'Escape') closeModeMenu();
337
- });
338
- modelPill?.addEventListener('click', () => openSettingsModal());
339
- yesInput?.addEventListener('change', renderModePill);
340
- strictInput?.addEventListener('change', () =>
341
- api('/api/settings', { method: 'PUT', body: { strictScope: strictInput.checked } }).catch(() => null)
342
- );
343
-
344
- themeToggle?.addEventListener('click', cycleTheme);
345
-
346
- buildAppearanceControls();
347
-
348
- if (logout) {
349
- logout.addEventListener('click', async (event) => {
350
- event.preventDefault();
351
- try {
352
- await api('/api/auth/logout', { method: 'POST' });
353
- } catch {
354
- // igual salimos
355
- }
356
- location.href = '/landing';
357
- });
358
- }
359
-
360
- initLayout();
361
-
362
- providerSelect.addEventListener('change', async () => {
363
- await loadModels({ save: false });
364
- await persistAgent({ provider: providerSelect.value, model: modelSelect.value || undefined });
365
- });
366
- modelSelect.addEventListener('change', () => persistAgent({ model: modelSelect.value }));
367
-
368
- export let restorePreviewMax = null;
369
-
370
- scrollTopBtn.addEventListener('click', () => activePane?.scrollTo({ top: 0, behavior: 'smooth' }));
371
- scrollDownBtn.addEventListener('click', () =>
372
- activePane?.scrollTo({ top: activePane.scrollHeight, behavior: 'smooth' })
373
- );
374
- // `scroll` no burbujea: se escucha en captura desde el contenedor de los panes.
375
- panesEl.addEventListener('scroll', scheduleScrollNav, true);
376
- window.addEventListener('resize', scheduleScrollNav);
377
- new MutationObserver(scheduleScrollNav).observe(panesEl, {
378
- childList: true,
379
- characterData: true,
380
- subtree: true,
381
- });
382
-
383
- document.addEventListener('click', (event) => {
384
- if (!openMessageMenu) return;
385
- if (event.target.closest('.msg-menu') || event.target.closest('.msg-menu-btn')) return;
386
- closeMessageMenus();
387
- });
388
- document.addEventListener('keydown', (event) => {
389
- if (event.key === 'Escape') closeMessageMenus();
390
- });
391
- window.addEventListener('resize', positionMessageMenu);
392
- document.addEventListener('scroll', positionMessageMenu, true);
393
-
394
- panesEl.addEventListener('click', (event) => {
395
- const button = event.target.closest('.code-copy');
396
- if (!button) return;
397
- const pre = button.closest('.code-block')?.querySelector('pre');
398
- if (!pre || !navigator.clipboard) return;
399
- navigator.clipboard.writeText(pre.textContent).then(() => {
400
- button.textContent = 'copiado';
401
- setTimeout(() => (button.textContent = 'copiar'), 1200);
402
- });
403
- });
404
-
405
- /** Tool -> momento en que se aprobo por ultima vez. */
406
- state.recentDecisions = new Map();
407
-
408
- $('#new-session').addEventListener('click', startNewSession);
409
- $('#new-session-top').addEventListener('click', startNewSession);
410
-
411
- if (stopBtn) {
412
- stopBtn.addEventListener('click', async () => {
413
- const id = state.sessionId;
414
- if (!id || stopBtn.disabled) return;
415
- stopBtn.disabled = true;
416
- renderFooterActivity('stopping');
417
- // Abortamos el stream local (corta el seguimiento ya) y pedimos al server que
418
- // aborte el run: sin esto el run es durable y seguiria generando.
419
- controllers.get(id)?.abort();
420
- try {
421
- await api(`/api/sessions/${id}/stop`, { method: 'POST' });
422
- } catch (error) {
423
- toast(`No se pudo detener el run: ${error.message}`);
424
- } finally {
425
- stopBtn.disabled = false;
426
- }
427
- });
428
- }
429
-
430
- if (interruptBtn) interruptBtn.addEventListener('click', sendInterrupt);
431
-
432
- chatForm.addEventListener('submit', (event) => {
433
- event.preventDefault();
434
- sendMessage();
435
- });
436
- chatInput.addEventListener('keydown', (event) => {
437
- if (handleSlashKey(event)) return;
438
- if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) {
439
- event.preventDefault();
440
- sendInterrupt();
441
- return;
442
- }
443
- if (event.key === 'Enter' && !event.shiftKey) {
444
- event.preventDefault();
445
- sendMessage();
446
- }
447
- });
448
- chatInput.addEventListener('input', onChatInputChanged);
449
-
450
- micBtn?.addEventListener('click', toggleMic);
451
-
452
- handsFreeBtn?.addEventListener('click', toggleHandsFree);
453
-
454
- $('#attach-image').addEventListener('click', () => attachInput.click());
455
- attachInput.addEventListener('change', () => {
456
- addImageFiles([...attachInput.files]);
457
- attachInput.value = '';
458
- });
459
- chatInput.addEventListener('paste', (event) => {
460
- const files = [...(event.clipboardData?.files ?? [])].filter((f) => f.type.startsWith('image/'));
461
- if (!files.length) return;
462
- event.preventDefault();
463
- addImageFiles(files);
464
- });
465
- chatForm.addEventListener('dragover', (event) => {
466
- event.preventDefault();
467
- chatForm.classList.add('dragging');
468
- });
469
- chatForm.addEventListener('dragleave', () => chatForm.classList.remove('dragging'));
470
- chatForm.addEventListener('drop', (event) => {
471
- const files = [...(event.dataTransfer?.files ?? [])].filter((f) => f.type.startsWith('image/'));
472
- if (!files.length) return;
473
- event.preventDefault();
474
- chatForm.classList.remove('dragging');
475
- addImageFiles(files);
476
- });
477
-
478
- chatInput.addEventListener('blur', () => setTimeout(hideSlashMenu, 120));
479
-
480
- $('#folder-new').addEventListener('click', openCreateFolder);
481
- $('#folder-new-cancel').addEventListener('click', closeCreateFolder);
482
- $('#folder-new-create').addEventListener('click', createFolder);
483
- folderNewName.addEventListener('keydown', (event) => {
484
- if (event.key === 'Enter') {
485
- event.preventDefault();
486
- createFolder();
487
- } else if (event.key === 'Escape') {
488
- event.preventDefault();
489
- closeCreateFolder();
490
- }
491
- });
492
- $('#folder-roots').addEventListener('click', () => loadFolders(''));
493
- $('#folder-home').addEventListener('click', () => loadFolders(state.home || ''));
494
- $('#folder-close').addEventListener('click', closeFolderPicker);
495
- $('#folder-cancel').addEventListener('click', () => {
496
- state.pendingInput = null;
497
- closeFolderPicker();
498
- });
499
- $('#folder-up').addEventListener('click', () => loadFolders(folderParent ?? ''));
500
- $('#folder-use').addEventListener('click', useFolder);
501
- $('#folder-go').addEventListener('click', gotoFolder);
502
- folderGoto.addEventListener('input', filterFolders);
503
- folderGoto.addEventListener('keydown', (event) => {
504
- if (event.key === 'Enter') {
505
- event.preventDefault();
506
- gotoFolder();
507
- }
508
- });
509
- folderList.addEventListener('keydown', (event) => {
510
- if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') return;
511
- const items = [...folderList.querySelectorAll('.folder-item')];
512
- if (!items.length) return;
513
- event.preventDefault();
514
- const current = items.indexOf(document.activeElement);
515
- const next = event.key === 'ArrowDown' ? Math.min(items.length - 1, current + 1) : Math.max(0, current - 1);
516
- items[current === -1 ? 0 : next].focus();
517
- });
518
- folderModal.addEventListener('click', (event) => {
519
- if (event.target === folderModal) {
520
- state.pendingInput = null;
521
- closeFolderPicker();
522
- }
523
- });
524
-
525
- $('#provider-new').addEventListener('click', () => openProviderForm());
526
- $('#pf-cancel').addEventListener('click', closeProviderForm);
527
- $('#pf-close').addEventListener('click', closeProviderForm);
528
- $('#pf-test').addEventListener('click', testProvider);
529
- providerForm.addEventListener('submit', submitProvider);
530
- providerModal.addEventListener('click', (event) => {
531
- if (event.target === providerModal) closeProviderForm();
532
- });
533
-
534
- ajustesTabs.forEach((tab) =>
535
- tab.addEventListener('click', () => activateAjustesPanel(tab.dataset.panel))
536
- );
537
-
538
- $('#limits-save')?.addEventListener('click', async () => {
539
- const body = {
540
- maxRounds: Math.max(0, Number(limitsRoundsEl?.value) || 0),
541
- maxTokens: Math.max(0, Number(limitsTokensEl?.value) || 0),
542
- maxDurationMs: Math.max(0, Math.round((Number(limitsDurationEl?.value) || 0) * 1000)),
543
- maxOutputTokens: Math.max(0, Number(limitsOutputEl?.value) || 0),
544
- promptCaching: Boolean(limitsCachingEl?.checked),
545
- };
546
- try {
547
- await api('/api/settings', { method: 'PUT', body });
548
- refreshLimits();
549
- setStatus('#limits-msg', 'guardado', 'ok');
550
- } catch (error) {
551
- setStatus('#limits-msg', error.message, 'err');
552
- }
553
- });
554
-
555
- $('#tr-save')?.addEventListener('click', submitTranscribe);
556
- trModeEl?.addEventListener('change', syncTranscribeMode);
557
-
558
- trTestBtn?.addEventListener('click', toggleTrTest);
559
-
560
- perfil.add?.addEventListener('click', () => addPerfilFieldRow());
561
- perfil.save?.addEventListener('click', submitProfile);
562
-
563
- if (mcpListEl) {
564
- $('#mcp-new').addEventListener('click', () => openMcpForm());
565
- $('#mcp-import-btn')?.addEventListener('click', importMcp);
566
- $('#mcp-import')?.addEventListener('keydown', (event) => {
567
- if (event.key === 'Enter') {
568
- event.preventDefault();
569
- importMcp();
570
- }
571
- });
572
- $('#skill-import-btn')?.addEventListener('click', importSkill);
573
- $('#skill-import')?.addEventListener('keydown', (event) => {
574
- if (event.key === 'Enter') {
575
- event.preventDefault();
576
- importSkill();
577
- }
578
- });
579
- $('#mcp-search-btn')?.addEventListener('click', searchMcp);
580
- $('#mcp-search')?.addEventListener('keydown', (event) => {
581
- if (event.key === 'Enter') {
582
- event.preventDefault();
583
- searchMcp();
584
- }
585
- });
586
- $('#skill-search-btn')?.addEventListener('click', searchSkills);
587
- $('#skill-search')?.addEventListener('keydown', (event) => {
588
- if (event.key === 'Enter') {
589
- event.preventDefault();
590
- searchSkills();
591
- }
592
- });
593
- refreshMcpCatalog();
594
- refreshSkillCatalog();
595
- $('#mcp-reload').addEventListener('click', () => refreshMcp({ connect: true }));
596
- mcpModeEl?.addEventListener('change', async () => {
597
- try {
598
- await api('/api/settings', { method: 'PUT', body: { mcpMode: mcpModeEl.value } });
599
- await refreshMcp({ connect: true });
600
- } catch (error) {
601
- toast(error.message);
602
- }
603
- });
604
- const savePinnedMcp = async () => {
605
- try {
606
- await api('/api/settings', { method: 'PUT', body: { mcpPinned: String(mcpPinnedEl.value || '') } });
607
- await refreshMcp({ connect: true });
608
- } catch (error) {
609
- toast(error.message);
610
- }
611
- };
612
- $('#mcp-pinned-save')?.addEventListener('click', savePinnedMcp);
613
- mcpPinnedEl?.addEventListener('keydown', (event) => {
614
- if (event.key === 'Enter') {
615
- event.preventDefault();
616
- savePinnedMcp();
617
- }
618
- });
619
- $('#tg-save')?.addEventListener('click', submitChannel);
620
- $('#tg-test')?.addEventListener('click', testChannel);
621
- $('#voz-save')?.addEventListener('click', submitVoz);
622
- $('#voz-test')?.addEventListener('click', testVoz);
623
- vozRateEl?.addEventListener('input', renderVozSliders);
624
- vozPitchEl?.addEventListener('input', renderVozSliders);
625
- $('#asesor-save')?.addEventListener('click', submitAsesor);
626
- $('#asesor-test')?.addEventListener('click', testAsesor);
627
- $('#mcp-close').addEventListener('click', closeMcpForm);
628
- $('#mf-cancel').addEventListener('click', closeMcpForm);
629
- $('#mf-transport').addEventListener('change', syncMcpFormFields);
630
- mcpForm.addEventListener('submit', submitMcp);
631
- mcpModal.addEventListener('click', (event) => {
632
- if (event.target === mcpModal) closeMcpForm();
633
- });
634
-
635
- $('#skill-new').addEventListener('click', () => openSkillForm());
636
- $('#skill-close').addEventListener('click', closeSkillForm);
637
- $('#sf-cancel').addEventListener('click', closeSkillForm);
638
- skillForm.addEventListener('submit', submitSkill);
639
- skillModal.addEventListener('click', (event) => {
640
- if (event.target === skillModal) closeSkillForm();
641
- });
642
-
643
- $('#memory-new')?.addEventListener('click', () => openMemoryForm());
644
- $('#memory-close')?.addEventListener('click', closeMemoryForm);
645
- $('#mem-cancel')?.addEventListener('click', closeMemoryForm);
646
- memoryForm?.addEventListener('submit', submitMemory);
647
- $('#memory-save-index')?.addEventListener('click', saveMemoryIndex);
648
- memoryModal?.addEventListener('click', (event) => {
649
- if (event.target === memoryModal) closeMemoryForm();
650
- });
651
-
652
- $('#sc-type')?.addEventListener('change', scheduleTypeFields);
653
- $('#schedule-new')?.addEventListener('click', () => openScheduleForm());
654
- $('#schedule-close')?.addEventListener('click', closeScheduleForm);
655
- $('#sc-cancel')?.addEventListener('click', closeScheduleForm);
656
- scheduleForm?.addEventListener('submit', submitSchedule);
657
- scheduleModal?.addEventListener('click', (event) => {
658
- if (event.target === scheduleModal) closeScheduleForm();
659
- });
660
- }
661
-
662
- evalTasks.value = [
663
- '{"id":"math","input":"¿Cuánto es 17 * 23? Respondé solo el número.","expect":{"equals":"391"}}',
664
- '{"id":"regex","input":"Escribí una oración que contenga un número.","expect":{"regex":"\\\\d"}}',
665
- ].join('\n');
666
-
667
- evalRunBtn.addEventListener('click', runEval);
668
-
669
- $('#runs-refresh').addEventListener('click', refreshRuns);
670
-
671
- $('#file-preview-toggle')?.addEventListener('click', async () => {
672
- const toggle = $('#file-preview-toggle');
673
- const code = $('#file-code');
674
- const stage = $('#file-stage');
675
- const frame = $('#file-frame');
676
- const render = $('#file-render');
677
- const device = $('#file-device');
678
- const maxButton = $('#file-max');
679
- const current = filePreviewState.current;
680
- if (!current) return;
681
-
682
- filePreviewState.open = !filePreviewState.open;
683
- const off = () => {
684
- toggle.textContent = 'vista previa';
685
- code.hidden = false;
686
- stage.hidden = true;
687
- render.hidden = true;
688
- device.hidden = true;
689
- maxButton.hidden = false;
690
- $('#file-modal').classList.remove('preview-maximized');
691
- };
692
- if (!filePreviewState.open) return off();
693
-
694
- if (filePreviewState.kind === 'markdown') {
695
- render.innerHTML = renderMarkdown(filePreviewState.content);
696
- code.hidden = true;
697
- stage.hidden = true;
698
- render.hidden = false;
699
- device.hidden = true;
700
- maxButton.hidden = false;
701
- toggle.textContent = 'ver código';
702
- return;
703
- }
704
-
705
- device.hidden = false;
706
- maxButton.hidden = false;
707
- if (!filePreviewState.loaded) {
708
- const data = await api('/api/files/preview', {
709
- method: 'POST',
710
- body: { root: state.cwd, path: current.relPath },
711
- }).catch((error) => ({ error: error.message }));
712
- if (data?.error) {
713
- setExplorerStatus(data.error, true);
714
- filePreviewState.open = false;
715
- return off();
716
- }
717
- frame.src = data.url;
718
- filePreviewState.loaded = true;
719
- filePreviewTools.applyDevice();
720
- }
721
- toggle.textContent = 'ver código';
722
- code.hidden = true;
723
- render.hidden = true;
724
- stage.hidden = false;
725
- });
726
-
727
- document.querySelectorAll('.xtab').forEach((tab) => tab.addEventListener('click', () => setExplorerMode(tab.dataset.xp)));
728
-
729
- restorePreviewMax = () => previewTools.setMax(false);
730
-
731
- $('#preview-open')?.addEventListener('click', () => loadPreview(previewUrlInput.value));
732
- previewUrlInput?.addEventListener('keydown', (event) => {
733
- if (event.key === 'Enter') {
734
- event.preventDefault();
735
- loadPreview(previewUrlInput.value);
736
- }
737
- });
738
- previewFrame?.addEventListener('load', () => {
739
- if (previewFrame.src) setPreviewStatus(previewFrame.src);
740
- });
741
- $('#preview-tunnel')?.addEventListener('click', () => {
742
- const match = String(previewUrlInput.value).match(/:(\d+)/);
743
- const port = match ? Number(match[1]) : 0;
744
- if (!port) return setPreviewStatus('Poné una URL con puerto (p. ej. http://127.0.0.1:5173).', true);
745
- openRemoteTunnel(port);
746
- });
747
- $('#preview-close')?.addEventListener('click', async () => {
748
- await api('/api/preview/tunnel', { method: 'DELETE' }).catch(() => null);
749
- setTunnelUi(null);
750
- setPreviewStatus('acceso remoto cerrado.');
751
- });
752
- $('#preview-device')?.addEventListener('change', (event) => storage.set(PREVIEW_DEVICE_KEY, event.target.value));
753
-
754
- // Restaurar lo tipeado y el tamano elegido: en la tablet la pagina se recarga
755
- // sola (iOS la descarta en background) y antes se perdia todo el estado.
756
- (function restorePreview() {
757
- const savedDevice = storage.get(PREVIEW_DEVICE_KEY, '');
758
- if (savedDevice && $('#preview-device')) $('#preview-device').value = savedDevice;
759
- previewTools.applyDevice();
760
- const savedUrl = storage.get(PREVIEW_URL_KEY, '');
761
- if (savedUrl) previewUrlInput.value = savedUrl;
762
- syncPreviewTunnel({ load: true });
763
- })();
764
-
765
- $('#procs-detect')?.addEventListener('click', detectProcs);
766
-
767
- gitFilterEl?.addEventListener('input', () => {
768
- if (explorerMode === 'git' && gitEntries.length) renderGitList();
769
- });
770
-
771
- $('#explorer-handle')?.addEventListener('click', () => setExplorerOpen(true));
772
- $('#explorer-close').addEventListener('click', () => setExplorerOpen(false));
773
- $('#explorer-refresh').addEventListener('click', refreshExplorerMode);
774
- explorerFilter.addEventListener('input', applyTreeFilter);
775
- $('#file-close').addEventListener('click', () => $('#file-modal').close());
776
- $('#file-modal').addEventListener('click', (event) => {
777
- if (event.target === $('#file-modal')) $('#file-modal').close();
778
- });
779
- $('#git-close')?.addEventListener('click', () => gitModalEl?.close());
780
- gitModalEl?.addEventListener('click', (event) => {
781
- if (event.target === gitModalEl) gitModalEl.close();
782
- });
783
-
784
- try {
785
- setExplorerOpen(storage.get('tamnora-explorer-open') === '1');
786
- } catch {
787
- setExplorerOpen(false);
788
- }
789
-
790
- $('#update-run')?.addEventListener('click', runUpdate);
791
- $('#update-later')?.addEventListener('click', dismissUpdate);
792
-
793
- if (brandVerEl) {
794
- brandVerEl.title = 'buscar actualización';
795
- brandVerEl.classList.add('ver-action');
796
- brandVerEl.addEventListener('click', forceUpdateCheck);
797
- }
798
-
799
- /* --- boot ---------------------------------------------------------------- */
800
-
224
+ if (settingsModal) {
225
+ $('#settings-toggle')?.addEventListener('click', openSettingsModal);
226
+ $('#settings-close')?.addEventListener('click', closeSettingsModal);
227
+ $('#settings-done')?.addEventListener('click', closeSettingsModal);
228
+ $('#settings-providers')?.addEventListener('click', () => {
229
+ closeSettingsModal();
230
+ activateTab('providers');
231
+ });
232
+ $('#agent-settings-open')?.addEventListener('click', openSettingsModal);
233
+ $('#agent-providers')?.addEventListener('click', () => activateTab('providers'));
234
+ $('#default-cwd-pick')?.addEventListener('click', () => openFolderPicker('default'));
235
+ $('#default-cwd-clear')?.addEventListener('click', () => {
236
+ writeDefaultCwd('');
237
+ syncDefaultCwd();
238
+ });
239
+ settingsModal.addEventListener('click', (event) => {
240
+ if (event.target === settingsModal) closeSettingsModal();
241
+ });
242
+ settingsModal.addEventListener('close', () => moveAgentControls(agentControlsSlot));
243
+ }
244
+
245
+ if (statusbarEl) {
246
+ statusbarEl.addEventListener('click', (event) => {
247
+ if (event.target.closest('#sb-context')) return;
248
+ openSettingsModal();
249
+ });
250
+ statusbarEl.addEventListener('keydown', (event) => {
251
+ if (event.key === 'Enter' || event.key === ' ') {
252
+ event.preventDefault();
253
+ openSettingsModal();
254
+ }
255
+ });
256
+ }
257
+
258
+ if (palette.modal) {
259
+ palette.input.addEventListener('input', () => {
260
+ const q = palette.input.value;
261
+ renderPalette(q);
262
+ clearTimeout(palette.timer);
263
+ palette.timer = setTimeout(async () => {
264
+ if (q.trim().length < 2) {
265
+ palette.results = { sessions: [], messages: [] };
266
+ renderPalette(q);
267
+ return;
268
+ }
269
+ const data = await api(`/api/search?q=${encodeURIComponent(q)}&limit=15`).catch(() => null);
270
+ if (data) {
271
+ palette.results = { sessions: data.sessions || [], messages: data.messages || [] };
272
+ renderPalette(q);
273
+ }
274
+ }, 180);
275
+ });
276
+ palette.input.addEventListener('keydown', (event) => {
277
+ if (event.key === 'ArrowDown') {
278
+ event.preventDefault();
279
+ setPaletteIndex(palette.index + 1);
280
+ } else if (event.key === 'ArrowUp') {
281
+ event.preventDefault();
282
+ setPaletteIndex(palette.index - 1);
283
+ } else if (event.key === 'Enter') {
284
+ event.preventDefault();
285
+ const item = palette.items[palette.index];
286
+ if (item) runPaletteItem(item);
287
+ }
288
+ });
289
+ palette.modal.addEventListener('click', (event) => {
290
+ if (event.target === palette.modal) palette.modal.close();
291
+ });
292
+ $('#search-toggle')?.addEventListener('click', openPalette);
293
+ document.addEventListener('keydown', (event) => {
294
+ if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'k') {
295
+ event.preventDefault();
296
+ openPalette();
297
+ }
298
+ });
299
+ }
300
+
301
+ modeSelect.addEventListener('change', () => setMode(modeSelect.value));
302
+
303
+ document.addEventListener('keydown', (event) => {
304
+ if ((event.ctrlKey || event.metaKey) && event.key === '.') {
305
+ event.preventDefault();
306
+ toggleMode();
307
+ }
308
+ });
309
+
310
+ cwdPill?.addEventListener('click', () => openFolderPicker('session'));
311
+ modePill?.addEventListener('click', (event) => {
312
+ event.stopPropagation();
313
+ if (!modeMenu) return;
314
+ modeMenu.hidden = !modeMenu.hidden;
315
+ modePill.setAttribute('aria-expanded', modeMenu.hidden ? 'false' : 'true');
316
+ });
317
+ modeMenu?.addEventListener('click', async (event) => {
318
+ const item = event.target.closest('.ctrl-menu-item');
319
+ if (!item) return;
320
+ const kind = item.dataset.kind;
321
+ closeModeMenu();
322
+ if (kind === 'full') {
323
+ await setMode('agent');
324
+ if (yesInput) yesInput.checked = true;
325
+ } else {
326
+ if (yesInput) yesInput.checked = false;
327
+ await setMode(kind === 'read' ? 'plan' : kind === 'voice' ? 'voice' : 'agent');
328
+ }
329
+ renderModePill();
330
+ });
331
+ document.addEventListener('click', (event) => {
332
+ if (modeMenu && !modeMenu.hidden && !event.target.closest('.ctrl-wrap')) closeModeMenu();
333
+ });
334
+ document.addEventListener('keydown', (event) => {
335
+ if (event.key === 'Escape') closeModeMenu();
336
+ });
337
+ modelPill?.addEventListener('click', () => openSettingsModal());
338
+ yesInput?.addEventListener('change', renderModePill);
339
+ strictInput?.addEventListener('change', () =>
340
+ api('/api/settings', { method: 'PUT', body: { strictScope: strictInput.checked } }).catch(() => null)
341
+ );
342
+
343
+ themeToggle?.addEventListener('click', cycleTheme);
344
+
345
+ buildAppearanceControls();
346
+
347
+ if (logout) {
348
+ logout.addEventListener('click', async (event) => {
349
+ event.preventDefault();
350
+ try {
351
+ await api('/api/auth/logout', { method: 'POST' });
352
+ } catch {
353
+ // igual salimos
354
+ }
355
+ location.href = '/landing';
356
+ });
357
+ }
358
+
359
+ initLayout();
360
+
361
+ providerSelect.addEventListener('change', async () => {
362
+ await loadModels({ save: false });
363
+ await persistAgent({ provider: providerSelect.value, model: modelSelect.value || undefined });
364
+ });
365
+ modelSelect.addEventListener('change', () => persistAgent({ model: modelSelect.value }));
366
+
367
+ export let restorePreviewMax = null;
368
+
369
+ scrollTopBtn.addEventListener('click', () => activePane?.scrollTo({ top: 0, behavior: scrollBehavior() }));
370
+ scrollDownBtn.addEventListener('click', () =>
371
+ activePane?.scrollTo({ top: activePane.scrollHeight, behavior: scrollBehavior() })
372
+ );
373
+ // `scroll` no burbujea: se escucha en captura desde el contenedor de los panes.
374
+ panesEl.addEventListener('scroll', scheduleScrollNav, true);
375
+ window.addEventListener('resize', scheduleScrollNav);
376
+ new MutationObserver(scheduleScrollNav).observe(panesEl, {
377
+ childList: true,
378
+ characterData: true,
379
+ subtree: true,
380
+ });
381
+
382
+ document.addEventListener('click', (event) => {
383
+ if (!openMessageMenu) return;
384
+ if (event.target.closest('.msg-menu') || event.target.closest('.msg-menu-btn')) return;
385
+ closeMessageMenus();
386
+ });
387
+ document.addEventListener('keydown', (event) => {
388
+ if (event.key === 'Escape') closeMessageMenus();
389
+ });
390
+ window.addEventListener('resize', positionMessageMenu);
391
+ document.addEventListener('scroll', positionMessageMenu, true);
392
+
393
+ panesEl.addEventListener('click', (event) => {
394
+ const button = event.target.closest('.code-copy');
395
+ if (!button) return;
396
+ const pre = button.closest('.code-block')?.querySelector('pre');
397
+ if (!pre || !navigator.clipboard) return;
398
+ navigator.clipboard.writeText(pre.textContent).then(() => {
399
+ button.textContent = 'copiado';
400
+ setTimeout(() => (button.textContent = 'copiar'), 1200);
401
+ });
402
+ });
403
+
404
+ /** Tool -> momento en que se aprobo por ultima vez. */
405
+ state.recentDecisions = new Map();
406
+
407
+ $('#new-session').addEventListener('click', startNewSession);
408
+ $('#new-session-top').addEventListener('click', startNewSession);
409
+
410
+ if (stopBtn) {
411
+ stopBtn.addEventListener('click', async () => {
412
+ const id = state.sessionId;
413
+ if (!id || stopBtn.disabled) return;
414
+ stopBtn.disabled = true;
415
+ renderFooterActivity('stopping');
416
+ // Abortamos el stream local (corta el seguimiento ya) y pedimos al server que
417
+ // aborte el run: sin esto el run es durable y seguiria generando.
418
+ controllers.get(id)?.abort();
419
+ try {
420
+ await api(`/api/sessions/${id}/stop`, { method: 'POST' });
421
+ } catch (error) {
422
+ toast(`No se pudo detener el run: ${error.message}`);
423
+ } finally {
424
+ stopBtn.disabled = false;
425
+ }
426
+ });
427
+ }
428
+
429
+ if (interruptBtn) interruptBtn.addEventListener('click', sendInterrupt);
430
+
431
+ chatForm.addEventListener('submit', (event) => {
432
+ event.preventDefault();
433
+ sendMessage();
434
+ });
435
+ chatInput.addEventListener('keydown', (event) => {
436
+ if (handleSlashKey(event)) return;
437
+ if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) {
438
+ event.preventDefault();
439
+ sendInterrupt();
440
+ return;
441
+ }
442
+ if (event.key === 'Enter' && !event.shiftKey) {
443
+ event.preventDefault();
444
+ sendMessage();
445
+ }
446
+ });
447
+ chatInput.addEventListener('input', onChatInputChanged);
448
+
449
+ micBtn?.addEventListener('click', toggleMic);
450
+
451
+ handsFreeBtn?.addEventListener('click', toggleHandsFree);
452
+
453
+ $('#attach-image').addEventListener('click', () => attachInput.click());
454
+ attachInput.addEventListener('change', () => {
455
+ addImageFiles([...attachInput.files]);
456
+ attachInput.value = '';
457
+ });
458
+ chatInput.addEventListener('paste', (event) => {
459
+ const files = [...(event.clipboardData?.files ?? [])].filter((f) => f.type.startsWith('image/'));
460
+ if (!files.length) return;
461
+ event.preventDefault();
462
+ addImageFiles(files);
463
+ });
464
+ chatForm.addEventListener('dragover', (event) => {
465
+ event.preventDefault();
466
+ chatForm.classList.add('dragging');
467
+ });
468
+ chatForm.addEventListener('dragleave', () => chatForm.classList.remove('dragging'));
469
+ chatForm.addEventListener('drop', (event) => {
470
+ const files = [...(event.dataTransfer?.files ?? [])].filter((f) => f.type.startsWith('image/'));
471
+ if (!files.length) return;
472
+ event.preventDefault();
473
+ chatForm.classList.remove('dragging');
474
+ addImageFiles(files);
475
+ });
476
+
477
+ chatInput.addEventListener('blur', () => setTimeout(hideSlashMenu, 120));
478
+
479
+ $('#folder-new').addEventListener('click', openCreateFolder);
480
+ $('#folder-new-cancel').addEventListener('click', closeCreateFolder);
481
+ $('#folder-new-create').addEventListener('click', createFolder);
482
+ folderNewName.addEventListener('keydown', (event) => {
483
+ if (event.key === 'Enter') {
484
+ event.preventDefault();
485
+ createFolder();
486
+ } else if (event.key === 'Escape') {
487
+ event.preventDefault();
488
+ closeCreateFolder();
489
+ }
490
+ });
491
+ $('#folder-roots').addEventListener('click', () => loadFolders(''));
492
+ $('#folder-home').addEventListener('click', () => loadFolders(state.home || ''));
493
+ $('#folder-close').addEventListener('click', closeFolderPicker);
494
+ $('#folder-cancel').addEventListener('click', () => {
495
+ state.pendingInput = null;
496
+ closeFolderPicker();
497
+ });
498
+ $('#folder-up').addEventListener('click', () => loadFolders(folderParent ?? ''));
499
+ $('#folder-use').addEventListener('click', useFolder);
500
+ $('#folder-go').addEventListener('click', gotoFolder);
501
+ folderGoto.addEventListener('input', filterFolders);
502
+ folderGoto.addEventListener('keydown', (event) => {
503
+ if (event.key === 'Enter') {
504
+ event.preventDefault();
505
+ gotoFolder();
506
+ }
507
+ });
508
+ folderList.addEventListener('keydown', (event) => {
509
+ if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') return;
510
+ const items = [...folderList.querySelectorAll('.folder-item')];
511
+ if (!items.length) return;
512
+ event.preventDefault();
513
+ const current = items.indexOf(document.activeElement);
514
+ const next = event.key === 'ArrowDown' ? Math.min(items.length - 1, current + 1) : Math.max(0, current - 1);
515
+ items[current === -1 ? 0 : next].focus();
516
+ });
517
+ folderModal.addEventListener('click', (event) => {
518
+ if (event.target === folderModal) {
519
+ state.pendingInput = null;
520
+ closeFolderPicker();
521
+ }
522
+ });
523
+
524
+ $('#provider-new').addEventListener('click', () => openProviderForm());
525
+ $('#pf-cancel').addEventListener('click', closeProviderForm);
526
+ $('#pf-close').addEventListener('click', closeProviderForm);
527
+ $('#pf-test').addEventListener('click', testProvider);
528
+ providerForm.addEventListener('submit', submitProvider);
529
+ providerModal.addEventListener('click', (event) => {
530
+ if (event.target === providerModal) closeProviderForm();
531
+ });
532
+
533
+ wireAjustesNav();
534
+
535
+ $('#limits-save')?.addEventListener('click', async () => {
536
+ const body = {
537
+ maxRounds: Math.max(0, Number(limitsRoundsEl?.value) || 0),
538
+ maxTokens: Math.max(0, Number(limitsTokensEl?.value) || 0),
539
+ maxDurationMs: Math.max(0, Math.round((Number(limitsDurationEl?.value) || 0) * 1000)),
540
+ maxOutputTokens: Math.max(0, Number(limitsOutputEl?.value) || 0),
541
+ promptCaching: Boolean(limitsCachingEl?.checked),
542
+ };
543
+ try {
544
+ await api('/api/settings', { method: 'PUT', body });
545
+ refreshLimits();
546
+ setStatus('#limits-msg', 'guardado', 'ok');
547
+ } catch (error) {
548
+ setStatus('#limits-msg', error.message, 'err');
549
+ }
550
+ });
551
+
552
+ $('#tr-save')?.addEventListener('click', submitTranscribe);
553
+ trModeEl?.addEventListener('change', syncTranscribeMode);
554
+
555
+ trTestBtn?.addEventListener('click', toggleTrTest);
556
+
557
+ perfil.add?.addEventListener('click', () => addPerfilFieldRow());
558
+ perfil.save?.addEventListener('click', submitProfile);
559
+
560
+ if (mcpListEl) {
561
+ $('#mcp-new').addEventListener('click', () => openMcpForm());
562
+ $('#mcp-import-btn')?.addEventListener('click', importMcp);
563
+ $('#mcp-import')?.addEventListener('keydown', (event) => {
564
+ if (event.key === 'Enter') {
565
+ event.preventDefault();
566
+ importMcp();
567
+ }
568
+ });
569
+ $('#skill-import-btn')?.addEventListener('click', importSkill);
570
+ $('#skill-import')?.addEventListener('keydown', (event) => {
571
+ if (event.key === 'Enter') {
572
+ event.preventDefault();
573
+ importSkill();
574
+ }
575
+ });
576
+ $('#mcp-search-btn')?.addEventListener('click', searchMcp);
577
+ $('#mcp-search')?.addEventListener('keydown', (event) => {
578
+ if (event.key === 'Enter') {
579
+ event.preventDefault();
580
+ searchMcp();
581
+ }
582
+ });
583
+ $('#skill-search-btn')?.addEventListener('click', searchSkills);
584
+ $('#skill-search')?.addEventListener('keydown', (event) => {
585
+ if (event.key === 'Enter') {
586
+ event.preventDefault();
587
+ searchSkills();
588
+ }
589
+ });
590
+ refreshMcpCatalog();
591
+ refreshSkillCatalog();
592
+ $('#mcp-reload').addEventListener('click', () => refreshMcp({ connect: true }));
593
+ mcpModeEl?.addEventListener('change', async () => {
594
+ try {
595
+ await api('/api/settings', { method: 'PUT', body: { mcpMode: mcpModeEl.value } });
596
+ await refreshMcp({ connect: true });
597
+ } catch (error) {
598
+ toast(error.message);
599
+ }
600
+ });
601
+ const savePinnedMcp = async () => {
602
+ try {
603
+ await api('/api/settings', { method: 'PUT', body: { mcpPinned: String(mcpPinnedEl.value || '') } });
604
+ await refreshMcp({ connect: true });
605
+ } catch (error) {
606
+ toast(error.message);
607
+ }
608
+ };
609
+ $('#mcp-pinned-save')?.addEventListener('click', savePinnedMcp);
610
+ mcpPinnedEl?.addEventListener('keydown', (event) => {
611
+ if (event.key === 'Enter') {
612
+ event.preventDefault();
613
+ savePinnedMcp();
614
+ }
615
+ });
616
+ $('#tg-save')?.addEventListener('click', submitChannel);
617
+ $('#tg-test')?.addEventListener('click', testChannel);
618
+ $('#voz-save')?.addEventListener('click', submitVoz);
619
+ $('#voz-test')?.addEventListener('click', testVoz);
620
+ vozRateEl?.addEventListener('input', renderVozSliders);
621
+ vozPitchEl?.addEventListener('input', renderVozSliders);
622
+ $('#asesor-save')?.addEventListener('click', submitAsesor);
623
+ $('#asesor-test')?.addEventListener('click', testAsesor);
624
+ $('#mcp-close').addEventListener('click', closeMcpForm);
625
+ $('#mf-cancel').addEventListener('click', closeMcpForm);
626
+ $('#mf-transport').addEventListener('change', syncMcpFormFields);
627
+ mcpForm.addEventListener('submit', submitMcp);
628
+ mcpModal.addEventListener('click', (event) => {
629
+ if (event.target === mcpModal) closeMcpForm();
630
+ });
631
+
632
+ $('#skill-new').addEventListener('click', () => openSkillForm());
633
+ $('#skill-close').addEventListener('click', closeSkillForm);
634
+ $('#sf-cancel').addEventListener('click', closeSkillForm);
635
+ skillForm.addEventListener('submit', submitSkill);
636
+ skillModal.addEventListener('click', (event) => {
637
+ if (event.target === skillModal) closeSkillForm();
638
+ });
639
+
640
+ $('#memory-new')?.addEventListener('click', () => openMemoryForm());
641
+ $('#memory-close')?.addEventListener('click', closeMemoryForm);
642
+ $('#mem-cancel')?.addEventListener('click', closeMemoryForm);
643
+ memoryForm?.addEventListener('submit', submitMemory);
644
+ $('#memory-save-index')?.addEventListener('click', saveMemoryIndex);
645
+ memoryModal?.addEventListener('click', (event) => {
646
+ if (event.target === memoryModal) closeMemoryForm();
647
+ });
648
+
649
+ $('#sc-type')?.addEventListener('change', scheduleTypeFields);
650
+ $('#schedule-new')?.addEventListener('click', () => openScheduleForm());
651
+ $('#schedule-close')?.addEventListener('click', closeScheduleForm);
652
+ $('#sc-cancel')?.addEventListener('click', closeScheduleForm);
653
+ scheduleForm?.addEventListener('submit', submitSchedule);
654
+ scheduleModal?.addEventListener('click', (event) => {
655
+ if (event.target === scheduleModal) closeScheduleForm();
656
+ });
657
+ }
658
+
659
+ evalTasks.value = [
660
+ '{"id":"math","input":"¿Cuánto es 17 * 23? Respondé solo el número.","expect":{"equals":"391"}}',
661
+ '{"id":"regex","input":"Escribí una oración que contenga un número.","expect":{"regex":"\\\\d"}}',
662
+ ].join('\n');
663
+
664
+ evalRunBtn.addEventListener('click', runEval);
665
+
666
+ $('#runs-refresh').addEventListener('click', refreshRuns);
667
+
668
+ $('#file-preview-toggle')?.addEventListener('click', async () => {
669
+ const toggle = $('#file-preview-toggle');
670
+ const code = $('#file-code');
671
+ const stage = $('#file-stage');
672
+ const frame = $('#file-frame');
673
+ const render = $('#file-render');
674
+ const device = $('#file-device');
675
+ const maxButton = $('#file-max');
676
+ const current = filePreviewState.current;
677
+ if (!current) return;
678
+
679
+ filePreviewState.open = !filePreviewState.open;
680
+ const off = () => {
681
+ toggle.textContent = 'vista previa';
682
+ code.hidden = false;
683
+ stage.hidden = true;
684
+ render.hidden = true;
685
+ device.hidden = true;
686
+ maxButton.hidden = false;
687
+ $('#file-modal').classList.remove('preview-maximized');
688
+ };
689
+ if (!filePreviewState.open) return off();
690
+
691
+ if (filePreviewState.kind === 'markdown') {
692
+ render.innerHTML = renderMarkdown(filePreviewState.content);
693
+ code.hidden = true;
694
+ stage.hidden = true;
695
+ render.hidden = false;
696
+ device.hidden = true;
697
+ maxButton.hidden = false;
698
+ toggle.textContent = 'ver código';
699
+ return;
700
+ }
701
+
702
+ device.hidden = false;
703
+ maxButton.hidden = false;
704
+ if (!filePreviewState.loaded) {
705
+ const data = await api('/api/files/preview', {
706
+ method: 'POST',
707
+ body: { root: state.cwd, path: current.relPath },
708
+ }).catch((error) => ({ error: error.message }));
709
+ if (data?.error) {
710
+ setExplorerStatus(data.error, true);
711
+ filePreviewState.open = false;
712
+ return off();
713
+ }
714
+ frame.src = data.url;
715
+ filePreviewState.loaded = true;
716
+ filePreviewTools.applyDevice();
717
+ }
718
+ toggle.textContent = 'ver código';
719
+ code.hidden = true;
720
+ render.hidden = true;
721
+ stage.hidden = false;
722
+ });
723
+
724
+ document.querySelectorAll('.xtab').forEach((tab) => tab.addEventListener('click', () => setExplorerMode(tab.dataset.xp)));
725
+
726
+ restorePreviewMax = () => previewTools.setMax(false);
727
+
728
+ $('#preview-open')?.addEventListener('click', () => loadPreview(previewUrlInput.value));
729
+ previewUrlInput?.addEventListener('keydown', (event) => {
730
+ if (event.key === 'Enter') {
731
+ event.preventDefault();
732
+ loadPreview(previewUrlInput.value);
733
+ }
734
+ });
735
+ previewFrame?.addEventListener('load', () => {
736
+ if (previewFrame.src) setPreviewStatus(previewFrame.src);
737
+ });
738
+ $('#preview-tunnel')?.addEventListener('click', () => {
739
+ const match = String(previewUrlInput.value).match(/:(\d+)/);
740
+ const port = match ? Number(match[1]) : 0;
741
+ if (!port) return setPreviewStatus('Poné una URL con puerto (p. ej. http://127.0.0.1:5173).', true);
742
+ openRemoteTunnel(port);
743
+ });
744
+ $('#preview-close')?.addEventListener('click', async () => {
745
+ await api('/api/preview/tunnel', { method: 'DELETE' }).catch(() => null);
746
+ setTunnelUi(null);
747
+ setPreviewStatus('acceso remoto cerrado.');
748
+ });
749
+ $('#preview-device')?.addEventListener('change', (event) => storage.set(PREVIEW_DEVICE_KEY, event.target.value));
750
+
751
+ // Restaurar lo tipeado y el tamano elegido: en la tablet la pagina se recarga
752
+ // sola (iOS la descarta en background) y antes se perdia todo el estado.
753
+ (function restorePreview() {
754
+ const savedDevice = storage.get(PREVIEW_DEVICE_KEY, '');
755
+ if (savedDevice && $('#preview-device')) $('#preview-device').value = savedDevice;
756
+ previewTools.applyDevice();
757
+ const savedUrl = storage.get(PREVIEW_URL_KEY, '');
758
+ if (savedUrl) previewUrlInput.value = savedUrl;
759
+ syncPreviewTunnel({ load: true });
760
+ })();
761
+
762
+ $('#procs-detect')?.addEventListener('click', detectProcs);
763
+
764
+ gitFilterEl?.addEventListener('input', () => {
765
+ if (explorerMode === 'git' && gitEntries.length) renderGitList();
766
+ });
767
+
768
+ $('#explorer-handle')?.addEventListener('click', () => setExplorerOpen(true));
769
+ $('#explorer-close').addEventListener('click', () => setExplorerOpen(false));
770
+ $('#explorer-refresh').addEventListener('click', refreshExplorerMode);
771
+ explorerFilter.addEventListener('input', applyTreeFilter);
772
+ $('#file-close').addEventListener('click', () => $('#file-modal').close());
773
+ $('#file-modal').addEventListener('click', (event) => {
774
+ if (event.target === $('#file-modal')) $('#file-modal').close();
775
+ });
776
+ $('#git-close')?.addEventListener('click', () => gitModalEl?.close());
777
+ gitModalEl?.addEventListener('click', (event) => {
778
+ if (event.target === gitModalEl) gitModalEl.close();
779
+ });
780
+
781
+ try {
782
+ setExplorerOpen(storage.get('tamnora-explorer-open') === '1');
783
+ } catch {
784
+ setExplorerOpen(false);
785
+ }
786
+
787
+ $('#update-run')?.addEventListener('click', runUpdate);
788
+ $('#update-later')?.addEventListener('click', dismissUpdate);
789
+
790
+ if (brandVerEl) {
791
+ brandVerEl.title = 'buscar actualización';
792
+ brandVerEl.classList.add('ver-action');
793
+ brandVerEl.addEventListener('click', forceUpdateCheck);
794
+ }
795
+
796
+ /* --- boot ---------------------------------------------------------------- */
797
+
801
798
  async function boot() {
802
- wireModelPicker();
803
- const auth = await api('/api/auth/me').catch(() => null);
804
- if (auth?.enabled && !auth.authenticated) {
805
- location.replace('/landing');
806
- return;
807
- }
808
- setCsrfToken(auth?.csrf || '');
809
- document.documentElement.dataset.auth = 'ok';
810
-
811
- const health = await api('/api/health').catch(() => null);
812
- if (health?.cwd) {
813
- document.title = `Tamnora Console · ${health.cwd}`;
814
- setCwd(health.cwd);
815
- }
816
- if (health?.home) state.home = health.home;
817
- if (health?.version) {
818
- const ver = document.querySelector('.brand .ver');
819
- if (ver) ver.textContent = `v${health.version}`;
820
- }
821
- refreshUpdate();
822
- await refreshProviders();
823
- await loadModes();
824
- await loadModels();
825
- await refreshTranscribe();
826
- // La config de voz se carga al boot (sin `await`, para no bloquear con el
827
- // fetch del catálogo de voces): si no, `voz.settings.enabled` queda en el
828
- // default `false` hasta abrir Ajustes y la narración no arranca al entrar.
829
- refreshVoz();
830
- await reconcile();
831
- await restoreTabs();
832
- renderAlertSignals();
833
- followEvents();
834
- watchLifecycle();
835
- // Latido de presencia: mantiene frescas las pestañas abiertas (TTL del server).
836
- setInterval(sendPresence, 30000);
837
- }
838
- boot();
799
+ wireModelPicker();
800
+ const auth = await api('/api/auth/me').catch(() => null);
801
+ if (auth?.enabled && !auth.authenticated) {
802
+ location.replace('/landing');
803
+ return;
804
+ }
805
+ setCsrfToken(auth?.csrf || '');
806
+ document.documentElement.dataset.auth = 'ok';
807
+
808
+ const health = await api('/api/health').catch(() => null);
809
+ if (health?.cwd) {
810
+ document.title = `Tamnora Console · ${health.cwd}`;
811
+ setCwd(health.cwd);
812
+ }
813
+ if (health?.home) state.home = health.home;
814
+ if (health?.version) {
815
+ const ver = document.querySelector('.brand .ver');
816
+ if (ver) ver.textContent = `v${health.version}`;
817
+ }
818
+ refreshUpdate();
819
+ await refreshProviders();
820
+ await loadModes();
821
+ await loadModels();
822
+ await refreshTranscribe();
823
+ // La config de voz se carga al boot (sin `await`, para no bloquear con el
824
+ // fetch del catálogo de voces): si no, `voz.settings.enabled` queda en el
825
+ // default `false` hasta abrir Ajustes y la narración no arranca al entrar.
826
+ refreshVoz();
827
+ await reconcile();
828
+ await restoreTabs();
829
+ renderAlertSignals();
830
+ followEvents();
831
+ watchLifecycle();
832
+ // Latido de presencia: mantiene frescas las pestañas abiertas (TTL del server).
833
+ setInterval(sendPresence, 30000);
834
+ }
835
+ boot();