ratacode 0.2.5

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 (58) hide show
  1. package/CREDITS.md +80 -0
  2. package/LICENSE +21 -0
  3. package/README.md +289 -0
  4. package/apreton/README.md +54 -0
  5. package/apreton/handshake.md +33 -0
  6. package/apreton/headless.md +58 -0
  7. package/apreton/mcp.md +221 -0
  8. package/apreton/navegador.md +58 -0
  9. package/bin/instalacion.js +148 -0
  10. package/bin/ratacode.js +1251 -0
  11. package/fabrica/settings.yaml +247 -0
  12. package/mcp/README.md +253 -0
  13. package/mcp/bin/ratacode-mcp.js +215 -0
  14. package/mcp/lib/actividad.js +49 -0
  15. package/mcp/lib/casa.js +126 -0
  16. package/mcp/lib/claves.js +148 -0
  17. package/mcp/lib/espacios.js +117 -0
  18. package/mcp/lib/http.js +184 -0
  19. package/mcp/lib/lectura.js +210 -0
  20. package/mcp/lib/modelos.js +235 -0
  21. package/mcp/lib/nucleo.js +332 -0
  22. package/mcp/lib/registro.js +21 -0
  23. package/mcp/lib/seguridad.js +283 -0
  24. package/mcp/lib/servidor.js +397 -0
  25. package/mcp/lib/tareas.js +406 -0
  26. package/mcp/package.json +22 -0
  27. package/mcp/tunel.mjs +209 -0
  28. package/modos/arquitecto/agent.cordis.yml +120 -0
  29. package/modos/arquitecto/preset.yml +3 -0
  30. package/modos/capataz/agent.cordis.yml +120 -0
  31. package/modos/capataz/preset.yml +3 -0
  32. package/modos/faro/agent.cordis.yml +120 -0
  33. package/modos/faro/preset.yml +3 -0
  34. package/modos/gepeto/agent.cordis.yml +191 -0
  35. package/modos/gepeto/preset.yml +3 -0
  36. package/modos/hero/agent.cordis.yml +198 -0
  37. package/modos/hero/preset.yml +3 -0
  38. package/modos/modo-rata/agent.cordis.yml +198 -0
  39. package/modos/modo-rata/preset.yml +3 -0
  40. package/modos/nex/agent.cordis.yml +213 -0
  41. package/modos/nex/preset.yml +3 -0
  42. package/modos/nex/skills/cordis-plugin-development/SKILL.md +420 -0
  43. package/modos/nex/skills/editing-cordis-compositions/SKILL.md +165 -0
  44. package/modos/pix/agent.cordis.yml +207 -0
  45. package/modos/pix/preset.yml +3 -0
  46. package/modos/tirita/agent.cordis.yml +130 -0
  47. package/modos/tirita/preset.yml +3 -0
  48. package/package.json +49 -0
  49. package/piel/activos/ratacode-emblema.svg +14 -0
  50. package/piel/activos/ratacode-es.js +1331 -0
  51. package/piel/activos/ratacode-identidad.css +50 -0
  52. package/piel/activos/ratacode-piel.css +217 -0
  53. package/piel/activos/ratacode-piel.js +487 -0
  54. package/piel/activos/ratacode-vida.js +424 -0
  55. package/piel/cordis.patch.yml +6 -0
  56. package/piel/lib/cliente.js +1020 -0
  57. package/piel/lib/index.js +1230 -0
  58. package/piel/package.json +36 -0
