agents-city 0.4.0 → 0.5.2

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 (98) 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 +170 -1
  11. package/bin/serve.py +201 -101
  12. package/bin/test +15 -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-diario.py +185 -0
  21. package/bin/test-doctor.py +31 -0
  22. package/bin/test-i18n.py +189 -0
  23. package/bin/test-navegador.py +61 -0
  24. package/bin/test-seat.py +78 -20
  25. package/bin/test-security.py +2 -0
  26. package/bin/test-serve.py +366 -0
  27. package/city/web/dist-hall/hall.js +789 -214
  28. package/city/web/src/bienvenida.ts +14 -13
  29. package/city/web/src/casa.ts +143 -56
  30. package/city/web/src/demo.ts +61 -31
  31. package/city/web/src/es.ts +187 -27
  32. package/city/web/src/explorador.ts +70 -43
  33. package/city/web/src/hall.ts +520 -103
  34. package/city/web/src/idioma.ts +21 -1
  35. package/city/web/src/motores.ts +48 -1
  36. package/city/web/src/vista.ts +58 -0
  37. package/demo/graba.py +5 -10
  38. package/docs/managed-connect.md +237 -0
  39. package/docs/security.md +41 -2
  40. package/package.json +8 -4
  41. package/plugin/.claude-plugin/plugin.json +1 -1
  42. package/plugin/channel/adapter-prompts.ts +10 -0
  43. package/plugin/channel/adapter.js +78 -31
  44. package/plugin/channel/agents_city_hybrid_crypto_bg.wasm +0 -0
  45. package/plugin/channel/bus.js +109 -65
  46. package/plugin/channel/bus.ts +11 -1
  47. package/plugin/channel/client.js +67 -30
  48. package/plugin/channel/delivery-queue.ts +160 -21
  49. package/plugin/channel/hub/remote-roads.ts +32 -1
  50. package/plugin/channel/hub/road-controller.ts +116 -16
  51. package/plugin/channel/kinsh_vodozemac_wasm_bg.wasm +0 -0
  52. package/plugin/channel/licenses/CONNECT_CLIENT_THIRD_PARTY_NOTICES.md +21 -0
  53. package/plugin/channel/licenses/HYBRID_CRYPTO_THIRD_PARTY_NOTICES.md +12 -0
  54. package/plugin/channel/licenses/hybrid-crypto-Apache-2.0.txt +201 -0
  55. package/plugin/channel/licenses/keyring-MIT.txt +21 -0
  56. package/plugin/channel/licenses/vodozemac-Apache-2.0.txt +201 -0
  57. package/plugin/channel/local-hub.js +2080 -97
  58. package/plugin/channel/local-hub.ts +5 -4
  59. package/plugin/channel/managed-connect/bridge.ts +208 -0
  60. package/plugin/channel/managed-connect/cli.ts +416 -0
  61. package/plugin/channel/managed-connect/device.ts +15 -0
  62. package/plugin/channel/managed-connect/local-cities.ts +94 -0
  63. package/plugin/channel/managed-connect/person-message.ts +74 -0
  64. package/plugin/channel/managed-connect/reception-bridge.ts +341 -0
  65. package/plugin/channel/managed-connect/relay-session.ts +7 -0
  66. package/plugin/channel/managed-connect/storage.ts +653 -0
  67. package/plugin/channel/managed-connect/transport.ts +87 -0
  68. package/plugin/channel/managed-connect-cli.js +4777 -0
  69. package/plugin/channel/managed-connect-cli.ts +7 -0
  70. package/plugin/channel/managed-connect-client.d.ts +261 -0
  71. package/plugin/channel/managed-connect-client.js +6572 -0
  72. package/plugin/channel/managed-connect-client.manifest.json +38 -0
  73. package/plugin/channel/package-lock.json +123 -105
  74. package/plugin/channel/package.json +4 -4
  75. package/plugin/channel/protocol.ts +4 -0
  76. package/plugin/channel/reception.ts +896 -0
  77. package/plugin/channel/road-cli.ts +1 -1
  78. package/plugin/channel/runtime/arnes.json +189 -0
  79. package/plugin/channel/runtime/arnes.ts +47 -0
  80. package/plugin/channel/runtime/claude.ts +18 -0
  81. package/plugin/channel/runtime/codex-config.ts +51 -1
  82. package/plugin/channel/runtime/codex.ts +31 -11
  83. package/plugin/channel/runtime/kimi.ts +6 -4
  84. package/plugin/channel/runtime-files.ts +39 -5
  85. package/plugin/channel/runtime-gateway.js +426 -100
  86. package/plugin/channel/runtime-gateway.ts +12 -0
  87. package/plugin/channel/trust/agents-city-sandbox-roots.json +66 -0
  88. package/plugin/scripts/arnes.py +271 -0
  89. package/plugin/scripts/busca.py +46 -10
  90. package/plugin/scripts/cage.py +5 -0
  91. package/plugin/scripts/city-session.sh +144 -28
  92. package/plugin/scripts/crecimiento.py +4 -1
  93. package/plugin/scripts/demos.py +58 -43
  94. package/plugin/scripts/desinstala.py +36 -21
  95. package/plugin/scripts/diario.py +119 -0
  96. package/plugin/scripts/doctor.py +90 -15
  97. package/plugin/scripts/read-card.py +48 -8
  98. 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,29 +626,44 @@ 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
+ this.pintaRoles();
450
649
  } catch {
451
650
  this.roles = [];
452
651
  }
453
652
  }
653
+ /** The role list, once it arrives, without touching anything else. */
654
+ pintaRoles() {
655
+ const sel = this.host?.querySelector("#bvRol");
656
+ if (!sel || !this.roles.length) return;
657
+ sel.innerHTML = this.opcionesDeRol();
658
+ sel.value = this.datos.rol;
659
+ sel.onchange = () => this.recoge();
660
+ this.recoge();
661
+ }
662
+ opcionesDeRol() {
663
+ return this.roles.map(
664
+ (r) => `<option value="${this.p.esc(r.id)}"${r.id === this.datos.rol ? " selected" : ""}>${this.p.esc(r.name)}</option>`
665
+ ).join("");
666
+ }
454
667
  /** The folder picker, built once so walking survives a repaint. */
455
668
  explorador = null;
456
669
  explora() {
@@ -461,7 +674,7 @@ var FormularioDeCasa = class {
461
674
  const ya = this.datos.montajes.indexOf(ruta);
462
675
  if (ya >= 0) this.datos.montajes.splice(ya, 1);
463
676
  else this.datos.montajes.push(ruta);
464
- this.repinta();
677
+ this.pintaElegidas();
465
678
  },
466
679
  () => this.datos.montajes
467
680
  );
@@ -479,7 +692,7 @@ var FormularioDeCasa = class {
479
692
  const roles = this.roles.map(
480
693
  (r) => `<option value="${esc3(r.id)}"${r.id === a.rol ? " selected" : ""}>${esc3(r.name)}</option>`
481
694
  ).join("");
482
- const esClaude = (a.runtime || "claude") === "claude";
695
+ const motor = motorDe(a.runtime);
483
696
  return `
484
697
  <div class="campo"><label>${t("What do you call it?")}</label>
485
698
  <input type="text" id="bvNombre" value="${esc3(a.nombre)}"
@@ -490,45 +703,69 @@ var FormularioDeCasa = class {
490
703
  <select id="bvRol">${roles}</select></div>
491
704
  <label class="bvEtiqueta">${t("What runs it?")}</label>
492
705
  <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."
706
+ "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
707
  )}</p>
495
708
  <div class="bvMotor">
496
709
  <div class="campo"><label>${t("provider")}</label>
497
710
  <select id="bvRuntime">${opciones(RUNTIMES, a.runtime, esc3, "claude")}</select></div>
498
711
  <div class="campo"><label>${t("engine")}</label>
499
- <select id="bvModelo" ${esClaude ? "" : "disabled"}>${opciones(
500
- [""].concat(MODELOS),
712
+ ${motor.modelos.length ? `<select id="bvModelo">${opciones(
713
+ [""].concat(motor.modelos),
501
714
  a.modelo,
502
715
  esc3,
503
716
  t("default")
504
- )}</select></div>
717
+ )}</select>` : `<input type="text" id="bvModelo" value="${esc3(a.modelo)}"
718
+ placeholder="${t("default")}" autocomplete="off" spellcheck="false">`}</div>
505
719
  <div class="campo"><label>${t("effort")}</label>
506
- <select id="bvEsfuerzo" ${esClaude ? "" : "disabled"}>${opciones(
720
+ <select id="bvEsfuerzo" ${motor.esfuerzo ? "" : "disabled"}
721
+ title="${motor.esfuerzo ? "" : t("This CLI has no effort setting.")}">${opciones(
507
722
  [""].concat(ESFUERZOS),
508
- a.esfuerzo,
723
+ motor.esfuerzo ? a.esfuerzo : "",
509
724
  esc3,
510
725
  t("default")
511
726
  )}</select></div>
512
727
  </div>
728
+ <p class="pista">${esc3(t(motor.pista))}</p>
513
729
  <label class="bvEtiqueta">${t("What does it work on?")}</label>
514
730
  <p class="pista">${t(`Walk your disk and pick whatever you like: a repository, a
