@danieltmn/tamnora 0.24.1 → 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.
- package/CHANGELOG.md +21 -0
- package/harness/packages/server/public/app.js +833 -834
- package/harness/packages/server/public/appearance.js +4 -0
- package/harness/packages/server/public/boot.js +49 -0
- package/harness/packages/server/public/chat.js +17 -0
- package/harness/packages/server/public/chrome.js +855 -852
- package/harness/packages/server/public/commands.js +448 -403
- package/harness/packages/server/public/core.js +229 -216
- package/harness/packages/server/public/favicon.ico +0 -0
- package/harness/packages/server/public/icons/icon-128x128.png +0 -0
- package/harness/packages/server/public/icons/icon-144x144.png +0 -0
- package/harness/packages/server/public/icons/icon-152x152.png +0 -0
- package/harness/packages/server/public/icons/icon-192x192.png +0 -0
- package/harness/packages/server/public/icons/icon-384x384.png +0 -0
- package/harness/packages/server/public/icons/icon-512x512.png +0 -0
- package/harness/packages/server/public/icons/icon-72x72.png +0 -0
- package/harness/packages/server/public/icons/icon-96x96.png +0 -0
- package/harness/packages/server/public/icons/logo.png +0 -0
- package/harness/packages/server/public/icons/logo_dark.png +0 -0
- package/harness/packages/server/public/icons/logo_light.png +0 -0
- package/harness/packages/server/public/icons/tamnora.ico +0 -0
- package/harness/packages/server/public/index.html +72 -101
- package/harness/packages/server/public/landing/boot.js +16 -0
- package/harness/packages/server/public/landing/index.html +123 -59
- package/harness/packages/server/public/landing/landing.js +11 -9
- package/harness/packages/server/public/logo.png +0 -0
- package/harness/packages/server/public/markdown.js +1 -1
- package/harness/packages/server/public/settings.js +1155 -1080
- package/harness/packages/server/public/settingsNav.js +81 -0
- package/harness/packages/server/public/styles.css +2818 -2515
- package/harness/packages/server/public/system.js +495 -495
- package/harness/packages/server/src/lib/appearance.js +4 -0
- package/harness/packages/server/src/lib/restart.js +20 -0
- package/harness/packages/server/src/lib/securityHeaders.js +53 -0
- package/harness/packages/server/src/routes/reload.js +46 -0
- package/harness/packages/server/src/server.js +6 -1
- package/harness/packages/server/test/accessibility.test.js +72 -0
- package/harness/packages/server/test/appearance.test.js +4 -0
- package/harness/packages/server/test/consoleModules.test.js +133 -57
- package/harness/packages/server/test/consoleTabs.test.js +228 -0
- package/harness/packages/server/test/docsSync.test.js +73 -0
- package/harness/packages/server/test/landingLogin.test.js +89 -0
- package/harness/packages/server/test/markdownSecurity.test.js +80 -0
- package/harness/packages/server/test/reload.test.js +95 -0
- package/harness/packages/server/test/securityHeaders.test.js +109 -0
- package/harness/packages/server/test/settingsNav.test.js +90 -0
- package/harness/packages/server/test/themeCss.js +110 -0
- package/harness/packages/server/test/themes.test.js +72 -0
- package/harness/scripts/restart.mjs +101 -0
- package/package.json +1 -1
|
@@ -1,403 +1,448 @@
|
|
|
1
|
-
import {
|
|
2
|
-
INIT_PROMPT,
|
|
3
|
-
activePane,
|
|
4
|
-
addCompactCard,
|
|
5
|
-
addMessage,
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
openFolderPicker,
|
|
19
|
-
openSettingsModal,
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
{ id: '
|
|
36
|
-
{ id: '
|
|
37
|
-
{ id: '
|
|
38
|
-
{ id: '
|
|
39
|
-
{ id: '
|
|
40
|
-
{ id: '
|
|
41
|
-
{ id: '
|
|
42
|
-
{ id: '
|
|
43
|
-
{ id: '
|
|
44
|
-
{ id: '
|
|
45
|
-
{ id: '
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
palette.
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
palette.
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
['/
|
|
148
|
-
]
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
export function
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
case '
|
|
205
|
-
const cwd =
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
})
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
if (out.
|
|
233
|
-
else if (out.
|
|
234
|
-
else notify(`Puerto ${port}:
|
|
235
|
-
return { handled: true };
|
|
236
|
-
}
|
|
237
|
-
case '
|
|
238
|
-
|
|
239
|
-
if (!
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
await api(`/api/sessions/${state.sessionId}`, { method: '
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
case '
|
|
258
|
-
if (!state.sessionId) return needSession();
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
case '
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
})
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
//
|
|
297
|
-
//
|
|
298
|
-
|
|
299
|
-
scrollMessages();
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
if (!
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
return
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
1
|
+
import {
|
|
2
|
+
INIT_PROMPT,
|
|
3
|
+
activePane,
|
|
4
|
+
addCompactCard,
|
|
5
|
+
addMessage,
|
|
6
|
+
autoGrowChatInput,
|
|
7
|
+
basename,
|
|
8
|
+
chatInput,
|
|
9
|
+
finishCompactCard,
|
|
10
|
+
refreshSessions,
|
|
11
|
+
scrollMessages,
|
|
12
|
+
sendMessage,
|
|
13
|
+
} from './chat.js';
|
|
14
|
+
import {
|
|
15
|
+
activateTab,
|
|
16
|
+
createSessionIn,
|
|
17
|
+
cycleTheme,
|
|
18
|
+
openFolderPicker,
|
|
19
|
+
openSettingsModal,
|
|
20
|
+
openSettingsPanel,
|
|
21
|
+
setSidebarOpen,
|
|
22
|
+
toggleMode,
|
|
23
|
+
} from './chrome.js';
|
|
24
|
+
import { $, api, clear, el, showConfirm, state } from './core.js';
|
|
25
|
+
import { setExplorerOpen } from './explorer.js';
|
|
26
|
+
import { renderMarkdown } from './markdown.js';
|
|
27
|
+
import { openTab, syncInterruptBtn } from './sessions.js';
|
|
28
|
+
import { filterSettings } from './settingsNav.js';
|
|
29
|
+
import { waitServerVersion } from './system.js';
|
|
30
|
+
import { reproducirVoz } from './voice.js';
|
|
31
|
+
|
|
32
|
+
/* --- command palette (Ctrl/Cmd+K) ----------------------------------------- */
|
|
33
|
+
|
|
34
|
+
export const PALETTE_COMMANDS = [
|
|
35
|
+
{ id: 'new', label: 'Nueva sesión en otra carpeta', hint: '/new', run: () => openFolderPicker() },
|
|
36
|
+
{ id: 'init', label: 'Analizar el proyecto y crear AGENTS.md', hint: '/init', run: () => runSlash('init') },
|
|
37
|
+
{ id: 'resumen', label: 'Resumir la conversación', hint: '/resumen', run: () => runSlash('resumen') },
|
|
38
|
+
{ id: 'audioresumen', label: 'Resumen como audio', hint: '/audioresumen', run: () => runSlash('audioresumen') },
|
|
39
|
+
{ id: 'compact', label: 'Compactar la conversación', hint: '/compact', run: () => runSlash('compact') },
|
|
40
|
+
{ id: 'clear', label: 'Limpiar la conversación', hint: '/clear', run: () => runSlash('clear') },
|
|
41
|
+
{ id: 'mode', label: 'Cambiar Plan / Build', hint: 'Ctrl+.', run: () => toggleMode() },
|
|
42
|
+
{ id: 'settings', label: 'Modelo, modo y auto (ventana rápida)', run: () => openSettingsModal() },
|
|
43
|
+
{ id: 'providers', label: 'Gestionar proveedores', run: () => activateTab('providers') },
|
|
44
|
+
{ id: 'ajustes', label: 'Abrir ajustes', run: () => activateTab('ajustes') },
|
|
45
|
+
{ id: 'explorer', label: 'Mostrar u ocultar archivos', run: () => setExplorerOpen(!state.explorerOpen) },
|
|
46
|
+
{ id: 'sidebar', label: 'Mostrar u ocultar sesiones', run: () => setSidebarOpen(!state.sidebarOpen) },
|
|
47
|
+
{ id: 'theme', label: 'Cambiar tema', run: () => cycleTheme() },
|
|
48
|
+
{ id: 'serverreload', label: 'Reiniciar el servidor', hint: '/serverreload', run: () => runSlash('serverreload') },
|
|
49
|
+
{ id: 'bridgereload', label: 'Abrir el puente remoto', hint: '/bridgereload', run: () => runSlash('bridgereload') },
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
export const palette = {
|
|
53
|
+
modal: $('#palette-modal'),
|
|
54
|
+
input: $('#palette-input'),
|
|
55
|
+
list: $('#palette-list'),
|
|
56
|
+
items: [],
|
|
57
|
+
index: 0,
|
|
58
|
+
timer: null,
|
|
59
|
+
results: { sessions: [], messages: [] },
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export function runSlash(name) {
|
|
63
|
+
chatInput.value = `/${name}`;
|
|
64
|
+
sendMessage();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function paletteItems(query) {
|
|
68
|
+
const q = query.trim().toLowerCase();
|
|
69
|
+
const items = PALETTE_COMMANDS.filter(
|
|
70
|
+
(c) => !q || c.label.toLowerCase().includes(q) || c.id.includes(q) || (c.hint ?? '').toLowerCase().includes(q)
|
|
71
|
+
).map((c) => ({ label: c.label, hint: c.hint || 'comando', run: c.run }));
|
|
72
|
+
|
|
73
|
+
if (q) {
|
|
74
|
+
for (const section of filterSettings(q).slice(0, 6)) {
|
|
75
|
+
items.push({ label: `Ajustes · ${section.label}`, hint: section.groupLabel, run: () => openSettingsPanel(section.id) });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
for (const session of palette.results.sessions) {
|
|
80
|
+
items.push({
|
|
81
|
+
label: session.title || '(sin título)',
|
|
82
|
+
hint: session.cwd ? basename(session.cwd) : 'sesión',
|
|
83
|
+
run: () => openTab(session.id),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
for (const message of palette.results.messages) {
|
|
87
|
+
items.push({
|
|
88
|
+
label: message.snippet || '(mensaje)',
|
|
89
|
+
hint: `${message.role} · ${message.title || ''}`,
|
|
90
|
+
run: () => {
|
|
91
|
+
openTab(message.sessionId);
|
|
92
|
+
setTimeout(() => document.getElementById(message.id)?.scrollIntoView({ block: 'center' }), 450);
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return items;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function setPaletteIndex(index) {
|
|
100
|
+
const count = palette.items.length;
|
|
101
|
+
if (!count) {
|
|
102
|
+
palette.index = 0;
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
palette.index = ((index % count) + count) % count;
|
|
106
|
+
[...palette.list.children].forEach((child, i) => child.classList.toggle('active', i === palette.index));
|
|
107
|
+
palette.list.children[palette.index]?.scrollIntoView({ block: 'nearest' });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function renderPalette(query) {
|
|
111
|
+
palette.items = paletteItems(query);
|
|
112
|
+
palette.list.replaceChildren();
|
|
113
|
+
if (!palette.items.length) {
|
|
114
|
+
palette.list.appendChild(el('li', 'palette-empty', 'sin resultados'));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
palette.items.forEach((item, i) => {
|
|
118
|
+
const li = el('li', 'palette-item');
|
|
119
|
+
const main = el('div', 'palette-item-main');
|
|
120
|
+
main.appendChild(el('span', 'palette-item-label', item.label));
|
|
121
|
+
if (item.hint) main.appendChild(el('span', 'palette-item-hint', item.hint));
|
|
122
|
+
li.appendChild(main);
|
|
123
|
+
li.addEventListener('mousemove', () => setPaletteIndex(i));
|
|
124
|
+
li.addEventListener('click', () => runPaletteItem(item));
|
|
125
|
+
palette.list.appendChild(li);
|
|
126
|
+
});
|
|
127
|
+
setPaletteIndex(0);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function runPaletteItem(item) {
|
|
131
|
+
palette.modal?.close();
|
|
132
|
+
setTimeout(() => item.run(), 0);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function openPalette() {
|
|
136
|
+
if (!palette.modal) return;
|
|
137
|
+
palette.input.value = '';
|
|
138
|
+
palette.results = { sessions: [], messages: [] };
|
|
139
|
+
renderPalette('');
|
|
140
|
+
palette.modal.showModal();
|
|
141
|
+
palette.input.focus();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/* --- slash commands ------------------------------------------------------- */
|
|
145
|
+
|
|
146
|
+
export const COMMANDS = [
|
|
147
|
+
['/new', 'nueva sesión (elegís carpeta)'],
|
|
148
|
+
['/init', 'analizar el proyecto y crear/actualizar AGENTS.md'],
|
|
149
|
+
['/goal <texto>', 'fijar el objetivo de la sesión'],
|
|
150
|
+
['/resumen', 'resumir la conversación (sin borrarla)'],
|
|
151
|
+
['/audioresumen', 'el resumen como audio'],
|
|
152
|
+
['/compact', 'resumir la conversación y liberar contexto'],
|
|
153
|
+
['/clear', 'borrar los mensajes de esta sesión'],
|
|
154
|
+
['/cwd', 'mostrar la carpeta de trabajo'],
|
|
155
|
+
['/port <puerto>', 'ver si un puerto está activo (y qué PID lo usa)'],
|
|
156
|
+
['/portkill <puerto>', 'matar el proceso que escucha en un puerto'],
|
|
157
|
+
['/serverreload', 'reiniciar el servidor (la consola se reconecta sola)'],
|
|
158
|
+
['/bridgereload', 'abrir o reiniciar el puente remoto (tailscale)'],
|
|
159
|
+
['/help', 'mostrar esta ayuda'],
|
|
160
|
+
];
|
|
161
|
+
|
|
162
|
+
export function notify(markdown, kind = 'notice') {
|
|
163
|
+
const message = addMessage('assistant', '');
|
|
164
|
+
// `kind` puede traer varias clases ("notice err"): classList.add no acepta
|
|
165
|
+
// espacios, asi que se agregan una por una.
|
|
166
|
+
for (const cls of String(kind).split(/\s+/).filter(Boolean)) message.textEl.classList.add(cls);
|
|
167
|
+
message.textEl.innerHTML = renderMarkdown(markdown);
|
|
168
|
+
scrollMessages();
|
|
169
|
+
return message;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Aviso flotante para eventos de fondo (un permiso en otra pestana, un run que
|
|
174
|
+
* arranco desde el celular).
|
|
175
|
+
*
|
|
176
|
+
* NO usar `notify` para esto: esa escribe un mensaje en la conversacion del pane
|
|
177
|
+
* activo, asi que un permiso caido en otra pestana terminaba-chatrandeando en
|
|
178
|
+
* el chat que estabas leyendo.
|
|
179
|
+
*/
|
|
180
|
+
export function toast(text) {
|
|
181
|
+
const host = $('#toasts');
|
|
182
|
+
if (!host) return;
|
|
183
|
+
const item = el('div', 'toast', text);
|
|
184
|
+
host.appendChild(item);
|
|
185
|
+
const timer = setTimeout(() => item.remove(), 6000);
|
|
186
|
+
item.addEventListener('click', () => {
|
|
187
|
+
clearTimeout(timer);
|
|
188
|
+
item.remove();
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function needSession() {
|
|
193
|
+
notify('Abrí o creá una sesión primero (`/new`).', 'notice err');
|
|
194
|
+
return { handled: true };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export async function runCommand(name, arg) {
|
|
198
|
+
switch (name) {
|
|
199
|
+
case 'help':
|
|
200
|
+
case 'ayuda':
|
|
201
|
+
notify(`### Comandos\n${COMMANDS.map(([cmd, desc]) => `- \`${cmd}\` — ${desc}`).join('\n')}`);
|
|
202
|
+
return { handled: true };
|
|
203
|
+
case 'new':
|
|
204
|
+
case 'nueva': {
|
|
205
|
+
const cwd = state.sessionId ? state.cwd : '';
|
|
206
|
+
if (cwd) {
|
|
207
|
+
const created = await createSessionIn(cwd);
|
|
208
|
+
if (created?.error) notify(`No se pudo crear la sesión: ${created.error}`, 'notice err');
|
|
209
|
+
} else {
|
|
210
|
+
openFolderPicker();
|
|
211
|
+
}
|
|
212
|
+
return { handled: true };
|
|
213
|
+
}
|
|
214
|
+
case 'init':
|
|
215
|
+
return { handled: false, input: INIT_PROMPT };
|
|
216
|
+
case 'cwd': {
|
|
217
|
+
const cwd = $('#rail-cwd').textContent;
|
|
218
|
+
notify(`Carpeta de trabajo: \`${cwd}\``);
|
|
219
|
+
return { handled: true };
|
|
220
|
+
}
|
|
221
|
+
case 'portkill':
|
|
222
|
+
case 'kill': {
|
|
223
|
+
const port = Number(arg);
|
|
224
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
225
|
+
notify('Uso: `/portkill <puerto>`, por ejemplo `/portkill 3000`.', 'notice err');
|
|
226
|
+
return { handled: true };
|
|
227
|
+
}
|
|
228
|
+
const out = await api('/api/ports/kill', { method: 'POST', body: { port } }).catch((error) => ({
|
|
229
|
+
error: error.message,
|
|
230
|
+
}));
|
|
231
|
+
if (out.error) notify(`No se pudo matar el puerto ${port}: ${out.error}`, 'notice err');
|
|
232
|
+
else if (!out.pids.length) notify(`No hay ningún proceso escuchando en el puerto ${port}.`);
|
|
233
|
+
else if (out.killed.length) notify(`Puerto ${port}: maté el PID ${out.killed.join(', ')}.`);
|
|
234
|
+
else notify(`Puerto ${port}: no se pudo matar (PID ${out.errors.join(', ')}).`, 'notice err');
|
|
235
|
+
return { handled: true };
|
|
236
|
+
}
|
|
237
|
+
case 'port': {
|
|
238
|
+
const port = Number(arg);
|
|
239
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
240
|
+
notify('Uso: `/port <puerto>`, por ejemplo `/port 3000`.', 'notice err');
|
|
241
|
+
return { handled: true };
|
|
242
|
+
}
|
|
243
|
+
const out = await api(`/api/ports/${port}`).catch((error) => ({ error: error.message }));
|
|
244
|
+
if (out.error) notify(`No se pudo consultar el puerto ${port}: ${out.error}`, 'notice err');
|
|
245
|
+
else if (out.listening) notify(`Puerto ${port}: **activo** (PID ${out.pids.join(', ')}).`);
|
|
246
|
+
else notify(`Puerto ${port}: nada escuchando.`);
|
|
247
|
+
return { handled: true };
|
|
248
|
+
}
|
|
249
|
+
case 'clear':
|
|
250
|
+
case 'limpiar': {
|
|
251
|
+
if (!state.sessionId) return needSession();
|
|
252
|
+
await api(`/api/sessions/${state.sessionId}/messages`, { method: 'DELETE' }).catch(() => null);
|
|
253
|
+
clear(activePane);
|
|
254
|
+
activePane.appendChild(el('div', 'empty', 'Sesión vacía.'));
|
|
255
|
+
return { handled: true };
|
|
256
|
+
}
|
|
257
|
+
case 'goal': {
|
|
258
|
+
if (!state.sessionId) return needSession();
|
|
259
|
+
if (!arg) {
|
|
260
|
+
const data = await api(`/api/sessions/${state.sessionId}`).catch(() => null);
|
|
261
|
+
notify(data?.session?.goal ? `Objetivo actual: **${data.session.goal}**` : 'No hay objetivo fijado.');
|
|
262
|
+
return { handled: true };
|
|
263
|
+
}
|
|
264
|
+
await api(`/api/sessions/${state.sessionId}`, { method: 'PUT', body: { goal: arg } }).catch(() => null);
|
|
265
|
+
notify(`Objetivo de la sesión: **${arg}**`);
|
|
266
|
+
return { handled: true };
|
|
267
|
+
}
|
|
268
|
+
case 'resumen':
|
|
269
|
+
case 'resume': {
|
|
270
|
+
if (!state.sessionId) return needSession();
|
|
271
|
+
notify('Resumiendo la conversación…');
|
|
272
|
+
const out = await api(`/api/sessions/${state.sessionId}/summary`, { method: 'POST' }).catch((error) => ({
|
|
273
|
+
error: error.message,
|
|
274
|
+
}));
|
|
275
|
+
if (out.error) notify(`No se pudo resumir: ${out.error}`, 'notice err');
|
|
276
|
+
else notify(`**Resumen**\n\n${out.summary}`);
|
|
277
|
+
return { handled: true };
|
|
278
|
+
}
|
|
279
|
+
case 'audioresumen':
|
|
280
|
+
case 'audio-resumen': {
|
|
281
|
+
if (!state.sessionId) return needSession();
|
|
282
|
+
notify('Preparando el resumen…');
|
|
283
|
+
const out = await api(`/api/sessions/${state.sessionId}/summary`, { method: 'POST' }).catch((error) => ({
|
|
284
|
+
error: error.message,
|
|
285
|
+
}));
|
|
286
|
+
if (out.error) {
|
|
287
|
+
notify(`No se pudo resumir: ${out.error}`, 'notice err');
|
|
288
|
+
return { handled: true };
|
|
289
|
+
}
|
|
290
|
+
notify(`**Resumen**\n\n${out.summary}`);
|
|
291
|
+
reproducirVoz(out.summary, { label: 'resumen' }).catch((error) => notify(`No se pudo reproducir el audio: ${error.message}`, 'notice err'));
|
|
292
|
+
return { handled: true };
|
|
293
|
+
}
|
|
294
|
+
case 'compact': {
|
|
295
|
+
if (!state.sessionId) return needSession();
|
|
296
|
+
// Animación en el pane: el corte queda a la vista con un loader mientras
|
|
297
|
+
// el modelo resume (puede tardar), y se completa con el resumen.
|
|
298
|
+
const card = addCompactCard(activePane, { running: true });
|
|
299
|
+
scrollMessages();
|
|
300
|
+
const out = await api(`/api/sessions/${state.sessionId}/compact`, { method: 'POST' }).catch((error) => ({
|
|
301
|
+
error: error.message,
|
|
302
|
+
}));
|
|
303
|
+
if (out.error) {
|
|
304
|
+
card?.remove();
|
|
305
|
+
notify(`No se pudo compactar: ${out.error}`, 'notice err');
|
|
306
|
+
return { handled: true };
|
|
307
|
+
}
|
|
308
|
+
// No borramos el pane: el historial queda a la vista y marcamos el corte
|
|
309
|
+
// con la línea + el resumen que ahora ve el modelo.
|
|
310
|
+
finishCompactCard(card, out.summary);
|
|
311
|
+
scrollMessages();
|
|
312
|
+
refreshSessions();
|
|
313
|
+
return { handled: true };
|
|
314
|
+
}
|
|
315
|
+
case 'serverreload':
|
|
316
|
+
case 'servereload':
|
|
317
|
+
case 'restart':
|
|
318
|
+
case 'reiniciar': {
|
|
319
|
+
if (
|
|
320
|
+
!(await showConfirm('¿Reiniciar el servidor? La consola se reconecta sola en unos segundos.', {
|
|
321
|
+
title: 'Reiniciar servidor',
|
|
322
|
+
confirmLabel: 'reiniciar',
|
|
323
|
+
danger: true,
|
|
324
|
+
}))
|
|
325
|
+
) {
|
|
326
|
+
return { handled: true };
|
|
327
|
+
}
|
|
328
|
+
notify('Reiniciando el servidor…');
|
|
329
|
+
const out = await api('/api/server/restart', { method: 'POST' }).catch((error) => ({ error: error.message }));
|
|
330
|
+
if (out.error) {
|
|
331
|
+
notify(`No se pudo reiniciar: ${out.error}`, 'notice err');
|
|
332
|
+
return { handled: true };
|
|
333
|
+
}
|
|
334
|
+
const version = await waitServerVersion();
|
|
335
|
+
if (version) location.reload();
|
|
336
|
+
else notify('No pude confirmar el reinicio. Recargá la página en unos segundos.', 'notice err');
|
|
337
|
+
return { handled: true };
|
|
338
|
+
}
|
|
339
|
+
case 'bridgereload':
|
|
340
|
+
case 'bridge': {
|
|
341
|
+
notify('Abriendo el puente remoto (tailscale)…');
|
|
342
|
+
const out = await api('/api/bridge/reload', { method: 'POST' }).catch((error) => ({ error: error.message }));
|
|
343
|
+
if (out.error) notify(`No pude abrir el puente: ${out.error}`, 'notice err');
|
|
344
|
+
else notify(`Puente activo: **${out.url}**`);
|
|
345
|
+
return { handled: true };
|
|
346
|
+
}
|
|
347
|
+
default:
|
|
348
|
+
notify(`Comando desconocido: \`/${name}\`. Probá \`/help\`.`, 'notice err');
|
|
349
|
+
return { handled: true };
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/* --- menú de comandos "/" ------------------------------------------------- */
|
|
354
|
+
|
|
355
|
+
export const slashMenuEl = $('#slash-menu');
|
|
356
|
+
export let slashMatches = [];
|
|
357
|
+
export let slashIndex = 0;
|
|
358
|
+
|
|
359
|
+
export function slashCommandEntries() {
|
|
360
|
+
return COMMANDS.map(([cmd, desc]) => {
|
|
361
|
+
const name = cmd.split(' ')[0];
|
|
362
|
+
const takesArgs = cmd.includes(' ');
|
|
363
|
+
return { name, desc, insert: takesArgs ? `${name} ` : name, takesArgs };
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export function hideSlashMenu() {
|
|
368
|
+
if (!slashMenuEl) return;
|
|
369
|
+
slashMenuEl.hidden = true;
|
|
370
|
+
slashMenuEl.replaceChildren();
|
|
371
|
+
slashMatches = [];
|
|
372
|
+
slashIndex = 0;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export function renderSlashMenu() {
|
|
376
|
+
if (!slashMenuEl) return;
|
|
377
|
+
slashMenuEl.replaceChildren();
|
|
378
|
+
slashMatches.forEach((entry, index) => {
|
|
379
|
+
const item = el('button', `slash-item${index === slashIndex ? ' active' : ''}`);
|
|
380
|
+
item.type = 'button';
|
|
381
|
+
item.append(el('span', 'slash-cmd', entry.name), el('span', 'slash-desc', entry.desc));
|
|
382
|
+
// mousedown (no click) para no perder el foco del textarea antes de elegir.
|
|
383
|
+
item.addEventListener('mousedown', (event) => {
|
|
384
|
+
event.preventDefault();
|
|
385
|
+
chooseSlash(index);
|
|
386
|
+
});
|
|
387
|
+
slashMenuEl.appendChild(item);
|
|
388
|
+
});
|
|
389
|
+
slashMenuEl.hidden = false;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
export function updateSlashMenu() {
|
|
393
|
+
if (!slashMenuEl) return;
|
|
394
|
+
const value = chatInput.value;
|
|
395
|
+
if (!value.startsWith('/') || /\s/.test(value)) return hideSlashMenu();
|
|
396
|
+
const query = value.slice(1).toLowerCase();
|
|
397
|
+
slashMatches = slashCommandEntries().filter(
|
|
398
|
+
(entry) =>
|
|
399
|
+
entry.name.slice(1).toLowerCase().startsWith(query) || entry.desc.toLowerCase().includes(query)
|
|
400
|
+
);
|
|
401
|
+
if (!slashMatches.length) return hideSlashMenu();
|
|
402
|
+
if (slashIndex >= slashMatches.length) slashIndex = 0;
|
|
403
|
+
renderSlashMenu();
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
export function chooseSlash(index) {
|
|
407
|
+
const entry = slashMatches[index];
|
|
408
|
+
if (!entry) return;
|
|
409
|
+
chatInput.value = entry.insert;
|
|
410
|
+
chatInput.focus();
|
|
411
|
+
hideSlashMenu();
|
|
412
|
+
if (!entry.takesArgs) sendMessage();
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
export function handleSlashKey(event) {
|
|
416
|
+
if (!slashMenuEl || slashMenuEl.hidden || !slashMatches.length) return false;
|
|
417
|
+
if (event.key === 'ArrowDown') {
|
|
418
|
+
event.preventDefault();
|
|
419
|
+
slashIndex = (slashIndex + 1) % slashMatches.length;
|
|
420
|
+
renderSlashMenu();
|
|
421
|
+
return true;
|
|
422
|
+
}
|
|
423
|
+
if (event.key === 'ArrowUp') {
|
|
424
|
+
event.preventDefault();
|
|
425
|
+
slashIndex = (slashIndex - 1 + slashMatches.length) % slashMatches.length;
|
|
426
|
+
renderSlashMenu();
|
|
427
|
+
return true;
|
|
428
|
+
}
|
|
429
|
+
if (event.key === 'Escape') {
|
|
430
|
+
event.preventDefault();
|
|
431
|
+
hideSlashMenu();
|
|
432
|
+
return true;
|
|
433
|
+
}
|
|
434
|
+
if (event.key === 'Enter' || event.key === 'Tab') {
|
|
435
|
+
event.preventDefault();
|
|
436
|
+
chooseSlash(slashIndex);
|
|
437
|
+
return true;
|
|
438
|
+
}
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
export function onChatInputChanged() {
|
|
443
|
+
slashIndex = 0;
|
|
444
|
+
updateSlashMenu();
|
|
445
|
+
autoGrowChatInput();
|
|
446
|
+
syncInterruptBtn();
|
|
447
|
+
}
|
|
448
|
+
|