agents-city 0.4.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.es.md +240 -11
  3. package/README.md +238 -11
  4. package/benchmarks/latency/fake-claude-cli.mjs +2 -1
  5. package/benchmarks/reception/README.md +16 -0
  6. package/benchmarks/reception/run.mjs +84 -0
  7. package/bin/agents-city.js +1 -0
  8. package/bin/connect +5 -0
  9. package/bin/hall.html +117 -18
  10. package/bin/navegador.mjs +62 -1
  11. package/bin/serve.py +106 -97
  12. package/bin/test +14 -3
  13. package/bin/test-arnes.py +192 -0
  14. package/bin/test-cage.py +9 -0
  15. package/bin/test-channel.py +178 -12
  16. package/bin/test-claude-runtime.py +132 -4
  17. package/bin/test-connect-client.mjs +609 -0
  18. package/bin/test-connect.py +52 -0
  19. package/bin/test-contracts.py +214 -0
  20. package/bin/test-doctor.py +31 -0
  21. package/bin/test-i18n.py +189 -0
  22. package/bin/test-navegador.py +52 -0
  23. package/bin/test-seat.py +78 -20
  24. package/bin/test-security.py +2 -0
  25. package/bin/test-serve.py +292 -0
  26. package/city/web/dist-hall/hall.js +733 -209
  27. package/city/web/src/bienvenida.ts +14 -13
  28. package/city/web/src/casa.ts +98 -56
  29. package/city/web/src/demo.ts +61 -31
  30. package/city/web/src/es.ts +187 -27
  31. package/city/web/src/explorador.ts +70 -43
  32. package/city/web/src/hall.ts +468 -98
  33. package/city/web/src/idioma.ts +21 -1
  34. package/city/web/src/motores.ts +48 -1
  35. package/city/web/src/vista.ts +58 -0
  36. package/demo/graba.py +5 -10
  37. package/docs/managed-connect.md +237 -0
  38. package/docs/security.md +41 -2
  39. package/package.json +8 -4
  40. package/plugin/.claude-plugin/plugin.json +1 -1
  41. package/plugin/channel/adapter-prompts.ts +10 -0
  42. package/plugin/channel/adapter.js +78 -31
  43. package/plugin/channel/agents_city_hybrid_crypto_bg.wasm +0 -0
  44. package/plugin/channel/bus.js +109 -65
  45. package/plugin/channel/bus.ts +11 -1
  46. package/plugin/channel/client.js +67 -30
  47. package/plugin/channel/delivery-queue.ts +160 -21
  48. package/plugin/channel/hub/remote-roads.ts +32 -1
  49. package/plugin/channel/hub/road-controller.ts +116 -16
  50. package/plugin/channel/kinsh_vodozemac_wasm_bg.wasm +0 -0
  51. package/plugin/channel/licenses/CONNECT_CLIENT_THIRD_PARTY_NOTICES.md +21 -0
  52. package/plugin/channel/licenses/HYBRID_CRYPTO_THIRD_PARTY_NOTICES.md +12 -0
  53. package/plugin/channel/licenses/hybrid-crypto-Apache-2.0.txt +201 -0
  54. package/plugin/channel/licenses/keyring-MIT.txt +21 -0
  55. package/plugin/channel/licenses/vodozemac-Apache-2.0.txt +201 -0
  56. package/plugin/channel/local-hub.js +2080 -97
  57. package/plugin/channel/local-hub.ts +5 -4
  58. package/plugin/channel/managed-connect/bridge.ts +208 -0
  59. package/plugin/channel/managed-connect/cli.ts +416 -0
  60. package/plugin/channel/managed-connect/device.ts +15 -0
  61. package/plugin/channel/managed-connect/local-cities.ts +94 -0
  62. package/plugin/channel/managed-connect/person-message.ts +74 -0
  63. package/plugin/channel/managed-connect/reception-bridge.ts +341 -0
  64. package/plugin/channel/managed-connect/relay-session.ts +7 -0
  65. package/plugin/channel/managed-connect/storage.ts +653 -0
  66. package/plugin/channel/managed-connect/transport.ts +87 -0
  67. package/plugin/channel/managed-connect-cli.js +4777 -0
  68. package/plugin/channel/managed-connect-cli.ts +7 -0
  69. package/plugin/channel/managed-connect-client.d.ts +261 -0
  70. package/plugin/channel/managed-connect-client.js +6572 -0
  71. package/plugin/channel/managed-connect-client.manifest.json +38 -0
  72. package/plugin/channel/package-lock.json +123 -105
  73. package/plugin/channel/package.json +4 -4
  74. package/plugin/channel/protocol.ts +4 -0
  75. package/plugin/channel/reception.ts +896 -0
  76. package/plugin/channel/road-cli.ts +1 -1
  77. package/plugin/channel/runtime/arnes.json +189 -0
  78. package/plugin/channel/runtime/arnes.ts +47 -0
  79. package/plugin/channel/runtime/claude.ts +18 -0
  80. package/plugin/channel/runtime/codex-config.ts +51 -1
  81. package/plugin/channel/runtime/codex.ts +31 -11
  82. package/plugin/channel/runtime/kimi.ts +6 -4
  83. package/plugin/channel/runtime-files.ts +39 -5
  84. package/plugin/channel/runtime-gateway.js +426 -100
  85. package/plugin/channel/runtime-gateway.ts +12 -0
  86. package/plugin/channel/trust/agents-city-sandbox-roots.json +66 -0
  87. package/plugin/scripts/arnes.py +271 -0
  88. package/plugin/scripts/busca.py +46 -10
  89. package/plugin/scripts/cage.py +5 -0
  90. package/plugin/scripts/city-session.sh +144 -28
  91. package/plugin/scripts/crecimiento.py +4 -1
  92. package/plugin/scripts/demos.py +58 -43
  93. package/plugin/scripts/desinstala.py +36 -21
  94. package/plugin/scripts/doctor.py +37 -15
  95. package/plugin/scripts/read-card.py +48 -8
  96. package/plugin/scripts/reception.py +665 -0
@@ -1,7 +1,15 @@
1
1
  // city/web/src/idioma.ts
2
2
  var ES = {};
3
+ var CLAVES = /* @__PURE__ */ new Map();
3
4
  function clave(fuente) {
4
- return fuente.replace(/\s+/g, " ").trim();
5
+ const hecha = CLAVES.get(fuente);
6
+ if (hecha !== void 0) return hecha;
7
+ const nueva = _clave(fuente);
8
+ CLAVES.set(fuente, nueva);
9
+ return nueva;
10
+ }
11
+ function _clave(fuente) {
12
+ return fuente.replace(/[\u2018\u2019]/g, "'").replace(/\s+/g, " ").trim();
5
13
  }