@@ -0,0 +1,1020 @@
1
+ /**
2
+ * ratacode-piel · LA CARA DE CLIENTE (mitad de navegador).
3
+ *
4
+ * Ésta es la vía OFICIAL de DSH para añadir una sección al menú de Ajustes:
5
+ * el servicio `slots` del frontend, con
6
+ * ctx.slots.inject('settings.section', () => ctx.slots.register({...}, Component))
7
+ * (el mismo camino que usa `@deepseek-ai/dsh-client-ui-agent-preset`,
8
+ * `lib/client.js:1519-1526`, y `dsh-client-ui-settings-general`, `:651-661`).
9
+ * El plugin se declara en `package.json` como `dsh.client.platform = "web"` con
10
+ * `exports["./client"]`, y `@deepseek-ai/dsh-client-modules` lo sirve en
11
+ * `/plugins/ratacode-piel/client.js` y lo monta como plugin del navegador.
12
+ *
13
+ * La sección «Conexiones» (R23; en R12 se llamaba de otra manera) son DOS TARJETAS,
14
+ * las dos en español y con pasos 1-2-3:
15
+ * 1) «Claude Code, Codex, OpenClaw…»: copia el texto de ESTA casa (corto, con su
16
+ * URL) y lo deja en `<casa>\handshake.md`.
17
+ * 2) «ChatGPT y Claude web»: enciende y apaga la conexión (el MCP por HTTP y su
18
+ * túnel, que los arranca la piel cuando el usuario pulsa), dice su estado en
19
+ * una palabra, copia la dirección que se pega en ChatGPT y avisa en UNA
20
+ * línea.
21
+ *
22
+ * R18 · Añade una SEGUNDA sección por la misma vía oficial, «Modelos locales»,
23
+ * con los dos runtimes que corren en el ordenador (Ollama y LM Studio): si están
24
+ * instalados, encendidos o apagados (lo pregunta a `/ratacode/runtimes`, que
25
+ * sondea `/v1/models` sin bloquear la página), sus modelos con una etiqueta
26
+ * corta, el botón de encender/apagar y la recomendación según la tarjeta del PC.
27
+ * Así Ajustes › Models queda SÓLO para las APIs con clave, que es lo que pidió
28
+ * Patxi.
29
+ *
30
+ * R21 · Añade, por la vía OFICIAL de idiomas de DSH (`ctx.locale`), el IDIOMA
31
+ * ESPAÑOL: `addLanguage({id:'es', label:'Español', fallback:'en'})` y un
32
+ * diccionario `es` por CADA espacio de nombres de la interfaz (38 espacios,
33
+ * 1038 textos, medidos del motor instalado). Los textos llegan en
34
+ * `window.__RATACODE_ES`, que deja el index antes que este módulo. El inglés y
35
+ * el chino siguen donde estaban.
36
+ *
37
+ * R21 · Y registra los TRES TEMAS de la casa por la vía OFICIAL de temas
38
+ * (`ctx.theme.register`), con sus fichas de color (los tokens `--dsw-*`); el
39
+ * aspecto se cambia desde Ajustes › General › Appearance y se recuerda.
40
+ *
41
+ * R24 · Añade TRES AVISOS de una línea, en los tres idiomas: bajo el permiso de
42
+ * Ajustes › General (sólo con «A rienda suelta» puesto), arriba en
43
+ * Ajustes › Modelos y bajo la caja del encargo. Los textos van por la vía
44
+ * oficial de idiomas (`ctx.locale.register` de `es`, `en` y `zh` en un espacio
45
+ * de nombres propio) y se pintan en el sitio que les toca; no se toca ningún
46
+ * componente del motor ni ningún ajuste.
47
+ *
48
+ * Este fichero NO es un módulo ES: es un bundle en el formato del cargador de
49
+ * módulos del navegador de DSH (`window.__ModuleLoader__.load({id, factory})`),
50
+ * que sólo REGISTRA una fábrica; el cuerpo se materializa al importarlo.
51
+ * Sólo se piden módulos de la tabla fija de la cáscara (`react`).
52
+ */
53
+ window.__ModuleLoader__.load({
54
+ id: 'ratacode-piel',
55
+ factory: (require) => {
56
+ var module = { exports: {} };
57
+ var exports = module.exports;
58
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
59
+ const React = require('react');
60
+ /** Alias corto de createElement, para no llenar el fichero de React.… */
61
+ const e = React.createElement;
62
+
63
+ /** El CSS de la sección (se inyecta una sola vez, como hace todo plugin). */
64
+ const CSS = [
65
+ // R23 · Conexiones y Modelos locales: lo mínimo para que se lea claro.
66
+ '.mr-cx{display:flex;flex-direction:column;gap:12px;padding:4px 0 18px;max-width:640px}',
67
+ '.mr-cx-tarjeta{border:1px solid var(--dsw-alias-border-secondary,#3a3f45);border-radius:10px;padding:14px;display:flex;flex-direction:column;gap:9px}',
68
+ '.mr-cx-cabeza{display:flex;align-items:center;gap:10px;flex-wrap:wrap}',
69
+ '.mr-cx-nombre{font-weight:600;font-size:15px}',
70
+ '.mr-hs-pre{margin:0;padding:10px;border-radius:8px;background:#0e1012;color:#dfe3e6;font-family:ui-monospace,Consolas,monospace;',
71
+ 'font-size:12px;line-height:17px;white-space:pre-wrap;word-break:break-word;max-height:320px;overflow:auto}',
72
+ '.mr-hs-acciones{display:flex;gap:8px;flex-wrap:wrap}',
73
+ '.mr-hs-accion{cursor:pointer;padding:6px 12px;border-radius:8px;border:1px solid var(--dsw-alias-border-secondary,#3a3f45);',
74
+ 'background:var(--dsw-alias-bg-secondary,#1b1e21);color:inherit;font:inherit;font-size:12px}',
75
+ '.mr-hs-accion:hover{border-color:#e4f226}',
76
+ '.mr-hs-accion[data-copiado="si"]{border-color:#a6e22e;color:#a6e22e}',
77
+ // R23 · Ajustes › Modelos locales
78
+ '.mr-ml{display:flex;flex-direction:column;gap:12px;padding:4px 0 18px;max-width:760px}',
79
+ '.mr-ml-tarjeta{border:1px solid var(--dsw-alias-border-secondary,#3a3f45);border-radius:10px;padding:12px 13px;display:flex;flex-direction:column;gap:8px}',
80
+ '.mr-ml-cabeza{display:flex;align-items:center;gap:10px;flex-wrap:wrap}',
81
+ '.mr-ml-nombre{font-weight:600;font-size:15px}',
82
+ '.mr-ml-pildora{border:1px solid var(--dsw-alias-border-secondary,#3a3f45);border-radius:999px;padding:1px 9px;font-size:12px}',
83
+ '.mr-ml-encendido{color:#a6e22e;border-color:#a6e22e}',
84
+ '.mr-ml-apagado{color:#ffb4b4;border-color:#ff6b6b}',
85
+ '.mr-ml-sin{color:var(--dsw-alias-text-secondary,#9aa0a6)}',
86
+ '.mr-ml-paso{display:flex;align-items:baseline;gap:8px;font-size:13px;line-height:19px}',
87
+ '.mr-ml-num{font-weight:700;color:#e4f226;min-width:12px}',
88
+ '.mr-ml-pasoTxt{min-width:0}',
89
+ '.mr-ml-modelo{display:flex;align-items:baseline;gap:8px;font-size:13px;cursor:pointer;text-align:left;',
90
+ 'background:none;border:0;border-radius:8px;padding:3px 6px;color:inherit;font-family:inherit}',
91
+ '.mr-ml-modelo:hover{background:var(--dsw-alias-bg-secondary,#1b1e21)}',
92
+ '.mr-ml-id{font-family:ui-monospace,Consolas,monospace;font-size:12px}',
93
+ '.mr-ml-clase{border-radius:6px;padding:1px 7px;font-size:11px}',
94
+ '.mr-ml-si{background:rgba(166,226,46,.14);color:#a6e22e}',
95
+ '.mr-ml-no{background:rgba(255,107,107,.14);color:#ffb4b4}',
96
+ '.mr-ml-duda{background:rgba(154,160,166,.16);color:#c9ced3}',
97
+ '.mr-ml-nota{margin:0;font-size:12px;line-height:17px;color:var(--dsw-alias-text-secondary,#9aa0a6)}',
98
+ '.mr-ml-aviso{margin:0;font-size:13px;line-height:19px;color:#ffb4b4}',
99
+ '.mr-ml-tarjeta-pc{margin:0;font-size:13px;line-height:20px;font-weight:600}',
100
+ '.mr-ml-acciones{display:flex;gap:8px;flex-wrap:wrap;align-items:center}',
101
+ '.mr-ml-boton{display:inline-block;cursor:pointer;text-decoration:none;padding:7px 14px;border-radius:8px;',
102
+ 'border:1px solid var(--dsw-alias-border-secondary,#3a3f45);background:var(--dsw-alias-bg-secondary,#1b1e21);',
103
+ 'color:inherit;font:inherit;font-size:13px}',
104
+ '.mr-ml-boton:hover{border-color:#e4f226}',
105
+ '.mr-ml-pre{margin:0;padding:9px;border-radius:8px;background:#0e1012;color:#dfe3e6;font-family:ui-monospace,Consolas,monospace;',
106
+ 'font-size:12px;line-height:17px;white-space:pre-wrap;word-break:break-word}',
107
+ '.mr-ml-avanzado{margin-top:2px}',
108
+ '.mr-ml-avanzado>summary{cursor:pointer;font-size:12px;color:var(--dsw-alias-text-secondary,#9aa0a6)}',
109
+ // R24 · los tres avisos de una línea: el del permiso (Ajustes › General y
110
+ // la caja del encargo) y el de los proveedores (Ajustes › Modelos).
111
+ '.mr-aviso-permiso{margin:0;color:var(--dsw-alias-label-tertiary,#9aa0a6);font-size:12px;font-weight:400;line-height:18px}',
112
+ '.mr-aviso-modelos{margin:0 0 4px;color:var(--dsw-alias-label-tertiary,#9aa0a6);font-size:13px;font-weight:400;line-height:20px}',
113
+ '.mr-aviso-caja{box-sizing:border-box;width:100%;max-width:var(--dsh-composer-card-max-width,780px);',
114
+ 'margin:0 auto;padding:2px 12px 0;color:var(--dsw-alias-label-tertiary,#9aa0a6);font-size:12px;line-height:18px}',
115
+ ].join('');
116
+
117
+ if (typeof document !== 'undefined') {
118
+ const tagId = 'ratacode-piel/secciones.css';
119
+ if (document.querySelector('style[data-plugin-css=' + JSON.stringify(tagId) + ']') === null) {
120
+ const tag = document.createElement('style');
121
+ tag.dataset.plugin = 'ratacode-piel';
122
+ tag.dataset.pluginCss = tagId;
123
+ tag.textContent = CSS;
124
+ document.head.appendChild(tag);
125
+ }
126
+ }
127
+
128
+ // ── R24 · LOS TRES AVISOS DE LA CASA, EN LOS TRES IDIOMAS ───────────────
129
+ // Una línea llana cada uno (como los textos de OpenAI y Anthropic), por la
130
+ // vía OFICIAL de idiomas: se registran los diccionarios de `es`, `en` y `zh`
131
+ // en un espacio de nombres propio y el texto que se pinta sale de
132
+ // `ctx.locale.bind`, así que cambia solo al cambiar el idioma en
133
+ // Ajustes › General › Language. El español NO va en `ratacode-es.js`: ese
134
+ // fichero es el diccionario del MOTOR, medido del motor instalado, y aquí
135
+ // son textos nuestros.
136
+ const AVISOS_NS = 'ratacode-avisos';
137
+ const AVISOS = {
138
+ es: {
139
+ 'permiso.fullAccess': 'El agente actúa sin pedirte permiso. Úsalo solo en carpetas tuyas.',
140
+ 'modelos.proveedor': 'Lo que envías va al proveedor que elijas y se rige por sus condiciones.',
141
+ 'conexion.espacio': 'Este chat solo puede leer y escribir en {carpeta}.',
142
+ },
143
+ en: {
144
+ 'permiso.fullAccess': 'The agent acts without asking your permission. Use it only in folders of your own.',
145
+ 'modelos.proveedor': 'What you send goes to the provider you choose and is governed by its terms.',
146
+ 'conexion.espacio': 'This chat can only read and write in {carpeta}.',
147
+ },
148
+ zh: {
149
+ 'permiso.fullAccess': '智能体不经你许可就会直接操作。请只在属于你自己的文件夹里使用。',
150
+ 'modelos.proveedor': '你发送的内容会交给你选择的提供商,并受其条款约束。',
151
+ 'conexion.espacio': '此对话只能在 {carpeta} 中读写。',
152
+ },
153
+ };
154
+
155
+ /**
156
+ * El traductor de {@link AVISOS_NS}, en una variable de este ámbito: lo pone
157
+ * `avisosDeLaCasa` (que es quien registra los diccionarios) y lo usa también
158
+ * la tarjeta de «Conexiones», que se pinta en otro momento. Si todavía no
159
+ * está, se dice en español: nunca se queda en blanco.
160
+ */
161
+ let tAvisos = null;
162
+ const AVISO_ESPACIO_ES = 'Este chat solo puede leer y escribir en {carpeta}.';
163
+
164
+ /** Un pedido a las rutas de la piel; nunca revienta: devuelve el error. */
165
+ async function pedir(ruta, metodo) {
166
+ try {
167
+ const res = await fetch(ruta, { method: metodo, credentials: 'same-origin', cache: 'no-store' });
168
+ const datos = await res.json().catch(() => null);
169
+ if (datos === null) return { ok: false, error: 'HTTP ' + res.status };
170
+ return datos;
171
+ } catch (err) {
172
+ return { ok: false, error: 'sin conexión con el servidor de la piel (' + (err?.message ?? err) + ')' };
173
+ }
174
+ }
175
+
176
+ /** Copiar al portapapeles, con el recurso clásico si no hay API. */
177
+ function copiarAlPortapapeles(texto) {
178
+ if (navigator.clipboard && navigator.clipboard.writeText) {
179
+ return navigator.clipboard.writeText(texto).then(() => true, () => recursoClasico(texto));
180
+ }
181
+ return Promise.resolve(recursoClasico(texto));
182
+ }
183
+ function recursoClasico(texto) {
184
+ try {
185
+ const caja = document.createElement('textarea');
186
+ caja.value = texto;
187
+ caja.setAttribute('readonly', '');
188
+ caja.style.position = 'fixed';
189
+ caja.style.left = '-9999px';
190
+ document.body.append(caja);
191
+ caja.select();
192
+ const ok = document.execCommand('copy');
193
+ caja.remove();
194
+ return ok;
195
+ } catch { return false; }
196
+ }
197
+
198
+ /** Un bloque de texto con su botón de copiar. */
199
+ function Bloque(props) {
200
+ const [copiado, setCopiado] = React.useState(false);
201
+ const copiar = () => {
202
+ Promise.resolve(copiarAlPortapapeles(props.texto)).then((ok) => {
203
+ setCopiado(ok);
204
+ if (ok) setTimeout(() => setCopiado(false), 2200);
205
+ });
206
+ };
207
+ return e('div', { className: 'mr-hs-acciones' },
208
+ e('button', {
209
+ type: 'button',
210
+ className: 'mr-hs-accion',
211
+ 'data-copiado': copiado ? 'si' : 'no',
212
+ onClick: copiar,
213
+ }, copiado ? 'Copiado ✓' : props.etiqueta));
214
+ }
215
+
216
+ /**
217
+ * La línea de la tarjeta de «Conexiones» (R25): dónde puede leer y escribir
218
+ * ESTE chat. La carpeta la dice el MCP (`/ratacode/conexion` la saca de
219
+ * `mcp.workspaces`), y el idioma lo pone el servicio de idiomas del motor.
220
+ * @param {{carpeta?: string}|null} d - el estado de la conexión, o null.
221
+ * @returns {string} la línea.
222
+ */
223
+ function avisoDeEspacio(d) {
224
+ const carpeta = typeof d?.carpeta === 'string' && d.carpeta !== '' ? d.carpeta : 'la carpeta autorizada';
225
+ if (tAvisos !== null) return tAvisos('conexion.espacio', { carpeta });
226
+ return AVISO_ESPACIO_ES.replace('{carpeta}', carpeta);
227
+ }
228
+
229
+ /**
230
+ * R23 · La sección «Conexiones» de Ajustes: dos tarjetas, cada una con sus
231
+ * pasos 1-2-3 y un botón por acción. Lo que se explica largo va plegado.
232
+ */
233
+ function SeccionConexiones() {
234
+ const [texto, setTexto] = React.useState(null);
235
+ const [copiado, setCopiado] = React.useState('');
236
+ const [conexion, setConexion] = React.useState({ fase: 'cargando' });
237
+ const [enCurso, setEnCurso] = React.useState('');
238
+ const [dicho, setDicho] = React.useState('');
239
+
240
+ const mirarConexion = () => {
241
+ setConexion((previo) => (previo.fase === 'listo' ? { ...previo, fase: 'remirando' } : { fase: 'cargando' }));
242
+ pedir('/ratacode/conexion', 'GET').then((r) => {
243
+ if (!r.ok) { setConexion({ fase: 'error', error: r.error ?? 'error' }); return; }
244
+ setConexion({ fase: 'listo', datos: r });
245
+ });
246
+ };
247
+ React.useEffect(() => { mirarConexion(); }, []);
248
+
249
+ const copiarConAviso = (etiqueta, valor) => {
250
+ Promise.resolve(copiarAlPortapapeles(valor)).then((ok) => {
251
+ setCopiado(ok ? etiqueta : '');
252
+ if (ok) setTimeout(() => setCopiado(''), 2200);
253
+ });
254
+ };
255
+
256
+ /** El botón «Copiar» de la primera tarjeta: copia y deja el texto en la casa. */
257
+ const pulsarCopiar = () => {
258
+ setDicho('');
259
+ pedir('/ratacode/handshake', 'POST').then((r) => {
260
+ if (!r.ok) { setDicho('No pude prepararlo: ' + (r.error ?? 'error')); return; }
261
+ setTexto(r.texto);
262
+ copiarConAviso('texto', r.texto);
263
+ });
264
+ };
265
+
266
+ const botonConexion = (cual) => {
267
+ setDicho('');
268
+ setEnCurso(cual);
269
+ fetch('/ratacode/conexion/' + cual, { method: 'POST', credentials: 'same-origin', cache: 'no-store' })
270
+ .then((res) => res.json().catch(() => null))
271
+ .then((r) => {
272
+ setEnCurso('');
273
+ if (r === null) setDicho('No pude hablar con RATACODE.');
274
+ else if (r.ok !== true) setDicho(r.motivo ?? 'No pude hacerlo.');
275
+ if (r !== null && r.estado !== undefined) setConexion({ fase: 'listo', datos: r.estado });
276
+ else mirarConexion();
277
+ })
278
+ .catch(() => { setEnCurso(''); setDicho('No pude hablar con RATACODE.'); });
279
+ };
280
+
281
+ const hijos = [];
282
+
283
+ // ── tarjeta 1 · los agentes con navegador ─────────────────────────────
284
+ hijos.push(e('div', { className: 'mr-cx-tarjeta', key: 'agentes' },
285
+ e('div', { className: 'mr-cx-cabeza' }, e('span', { className: 'mr-cx-nombre' }, 'Claude Code, Codex, OpenClaw…')),
286
+ e(Paso, { n: '1' }, 'Copia esto',
287
+ e('button', { type: 'button', className: 'mr-ml-boton', onClick: pulsarCopiar }, copiado === 'texto' ? 'Copiado ✓' : 'Copiar')),
288
+ e(Paso, { n: '2' }, 'Pégalo en tu chat'),
289
+ e(Paso, { n: '3' }, 'Listo'),
290
+ texto === null ? null : e('details', { className: 'mr-ml-avanzado', key: 'ver' },
291
+ e('summary', {}, 'Ver lo que se copia'),
292
+ e('pre', { className: 'mr-hs-pre' }, texto))));
293
+
294
+ // ── tarjeta 2 · los chats web (ChatGPT y Claude web) ──────────────────
295
+ const d = conexion.fase === 'listo' ? conexion.datos : null;
296
+ const conectado = d !== null && d.conectado === true;
297
+ const tarjeta = [
298
+ e('div', { className: 'mr-cx-cabeza', key: 'cabeza' },
299
+ e('span', { className: 'mr-cx-nombre' }, 'ChatGPT y Claude web'),
300
+ e('span', { className: 'mr-ml-pildora ' + (conectado ? 'mr-ml-encendido' : 'mr-ml-sin') },
301
+ conectado ? 'Conectado' : 'Sin conectar')),
302
+ e(Paso, { n: '1', key: 'p1' }, 'Enciende la conexión',
303
+ conectado
304
+ ? e('button', { type: 'button', className: 'mr-ml-boton', onClick: () => botonConexion('apagar') },
305
+ enCurso === 'apagar' ? 'Apagando…' : 'Apagar')
306
+ : e('button', { type: 'button', className: 'mr-ml-boton', 'data-accion': 'encender', onClick: () => botonConexion('encender') },
307
+ enCurso === 'encender' ? 'Encendiendo…' : 'Encender')),
308
+ e(Paso, { n: '2', key: 'p2' }, 'Copia la dirección',
309
+ conectado
310
+ ? e('button', { type: 'button', className: 'mr-ml-boton', onClick: () => copiarConAviso('direccion', d.direccion) },
311
+ copiado === 'direccion' ? 'Copiada ✓' : 'Copiar dirección')
312
+ : null),
313
+ e(Paso, { n: '3', key: 'p3' }, 'Pégala en ChatGPT › Ajustes › Conectores'),
314
+ e('p', { className: 'mr-ml-nota', key: 'aviso' }, avisoDeEspacio(d)),
315
+ ];
316
+ if (conexion.fase === 'error') {
317
+ tarjeta.push(e('p', { className: 'mr-ml-aviso', key: 'error' }, 'No pude mirar la conexión: ' + conexion.error));
318
+ }
319
+ if (d !== null && !d.nuestro && conectado) {
320
+ tarjeta.push(e('p', { className: 'mr-ml-nota', key: 'ajena' },
321
+ 'La encendiste tú (en una ventana): se apaga con Ctrl+C ahí.'));
322
+ }
323
+ if (d !== null && !conectado && d.comandos !== undefined) {
324
+ tarjeta.push(e('details', { className: 'mr-ml-avanzado', key: 'comandos' },
325
+ e('summary', {}, 'A mano'),
326
+ e('pre', { className: 'mr-hs-pre' }, d.comandos)));
327
+ }
328
+ if (dicho !== '') tarjeta.push(e('p', { className: 'mr-ml-aviso', key: 'dicho' }, dicho));
329
+ hijos.push(e('div', { className: 'mr-cx-tarjeta', key: 'web' }, tarjeta));
330
+
331
+ return e('div', { className: 'mr-cx' }, hijos);
332
+ }
333
+
334
+ // ── R23 · Ajustes › Modelos locales (una tarjeta por programa) ──────────
335
+
336
+ /** Las tres palabras de estado. Una, y corta. */
337
+ function estadoDe(runtime) {
338
+ if (runtime.encendido) return 'Encendido';
339
+ return runtime.instalado ? 'Apagado' : 'No instalado';
340
+ }
341
+
342
+ /** La píldora del estado: verde si está encendido, gris si no. */
343
+ function Pildora(props) {
344
+ const clase = props.estado === 'Encendido' ? 'mr-ml-encendido' : (props.estado === 'Apagado' ? 'mr-ml-apagado' : 'mr-ml-sin');
345
+ return e('span', { className: 'mr-ml-pildora ' + clase, 'data-estado': props.estado }, props.estado);
346
+ }
347
+
348
+ /**
349
+ * «1 · Enciéndelo»: el número, el texto y, si toca, su botón al lado.
350
+ */
351
+ function Paso(props) {
352
+ const hijos = Array.isArray(props.children) ? props.children : [props.children];
353
+ return e('div', { className: 'mr-ml-paso' },
354
+ e('span', { className: 'mr-ml-num' }, props.n),
355
+ e('span', { className: 'mr-ml-pasoTxt' }, hijos[0]),
356
+ hijos.slice(1));
357
+ }
358
+
359
+ /**
360
+ * Un modelo del runtime: su id, su etiqueta corta y, al pulsarlo, se elige en
361
+ * la caja si se puede.
362
+ */
363
+ function ModeloLocal(props) {
364
+ const m = props.modelo;
365
+ const etiqueta = m.clase === 'agente' ? 'Recomendado' : (m.clase === 'no' ? 'No sirve para agentes' : 'Sin datos');
366
+ const clase = m.clase === 'agente' ? 'mr-ml-si' : (m.clase === 'no' ? 'mr-ml-no' : 'mr-ml-duda');
367
+ return e('button', {
368
+ type: 'button',
369
+ className: 'mr-ml-modelo',
370
+ 'data-modelo': m.id,
371
+ title: 'Ponlo en la caja',
372
+ onClick: () => props.onElegir(m.id),
373
+ },
374
+ e('span', { className: 'mr-ml-id' }, m.id),
375
+ e('span', { className: 'mr-ml-clase ' + clase }, etiqueta));
376
+ }
377
+
378
+ /** Una tarjeta de programa: nombre, estado, sus pasos y sus botones. */
379
+ function TarjetaRuntime(props) {
380
+ const r = props.runtime;
381
+ const estado = estadoDe(r);
382
+ const hijos = [];
383
+ hijos.push(e('div', { className: 'mr-ml-cabeza', key: 'cabeza' },
384
+ e('span', { className: 'mr-ml-nombre' }, r.nombre),
385
+ e(Pildora, { key: 'p', estado })));
386
+
387
+ if (estado === 'No instalado') {
388
+ hijos.push(e(Paso, { key: 'p1', n: '1' }, 'Descárgalo'));
389
+ hijos.push(e('div', { className: 'mr-ml-acciones', key: 'a1' },
390
+ e('a', { className: 'mr-ml-boton', href: r.enlace, target: '_blank', rel: 'noreferrer' }, r.descarga)));
391
+ hijos.push(e(Paso, { key: 'p2', n: '2' }, 'Instálalo y ábrelo'));
392
+ hijos.push(e(Paso, { key: 'p3', n: '3' }, r.pull === null ? 'Descarga un modelo desde la app' : 'Descarga un modelo'));
393
+ if (r.pull !== null) {
394
+ hijos.push(e('div', { className: 'mr-ml-acciones', key: 'a3' },
395
+ e(Bloque, { key: 'c', texto: r.pull, etiqueta: 'Copiar «' + r.pull + '»' })));
396
+ }
397
+ } else if (estado === 'Apagado') {
398
+ if (r.puedeEncender && props.aMano !== true) {
399
+ hijos.push(e('div', { className: 'mr-ml-acciones', key: 'enc' },
400
+ e('button', { type: 'button', className: 'mr-ml-boton', 'data-accion': 'encender', 'data-runtime': r.id, onClick: () => props.onEncender() }, props.encendiendo ? 'Encendiendo…' : 'Encender')));
401
+ } else {
402
+ // No se puede encender desde aquí (o lo hemos intentado y su programa no
403
+ // arrancó): el comando, en UN bloque y con su botón de copiar.
404
+ hijos.push(e(Paso, { key: 'p1', n: '1' }, 'Enciéndelo con este comando'));
405
+ hijos.push(e('pre', { className: 'mr-ml-pre', key: 'pre' }, r.arranque));
406
+ hijos.push(e('div', { className: 'mr-ml-acciones', key: 'acc' },
407
+ e(Bloque, { key: 'c', texto: r.arranque, etiqueta: 'Copiar comando' })));
408
+ }
409
+ } else {
410
+ hijos.push(e('div', { className: 'mr-ml-acciones', key: 'apag' },
411
+ r.puedeApagar
412
+ ? e('button', { type: 'button', className: 'mr-ml-boton', 'data-accion': 'apagar', 'data-runtime': r.id, onClick: () => props.onApagar() }, props.apagando ? 'Apagando…' : 'Apagar')
413
+ : e('span', { className: 'mr-ml-nota', key: 'amano' }, r.apagadoAMano ?? '')));
414
+ if (r.modelos.length === 0) {
415
+ hijos.push(e('p', { className: 'mr-ml-nota', key: 'vacio' },
416
+ r.pull === null ? 'Sin modelos: descarga uno desde la app.' : 'Sin modelos: ' + r.pull));
417
+ } else {
418
+ for (const m of r.modelos) hijos.push(e(ModeloLocal, { key: m.id, modelo: m, onElegir: props.onElegir }));
419
+ }
420
+ }
421
+
422
+ hijos.push(e('details', { className: 'mr-ml-avanzado', key: 'avanzado' },
423
+ e('summary', {}, 'Avanzado'),
424
+ e('p', { className: 'mr-ml-nota' }, r.cambiar)));
425
+ return e('div', { className: 'mr-ml-tarjeta', key: r.id, 'data-runtime': r.id, 'data-estado': estado }, hijos);
426
+ }
427
+
428
+ /**
429
+ * La pestaña «Modelos locales»: una tarjeta por programa (Ollama y LM Studio),
430
+ * con su estado, sus pasos 1-2-3 y un botón por acción. Nada de explicar
431
+ * tripas: lo avanzado (dirección y puerto) va plegado.
432
+ */
433
+ function SeccionModelosLocales() {
434
+ const [estado, setEstado] = React.useState({ fase: 'cargando' });
435
+ const [enCurso, setEnCurso] = React.useState({});
436
+ const [aMano, setAMano] = React.useState({});
437
+ const [dicho, setDicho] = React.useState('');
438
+
439
+ const mirar = () => {
440
+ setEstado((previo) => (previo.fase === 'listo' ? { fase: 'remirando', datos: previo.datos } : { fase: 'cargando' }));
441
+ pedir('/ratacode/runtimes', 'GET').then((r) => {
442
+ if (!r.ok) { setEstado({ fase: 'error', error: r.error ?? 'error' }); return; }
443
+ setEstado({ fase: 'listo', datos: r });
444
+ });
445
+ };
446
+ React.useEffect(() => { mirar(); }, []);
447
+
448
+ const accion = (id, cual) => {
449
+ setDicho('');
450
+ setAMano((previo) => ({ ...previo, [id]: false }));
451
+ setEnCurso((previo) => ({ ...previo, [id]: cual }));
452
+ fetch('/ratacode/runtimes/' + cual, {
453
+ method: 'POST', credentials: 'same-origin', cache: 'no-store',
454
+ headers: { 'content-type': 'application/json' },
455
+ body: JSON.stringify({ id }),
456
+ }).then((res) => res.json().catch(() => null)).then((r) => {
457
+ setEnCurso((previo) => ({ ...previo, [id]: null }));
458
+ if (r === null) { setDicho('No pude hablar con RATACODE.'); mirar(); return; }
459
+ if (r.ok !== true) {
460
+ setDicho(r.motivo ?? 'No pude hacerlo.');
461
+ // No se ha podido desde aquí: la tarjeta deja el COMANDO, que es la
462
+ // otra vía (y así el usuario no se queda sin nada que hacer).
463
+ if (r.aMano === true) setAMano((previo) => ({ ...previo, [id]: true }));
464
+ }
465
+ mirar();
466
+ }).catch(() => {
467
+ setEnCurso((previo) => ({ ...previo, [id]: null }));
468
+ setDicho('No pude hablar con RATACODE.');
469
+ });
470
+ };
471
+
472
+ const elegir = (id) => {
473
+ setDicho('');
474
+ elegirModeloEnLaCaja(id).then((ok) => {
475
+ if (!ok) setDicho('No pude ponerlo en la caja: elígelo en el selector de modelos.');
476
+ });
477
+ };
478
+
479
+ if (estado.fase === 'cargando') return e('div', { className: 'mr-ml' }, e('p', { className: 'mr-ml-nota' }, 'Mirando…'));
480
+ if (estado.fase === 'error') {
481
+ return e('div', { className: 'mr-ml' },
482
+ e('p', { className: 'mr-ml-aviso' }, 'No pude mirar los programas locales: ' + estado.error),
483
+ e('button', { type: 'button', className: 'mr-ml-boton', onClick: mirar }, 'Volver a mirar'));
484
+ }
485
+
486
+ const d = estado.datos;
487
+ const runtimes = Array.isArray(d.runtimes) ? d.runtimes : [];
488
+ return e('div', { className: 'mr-ml' },
489
+ runtimes.map((r) => e(TarjetaRuntime, {
490
+ key: r.id,
491
+ runtime: r,
492
+ encendiendo: enCurso[r.id] === 'encender',
493
+ apagando: enCurso[r.id] === 'apagar',
494
+ aMano: aMano[r.id] === true,
495
+ onEncender: () => accion(r.id, 'encender'),
496
+ onApagar: () => accion(r.id, 'apagar'),
497
+ onElegir: elegir,
498
+ })),
499
+ e('p', { className: 'mr-ml-tarjeta-pc', key: 'pc' },
500
+ d.tarjeta === null || d.tarjeta === undefined
501
+ ? 'No sé qué tarjeta tienes. Recomendados: 8 GB → qwen3:8b · 12 GB → gemma4:12b · 16 GB → gpt-oss:20b · 24 GB → muse-glimmer:30b · Solo CPU → granite4.1:3b'
502
+ : 'Tu tarjeta: ' + d.tarjeta.gb + ' GB → recomendado: ' + (d.recomendado ?? 'qwen3:8b')),
503
+ dicho === '' ? null : e('p', { className: 'mr-ml-aviso', key: 'dicho' }, dicho),
504
+ e('div', { className: 'mr-ml-acciones', key: 'mirar' },
505
+ e('button', { type: 'button', className: 'mr-ml-boton', onClick: mirar }, estado.fase === 'remirando' ? 'Mirando otra vez…' : 'Volver a mirar')));
506
+ }
507
+
508
+ /**
509
+ * R23 · Poner un modelo local en la CAJA: se abre el selector de modelos del
510
+ * propio motor y se pulsa el suyo. Es lo que hace un dedo, hecho por código.
511
+ * Si no se puede (el modelo no está en el catálogo de la casa, o el selector
512
+ * no está montado), se devuelve `false` y la pestaña lo dice en una línea.
513
+ * @param id - el id del modelo (`qwen3:8b`).
514
+ * @returns si se ha podido elegir.
515
+ */
516
+ async function elegirModeloEnLaCaja(id) {
517
+ const normal = (t) => String(t ?? '').toLowerCase().replace(/[^a-z0-9]/g, '');
518
+ const esperar = (ms) => new Promise((listo) => setTimeout(listo, ms));
519
+ try {
520
+ const caja = document.querySelector('[data-composer-card]');
521
+ if (caja === null) return false;
522
+ const disparador = [...caja.querySelectorAll('button[aria-haspopup="menu"]')].find((b) => /trigger/.test(String(b.className)));
523
+ if (disparador === undefined) return false;
524
+ disparador.click();
525
+ await esperar(300);
526
+ const celda = [...document.querySelectorAll('[role="menuitem"]')]
527
+ .find((x) => ((x.querySelector('[class*="cellLabel"]')?.textContent ?? '').trim()) === 'Model');
528
+ if (celda !== undefined) { celda.click(); await esperar(400); }
529
+ const opciones = [...document.querySelectorAll('[role="menuitemradio"]')];
530
+ const buscado = normal(id);
531
+ const suyo = opciones.find((o) => normal(o.querySelector('[class*="modelName"]')?.textContent) === buscado)
532
+ ?? opciones.find((o) => normal(o.textContent).includes(buscado));
533
+ if (suyo === undefined) { document.body.click(); return false; }
534
+ suyo.click();
535
+ return true;
536
+ } catch { return false; }
537
+ }
538
+
539
+ // ── R21 · EL ESPAÑOL, POR LA VÍA OFICIAL DE IDIOMAS DE DSH ─────────────
540
+ // DSH tiene su propio servicio de idiomas (`ctx.locale`, paquete
541
+ // `@deepseek-ai/dsh-client-locale`) y su fila en Ajustes › General ›
542
+ // Language. Añadir un idioma es EXACTAMENTE esto: declararlo con
543
+ // `addLanguage` y registrar un diccionario por espacio de nombres con
544
+ // `register(ns, 'es', {…})` (README del paquete: «Registering a language
545
+ // pack»). Aquí no se traduce nada sobre la pantalla: eso sería deuda, y
546
+ // cada clave nueva del motor volvería a salir en inglés.
547
+ //
548
+ // Los textos los deja el index en `window.__RATACODE_ES` (activo
549
+ // `piel\activos\ratacode-es.js`, medido del motor instalado). Si no
550
+ // estuviera —una instalación vieja, o el index sin vestir—, se declara el
551
+ // idioma igual y los textos caen al inglés por la cadena de respaldo.
552
+ const IDIOMA = 'es';
553
+ const IDIOMA_ETIQUETA = 'Español';
554
+
555
+ /**
556
+ * Declara el español y registra todos sus diccionarios.
557
+ * @param ctx - contexto del plugin de navegador (con `locale`).
558
+ * @returns la lista de espacios de nombres registrados.
559
+ */
560
+ function paqueteDeIdioma(ctx) {
561
+ const diccionarios = (typeof window === 'undefined' ? null : window.__RATACODE_ES) ?? null;
562
+ ctx.effect(
563
+ () => ctx.locale.addLanguage({ id: IDIOMA, label: IDIOMA_ETIQUETA, fallback: 'en' }),
564
+ 'ratacode-piel: idioma español',
565
+ );
566
+ if (diccionarios === null) return [];
567
+ const puestos = [];
568
+ for (const [ns, dict] of Object.entries(diccionarios)) {
569
+ if (typeof ns !== 'string' || ns === '' || dict === null || typeof dict !== 'object') continue;
570
+ ctx.effect(() => ctx.locale.register(ns, IDIOMA, dict), 'ratacode-piel: diccionario es · ' + ns);
571
+ puestos.push(ns);
572
+ }
573
+ return puestos;
574
+ }
575
+
576
+ // ── R21 · LOS TRES TEMAS, POR LA VÍA OFICIAL DE TEMAS DE DSH ───────────
577
+ // El motor tiene su registro de temas (`ctx.theme`, paquete
578
+ // `@deepseek-ai/dsh-client-ui-theme`): un tema es un id, un `colorScheme` y
579
+ // una ficha de tokens `--dsw-*` que el presentador del propio DSH escribe
580
+ // como estilo en línea en el `body`. Con eso cambia TODO el panel —fondos,
581
+ // textos, botones y su reacción, bordes, iconos— sin repintar nada a mano:
582
+ // los componentes del motor ya leen esos tokens. La paleta de partida es la
583
+ // oscura de verdad del motor (`trabajo\R21\sacar-paleta.mjs`), así que sólo
584
+ // se cambia lo que tiene que cambiar.
585
+ //
586
+ // Lo que el motor NO da (y por eso lo pone la piel, y se dice):
587
+ // · la fila «Aspecto» nativa está fija a Claro/Oscuro/Sistema
588
+ // (`AppearanceRow.js`: `CUBES`), sin ranura para sustituirla. Se esconde
589
+ // por CSS y en su sitio va la fila de RATACODE;
590
+ // · un id de tema de fuera NO se guarda en los ajustes del motor (su
591
+ // esquema sólo admite `light`/`dark`/`system`: `THEME_PREFERENCES`), así
592
+ // que el aspecto elegido se recuerda en la casa (`<casa>\tema.txt`, por
593
+ // `/ratacode/tema`) y se vuelve a aplicar al abrir.
594
+ const TEMAS = [
595
+ {
596
+ id: 'ratacode-pink',
597
+ etiqueta: 'RATACODE PINK',
598
+ // rosa principal · detalles en amarillo, negro y gris
599
+ principal: '#ff268e', detalle: '#e4f226', acento: '#e4f226',
600
+ // El rosa manda: el botón principal es rosa con texto negro.
601
+ textoDelBoton: '#141612',
602
+ hover: '#ff5aa8',
603
+ suave: '#4a1c33',
604
+ seleccion: '#33232d',
605
+ filoActivo: '#ff268e',
606
+ brillo: { 200: '#ffd6ea', 300: '#ffa8d2', 400: '#ff268e', 450: '#ff268e', 500: '#ff268e', 600: '#c01a68' },
607
+ marca: '#ff268e',
608
+ },
609
+ {
610
+ id: 'ratacode-yellow',
611
+ etiqueta: 'RATACODE YELLOW',
612
+ // amarillo principal · secundarios en rosa, negro y gris
613
+ principal: '#e4f226', detalle: '#ff268e', acento: '#ff268e',
614
+ textoDelBoton: '#141612',
615
+ hover: '#f1ff45',
616
+ suave: '#3a3320',
617
+ seleccion: '#382333',
618
+ filoActivo: '#e4f226',
619
+ brillo: { 200: '#f7ffb0', 300: '#eef86a', 400: '#e4f226', 450: '#e4f226', 500: '#cddb16', 600: '#9aa50f' },
620
+ marca: '#e4f226',
621
+ },
622
+ {
623
+ id: 'minimal',
624
+ etiqueta: 'MINIMAL',
625
+ // todo negro y gris: sólo líneas y pequeños detalles (y sin ratitas)
626
+ principal: '#e8e6df', detalle: '#8c9396', acento: '#b8bdbb',
627
+ textoDelBoton: '#101113',
628
+ hover: '#f5f4ef',
629
+ suave: '#26282b',
630
+ seleccion: '#2a2d31',
631
+ filoActivo: '#8c9396',
632
+ brillo: { 200: '#e8e6df', 300: '#b8bdbb', 400: '#9ba3a5', 450: '#8c9396', 500: '#6f7679', 600: '#545a5d' },
633
+ marca: '#e8e6df',
634
+ },
635
+ ];
636
+ /** El tema de fábrica de RATACODE (el primero de la lista). */
637
+ const TEMA_POR_DEFECTO = TEMAS[0].id;
638
+
639
+ /** Los grises de la casa: los mismos en los tres temas (negro y gris). */
640
+ const GRISES = {
641
+ '--dsw-alias-bg-base': '#101113',
642
+ '--dsw-alias-bg-layer-1': '#181a1d',
643
+ '--dsw-alias-bg-layer-2': '#222529',
644
+ '--dsw-alias-bg-layer-3': '#2a2d31',
645
+ '--dsw-alias-bg-overlay': '#26282c',
646
+ '--dsw-alias-bg-module-platform': '#2a2d31',
647
+ '--dsw-alias-bg-multi-select': '#212123',
648
+ '--dsw-alias-bg-skeleton': '#ffffff14',
649
+ '--dsw-alias-border-l1': '#282c30',
650
+ '--dsw-alias-border-l2': '#353a3d',
651
+ '--dsw-alias-border-l3': '#464d52',
652
+ '--dsw-alias-border-l4': '#626b70',
653
+ '--dsw-alias-label-primary': '#f0eee6',
654
+ '--dsw-alias-label-secondary': '#b8bdbb',
655
+ '--dsw-alias-label-tertiary': '#9ba3a5',
656
+ '--dsw-alias-label-dimmed': '#818a8e',
657
+ '--dsw-alias-markdown-code-block': '#15171a',
658
+ '--dsw-alias-markdown-code-block-banner': '#222529',
659
+ '--dsw-alias-markdown-inline-code': '#292d31',
660
+ '--dsw-alias-markdown-placeholder': '#222529',
661
+ '--dsw-alias-markdown-tag': '#222529',
662
+ '--dsw-alias-scrollbar-bg-l1': '#3c3c3d',
663
+ '--dsw-alias-scrollbar-bg-l2': '#545557',
664
+ '--dsw-alias-scrollbar-hover-l1': '#545557',
665
+ '--dsw-alias-scrollbar-hover-l2': '#65676b',
666
+ '--dsw-specific-bubble': '#222529',
667
+ '--dsw-specific-bubble-highlight': '#464d52',
668
+ '--dsw-specific-input-major': '#222529',
669
+ '--dsw-specific-selector': '#2a2d31',
670
+ '--dsw-specific-tip': '#2a2d31',
671
+ '--dsw-specific-menu': '#2a2d31',
672
+ '--dsw-specific-sidebar-fill': '#16181b',
673
+ '--dsw-static-neutral-900': '#0f0f0f',
674
+ '--dsw-static-neutral-850': '#212123',
675
+ '--dsw-static-neutral-800': '#292929',
676
+ '--dsw-static-neutral-bluish-950': '#101113',
677
+ '--dsw-static-neutral-bluish-900': '#181a1d',
678
+ '--dsw-static-neutral-bluish-875': '#181a1d',
679
+ '--dsw-static-neutral-bluish-850': '#222529',
680
+ '--dsw-static-neutral-bluish-800': '#2a2d31',
681
+ '--dsw-static-neutral-bluish-750': '#3a3d42',
682
+ '--dsw-static-neutral-bluish-700': '#464d52',
683
+ '--dsw-static-neutral-bluish-600': '#818a8e',
684
+ '--dsw-static-neutral-bluish-500': '#9ba3a5',
685
+ '--dsw-static-neutral-bluish-400': '#b8bdbb',
686
+ '--dsw-static-neutral-bluish-300': '#c9ced3',
687
+ '--dsw-static-neutral-bluish-200': '#dfe3e6',
688
+ '--dsw-static-neutral-bluish-150': '#e9ecf2',
689
+ '--dsw-static-neutral-bluish-100': '#f0eee6',
690
+ '--dsw-static-neutral-bluish-75': '#f4f2ec',
691
+ '--dsw-static-neutral-bluish-60': '#f7f5ef',
692
+ '--dsw-static-neutral-bluish-50': '#f0eee6',
693
+ '--dsw-static-neutral-600': '#545557',
694
+ '--dsw-static-neutral-550': '#65676b',
695
+ '--dsw-static-neutral-500': '#7f8287',
696
+ '--dsw-static-neutral-400': '#a2a4a6',
697
+ '--dsw-static-neutral-300': '#d4d4d4',
698
+ '--dsw-static-neutral-250': '#dcdcdc',
699
+ '--dsw-static-neutral-200': '#e5e5e5',
700
+ '--dsw-static-neutral-150': '#ededed',
701
+ '--dsw-static-neutral-100': '#f5f5f5',
702
+ '--dsw-static-neutral-50': '#fafafa',
703
+ };
704
+
705
+ /**
706
+ * La ficha de tokens de un tema: los grises de la casa + lo que cambia con
707
+ * el color de la marca (marca, botones y su reacción, enlaces, estados,
708
+ * realces, barra lateral y el brillo de «trabajando», que en DSH es azul).
709
+ * @param nivel - una ficha de {@link TEMAS}.
710
+ * @returns el mapa `--dsw-*` → valor.
711
+ */
712
+ function tokensDelTema(nivel) {
713
+ return {
714
+ ...GRISES,
715
+ '--dsw-alias-brand-primary': nivel.principal,
716
+ '--dsw-alias-brand-text': nivel.principal,
717
+ '--dsw-alias-brand-primary-invert': nivel.principal,
718
+ '--dsw-alias-button-primary-fill': nivel.principal,
719
+ '--dsw-alias-button-primary-hover': nivel.hover,
720
+ '--dsw-alias-button-primary-dimmed': nivel.suave,
721
+ '--dsw-alias-label-primary-foreground': nivel.textoDelBoton,
722
+ '--dsw-alias-button-ghost-active-fill': nivel.suave,
723
+ '--dsw-alias-button-ghost-active-border': nivel.principal,
724
+ '--dsw-alias-button-ghost-active-hover': nivel.seleccion,
725
+ '--dsw-alias-button-elevated-fill': '#222529',
726
+ '--dsw-alias-button-floating-fill': '#222529',
727
+ '--dsw-alias-button-floating-hover': '#2a2d31',
728
+ '--dsw-alias-button-contrast-fill': '#f0eee6',
729
+ '--dsw-alias-button-tool-bar-fill': '#222529',
730
+ '--dsw-alias-button-tool-bar-hover': '#30353a',
731
+ '--dsw-alias-interactive-bg-hover': '#292c30',
732
+ '--dsw-alias-interactive-bg-active': nivel.seleccion,
733
+ '--dsw-alias-interactive-bg-hover-solid': '#2a2d31',
734
+ '--dsw-alias-interactive-bg-hover-accent': nivel.suave,
735
+ '--dsw-alias-state-success-primary': nivel.detalle,
736
+ '--dsw-alias-state-success-secondary': nivel.detalle,
737
+ '--dsw-alias-state-success-tertiary': nivel.suave,
738
+ '--dsw-alias-state-warn-primary': nivel.detalle,
739
+ '--dsw-alias-state-warn-secondary': nivel.detalle,
740
+ '--dsw-alias-state-warn-tertiary': nivel.suave,
741
+ '--dsw-alias-state-warn-label': nivel.detalle,
742
+ // El error SÍ tiene que leerse como error en los tres temas: en MINIMAL
743
+ // es un rojo apagado, que es lo más «negro y gris» que sigue avisando.
744
+ '--dsw-alias-state-error-primary': nivel.id === 'minimal' ? '#c9707a' : '#ff5c7a',
745
+ '--dsw-alias-state-error-secondary': nivel.id === 'minimal' ? '#c9707a' : '#ff5c7a',
746
+ '--dsw-alias-state-business-primary': nivel.detalle,
747
+ '--dsw-alias-state-business-tertiary': nivel.suave,
748
+ '--dsw-alias-link': nivel.acento,
749
+ '--dsw-alias-toast-bg': '#2a2d31',
750
+ '--dsw-alias-tooltip-bg': '#2a2d31',
751
+ '--dsw-specific-sidebar-nav-item-active': nivel.suave,
752
+ '--dsw-specific-sidebar-nav-item-active-accent': nivel.filoActivo,
753
+ '--dsw-specific-sidebar-nav-item-hover': '#202225',
754
+ '--dsw-specific-login-input': '#181a1d',
755
+ // El brillo de «trabajando» del motor es azul DeepSeek puro
756
+ // (`--dsw-static-deepseek-*`): aquí pasa a ser el color de la casa.
757
+ '--dsw-static-deepseek-200': nivel.brillo[200],
758
+ '--dsw-static-deepseek-300': nivel.brillo[300],
759
+ '--dsw-static-deepseek-400': nivel.brillo[400],
760
+ '--dsw-static-deepseek-450': nivel.brillo[450],
761
+ '--dsw-static-deepseek-500': nivel.brillo[500],
762
+ '--dsw-static-deepseek-600': nivel.brillo[600],
763
+ };
764
+ }
765
+
766
+ /** ¿Es uno de los tres temas de RATACODE? */
767
+ function esTemaNuestro(id) {
768
+ return TEMAS.some((t) => t.id === id);
769
+ }
770
+
771
+ /**
772
+ * Registra los tres temas y deja puesto el que la casa recuerde.
773
+ * @param ctx - contexto del plugin de navegador (con `theme`).
774
+ * @returns el id que ha quedado activo.
775
+ */
776
+ function paqueteDeTemas(ctx) {
777
+ for (const nivel of TEMAS) {
778
+ ctx.effect(
779
+ () => ctx.theme.register({ id: nivel.id, colorScheme: 'dark', tokens: tokensDelTema(nivel) }),
780
+ 'ratacode-piel: tema ' + nivel.id,
781
+ );
782
+ }
783
+ // El que la casa recuerde. Mientras llega la respuesta se pone el de
784
+ // fábrica, para que no haya un parpadeo con el tema del motor.
785
+ const estado = { activo: TEMA_POR_DEFECTO };
786
+ const pon = (id) => {
787
+ const bueno = esTemaNuestro(id) ? id : TEMA_POR_DEFECTO;
788
+ estado.activo = bueno;
789
+ try { ctx.theme.setTheme(bueno); } catch { /* tema no registrado: se queda el que haya */ }
790
+ marcarEnElDocumento(bueno);
791
+ pintarFilaDeTemas(ctx, estado);
792
+ };
793
+ pon(TEMA_POR_DEFECTO);
794
+ pedir('/ratacode/tema', 'GET').then((r) => {
795
+ if (r.ok === true && typeof r.tema === 'string') pon(r.tema);
796
+ });
797
+ // El motor re-adopta SU preferencia durable (`light`/`dark`/`system`) cada
798
+ // vez que su ámbito de ajustes cambia —y cambia al tocar el tamaño de
799
+ // letra—, y como un id de tema de fuera NO cabe en su esquema, al adoptar
800
+ // se cae el nuestro. Por eso, cuando el que llega es uno de los suyos, se
801
+ // vuelve a poner el nuestro; cuando el que llega es uno de los tres de la
802
+ // casa, es que lo hemos puesto nosotros y sólo hay que apuntarlo. Termina
803
+ // solo: `setTheme` con el mismo valor no publica nada, así que esto no
804
+ // puede quedarse girando.
805
+ ctx.on('theme/change', (instantanea) => {
806
+ const suyo = instantanea?.preference;
807
+ if (suyo === 'light' || suyo === 'dark' || suyo === 'system') { pon(estado.activo); return; }
808
+ if (esTemaNuestro(suyo)) estado.activo = suyo;
809
+ marcarEnElDocumento(estado.activo);
810
+ pintarFilaDeTemas(ctx, estado);
811
+ });
812
+ // La fila del aspecto la repinta el motor al re-renderizar: se vuelve a
813
+ // poner sin prisa, y sólo si de verdad falta.
814
+ setInterval(() => pintarFilaDeTemas(ctx, estado), 900);
815
+ return estado.activo;
816
+ }
817
+
818
+ /**
819
+ * Apunta el tema en el documento: `<html data-ratacode-tema="…">`. De ahí
820
+ * tiran el CSS de la casa (los colores de la marca y las ratitas de fondo) y
821
+ * `ratacode-vida.js`, que para su fotograma en MINIMAL.
822
+ * @param id - el tema activo.
823
+ */
824
+ function marcarEnElDocumento(id) {
825
+ if (typeof document === 'undefined') return;
826
+ if (document.documentElement.dataset.ratacodeTema !== id) document.documentElement.dataset.ratacodeTema = id;
827
+ }
828
+
829
+ /**
830
+ * La fila «Aspecto» de RATACODE, en su sitio: la nativa (Claro · Oscuro ·
831
+ * Sistema) la esconde el CSS (`.mr-temas` la sustituye) y aquí se monta la
832
+ * nuestra, con los tres temas EN SU ORDEN y su color a la vista. Se vuelve a
833
+ * montar si el motor repinta la sección.
834
+ * @param ctx - contexto del plugin (para el servicio de temas).
835
+ * @param estado - `{activo}`: el tema que está puesto (se actualiza aquí al pulsar).
836
+ */
837
+ function pintarFilaDeTemas(ctx, estado) {
838
+ if (typeof document === 'undefined') return;
839
+ const cuboNativo = document.querySelector('[class*="_themeCube"]');
840
+ if (cuboNativo === null) return; // Ajustes no está abierto
841
+ const grupo = cuboNativo.closest('[class*="_group"]');
842
+ if (grupo === null) return;
843
+ let fila = grupo.parentElement?.querySelector(':scope > .mr-temas');
844
+ if (!fila) {
845
+ fila = document.createElement('div');
846
+ fila.className = 'mr-temas';
847
+ const titulo = document.createElement('div');
848
+ titulo.className = 'mr-temas-titulo';
849
+ titulo.textContent = 'Aspecto';
850
+ const filaCubos = document.createElement('div');
851
+ filaCubos.className = 'mr-temas-fila';
852
+ for (const nivel of TEMAS) {
853
+ const boton = document.createElement('button');
854
+ boton.type = 'button';
855
+ boton.className = 'mr-tema';
856
+ boton.dataset.tema = nivel.id;
857
+ const punto = document.createElement('span');
858
+ punto.className = 'mr-tema-punto';
859
+ punto.style.background = nivel.principal;
860
+ boton.append(punto, document.createTextNode(nivel.etiqueta));
861
+ boton.addEventListener('click', () => {
862
+ estado.activo = nivel.id;
863
+ try { ctx.theme.setTheme(nivel.id); } catch { /* sin servicio: se apunta igual */ }
864
+ marcarEnElDocumento(nivel.id);
865
+ pintarFilaDeTemas(ctx, estado);
866
+ fetch('/ratacode/tema', {
867
+ method: 'POST', credentials: 'same-origin', cache: 'no-store',
868
+ headers: { 'content-type': 'application/json' },
869
+ body: JSON.stringify({ tema: nivel.id }),
870
+ }).catch(() => { /* sin casa que apunte: el tema sigue puesto en esta ventana */ });
871
+ });
872
+ filaCubos.append(boton);
873
+ }
874
+ fila.append(titulo, filaCubos);
875
+ grupo.after(fila);
876
+ }
877
+ for (const boton of fila.querySelectorAll('.mr-tema')) {
878
+ boton.setAttribute('aria-pressed', boton.dataset.tema === estado.activo ? 'true' : 'false');
879
+ }
880
+ }
881
+
882
+ /**
883
+ * R24 · Registra los avisos en los tres idiomas y los pinta (y los vuelve a
884
+ * pintar si el motor repinta la pantalla, cada 900 ms, como la fila
885
+ * «Aspecto»): no se toca ningún componente del motor ni ningún fichero de
886
+ * ajustes, sólo se añade una línea donde toca.
887
+ * @param ctx - contexto del plugin de navegador (con `locale`).
888
+ */
889
+ function avisosDeLaCasa(ctx) {
890
+ ctx.effect(() => ctx.locale.register(AVISOS_NS, AVISOS), 'ratacode-piel: los avisos, en los tres idiomas');
891
+ const t = ctx.locale.bind(AVISOS_NS);
892
+ tAvisos = t;
893
+ const pintar = () => { try { pintarAvisos(ctx, t); } catch { /* un aviso nunca tumba la pantalla */ } };
894
+ pintar();
895
+ setInterval(pintar, 900);
896
+ }
897
+
898
+ /**
899
+ * Las tres líneas, cada una en su sitio:
900
+ * A · bajo el permiso de Ajustes › General, SÓLO si está «A rienda suelta»
901
+ * (con los otros dos modos la frase no sería verdad);
902
+ * B · arriba en Ajustes › Modelos, bajo su rótulo y su intro, encima de
903
+ * todos los proveedores;
904
+ * C · bajo la caja del encargo, alineada con ella, SÓLO si su selector de
905
+ * permiso está en «A rienda suelta».
906
+ * El texto lo pone el servicio de idiomas del motor: aquí no se traduce nada.
907
+ * @param ctx - contexto del plugin (para los diccionarios del motor).
908
+ * @param t - el traductor de {@link AVISOS_NS}.
909
+ */
910
+ function pintarAvisos(ctx, t) {
911
+ if (typeof document === 'undefined') return;
912
+ const permiso = ctx.locale.bind('settings.permission');
913
+ const conversacion = ctx.locale.bind('conversation');
914
+ const modelos = ctx.locale.bind('settings.models');
915
+ const riendaSuelta = permiso('preset.fullAccess');
916
+ const enLaCaja = conversacion('access.preset.fullAccess');
917
+
918
+ // A · Ajustes › General: la fila del permiso, por su rótulo (en el idioma puesto).
919
+ const texto = [...document.querySelectorAll('[class*="_rowText"]')]
920
+ .find((c) => (c.querySelector('[class*="_title"]')?.textContent ?? '').trim() === permiso('title'));
921
+ if (texto !== undefined) {
922
+ const fila = texto.parentElement;
923
+ const puesto = (fila?.querySelector('[class*="_selector"]')?.textContent ?? '').trim();
924
+ let linea = texto.querySelector(':scope > .mr-aviso-permiso');
925
+ if (puesto !== riendaSuelta) {
926
+ if (linea !== null) linea.remove();
927
+ } else {
928
+ if (linea === null) {
929
+ linea = document.createElement('p');
930
+ linea.className = 'mr-aviso-permiso';
931
+ texto.append(linea);
932
+ }
933
+ if (linea.textContent !== t('permiso.fullAccess')) linea.textContent = t('permiso.fullAccess');
934
+ }
935
+ }
936
+
937
+ // B · Ajustes › Modelos: arriba del todo lo que se lee del proveedor.
938
+ const rotulo = [...document.querySelectorAll('h2[class*="_title"]')]
939
+ .find((h) => (h.textContent ?? '').trim() === modelos('title'));
940
+ const seccion = rotulo?.parentElement;
941
+ if (seccion !== undefined && seccion !== null) {
942
+ const intro = seccion.querySelector(':scope > [class*="_intro"]');
943
+ if (intro !== null) {
944
+ let linea = seccion.querySelector(':scope > .mr-aviso-modelos');
945
+ if (linea === null) {
946
+ linea = document.createElement('p');
947
+ linea.className = 'mr-aviso-modelos';
948
+ intro.after(linea);
949
+ }
950
+ if (linea.textContent !== t('modelos.proveedor')) linea.textContent = t('modelos.proveedor');
951
+ }
952
+ }
953
+
954
+ // C · La caja del encargo: el mismo aviso, bajo ella, si su selector está
955
+ // en «A rienda suelta». Se busca el rótulo del selector (el del motor),
956
+ // nunca se pulsa nada.
957
+ for (const caja of document.querySelectorAll('[data-composer-card]')) {
958
+ const padre = caja.parentElement;
959
+ if (padre === null) continue;
960
+ const suyo = [...caja.querySelectorAll('[class*="_triggerLabel"]')]
961
+ .some((s) => (s.textContent ?? '').trim() === enLaCaja);
962
+ let linea = padre.querySelector(':scope > .mr-aviso-caja');
963
+ if (!suyo) {
964
+ if (linea !== null) linea.remove();
965
+ continue;
966
+ }
967
+ if (linea === null) {
968
+ linea = document.createElement('div');
969
+ linea.className = 'mr-aviso-caja';
970
+ padre.insertBefore(linea, caja.nextSibling);
971
+ }
972
+ if (linea.textContent !== t('permiso.fullAccess')) linea.textContent = t('permiso.fullAccess');
973
+ }
974
+ }
975
+
976
+ /** Servicios que necesita el plugin de cliente. */
977
+ const inject = ['slots', 'locale', 'theme'];
978
+
979
+ /**
980
+ * Monta las secciones de la piel en el menú de Ajustes: «Modelos locales» (R18,
981
+ * justo detrás de Models) y «Conexiones» (R12/R23).
982
+ * @param ctx - contexto del plugin de navegador.
983
+ */
984
+ function apply(ctx) {
985
+ // R21 · el español, por la vía oficial de idiomas. Va LO PRIMERO: cuanto
986
+ // antes esté el diccionario, antes sale la pantalla en cristiano.
987
+ const idiomas = paqueteDeIdioma(ctx);
988
+ // R21 · los tres temas, por la vía oficial de temas, y el que la casa
989
+ // recuerde puesto. Va detrás del idioma a propósito: el tema se apunta en
990
+ // `<html data-ratacode-tema>`, que es de donde tira el CSS de la casa.
991
+ const tema = paqueteDeTemas(ctx);
992
+ // R24 · los tres avisos de una línea (el del permiso, DOS veces: en
993
+ // Ajustes › General y bajo la caja del encargo; y el de los proveedores,
994
+ // arriba en Ajustes › Modelos), en los tres idiomas. Van detrás del
995
+ // idioma: los pinta el traductor, que tiene que estar puesto.
996
+ avisosDeLaCasa(ctx);
997
+ ctx.slots.inject('settings.section', () => ctx.slots.register({
998
+ name: 'settings.section',
999
+ id: 'modelos-locales',
1000
+ order: 12,
1001
+ label: () => 'Modelos locales',
1002
+ }, SeccionModelosLocales));
1003
+ ctx.slots.inject('settings.section', () => ctx.slots.register({
1004
+ name: 'settings.section',
1005
+ id: 'conexiones',
1006
+ order: 25,
1007
+ label: () => 'Conexiones',
1008
+ }, SeccionConexiones));
1009
+ if (typeof console !== 'undefined') {
1010
+ console.info('RATACODE · Ajustes › General › Language: ' + IDIOMA_ETIQUETA
1011
+ + ' (' + IDIOMA + ', ' + idiomas.length + ' diccionarios)'
1012
+ + ' · Aspecto: ' + tema);
1013
+ }
1014
+ }
1015
+
1016
+ exports.apply = apply;
1017
+ exports.inject = inject;
1018
+ return module.exports;
1019
+ },
1020
+ });