Spaces:
Running
Running
Sync with production: full i18n (EN/ES), English metadata, 15-min challenge banner
Browse files- README.md +17 -3
- index.html +1 -0
- js/challenge.js +74 -0
- js/humanize.js +52 -19
- js/i18n.js +144 -0
- js/main.js +75 -73
- 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 +
|
| 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://
|
| 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
|
| 11 |
-
case 'code.write': case 'fs.write': return
|
| 12 |
-
case 'code.tree': return
|
| 13 |
-
case 'fs.list': return
|
| 14 |
-
case 'code.search': return
|
| 15 |
-
case 'fs.pick_folder': return
|
| 16 |
-
case 'fs.copy': return
|
| 17 |
-
case 'fs.watch': return
|
| 18 |
-
case 'terminal.run': return
|
| 19 |
-
case 'app.create': return
|
| 20 |
-
case 'app.open': return
|
| 21 |
-
case 'skill.create': return
|
| 22 |
-
case 'memory.save': return
|
| 23 |
-
case 'tasks.add': return
|
| 24 |
-
case 'web.search': return
|
| 25 |
-
case 'web.images': return
|
| 26 |
-
case 'web.fetch': return
|
| 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
|
| 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) ?
|
| 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 = '
|
| 130 |
const gen = document.createElement('div');
|
| 131 |
gen.className = 'gen';
|
| 132 |
div.append(label, gen);
|
| 133 |
let buf = '';
|
| 134 |
return {
|
| 135 |
-
tick(
|
| 136 |
-
buf +=
|
| 137 |
-
label.textContent =
|
| 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 =
|
| 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 =
|
| 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 = '
|
| 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('
|
| 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 = '
|
| 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
|
| 275 |
-
|
| 276 |
-
|
| 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 = '
|
| 284 |
del.textContent = '🗑';
|
| 285 |
del.onclick = async e => {
|
| 286 |
e.stopPropagation();
|
| 287 |
-
if (!confirm(
|
| 288 |
await conv.remove(c.id);
|
| 289 |
renderHistoryPanel();
|
| 290 |
};
|
| 291 |
-
row.append(
|
| 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(
|
| 392 |
}
|
| 393 |
});
|
| 394 |
conv.setProvider(mod);
|
| 395 |
activeModel = id;
|
| 396 |
localStorage.setItem('elffusscode.model', id);
|
| 397 |
$('model-dot').className = 'dot on';
|
| 398 |
-
showModelProgress('
|
| 399 |
setTimeout(() => showModelProgress(null), 2500);
|
| 400 |
-
$('statusbar').textContent = '
|
| 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(
|
| 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',
|
| 527 |
const storeCard = el('div', 'prov-card');
|
| 528 |
-
const storeInfo = el('span', null, '
|
| 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', '
|
| 532 |
clearBtn.style.marginTop = '8px';
|
| 533 |
-
clearBtn.onclick = async () => { clearBtn.textContent = '
|
| 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 ?
|
| 541 |
-
storeMuted.textContent = (persisted ?
|
| 542 |
-
+ (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">
|
| 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="
|
| 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 =
|
| 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' ?
|
| 675 |
-
fields.append(labeled('
|
| 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',
|
| 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('
|
| 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('
|
| 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: '
|
| 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 = '
|
| 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: '
|
| 1182 |
-
{ label: '/tree', hint: '
|
| 1183 |
-
{ label: '/search', hint: '
|
| 1184 |
-
{ label: '/skills', hint: '
|
| 1185 |
-
{ label: '/model', hint: '
|
| 1186 |
-
{ label: '/clear', hint: '
|
| 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:
|
| 1196 |
-
if (currentFile) items.push({ label: '@' + currentFile, hint: '
|
| 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: '
|
| 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 ? '
|
| 1213 |
-
b.title = autoEdit ?
|
| 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: '
|
| 1359 |
-
{ label: '
|
| 1360 |
-
{ label: '
|
| 1361 |
-
{ label: '
|
| 1362 |
-
{ label: '
|
| 1363 |
-
{ label: '
|
| 1364 |
-
{ label: '
|
| 1365 |
-
{ label:
|
| 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: '
|
| 1396 |
} else if (raw.startsWith('@')) { // símbolos del archivo abierto (Monaco)
|
| 1397 |
-
palItems = [{ label: '
|
| 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', '
|
| 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: '
|
| 1440 |
-
{ label: '
|
| 1441 |
-
{ label: '
|
| 1442 |
];
|
| 1443 |
if (which === 'edit') return [
|
| 1444 |
-
{ label: '
|
| 1445 |
-
{ label: '
|
| 1446 |
-
{ label: '
|
| 1447 |
];
|
| 1448 |
if (which === 'view') return [
|
| 1449 |
-
{ label: '
|
| 1450 |
-
{ label: '
|
| 1451 |
-
{ label: '
|
| 1452 |
-
{ label: '
|
| 1453 |
{ label: 'Command Palette', hint: 'Cmd+Shift+P', run: () => openPalette('>') },
|
| 1454 |
{ label: 'Skills', run: () => openSkillsPanel() },
|
| 1455 |
-
{ label:
|
| 1456 |
];
|
| 1457 |
if (which === 'git') {
|
| 1458 |
const g = await codeTools.gitInfo();
|
| 1459 |
-
if (!g.isRepo) return [{ label: '
|
| 1460 |
-
const items = [{ label: '
|
| 1461 |
-
if (g.lastCommit) items.push({ label: '
|
| 1462 |
-
items.push({ label: '
|
| 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 |
-
|
| 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>' +
|