6
14
  function anota(pares) {
7
15
  for (const [fuente, texto] of Object.entries(pares)) ES[clave(fuente)] = texto;
@@ -49,6 +57,7 @@ anota({
49
57
  "My seat": "Mi asiento",
50
58
  Districts: "Distritos",
51
59
  Roads: "Carreteras",
60
+ Reception: "Recepci\xF3n",
52
61
  Committee: "Comit\xE9",
53
62
  Houses: "Casas",
54
63
  "All cities": "Todas las ciudades",
@@ -56,6 +65,69 @@ anota({
56
65
  "{n} house": "{n} casa",
57
66
  "{n} houses": "{n} casas",
58
67
  "Create another city \u2192": "Crear otra ciudad \u2192",
68
+ // ── reception ────────────────────────────────────────────────────────────
69
+ reception: "recepci\xF3n",
70
+ "Messages wait for you, not your agents": "Los mensajes te esperan a ti, no a tus agentes",
71
+ "Remote text stops here as inert text. Read it, reject it with a reason, or choose the cities that should receive it. Until then no model can read it.": "El texto remoto se detiene aqu\xED como texto inerte. L\xE9elo, rech\xE1zalo con un motivo o elige qu\xE9 ciudades deben recibirlo. Hasta entonces ning\xFAn modelo puede leerlo.",
72
+ "routing mode": "modo de enrutado",
73
+ "Manual review": "Revisi\xF3n manual",
74
+ "Every message needs a person before it reaches a city.": "Cada mensaje necesita a una persona antes de llegar a una ciudad.",
75
+ "Auto router not configured": "Router autom\xE1tico sin configurar",
76
+ "Configure Auto": "Configurar Auto",
77
+ "Automatic routing": "Enrutado autom\xE1tico",
78
+ "Only one clear, low-risk rule match can leave the human queue automatically.": "Solo una coincidencia clara y de bajo riesgo puede salir autom\xE1ticamente de la cola humana.",
79
+ "Routing policy": "Pol\xEDtica de enrutado",
80
+ "Keep human review": "Mantener la revisi\xF3n humana",
81
+ "Use the local rule router": "Usar el router local de reglas",
82
+ "Write comma-separated words or phrases for each destination. Empty cities are never selected.": "Escribe palabras o frases separadas por comas para cada destino. Las ciudades vac\xEDas nunca se eligen.",
83
+ "e.g. contract, privacy, legal review": "p. ej. contrato, privacidad, revisi\xF3n legal",
84
+ "Suspicious, unmatched, or ambiguous text always waits for you. Auto never executes, answers, or opens links.": "El texto sospechoso, sin coincidencia o ambiguo siempre te espera. Auto nunca ejecuta, responde ni abre enlaces.",
85
+ "Save routing policy": "Guardar pol\xEDtica de enrutado",
86
+ "Could not save the routing policy": "No se pudo guardar la pol\xEDtica de enrutado",
87
+ "Routing policy saved.": "Pol\xEDtica de enrutado guardada.",
88
+ "Reading your reception\u2026": "Leyendo tu recepci\xF3n\u2026",
89
+ "Could not read your reception": "No se ha podido leer tu recepci\xF3n",
90
+ "Reception clear": "Recepci\xF3n despejada",
91
+ "No remote message is waiting for a decision.": "No hay mensajes remotos esperando una decisi\xF3n.",
92
+ "message waiting": "{n} mensaje en espera",
93
+ "messages waiting": "{n} mensajes en espera",
94
+ "local only": "solo local",
95
+ "No agent has read this": "Ning\xFAn agente ha le\xEDdo esto",
96
+ "Send to": "Dirigir a",
97
+ "Route to selected cities": "Dirigir a las ciudades elegidas",
98
+ "Reject and reply with a reason": "Rechazar y responder con un motivo",
99
+ "Reason sent back securely": "Motivo que se devolver\xE1 cifrado",
100
+ "Reason for rejecting this message": "Motivo para rechazar este mensaje",
101
+ "Reject and send reason": "Rechazar y enviar el motivo",
102
+ "Could not route the message": "No se pudo dirigir el mensaje",
103
+ "Message routed. Only the selected cities can now read it.": "Mensaje dirigido. Solo las ciudades elegidas pueden leerlo ahora.",
104
+ "Could not reject the message": "No se pudo rechazar el mensaje",
105
+ "Message rejected. Your reason is queued for encrypted delivery.": "Mensaje rechazado. Tu motivo ha quedado en cola para enviarse cifrado.",
106
+ "New encrypted message": "Nuevo mensaje cifrado",
107
+ "Messages leave from this computer with end-to-end encryption.": "Los mensajes salen de este ordenador con cifrado de extremo a extremo.",
108
+ "message queued": "{n} mensaje en cola",
109
+ "messages queued": "{n} mensajes en cola",
110
+ "Write to": "Escribir a",
111
+ Message: "Mensaje",
112
+ "Your message will stop in their private reception.": "Tu mensaje se detendr\xE1 en su recepci\xF3n privada.",
113
+ "Send securely": "Enviar de forma segura",
114
+ "Reply to a rejected message": "Respuesta a un mensaje rechazado",
115
+ Dismiss: "Descartar",
116
+ "Could not queue the message": "No se pudo poner el mensaje en cola",
117
+ "Message queued on this computer.": "Mensaje puesto en cola en este ordenador.",
118
+ "Could not dismiss the response": "No se pudo descartar la respuesta",
119
+ // ── live activity rail ──────────────────────────────────────────────────
120
+ "City live": "Ciudad en directo",
121
+ waiting: "esperando",
122
+ "A moderated conversation over the WebSocket bus. The seat routes each turn; private model reasoning is never shown or stored.": "Una conversaci\xF3n moderada sobre el bus WebSocket. El asiento dirige cada turno; el razonamiento privado de los modelos no se muestra ni se guarda.",
123
+ "all conversations": "todas las conversaciones",
124
+ "show work": "mostrar trabajo",
125
+ "hide work": "ocultar trabajo",
126
+ "websocket live": "websocket activo",
127
+ reconnecting: "reconectando",
128
+ "session offline": "sesi\xF3n desconectada",
129
+ "The bus is live. Questions, positions and moderated replies will appear here.": "El bus est\xE1 activo. Aqu\xED aparecer\xE1n preguntas, posiciones y respuestas moderadas.",
130
+ "Start the city session. Its visible conversation will appear here as it happens.": "Inicia la sesi\xF3n de la ciudad. Su conversaci\xF3n visible aparecer\xE1 aqu\xED mientras sucede.",
59
131
  // ── the guide ─────────────────────────────────────────────────────────────
60
132
  Welcome: "Bienvenida",
61
133
  "The work": "El trabajo",
@@ -95,7 +167,6 @@ anota({
95
167
  "Set a goal": "Poner un objetivo",
96
168
  "{city} is alive": "{city} est\xE1 viva",
97
169
  // ── the houses ────────────────────────────────────────────────────────────
98
- "the houses of {city}": "las casas de {city}",
99
170
  "Who lives in {city}": "Qui\xE9n vive en {city}",
100
171
  "+ Build a house": "+ Construir una casa",
101
172
  "+ folder": "+ carpeta",
@@ -122,8 +193,6 @@ anota({
122
193
  open: "abrir",
123
194
  archive: "archivar",
124
195
  "\xB7 open now": "\xB7 abierta ahora",
125
- "{n} house of its own": "{n} casa propia",
126
- "{n} houses of its own": "{n} casas propias",
127
196
  "Start this city over": "Empezar esta ciudad de cero",
128
197
  "Start over\u2026": "Empezar de cero\u2026",
129
198
  // ── the map ───────────────────────────────────────────────────────────────
@@ -143,20 +212,8 @@ anota({
143
212
  "town hall": "ayuntamiento",
144
213
  "Manage one autonomous city: its seat, repo agents, roads and map. Everything here is a plain file you can also inspect by hand.": "Gobierna una ciudad aut\xF3noma: su asiento, sus agentes, sus carreteras y su mapa. Todo lo que hay aqu\xED es un fichero de texto que tambi\xE9n puedes abrir a mano.",
145
214
  // ── looking through the disk ──────────────────────────────────────────────
146
- "search your disk \u2014 a name, or part of a path": "busca en tu disco \u2014 un nombre, o parte de una ruta",
147
- "Look through the disk again \u2014 for something cloned a minute ago": "Vuelve a mirar el disco \u2014 por si acabas de clonar algo",
148
- "Search again": "Buscar otra vez",
149
- "searching\u2026": "buscando\u2026",
150
- "looking through your disk\u2026": "mirando en tu disco\u2026",
151
- "{shown} of {total} places match": "{shown} de {total} sitios encajan",
152
- "{total} places found \u2014 type to narrow it down": "{total} sitios encontrados \u2014 escribe para acotar",
153
- "Nothing matches that. Add it by path below.": "Nada encaja con eso. A\xF1\xE1delo por ruta aqu\xED abajo.",
154
- "Nothing found on this disk \u2014 add folders by path below.": "No hay nada en este disco \u2014 a\xF1ade carpetas por ruta aqu\xED abajo.",
155
- "\u2026or a path, and Enter": "\u2026o una ruta, y Enter",
156
- // ── what runs an agent ────────────────────────────────────────────────────
157
215
  "Its role \u2014 its speciality, never authority": "Su rol \u2014 su especialidad, nunca su autoridad",
158
- "What runs it?": "\xBFQu\xE9 lo mueve?",
159
- "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.": "D\xE9jalos los tres por defecto y funcionar\xE1 como t\xFA. El modelo y el esfuerzo los lee Claude; otra CLI lleva sus propias opciones."
216
+ "What runs it?": "\xBFQu\xE9 lo mueve?"
160
217
  });
161
218
  anota({
162
219
  // ── the guide, in prose ───────────────────────────────────────────────────
@@ -166,8 +223,6 @@ anota({
166
223
  "It decides the vocabulary, the roles you will be offered and what counts as evidence in a decision. A clinic does not ship pull requests, and a law firm does not measure story points. Pick the closest one \u2014 you can change it later.": "Decide el vocabulario, los roles que se te ofrecen y qu\xE9 cuenta como prueba en una decisi\xF3n. Una cl\xEDnica no entrega pull requests, y un despacho no mide story points. Elige el m\xE1s cercano \u2014 luego se cambia.",
167
224
  "You chair this city whatever you answer \u2014 this is your <b>speciality</b>, not your authority. It shapes the perspective you bring to a decision and the knowledge files the city writes for you. <b>Blank</b> is a real answer: it means no preset knowledge.": "Presides esta ciudad respondas lo que respondas \u2014 esto es tu <b>especialidad</b>, no tu autoridad. Da forma a la perspectiva con la que entras en una decisi\xF3n y a los ficheros de conocimiento que la ciudad escribe para ti. <b>En blanco</b> es una respuesta de verdad: significa sin conocimiento previo.",
168
225
  "Every house holds one worker \u2014 an <b>agent</b> \u2014 with its own window, its own role and its own corner of your disk. A house is <b>not</b> a repository: one can answer for three services and a folder of documents at once, and a house whose work is documents needs no git anywhere.": "Cada casa aloja a un trabajador \u2014 un <b>agente</b> \u2014 con su ventana, su rol y su rinc\xF3n de tu disco. Una casa <b>no</b> es un repositorio: una sola puede responder por tres servicios y una carpeta de documentos a la vez, y una casa cuyo trabajo son documentos no necesita git en ninguna parte.",
169
- "Everything found on this disk: <b>repositories</b>, the <b>worktrees</b> an isolated agent works in, and <b>folders of documents</b> with no git at all. Pick as many as you like \u2014 this is the part that makes it an agent and not a repo.": "Todo lo que hay en este disco: <b>repositorios</b>, los <b>worktrees</b> en los que trabaja un agente aislado, y <b>carpetas de documentos</b> sin git ninguno. Elige las que quieras \u2014 esta es la parte que lo convierte en un agente y no en un repo.",
170
- "Working on: {que}": "Trabaja sobre: {que}",
171
226
  "Your seat is written and {cuantas}. Everything you just answered is a plain file you can read and edit.": "Tu asiento queda escrito y {cuantas}. Todo lo que acabas de responder es un fichero de texto que puedes leer y editar.",
172
227
  "<b>{n}</b> house stands around it": "a su alrededor hay <b>{n}</b> casa",
173
228
  "<b>{n}</b> houses stand around it": "a su alrededor hay <b>{n}</b> casas",
@@ -207,7 +262,6 @@ anota({
207
262
  "{name} has a house now": "{name} ya tiene casa",
208
263
  "{name} joined the city": "{name} se ha mudado a la ciudad",
209
264
  "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
265
  "Mount it": "Montarlo",
212
266
  Mounted: "Montado",
213
267
  "Stop this agent working on {what}?": "\xBFQue este agente deje de trabajar sobre {what}?",
@@ -254,11 +308,120 @@ anota({
254
308
  "Nothing recorded here": "Aqu\xED no hay nada grabado",
255
309
  "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
310
  });
311
+ anota({
312
+ "{n} folder": "{n} carpeta",
313
+ "{n} folders": "{n} carpetas",
314
+ "{n} file": "{n} fichero",
315
+ "{n} files": "{n} ficheros",
316
+ "files are below the folders": "los ficheros van debajo de las carpetas"
317
+ });
318
+ anota({
319
+ // ── what runs a house ─────────────────────────────────────────────────────
320
+ "Leave all three on default and it runs the way you do. Whatever you set here is written once on the card, and the launcher hands it to whichever CLI runs this house.": "D\xE9jalos los tres por defecto y funcionar\xE1 como t\xFA. Lo que pongas aqu\xED se escribe una sola vez en la ficha, y el lanzador se lo pasa a la CLI que mueva esta casa.",
321
+ "This CLI has no effort setting.": "Esta CLI no tiene ajuste de esfuerzo.",
322
+ "An alias the Claude CLI resolves when the window opens.": "Un alias que la CLI de Claude resuelve al abrir la ventana.",
323
+ "The model name your Codex uses \u2014 the one in ~/.codex/config.toml.": "El nombre del modelo que usa tu Codex \u2014 el de ~/.codex/config.toml.",
324
+ "OpenCode names a model provider/model, like anthropic/claude-sonnet-4.": "OpenCode nombra los modelos proveedor/modelo, como anthropic/claude-sonnet-4.",
325
+ "The model name your Kimi CLI uses.": "El nombre del modelo que usa tu CLI de Kimi."
326
+ });
327
+ anota({
328
+ // ── the seat, the goal, the day ───────────────────────────────────────────
329
+ "my seat": "mi asiento",
330
+ "Save my seat": "Guardar mi asiento",
331
+ "work domain": "dominio de trabajo",
332
+ "your day": "tu d\xEDa",
333
+ "the agents": "los agentes",
334
+ "repo agents": "agentes de repo",
335
+ "one goal \u2014 optional": "un objetivo \u2014 opcional",
336
+ "The goal, in one line": "El objetivo, en una l\xEDnea",
337
+ "Concrete enough to argue with \u2014 empty skips it": "Lo bastante concreto como para discutirlo \u2014 vac\xEDo lo salta",
338
+ "How it is measured": "C\xF3mo se mide",
339
+ "in prose: the architect reads the AGENTS.md files on Fridays": "en prosa: el arquitecto lee los AGENTS.md los viernes",
340
+ "The command that returns it, if a command can": "El comando que lo devuelve, si un comando puede",
341
+ "What it returns today": "Lo que devuelve hoy",
342
+ "Where it has to get to": "A d\xF3nde tiene que llegar",
343
+ "By when": "Para cu\xE1ndo",
344
+ "empty for a qualitative goal": "vac\xEDo para un objetivo cualitativo",
345
+ "\u2026or who judges it, and how often": "\u2026o qui\xE9n lo juzga, y cada cu\xE1nto",
346
+ "what is left": "lo que queda",
347
+ "open my session": "abrir mi sesi\xF3n",
348
+ "Open the roster": "Abrir la lista",
349
+ // ── districts, roads, the committee ───────────────────────────────────────
350
+ "districts & houses": "distritos y casas",
351
+ "Save districts & houses": "Guardar distritos y casas",
352
+ "another district\u2026": "otro distrito\u2026",
353
+ "The modelling no tool can do for you": "El modelado que ninguna herramienta puede hacer por ti",
354
+ "A house is <b>not a repo</b> \u2014 it is a parcel, a slice of one serving a single business unit. Split the interesting repos, give every house its district, and the map can say \u201Cthis change touches banking\u201D instead of \u201Cthis change touches src/lib\u201D.": "Una casa <b>no es un repo</b> \u2014 es una parcela, una porci\xF3n de uno al servicio de una sola unidad de negocio. Parte los repos interesantes, dale a cada casa su distrito, y el mapa podr\xE1 decir \xABeste cambio toca banca\xBB en vez de \xABeste cambio toca src/lib\xBB.",
355
+ "Cities this one may reach": "Ciudades a las que \xE9sta puede llegar",
356
+ "No roads yet. This city is isolated on purpose.": "A\xFAn no hay carreteras. Esta ciudad est\xE1 aislada a prop\xF3sito.",
357
+ "No unconnected local cities.": "No hay ciudades locales sin conectar.",
358
+ "other cities on this machine": "otras ciudades en esta m\xE1quina",
359
+ "remote invitation \xB7 public, no token": "invitaci\xF3n remota \xB7 p\xFAblica, sin token",
360
+ "committee acts": "actas del comit\xE9",
361
+ "Decisions with a visible chain of custody": "Decisiones con una cadena de custodia visible",
362
+ "No committee acts yet. Open one only when the seat needs specialised evidence.": "A\xFAn no hay actas. Abre un comit\xE9 s\xF3lo cuando el asiento necesite pruebas especializadas.",
363
+ "agents-city committee schema open": "agents-city committee schema open",
364
+ "guided committee": "comit\xE9 guiado",
365
+ "Play the guided committee from the top": "Reproducir el comit\xE9 guiado desde el principio",
366
+ "Pause or resume mid-scene": "Pausar o seguir a mitad de escena",
367
+ "\u27F3 replay": "\u27F3 repetir",
368
+ "\u23F8 pause": "\u23F8 pausa",
369
+ "seat moderates": "el asiento modera",
370
+ "read full message": "leer el mensaje entero",
371
+ // ── the cities view ───────────────────────────────────────────────────────
372
+ 'One person, several autonomous cities: each has its own identity, domain, chair, <b>its own houses</b> and its own roads. A house is not shared \u2014 it stands inside the city that owns it, with its workspace and mounts under that city\u2019s folder \u2014 so two cities can each have a <code class="mono">docs</code> house and they are two different workers. They share nothing unless you build a road between them.': 'Una persona, varias ciudades aut\xF3nomas: cada una con su identidad, su dominio, su silla, <b>sus propias casas</b> y sus carreteras. Una casa no se comparte \u2014 est\xE1 dentro de la ciudad que la posee, con su espacio de trabajo y sus montajes bajo la carpeta de esa ciudad \u2014 as\xED que dos ciudades pueden tener cada una su casa <code class="mono">docs</code> y son dos trabajadores distintos. No comparten nada salvo que construyas una carretera entre ellas.',
373
+ "client-a, research, the book\u2026": "cliente-a, investigaci\xF3n, el libro\u2026",
374
+ "which city this hall manages": "qu\xE9 ciudad gobierna este ayuntamiento",
375
+ "Takes {city} back to its first day: no seat, no agents, no committee history, no map. Your repositories and document folders are <b>never touched</b> \u2014 only the city that points at them.": "Devuelve {city} a su primer d\xEDa: sin asiento, sin agentes, sin historial de comit\xE9, sin mapa. Tus repositorios y carpetas de documentos <b>no se tocan</b> \u2014 s\xF3lo la ciudad que apunta a ellos.",
376
+ // ── the houses ────────────────────────────────────────────────────────────
377
+ "Agents &amp; skills": "Agentes y skills",
378
+ '<b>A house is where an agent lives and works</b>, and many houses are a city \u2014 it is the same thing the map draws, growing with what that agent actually does. The card and the CLI call them <code class="mono">agents</code>; here you see their houses.': '<b>Una casa es donde vive y trabaja un agente</b>, y muchas casas son una ciudad \u2014 es lo mismo que dibuja el mapa, creciendo con lo que ese agente hace de verdad. La ficha y la CLI los llaman <code class="mono">agents</code>; aqu\xED ves sus casas.',
379
+ "They belong to <b>this</b> city and only to it: each one\u2019s workspace and its mounts live inside {donde}, so another city has its own people even if you give them the same names.": "Pertenecen a <b>esta</b> ciudad y s\xF3lo a ella: el espacio de trabajo de cada una y sus montajes viven dentro de {donde}, as\xED que otra ciudad tiene su propia gente aunque les pongas los mismos nombres.",
380
+ "a legacy repo agent works on its own repo": "un agente de repo heredado trabaja sobre su propio repo",
381
+ "data repo": "repo de datos",
382
+ "skills recognised": "skills reconocidas",
383
+ "plugin installed": "plugin instalado",
384
+ "Reroll this agent\u2019s face \u2014 deterministic, persisted on the card": "Vuelve a tirar la cara de este agente \u2014 determinista, guardada en la ficha",
385
+ "Instructions the Claude runtime reads": "Instrucciones que lee el runtime de Claude",
386
+ "Instructions Codex, OpenCode and Kimi read": "Instrucciones que leen Codex, OpenCode y Kimi",
387
+ "Run the engine for real: --version, and the login state on Claude": "Ejecuta el motor de verdad: --version, y el estado de sesi\xF3n en Claude",
388
+ "Install a skill zip into this agent\u2019s own home \u2014 the Claude runtime reads skills; other engines ignore them": "Instala un zip de skill en la casa de este agente \u2014 el runtime de Claude lee skills; los dem\xE1s motores las ignoran",
389
+ "Remove this skill from the agent\u2019s home": "Quitar esta skill de la casa del agente",
390
+ // ── while it is reading ───────────────────────────────────────────────────
391
+ "reading the domain packs": "leyendo los paquetes de dominio",
392
+ "reading the role files": "leyendo los ficheros de rol",
393
+ "an isometric house": "una casa isom\xE9trica"
394
+ });
395
+
396
+ // city/web/src/vista.ts
397
+ var Montada = class {
398
+ host = null;
399
+ monta(host) {
400
+ this.host = host;
401
+ this.dibuja();
402
+ }
403
+ /**
404
+ * Put the markup in and wire it. Final in spirit: `monta` calls THIS and not
405
+ * `repinta`, because a subclass whose repaint has to rebuild something
406
+ * nested — the house form remounts its folder picker — overrides `repinta`
407
+ * to call `monta`, and a base that repainted on mount would then call itself
408
+ * until the stack ran out. It did.
409
+ */
410
+ dibuja() {
411
+ if (!this.host) return;
412
+ this.host.innerHTML = this.html();
413
+ this.enlaza(this.host);
414
+ }
415
+ repinta() {
416
+ this.dibuja();
417
+ }
418
+ };
257
419
 
258
420
  // city/web/src/explorador.ts
259
421
  var MARCA = { repo: "git", worktree: "wt" };
260
- var Explorador = class {
422
+ var Explorador = class extends Montada {
261
423
  constructor(p, elige, yaElegidas) {
424
+ super();
262
425
  this.p = p;
263
426
  this.elige = elige;
264
427
  this.yaElegidas = yaElegidas;
@@ -267,25 +430,15 @@ var Explorador = class {
267
430
  listado = null;
268
431
  cargando = false;
269
432
  fallo = "";
270
- host = null;
271
- /** Render into `host` and wire it. Safe to call again: that is the repaint. */
272
433
  monta(host) {
273
- this.host = host;
434
+ super.monta(host);
274
435
  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
436
  }
284
437
  /** Walk to a folder. An unreadable one is said out loud, not swallowed. */
285
438
  async ve(ruta) {
286
439
  this.cargando = true;
287
440
  this.fallo = "";
288
- this.repinta();
441
+ if (!this.listado) this.repinta();
289
442
  try {
290
443
  const r = await this.p.api("/api/carpeta?path=" + encodeURIComponent(ruta));
291
444
  if (r.error) this.fallo = r.error;
@@ -300,10 +453,6 @@ var Explorador = class {
300
453
  this.repinta();
301
454
  }
302
455
  }
303
- /** The path being looked at right now — what "add this folder" means. */
304
- get carpeta() {
305
- return this.aqui;
306
- }
307
456
  migas() {
308
457
  const esc3 = this.p.esc;
309
458
  const l = this.listado;
@@ -322,14 +471,15 @@ var Explorador = class {
322
471
  const esc3 = this.p.esc;
323
472
  const l = this.listado;
324
473
  const elegidas = new Set(this.yaElegidas());
474
+ const yaEsta = t("Already chosen");
475
+ const anade = t("Add this");
325
476
  const filas = (l?.entradas ?? []).map((e) => {
326
477
  const puesta = elegidas.has(e.ruta);
327
478
  return `<div class="expFila ${e.dir ? "dir" : "fich"} ${puesta ? "puesta" : ""}">
328
479
  <button type="button" class="expNombre" ${e.dir ? `data-exp="ve" data-ruta="${esc3(e.ruta)}"` : "disabled"} title="${esc3(e.ruta)}">
329
480
  <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
481
  <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>
482
+ title="${esc3(puesta ? yaEsta : anade)}">${puesta ? "\u2713" : "+"}</button>
333
483
  </div>`;
334
484
  }).join("");
335
485
  const atajos = (l?.atajos ?? []).map(
@@ -344,34 +494,81 @@ var Explorador = class {
344
494
  title="${t("Up one folder")}">\u2191</button>
345
495
  <div class="expMigas">${this.migas()}</div>
346
496
  </div>
497
+ ${this.cuenta(l)}
347
498
  ${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
499
  ${l?.recortada ? `<p class="pista">${t("Only the first 2000 shown.")}</p>` : ""}
349
500
  <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>
501
+ data-ruta="${esc3(l?.ruta ?? "")}" ${l ? "" : "disabled"}>${t("Add this folder")}</button>
353
502
  </div>`;
354
503
  }
504
+ /**
505
+ * What is in this folder, said before you scroll.
506
+ *
507
+ * Folders sort first, as every file picker has done for forty years — which
508
+ * means in a folder with eight of them the files start below the fold, and a
509
+ * list that does not say they are there reads as a folder-only picker. This
510
+ * line is the difference between "there are no files here" and "keep going".
511
+ */
512
+ cuenta(l) {
513
+ if (!l || !l.entradas.length) return "";
514
+ const carpetas = l.entradas.filter((e) => e.dir).length;
515
+ const ficheros = l.entradas.length - carpetas;
516
+ const trozos = [];
517
+ if (carpetas) trozos.push(plural(carpetas, "{n} folder", "{n} folders"));
518
+ if (ficheros) trozos.push(plural(ficheros, "{n} file", "{n} files"));
519
+ return `<p class="expCuenta">${this.p.esc(trozos.join(" \xB7 "))}${ficheros ? ` \u2014 ${t("files are below the folders")}` : ""}</p>`;
520
+ }
521
+ /**
522
+ * One listener on the root, not one per row.
523
+ *
524
+ * A folder with two thousand entries has four thousand buttons in it, and
525
+ * binding a closure to each of them on every repaint is most of what a
526
+ * repaint costs. Delegation also survives the repaint, so the handler is
527
+ * attached exactly once for the life of the picker.
528
+ */
355
529
  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
- };
530
+ if (raiz.dataset.expEnlazado === "1") return;
531
+ raiz.dataset.expEnlazado = "1";
532
+ raiz.addEventListener("click", (evento) => {
533
+ const el = evento.target?.closest("[data-exp]");
534
+ if (!el || !raiz.contains(el)) return;
535
+ evento.preventDefault();
536
+ evento.stopPropagation();
537
+ const ruta = el.dataset.ruta ?? "";
538
+ if (!ruta) return;
539
+ if (el.dataset.exp === "ve") void this.ve(ruta);
540
+ else {
541
+ this.elige(ruta);
542
+ this.repinta();
543
+ }
368
544
  });
369
545
  }
370
546
  };
371
547
 
372
548
  // city/web/src/motores.ts
373
549
  var RUNTIMES = ["claude", "codex", "opencode", "kimi"];
374
- var MODELOS = ["haiku", "sonnet", "opus", "fable"];
550
+ var MOTOR = {
551
+ claude: {
552
+ modelos: ["haiku", "sonnet", "opus", "fable"],
553
+ esfuerzo: true,
554
+ pista: "An alias the Claude CLI resolves when the window opens."
555
+ },
556
+ codex: {
557
+ modelos: [],
558
+ esfuerzo: true,
559
+ pista: "The model name your Codex uses \u2014 the one in ~/.codex/config.toml."
560
+ },
561
+ opencode: {
562
+ modelos: [],
563
+ esfuerzo: false,
564
+ pista: "OpenCode names a model provider/model, like anthropic/claude-sonnet-4."
565
+ },
566
+ kimi: { modelos: [], esfuerzo: false, pista: "The model name your Kimi CLI uses." }
567
+ };
568
+ function motorDe(runtime) {
569
+ return MOTOR[runtime || "claude"] ?? MOTOR.claude;
570
+ }
571
+ var MODELOS = MOTOR.claude.modelos;
375
572
  var NIVEL_ESFUERZO = {
376
573
  low: 1,
377
574
  medium: 2,
@@ -413,8 +610,9 @@ var CLASES = [
413
610
  ],
414
611
  ["coordinator", "It coordinates", "Its house grows with the decisions it records."]
415
612
  ];
416
- var FormularioDeCasa = class {
613
+ var FormularioDeCasa = class extends Montada {
417
614
  constructor(p, roles = []) {
615
+ super();
418
616
  this.p = p;
419
617
  this.roles = roles;
420
618
  }
@@ -428,25 +626,26 @@ var FormularioDeCasa = class {
428
626
  esfuerzo: ""
429
627
  };
430
628
  roles = [];
431
- host = null;
432
- /** Render into `host` and wire it. Safe to call again: that is the repaint. */
629
+ rolesPedidos = false;
433
630
  monta(host) {
434
- this.host = host;
435
631
  void this.aseguraRoles();
436
- host.innerHTML = this.html();
437
- this.enlaza(host);
632
+ super.monta(host);
438
633
  const hueco = host.querySelector(".casaExp");
439
634
  if (hueco) this.explora().monta(hueco);
440
635
  }
441
636
  repinta() {
442
637
  if (this.host) this.monta(this.host);
443
638
  }
639
+ /** Asked once, ever. `monta` runs on every repaint, so a guard on "did we get
640
+ * any roles back" would refetch — and repaint, and refetch — forever the day
641
+ * the catalogue comes back empty. */
444
642
  async aseguraRoles() {
445
- if (this.roles.length) return;
643
+ if (this.rolesPedidos) return;
644
+ this.rolesPedidos = true;
446
645
  try {
447
646
  const r = await this.p.api("/api/roles?scope=agent");
448
647
  this.roles = r.roles ?? [];
449
- this.repinta();
648
+ if (this.roles.length) this.repinta();
450
649
  } catch {
451
650
  this.roles = [];
452
651
  }
@@ -461,7 +660,7 @@ var FormularioDeCasa = class {
461
660
  const ya = this.datos.montajes.indexOf(ruta);
462
661
  if (ya >= 0) this.datos.montajes.splice(ya, 1);
463
662
  else this.datos.montajes.push(ruta);
464
- this.repinta();
663
+ this.pintaElegidas();
465
664
  },
466
665
  () => this.datos.montajes
467
666
  );
@@ -479,7 +678,7 @@ var FormularioDeCasa = class {
479
678
  const roles = this.roles.map(
480
679
  (r) => `<option value="${esc3(r.id)}"${r.id === a.rol ? " selected" : ""}>${esc3(r.name)}</option>`
481
680
  ).join("");
482
- const esClaude = (a.runtime || "claude") === "claude";
681
+ const motor = motorDe(a.runtime);
483
682
  return `
484
683
  <div class="campo"><label>${t("What do you call it?")}</label>
485
684
  <input type="text" id="bvNombre" value="${esc3(a.nombre)}"
@@ -490,45 +689,63 @@ var FormularioDeCasa = class {
490
689
  <select id="bvRol">${roles}</select></div>
491
690
  <label class="bvEtiqueta">${t("What runs it?")}</label>
492
691
  <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."
692
+ "Leave all three on default and it runs the way you do. Whatever you set here is written once on the card, and the launcher hands it to whichever CLI runs this house."
494
693
  )}</p>
495
694
  <div class="bvMotor">
496
695
  <div class="campo"><label>${t("provider")}</label>
497
696
  <select id="bvRuntime">${opciones(RUNTIMES, a.runtime, esc3, "claude")}</select></div>
498
697
  <div class="campo"><label>${t("engine")}</label>
499
- <select id="bvModelo" ${esClaude ? "" : "disabled"}>${opciones(
500
- [""].concat(MODELOS),
698
+ ${motor.modelos.length ? `<select id="bvModelo">${opciones(
699
+ [""].concat(motor.modelos),
501
700
  a.modelo,
502
701
  esc3,
503
702
  t("default")
504
- )}</select></div>
703
+ )}</select>` : `<input type="text" id="bvModelo" value="${esc3(a.modelo)}"
704
+ placeholder="${t("default")}" autocomplete="off" spellcheck="false">`}</div>
505
705
  <div class="campo"><label>${t("effort")}</label>
506
- <select id="bvEsfuerzo" ${esClaude ? "" : "disabled"}>${opciones(
706
+ <select id="bvEsfuerzo" ${motor.esfuerzo ? "" : "disabled"}
707
+ title="${motor.esfuerzo ? "" : t("This CLI has no effort setting.")}">${opciones(
507
708
  [""].concat(ESFUERZOS),
508
- a.esfuerzo,
709
+ motor.esfuerzo ? a.esfuerzo : "",
509
710
  esc3,
510
711
  t("default")
511
712
  )}</select></div>
512
713
  </div>
714
+ <p class="pista">${esc3(t(motor.pista))}</p>
513
715
  <label class="bvEtiqueta">${t("What does it work on?")}</label>
514
716
  <p class="pista">${t(`Walk your disk and pick whatever you like: a repository, a
515
717
  worktree, a folder of documents, one exact file. As many as you want. Nothing is
516
718
  copied \u2014 each one is linked into this agent's own home.`)}</p>
517
719
  <div class="casaExp"></div>
518
- ${a.montajes.length ? `<div class="expElegidas"><label class="bvEtiqueta">${t("Working on")}</label>
519
- ${a.montajes.map(
720
+ <div id="casaElegidas">${this.elegidas()}</div>`;
721
+ }
722
+ /** The mounts chosen so far, as chips you can take back off. Rendered on its
723
+ * own so ticking a row does not rebuild the picker underneath the cursor. */
724
+ elegidas() {
725
+ const esc3 = this.p.esc;
726
+ const montajes = this.datos.montajes;
727
+ if (!montajes.length)
728
+ return `<p class="pista">${t("Nothing chosen yet \u2014 an agent with no mounts is fine too.")}</p>`;
729
+ return `<div class="expElegidas"><label class="bvEtiqueta">${t("Working on")}</label>
730
+ ${montajes.map(
520
731
  (m) => `<button type="button" class="bvChip elegida" data-bv="quita"
521
- data-ruta="${esc3(m)}" title="${esc3(m)}">${esc3(
732
+ data-ruta="${esc3(m)}" title="${esc3(m)}">${esc3(
522
733
  m.split("/").pop() || m
523
734
  )}<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>`}`;
735
+ ).join("")}</div>`;
736
+ }
737
+ pintaElegidas() {
738
+ const hueco = this.host?.querySelector("#casaElegidas");
739
+ if (!hueco) return;
740
+ hueco.innerHTML = this.elegidas();
741
+ this.enlaza(hueco);
525
742
  }
526
743
  enlaza(raiz) {
527
744
  raiz.querySelectorAll("[data-bv]").forEach((el) => {
528
745
  el.onclick = (evento) => {
529
746
  evento.preventDefault();
530
747
  const que = el.dataset.bv;
531
- this.recoge(raiz);
748
+ this.recoge();
532
749
  if (que === "clase") this.datos.clase = el.dataset.id ?? "code";
533
750
  else if (que === "quita") {
534
751
  const ya = this.datos.montajes.indexOf(el.dataset.ruta ?? "");
@@ -538,12 +755,19 @@ var FormularioDeCasa = class {
538
755
  };
539
756
  });
540
757
  raiz.querySelector("#bvRuntime")?.addEventListener("change", () => {
541
- this.recoge(raiz);
758
+ this.recoge();
542
759
  this.repinta();
543
760
  });
544
761
  }
545
- /** Keep what is typed when a click forces a repaint. */
546
- recoge(raiz) {
762
+ /** Keep what is typed when a click forces a repaint.
763
+ *
764
+ * Always from the form's own root, never from whatever element the click
765
+ * came through: the chosen-chips strip re-uses this wiring, and reading the
766
+ * fields relative to that strip would find none of them and quietly throw
767
+ * away everything the person had typed. */
768
+ recoge() {
769
+ const raiz = this.host;
770
+ if (!raiz) return;
547
771
  const nombre = raiz.querySelector("#bvNombre");
548
772
  const rol = raiz.querySelector("#bvRol");
549
773
  if (nombre) this.datos.nombre = nombre.value;
@@ -554,7 +778,7 @@ var FormularioDeCasa = class {
554
778
  ["#bvEsfuerzo", "esfuerzo"]
555
779
  ]) {
556
780
  const sel = raiz.querySelector(id);
557
- if (sel) this.datos[campo] = sel.value;
781
+ if (sel) this.datos[campo] = sel.value.trim();
558
782
  }
559
783
  }
560
784
  /**
@@ -566,7 +790,7 @@ var FormularioDeCasa = class {
566
790
  * another's job just to save a round trip.
567
791
  */
568
792
  async guarda() {
569
- if (this.host) this.recoge(this.host);
793
+ this.recoge();
570
794
  const a = this.datos;
571
795
  const nombre = a.nombre.trim();
572
796
  if (!nombre) {
@@ -582,13 +806,15 @@ var FormularioDeCasa = class {
582
806
  this.p.aviso(r.error || t("Could not add that agent"), true);
583
807
  return null;
584
808
  }
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
- }
809
+ const montados = await Promise.all(
810
+ a.montajes.map(
811
+ (ruta) => this.p.api("/api/montaje", {
812
+ method: "POST",
813
+ body: JSON.stringify({ agent: r.agent, add: ruta })
814
+ }).then((m) => m.ok ? "" : `${ruta}: ${m.error ?? t("could not mount")}`).catch((e) => `${ruta}: ${String(e)}`)
815
+ )
816
+ );
817
+ for (const fallo of montados.filter(Boolean)) this.p.aviso(fallo, true);
592
818
  const motor = {};
593
819
  if (a.runtime) motor.runtime = a.runtime;
594
820
  if (a.modelo) motor.model = a.modelo;
@@ -725,8 +951,9 @@ var VELOCIDADES = [
725
951
  ["2\xD7", 700],
726
952
  ["4\xD7", 350]
727
953
  ];
728
- var Demos = class {
954
+ var Demos = class extends Montada {
729
955
  constructor(p) {
956
+ super();
730
957
  this.p = p;
731
958
  }
732
959
  catalogo = [];
@@ -736,15 +963,13 @@ var Demos = class {
736
963
  corriendo = false;
737
964
  velocidad = 1450;
738
965
  reloj = null;
739
- host = null;
740
966
  monta(host) {
741
- this.host = host;
742
967
  if (!this.catalogo.length && !this.cargando) void this.lee();
743
- this.repinta();
968
+ super.monta(host);
744
969
  }
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() {
970
+ /** Stop the clock. The router calls this when the section is left: a timer
971
+ * still firing into a detached node is a leak with a soundtrack. */
972
+ desmonta() {
748
973
  this.corriendo = false;
749
974
  if (this.reloj !== null) {
750
975
  clearTimeout(this.reloj);
@@ -752,13 +977,9 @@ var Demos = class {
752
977
  }
753
978
  }
754
979
  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
- }
980
+ super.repinta();
981
+ const lista = this.elegida ? this.host?.querySelector("#demoLista") : null;
982
+ if (lista) lista.scrollTop = lista.scrollHeight;
762
983
  }
763
984
  async lee() {
764
985
  this.cargando = true;
@@ -773,7 +994,7 @@ var Demos = class {
773
994
  }
774
995
  }
775
996
  async abre(id) {
776
- this.para();
997
+ this.desmonta();
777
998
  this.hasta = 0;
778
999
  this.elegida = null;
779
1000
  this.repinta();
@@ -797,17 +1018,38 @@ var Demos = class {
797
1018
  if (this.reloj !== null) clearTimeout(this.reloj);
798
1019
  this.reloj = window.setTimeout(() => {
799
1020
  if (!this.corriendo || !this.elegida) return;
1021
+ const evento = this.elegida.eventos[this.hasta];
800
1022
  this.hasta += 1;
801
- if (this.hasta >= this.elegida.eventos.length) {
1023
+ const final = this.hasta >= this.elegida.eventos.length;
1024
+ if (final) {
802
1025
  this.hasta = this.elegida.eventos.length;
803
1026
  this.corriendo = false;
804
- this.repinta();
805
- return;
806
1027
  }
807
- this.repinta();
808
- this.siguiente();
1028
+ if (evento && !final) this.anade(evento);
1029
+ else this.repinta();
1030
+ if (!final) this.siguiente();
809
1031
  }, this.velocidad);
810
1032
  }
1033
+ /** One more turn on screen, without taking the screen down. */
1034
+ anade(evento) {
1035
+ const lista = this.host?.querySelector("#demoLista");
1036
+ if (!lista || !this.elegida) return this.repinta();
1037
+ const vacia = lista.querySelector(".liveEmpty");
1038
+ if (vacia) lista.innerHTML = "";
1039
+ lista.insertAdjacentHTML("beforeend", this.p.pinta(evento));
1040
+ lista.scrollTop = lista.scrollHeight;
1041
+ this.marcador();
1042
+ }
1043
+ /** The counter and the bar, which are the only other things a turn moves. */
1044
+ marcador() {
1045
+ if (!this.host || !this.elegida) return;
1046
+ const total = this.elegida.eventos.length;
1047
+ const hecho = Math.min(this.hasta, total);
1048
+ const cuenta = this.host.querySelector(".demoCuenta");
1049
+ if (cuenta) cuenta.textContent = t("{done} of {total}", { done: hecho, total });
1050
+ const barra2 = this.host.querySelector(".demoBarra i");
1051
+ if (barra2) barra2.style.width = `${total ? hecho / total * 100 : 0}%`;
1052
+ }
811
1053
  html() {
812
1054
  if (this.elegida) return this.reproductor(this.elegida);
813
1055
  if (this.cargando) return `<p class="cargando">${t("reading the demo shelf")}</p>`;
@@ -848,9 +1090,10 @@ var Demos = class {
848
1090
  </div>
849
1091
  <div class="demoMandos">
850
1092
  <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>
1093
+ ${// Once it has finished, the primary button already says Replay and
1094
+ // does exactly this. Two identical controls side by side is a person
1095
+ // wondering which one is the real one.
1096
+ hecho && !acabado ? `<button type="button" class="bt" data-demo="replay">\u21BA ${t("Replay")}</button>` : ""}
854
1097
  <span class="demoVel">${VELOCIDADES.map(
855
1098
  ([etiqueta, ms]) => `<button type="button" class="demoVelBoton ${ms === this.velocidad ? "aqui" : ""}" data-demo="vel" data-ms="${ms}">${esc3(etiqueta)}</button>`
856
1099
  ).join("")}</span>
@@ -870,7 +1113,7 @@ var Demos = class {
870
1113
  void this.abre(el.dataset.id ?? "");
871
1114
  break;
872
1115
  case "atras":
873
- this.para();
1116
+ this.desmonta();
874
1117
  this.elegida = null;
875
1118
  this.hasta = 0;
876
1119
  this.repinta();
@@ -879,11 +1122,11 @@ var Demos = class {
879
1122
  this.arranca();
880
1123
  break;
881
1124
  case "pausa":
882
- this.para();
1125
+ this.desmonta();
883
1126
  this.repinta();
884
1127
  break;
885
1128
  case "replay":
886
- this.para();
1129
+ this.desmonta();
887
1130
  this.hasta = 0;
888
1131
  this.arranca();
889
1132
  break;
@@ -902,7 +1145,7 @@ var Demos = class {
902
1145
  // city/web/src/bienvenida.ts
903
1146
  var PASOS = ["Welcome", "The work", "Your chair", "The houses", "Ready"];
904
1147
  function casita(color) {
905
- return `<svg viewBox="0 0 120 108" role="img" aria-label="an isometric house" class="bvCasa">
1148
+ return `<svg viewBox="0 0 120 108" role="img" aria-label="${t("an isometric house")}" class="bvCasa">
906
1149
  <polygon points="60,10 112,40 60,70 8,40" fill="${color}" opacity=".95"/>
907
1150
  <polygon points="8,40 60,70 60,100 8,70" fill="${color}" opacity=".62"/>
908
1151
  <polygon points="112,40 60,70 60,100 112,70" fill="${color}" opacity=".40"/>
@@ -922,7 +1165,17 @@ var Bienvenida = class {
922
1165
  enCurso = null;
923
1166
  guardando = false;
924
1167
  lienzo = null;
925
- /** Draw the current step into `lienzo`. The only entry point. */
1168
+ /**
1169
+ * The view contract, so the Hall's dispatcher treats every section the same.
1170
+ *
1171
+ * The painting itself is async — the first one asks the server for the domain
1172
+ * packs — and a router cannot wait on a view. It does not need to: the guide
1173
+ * draws a loading line first and fills itself in.
1174
+ */
1175
+ monta(host) {
1176
+ void this.pinta(host);
1177
+ }
1178
+ /** Draw the current step into `lienzo`. */
926
1179
  async pinta(lienzo) {
927
1180
  this.lienzo = lienzo;
928
1181
  if (!this.dominios.length) {
@@ -1076,16 +1329,6 @@ var Bienvenida = class {
1076
1329
  </div>
1077
1330
  </div>`;
1078
1331
  }
1079
- /**
1080
- * The places on this disk worth offering for this house, ranked.
1081
- *
1082
- * With nothing typed it is what you touched recently, because on a machine
1083
- * with three hundred clones a list is not an answer. What is typed matches
1084
- * the name and the path both — people look for `rankia` and for `Documents`
1085
- * with equal frequency, and only one of those is a repository name. A house
1086
- * that keeps knowledge sees document folders first, because that is the one
1087
- * kind of agent the old picker could not help at all.
1088
- */
1089
1332
  // ── 4. done ───────────────────────────────────────────────────────────────
1090
1333
  pantallaFinal() {
1091
1334
  return `
@@ -1276,6 +1519,7 @@ var SECCIONES = [
1276
1519
  ["mapa", "The map"],
1277
1520
  ["puesto", "My seat"],
1278
1521
  ["barrios", "Districts"],
1522
+ ["recepcion", "Reception"],
1279
1523
  ["red", "Roads"],
1280
1524
  ["committee", "Committee"],
1281
1525
  ["gente", "Houses"],
@@ -1313,7 +1557,7 @@ function pintaDemoControles() {
1313
1557
  if (previa) return;
1314
1558
  const caja = document.createElement("div");
1315
1559
  caja.id = "demoControles";
1316
- caja.innerHTML = `<span>guided committee</span><button id="demoReplay" type="button" title="Play the guided committee from the top">\u27F3 replay</button><button id="demoPausa" type="button" title="Pause or resume mid-scene">\u23F8 pause</button>`;
1560
+ caja.innerHTML = `<span>${t("guided committee")}</span><button id="demoReplay" type="button" title="${t("Play the guided committee from the top")}">${t("\u27F3 replay")}</button><button id="demoPausa" type="button" title="${t("Pause or resume mid-scene")}">${t("\u23F8 pause")}</button>`;
1317
1561
  header.appendChild(caja);
1318
1562
  const pausa = q("#demoPausa", caja);
1319
1563
  const manda = (action) => api("/api/demo", {
@@ -1440,14 +1684,14 @@ function pintaActividad(seguir = false) {
1440
1684
  const workToggle = document.querySelector("#liveWorkToggle");
1441
1685
  if (!dot || !state || !list || !filter || !context || !workToggle) return;
1442
1686
  dot.classList.toggle("on", liveConnected);
1443
- state.textContent = liveConnected ? "websocket live" : liveUrl ? "reconnecting" : "session offline";
1687
+ state.textContent = liveConnected ? t("websocket live") : liveUrl ? t("reconnecting") : t("session offline");
1444
1688
  const opened = /* @__PURE__ */ new Map();
1445
1689
  for (const event of liveEvents) {
1446
1690
  if (event.thread && (event.kind === "committee.opened" || event.kind === "conversation.user") && !esPromptDeTransporte(event) && !opened.has(event.thread)) {
1447
1691
  opened.set(event.thread, event.summary);
1448
1692
  }
1449
1693
  }
1450
- filter.innerHTML = '<option value="">all conversations</option>' + [...opened.entries()].reverse().map(
1694
+ filter.innerHTML = `<option value="">${t("all conversations")}</option>` + [...opened.entries()].reverse().map(
1451
1695
  ([thread, question]) => `<option value="${esc2(thread)}">${esc2(question.slice(0, 54) || thread)}</option>`
1452
1696
  ).join("");
1453
1697
  const selected = liveSelectedThread || "";
@@ -1459,7 +1703,7 @@ function pintaActividad(seguir = false) {
1459
1703
  };
1460
1704
  workToggle.classList.toggle("on", liveShowWork);
1461
1705
  workToggle.setAttribute("aria-pressed", String(liveShowWork));
1462
- workToggle.textContent = liveShowWork ? "hide work" : "show work";
1706
+ workToggle.textContent = liveShowWork ? t("hide work") : t("show work");
1463
1707
  workToggle.onclick = () => {
1464
1708
  liveShowWork = !liveShowWork;
1465
1709
  pintaActividad(true);
@@ -1470,7 +1714,7 @@ function pintaActividad(seguir = false) {
1470
1714
  (event) => !esPromptDeTransporte(event) && (liveShowWork || !esRuidoDeTrabajo(event) || event.tone === "error")
1471
1715
  );
1472
1716
  if (!visible.length) {
1473
- list.innerHTML = `<li class="liveEmpty">${liveConnected ? "The bus is live. Questions, positions and moderated replies will appear here." : "Start the city session. Its visible conversation will appear here as it happens."}</li>`;
1717
+ list.innerHTML = `<li class="liveEmpty">${liveConnected ? t("The bus is live. Questions, positions and moderated replies will appear here.") : t("Start the city session. Its visible conversation will appear here as it happens.")}</li>`;
1474
1718
  return;
1475
1719
  }
1476
1720
  const wasNearBottom = list.scrollHeight - list.scrollTop - list.clientHeight < 90;
@@ -1487,7 +1731,7 @@ function renderActividad(event) {
1487
1731
  const compact = resumenVisible(summary);
1488
1732
  const long = compact !== summary;
1489
1733
  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>` : "";
1734
+ const full = long ? `<details class="liveFull"><summary>${t("read full message")}</summary><p>${esc2(summary)}</p></details>` : "";
1491
1735
  return `<li class="liveTurn ${side} ${esc2(event.tone)}">
1492
1736
  <div class="liveAvatar${E?.avatars?.[event.actor] ? " conCara" : ""}"
1493
1737
  style="--actor-hue:${tonoActor(event.actor)}" aria-hidden="true">
@@ -1529,7 +1773,7 @@ function pintaContexto(thread, container) {
1529
1773
  (actor) => `<span class="livePerson${E?.avatars?.[actor] ? " conCara" : ""}" title="${esc2(
1530
1774
  actor === "seat" ? "seat \xB7 chair" : `${actor} \xB7 ${rolOperativo(actor) || "member"}`
1531
1775
  )}" style="--actor-hue:${tonoActor(actor)}">${cara(actor, actor === "seat")}</span>`
1532
- ).join("")}</div><span class="liveContextText"><b>seat moderates</b> \xB7 ${esc2(
1776
+ ).join("")}</div><span class="liveContextText"><b>${t("seat moderates")}</b> \xB7 ${esc2(
1533
1777
  received
1534
1778
  )}/${esc2(total)} positions \xB7 ${esc2(status)}</span>`;
1535
1779
  }
@@ -1610,23 +1854,32 @@ function rail() {
1610
1854
  <b>${esc2(E.city_name)}</b>
1611
1855
  <span class="railDe">${esc2(E.domain)} \xB7 ${plural(cuantos, "{n} house", "{n} houses")}</span>`;
1612
1856
  q("#rail").innerHTML = SECCIONES.filter(([id]) => id !== "ciudades").map(([id, et]) => {
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) : "";
1614
- return `<li class="${id === SECCION ? "aqui" : ""}" data-s="${id}">
1857
+ const n = id === "demos" ? "" : id === "gente" ? String(Object.keys(E.skills).length) : id === "committee" ? String(E.deliberations.length) : id === "recepcion" ? String(E.reception?.pending ?? 0) : id === "red" ? String(E.roads.length) : id === "barrios" ? String(E.parcelas.length) : "";
1858
+ return `<li class="${id === SECCION ? "aqui" : ""}" data-s="${id}" role="button"
1859
+ tabindex="0" ${id === SECCION ? 'aria-current="page"' : ""}>
1615
1860
  <span>${esc2(t(et))}</span>${n ? `<span class="n">${n}</span>` : ""}</li>`;
1616
1861
  }).join("");
1617
- q("#railOtras").innerHTML = `<li class="${SECCION === "ciudades" ? "aqui" : ""}" data-s="ciudades">
1862
+ q("#railOtras").innerHTML = `<li class="${SECCION === "ciudades" ? "aqui" : ""}" data-s="ciudades" role="button"
1863
+ tabindex="0" ${SECCION === "ciudades" ? 'aria-current="page"' : ""}>
1618
1864
  <span>${t("All cities")}</span><span class="n">${E.ciudades.length}</span></li>`;
1619
1865
  todos("#rail li,#railOtras li").forEach((li) => {
1620
- li.onclick = () => {
1866
+ const abre = () => {
1621
1867
  document.body.classList.remove("enGuia");
1622
1868
  SECCION = li.dataset.s ?? "mapa";
1623
1869
  pinta();
1624
1870
  };
1871
+ li.onclick = abre;
1872
+ li.onkeydown = (event) => {
1873
+ if (event.key !== "Enter" && event.key !== " ") return;
1874
+ event.preventDefault();
1875
+ abre();
1876
+ };
1625
1877
  });
1626
1878
  q("#dondeDatos").textContent = corto(E.datos);
1627
1879
  const cs = q("#ciudades");
1628
1880
  if (E.ciudades.length > 1) {
1629
- cs.innerHTML = `<select id="cambiaCiudad" title="which city this hall manages">
1881
+ cs.innerHTML = `<select id="cambiaCiudad" aria-label="${t("which city this hall manages")}"
1882
+ title="${t("which city this hall manages")}">
1630
1883
  ${E.ciudades.map(
1631
1884
  (c) => `<option value="${esc2(c.ruta)}" ${c.actual ? "selected" : ""}>
1632
1885
  ${esc2(c.nombre)}</option>`
@@ -1668,8 +1921,10 @@ function rail() {
1668
1921
  location.href = "/?PASE=" + encodeURIComponent(PASE) + "&city=" + encodeURIComponent(r.city);
1669
1922
  };
1670
1923
  }
1924
+ var montada = null;
1671
1925
  function pinta() {
1672
- if (SECCION !== "demos") demos?.para();
1926
+ montada?.desmonta?.();
1927
+ montada = null;
1673
1928
  rail();
1674
1929
  q(".cuerpo").style.padding = "";
1675
1930
  q("#lienzo").style.maxWidth = "";
@@ -1694,7 +1949,8 @@ VISTAS.bienvenida = () => {
1694
1949
  yo: E.yo,
1695
1950
  datos: corto(E.datos)
1696
1951
  });
1697
- void guia.pinta(q("#lienzo"));
1952
+ guia.monta(q("#lienzo"));
1953
+ montada = guia;
1698
1954
  };
1699
1955
  VISTAS.ciudades = () => {
1700
1956
  const filas = E.ciudades.map(
@@ -1712,23 +1968,24 @@ VISTAS.ciudades = () => {
1712
1968
  ).join("");
1713
1969
  q("#lienzo").innerHTML = `<div><span class="sub">cities</span>
1714
1970
  <h1 style="margin-top:6px">${t("Your cities")}</h1>
1715
- <p class="prosa" style="margin-top:8px">One person, several autonomous cities: each has
1971
+ <p class="prosa" style="margin-top:8px">${t(`One person, several autonomous cities: each has
1716
1972
  its own identity, domain, chair, <b>its own houses</b> and its own roads. A house is not
1717
1973
  shared \u2014 it stands inside the city that owns it, with its workspace and mounts under that
1718
1974
  city's folder \u2014 so two cities can each have a <code class="mono">docs</code> house and
1719
1975
  they are two different workers. They share nothing unless you build a road between
1720
- them.</p></div>
1976
+ them.`)}</p></div>
1721
1977
  <div class="lista"><div class="filas">${filas}</div></div>
1722
1978
  <div class="campos" style="max-width:420px">
1723
1979
  <div class="campo"><label>${t("Start another city")}</label>
1724
- <input type="text" id="nuevaCiudad" placeholder="client-a, research, the book\u2026"></div>
1980
+ <input type="text" id="nuevaCiudad" placeholder="${t("client-a, research, the book\u2026")}"></div>
1725
1981
  <button class="bt ppal" id="creaCiudad">Create it</button>
1726
1982
  </div>
1727
1983
  <div class="peligro">
1728
1984
  <h3>${t("Start this city over")}</h3>
1729
- <p class="prosa">Takes <b>${esc2(E.city_name)}</b> back to its first day: no seat, no
1730
- agents, no committee history, no map. Your repositories and document folders are
1731
- <b>never touched</b> \u2014 only the city that points at them.</p>
1985
+ <p class="prosa">${t(
1986
+ "Takes {city} back to its first day: no seat, no agents, no committee history, no map. Your repositories and document folders are <b>never touched</b> \u2014 only the city that points at them.",
1987
+ { city: `<b>${esc2(E.city_name)}</b>` }
1988
+ )}</p>
1732
1989
  <button class="bt malo" id="reiniciaCiudad">${t("Start over\u2026")}</button>
1733
1990
  </div>
1734
1991
  <p class="pista">Archiving <b>moves</b> a city into
@@ -1819,9 +2076,10 @@ VISTAS.ciudades = () => {
1819
2076
  };
1820
2077
  var demos = null;
1821
2078
  VISTAS.demos = () => {
1822
- if (!demos) demos = new Demos({ api, esc: esc2, pinta: renderActividad });
2079
+ demos ??= new Demos({ api, esc: esc2, pinta: renderActividad });
1823
2080
  q("#lienzo").innerHTML = '<div id="demoHueco"></div>';
1824
2081
  demos.monta(q("#demoHueco"));
2082
+ montada = demos;
1825
2083
  };
1826
2084
  VISTAS.resumen = () => {
1827
2085
  const mia = E.tarjetas.find((t2) => t2.user === E.yo);
@@ -1870,19 +2128,19 @@ VISTAS.resumen = () => {
1870
2128
  <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
2129
  counted with <code class="mono">${esc2(E.grow || "nothing yet")}</code>.</p>`}</div>
1872
2130
  <div class="cifras">
1873
- <div class="cifra"><b>${Object.keys(E.skills).length}</b><span>repo agents</span></div>
2131
+ <div class="cifra"><b>${Object.keys(E.skills).length}</b><span>${t("repo agents")}</span></div>
1874
2132
  <div class="cifra"><b>${E.parcelas.length}</b><span>houses</span></div>
1875
2133
  <div class="cifra"><b>${E.roads.length}</b><span>roads</span></div>
1876
- <div class="cifra"><b>${E.deliberations.length}</b><span>committee acts</span></div>
1877
- <div class="cifra"><b>${Object.values(E.skills).reduce((n, a) => n + a.skills.length, 0)}</b><span>skills recognised</span></div>
2134
+ <div class="cifra"><b>${E.deliberations.length}</b><span>${t("committee acts")}</span></div>
2135
+ <div class="cifra"><b>${Object.values(E.skills).reduce((n, a) => n + a.skills.length, 0)}</b><span>${t("skills recognised")}</span></div>
1878
2136
  </div>
1879
2137
  <div class="luces">
1880
- <span class="luz ${E.tarjetas.length ? "on" : ""}">data repo</span>
2138
+ <span class="luz ${E.tarjetas.length ? "on" : ""}">${t("data repo")}</span>
1881
2139
  <span class="luz ${E.gh ? "on" : "neutra"}">github${E.gh ? "" : " \u2014 optional"}</span>
1882
- <span class="luz ${E.plugin ? "on" : ""}">plugin installed</span>
2140
+ <span class="luz ${E.plugin ? "on" : ""}">${t("plugin installed")}</span>
1883
2141
  <span class="luz ${sesionArriba ? "on" : "neutra"}">tmux session${sesionArriba ? " up" : ""}</span>
1884
2142
  </div>
1885
- <div><span class="sub">what is left</span>
2143
+ <div><span class="sub">${t("what is left")}</span>
1886
2144
  <div class="tareas" style="margin-top:9px">${tareas.map(
1887
2145
  (t2) => `
1888
2146
  <div class="tarea ${t2.ok ? "hecha" : ""}">${t2.txt}
@@ -1891,9 +2149,9 @@ VISTAS.resumen = () => {
1891
2149
  </div></div>
1892
2150
  <div><span class="sub">work</span>
1893
2151
  <div style="display:flex;flex-direction:column;gap:9px;margin-top:9px">
1894
- <div class="orden"><span class="et2">your day</span>
2152
+ <div class="orden"><span class="et2">${t("your day")}</span>
1895
2153
  <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>'}
2154
+ ${sesionArriba ? `<button class="bt mini" data-copia="tmux attach -t ${esc2(E.sesion)}">copy</button>` : `<button class="bt mini ppal" id="abreSesion">${t("open my session")}</button>`}
1897
2155
  </div>
1898
2156
  <div class="orden"><span class="et2">the map</span>
1899
2157
  <code>${E.mapa ? esc2(E.mapa) : "not drawn yet"}</code>
@@ -1986,26 +2244,26 @@ VISTAS.puesto = () => {
1986
2244
  };
1987
2245
  async function puesto() {
1988
2246
  const mia = E.tarjetas.find((t2) => t2.user === E.yo);
1989
- q("#lienzo").innerHTML = `<div><span class="sub">my seat</span>
2247
+ q("#lienzo").innerHTML = `<div><span class="sub">${t("my seat")}</span>
1990
2248
  <h1 style="margin-top:6px">${esc2(E.yo)}</h1>
1991
2249
  <p class="prosa" style="margin-top:8px">This is your chair: the work domain, your
1992
2250
  role inside it, and one goal. The seat stays the boss even when its
1993
2251
  professional role is blank. Saving writes your card \u2014
1994
2252
  <code class="mono">${esc2(corto(E.datos))}/${esc2(E.yo)}.md</code> \u2014 the same file
1995
2253
  every other door writes, and it never touches your roster.</p></div>
1996
- <div><span class="sub">work domain</span><div class="rolejilla" id="domains" style="margin-top:9px">
1997
- <p class="cargando">reading the domain packs</p></div></div>
2254
+ <div><span class="sub">${t("work domain")}</span><div class="rolejilla" id="domains" style="margin-top:9px">
2255
+ <p class="cargando">${t("reading the domain packs")}</p></div></div>
1998
2256
  <div><span class="sub">role</span><div class="rolejilla" id="roles" style="margin-top:9px">
1999
- <p class="cargando">reading the role files</p></div></div>
2000
- <div><span class="sub">the agents</span>
2257
+ <p class="cargando">${t("reading the role files")}</p></div></div>
2258
+ <div><span class="sub">${t("the agents")}</span>
2001
2259
  <p class="prosa" style="margin-top:8px">Who works in this city \u2014 and what each
2002
- one works on \u2014 lives in <b>Agents &amp; skills</b>, where an agent is asked for
2260
+ one works on \u2014 lives in <b>${t("Agents &amp; skills")}</b>, where an agent is asked for
2003
2261
  whole: its kind, its role, its repositories and document folders, its engine
2004
2262
  and its skills. One place, so the terminal and this page cannot disagree.
2005
- <button class="bt" type="button" data-ir="gente" style="margin-left:8px">Open the roster</button></p></div>
2006
- <div><span class="sub">one goal \u2014 optional</span><div class="campos" id="meta" style="margin-top:9px"></div></div>
2263
+ <button class="bt" type="button" data-ir="gente" style="margin-left:8px">${t("Open the roster")}</button></p></div>
2264
+ <div><span class="sub">${t("one goal \u2014 optional")}</span><div class="campos" id="meta" style="margin-top:9px"></div></div>
2007
2265
  <div style="display:flex;gap:10px;align-items:center">
2008
- <button class="bt ppal" id="guardaPuesto">Save my seat</button>
2266
+ <button class="bt ppal" id="guardaPuesto">${t("Save my seat")}</button>
2009
2267
  <span class="cargando" id="puestoEstado" style="display:none">writing</span></div>`;
2010
2268
  const domainsR = (await api("/api/domains")).domains;
2011
2269
  let domain = E.domain || E.kind || "software";
@@ -2049,28 +2307,28 @@ async function puesto() {
2049
2307
  await cargaRoles();
2050
2308
  const o = mia && mia.objetivo || {};
2051
2309
  q("#meta").innerHTML = `
2052
- <div class="campo"><label>The goal, in one line</label>
2053
- <input type="text" id="g_title" value="${esc2(o.title)}" placeholder="Concrete enough to argue with \u2014 empty skips it">
2310
+ <div class="campo"><label>${t("The goal, in one line")}</label>
2311
+ <input type="text" id="g_title" value="${esc2(o.title)}" placeholder="${t("Concrete enough to argue with \u2014 empty skips it")}">
2054
2312
  </div>
2055
2313
  <div class="dos">
2056
- <div class="campo"><label>How it is measured</label>
2314
+ <div class="campo"><label>${t("How it is measured")}</label>
2057
2315
  <input type="text" id="g_signal" value="${esc2(o.signal)}"></div>
2058
- <div class="campo"><label>The command that returns it, if a command can</label>
2316
+ <div class="campo"><label>${t("The command that returns it, if a command can")}</label>
2059
2317
  <input type="text" class="mono" id="g_command" value="${esc2(o.command)}"
2060
- placeholder="empty for a qualitative goal"></div>
2318
+ placeholder="${t("empty for a qualitative goal")}"></div>
2061
2319
  </div>
2062
- <div class="campo"><label>\u2026or who judges it, and how often</label>
2320
+ <div class="campo"><label>${t("\u2026or who judges it, and how often")}</label>
2063
2321
  <input type="text" id="g_manual" value="${esc2(o.manual)}"
2064
- placeholder="in prose: the architect reads the AGENTS.md files on Fridays">
2322
+ placeholder="${t("in prose: the architect reads the AGENTS.md files on Fridays")}">
2065
2323
  <p class="pista">A goal judged by a person is a real goal \u2014 plenty of quality
2066
2324
  is prose, not a number. Used when the command is empty.</p></div>
2067
2325
  <div class="dos">
2068
- <div class="campo"><label>What it returns today</label>
2326
+ <div class="campo"><label>${t("What it returns today")}</label>
2069
2327
  <input type="text" class="mono" id="g_baseline" value="${esc2(o.baseline)}"></div>
2070
- <div class="campo"><label>Where it has to get to</label>
2328
+ <div class="campo"><label>${t("Where it has to get to")}</label>
2071
2329
  <input type="text" class="mono" id="g_target" value="${esc2(o.target)}"></div>
2072
2330
  </div>
2073
- <div class="campo" style="max-width:240px"><label>By when</label>
2331
+ <div class="campo" style="max-width:240px"><label>${t("By when")}</label>
2074
2332
  <input type="text" id="g_by" value="${esc2(o.by ?? "this quarter")}"></div>`;
2075
2333
  const val = (id) => q(id).value.trim();
2076
2334
  q("#guardaPuesto").onclick = async () => {
@@ -2117,23 +2375,23 @@ VISTAS.barrios = () => {
2117
2375
  const slug = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 20);
2118
2376
  const render = () => {
2119
2377
  const idsValidos = /* @__PURE__ */ new Set([...unidades.map((u) => u.id), "none"]);
2120
- q("#lienzo").innerHTML = `<div><span class="sub">districts & houses</span>
2121
- <h1 style="margin-top:6px">The modelling no tool can do for you</h1>
2122
- <p class="prosa" style="margin-top:8px">A house is <b>not a repo</b> \u2014 it is a
2378
+ q("#lienzo").innerHTML = `<div><span class="sub">${t("districts & houses")}</span>
2379
+ <h1 style="margin-top:6px">${t("The modelling no tool can do for you")}</h1>
2380
+ <p class="prosa" style="margin-top:8px">${t(`A house is <b>not a repo</b> \u2014 it is a
2123
2381
  parcel, a slice of one serving a single business unit. Split the interesting
2124
2382
  repos, give every house its district, and the map can say
2125
- \u201Cthis change touches banking\u201D instead of \u201Cthis change touches src/lib\u201D.</p></div>
2383
+ \u201Cthis change touches banking\u201D instead of \u201Cthis change touches src/lib\u201D.`)}</p></div>
2126
2384
 
2127
2385
  <div><span class="sub">districts</span>
2128
2386
  <div class="tabla" id="unidades" style="margin-top:9px"></div>
2129
2387
  <div style="display:flex;gap:9px;margin-top:9px">
2130
- <input type="text" id="nuevaU" placeholder="another district\u2026" style="flex:1">
2388
+ <input type="text" id="nuevaU" placeholder="${t("another district\u2026")}" style="flex:1">
2131
2389
  <button class="bt" id="masU">Add</button></div></div>
2132
2390
 
2133
2391
  <div><span class="sub">houses</span><div id="parcelas" style="margin-top:9px"></div></div>
2134
2392
 
2135
2393
  <div style="display:flex;gap:10px;align-items:center">
2136
- <button class="bt ppal" id="guardaBarrios">Save districts & houses</button>
2394
+ <button class="bt ppal" id="guardaBarrios">${t("Save districts & houses")}</button>
2137
2395
  <span style="font-size:12px;color:var(--tinta3);font-style:italic">
2138
2396
  writes units.yml and parcels.yml \u2014 reseed the map to see it drawn</span></div>`;
2139
2397
  q("#unidades").innerHTML = unidades.map(
@@ -2262,7 +2520,7 @@ VISTAS.red = () => {
2262
2520
  const locales = E.ciudades.filter((c) => !c.actual && c.id && !conectadas.has(c.id));
2263
2521
  const invitation = JSON.stringify(E.invitation);
2264
2522
  q("#lienzo").innerHTML = `<div><span class="sub">roads</span>
2265
- <h1 style="margin-top:6px">Cities this one may reach</h1>
2523
+ <h1 style="margin-top:6px">${t("Cities this one may reach")}</h1>
2266
2524
  <p class="prosa" style="margin-top:8px">A road joins city seats. It may stay on
2267
2525
  this machine or continue over the remote bus; the city sees one explicit
2268
2526
  connection either way.</p></div>
@@ -2271,14 +2529,14 @@ VISTAS.red = () => {
2271
2529
  <span class="ag">${esc2(r.address)}</span>
2272
2530
  <span class="rp">${r.local ? "local road" : "remote road"}</span>
2273
2531
  <button class="bt mini" data-road-close="${esc2(r.id)}">close</button></div>`
2274
- ).join("") || '<p class="prosa">No roads yet. This city is isolated on purpose.</p>'}</div>
2275
- <div><span class="sub">other cities on this machine</span>
2532
+ ).join("") || `<p class="prosa">${t("No roads yet. This city is isolated on purpose.")}</p>`}</div>
2533
+ <div><span class="sub">${t("other cities on this machine")}</span>
2276
2534
  <div class="gente" style="margin-top:9px">${locales.map(
2277
2535
  (c) => `<div class="persona"><h3>${esc2(c.nombre)}</h3>
2278
2536
  <span class="ag">${esc2(c.slug ?? c.ruta)}</span>
2279
2537
  <button class="bt mini ppal" data-road-open="${esc2(c.id)}">open road</button></div>`
2280
- ).join("") || '<p class="prosa">No unconnected local cities.</p>'}</div></div>
2281
- <div class="orden"><span class="et2">remote invitation \xB7 public, no token</span>
2538
+ ).join("") || `<p class="prosa">${t("No unconnected local cities.")}</p>`}</div></div>
2539
+ <div class="orden"><span class="et2">${t("remote invitation \xB7 public, no token")}</span>
2282
2540
  <code>${esc2(invitation)}</code>
2283
2541
  <button class="bt mini" data-copia="${esc2(invitation)}">copy</button>
2284
2542
  </div>`;
@@ -2306,15 +2564,269 @@ VISTAS.red = () => {
2306
2564
  };
2307
2565
  });
2308
2566
  };
2567
+ VISTAS.recepcion = () => {
2568
+ q("#lienzo").innerHTML = `<div><span class="sub">${t("reception")}</span>
2569
+ <h1 style="margin-top:6px">${t("Messages wait for you, not your agents")}</h1>
2570
+ <p class="prosa" style="margin-top:8px">${t(
2571
+ "Remote text stops here as inert text. Read it, reject it with a reason, or choose the cities that should receive it. Until then no model can read it."
2572
+ )}</p></div>
2573
+ <div class="recModo" id="recModo">
2574
+ <div><span class="et2">${t("routing mode")}</span><b>${t("Manual review")}</b>
2575
+ <p>${t("Every message needs a person before it reaches a city.")}</p></div>
2576
+ <button class="bt mini" type="button" data-rec-open-config>${t("Configure Auto")}</button>
2577
+ </div>
2578
+ <div id="recConfig" hidden></div>
2579
+ <div id="recLista"><p class="prosa">${t("Reading your reception\u2026")}</p></div>`;
2580
+ void cargaRecepcion();
2581
+ };
2582
+ async function cargaRecepcion() {
2583
+ const hueco = q("#recLista");
2584
+ let estado;
2585
+ try {
2586
+ estado = await api("/api/reception");
2587
+ } catch {
2588
+ hueco.innerHTML = `<div class="recError">${t("Could not read your reception")}</div>`;
2589
+ return;
2590
+ }
2591
+ if (estado.error) {
2592
+ hueco.innerHTML = `<div class="recError">${esc2(estado.error)}</div>`;
2593
+ return;
2594
+ }
2595
+ E.reception = {
2596
+ pending: estado.summary.pending,
2597
+ pendingBytes: estado.summary.pendingBytes,
2598
+ routingMode: estado.settings.routingMode,
2599
+ reviewPolicy: estado.settings.reviewPolicy,
2600
+ routerProfile: estado.settings.routerProfile,
2601
+ autoAvailable: estado.settings.autoAvailable
2602
+ };
2603
+ const reglas = new Map(estado.settings.autoRules.map((rule) => [rule.cityId, rule.keywords]));
2604
+ const modo = q("#recModo");
2605
+ modo.innerHTML = `<div><span class="et2">${t("routing mode")}</span>
2606
+ <b>${estado.settings.routingMode === "auto" ? t("Automatic routing") : t("Manual review")}</b>
2607
+ <p>${estado.settings.routingMode === "auto" ? t("Only one clear, low-risk rule match can leave the human queue automatically.") : t("Every message needs a person before it reaches a city.")}</p></div>
2608
+ <button class="bt mini" type="button" data-rec-open-config>${t("Configure Auto")}</button>`;
2609
+ const configuracion = q("#recConfig");
2610
+ configuracion.innerHTML = `<form class="recAutoConfig" data-rec-config>
2611
+ <fieldset><legend>${t("Routing policy")}</legend>
2612
+ <label><input type="radio" name="routing_mode" value="manual"
2613
+ ${estado.settings.routingMode === "manual" ? "checked" : ""}> ${t("Keep human review")}</label>
2614
+ <label><input type="radio" name="routing_mode" value="auto"
2615
+ ${estado.settings.routingMode === "auto" ? "checked" : ""}> ${t("Use the local rule router")}</label>
2616
+ </fieldset>
2617
+ <div class="recReglas"><p>${t("Write comma-separated words or phrases for each destination. Empty cities are never selected.")}</p>
2618
+ ${estado.cities.map(
2619
+ (city) => `<label><span><b>${esc2(city.name)}</b><small>${esc2(city.address)}</small></span>
2620
+ <input type="text" name="rule-${esc2(city.id)}" maxlength="1200"
2621
+ data-rule-city="${esc2(city.id)}" value="${esc2((reglas.get(city.id) ?? []).join(", "))}"
2622
+ placeholder="${t("e.g. contract, privacy, legal review")}"></label>`
2623
+ ).join("")}</div>
2624
+ <p class="recAutoAviso">${t("Suspicious, unmatched, or ambiguous text always waits for you. Auto never executes, answers, or opens links.")}</p>
2625
+ <button class="bt ppal" type="submit">${t("Save routing policy")}</button>
2626
+ </form>`;
2627
+ q("[data-rec-open-config]", modo).onclick = () => {
2628
+ configuracion.hidden = !configuracion.hidden;
2629
+ };
2630
+ rail();
2631
+ const conexiones = estado.connections.length ? `<section class="recConexiones"><div class="recSeccion"><b>${t("New encrypted message")}</b>
2632
+ <span>${estado.outbox.queued ? plural(estado.outbox.queued, "message queued", "messages queued") : t("Messages leave from this computer with end-to-end encryption.")}</span></div>
2633
+ <form class="recCompose" data-rec-send>
2634
+ <label class="recComposePersona" for="rec-send-connection"><span>${t("Write to")}</span>
2635
+ <select id="rec-send-connection" name="connection" required>
2636
+ ${estado.connections.map(
2637
+ (conexion) => `<option value="${esc2(conexion.id)}">${esc2(conexion.name)}</option>`
2638
+ ).join("")}
2639
+ </select>
2640
+ </label>
2641
+ <label class="recComposeTexto" for="rec-send-text"><span>${t("Message")}</span>
2642
+ <textarea id="rec-send-text" name="text" rows="3" maxlength="11500" required
2643
+ placeholder="${t("Your message will stop in their private reception.")}"></textarea>
2644
+ </label>
2645
+ <button class="bt ppal" type="submit">${t("Send securely")}</button>
2646
+ </form></section>` : "";
2647
+ const vacia = !estado.messages.length ? `<div class="recVacia"><b>${t("Reception clear")}</b>
2648
+ <p>${t("No remote message is waiting for a decision.")}</p></div>` : "";
2649
+ const ciudades = estado.cities.map(
2650
+ (ciudad) => `<label class="recDestino"><input type="checkbox" name="destination"
2651
+ value="${esc2(ciudad.id)}"><span><b>${esc2(ciudad.name)}</b>
2652
+ <small>${esc2(ciudad.address)}</small></span></label>`
2653
+ ).join("");
2654
+ const pendientes = estado.messages.length ? `<div class="recResumen"><b>${estado.summary.pending}</b>
2655
+ <span>${plural(estado.summary.pending, "message waiting", "messages waiting")}</span>
2656
+ <small>${formateaBytes(estado.summary.pendingBytes)} \xB7 ${t("local only")}</small></div>
2657
+ <div class="recMensajes">${estado.messages.map(
2658
+ (mensaje) => `<article class="recMensaje">
2659
+ <header><div><span class="recOrigen">${esc2(mensaje.fromName)}</span>
2660
+ <span>${esc2(fechaRecepcion(mensaje.receivedAt))}</span></div>
2661
+ <span class="recSeguro">${mensaje.kind === "rejection" ? t("Reply to a rejected message") : t("No agent has read this")}</span></header>
2662
+ <pre class="recTexto">${esc2(mensaje.text)}</pre>
2663
+ ${mensaje.kind === "rejection" ? `<div class="recAvisoAccion">
2664
+ <button class="bt mini" type="button" data-rec-dismiss="${esc2(mensaje.id)}">${t("Dismiss")}</button>
2665
+ </div>` : `<div class="recAcciones">
2666
+ <form data-rec-route="${esc2(mensaje.id)}">
2667
+ <fieldset><legend>${t("Send to")}</legend><div class="recDestinos">${ciudades}</div></fieldset>
2668
+ <button class="bt ppal" type="submit" disabled>${t("Route to selected cities")}</button>
2669
+ </form>
2670
+ <details><summary>${t("Reject and reply with a reason")}</summary>
2671
+ <form data-rec-reject="${esc2(mensaje.id)}">
2672
+ <label for="rec-reason-${esc2(mensaje.id)}">${t("Reason sent back securely")}</label>
2673
+ <input id="rec-reason-${esc2(mensaje.id)}" type="text" name="reason"
2674
+ maxlength="500" required
2675
+ placeholder="${t("Reason for rejecting this message")}">
2676
+ <button class="bt malo" type="submit">${t("Reject and send reason")}</button>
2677
+ </form>
2678
+ </details>
2679
+ </div>`}
2680
+ </article>`
2681
+ ).join("")}</div>` : "";
2682
+ hueco.innerHTML = `${vacia}${pendientes}${conexiones}`;
2683
+ enlazaRecepcion();
2684
+ }
2685
+ function enlazaRecepcion() {
2686
+ q("[data-rec-config]").onsubmit = async (evento) => {
2687
+ evento.preventDefault();
2688
+ const formulario = evento.currentTarget;
2689
+ const boton = q("button[type=submit]", formulario);
2690
+ const routingMode = q("input[name=routing_mode]:checked", formulario).value;
2691
+ const rules = todos("[data-rule-city]", formulario).map((campo) => ({
2692
+ city_id: campo.dataset.ruleCity,
2693
+ keywords: campo.value.split(",").map((value) => value.trim()).filter(Boolean)
2694
+ })).filter((rule) => rule.keywords.length);
2695
+ boton.disabled = true;
2696
+ const respuesta = await api("/api/reception", {
2697
+ method: "POST",
2698
+ body: JSON.stringify({ action: "configure", routing_mode: routingMode, rules })
2699
+ });
2700
+ if (!respuesta.ok) {
2701
+ boton.disabled = false;
2702
+ toast(respuesta.error ?? t("Could not save the routing policy"), true);
2703
+ return;
2704
+ }
2705
+ toast(t("Routing policy saved."));
2706
+ await cargaRecepcion();
2707
+ };
2708
+ todos("[data-rec-send]").forEach((formulario) => {
2709
+ formulario.onsubmit = async (evento) => {
2710
+ evento.preventDefault();
2711
+ const boton = q("button[type=submit]", formulario);
2712
+ const campo = q("textarea[name=text]", formulario);
2713
+ const connectionId = q("select[name=connection]", formulario).value;
2714
+ if (!connectionId || !campo.value.trim()) return;
2715
+ boton.disabled = true;
2716
+ const respuesta = await api("/api/reception", {
2717
+ method: "POST",
2718
+ body: JSON.stringify({
2719
+ action: "send",
2720
+ connection_id: connectionId,
2721
+ text: campo.value
2722
+ })
2723
+ });
2724
+ if (!respuesta.ok) {
2725
+ boton.disabled = false;
2726
+ toast(respuesta.error ?? t("Could not queue the message"), true);
2727
+ return;
2728
+ }
2729
+ campo.value = "";
2730
+ toast(t("Message queued on this computer."));
2731
+ await cargaRecepcion();
2732
+ };
2733
+ });
2734
+ todos("[data-rec-dismiss]").forEach((boton) => {
2735
+ boton.onclick = async () => {
2736
+ boton.disabled = true;
2737
+ const respuesta = await api("/api/reception", {
2738
+ method: "POST",
2739
+ body: JSON.stringify({
2740
+ action: "reject",
2741
+ message_id: boton.dataset.recDismiss,
2742
+ reason: "Dismissed response"
2743
+ })
2744
+ });
2745
+ if (!respuesta.ok) {
2746
+ boton.disabled = false;
2747
+ toast(respuesta.error ?? t("Could not dismiss the response"), true);
2748
+ return;
2749
+ }
2750
+ await cargaRecepcion();
2751
+ };
2752
+ });
2753
+ todos("[data-rec-route]").forEach((formulario) => {
2754
+ const boton = q("button[type=submit]", formulario);
2755
+ const actualiza = () => {
2756
+ boton.disabled = !formulario.querySelector("input:checked");
2757
+ };
2758
+ formulario.onchange = actualiza;
2759
+ formulario.onsubmit = async (evento) => {
2760
+ evento.preventDefault();
2761
+ const destinations = todos(
2762
+ "input[name=destination]:checked",
2763
+ formulario
2764
+ ).map((campo) => campo.value);
2765
+ if (!destinations.length) return;
2766
+ boton.disabled = true;
2767
+ const respuesta = await api("/api/reception", {
2768
+ method: "POST",
2769
+ body: JSON.stringify({
2770
+ action: "route",
2771
+ message_id: formulario.dataset.recRoute,
2772
+ destinations
2773
+ })
2774
+ });
2775
+ if (!respuesta.ok) {
2776
+ boton.disabled = false;
2777
+ toast(respuesta.error ?? t("Could not route the message"), true);
2778
+ return;
2779
+ }
2780
+ toast(t("Message routed. Only the selected cities can now read it."));
2781
+ await cargaRecepcion();
2782
+ };
2783
+ });
2784
+ todos("[data-rec-reject]").forEach((formulario) => {
2785
+ formulario.onsubmit = async (evento) => {
2786
+ evento.preventDefault();
2787
+ const boton = q("button[type=submit]", formulario);
2788
+ const reason = q("input[name=reason]", formulario).value.trim();
2789
+ if (!reason) return;
2790
+ boton.disabled = true;
2791
+ const respuesta = await api("/api/reception", {
2792
+ method: "POST",
2793
+ body: JSON.stringify({
2794
+ action: "reject",
2795
+ message_id: formulario.dataset.recReject,
2796
+ reason
2797
+ })
2798
+ });
2799
+ if (!respuesta.ok) {
2800
+ boton.disabled = false;
2801
+ toast(respuesta.error ?? t("Could not reject the message"), true);
2802
+ return;
2803
+ }
2804
+ toast(t("Message rejected. Your reason is queued for encrypted delivery."));
2805
+ await cargaRecepcion();
2806
+ };
2807
+ });
2808
+ }
2809
+ function fechaRecepcion(value) {
2810
+ const date = new Date(value);
2811
+ return Number.isNaN(date.getTime()) ? value : date.toLocaleString(idioma() === "es" ? "es-ES" : "en-GB", {
2812
+ dateStyle: "medium",
2813
+ timeStyle: "short"
2814
+ });
2815
+ }
2816
+ function formateaBytes(bytes) {
2817
+ if (bytes < 1024) return `${bytes} B`;
2818
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
2819
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
2820
+ }
2309
2821
  VISTAS.committee = () => {
2310
2822
  q("#lienzo").innerHTML = `<div><span class="sub">committee</span>
2311
- <h1 style="margin-top:6px">Decisions with a visible chain of custody</h1>
2823
+ <h1 style="margin-top:6px">${t("Decisions with a visible chain of custody")}</h1>
2312
2824
  <p class="prosa" style="margin-top:8px">The seat selects relevant repo agents,
2313
2825
  gathers isolated positions, controls the floor, decides and assigns an
2314
2826
  independent verification. This view is read-only; the vendor-neutral CLI
2315
2827
  drives the protocol.</p></div>
2316
2828
  <div class="orden"><span class="et2">start</span>
2317
- <code>agents-city committee schema open</code>
2829
+ <code>${t("agents-city committee schema open")}</code>
2318
2830
  <button class="bt mini" data-copia="agents-city committee schema open">copy</button>
2319
2831
  </div>
2320
2832
  <div class="gente">${E.deliberations.map(
@@ -2328,7 +2840,7 @@ VISTAS.committee = () => {
2328
2840
  ${act.verifier ? ` \xB7 verifier ${esc2(act.verifier)} ${esc2(act.verification)}` : ""}</span>
2329
2841
  <code class="mono">${esc2(corto(act.act))}</code>
2330
2842
  </div>`
2331
- ).join("") || '<p class="prosa">No committee acts yet. Open one only when the seat needs specialised evidence.</p>'}</div>`;
2843
+ ).join("") || `<p class="prosa">${t("No committee acts yet. Open one only when the seat needs specialised evidence.")}</p>`}</div>`;
2332
2844
  enlaza();
2333
2845
  };
2334
2846
  function opciones2(valores, actual) {
@@ -2350,9 +2862,12 @@ VISTAS.gente = () => {
2350
2862
  const g = a.growth;
2351
2863
  const crecido = Math.min(1, Math.log2(g.floors + 1) / 8);
2352
2864
  const skills = E.skills[a.name]?.skills ?? [];
2353
- const esClaude = a.runtime === "claude";
2354
- const opcionesModelo = opciones2([""].concat(MODELOS), a.model) + '<option value="__otro__">custom\u2026</option>';
2355
- const opcionesEsfuerzo = opciones2([""].concat(ESFUERZOS), a.effort);
2865
+ const motorDelAgente = motorDe(a.runtime);
2866
+ const opcionesModelo = opciones2([""].concat(motorDelAgente.modelos), a.model) + `<option value="__otro__">${t("custom\u2026")}</option>`;
2867
+ const opcionesEsfuerzo = opciones2(
2868
+ [""].concat(ESFUERZOS),
2869
+ motorDelAgente.esfuerzo ? a.effort : ""
2870
+ );
2356
2871
  const opcionesRuntime = opciones2(RUNTIMES, a.runtime);
2357
2872
  const montajes = a.mounts ?? [];
2358
2873
  return `
@@ -2364,22 +2879,21 @@ VISTAS.gente = () => {
2364
2879
  <span class="rpgHogar">${a.legacy ? "repo" : `workspace \xB7 ${montajes.length} ${montajes.length === 1 ? "mount" : "mounts"}`}</span></span></div>
2365
2880
  <span class="rpgBotones">
2366
2881
  <button class="rpgMini rpgDado" type="button" data-agente="${esc2(a.slug)}"
2367
- title="Reroll this agent's face \u2014 deterministic, persisted on the card">\u{1F3B2}</button>
2882
+ title="${t("Reroll this agent's face \u2014 deterministic, persisted on the card")}">\u{1F3B2}</button>
2368
2883
  <button class="rpgMini rpgIns" type="button" data-agente="${esc2(a.slug)}"
2369
- data-file="CLAUDE.md" title="Instructions the Claude runtime reads">CLAUDE.md</button>
2884
+ data-file="CLAUDE.md" title="${t("Instructions the Claude runtime reads")}">CLAUDE.md</button>
2370
2885
  <button class="rpgMini rpgIns" type="button" data-agente="${esc2(a.slug)}"
2371
- data-file="AGENTS.md" title="Instructions Codex, OpenCode and Kimi read">AGENTS.md</button>
2886
+ data-file="AGENTS.md" title="${t("Instructions Codex, OpenCode and Kimi read")}">AGENTS.md</button>
2372
2887
  </span>
2373
2888
  </div>
2374
2889
  <div class="rpgFila"><label>${t("engine")}</label>
2375
2890
  ${barra(motor.ancho, motor.defecto ? "defecto" : "")}
2376
- <select class="rpgSel" data-agente="${esc2(a.slug)}" data-campo="model"
2377
- ${esClaude ? "" : 'disabled title="This window runs its own CLI; its model lives in its own flags"'}>
2891
+ <select class="rpgSel" data-agente="${esc2(a.slug)}" data-campo="model">
2378
2892
  ${opcionesModelo}</select></div>
2379
2893
  <div class="rpgFila"><label>${t("effort")}</label>
2380
2894
  ${barra(esfuerzo / 5, esfuerzo ? "" : "defecto")}
2381
2895
  <select class="rpgSel" data-agente="${esc2(a.slug)}" data-campo="effort"
2382
- ${esClaude ? "" : 'disabled title="This window runs its own CLI; its effort lives in its own flags"'}>
2896
+ ${motorDelAgente.esfuerzo ? "" : `disabled title="${t("This CLI has no effort setting.")}"`}>
2383
2897
  ${opcionesEsfuerzo}</select></div>
2384
2898
  <div class="rpgFila"><label>${t("provider")}</label>
2385
2899
  <span class="rpgDato"><span class="cliDot ${a.cli.connected ? "on" : a.cli.installed ? "idle" : "off"}" title="${esc2(
@@ -2388,7 +2902,7 @@ VISTAS.gente = () => {
2388
2902
  t(a.cli.connected ? "connected" : a.cli.installed ? "idle" : "missing")
2389
2903
  )} \xB7 ${esc2(a.cli.binary)}
2390
2904
  <button class="rpgMini rpgTest" type="button" data-agente="${esc2(a.slug)}"
2391
- title="Run the engine for real: --version, and the login state on Claude">test</button></span>
2905
+ title="${t("Run the engine for real: --version, and the login state on Claude")}">test</button></span>
2392
2906
  <select class="rpgSel" data-agente="${esc2(a.slug)}" data-campo="runtime">
2393
2907
  ${opcionesRuntime}</select></div>
2394
2908
  <div class="rpgSkills"><label>${t("works on")} \xB7 ${montajes.length}</label>
@@ -2397,7 +2911,7 @@ VISTAS.gente = () => {
2397
2911
  data-agente="${esc2(a.slug)}" data-mount="${esc2(m.label)}"
2398
2912
  title="Stop this agent working on ${esc2(m.target)}">\xD7</button>`}</code>`
2399
2913
  ).join(" ") : '<span class="rpgDato">nothing mounted yet</span>'}
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)}"
2914
+ ${a.legacy ? `<span class="rpgDato">${t("a legacy repo agent works on its own repo")}</span>` : `<button class="rpgMini rpgMonta" type="button" data-agente="${esc2(a.slug)}"
2401
2915
  title="A repo, a worktree or a folder of documents \u2014 this agent works on all of them">+ folder</button>`}</div>
2402
2916
  <div class="rpgFila"><label>${t("growth")}</label>
2403
2917
  ${barra(crecido)}
@@ -2405,23 +2919,23 @@ VISTAS.gente = () => {
2405
2919
  <div class="rpgSkills"><label>${t("skills")} \xB7 ${skills.length}</label>
2406
2920
  ${skills.length ? skills.map(
2407
2921
  (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>`
2922
+ data-skill="${esc2(s.dir)}" title="${t("Remove this skill from the agent's home")}">\xD7</button>` : ""}</code>`
2409
2923
  ).join(" ") : '<span class="rpgDato">none discovered</span>'}
2410
2924
  <label class="rpgMini rpgSubir"
2411
- title="Install a skill zip into this agent's own home \u2014 the Claude runtime reads skills; other engines ignore them">
2925
+ title="${t("Install a skill zip into this agent's own home \u2014 the Claude runtime reads skills; other engines ignore them")}">
2412
2926
  + zip<input type="file" accept=".zip" data-agente="${esc2(a.slug)}"></label></div>
2413
2927
  </div>`;
2414
2928
  };
2415
2929
  q("#lienzo").innerHTML = `<div><span class="sub">the houses of ${esc2(E.city_name)}</span>
2416
2930
  <h1 style="margin-top:6px">${t("Who lives in {city}", { city: esc2(E.city_name) })}</h1>
2417
- <p class="prosa" style="margin-top:8px"><b>A house is where an agent lives and works</b>,
2931
+ <p class="prosa" style="margin-top:8px">${t(`<b>A house is where an agent lives and works</b>,
2418
2932
  and many houses are a city \u2014 it is the same thing the map draws, growing with what that
2419
2933
  agent actually does. The card and the CLI call them <code class="mono">agents</code>;
2420
- here you see their houses.</p>
2421
- <p class="prosa">They belong to <b>this</b> city and only
2422
- to it: each one's workspace and its mounts live inside
2423
- <code class="mono">${esc2(corto(E.datos))}/agents/</code>, so another city has its own
2424
- people even if you give them the same names. Every agent is whole here: the kind of work
2934
+ here you see their houses.`)}</p>
2935
+ <p class="prosa">${t(
2936
+ "They belong to <b>this</b> city and only to it: each one\u2019s workspace and its mounts live inside {donde}, so another city has its own people even if you give them the same names.",
2937
+ { donde: `<code class="mono">${esc2(corto(E.datos))}/agents/</code>` }
2938
+ )} Every agent is whole here: the kind of work
2425
2939
  it does, its role, everything it works on \u2014 any number of repositories and document
2426
2940
  folders at once \u2014 the engine and effort that run it, and the skills in its own home.</p>
2427
2941
  <p class="prosa">Every number is real: growth from what the agent actually produced,
@@ -2613,8 +3127,18 @@ VISTAS.gente = () => {
2613
3127
  }
2614
3128
  });
2615
3129
  if (dicho === null || !elegidas.length) return;
2616
- for (const ruta of elegidas)
2617
- await montaje({ agent: boton.dataset.agente ?? "", add: ruta }, t("Mounted"));
3130
+ const agente = boton.dataset.agente ?? "";
3131
+ const fallos = await Promise.all(
3132
+ elegidas.map(
3133
+ (ruta) => api("/api/montaje", {
3134
+ method: "POST",
3135
+ body: JSON.stringify({ agent: agente, add: ruta })
3136
+ }).then((r) => r.ok ? "" : `${ruta}: ${r.error ?? "could not mount"}`).catch((e) => `${ruta}: ${String(e)}`)
3137
+ )
3138
+ );
3139
+ const malos = fallos.filter(Boolean);
3140
+ toast(malos.length ? malos.join("; ") : t("Mounted"), malos.length > 0);
3141
+ void refresca();
2618
3142
  };
2619
3143
  });
2620
3144
  todos(".rpgDesmonta").forEach((boton) => {