515
731
  worktree, a folder of documents, one exact file. As many as you want. Nothing is
516
732
  copied \u2014 each one is linked into this agent's own home.`)}</p>
517
733
  <div class="casaExp"></div>
518
- ${a.montajes.length ? `<div class="expElegidas"><label class="bvEtiqueta">${t("Working on")}</label>
519
- ${a.montajes.map(
734
+ <div id="casaElegidas">${this.elegidas()}</div>`;
735
+ }
736
+ /** The mounts chosen so far, as chips you can take back off. Rendered on its
737
+ * own so ticking a row does not rebuild the picker underneath the cursor. */
738
+ elegidas() {
739
+ const esc3 = this.p.esc;
740
+ const montajes = this.datos.montajes;
741
+ if (!montajes.length)
742
+ return `<p class="pista">${t("Nothing chosen yet \u2014 an agent with no mounts is fine too.")}</p>`;
743
+ return `<div class="expElegidas"><label class="bvEtiqueta">${t("Working on")}</label>
744
+ ${montajes.map(
520
745
  (m) => `<button type="button" class="bvChip elegida" data-bv="quita"
521
- data-ruta="${esc3(m)}" title="${esc3(m)}">${esc3(
746
+ data-ruta="${esc3(m)}" title="${esc3(m)}">${esc3(
522
747
  m.split("/").pop() || m
523
748
  )}<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>`}`;
749
+ ).join("")}</div>`;
750
+ }
751
+ pintaElegidas() {
752
+ const hueco = this.host?.querySelector("#casaElegidas");
753
+ if (!hueco) return;
754
+ hueco.innerHTML = this.elegidas();
755
+ this.enlaza(hueco);
525
756
  }
526
757
  enlaza(raiz) {
758
+ raiz.querySelectorAll(
759
+ "#bvNombre,#bvRol,#bvModelo,#bvEsfuerzo"
760
+ ).forEach((el) => {
761
+ el.oninput = () => this.recoge();
762
+ el.onchange = () => this.recoge();
763
+ });
527
764
  raiz.querySelectorAll("[data-bv]").forEach((el) => {
528
765
  el.onclick = (evento) => {
529
766
  evento.preventDefault();
530
767
  const que = el.dataset.bv;
531
- this.recoge(raiz);
768
+ this.recoge();
532
769
  if (que === "clase") this.datos.clase = el.dataset.id ?? "code";
533
770
  else if (que === "quita") {
534
771
  const ya = this.datos.montajes.indexOf(el.dataset.ruta ?? "");
@@ -538,12 +775,19 @@ var FormularioDeCasa = class {
538
775
  };
539
776
  });
540
777
  raiz.querySelector("#bvRuntime")?.addEventListener("change", () => {
541
- this.recoge(raiz);
778
+ this.recoge();
542
779
  this.repinta();
543
780
  });
544
781
  }
545
- /** Keep what is typed when a click forces a repaint. */
546
- recoge(raiz) {
782
+ /** Keep what is typed when a click forces a repaint.
783
+ *
784
+ * Always from the form's own root, never from whatever element the click
785
+ * came through: the chosen-chips strip re-uses this wiring, and reading the
786
+ * fields relative to that strip would find none of them and quietly throw
787
+ * away everything the person had typed. */
788
+ recoge() {
789
+ const raiz = this.host;
790
+ if (!raiz) return;
547
791
  const nombre = raiz.querySelector("#bvNombre");
548
792
  const rol = raiz.querySelector("#bvRol");
549
793
  if (nombre) this.datos.nombre = nombre.value;
@@ -554,7 +798,7 @@ var FormularioDeCasa = class {
554
798
  ["#bvEsfuerzo", "esfuerzo"]
555
799
  ]) {
556
800
  const sel = raiz.querySelector(id);
557
- if (sel) this.datos[campo] = sel.value;
801
+ if (sel) this.datos[campo] = sel.value.trim();
558
802
  }
559
803
  }
560
804
  /**
@@ -566,7 +810,7 @@ var FormularioDeCasa = class {
566
810
  * another's job just to save a round trip.
567
811
  */
568
812
  async guarda() {
569
- if (this.host) this.recoge(this.host);
813
+ this.recoge();
570
814
  const a = this.datos;
571
815
  const nombre = a.nombre.trim();
572
816
  if (!nombre) {
@@ -582,13 +826,15 @@ var FormularioDeCasa = class {
582
826
  this.p.aviso(r.error || t("Could not add that agent"), true);
583
827
  return null;
584
828
  }
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
- }
829
+ const montados = await Promise.all(
830
+ a.montajes.map(
831
+ (ruta) => this.p.api("/api/montaje", {
832
+ method: "POST",
833
+ body: JSON.stringify({ agent: r.agent, add: ruta })
834
+ }).then((m) => m.ok ? "" : `${ruta}: ${m.error ?? t("could not mount")}`).catch((e) => `${ruta}: ${String(e)}`)
835
+ )
836
+ );
837
+ for (const fallo of montados.filter(Boolean)) this.p.aviso(fallo, true);
592
838
  const motor = {};
593
839
  if (a.runtime) motor.runtime = a.runtime;
594
840
  if (a.modelo) motor.model = a.modelo;
@@ -725,8 +971,9 @@ var VELOCIDADES = [
725
971
  ["2\xD7", 700],
726
972
  ["4\xD7", 350]
727
973
  ];
728
- var Demos = class {
974
+ var Demos = class extends Montada {
729
975
  constructor(p) {
976
+ super();
730
977
  this.p = p;
731
978
  }
732
979
  catalogo = [];
@@ -736,15 +983,13 @@ var Demos = class {
736
983
  corriendo = false;
737
984
  velocidad = 1450;
738
985
  reloj = null;
739
- host = null;
740
986
  monta(host) {
741
- this.host = host;
742
987
  if (!this.catalogo.length && !this.cargando) void this.lee();
743
- this.repinta();
988
+ super.monta(host);
744
989
  }
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() {
990
+ /** Stop the clock. The router calls this when the section is left: a timer
991
+ * still firing into a detached node is a leak with a soundtrack. */
992
+ desmonta() {
748
993
  this.corriendo = false;
749
994
  if (this.reloj !== null) {
750
995
  clearTimeout(this.reloj);
@@ -752,13 +997,9 @@ var Demos = class {
752
997
  }
753
998
  }
754
999
  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
- }
1000
+ super.repinta();
1001
+ const lista = this.elegida ? this.host?.querySelector("#demoLista") : null;
1002
+ if (lista) lista.scrollTop = lista.scrollHeight;
762
1003
  }
763
1004
  async lee() {
764
1005
  this.cargando = true;
@@ -773,7 +1014,7 @@ var Demos = class {
773
1014
  }
774
1015
  }
775
1016
  async abre(id) {
776
- this.para();
1017
+ this.desmonta();
777
1018
  this.hasta = 0;
778
1019
  this.elegida = null;
779
1020
  this.repinta();
@@ -797,17 +1038,38 @@ var Demos = class {
797
1038
  if (this.reloj !== null) clearTimeout(this.reloj);
798
1039
  this.reloj = window.setTimeout(() => {
799
1040
  if (!this.corriendo || !this.elegida) return;
1041
+ const evento = this.elegida.eventos[this.hasta];
800
1042
  this.hasta += 1;
801
- if (this.hasta >= this.elegida.eventos.length) {
1043
+ const final = this.hasta >= this.elegida.eventos.length;
1044
+ if (final) {
802
1045
  this.hasta = this.elegida.eventos.length;
803
1046
  this.corriendo = false;
804
- this.repinta();
805
- return;
806
1047
  }
807
- this.repinta();
808
- this.siguiente();
1048
+ if (evento && !final) this.anade(evento);
1049
+ else this.repinta();
1050
+ if (!final) this.siguiente();
809
1051
  }, this.velocidad);
810
1052
  }
1053
+ /** One more turn on screen, without taking the screen down. */
1054
+ anade(evento) {
1055
+ const lista = this.host?.querySelector("#demoLista");
1056
+ if (!lista || !this.elegida) return this.repinta();
1057
+ const vacia = lista.querySelector(".liveEmpty");
1058
+ if (vacia) lista.innerHTML = "";
1059
+ lista.insertAdjacentHTML("beforeend", this.p.pinta(evento));
1060
+ lista.scrollTop = lista.scrollHeight;
1061
+ this.marcador();
1062
+ }
1063
+ /** The counter and the bar, which are the only other things a turn moves. */
1064
+ marcador() {
1065
+ if (!this.host || !this.elegida) return;
1066
+ const total = this.elegida.eventos.length;
1067
+ const hecho = Math.min(this.hasta, total);
1068
+ const cuenta = this.host.querySelector(".demoCuenta");
1069
+ if (cuenta) cuenta.textContent = t("{done} of {total}", { done: hecho, total });
1070
+ const barra2 = this.host.querySelector(".demoBarra i");
1071
+ if (barra2) barra2.style.width = `${total ? hecho / total * 100 : 0}%`;
1072
+ }
811
1073
  html() {
812
1074
  if (this.elegida) return this.reproductor(this.elegida);
813
1075
  if (this.cargando) return `<p class="cargando">${t("reading the demo shelf")}</p>`;
@@ -848,9 +1110,10 @@ var Demos = class {
848
1110
  </div>
849
1111
  <div class="demoMandos">
850
1112
  <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>
1113
+ ${// Once it has finished, the primary button already says Replay and
1114
+ // does exactly this. Two identical controls side by side is a person
1115
+ // wondering which one is the real one.
1116
+ hecho && !acabado ? `<button type="button" class="bt" data-demo="replay">\u21BA ${t("Replay")}</button>` : ""}
854
1117
  <span class="demoVel">${VELOCIDADES.map(
855
1118
  ([etiqueta, ms]) => `<button type="button" class="demoVelBoton ${ms === this.velocidad ? "aqui" : ""}" data-demo="vel" data-ms="${ms}">${esc3(etiqueta)}</button>`
856
1119
  ).join("")}</span>
@@ -870,7 +1133,7 @@ var Demos = class {
870
1133
  void this.abre(el.dataset.id ?? "");
871
1134
  break;
872
1135
  case "atras":
873
- this.para();
1136
+ this.desmonta();
874
1137
  this.elegida = null;
875
1138
  this.hasta = 0;
876
1139
  this.repinta();
@@ -879,11 +1142,11 @@ var Demos = class {
879
1142
  this.arranca();
880
1143
  break;
881
1144
  case "pausa":
882
- this.para();
1145
+ this.desmonta();
883
1146
  this.repinta();
884
1147
  break;
885
1148
  case "replay":
886
- this.para();
1149
+ this.desmonta();
887
1150
  this.hasta = 0;
888
1151
  this.arranca();
889
1152
  break;
@@ -902,7 +1165,7 @@ var Demos = class {
902
1165
  // city/web/src/bienvenida.ts
903
1166
  var PASOS = ["Welcome", "The work", "Your chair", "The houses", "Ready"];
904
1167
  function casita(color) {
905
- return `<svg viewBox="0 0 120 108" role="img" aria-label="an isometric house" class="bvCasa">
1168
+ return `<svg viewBox="0 0 120 108" role="img" aria-label="${t("an isometric house")}" class="bvCasa">
906
1169
  <polygon points="60,10 112,40 60,70 8,40" fill="${color}" opacity=".95"/>
907
1170
  <polygon points="8,40 60,70 60,100 8,70" fill="${color}" opacity=".62"/>
908
1171
  <polygon points="112,40 60,70 60,100 112,70" fill="${color}" opacity=".40"/>
@@ -922,7 +1185,17 @@ var Bienvenida = class {
922
1185
  enCurso = null;
923
1186
  guardando = false;
924
1187
  lienzo = null;
925
- /** Draw the current step into `lienzo`. The only entry point. */
1188
+ /**
1189
+ * The view contract, so the Hall's dispatcher treats every section the same.
1190
+ *
1191
+ * The painting itself is async — the first one asks the server for the domain
1192
+ * packs — and a router cannot wait on a view. It does not need to: the guide
1193
+ * draws a loading line first and fills itself in.
1194
+ */
1195
+ monta(host) {
1196
+ void this.pinta(host);
1197
+ }
1198
+ /** Draw the current step into `lienzo`. */
926
1199
  async pinta(lienzo) {
927
1200
  this.lienzo = lienzo;
928
1201
  if (!this.dominios.length) {
@@ -1076,16 +1349,6 @@ var Bienvenida = class {
1076
1349
  </div>
1077
1350
  </div>`;
1078
1351
  }
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
1352
  // ── 4. done ───────────────────────────────────────────────────────────────
