KikoCis commited on
Commit
6b45f01
·
verified ·
1 Parent(s): 13508be

Sync with production: full i18n (EN/ES), English metadata, 15-min challenge banner

Browse files
Files changed (7) hide show
  1. README.md +17 -3
  2. index.html +1 -0
  3. js/challenge.js +74 -0
  4. js/humanize.js +52 -19
  5. js/i18n.js +144 -0
  6. js/main.js +75 -73
  7. js/mind.js +2 -1
README.md CHANGED
@@ -23,6 +23,13 @@ Open a local folder and a full web IDE unfolds around it — real Monaco (the VS
23
  a file tree, tabs — with an AI agent that **reads, searches and edits your project for real**.
24
  The model runs on **your** GPU via WebGPU. Your code never leaves your machine.
25
 
 
 
 
 
 
 
 
26
  > ⚠️ **Open it full-screen** ([direct link](https://kikocis-elffuss-code.static.hf.space)) — the
27
  > File System Access API (opening a local folder) is blocked inside the embedded iframe.
28
 
@@ -34,15 +41,22 @@ bug and fixes it with a minimal edit — genuine tool-calling, not autocomplete.
34
  ## Why it's different
35
 
36
  - **Your code stays local.** The folder is opened via the File System Access API — no upload, no account.
37
- - **The model runs on your GPU** (WebGPU, Gemma-4 via LiteRT-LM; ONNX/transformers.js + Qwen2.5-0.5B as fallback).
38
  - **Real tool-calling**: `code.tree`, `code.read`, `code.search`, `code.edit`, `code.write` — you watch every call.
39
- - **Real editor**: Monaco, file tree, tabs, `Ctrl+S`.
40
  - **Zero install, zero build.** Vanilla ES modules.
 
41
  - External providers (OpenAI, Anthropic, Ollama) are **opt-in**; keys stay in your browser.
 
 
 
 
 
 
42
 
43
  ## Links
44
 
45
  - 🌐 Full app: <https://elffuss-code.utopiaia.com>
46
  - 💻 Source (Apache-2.0): <https://github.com/KikoCis/elffuss-code>
47
- - ✳️ Sibling project — Elffuss Claw (agentic OS): <https://github.com/KikoCis/elffuss-claw>
48
  - 🧬 Shared core: <https://github.com/KikoCis/elffuss>
 
23
  a file tree, tabs — with an AI agent that **reads, searches and edits your project for real**.
24
  The model runs on **your** GPU via WebGPU. Your code never leaves your machine.
25
 
26
+ > ⚡ **Build the best thing you can with Elffuss — 15-minute challenge, open until Sun 9 Aug 2026.**
27
+ > Open one of your own folders and let the agent fix a real bug or add a small feature in 15
28
+ > minutes, then post a screenshot of the diff (plus your prompt) as a reply on
29
+ > [X](https://x.com/KikoCisneros/status/2085358719190130963) or a comment on
30
+ > [LinkedIn](https://www.linkedin.com/feed/update/urn:li:activity:7491125059768516608/).
31
+ > **The most-liked entry wins** and gets featured in this Space and in the repo.
32
+
33
  > ⚠️ **Open it full-screen** ([direct link](https://kikocis-elffuss-code.static.hf.space)) — the
34
  > File System Access API (opening a local folder) is blocked inside the embedded iframe.
35
 
 
41
  ## Why it's different
42
 
43
  - **Your code stays local.** The folder is opened via the File System Access API — no upload, no account.
44
+ - **The model runs on your GPU** (WebGPU, Gemma-4 E4B via LiteRT-LM; ONNX/transformers.js + our healed LFM2.5-1.2B as fallback).
45
  - **Real tool-calling**: `code.tree`, `code.read`, `code.search`, `code.edit`, `code.write` — you watch every call.
46
+ - **Real editor**: Monaco, file tree, tabs, `Ctrl+S`, and git branch/last commit parsed straight from `.git`.
47
  - **Zero install, zero build.** Vanilla ES modules.
48
+ - **Your language**: the whole IDE follows your browser's language (English, Spanish and more).
49
  - External providers (OpenAI, Anthropic, Ollama) are **opt-in**; keys stay in your browser.
50
+ - Optional local **bridge** for real `node` / `npm` / `python` execution — downloaded from the main site.
51
+
52
+ ## How it's built
53
+
54
+ Four write-ups on the full stack — model, runtime, agent harness, editor and filesystem access,
55
+ all in one tab: **<https://bitacora.utopiaia.com/#elffuss>**
56
 
57
  ## Links
58
 
59
  - 🌐 Full app: <https://elffuss-code.utopiaia.com>
60
  - 💻 Source (Apache-2.0): <https://github.com/KikoCis/elffuss-code>
61
+ - ✳️ Sibling project — Elffuss Claw (agentic OS): <https://huggingface.co/spaces/KikoCis/elffuss-claw>
62
  - 🧬 Shared core: <https://github.com/KikoCis/elffuss>
index.html CHANGED
@@ -147,5 +147,6 @@
147
  </div>
148
 
149
  <script type="module" src="js/main.js"></script>
 
150
  </body>
151
  </html>
 
147
  </div>
148
 
149
  <script type="module" src="js/main.js"></script>
150
+ <script type="module" src="js/challenge.js"></script>
151
  </body>
152
  </html>
js/challenge.js ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // challenge.js — top bar announcing the 15-minute challenge, with the two places to post
2
+ // the result. Self-contained (injects its own CSS), dismissible, and it removes itself
3
+ // after the deadline so nobody has to remember to take it down.
4
+ import { uiLang } from './i18n.js';
5
+
6
+ const DEADLINE = Date.parse('2026-08-10T00:00:00Z'); // winner announced Sun Aug 9
7
+ const KEY = 'elffuss_challenge_v1_dismissed';
8
+ const X_URL = 'https://x.com/KikoCisneros/status/2085358719190130963';
9
+ const LI_URL = 'https://www.linkedin.com/feed/update/urn:li:activity:7491125059768516608/';
10
+
11
+ const COPY = {
12
+ es: {
13
+ text: '⚡ Reto de 15 min: abre una carpeta tuya y deja que Elffuss arregle un bug real. Gana el comentario con más likes — ganador el domingo 9.',
14
+ x: 'Publicar en X', li: 'En LinkedIn', close: 'Cerrar aviso',
15
+ },
16
+ en: {
17
+ text: '⚡ 15-min challenge: open one of your folders and let Elffuss fix a real bug. Most-liked comment wins — winner on Sun Aug 9.',
18
+ x: 'Post on X', li: 'On LinkedIn', close: 'Dismiss',
19
+ },
20
+ };
21
+
22
+ export function mountChallengeBar() {
23
+ if (Date.now() > DEADLINE) return;
24
+ try { if (localStorage.getItem(KEY)) return; } catch (e) {}
25
+ const c = COPY[uiLang() === 'es' ? 'es' : 'en'];
26
+
27
+ const css = document.createElement('style');
28
+ css.textContent = `
29
+ body.elf-ch { padding-top: 38px; }
30
+ body.elf-ch #ide { height: calc(100vh - 38px); }
31
+ .elf-ch-bar {
32
+ position: fixed; inset: 0 0 auto 0; z-index: 400; height: 38px;
33
+ display: flex; align-items: center; justify-content: center; gap: 10px;
34
+ padding: 0 42px 0 12px; font-size: .82rem; color: #fff;
35
+ background: linear-gradient(135deg, var(--accent, #7c5cff), var(--accent2, #ff5cd6));
36
+ box-shadow: 0 2px 12px rgba(0,0,0,.35);
37
+ }
38
+ .elf-ch-bar span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
39
+ .elf-ch-bar a {
40
+ flex: none; padding: 3px 10px; border-radius: 999px; text-decoration: none;
41
+ color: #fff; background: rgba(0,0,0,.28); font-weight: 600;
42
+ }
43
+ .elf-ch-bar a:hover { background: rgba(0,0,0,.45); }
44
+ .elf-ch-bar button {
45
+ position: absolute; right: 8px; top: 50%; transform: translateY(-50%);
46
+ background: none; border: none; color: #fff; font-size: 1.1rem; cursor: pointer; opacity: .8;
47
+ }
48
+ .elf-ch-bar button:hover { opacity: 1; }
49
+ @media (max-width: 620px) { .elf-ch-bar span { font-size: .74rem; } }
50
+ `;
51
+ document.head.appendChild(css);
52
+
53
+ const bar = document.createElement('div');
54
+ bar.className = 'elf-ch-bar';
55
+ bar.innerHTML = `<span></span>
56
+ <a target="_blank" rel="noopener" href="${X_URL}"></a>
57
+ <a target="_blank" rel="noopener" href="${LI_URL}"></a>
58
+ <button type="button" aria-label=""></button>`;
59
+ bar.querySelector('span').textContent = c.text;
60
+ const [ax, ali] = bar.querySelectorAll('a');
61
+ ax.textContent = c.x; ali.textContent = c.li;
62
+ const btn = bar.querySelector('button');
63
+ btn.textContent = '✕'; btn.setAttribute('aria-label', c.close);
64
+ btn.addEventListener('click', () => {
65
+ try { localStorage.setItem(KEY, '1'); } catch (e) {}
66
+ bar.remove(); document.body.classList.remove('elf-ch');
67
+ });
68
+
69
+ document.body.classList.add('elf-ch');
70
+ document.body.appendChild(bar);
71
+ }
72
+
73
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', mountChallengeBar);
74
+ else mountChallengeBar();
js/humanize.js CHANGED
@@ -4,26 +4,59 @@
4
  // nombres de herramienta de Elffuss Code (code.*, terminal.*) y de Elffuss
5
  // Claw (fs.*, app.*, web.*, skill.*, memory.*, tasks.*) — un tool desconocido
6
  // cae al genérico «nombre argumento» sin romper nada.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  export function humanizeTool(name, args) {
8
  const p = args?.path, q = args?.query, c = args?.command, n = args?.name;
 
9
  switch (name) {
10
- case 'code.read': case 'fs.read': return `leyendo ${p}…`;
11
- case 'code.write': case 'fs.write': return `escribiendo ${p}…`;
12
- case 'code.tree': return `explorando ${p || 'el proyecto'}…`;
13
- case 'fs.list': return `explorando ${p || 'la carpeta'}…`;
14
- case 'code.search': return `buscando «${q}»…`;
15
- case 'fs.pick_folder': return 'pidiendo acceso a una carpeta…';
16
- case 'fs.copy': return `copiando ${args?.pattern || 'archivos'}…`;
17
- case 'fs.watch': return `vigilando ${args?.from || 'una carpeta'}…`;
18
- case 'terminal.run': return `ejecutando: ${c}`;
19
- case 'app.create': return `creando la app «${n}»…`;
20
- case 'app.open': return `abriendo la app «${n}»…`;
21
- case 'skill.create': return `creando la skill «${n}»…`;
22
- case 'memory.save': return `recordando: ${args?.fact}…`;
23
- case 'tasks.add': return 'programando una tarea…';
24
- case 'web.search': return `buscando en internet «${q}»…`;
25
- case 'web.images': return `buscando imágenes de «${q}»…`;
26
- case 'web.fetch': return `leyendo ${p || args?.url}…`;
27
  default: return name + (p || q || c || n ? ' ' + (p || q || c || n) : '');
28
  }
29
  }
@@ -36,7 +69,7 @@ export function humanizeTool(name, args) {
36
  export function humanizeStreamPreview(buf) {
37
  if (buf.search(/```/) === -1 && !/^\s*\{\s*"tool"/.test(buf)) return null;
38
  const toolM = buf.match(/"tool"\s*:\s*"([\w.]+)"/);
39
- if (!toolM) return 'preparando una acción…';
40
  const pathM = buf.match(/"path"\s*:\s*"((?:[^"\\]|\\.)*)"/);
41
  const queryM = buf.match(/"query"\s*:\s*"((?:[^"\\]|\\.)*)"/);
42
  const cmdM = buf.match(/"command"\s*:\s*"((?:[^"\\]|\\.)*)"/);
@@ -44,5 +77,5 @@ export function humanizeStreamPreview(buf) {
44
  const out = humanizeTool(toolM[1], { path: pathM?.[1], query: queryM?.[1], command: cmdM?.[1], name: nameM?.[1] });
45
  // el nombre de la tool ya se ve pero su campo (path/query/…) aún no llegó:
46
  // mejor el genérico que un «leyendo undefined…» a medio streamear
47
- return /undefined/.test(out) ? 'preparando una acción…' : out;
48
  }
 
4
  // nombres de herramienta de Elffuss Code (code.*, terminal.*) y de Elffuss
5
  // Claw (fs.*, app.*, web.*, skill.*, memory.*, tasks.*) — un tool desconocido
6
  // cae al genérico «nombre argumento» sin romper nada.
7
+ //
8
+ // Localizado: ES por defecto (usuarios españoles intactos), EN de fallback.
9
+ // Autocontenido a propósito — este fichero es IDÉNTICO en Code y en Claw, y
10
+ // Code no tiene i18n.js, así que la tabla de idioma vive aquí.
11
+ const HL = {
12
+ es: {
13
+ read: p => `leyendo ${p}…`, write: p => `escribiendo ${p}…`,
14
+ tree: p => `explorando ${p || 'el proyecto'}…`, list: p => `explorando ${p || 'la carpeta'}…`,
15
+ search: q => `buscando «${q}»…`, pick: () => 'pidiendo acceso a una carpeta…',
16
+ copy: x => `copiando ${x || 'archivos'}…`, watch: x => `vigilando ${x || 'una carpeta'}…`,
17
+ run: c => `ejecutando: ${c}`,
18
+ appCreate: n => `creando la app «${n}»…`, appOpen: n => `abriendo la app «${n}»…`,
19
+ skillCreate: n => `creando la skill «${n}»…`, memSave: f => `recordando: ${f}…`,
20
+ taskAdd: () => 'programando una tarea…',
21
+ webSearch: q => `buscando en internet «${q}»…`, webImages: q => `buscando imágenes de «${q}»…`,
22
+ webFetch: u => `leyendo ${u}…`, prep: 'preparando una acción…',
23
+ },
24
+ en: {
25
+ read: p => `reading ${p}…`, write: p => `writing ${p}…`,
26
+ tree: p => `exploring ${p || 'the project'}…`, list: p => `exploring ${p || 'the folder'}…`,
27
+ search: q => `searching “${q}”…`, pick: () => 'requesting folder access…',
28
+ copy: x => `copying ${x || 'files'}…`, watch: x => `watching ${x || 'a folder'}…`,
29
+ run: c => `running: ${c}`,
30
+ appCreate: n => `creating the app “${n}”…`, appOpen: n => `opening the app “${n}”…`,
31
+ skillCreate: n => `creating the skill “${n}”…`, memSave: f => `remembering: ${f}…`,
32
+ taskAdd: () => 'scheduling a task…',
33
+ webSearch: q => `searching the web for “${q}”…`, webImages: q => `searching images of “${q}”…`,
34
+ webFetch: u => `reading ${u}…`, prep: 'preparing an action…',
35
+ },
36
+ };
37
+ function hl() { return HL[(navigator.language || 'es').slice(0, 2).toLowerCase()] || HL.en; }
38
+
39
  export function humanizeTool(name, args) {
40
  const p = args?.path, q = args?.query, c = args?.command, n = args?.name;
41
+ const t = hl();
42
  switch (name) {
43
+ case 'code.read': case 'fs.read': return t.read(p);
44
+ case 'code.write': case 'fs.write': return t.write(p);
45
+ case 'code.tree': return t.tree(p);
46
+ case 'fs.list': return t.list(p);
47
+ case 'code.search': return t.search(q);
48
+ case 'fs.pick_folder': return t.pick();
49
+ case 'fs.copy': return t.copy(args?.pattern);
50
+ case 'fs.watch': return t.watch(args?.from);
51
+ case 'terminal.run': return t.run(c);
52
+ case 'app.create': return t.appCreate(n);
53
+ case 'app.open': return t.appOpen(n);
54
+ case 'skill.create': return t.skillCreate(n);
55
+ case 'memory.save': return t.memSave(args?.fact);
56
+ case 'tasks.add': return t.taskAdd();
57
+ case 'web.search': return t.webSearch(q);
58
+ case 'web.images': return t.webImages(q);
59
+ case 'web.fetch': return t.webFetch(p || args?.url);
60
  default: return name + (p || q || c || n ? ' ' + (p || q || c || n) : '');
61
  }
62
  }
 
69
  export function humanizeStreamPreview(buf) {
70
  if (buf.search(/```/) === -1 && !/^\s*\{\s*"tool"/.test(buf)) return null;
71
  const toolM = buf.match(/"tool"\s*:\s*"([\w.]+)"/);
72
+ if (!toolM) return hl().prep;
73
  const pathM = buf.match(/"path"\s*:\s*"((?:[^"\\]|\\.)*)"/);
74
  const queryM = buf.match(/"query"\s*:\s*"((?:[^"\\]|\\.)*)"/);
75
  const cmdM = buf.match(/"command"\s*:\s*"((?:[^"\\]|\\.)*)"/);
 
77
  const out = humanizeTool(toolM[1], { path: pathM?.[1], query: queryM?.[1], command: cmdM?.[1], name: nameM?.[1] });
78
  // el nombre de la tool ya se ve pero su campo (path/query/…) aún no llegó:
79
  // mejor el genérico que un «leyendo undefined…» a medio streamear
80
+ return /undefined/.test(out) ? hl().prep : out;
81
  }
js/i18n.js ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // i18n ligero del «chrome» de Elffuss Code. El modelo ya responde en el idioma
2
+ // del navegador (agent.js → userLang); esto localiza los textos estáticos y de
3
+ // runtime de la UI (placeholder, indicadores «pensando/escribe», estado del
4
+ // modelo, saludo del proyecto, la Mente). ES por defecto — cero impacto en
5
+ // usuarios españoles — con inglés de fallback. Sustitución simple {var}.
6
+ const L = {
7
+ es: {
8
+ ph: 'Pídele código a Elffuss…',
9
+ thinking: 'Elffuss está pensando', writing: 'Elffuss escribe · {n} car.', using: 'Elffuss usa {name}',
10
+ modelLoading: 'Descargando el modelo IA · {pct}% · {loaded}/{total} MB',
11
+ modelReady: 'Modelo IA listo', modelReadyBadge: 'Modelo IA listo · готово ✳',
12
+ welcome: 'Привіт 👋 Proyecto «{name}» abierto. Pregúntame por el código, pídeme cambios o dime «árbol». Todo se queda en tu máquina.',
13
+ mindTitle: 'MENTE DE ELFFUSS', avatarTitle: 'Mente de Elffuss — cerebro autónomo',
14
+ // landing (estático) + tooltips de la barra/toolbar
15
+ tagline: 'Abre tu carpeta de código y programa con una elfa al lado.',
16
+ tagline2: 'Editor tipo VS Code + agente <b>en tu navegador</b>. Nada sale de tu máquina. 😉',
17
+ openProject: '📁 Abrir carpeta de código', resume: 'Reanudar', discard: 'Descartar', cloneOpen: 'Descargar y abrir',
18
+ clonePh: 'pega la URL de un repo público (GitHub/Bitbucket)…',
19
+ searchPh: 'Buscar archivos por nombre (: para ir a línea, @ para símbolo, > para comandos)',
20
+ tFiles: 'Explorador', tSearch: 'Buscar (Cmd/Ctrl+P)', tArch: 'Arquitectura (grafo de dependencias)',
21
+ tCity: 'Ciudad 3D', tTerm: 'Terminal (Ctrl+`)', tSkills: 'Skills', tActSettings: 'Ajustes y modelo',
22
+ tGitBranch: 'rama actual', tHistory: 'Historial de conversaciones', tTopSettings: 'Ajustes, modelo y API keys',
23
+ tTermClose: 'cerrar (Ctrl+`)', tTermResize: 'arrastra para redimensionar',
24
+ tCtx: 'contexto acumulado en la conversación', tPlus: 'Adjuntar archivo del proyecto (@)',
25
+ tAutoedit: 'Editar archivos automáticamente',
26
+ tGoal: 'Modo Objetivo: descompone el mensaje en tareas y las ejecuta una a una (planificador + ejecutor)',
27
+ tFlip: 'cambiar entre chat y editor', tSlash: 'Comandos',
28
+ // pass-3: chrome profundo del IDE (paleta, menús, historial, ajustes, notifs)
29
+ tabClose: 'Cerrar pestaña (no borra la conversación)', newConv: 'Nueva conversación',
30
+ histEmpty: 'Sin conversaciones guardadas todavía.', histDel: 'Eliminar para siempre',
31
+ confirmDel: '¿Eliminar «{title}» para siempre?', thisConv: 'esta conversación',
32
+ cmdsSep: 'Comandos', slTree: 'árbol del proyecto', slSearch: 'buscar en el código',
33
+ slSkills: 'gestionar skills', slModel: 'proveedores y modelo', slClear: 'nueva conversación',
34
+ pTree: 'árbol', pSearch: 'busca ', pCommit: 'resume los cambios y prepárame el mensaje de commit',
35
+ pProposal: 'Implementa esta propuesta de mejora con cambios reales en el código:\n\n',
36
+ attachSep: 'Adjuntar al mensaje (@)', attachOpen: 'archivo abierto', openProjFirst: 'Abre un proyecto primero',
37
+ autoTxt: 'Auto', reviewTxt: 'Revisar',
38
+ autoOn: 'Editar archivos automáticamente (clic para revisar antes)',
39
+ autoOff: 'Pedir confirmación antes de escribir (clic para automático)',
40
+ pSearchCode: 'Buscar en el código…', pManageSkills: 'Gestionar skills', pToggleExplorer: 'Alternar explorador',
41
+ pToggleTerm: 'Alternar terminal', pSave: 'Guardar (Ctrl+S)', palCmd: 'comando',
42
+ palGotoSym: 'Ir a símbolo en el editor…', palEmpty: 'Sin resultados',
43
+ mNewFile: 'Nuevo archivo…', promptNewPath: 'Ruta del nuevo archivo:', mSave: 'Guardar', mOpenFolder: 'Abrir carpeta…',
44
+ mSearchCode: 'Buscar en el código', mGotoFile: 'Ir a archivo…', mGotoSym: 'Ir a símbolo…',
45
+ mTerminal: 'Terminal', mArch: 'Arquitectura', mAbout: 'Acerca de Elffuss Code (GitHub)',
46
+ gNotRepo: 'No es un repositorio git', gBranch: 'Rama: {b}', gLast: 'Último: {m}', gCommit: 'Pídele a Elffuss que commitee',
47
+ nExec: '▶ Ejecutar', nView: 'Ver en la Mente',
48
+ fallbackToast: 'Ese Gemma no cargó (memoria/GPU) — uso Elffuss LM (ligero)…',
49
+ setStoreTitle: 'Modelo descargado (se cachea en tu navegador)', setClear: 'Vaciar caché', setClearing: 'Vaciando…',
50
+ setCached: '{gb} en caché', setNone: 'Nada cacheado todavía',
51
+ setBrToken: 'Token (lo imprime el programa al arrancarlo)', brTokenPh: 'pega aquí el token…',
52
+ setFeedbackPh: 'Cuéntanos qué ha pasado o qué te gustaría que hiciera…',
53
+ lblModel: 'Modelo', lblApiKey: 'API key',
54
+ setCalc: 'Calculando espacio…', setPersist: '✓ almacenamiento persistente (no se borra solo)',
55
+ setNoPersist: '⚠ sin persistencia: el navegador podría desalojarlo', setLimit: ' · límite ~{gb}',
56
+ setModelPh: 'modelo (p. ej. gpt-4o-mini)', keyNeeded: 'clave (no necesaria)',
57
+ },
58
+ en: {
59
+ ph: 'Ask Elffuss for code…',
60
+ thinking: 'Elffuss is thinking', writing: 'Elffuss writes · {n} chars', using: 'Elffuss uses {name}',
61
+ modelLoading: 'Downloading AI model · {pct}% · {loaded}/{total} MB',
62
+ modelReady: 'AI model ready', modelReadyBadge: 'AI model ready · готово ✳',
63
+ welcome: 'Привіт 👋 Project “{name}” open. Ask me about the code, request changes, or say “tree”. Everything stays on your machine.',
64
+ mindTitle: 'ELFFUSS MIND', avatarTitle: 'Elffuss Mind — autonomous brain',
65
+ tagline: 'Open your code folder and program with an elf by your side.',
66
+ tagline2: 'VS Code–style editor + agent <b>in your browser</b>. Nothing leaves your machine. 😉',
67
+ openProject: '📁 Open code folder', resume: 'Resume', discard: 'Discard', cloneOpen: 'Download & open',
68
+ clonePh: 'paste the URL of a public repo (GitHub/Bitbucket)…',
69
+ searchPh: 'Search files by name (: to go to line, @ for symbol, > for commands)',
70
+ tFiles: 'Explorer', tSearch: 'Search (Cmd/Ctrl+P)', tArch: 'Architecture (dependency graph)',
71
+ tCity: '3D City', tTerm: 'Terminal (Ctrl+`)', tSkills: 'Skills', tActSettings: 'Settings & model',
72
+ tGitBranch: 'current branch', tHistory: 'Conversation history', tTopSettings: 'Settings, model & API keys',
73
+ tTermClose: 'close (Ctrl+`)', tTermResize: 'drag to resize',
74
+ tCtx: 'context accumulated in the conversation', tPlus: 'Attach a project file (@)',
75
+ tAutoedit: 'Edit files automatically',
76
+ tGoal: 'Goal mode: breaks the message into tasks and runs them one by one (planner + executor)',
77
+ tFlip: 'switch between chat and editor', tSlash: 'Commands',
78
+ tabClose: 'Close tab (doesn’t delete the conversation)', newConv: 'New conversation',
79
+ histEmpty: 'No saved conversations yet.', histDel: 'Delete forever',
80
+ confirmDel: 'Delete “{title}” forever?', thisConv: 'this conversation',
81
+ cmdsSep: 'Commands', slTree: 'project tree', slSearch: 'search the code',
82
+ slSkills: 'manage skills', slModel: 'providers & model', slClear: 'new conversation',
83
+ pTree: 'tree', pSearch: 'search ', pCommit: 'summarize the changes and prepare my commit message',
84
+ pProposal: 'Implement this improvement proposal with real changes to the code:\n\n',
85
+ attachSep: 'Attach to message (@)', attachOpen: 'open file', openProjFirst: 'Open a project first',
86
+ autoTxt: 'Auto', reviewTxt: 'Review',
87
+ autoOn: 'Edit files automatically (click to review first)',
88
+ autoOff: 'Ask before writing (click for automatic)',
89
+ pSearchCode: 'Search the code…', pManageSkills: 'Manage skills', pToggleExplorer: 'Toggle explorer',
90
+ pToggleTerm: 'Toggle terminal', pSave: 'Save (Ctrl+S)', palCmd: 'command',
91
+ palGotoSym: 'Go to symbol in editor…', palEmpty: 'No results',
92
+ mNewFile: 'New file…', promptNewPath: 'New file path:', mSave: 'Save', mOpenFolder: 'Open folder…',
93
+ mSearchCode: 'Search the code', mGotoFile: 'Go to file…', mGotoSym: 'Go to symbol…',
94
+ mTerminal: 'Terminal', mArch: 'Architecture', mAbout: 'About Elffuss Code (GitHub)',
95
+ gNotRepo: 'Not a git repository', gBranch: 'Branch: {b}', gLast: 'Latest: {m}', gCommit: 'Ask Elffuss to commit',
96
+ nExec: '▶ Run', nView: 'View in the Mind',
97
+ fallbackToast: 'That Gemma didn’t load (memory/GPU) — using Elffuss LM (light)…',
98
+ setStoreTitle: 'Downloaded model (cached in your browser)', setClear: 'Clear cache', setClearing: 'Clearing…',
99
+ setCached: '{gb} cached', setNone: 'Nothing cached yet',
100
+ setBrToken: 'Token (printed by the program on startup)', brTokenPh: 'paste the token here…',
101
+ setFeedbackPh: 'Tell us what happened or what you’d like it to do…',
102
+ lblModel: 'Model', lblApiKey: 'API key',
103
+ setCalc: 'Calculating space…', setPersist: '✓ persistent storage (won’t be auto-cleared)',
104
+ setNoPersist: '⚠ not persistent: the browser could evict it', setLimit: ' · limit ~{gb}',
105
+ setModelPh: 'model (e.g. gpt-4o-mini)', keyNeeded: 'key (not needed)',
106
+ },
107
+ };
108
+
109
+ // Idioma del chrome: 2 letras, ES por defecto.
110
+ export function uiLang() {
111
+ return (navigator.language || 'es').slice(0, 2).toLowerCase();
112
+ }
113
+
114
+ // Traduce una clave de runtime con sustitución {var}. Fallback: idioma →
115
+ // inglés → la propia clave.
116
+ export function t(key, vars) {
117
+ const s = (L[uiLang()] || L.en)[key] ?? L.en[key] ?? key;
118
+ return vars ? s.replace(/\{(\w+)\}/g, (_, k) => (vars[k] ?? '')) : s;
119
+ }
120
+
121
+ // Textos estáticos del HTML (landing, placeholders, tooltips) que no pasan por
122
+ // t() en runtime. Se cablean una vez al arrancar; ES es el HTML por defecto.
123
+ export function applyI18n() {
124
+ document.documentElement.lang = uiLang();
125
+ const set = (sel, prop, key) => { const e = document.querySelector(sel); if (e) e[prop] = t(key); };
126
+ set('#prompt', 'placeholder', 'ph');
127
+ const tag = document.querySelector('#landing .tag');
128
+ if (tag) tag.innerHTML = `${t('tagline')}<br>${t('tagline2')}`;
129
+ set('#open-project', 'textContent', 'openProject');
130
+ set('#resume-clone-btn', 'textContent', 'resume');
131
+ set('#discard-clone-btn', 'textContent', 'discard');
132
+ set('#clone-btn', 'textContent', 'cloneOpen');
133
+ set('#clone-url', 'placeholder', 'clonePh');
134
+ set('#pal-input', 'placeholder', 'searchPh');
135
+ const titles = {
136
+ '#act-files': 'tFiles', '#act-search': 'tSearch', '#act-arch': 'tArch', '#act-city': 'tCity',
137
+ '#act-term': 'tTerm', '#act-skills': 'tSkills', '#act-settings': 'tActSettings',
138
+ '#git-branch': 'tGitBranch', '#btn-history': 'tHistory', '#btn-settings': 'tTopSettings',
139
+ '#term-close': 'tTermClose', '#term-resize': 'tTermResize', '#ctx-meter': 'tCtx',
140
+ '#btn-plus': 'tPlus', '#btn-autoedit': 'tAutoedit', '#btn-goal': 'tGoal',
141
+ '#code-flip': 'tFlip', '#btn-slash': 'tSlash',
142
+ };
143
+ for (const [sel, key] of Object.entries(titles)) set(sel, 'title', key);
144
+ }
js/main.js CHANGED
@@ -16,6 +16,7 @@ import * as ceo from './ceo.js';
16
  import * as mind from './mind.js';
17
  import { buildCityAdapter, loadThoughtsAdapter } from './mind-adapter.js';
18
  import { humanizeStreamPreview } from './humanize.js';
 
19
  import * as bridge from './bridge.js';
20
  import * as conv from './conversations.js';
21
  import { TASK_PREFIX } from './goal.js';
@@ -126,15 +127,15 @@ function renderPlanCard(plan) {
126
  export function thinkingBubble() {
127
  const div = addMsg('thinking', '');
128
  const label = document.createElement('span');
129
- label.textContent = 'Elffuss está pensando';
130
  const gen = document.createElement('div');
131
  gen.className = 'gen';
132
  div.append(label, gen);
133
  let buf = '';
134
  return {
135
- tick(t) {
136
- buf += t;
137
- label.textContent = `Elffuss escribe · ${buf.length} car.`; // caracteres, no tokens
138
  // si el buffer entró en un bloque de tool-call, no enseñes el JSON crudo
139
  // según va llegando: una frase humana («leyendo app.js…») en su lugar.
140
  const preview = humanizeStreamPreview(buf);
@@ -142,7 +143,7 @@ export function thinkingBubble() {
142
  gen.textContent = preview ? '⟐ ' + preview : (buf.length > 200 ? '…' : '') + buf.slice(-200);
143
  $('chat-log').scrollTop = $('chat-log').scrollHeight;
144
  },
145
- tool(name) { buf = ''; gen.textContent = ''; label.textContent = `Elffuss usa ${name}`; },
146
  remove() { div.remove(); },
147
  };
148
  }
@@ -232,7 +233,7 @@ function renderTabsBar() {
232
  name.title = title;
233
  const x = document.createElement('b');
234
  x.textContent = '×';
235
- x.title = 'Cerrar pestaña (no borra la conversación)';
236
  x.onclick = e => { e.stopPropagation(); conv.closeTab(c.id); };
237
  el.append(name, x);
238
  el.onclick = () => conv.switchTo(c.id);
@@ -240,7 +241,7 @@ function renderTabsBar() {
240
  }
241
  const plus = document.createElement('button');
242
  plus.className = 'tab-add';
243
- plus.title = 'Nueva conversación';
244
  plus.textContent = '+';
245
  plus.onclick = () => conv.create();
246
  bar.appendChild(plus);
@@ -251,7 +252,7 @@ async function renderHistoryPanel() {
251
  panel.replaceChildren();
252
  const head = document.createElement('div');
253
  head.className = 'hp-head';
254
- head.append(document.createTextNode('Historial de conversaciones'));
255
  const xBtn = document.createElement('button');
256
  xBtn.textContent = '✕';
257
  xBtn.onclick = () => { panel.hidden = true; };
@@ -264,31 +265,31 @@ async function renderHistoryPanel() {
264
  if (!all.length) {
265
  const empty = document.createElement('div');
266
  empty.className = 'hp-empty';
267
- empty.textContent = 'Sin conversaciones guardadas todavía.';
268
  list.appendChild(empty);
269
  return;
270
  }
271
  for (const c of all) {
272
  const row = document.createElement('div');
273
  row.className = 'hp-row';
274
- const t = document.createElement('div');
275
- t.className = 'hp-title';
276
- t.textContent = c.title || 'Nueva conversación';
277
  const d = new Date(c.updatedAt);
278
  const date = document.createElement('div');
279
  date.className = 'hp-date';
280
  date.textContent = d.toLocaleDateString() + ' ' + d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
281
  const del = document.createElement('button');
282
  del.className = 'hp-del';
283
- del.title = 'Eliminar para siempre';
284
  del.textContent = '🗑';
285
  del.onclick = async e => {
286
  e.stopPropagation();
287
- if (!confirm(`¿Eliminar «${c.title || 'esta conversación' para siempre?`)) return;
288
  await conv.remove(c.id);
289
  renderHistoryPanel();
290
  };
291
- row.append(t, date, del);
292
  row.onclick = async () => { await conv.open(c.id); panel.hidden = true; };
293
  list.appendChild(row);
294
  }
@@ -388,16 +389,16 @@ async function changeModel(id) {
388
  if (typeof p === 'string') return showModelProgress(p);
389
  if (p?.status === 'progress' && p.total) {
390
  const pct = Math.round(p.loaded / p.total * 100);
391
- showModelProgress(`Descargando el modelo IA · ${pct}% · ${(p.loaded / 1e6 | 0)}/${(p.total / 1e6 | 0)} MB`, pct);
392
  }
393
  });
394
  conv.setProvider(mod);
395
  activeModel = id;
396
  localStorage.setItem('elffusscode.model', id);
397
  $('model-dot').className = 'dot on';
398
- showModelProgress('Modelo IA listo · готово ✳', 100);
399
  setTimeout(() => showModelProgress(null), 2500);
400
- $('statusbar').textContent = 'Modelo IA listo';
401
  rebuildSelect();
402
  return true;
403
  } catch (e) {
@@ -406,7 +407,7 @@ async function changeModel(id) {
406
  if (id.startsWith('litert') && !_fellBack) {
407
  _fellBack = true;
408
  sessionStorage.setItem('elffusscode.skipGemma', '1');
409
- showModelProgress('Ese Gemma no cargó (memoria/GPU) — uso Elffuss LM (ligero)…');
410
  loadingId = null;
411
  const okc = await changeModel('onnx');
412
  _fellBack = false;
@@ -523,23 +524,23 @@ function renderSettings() {
523
  box.appendChild(grid);
524
 
525
  // --- Almacenamiento del modelo (caché persistente) ---
526
- box.append(el('div', 'sk-h', 'Modelo descargado (se cachea en tu navegador)'));
527
  const storeCard = el('div', 'prov-card');
528
- const storeInfo = el('span', null, 'Calculando espacio…'); storeInfo.style.cssText = 'font-size:.8rem;color:var(--fg)';
529
  const storeSub = el('div', 'field'); storeSub.style.marginTop = '2px';
530
  const storeMuted = el('span', 'muted', ''); storeMuted.style.fontSize = '.72rem';
531
- const clearBtn = el('button', 'prov-use', 'Vaciar caché');
532
  clearBtn.style.marginTop = '8px';
533
- clearBtn.onclick = async () => { clearBtn.textContent = 'Vaciando…'; await clearModelCache(); await paintStorage(); clearBtn.textContent = 'Vaciar caché'; };
534
  const storeHead = el('div', 'prov-head'); storeHead.append(storeInfo);
535
  storeCard.append(storeHead, storeMuted, clearBtn);
536
  box.appendChild(storeCard);
537
  async function paintStorage() {
538
  const { usage, quota, persisted } = await cacheEstimate();
539
  const gb = n => (n / 1073741824).toFixed(2) + ' GB';
540
- storeInfo.textContent = usage ? `${gb(usage)} en caché` : 'Nada cacheado todavía';
541
- storeMuted.textContent = (persisted ? '✓ almacenamiento persistente (no se borra solo)' : ' sin persistencia: el navegador podría desalojarlo')
542
- + (quota ? ` · límite ~${gb(quota)}` : '');
543
  }
544
  paintStorage();
545
 
@@ -562,7 +563,7 @@ function renderSettings() {
562
  `<details style="margin-top:6px"><summary class="muted" style="font-size:.7rem;cursor:pointer">otro sistema operativo</summary>` +
563
  Object.entries(OTHER).filter(([f]) => f !== primary).map(([f, label]) => `<div><a href="https://elffuss-code.utopiaia.com/bridge-dl/${f}" download style="color:var(--accent2);font-size:.72rem">${label}</a></div>`).join('') +
564
  `</details>` +
565
- `<div class="field" style="margin-top:8px"><label class="muted" style="font-size:.68rem">Token (lo imprime el programa al arrancarlo)</label><input id="br-token" placeholder="pega aquí el token…"></div>` +
566
  `<div class="field" style="margin-top:6px"><label class="muted" style="font-size:.68rem">Carpeta de trabajo (opcional — si no, usa una temporal)</label><input id="br-folder" placeholder="/ruta/completa/a/tu/proyecto"></div>` +
567
  `<button id="br-connect" class="prov-use" style="margin-top:8px">Conectar</button>`;
568
  box.appendChild(brCard);
@@ -636,7 +637,7 @@ function renderSettings() {
636
  `</label>` +
637
  `<p class="muted" style="font-size:.7rem;margin:6px 0 10px">Apagado por defecto — tu código y el contenido de tus proyectos NUNCA se incluyen, solo el mensaje de error técnico, la pila y datos del navegador.</p>` +
638
  `<label class="muted" style="font-size:.68rem">O manda algo tú directamente (un fallo que viste, algo que eches en falta…)</label>` +
639
- `<textarea id="tel-feedback" rows="2" style="width:100%;margin-top:4px;resize:vertical;background:var(--bg2);border:1px solid var(--line);border-radius:8px;color:var(--fg);padding:6px 8px;font:inherit" placeholder="Cuéntanos qué ha pasado o qué te gustaría que hiciera…"></textarea>` +
640
  `<button id="tel-send" class="prov-use" style="margin-top:6px">Enviar</button>` +
641
  `<span id="tel-msg" class="muted" style="font-size:.7rem;margin-left:8px"></span>`;
642
  box.appendChild(telCard);
@@ -669,10 +670,10 @@ function renderSettings() {
669
  use.onclick = () => { changeModel('ext:' + id); renderSettings(); };
670
  head.append(toggle, title, use);
671
  const fields = el('div', 'prov-fields' + (c.enabled ? '' : ' hide'));
672
- const model = document.createElement('input'); model.value = c.model || ''; model.placeholder = 'modelo (p. ej. gpt-4o-mini)';
673
  const key = document.createElement('input'); key.type = 'password'; key.value = c.apiKey || '';
674
- key.placeholder = c.kind === 'anthropic' ? 'sk-ant-…' : (id === 'ollama' || id === 'server' ? 'clave (no necesaria)' : 'sk-…');
675
- fields.append(labeled('Modelo', model), labeled('API key', key));
676
  const save = () => {
677
  settings.update(id, { enabled: toggle.checked, model: model.value.trim(), apiKey: key.value });
678
  rebuildSelect();
@@ -712,12 +713,13 @@ async function enterIDE(handle) {
712
  await conv.init({ onEvent: onConvEvent });
713
  renderTabsBar();
714
  renderActiveLog();
715
- addMsg('sys', `Привіт 👋 Proyecto «${name abierto. Pregúntame por el código, pídeme cambios o dime «árbol». Todo se queda en tu máquina.`);
716
  refreshGit();
717
  preloadModel(); // por si el arranque en la landing no llegó a dispararse
718
  }
719
 
720
  async function boot() {
 
721
  // Caché de modelos (persistente + service worker) ANTES de descargar nada,
722
  // para que hasta la primera descarga de pesos quede cacheada y no se repita.
723
  ensureModelCache().finally(() => preloadModel());
@@ -920,7 +922,7 @@ mind.init({ buildCity: buildCityAdapter, loadThoughts: loadThoughtsAdapter });
920
  // «Ejecutar esta propuesta»: la manda a la MISMA cola/agente del chat normal
921
  // (mismas herramientas reales, mismo gate de Auto-edit) — no un auto-apply
922
  // aparte sin supervisión.
923
- mind.setExecuteProposal(md => { mind.closeMind(); send('Implementa esta propuesta de mejora con cambios reales en el código:\n\n' + md); });
924
  // «perfiles»: cada uno es una línea de pensamiento paralela (foco + color de
925
  // su estrella) del cerebro CEO (compartido, core/ceo.js). El usuario los
926
  // edita/crea/borra desde ⚙ en la Mente.
@@ -979,7 +981,7 @@ export function isNoteworthy(ev) {
979
  }
980
  // manda una propuesta (el .md completo del ciclo) a la MISMA cola/agente real
981
  // del chat — usado tanto por el botón del chat como por el de la notificación.
982
- function executeProposal(md) { send('Implementa esta propuesta de mejora con cambios reales en el código:\n\n' + md); }
983
 
984
  export function reportImprovements(ev) {
985
  const props = ev.proposals || [];
@@ -1020,7 +1022,7 @@ async function notify(title, body, md) {
1020
  if (reg.showNotification) {
1021
  await reg.showNotification(title, {
1022
  body, icon: 'img/elffuss-code.svg',
1023
- actions: [{ action: 'execute', title: '▶ Ejecutar' }, { action: 'open', title: 'Ver en la Mente' }],
1024
  data: { md },
1025
  });
1026
  return;
@@ -1044,7 +1046,7 @@ document.addEventListener('keydown', noteAct, true);
1044
  const elfAvatar = document.querySelector('#activity img');
1045
  if (elfAvatar) {
1046
  elfAvatar.style.cursor = 'pointer';
1047
- elfAvatar.title = 'Mente de Elffuss — cerebro autónomo';
1048
  elfAvatar.addEventListener('click', () => {
1049
  // auto-activa SOLO la primera vez (nunca decidiste play/stop); si lo
1050
  // pausaste a propósito, abrir la Mente NO debe reactivarlo por sorpresa.
@@ -1178,12 +1180,12 @@ document.addEventListener('click', e => {
1178
 
1179
  // [/] comandos
1180
  $('btn-slash').addEventListener('click', () => openMenu([
1181
- { sep: 'Comandos' },
1182
- { label: '/tree', hint: 'árbol del proyecto', run: () => send('árbol') },
1183
- { label: '/search', hint: 'buscar en el código', run: () => { $('prompt').value = 'busca '; $('prompt').focus(); } },
1184
- { label: '/skills', hint: 'gestionar skills', run: () => openSkillsPanel() },
1185
- { label: '/model', hint: 'proveedores y modelo', run: () => $('btn-settings').click() },
1186
- { label: '/clear', hint: 'nueva conversación', run: () => conv.create() },
1187
  ]));
1188
 
1189
  // [+] adjuntar archivo del proyecto como @ruta
@@ -1192,11 +1194,11 @@ $('btn-plus').addEventListener('click', async () => {
1192
  try { tree = await codeTools.tree({ depth: 3 }); } catch { /* sin proyecto */ }
1193
  const files = tree.split('\n').filter(l => l.trim() && !l.includes('📁')).map(l => l.trim()).slice(0, 40);
1194
  const { currentFile } = codeTools.current();
1195
- const items = [{ sep: 'Adjuntar al mensaje (@)' }];
1196
- if (currentFile) items.push({ label: '@' + currentFile, hint: 'archivo abierto', run: () => attach(currentFile) });
1197
  for (const f of files.filter(f => f !== currentFile).slice(0, 20))
1198
  items.push({ label: '@' + f, run: () => attach(f) });
1199
- openMenu(items.length > 1 ? items : [{ sep: 'Abre un proyecto primero' }]);
1200
  });
1201
  function attach(path) {
1202
  const p = $('prompt');
@@ -1209,8 +1211,8 @@ let autoEdit = localStorage.getItem('elffusscode.autoedit') !== '0';
1209
  function paintAuto() {
1210
  const b = $('btn-autoedit');
1211
  b.classList.toggle('on', autoEdit);
1212
- b.querySelector('.ae-txt').textContent = autoEdit ? 'Auto' : 'Revisar';
1213
- b.title = autoEdit ? 'Editar archivos automáticamente (clic para revisar antes)' : 'Pedir confirmación antes de escribir (clic para automático)';
1214
  }
1215
  $('btn-autoedit').addEventListener('click', () => {
1216
  autoEdit = !autoEdit;
@@ -1355,14 +1357,14 @@ async function browseRepo(repo, box) {
1355
  // ---------- command palette (Cmd/Ctrl+P) ----------
1356
  let palItems = [], palSel = 0;
1357
  const COMMANDS = [
1358
- { label: 'Nueva conversación', run: () => conv.create() },
1359
- { label: 'Historial de conversaciones', run: () => $('btn-history').click() },
1360
- { label: 'Buscar en el código…', run: () => { closePalette(); $('prompt').value = 'busca '; $('prompt').focus(); } },
1361
- { label: 'Gestionar skills', run: () => { closePalette(); openSkillsPanel(); } },
1362
- { label: 'Ajustes y modelo', run: () => { closePalette(); renderSettings(); } },
1363
- { label: 'Alternar explorador', run: () => $('act-files').click() },
1364
- { label: 'Alternar terminal', hint: 'Ctrl+`', run: () => { closePalette(); toggleTerminal(); } },
1365
- { label: 'Guardar (Ctrl+S)', run: () => triggerEditor('') || (hasEditor() && triggerEditor('workbench.action.files.save')) },
1366
  ];
1367
 
1368
  function openPalette(prefix = '') {
@@ -1392,9 +1394,9 @@ async function renderPalette() {
1392
 
1393
  if (raw.startsWith('>')) { // comandos
1394
  const q = raw.slice(1).trim().toLowerCase();
1395
- palItems = COMMANDS.filter(c => c.label.toLowerCase().includes(q)).map(c => ({ label: c.label, hint: 'comando', run: c.run }));
1396
  } else if (raw.startsWith('@')) { // símbolos del archivo abierto (Monaco)
1397
- palItems = [{ label: 'Ir a símbolo en el editor…', hint: '@', run: () => { closePalette(); triggerEditor('editor.action.quickOutline'); } }];
1398
  } else { // archivos (con :línea opcional)
1399
  const [pathQ, lineQ] = raw.split(':');
1400
  const files = await codeTools.fileList().catch(() => []);
@@ -1414,7 +1416,7 @@ async function renderPalette() {
1414
  row.onclick = () => it.run();
1415
  list.appendChild(row);
1416
  });
1417
- if (!palItems.length) list.appendChild(el('div', 'pal-empty', 'Sin resultados'));
1418
  }
1419
  function paintPalSel() {
1420
  [...$('pal-list').children].forEach((c, i) => c.classList.toggle('sel', i === palSel));
@@ -1436,30 +1438,30 @@ document.addEventListener('click', e => { if (!e.target.closest('#palette')) clo
1436
  // ---------- barra de menú File/Edit/View/Git ----------
1437
  async function menuFor(which) {
1438
  if (which === 'file') return [
1439
- { label: 'Nuevo archivo…', run: () => { const p = prompt('Ruta del nuevo archivo:'); if (p) codeTools.write({ path: p, content: '' }).then(() => { openFile(p); refreshTree(); }); } },
1440
- { label: 'Guardar', hint: 'Ctrl+S', run: () => triggerEditor('workbench.action.files.save') },
1441
- { label: 'Abrir carpeta…', run: async () => { try { await enterIDE(await window.showDirectoryPicker({ mode: 'readwrite' })); } catch {} } },
1442
  ];
1443
  if (which === 'edit') return [
1444
- { label: 'Buscar en el código', run: () => { $('prompt').value = 'busca '; $('prompt').focus(); } },
1445
- { label: 'Ir a archivo…', hint: 'Cmd+P', run: () => openPalette('') },
1446
- { label: 'Ir a símbolo…', run: () => triggerEditor('editor.action.quickOutline') },
1447
  ];
1448
  if (which === 'view') return [
1449
- { label: 'Explorador', run: () => $('act-files').click() },
1450
- { label: 'Terminal', hint: 'Ctrl+`', run: () => toggleTerminal(true) },
1451
- { label: 'Arquitectura', run: () => openView('arch') },
1452
- { label: 'Ciudad 3D', run: () => openView('city') },
1453
  { label: 'Command Palette', hint: 'Cmd+Shift+P', run: () => openPalette('>') },
1454
  { label: 'Skills', run: () => openSkillsPanel() },
1455
- { label: 'Acerca de Elffuss Code (GitHub)', hint: '↗', run: () => window.open('https://github.com/KikoCis/elffuss-code', '_blank', 'noopener') },
1456
  ];
1457
  if (which === 'git') {
1458
  const g = await codeTools.gitInfo();
1459
- if (!g.isRepo) return [{ label: 'No es un repositorio git', hint: '', run: () => {} }];
1460
- const items = [{ label: 'Rama: ' + g.branch, hint: 'git', run: () => {} }];
1461
- if (g.lastCommit) items.push({ label: 'Último: ' + g.lastCommit.msg.slice(0, 40), hint: g.lastCommit.author, run: () => {} });
1462
- items.push({ label: 'Pídele a Elffuss que commitee', run: () => { $('prompt').value = 'resume los cambios y prepárame el mensaje de commit'; $('prompt').focus(); } });
1463
  return items;
1464
  }
1465
  return [];
 
16
  import * as mind from './mind.js';
17
  import { buildCityAdapter, loadThoughtsAdapter } from './mind-adapter.js';
18
  import { humanizeStreamPreview } from './humanize.js';
19
+ import { t, applyI18n } from './i18n.js';
20
  import * as bridge from './bridge.js';
21
  import * as conv from './conversations.js';
22
  import { TASK_PREFIX } from './goal.js';
 
127
  export function thinkingBubble() {
128
  const div = addMsg('thinking', '');
129
  const label = document.createElement('span');
130
+ label.textContent = t('thinking');
131
  const gen = document.createElement('div');
132
  gen.className = 'gen';
133
  div.append(label, gen);
134
  let buf = '';
135
  return {
136
+ tick(tok) {
137
+ buf += tok;
138
+ label.textContent = t('writing', { n: buf.length }); // caracteres, no tokens
139
  // si el buffer entró en un bloque de tool-call, no enseñes el JSON crudo
140
  // según va llegando: una frase humana («leyendo app.js…») en su lugar.
141
  const preview = humanizeStreamPreview(buf);
 
143
  gen.textContent = preview ? '⟐ ' + preview : (buf.length > 200 ? '…' : '') + buf.slice(-200);
144
  $('chat-log').scrollTop = $('chat-log').scrollHeight;
145
  },
146
+ tool(name) { buf = ''; gen.textContent = ''; label.textContent = t('using', { name }); },
147
  remove() { div.remove(); },
148
  };
149
  }
 
233
  name.title = title;
234
  const x = document.createElement('b');
235
  x.textContent = '×';
236
+ x.title = t('tabClose');
237
  x.onclick = e => { e.stopPropagation(); conv.closeTab(c.id); };
238
  el.append(name, x);
239
  el.onclick = () => conv.switchTo(c.id);
 
241
  }
242
  const plus = document.createElement('button');
243
  plus.className = 'tab-add';
244
+ plus.title = t('newConv');
245
  plus.textContent = '+';
246
  plus.onclick = () => conv.create();
247
  bar.appendChild(plus);
 
252
  panel.replaceChildren();
253
  const head = document.createElement('div');
254
  head.className = 'hp-head';
255
+ head.append(document.createTextNode(t('tHistory')));
256
  const xBtn = document.createElement('button');
257
  xBtn.textContent = '✕';
258
  xBtn.onclick = () => { panel.hidden = true; };
 
265
  if (!all.length) {
266
  const empty = document.createElement('div');
267
  empty.className = 'hp-empty';
268
+ empty.textContent = t('histEmpty');
269
  list.appendChild(empty);
270
  return;
271
  }
272
  for (const c of all) {
273
  const row = document.createElement('div');
274
  row.className = 'hp-row';
275
+ const titEl = document.createElement('div');
276
+ titEl.className = 'hp-title';
277
+ titEl.textContent = c.title || t('newConv');
278
  const d = new Date(c.updatedAt);
279
  const date = document.createElement('div');
280
  date.className = 'hp-date';
281
  date.textContent = d.toLocaleDateString() + ' ' + d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
282
  const del = document.createElement('button');
283
  del.className = 'hp-del';
284
+ del.title = t('histDel');
285
  del.textContent = '🗑';
286
  del.onclick = async e => {
287
  e.stopPropagation();
288
+ if (!confirm(t('confirmDel', { title: c.title || t('thisConv') }))) return;
289
  await conv.remove(c.id);
290
  renderHistoryPanel();
291
  };
292
+ row.append(titEl, date, del);
293
  row.onclick = async () => { await conv.open(c.id); panel.hidden = true; };
294
  list.appendChild(row);
295
  }
 
389
  if (typeof p === 'string') return showModelProgress(p);
390
  if (p?.status === 'progress' && p.total) {
391
  const pct = Math.round(p.loaded / p.total * 100);
392
+ showModelProgress(t('modelLoading', { pct, loaded: (p.loaded / 1e6 | 0), total: (p.total / 1e6 | 0) }), pct);
393
  }
394
  });
395
  conv.setProvider(mod);
396
  activeModel = id;
397
  localStorage.setItem('elffusscode.model', id);
398
  $('model-dot').className = 'dot on';
399
+ showModelProgress(t('modelReadyBadge'), 100);
400
  setTimeout(() => showModelProgress(null), 2500);
401
+ $('statusbar').textContent = t('modelReady');
402
  rebuildSelect();
403
  return true;
404
  } catch (e) {
 
407
  if (id.startsWith('litert') && !_fellBack) {
408
  _fellBack = true;
409
  sessionStorage.setItem('elffusscode.skipGemma', '1');
410
+ showModelProgress(t('fallbackToast'));
411
  loadingId = null;
412
  const okc = await changeModel('onnx');
413
  _fellBack = false;
 
524
  box.appendChild(grid);
525
 
526
  // --- Almacenamiento del modelo (caché persistente) ---
527
+ box.append(el('div', 'sk-h', t('setStoreTitle')));
528
  const storeCard = el('div', 'prov-card');
529
+ const storeInfo = el('span', null, t('setCalc')); storeInfo.style.cssText = 'font-size:.8rem;color:var(--fg)';
530
  const storeSub = el('div', 'field'); storeSub.style.marginTop = '2px';
531
  const storeMuted = el('span', 'muted', ''); storeMuted.style.fontSize = '.72rem';
532
+ const clearBtn = el('button', 'prov-use', t('setClear'));
533
  clearBtn.style.marginTop = '8px';
534
+ clearBtn.onclick = async () => { clearBtn.textContent = t('setClearing'); await clearModelCache(); await paintStorage(); clearBtn.textContent = t('setClear'); };
535
  const storeHead = el('div', 'prov-head'); storeHead.append(storeInfo);
536
  storeCard.append(storeHead, storeMuted, clearBtn);
537
  box.appendChild(storeCard);
538
  async function paintStorage() {
539
  const { usage, quota, persisted } = await cacheEstimate();
540
  const gb = n => (n / 1073741824).toFixed(2) + ' GB';
541
+ storeInfo.textContent = usage ? t('setCached', { gb: gb(usage) }) : t('setNone');
542
+ storeMuted.textContent = (persisted ? t('setPersist') : t('setNoPersist'))
543
+ + (quota ? t('setLimit', { gb: gb(quota) }) : '');
544
  }
545
  paintStorage();
546
 
 
563
  `<details style="margin-top:6px"><summary class="muted" style="font-size:.7rem;cursor:pointer">otro sistema operativo</summary>` +
564
  Object.entries(OTHER).filter(([f]) => f !== primary).map(([f, label]) => `<div><a href="https://elffuss-code.utopiaia.com/bridge-dl/${f}" download style="color:var(--accent2);font-size:.72rem">${label}</a></div>`).join('') +
565
  `</details>` +
566
+ `<div class="field" style="margin-top:8px"><label class="muted" style="font-size:.68rem">${t('setBrToken')}</label><input id="br-token" placeholder="${t('brTokenPh')}"></div>` +
567
  `<div class="field" style="margin-top:6px"><label class="muted" style="font-size:.68rem">Carpeta de trabajo (opcional — si no, usa una temporal)</label><input id="br-folder" placeholder="/ruta/completa/a/tu/proyecto"></div>` +
568
  `<button id="br-connect" class="prov-use" style="margin-top:8px">Conectar</button>`;
569
  box.appendChild(brCard);
 
637
  `</label>` +
638
  `<p class="muted" style="font-size:.7rem;margin:6px 0 10px">Apagado por defecto — tu código y el contenido de tus proyectos NUNCA se incluyen, solo el mensaje de error técnico, la pila y datos del navegador.</p>` +
639
  `<label class="muted" style="font-size:.68rem">O manda algo tú directamente (un fallo que viste, algo que eches en falta…)</label>` +
640
+ `<textarea id="tel-feedback" rows="2" style="width:100%;margin-top:4px;resize:vertical;background:var(--bg2);border:1px solid var(--line);border-radius:8px;color:var(--fg);padding:6px 8px;font:inherit" placeholder="${t('setFeedbackPh')}"></textarea>` +
641
  `<button id="tel-send" class="prov-use" style="margin-top:6px">Enviar</button>` +
642
  `<span id="tel-msg" class="muted" style="font-size:.7rem;margin-left:8px"></span>`;
643
  box.appendChild(telCard);
 
670
  use.onclick = () => { changeModel('ext:' + id); renderSettings(); };
671
  head.append(toggle, title, use);
672
  const fields = el('div', 'prov-fields' + (c.enabled ? '' : ' hide'));
673
+ const model = document.createElement('input'); model.value = c.model || ''; model.placeholder = t('setModelPh');
674
  const key = document.createElement('input'); key.type = 'password'; key.value = c.apiKey || '';
675
+ key.placeholder = c.kind === 'anthropic' ? 'sk-ant-…' : (id === 'ollama' || id === 'server' ? t('keyNeeded') : 'sk-…');
676
+ fields.append(labeled(t('lblModel'), model), labeled(t('lblApiKey'), key));
677
  const save = () => {
678
  settings.update(id, { enabled: toggle.checked, model: model.value.trim(), apiKey: key.value });
679
  rebuildSelect();
 
713
  await conv.init({ onEvent: onConvEvent });
714
  renderTabsBar();
715
  renderActiveLog();
716
+ addMsg('sys', t('welcome', { name }));
717
  refreshGit();
718
  preloadModel(); // por si el arranque en la landing no llegó a dispararse
719
  }
720
 
721
  async function boot() {
722
+ applyI18n(); // localiza el chrome estático (placeholder) antes de pintar nada
723
  // Caché de modelos (persistente + service worker) ANTES de descargar nada,
724
  // para que hasta la primera descarga de pesos quede cacheada y no se repita.
725
  ensureModelCache().finally(() => preloadModel());
 
922
  // «Ejecutar esta propuesta»: la manda a la MISMA cola/agente del chat normal
923
  // (mismas herramientas reales, mismo gate de Auto-edit) — no un auto-apply
924
  // aparte sin supervisión.
925
+ mind.setExecuteProposal(md => { mind.closeMind(); send(t('pProposal') + md); });
926
  // «perfiles»: cada uno es una línea de pensamiento paralela (foco + color de
927
  // su estrella) del cerebro CEO (compartido, core/ceo.js). El usuario los
928
  // edita/crea/borra desde ⚙ en la Mente.
 
981
  }
982
  // manda una propuesta (el .md completo del ciclo) a la MISMA cola/agente real
983
  // del chat — usado tanto por el botón del chat como por el de la notificación.
984
+ function executeProposal(md) { send(t('pProposal') + md); }
985
 
986
  export function reportImprovements(ev) {
987
  const props = ev.proposals || [];
 
1022
  if (reg.showNotification) {
1023
  await reg.showNotification(title, {
1024
  body, icon: 'img/elffuss-code.svg',
1025
+ actions: [{ action: 'execute', title: t('nExec') }, { action: 'open', title: t('nView') }],
1026
  data: { md },
1027
  });
1028
  return;
 
1046
  const elfAvatar = document.querySelector('#activity img');
1047
  if (elfAvatar) {
1048
  elfAvatar.style.cursor = 'pointer';
1049
+ elfAvatar.title = t('avatarTitle');
1050
  elfAvatar.addEventListener('click', () => {
1051
  // auto-activa SOLO la primera vez (nunca decidiste play/stop); si lo
1052
  // pausaste a propósito, abrir la Mente NO debe reactivarlo por sorpresa.
 
1180
 
1181
  // [/] comandos
1182
  $('btn-slash').addEventListener('click', () => openMenu([
1183
+ { sep: t('cmdsSep') },
1184
+ { label: '/tree', hint: t('slTree'), run: () => send(t('pTree')) },
1185
+ { label: '/search', hint: t('slSearch'), run: () => { $('prompt').value = t('pSearch'); $('prompt').focus(); } },
1186
+ { label: '/skills', hint: t('slSkills'), run: () => openSkillsPanel() },
1187
+ { label: '/model', hint: t('slModel'), run: () => $('btn-settings').click() },
1188
+ { label: '/clear', hint: t('slClear'), run: () => conv.create() },
1189
  ]));
1190
 
1191
  // [+] adjuntar archivo del proyecto como @ruta
 
1194
  try { tree = await codeTools.tree({ depth: 3 }); } catch { /* sin proyecto */ }
1195
  const files = tree.split('\n').filter(l => l.trim() && !l.includes('📁')).map(l => l.trim()).slice(0, 40);
1196
  const { currentFile } = codeTools.current();
1197
+ const items = [{ sep: t('attachSep') }];
1198
+ if (currentFile) items.push({ label: '@' + currentFile, hint: t('attachOpen'), run: () => attach(currentFile) });
1199
  for (const f of files.filter(f => f !== currentFile).slice(0, 20))
1200
  items.push({ label: '@' + f, run: () => attach(f) });
1201
+ openMenu(items.length > 1 ? items : [{ sep: t('openProjFirst') }]);
1202
  });
1203
  function attach(path) {
1204
  const p = $('prompt');
 
1211
  function paintAuto() {
1212
  const b = $('btn-autoedit');
1213
  b.classList.toggle('on', autoEdit);
1214
+ b.querySelector('.ae-txt').textContent = autoEdit ? t('autoTxt') : t('reviewTxt');
1215
+ b.title = autoEdit ? t('autoOn') : t('autoOff');
1216
  }
1217
  $('btn-autoedit').addEventListener('click', () => {
1218
  autoEdit = !autoEdit;
 
1357
  // ---------- command palette (Cmd/Ctrl+P) ----------
1358
  let palItems = [], palSel = 0;
1359
  const COMMANDS = [
1360
+ { label: t('newConv'), run: () => conv.create() },
1361
+ { label: t('tHistory'), run: () => $('btn-history').click() },
1362
+ { label: t('pSearchCode'), run: () => { closePalette(); $('prompt').value = t('pSearch'); $('prompt').focus(); } },
1363
+ { label: t('pManageSkills'), run: () => { closePalette(); openSkillsPanel(); } },
1364
+ { label: t('tActSettings'), run: () => { closePalette(); renderSettings(); } },
1365
+ { label: t('pToggleExplorer'), run: () => $('act-files').click() },
1366
+ { label: t('pToggleTerm'), hint: 'Ctrl+`', run: () => { closePalette(); toggleTerminal(); } },
1367
+ { label: t('pSave'), run: () => triggerEditor('') || (hasEditor() && triggerEditor('workbench.action.files.save')) },
1368
  ];
1369
 
1370
  function openPalette(prefix = '') {
 
1394
 
1395
  if (raw.startsWith('>')) { // comandos
1396
  const q = raw.slice(1).trim().toLowerCase();
1397
+ palItems = COMMANDS.filter(c => c.label.toLowerCase().includes(q)).map(c => ({ label: c.label, hint: t('palCmd'), run: c.run }));
1398
  } else if (raw.startsWith('@')) { // símbolos del archivo abierto (Monaco)
1399
+ palItems = [{ label: t('palGotoSym'), hint: '@', run: () => { closePalette(); triggerEditor('editor.action.quickOutline'); } }];
1400
  } else { // archivos (con :línea opcional)
1401
  const [pathQ, lineQ] = raw.split(':');
1402
  const files = await codeTools.fileList().catch(() => []);
 
1416
  row.onclick = () => it.run();
1417
  list.appendChild(row);
1418
  });
1419
+ if (!palItems.length) list.appendChild(el('div', 'pal-empty', t('palEmpty')));
1420
  }
1421
  function paintPalSel() {
1422
  [...$('pal-list').children].forEach((c, i) => c.classList.toggle('sel', i === palSel));
 
1438
  // ---------- barra de menú File/Edit/View/Git ----------
1439
  async function menuFor(which) {
1440
  if (which === 'file') return [
1441
+ { label: t('mNewFile'), run: () => { const p = prompt(t('promptNewPath')); if (p) codeTools.write({ path: p, content: '' }).then(() => { openFile(p); refreshTree(); }); } },
1442
+ { label: t('mSave'), hint: 'Ctrl+S', run: () => triggerEditor('workbench.action.files.save') },
1443
+ { label: t('mOpenFolder'), run: async () => { try { await enterIDE(await window.showDirectoryPicker({ mode: 'readwrite' })); } catch {} } },
1444
  ];
1445
  if (which === 'edit') return [
1446
+ { label: t('mSearchCode'), run: () => { $('prompt').value = t('pSearch'); $('prompt').focus(); } },
1447
+ { label: t('mGotoFile'), hint: 'Cmd+P', run: () => openPalette('') },
1448
+ { label: t('mGotoSym'), run: () => triggerEditor('editor.action.quickOutline') },
1449
  ];
1450
  if (which === 'view') return [
1451
+ { label: t('tFiles'), run: () => $('act-files').click() },
1452
+ { label: t('mTerminal'), hint: 'Ctrl+`', run: () => toggleTerminal(true) },
1453
+ { label: t('mArch'), run: () => openView('arch') },
1454
+ { label: t('tCity'), run: () => openView('city') },
1455
  { label: 'Command Palette', hint: 'Cmd+Shift+P', run: () => openPalette('>') },
1456
  { label: 'Skills', run: () => openSkillsPanel() },
1457
+ { label: t('mAbout'), hint: '↗', run: () => window.open('https://github.com/KikoCis/elffuss-code', '_blank', 'noopener') },
1458
  ];
1459
  if (which === 'git') {
1460
  const g = await codeTools.gitInfo();
1461
+ if (!g.isRepo) return [{ label: t('gNotRepo'), hint: '', run: () => {} }];
1462
+ const items = [{ label: t('gBranch', { b: g.branch }), hint: 'git', run: () => {} }];
1463
+ if (g.lastCommit) items.push({ label: t('gLast', { m: g.lastCommit.msg.slice(0, 40) }), hint: g.lastCommit.author, run: () => {} });
1464
+ items.push({ label: t('gCommit'), run: () => { $('prompt').value = t('pCommit'); $('prompt').focus(); } });
1465
  return items;
1466
  }
1467
  return [];
js/mind.js CHANGED
@@ -17,6 +17,7 @@ import { OutputPass } from 'three/addons/postprocessing/OutputPass.js';
17
  import * as ceo from './ceo.js';
18
  import * as db from './db.js';
19
  import { renderMarkdown } from './md.js';
 
20
 
21
  const SOUNDCLOUD = 'https://soundcloud.com/dekel-official/dekel-baoba-festival-2023';
22
  const CEO_COLOR = '#ff4d8d';
@@ -450,7 +451,7 @@ export async function openMind() {
450
  overlay.id = 'mind-overlay';
451
  overlay.innerHTML =
452
  '<canvas id="mind-canvas"></canvas>' +
453
- '<div id="mind-title">MENTE DE ELFFUSS</div>' +
454
  '<div id="mind-legend"></div>' +
455
  '<button id="mind-playstop" title="pausar/reanudar el cerebro"></button>' +
456
  '<button id="mind-config" title="reprogramar el cerebro">⚙ cerebro</button>' +
 
17
  import * as ceo from './ceo.js';
18
  import * as db from './db.js';
19
  import { renderMarkdown } from './md.js';
20
+ import { t } from './i18n.js';
21
 
22
  const SOUNDCLOUD = 'https://soundcloud.com/dekel-official/dekel-baoba-festival-2023';
23
  const CEO_COLOR = '#ff4d8d';
 
451
  overlay.id = 'mind-overlay';
452
  overlay.innerHTML =
453
  '<canvas id="mind-canvas"></canvas>' +
454
+ `<div id="mind-title">${t('mindTitle')}</div>` +
455
  '<div id="mind-legend"></div>' +
456
  '<button id="mind-playstop" title="pausar/reanudar el cerebro"></button>' +
457
  '<button id="mind-config" title="reprogramar el cerebro">⚙ cerebro</button>' +