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
@@ -16,6 +16,7 @@ anota({
16
16
  'My seat': 'Mi asiento',
17
17
  Districts: 'Distritos',
18
18
  Roads: 'Carreteras',
19
+ Reception: 'Recepción',
19
20
  Committee: 'Comité',
20
21
  Houses: 'Casas',
21
22
  'All cities': 'Todas las ciudades',
@@ -24,6 +25,83 @@ anota({
24
25
  '{n} houses': '{n} casas',
25
26
  'Create another city →': 'Crear otra ciudad →',
26
27
 
28
+ // ── reception ────────────────────────────────────────────────────────────
29
+ reception: 'recepción',
30
+ 'Messages wait for you, not your agents': 'Los mensajes te esperan a ti, no a tus agentes',
31
+ '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.':
32
+ 'El texto remoto se detiene aquí como texto inerte. Léelo, recházalo con un motivo o elige qué ciudades deben recibirlo. Hasta entonces ningún modelo puede leerlo.',
33
+ 'routing mode': 'modo de enrutado',
34
+ 'Manual review': 'Revisión manual',
35
+ 'Every message needs a person before it reaches a city.':
36
+ 'Cada mensaje necesita a una persona antes de llegar a una ciudad.',
37
+ 'Auto router not configured': 'Router automático sin configurar',
38
+ 'Configure Auto': 'Configurar Auto',
39
+ 'Automatic routing': 'Enrutado automático',
40
+ 'Only one clear, low-risk rule match can leave the human queue automatically.':
41
+ 'Solo una coincidencia clara y de bajo riesgo puede salir automáticamente de la cola humana.',
42
+ 'Routing policy': 'Política de enrutado',
43
+ 'Keep human review': 'Mantener la revisión humana',
44
+ 'Use the local rule router': 'Usar el router local de reglas',
45
+ 'Write comma-separated words or phrases for each destination. Empty cities are never selected.':
46
+ 'Escribe palabras o frases separadas por comas para cada destino. Las ciudades vacías nunca se eligen.',
47
+ 'e.g. contract, privacy, legal review': 'p. ej. contrato, privacidad, revisión legal',
48
+ 'Suspicious, unmatched, or ambiguous text always waits for you. Auto never executes, answers, or opens links.':
49
+ 'El texto sospechoso, sin coincidencia o ambiguo siempre te espera. Auto nunca ejecuta, responde ni abre enlaces.',
50
+ 'Save routing policy': 'Guardar política de enrutado',
51
+ 'Could not save the routing policy': 'No se pudo guardar la política de enrutado',
52
+ 'Routing policy saved.': 'Política de enrutado guardada.',
53
+ 'Reading your reception…': 'Leyendo tu recepción…',
54
+ 'Could not read your reception': 'No se ha podido leer tu recepción',
55
+ 'Reception clear': 'Recepción despejada',
56
+ 'No remote message is waiting for a decision.': 'No hay mensajes remotos esperando una decisión.',
57
+ 'message waiting': '{n} mensaje en espera',
58
+ 'messages waiting': '{n} mensajes en espera',
59
+ 'local only': 'solo local',
60
+ 'No agent has read this': 'Ningún agente ha leído esto',
61
+ 'Send to': 'Dirigir a',
62
+ 'Route to selected cities': 'Dirigir a las ciudades elegidas',
63
+ 'Reject and reply with a reason': 'Rechazar y responder con un motivo',
64
+ 'Reason sent back securely': 'Motivo que se devolverá cifrado',
65
+ 'Reason for rejecting this message': 'Motivo para rechazar este mensaje',
66
+ 'Reject and send reason': 'Rechazar y enviar el motivo',
67
+ 'Could not route the message': 'No se pudo dirigir el mensaje',
68
+ 'Message routed. Only the selected cities can now read it.':
69
+ 'Mensaje dirigido. Solo las ciudades elegidas pueden leerlo ahora.',
70
+ 'Could not reject the message': 'No se pudo rechazar el mensaje',
71
+ 'Message rejected. Your reason is queued for encrypted delivery.':
72
+ 'Mensaje rechazado. Tu motivo ha quedado en cola para enviarse cifrado.',
73
+ 'New encrypted message': 'Nuevo mensaje cifrado',
74
+ 'Messages leave from this computer with end-to-end encryption.':
75
+ 'Los mensajes salen de este ordenador con cifrado de extremo a extremo.',
76
+ 'message queued': '{n} mensaje en cola',
77
+ 'messages queued': '{n} mensajes en cola',
78
+ 'Write to': 'Escribir a',
79
+ Message: 'Mensaje',
80
+ 'Your message will stop in their private reception.':
81
+ 'Tu mensaje se detendrá en su recepción privada.',
82
+ 'Send securely': 'Enviar de forma segura',
83
+ 'Reply to a rejected message': 'Respuesta a un mensaje rechazado',
84
+ Dismiss: 'Descartar',
85
+ 'Could not queue the message': 'No se pudo poner el mensaje en cola',
86
+ 'Message queued on this computer.': 'Mensaje puesto en cola en este ordenador.',
87
+ 'Could not dismiss the response': 'No se pudo descartar la respuesta',
88
+
89
+ // ── live activity rail ──────────────────────────────────────────────────
90
+ 'City live': 'Ciudad en directo',
91
+ waiting: 'esperando',
92
+ 'A moderated conversation over the WebSocket bus. The seat routes each turn; private model reasoning is never shown or stored.':
93
+ 'Una conversación moderada sobre el bus WebSocket. El asiento dirige cada turno; el razonamiento privado de los modelos no se muestra ni se guarda.',
94
+ 'all conversations': 'todas las conversaciones',
95
+ 'show work': 'mostrar trabajo',
96
+ 'hide work': 'ocultar trabajo',
97
+ 'websocket live': 'websocket activo',
98
+ reconnecting: 'reconectando',
99
+ 'session offline': 'sesión desconectada',
100
+ 'The bus is live. Questions, positions and moderated replies will appear here.':
101
+ 'El bus está activo. Aquí aparecerán preguntas, posiciones y respuestas moderadas.',
102
+ 'Start the city session. Its visible conversation will appear here as it happens.':
103
+ 'Inicia la sesión de la ciudad. Su conversación visible aparecerá aquí mientras sucede.',
104
+
27
105
  // ── the guide ─────────────────────────────────────────────────────────────
28
106
  Welcome: 'Bienvenida',
29
107
  'The work': 'El trabajo',
@@ -64,7 +142,6 @@ anota({
64
142
  '{city} is alive': '{city} está viva',
65
143
 
66
144
  // ── the houses ────────────────────────────────────────────────────────────
67
- 'the houses of {city}': 'las casas de {city}',
68
145
  'Who lives in {city}': 'Quién vive en {city}',
69
146
  '+ Build a house': '+ Construir una casa',
70
147
  '+ folder': '+ carpeta',
@@ -92,8 +169,6 @@ anota({
92
169
  open: 'abrir',
93
170
  archive: 'archivar',
94
171
  '· open now': '· abierta ahora',
95
- '{n} house of its own': '{n} casa propia',
96
- '{n} houses of its own': '{n} casas propias',
97
172
  'Start this city over': 'Empezar esta ciudad de cero',
98
173
  'Start over…': 'Empezar de cero…',
99
174
 
@@ -119,27 +194,8 @@ anota({
119
194
  'Gobierna una ciudad autónoma: su asiento, sus agentes, sus carreteras y su mapa. Todo lo que hay aquí es un fichero de texto que también puedes abrir a mano.',
120
195
 
121
196
  // ── looking through the disk ──────────────────────────────────────────────
122
- 'search your disk — a name, or part of a path':
123
- 'busca en tu disco — un nombre, o parte de una ruta',
124
- 'Look through the disk again — for something cloned a minute ago':
125
- 'Vuelve a mirar el disco — por si acabas de clonar algo',
126
- 'Search again': 'Buscar otra vez',
127
- 'searching…': 'buscando…',
128
- 'looking through your disk…': 'mirando en tu disco…',
129
- '{shown} of {total} places match': '{shown} de {total} sitios encajan',
130
- '{total} places found — type to narrow it down':
131
- '{total} sitios encontrados — escribe para acotar',
132
- 'Nothing matches that. Add it by path below.':
133
- 'Nada encaja con eso. Añádelo por ruta aquí abajo.',
134
- 'Nothing found on this disk — add folders by path below.':
135
- 'No hay nada en este disco — añade carpetas por ruta aquí abajo.',
136
- '…or a path, and Enter': '…o una ruta, y Enter',
137
-
138
- // ── what runs an agent ────────────────────────────────────────────────────
139
197
  'Its role — its speciality, never authority': 'Su rol — su especialidad, nunca su autoridad',
140
198
  'What runs it?': '¿Qué lo mueve?',
141
- 'Leave all three on default and it runs the way you do. Model and effort are Claude’s to read; another CLI carries its own flags.':
142
- 'Déjalos los tres por defecto y funcionará como tú. El modelo y el esfuerzo los lee Claude; otra CLI lleva sus propias opciones.',
143
199
  });
144
200
 
145
201
  anota({
@@ -156,9 +212,6 @@ anota({
156
212
  'Presides esta ciudad respondas lo que respondas — esto es tu <b>especialidad</b>, no tu autoridad. Da forma a la perspectiva con la que entras en una decisión 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.',
157
213
  'Every house holds one worker — an <b>agent</b> — 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.':
158
214
  'Cada casa aloja a un trabajador — un <b>agente</b> — con su ventana, su rol y su rincón 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.',
159
- '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 — this is the part that makes it an agent and not a repo.':
160
- '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 — esta es la parte que lo convierte en un agente y no en un repo.',
161
- 'Working on: {que}': 'Trabaja sobre: {que}',
162
215
  'Your seat is written and {cuantas}. Everything you just answered is a plain file you can read and edit.':
163
216
  'Tu asiento queda escrito y {cuantas}. Todo lo que acabas de responder es un fichero de texto que puedes leer y editar.',
164
217
  '<b>{n}</b> house stands around it': 'a su alrededor hay <b>{n}</b> casa',
@@ -213,8 +266,6 @@ anota({
213
266
  '{name} has a house now': '{name} ya tiene casa',
214
267
  '{name} joined the city': '{name} se ha mudado a la ciudad',
215
268
  'What else does it work on?': '¿Sobre qué más trabaja?',
216
- 'A repository, a worktree, or a folder of documents. It is linked, never copied.':
217
- 'Un repositorio, un worktree o una carpeta de documentos. Se enlaza, nunca se copia.',
218
269
  'Mount it': 'Montarlo',
219
270
  Mounted: 'Montado',
220
271
  'Stop this agent working on {what}?': '¿Que este agente deje de trabajar sobre {what}?',
@@ -274,3 +325,112 @@ anota({
274
325
  'This install has no demo recordings. Make them with demo/graba.py, or run the full thing from a terminal with agents-city demo.':
275
326
  'Esta instalación no tiene grabaciones. Créalas con demo/graba.py, o lanza la cosa entera desde terminal con agents-city demo.',
276
327
  });
328
+
329
+ anota({
330
+ '{n} folder': '{n} carpeta',
331
+ '{n} folders': '{n} carpetas',
332
+ '{n} file': '{n} fichero',
333
+ '{n} files': '{n} ficheros',
334
+ 'files are below the folders': 'los ficheros van debajo de las carpetas',
335
+ });
336
+
337
+ anota({
338
+ // ── what runs a house ─────────────────────────────────────────────────────
339
+ '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.':
340
+ 'Déjalos los tres por defecto y funcionará como tú. Lo que pongas aquí se escribe una sola vez en la ficha, y el lanzador se lo pasa a la CLI que mueva esta casa.',
341
+ 'This CLI has no effort setting.': 'Esta CLI no tiene ajuste de esfuerzo.',
342
+ 'An alias the Claude CLI resolves when the window opens.':
343
+ 'Un alias que la CLI de Claude resuelve al abrir la ventana.',
344
+ 'The model name your Codex uses — the one in ~/.codex/config.toml.':
345
+ 'El nombre del modelo que usa tu Codex — el de ~/.codex/config.toml.',
346
+ 'OpenCode names a model provider/model, like anthropic/claude-sonnet-4.':
347
+ 'OpenCode nombra los modelos proveedor/modelo, como anthropic/claude-sonnet-4.',
348
+ 'The model name your Kimi CLI uses.': 'El nombre del modelo que usa tu CLI de Kimi.',
349
+ });
350
+
351
+ anota({
352
+ // ── the seat, the goal, the day ───────────────────────────────────────────
353
+ 'my seat': 'mi asiento',
354
+ 'Save my seat': 'Guardar mi asiento',
355
+ 'work domain': 'dominio de trabajo',
356
+ 'your day': 'tu día',
357
+ 'the agents': 'los agentes',
358
+ 'repo agents': 'agentes de repo',
359
+ 'one goal — optional': 'un objetivo — opcional',
360
+ 'The goal, in one line': 'El objetivo, en una línea',
361
+ 'Concrete enough to argue with — empty skips it':
362
+ 'Lo bastante concreto como para discutirlo — vacío lo salta',
363
+ 'How it is measured': 'Cómo se mide',
364
+ 'in prose: the architect reads the AGENTS.md files on Fridays':
365
+ 'en prosa: el arquitecto lee los AGENTS.md los viernes',
366
+ 'The command that returns it, if a command can':
367
+ 'El comando que lo devuelve, si un comando puede',
368
+ 'What it returns today': 'Lo que devuelve hoy',
369
+ 'Where it has to get to': 'A dónde tiene que llegar',
370
+ 'By when': 'Para cuándo',
371
+ 'empty for a qualitative goal': 'vacío para un objetivo cualitativo',
372
+ '…or who judges it, and how often': '…o quién lo juzga, y cada cuánto',
373
+ 'what is left': 'lo que queda',
374
+ 'open my session': 'abrir mi sesión',
375
+ 'Open the roster': 'Abrir la lista',
376
+
377
+ // ── districts, roads, the committee ───────────────────────────────────────
378
+ 'districts & houses': 'distritos y casas',
379
+ 'Save districts & houses': 'Guardar distritos y casas',
380
+ 'another district…': 'otro distrito…',
381
+ 'The modelling no tool can do for you': 'El modelado que ninguna herramienta puede hacer por ti',
382
+ 'A house is <b>not a repo</b> — 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 “this change touches banking” instead of “this change touches src/lib”.':
383
+ 'Una casa <b>no es un repo</b> — es una parcela, una porción de uno al servicio de una sola unidad de negocio. Parte los repos interesantes, dale a cada casa su distrito, y el mapa podrá decir «este cambio toca banca» en vez de «este cambio toca src/lib».',
384
+ 'Cities this one may reach': 'Ciudades a las que ésta puede llegar',
385
+ 'No roads yet. This city is isolated on purpose.':
386
+ 'Aún no hay carreteras. Esta ciudad está aislada a propósito.',
387
+ 'No unconnected local cities.': 'No hay ciudades locales sin conectar.',
388
+ 'other cities on this machine': 'otras ciudades en esta máquina',
389
+ 'remote invitation · public, no token': 'invitación remota · pública, sin token',
390
+ 'committee acts': 'actas del comité',
391
+ 'Decisions with a visible chain of custody': 'Decisiones con una cadena de custodia visible',
392
+ 'No committee acts yet. Open one only when the seat needs specialised evidence.':
393
+ 'Aún no hay actas. Abre un comité sólo cuando el asiento necesite pruebas especializadas.',
394
+ 'agents-city committee schema open': 'agents-city committee schema open',
395
+ 'guided committee': 'comité guiado',
396
+ 'Play the guided committee from the top': 'Reproducir el comité guiado desde el principio',
397
+ 'Pause or resume mid-scene': 'Pausar o seguir a mitad de escena',
398
+ '⟳ replay': '⟳ repetir',
399
+ '⏸ pause': '⏸ pausa',
400
+ 'seat moderates': 'el asiento modera',
401
+ 'read full message': 'leer el mensaje entero',
402
+
403
+ // ── the cities view ───────────────────────────────────────────────────────
404
+ '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 — it stands inside the city that owns it, with its workspace and mounts under that city’s folder — 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.':
405
+ 'Una persona, varias ciudades autónomas: cada una con su identidad, su dominio, su silla, <b>sus propias casas</b> y sus carreteras. Una casa no se comparte — está dentro de la ciudad que la posee, con su espacio de trabajo y sus montajes bajo la carpeta de esa ciudad — así 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.',
406
+ 'client-a, research, the book…': 'cliente-a, investigación, el libro…',
407
+ 'which city this hall manages': 'qué ciudad gobierna este ayuntamiento',
408
+ '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> — only the city that points at them.':
409
+ 'Devuelve {city} a su primer día: sin asiento, sin agentes, sin historial de comité, sin mapa. Tus repositorios y carpetas de documentos <b>no se tocan</b> — sólo la ciudad que apunta a ellos.',
410
+
411
+ // ── the houses ────────────────────────────────────────────────────────────
412
+ 'Agents &amp; skills': 'Agentes y skills',
413
+ '<b>A house is where an agent lives and works</b>, and many houses are a city — 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.':
414
+ '<b>Una casa es donde vive y trabaja un agente</b>, y muchas casas son una ciudad — 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í ves sus casas.',
415
+ 'They belong to <b>this</b> city and only to it: each one’s workspace and its mounts live inside {donde}, so another city has its own people even if you give them the same names.':
416
+ 'Pertenecen a <b>esta</b> ciudad y sólo a ella: el espacio de trabajo de cada una y sus montajes viven dentro de {donde}, así que otra ciudad tiene su propia gente aunque les pongas los mismos nombres.',
417
+ 'a legacy repo agent works on its own repo':
418
+ 'un agente de repo heredado trabaja sobre su propio repo',
419
+ 'data repo': 'repo de datos',
420
+ 'skills recognised': 'skills reconocidas',
421
+ 'plugin installed': 'plugin instalado',
422
+ 'Reroll this agent’s face — deterministic, persisted on the card':
423
+ 'Vuelve a tirar la cara de este agente — determinista, guardada en la ficha',
424
+ 'Instructions the Claude runtime reads': 'Instrucciones que lee el runtime de Claude',
425
+ 'Instructions Codex, OpenCode and Kimi read': 'Instrucciones que leen Codex, OpenCode y Kimi',
426
+ 'Run the engine for real: --version, and the login state on Claude':
427
+ 'Ejecuta el motor de verdad: --version, y el estado de sesión en Claude',
428
+ 'Install a skill zip into this agent’s own home — the Claude runtime reads skills; other engines ignore them':
429
+ 'Instala un zip de skill en la casa de este agente — el runtime de Claude lee skills; los demás motores las ignoran',
430
+ 'Remove this skill from the agent’s home': 'Quitar esta skill de la casa del agente',
431
+
432
+ // ── while it is reading ───────────────────────────────────────────────────
433
+ 'reading the domain packs': 'leyendo los paquetes de dominio',
434
+ 'reading the role files': 'leyendo los ficheros de rol',
435
+ 'an isometric house': 'una casa isométrica',
436
+ });
@@ -16,7 +16,8 @@
16
16
  * are allowed to pick.
17
17
  */
18
18
 
19
- import { t as _ } from './idioma';
19
+ import { plural, t as _ } from './idioma';
20
+ import { Montada } from './vista';
20
21
 
21
22
  export interface Entrada {
22
23
  nombre: string;
@@ -30,7 +31,6 @@ export interface Entrada {
30
31
  interface Listado {
31
32
  ruta: string;
32
33
  arriba: string;
33
- casa: string;
34
34
  atajos: Array<{ nombre: string; ruta: string }>;
35
35
  entradas: Entrada[];
36
36
  recortada: boolean;
@@ -44,41 +44,38 @@ export interface PuertaExp {
44
44
 
45
45
  const MARCA: Record<string, string> = { repo: 'git', worktree: 'wt' };
46
46
 
47
- export class Explorador {
47
+ export class Explorador extends Montada {
48
48
  private aqui = '';
49
49
  private listado: Listado | null = null;
50
50
  private cargando = false;
51
51
  private fallo = '';
52
- private host: HTMLElement | null = null;
53
52
 
54
53
  constructor(
55
54
  private readonly p: PuertaExp,
56
55
  /** Called with a path when a row's + is pressed. */
57
- private readonly elige: (ruta: string, esCarpeta: boolean) => void,
56
+ private readonly elige: (ruta: string) => void,
58
57
  /** Which paths are already chosen, so their rows read as chosen. */
59
58
  private readonly yaElegidas: () => string[],
60
- ) {}
61
-
62
- /** Render into `host` and wire it. Safe to call again: that is the repaint. */
63
- monta(host: HTMLElement): void {
64
- this.host = host;
65
- if (!this.listado && !this.cargando) void this.ve(this.aqui || '~');
66
- host.innerHTML = this.html();
67
- this.enlaza(host);
59
+ ) {
60
+ super();
68
61
  }
69
62
 
70
- private repinta(): void {
71
- if (this.host) {
72
- this.host.innerHTML = this.html();
73
- this.enlaza(this.host);
74
- }
63
+ override monta(host: HTMLElement): void {
64
+ super.monta(host);
65
+ // The first mount is also what sends it walking; every later one is a
66
+ // repaint of where it already is.
67
+ if (!this.listado && !this.cargando) void this.ve(this.aqui || '~');
75
68
  }
76
69
 
77
70
  /** Walk to a folder. An unreadable one is said out loud, not swallowed. */
78
71
  async ve(ruta: string): Promise<void> {
79
72
  this.cargando = true;
80
73
  this.fallo = '';
81
- this.repinta();
74
+ // Only when there is nothing on screen yet. Otherwise this rendered markup
75
+ // byte-identical to what was already there — the loading branch only draws
76
+ // when there is no listing — which made every step into a folder cost two
77
+ // full renders instead of one.
78
+ if (!this.listado) this.repinta();
82
79
  try {
83
80
  const r = await this.p.api<Listado>('/api/carpeta?path=' + encodeURIComponent(ruta));
84
81
  if (r.error) this.fallo = r.error;
@@ -94,11 +91,6 @@ export class Explorador {
94
91
  }
95
92
  }
96
93
 
97
- /** The path being looked at right now — what "add this folder" means. */
98
- get carpeta(): string {
99
- return this.aqui;
100
- }
101
-
102
94
  private migas(): string {
103
95
  const esc = this.p.esc;
104
96
  const l = this.listado;
@@ -117,10 +109,13 @@ export class Explorador {
117
109
  );
118
110
  }
119
111
 
120
- html(): string {
112
+ protected html(): string {
121
113
  const esc = this.p.esc;
122
114
  const l = this.listado;
123
115
  const elegidas = new Set(this.yaElegidas());
116
+ // Out of the loop: two fixed labels, translated once instead of per row.
117
+ const yaEsta = _('Already chosen');
118
+ const anade = _('Add this');
124
119
  const filas = (l?.entradas ?? [])
125
120
  .map((e) => {
126
121
  const puesta = elegidas.has(e.ruta);
@@ -132,8 +127,7 @@ export class Explorador {
132
127
  e.git ? `<em class="expMarca">${esc(MARCA[e.git] ?? e.git)}</em>` : ''
133
128
  }${e.enlace ? '<em class="expMarca">link</em>' : ''}</button>
134
129
  <button type="button" class="expMas" data-exp="toma" data-ruta="${esc(e.ruta)}"
135
- data-dir="${e.dir ? '1' : ''}"
136
- title="${esc(puesta ? _('Already chosen') : _('Add this'))}">${puesta ? '✓' : '+'}</button>
130
+ title="${esc(puesta ? yaEsta : anade)}">${puesta ? '✓' : '+'}</button>
137
131
  </div>`;
138
132
  })
139
133
  .join('');
@@ -154,6 +148,7 @@ export class Explorador {
154
148
  title="${_('Up one folder')}">↑</button>
155
149
  <div class="expMigas">${this.migas()}</div>
156
150
  </div>
151
+ ${this.cuenta(l)}
157
152
  ${
158
153
  this.fallo
159
154
  ? `<p class="pista malo">${esc(this.fallo)}</p>`
@@ -165,25 +160,57 @@ export class Explorador {
165
160
  }
166
161
  ${l?.recortada ? `<p class="pista">${_('Only the first 2000 shown.')}</p>` : ''}
167
162
  <button type="button" class="bt bvMini expTomaAqui" data-exp="toma"
168
- data-ruta="${esc(l?.ruta ?? '')}" data-dir="1" ${l ? '' : 'disabled'}>${_(
169
- 'Add this folder',
170
- )}</button>
163
+ data-ruta="${esc(l?.ruta ?? '')}" ${l ? '' : 'disabled'}>${_('Add this folder')}</button>
171
164
  </div>`;
172
165
  }
173
166
 
174
- private enlaza(raiz: HTMLElement): void {
175
- raiz.querySelectorAll<HTMLElement>('[data-exp]').forEach((el) => {
176
- el.onclick = (evento) => {
177
- evento.preventDefault();
178
- evento.stopPropagation();
179
- const ruta = el.dataset.ruta ?? '';
180
- if (!ruta) return;
181
- if (el.dataset.exp === 've') void this.ve(ruta);
182
- else {
183
- this.elige(ruta, el.dataset.dir === '1');
184
- this.repinta();
185
- }
186
- };
167
+ /**
168
+ * What is in this folder, said before you scroll.
169
+ *
170
+ * Folders sort first, as every file picker has done for forty years — which
171
+ * means in a folder with eight of them the files start below the fold, and a
172
+ * list that does not say they are there reads as a folder-only picker. This
173
+ * line is the difference between "there are no files here" and "keep going".
174
+ */
175
+ private cuenta(l: Listado | null): string {
176
+ if (!l || !l.entradas.length) return '';
177
+ const carpetas = l.entradas.filter((e) => e.dir).length;
178
+ const ficheros = l.entradas.length - carpetas;
179
+ const trozos = [];
180
+ if (carpetas) trozos.push(plural(carpetas, '{n} folder', '{n} folders'));
181
+ if (ficheros) trozos.push(plural(ficheros, '{n} file', '{n} files'));
182
+ return `<p class="expCuenta">${this.p.esc(trozos.join(' · '))}${
183
+ ficheros ? ` — ${_('files are below the folders')}` : ''
184
+ }</p>`;
185
+ }
186
+
187
+ /**
188
+ * One listener on the root, not one per row.
189
+ *
190
+ * A folder with two thousand entries has four thousand buttons in it, and
191
+ * binding a closure to each of them on every repaint is most of what a
192
+ * repaint costs. Delegation also survives the repaint, so the handler is
193
+ * attached exactly once for the life of the picker.
194
+ */
195
+ protected enlaza(raiz: HTMLElement): void {
196
+ if (raiz.dataset.expEnlazado === '1') return;
197
+ raiz.dataset.expEnlazado = '1';
198
+ raiz.addEventListener('click', (evento) => {
199
+ const el = (evento.target as HTMLElement | null)?.closest<HTMLElement>('[data-exp]');
200
+ if (!el || !raiz.contains(el)) return;
201
+ evento.preventDefault();
202
+ evento.stopPropagation();
203
+ const ruta = el.dataset.ruta ?? '';
204
+ if (!ruta) return;
205
+ // A row's own tick is this picker's business — the Hall's mount dialog
206
+ // has no other way to show it. What the host must NOT do is repaint
207
+ // itself around us on every click; that is what rendered the listing
208
+ // twice per tick, and it is fixed on the host's side.
209
+ if (el.dataset.exp === 've') void this.ve(ruta);
210
+ else {
211
+ this.elige(ruta);
212
+ this.repinta();
213
+ }
187
214
  });
188
215
  }
189
216
  }