1090
1353
  pantallaFinal() {
1091
1354
  return `
@@ -1234,14 +1497,37 @@ function isSpeechEvent(event) {
1234
1497
  // city/web/src/hall.ts
1235
1498
  var PASE = window.PASE;
1236
1499
  var CIUDAD = new URLSearchParams(location.search).get("city") ?? "";
1500
+ function anota2(que, detalle, donde) {
1501
+ try {
1502
+ let u = "/api/diario?PASE=" + encodeURIComponent(PASE);
1503
+ if (CIUDAD) u += "&city=" + encodeURIComponent(CIUDAD);
1504
+ void fetch(u, {
1505
+ method: "POST",
1506
+ headers: { "X-City-Pase": PASE, "Content-Type": "application/json" },
1507
+ body: JSON.stringify({ que, detalle, donde: donde ?? location.hash ?? "" }),
1508
+ keepalive: true
1509
+ }).catch(() => {
1510
+ });
1511
+ } catch {
1512
+ }
1513
+ }
1237
1514
  async function api(ruta, opts) {
1238
1515
  let u = ruta + (ruta.includes("?") ? "&" : "?") + "PASE=" + encodeURIComponent(PASE);
1239
1516
  if (CIUDAD) u += "&city=" + encodeURIComponent(CIUDAD);
1240
- const r = await fetch(u, {
1241
- headers: { "X-City-Pase": PASE, "Content-Type": "application/json" },
1242
- ...opts
1243
- });
1244
- return r.json();
1517
+ let r;
1518
+ try {
1519
+ r = await fetch(u, {
1520
+ headers: { "X-City-Pase": PASE, "Content-Type": "application/json" },
1521
+ ...opts
1522
+ });
1523
+ } catch (e) {
1524
+ if (ruta !== "/api/diario") anota2("fetch failed", String(e), ruta);
1525
+ throw e;
1526
+ }
1527
+ const cuerpo = await r.json();
1528
+ if (ruta !== "/api/diario" && (!r.ok || cuerpo?.error))
1529
+ anota2("api refused", { estado: r.status, error: cuerpo?.error }, ruta);
1530
+ return cuerpo;
1245
1531
  }
1246
1532
  function q(sel, raiz = document) {
1247
1533
  const el = raiz.querySelector(sel);
@@ -1276,6 +1562,7 @@ var SECCIONES = [
1276
1562
  ["mapa", "The map"],
1277
1563
  ["puesto", "My seat"],
1278
1564
  ["barrios", "Districts"],
1565
+ ["recepcion", "Reception"],
1279
1566
  ["red", "Roads"],
1280
1567
  ["committee", "Committee"],
1281
1568
  ["gente", "Houses"],
@@ -1313,7 +1600,7 @@ function pintaDemoControles() {
1313
1600
  if (previa) return;
1314
1601
  const caja = document.createElement("div");
1315
1602
  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>`;
1603
+ 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
1604
  header.appendChild(caja);
1318
1605
  const pausa = q("#demoPausa", caja);
1319
1606
  const manda = (action) => api("/api/demo", {
@@ -1440,14 +1727,14 @@ function pintaActividad(seguir = false) {
1440
1727
  const workToggle = document.querySelector("#liveWorkToggle");
1441
1728
  if (!dot || !state || !list || !filter || !context || !workToggle) return;
1442
1729
  dot.classList.toggle("on", liveConnected);
1443
- state.textContent = liveConnected ? "websocket live" : liveUrl ? "reconnecting" : "session offline";
1730
+ state.textContent = liveConnected ? t("websocket live") : liveUrl ? t("reconnecting") : t("session offline");
1444
1731
  const opened = /* @__PURE__ */ new Map();
1445
1732
  for (const event of liveEvents) {
1446
1733
  if (event.thread && (event.kind === "committee.opened" || event.kind === "conversation.user") && !esPromptDeTransporte(event) && !opened.has(event.thread)) {
1447
1734
  opened.set(event.thread, event.summary);
1448
1735
  }
1449
1736
  }
1450
- filter.innerHTML = '<option value="">all conversations</option>' + [...opened.entries()].reverse().map(
1737
+ filter.innerHTML = `<option value="">${t("all conversations")}</option>` + [...opened.entries()].reverse().map(
1451
1738
  ([thread, question]) => `<option value="${esc2(thread)}">${esc2(question.slice(0, 54) || thread)}</option>`
1452
1739
  ).join("");
1453
1740
  const selected = liveSelectedThread || "";
@@ -1459,7 +1746,7 @@ function pintaActividad(seguir = false) {
1459
1746
  };
1460
1747
  workToggle.classList.toggle("on", liveShowWork);
1461
1748
  workToggle.setAttribute("aria-pressed", String(liveShowWork));
1462
- workToggle.textContent = liveShowWork ? "hide work" : "show work";
1749
+ workToggle.textContent = liveShowWork ? t("hide work") : t("show work");
1463
1750
  workToggle.onclick = () => {
1464
1751
  liveShowWork = !liveShowWork;
1465
1752
  pintaActividad(true);
@@ -1470,7 +1757,7 @@ function pintaActividad(seguir = false) {
1470
1757
  (event) => !esPromptDeTransporte(event) && (liveShowWork || !esRuidoDeTrabajo(event) || event.tone === "error")
1471
1758
  );
1472
1759
  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>`;
1760
+ 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
1761
  return;
1475
1762
  }
1476
1763
  const wasNearBottom = list.scrollHeight - list.scrollTop - list.clientHeight < 90;
@@ -1487,7 +1774,7 @@ function renderActividad(event) {
1487
1774
  const compact = resumenVisible(summary);
1488
1775
  const long = compact !== summary;
1489
1776
  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>` : "";
1777
+ const full = long ? `<details class="liveFull"><summary>${t("read full message")}</summary><p>${esc2(summary)}</p></details>` : "";
1491
1778
  return `<li class="liveTurn ${side} ${esc2(event.tone)}">
1492
1779
  <div class="liveAvatar${E?.avatars?.[event.actor] ? " conCara" : ""}"
1493
1780
  style="--actor-hue:${tonoActor(event.actor)}" aria-hidden="true">
@@ -1529,7 +1816,7 @@ function pintaContexto(thread, container) {
1529
1816
  (actor) => `<span class="livePerson${E?.avatars?.[actor] ? " conCara" : ""}" title="${esc2(
1530
1817
  actor === "seat" ? "seat \xB7 chair" : `${actor} \xB7 ${rolOperativo(actor) || "member"}`
1531
1818
  )}" style="--actor-hue:${tonoActor(actor)}">${cara(actor, actor === "seat")}</span>`
1532
- ).join("")}</div><span class="liveContextText"><b>seat moderates</b> \xB7 ${esc2(
1819
+ ).join("")}</div><span class="liveContextText"><b>${t("seat moderates")}</b> \xB7 ${esc2(
1533
1820
  received
1534
1821
  )}/${esc2(total)} positions \xB7 ${esc2(status)}</span>`;
1535
1822
  }
@@ -1610,23 +1897,32 @@ function rail() {
1610
1897
  <b>${esc2(E.city_name)}</b>
1611
1898
  <span class="railDe">${esc2(E.domain)} \xB7 ${plural(cuantos, "{n} house", "{n} houses")}</span>`;
1612
1899
  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}">
1900
+ 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) : "";
1901
+ return `<li class="${id === SECCION ? "aqui" : ""}" data-s="${id}" role="button"
1902
+ tabindex="0" ${id === SECCION ? 'aria-current="page"' : ""}>
1615
1903
  <span>${esc2(t(et))}</span>${n ? `<span class="n">${n}</span>` : ""}</li>`;
