agents-city 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/README.es.md +54 -5
- package/README.md +53 -5
- package/bin/agents-city.js +4 -0
- package/bin/hall.html +92 -5
- package/bin/navegador.mjs +141 -12
- package/bin/serve.py +94 -38
- package/bin/test +6 -3
- package/bin/test-demo.py +67 -0
- package/bin/test-desinstala.py +170 -0
- package/bin/test-navegador.py +2 -0
- package/bin/test-serve.py +122 -2
- package/bin/uninstall +5 -0
- package/city/web/dist/city.js +65 -65
- package/city/web/dist/index.html +18 -5
- package/city/web/dist-hall/hall.js +941 -385
- package/city/web/index.html +17 -4
- package/city/web/src/bienvenida.ts +35 -329
- package/city/web/src/casa.ts +282 -0
- package/city/web/src/demo.ts +247 -0
- package/city/web/src/dialogo.ts +178 -0
- package/city/web/src/es.ts +96 -0
- package/city/web/src/explorador.ts +189 -0
- package/city/web/src/hall.ts +157 -54
- package/city/web/src/main.ts +28 -5
- package/demo/graba.py +132 -0
- package/demo/grabaciones/legal.jsonl +22 -0
- package/demo/grabaciones/medicina.jsonl +22 -0
- package/demo/grabaciones/software.jsonl +22 -0
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/scripts/demos.py +107 -0
- package/plugin/scripts/desinstala.py +222 -0
|
@@ -180,6 +180,194 @@ anota({
|
|
|
180
180
|
"Its house grows with the decisions it records.": "Su casa crece con las decisiones que deja escritas.",
|
|
181
181
|
"urgencias, api, the handbook \u2014 whatever you would say out loud": "urgencias, api, el manual \u2014 como lo llamar\xEDas en voz alta"
|
|
182
182
|
});
|
|
183
|
+
anota({
|
|
184
|
+
// ── the app's own dialogs ─────────────────────────────────────────────────
|
|
185
|
+
Yes: "S\xED",
|
|
186
|
+
"What is it called?": "\xBFC\xF3mo se llama?",
|
|
187
|
+
"home, clients, the lab \u2014 whatever you would say out loud": "casa, clientes, el laboratorio \u2014 como lo dir\xEDas en voz alta",
|
|
188
|
+
"A city is a place with its own seat, its own houses and its own map. Yours stay where they are.": "Una ciudad es un sitio con su propio asiento, sus casas y su mapa. Las que ya tienes se quedan donde est\xE1n.",
|
|
189
|
+
"Start over {city}?": "\xBFEmpezar {city} de cero?",
|
|
190
|
+
"Start over": "Empezar de cero",
|
|
191
|
+
"You lose:": "Pierdes:",
|
|
192
|
+
"You keep:": "Conservas:",
|
|
193
|
+
"Right now that is {agents} house(s), {roads} road(s) and {acts} committee act(s).": "Ahora mismo eso son {agents} casa(s), {roads} carretera(s) y {acts} acta(s) de comit\xE9.",
|
|
194
|
+
"Type the city\u2019s name to confirm": "Escribe el nombre de la ciudad para confirmar",
|
|
195
|
+
"Archive {city}?": "\xBFArchivar {city}?",
|
|
196
|
+
"Archive it": "Archivarla",
|
|
197
|
+
"It moves into .backups. Nothing is deleted, and you can put it back by hand.": "Se mueve a .backups. No se borra nada, y puedes devolverla a mano.",
|
|
198
|
+
"Which engine?": "\xBFQu\xE9 motor?",
|
|
199
|
+
"Model alias": "Alias del modelo",
|
|
200
|
+
"An alias the CLI resolves when the window opens. Anything it accepts works here.": "Un alias que la CLI resuelve al abrir la ventana. Vale cualquiera que ella acepte.",
|
|
201
|
+
"Use it": "Usarlo",
|
|
202
|
+
"Remove the skill {skill}?": "\xBFQuitar la skill {skill}?",
|
|
203
|
+
"Remove it": "Quitarla",
|
|
204
|
+
"It is deleted from this agent\u2019s own home. Nothing outside that folder is touched.": "Se borra de la casa de este agente. Nada fuera de esa carpeta se toca.",
|
|
205
|
+
"Build a house": "Construir una casa",
|
|
206
|
+
"One worker, its own window, its own corner of your disk. Everything here can be changed afterwards.": "Un trabajador, su ventana y su rinc\xF3n de tu disco. Todo esto se puede cambiar despu\xE9s.",
|
|
207
|
+
"{name} has a house now": "{name} ya tiene casa",
|
|
208
|
+
"{name} joined the city": "{name} se ha mudado a la ciudad",
|
|
209
|
+
"What else does it work on?": "\xBFSobre qu\xE9 m\xE1s trabaja?",
|
|
210
|
+
"A repository, a worktree, or a folder of documents. It is linked, never copied.": "Un repositorio, un worktree o una carpeta de documentos. Se enlaza, nunca se copia.",
|
|
211
|
+
"Mount it": "Montarlo",
|
|
212
|
+
Mounted: "Montado",
|
|
213
|
+
"Stop this agent working on {what}?": "\xBFQue este agente deje de trabajar sobre {what}?",
|
|
214
|
+
"Unmount it": "Desmontarlo",
|
|
215
|
+
"The link goes. The folder it points at stays exactly where it is.": "Se va el enlace. La carpeta a la que apunta se queda exactamente donde est\xE1.",
|
|
216
|
+
// ── what the form says when something is missing ──────────────────────────
|
|
217
|
+
"Give it a name \u2014 it is how you will call it in its window": "Ponle un nombre \u2014 es como la llamar\xE1s en su ventana",
|
|
218
|
+
"Could not add that agent": "No se pudo a\xF1adir ese agente",
|
|
219
|
+
"could not mount": "no se pudo montar",
|
|
220
|
+
"Could not set its engine": "No se pudo fijar su motor",
|
|
221
|
+
"{n} mount": "{n} montaje",
|
|
222
|
+
"{n} mounts": "{n} montajes"
|
|
223
|
+
});
|
|
224
|
+
anota({
|
|
225
|
+
// ── walking the disk ──────────────────────────────────────────────────────
|
|
226
|
+
"Walk your disk and pick whatever you like: a repository, a worktree, a folder of documents, one exact file. As many as you want. Nothing is copied \u2014 each one is linked into this agent\u2019s own home.": "Recorre tu disco y coge lo que quieras: un repositorio, un worktree, una carpeta de documentos, un fichero exacto. Tantos como quieras. No se copia nada \u2014 cada uno se enlaza dentro de la casa de este agente.",
|
|
227
|
+
"Working on": "Trabaja sobre",
|
|
228
|
+
"Nothing chosen yet \u2014 an agent with no mounts is fine too.": "A\xFAn no has cogido nada \u2014 un agente sin montajes tambi\xE9n vale.",
|
|
229
|
+
"Add this folder": "A\xF1adir esta carpeta",
|
|
230
|
+
"Add this": "A\xF1adir esto",
|
|
231
|
+
"Already chosen": "Ya est\xE1 cogido",
|
|
232
|
+
"Up one folder": "Subir una carpeta",
|
|
233
|
+
"reading that folder": "leyendo esa carpeta",
|
|
234
|
+
"This folder is empty.": "Esta carpeta est\xE1 vac\xEDa.",
|
|
235
|
+
"Only the first 2000 shown.": "S\xF3lo se muestran los 2000 primeros.",
|
|
236
|
+
"A repository, a worktree, a folder of documents, one exact file. It is linked, never copied.": "Un repositorio, un worktree, una carpeta de documentos, un fichero exacto. Se enlaza, nunca se copia."
|
|
237
|
+
});
|
|
238
|
+
anota({
|
|
239
|
+
// ── the demo shelf ────────────────────────────────────────────────────────
|
|
240
|
+
Demos: "Demos",
|
|
241
|
+
demos: "demos",
|
|
242
|
+
"See a committee happen": "Mira un comit\xE9 de verdad",
|
|
243
|
+
"reading the demo shelf": "leyendo la estanter\xEDa de demos",
|
|
244
|
+
"A real question, answered by a city of agents that never talk to each other behind the chair\u2019s back. Pick the field closest to yours \u2014 the machine is the same one in all three; only the work changes.": "Una pregunta de verdad, respondida por una ciudad de agentes que nunca hablan entre ellos a espaldas de quien preside. Elige el campo m\xE1s cercano al tuyo \u2014 la maquinaria es la misma en los tres; lo que cambia es el trabajo.",
|
|
245
|
+
"{n} turns": "{n} turnos",
|
|
246
|
+
"These are recordings of real runs over the real bus, played back here. To run one live in a terminal: agents-city demo --domain software.": "Son grabaciones de ejecuciones reales sobre el bus real, reproducidas aqu\xED. Para lanzar una en vivo desde terminal: agents-city demo --domain software.",
|
|
247
|
+
"All demos": "Todas las demos",
|
|
248
|
+
Play: "Reproducir",
|
|
249
|
+
Pause: "Pausa",
|
|
250
|
+
Replay: "Repetir",
|
|
251
|
+
"{done} of {total}": "{done} de {total}",
|
|
252
|
+
"Press play. The turns arrive one by one, exactly as they did.": "Dale a reproducir. Los turnos van llegando uno a uno, tal cual llegaron.",
|
|
253
|
+
"A recording of a real run: these events came off the real bus, from the real committee. Nothing here is being decided now.": "Una grabaci\xF3n de una ejecuci\xF3n real: estos eventos salieron del bus real, del comit\xE9 real. Aqu\xED no se est\xE1 decidiendo nada ahora mismo.",
|
|
254
|
+
"Nothing recorded here": "Aqu\xED no hay nada grabado",
|
|
255
|
+
"This install has no demo recordings. Make them with demo/graba.py, or run the full thing from a terminal with agents-city demo.": "Esta instalaci\xF3n no tiene grabaciones. Cr\xE9alas con demo/graba.py, o lanza la cosa entera desde terminal con agents-city demo."
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// city/web/src/explorador.ts
|
|
259
|
+
var MARCA = { repo: "git", worktree: "wt" };
|
|
260
|
+
var Explorador = class {
|
|
261
|
+
constructor(p, elige, yaElegidas) {
|
|
262
|
+
this.p = p;
|
|
263
|
+
this.elige = elige;
|
|
264
|
+
this.yaElegidas = yaElegidas;
|
|
265
|
+
}
|
|
266
|
+
aqui = "";
|
|
267
|
+
listado = null;
|
|
268
|
+
cargando = false;
|
|
269
|
+
fallo = "";
|
|
270
|
+
host = null;
|
|
271
|
+
/** Render into `host` and wire it. Safe to call again: that is the repaint. */
|
|
272
|
+
monta(host) {
|
|
273
|
+
this.host = host;
|
|
274
|
+
if (!this.listado && !this.cargando) void this.ve(this.aqui || "~");
|
|
275
|
+
host.innerHTML = this.html();
|
|
276
|
+
this.enlaza(host);
|
|
277
|
+
}
|
|
278
|
+
repinta() {
|
|
279
|
+
if (this.host) {
|
|
280
|
+
this.host.innerHTML = this.html();
|
|
281
|
+
this.enlaza(this.host);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
/** Walk to a folder. An unreadable one is said out loud, not swallowed. */
|
|
285
|
+
async ve(ruta) {
|
|
286
|
+
this.cargando = true;
|
|
287
|
+
this.fallo = "";
|
|
288
|
+
this.repinta();
|
|
289
|
+
try {
|
|
290
|
+
const r = await this.p.api("/api/carpeta?path=" + encodeURIComponent(ruta));
|
|
291
|
+
if (r.error) this.fallo = r.error;
|
|
292
|
+
else {
|
|
293
|
+
this.listado = r;
|
|
294
|
+
this.aqui = r.ruta;
|
|
295
|
+
}
|
|
296
|
+
} catch (e) {
|
|
297
|
+
this.fallo = String(e);
|
|
298
|
+
} finally {
|
|
299
|
+
this.cargando = false;
|
|
300
|
+
this.repinta();
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
/** The path being looked at right now — what "add this folder" means. */
|
|
304
|
+
get carpeta() {
|
|
305
|
+
return this.aqui;
|
|
306
|
+
}
|
|
307
|
+
migas() {
|
|
308
|
+
const esc3 = this.p.esc;
|
|
309
|
+
const l = this.listado;
|
|
310
|
+
if (!l) return "";
|
|
311
|
+
const partes = l.ruta.split("/").filter(Boolean);
|
|
312
|
+
let acumulado = "";
|
|
313
|
+
const trozos = partes.map((parte) => {
|
|
314
|
+
acumulado += "/" + parte;
|
|
315
|
+
return `<button type="button" class="expMiga" data-exp="ve" data-ruta="${esc3(
|
|
316
|
+
acumulado
|
|
317
|
+
)}">${esc3(parte)}</button>`;
|
|
318
|
+
});
|
|
319
|
+
return `<button type="button" class="expMiga" data-exp="ve" data-ruta="/">/</button>` + trozos.join("<i>/</i>");
|
|
320
|
+
}
|
|
321
|
+
html() {
|
|
322
|
+
const esc3 = this.p.esc;
|
|
323
|
+
const l = this.listado;
|
|
324
|
+
const elegidas = new Set(this.yaElegidas());
|
|
325
|
+
const filas = (l?.entradas ?? []).map((e) => {
|
|
326
|
+
const puesta = elegidas.has(e.ruta);
|
|
327
|
+
return `<div class="expFila ${e.dir ? "dir" : "fich"} ${puesta ? "puesta" : ""}">
|
|
328
|
+
<button type="button" class="expNombre" ${e.dir ? `data-exp="ve" data-ruta="${esc3(e.ruta)}"` : "disabled"} title="${esc3(e.ruta)}">
|
|
329
|
+
<i class="expIcono">${e.dir ? "\u25B8" : "\xB7"}</i>${esc3(e.nombre)}${e.git ? `<em class="expMarca">${esc3(MARCA[e.git] ?? e.git)}</em>` : ""}${e.enlace ? '<em class="expMarca">link</em>' : ""}</button>
|
|
330
|
+
<button type="button" class="expMas" data-exp="toma" data-ruta="${esc3(e.ruta)}"
|
|
331
|
+
data-dir="${e.dir ? "1" : ""}"
|
|
332
|
+
title="${esc3(puesta ? t("Already chosen") : t("Add this"))}">${puesta ? "\u2713" : "+"}</button>
|
|
333
|
+
</div>`;
|
|
334
|
+
}).join("");
|
|
335
|
+
const atajos = (l?.atajos ?? []).map(
|
|
336
|
+
(a) => `<button type="button" class="expAtajo ${a.ruta === l?.ruta ? "aqui" : ""}" data-exp="ve" data-ruta="${esc3(a.ruta)}">${esc3(a.nombre)}</button>`
|
|
337
|
+
).join("");
|
|
338
|
+
return `
|
|
339
|
+
<div class="exp">
|
|
340
|
+
<div class="expAtajos">${atajos}</div>
|
|
341
|
+
<div class="expBarra">
|
|
342
|
+
<button type="button" class="expSubir" data-exp="ve"
|
|
343
|
+
data-ruta="${esc3(l?.arriba ?? "~")}" ${l?.arriba ? "" : "disabled"}
|
|
344
|
+
title="${t("Up one folder")}">\u2191</button>
|
|
345
|
+
<div class="expMigas">${this.migas()}</div>
|
|
346
|
+
</div>
|
|
347
|
+
${this.fallo ? `<p class="pista malo">${esc3(this.fallo)}</p>` : this.cargando && !l ? `<p class="cargando">${t("reading that folder")}</p>` : filas ? `<div class="expLista">${filas}</div>` : `<p class="pista">${t("This folder is empty.")}</p>`}
|
|
348
|
+
${l?.recortada ? `<p class="pista">${t("Only the first 2000 shown.")}</p>` : ""}
|
|
349
|
+
<button type="button" class="bt bvMini expTomaAqui" data-exp="toma"
|
|
350
|
+
data-ruta="${esc3(l?.ruta ?? "")}" data-dir="1" ${l ? "" : "disabled"}>${t(
|
|
351
|
+
"Add this folder"
|
|
352
|
+
)}</button>
|
|
353
|
+
</div>`;
|
|
354
|
+
}
|
|
355
|
+
enlaza(raiz) {
|
|
356
|
+
raiz.querySelectorAll("[data-exp]").forEach((el) => {
|
|
357
|
+
el.onclick = (evento) => {
|
|
358
|
+
evento.preventDefault();
|
|
359
|
+
evento.stopPropagation();
|
|
360
|
+
const ruta = el.dataset.ruta ?? "";
|
|
361
|
+
if (!ruta) return;
|
|
362
|
+
if (el.dataset.exp === "ve") void this.ve(ruta);
|
|
363
|
+
else {
|
|
364
|
+
this.elige(ruta, el.dataset.dir === "1");
|
|
365
|
+
this.repinta();
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
};
|
|
183
371
|
|
|
184
372
|
// city/web/src/motores.ts
|
|
185
373
|
var RUNTIMES = ["claude", "codex", "opencode", "kimi"];
|
|
@@ -192,9 +380,9 @@ var NIVEL_ESFUERZO = {
|
|
|
192
380
|
max: 5
|
|
193
381
|
};
|
|
194
382
|
var ESFUERZOS = Object.keys(NIVEL_ESFUERZO);
|
|
195
|
-
function opciones(valores, actual,
|
|
383
|
+
function opciones(valores, actual, esc3, porDefecto = "default") {
|
|
196
384
|
return valores.concat(actual && !valores.includes(actual) ? [actual] : []).map(
|
|
197
|
-
(v) => `<option value="${
|
|
385
|
+
(v) => `<option value="${esc3(v)}"${v === actual ? " selected" : ""}>${esc3(v || porDefecto)}</option>`
|
|
198
386
|
).join("");
|
|
199
387
|
}
|
|
200
388
|
function nivelDeMotor(model) {
|
|
@@ -211,7 +399,7 @@ function nivelDeMotor(model) {
|
|
|
211
399
|
return { ancho: 0.6, texto: alias, defecto: false };
|
|
212
400
|
}
|
|
213
401
|
|
|
214
|
-
// city/web/src/
|
|
402
|
+
// city/web/src/casa.ts
|
|
215
403
|
var CLASES = [
|
|
216
404
|
[
|
|
217
405
|
"code",
|
|
@@ -225,7 +413,493 @@ var CLASES = [
|
|
|
225
413
|
],
|
|
226
414
|
["coordinator", "It coordinates", "Its house grows with the decisions it records."]
|
|
227
415
|
];
|
|
228
|
-
var
|
|
416
|
+
var FormularioDeCasa = class {
|
|
417
|
+
constructor(p, roles = []) {
|
|
418
|
+
this.p = p;
|
|
419
|
+
this.roles = roles;
|
|
420
|
+
}
|
|
421
|
+
datos = {
|
|
422
|
+
nombre: "",
|
|
423
|
+
clase: "code",
|
|
424
|
+
rol: "blank",
|
|
425
|
+
montajes: [],
|
|
426
|
+
runtime: "",
|
|
427
|
+
modelo: "",
|
|
428
|
+
esfuerzo: ""
|
|
429
|
+
};
|
|
430
|
+
roles = [];
|
|
431
|
+
host = null;
|
|
432
|
+
/** Render into `host` and wire it. Safe to call again: that is the repaint. */
|
|
433
|
+
monta(host) {
|
|
434
|
+
this.host = host;
|
|
435
|
+
void this.aseguraRoles();
|
|
436
|
+
host.innerHTML = this.html();
|
|
437
|
+
this.enlaza(host);
|
|
438
|
+
const hueco = host.querySelector(".casaExp");
|
|
439
|
+
if (hueco) this.explora().monta(hueco);
|
|
440
|
+
}
|
|
441
|
+
repinta() {
|
|
442
|
+
if (this.host) this.monta(this.host);
|
|
443
|
+
}
|
|
444
|
+
async aseguraRoles() {
|
|
445
|
+
if (this.roles.length) return;
|
|
446
|
+
try {
|
|
447
|
+
const r = await this.p.api("/api/roles?scope=agent");
|
|
448
|
+
this.roles = r.roles ?? [];
|
|
449
|
+
this.repinta();
|
|
450
|
+
} catch {
|
|
451
|
+
this.roles = [];
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
/** The folder picker, built once so walking survives a repaint. */
|
|
455
|
+
explorador = null;
|
|
456
|
+
explora() {
|
|
457
|
+
if (!this.explorador)
|
|
458
|
+
this.explorador = new Explorador(
|
|
459
|
+
this.p,
|
|
460
|
+
(ruta) => {
|
|
461
|
+
const ya = this.datos.montajes.indexOf(ruta);
|
|
462
|
+
if (ya >= 0) this.datos.montajes.splice(ya, 1);
|
|
463
|
+
else this.datos.montajes.push(ruta);
|
|
464
|
+
this.repinta();
|
|
465
|
+
},
|
|
466
|
+
() => this.datos.montajes
|
|
467
|
+
);
|
|
468
|
+
return this.explorador;
|
|
469
|
+
}
|
|
470
|
+
html() {
|
|
471
|
+
const a = this.datos;
|
|
472
|
+
const esc3 = this.p.esc;
|
|
473
|
+
const clases = CLASES.map(
|
|
474
|
+
([id, titulo, porque]) => `
|
|
475
|
+
<button type="button" class="bvOpcion ${a.clase === id ? "elegida" : ""}"
|
|
476
|
+
data-bv="clase" data-id="${id}">
|
|
477
|
+
<b>${t(titulo)}</b><span>${t(porque)}</span></button>`
|
|
478
|
+
).join("");
|
|
479
|
+
const roles = this.roles.map(
|
|
480
|
+
(r) => `<option value="${esc3(r.id)}"${r.id === a.rol ? " selected" : ""}>${esc3(r.name)}</option>`
|
|
481
|
+
).join("");
|
|
482
|
+
const esClaude = (a.runtime || "claude") === "claude";
|
|
483
|
+
return `
|
|
484
|
+
<div class="campo"><label>${t("What do you call it?")}</label>
|
|
485
|
+
<input type="text" id="bvNombre" value="${esc3(a.nombre)}"
|
|
486
|
+
placeholder="${t("urgencias, api, the handbook \u2014 whatever you would say out loud")}"></div>
|
|
487
|
+
<label class="bvEtiqueta">${t("What kind of work does it do?")}</label>
|
|
488
|
+
<div class="bvRejilla bvTres">${clases}</div>
|
|
489
|
+
<div class="campo"><label>${t("Its role \u2014 its speciality, never authority")}</label>
|
|
490
|
+
<select id="bvRol">${roles}</select></div>
|
|
491
|
+
<label class="bvEtiqueta">${t("What runs it?")}</label>
|
|
492
|
+
<p class="pista">${t(
|
|
493
|
+
"Leave all three on default and it runs the way you do. Model and effort are Claude\u2019s to read; another CLI carries its own flags."
|
|
494
|
+
)}</p>
|
|
495
|
+
<div class="bvMotor">
|
|
496
|
+
<div class="campo"><label>${t("provider")}</label>
|
|
497
|
+
<select id="bvRuntime">${opciones(RUNTIMES, a.runtime, esc3, "claude")}</select></div>
|
|
498
|
+
<div class="campo"><label>${t("engine")}</label>
|
|
499
|
+
<select id="bvModelo" ${esClaude ? "" : "disabled"}>${opciones(
|
|
500
|
+
[""].concat(MODELOS),
|
|
501
|
+
a.modelo,
|
|
502
|
+
esc3,
|
|
503
|
+
t("default")
|
|
504
|
+
)}</select></div>
|
|
505
|
+
<div class="campo"><label>${t("effort")}</label>
|
|
506
|
+
<select id="bvEsfuerzo" ${esClaude ? "" : "disabled"}>${opciones(
|
|
507
|
+
[""].concat(ESFUERZOS),
|
|
508
|
+
a.esfuerzo,
|
|
509
|
+
esc3,
|
|
510
|
+
t("default")
|
|
511
|
+
)}</select></div>
|
|
512
|
+
</div>
|
|
513
|
+
<label class="bvEtiqueta">${t("What does it work on?")}</label>
|
|
514
|
+
<p class="pista">${t(`Walk your disk and pick whatever you like: a repository, a
|
|
515
|
+
worktree, a folder of documents, one exact file. As many as you want. Nothing is
|
|
516
|
+
copied \u2014 each one is linked into this agent's own home.`)}</p>
|
|
517
|
+
<div class="casaExp"></div>
|
|
518
|
+
${a.montajes.length ? `<div class="expElegidas"><label class="bvEtiqueta">${t("Working on")}</label>
|
|
519
|
+
${a.montajes.map(
|
|
520
|
+
(m) => `<button type="button" class="bvChip elegida" data-bv="quita"
|
|
521
|
+
data-ruta="${esc3(m)}" title="${esc3(m)}">${esc3(
|
|
522
|
+
m.split("/").pop() || m
|
|
523
|
+
)}<i class="expQuita">\u2715</i></button>`
|
|
524
|
+
).join("")}</div>` : `<p class="pista">${t("Nothing chosen yet \u2014 an agent with no mounts is fine too.")}</p>`}`;
|
|
525
|
+
}
|
|
526
|
+
enlaza(raiz) {
|
|
527
|
+
raiz.querySelectorAll("[data-bv]").forEach((el) => {
|
|
528
|
+
el.onclick = (evento) => {
|
|
529
|
+
evento.preventDefault();
|
|
530
|
+
const que = el.dataset.bv;
|
|
531
|
+
this.recoge(raiz);
|
|
532
|
+
if (que === "clase") this.datos.clase = el.dataset.id ?? "code";
|
|
533
|
+
else if (que === "quita") {
|
|
534
|
+
const ya = this.datos.montajes.indexOf(el.dataset.ruta ?? "");
|
|
535
|
+
if (ya >= 0) this.datos.montajes.splice(ya, 1);
|
|
536
|
+
}
|
|
537
|
+
this.repinta();
|
|
538
|
+
};
|
|
539
|
+
});
|
|
540
|
+
raiz.querySelector("#bvRuntime")?.addEventListener("change", () => {
|
|
541
|
+
this.recoge(raiz);
|
|
542
|
+
this.repinta();
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
/** Keep what is typed when a click forces a repaint. */
|
|
546
|
+
recoge(raiz) {
|
|
547
|
+
const nombre = raiz.querySelector("#bvNombre");
|
|
548
|
+
const rol = raiz.querySelector("#bvRol");
|
|
549
|
+
if (nombre) this.datos.nombre = nombre.value;
|
|
550
|
+
if (rol) this.datos.rol = rol.value;
|
|
551
|
+
for (const [id, campo] of [
|
|
552
|
+
["#bvRuntime", "runtime"],
|
|
553
|
+
["#bvModelo", "modelo"],
|
|
554
|
+
["#bvEsfuerzo", "esfuerzo"]
|
|
555
|
+
]) {
|
|
556
|
+
const sel = raiz.querySelector(id);
|
|
557
|
+
if (sel) this.datos[campo] = sel.value;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Build it. Returns the new agent's slug, or null when nothing was written.
|
|
562
|
+
*
|
|
563
|
+
* Three endpoints by necessity, not by sloppiness: `/api/agentes` owns who is
|
|
564
|
+
* on the roster, `/api/montaje` owns what an agent can reach, and
|
|
565
|
+
* `/api/agente` owns how one is run. None of them is allowed to learn
|
|
566
|
+
* another's job just to save a round trip.
|
|
567
|
+
*/
|
|
568
|
+
async guarda() {
|
|
569
|
+
if (this.host) this.recoge(this.host);
|
|
570
|
+
const a = this.datos;
|
|
571
|
+
const nombre = a.nombre.trim();
|
|
572
|
+
if (!nombre) {
|
|
573
|
+
this.p.aviso(t("Give it a name \u2014 it is how you will call it in its window"), true);
|
|
574
|
+
return null;
|
|
575
|
+
}
|
|
576
|
+
try {
|
|
577
|
+
const r = await this.p.api("/api/agentes", {
|
|
578
|
+
method: "POST",
|
|
579
|
+
body: JSON.stringify({ name: nombre, kind: a.clase, role: a.rol })
|
|
580
|
+
});
|
|
581
|
+
if (!r.ok || !r.agent) {
|
|
582
|
+
this.p.aviso(r.error || t("Could not add that agent"), true);
|
|
583
|
+
return null;
|
|
584
|
+
}
|
|
585
|
+
for (const ruta of a.montajes) {
|
|
586
|
+
const m = await this.p.api("/api/montaje", {
|
|
587
|
+
method: "POST",
|
|
588
|
+
body: JSON.stringify({ agent: r.agent, add: ruta })
|
|
589
|
+
});
|
|
590
|
+
if (!m.ok) this.p.aviso(`${ruta}: ${m.error ?? t("could not mount")}`, true);
|
|
591
|
+
}
|
|
592
|
+
const motor = {};
|
|
593
|
+
if (a.runtime) motor.runtime = a.runtime;
|
|
594
|
+
if (a.modelo) motor.model = a.modelo;
|
|
595
|
+
if (a.esfuerzo) motor.effort = a.esfuerzo;
|
|
596
|
+
if (Object.keys(motor).length) {
|
|
597
|
+
const m = await this.p.api("/api/agente", {
|
|
598
|
+
method: "POST",
|
|
599
|
+
body: JSON.stringify({ agent: r.agent, ...motor })
|
|
600
|
+
});
|
|
601
|
+
if (!m.ok) this.p.aviso(m.error ?? t("Could not set its engine"), true);
|
|
602
|
+
}
|
|
603
|
+
return { slug: r.agent, nombre };
|
|
604
|
+
} catch (e) {
|
|
605
|
+
this.p.aviso(String(e), true);
|
|
606
|
+
return null;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
/** How this house reads on a list, once it is built. */
|
|
610
|
+
resumen() {
|
|
611
|
+
const a = this.datos;
|
|
612
|
+
return a.montajes.length ? plural(a.montajes.length, "{n} mount", "{n} mounts") : t("nothing mounted yet");
|
|
613
|
+
}
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
// city/web/src/dialogo.ts
|
|
617
|
+
function esc(s) {
|
|
618
|
+
return String(s).replace(
|
|
619
|
+
/[&<>"']/g,
|
|
620
|
+
(c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
var abierto = null;
|
|
624
|
+
function pregunta(p) {
|
|
625
|
+
abierto?.remove();
|
|
626
|
+
const campos = p.campos ?? [];
|
|
627
|
+
const fondo = document.createElement("div");
|
|
628
|
+
fondo.className = "dlgFondo";
|
|
629
|
+
fondo.innerHTML = `
|
|
630
|
+
<div class="dlg" role="dialog" aria-modal="true" aria-label="${esc(p.titulo)}"
|
|
631
|
+
style="${p.ancho ? `max-width:${p.ancho}px` : ""}">
|
|
632
|
+
<h2>${esc(p.titulo)}</h2>
|
|
633
|
+
${(p.cuerpo ?? []).map((l) => `<p class="dlgProsa">${esc(l)}</p>`).join("")}
|
|
634
|
+
${campos.map(
|
|
635
|
+
(c) => `<div class="campo"><label for="dlg-${esc(c.id)}">${esc(c.etiqueta)}</label>
|
|
636
|
+
${c.opciones ? `<select id="dlg-${esc(c.id)}" data-campo="${esc(c.id)}">${c.opciones.map(
|
|
637
|
+
(o) => `<option value="${esc(o.valor)}"${o.valor === (c.valor ?? "") ? " selected" : ""}>${esc(o.texto)}</option>`
|
|
638
|
+
).join("")}</select>` : `<input id="dlg-${esc(c.id)}" data-campo="${esc(c.id)}" type="text"
|
|
639
|
+
value="${esc(c.valor ?? "")}" placeholder="${esc(c.pista ?? "")}"
|
|
640
|
+
autocomplete="off" spellcheck="false">`}</div>`
|
|
641
|
+
).join("")}
|
|
642
|
+
${p.contenido ?? ""}
|
|
643
|
+
<div class="dlgBotones">
|
|
644
|
+
<button type="button" class="bt" data-dlg="no">${esc(p.cancelar ?? "Cancel")}</button>
|
|
645
|
+
<button type="button" class="bt ${p.peligro ? "malo" : "ppal"}" data-dlg="si">${esc(
|
|
646
|
+
p.aceptar ?? "OK"
|
|
647
|
+
)}</button>
|
|
648
|
+
</div>
|
|
649
|
+
</div>`;
|
|
650
|
+
document.body.appendChild(fondo);
|
|
651
|
+
abierto = fondo;
|
|
652
|
+
const devolvia = document.activeElement;
|
|
653
|
+
const caja = fondo.querySelector(".dlg");
|
|
654
|
+
const si = fondo.querySelector('[data-dlg="si"]');
|
|
655
|
+
return new Promise((resuelve) => {
|
|
656
|
+
let cerrado = false;
|
|
657
|
+
const cierra = (respuesta) => {
|
|
658
|
+
if (cerrado) return;
|
|
659
|
+
cerrado = true;
|
|
660
|
+
document.removeEventListener("keydown", tecla, true);
|
|
661
|
+
fondo.remove();
|
|
662
|
+
if (abierto === fondo) abierto = null;
|
|
663
|
+
devolvia?.focus?.();
|
|
664
|
+
resuelve(respuesta);
|
|
665
|
+
};
|
|
666
|
+
const lee = () => {
|
|
667
|
+
const fuera = {};
|
|
668
|
+
fondo.querySelectorAll("[data-campo]").forEach((el) => {
|
|
669
|
+
fuera[el.dataset.campo ?? ""] = el.value;
|
|
670
|
+
});
|
|
671
|
+
return fuera;
|
|
672
|
+
};
|
|
673
|
+
const acepta = () => {
|
|
674
|
+
const valores = lee();
|
|
675
|
+
if (campos.some((c) => c.requerido && !valores[c.id]?.trim())) return;
|
|
676
|
+
cierra(valores);
|
|
677
|
+
};
|
|
678
|
+
const revisa = () => {
|
|
679
|
+
const valores = lee();
|
|
680
|
+
const falta = campos.some((c) => c.requerido && !valores[c.id]?.trim());
|
|
681
|
+
const mal = p.exige !== void 0 && (valores[campos[0]?.id ?? ""] ?? "").trim() !== p.exige;
|
|
682
|
+
si.disabled = falta || mal;
|
|
683
|
+
};
|
|
684
|
+
si.onclick = acepta;
|
|
685
|
+
fondo.querySelector('[data-dlg="no"]').onclick = () => cierra(null);
|
|
686
|
+
fondo.onclick = (e) => {
|
|
687
|
+
if (e.target === fondo) cierra(null);
|
|
688
|
+
};
|
|
689
|
+
caja.addEventListener("input", revisa);
|
|
690
|
+
caja.addEventListener("change", revisa);
|
|
691
|
+
fondo.querySelectorAll("input").forEach((el) => {
|
|
692
|
+
el.onkeydown = (e) => {
|
|
693
|
+
if (e.key === "Enter" && !si.disabled) {
|
|
694
|
+
e.preventDefault();
|
|
695
|
+
acepta();
|
|
696
|
+
}
|
|
697
|
+
};
|
|
698
|
+
});
|
|
699
|
+
const tecla = (e) => {
|
|
700
|
+
if (e.key === "Escape") {
|
|
701
|
+
e.preventDefault();
|
|
702
|
+
cierra(null);
|
|
703
|
+
}
|
|
704
|
+
};
|
|
705
|
+
document.addEventListener("keydown", tecla, true);
|
|
706
|
+
p.enlaza?.(caja, cierra);
|
|
707
|
+
revisa();
|
|
708
|
+
(caja.querySelector("input,select,textarea") ?? (si.disabled ? caja : si)).focus?.();
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
async function confirma(titulo, cuerpo = [], opciones3 = {}) {
|
|
712
|
+
const r = await pregunta({
|
|
713
|
+
titulo,
|
|
714
|
+
cuerpo,
|
|
715
|
+
aceptar: opciones3.aceptar ?? "Yes",
|
|
716
|
+
peligro: opciones3.peligro
|
|
717
|
+
});
|
|
718
|
+
return r !== null;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// city/web/src/demo.ts
|
|
722
|
+
var VELOCIDADES = [
|
|
723
|
+
["0.5\xD7", 2900],
|
|
724
|
+
["1\xD7", 1450],
|
|
725
|
+
["2\xD7", 700],
|
|
726
|
+
["4\xD7", 350]
|
|
727
|
+
];
|
|
728
|
+
var Demos = class {
|
|
729
|
+
constructor(p) {
|
|
730
|
+
this.p = p;
|
|
731
|
+
}
|
|
732
|
+
catalogo = [];
|
|
733
|
+
cargando = false;
|
|
734
|
+
elegida = null;
|
|
735
|
+
hasta = 0;
|
|
736
|
+
corriendo = false;
|
|
737
|
+
velocidad = 1450;
|
|
738
|
+
reloj = null;
|
|
739
|
+
host = null;
|
|
740
|
+
monta(host) {
|
|
741
|
+
this.host = host;
|
|
742
|
+
if (!this.catalogo.length && !this.cargando) void this.lee();
|
|
743
|
+
this.repinta();
|
|
744
|
+
}
|
|
745
|
+
/** Stop the clock. Called when the section is left, or the page repaints
|
|
746
|
+
* elsewhere: a timer still firing into a detached node is a leak with a view. */
|
|
747
|
+
para() {
|
|
748
|
+
this.corriendo = false;
|
|
749
|
+
if (this.reloj !== null) {
|
|
750
|
+
clearTimeout(this.reloj);
|
|
751
|
+
this.reloj = null;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
repinta() {
|
|
755
|
+
if (!this.host) return;
|
|
756
|
+
this.host.innerHTML = this.html();
|
|
757
|
+
this.enlaza(this.host);
|
|
758
|
+
if (this.elegida) {
|
|
759
|
+
const lista = this.host.querySelector("#demoLista");
|
|
760
|
+
if (lista) lista.scrollTop = lista.scrollHeight;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
async lee() {
|
|
764
|
+
this.cargando = true;
|
|
765
|
+
try {
|
|
766
|
+
const r = await this.p.api("/api/demos");
|
|
767
|
+
this.catalogo = r.demos ?? [];
|
|
768
|
+
} catch {
|
|
769
|
+
this.catalogo = [];
|
|
770
|
+
} finally {
|
|
771
|
+
this.cargando = false;
|
|
772
|
+
this.repinta();
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
async abre(id) {
|
|
776
|
+
this.para();
|
|
777
|
+
this.hasta = 0;
|
|
778
|
+
this.elegida = null;
|
|
779
|
+
this.repinta();
|
|
780
|
+
try {
|
|
781
|
+
const r = await this.p.api("/api/demos?story=" + encodeURIComponent(id));
|
|
782
|
+
if (!r.error) this.elegida = r;
|
|
783
|
+
} catch {
|
|
784
|
+
this.elegida = null;
|
|
785
|
+
}
|
|
786
|
+
this.repinta();
|
|
787
|
+
if (this.elegida) this.arranca();
|
|
788
|
+
}
|
|
789
|
+
arranca() {
|
|
790
|
+
if (!this.elegida) return;
|
|
791
|
+
if (this.hasta >= this.elegida.eventos.length) this.hasta = 0;
|
|
792
|
+
this.corriendo = true;
|
|
793
|
+
this.repinta();
|
|
794
|
+
this.siguiente();
|
|
795
|
+
}
|
|
796
|
+
siguiente() {
|
|
797
|
+
if (this.reloj !== null) clearTimeout(this.reloj);
|
|
798
|
+
this.reloj = window.setTimeout(() => {
|
|
799
|
+
if (!this.corriendo || !this.elegida) return;
|
|
800
|
+
this.hasta += 1;
|
|
801
|
+
if (this.hasta >= this.elegida.eventos.length) {
|
|
802
|
+
this.hasta = this.elegida.eventos.length;
|
|
803
|
+
this.corriendo = false;
|
|
804
|
+
this.repinta();
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
this.repinta();
|
|
808
|
+
this.siguiente();
|
|
809
|
+
}, this.velocidad);
|
|
810
|
+
}
|
|
811
|
+
html() {
|
|
812
|
+
if (this.elegida) return this.reproductor(this.elegida);
|
|
813
|
+
if (this.cargando) return `<p class="cargando">${t("reading the demo shelf")}</p>`;
|
|
814
|
+
if (!this.catalogo.length)
|
|
815
|
+
return `<div><span class="sub">${t("demos")}</span>
|
|
816
|
+
<h1 style="margin-top:6px">${t("Nothing recorded here")}</h1>
|
|
817
|
+
<p class="prosa">${t("This install has no demo recordings. Make them with demo/graba.py, or run the full thing from a terminal with agents-city demo.")}</p></div>`;
|
|
818
|
+
const esc3 = this.p.esc;
|
|
819
|
+
return `<div>
|
|
820
|
+
<span class="sub">${t("demos")}</span>
|
|
821
|
+
<h1 style="margin-top:6px">${t("See a committee happen")}</h1>
|
|
822
|
+
<p class="prosa">${t(`A real question, answered by a city of agents that never talk to each
|
|
823
|
+
other behind the chair's back. Pick the field closest to yours \u2014 the machine is the same
|
|
824
|
+
one in all three; only the work changes.`)}</p>
|
|
825
|
+
<div class="demoRejilla">${this.catalogo.map(
|
|
826
|
+
(d) => `<button type="button" class="demoCarta" data-demo="abre" data-id="${esc3(d.id)}">
|
|
827
|
+
<span class="demoDominio">${esc3(d.dominio)}</span>
|
|
828
|
+
<b>${esc3(d.titulo)}</b>
|
|
829
|
+
<span class="demoCiudad">${esc3(d.ciudad)}</span>
|
|
830
|
+
<span class="demoReparto">${esc3(d.reparto.join(" \xB7 "))}</span>
|
|
831
|
+
<span class="demoPlay">\u25B6 ${t("{n} turns", { n: d.turnos })}</span>
|
|
832
|
+
</button>`
|
|
833
|
+
).join("")}</div>
|
|
834
|
+
<p class="pista">${t("These are recordings of real runs over the real bus, played back here. To run one live in a terminal: agents-city demo --domain software.")}</p>
|
|
835
|
+
</div>`;
|
|
836
|
+
}
|
|
837
|
+
reproductor(d) {
|
|
838
|
+
const esc3 = this.p.esc;
|
|
839
|
+
const total = d.eventos.length;
|
|
840
|
+
const hecho = Math.min(this.hasta, total);
|
|
841
|
+
const turnos = d.eventos.slice(0, hecho).map(this.p.pinta).join("");
|
|
842
|
+
const acabado = hecho >= total;
|
|
843
|
+
return `<div class="demoPlayer">
|
|
844
|
+
<div class="demoCab">
|
|
845
|
+
<button type="button" class="bt bvMini" data-demo="atras">\u2190 ${t("All demos")}</button>
|
|
846
|
+
<div><span class="sub">${esc3(d.dominio)} \xB7 ${esc3(d.ciudad)}</span>
|
|
847
|
+
<h2>${esc3(d.titulo)}</h2></div>
|
|
848
|
+
</div>
|
|
849
|
+
<div class="demoMandos">
|
|
850
|
+
<button type="button" class="bt ppal" data-demo="${this.corriendo ? "pausa" : "play"}">${this.corriendo ? `\u275A\u275A ${t("Pause")}` : acabado ? `\u21BA ${t("Replay")}` : `\u25B6 ${t("Play")}`}</button>
|
|
851
|
+
<button type="button" class="bt" data-demo="replay" ${hecho ? "" : "disabled"}>\u21BA ${t(
|
|
852
|
+
"Replay"
|
|
853
|
+
)}</button>
|
|
854
|
+
<span class="demoVel">${VELOCIDADES.map(
|
|
855
|
+
([etiqueta, ms]) => `<button type="button" class="demoVelBoton ${ms === this.velocidad ? "aqui" : ""}" data-demo="vel" data-ms="${ms}">${esc3(etiqueta)}</button>`
|
|
856
|
+
).join("")}</span>
|
|
857
|
+
<span class="demoCuenta">${t("{done} of {total}", { done: hecho, total })}</span>
|
|
858
|
+
</div>
|
|
859
|
+
<div class="demoBarra"><i style="width:${total ? hecho / total * 100 : 0}%"></i></div>
|
|
860
|
+
<ol class="liveLista" id="demoLista">${turnos || `<li class="liveEmpty">${t("Press play. The turns arrive one by one, exactly as they did.")}</li>`}</ol>
|
|
861
|
+
<p class="pista">${t("A recording of a real run: these events came off the real bus, from the real committee. Nothing here is being decided now.")}</p>
|
|
862
|
+
</div>`;
|
|
863
|
+
}
|
|
864
|
+
enlaza(raiz) {
|
|
865
|
+
raiz.querySelectorAll("[data-demo]").forEach((el) => {
|
|
866
|
+
el.onclick = (evento) => {
|
|
867
|
+
evento.preventDefault();
|
|
868
|
+
switch (el.dataset.demo) {
|
|
869
|
+
case "abre":
|
|
870
|
+
void this.abre(el.dataset.id ?? "");
|
|
871
|
+
break;
|
|
872
|
+
case "atras":
|
|
873
|
+
this.para();
|
|
874
|
+
this.elegida = null;
|
|
875
|
+
this.hasta = 0;
|
|
876
|
+
this.repinta();
|
|
877
|
+
break;
|
|
878
|
+
case "play":
|
|
879
|
+
this.arranca();
|
|
880
|
+
break;
|
|
881
|
+
case "pausa":
|
|
882
|
+
this.para();
|
|
883
|
+
this.repinta();
|
|
884
|
+
break;
|
|
885
|
+
case "replay":
|
|
886
|
+
this.para();
|
|
887
|
+
this.hasta = 0;
|
|
888
|
+
this.arranca();
|
|
889
|
+
break;
|
|
890
|
+
case "vel": {
|
|
891
|
+
this.velocidad = Number(el.dataset.ms) || 1450;
|
|
892
|
+
if (this.corriendo) this.siguiente();
|
|
893
|
+
this.repinta();
|
|
894
|
+
break;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
};
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
};
|
|
901
|
+
|
|
902
|
+
// city/web/src/bienvenida.ts
|
|
229
903
|
var PASOS = ["Welcome", "The work", "Your chair", "The houses", "Ready"];
|
|
230
904
|
function casita(color) {
|
|
231
905
|
return `<svg viewBox="0 0 120 108" role="img" aria-label="an isometric house" class="bvCasa">
|
|
@@ -242,16 +916,11 @@ var Bienvenida = class {
|
|
|
242
916
|
dominios = [];
|
|
243
917
|
roles = [];
|
|
244
918
|
rolesDeAgente = [];
|
|
245
|
-
repos = [];
|
|
246
919
|
dominio = "";
|
|
247
920
|
rol = "";
|
|
248
921
|
roster = [];
|
|
249
922
|
enCurso = null;
|
|
250
923
|
guardando = false;
|
|
251
|
-
buscandoRepos = false;
|
|
252
|
-
reposListos = false;
|
|
253
|
-
/** What is typed in the mount search box, kept across repaints. */
|
|
254
|
-
filtro = "";
|
|
255
924
|
lienzo = null;
|
|
256
925
|
/** Draw the current step into `lienzo`. The only entry point. */
|
|
257
926
|
async pinta(lienzo) {
|
|
@@ -365,24 +1034,19 @@ var Bienvenida = class {
|
|
|
365
1034
|
}
|
|
366
1035
|
// ── 3. the agents ─────────────────────────────────────────────────────────
|
|
367
1036
|
pantallaAgentes() {
|
|
368
|
-
this.buscaReposUnaVez();
|
|
369
1037
|
if (!this.enCurso) return this.listaDeAgentes();
|
|
370
|
-
return
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
this.
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
this.buscandoRepos = false;
|
|
383
|
-
this.reposListos = true;
|
|
384
|
-
if (this.lienzo && this.paso === 3) void this.pinta(this.lienzo);
|
|
385
|
-
});
|
|
1038
|
+
return `
|
|
1039
|
+
<div class="bvPaso">
|
|
1040
|
+
<span class="sub">${t("a new house")}</span>
|
|
1041
|
+
<h1>${t("Who lives in it?")}</h1>
|
|
1042
|
+
<div id="bvCasa"></div>
|
|
1043
|
+
<div class="bvBotones">
|
|
1044
|
+
<button class="bt" data-bv="cancela">${t("Cancel")}</button>
|
|
1045
|
+
<button class="bt ppal" data-bv="guarda" ${this.guardando ? "disabled" : ""}>${t(
|
|
1046
|
+
this.guardando ? "building\u2026" : "Build it"
|
|
1047
|
+
)}</button>
|
|
1048
|
+
</div>
|
|
1049
|
+
</div>`;
|
|
386
1050
|
}
|
|
387
1051
|
listaDeAgentes() {
|
|
388
1052
|
const fichas = this.roster.map(
|
|
@@ -422,101 +1086,6 @@ var Bienvenida = class {
|
|
|
422
1086
|
* that keeps knowledge sees document folders first, because that is the one
|
|
423
1087
|
* kind of agent the old picker could not help at all.
|
|
424
1088
|
*/
|
|
425
|
-
buscaEnElDisco(a) {
|
|
426
|
-
const q2 = this.filtro.trim().toLowerCase();
|
|
427
|
-
const quiereDocs = a.clase === "knowledge";
|
|
428
|
-
const puntua = (r) => (r.clase === "docs" ? quiereDocs ? -2 : 1 : 0) + (r.mio ? -1 : 0);
|
|
429
|
-
const encaja = q2 ? this.repos.filter(
|
|
430
|
-
(r) => r.nombre.toLowerCase().includes(q2) || r.ruta.toLowerCase().includes(q2)
|
|
431
|
-
) : this.repos.filter((r) => r.mio);
|
|
432
|
-
const elegidas = this.repos.filter((r) => a.montajes.includes(r.ruta) && !encaja.includes(r));
|
|
433
|
-
return elegidas.concat(encaja).sort((x, y) => puntua(x) - puntua(y)).slice(0, 40);
|
|
434
|
-
}
|
|
435
|
-
formularioDeAgente(a) {
|
|
436
|
-
const clases = CLASES.map(
|
|
437
|
-
([id, titulo, porque]) => `
|
|
438
|
-
<button class="bvOpcion ${a.clase === id ? "elegida" : ""}" data-bv="clase" data-id="${id}">
|
|
439
|
-
<b>${t(titulo)}</b><span>${t(porque)}</span></button>`
|
|
440
|
-
).join("");
|
|
441
|
-
const roles = this.rolesDeAgente.map(
|
|
442
|
-
(r) => `<option value="${this.p.esc(r.id)}"${r.id === a.rol ? " selected" : ""}>${this.p.esc(
|
|
443
|
-
r.name
|
|
444
|
-
)}</option>`
|
|
445
|
-
).join("");
|
|
446
|
-
const esClaude = (a.runtime || "claude") === "claude";
|
|
447
|
-
const encontrados = this.buscaEnElDisco(a);
|
|
448
|
-
const sugeridos = encontrados.map(
|
|
449
|
-
(r) => `<button class="bvChip cl-${this.p.esc(r.clase)} ${a.montajes.includes(r.ruta) ? "elegida" : ""}"
|
|
450
|
-
data-bv="monta" data-ruta="${this.p.esc(r.ruta)}"
|
|
451
|
-
title="${this.p.esc(r.ruta)}${r.cuando ? ` \xB7 ${this.p.esc(r.cuando)}` : ""}"
|
|
452
|
-
><i class="bvClase">${this.p.esc(CLASE_CORTA[r.clase] ?? r.clase)}</i>${this.p.esc(
|
|
453
|
-
r.nombre
|
|
454
|
-
)}</button>`
|
|
455
|
-
).join("");
|
|
456
|
-
return `
|
|
457
|
-
<div class="bvPaso">
|
|
458
|
-
<span class="sub">${t("a new house")}</span>
|
|
459
|
-
<h1>${t("Who lives in it?")}</h1>
|
|
460
|
-
<div class="campo"><label>${t("What do you call it?")}</label>
|
|
461
|
-
<input type="text" id="bvNombre" value="${this.p.esc(a.nombre)}"
|
|
462
|
-
placeholder="${t("urgencias, api, the handbook \u2014 whatever you would say out loud")}"></div>
|
|
463
|
-
<label class="bvEtiqueta">${t("What kind of work does it do?")}</label>
|
|
464
|
-
<div class="bvRejilla bvTres">${clases}</div>
|
|
465
|
-
<div class="campo"><label>${t("Its role \u2014 its speciality, never authority")}</label>
|
|
466
|
-
<select id="bvRol">${roles}</select></div>
|
|
467
|
-
<label class="bvEtiqueta">${t("What runs it?")}</label>
|
|
468
|
-
<p class="pista">${t(
|
|
469
|
-
"Leave all three on default and it runs the way you do. Model and effort are Claude\u2019s to read; another CLI carries its own flags."
|
|
470
|
-
)}</p>
|
|
471
|
-
<div class="bvMotor">
|
|
472
|
-
<div class="campo"><label>${t("provider")}</label>
|
|
473
|
-
<select id="bvRuntime">${opciones(RUNTIMES, a.runtime, this.p.esc, "claude")}</select></div>
|
|
474
|
-
<div class="campo"><label>${t("engine")}</label>
|
|
475
|
-
<select id="bvModelo" ${esClaude ? "" : "disabled"}>${opciones(
|
|
476
|
-
[""].concat(MODELOS),
|
|
477
|
-
a.modelo,
|
|
478
|
-
this.p.esc,
|
|
479
|
-
t("default")
|
|
480
|
-
)}</select></div>
|
|
481
|
-
<div class="campo"><label>${t("effort")}</label>
|
|
482
|
-
<select id="bvEsfuerzo" ${esClaude ? "" : "disabled"}>${opciones(
|
|
483
|
-
[""].concat(ESFUERZOS),
|
|
484
|
-
a.esfuerzo,
|
|
485
|
-
this.p.esc,
|
|
486
|
-
t("default")
|
|
487
|
-
)}</select></div>
|
|
488
|
-
</div>
|
|
489
|
-
<label class="bvEtiqueta">${t("What does it work on?")}</label>
|
|
490
|
-
<p class="pista">${t(`Everything found on this disk: <b>repositories</b>, the
|
|
491
|
-
<b>worktrees</b> an isolated agent works in, and <b>folders of documents</b> with no git
|
|
492
|
-
at all. Pick as many as you like \u2014 this is the part that makes it an agent and not a
|
|
493
|
-
repo.`)}</p>
|
|
494
|
-
<div class="bvBuscador">
|
|
495
|
-
<input type="search" id="bvBusca" value="${this.p.esc(this.filtro)}"
|
|
496
|
-
placeholder="${t("search your disk \u2014 a name, or part of a path")}">
|
|
497
|
-
<button class="bt bvMini" data-bv="rebusca" ${this.buscandoRepos ? "disabled" : ""}
|
|
498
|
-
title="${t("Look through the disk again \u2014 for something cloned a minute ago")}">${t(
|
|
499
|
-
this.buscandoRepos ? "searching\u2026" : "Search again"
|
|
500
|
-
)}</button>
|
|
501
|
-
</div>
|
|
502
|
-
${sugeridos ? `<div class="bvChips">${sugeridos}</div>
|
|
503
|
-
<p class="pista">${this.filtro ? t("{shown} of {total} places match", {
|
|
504
|
-
shown: String(encontrados.length),
|
|
505
|
-
total: String(this.repos.length)
|
|
506
|
-
}) : t("{total} places found \u2014 type to narrow it down", {
|
|
507
|
-
total: String(this.repos.length)
|
|
508
|
-
})}</p>` : this.buscandoRepos ? `<p class="cargando">${t("looking through your disk\u2026")}</p>` : this.filtro ? `<p class="pista">${t("Nothing matches that. Add it by path below.")}</p>` : `<p class="pista">${t("Nothing found on this disk \u2014 add folders by path below.")}</p>`}
|
|
509
|
-
<div class="campo"><label>${t("\u2026or a path, and Enter")}</label>
|
|
510
|
-
<input type="text" id="bvRuta" placeholder="~/documents/handbook"></div>
|
|
511
|
-
${a.montajes.length ? `<p class="pista">${t("Working on: {que}", { que: `<b>${this.p.esc(a.montajes.join(", "))}</b>` })}</p>` : ""}
|
|
512
|
-
<div class="bvBotones">
|
|
513
|
-
<button class="bt" data-bv="cancela">${t("Cancel")}</button>
|
|
514
|
-
<button class="bt ppal" data-bv="guarda" ${this.guardando ? "disabled" : ""}>${t(
|
|
515
|
-
this.guardando ? "building\u2026" : "Build it"
|
|
516
|
-
)}</button>
|
|
517
|
-
</div>
|
|
518
|
-
</div>`;
|
|
519
|
-
}
|
|
520
1089
|
// ── 4. done ───────────────────────────────────────────────────────────────
|
|
521
1090
|
pantallaFinal() {
|
|
522
1091
|
return `
|
|
@@ -567,33 +1136,10 @@ var Bienvenida = class {
|
|
|
567
1136
|
return repinta();
|
|
568
1137
|
}
|
|
569
1138
|
if (que === "nuevo") {
|
|
570
|
-
this.enCurso =
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
montajes: [],
|
|
575
|
-
runtime: "",
|
|
576
|
-
modelo: "",
|
|
577
|
-
esfuerzo: ""
|
|
578
|
-
};
|
|
579
|
-
return repinta();
|
|
580
|
-
}
|
|
581
|
-
if (que === "clase" && this.enCurso) {
|
|
582
|
-
this.enCurso.clase = el.dataset.id ?? "code";
|
|
583
|
-
this.recogeFormulario(lienzo);
|
|
584
|
-
return repinta();
|
|
585
|
-
}
|
|
586
|
-
if (que === "monta" && this.enCurso) {
|
|
587
|
-
const ruta2 = el.dataset.ruta ?? "";
|
|
588
|
-
const ya = this.enCurso.montajes.indexOf(ruta2);
|
|
589
|
-
if (ya >= 0) this.enCurso.montajes.splice(ya, 1);
|
|
590
|
-
else this.enCurso.montajes.push(ruta2);
|
|
591
|
-
this.recogeFormulario(lienzo);
|
|
592
|
-
return repinta();
|
|
593
|
-
}
|
|
594
|
-
if (que === "rebusca") {
|
|
595
|
-
this.recogeFormulario(lienzo);
|
|
596
|
-
this.buscaReposUnaVez(true);
|
|
1139
|
+
this.enCurso = new FormularioDeCasa(
|
|
1140
|
+
{ api: this.p.api, esc: this.p.esc, aviso: this.p.aviso, yo: this.p.yo },
|
|
1141
|
+
this.rolesDeAgente
|
|
1142
|
+
);
|
|
597
1143
|
return repinta();
|
|
598
1144
|
}
|
|
599
1145
|
if (que === "cancela") {
|
|
@@ -608,97 +1154,25 @@ var Bienvenida = class {
|
|
|
608
1154
|
if (que === "ir-puesto") return this.p.vete("puesto");
|
|
609
1155
|
};
|
|
610
1156
|
});
|
|
611
|
-
const
|
|
612
|
-
if (
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
const donde = busca.selectionStart ?? busca.value.length;
|
|
616
|
-
void this.pinta(lienzo).then(() => {
|
|
617
|
-
const otra = lienzo.querySelector("#bvBusca");
|
|
618
|
-
if (!otra) return;
|
|
619
|
-
otra.focus();
|
|
620
|
-
otra.setSelectionRange(donde, donde);
|
|
621
|
-
});
|
|
622
|
-
};
|
|
1157
|
+
const hueco = lienzo.querySelector("#bvCasa");
|
|
1158
|
+
if (hueco && this.enCurso) {
|
|
1159
|
+
this.enCurso.monta(hueco);
|
|
1160
|
+
hueco.querySelector("#bvNombre")?.focus();
|
|
623
1161
|
}
|
|
624
|
-
const ruta = lienzo.querySelector("#bvRuta");
|
|
625
|
-
if (ruta)
|
|
626
|
-
ruta.onkeydown = (e) => {
|
|
627
|
-
if (e.key !== "Enter" || !this.enCurso) return;
|
|
628
|
-
e.preventDefault();
|
|
629
|
-
const valor = ruta.value.trim();
|
|
630
|
-
if (valor) this.enCurso.montajes.push(valor);
|
|
631
|
-
this.recogeFormulario(lienzo);
|
|
632
|
-
void this.pinta(lienzo);
|
|
633
|
-
};
|
|
634
|
-
lienzo.querySelector("#bvRuntime")?.addEventListener("change", () => {
|
|
635
|
-
this.recogeFormulario(lienzo);
|
|
636
|
-
void this.pinta(lienzo);
|
|
637
|
-
});
|
|
638
|
-
if (!busca?.value) lienzo.querySelector("#bvNombre")?.focus();
|
|
639
|
-
}
|
|
640
|
-
/** Keep what is typed when a click forces a repaint. */
|
|
641
|
-
recogeFormulario(lienzo) {
|
|
642
|
-
if (!this.enCurso) return;
|
|
643
|
-
const nombre = lienzo.querySelector("#bvNombre");
|
|
644
|
-
const rol = lienzo.querySelector("#bvRol");
|
|
645
|
-
if (nombre) this.enCurso.nombre = nombre.value;
|
|
646
|
-
if (rol) this.enCurso.rol = rol.value;
|
|
647
|
-
for (const [id, campo] of [
|
|
648
|
-
["#bvRuntime", "runtime"],
|
|
649
|
-
["#bvModelo", "modelo"],
|
|
650
|
-
["#bvEsfuerzo", "esfuerzo"]
|
|
651
|
-
]) {
|
|
652
|
-
const sel = lienzo.querySelector(id);
|
|
653
|
-
if (sel) this.enCurso[campo] = sel.value;
|
|
654
|
-
}
|
|
655
|
-
const busca = lienzo.querySelector("#bvBusca");
|
|
656
|
-
if (busca) this.filtro = busca.value;
|
|
657
1162
|
}
|
|
658
1163
|
async guardaAgente(lienzo) {
|
|
659
|
-
this.
|
|
660
|
-
|
|
661
|
-
if (!a) return;
|
|
662
|
-
if (!a.nombre.trim()) {
|
|
663
|
-
this.p.aviso("Give it a name \u2014 it is how you will call it in its window", true);
|
|
664
|
-
return;
|
|
665
|
-
}
|
|
1164
|
+
const forma = this.enCurso;
|
|
1165
|
+
if (!forma) return;
|
|
666
1166
|
this.guardando = true;
|
|
667
1167
|
await this.pinta(lienzo);
|
|
668
1168
|
try {
|
|
669
1169
|
await this.aseguraFicha();
|
|
670
|
-
const
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
});
|
|
674
|
-
if (!r.ok || !r.agent) {
|
|
675
|
-
this.p.aviso(r.error || "Could not add that agent", true);
|
|
676
|
-
return;
|
|
677
|
-
}
|
|
678
|
-
for (const ruta of a.montajes) {
|
|
679
|
-
const m = await this.p.api("/api/montaje", {
|
|
680
|
-
method: "POST",
|
|
681
|
-
body: JSON.stringify({ agent: r.agent, add: ruta })
|
|
682
|
-
});
|
|
683
|
-
if (!m.ok) this.p.aviso(`${ruta}: ${m.error ?? "could not mount"}`, true);
|
|
684
|
-
}
|
|
685
|
-
const motor = {};
|
|
686
|
-
if (a.runtime) motor.runtime = a.runtime;
|
|
687
|
-
if (a.modelo) motor.model = a.modelo;
|
|
688
|
-
if (a.esfuerzo) motor.effort = a.esfuerzo;
|
|
689
|
-
if (Object.keys(motor).length) {
|
|
690
|
-
const m = await this.p.api("/api/agente", {
|
|
691
|
-
method: "POST",
|
|
692
|
-
body: JSON.stringify({ agent: r.agent, ...motor })
|
|
693
|
-
});
|
|
694
|
-
if (!m.ok) this.p.aviso(m.error ?? "Could not set its engine", true);
|
|
695
|
-
}
|
|
696
|
-
this.roster.push({ ...a, nombre: a.nombre.trim() });
|
|
1170
|
+
const hecho = await forma.guarda();
|
|
1171
|
+
if (!hecho) return;
|
|
1172
|
+
this.roster.push({ ...forma.datos, nombre: hecho.nombre });
|
|
697
1173
|
this.enCurso = null;
|
|
698
|
-
this.p.aviso(
|
|
1174
|
+
this.p.aviso(t("{name} joined the city", { name: hecho.nombre }));
|
|
699
1175
|
await this.p.refresca();
|
|
700
|
-
} catch (e) {
|
|
701
|
-
this.p.aviso(String(e), true);
|
|
702
1176
|
} finally {
|
|
703
1177
|
this.guardando = false;
|
|
704
1178
|
await this.pinta(lienzo);
|
|
@@ -777,7 +1251,7 @@ function q(sel, raiz = document) {
|
|
|
777
1251
|
function todos(sel, raiz = document) {
|
|
778
1252
|
return [...raiz.querySelectorAll(sel)];
|
|
779
1253
|
}
|
|
780
|
-
function
|
|
1254
|
+
function esc2(s) {
|
|
781
1255
|
return String(s ?? "").replace(
|
|
782
1256
|
/[&<>"']/g,
|
|
783
1257
|
(c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]
|
|
@@ -805,6 +1279,7 @@ var SECCIONES = [
|
|
|
805
1279
|
["red", "Roads"],
|
|
806
1280
|
["committee", "Committee"],
|
|
807
1281
|
["gente", "Houses"],
|
|
1282
|
+
["demos", "Demos"],
|
|
808
1283
|
["ciudades", "Cities"]
|
|
809
1284
|
];
|
|
810
1285
|
var primeraCarga = true;
|
|
@@ -973,7 +1448,7 @@ function pintaActividad(seguir = false) {
|
|
|
973
1448
|
}
|
|
974
1449
|
}
|
|
975
1450
|
filter.innerHTML = '<option value="">all conversations</option>' + [...opened.entries()].reverse().map(
|
|
976
|
-
([thread, question]) => `<option value="${
|
|
1451
|
+
([thread, question]) => `<option value="${esc2(thread)}">${esc2(question.slice(0, 54) || thread)}</option>`
|
|
977
1452
|
).join("");
|
|
978
1453
|
const selected = liveSelectedThread || "";
|
|
979
1454
|
filter.value = [...filter.options].some((option) => option.value === selected) ? selected : "";
|
|
@@ -1011,19 +1486,19 @@ function renderActividad(event) {
|
|
|
1011
1486
|
const summary = event.summary.trim();
|
|
1012
1487
|
const compact = resumenVisible(summary);
|
|
1013
1488
|
const long = compact !== summary;
|
|
1014
|
-
const details = event.details.length ? `<details class="liveEvidence"><summary>${event.details.length} ${event.details.length === 1 ? "detail" : "details"}</summary><ul>${event.details.map((detail) => `<li>${
|
|
1015
|
-
const full = long ? `<details class="liveFull"><summary>read full message</summary><p>${
|
|
1016
|
-
return `<li class="liveTurn ${side} ${
|
|
1489
|
+
const details = event.details.length ? `<details class="liveEvidence"><summary>${event.details.length} ${event.details.length === 1 ? "detail" : "details"}</summary><ul>${event.details.map((detail) => `<li>${esc2(detail)}</li>`).join("")}</ul></details>` : "";
|
|
1490
|
+
const full = long ? `<details class="liveFull"><summary>read full message</summary><p>${esc2(summary)}</p></details>` : "";
|
|
1491
|
+
return `<li class="liveTurn ${side} ${esc2(event.tone)}">
|
|
1017
1492
|
<div class="liveAvatar${E?.avatars?.[event.actor] ? " conCara" : ""}"
|
|
1018
1493
|
style="--actor-hue:${tonoActor(event.actor)}" aria-hidden="true">
|
|
1019
1494
|
${cara(event.actor, event.role === "chair")}
|
|
1020
1495
|
</div>
|
|
1021
1496
|
<article class="liveBubble">
|
|
1022
|
-
<div class="liveMeta"><span class="liveActor">${
|
|
1023
|
-
${role ? `<span class="liveRole">${
|
|
1024
|
-
${route ? `<span>${
|
|
1025
|
-
<div class="liveHeading">${icono(event.kind)}<h3>${
|
|
1026
|
-
<p>${
|
|
1497
|
+
<div class="liveMeta"><span class="liveActor">${esc2(event.actor)}</span>
|
|
1498
|
+
${role ? `<span class="liveRole">${esc2(role)}</span>` : ""}
|
|
1499
|
+
${route ? `<span>${esc2(route)}</span>` : ""}<span class="liveWhen">${esc2(time)}</span></div>
|
|
1500
|
+
<div class="liveHeading">${icono(event.kind)}<h3>${esc2(tituloVisible(event))}</h3></div>
|
|
1501
|
+
<p>${esc2(compact)}</p>${full}${details}
|
|
1027
1502
|
</article>
|
|
1028
1503
|
</li>`;
|
|
1029
1504
|
}
|
|
@@ -1051,12 +1526,12 @@ function pintaContexto(thread, container) {
|
|
|
1051
1526
|
const actors = ["seat", ...participants];
|
|
1052
1527
|
container.hidden = false;
|
|
1053
1528
|
container.innerHTML = `<div class="livePeople">${actors.map(
|
|
1054
|
-
(actor) => `<span class="livePerson${E?.avatars?.[actor] ? " conCara" : ""}" title="${
|
|
1529
|
+
(actor) => `<span class="livePerson${E?.avatars?.[actor] ? " conCara" : ""}" title="${esc2(
|
|
1055
1530
|
actor === "seat" ? "seat \xB7 chair" : `${actor} \xB7 ${rolOperativo(actor) || "member"}`
|
|
1056
1531
|
)}" style="--actor-hue:${tonoActor(actor)}">${cara(actor, actor === "seat")}</span>`
|
|
1057
|
-
).join("")}</div><span class="liveContextText"><b>seat moderates</b> \xB7 ${
|
|
1532
|
+
).join("")}</div><span class="liveContextText"><b>seat moderates</b> \xB7 ${esc2(
|
|
1058
1533
|
received
|
|
1059
|
-
)}/${
|
|
1534
|
+
)}/${esc2(total)} positions \xB7 ${esc2(status)}</span>`;
|
|
1060
1535
|
}
|
|
1061
1536
|
function esPromptDeTransporte(event) {
|
|
1062
1537
|
if (event.kind !== "conversation.user") return false;
|
|
@@ -1073,9 +1548,9 @@ function rolOperativo(actor) {
|
|
|
1073
1548
|
function cara(actor, esChair) {
|
|
1074
1549
|
const uri = E?.avatars?.[actor];
|
|
1075
1550
|
if (uri && uri.startsWith("data:image/svg+xml;base64,")) {
|
|
1076
|
-
return `<img class="liveCara" src="${
|
|
1551
|
+
return `<img class="liveCara" src="${esc2(uri)}" alt="">`;
|
|
1077
1552
|
}
|
|
1078
|
-
return esChair ? icono("chair") :
|
|
1553
|
+
return esChair ? icono("chair") : esc2(iniciales(actor));
|
|
1079
1554
|
}
|
|
1080
1555
|
function iniciales(actor) {
|
|
1081
1556
|
const parts = actor.split(/[-_.\s]+/).filter(Boolean);
|
|
@@ -1132,12 +1607,12 @@ function icono(kind) {
|
|
|
1132
1607
|
function rail() {
|
|
1133
1608
|
const cuantos = (E.agents ?? []).length;
|
|
1134
1609
|
q("#railCiudad").innerHTML = `<span class="railEtiqueta">${t("you are in")}</span>
|
|
1135
|
-
<b>${
|
|
1136
|
-
<span class="railDe">${
|
|
1610
|
+
<b>${esc2(E.city_name)}</b>
|
|
1611
|
+
<span class="railDe">${esc2(E.domain)} \xB7 ${plural(cuantos, "{n} house", "{n} houses")}</span>`;
|
|
1137
1612
|
q("#rail").innerHTML = SECCIONES.filter(([id]) => id !== "ciudades").map(([id, et]) => {
|
|
1138
|
-
const n = id === "gente" ? String(Object.keys(E.skills).length) : id === "committee" ? String(E.deliberations.length) : id === "red" ? String(E.roads.length) : id === "barrios" ? String(E.parcelas.length) : "";
|
|
1613
|
+
const n = id === "demos" ? "" : id === "gente" ? String(Object.keys(E.skills).length) : id === "committee" ? String(E.deliberations.length) : id === "red" ? String(E.roads.length) : id === "barrios" ? String(E.parcelas.length) : "";
|
|
1139
1614
|
return `<li class="${id === SECCION ? "aqui" : ""}" data-s="${id}">
|
|
1140
|
-
<span>${
|
|
1615
|
+
<span>${esc2(t(et))}</span>${n ? `<span class="n">${n}</span>` : ""}</li>`;
|
|
1141
1616
|
}).join("");
|
|
1142
1617
|
q("#railOtras").innerHTML = `<li class="${SECCION === "ciudades" ? "aqui" : ""}" data-s="ciudades">
|
|
1143
1618
|
<span>${t("All cities")}</span><span class="n">${E.ciudades.length}</span></li>`;
|
|
@@ -1153,8 +1628,8 @@ function rail() {
|
|
|
1153
1628
|
if (E.ciudades.length > 1) {
|
|
1154
1629
|
cs.innerHTML = `<select id="cambiaCiudad" title="which city this hall manages">
|
|
1155
1630
|
${E.ciudades.map(
|
|
1156
|
-
(c) => `<option value="${
|
|
1157
|
-
${
|
|
1631
|
+
(c) => `<option value="${esc2(c.ruta)}" ${c.actual ? "selected" : ""}>
|
|
1632
|
+
${esc2(c.nombre)}</option>`
|
|
1158
1633
|
).join("")}</select>`;
|
|
1159
1634
|
q("#cambiaCiudad").onchange = (ev) => {
|
|
1160
1635
|
const ruta = ev.target.value;
|
|
@@ -1166,7 +1641,24 @@ function rail() {
|
|
|
1166
1641
|
const nueva = q("#alWizard");
|
|
1167
1642
|
nueva.onclick = async (ev) => {
|
|
1168
1643
|
ev.preventDefault();
|
|
1169
|
-
const
|
|
1644
|
+
const dicho = await pregunta({
|
|
1645
|
+
titulo: t("Start another city"),
|
|
1646
|
+
cuerpo: [
|
|
1647
|
+
t(
|
|
1648
|
+
"A city is a place with its own seat, its own houses and its own map. Yours stay where they are."
|
|
1649
|
+
)
|
|
1650
|
+
],
|
|
1651
|
+
campos: [
|
|
1652
|
+
{
|
|
1653
|
+
id: "name",
|
|
1654
|
+
etiqueta: t("What is it called?"),
|
|
1655
|
+
pista: t("home, clients, the lab \u2014 whatever you would say out loud"),
|
|
1656
|
+
requerido: true
|
|
1657
|
+
}
|
|
1658
|
+
],
|
|
1659
|
+
aceptar: t("Create it")
|
|
1660
|
+
});
|
|
1661
|
+
const name = dicho?.name;
|
|
1170
1662
|
if (!name?.trim()) return;
|
|
1171
1663
|
const r = await api("/api/ciudades", {
|
|
1172
1664
|
method: "POST",
|
|
@@ -1177,6 +1669,7 @@ function rail() {
|
|
|
1177
1669
|
};
|
|
1178
1670
|
}
|
|
1179
1671
|
function pinta() {
|
|
1672
|
+
if (SECCION !== "demos") demos?.para();
|
|
1180
1673
|
rail();
|
|
1181
1674
|
q(".cuerpo").style.padding = "";
|
|
1182
1675
|
q("#lienzo").style.maxWidth = "";
|
|
@@ -1189,7 +1682,7 @@ VISTAS.bienvenida = () => {
|
|
|
1189
1682
|
document.body.classList.add("enGuia");
|
|
1190
1683
|
guia ??= new Bienvenida({
|
|
1191
1684
|
api,
|
|
1192
|
-
esc,
|
|
1685
|
+
esc: esc2,
|
|
1193
1686
|
aviso: toast,
|
|
1194
1687
|
refresca,
|
|
1195
1688
|
vete: (seccion) => {
|
|
@@ -1207,13 +1700,13 @@ VISTAS.ciudades = () => {
|
|
|
1207
1700
|
const filas = E.ciudades.map(
|
|
1208
1701
|
(c) => `
|
|
1209
1702
|
<div class="fila ${c.actual ? "on" : ""}">
|
|
1210
|
-
<span class="et">${
|
|
1703
|
+
<span class="et">${esc2(c.nombre)}${c.actual ? ' <b class="de">\xB7 open now</b>' : ""}</span>
|
|
1211
1704
|
<span class="de">${c.agentes ?? 0} house${(c.agentes ?? 0) === 1 ? "" : "s"} of its own</span>
|
|
1212
|
-
<span class="de mono">${
|
|
1705
|
+
<span class="de mono">${esc2(corto(c.ruta))}</span>
|
|
1213
1706
|
<span style="margin-left:auto;display:flex;gap:6px">
|
|
1214
1707
|
${c.actual ? "" : `<a class="bt mini" href="?city=${encodeURIComponent(c.ruta)}">open</a>
|
|
1215
|
-
<button class="bt mini archiva" data-ciudad="${
|
|
1216
|
-
data-nombre="${
|
|
1708
|
+
<button class="bt mini archiva" data-ciudad="${esc2(c.ruta)}"
|
|
1709
|
+
data-nombre="${esc2(c.nombre)}">archive</button>`}
|
|
1217
1710
|
</span>
|
|
1218
1711
|
</div>`
|
|
1219
1712
|
).join("");
|
|
@@ -1233,7 +1726,7 @@ VISTAS.ciudades = () => {
|
|
|
1233
1726
|
</div>
|
|
1234
1727
|
<div class="peligro">
|
|
1235
1728
|
<h3>${t("Start this city over")}</h3>
|
|
1236
|
-
<p class="prosa">Takes <b>${
|
|
1729
|
+
<p class="prosa">Takes <b>${esc2(E.city_name)}</b> back to its first day: no seat, no
|
|
1237
1730
|
agents, no committee history, no map. Your repositories and document folders are
|
|
1238
1731
|
<b>never touched</b> \u2014 only the city that points at them.</p>
|
|
1239
1732
|
<button class="bt malo" id="reiniciaCiudad">${t("Start over\u2026")}</button>
|
|
@@ -1273,23 +1766,23 @@ VISTAS.ciudades = () => {
|
|
|
1273
1766
|
return;
|
|
1274
1767
|
}
|
|
1275
1768
|
const p = previa.preview;
|
|
1276
|
-
const
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1769
|
+
const dicho = await pregunta({
|
|
1770
|
+
titulo: t("Start over {city}?", { city: p.city }),
|
|
1771
|
+
cuerpo: [
|
|
1772
|
+
t("You lose:") + " " + p.loses.join(", ") + ".",
|
|
1773
|
+
t("Right now that is {agents} house(s), {roads} road(s) and {acts} committee act(s).", {
|
|
1774
|
+
agents: p.agents,
|
|
1775
|
+
roads: p.roads,
|
|
1776
|
+
acts: p.deliberations
|
|
1777
|
+
}),
|
|
1778
|
+
t("You keep:") + " " + p.keeps.join(", ") + "."
|
|
1779
|
+
],
|
|
1780
|
+
campos: [{ id: "nombre", etiqueta: t("Type the city\u2019s name to confirm"), requerido: true }],
|
|
1781
|
+
exige: p.city,
|
|
1782
|
+
aceptar: t("Start over"),
|
|
1783
|
+
peligro: true
|
|
1784
|
+
});
|
|
1785
|
+
if (dicho === null) return;
|
|
1293
1786
|
const r = await api(
|
|
1294
1787
|
"/api/ciudad-reinicia",
|
|
1295
1788
|
{ method: "POST", body: JSON.stringify({ confirm: p.city }) }
|
|
@@ -1305,8 +1798,12 @@ Type the city's name to confirm:`;
|
|
|
1305
1798
|
todos(".archiva").forEach((boton) => {
|
|
1306
1799
|
boton.onclick = async () => {
|
|
1307
1800
|
const nombre = boton.dataset.nombre ?? "";
|
|
1308
|
-
|
|
1309
|
-
|
|
1801
|
+
const vale = await confirma(
|
|
1802
|
+
t("Archive {city}?", { city: nombre }),
|
|
1803
|
+
[t("It moves into .backups. Nothing is deleted, and you can put it back by hand.")],
|
|
1804
|
+
{ aceptar: t("Archive it") }
|
|
1805
|
+
);
|
|
1806
|
+
if (!vale) return;
|
|
1310
1807
|
const r = await api(
|
|
1311
1808
|
"/api/ciudad-archiva",
|
|
1312
1809
|
{ method: "POST", body: JSON.stringify({ city: boton.dataset.ciudad ?? "" }) }
|
|
@@ -1320,6 +1817,12 @@ Type the city's name to confirm:`;
|
|
|
1320
1817
|
};
|
|
1321
1818
|
});
|
|
1322
1819
|
};
|
|
1820
|
+
var demos = null;
|
|
1821
|
+
VISTAS.demos = () => {
|
|
1822
|
+
if (!demos) demos = new Demos({ api, esc: esc2, pinta: renderActividad });
|
|
1823
|
+
q("#lienzo").innerHTML = '<div id="demoHueco"></div>';
|
|
1824
|
+
demos.monta(q("#demoHueco"));
|
|
1825
|
+
};
|
|
1323
1826
|
VISTAS.resumen = () => {
|
|
1324
1827
|
const mia = E.tarjetas.find((t2) => t2.user === E.yo);
|
|
1325
1828
|
const sinUnidad = E.parcelas.filter((p) => p.unidad === "none" || p.unidad === "mine").length;
|
|
@@ -1327,12 +1830,12 @@ VISTAS.resumen = () => {
|
|
|
1327
1830
|
const tareas = [
|
|
1328
1831
|
{
|
|
1329
1832
|
ok: !!mia,
|
|
1330
|
-
txt: mia ? `Your seat is taken \u2014 you are <b>${
|
|
1833
|
+
txt: mia ? `Your seat is taken \u2014 you are <b>${esc2(mia.agent)}</b> in <b>${esc2(E.domain)}</b>` : "Take your seat: the work domain and your role in it",
|
|
1331
1834
|
ir: "puesto"
|
|
1332
1835
|
},
|
|
1333
1836
|
{
|
|
1334
1837
|
ok: agentes.length > 0,
|
|
1335
|
-
txt: agentes.length === 0 ? "Build the <b>houses</b> of this city \u2014 one per worker, each whole: its kind, its role, and everything it works on" : `<b>${agentes.length}</b> house${agentes.length === 1 ? "" : "s"} in this city: ${
|
|
1838
|
+
txt: agentes.length === 0 ? "Build the <b>houses</b> of this city \u2014 one per worker, each whole: its kind, its role, and everything it works on" : `<b>${agentes.length}</b> house${agentes.length === 1 ? "" : "s"} in this city: ${esc2(
|
|
1336
1839
|
agentes.map((a) => a.name).join(", ")
|
|
1337
1840
|
)}`,
|
|
1338
1841
|
ir: "gente"
|
|
@@ -1344,7 +1847,7 @@ VISTAS.resumen = () => {
|
|
|
1344
1847
|
},
|
|
1345
1848
|
{
|
|
1346
1849
|
ok: !!(mia && mia.goals_defined),
|
|
1347
|
-
txt: mia && mia.goals_defined ? `Your goal: <b>${
|
|
1850
|
+
txt: mia && mia.goals_defined ? `Your goal: <b>${esc2(mia.objetivo ? mia.objetivo.title : "")}</b>` : "Set <b>one goal</b>, with the command that measures it",
|
|
1348
1851
|
ir: "puesto"
|
|
1349
1852
|
},
|
|
1350
1853
|
{
|
|
@@ -1357,15 +1860,15 @@ VISTAS.resumen = () => {
|
|
|
1357
1860
|
const sesionArriba = E.tmux.includes(E.sesion);
|
|
1358
1861
|
q("#lienzo").innerHTML = `
|
|
1359
1862
|
<div><span class="sub">${empezando ? "welcome" : "your city"}</span>
|
|
1360
|
-
<h1 style="margin-top:6px">${empezando ? "Let\u2019s build " +
|
|
1361
|
-
<p class="de mono" style="margin-top:5px">${
|
|
1863
|
+
<h1 style="margin-top:6px">${empezando ? "Let\u2019s build " + esc2(E.city_name) : esc2(E.city_name)}</h1>
|
|
1864
|
+
<p class="de mono" style="margin-top:5px">${esc2(E.address)}</p>
|
|
1362
1865
|
${empezando ? `<p class="prosa" style="margin-top:8px">A city is a chair and the agents behind
|
|
1363
1866
|
it. Two steps and it is alive: take your seat \u2014 the work domain and your role in
|
|
1364
1867
|
it \u2014 then add the agents, each one asked for whole: what kind of work it does,
|
|
1365
1868
|
its role, everything it works on, the engine that runs it and the skills it
|
|
1366
1869
|
starts with. Everything below is a plain file in
|
|
1367
|
-
<code class="mono">${
|
|
1368
|
-
counted with <code class="mono">${
|
|
1870
|
+
<code class="mono">${esc2(corto(E.datos))}</code> you can also edit by hand.</p>` : `<p class="prosa" style="margin-top:8px">Domain: <b>${esc2(E.domain)}</b>. Growth is
|
|
1871
|
+
counted with <code class="mono">${esc2(E.grow || "nothing yet")}</code>.</p>`}</div>
|
|
1369
1872
|
<div class="cifras">
|
|
1370
1873
|
<div class="cifra"><b>${Object.keys(E.skills).length}</b><span>repo agents</span></div>
|
|
1371
1874
|
<div class="cifra"><b>${E.parcelas.length}</b><span>houses</span></div>
|
|
@@ -1389,11 +1892,11 @@ VISTAS.resumen = () => {
|
|
|
1389
1892
|
<div><span class="sub">work</span>
|
|
1390
1893
|
<div style="display:flex;flex-direction:column;gap:9px;margin-top:9px">
|
|
1391
1894
|
<div class="orden"><span class="et2">your day</span>
|
|
1392
|
-
<code>${sesionArriba ? "tmux attach -t " +
|
|
1393
|
-
${sesionArriba ? `<button class="bt mini" data-copia="tmux attach -t ${
|
|
1895
|
+
<code>${sesionArriba ? "tmux attach -t " + esc2(E.sesion) : "one window per folder, an agent in each"}</code>
|
|
1896
|
+
${sesionArriba ? `<button class="bt mini" data-copia="tmux attach -t ${esc2(E.sesion)}">copy</button>` : '<button class="bt mini ppal" id="abreSesion">open my session</button>'}
|
|
1394
1897
|
</div>
|
|
1395
1898
|
<div class="orden"><span class="et2">the map</span>
|
|
1396
|
-
<code>${E.mapa ?
|
|
1899
|
+
<code>${E.mapa ? esc2(E.mapa) : "not drawn yet"}</code>
|
|
1397
1900
|
<button class="bt mini ${E.mapa ? "" : "ppal"}" data-ir="mapa">${E.mapa ? "open" : "draw it"}</button></div>
|
|
1398
1901
|
</div></div>`;
|
|
1399
1902
|
enlaza();
|
|
@@ -1421,7 +1924,7 @@ VISTAS.mapa = () => {
|
|
|
1421
1924
|
const mapUrl = new URL(E.mapa, location.href);
|
|
1422
1925
|
mapUrl.searchParams.set("embed", "1");
|
|
1423
1926
|
mapUrl.searchParams.set("parent_origin", location.origin);
|
|
1424
|
-
q("#lienzo").innerHTML = `<iframe id="cityMapFrame" src="${
|
|
1927
|
+
q("#lienzo").innerHTML = `<iframe id="cityMapFrame" src="${esc2(mapUrl.toString())}" title="the map"
|
|
1425
1928
|
allow="fullscreen" allowfullscreen
|
|
1426
1929
|
style="width:100%;height:calc(100vh - 2px);border:0;display:block"></iframe>`;
|
|
1427
1930
|
const frame = q("#cityMapFrame");
|
|
@@ -1484,11 +1987,11 @@ VISTAS.puesto = () => {
|
|
|
1484
1987
|
async function puesto() {
|
|
1485
1988
|
const mia = E.tarjetas.find((t2) => t2.user === E.yo);
|
|
1486
1989
|
q("#lienzo").innerHTML = `<div><span class="sub">my seat</span>
|
|
1487
|
-
<h1 style="margin-top:6px">${
|
|
1990
|
+
<h1 style="margin-top:6px">${esc2(E.yo)}</h1>
|
|
1488
1991
|
<p class="prosa" style="margin-top:8px">This is your chair: the work domain, your
|
|
1489
1992
|
role inside it, and one goal. The seat stays the boss even when its
|
|
1490
1993
|
professional role is blank. Saving writes your card \u2014
|
|
1491
|
-
<code class="mono">${
|
|
1994
|
+
<code class="mono">${esc2(corto(E.datos))}/${esc2(E.yo)}.md</code> \u2014 the same file
|
|
1492
1995
|
every other door writes, and it never touches your roster.</p></div>
|
|
1493
1996
|
<div><span class="sub">work domain</span><div class="rolejilla" id="domains" style="margin-top:9px">
|
|
1494
1997
|
<p class="cargando">reading the domain packs</p></div></div>
|
|
@@ -1511,9 +2014,9 @@ async function puesto() {
|
|
|
1511
2014
|
const pintaRoles = () => {
|
|
1512
2015
|
q("#roles").innerHTML = rolesR.map(
|
|
1513
2016
|
(r) => `
|
|
1514
|
-
<button class="rol ${r.id === rol ? "on" : ""}" data-r="${
|
|
1515
|
-
<h3>${
|
|
1516
|
-
<p>${
|
|
2017
|
+
<button class="rol ${r.id === rol ? "on" : ""}" data-r="${esc2(r.id)}">
|
|
2018
|
+
<h3>${esc2(r.name)}</h3><span class="de">${esc2(r.id)} \xB7 ${esc2(r.trade.toLowerCase())}</span>
|
|
2019
|
+
<p>${esc2(r.summary)}</p></button>`
|
|
1517
2020
|
).join("");
|
|
1518
2021
|
todos("#roles .rol").forEach((b) => {
|
|
1519
2022
|
b.onclick = () => {
|
|
@@ -1530,9 +2033,9 @@ async function puesto() {
|
|
|
1530
2033
|
const pintaDominios = () => {
|
|
1531
2034
|
q("#domains").innerHTML = domainsR.map(
|
|
1532
2035
|
(d) => `
|
|
1533
|
-
<button class="rol ${d.id === domain ? "on" : ""}" data-d="${
|
|
1534
|
-
<h3>${
|
|
1535
|
-
<p>${
|
|
2036
|
+
<button class="rol ${d.id === domain ? "on" : ""}" data-d="${esc2(d.id)}">
|
|
2037
|
+
<h3>${esc2(d.name)}</h3><span class="de">${esc2(d.id)}</span>
|
|
2038
|
+
<p>${esc2(d.summary)}</p></button>`
|
|
1536
2039
|
).join("");
|
|
1537
2040
|
todos("#domains .rol").forEach((b) => {
|
|
1538
2041
|
b.onclick = () => {
|
|
@@ -1547,28 +2050,28 @@ async function puesto() {
|
|
|
1547
2050
|
const o = mia && mia.objetivo || {};
|
|
1548
2051
|
q("#meta").innerHTML = `
|
|
1549
2052
|
<div class="campo"><label>The goal, in one line</label>
|
|
1550
|
-
<input type="text" id="g_title" value="${
|
|
2053
|
+
<input type="text" id="g_title" value="${esc2(o.title)}" placeholder="Concrete enough to argue with \u2014 empty skips it">
|
|
1551
2054
|
</div>
|
|
1552
2055
|
<div class="dos">
|
|
1553
2056
|
<div class="campo"><label>How it is measured</label>
|
|
1554
|
-
<input type="text" id="g_signal" value="${
|
|
2057
|
+
<input type="text" id="g_signal" value="${esc2(o.signal)}"></div>
|
|
1555
2058
|
<div class="campo"><label>The command that returns it, if a command can</label>
|
|
1556
|
-
<input type="text" class="mono" id="g_command" value="${
|
|
2059
|
+
<input type="text" class="mono" id="g_command" value="${esc2(o.command)}"
|
|
1557
2060
|
placeholder="empty for a qualitative goal"></div>
|
|
1558
2061
|
</div>
|
|
1559
2062
|
<div class="campo"><label>\u2026or who judges it, and how often</label>
|
|
1560
|
-
<input type="text" id="g_manual" value="${
|
|
2063
|
+
<input type="text" id="g_manual" value="${esc2(o.manual)}"
|
|
1561
2064
|
placeholder="in prose: the architect reads the AGENTS.md files on Fridays">
|
|
1562
2065
|
<p class="pista">A goal judged by a person is a real goal \u2014 plenty of quality
|
|
1563
2066
|
is prose, not a number. Used when the command is empty.</p></div>
|
|
1564
2067
|
<div class="dos">
|
|
1565
2068
|
<div class="campo"><label>What it returns today</label>
|
|
1566
|
-
<input type="text" class="mono" id="g_baseline" value="${
|
|
2069
|
+
<input type="text" class="mono" id="g_baseline" value="${esc2(o.baseline)}"></div>
|
|
1567
2070
|
<div class="campo"><label>Where it has to get to</label>
|
|
1568
|
-
<input type="text" class="mono" id="g_target" value="${
|
|
2071
|
+
<input type="text" class="mono" id="g_target" value="${esc2(o.target)}"></div>
|
|
1569
2072
|
</div>
|
|
1570
2073
|
<div class="campo" style="max-width:240px"><label>By when</label>
|
|
1571
|
-
<input type="text" id="g_by" value="${
|
|
2074
|
+
<input type="text" id="g_by" value="${esc2(o.by ?? "this quarter")}"></div>`;
|
|
1572
2075
|
const val = (id) => q(id).value.trim();
|
|
1573
2076
|
q("#guardaPuesto").onclick = async () => {
|
|
1574
2077
|
if (!rol) {
|
|
@@ -1636,35 +2139,35 @@ VISTAS.barrios = () => {
|
|
|
1636
2139
|
q("#unidades").innerHTML = unidades.map(
|
|
1637
2140
|
(u, i) => `
|
|
1638
2141
|
<div class="tr">
|
|
1639
|
-
<input type="text" value="${
|
|
1640
|
-
<span class="id">${
|
|
2142
|
+
<input type="text" value="${esc2(u.name)}" data-i="${i}" style="flex:1">
|
|
2143
|
+
<span class="id">${esc2(u.id)}</span>
|
|
1641
2144
|
<span class="pips" data-i="${i}">${E.paleta.map(
|
|
1642
2145
|
(p) => `
|
|
1643
2146
|
<button class="pip ${p.hex === u.color ? "on" : ""}" data-c="${p.hex}"
|
|
1644
|
-
style="background:#${p.hex}" title="${
|
|
2147
|
+
style="background:#${p.hex}" title="${esc2(p.nombre)}"></button>`
|
|
1645
2148
|
).join("")}</span>
|
|
1646
2149
|
<button class="x" data-x="${i}">\xD7</button></div>`
|
|
1647
2150
|
).join("") || '<div class="tr" style="color:var(--tinta3);font-size:12.5px">No districts yet \u2014 everything sits in \u201Cno unit\u201D.</div>';
|
|
1648
2151
|
q("#parcelas").innerHTML = Object.keys(porRepo).sort().map(
|
|
1649
2152
|
(repo) => `
|
|
1650
|
-
<div class="repoBloque" data-repo="${
|
|
1651
|
-
<div class="repoCab"><span>${
|
|
1652
|
-
<button class="bt mini" data-split="${
|
|
1653
|
-
<label class="lab"><input type="checkbox" data-lab="${
|
|
2153
|
+
<div class="repoBloque" data-repo="${esc2(repo)}">
|
|
2154
|
+
<div class="repoCab"><span>${esc2(repo)}</span>
|
|
2155
|
+
<button class="bt mini" data-split="${esc2(repo)}">split</button>
|
|
2156
|
+
<label class="lab"><input type="checkbox" data-lab="${esc2(repo)}"
|
|
1654
2157
|
${lab.has(repo) ? "checked" : ""}> lab</label></div>
|
|
1655
2158
|
${(porRepo[repo] ?? []).map(
|
|
1656
2159
|
(f, j) => `
|
|
1657
2160
|
<div class="parcela">
|
|
1658
|
-
<input type="text" value="${
|
|
1659
|
-
<input type="text" class="mono" value="${
|
|
2161
|
+
<input type="text" value="${esc2(f.nombre)}" data-k="nombre" data-r="${esc2(repo)}" data-j="${j}" title="name on the map">
|
|
2162
|
+
<input type="text" class="mono" value="${esc2(f.ruta)}" data-k="ruta" data-r="${esc2(repo)}" data-j="${j}"
|
|
1660
2163
|
placeholder="glob inside the repo \u2014 empty = the whole repo">
|
|
1661
|
-
<select data-k="unidad" data-r="${
|
|
2164
|
+
<select data-k="unidad" data-r="${esc2(repo)}" data-j="${j}">
|
|
1662
2165
|
${[...unidades.map((u) => u.id), "none"].map(
|
|
1663
2166
|
(id) => `
|
|
1664
|
-
<option value="${
|
|
2167
|
+
<option value="${esc2(id)}" ${(idsValidos.has(f.unidad) ? f.unidad : "none") === id ? "selected" : ""}>${esc2(id)}</option>`
|
|
1665
2168
|
).join("")}
|
|
1666
2169
|
</select>
|
|
1667
|
-
${(porRepo[repo] ?? []).length > 1 ? `<button class="x" data-quita="${
|
|
2170
|
+
${(porRepo[repo] ?? []).length > 1 ? `<button class="x" data-quita="${esc2(repo)}" data-j="${j}">\xD7</button>` : "<span></span>"}
|
|
1668
2171
|
</div>`
|
|
1669
2172
|
).join("")}
|
|
1670
2173
|
</div>`
|
|
@@ -1764,20 +2267,20 @@ VISTAS.red = () => {
|
|
|
1764
2267
|
this machine or continue over the remote bus; the city sees one explicit
|
|
1765
2268
|
connection either way.</p></div>
|
|
1766
2269
|
<div class="gente">${E.roads.map(
|
|
1767
|
-
(r) => `<div class="persona"><h3>${
|
|
1768
|
-
<span class="ag">${
|
|
2270
|
+
(r) => `<div class="persona"><h3>${esc2(r.name)}</h3>
|
|
2271
|
+
<span class="ag">${esc2(r.address)}</span>
|
|
1769
2272
|
<span class="rp">${r.local ? "local road" : "remote road"}</span>
|
|
1770
|
-
<button class="bt mini" data-road-close="${
|
|
2273
|
+
<button class="bt mini" data-road-close="${esc2(r.id)}">close</button></div>`
|
|
1771
2274
|
).join("") || '<p class="prosa">No roads yet. This city is isolated on purpose.</p>'}</div>
|
|
1772
2275
|
<div><span class="sub">other cities on this machine</span>
|
|
1773
2276
|
<div class="gente" style="margin-top:9px">${locales.map(
|
|
1774
|
-
(c) => `<div class="persona"><h3>${
|
|
1775
|
-
<span class="ag">${
|
|
1776
|
-
<button class="bt mini ppal" data-road-open="${
|
|
2277
|
+
(c) => `<div class="persona"><h3>${esc2(c.nombre)}</h3>
|
|
2278
|
+
<span class="ag">${esc2(c.slug ?? c.ruta)}</span>
|
|
2279
|
+
<button class="bt mini ppal" data-road-open="${esc2(c.id)}">open road</button></div>`
|
|
1777
2280
|
).join("") || '<p class="prosa">No unconnected local cities.</p>'}</div></div>
|
|
1778
2281
|
<div class="orden"><span class="et2">remote invitation \xB7 public, no token</span>
|
|
1779
|
-
<code>${
|
|
1780
|
-
<button class="bt mini" data-copia="${
|
|
2282
|
+
<code>${esc2(invitation)}</code>
|
|
2283
|
+
<button class="bt mini" data-copia="${esc2(invitation)}">copy</button>
|
|
1781
2284
|
</div>`;
|
|
1782
2285
|
enlaza();
|
|
1783
2286
|
todos("[data-road-open]").forEach((b) => {
|
|
@@ -1816,23 +2319,23 @@ VISTAS.committee = () => {
|
|
|
1816
2319
|
</div>
|
|
1817
2320
|
<div class="gente">${E.deliberations.map(
|
|
1818
2321
|
(act) => `<div class="persona">
|
|
1819
|
-
<h3>${
|
|
1820
|
-
<span class="ag">${
|
|
2322
|
+
<h3>${esc2(act.question)}</h3>
|
|
2323
|
+
<span class="ag">${esc2(act.status)} \xB7 revision ${act.revision} \xB7
|
|
1821
2324
|
${act.received}/${act.total} positions</span>
|
|
1822
|
-
<span class="rp">${
|
|
1823
|
-
<span class="de">${
|
|
1824
|
-
${act.contributors.length ? ` \xB7 decisive ${
|
|
1825
|
-
${act.verifier ? ` \xB7 verifier ${
|
|
1826
|
-
<code class="mono">${
|
|
2325
|
+
<span class="rp">${esc2(act.decision || act.desired_outcome)}</span>
|
|
2326
|
+
<span class="de">${esc2(act.participants.join(", ") || "no participants")}
|
|
2327
|
+
${act.contributors.length ? ` \xB7 decisive ${esc2(act.contributors.join(", "))}` : ""}
|
|
2328
|
+
${act.verifier ? ` \xB7 verifier ${esc2(act.verifier)} ${esc2(act.verification)}` : ""}</span>
|
|
2329
|
+
<code class="mono">${esc2(corto(act.act))}</code>
|
|
1827
2330
|
</div>`
|
|
1828
2331
|
).join("") || '<p class="prosa">No committee acts yet. Open one only when the seat needs specialised evidence.</p>'}</div>`;
|
|
1829
2332
|
enlaza();
|
|
1830
2333
|
};
|
|
1831
2334
|
function opciones2(valores, actual) {
|
|
1832
|
-
return opciones(valores, actual,
|
|
2335
|
+
return opciones(valores, actual, esc2);
|
|
1833
2336
|
}
|
|
1834
2337
|
function caraRPG(uri) {
|
|
1835
|
-
return uri?.startsWith("data:image/svg+xml;base64,") ? `<img class="rpgCara" src="${
|
|
2338
|
+
return uri?.startsWith("data:image/svg+xml;base64,") ? `<img class="rpgCara" src="${esc2(uri)}" alt="">` : "";
|
|
1836
2339
|
}
|
|
1837
2340
|
function barra(ancho, clase = "") {
|
|
1838
2341
|
const pct = Math.round(Math.max(0, Math.min(1, ancho)) * 100);
|
|
@@ -1855,69 +2358,69 @@ VISTAS.gente = () => {
|
|
|
1855
2358
|
return `
|
|
1856
2359
|
<div class="fichaRPG">
|
|
1857
2360
|
<div class="rpgCab">${caraRPG(a.avatar)}
|
|
1858
|
-
<div><h3>${
|
|
1859
|
-
<span class="rpgTags"><span class="kindChip ${
|
|
1860
|
-
<span class="rpgRol">${
|
|
2361
|
+
<div><h3>${esc2(a.name)}</h3>
|
|
2362
|
+
<span class="rpgTags"><span class="kindChip ${esc2(a.kind)}">${esc2(a.kind)}</span>
|
|
2363
|
+
<span class="rpgRol">${esc2(a.role)}</span>
|
|
1861
2364
|
<span class="rpgHogar">${a.legacy ? "repo" : `workspace \xB7 ${montajes.length} ${montajes.length === 1 ? "mount" : "mounts"}`}</span></span></div>
|
|
1862
2365
|
<span class="rpgBotones">
|
|
1863
|
-
<button class="rpgMini rpgDado" type="button" data-agente="${
|
|
2366
|
+
<button class="rpgMini rpgDado" type="button" data-agente="${esc2(a.slug)}"
|
|
1864
2367
|
title="Reroll this agent's face \u2014 deterministic, persisted on the card">\u{1F3B2}</button>
|
|
1865
|
-
<button class="rpgMini rpgIns" type="button" data-agente="${
|
|
2368
|
+
<button class="rpgMini rpgIns" type="button" data-agente="${esc2(a.slug)}"
|
|
1866
2369
|
data-file="CLAUDE.md" title="Instructions the Claude runtime reads">CLAUDE.md</button>
|
|
1867
|
-
<button class="rpgMini rpgIns" type="button" data-agente="${
|
|
2370
|
+
<button class="rpgMini rpgIns" type="button" data-agente="${esc2(a.slug)}"
|
|
1868
2371
|
data-file="AGENTS.md" title="Instructions Codex, OpenCode and Kimi read">AGENTS.md</button>
|
|
1869
2372
|
</span>
|
|
1870
2373
|
</div>
|
|
1871
2374
|
<div class="rpgFila"><label>${t("engine")}</label>
|
|
1872
2375
|
${barra(motor.ancho, motor.defecto ? "defecto" : "")}
|
|
1873
|
-
<select class="rpgSel" data-agente="${
|
|
2376
|
+
<select class="rpgSel" data-agente="${esc2(a.slug)}" data-campo="model"
|
|
1874
2377
|
${esClaude ? "" : 'disabled title="This window runs its own CLI; its model lives in its own flags"'}>
|
|
1875
2378
|
${opcionesModelo}</select></div>
|
|
1876
2379
|
<div class="rpgFila"><label>${t("effort")}</label>
|
|
1877
2380
|
${barra(esfuerzo / 5, esfuerzo ? "" : "defecto")}
|
|
1878
|
-
<select class="rpgSel" data-agente="${
|
|
2381
|
+
<select class="rpgSel" data-agente="${esc2(a.slug)}" data-campo="effort"
|
|
1879
2382
|
${esClaude ? "" : 'disabled title="This window runs its own CLI; its effort lives in its own flags"'}>
|
|
1880
2383
|
${opcionesEsfuerzo}</select></div>
|
|
1881
2384
|
<div class="rpgFila"><label>${t("provider")}</label>
|
|
1882
|
-
<span class="rpgDato"><span class="cliDot ${a.cli.connected ? "on" : a.cli.installed ? "idle" : "off"}" title="${
|
|
2385
|
+
<span class="rpgDato"><span class="cliDot ${a.cli.connected ? "on" : a.cli.installed ? "idle" : "off"}" title="${esc2(
|
|
1883
2386
|
a.cli.connected ? "connected: this agent\u2019s window is alive right now" : a.cli.installed ? "installed on this machine, not in use by this agent" : `${a.cli.binary} is not installed`
|
|
1884
|
-
)}"></span>${
|
|
2387
|
+
)}"></span>${esc2(
|
|
1885
2388
|
t(a.cli.connected ? "connected" : a.cli.installed ? "idle" : "missing")
|
|
1886
|
-
)} \xB7 ${
|
|
1887
|
-
<button class="rpgMini rpgTest" type="button" data-agente="${
|
|
2389
|
+
)} \xB7 ${esc2(a.cli.binary)}
|
|
2390
|
+
<button class="rpgMini rpgTest" type="button" data-agente="${esc2(a.slug)}"
|
|
1888
2391
|
title="Run the engine for real: --version, and the login state on Claude">test</button></span>
|
|
1889
|
-
<select class="rpgSel" data-agente="${
|
|
2392
|
+
<select class="rpgSel" data-agente="${esc2(a.slug)}" data-campo="runtime">
|
|
1890
2393
|
${opcionesRuntime}</select></div>
|
|
1891
2394
|
<div class="rpgSkills"><label>${t("works on")} \xB7 ${montajes.length}</label>
|
|
1892
2395
|
${montajes.length ? montajes.map(
|
|
1893
|
-
(m) => `<code class="mono" title="${
|
|
1894
|
-
data-agente="${
|
|
1895
|
-
title="Stop this agent working on ${
|
|
2396
|
+
(m) => `<code class="mono" title="${esc2(m.target)}">${esc2(m.label)}${m.fixed ? "" : `<button class="rpgQuitar rpgDesmonta" type="button"
|
|
2397
|
+
data-agente="${esc2(a.slug)}" data-mount="${esc2(m.label)}"
|
|
2398
|
+
title="Stop this agent working on ${esc2(m.target)}">\xD7</button>`}</code>`
|
|
1896
2399
|
).join(" ") : '<span class="rpgDato">nothing mounted yet</span>'}
|
|
1897
|
-
${a.legacy ? '<span class="rpgDato">a legacy repo agent works on its own repo</span>' : `<button class="rpgMini rpgMonta" type="button" data-agente="${
|
|
2400
|
+
${a.legacy ? '<span class="rpgDato">a legacy repo agent works on its own repo</span>' : `<button class="rpgMini rpgMonta" type="button" data-agente="${esc2(a.slug)}"
|
|
1898
2401
|
title="A repo, a worktree or a folder of documents \u2014 this agent works on all of them">+ folder</button>`}</div>
|
|
1899
2402
|
<div class="rpgFila"><label>${t("growth")}</label>
|
|
1900
2403
|
${barra(crecido)}
|
|
1901
2404
|
<span class="rpgDato">${g.floors | 0} floors \xB7 ${g.bricks | 0} bricks \xB7 ${g.activity30 | 0}/30d</span></div>
|
|
1902
2405
|
<div class="rpgSkills"><label>${t("skills")} \xB7 ${skills.length}</label>
|
|
1903
2406
|
${skills.length ? skills.map(
|
|
1904
|
-
(s) => `<code class="mono" title="${
|
|
1905
|
-
data-skill="${
|
|
2407
|
+
(s) => `<code class="mono" title="${esc2(s.description)}">${esc2(s.name)}${s.removable && s.dir ? `<button class="rpgQuitar" type="button" data-agente="${esc2(a.slug)}"
|
|
2408
|
+
data-skill="${esc2(s.dir)}" title="Remove this skill from the agent's home">\xD7</button>` : ""}</code>`
|
|
1906
2409
|
).join(" ") : '<span class="rpgDato">none discovered</span>'}
|
|
1907
2410
|
<label class="rpgMini rpgSubir"
|
|
1908
2411
|
title="Install a skill zip into this agent's own home \u2014 the Claude runtime reads skills; other engines ignore them">
|
|
1909
|
-
+ zip<input type="file" accept=".zip" data-agente="${
|
|
2412
|
+
+ zip<input type="file" accept=".zip" data-agente="${esc2(a.slug)}"></label></div>
|
|
1910
2413
|
</div>`;
|
|
1911
2414
|
};
|
|
1912
|
-
q("#lienzo").innerHTML = `<div><span class="sub">the houses of ${
|
|
1913
|
-
<h1 style="margin-top:6px">${t("Who lives in {city}", { city:
|
|
2415
|
+
q("#lienzo").innerHTML = `<div><span class="sub">the houses of ${esc2(E.city_name)}</span>
|
|
2416
|
+
<h1 style="margin-top:6px">${t("Who lives in {city}", { city: esc2(E.city_name) })}</h1>
|
|
1914
2417
|
<p class="prosa" style="margin-top:8px"><b>A house is where an agent lives and works</b>,
|
|
1915
2418
|
and many houses are a city \u2014 it is the same thing the map draws, growing with what that
|
|
1916
2419
|
agent actually does. The card and the CLI call them <code class="mono">agents</code>;
|
|
1917
2420
|
here you see their houses.</p>
|
|
1918
2421
|
<p class="prosa">They belong to <b>this</b> city and only
|
|
1919
2422
|
to it: each one's workspace and its mounts live inside
|
|
1920
|
-
<code class="mono">${
|
|
2423
|
+
<code class="mono">${esc2(corto(E.datos))}/agents/</code>, so another city has its own
|
|
1921
2424
|
people even if you give them the same names. Every agent is whole here: the kind of work
|
|
1922
2425
|
it does, its role, everything it works on \u2014 any number of repositories and document
|
|
1923
2426
|
folders at once \u2014 the engine and effort that run it, and the skills in its own home.</p>
|
|
@@ -1930,10 +2433,10 @@ VISTAS.gente = () => {
|
|
|
1930
2433
|
<div class="rpgCab">${caraRPG(E.avatars?.["seat"])}
|
|
1931
2434
|
<div><h3>seat</h3>
|
|
1932
2435
|
<span class="rpgTags"><span class="kindChip coordinator">chair</span>
|
|
1933
|
-
<span class="rpgRol">${
|
|
2436
|
+
<span class="rpgRol">${esc2(mia?.role ?? "not configured")}</span></span></div>
|
|
1934
2437
|
</div>
|
|
1935
|
-
<div class="rpgFila"><span class="rpgDato">${
|
|
1936
|
-
<div class="rpgFila"><span class="rpgDato">${
|
|
2438
|
+
<div class="rpgFila"><span class="rpgDato">${esc2(E.address)}</span></div>
|
|
2439
|
+
<div class="rpgFila"><span class="rpgDato">${esc2(mia?.objetivo?.title ?? "No goal yet")}</span></div>
|
|
1937
2440
|
</div>
|
|
1938
2441
|
${agentes.map(fichaDe).join("")}
|
|
1939
2442
|
</div>`;
|
|
@@ -1960,7 +2463,17 @@ VISTAS.gente = () => {
|
|
|
1960
2463
|
sel.onchange = async () => {
|
|
1961
2464
|
let valor = sel.value;
|
|
1962
2465
|
if (valor === "__otro__") {
|
|
1963
|
-
const
|
|
2466
|
+
const dicho = await pregunta({
|
|
2467
|
+
titulo: t("Which engine?"),
|
|
2468
|
+
cuerpo: [
|
|
2469
|
+
t("An alias the CLI resolves when the window opens. Anything it accepts works here.")
|
|
2470
|
+
],
|
|
2471
|
+
campos: [
|
|
2472
|
+
{ id: "alias", etiqueta: t("Model alias"), pista: "claude-opus-5", requerido: true }
|
|
2473
|
+
],
|
|
2474
|
+
aceptar: t("Use it")
|
|
2475
|
+
});
|
|
2476
|
+
const propio = dicho?.alias ?? null;
|
|
1964
2477
|
if (propio === null) {
|
|
1965
2478
|
pinta();
|
|
1966
2479
|
return;
|
|
@@ -2006,7 +2519,12 @@ VISTAS.gente = () => {
|
|
|
2006
2519
|
todos(".rpgQuitar").forEach((boton) => {
|
|
2007
2520
|
boton.onclick = async () => {
|
|
2008
2521
|
const nombre = boton.dataset.skill ?? "";
|
|
2009
|
-
|
|
2522
|
+
const vale = await confirma(
|
|
2523
|
+
t("Remove the skill {skill}?", { skill: nombre }),
|
|
2524
|
+
[t("It is deleted from this agent\u2019s own home. Nothing outside that folder is touched.")],
|
|
2525
|
+
{ aceptar: t("Remove it"), peligro: true }
|
|
2526
|
+
);
|
|
2527
|
+
if (!vale) return;
|
|
2010
2528
|
const r = await api("/api/skill", {
|
|
2011
2529
|
method: "POST",
|
|
2012
2530
|
body: JSON.stringify({ agent: boton.dataset.agente ?? "", remove: nombre })
|
|
@@ -2022,26 +2540,34 @@ VISTAS.gente = () => {
|
|
|
2022
2540
|
const alta = document.getElementById("altaAgente");
|
|
2023
2541
|
if (alta)
|
|
2024
2542
|
alta.onclick = async () => {
|
|
2025
|
-
const
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2543
|
+
const forma = new FormularioDeCasa({ api, esc: esc2, aviso: toast, yo: E.yo });
|
|
2544
|
+
let hecho = null;
|
|
2545
|
+
await pregunta({
|
|
2546
|
+
titulo: t("Build a house"),
|
|
2547
|
+
cuerpo: [
|
|
2548
|
+
t(
|
|
2549
|
+
"One worker, its own window, its own corner of your disk. Everything here can be changed afterwards."
|
|
2550
|
+
)
|
|
2551
|
+
],
|
|
2552
|
+
contenido: '<div id="altaCasa"></div>',
|
|
2553
|
+
aceptar: t("Build it"),
|
|
2554
|
+
ancho: 640,
|
|
2555
|
+
enlaza: (raiz, cierra) => {
|
|
2556
|
+
const hueco = raiz.querySelector("#altaCasa");
|
|
2557
|
+
if (hueco) forma.monta(hueco);
|
|
2558
|
+
const boton = raiz.querySelector('[data-dlg="si"]');
|
|
2559
|
+
if (!boton) return;
|
|
2560
|
+
boton.onclick = async () => {
|
|
2561
|
+
boton.disabled = true;
|
|
2562
|
+
hecho = await forma.guarda();
|
|
2563
|
+
boton.disabled = false;
|
|
2564
|
+
if (hecho) cierra({});
|
|
2565
|
+
};
|
|
2039
2566
|
}
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
}
|
|
2043
|
-
|
|
2044
|
-
}
|
|
2567
|
+
});
|
|
2568
|
+
if (!hecho) return;
|
|
2569
|
+
toast(t("{name} has a house now", { name: hecho.nombre }));
|
|
2570
|
+
void refresca();
|
|
2045
2571
|
};
|
|
2046
2572
|
const montaje = async (carga, hecho) => {
|
|
2047
2573
|
try {
|
|
@@ -2060,16 +2586,46 @@ VISTAS.gente = () => {
|
|
|
2060
2586
|
}
|
|
2061
2587
|
};
|
|
2062
2588
|
todos(".rpgMonta").forEach((boton) => {
|
|
2063
|
-
boton.onclick = () => {
|
|
2064
|
-
const
|
|
2065
|
-
|
|
2066
|
-
|
|
2589
|
+
boton.onclick = async () => {
|
|
2590
|
+
const elegidas = [];
|
|
2591
|
+
const exp = new Explorador(
|
|
2592
|
+
{ api, esc: esc2 },
|
|
2593
|
+
(ruta) => {
|
|
2594
|
+
const ya = elegidas.indexOf(ruta);
|
|
2595
|
+
if (ya >= 0) elegidas.splice(ya, 1);
|
|
2596
|
+
else elegidas.push(ruta);
|
|
2597
|
+
},
|
|
2598
|
+
() => elegidas
|
|
2599
|
+
);
|
|
2600
|
+
const dicho = await pregunta({
|
|
2601
|
+
titulo: t("What else does it work on?"),
|
|
2602
|
+
cuerpo: [
|
|
2603
|
+
t(
|
|
2604
|
+
"A repository, a worktree, a folder of documents, one exact file. It is linked, never copied."
|
|
2605
|
+
)
|
|
2606
|
+
],
|
|
2607
|
+
contenido: '<div id="montaExp"></div>',
|
|
2608
|
+
aceptar: t("Mount it"),
|
|
2609
|
+
ancho: 620,
|
|
2610
|
+
enlaza: (raiz) => {
|
|
2611
|
+
const hueco = raiz.querySelector("#montaExp");
|
|
2612
|
+
if (hueco) exp.monta(hueco);
|
|
2613
|
+
}
|
|
2614
|
+
});
|
|
2615
|
+
if (dicho === null || !elegidas.length) return;
|
|
2616
|
+
for (const ruta of elegidas)
|
|
2617
|
+
await montaje({ agent: boton.dataset.agente ?? "", add: ruta }, t("Mounted"));
|
|
2067
2618
|
};
|
|
2068
2619
|
});
|
|
2069
2620
|
todos(".rpgDesmonta").forEach((boton) => {
|
|
2070
|
-
boton.onclick = () => {
|
|
2621
|
+
boton.onclick = async () => {
|
|
2071
2622
|
const etiqueta = boton.dataset.mount ?? "";
|
|
2072
|
-
|
|
2623
|
+
const vale = await confirma(
|
|
2624
|
+
t("Stop this agent working on {what}?", { what: etiqueta }),
|
|
2625
|
+
[t("The link goes. The folder it points at stays exactly where it is.")],
|
|
2626
|
+
{ aceptar: t("Unmount it") }
|
|
2627
|
+
);
|
|
2628
|
+
if (!vale) return;
|
|
2073
2629
|
void montaje(
|
|
2074
2630
|
{ agent: boton.dataset.agente ?? "", remove: etiqueta },
|
|
2075
2631
|
`${etiqueta} unmounted \u2014 the folder itself is untouched`
|