@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.
- package/CHANGELOG.md +14 -0
- package/harness/packages/server/public/app.js +831 -834
- package/harness/packages/server/public/appearance.js +4 -0
- package/harness/packages/server/public/boot.js +49 -0
- package/harness/packages/server/public/chrome.js +853 -852
- package/harness/packages/server/public/commands.js +448 -405
- 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 +2807 -2525
- package/harness/packages/server/public/system.js +2 -2
- 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,216 +1,229 @@
|
|
|
1
|
-
import { streamSse } from './runStream.js';
|
|
2
|
-
|
|
3
|
-
let csrfToken = '';
|
|
4
|
-
|
|
5
|
-
/** El token CSRF lo fija `boot()` tras leer `/api/auth/me`. */
|
|
6
|
-
export function setCsrfToken(token) {
|
|
7
|
-
csrfToken = token || '';
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export const $ = (sel) => document.querySelector(sel);
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
node
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
node.
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
)
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
1
|
+
import { streamSse } from './runStream.js';
|
|
2
|
+
|
|
3
|
+
let csrfToken = '';
|
|
4
|
+
|
|
5
|
+
/** El token CSRF lo fija `boot()` tras leer `/api/auth/me`. */
|
|
6
|
+
export function setCsrfToken(token) {
|
|
7
|
+
csrfToken = token || '';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const $ = (sel) => document.querySelector(sel);
|
|
11
|
+
|
|
12
|
+
/** `smooth` salvo que el sistema pida movimiento reducido. */
|
|
13
|
+
export function scrollBehavior() {
|
|
14
|
+
return window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const state = {
|
|
18
|
+
sessionId: null,
|
|
19
|
+
streaming: false,
|
|
20
|
+
providers: [],
|
|
21
|
+
active: { provider: '', model: '' },
|
|
22
|
+
// Catalogo de modelos del proveedor y su seleccion. Se cachea porque el
|
|
23
|
+
// catalogo son cientos de modelos (~591 KB) y `loadModels` corre en cada
|
|
24
|
+
// cambio de pestana: refetchearlo hacia parpadear la consola.
|
|
25
|
+
modelCatalog: [],
|
|
26
|
+
modelCatalogProvider: null,
|
|
27
|
+
modelSignature: null,
|
|
28
|
+
visibleModels: [],
|
|
29
|
+
pendingInput: null,
|
|
30
|
+
permissionCards: new Map(),
|
|
31
|
+
questionCards: new Map(),
|
|
32
|
+
attachments: [],
|
|
33
|
+
home: null,
|
|
34
|
+
cwd: '',
|
|
35
|
+
explorerOpen: false,
|
|
36
|
+
sidebarOpen: true,
|
|
37
|
+
panes: new Map(),
|
|
38
|
+
tabOrder: [],
|
|
39
|
+
activeTab: null,
|
|
40
|
+
queues: new Map(),
|
|
41
|
+
// Grupos de sesiones abiertos a mano. Arranca vacio y NO se persiste: el
|
|
42
|
+
// sidebar siempre empieza con todas las carpetas sin expandir.
|
|
43
|
+
expandedGroups: new Set(),
|
|
44
|
+
// Sesiones con un run vivo segun el canal global de eventos. El sidebar lo
|
|
45
|
+
// usa para el punto de actividad antes de abrir la sesion.
|
|
46
|
+
remoteActive: new Set(),
|
|
47
|
+
// Permisos esperando decision: askId -> { sessionId, name, args }. Vive fuera
|
|
48
|
+
// del stream a proposito: un cliente sin stream abierto igual tiene que poder
|
|
49
|
+
// ver que la IA esta esperando una autorizacion suya.
|
|
50
|
+
pendingAsks: new Map(),
|
|
51
|
+
// Formularios interactivos (`ask_user`) esperando respuesta: askId -> { sessionId }.
|
|
52
|
+
pendingQuestions: new Map(),
|
|
53
|
+
// Ponerlo en cola para reconciliar apenas termine la que esta en curso.
|
|
54
|
+
reconcileAgain: false,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/* --- DOM helpers --------------------------------------------------------- */
|
|
58
|
+
|
|
59
|
+
export function el(tag, className = '', text = '') {
|
|
60
|
+
const node = document.createElement(tag);
|
|
61
|
+
if (className) node.className = className;
|
|
62
|
+
if (text) node.textContent = text;
|
|
63
|
+
return node;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Item de lista vacia que dice que paso y que hacer, en vez de una linea gris suelta. */
|
|
67
|
+
export function emptyState(title, hint = '') {
|
|
68
|
+
const node = el('li', 'empty-state');
|
|
69
|
+
node.appendChild(el('strong', '', title));
|
|
70
|
+
if (hint) node.appendChild(el('span', 'muted', hint));
|
|
71
|
+
return node;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function button(label, onClick, className = 'btn') {
|
|
75
|
+
const node = el('button', className, label);
|
|
76
|
+
node.type = 'button';
|
|
77
|
+
node.addEventListener('click', onClick);
|
|
78
|
+
return node;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function clear(node) {
|
|
82
|
+
node.replaceChildren();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export const ICON_PATHS = {
|
|
86
|
+
x: ['M18 6 6 18', 'm6 6 12 12'],
|
|
87
|
+
'chevron-right': ['m9 18 6-6-6-6'],
|
|
88
|
+
'chevron-down': ['m6 9 6 6 6-6'],
|
|
89
|
+
folder: ['M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z'],
|
|
90
|
+
'folder-open': ['m6 14 1.45-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.55 6a2 2 0 0 1-1.94 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2'],
|
|
91
|
+
file: ['M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z', 'M14 2v4a2 2 0 0 0 2 2h4'],
|
|
92
|
+
'refresh-cw': ['M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8', 'M21 3v5h-5', 'M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16', 'M8 16H3v5'],
|
|
93
|
+
undo: ['M3 7v6h6', 'M3 13a9 9 0 1 1 2.64 6.36'],
|
|
94
|
+
trash: ['M3 6h18', 'M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6', 'M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2', 'M10 11v6', 'M14 11v6'],
|
|
95
|
+
'dots-vertical': ['M12 5h.01', 'M12 12h.01', 'M12 19h.01'],
|
|
96
|
+
'dots-horizontal': ['M5 12h.01', 'M12 12h.01', 'M19 12h.01'],
|
|
97
|
+
play: ['m6 4 14 8-14 8z'],
|
|
98
|
+
pause: ['M8 5v14', 'M16 5v14'],
|
|
99
|
+
square: ['M6 6h12v12H6z'],
|
|
100
|
+
copy: ['M8 8V5a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2h-3', 'M5 8h9a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-9a2 2 0 0 1 2-2Z'],
|
|
101
|
+
pencil: ['M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z', 'm15 5 4 4'],
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export function icon(name, size = 16, className = '') {
|
|
105
|
+
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
106
|
+
if (className) svg.setAttribute('class', className);
|
|
107
|
+
for (const [key, value] of Object.entries({ viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', 'stroke-width': '1.8', 'stroke-linecap': 'round', 'stroke-linejoin': 'round', width: String(size), height: String(size), 'aria-hidden': 'true' })) {
|
|
108
|
+
svg.setAttribute(key, value);
|
|
109
|
+
}
|
|
110
|
+
for (const d of ICON_PATHS[name] ?? []) {
|
|
111
|
+
const node = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
112
|
+
node.setAttribute('d', d);
|
|
113
|
+
svg.appendChild(node);
|
|
114
|
+
}
|
|
115
|
+
return svg;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Confirmacion accesible (reemplaza `window.confirm`): `<dialog>` modal con
|
|
120
|
+
* Escape, foco inicial en cancelar y respuesta booleana.
|
|
121
|
+
*/
|
|
122
|
+
export function showConfirm(
|
|
123
|
+
message,
|
|
124
|
+
{ title = 'Confirmar', confirmLabel = 'aceptar', cancelLabel = 'cancelar', danger = false } = {}
|
|
125
|
+
) {
|
|
126
|
+
return new Promise((resolve) => {
|
|
127
|
+
const dialog = document.createElement('dialog');
|
|
128
|
+
dialog.className = 'modal confirm-modal';
|
|
129
|
+
dialog.setAttribute('aria-label', title);
|
|
130
|
+
const card = el('div', 'modal-card');
|
|
131
|
+
card.append(el('h3', '', title), el('p', 'confirm-text', message));
|
|
132
|
+
const actions = el('div', 'confirm-actions');
|
|
133
|
+
const cancel = button(cancelLabel, () => dialog.close('cancel'), 'btn');
|
|
134
|
+
const ok = button(confirmLabel, () => dialog.close('confirm'), danger ? 'btn danger' : 'btn primary');
|
|
135
|
+
actions.append(cancel, ok);
|
|
136
|
+
card.append(actions);
|
|
137
|
+
dialog.append(card);
|
|
138
|
+
dialog.addEventListener('close', () => {
|
|
139
|
+
resolve(dialog.returnValue === 'confirm');
|
|
140
|
+
dialog.remove();
|
|
141
|
+
});
|
|
142
|
+
document.body.append(dialog);
|
|
143
|
+
dialog.showModal();
|
|
144
|
+
cancel.focus();
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/* --- HTTP / SSE ---------------------------------------------------------- */
|
|
149
|
+
|
|
150
|
+
// La cookie de sesion puede vencer con la pestana abierta: sin este interceptor
|
|
151
|
+
// la UI mostraba tostadas genericas en cada accion. Un 401 = a la pantalla de
|
|
152
|
+
// acceso, una sola vez.
|
|
153
|
+
let sessionExpired = false;
|
|
154
|
+
|
|
155
|
+
export async function api(path, { method = 'GET', body, headers = {} } = {}) {
|
|
156
|
+
const init = {};
|
|
157
|
+
if (body) init['Content-Type'] = 'application/json';
|
|
158
|
+
if (method !== 'GET' && csrfToken) init['x-csrf'] = csrfToken;
|
|
159
|
+
const res = await fetch(path, {
|
|
160
|
+
method,
|
|
161
|
+
headers: { ...init, ...headers },
|
|
162
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
163
|
+
});
|
|
164
|
+
if (!res.ok) {
|
|
165
|
+
let message = res.statusText;
|
|
166
|
+
try {
|
|
167
|
+
message = (await res.json()).error ?? message;
|
|
168
|
+
} catch {
|
|
169
|
+
// keep statusText
|
|
170
|
+
}
|
|
171
|
+
const error = new Error(message);
|
|
172
|
+
// El status importa: un 409 significa que otro cliente tiene el turno.
|
|
173
|
+
error.status = res.status;
|
|
174
|
+
if (res.status === 401 && !sessionExpired) {
|
|
175
|
+
sessionExpired = true;
|
|
176
|
+
location.assign('/landing');
|
|
177
|
+
}
|
|
178
|
+
throw error;
|
|
179
|
+
}
|
|
180
|
+
return res.json();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export const streamPost = (url, body, onEvent, signal) =>
|
|
184
|
+
streamSse(url, { method: 'POST', body, csrf: csrfToken, signal }, onEvent);
|
|
185
|
+
|
|
186
|
+
/** SSE GET de la consola (/api/sessions/:id/stream, /api/events). */
|
|
187
|
+
export const openStream = (url, opts, onEvent) => streamSse(url, opts, onEvent);
|
|
188
|
+
|
|
189
|
+
/* --- localStorage (todas las lecturas/escrituras pasan por aca) ------------ */
|
|
190
|
+
|
|
191
|
+
export const storage = {
|
|
192
|
+
get(key, fallback = null) {
|
|
193
|
+
try {
|
|
194
|
+
const raw = localStorage.getItem(key);
|
|
195
|
+
return raw === null ? fallback : raw;
|
|
196
|
+
} catch {
|
|
197
|
+
return fallback;
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
getJson(key, fallback) {
|
|
201
|
+
try {
|
|
202
|
+
const raw = localStorage.getItem(key);
|
|
203
|
+
return raw === null ? fallback : JSON.parse(raw);
|
|
204
|
+
} catch {
|
|
205
|
+
return fallback;
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
set(key, value) {
|
|
209
|
+
try {
|
|
210
|
+
localStorage.setItem(key, value);
|
|
211
|
+
} catch {
|
|
212
|
+
// storage unavailable
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
setJson(key, value) {
|
|
216
|
+
try {
|
|
217
|
+
localStorage.setItem(key, JSON.stringify(value));
|
|
218
|
+
} catch {
|
|
219
|
+
// storage unavailable
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
remove(key) {
|
|
223
|
+
try {
|
|
224
|
+
localStorage.removeItem(key);
|
|
225
|
+
} catch {
|
|
226
|
+
// storage unavailable
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
};
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|