1616
1904
  }).join("");
1617
- q("#railOtras").innerHTML = `<li class="${SECCION === "ciudades" ? "aqui" : ""}" data-s="ciudades">
1905
+ q("#railOtras").innerHTML = `<li class="${SECCION === "ciudades" ? "aqui" : ""}" data-s="ciudades" role="button"
1906
+ tabindex="0" ${SECCION === "ciudades" ? 'aria-current="page"' : ""}>
1618
1907
  <span>${t("All cities")}</span><span class="n">${E.ciudades.length}</span></li>`;
1619
1908
  todos("#rail li,#railOtras li").forEach((li) => {
1620
- li.onclick = () => {
1909
+ const abre = () => {
1621
1910
  document.body.classList.remove("enGuia");
1622
1911
  SECCION = li.dataset.s ?? "mapa";
1623
1912
  pinta();
1624
1913
  };
1914
+ li.onclick = abre;
1915
+ li.onkeydown = (event) => {
1916
+ if (event.key !== "Enter" && event.key !== " ") return;
1917
+ event.preventDefault();
1918
+ abre();
1919
+ };
1625
1920
  });
1626
1921
  q("#dondeDatos").textContent = corto(E.datos);
1627
1922
  const cs = q("#ciudades");
1628
1923
  if (E.ciudades.length > 1) {
1629
- cs.innerHTML = `<select id="cambiaCiudad" title="which city this hall manages">
1924
+ cs.innerHTML = `<select id="cambiaCiudad" aria-label="${t("which city this hall manages")}"
1925
+ title="${t("which city this hall manages")}">
1630
1926
  ${E.ciudades.map(
1631
1927
  (c) => `<option value="${esc2(c.ruta)}" ${c.actual ? "selected" : ""}>
1632
1928
  ${esc2(c.nombre)}</option>`
@@ -1668,8 +1964,10 @@ function rail() {
1668
1964
  location.href = "/?PASE=" + encodeURIComponent(PASE) + "&city=" + encodeURIComponent(r.city);
1669
1965
  };
1670
1966
  }
1967
+ var montada = null;
1671
1968
  function pinta() {
1672
- if (SECCION !== "demos") demos?.para();
1969
+ montada?.desmonta?.();
1970
+ montada = null;
1673
1971
  rail();
1674
1972
  q(".cuerpo").style.padding = "";
1675
1973
  q("#lienzo").style.maxWidth = "";
@@ -1694,7 +1992,8 @@ VISTAS.bienvenida = () => {
1694
1992
  yo: E.yo,
1695
1993
  datos: corto(E.datos)
1696
1994
  });
1697
- void guia.pinta(q("#lienzo"));
1995
+ guia.monta(q("#lienzo"));
1996
+ montada = guia;
1698
1997
  };
1699
1998
  VISTAS.ciudades = () => {
1700
1999
  const filas = E.ciudades.map(
@@ -1712,23 +2011,24 @@ VISTAS.ciudades = () => {
1712
2011
  ).join("");
1713
2012
  q("#lienzo").innerHTML = `<div><span class="sub">cities</span>
1714
2013
  <h1 style="margin-top:6px">${t("Your cities")}</h1>
1715
- <p class="prosa" style="margin-top:8px">One person, several autonomous cities: each has
2014
+ <p class="prosa" style="margin-top:8px">${t(`One person, several autonomous cities: each has
1716
2015
  its own identity, domain, chair, <b>its own houses</b> and its own roads. A house is not
1717
2016
  shared \u2014 it stands inside the city that owns it, with its workspace and mounts under that
1718
2017
  city's folder \u2014 so two cities can each have a <code class="mono">docs</code> house and
1719
2018
  they are two different workers. They share nothing unless you build a road between
1720
- them.</p></div>
2019
+ them.`)}</p></div>
1721
2020
  <div class="lista"><div class="filas">${filas}</div></div>
1722
2021
  <div class="campos" style="max-width:420px">
1723
2022
  <div class="campo"><label>${t("Start another city")}</label>
1724
- <input type="text" id="nuevaCiudad" placeholder="client-a, research, the book\u2026"></div>
2023
+ <input type="text" id="nuevaCiudad" placeholder="${t("client-a, research, the book\u2026")}"></div>
1725
2024
  <button class="bt ppal" id="creaCiudad">Create it</button>
1726
2025
  </div>
1727
2026
  <div class="peligro">
1728
2027
  <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>
2028
+ <p class="prosa">${t(
2029
+ "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.",
2030
+ { city: `<b>${esc2(E.city_name)}</b>` }
2031
+ )}</p>
1732
2032
  <button class="bt malo" id="reiniciaCiudad">${t("Start over\u2026")}</button>
1733
2033
  </div>
1734
2034
  <p class="pista">Archiving <b>moves</b> a city into
@@ -1819,9 +2119,10 @@ VISTAS.ciudades = () => {
1819
2119
  };
1820
2120
  var demos = null;
