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.
- package/.claude-plugin/marketplace.json +1 -1
- package/README.es.md +240 -11
- package/README.md +238 -11
- package/benchmarks/latency/fake-claude-cli.mjs +2 -1
- package/benchmarks/reception/README.md +16 -0
- package/benchmarks/reception/run.mjs +84 -0
- package/bin/agents-city.js +1 -0
- package/bin/connect +5 -0
- package/bin/hall.html +117 -18
- package/bin/navegador.mjs +170 -1
- package/bin/serve.py +201 -101
- package/bin/test +15 -3
- package/bin/test-arnes.py +192 -0
- package/bin/test-cage.py +9 -0
- package/bin/test-channel.py +178 -12
- package/bin/test-claude-runtime.py +132 -4
- package/bin/test-connect-client.mjs +609 -0
- package/bin/test-connect.py +52 -0
- package/bin/test-contracts.py +214 -0
- package/bin/test-diario.py +185 -0
- package/bin/test-doctor.py +31 -0
- package/bin/test-i18n.py +189 -0
- package/bin/test-navegador.py +61 -0
- package/bin/test-seat.py +78 -20
- package/bin/test-security.py +2 -0
- package/bin/test-serve.py +366 -0
- package/city/web/dist-hall/hall.js +789 -214
- package/city/web/src/bienvenida.ts +14 -13
- package/city/web/src/casa.ts +143 -56
- package/city/web/src/demo.ts +61 -31
- package/city/web/src/es.ts +187 -27
- package/city/web/src/explorador.ts +70 -43
- package/city/web/src/hall.ts +520 -103
- package/city/web/src/idioma.ts +21 -1
- package/city/web/src/motores.ts +48 -1
- package/city/web/src/vista.ts +58 -0
- package/demo/graba.py +5 -10
- package/docs/managed-connect.md +237 -0
- package/docs/security.md +41 -2
- package/package.json +8 -4
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/channel/adapter-prompts.ts +10 -0
- package/plugin/channel/adapter.js +78 -31
- package/plugin/channel/agents_city_hybrid_crypto_bg.wasm +0 -0
- package/plugin/channel/bus.js +109 -65
- package/plugin/channel/bus.ts +11 -1
- package/plugin/channel/client.js +67 -30
- package/plugin/channel/delivery-queue.ts +160 -21
- package/plugin/channel/hub/remote-roads.ts +32 -1
- package/plugin/channel/hub/road-controller.ts +116 -16
- package/plugin/channel/kinsh_vodozemac_wasm_bg.wasm +0 -0
- package/plugin/channel/licenses/CONNECT_CLIENT_THIRD_PARTY_NOTICES.md +21 -0
- package/plugin/channel/licenses/HYBRID_CRYPTO_THIRD_PARTY_NOTICES.md +12 -0
- package/plugin/channel/licenses/hybrid-crypto-Apache-2.0.txt +201 -0
- package/plugin/channel/licenses/keyring-MIT.txt +21 -0
- package/plugin/channel/licenses/vodozemac-Apache-2.0.txt +201 -0
- package/plugin/channel/local-hub.js +2080 -97
- package/plugin/channel/local-hub.ts +5 -4
- package/plugin/channel/managed-connect/bridge.ts +208 -0
- package/plugin/channel/managed-connect/cli.ts +416 -0
- package/plugin/channel/managed-connect/device.ts +15 -0
- package/plugin/channel/managed-connect/local-cities.ts +94 -0
- package/plugin/channel/managed-connect/person-message.ts +74 -0
- package/plugin/channel/managed-connect/reception-bridge.ts +341 -0
- package/plugin/channel/managed-connect/relay-session.ts +7 -0
- package/plugin/channel/managed-connect/storage.ts +653 -0
- package/plugin/channel/managed-connect/transport.ts +87 -0
- package/plugin/channel/managed-connect-cli.js +4777 -0
- package/plugin/channel/managed-connect-cli.ts +7 -0
- package/plugin/channel/managed-connect-client.d.ts +261 -0
- package/plugin/channel/managed-connect-client.js +6572 -0
- package/plugin/channel/managed-connect-client.manifest.json +38 -0
- package/plugin/channel/package-lock.json +123 -105
- package/plugin/channel/package.json +4 -4
- package/plugin/channel/protocol.ts +4 -0
- package/plugin/channel/reception.ts +896 -0
- package/plugin/channel/road-cli.ts +1 -1
- package/plugin/channel/runtime/arnes.json +189 -0
- package/plugin/channel/runtime/arnes.ts +47 -0
- package/plugin/channel/runtime/claude.ts +18 -0
- package/plugin/channel/runtime/codex-config.ts +51 -1
- package/plugin/channel/runtime/codex.ts +31 -11
- package/plugin/channel/runtime/kimi.ts +6 -4
- package/plugin/channel/runtime-files.ts +39 -5
- package/plugin/channel/runtime-gateway.js +426 -100
- package/plugin/channel/runtime-gateway.ts +12 -0
- package/plugin/channel/trust/agents-city-sandbox-roots.json +66 -0
- package/plugin/scripts/arnes.py +271 -0
- package/plugin/scripts/busca.py +46 -10
- package/plugin/scripts/cage.py +5 -0
- package/plugin/scripts/city-session.sh +144 -28
- package/plugin/scripts/crecimiento.py +4 -1
- package/plugin/scripts/demos.py +58 -43
- package/plugin/scripts/desinstala.py +36 -21
- package/plugin/scripts/diario.py +119 -0
- package/plugin/scripts/doctor.py +90 -15
- package/plugin/scripts/read-card.py +48 -8
- 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
|
-
|
|
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 & 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
|
-
|
|
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
|
-
|
|
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 ?? "")}"
|
|
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.
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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
|
-
|
|
500
|
-
[""].concat(
|
|
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
|
|
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" ${
|
|
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
|
-
|
|
519
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
778
|
+
this.recoge();
|
|
542
779
|
this.repinta();
|
|
543
780
|
});
|
|
544
781
|
}
|
|
545
|
-
/** Keep what is typed when a click forces a repaint.
|
|
546
|
-
|
|
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
|
-
|
|
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
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
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
|
-
|
|
988
|
+
super.monta(host);
|
|
744
989
|
}
|
|
745
|
-
/** Stop the clock.
|
|
746
|
-
*
|
|
747
|
-
|
|
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
|
-
|
|
756
|
-
this.
|
|
757
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
808
|
-
this.
|
|
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
|
-
|
|
852
|
-
|
|
853
|
-
|
|
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.
|
|
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.
|
|
1145
|
+
this.desmonta();
|
|
883
1146
|
this.repinta();
|
|
884
1147
|
break;
|
|
885
1148
|
case "replay":
|
|
886
|
-
this.
|
|
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
|
-
/**
|
|
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
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
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"
|
|
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
|
-
|
|
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
|
-
|
|
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"
|
|
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
|
|
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"
|
|
1730
|
-
|
|
1731
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
1877
|
-
<div class="cifra"><b>${Object.values(E.skills).reduce((n, a) => n + a.skills.length, 0)}</b><span
|
|
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" : ""}"
|
|
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" : ""}"
|
|
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"
|
|
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"
|
|
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>` :
|
|
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"
|
|
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"
|
|
1997
|
-
<p class="cargando"
|
|
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"
|
|
2000
|
-
<div><span class="sub"
|
|
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
|
|
2303
|
+
one works on \u2014 lives in <b>${t("Agents & 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"
|
|
2006
|
-
<div><span class="sub"
|
|
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"
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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"
|
|
2121
|
-
<h1 style="margin-top:6px"
|
|
2122
|
-
<p class="prosa" style="margin-top:8px"
|
|
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
|
|
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"
|
|
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"
|
|
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("") ||
|
|
2275
|
-
<div><span class="sub"
|
|
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("") ||
|
|
2281
|
-
<div class="orden"><span class="et2"
|
|
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"
|
|
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
|
|
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("") ||
|
|
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
|
|
2354
|
-
const opcionesModelo = opciones2([""].concat(
|
|
2355
|
-
const opcionesEsfuerzo = opciones2(
|
|
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
|
-
${
|
|
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 ?
|
|
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"
|
|
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
|
|
2421
|
-
<p class="prosa"
|
|
2422
|
-
to it: each one
|
|
2423
|
-
|
|
2424
|
-
|
|
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
|
-
|
|
2617
|
-
|
|
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();
|