1821
2121
  VISTAS.demos = () => {
1822
- if (!demos) demos = new Demos({ api, esc: esc2, pinta: renderActividad });
2122
+ demos ??= new Demos({ api, esc: esc2, pinta: renderActividad });
1823
2123
  q("#lienzo").innerHTML = '<div id="demoHueco"></div>';
1824
2124
  demos.monta(q("#demoHueco"));
2125
+ montada = demos;
1825
2126
  };
1826
2127
  VISTAS.resumen = () => {
1827
2128
  const mia = E.tarjetas.find((t2) => t2.user === E.yo);
@@ -1870,19 +2171,19 @@ VISTAS.resumen = () => {
1870
2171
  <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
2172
  counted with <code class="mono">${esc2(E.grow || "nothing yet")}</code>.</p>`}</div>
1872
2173
  <div class="cifras">
1873
- <div class="cifra"><b>${Object.keys(E.skills).length}</b><span>repo agents</span></div>
2174
+ <div class="cifra"><b>${Object.keys(E.skills).length}</b><span>${t("repo agents")}</span></div>
1874
2175
  <div class="cifra"><b>${E.parcelas.length}</b><span>houses</span></div>
1875
2176
  <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>
2177
+ <div class="cifra"><b>${E.deliberations.length}</b><span>${t("committee acts")}</span></div>
2178
+ <div class="cifra"><b>${Object.values(E.skills).reduce((n, a) => n + a.skills.length, 0)}</b><span>${t("skills recognised")}</span></div>
1878
2179
  </div>
1879
2180
  <div class="luces">
1880
- <span class="luz ${E.tarjetas.length ? "on" : ""}">data repo</span>
2181
+ <span class="luz ${E.tarjetas.length ? "on" : ""}">${t("data repo")}</span>
1881
2182
  <span class="luz ${E.gh ? "on" : "neutra"}">github${E.gh ? "" : " \u2014 optional"}</span>
1882
- <span class="luz ${E.plugin ? "on" : ""}">plugin installed</span>
2183
+ <span class="luz ${E.plugin ? "on" : ""}">${t("plugin installed")}</span>
1883
2184
  <span class="luz ${sesionArriba ? "on" : "neutra"}">tmux session${sesionArriba ? " up" : ""}</span>
1884
2185
  </div>
1885
- <div><span class="sub">what is left</span>
2186
+ <div><span class="sub">${t("what is left")}</span>
1886
2187
  <div class="tareas" style="margin-top:9px">${tareas.map(
1887
2188
  (t2) => `
1888
2189
  <div class="tarea ${t2.ok ? "hecha" : ""}">${t2.txt}
@@ -1891,9 +2192,9 @@ VISTAS.resumen = () => {
1891
2192
  </div></div>
1892
2193
  <div><span class="sub">work</span>
1893
2194
  <div style="display:flex;flex-direction:column;gap:9px;margin-top:9px">
1894
- <div class="orden"><span class="et2">your day</span>
2195
+ <div class="orden"><span class="et2">${t("your day")}</span>
1895
2196
  <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>'}
2197
+ ${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
2198
  </div>
1898
2199
  <div class="orden"><span class="et2">the map</span>
1899
2200
  <code>${E.mapa ? esc2(E.mapa) : "not drawn yet"}</code>
@@ -1986,26 +2287,26 @@ VISTAS.puesto = () => {
1986
2287
  };
1987
2288
  async function puesto() {
1988
2289
  const mia = E.tarjetas.find((t2) => t2.user === E.yo);
1989
- q("#lienzo").innerHTML = `<div><span class="sub">my seat</span>
2290
+ q("#lienzo").innerHTML = `<div><span class="sub">${t("my seat")}</span>
1990
2291
  <h1 style="margin-top:6px">${esc2(E.yo)}</h1>
1991
2292
  <p class="prosa" style="margin-top:8px">This is your chair: the work domain, your
1992
2293
  role inside it, and one goal. The seat stays the boss even when its
1993
2294
  professional role is blank. Saving writes your card \u2014
1994
2295
  <code class="mono">${esc2(corto(E.datos))}/${esc2(E.yo)}.md</code> \u2014 the same file
1995
2296
  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>
2297
+ <div><span class="sub">${t("work domain")}</span><div class="rolejilla" id="domains" style="margin-top:9px">
2298
+ <p class="cargando">${t("reading the domain packs")}</p></div></div>
1998
2299
  <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>
2300
+ <p class="cargando">${t("reading the role files")}</p></div></div>
2301
+ <div><span class="sub">${t("the agents")}</span>
2001
2302
  <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
2303
+ one works on \u2014 lives in <b>${t("Agents &amp; skills")}</b>, where an agent is asked for
2003
2304
  whole: its kind, its role, its repositories and document folders, its engine
2004
2305
  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>
2306
+ <button class="bt" type="button" data-ir="gente" style="margin-left:8px">${t("Open the roster")}</button></p></div>
2307
+ <div><span class="sub">${t("one goal \u2014 optional")}</span><div class="campos" id="meta" style="margin-top:9px"></div></div>
2007
2308
  <div style="display:flex;gap:10px;align-items:center">
2008
- <button class="bt ppal" id="guardaPuesto">Save my seat</button>
2309
+ <button class="bt ppal" id="guardaPuesto">${t("Save my seat")}</button>
2009
2310
  <span class="cargando" id="puestoEstado" style="display:none">writing</span></div>`;
2010
2311
  const domainsR = (await api("/api/domains")).domains;
2011
2312
  let domain = E.domain || E.kind || "software";
@@ -2049,28 +2350,28 @@ async function puesto() {
2049
2350
  await cargaRoles();
2050
2351
  const o = mia && mia.objetivo || {};
2051
2352
  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">
2353
+ <div class="campo"><label>${t("The goal, in one line")}</label>
2354
+ <input type="text" id="g_title" value="${esc2(o.title)}" placeholder="${t("Concrete enough to argue with \u2014 empty skips it")}">
2054
2355
  </div>
2055
2356
  <div class="dos">
2056
- <div class="campo"><label>How it is measured</label>
2357
+ <div class="campo"><label>${t("How it is measured")}</label>
2057
2358
  <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>
2359
+ <div class="campo"><label>${t("The command that returns it, if a command can")}</label>
2059
2360
  <input type="text" class="mono" id="g_command" value="${esc2(o.command)}"
2060
- placeholder="empty for a qualitative goal"></div>
2361
+ placeholder="${t("empty for a qualitative goal")}"></div>
2061
2362
  </div>
2062
- <div class="campo"><label>\u2026or who judges it, and how often</label>
2363
+ <div class="campo"><label>${t("\u2026or who judges it, and how often")}</label>
2063
2364
  <input type="text" id="g_manual" value="${esc2(o.manual)}"
2064
- placeholder="in prose: the architect reads the AGENTS.md files on Fridays">
2365
+ placeholder="${t("in prose: the architect reads the AGENTS.md files on Fridays")}">
2065
2366
  <p class="pista">A goal judged by a person is a real goal \u2014 plenty of quality
2066
2367
  is prose, not a number. Used when the command is empty.</p></div>
2067
2368
  <div class="dos">
2068
- <div class="campo"><label>What it returns today</label>
2369
+ <div class="campo"><label>${t("What it returns today")}</label>
2069
2370
  <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>
2371
+ <div class="campo"><label>${t("Where it has to get to")}</label>
2071
2372
  <input type="text" class="mono" id="g_target" value="${esc2(o.target)}"></div>
2072
2373
  </div>
2073
- <div class="campo" style="max-width:240px"><label>By when</label>
2374
+ <div class="campo" style="max-width:240px"><label>${t("By when")}</label>
2074
2375
  <input type="text" id="g_by" value="${esc2(o.by ?? "this quarter")}"></div>`;
2075
2376
  const val = (id) => q(id).value.trim();
2076
2377
  q("#guardaPuesto").onclick = async () => {
@@ -2117,23 +2418,23 @@ VISTAS.barrios = () => {
2117
2418
  const slug = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 20);
2118
2419
  const render = () => {
2119
2420
  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
2421
+ q("#lienzo").innerHTML = `<div><span class="sub">${t("districts & houses")}</span>
2422
+ <h1 style="margin-top:6px">${t("The modelling no tool can do for you")}</h1>
2423
+ <p class="prosa" style="margin-top:8px">${t(`A house is <b>not a repo</b> \u2014 it is a
2123
2424
  parcel, a slice of one serving a single business unit. Split the interesting
2124
2425
  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>
2426
+ \u201Cthis change touches banking\u201D instead of \u201Cthis change touches src/lib\u201D.`)}</p></div>
2126
2427
 
2127
2428
  <div><span class="sub">districts</span>
2128
2429
  <div class="tabla" id="unidades" style="margin-top:9px"></div>
2129
2430
  <div style="display:flex;gap:9px;margin-top:9px">
2130
- <input type="text" id="nuevaU" placeholder="another district\u2026" style="flex:1">
2431
+ <input type="text" id="nuevaU" placeholder="${t("another district\u2026")}" style="flex:1">
2131
2432
  <button class="bt" id="masU">Add</button></div></div>
2132
2433
 
2133
2434
  <div><span class="sub">houses</span><div id="parcelas" style="margin-top:9px"></div></div>
2134
2435
 
2135
2436
  <div style="display:flex;gap:10px;align-items:center">
2136
- <button class="bt ppal" id="guardaBarrios">Save districts & houses</button>
2437
+ <button class="bt ppal" id="guardaBarrios">${t("Save districts & houses")}</button>
2137
2438
  <span style="font-size:12px;color:var(--tinta3);font-style:italic">
2138
2439
  writes units.yml and parcels.yml \u2014 reseed the map to see it drawn</span></div>`;
2139
2440
  q("#unidades").innerHTML = unidades.map(
@@ -2262,7 +2563,7 @@ VISTAS.red = () => {
2262
2563
  const locales = E.ciudades.filter((c) => !c.actual && c.id && !conectadas.has(c.id));
2263
2564
  const invitation = JSON.stringify(E.invitation);
2264
2565
  q("#lienzo").innerHTML = `<div><span class="sub">roads</span>
2265
- <h1 style="margin-top:6px">Cities this one may reach</h1>
2566
+ <h1 style="margin-top:6px">${t("Cities this one may reach")}</h1>
2266
2567
  <p class="prosa" style="margin-top:8px">A road joins city seats. It may stay on
2267
2568
  this machine or continue over the remote bus; the city sees one explicit
2268
2569
  connection either way.</p></div>
@@ -2271,14 +2572,14 @@ VISTAS.red = () => {
2271
2572
  <span class="ag">${esc2(r.address)}</span>
2272
2573
  <span class="rp">${r.local ? "local road" : "remote road"}</span>
2273
2574
  <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>
2575
+ ).join("") || `<p class="prosa">${t("No roads yet. This city is isolated on purpose.")}</p>`}</div>
2576
+ <div><span class="sub">${t("other cities on this machine")}</span>
2276
2577
  <div class="gente" style="margin-top:9px">${locales.map(
2277
2578
  (c) => `<div class="persona"><h3>${esc2(c.nombre)}</h3>
2278
2579
  <span class="ag">${esc2(c.slug ?? c.ruta)}</span>
2279
2580
  <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>
2581
+ ).join("") || `<p class="prosa">${t("No unconnected local cities.")}</p>`}</div></div>
2582
+ <div class="orden"><span class="et2">${t("remote invitation \xB7 public, no token")}</span>
2282
2583
  <code>${esc2(invitation)}</code>
2283
2584
  <button class="bt mini" data-copia="${esc2(invitation)}">copy</button>
2284
2585
  </div>`;
@@ -2306,15 +2607,269 @@ VISTAS.red = () => {
2306
2607
  };
2307
2608
  });
2308
2609
  };
2610
+ VISTAS.recepcion = () => {
2611
+ q("#lienzo").innerHTML = `<div><span class="sub">${t("reception")}</span>
2612
+ <h1 style="margin-top:6px">${t("Messages wait for you, not your agents")}</h1>
2613
+ <p class="prosa" style="margin-top:8px">${t(
2614
+ "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."
2615
+ )}</p></div>
2616
+ <div class="recModo" id="recModo">
2617
+ <div><span class="et2">${t("routing mode")}</span><b>${t("Manual review")}</b>
2618
+ <p>${t("Every message needs a person before it reaches a city.")}</p></div>
2619
+ <button class="bt mini" type="button" data-rec-open-config>${t("Configure Auto")}</button>
2620
+ </div>
2621
+ <div id="recConfig" hidden></div>
2622
+ <div id="recLista"><p class="prosa">${t("Reading your reception\u2026")}</p></div>`;
2623
+ void cargaRecepcion();
2624
+ };
2625
+ async function cargaRecepcion() {
2626
+ const hueco = q("#recLista");
2627
+ let estado;
2628
+ try {
2629
+ estado = await api("/api/reception");
2630
+ } catch {
2631
+ hueco.innerHTML = `<div class="recError">${t("Could not read your reception")}</div>`;
2632
+ return;
2633
+ }
2634
+ if (estado.error) {
2635
+ hueco.innerHTML = `<div class="recError">${esc2(estado.error)}</div>`;
2636
+ return;
2637
+ }
2638
+ E.reception = {
2639
+ pending: estado.summary.pending,
2640
+ pendingBytes: estado.summary.pendingBytes,
2641
+ routingMode: estado.settings.routingMode,
2642
+ reviewPolicy: estado.settings.reviewPolicy,
2643
+ routerProfile: estado.settings.routerProfile,
2644
+ autoAvailable: estado.settings.autoAvailable
2645
+ };
2646
+ const reglas = new Map(estado.settings.autoRules.map((rule) => [rule.cityId, rule.keywords]));
2647
+ const modo = q("#recModo");
2648
+ modo.innerHTML = `<div><span class="et2">${t("routing mode")}</span>
2649
+ <b>${estado.settings.routingMode === "auto" ? t("Automatic routing") : t("Manual review")}</b>
2650
+ <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>
2651
+ <button class="bt mini" type="button" data-rec-open-config>${t("Configure Auto")}</button>`;
2652
+ const configuracion = q("#recConfig");
2653
+ configuracion.innerHTML = `<form class="recAutoConfig" data-rec-config>
2654
+ <fieldset><legend>${t("Routing policy")}</legend>
2655
+ <label><input type="radio" name="routing_mode" value="manual"
2656
+ ${estado.settings.routingMode === "manual" ? "checked" : ""}> ${t("Keep human review")}</label>
2657
+ <label><input type="radio" name="routing_mode" value="auto"
2658
+ ${estado.settings.routingMode === "auto" ? "checked" : ""}> ${t("Use the local rule router")}</label>
2659
+ </fieldset>
2660
+ <div class="recReglas"><p>${t("Write comma-separated words or phrases for each destination. Empty cities are never selected.")}</p>
2661
+ ${estado.cities.map(
2662
+ (city) => `<label><span><b>${esc2(city.name)}</b><small>${esc2(city.address)}</small></span>
2663
+ <input type="text" name="rule-${esc2(city.id)}" maxlength="1200"
2664
+ data-rule-city="${esc2(city.id)}" value="${esc2((reglas.get(city.id) ?? []).join(", "))}"
2665
+ placeholder="${t("e.g. contract, privacy, legal review")}"></label>`
2666
+ ).join("")}</div>
2667
+ <p class="recAutoAviso">${t("Suspicious, unmatched, or ambiguous text always waits for you. Auto never executes, answers, or opens links.")}</p>
2668
+ <button class="bt ppal" type="submit">${t("Save routing policy")}</button>
2669
+ </form>`;
2670
+ q("[data-rec-open-config]", modo).onclick = () => {
2671
+ configuracion.hidden = !configuracion.hidden;
2672
+ };
2673
+ rail();
2674
+ const conexiones = estado.connections.length ? `<section class="recConexiones"><div class="recSeccion"><b>${t("New encrypted message")}</b>
2675
+ <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>
2676
+ <form class="recCompose" data-rec-send>
2677
+ <label class="recComposePersona" for="rec-send-connection"><span>${t("Write to")}</span>
2678
+ <select id="rec-send-connection" name="connection" required>
2679
+ ${estado.connections.map(
2680
+ (conexion) => `<option value="${esc2(conexion.id)}">${esc2(conexion.name)}</option>`
2681
+ ).join("")}
2682
+ </select>
2683
+ </label>
2684
+ <label class="recComposeTexto" for="rec-send-text"><span>${t("Message")}</span>
2685
+ <textarea id="rec-send-text" name="text" rows="3" maxlength="11500" required
2686
+ placeholder="${t("Your message will stop in their private reception.")}"></textarea>
2687
+ </label>
2688
+ <button class="bt ppal" type="submit">${t("Send securely")}</button>
2689
+ </form></section>` : "";
2690
+ const vacia = !estado.messages.length ? `<div class="recVacia"><b>${t("Reception clear")}</b>
2691
+ <p>${t("No remote message is waiting for a decision.")}</p></div>` : "";
2692
+ const ciudades = estado.cities.map(
2693
+ (ciudad) => `<label class="recDestino"><input type="checkbox" name="destination"
2694
+ value="${esc2(ciudad.id)}"><span><b>${esc2(ciudad.name)}</b>
2695
+ <small>${esc2(ciudad.address)}</small></span></label>`
2696
+ ).join("");
2697
+ const pendientes = estado.messages.length ? `<div class="recResumen"><b>${estado.summary.pending}</b>
2698
+ <span>${plural(estado.summary.pending, "message waiting", "messages waiting")}</span>
2699
+ <small>${formateaBytes(estado.summary.pendingBytes)} \xB7 ${t("local only")}</small></div>
2700
+ <div class="recMensajes">${estado.messages.map(
2701
+ (mensaje) => `<article class="recMensaje">
2702
+ <header><div><span class="recOrigen">${esc2(mensaje.fromName)}</span>
2703
+ <span>${esc2(fechaRecepcion(mensaje.receivedAt))}</span></div>
2704
+ <span class="recSeguro">${mensaje.kind === "rejection" ? t("Reply to a rejected message") : t("No agent has read this")}</span></header>
2705
+ <pre class="recTexto">${esc2(mensaje.text)}</pre>
2706
+ ${mensaje.kind === "rejection" ? `<div class="recAvisoAccion">
2707
+ <button class="bt mini" type="button" data-rec-dismiss="${esc2(mensaje.id)}">${t("Dismiss")}</button>
2708
+ </div>` : `<div class="recAcciones">
2709
+ <form data-rec-route="${esc2(mensaje.id)}">
2710
+ <fieldset><legend>${t("Send to")}</legend><div class="recDestinos">${ciudades}</div></fieldset>
2711
+ <button class="bt ppal" type="submit" disabled>${t("Route to selected cities")}</button>
2712
+ </form>
2713
+ <details><summary>${t("Reject and reply with a reason")}</summary>
2714
+ <form data-rec-reject="${esc2(mensaje.id)}">
2715
+ <label for="rec-reason-${esc2(mensaje.id)}">${t("Reason sent back securely")}</label>
2716
+ <input id="rec-reason-${esc2(mensaje.id)}" type="text" name="reason"
2717
+ maxlength="500" required
2718
+ placeholder="${t("Reason for rejecting this message")}">
2719
+ <button class="bt malo" type="submit">${t("Reject and send reason")}</button>
2720
+ </form>
2721
+ </details>
2722
+ </div>`}
2723
+ </article>`
2724
+ ).join("")}</div>` : "";
2725
+ hueco.innerHTML = `${vacia}${pendientes}${conexiones}`;
2726
+ enlazaRecepcion();
2727
+ }
2728
+ function enlazaRecepcion() {
2729
+ q("[data-rec-config]").onsubmit = async (evento) => {
2730
+ evento.preventDefault();
2731
+ const formulario = evento.currentTarget;
2732
+ const boton = q("button[type=submit]", formulario);
2733
+ const routingMode = q("input[name=routing_mode]:checked", formulario).value;
2734
+ const rules = todos("[data-rule-city]", formulario).map((campo) => ({
2735
+ city_id: campo.dataset.ruleCity,
2736
+ keywords: campo.value.split(",").map((value) => value.trim()).filter(Boolean)
2737
+ })).filter((rule) => rule.keywords.length);
2738
+ boton.disabled = true;
2739
+ const respuesta = await api("/api/reception", {
2740
+ method: "POST",
2741
+ body: JSON.stringify({ action: "configure", routing_mode: routingMode, rules })
2742
+ });
2743
+ if (!respuesta.ok) {
2744
+ boton.disabled = false;
2745
+ toast(respuesta.error ?? t("Could not save the routing policy"), true);
2746
+ return;
2747
+ }
2748
+ toast(t("Routing policy saved."));
2749
+ await cargaRecepcion();
2750
+ };
2751
+ todos("[data-rec-send]").forEach((formulario) => {
2752
+ formulario.onsubmit = async (evento) => {
2753
+ evento.preventDefault();
2754
+ const boton = q("button[type=submit]", formulario);
2755
+ const campo = q("textarea[name=text]", formulario);
2756
+ const connectionId = q("select[name=connection]", formulario).value;
2757
+ if (!connectionId || !campo.value.trim()) return;
2758
+ boton.disabled = true;
2759
+ const respuesta = await api("/api/reception", {
2760
+ method: "POST",
2761
+ body: JSON.stringify({
2762
+ action: "send",
2763
+ connection_id: connectionId,
2764
+ text: campo.value
2765
+ })
2766
+ });
2767
+ if (!respuesta.ok) {
2768
+ boton.disabled = false;
2769
+ toast(respuesta.error ?? t("Could not queue the message"), true);
2770
+ return;
2771
+ }
2772
+ campo.value = "";
2773
+ toast(t("Message queued on this computer."));
2774
+ await cargaRecepcion();
2775
+ };
2776
+ });
2777
+ todos("[data-rec-dismiss]").forEach((boton) => {
2778
+ boton.onclick = async () => {
2779
+ boton.disabled = true;
2780
+ const respuesta = await api("/api/reception", {
2781
+ method: "POST",
2782
+ body: JSON.stringify({
2783
+ action: "reject",
2784
+ message_id: boton.dataset.recDismiss,
2785
+ reason: "Dismissed response"
2786
+ })
2787
+ });
2788
+ if (!respuesta.ok) {
2789
+ boton.disabled = false;
2790
+ toast(respuesta.error ?? t("Could not dismiss the response"), true);
2791
+ return;
2792
+ }
2793
+ await cargaRecepcion();
2794
+ };
2795
+ });
2796
+ todos("[data-rec-route]").forEach((formulario) => {
2797
+ const boton = q("button[type=submit]", formulario);
2798
+ const actualiza = () => {
2799
+ boton.disabled = !formulario.querySelector("input:checked");
2800
+ };
2801
+ formulario.onchange = actualiza;
2802
+ formulario.onsubmit = async (evento) => {
2803
+ evento.preventDefault();
2804
+ const destinations = todos(
2805
+ "input[name=destination]:checked",
2806
+ formulario
2807
+ ).map((campo) => campo.value);
2808
+ if (!destinations.length) return;
2809
+ boton.disabled = true;
2810
+ const respuesta = await api("/api/reception", {
2811
+ method: "POST",
2812
+ body: JSON.stringify({
2813
+ action: "route",
2814
+ message_id: formulario.dataset.recRoute,
2815
+ destinations
2816
+ })
2817
+ });
2818
+ if (!respuesta.ok) {
2819
+ boton.disabled = false;
2820
+ toast(respuesta.error ?? t("Could not route the message"), true);
2821
+ return;
2822
+ }
2823
+ toast(t("Message routed. Only the selected cities can now read it."));
2824
+ await cargaRecepcion();
2825
+ };
2826
+ });
2827
+ todos("[data-rec-reject]").forEach((formulario) => {
2828
+ formulario.onsubmit = async (evento) => {
2829
+ evento.preventDefault();
2830
+ const boton = q("button[type=submit]", formulario);
2831
+ const reason = q("input[name=reason]", formulario).value.trim();
2832
+ if (!reason) return;
2833
+ boton.disabled = true;
2834
+ const respuesta = await api("/api/reception", {
2835
+ method: "POST",
2836
+ body: JSON.stringify({
2837
+ action: "reject",
2838
+ message_id: formulario.dataset.recReject,
2839
+ reason
2840
+ })
2841
+ });
2842
+ if (!respuesta.ok) {
2843
+ boton.disabled = false;
2844
+ toast(respuesta.error ?? t("Could not reject the message"), true);
2845
+ return;
2846
+ }
2847
+ toast(t("Message rejected. Your reason is queued for encrypted delivery."));
2848
+ await cargaRecepcion();
2849
+ };
2850
+ });
2851
+ }
2852
+ function fechaRecepcion(value) {
2853
+ const date = new Date(value);
2854
+ return Number.isNaN(date.getTime()) ? value : date.toLocaleString(idioma() === "es" ? "es-ES" : "en-GB", {
2855
+ dateStyle: "medium",
2856
+ timeStyle: "short"
2857
+ });
2858
+ }
2859
+ function formateaBytes(bytes) {
2860
+ if (bytes < 1024) return `${bytes} B`;
2861
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
2862
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
2863
+ }
2309
2864
  VISTAS.committee = () => {
2310
2865
  q("#lienzo").innerHTML = `<div><span class="sub">committee</span>
2311
- <h1 style="margin-top:6px">Decisions with a visible chain of custody</h1>
2866
+ <h1 style="margin-top:6px">${t("Decisions with a visible chain of custody")}</h1>
2312
2867
  <p class="prosa" style="margin-top:8px">The seat selects relevant repo agents,
2313
2868
  gathers isolated positions, controls the floor, decides and assigns an
2314
2869
  independent verification. This view is read-only; the vendor-neutral CLI
2315
2870
  drives the protocol.</p></div>
2316
2871
  <div class="orden"><span class="et2">start</span>
2317
- <code>agents-city committee schema open</code>
2872
+ <code>${t("agents-city committee schema open")}</code>
2318
2873
  <button class="bt mini" data-copia="agents-city committee schema open">copy</button>
2319
2874
  </div>
2320
2875
  <div class="gente">${E.deliberations.map(
@@ -2328,7 +2883,7 @@ VISTAS.committee = () => {
2328
2883
  ${act.verifier ? ` \xB7 verifier ${esc2(act.verifier)} ${esc2(act.verification)}` : ""}</span>
2329
2884
  <code class="mono">${esc2(corto(act.act))}</code>
2330
2885
  </div>`
2331
- ).join("") || '<p class="prosa">No committee acts yet. Open one only when the seat needs specialised evidence.</p>'}</div>`;
2886
+ ).join("") || `<p class="prosa">${t("No committee acts yet. Open one only when the seat needs specialised evidence.")}</p>`}</div>`;
2332
2887
  enlaza();
2333
2888
  };
2334
2889
  function opciones2(valores, actual) {
@@ -2350,9 +2905,12 @@ VISTAS.gente = () => {
2350
2905
  const g = a.growth;
2351
2906
  const crecido = Math.min(1, Math.log2(g.floors + 1) / 8);
2352
2907
  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);
2908
+ const motorDelAgente = motorDe(a.runtime);
2909
+ const opcionesModelo = opciones2([""].concat(motorDelAgente.modelos), a.model) + `<option value="__otro__">${t("custom\u2026")}</option>`;
2910
+ const opcionesEsfuerzo = opciones2(
2911
+ [""].concat(ESFUERZOS),
2912
+ motorDelAgente.esfuerzo ? a.effort : ""
2913
+ );
2356
2914
  const opcionesRuntime = opciones2(RUNTIMES, a.runtime);
2357
2915
  const montajes = a.mounts ?? [];
2358
2916
  return `
@@ -2364,22 +2922,21 @@ VISTAS.gente = () => {
2364
2922
  <span class="rpgHogar">${a.legacy ? "repo" : `workspace \xB7 ${montajes.length} ${montajes.length === 1 ? "mount" : "mounts"}`}</span></span></div>
2365
2923
  <span class="rpgBotones">
2366
2924
  <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>
2925
+ title="${t("Reroll this agent's face \u2014 deterministic, persisted on the card")}">\u{1F3B2}</button>
2368
2926
  <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>
2927
+ data-file="CLAUDE.md" title="${t("Instructions the Claude runtime reads")}">CLAUDE.md</button>
2370
2928
  <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>
2929
+ data-file="AGENTS.md" title="${t("Instructions Codex, OpenCode and Kimi read")}">AGENTS.md</button>
2372
2930
  </span>
2373
2931
  </div>
2374
2932
  <div class="rpgFila"><label>${t("engine")}</label>
2375
2933
  ${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"'}>
2934
+ <select class="rpgSel" data-agente="${esc2(a.slug)}" data-campo="model">
2378
2935
  ${opcionesModelo}</select></div>
2379
2936
  <div class="rpgFila"><label>${t("effort")}</label>
2380
2937
  ${barra(esfuerzo / 5, esfuerzo ? "" : "defecto")}
2381
2938
  <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"'}>
2939
+ ${motorDelAgente.esfuerzo ? "" : `disabled title="${t("This CLI has no effort setting.")}"`}>
2383
2940
  ${opcionesEsfuerzo}</select></div>
2384
2941
  <div class="rpgFila"><label>${t("provider")}</label>
2385
2942
  <span class="rpgDato"><span class="cliDot ${a.cli.connected ? "on" : a.cli.installed ? "idle" : "off"}" title="${esc2(
@@ -2388,7 +2945,7 @@ VISTAS.gente = () => {
2388
2945
  t(a.cli.connected ? "connected" : a.cli.installed ? "idle" : "missing")
2389
2946
  )} \xB7 ${esc2(a.cli.binary)}
2390
2947
  <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>
2948
+ title="${t("Run the engine for real: --version, and the login state on Claude")}">test</button></span>
2392
2949
  <select class="rpgSel" data-agente="${esc2(a.slug)}" data-campo="runtime">
2393
2950
  ${opcionesRuntime}</select></div>
2394
2951
  <div class="rpgSkills"><label>${t("works on")} \xB7 ${montajes.length}</label>
@@ -2397,7 +2954,7 @@ VISTAS.gente = () => {
2397
2954
  data-agente="${esc2(a.slug)}" data-mount="${esc2(m.label)}"
2398
2955
  title="Stop this agent working on ${esc2(m.target)}">\xD7</button>`}</code>`
2399
2956
  ).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)}"
2957
+ ${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
2958
  title="A repo, a worktree or a folder of documents \u2014 this agent works on all of them">+ folder</button>`}</div>
2402
2959
  <div class="rpgFila"><label>${t("growth")}</label>
2403
2960
  ${barra(crecido)}
@@ -2405,23 +2962,23 @@ VISTAS.gente = () => {
2405
2962
  <div class="rpgSkills"><label>${t("skills")} \xB7 ${skills.length}</label>
2406
2963
  ${skills.length ? skills.map(
2407
2964
  (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>`
2965
+ data-skill="${esc2(s.dir)}" title="${t("Remove this skill from the agent's home")}">\xD7</button>` : ""}</code>`
2409
2966
  ).join(" ") : '<span class="rpgDato">none discovered</span>'}
2410
2967
  <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">
2968
+ title="${t("Install a skill zip into this agent's own home \u2014 the Claude runtime reads skills; other engines ignore them")}">
2412
2969
  + zip<input type="file" accept=".zip" data-agente="${esc2(a.slug)}"></label></div>
2413
2970
  </div>`;
2414
2971
  };
2415
2972
  q("#lienzo").innerHTML = `<div><span class="sub">the houses of ${esc2(E.city_name)}</span>
2416
2973
  <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>,
2974
+ <p class="prosa" style="margin-top:8px">${t(`<b>A house is where an agent lives and works</b>,
2418
2975
  and many houses are a city \u2014 it is the same thing the map draws, growing with what that
2419
2976
  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
2977
+ here you see their houses.`)}</p>
2978
+ <p class="prosa">${t(
2979
+ "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.",
2980
+ { donde: `<code class="mono">${esc2(corto(E.datos))}/agents/</code>` }
2981
+ )} Every agent is whole here: the kind of work
2425
2982
  it does, its role, everything it works on \u2014 any number of repositories and document
2426
2983
  folders at once \u2014 the engine and effort that run it, and the skills in its own home.</p>
2427
2984
  <p class="prosa">Every number is real: growth from what the agent actually produced,
@@ -2613,8 +3170,18 @@ VISTAS.gente = () => {
2613
3170
  }
2614
3171
  });
2615
3172
  if (dicho === null || !elegidas.length) return;
2616
- for (const ruta of elegidas)
2617
- await montaje({ agent: boton.dataset.agente ?? "", add: ruta }, t("Mounted"));
3173
+ const agente = boton.dataset.agente ?? "";
3174
+ const fallos = await Promise.all(
3175
+ elegidas.map(
3176
+ (ruta) => api("/api/montaje", {
3177
+ method: "POST",
3178
+ body: JSON.stringify({ agent: agente, add: ruta })
3179
+ }).then((r) => r.ok ? "" : `${ruta}: ${r.error ?? "could not mount"}`).catch((e) => `${ruta}: ${String(e)}`)
3180
+ )
3181
+ );
3182
+ const malos = fallos.filter(Boolean);
3183
+ toast(malos.length ? malos.join("; ") : t("Mounted"), malos.length > 0);
3184
+ void refresca();
2618
3185
  };
2619
3186
  });
2620
3187
  todos(".rpgDesmonta").forEach((boton) => {
@@ -2833,6 +3400,14 @@ function interruptorDeIdioma() {
2833
3400
  void refresca();
2834
3401
  };
2835
3402
  }
3403
+ window.addEventListener(
3404
+ "error",
3405
+ (e) => anota2("uncaught error", { mensaje: e.message, fichero: e.filename, linea: e.lineno })
3406
+ );
3407
+ window.addEventListener(
3408
+ "unhandledrejection",
3409
+ (e) => anota2("unhandled rejection", String(e.reason))
3410
+ );
2836
3411
  arrastreDelRail();
2837
3412
  interruptorDeIdioma();
2838
3413
  tema();