agents-city 0.5.2 → 0.5.4
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 +11 -0
- package/README.md +10 -0
- package/benchmarks/latency/fake-claude-cli.mjs +3 -2
- package/bin/serve.py +63 -6
- package/bin/test +2 -2
- package/bin/test-alcance.py +470 -0
- package/bin/test-arnes.py +109 -0
- package/bin/test-channel.py +295 -9
- package/bin/test-cities.py +57 -0
- package/bin/test-claude-runtime.py +17 -6
- package/bin/test-connect-client.mjs +8 -0
- package/bin/test-connect.py +1 -1
- package/bin/test-contracts.py +129 -3
- package/bin/test-e2e.py +371 -0
- package/bin/test-seat.py +154 -1
- package/bin/test-serve.py +83 -0
- package/city/web/dist-hall/hall.js +384 -3240
- package/docs/managed-connect.md +5 -0
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/channel/hub/lo-que-te-llega.ts +78 -0
- package/plugin/channel/hub/local-roads.ts +36 -0
- package/plugin/channel/hub/road-controller.ts +30 -5
- package/plugin/channel/local-hub.js +177 -105
- package/plugin/channel/local-hub.ts +8 -0
- package/plugin/channel/managed-connect-client.js +53 -3
- package/plugin/channel/managed-connect-client.manifest.json +2 -2
- package/plugin/channel/protocol.ts +24 -0
- package/plugin/commands/notice.md +14 -0
- package/plugin/hooks/ask-the-house.sh +25 -0
- package/plugin/hooks/hooks.json +10 -0
- package/plugin/hooks/notice-on-stop.sh +7 -1
- package/plugin/scripts/alcance.py +318 -0
- package/plugin/scripts/arnes.py +10 -1
- package/plugin/scripts/busca.py +13 -0
- package/plugin/scripts/card.py +40 -0
- package/plugin/scripts/cities.py +46 -4
- package/plugin/scripts/city-session.sh +80 -7
- package/plugin/scripts/domains.py +7 -0
- package/plugin/scripts/seat.py +33 -13
- package/plugin/skills/city/SKILL.md +12 -0
|
@@ -1,2426 +1,349 @@
|
|
|
1
|
-
// city/web/src/idioma.ts
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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();
|
|
13
|
-
}
|
|
14
|
-
function anota(pares) {
|
|
15
|
-
for (const [fuente, texto] of Object.entries(pares)) ES[clave(fuente)] = texto;
|
|
16
|
-
}
|
|
17
|
-
var ELEGIDO = "";
|
|
18
|
-
function idioma() {
|
|
19
|
-
if (ELEGIDO === "es" || ELEGIDO === "en") return ELEGIDO;
|
|
20
|
-
try {
|
|
21
|
-
const guardado = localStorage.getItem("hall-idioma");
|
|
22
|
-
if (guardado === "es" || guardado === "en") {
|
|
23
|
-
ELEGIDO = guardado;
|
|
24
|
-
return ELEGIDO;
|
|
25
|
-
}
|
|
26
|
-
} catch {
|
|
27
|
-
}
|
|
28
|
-
const suyo = (navigator.language || "en").toLowerCase();
|
|
29
|
-
ELEGIDO = suyo.startsWith("es") ? "es" : "en";
|
|
30
|
-
return ELEGIDO;
|
|
31
|
-
}
|
|
32
|
-
function ponIdioma(cual) {
|
|
33
|
-
ELEGIDO = cual;
|
|
34
|
-
try {
|
|
35
|
-
localStorage.setItem("hall-idioma", cual);
|
|
36
|
-
} catch {
|
|
37
|
-
}
|
|
38
|
-
document.documentElement.lang = cual;
|
|
39
|
-
}
|
|
40
|
-
function t(fuente, valores) {
|
|
41
|
-
const texto = idioma() === "es" ? ES[clave(fuente)] ?? fuente : fuente;
|
|
42
|
-
if (!valores) return texto;
|
|
43
|
-
return texto.replace(
|
|
44
|
-
/\{(\w+)\}/g,
|
|
45
|
-
(_, clave2) => valores[clave2] === void 0 ? `{${clave2}}` : String(valores[clave2])
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
function plural(n, una, varias) {
|
|
49
|
-
return t(n === 1 ? una : varias, { n: String(n) });
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// city/web/src/es.ts
|
|
53
|
-
anota({
|
|
54
|
-
// ── the rail ──────────────────────────────────────────────────────────────
|
|
55
|
-
Overview: "Resumen",
|
|
56
|
-
"The map": "El mapa",
|
|
57
|
-
"My seat": "Mi asiento",
|
|
58
|
-
Districts: "Distritos",
|
|
59
|
-
Roads: "Carreteras",
|
|
60
|
-
Reception: "Recepci\xF3n",
|
|
61
|
-
Committee: "Comit\xE9",
|
|
62
|
-
Houses: "Casas",
|
|
63
|
-
"All cities": "Todas las ciudades",
|
|
64
|
-
"you are in": "est\xE1s en",
|
|
65
|
-
"{n} house": "{n} casa",
|
|
66
|
-
"{n} houses": "{n} casas",
|
|
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.",
|
|
131
|
-
// ── the guide ─────────────────────────────────────────────────────────────
|
|
132
|
-
Welcome: "Bienvenida",
|
|
133
|
-
"The work": "El trabajo",
|
|
134
|
-
"Your chair": "Tu silla",
|
|
135
|
-
"The houses": "Las casas",
|
|
136
|
-
Ready: "Listo",
|
|
137
|
-
"A city is you, and the houses around you": "Una ciudad eres t\xFA y las casas que te rodean",
|
|
138
|
-
"Let\u2019s build it": "Vamos a construirla",
|
|
139
|
-
"I\u2019ll set it up myself": "Ya lo configuro yo",
|
|
140
|
-
"What kind of work happens here?": "\xBFQu\xE9 clase de trabajo se hace aqu\xED?",
|
|
141
|
-
"And what are you, here?": "\xBFY t\xFA qu\xE9 eres aqu\xED?",
|
|
142
|
-
"Who lives in your city?": "\xBFQui\xE9n vive en tu ciudad?",
|
|
143
|
-
"Who else lives here?": "\xBFQui\xE9n m\xE1s vive aqu\xED?",
|
|
144
|
-
"Build the first house": "Construye la primera casa",
|
|
145
|
-
"Add another house": "A\xF1ade otra casa",
|
|
146
|
-
"That is everyone": "Ya est\xE1n todos",
|
|
147
|
-
"Skip \u2014 just me for now": "S\xE1ltalo \u2014 de momento solo yo",
|
|
148
|
-
"a new house": "una casa nueva",
|
|
149
|
-
"Who lives in it?": "\xBFQui\xE9n vive en ella?",
|
|
150
|
-
"What do you call it?": "\xBFC\xF3mo la llamas?",
|
|
151
|
-
"What kind of work does it do?": "\xBFQu\xE9 clase de trabajo hace?",
|
|
152
|
-
"It writes code": "Escribe c\xF3digo",
|
|
153
|
-
"It keeps knowledge": "Guarda conocimiento",
|
|
154
|
-
"It coordinates": "Coordina",
|
|
155
|
-
"What does it work on?": "\xBFSobre qu\xE9 trabaja?",
|
|
156
|
-
"Build it": "Constr\xFAyela",
|
|
157
|
-
"building\u2026": "construyendo\u2026",
|
|
158
|
-
Cancel: "Cancelar",
|
|
159
|
-
Back: "Atr\xE1s",
|
|
160
|
-
Next: "Siguiente",
|
|
161
|
-
"question 1 of 4": "pregunta 1 de 4",
|
|
162
|
-
"question 2 of 4": "pregunta 2 de 4",
|
|
163
|
-
"question 3 of 4": "pregunta 3 de 4",
|
|
164
|
-
"Open my session": "Abrir mi sesi\xF3n",
|
|
165
|
-
"See the houses": "Ver las casas",
|
|
166
|
-
"Draw the map": "Dibujar el mapa",
|
|
167
|
-
"Set a goal": "Poner un objetivo",
|
|
168
|
-
"{city} is alive": "{city} est\xE1 viva",
|
|
169
|
-
// ── the houses ────────────────────────────────────────────────────────────
|
|
170
|
-
"Who lives in {city}": "Qui\xE9n vive en {city}",
|
|
171
|
-
"+ Build a house": "+ Construir una casa",
|
|
172
|
-
"+ folder": "+ carpeta",
|
|
173
|
-
"+ zip": "+ zip",
|
|
174
|
-
test: "probar",
|
|
175
|
-
engine: "motor",
|
|
176
|
-
effort: "esfuerzo",
|
|
177
|
-
provider: "proveedor",
|
|
178
|
-
growth: "crecimiento",
|
|
179
|
-
"works on": "trabaja sobre",
|
|
180
|
-
skills: "skills",
|
|
181
|
-
"nothing mounted yet": "a\xFAn sin nada montado",
|
|
182
|
-
"none discovered": "ninguna descubierta",
|
|
183
|
-
connected: "conectado",
|
|
184
|
-
idle: "inactivo",
|
|
185
|
-
missing: "no instalado",
|
|
186
|
-
default: "por defecto",
|
|
187
|
-
"custom\u2026": "otro\u2026",
|
|
188
|
-
// ── cities ────────────────────────────────────────────────────────────────
|
|
189
|
-
cities: "ciudades",
|
|
190
|
-
"Your cities": "Tus ciudades",
|
|
191
|
-
"Start another city": "Empezar otra ciudad",
|
|
192
|
-
"Create it": "Crearla",
|
|
193
|
-
open: "abrir",
|
|
194
|
-
archive: "archivar",
|
|
195
|
-
"\xB7 open now": "\xB7 abierta ahora",
|
|
196
|
-
"Start this city over": "Empezar esta ciudad de cero",
|
|
197
|
-
"Start over\u2026": "Empezar de cero\u2026",
|
|
198
|
-
// ── the map ───────────────────────────────────────────────────────────────
|
|
199
|
-
"the map": "el mapa",
|
|
200
|
-
"Your city, drawn": "Tu ciudad, dibujada",
|
|
201
|
-
"Draw my city": "Dibujar mi ciudad",
|
|
202
|
-
"Draw it anyway": "Dibujarla igualmente",
|
|
203
|
-
"baking the map \u2014 first time takes a minute": "horneando el mapa \u2014 la primera vez tarda un minuto",
|
|
204
|
-
// ── shared ────────────────────────────────────────────────────────────────
|
|
205
|
-
Saved: "Guardado",
|
|
206
|
-
Copied: "Copiado",
|
|
207
|
-
"reading your city": "leyendo tu ciudad",
|
|
208
|
-
"Could not save": "No se pudo guardar"
|
|
209
|
-
});
|
|
210
|
-
anota({
|
|
211
|
-
// ── the chrome that lives in the page itself ──────────────────────────────
|
|
212
|
-
"town hall": "ayuntamiento",
|
|
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.",
|
|
214
|
-
// ── looking through the disk ──────────────────────────────────────────────
|
|
215
|
-
"Its role \u2014 its speciality, never authority": "Su rol \u2014 su especialidad, nunca su autoridad",
|
|
216
|
-
"What runs it?": "\xBFQu\xE9 lo mueve?"
|
|
217
|
-
});
|
|
218
|
-
anota({
|
|
219
|
-
// ── the guide, in prose ───────────────────────────────────────────────────
|
|
220
|
-
"You take the chair. Around it, one <b>house</b> per worker \u2014 an <b>agent</b> \u2014 each with its own window, its own role and its own corner of your disk: a repository, three of them, a folder of documents, whatever it actually works on. The map draws them as houses that grow with the work done in them, and many houses are a city. They never talk to each other behind your back: you chair, they answer.": "T\xFA presides. A tu alrededor, una <b>casa</b> por trabajador \u2014 un <b>agente</b> \u2014 cada una con su ventana, su rol y su rinc\xF3n de tu disco: un repositorio, tres, una carpeta de documentos, lo que de verdad tenga entre manos. El mapa las dibuja como casas que crecen con el trabajo hecho dentro, y muchas casas son una ciudad. Nunca hablan entre ellas a tus espaldas: t\xFA presides, ellas responden.",
|
|
221
|
-
"Everything lives as plain files in {donde}. Nothing leaves this machine, there is no account, and you can edit any of it by hand afterwards.": "Todo vive como ficheros de texto en {donde}. Nada sale de esta m\xE1quina, no hay cuenta que crear, y luego puedes editarlo a mano.",
|
|
222
|
-
"<b>Four questions and you are working.</b> You can skip any of them and change everything later.": "<b>Cuatro preguntas y a trabajar.</b> Puedes saltarte cualquiera y cambiarlo todo despu\xE9s.",
|
|
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.",
|
|
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.",
|
|
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.",
|
|
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.",
|
|
227
|
-
"<b>{n}</b> house stands around it": "a su alrededor hay <b>{n}</b> casa",
|
|
228
|
-
"<b>{n}</b> houses stand around it": "a su alrededor hay <b>{n}</b> casas",
|
|
229
|
-
"the city is waiting for its first house": "la ciudad espera su primera casa",
|
|
230
|
-
"What people usually do next:": "Lo que suele hacerse ahora:",
|
|
231
|
-
"A goal is optional, and the city works without one \u2014 but a round with no goal is a status report, because there is nothing to argue against.": "El objetivo es opcional y la ciudad funciona sin \xE9l \u2014 pero una ronda sin objetivo es un parte de estado, porque no hay nada contra lo que discutir.",
|
|
232
|
-
// ── what a house does for a living ────────────────────────────────────────
|
|
233
|
-
"Its house grows with the pull requests it merges. Mounts repositories and worktrees.": "Su casa crece con las pull requests que fusiona. Monta repositorios y worktrees.",
|
|
234
|
-
"Its house grows with the documents it writes. Needs no git at all.": "Su casa crece con los documentos que escribe. No necesita git para nada.",
|
|
235
|
-
"Its house grows with the decisions it records.": "Su casa crece con las decisiones que deja escritas.",
|
|
236
|
-
"urgencias, api, the handbook \u2014 whatever you would say out loud": "urgencias, api, el manual \u2014 como lo llamar\xEDas en voz alta"
|
|
237
|
-
});
|
|
238
|
-
anota({
|
|
239
|
-
// ── the app's own dialogs ─────────────────────────────────────────────────
|
|
240
|
-
Yes: "S\xED",
|
|
241
|
-
"What is it called?": "\xBFC\xF3mo se llama?",
|
|
242
|
-
"home, clients, the lab \u2014 whatever you would say out loud": "casa, clientes, el laboratorio \u2014 como lo dir\xEDas en voz alta",
|
|
243
|
-
"A city is a place with its own seat, its own houses and its own map. Yours stay where they are.": "Una ciudad es un sitio con su propio asiento, sus casas y su mapa. Las que ya tienes se quedan donde est\xE1n.",
|
|
244
|
-
"Start over {city}?": "\xBFEmpezar {city} de cero?",
|
|
245
|
-
"Start over": "Empezar de cero",
|
|
246
|
-
"You lose:": "Pierdes:",
|
|
247
|
-
"You keep:": "Conservas:",
|
|
248
|
-
"Right now that is {agents} house(s), {roads} road(s) and {acts} committee act(s).": "Ahora mismo eso son {agents} casa(s), {roads} carretera(s) y {acts} acta(s) de comit\xE9.",
|
|
249
|
-
"Type the city\u2019s name to confirm": "Escribe el nombre de la ciudad para confirmar",
|
|
250
|
-
"Archive {city}?": "\xBFArchivar {city}?",
|
|
251
|
-
"Archive it": "Archivarla",
|
|
252
|
-
"It moves into .backups. Nothing is deleted, and you can put it back by hand.": "Se mueve a .backups. No se borra nada, y puedes devolverla a mano.",
|
|
253
|
-
"Which engine?": "\xBFQu\xE9 motor?",
|
|
254
|
-
"Model alias": "Alias del modelo",
|
|
255
|
-
"An alias the CLI resolves when the window opens. Anything it accepts works here.": "Un alias que la CLI resuelve al abrir la ventana. Vale cualquiera que ella acepte.",
|
|
256
|
-
"Use it": "Usarlo",
|
|
257
|
-
"Remove the skill {skill}?": "\xBFQuitar la skill {skill}?",
|
|
258
|
-
"Remove it": "Quitarla",
|
|
259
|
-
"It is deleted from this agent\u2019s own home. Nothing outside that folder is touched.": "Se borra de la casa de este agente. Nada fuera de esa carpeta se toca.",
|
|
260
|
-
"Build a house": "Construir una casa",
|
|
261
|
-
"One worker, its own window, its own corner of your disk. Everything here can be changed afterwards.": "Un trabajador, su ventana y su rinc\xF3n de tu disco. Todo esto se puede cambiar despu\xE9s.",
|
|
262
|
-
"{name} has a house now": "{name} ya tiene casa",
|
|
263
|
-
"{name} joined the city": "{name} se ha mudado a la ciudad",
|
|
264
|
-
"What else does it work on?": "\xBFSobre qu\xE9 m\xE1s trabaja?",
|
|
265
|
-
"Mount it": "Montarlo",
|
|
266
|
-
Mounted: "Montado",
|
|
267
|
-
"Stop this agent working on {what}?": "\xBFQue este agente deje de trabajar sobre {what}?",
|
|
268
|
-
"Unmount it": "Desmontarlo",
|
|
269
|
-
"The link goes. The folder it points at stays exactly where it is.": "Se va el enlace. La carpeta a la que apunta se queda exactamente donde est\xE1.",
|
|
270
|
-
// ── what the form says when something is missing ──────────────────────────
|
|
271
|
-
"Give it a name \u2014 it is how you will call it in its window": "Ponle un nombre \u2014 es como la llamar\xE1s en su ventana",
|
|
272
|
-
"Could not add that agent": "No se pudo a\xF1adir ese agente",
|
|
273
|
-
"could not mount": "no se pudo montar",
|
|
274
|
-
"Could not set its engine": "No se pudo fijar su motor",
|
|
275
|
-
"{n} mount": "{n} montaje",
|
|
276
|
-
"{n} mounts": "{n} montajes"
|
|
277
|
-
});
|
|
278
|
-
anota({
|
|
279
|
-
// ── walking the disk ──────────────────────────────────────────────────────
|
|
280
|
-
"Walk your disk and pick whatever you like: a repository, a worktree, a folder of documents, one exact file. As many as you want. Nothing is copied \u2014 each one is linked into this agent\u2019s own home.": "Recorre tu disco y coge lo que quieras: un repositorio, un worktree, una carpeta de documentos, un fichero exacto. Tantos como quieras. No se copia nada \u2014 cada uno se enlaza dentro de la casa de este agente.",
|
|
281
|
-
"Working on": "Trabaja sobre",
|
|
282
|
-
"Nothing chosen yet \u2014 an agent with no mounts is fine too.": "A\xFAn no has cogido nada \u2014 un agente sin montajes tambi\xE9n vale.",
|
|
283
|
-
"Add this folder": "A\xF1adir esta carpeta",
|
|
284
|
-
"Add this": "A\xF1adir esto",
|
|
285
|
-
"Already chosen": "Ya est\xE1 cogido",
|
|
286
|
-
"Up one folder": "Subir una carpeta",
|
|
287
|
-
"reading that folder": "leyendo esa carpeta",
|
|
288
|
-
"This folder is empty.": "Esta carpeta est\xE1 vac\xEDa.",
|
|
289
|
-
"Only the first 2000 shown.": "S\xF3lo se muestran los 2000 primeros.",
|
|
290
|
-
"A repository, a worktree, a folder of documents, one exact file. It is linked, never copied.": "Un repositorio, un worktree, una carpeta de documentos, un fichero exacto. Se enlaza, nunca se copia."
|
|
291
|
-
});
|
|
292
|
-
anota({
|
|
293
|
-
// ── the demo shelf ────────────────────────────────────────────────────────
|
|
294
|
-
Demos: "Demos",
|
|
295
|
-
demos: "demos",
|
|
296
|
-
"See a committee happen": "Mira un comit\xE9 de verdad",
|
|
297
|
-
"reading the demo shelf": "leyendo la estanter\xEDa de demos",
|
|
298
|
-
"A real question, answered by a city of agents that never talk to each other behind the chair\u2019s back. Pick the field closest to yours \u2014 the machine is the same one in all three; only the work changes.": "Una pregunta de verdad, respondida por una ciudad de agentes que nunca hablan entre ellos a espaldas de quien preside. Elige el campo m\xE1s cercano al tuyo \u2014 la maquinaria es la misma en los tres; lo que cambia es el trabajo.",
|
|
299
|
-
"{n} turns": "{n} turnos",
|
|
300
|
-
"These are recordings of real runs over the real bus, played back here. To run one live in a terminal: agents-city demo --domain software.": "Son grabaciones de ejecuciones reales sobre el bus real, reproducidas aqu\xED. Para lanzar una en vivo desde terminal: agents-city demo --domain software.",
|
|
301
|
-
"All demos": "Todas las demos",
|
|
302
|
-
Play: "Reproducir",
|
|
303
|
-
Pause: "Pausa",
|
|
304
|
-
Replay: "Repetir",
|
|
305
|
-
"{done} of {total}": "{done} de {total}",
|
|
306
|
-
"Press play. The turns arrive one by one, exactly as they did.": "Dale a reproducir. Los turnos van llegando uno a uno, tal cual llegaron.",
|
|
307
|
-
"A recording of a real run: these events came off the real bus, from the real committee. Nothing here is being decided now.": "Una grabaci\xF3n de una ejecuci\xF3n real: estos eventos salieron del bus real, del comit\xE9 real. Aqu\xED no se est\xE1 decidiendo nada ahora mismo.",
|
|
308
|
-
"Nothing recorded here": "Aqu\xED no hay nada grabado",
|
|
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."
|
|
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
|
-
};
|
|
419
|
-
|
|
420
|
-
// city/web/src/explorador.ts
|
|
421
|
-
var MARCA = { repo: "git", worktree: "wt" };
|
|
422
|
-
var Explorador = class extends Montada {
|
|
423
|
-
constructor(p, elige, yaElegidas) {
|
|
424
|
-
super();
|
|
425
|
-
this.p = p;
|
|
426
|
-
this.elige = elige;
|
|
427
|
-
this.yaElegidas = yaElegidas;
|
|
428
|
-
}
|
|
429
|
-
aqui = "";
|
|
430
|
-
listado = null;
|
|
431
|
-
cargando = false;
|
|
432
|
-
fallo = "";
|
|
433
|
-
monta(host) {
|
|
434
|
-
super.monta(host);
|
|
435
|
-
if (!this.listado && !this.cargando) void this.ve(this.aqui || "~");
|
|
436
|
-
}
|
|
437
|
-
/** Walk to a folder. An unreadable one is said out loud, not swallowed. */
|
|
438
|
-
async ve(ruta) {
|
|
439
|
-
this.cargando = true;
|
|
440
|
-
this.fallo = "";
|
|
441
|
-
if (!this.listado) this.repinta();
|
|
442
|
-
try {
|
|
443
|
-
const r = await this.p.api("/api/carpeta?path=" + encodeURIComponent(ruta));
|
|
444
|
-
if (r.error) this.fallo = r.error;
|
|
445
|
-
else {
|
|
446
|
-
this.listado = r;
|
|
447
|
-
this.aqui = r.ruta;
|
|
448
|
-
}
|
|
449
|
-
} catch (e) {
|
|
450
|
-
this.fallo = String(e);
|
|
451
|
-
} finally {
|
|
452
|
-
this.cargando = false;
|
|
453
|
-
this.repinta();
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
migas() {
|
|
457
|
-
const esc3 = this.p.esc;
|
|
458
|
-
const l = this.listado;
|
|
459
|
-
if (!l) return "";
|
|
460
|
-
const partes = l.ruta.split("/").filter(Boolean);
|
|
461
|
-
let acumulado = "";
|
|
462
|
-
const trozos = partes.map((parte) => {
|
|
463
|
-
acumulado += "/" + parte;
|
|
464
|
-
return `<button type="button" class="expMiga" data-exp="ve" data-ruta="${esc3(
|
|
465
|
-
acumulado
|
|
466
|
-
)}">${esc3(parte)}</button>`;
|
|
467
|
-
});
|
|
468
|
-
return `<button type="button" class="expMiga" data-exp="ve" data-ruta="/">/</button>` + trozos.join("<i>/</i>");
|
|
469
|
-
}
|
|
470
|
-
html() {
|
|
471
|
-
const esc3 = this.p.esc;
|
|
472
|
-
const l = this.listado;
|
|
473
|
-
const elegidas = new Set(this.yaElegidas());
|
|
474
|
-
const yaEsta = t("Already chosen");
|
|
475
|
-
const anade = t("Add this");
|
|
476
|
-
const filas = (l?.entradas ?? []).map((e) => {
|
|
477
|
-
const puesta = elegidas.has(e.ruta);
|
|
478
|
-
return `<div class="expFila ${e.dir ? "dir" : "fich"} ${puesta ? "puesta" : ""}">
|
|
479
|
-
<button type="button" class="expNombre" ${e.dir ? `data-exp="ve" data-ruta="${esc3(e.ruta)}"` : "disabled"} title="${esc3(e.ruta)}">
|
|
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>
|
|
481
|
-
<button type="button" class="expMas" data-exp="toma" data-ruta="${esc3(e.ruta)}"
|
|
482
|
-
title="${esc3(puesta ? yaEsta : anade)}">${puesta ? "\u2713" : "+"}</button>
|
|
483
|
-
</div>`;
|
|
484
|
-
}).join("");
|
|
485
|
-
const atajos = (l?.atajos ?? []).map(
|
|
486
|
-
(a) => `<button type="button" class="expAtajo ${a.ruta === l?.ruta ? "aqui" : ""}" data-exp="ve" data-ruta="${esc3(a.ruta)}">${esc3(a.nombre)}</button>`
|
|
487
|
-
).join("");
|
|
488
|
-
return `
|
|
1
|
+
var ke={},$e=new Map;function Ee(e){let n=$e.get(e);if(n!==void 0)return n;let t=_e(e);return $e.set(e,t),t}function _e(e){return e.replace(/[\u2018\u2019]/g,"'").replace(/\s+/g," ").trim()}function j(e){for(let[n,t]of Object.entries(e))ke[Ee(n)]=t}var q="";function D(){if(q==="es"||q==="en")return q;try{let n=localStorage.getItem("hall-idioma");if(n==="es"||n==="en")return q=n,q}catch{}return q=(navigator.language||"en").toLowerCase().startsWith("es")?"es":"en",q}function Te(e){q=e;try{localStorage.setItem("hall-idioma",e)}catch{}document.documentElement.lang=e}function s(e,n){let t=D()==="es"?ke[Ee(e)]??e:e;return n?t.replace(/\{(\w+)\}/g,(a,i)=>n[i]===void 0?`{${i}}`:String(n[i])):t}function x(e,n,t){return s(e===1?n:t,{n:String(e)})}j({Overview:"Resumen","The map":"El mapa","My seat":"Mi asiento",Districts:"Distritos",Roads:"Carreteras",Reception:"Recepci\xF3n",Committee:"Comit\xE9",Houses:"Casas","All cities":"Todas las ciudades","you are in":"est\xE1s en","{n} house":"{n} casa","{n} houses":"{n} casas","Create another city \u2192":"Crear otra ciudad \u2192",reception:"recepci\xF3n","Messages wait for you, not your agents":"Los mensajes te esperan a ti, no a tus agentes","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.","routing mode":"modo de enrutado","Manual review":"Revisi\xF3n manual","Every message needs a person before it reaches a city.":"Cada mensaje necesita a una persona antes de llegar a una ciudad.","Auto router not configured":"Router autom\xE1tico sin configurar","Configure Auto":"Configurar Auto","Automatic routing":"Enrutado autom\xE1tico","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.","Routing policy":"Pol\xEDtica de enrutado","Keep human review":"Mantener la revisi\xF3n humana","Use the local rule router":"Usar el router local de reglas","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.","e.g. contract, privacy, legal review":"p. ej. contrato, privacidad, revisi\xF3n legal","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.","Save routing policy":"Guardar pol\xEDtica de enrutado","Could not save the routing policy":"No se pudo guardar la pol\xEDtica de enrutado","Routing policy saved.":"Pol\xEDtica de enrutado guardada.","Reading your reception\u2026":"Leyendo tu recepci\xF3n\u2026","Could not read your reception":"No se ha podido leer tu recepci\xF3n","Reception clear":"Recepci\xF3n despejada","No remote message is waiting for a decision.":"No hay mensajes remotos esperando una decisi\xF3n.","message waiting":"{n} mensaje en espera","messages waiting":"{n} mensajes en espera","local only":"solo local","No agent has read this":"Ning\xFAn agente ha le\xEDdo esto","Send to":"Dirigir a","Route to selected cities":"Dirigir a las ciudades elegidas","Reject and reply with a reason":"Rechazar y responder con un motivo","Reason sent back securely":"Motivo que se devolver\xE1 cifrado","Reason for rejecting this message":"Motivo para rechazar este mensaje","Reject and send reason":"Rechazar y enviar el motivo","Could not route the message":"No se pudo dirigir el mensaje","Message routed. Only the selected cities can now read it.":"Mensaje dirigido. Solo las ciudades elegidas pueden leerlo ahora.","Could not reject the message":"No se pudo rechazar el mensaje","Message rejected. Your reason is queued for encrypted delivery.":"Mensaje rechazado. Tu motivo ha quedado en cola para enviarse cifrado.","New encrypted message":"Nuevo mensaje cifrado","Messages leave from this computer with end-to-end encryption.":"Los mensajes salen de este ordenador con cifrado de extremo a extremo.","message queued":"{n} mensaje en cola","messages queued":"{n} mensajes en cola","Write to":"Escribir a",Message:"Mensaje","Your message will stop in their private reception.":"Tu mensaje se detendr\xE1 en su recepci\xF3n privada.","Send securely":"Enviar de forma segura","Reply to a rejected message":"Respuesta a un mensaje rechazado",Dismiss:"Descartar","Could not queue the message":"No se pudo poner el mensaje en cola","Message queued on this computer.":"Mensaje puesto en cola en este ordenador.","Could not dismiss the response":"No se pudo descartar la respuesta","City live":"Ciudad en directo",waiting:"esperando","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.","all conversations":"todas las conversaciones","show work":"mostrar trabajo","hide work":"ocultar trabajo","websocket live":"websocket activo",reconnecting:"reconectando","session offline":"sesi\xF3n desconectada","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.","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.",Welcome:"Bienvenida","The work":"El trabajo","Your chair":"Tu silla","The houses":"Las casas",Ready:"Listo","A city is you, and the houses around you":"Una ciudad eres t\xFA y las casas que te rodean","Let\u2019s build it":"Vamos a construirla","I\u2019ll set it up myself":"Ya lo configuro yo","What kind of work happens here?":"\xBFQu\xE9 clase de trabajo se hace aqu\xED?","And what are you, here?":"\xBFY t\xFA qu\xE9 eres aqu\xED?","Who lives in your city?":"\xBFQui\xE9n vive en tu ciudad?","Who else lives here?":"\xBFQui\xE9n m\xE1s vive aqu\xED?","Build the first house":"Construye la primera casa","Add another house":"A\xF1ade otra casa","That is everyone":"Ya est\xE1n todos","Skip \u2014 just me for now":"S\xE1ltalo \u2014 de momento solo yo","a new house":"una casa nueva","Who lives in it?":"\xBFQui\xE9n vive en ella?","What do you call it?":"\xBFC\xF3mo la llamas?","What kind of work does it do?":"\xBFQu\xE9 clase de trabajo hace?","It writes code":"Escribe c\xF3digo","It keeps knowledge":"Guarda conocimiento","It coordinates":"Coordina","What does it work on?":"\xBFSobre qu\xE9 trabaja?","Build it":"Constr\xFAyela","building\u2026":"construyendo\u2026",Cancel:"Cancelar",Back:"Atr\xE1s",Next:"Siguiente","question 1 of 4":"pregunta 1 de 4","question 2 of 4":"pregunta 2 de 4","question 3 of 4":"pregunta 3 de 4","Open my session":"Abrir mi sesi\xF3n","See the houses":"Ver las casas","Draw the map":"Dibujar el mapa","Set a goal":"Poner un objetivo","{city} is alive":"{city} est\xE1 viva","Who lives in {city}":"Qui\xE9n vive en {city}","+ Build a house":"+ Construir una casa","+ folder":"+ carpeta","+ zip":"+ zip",test:"probar",engine:"motor",effort:"esfuerzo",provider:"proveedor",growth:"crecimiento","works on":"trabaja sobre",skills:"skills","nothing mounted yet":"a\xFAn sin nada montado","none discovered":"ninguna descubierta",connected:"conectado",idle:"inactivo",missing:"no instalado",default:"por defecto","custom\u2026":"otro\u2026",cities:"ciudades","Your cities":"Tus ciudades","Start another city":"Empezar otra ciudad","Create it":"Crearla",open:"abrir",archive:"archivar","\xB7 open now":"\xB7 abierta ahora","Start this city over":"Empezar esta ciudad de cero","Start over\u2026":"Empezar de cero\u2026","the map":"el mapa","Your city, drawn":"Tu ciudad, dibujada","Draw my city":"Dibujar mi ciudad","Draw it anyway":"Dibujarla igualmente","baking the map \u2014 first time takes a minute":"horneando el mapa \u2014 la primera vez tarda un minuto",Saved:"Guardado",Copied:"Copiado","reading your city":"leyendo tu ciudad","Could not save":"No se pudo guardar"});j({"town hall":"ayuntamiento","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.","Its role \u2014 its speciality, never authority":"Su rol \u2014 su especialidad, nunca su autoridad","What runs it?":"\xBFQu\xE9 lo mueve?"});j({"You take the chair. Around it, one <b>house</b> per worker \u2014 an <b>agent</b> \u2014 each with its own window, its own role and its own corner of your disk: a repository, three of them, a folder of documents, whatever it actually works on. The map draws them as houses that grow with the work done in them, and many houses are a city. They never talk to each other behind your back: you chair, they answer.":"T\xFA presides. A tu alrededor, una <b>casa</b> por trabajador \u2014 un <b>agente</b> \u2014 cada una con su ventana, su rol y su rinc\xF3n de tu disco: un repositorio, tres, una carpeta de documentos, lo que de verdad tenga entre manos. El mapa las dibuja como casas que crecen con el trabajo hecho dentro, y muchas casas son una ciudad. Nunca hablan entre ellas a tus espaldas: t\xFA presides, ellas responden.","Everything lives as plain files in {donde}. Nothing leaves this machine, there is no account, and you can edit any of it by hand afterwards.":"Todo vive como ficheros de texto en {donde}. Nada sale de esta m\xE1quina, no hay cuenta que crear, y luego puedes editarlo a mano.","<b>Four questions and you are working.</b> You can skip any of them and change everything later.":"<b>Cuatro preguntas y a trabajar.</b> Puedes saltarte cualquiera y cambiarlo todo despu\xE9s.","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.","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.","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.","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.","<b>{n}</b> house stands around it":"a su alrededor hay <b>{n}</b> casa","<b>{n}</b> houses stand around it":"a su alrededor hay <b>{n}</b> casas","the city is waiting for its first house":"la ciudad espera su primera casa","What people usually do next:":"Lo que suele hacerse ahora:","A goal is optional, and the city works without one \u2014 but a round with no goal is a status report, because there is nothing to argue against.":"El objetivo es opcional y la ciudad funciona sin \xE9l \u2014 pero una ronda sin objetivo es un parte de estado, porque no hay nada contra lo que discutir.","Its house grows with the pull requests it merges. Mounts repositories and worktrees.":"Su casa crece con las pull requests que fusiona. Monta repositorios y worktrees.","Its house grows with the documents it writes. Needs no git at all.":"Su casa crece con los documentos que escribe. No necesita git para nada.","Its house grows with the decisions it records.":"Su casa crece con las decisiones que deja escritas.","urgencias, api, the handbook \u2014 whatever you would say out loud":"urgencias, api, el manual \u2014 como lo llamar\xEDas en voz alta"});j({Yes:"S\xED","What is it called?":"\xBFC\xF3mo se llama?","home, clients, the lab \u2014 whatever you would say out loud":"casa, clientes, el laboratorio \u2014 como lo dir\xEDas en voz alta","A city is a place with its own seat, its own houses and its own map. Yours stay where they are.":"Una ciudad es un sitio con su propio asiento, sus casas y su mapa. Las que ya tienes se quedan donde est\xE1n.","Start over {city}?":"\xBFEmpezar {city} de cero?","Start over":"Empezar de cero","You lose:":"Pierdes:","You keep:":"Conservas:","Right now that is {agents} house(s), {roads} road(s) and {acts} committee act(s).":"Ahora mismo eso son {agents} casa(s), {roads} carretera(s) y {acts} acta(s) de comit\xE9.","Type the city\u2019s name to confirm":"Escribe el nombre de la ciudad para confirmar","Archive {city}?":"\xBFArchivar {city}?","Archive it":"Archivarla","It moves into .backups. Nothing is deleted, and you can put it back by hand.":"Se mueve a .backups. No se borra nada, y puedes devolverla a mano.","Which engine?":"\xBFQu\xE9 motor?","Model alias":"Alias del modelo","An alias the CLI resolves when the window opens. Anything it accepts works here.":"Un alias que la CLI resuelve al abrir la ventana. Vale cualquiera que ella acepte.","Use it":"Usarlo","Remove the skill {skill}?":"\xBFQuitar la skill {skill}?","Remove it":"Quitarla","It is deleted from this agent\u2019s own home. Nothing outside that folder is touched.":"Se borra de la casa de este agente. Nada fuera de esa carpeta se toca.","Build a house":"Construir una casa","One worker, its own window, its own corner of your disk. Everything here can be changed afterwards.":"Un trabajador, su ventana y su rinc\xF3n de tu disco. Todo esto se puede cambiar despu\xE9s.","{name} has a house now":"{name} ya tiene casa","{name} joined the city":"{name} se ha mudado a la ciudad","What else does it work on?":"\xBFSobre qu\xE9 m\xE1s trabaja?","Mount it":"Montarlo",Mounted:"Montado","Stop this agent working on {what}?":"\xBFQue este agente deje de trabajar sobre {what}?","Unmount it":"Desmontarlo","The link goes. The folder it points at stays exactly where it is.":"Se va el enlace. La carpeta a la que apunta se queda exactamente donde est\xE1.","Give it a name \u2014 it is how you will call it in its window":"Ponle un nombre \u2014 es como la llamar\xE1s en su ventana","Could not add that agent":"No se pudo a\xF1adir ese agente","could not mount":"no se pudo montar","Could not set its engine":"No se pudo fijar su motor","{n} mount":"{n} montaje","{n} mounts":"{n} montajes"});j({"Walk your disk and pick whatever you like: a repository, a worktree, a folder of documents, one exact file. As many as you want. Nothing is copied \u2014 each one is linked into this agent\u2019s own home.":"Recorre tu disco y coge lo que quieras: un repositorio, un worktree, una carpeta de documentos, un fichero exacto. Tantos como quieras. No se copia nada \u2014 cada uno se enlaza dentro de la casa de este agente.","Working on":"Trabaja sobre","Nothing chosen yet \u2014 an agent with no mounts is fine too.":"A\xFAn no has cogido nada \u2014 un agente sin montajes tambi\xE9n vale.","Add this folder":"A\xF1adir esta carpeta","Add this":"A\xF1adir esto","Already chosen":"Ya est\xE1 cogido","Up one folder":"Subir una carpeta","reading that folder":"leyendo esa carpeta","This folder is empty.":"Esta carpeta est\xE1 vac\xEDa.","Only the first 2000 shown.":"S\xF3lo se muestran los 2000 primeros.","A repository, a worktree, a folder of documents, one exact file. It is linked, never copied.":"Un repositorio, un worktree, una carpeta de documentos, un fichero exacto. Se enlaza, nunca se copia."});j({Demos:"Demos",demos:"demos","See a committee happen":"Mira un comit\xE9 de verdad","reading the demo shelf":"leyendo la estanter\xEDa de demos","A real question, answered by a city of agents that never talk to each other behind the chair\u2019s back. Pick the field closest to yours \u2014 the machine is the same one in all three; only the work changes.":"Una pregunta de verdad, respondida por una ciudad de agentes que nunca hablan entre ellos a espaldas de quien preside. Elige el campo m\xE1s cercano al tuyo \u2014 la maquinaria es la misma en los tres; lo que cambia es el trabajo.","{n} turns":"{n} turnos","These are recordings of real runs over the real bus, played back here. To run one live in a terminal: agents-city demo --domain software.":"Son grabaciones de ejecuciones reales sobre el bus real, reproducidas aqu\xED. Para lanzar una en vivo desde terminal: agents-city demo --domain software.","All demos":"Todas las demos",Play:"Reproducir",Pause:"Pausa",Replay:"Repetir","{done} of {total}":"{done} de {total}","Press play. The turns arrive one by one, exactly as they did.":"Dale a reproducir. Los turnos van llegando uno a uno, tal cual llegaron.","A recording of a real run: these events came off the real bus, from the real committee. Nothing here is being decided now.":"Una grabaci\xF3n de una ejecuci\xF3n real: estos eventos salieron del bus real, del comit\xE9 real. Aqu\xED no se est\xE1 decidiendo nada ahora mismo.","Nothing recorded here":"Aqu\xED no hay nada grabado","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."});j({"{n} folder":"{n} carpeta","{n} folders":"{n} carpetas","{n} file":"{n} fichero","{n} files":"{n} ficheros","files are below the folders":"los ficheros van debajo de las carpetas"});j({"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.","This CLI has no effort setting.":"Esta CLI no tiene ajuste de esfuerzo.","An alias the Claude CLI resolves when the window opens.":"Un alias que la CLI de Claude resuelve al abrir la ventana.","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.","OpenCode names a model provider/model, like anthropic/claude-sonnet-4.":"OpenCode nombra los modelos proveedor/modelo, como anthropic/claude-sonnet-4.","The model name your Kimi CLI uses.":"El nombre del modelo que usa tu CLI de Kimi."});j({"my seat":"mi asiento","Save my seat":"Guardar mi asiento","work domain":"dominio de trabajo","your day":"tu d\xEDa","the agents":"los agentes","repo agents":"agentes de repo","one goal \u2014 optional":"un objetivo \u2014 opcional","The goal, in one line":"El objetivo, en una l\xEDnea","Concrete enough to argue with \u2014 empty skips it":"Lo bastante concreto como para discutirlo \u2014 vac\xEDo lo salta","How it is measured":"C\xF3mo se mide","in prose: the architect reads the AGENTS.md files on Fridays":"en prosa: el arquitecto lee los AGENTS.md los viernes","The command that returns it, if a command can":"El comando que lo devuelve, si un comando puede","What it returns today":"Lo que devuelve hoy","Where it has to get to":"A d\xF3nde tiene que llegar","By when":"Para cu\xE1ndo","empty for a qualitative goal":"vac\xEDo para un objetivo cualitativo","\u2026or who judges it, and how often":"\u2026o qui\xE9n lo juzga, y cada cu\xE1nto","what is left":"lo que queda","open my session":"abrir mi sesi\xF3n","Open the roster":"Abrir la lista","districts & houses":"distritos y casas","Save districts & houses":"Guardar distritos y casas","another district\u2026":"otro distrito\u2026","The modelling no tool can do for you":"El modelado que ninguna herramienta puede hacer por ti","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.","Cities this one may reach":"Ciudades a las que \xE9sta puede llegar","No roads yet. This city is isolated on purpose.":"A\xFAn no hay carreteras. Esta ciudad est\xE1 aislada a prop\xF3sito.","No unconnected local cities.":"No hay ciudades locales sin conectar.","other cities on this machine":"otras ciudades en esta m\xE1quina","remote invitation \xB7 public, no token":"invitaci\xF3n remota \xB7 p\xFAblica, sin token","committee acts":"actas del comit\xE9","Decisions with a visible chain of custody":"Decisiones con una cadena de custodia visible","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.","agents-city committee schema open":"agents-city committee schema open","guided committee":"comit\xE9 guiado","Play the guided committee from the top":"Reproducir el comit\xE9 guiado desde el principio","Pause or resume mid-scene":"Pausar o seguir a mitad de escena","\u27F3 replay":"\u27F3 repetir","\u23F8 pause":"\u23F8 pausa","seat moderates":"el asiento modera","read full message":"leer el mensaje entero",'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.',"client-a, research, the book\u2026":"cliente-a, investigaci\xF3n, el libro\u2026","which city this hall manages":"qu\xE9 ciudad gobierna este ayuntamiento","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.","Agents & skills":"Agentes y skills",'<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.',"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.","a legacy repo agent works on its own repo":"un agente de repo heredado trabaja sobre su propio repo","data repo":"repo de datos","skills recognised":"skills reconocidas","plugin installed":"plugin instalado","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","Instructions the Claude runtime reads":"Instrucciones que lee el runtime de Claude","Instructions Codex, OpenCode and Kimi read":"Instrucciones que leen Codex, OpenCode y Kimi","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","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","Remove this skill from the agent\u2019s home":"Quitar esta skill de la casa del agente","reading the domain packs":"leyendo los paquetes de dominio","reading the role files":"leyendo los ficheros de rol","an isometric house":"una casa isom\xE9trica"});var H=class{host=null;monta(n){this.host=n,this.dibuja()}dibuja(){this.host&&(this.host.innerHTML=this.html(),this.enlaza(this.host))}repinta(){this.dibuja()}};var Ue={repo:"git",worktree:"wt"},B=class extends H{constructor(t,a,i){super();this.p=t;this.elige=a;this.yaElegidas=i}aqui="";listado=null;cargando=!1;fallo="";monta(t){super.monta(t),!this.listado&&!this.cargando&&this.ve(this.aqui||"~")}async ve(t){this.cargando=!0,this.fallo="",this.listado||this.repinta();try{let a=await this.p.api("/api/carpeta?path="+encodeURIComponent(t));a.error?this.fallo=a.error:(this.listado=a,this.aqui=a.ruta)}catch(a){this.fallo=String(a)}finally{this.cargando=!1,this.repinta()}}migas(){let t=this.p.esc,a=this.listado;if(!a)return"";let i=a.ruta.split("/").filter(Boolean),c="";return'<button type="button" class="expMiga" data-exp="ve" data-ruta="/">/</button>'+i.map(r=>(c+="/"+r,`<button type="button" class="expMiga" data-exp="ve" data-ruta="${t(c)}">${t(r)}</button>`)).join("<i>/</i>")}html(){let t=this.p.esc,a=this.listado,i=new Set(this.yaElegidas()),c=s("Already chosen"),o=s("Add this"),r=(a?.entradas??[]).map(u=>{let h=i.has(u.ruta);return`<div class="expFila ${u.dir?"dir":"fich"} ${h?"puesta":""}">
|
|
2
|
+
<button type="button" class="expNombre" ${u.dir?`data-exp="ve" data-ruta="${t(u.ruta)}"`:"disabled"} title="${t(u.ruta)}">
|
|
3
|
+
<i class="expIcono">${u.dir?"\u25B8":"\xB7"}</i>${t(u.nombre)}${u.git?`<em class="expMarca">${t(Ue[u.git]??u.git)}</em>`:""}${u.enlace?'<em class="expMarca">link</em>':""}</button>
|
|
4
|
+
<button type="button" class="expMas" data-exp="toma" data-ruta="${t(u.ruta)}"
|
|
5
|
+
title="${t(h?c:o)}">${h?"\u2713":"+"}</button>
|
|
6
|
+
</div>`}).join("");return`
|
|
489
7
|
<div class="exp">
|
|
490
|
-
<div class="expAtajos">${atajos}</div>
|
|
8
|
+
<div class="expAtajos">${(a?.atajos??[]).map(u=>`<button type="button" class="expAtajo ${u.ruta===a?.ruta?"aqui":""}" data-exp="ve" data-ruta="${t(u.ruta)}">${t(u.nombre)}</button>`).join("")}</div>
|
|
491
9
|
<div class="expBarra">
|
|
492
10
|
<button type="button" class="expSubir" data-exp="ve"
|
|
493
|
-
data-ruta="${
|
|
494
|
-
title="${
|
|
11
|
+
data-ruta="${t(a?.arriba??"~")}" ${a?.arriba?"":"disabled"}
|
|
12
|
+
title="${s("Up one folder")}">\u2191</button>
|
|
495
13
|
<div class="expMigas">${this.migas()}</div>
|
|
496
14
|
</div>
|
|
497
|
-
${this.cuenta(
|
|
498
|
-
${this.fallo
|
|
499
|
-
${
|
|
15
|
+
${this.cuenta(a)}
|
|
16
|
+
${this.fallo?`<p class="pista malo">${t(this.fallo)}</p>`:this.cargando&&!a?`<p class="cargando">${s("reading that folder")}</p>`:r?`<div class="expLista">${r}</div>`:`<p class="pista">${s("This folder is empty.")}</p>`}
|
|
17
|
+
${a?.recortada?`<p class="pista">${s("Only the first 2000 shown.")}</p>`:""}
|
|
500
18
|
<button type="button" class="bt bvMini expTomaAqui" data-exp="toma"
|
|
501
|
-
data-ruta="${
|
|
502
|
-
</div
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
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
|
-
*/
|
|
529
|
-
enlaza(raiz) {
|
|
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
|
-
}
|
|
544
|
-
});
|
|
545
|
-
}
|
|
546
|
-
};
|
|
547
|
-
|
|
548
|
-
// city/web/src/motores.ts
|
|
549
|
-
var RUNTIMES = ["claude", "codex", "opencode", "kimi"];
|
|
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;
|
|
572
|
-
var NIVEL_ESFUERZO = {
|
|
573
|
-
low: 1,
|
|
574
|
-
medium: 2,
|
|
575
|
-
high: 3,
|
|
576
|
-
xhigh: 4,
|
|
577
|
-
max: 5
|
|
578
|
-
};
|
|
579
|
-
var ESFUERZOS = Object.keys(NIVEL_ESFUERZO);
|
|
580
|
-
function opciones(valores, actual, esc3, porDefecto = "default") {
|
|
581
|
-
return valores.concat(actual && !valores.includes(actual) ? [actual] : []).map(
|
|
582
|
-
(v) => `<option value="${esc3(v)}"${v === actual ? " selected" : ""}>${esc3(v || porDefecto)}</option>`
|
|
583
|
-
).join("");
|
|
584
|
-
}
|
|
585
|
-
function nivelDeMotor(model) {
|
|
586
|
-
const alias = model.toLowerCase();
|
|
587
|
-
if (!alias) return { ancho: 0.5, texto: "default", defecto: true };
|
|
588
|
-
for (const [clave2, ancho] of [
|
|
589
|
-
["fable", 1],
|
|
590
|
-
["opus", 0.8],
|
|
591
|
-
["sonnet", 0.55],
|
|
592
|
-
["haiku", 0.35]
|
|
593
|
-
]) {
|
|
594
|
-
if (alias.includes(clave2)) return { ancho, texto: alias, defecto: false };
|
|
595
|
-
}
|
|
596
|
-
return { ancho: 0.6, texto: alias, defecto: false };
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
// city/web/src/casa.ts
|
|
600
|
-
var CLASES = [
|
|
601
|
-
[
|
|
602
|
-
"code",
|
|
603
|
-
"It writes code",
|
|
604
|
-
"Its house grows with the pull requests it merges. Mounts repositories and worktrees."
|
|
605
|
-
],
|
|
606
|
-
[
|
|
607
|
-
"knowledge",
|
|
608
|
-
"It keeps knowledge",
|
|
609
|
-
"Its house grows with the documents it writes. Needs no git at all."
|
|
610
|
-
],
|
|
611
|
-
["coordinator", "It coordinates", "Its house grows with the decisions it records."]
|
|
612
|
-
];
|
|
613
|
-
var FormularioDeCasa = class extends Montada {
|
|
614
|
-
constructor(p, roles = []) {
|
|
615
|
-
super();
|
|
616
|
-
this.p = p;
|
|
617
|
-
this.roles = roles;
|
|
618
|
-
}
|
|
619
|
-
datos = {
|
|
620
|
-
nombre: "",
|
|
621
|
-
clase: "code",
|
|
622
|
-
rol: "blank",
|
|
623
|
-
montajes: [],
|
|
624
|
-
runtime: "",
|
|
625
|
-
modelo: "",
|
|
626
|
-
esfuerzo: ""
|
|
627
|
-
};
|
|
628
|
-
roles = [];
|
|
629
|
-
rolesPedidos = false;
|
|
630
|
-
monta(host) {
|
|
631
|
-
void this.aseguraRoles();
|
|
632
|
-
super.monta(host);
|
|
633
|
-
const hueco = host.querySelector(".casaExp");
|
|
634
|
-
if (hueco) this.explora().monta(hueco);
|
|
635
|
-
}
|
|
636
|
-
repinta() {
|
|
637
|
-
if (this.host) this.monta(this.host);
|
|
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. */
|
|
642
|
-
async aseguraRoles() {
|
|
643
|
-
if (this.rolesPedidos) return;
|
|
644
|
-
this.rolesPedidos = true;
|
|
645
|
-
try {
|
|
646
|
-
const r = await this.p.api("/api/roles?scope=agent");
|
|
647
|
-
this.roles = r.roles ?? [];
|
|
648
|
-
this.pintaRoles();
|
|
649
|
-
} catch {
|
|
650
|
-
this.roles = [];
|
|
651
|
-
}
|
|
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
|
-
}
|
|
667
|
-
/** The folder picker, built once so walking survives a repaint. */
|
|
668
|
-
explorador = null;
|
|
669
|
-
explora() {
|
|
670
|
-
if (!this.explorador)
|
|
671
|
-
this.explorador = new Explorador(
|
|
672
|
-
this.p,
|
|
673
|
-
(ruta) => {
|
|
674
|
-
const ya = this.datos.montajes.indexOf(ruta);
|
|
675
|
-
if (ya >= 0) this.datos.montajes.splice(ya, 1);
|
|
676
|
-
else this.datos.montajes.push(ruta);
|
|
677
|
-
this.pintaElegidas();
|
|
678
|
-
},
|
|
679
|
-
() => this.datos.montajes
|
|
680
|
-
);
|
|
681
|
-
return this.explorador;
|
|
682
|
-
}
|
|
683
|
-
html() {
|
|
684
|
-
const a = this.datos;
|
|
685
|
-
const esc3 = this.p.esc;
|
|
686
|
-
const clases = CLASES.map(
|
|
687
|
-
([id, titulo, porque]) => `
|
|
688
|
-
<button type="button" class="bvOpcion ${a.clase === id ? "elegida" : ""}"
|
|
689
|
-
data-bv="clase" data-id="${id}">
|
|
690
|
-
<b>${t(titulo)}</b><span>${t(porque)}</span></button>`
|
|
691
|
-
).join("");
|
|
692
|
-
const roles = this.roles.map(
|
|
693
|
-
(r) => `<option value="${esc3(r.id)}"${r.id === a.rol ? " selected" : ""}>${esc3(r.name)}</option>`
|
|
694
|
-
).join("");
|
|
695
|
-
const motor = motorDe(a.runtime);
|
|
696
|
-
return `
|
|
697
|
-
<div class="campo"><label>${t("What do you call it?")}</label>
|
|
698
|
-
<input type="text" id="bvNombre" value="${esc3(a.nombre)}"
|
|
699
|
-
placeholder="${t("urgencias, api, the handbook \u2014 whatever you would say out loud")}"></div>
|
|
700
|
-
<label class="bvEtiqueta">${t("What kind of work does it do?")}</label>
|
|
701
|
-
<div class="bvRejilla bvTres">${clases}</div>
|
|
702
|
-
<div class="campo"><label>${t("Its role \u2014 its speciality, never authority")}</label>
|
|
703
|
-
<select id="bvRol">${roles}</select></div>
|
|
704
|
-
<label class="bvEtiqueta">${t("What runs it?")}</label>
|
|
705
|
-
<p class="pista">${t(
|
|
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."
|
|
707
|
-
)}</p>
|
|
19
|
+
data-ruta="${t(a?.ruta??"")}" ${a?"":"disabled"}>${s("Add this folder")}</button>
|
|
20
|
+
</div>`}cuenta(t){if(!t||!t.entradas.length)return"";let a=t.entradas.filter(o=>o.dir).length,i=t.entradas.length-a,c=[];return a&&c.push(x(a,"{n} folder","{n} folders")),i&&c.push(x(i,"{n} file","{n} files")),`<p class="expCuenta">${this.p.esc(c.join(" \xB7 "))}${i?` \u2014 ${s("files are below the folders")}`:""}</p>`}enlaza(t){t.dataset.expEnlazado!=="1"&&(t.dataset.expEnlazado="1",t.addEventListener("click",a=>{let i=a.target?.closest("[data-exp]");if(!i||!t.contains(i))return;a.preventDefault(),a.stopPropagation();let c=i.dataset.ruta??"";c&&(i.dataset.exp==="ve"?this.ve(c):(this.elige(c),this.repinta()))}))}};var G=["claude","codex","opencode","kimi"],ce={claude:{modelos:["haiku","sonnet","opus","fable"],esfuerzo:!0,pista:"An alias the Claude CLI resolves when the window opens."},codex:{modelos:[],esfuerzo:!0,pista:"The model name your Codex uses \u2014 the one in ~/.codex/config.toml."},opencode:{modelos:[],esfuerzo:!1,pista:"OpenCode names a model provider/model, like anthropic/claude-sonnet-4."},kimi:{modelos:[],esfuerzo:!1,pista:"The model name your Kimi CLI uses."}};function J(e){return ce[e||"claude"]??ce.claude}var bt=ce.claude.modelos,de={low:1,medium:2,high:3,xhigh:4,max:5},Q=Object.keys(de);function z(e,n,t,a="default"){return e.concat(n&&!e.includes(n)?[n]:[]).map(i=>`<option value="${t(i)}"${i===n?" selected":""}>${t(i||a)}</option>`).join("")}function Me(e){let n=e.toLowerCase();if(!n)return{ancho:.5,texto:"default",defecto:!0};for(let[t,a]of[["fable",1],["opus",.8],["sonnet",.55],["haiku",.35]])if(n.includes(t))return{ancho:a,texto:n,defecto:!1};return{ancho:.6,texto:n,defecto:!1}}var We=[["code","It writes code","Its house grows with the pull requests it merges. Mounts repositories and worktrees."],["knowledge","It keeps knowledge","Its house grows with the documents it writes. Needs no git at all."],["coordinator","It coordinates","Its house grows with the decisions it records."]],_=class extends H{constructor(t,a=[]){super();this.p=t;this.roles=a}datos={nombre:"",clase:"code",rol:"blank",montajes:[],runtime:"",modelo:"",esfuerzo:""};roles=[];rolesPedidos=!1;monta(t){this.aseguraRoles(),super.monta(t);let a=t.querySelector(".casaExp");a&&this.explora().monta(a)}repinta(){this.host&&this.monta(this.host)}async aseguraRoles(){if(!this.rolesPedidos){this.rolesPedidos=!0;try{let t=await this.p.api("/api/roles?scope=agent");this.roles=t.roles??[],this.pintaRoles()}catch{this.roles=[]}}}pintaRoles(){let t=this.host?.querySelector("#bvRol");!t||!this.roles.length||(t.innerHTML=this.opcionesDeRol(),t.value=this.datos.rol,t.onchange=()=>this.recoge(),this.recoge())}opcionesDeRol(){return this.roles.map(t=>`<option value="${this.p.esc(t.id)}"${t.id===this.datos.rol?" selected":""}>${this.p.esc(t.name)}</option>`).join("")}explorador=null;explora(){return this.explorador||(this.explorador=new B(this.p,t=>{let a=this.datos.montajes.indexOf(t);a>=0?this.datos.montajes.splice(a,1):this.datos.montajes.push(t),this.pintaElegidas()},()=>this.datos.montajes)),this.explorador}html(){let t=this.datos,a=this.p.esc,i=We.map(([r,p,u])=>`
|
|
21
|
+
<button type="button" class="bvOpcion ${t.clase===r?"elegida":""}"
|
|
22
|
+
data-bv="clase" data-id="${r}">
|
|
23
|
+
<b>${s(p)}</b><span>${s(u)}</span></button>`).join(""),c=this.roles.map(r=>`<option value="${a(r.id)}"${r.id===t.rol?" selected":""}>${a(r.name)}</option>`).join(""),o=J(t.runtime);return`
|
|
24
|
+
<div class="campo"><label>${s("What do you call it?")}</label>
|
|
25
|
+
<input type="text" id="bvNombre" value="${a(t.nombre)}"
|
|
26
|
+
placeholder="${s("urgencias, api, the handbook \u2014 whatever you would say out loud")}"></div>
|
|
27
|
+
<label class="bvEtiqueta">${s("What kind of work does it do?")}</label>
|
|
28
|
+
<div class="bvRejilla bvTres">${i}</div>
|
|
29
|
+
<div class="campo"><label>${s("Its role \u2014 its speciality, never authority")}</label>
|
|
30
|
+
<select id="bvRol">${c}</select></div>
|
|
31
|
+
<label class="bvEtiqueta">${s("What runs it?")}</label>
|
|
32
|
+
<p class="pista">${s("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.")}</p>
|
|
708
33
|
<div class="bvMotor">
|
|
709
|
-
<div class="campo"><label>${
|
|
710
|
-
<select id="bvRuntime">${
|
|
711
|
-
<div class="campo"><label>${
|
|
712
|
-
${
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
)}</select>` : `<input type="text" id="bvModelo" value="${esc3(a.modelo)}"
|
|
718
|
-
placeholder="${t("default")}" autocomplete="off" spellcheck="false">`}</div>
|
|
719
|
-
<div class="campo"><label>${t("effort")}</label>
|
|
720
|
-
<select id="bvEsfuerzo" ${motor.esfuerzo ? "" : "disabled"}
|
|
721
|
-
title="${motor.esfuerzo ? "" : t("This CLI has no effort setting.")}">${opciones(
|
|
722
|
-
[""].concat(ESFUERZOS),
|
|
723
|
-
motor.esfuerzo ? a.esfuerzo : "",
|
|
724
|
-
esc3,
|
|
725
|
-
t("default")
|
|
726
|
-
)}</select></div>
|
|
34
|
+
<div class="campo"><label>${s("provider")}</label>
|
|
35
|
+
<select id="bvRuntime">${z(G,t.runtime,a,"claude")}</select></div>
|
|
36
|
+
<div class="campo"><label>${s("engine")}</label>
|
|
37
|
+
${o.modelos.length?`<select id="bvModelo">${z([""].concat(o.modelos),t.modelo,a,s("default"))}</select>`:`<input type="text" id="bvModelo" value="${a(t.modelo)}"
|
|
38
|
+
placeholder="${s("default")}" autocomplete="off" spellcheck="false">`}</div>
|
|
39
|
+
<div class="campo"><label>${s("effort")}</label>
|
|
40
|
+
<select id="bvEsfuerzo" ${o.esfuerzo?"":"disabled"}
|
|
41
|
+
title="${o.esfuerzo?"":s("This CLI has no effort setting.")}">${z([""].concat(Q),o.esfuerzo?t.esfuerzo:"",a,s("default"))}</select></div>
|
|
727
42
|
</div>
|
|
728
|
-
<p class="pista">${
|
|
729
|
-
<label class="bvEtiqueta">${
|
|
730
|
-
<p class="pista">${
|
|
43
|
+
<p class="pista">${a(s(o.pista))}</p>
|
|
44
|
+
<label class="bvEtiqueta">${s("What does it work on?")}</label>
|
|
45
|
+
<p class="pista">${s(`Walk your disk and pick whatever you like: a repository, a
|
|
731
46
|
worktree, a folder of documents, one exact file. As many as you want. Nothing is
|
|
732
47
|
copied \u2014 each one is linked into this agent's own home.`)}</p>
|
|
733
48
|
<div class="casaExp"></div>
|
|
734
|
-
<div id="casaElegidas">${this.elegidas()}</div
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
data-ruta="${esc3(m)}" title="${esc3(m)}">${esc3(
|
|
747
|
-
m.split("/").pop() || m
|
|
748
|
-
)}<i class="expQuita">\u2715</i></button>`
|
|
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);
|
|
756
|
-
}
|
|
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
|
-
});
|
|
764
|
-
raiz.querySelectorAll("[data-bv]").forEach((el) => {
|
|
765
|
-
el.onclick = (evento) => {
|
|
766
|
-
evento.preventDefault();
|
|
767
|
-
const que = el.dataset.bv;
|
|
768
|
-
this.recoge();
|
|
769
|
-
if (que === "clase") this.datos.clase = el.dataset.id ?? "code";
|
|
770
|
-
else if (que === "quita") {
|
|
771
|
-
const ya = this.datos.montajes.indexOf(el.dataset.ruta ?? "");
|
|
772
|
-
if (ya >= 0) this.datos.montajes.splice(ya, 1);
|
|
773
|
-
}
|
|
774
|
-
this.repinta();
|
|
775
|
-
};
|
|
776
|
-
});
|
|
777
|
-
raiz.querySelector("#bvRuntime")?.addEventListener("change", () => {
|
|
778
|
-
this.recoge();
|
|
779
|
-
this.repinta();
|
|
780
|
-
});
|
|
781
|
-
}
|
|
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;
|
|
791
|
-
const nombre = raiz.querySelector("#bvNombre");
|
|
792
|
-
const rol = raiz.querySelector("#bvRol");
|
|
793
|
-
if (nombre) this.datos.nombre = nombre.value;
|
|
794
|
-
if (rol) this.datos.rol = rol.value;
|
|
795
|
-
for (const [id, campo] of [
|
|
796
|
-
["#bvRuntime", "runtime"],
|
|
797
|
-
["#bvModelo", "modelo"],
|
|
798
|
-
["#bvEsfuerzo", "esfuerzo"]
|
|
799
|
-
]) {
|
|
800
|
-
const sel = raiz.querySelector(id);
|
|
801
|
-
if (sel) this.datos[campo] = sel.value.trim();
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
/**
|
|
805
|
-
* Build it. Returns the new agent's slug, or null when nothing was written.
|
|
806
|
-
*
|
|
807
|
-
* Three endpoints by necessity, not by sloppiness: `/api/agentes` owns who is
|
|
808
|
-
* on the roster, `/api/montaje` owns what an agent can reach, and
|
|
809
|
-
* `/api/agente` owns how one is run. None of them is allowed to learn
|
|
810
|
-
* another's job just to save a round trip.
|
|
811
|
-
*/
|
|
812
|
-
async guarda() {
|
|
813
|
-
this.recoge();
|
|
814
|
-
const a = this.datos;
|
|
815
|
-
const nombre = a.nombre.trim();
|
|
816
|
-
if (!nombre) {
|
|
817
|
-
this.p.aviso(t("Give it a name \u2014 it is how you will call it in its window"), true);
|
|
818
|
-
return null;
|
|
819
|
-
}
|
|
820
|
-
try {
|
|
821
|
-
const r = await this.p.api("/api/agentes", {
|
|
822
|
-
method: "POST",
|
|
823
|
-
body: JSON.stringify({ name: nombre, kind: a.clase, role: a.rol })
|
|
824
|
-
});
|
|
825
|
-
if (!r.ok || !r.agent) {
|
|
826
|
-
this.p.aviso(r.error || t("Could not add that agent"), true);
|
|
827
|
-
return null;
|
|
828
|
-
}
|
|
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);
|
|
838
|
-
const motor = {};
|
|
839
|
-
if (a.runtime) motor.runtime = a.runtime;
|
|
840
|
-
if (a.modelo) motor.model = a.modelo;
|
|
841
|
-
if (a.esfuerzo) motor.effort = a.esfuerzo;
|
|
842
|
-
if (Object.keys(motor).length) {
|
|
843
|
-
const m = await this.p.api("/api/agente", {
|
|
844
|
-
method: "POST",
|
|
845
|
-
body: JSON.stringify({ agent: r.agent, ...motor })
|
|
846
|
-
});
|
|
847
|
-
if (!m.ok) this.p.aviso(m.error ?? t("Could not set its engine"), true);
|
|
848
|
-
}
|
|
849
|
-
return { slug: r.agent, nombre };
|
|
850
|
-
} catch (e) {
|
|
851
|
-
this.p.aviso(String(e), true);
|
|
852
|
-
return null;
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
|
-
/** How this house reads on a list, once it is built. */
|
|
856
|
-
resumen() {
|
|
857
|
-
const a = this.datos;
|
|
858
|
-
return a.montajes.length ? plural(a.montajes.length, "{n} mount", "{n} mounts") : t("nothing mounted yet");
|
|
859
|
-
}
|
|
860
|
-
};
|
|
861
|
-
|
|
862
|
-
// city/web/src/dialogo.ts
|
|
863
|
-
function esc(s) {
|
|
864
|
-
return String(s).replace(
|
|
865
|
-
/[&<>"']/g,
|
|
866
|
-
(c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]
|
|
867
|
-
);
|
|
868
|
-
}
|
|
869
|
-
var abierto = null;
|
|
870
|
-
function pregunta(p) {
|
|
871
|
-
abierto?.remove();
|
|
872
|
-
const campos = p.campos ?? [];
|
|
873
|
-
const fondo = document.createElement("div");
|
|
874
|
-
fondo.className = "dlgFondo";
|
|
875
|
-
fondo.innerHTML = `
|
|
876
|
-
<div class="dlg" role="dialog" aria-modal="true" aria-label="${esc(p.titulo)}"
|
|
877
|
-
style="${p.ancho ? `max-width:${p.ancho}px` : ""}">
|
|
878
|
-
<h2>${esc(p.titulo)}</h2>
|
|
879
|
-
${(p.cuerpo ?? []).map((l) => `<p class="dlgProsa">${esc(l)}</p>`).join("")}
|
|
880
|
-
${campos.map(
|
|
881
|
-
(c) => `<div class="campo"><label for="dlg-${esc(c.id)}">${esc(c.etiqueta)}</label>
|
|
882
|
-
${c.opciones ? `<select id="dlg-${esc(c.id)}" data-campo="${esc(c.id)}">${c.opciones.map(
|
|
883
|
-
(o) => `<option value="${esc(o.valor)}"${o.valor === (c.valor ?? "") ? " selected" : ""}>${esc(o.texto)}</option>`
|
|
884
|
-
).join("")}</select>` : `<input id="dlg-${esc(c.id)}" data-campo="${esc(c.id)}" type="text"
|
|
885
|
-
value="${esc(c.valor ?? "")}" placeholder="${esc(c.pista ?? "")}"
|
|
886
|
-
autocomplete="off" spellcheck="false">`}</div>`
|
|
887
|
-
).join("")}
|
|
888
|
-
${p.contenido ?? ""}
|
|
49
|
+
<div id="casaElegidas">${this.elegidas()}</div>`}elegidas(){let t=this.p.esc,a=this.datos.montajes;return a.length?`<div class="expElegidas"><label class="bvEtiqueta">${s("Working on")}</label>
|
|
50
|
+
${a.map(i=>`<button type="button" class="bvChip elegida" data-bv="quita"
|
|
51
|
+
data-ruta="${t(i)}" title="${t(i)}">${t(i.split("/").pop()||i)}<i class="expQuita">\u2715</i></button>`).join("")}</div>`:`<p class="pista">${s("Nothing chosen yet \u2014 an agent with no mounts is fine too.")}</p>`}pintaElegidas(){let t=this.host?.querySelector("#casaElegidas");t&&(t.innerHTML=this.elegidas(),this.enlaza(t))}enlaza(t){t.querySelectorAll("#bvNombre,#bvRol,#bvModelo,#bvEsfuerzo").forEach(a=>{a.oninput=()=>this.recoge(),a.onchange=()=>this.recoge()}),t.querySelectorAll("[data-bv]").forEach(a=>{a.onclick=i=>{i.preventDefault();let c=a.dataset.bv;if(this.recoge(),c==="clase")this.datos.clase=a.dataset.id??"code";else if(c==="quita"){let o=this.datos.montajes.indexOf(a.dataset.ruta??"");o>=0&&this.datos.montajes.splice(o,1)}this.repinta()}}),t.querySelector("#bvRuntime")?.addEventListener("change",()=>{this.recoge(),this.repinta()})}recoge(){let t=this.host;if(!t)return;let a=t.querySelector("#bvNombre"),i=t.querySelector("#bvRol");a&&(this.datos.nombre=a.value),i&&(this.datos.rol=i.value);for(let[c,o]of[["#bvRuntime","runtime"],["#bvModelo","modelo"],["#bvEsfuerzo","esfuerzo"]]){let r=t.querySelector(c);r&&(this.datos[o]=r.value.trim())}}async guarda(){this.recoge();let t=this.datos,a=t.nombre.trim();if(!a)return this.p.aviso(s("Give it a name \u2014 it is how you will call it in its window"),!0),null;try{let i=await this.p.api("/api/agentes",{method:"POST",body:JSON.stringify({name:a,kind:t.clase,role:t.rol})});if(!i.ok||!i.agent)return this.p.aviso(i.error||s("Could not add that agent"),!0),null;let c=await Promise.all(t.montajes.map(r=>this.p.api("/api/montaje",{method:"POST",body:JSON.stringify({agent:i.agent,add:r})}).then(p=>p.ok?"":`${r}: ${p.error??s("could not mount")}`).catch(p=>`${r}: ${String(p)}`)));for(let r of c.filter(Boolean))this.p.aviso(r,!0);let o={};if(t.runtime&&(o.runtime=t.runtime),t.modelo&&(o.model=t.modelo),t.esfuerzo&&(o.effort=t.esfuerzo),Object.keys(o).length){let r=await this.p.api("/api/agente",{method:"POST",body:JSON.stringify({agent:i.agent,...o})});r.ok||this.p.aviso(r.error??s("Could not set its engine"),!0)}return{slug:i.agent,nombre:a}}catch(i){return this.p.aviso(String(i),!0),null}}resumen(){let t=this.datos;return t.montajes.length?x(t.montajes.length,"{n} mount","{n} mounts"):s("nothing mounted yet")}};function E(e){return String(e).replace(/[&<>"']/g,n=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[n])}var K=null;function I(e){K?.remove();let n=e.campos??[],t=document.createElement("div");t.className="dlgFondo",t.innerHTML=`
|
|
52
|
+
<div class="dlg" role="dialog" aria-modal="true" aria-label="${E(e.titulo)}"
|
|
53
|
+
style="${e.ancho?`max-width:${e.ancho}px`:""}">
|
|
54
|
+
<h2>${E(e.titulo)}</h2>
|
|
55
|
+
${(e.cuerpo??[]).map(o=>`<p class="dlgProsa">${E(o)}</p>`).join("")}
|
|
56
|
+
${n.map(o=>`<div class="campo"><label for="dlg-${E(o.id)}">${E(o.etiqueta)}</label>
|
|
57
|
+
${o.opciones?`<select id="dlg-${E(o.id)}" data-campo="${E(o.id)}">${o.opciones.map(r=>`<option value="${E(r.valor)}"${r.valor===(o.valor??"")?" selected":""}>${E(r.texto)}</option>`).join("")}</select>`:`<input id="dlg-${E(o.id)}" data-campo="${E(o.id)}" type="text"
|
|
58
|
+
value="${E(o.valor??"")}" placeholder="${E(o.pista??"")}"
|
|
59
|
+
autocomplete="off" spellcheck="false">`}</div>`).join("")}
|
|
60
|
+
${e.contenido??""}
|
|
889
61
|
<div class="dlgBotones">
|
|
890
|
-
<button type="button" class="bt" data-dlg="no">${
|
|
891
|
-
<button type="button" class="bt ${
|
|
892
|
-
p.aceptar ?? "OK"
|
|
893
|
-
)}</button>
|
|
62
|
+
<button type="button" class="bt" data-dlg="no">${E(e.cancelar??"Cancel")}</button>
|
|
63
|
+
<button type="button" class="bt ${e.peligro?"malo":"ppal"}" data-dlg="si">${E(e.aceptar??"OK")}</button>
|
|
894
64
|
</div>
|
|
895
|
-
</div>`;
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
return new Promise((resuelve) => {
|
|
902
|
-
let cerrado = false;
|
|
903
|
-
const cierra = (respuesta) => {
|
|
904
|
-
if (cerrado) return;
|
|
905
|
-
cerrado = true;
|
|
906
|
-
document.removeEventListener("keydown", tecla, true);
|
|
907
|
-
fondo.remove();
|
|
908
|
-
if (abierto === fondo) abierto = null;
|
|
909
|
-
devolvia?.focus?.();
|
|
910
|
-
resuelve(respuesta);
|
|
911
|
-
};
|
|
912
|
-
const lee = () => {
|
|
913
|
-
const fuera = {};
|
|
914
|
-
fondo.querySelectorAll("[data-campo]").forEach((el) => {
|
|
915
|
-
fuera[el.dataset.campo ?? ""] = el.value;
|
|
916
|
-
});
|
|
917
|
-
return fuera;
|
|
918
|
-
};
|
|
919
|
-
const acepta = () => {
|
|
920
|
-
const valores = lee();
|
|
921
|
-
if (campos.some((c) => c.requerido && !valores[c.id]?.trim())) return;
|
|
922
|
-
cierra(valores);
|
|
923
|
-
};
|
|
924
|
-
const revisa = () => {
|
|
925
|
-
const valores = lee();
|
|
926
|
-
const falta = campos.some((c) => c.requerido && !valores[c.id]?.trim());
|
|
927
|
-
const mal = p.exige !== void 0 && (valores[campos[0]?.id ?? ""] ?? "").trim() !== p.exige;
|
|
928
|
-
si.disabled = falta || mal;
|
|
929
|
-
};
|
|
930
|
-
si.onclick = acepta;
|
|
931
|
-
fondo.querySelector('[data-dlg="no"]').onclick = () => cierra(null);
|
|
932
|
-
fondo.onclick = (e) => {
|
|
933
|
-
if (e.target === fondo) cierra(null);
|
|
934
|
-
};
|
|
935
|
-
caja.addEventListener("input", revisa);
|
|
936
|
-
caja.addEventListener("change", revisa);
|
|
937
|
-
fondo.querySelectorAll("input").forEach((el) => {
|
|
938
|
-
el.onkeydown = (e) => {
|
|
939
|
-
if (e.key === "Enter" && !si.disabled) {
|
|
940
|
-
e.preventDefault();
|
|
941
|
-
acepta();
|
|
942
|
-
}
|
|
943
|
-
};
|
|
944
|
-
});
|
|
945
|
-
const tecla = (e) => {
|
|
946
|
-
if (e.key === "Escape") {
|
|
947
|
-
e.preventDefault();
|
|
948
|
-
cierra(null);
|
|
949
|
-
}
|
|
950
|
-
};
|
|
951
|
-
document.addEventListener("keydown", tecla, true);
|
|
952
|
-
p.enlaza?.(caja, cierra);
|
|
953
|
-
revisa();
|
|
954
|
-
(caja.querySelector("input,select,textarea") ?? (si.disabled ? caja : si)).focus?.();
|
|
955
|
-
});
|
|
956
|
-
}
|
|
957
|
-
async function confirma(titulo, cuerpo = [], opciones3 = {}) {
|
|
958
|
-
const r = await pregunta({
|
|
959
|
-
titulo,
|
|
960
|
-
cuerpo,
|
|
961
|
-
aceptar: opciones3.aceptar ?? "Yes",
|
|
962
|
-
peligro: opciones3.peligro
|
|
963
|
-
});
|
|
964
|
-
return r !== null;
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
// city/web/src/demo.ts
|
|
968
|
-
var VELOCIDADES = [
|
|
969
|
-
["0.5\xD7", 2900],
|
|
970
|
-
["1\xD7", 1450],
|
|
971
|
-
["2\xD7", 700],
|
|
972
|
-
["4\xD7", 350]
|
|
973
|
-
];
|
|
974
|
-
var Demos = class extends Montada {
|
|
975
|
-
constructor(p) {
|
|
976
|
-
super();
|
|
977
|
-
this.p = p;
|
|
978
|
-
}
|
|
979
|
-
catalogo = [];
|
|
980
|
-
cargando = false;
|
|
981
|
-
elegida = null;
|
|
982
|
-
hasta = 0;
|
|
983
|
-
corriendo = false;
|
|
984
|
-
velocidad = 1450;
|
|
985
|
-
reloj = null;
|
|
986
|
-
monta(host) {
|
|
987
|
-
if (!this.catalogo.length && !this.cargando) void this.lee();
|
|
988
|
-
super.monta(host);
|
|
989
|
-
}
|
|
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() {
|
|
993
|
-
this.corriendo = false;
|
|
994
|
-
if (this.reloj !== null) {
|
|
995
|
-
clearTimeout(this.reloj);
|
|
996
|
-
this.reloj = null;
|
|
997
|
-
}
|
|
998
|
-
}
|
|
999
|
-
repinta() {
|
|
1000
|
-
super.repinta();
|
|
1001
|
-
const lista = this.elegida ? this.host?.querySelector("#demoLista") : null;
|
|
1002
|
-
if (lista) lista.scrollTop = lista.scrollHeight;
|
|
1003
|
-
}
|
|
1004
|
-
async lee() {
|
|
1005
|
-
this.cargando = true;
|
|
1006
|
-
try {
|
|
1007
|
-
const r = await this.p.api("/api/demos");
|
|
1008
|
-
this.catalogo = r.demos ?? [];
|
|
1009
|
-
} catch {
|
|
1010
|
-
this.catalogo = [];
|
|
1011
|
-
} finally {
|
|
1012
|
-
this.cargando = false;
|
|
1013
|
-
this.repinta();
|
|
1014
|
-
}
|
|
1015
|
-
}
|
|
1016
|
-
async abre(id) {
|
|
1017
|
-
this.desmonta();
|
|
1018
|
-
this.hasta = 0;
|
|
1019
|
-
this.elegida = null;
|
|
1020
|
-
this.repinta();
|
|
1021
|
-
try {
|
|
1022
|
-
const r = await this.p.api("/api/demos?story=" + encodeURIComponent(id));
|
|
1023
|
-
if (!r.error) this.elegida = r;
|
|
1024
|
-
} catch {
|
|
1025
|
-
this.elegida = null;
|
|
1026
|
-
}
|
|
1027
|
-
this.repinta();
|
|
1028
|
-
if (this.elegida) this.arranca();
|
|
1029
|
-
}
|
|
1030
|
-
arranca() {
|
|
1031
|
-
if (!this.elegida) return;
|
|
1032
|
-
if (this.hasta >= this.elegida.eventos.length) this.hasta = 0;
|
|
1033
|
-
this.corriendo = true;
|
|
1034
|
-
this.repinta();
|
|
1035
|
-
this.siguiente();
|
|
1036
|
-
}
|
|
1037
|
-
siguiente() {
|
|
1038
|
-
if (this.reloj !== null) clearTimeout(this.reloj);
|
|
1039
|
-
this.reloj = window.setTimeout(() => {
|
|
1040
|
-
if (!this.corriendo || !this.elegida) return;
|
|
1041
|
-
const evento = this.elegida.eventos[this.hasta];
|
|
1042
|
-
this.hasta += 1;
|
|
1043
|
-
const final = this.hasta >= this.elegida.eventos.length;
|
|
1044
|
-
if (final) {
|
|
1045
|
-
this.hasta = this.elegida.eventos.length;
|
|
1046
|
-
this.corriendo = false;
|
|
1047
|
-
}
|
|
1048
|
-
if (evento && !final) this.anade(evento);
|
|
1049
|
-
else this.repinta();
|
|
1050
|
-
if (!final) this.siguiente();
|
|
1051
|
-
}, this.velocidad);
|
|
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
|
-
}
|
|
1073
|
-
html() {
|
|
1074
|
-
if (this.elegida) return this.reproductor(this.elegida);
|
|
1075
|
-
if (this.cargando) return `<p class="cargando">${t("reading the demo shelf")}</p>`;
|
|
1076
|
-
if (!this.catalogo.length)
|
|
1077
|
-
return `<div><span class="sub">${t("demos")}</span>
|
|
1078
|
-
<h1 style="margin-top:6px">${t("Nothing recorded here")}</h1>
|
|
1079
|
-
<p class="prosa">${t("This install has no demo recordings. Make them with demo/graba.py, or run the full thing from a terminal with agents-city demo.")}</p></div>`;
|
|
1080
|
-
const esc3 = this.p.esc;
|
|
1081
|
-
return `<div>
|
|
1082
|
-
<span class="sub">${t("demos")}</span>
|
|
1083
|
-
<h1 style="margin-top:6px">${t("See a committee happen")}</h1>
|
|
1084
|
-
<p class="prosa">${t(`A real question, answered by a city of agents that never talk to each
|
|
65
|
+
</div>`,document.body.appendChild(t),K=t;let a=document.activeElement,i=t.querySelector(".dlg"),c=t.querySelector('[data-dlg="si"]');return new Promise(o=>{let r=!1,p=y=>{r||(r=!0,document.removeEventListener("keydown",w,!0),t.remove(),K===t&&(K=null),a?.focus?.(),o(y))},u=()=>{let y={};return t.querySelectorAll("[data-campo]").forEach($=>{y[$.dataset.campo??""]=$.value}),y},h=()=>{let y=u();n.some($=>$.requerido&&!y[$.id]?.trim())||p(y)},v=()=>{let y=u(),$=n.some(R=>R.requerido&&!y[R.id]?.trim()),le=e.exige!==void 0&&(y[n[0]?.id??""]??"").trim()!==e.exige;c.disabled=$||le};c.onclick=h,t.querySelector('[data-dlg="no"]').onclick=()=>p(null),t.onclick=y=>{y.target===t&&p(null)},i.addEventListener("input",v),i.addEventListener("change",v),t.querySelectorAll("input").forEach(y=>{y.onkeydown=$=>{$.key==="Enter"&&!c.disabled&&($.preventDefault(),h())}});let w=y=>{y.key==="Escape"&&(y.preventDefault(),p(null))};document.addEventListener("keydown",w,!0),e.enlaza?.(i,p),v(),(i.querySelector("input,select,textarea")??(c.disabled?i:c)).focus?.()})}async function Z(e,n=[],t={}){return await I({titulo:e,cuerpo:n,aceptar:t.aceptar??"Yes",peligro:t.peligro})!==null}var Fe=[["0.5\xD7",2900],["1\xD7",1450],["2\xD7",700],["4\xD7",350]],X=class extends H{constructor(t){super();this.p=t}catalogo=[];cargando=!1;elegida=null;hasta=0;corriendo=!1;velocidad=1450;reloj=null;monta(t){!this.catalogo.length&&!this.cargando&&this.lee(),super.monta(t)}desmonta(){this.corriendo=!1,this.reloj!==null&&(clearTimeout(this.reloj),this.reloj=null)}repinta(){super.repinta();let t=this.elegida?this.host?.querySelector("#demoLista"):null;t&&(t.scrollTop=t.scrollHeight)}async lee(){this.cargando=!0;try{let t=await this.p.api("/api/demos");this.catalogo=t.demos??[]}catch{this.catalogo=[]}finally{this.cargando=!1,this.repinta()}}async abre(t){this.desmonta(),this.hasta=0,this.elegida=null,this.repinta();try{let a=await this.p.api("/api/demos?story="+encodeURIComponent(t));a.error||(this.elegida=a)}catch{this.elegida=null}this.repinta(),this.elegida&&this.arranca()}arranca(){this.elegida&&(this.hasta>=this.elegida.eventos.length&&(this.hasta=0),this.corriendo=!0,this.repinta(),this.siguiente())}siguiente(){this.reloj!==null&&clearTimeout(this.reloj),this.reloj=window.setTimeout(()=>{if(!this.corriendo||!this.elegida)return;let t=this.elegida.eventos[this.hasta];this.hasta+=1;let a=this.hasta>=this.elegida.eventos.length;a&&(this.hasta=this.elegida.eventos.length,this.corriendo=!1),t&&!a?this.anade(t):this.repinta(),a||this.siguiente()},this.velocidad)}anade(t){let a=this.host?.querySelector("#demoLista");if(!a||!this.elegida)return this.repinta();a.querySelector(".liveEmpty")&&(a.innerHTML=""),a.insertAdjacentHTML("beforeend",this.p.pinta(t)),a.scrollTop=a.scrollHeight,this.marcador()}marcador(){if(!this.host||!this.elegida)return;let t=this.elegida.eventos.length,a=Math.min(this.hasta,t),i=this.host.querySelector(".demoCuenta");i&&(i.textContent=s("{done} of {total}",{done:a,total:t}));let c=this.host.querySelector(".demoBarra i");c&&(c.style.width=`${t?a/t*100:0}%`)}html(){if(this.elegida)return this.reproductor(this.elegida);if(this.cargando)return`<p class="cargando">${s("reading the demo shelf")}</p>`;if(!this.catalogo.length)return`<div><span class="sub">${s("demos")}</span>
|
|
66
|
+
<h1 style="margin-top:6px">${s("Nothing recorded here")}</h1>
|
|
67
|
+
<p class="prosa">${s("This install has no demo recordings. Make them with demo/graba.py, or run the full thing from a terminal with agents-city demo.")}</p></div>`;let t=this.p.esc;return`<div>
|
|
68
|
+
<span class="sub">${s("demos")}</span>
|
|
69
|
+
<h1 style="margin-top:6px">${s("See a committee happen")}</h1>
|
|
70
|
+
<p class="prosa">${s(`A real question, answered by a city of agents that never talk to each
|
|
1085
71
|
other behind the chair's back. Pick the field closest to yours \u2014 the machine is the same
|
|
1086
72
|
one in all three; only the work changes.`)}</p>
|
|
1087
|
-
<div class="demoRejilla">${this.catalogo.map(
|
|
1088
|
-
|
|
1089
|
-
<
|
|
1090
|
-
<
|
|
1091
|
-
<span class="
|
|
1092
|
-
<span class="
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
).join("")
|
|
1096
|
-
<p class="pista">${t("These are recordings of real runs over the real bus, played back here. To run one live in a terminal: agents-city demo --domain software.")}</p>
|
|
1097
|
-
</div>`;
|
|
1098
|
-
}
|
|
1099
|
-
reproductor(d) {
|
|
1100
|
-
const esc3 = this.p.esc;
|
|
1101
|
-
const total = d.eventos.length;
|
|
1102
|
-
const hecho = Math.min(this.hasta, total);
|
|
1103
|
-
const turnos = d.eventos.slice(0, hecho).map(this.p.pinta).join("");
|
|
1104
|
-
const acabado = hecho >= total;
|
|
1105
|
-
return `<div class="demoPlayer">
|
|
73
|
+
<div class="demoRejilla">${this.catalogo.map(a=>`<button type="button" class="demoCarta" data-demo="abre" data-id="${t(a.id)}">
|
|
74
|
+
<span class="demoDominio">${t(a.dominio)}</span>
|
|
75
|
+
<b>${t(a.titulo)}</b>
|
|
76
|
+
<span class="demoCiudad">${t(a.ciudad)}</span>
|
|
77
|
+
<span class="demoReparto">${t(a.reparto.join(" \xB7 "))}</span>
|
|
78
|
+
<span class="demoPlay">\u25B6 ${s("{n} turns",{n:a.turnos})}</span>
|
|
79
|
+
</button>`).join("")}</div>
|
|
80
|
+
<p class="pista">${s("These are recordings of real runs over the real bus, played back here. To run one live in a terminal: agents-city demo --domain software.")}</p>
|
|
81
|
+
</div>`}reproductor(t){let a=this.p.esc,i=t.eventos.length,c=Math.min(this.hasta,i),o=t.eventos.slice(0,c).map(this.p.pinta).join(""),r=c>=i;return`<div class="demoPlayer">
|
|
1106
82
|
<div class="demoCab">
|
|
1107
|
-
<button type="button" class="bt bvMini" data-demo="atras">\u2190 ${
|
|
1108
|
-
<div><span class="sub">${
|
|
1109
|
-
<h2>${
|
|
83
|
+
<button type="button" class="bt bvMini" data-demo="atras">\u2190 ${s("All demos")}</button>
|
|
84
|
+
<div><span class="sub">${a(t.dominio)} \xB7 ${a(t.ciudad)}</span>
|
|
85
|
+
<h2>${a(t.titulo)}</h2></div>
|
|
1110
86
|
</div>
|
|
1111
87
|
<div class="demoMandos">
|
|
1112
|
-
<button type="button" class="bt ppal" data-demo="${this.corriendo
|
|
1113
|
-
${
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
hecho && !acabado ? `<button type="button" class="bt" data-demo="replay">\u21BA ${t("Replay")}</button>` : ""}
|
|
1117
|
-
<span class="demoVel">${VELOCIDADES.map(
|
|
1118
|
-
([etiqueta, ms]) => `<button type="button" class="demoVelBoton ${ms === this.velocidad ? "aqui" : ""}" data-demo="vel" data-ms="${ms}">${esc3(etiqueta)}</button>`
|
|
1119
|
-
).join("")}</span>
|
|
1120
|
-
<span class="demoCuenta">${t("{done} of {total}", { done: hecho, total })}</span>
|
|
88
|
+
<button type="button" class="bt ppal" data-demo="${this.corriendo?"pausa":"play"}">${this.corriendo?`\u275A\u275A ${s("Pause")}`:r?`\u21BA ${s("Replay")}`:`\u25B6 ${s("Play")}`}</button>
|
|
89
|
+
${c&&!r?`<button type="button" class="bt" data-demo="replay">\u21BA ${s("Replay")}</button>`:""}
|
|
90
|
+
<span class="demoVel">${Fe.map(([p,u])=>`<button type="button" class="demoVelBoton ${u===this.velocidad?"aqui":""}" data-demo="vel" data-ms="${u}">${a(p)}</button>`).join("")}</span>
|
|
91
|
+
<span class="demoCuenta">${s("{done} of {total}",{done:c,total:i})}</span>
|
|
1121
92
|
</div>
|
|
1122
|
-
<div class="demoBarra"><i style="width:${
|
|
1123
|
-
<ol class="liveLista" id="demoLista">${
|
|
1124
|
-
<p class="pista">${
|
|
1125
|
-
</div
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
switch (el.dataset.demo) {
|
|
1132
|
-
case "abre":
|
|
1133
|
-
void this.abre(el.dataset.id ?? "");
|
|
1134
|
-
break;
|
|
1135
|
-
case "atras":
|
|
1136
|
-
this.desmonta();
|
|
1137
|
-
this.elegida = null;
|
|
1138
|
-
this.hasta = 0;
|
|
1139
|
-
this.repinta();
|
|
1140
|
-
break;
|
|
1141
|
-
case "play":
|
|
1142
|
-
this.arranca();
|
|
1143
|
-
break;
|
|
1144
|
-
case "pausa":
|
|
1145
|
-
this.desmonta();
|
|
1146
|
-
this.repinta();
|
|
1147
|
-
break;
|
|
1148
|
-
case "replay":
|
|
1149
|
-
this.desmonta();
|
|
1150
|
-
this.hasta = 0;
|
|
1151
|
-
this.arranca();
|
|
1152
|
-
break;
|
|
1153
|
-
case "vel": {
|
|
1154
|
-
this.velocidad = Number(el.dataset.ms) || 1450;
|
|
1155
|
-
if (this.corriendo) this.siguiente();
|
|
1156
|
-
this.repinta();
|
|
1157
|
-
break;
|
|
1158
|
-
}
|
|
1159
|
-
}
|
|
1160
|
-
};
|
|
1161
|
-
});
|
|
1162
|
-
}
|
|
1163
|
-
};
|
|
1164
|
-
|
|
1165
|
-
// city/web/src/bienvenida.ts
|
|
1166
|
-
var PASOS = ["Welcome", "The work", "Your chair", "The houses", "Ready"];
|
|
1167
|
-
function casita(color) {
|
|
1168
|
-
return `<svg viewBox="0 0 120 108" role="img" aria-label="${t("an isometric house")}" class="bvCasa">
|
|
1169
|
-
<polygon points="60,10 112,40 60,70 8,40" fill="${color}" opacity=".95"/>
|
|
1170
|
-
<polygon points="8,40 60,70 60,100 8,70" fill="${color}" opacity=".62"/>
|
|
1171
|
-
<polygon points="112,40 60,70 60,100 112,70" fill="${color}" opacity=".40"/>
|
|
1172
|
-
</svg>`;
|
|
1173
|
-
}
|
|
1174
|
-
var Bienvenida = class {
|
|
1175
|
-
constructor(p) {
|
|
1176
|
-
this.p = p;
|
|
1177
|
-
}
|
|
1178
|
-
paso = 0;
|
|
1179
|
-
dominios = [];
|
|
1180
|
-
roles = [];
|
|
1181
|
-
rolesDeAgente = [];
|
|
1182
|
-
dominio = "";
|
|
1183
|
-
rol = "";
|
|
1184
|
-
roster = [];
|
|
1185
|
-
enCurso = null;
|
|
1186
|
-
guardando = false;
|
|
1187
|
-
lienzo = null;
|
|
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`. */
|
|
1199
|
-
async pinta(lienzo) {
|
|
1200
|
-
this.lienzo = lienzo;
|
|
1201
|
-
if (!this.dominios.length) {
|
|
1202
|
-
lienzo.innerHTML = '<p class="cargando">reading the domain packs</p>';
|
|
1203
|
-
const [d, ra] = await Promise.all([
|
|
1204
|
-
this.p.api("/api/domains"),
|
|
1205
|
-
this.p.api("/api/roles?scope=agent")
|
|
1206
|
-
]);
|
|
1207
|
-
this.dominios = d.domains;
|
|
1208
|
-
this.rolesDeAgente = ra.roles;
|
|
1209
|
-
}
|
|
1210
|
-
lienzo.innerHTML = `<div class="bv">${this.cabecera()}${await this.cuerpo()}</div>`;
|
|
1211
|
-
this.enlaza(lienzo);
|
|
1212
|
-
}
|
|
1213
|
-
cabecera() {
|
|
1214
|
-
const puntos = PASOS.map(
|
|
1215
|
-
(nombre, i) => `<span class="bvPunto ${i === this.paso ? "aqui" : i < this.paso ? "hecho" : ""}"
|
|
1216
|
-
title="${this.p.esc(t(nombre))}"><i></i>${this.p.esc(t(nombre))}</span>`
|
|
1217
|
-
).join("");
|
|
1218
|
-
return `<div class="bvPasos">${puntos}</div>`;
|
|
1219
|
-
}
|
|
1220
|
-
async cuerpo() {
|
|
1221
|
-
switch (this.paso) {
|
|
1222
|
-
case 0:
|
|
1223
|
-
return this.pantallaHola();
|
|
1224
|
-
case 1:
|
|
1225
|
-
return this.pantallaDominio();
|
|
1226
|
-
case 2:
|
|
1227
|
-
return await this.pantallaRol();
|
|
1228
|
-
case 3:
|
|
1229
|
-
return this.pantallaAgentes();
|
|
1230
|
-
default:
|
|
1231
|
-
return this.pantallaFinal();
|
|
1232
|
-
}
|
|
1233
|
-
}
|
|
1234
|
-
// ── 0. what this is ────────────────────────────────────────────────────────
|
|
1235
|
-
pantallaHola() {
|
|
1236
|
-
return `
|
|
93
|
+
<div class="demoBarra"><i style="width:${i?c/i*100:0}%"></i></div>
|
|
94
|
+
<ol class="liveLista" id="demoLista">${o||`<li class="liveEmpty">${s("Press play. The turns arrive one by one, exactly as they did.")}</li>`}</ol>
|
|
95
|
+
<p class="pista">${s("A recording of a real run: these events came off the real bus, from the real committee. Nothing here is being decided now.")}</p>
|
|
96
|
+
</div>`}enlaza(t){t.querySelectorAll("[data-demo]").forEach(a=>{a.onclick=i=>{switch(i.preventDefault(),a.dataset.demo){case"abre":this.abre(a.dataset.id??"");break;case"atras":this.desmonta(),this.elegida=null,this.hasta=0,this.repinta();break;case"play":this.arranca();break;case"pausa":this.desmonta(),this.repinta();break;case"replay":this.desmonta(),this.hasta=0,this.arranca();break;case"vel":{this.velocidad=Number(a.dataset.ms)||1450,this.corriendo&&this.siguiente(),this.repinta();break}}}})}};var xe=["Welcome","The work","Your chair","The houses","Ready"];function ue(e){return`<svg viewBox="0 0 120 108" role="img" aria-label="${s("an isometric house")}" class="bvCasa">
|
|
97
|
+
<polygon points="60,10 112,40 60,70 8,40" fill="${e}" opacity=".95"/>
|
|
98
|
+
<polygon points="8,40 60,70 60,100 8,70" fill="${e}" opacity=".62"/>
|
|
99
|
+
<polygon points="112,40 60,70 60,100 112,70" fill="${e}" opacity=".40"/>
|
|
100
|
+
</svg>`}var ee=class{constructor(n){this.p=n}paso=0;dominios=[];roles=[];rolesDeAgente=[];dominio="";rol="";roster=[];enCurso=null;guardando=!1;lienzo=null;monta(n){this.pinta(n)}async pinta(n){if(this.lienzo=n,!this.dominios.length){n.innerHTML='<p class="cargando">reading the domain packs</p>';let[t,a]=await Promise.all([this.p.api("/api/domains"),this.p.api("/api/roles?scope=agent")]);this.dominios=t.domains,this.rolesDeAgente=a.roles}n.innerHTML=`<div class="bv">${this.cabecera()}${await this.cuerpo()}</div>`,this.enlaza(n)}cabecera(){return`<div class="bvPasos">${xe.map((t,a)=>`<span class="bvPunto ${a===this.paso?"aqui":a<this.paso?"hecho":""}"
|
|
101
|
+
title="${this.p.esc(s(t))}"><i></i>${this.p.esc(s(t))}</span>`).join("")}</div>`}async cuerpo(){switch(this.paso){case 0:return this.pantallaHola();case 1:return this.pantallaDominio();case 2:return await this.pantallaRol();case 3:return this.pantallaAgentes();default:return this.pantallaFinal()}}pantallaHola(){return`
|
|
1237
102
|
<div class="bvHola">
|
|
1238
|
-
<div class="bvArte">${
|
|
103
|
+
<div class="bvArte">${ue("#7048e8")}${ue("#45b9a7")}${ue("#f2674a")}</div>
|
|
1239
104
|
<div>
|
|
1240
105
|
<span class="sub">welcome</span>
|
|
1241
|
-
<h1>${
|
|
1242
|
-
<p class="prosa">${
|
|
106
|
+
<h1>${s("A city is you, and the houses around you")}</h1>
|
|
107
|
+
<p class="prosa">${s(`You take the chair. Around it, one <b>house</b> per worker \u2014 an
|
|
1243
108
|
<b>agent</b> \u2014 each with its own window, its own role and its own corner of your
|
|
1244
109
|
disk: a repository, three of them, a folder of documents, whatever it actually
|
|
1245
110
|
works on. The map draws them as houses that grow with the work done in them, and
|
|
1246
111
|
many houses are a city. They never talk to each other behind your back: you chair,
|
|
1247
112
|
they answer.`)}</p>
|
|
1248
|
-
<p class="prosa">${
|
|
1249
|
-
<p class="prosa">${
|
|
113
|
+
<p class="prosa">${s("Everything lives as plain files in {donde}. Nothing leaves this machine, there is no account, and you can edit any of it by hand afterwards.",{donde:`<code class="mono">${this.p.esc(this.p.datos)}</code>`})}</p>
|
|
114
|
+
<p class="prosa">${s("<b>Four questions and you are working.</b> You can skip any of them and change everything later.")}</p>
|
|
1250
115
|
<div class="bvBotones">
|
|
1251
|
-
<button class="bt ppal" data-bv="siguiente">${
|
|
1252
|
-
<button class="bt" data-bv="salir">${
|
|
116
|
+
<button class="bt ppal" data-bv="siguiente">${s("Let\u2019s build it")}</button>
|
|
117
|
+
<button class="bt" data-bv="salir">${s("I\u2019ll set it up myself")}</button>
|
|
1253
118
|
</div>
|
|
1254
119
|
</div>
|
|
1255
|
-
</div
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
const tarjetas = this.dominios.map(
|
|
1260
|
-
(d) => `
|
|
1261
|
-
<button class="bvOpcion ${this.dominio === d.id ? "elegida" : ""}" data-bv="dominio"
|
|
1262
|
-
data-id="${this.p.esc(d.id)}">
|
|
1263
|
-
<b>${this.p.esc(d.name)}</b><span>${this.p.esc(d.summary)}</span></button>`
|
|
1264
|
-
).join("");
|
|
1265
|
-
return `
|
|
120
|
+
</div>`}pantallaDominio(){let n=this.dominios.map(t=>`
|
|
121
|
+
<button class="bvOpcion ${this.dominio===t.id?"elegida":""}" data-bv="dominio"
|
|
122
|
+
data-id="${this.p.esc(t.id)}">
|
|
123
|
+
<b>${this.p.esc(t.name)}</b><span>${this.p.esc(t.summary)}</span></button>`).join("");return`
|
|
1266
124
|
<div class="bvPaso">
|
|
1267
|
-
<span class="sub">${
|
|
1268
|
-
<h1>${
|
|
1269
|
-
<p class="prosa">${
|
|
125
|
+
<span class="sub">${s("question 1 of 4")}</span>
|
|
126
|
+
<h1>${s("What kind of work happens here?")}</h1>
|
|
127
|
+
<p class="prosa">${s(`It decides the vocabulary, the roles you will be offered and what
|
|
1270
128
|
counts as evidence in a decision. A clinic does not ship pull requests, and a law
|
|
1271
129
|
firm does not measure story points. Pick the closest one \u2014 you can change it later.`)}</p>
|
|
1272
|
-
<div class="bvRejilla">${
|
|
130
|
+
<div class="bvRejilla">${n}</div>
|
|
1273
131
|
<div class="bvBotones">
|
|
1274
|
-
<button class="bt" data-bv="atras">${
|
|
1275
|
-
<button class="bt ppal" data-bv="siguiente" ${this.dominio
|
|
132
|
+
<button class="bt" data-bv="atras">${s("Back")}</button>
|
|
133
|
+
<button class="bt ppal" data-bv="siguiente" ${this.dominio?"":"disabled"}>${s("Next")}</button>
|
|
1276
134
|
</div>
|
|
1277
|
-
</div
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
if (!this.roles.length || this.roles[0]?.id === void 0) {
|
|
1282
|
-
const r = await this.p.api(
|
|
1283
|
-
"/api/roles?domain=" + encodeURIComponent(this.dominio)
|
|
1284
|
-
);
|
|
1285
|
-
this.roles = r.roles;
|
|
1286
|
-
}
|
|
1287
|
-
const tarjetas = this.roles.map(
|
|
1288
|
-
(r) => `
|
|
1289
|
-
<button class="bvOpcion ${this.rol === r.id ? "elegida" : ""}" data-bv="rol"
|
|
1290
|
-
data-id="${this.p.esc(r.id)}">
|
|
1291
|
-
<b>${this.p.esc(r.name)}</b><span>${this.p.esc(r.summary || r.trade)}</span></button>`
|
|
1292
|
-
).join("");
|
|
1293
|
-
return `
|
|
135
|
+
</div>`}async pantallaRol(){if(!this.roles.length||this.roles[0]?.id===void 0){let t=await this.p.api("/api/roles?domain="+encodeURIComponent(this.dominio));this.roles=t.roles}let n=this.roles.map(t=>`
|
|
136
|
+
<button class="bvOpcion ${this.rol===t.id?"elegida":""}" data-bv="rol"
|
|
137
|
+
data-id="${this.p.esc(t.id)}">
|
|
138
|
+
<b>${this.p.esc(t.name)}</b><span>${this.p.esc(t.summary||t.trade)}</span></button>`).join("");return`
|
|
1294
139
|
<div class="bvPaso">
|
|
1295
|
-
<span class="sub">${
|
|
1296
|
-
<h1>${
|
|
1297
|
-
<p class="prosa">${
|
|
140
|
+
<span class="sub">${s("question 2 of 4")}</span>
|
|
141
|
+
<h1>${s("And what are you, here?")}</h1>
|
|
142
|
+
<p class="prosa">${s(`You chair this city whatever you answer \u2014 this is your
|
|
1298
143
|
<b>speciality</b>, not your authority. It shapes the perspective you bring to a
|
|
1299
144
|
decision and the knowledge files the city writes for you. <b>Blank</b> is a real
|
|
1300
145
|
answer: it means no preset knowledge.`)}</p>
|
|
1301
|
-
<div class="bvRejilla">${
|
|
146
|
+
<div class="bvRejilla">${n}</div>
|
|
1302
147
|
<div class="bvBotones">
|
|
1303
|
-
<button class="bt" data-bv="atras">${
|
|
1304
|
-
<button class="bt ppal" data-bv="siguiente" ${this.rol
|
|
148
|
+
<button class="bt" data-bv="atras">${s("Back")}</button>
|
|
149
|
+
<button class="bt ppal" data-bv="siguiente" ${this.rol?"":"disabled"}>Next</button>
|
|
1305
150
|
</div>
|
|
1306
|
-
</div
|
|
1307
|
-
}
|
|
1308
|
-
// ── 3. the agents ─────────────────────────────────────────────────────────
|
|
1309
|
-
pantallaAgentes() {
|
|
1310
|
-
if (!this.enCurso) return this.listaDeAgentes();
|
|
1311
|
-
return `
|
|
151
|
+
</div>`}pantallaAgentes(){return this.enCurso?`
|
|
1312
152
|
<div class="bvPaso">
|
|
1313
|
-
<span class="sub">${
|
|
1314
|
-
<h1>${
|
|
153
|
+
<span class="sub">${s("a new house")}</span>
|
|
154
|
+
<h1>${s("Who lives in it?")}</h1>
|
|
1315
155
|
<div id="bvCasa"></div>
|
|
1316
156
|
<div class="bvBotones">
|
|
1317
|
-
<button class="bt" data-bv="cancela">${
|
|
1318
|
-
<button class="bt ppal" data-bv="guarda" ${this.guardando
|
|
1319
|
-
this.guardando ? "building\u2026" : "Build it"
|
|
1320
|
-
)}</button>
|
|
157
|
+
<button class="bt" data-bv="cancela">${s("Cancel")}</button>
|
|
158
|
+
<button class="bt ppal" data-bv="guarda" ${this.guardando?"disabled":""}>${s(this.guardando?"building\u2026":"Build it")}</button>
|
|
1321
159
|
</div>
|
|
1322
|
-
</div
|
|
1323
|
-
}
|
|
1324
|
-
listaDeAgentes() {
|
|
1325
|
-
const fichas = this.roster.map(
|
|
1326
|
-
(a) => `
|
|
160
|
+
</div>`:this.listaDeAgentes()}listaDeAgentes(){let n=this.roster.map(t=>`
|
|
1327
161
|
<div class="bvFicha">
|
|
1328
|
-
<b>${this.p.esc(
|
|
1329
|
-
<span>${this.p.esc(
|
|
1330
|
-
<span class="de">${
|
|
1331
|
-
</div>`
|
|
1332
|
-
).join("");
|
|
1333
|
-
return `
|
|
162
|
+
<b>${this.p.esc(t.nombre)}</b>
|
|
163
|
+
<span>${this.p.esc(t.clase)} \xB7 ${this.p.esc(t.rol)}</span>
|
|
164
|
+
<span class="de">${t.montajes.length?this.p.esc(t.montajes.map(a=>a.split("/").pop()).join(", ")):s("nothing mounted yet")}</span>
|
|
165
|
+
</div>`).join("");return`
|
|
1334
166
|
<div class="bvPaso">
|
|
1335
|
-
<span class="sub">${
|
|
1336
|
-
<h1>${
|
|
1337
|
-
<p class="prosa">${
|
|
167
|
+
<span class="sub">${s("question 3 of 4")}</span>
|
|
168
|
+
<h1>${s(this.roster.length?"Who else lives here?":"Who lives in your city?")}</h1>
|
|
169
|
+
<p class="prosa">${s(`Every house holds one worker \u2014 an <b>agent</b> \u2014 with its own window,
|
|
1338
170
|
its own role and its own corner of your disk. A house is <b>not</b> a repository: one
|
|
1339
171
|
can answer for three services and a folder of documents at once, and a house whose
|
|
1340
172
|
work is documents needs no git anywhere.`)}</p>
|
|
1341
|
-
${this.roster.length
|
|
173
|
+
${this.roster.length?`<div class="bvFichas">${n}</div>`:""}
|
|
1342
174
|
<div class="bvBotones">
|
|
1343
|
-
<button class="bt ppal" data-bv="nuevo">${
|
|
1344
|
-
|
|
1345
|
-
)}</button>
|
|
1346
|
-
<button class="bt" data-bv="siguiente">${t(
|
|
1347
|
-
this.roster.length ? "That is everyone" : "Skip \u2014 just me for now"
|
|
1348
|
-
)}</button>
|
|
175
|
+
<button class="bt ppal" data-bv="nuevo">${s(this.roster.length?"Add another house":"Build the first house")}</button>
|
|
176
|
+
<button class="bt" data-bv="siguiente">${s(this.roster.length?"That is everyone":"Skip \u2014 just me for now")}</button>
|
|
1349
177
|
</div>
|
|
1350
|
-
</div
|
|
1351
|
-
}
|
|
1352
|
-
// ── 4. done ───────────────────────────────────────────────────────────────
|
|
1353
|
-
pantallaFinal() {
|
|
1354
|
-
return `
|
|
178
|
+
</div>`}pantallaFinal(){return`
|
|
1355
179
|
<div class="bvPaso">
|
|
1356
|
-
<span class="sub">${
|
|
1357
|
-
<h1>${
|
|
1358
|
-
<p class="prosa">${
|
|
1359
|
-
|
|
1360
|
-
{
|
|
1361
|
-
cuantas: this.roster.length ? plural(
|
|
1362
|
-
this.roster.length,
|
|
1363
|
-
"<b>{n}</b> house stands around it",
|
|
1364
|
-
"<b>{n}</b> houses stand around it"
|
|
1365
|
-
) : t("the city is waiting for its first house")
|
|
1366
|
-
}
|
|
1367
|
-
)}</p>
|
|
1368
|
-
<p class="prosa">${t("What people usually do next:")}</p>
|
|
180
|
+
<span class="sub">${s("Ready").toLowerCase()}</span>
|
|
181
|
+
<h1>${s("{city} is alive",{city:this.p.esc(this.p.ciudad)})}</h1>
|
|
182
|
+
<p class="prosa">${s("Your seat is written and {cuantas}. Everything you just answered is a plain file you can read and edit.",{cuantas:this.roster.length?x(this.roster.length,"<b>{n}</b> house stands around it","<b>{n}</b> houses stand around it"):s("the city is waiting for its first house")})}</p>
|
|
183
|
+
<p class="prosa">${s("What people usually do next:")}</p>
|
|
1369
184
|
<div class="bvSiguientes">
|
|
1370
|
-
<button class="bt ppal" data-bv="sesion">${
|
|
1371
|
-
<button class="bt" data-bv="ir-gente">${
|
|
1372
|
-
<button class="bt" data-bv="ir-mapa">${
|
|
1373
|
-
<button class="bt" data-bv="ir-puesto">${
|
|
185
|
+
<button class="bt ppal" data-bv="sesion">${s("Open my session")}</button>
|
|
186
|
+
<button class="bt" data-bv="ir-gente">${s("See the houses")}</button>
|
|
187
|
+
<button class="bt" data-bv="ir-mapa">${s("Draw the map")}</button>
|
|
188
|
+
<button class="bt" data-bv="ir-puesto">${s("Set a goal")}</button>
|
|
1374
189
|
</div>
|
|
1375
|
-
<p class="pista">${
|
|
190
|
+
<p class="pista">${s(`A goal is optional, and the city works without one \u2014 but a round with
|
|
1376
191
|
no goal is a status report, because there is nothing to argue against.`)}</p>
|
|
1377
|
-
</div>`;
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
const repinta = () => void this.pinta(lienzo);
|
|
1382
|
-
lienzo.querySelectorAll("[data-bv]").forEach((el) => {
|
|
1383
|
-
el.onclick = async (evento) => {
|
|
1384
|
-
evento.preventDefault();
|
|
1385
|
-
const que = el.dataset.bv;
|
|
1386
|
-
if (que === "salir") return this.p.vete("resumen");
|
|
1387
|
-
if (que === "atras") {
|
|
1388
|
-
this.paso = Math.max(0, this.paso - 1);
|
|
1389
|
-
return repinta();
|
|
1390
|
-
}
|
|
1391
|
-
if (que === "dominio") {
|
|
1392
|
-
this.dominio = el.dataset.id ?? "";
|
|
1393
|
-
this.roles = [];
|
|
1394
|
-
this.rol = "";
|
|
1395
|
-
return repinta();
|
|
1396
|
-
}
|
|
1397
|
-
if (que === "rol") {
|
|
1398
|
-
this.rol = el.dataset.id ?? "";
|
|
1399
|
-
return repinta();
|
|
1400
|
-
}
|
|
1401
|
-
if (que === "nuevo") {
|
|
1402
|
-
this.enCurso = new FormularioDeCasa(
|
|
1403
|
-
{ api: this.p.api, esc: this.p.esc, aviso: this.p.aviso, yo: this.p.yo },
|
|
1404
|
-
this.rolesDeAgente
|
|
1405
|
-
);
|
|
1406
|
-
return repinta();
|
|
1407
|
-
}
|
|
1408
|
-
if (que === "cancela") {
|
|
1409
|
-
this.enCurso = null;
|
|
1410
|
-
return repinta();
|
|
1411
|
-
}
|
|
1412
|
-
if (que === "guarda") return void await this.guardaAgente(lienzo);
|
|
1413
|
-
if (que === "siguiente") return void await this.avanza(lienzo);
|
|
1414
|
-
if (que === "sesion") return void await this.abreSesion();
|
|
1415
|
-
if (que === "ir-gente") return this.p.vete("gente");
|
|
1416
|
-
if (que === "ir-mapa") return this.p.vete("mapa");
|
|
1417
|
-
if (que === "ir-puesto") return this.p.vete("puesto");
|
|
1418
|
-
};
|
|
1419
|
-
});
|
|
1420
|
-
const hueco = lienzo.querySelector("#bvCasa");
|
|
1421
|
-
if (hueco && this.enCurso) {
|
|
1422
|
-
this.enCurso.monta(hueco);
|
|
1423
|
-
hueco.querySelector("#bvNombre")?.focus();
|
|
1424
|
-
}
|
|
1425
|
-
}
|
|
1426
|
-
async guardaAgente(lienzo) {
|
|
1427
|
-
const forma = this.enCurso;
|
|
1428
|
-
if (!forma) return;
|
|
1429
|
-
this.guardando = true;
|
|
1430
|
-
await this.pinta(lienzo);
|
|
1431
|
-
try {
|
|
1432
|
-
await this.aseguraFicha();
|
|
1433
|
-
const hecho = await forma.guarda();
|
|
1434
|
-
if (!hecho) return;
|
|
1435
|
-
this.roster.push({ ...forma.datos, nombre: hecho.nombre });
|
|
1436
|
-
this.enCurso = null;
|
|
1437
|
-
this.p.aviso(t("{name} joined the city", { name: hecho.nombre }));
|
|
1438
|
-
await this.p.refresca();
|
|
1439
|
-
} finally {
|
|
1440
|
-
this.guardando = false;
|
|
1441
|
-
await this.pinta(lienzo);
|
|
1442
|
-
}
|
|
1443
|
-
}
|
|
1444
|
-
async aseguraFicha() {
|
|
1445
|
-
await this.p.api("/api/ficha", {
|
|
1446
|
-
method: "POST",
|
|
1447
|
-
body: JSON.stringify({ domain: this.dominio || "software", role: this.rol || "blank" })
|
|
1448
|
-
});
|
|
1449
|
-
}
|
|
1450
|
-
async avanza(lienzo) {
|
|
1451
|
-
const escribeLaFicha = this.paso === 2;
|
|
1452
|
-
this.paso = Math.min(PASOS.length - 1, this.paso + 1);
|
|
1453
|
-
await this.pinta(lienzo);
|
|
1454
|
-
if (escribeLaFicha) {
|
|
1455
|
-
await this.aseguraFicha();
|
|
1456
|
-
await this.p.refresca();
|
|
1457
|
-
}
|
|
1458
|
-
}
|
|
1459
|
-
async abreSesion() {
|
|
1460
|
-
try {
|
|
1461
|
-
const r = await this.p.api("/api/sesion", {
|
|
1462
|
-
method: "POST",
|
|
1463
|
-
body: JSON.stringify({ user: this.p.yo })
|
|
1464
|
-
});
|
|
1465
|
-
this.p.aviso(
|
|
1466
|
-
r.ok && r.attach ? `Session built \u2014 attach with: ${r.attach}` : r.error ?? "Could not",
|
|
1467
|
-
!r.ok
|
|
1468
|
-
);
|
|
1469
|
-
} catch (e) {
|
|
1470
|
-
this.p.aviso(String(e), true);
|
|
1471
|
-
}
|
|
1472
|
-
}
|
|
1473
|
-
};
|
|
1474
|
-
|
|
1475
|
-
// city/web/src/activity.ts
|
|
1476
|
-
var MAP_ACTIVITY_PROTOCOL = "agents-city-map-activity/1";
|
|
1477
|
-
function isMapNavMessage(value) {
|
|
1478
|
-
if (!value || typeof value !== "object") return false;
|
|
1479
|
-
const message = value;
|
|
1480
|
-
return message.protocol === MAP_ACTIVITY_PROTOCOL && message.type === "map.nav" && typeof message.view === "string";
|
|
1481
|
-
}
|
|
1482
|
-
function isActivityEvent(value) {
|
|
1483
|
-
if (!value || typeof value !== "object") return false;
|
|
1484
|
-
const event = value;
|
|
1485
|
-
return event.protocol === "agents-city-activity/1" && typeof event.id === "string" && typeof event.seq === "number" && typeof event.kind === "string" && typeof event.actor === "string" && typeof event.summary === "string" && typeof event.title === "string" && Array.isArray(event.details);
|
|
1486
|
-
}
|
|
1487
|
-
function isPresenceEvent(event) {
|
|
1488
|
-
return event.kind === "conversation.user" || event.kind === "conversation.agent" || event.kind === "runtime.session.started" || event.kind === "runtime.session.ended";
|
|
1489
|
-
}
|
|
1490
|
-
function isSpeechEvent(event) {
|
|
1491
|
-
if (event.kind === "conversation.agent" || event.kind === "conversation.agent.commentary") {
|
|
1492
|
-
return Boolean(event.summary.trim());
|
|
1493
|
-
}
|
|
1494
|
-
return event.kind === "committee.opened" || event.kind === "committee.position.revealed" || event.kind === "committee.synthesis.published" || event.kind.startsWith("committee.floor.") || event.kind === "committee.decision.recorded" || event.kind.startsWith("committee.verification.") || event.kind === "committee.replanned" || event.kind === "committee.closed" || event.kind === "committee.command.rejected" && event.tone === "error";
|
|
1495
|
-
}
|
|
1496
|
-
|
|
1497
|
-
// city/web/src/hall.ts
|
|
1498
|
-
var PASE = window.PASE;
|
|
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
|
-
}
|
|
1514
|
-
async function api(ruta, opts) {
|
|
1515
|
-
let u = ruta + (ruta.includes("?") ? "&" : "?") + "PASE=" + encodeURIComponent(PASE);
|
|
1516
|
-
if (CIUDAD) u += "&city=" + encodeURIComponent(CIUDAD);
|
|
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;
|
|
1531
|
-
}
|
|
1532
|
-
function q(sel, raiz = document) {
|
|
1533
|
-
const el = raiz.querySelector(sel);
|
|
1534
|
-
if (!el) throw new Error(`no element matches ${sel}`);
|
|
1535
|
-
return el;
|
|
1536
|
-
}
|
|
1537
|
-
function todos(sel, raiz = document) {
|
|
1538
|
-
return [...raiz.querySelectorAll(sel)];
|
|
1539
|
-
}
|
|
1540
|
-
function esc2(s) {
|
|
1541
|
-
return String(s ?? "").replace(
|
|
1542
|
-
/[&<>"']/g,
|
|
1543
|
-
(c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]
|
|
1544
|
-
);
|
|
1545
|
-
}
|
|
1546
|
-
var corto = (p) => p.replace(E.casa, "~");
|
|
1547
|
-
var toastTimer = 0;
|
|
1548
|
-
function toast(msg, mal = false) {
|
|
1549
|
-
const t2 = q("#toast");
|
|
1550
|
-
t2.textContent = msg;
|
|
1551
|
-
t2.className = "ver" + (mal ? " mal" : "");
|
|
1552
|
-
clearTimeout(toastTimer);
|
|
1553
|
-
toastTimer = window.setTimeout(() => {
|
|
1554
|
-
t2.className = "";
|
|
1555
|
-
}, 3200);
|
|
1556
|
-
}
|
|
1557
|
-
var espera = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
1558
|
-
var E;
|
|
1559
|
-
var SECCION = "mapa";
|
|
1560
|
-
var SECCIONES = [
|
|
1561
|
-
["resumen", "Overview"],
|
|
1562
|
-
["mapa", "The map"],
|
|
1563
|
-
["puesto", "My seat"],
|
|
1564
|
-
["barrios", "Districts"],
|
|
1565
|
-
["recepcion", "Reception"],
|
|
1566
|
-
["red", "Roads"],
|
|
1567
|
-
["committee", "Committee"],
|
|
1568
|
-
["gente", "Houses"],
|
|
1569
|
-
["demos", "Demos"],
|
|
1570
|
-
["ciudades", "Cities"]
|
|
1571
|
-
];
|
|
1572
|
-
var primeraCarga = true;
|
|
1573
|
-
async function refresca() {
|
|
1574
|
-
E = await api("/api/estado");
|
|
1575
|
-
if (primeraCarga) {
|
|
1576
|
-
primeraCarga = false;
|
|
1577
|
-
const mia = E.tarjetas.find((t2) => t2.user === E.yo);
|
|
1578
|
-
if (!mia || (E.agents ?? []).length === 0) SECCION = "bienvenida";
|
|
1579
|
-
else if (!E.mapa) SECCION = "resumen";
|
|
1580
|
-
}
|
|
1581
|
-
pinta();
|
|
1582
|
-
pintaAvisoDeVersion();
|
|
1583
|
-
pintaDemoControles();
|
|
1584
|
-
sincronizaActividad(E.live_bus);
|
|
1585
|
-
}
|
|
1586
|
-
function pintaAvisoDeVersion() {
|
|
1587
|
-
const caja = document.getElementById("avisoVersion");
|
|
1588
|
-
if (!caja) return;
|
|
1589
|
-
const texto = (E.update ?? "").trim();
|
|
1590
|
-
caja.textContent = texto;
|
|
1591
|
-
caja.hidden = !texto;
|
|
1592
|
-
}
|
|
1593
|
-
function pintaDemoControles() {
|
|
1594
|
-
const header = document.querySelector("#livePanel header");
|
|
1595
|
-
const previa = document.getElementById("demoControles");
|
|
1596
|
-
if (!header || !E.demo) {
|
|
1597
|
-
previa?.remove();
|
|
1598
|
-
return;
|
|
1599
|
-
}
|
|
1600
|
-
if (previa) return;
|
|
1601
|
-
const caja = document.createElement("div");
|
|
1602
|
-
caja.id = "demoControles";
|
|
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>`;
|
|
1604
|
-
header.appendChild(caja);
|
|
1605
|
-
const pausa = q("#demoPausa", caja);
|
|
1606
|
-
const manda = (action) => api("/api/demo", {
|
|
1607
|
-
method: "POST",
|
|
1608
|
-
body: JSON.stringify({ action })
|
|
1609
|
-
});
|
|
1610
|
-
q("#demoReplay", caja).onclick = async () => {
|
|
1611
|
-
const r = await manda("restart");
|
|
1612
|
-
pausa.textContent = "\u23F8 pause";
|
|
1613
|
-
if (!r.ok) toast("Could not restart the demo", true);
|
|
1614
|
-
};
|
|
1615
|
-
pausa.onclick = async () => {
|
|
1616
|
-
const antes = pausa.textContent?.includes("resume") ?? false;
|
|
1617
|
-
const r = await manda(antes ? "resume" : "pause");
|
|
1618
|
-
if (!r.running) {
|
|
1619
|
-
pausa.textContent = "\u23F8 pause";
|
|
1620
|
-
toast("Nothing playing \u2014 hit replay", true);
|
|
1621
|
-
return;
|
|
1622
|
-
}
|
|
1623
|
-
pausa.textContent = r.paused ? "\u25B6 resume" : "\u23F8 pause";
|
|
1624
|
-
};
|
|
1625
|
-
}
|
|
1626
|
-
var liveSocket = null;
|
|
1627
|
-
var liveUrl = "";
|
|
1628
|
-
var liveEvents = [];
|
|
1629
|
-
var liveConnected = false;
|
|
1630
|
-
var liveRetry = 0;
|
|
1631
|
-
var liveSelectedThread = null;
|
|
1632
|
-
var liveFilterTouched = false;
|
|
1633
|
-
var liveShowWork = false;
|
|
1634
|
-
var pendingMapActivity = null;
|
|
1635
|
-
function sincronizaActividad(endpoint) {
|
|
1636
|
-
if (!endpoint?.online || !endpoint.url) {
|
|
1637
|
-
liveConnected = false;
|
|
1638
|
-
pintaActividad();
|
|
1639
|
-
programaDescubrimiento();
|
|
1640
|
-
return;
|
|
1641
|
-
}
|
|
1642
|
-
if (endpoint.url === liveUrl && liveSocket && (liveSocket.readyState === WebSocket.OPEN || liveSocket.readyState === WebSocket.CONNECTING)) {
|
|
1643
|
-
return;
|
|
1644
|
-
}
|
|
1645
|
-
const anterior = liveSocket;
|
|
1646
|
-
if (anterior) {
|
|
1647
|
-
anterior.onclose = null;
|
|
1648
|
-
anterior.close();
|
|
1649
|
-
}
|
|
1650
|
-
liveUrl = endpoint.url;
|
|
1651
|
-
const socket = new WebSocket(endpoint.url);
|
|
1652
|
-
liveSocket = socket;
|
|
1653
|
-
liveConnected = false;
|
|
1654
|
-
pintaActividad();
|
|
1655
|
-
socket.onopen = () => {
|
|
1656
|
-
if (liveSocket !== socket) return;
|
|
1657
|
-
liveConnected = true;
|
|
1658
|
-
liveRetry = 0;
|
|
1659
|
-
pintaActividad();
|
|
1660
|
-
};
|
|
1661
|
-
socket.onmessage = (message) => {
|
|
1662
|
-
if (liveSocket !== socket) return;
|
|
1663
|
-
let value;
|
|
1664
|
-
try {
|
|
1665
|
-
value = JSON.parse(String(message.data));
|
|
1666
|
-
} catch {
|
|
1667
|
-
return;
|
|
1668
|
-
}
|
|
1669
|
-
if (value.type === "activity.state") {
|
|
1670
|
-
liveEvents = Array.isArray(value.events) ? value.events.filter(isActivityEvent) : [];
|
|
1671
|
-
liveEvents.sort((a, b) => a.seq - b.seq);
|
|
1672
|
-
if (liveSelectedThread === null) liveSelectedThread = newestConversationThread();
|
|
1673
|
-
pintaActividad(true);
|
|
1674
|
-
} else if (value.type === "activity.event" && isActivityEvent(value.event)) {
|
|
1675
|
-
const event = value.event;
|
|
1676
|
-
if (!liveEvents.some((item) => item.id === event.id)) liveEvents.push(event);
|
|
1677
|
-
liveEvents = liveEvents.sort((a, b) => a.seq - b.seq).slice(-200);
|
|
1678
|
-
if (!liveFilterTouched && event.kind === "committee.opened" && event.thread) {
|
|
1679
|
-
liveSelectedThread = event.thread;
|
|
1680
|
-
}
|
|
1681
|
-
pintaActividad(true);
|
|
1682
|
-
enviaActividadAlMapa(event);
|
|
1683
|
-
}
|
|
1684
|
-
};
|
|
1685
|
-
socket.onclose = () => {
|
|
1686
|
-
if (liveSocket !== socket) return;
|
|
1687
|
-
liveSocket = null;
|
|
1688
|
-
liveConnected = false;
|
|
1689
|
-
pintaActividad();
|
|
1690
|
-
programaDescubrimiento();
|
|
1691
|
-
};
|
|
1692
|
-
socket.onerror = () => {
|
|
1693
|
-
};
|
|
1694
|
-
}
|
|
1695
|
-
function enviaActividadAlMapa(event) {
|
|
1696
|
-
const esComite = event.kind.startsWith("committee.");
|
|
1697
|
-
if (!isSpeechEvent(event) && !isPresenceEvent(event) && !esComite) return;
|
|
1698
|
-
const frame = document.querySelector("#cityMapFrame");
|
|
1699
|
-
if (!frame) return;
|
|
1700
|
-
if (frame.dataset.ready !== "1") {
|
|
1701
|
-
pendingMapActivity = event;
|
|
1702
|
-
return;
|
|
1703
|
-
}
|
|
1704
|
-
const origin = new URL(frame.src, location.href).origin;
|
|
1705
|
-
frame.contentWindow?.postMessage(
|
|
1706
|
-
{ protocol: MAP_ACTIVITY_PROTOCOL, type: "activity.event", event },
|
|
1707
|
-
origin
|
|
1708
|
-
);
|
|
1709
|
-
}
|
|
1710
|
-
function programaDescubrimiento() {
|
|
1711
|
-
if (liveRetry) return;
|
|
1712
|
-
liveRetry = window.setTimeout(async () => {
|
|
1713
|
-
liveRetry = 0;
|
|
1714
|
-
try {
|
|
1715
|
-
sincronizaActividad(await api("/api/live"));
|
|
1716
|
-
} catch {
|
|
1717
|
-
programaDescubrimiento();
|
|
1718
|
-
}
|
|
1719
|
-
}, 1500);
|
|
1720
|
-
}
|
|
1721
|
-
function pintaActividad(seguir = false) {
|
|
1722
|
-
const dot = document.querySelector("#liveDot");
|
|
1723
|
-
const state = document.querySelector("#liveState");
|
|
1724
|
-
const list = document.querySelector("#liveEvents");
|
|
1725
|
-
const filter = document.querySelector("#liveFilter");
|
|
1726
|
-
const context = document.querySelector("#liveContext");
|
|
1727
|
-
const workToggle = document.querySelector("#liveWorkToggle");
|
|
1728
|
-
if (!dot || !state || !list || !filter || !context || !workToggle) return;
|
|
1729
|
-
dot.classList.toggle("on", liveConnected);
|
|
1730
|
-
state.textContent = liveConnected ? t("websocket live") : liveUrl ? t("reconnecting") : t("session offline");
|
|
1731
|
-
const opened = /* @__PURE__ */ new Map();
|
|
1732
|
-
for (const event of liveEvents) {
|
|
1733
|
-
if (event.thread && (event.kind === "committee.opened" || event.kind === "conversation.user") && !esPromptDeTransporte(event) && !opened.has(event.thread)) {
|
|
1734
|
-
opened.set(event.thread, event.summary);
|
|
1735
|
-
}
|
|
1736
|
-
}
|
|
1737
|
-
filter.innerHTML = `<option value="">${t("all conversations")}</option>` + [...opened.entries()].reverse().map(
|
|
1738
|
-
([thread, question]) => `<option value="${esc2(thread)}">${esc2(question.slice(0, 54) || thread)}</option>`
|
|
1739
|
-
).join("");
|
|
1740
|
-
const selected = liveSelectedThread || "";
|
|
1741
|
-
filter.value = [...filter.options].some((option) => option.value === selected) ? selected : "";
|
|
1742
|
-
filter.onchange = () => {
|
|
1743
|
-
liveSelectedThread = filter.value;
|
|
1744
|
-
liveFilterTouched = true;
|
|
1745
|
-
pintaActividad(true);
|
|
1746
|
-
};
|
|
1747
|
-
workToggle.classList.toggle("on", liveShowWork);
|
|
1748
|
-
workToggle.setAttribute("aria-pressed", String(liveShowWork));
|
|
1749
|
-
workToggle.textContent = liveShowWork ? t("hide work") : t("show work");
|
|
1750
|
-
workToggle.onclick = () => {
|
|
1751
|
-
liveShowWork = !liveShowWork;
|
|
1752
|
-
pintaActividad(true);
|
|
1753
|
-
};
|
|
1754
|
-
pintaContexto(filter.value, context);
|
|
1755
|
-
const scoped = filter.value ? liveEvents.filter((event) => event.thread === filter.value) : liveEvents;
|
|
1756
|
-
const visible = scoped.filter(
|
|
1757
|
-
(event) => !esPromptDeTransporte(event) && (liveShowWork || !esRuidoDeTrabajo(event) || event.tone === "error")
|
|
1758
|
-
);
|
|
1759
|
-
if (!visible.length) {
|
|
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>`;
|
|
1761
|
-
return;
|
|
1762
|
-
}
|
|
1763
|
-
const wasNearBottom = list.scrollHeight - list.scrollTop - list.clientHeight < 90;
|
|
1764
|
-
list.innerHTML = visible.map(renderActividad).join("");
|
|
1765
|
-
if (seguir || wasNearBottom) list.scrollTop = list.scrollHeight;
|
|
1766
|
-
}
|
|
1767
|
-
function renderActividad(event) {
|
|
1768
|
-
const when = new Date(event.at);
|
|
1769
|
-
const time = Number.isNaN(when.getTime()) ? "" : when.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
|
1770
|
-
const side = event.role === "chair" ? "chair" : event.role === "member" ? "member" : "system";
|
|
1771
|
-
const role = rolOperativo(event.actor);
|
|
1772
|
-
const route = event.target && !["committee", "seat"].includes(event.target) ? `to ${event.target}` : event.target === "committee" ? "to the committee" : "";
|
|
1773
|
-
const summary = event.summary.trim();
|
|
1774
|
-
const compact = resumenVisible(summary);
|
|
1775
|
-
const long = compact !== summary;
|
|
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>` : "";
|
|
1777
|
-
const full = long ? `<details class="liveFull"><summary>${t("read full message")}</summary><p>${esc2(summary)}</p></details>` : "";
|
|
1778
|
-
return `<li class="liveTurn ${side} ${esc2(event.tone)}">
|
|
1779
|
-
<div class="liveAvatar${E?.avatars?.[event.actor] ? " conCara" : ""}"
|
|
1780
|
-
style="--actor-hue:${tonoActor(event.actor)}" aria-hidden="true">
|
|
1781
|
-
${cara(event.actor, event.role === "chair")}
|
|
192
|
+
</div>`}enlaza(n){let t=()=>void this.pinta(n);n.querySelectorAll("[data-bv]").forEach(i=>{i.onclick=async c=>{c.preventDefault();let o=i.dataset.bv;if(o==="salir")return this.p.vete("resumen");if(o==="atras")return this.paso=Math.max(0,this.paso-1),t();if(o==="dominio")return this.dominio=i.dataset.id??"",this.roles=[],this.rol="",t();if(o==="rol")return this.rol=i.dataset.id??"",t();if(o==="nuevo")return this.enCurso=new _({api:this.p.api,esc:this.p.esc,aviso:this.p.aviso,yo:this.p.yo},this.rolesDeAgente),t();if(o==="cancela")return this.enCurso=null,t();if(o==="guarda")return void await this.guardaAgente(n);if(o==="siguiente")return void await this.avanza(n);if(o==="sesion")return void await this.abreSesion();if(o==="ir-gente")return this.p.vete("gente");if(o==="ir-mapa")return this.p.vete("mapa");if(o==="ir-puesto")return this.p.vete("puesto")}});let a=n.querySelector("#bvCasa");a&&this.enCurso&&(this.enCurso.monta(a),a.querySelector("#bvNombre")?.focus())}async guardaAgente(n){let t=this.enCurso;if(t){this.guardando=!0,await this.pinta(n);try{await this.aseguraFicha();let a=await t.guarda();if(!a)return;this.roster.push({...t.datos,nombre:a.nombre}),this.enCurso=null,this.p.aviso(s("{name} joined the city",{name:a.nombre})),await this.p.refresca()}finally{this.guardando=!1,await this.pinta(n)}}}async aseguraFicha(){await this.p.api("/api/ficha",{method:"POST",body:JSON.stringify({domain:this.dominio||"software",role:this.rol||"blank"})})}async avanza(n){let t=this.paso===2;this.paso=Math.min(xe.length-1,this.paso+1),await this.pinta(n),t&&(await this.aseguraFicha(),await this.p.refresca())}async abreSesion(){try{let n=await this.p.api("/api/sesion",{method:"POST",body:JSON.stringify({user:this.p.yo})});this.p.aviso(n.ok&&n.attach?`Session built \u2014 attach with: ${n.attach}`:n.error??"Could not",!n.ok)}catch(n){this.p.aviso(String(n),!0)}}};var te="agents-city-map-activity/1";function Le(e){if(!e||typeof e!="object")return!1;let n=e;return n.protocol===te&&n.type==="map.nav"&&typeof n.view=="string"}function pe(e){if(!e||typeof e!="object")return!1;let n=e;return n.protocol==="agents-city-activity/1"&&typeof n.id=="string"&&typeof n.seq=="number"&&typeof n.kind=="string"&&typeof n.actor=="string"&&typeof n.summary=="string"&&typeof n.title=="string"&&Array.isArray(n.details)}function Se(e){return e.kind==="conversation.user"||e.kind==="conversation.agent"||e.kind==="runtime.session.started"||e.kind==="runtime.session.ended"}function Ce(e){return e.kind==="conversation.agent"||e.kind==="conversation.agent.commentary"?!!e.summary.trim():e.kind==="committee.opened"||e.kind==="committee.position.revealed"||e.kind==="committee.synthesis.published"||e.kind.startsWith("committee.floor.")||e.kind==="committee.decision.recorded"||e.kind.startsWith("committee.verification.")||e.kind==="committee.replanned"||e.kind==="committee.closed"||e.kind==="committee.command.rejected"&&e.tone==="error"}var F=window.PASE,oe=new URLSearchParams(location.search).get("city")??"";function ne(e,n,t){try{let a="/api/diario?PASE="+encodeURIComponent(F);oe&&(a+="&city="+encodeURIComponent(oe)),fetch(a,{method:"POST",headers:{"X-City-Pase":F,"Content-Type":"application/json"},body:JSON.stringify({que:e,detalle:n,donde:t??location.hash??""}),keepalive:!0}).catch(()=>{})}catch{}}async function b(e,n){let t=e+(e.includes("?")?"&":"?")+"PASE="+encodeURIComponent(F);oe&&(t+="&city="+encodeURIComponent(oe));let a;try{a=await fetch(t,{headers:{"X-City-Pase":F,"Content-Type":"application/json"},...n})}catch(c){throw e!=="/api/diario"&&ne("fetch failed",String(c),e),c}let i=await a.json();return e!=="/api/diario"&&(!a.ok||i?.error)&&ne("api refused",{estado:a.status,error:i?.error},e),i}function m(e,n=document){let t=n.querySelector(e);if(!t)throw new Error(`no element matches ${e}`);return t}function f(e,n=document){return[...n.querySelectorAll(e)]}function l(e){return String(e??"").replace(/[&<>"']/g,n=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[n])}var N=e=>e.replace(d.casa,"~"),je=0;function g(e,n=!1){let t=m("#toast");t.textContent=e,t.className="ver"+(n?" mal":""),clearTimeout(je),je=window.setTimeout(()=>{t.className=""},3200)}var Re=e=>new Promise(n=>setTimeout(n,e)),d,S="mapa",Ye=[["resumen","Overview"],["mapa","The map"],["puesto","My seat"],["barrios","Districts"],["recepcion","Reception"],["red","Roads"],["committee","Committee"],["gente","Houses"],["demos","Demos"],["ciudades","Cities"]],be=!0;async function k(){d=await b("/api/estado"),be&&(be=!1,!d.tarjetas.find(n=>n.user===d.yo)||(d.agents??[]).length===0?S="bienvenida":d.mapa||(S="resumen")),C(),Ve(),Ge(),He(d.live_bus)}function Ve(){let e=document.getElementById("avisoVersion");if(!e)return;let n=(d.update??"").trim();e.textContent=n,e.hidden=!n}function Ge(){let e=document.querySelector("#livePanel header"),n=document.getElementById("demoControles");if(!e||!d.demo){n?.remove();return}if(n)return;let t=document.createElement("div");t.id="demoControles",t.innerHTML=`<span>${s("guided committee")}</span><button id="demoReplay" type="button" title="${s("Play the guided committee from the top")}">${s("\u27F3 replay")}</button><button id="demoPausa" type="button" title="${s("Pause or resume mid-scene")}">${s("\u23F8 pause")}</button>`,e.appendChild(t);let a=m("#demoPausa",t),i=c=>b("/api/demo",{method:"POST",body:JSON.stringify({action:c})});m("#demoReplay",t).onclick=async()=>{let c=await i("restart");a.textContent="\u23F8 pause",c.ok||g("Could not restart the demo",!0)},a.onclick=async()=>{let c=a.textContent?.includes("resume")??!1,o=await i(c?"resume":"pause");if(!o.running){a.textContent="\u23F8 pause",g("Nothing playing \u2014 hit replay",!0);return}a.textContent=o.paused?"\u25B6 resume":"\u23F8 pause"}}var A=null,ye="",L=[],O=!1,ae=0,Y=null,qe=!1,U=!1,se=null;function He(e){if(!e?.online||!e.url){O=!1,P(),fe();return}if(e.url===ye&&A&&(A.readyState===WebSocket.OPEN||A.readyState===WebSocket.CONNECTING))return;let n=A;n&&(n.onclose=null,n.close()),ye=e.url;let t=new WebSocket(e.url);A=t,O=!1,P(),t.onopen=()=>{A===t&&(O=!0,ae=0,P())},t.onmessage=a=>{if(A!==t)return;let i;try{i=JSON.parse(String(a.data))}catch{return}if(i.type==="activity.state")L=Array.isArray(i.events)?i.events.filter(pe):[],L.sort((c,o)=>c.seq-o.seq),Y===null&&(Y=Je()),P(!0);else if(i.type==="activity.event"&&pe(i.event)){let c=i.event;L.some(o=>o.id===c.id)||L.push(c),L=L.sort((o,r)=>o.seq-r.seq).slice(-200),!qe&&c.kind==="committee.opened"&&c.thread&&(Y=c.thread),P(!0),Pe(c)}},t.onclose=()=>{A===t&&(A=null,O=!1,P(),fe())},t.onerror=()=>{}}function Pe(e){let n=e.kind.startsWith("committee.");if(!Ce(e)&&!Se(e)&&!n)return;let t=document.querySelector("#cityMapFrame");if(!t)return;if(t.dataset.ready!=="1"){se=e;return}let a=new URL(t.src,location.href).origin;t.contentWindow?.postMessage({protocol:te,type:"activity.event",event:e},a)}function fe(){ae||(ae=window.setTimeout(async()=>{ae=0;try{He(await b("/api/live"))}catch{fe()}},1500))}function P(e=!1){let n=document.querySelector("#liveDot"),t=document.querySelector("#liveState"),a=document.querySelector("#liveEvents"),i=document.querySelector("#liveFilter"),c=document.querySelector("#liveContext"),o=document.querySelector("#liveWorkToggle");if(!n||!t||!a||!i||!c||!o)return;n.classList.toggle("on",O),t.textContent=O?s("websocket live"):ye?s("reconnecting"):s("session offline");let r=new Map;for(let w of L)w.thread&&(w.kind==="committee.opened"||w.kind==="conversation.user")&&!we(w)&&!r.has(w.thread)&&r.set(w.thread,w.summary);i.innerHTML=`<option value="">${s("all conversations")}</option>`+[...r.entries()].reverse().map(([w,y])=>`<option value="${l(w)}">${l(y.slice(0,54)||w)}</option>`).join("");let p=Y||"";i.value=[...i.options].some(w=>w.value===p)?p:"",i.onchange=()=>{Y=i.value,qe=!0,P(!0)},o.classList.toggle("on",U),o.setAttribute("aria-pressed",String(U)),o.textContent=U?s("hide work"):s("show work"),o.onclick=()=>{U=!U,P(!0)},Qe(i.value,c);let h=(i.value?L.filter(w=>w.thread===i.value):L).filter(w=>!we(w)&&(U||!Ke(w)||w.tone==="error"));if(!h.length){a.innerHTML=`<li class="liveEmpty">${O?s("The bus is live. Questions, positions and moderated replies will appear here."):s("Start the city session. Its visible conversation will appear here as it happens.")}</li>`;return}let v=a.scrollHeight-a.scrollTop-a.clientHeight<90;a.innerHTML=h.map(Ie).join(""),(e||v)&&(a.scrollTop=a.scrollHeight)}function Ie(e){let n=new Date(e.at),t=Number.isNaN(n.getTime())?"":n.toLocaleTimeString([],{hour:"2-digit",minute:"2-digit",second:"2-digit"}),a=e.role==="chair"?"chair":e.role==="member"?"member":"system",i=Oe(e.actor),c=e.target&&!["committee","seat"].includes(e.target)?`to ${e.target}`:e.target==="committee"?"to the committee":"",o=e.summary.trim(),r=Xe(o),p=r!==o,u=e.details.length?`<details class="liveEvidence"><summary>${e.details.length} ${e.details.length===1?"detail":"details"}</summary><ul>${e.details.map(v=>`<li>${l(v)}</li>`).join("")}</ul></details>`:"",h=p?`<details class="liveFull"><summary>${s("read full message")}</summary><p>${l(o)}</p></details>`:"";return`<li class="liveTurn ${a} ${l(e.tone)}">
|
|
193
|
+
<div class="liveAvatar${d?.avatars?.[e.actor]?" conCara":""}"
|
|
194
|
+
style="--actor-hue:${De(e.actor)}" aria-hidden="true">
|
|
195
|
+
${Ne(e.actor,e.role==="chair")}
|
|
1782
196
|
</div>
|
|
1783
197
|
<article class="liveBubble">
|
|
1784
|
-
<div class="liveMeta"><span class="liveActor">${
|
|
1785
|
-
${
|
|
1786
|
-
${
|
|
1787
|
-
<div class="liveHeading">${
|
|
1788
|
-
<p>${
|
|
198
|
+
<div class="liveMeta"><span class="liveActor">${l(e.actor)}</span>
|
|
199
|
+
${i?`<span class="liveRole">${l(i)}</span>`:""}
|
|
200
|
+
${c?`<span>${l(c)}</span>`:""}<span class="liveWhen">${l(t)}</span></div>
|
|
201
|
+
<div class="liveHeading">${Be(e.kind)}<h3>${l(et(e))}</h3></div>
|
|
202
|
+
<p>${l(r)}</p>${h}${u}
|
|
1789
203
|
</article>
|
|
1790
|
-
</li
|
|
1791
|
-
}
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
}
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
return;
|
|
1805
|
-
}
|
|
1806
|
-
const invited = opened?.details.find((detail) => detail.startsWith("Invited:"))?.slice("Invited:".length).split(",").map((actor) => actor.trim()).filter(Boolean) || [];
|
|
1807
|
-
const participants = deliberation?.participants || invited;
|
|
1808
|
-
const received = deliberation?.received ?? new Set(
|
|
1809
|
-
conversation.filter((event) => event.kind === "committee.position.submitted").map((event) => event.actor)
|
|
1810
|
-
).size;
|
|
1811
|
-
const total = deliberation?.total ?? participants.length;
|
|
1812
|
-
const status = conversation.at(-1)?.phase || deliberation?.status || "starting";
|
|
1813
|
-
const actors = ["seat", ...participants];
|
|
1814
|
-
container.hidden = false;
|
|
1815
|
-
container.innerHTML = `<div class="livePeople">${actors.map(
|
|
1816
|
-
(actor) => `<span class="livePerson${E?.avatars?.[actor] ? " conCara" : ""}" title="${esc2(
|
|
1817
|
-
actor === "seat" ? "seat \xB7 chair" : `${actor} \xB7 ${rolOperativo(actor) || "member"}`
|
|
1818
|
-
)}" style="--actor-hue:${tonoActor(actor)}">${cara(actor, actor === "seat")}</span>`
|
|
1819
|
-
).join("")}</div><span class="liveContextText"><b>${t("seat moderates")}</b> \xB7 ${esc2(
|
|
1820
|
-
received
|
|
1821
|
-
)}/${esc2(total)} positions \xB7 ${esc2(status)}</span>`;
|
|
1822
|
-
}
|
|
1823
|
-
function esPromptDeTransporte(event) {
|
|
1824
|
-
if (event.kind !== "conversation.user") return false;
|
|
1825
|
-
const summary = event.summary.trimStart();
|
|
1826
|
-
return summary.startsWith("[Agents City authenticated local bus]") || summary.startsWith("<channel") && summary.slice(0, 500).includes("plugin:city:city-bus");
|
|
1827
|
-
}
|
|
1828
|
-
function esRuidoDeTrabajo(event) {
|
|
1829
|
-
return event.kind === "work.command.started" || event.kind === "work.command.completed" || event.kind === "runtime.turn.started" || event.kind === "runtime.turn.completed" || event.kind === "runtime.gateway.ready" || event.kind === "runtime.session.started" || event.kind === "runtime.session.ended";
|
|
1830
|
-
}
|
|
1831
|
-
function rolOperativo(actor) {
|
|
1832
|
-
if (actor === "seat") return "chair";
|
|
1833
|
-
return E?.skills?.[actor]?.role || "";
|
|
1834
|
-
}
|
|
1835
|
-
function cara(actor, esChair) {
|
|
1836
|
-
const uri = E?.avatars?.[actor];
|
|
1837
|
-
if (uri && uri.startsWith("data:image/svg+xml;base64,")) {
|
|
1838
|
-
return `<img class="liveCara" src="${esc2(uri)}" alt="">`;
|
|
1839
|
-
}
|
|
1840
|
-
return esChair ? icono("chair") : esc2(iniciales(actor));
|
|
1841
|
-
}
|
|
1842
|
-
function iniciales(actor) {
|
|
1843
|
-
const parts = actor.split(/[-_.\s]+/).filter(Boolean);
|
|
1844
|
-
const initials = parts.length > 1 ? parts.slice(0, 2).map((part) => part[0]).join("") : actor.slice(0, 2);
|
|
1845
|
-
return initials.toUpperCase();
|
|
1846
|
-
}
|
|
1847
|
-
function tonoActor(actor) {
|
|
1848
|
-
let hash = 17;
|
|
1849
|
-
for (const character of actor) hash = (hash * 31 + character.charCodeAt(0)) % 360;
|
|
1850
|
-
return hash;
|
|
1851
|
-
}
|
|
1852
|
-
function resumenVisible(summary) {
|
|
1853
|
-
if (summary.length <= 520) return summary;
|
|
1854
|
-
const boundary = summary.slice(0, 520).lastIndexOf(" ");
|
|
1855
|
-
return summary.slice(0, boundary > 360 ? boundary : 520).trimEnd() + "\u2026";
|
|
1856
|
-
}
|
|
1857
|
-
function tituloVisible(event) {
|
|
1858
|
-
if (event.kind === "conversation.user")
|
|
1859
|
-
return event.actor === "seat" ? "Question" : "Brief received";
|
|
1860
|
-
if (event.kind === "conversation.agent.commentary") return "Working note";
|
|
1861
|
-
if (event.kind === "conversation.agent") return "Response";
|
|
1862
|
-
if (event.kind === "committee.opened") return "Question to the committee";
|
|
1863
|
-
if (event.kind === "committee.position.submitted") return "Position sealed";
|
|
1864
|
-
if (event.kind === "committee.positions.revealed") return "Blind round complete";
|
|
1865
|
-
if (event.kind === "committee.position.revealed") return "Position";
|
|
1866
|
-
if (event.kind === "committee.synthesis.published") return "Chair synthesis";
|
|
1867
|
-
if (event.kind === "committee.floor.requested") return "Requests the floor";
|
|
1868
|
-
if (event.kind === "committee.floor.granted") return "Floor granted";
|
|
1869
|
-
if (event.kind === "committee.floor.denied") return "Floor denied";
|
|
1870
|
-
if (event.kind === "committee.floor.spoke") return "Intervention";
|
|
1871
|
-
if (event.kind === "committee.decision.recorded") return "Decision";
|
|
1872
|
-
if (event.kind.startsWith("committee.verification.")) return "Verification";
|
|
1873
|
-
if (event.kind === "committee.closed") return "Committee closed";
|
|
1874
|
-
return event.title;
|
|
1875
|
-
}
|
|
1876
|
-
function icono(kind) {
|
|
1877
|
-
if (kind === "chair") {
|
|
1878
|
-
return '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 17h16l-1.4-9-4.1 3L12 5l-2.5 6-4.1-3L4 17Zm1 2h14"/></svg>';
|
|
1879
|
-
}
|
|
1880
|
-
if (kind.includes("question") || kind === "committee.opened" || kind === "conversation.user") {
|
|
1881
|
-
return '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 5h14v10H9l-4 4V5Zm5 4a2 2 0 1 1 3.4 1.4c-.9.6-1.4 1-1.4 2.1M12 14.7v.1"/></svg>';
|
|
1882
|
-
}
|
|
1883
|
-
if (kind.includes("decision") || kind.includes("verification") || kind === "committee.closed") {
|
|
1884
|
-
return '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m5 12 4 4L19 6"/></svg>';
|
|
1885
|
-
}
|
|
1886
|
-
if (kind.includes("floor")) {
|
|
1887
|
-
return '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 11V5a1.5 1.5 0 0 1 3 0v5-7a1.5 1.5 0 0 1 3 0v7-5a1.5 1.5 0 0 1 3 0v7l1-2a1.5 1.5 0 0 1 2.6 1.5L17 18a4 4 0 0 1-3.5 2H10a5 5 0 0 1-5-5v-3.5a1.5 1.5 0 0 1 3 0V13"/></svg>';
|
|
1888
|
-
}
|
|
1889
|
-
if (kind.includes("command") || kind.includes("work.")) {
|
|
1890
|
-
return '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m6 8 4 4-4 4m6 0h6"/></svg>';
|
|
1891
|
-
}
|
|
1892
|
-
return '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 6h14v12H5zM8 10h8m-8 4h5"/></svg>';
|
|
1893
|
-
}
|
|
1894
|
-
function rail() {
|
|
1895
|
-
const cuantos = (E.agents ?? []).length;
|
|
1896
|
-
q("#railCiudad").innerHTML = `<span class="railEtiqueta">${t("you are in")}</span>
|
|
1897
|
-
<b>${esc2(E.city_name)}</b>
|
|
1898
|
-
<span class="railDe">${esc2(E.domain)} \xB7 ${plural(cuantos, "{n} house", "{n} houses")}</span>`;
|
|
1899
|
-
q("#rail").innerHTML = SECCIONES.filter(([id]) => id !== "ciudades").map(([id, et]) => {
|
|
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"' : ""}>
|
|
1903
|
-
<span>${esc2(t(et))}</span>${n ? `<span class="n">${n}</span>` : ""}</li>`;
|
|
1904
|
-
}).join("");
|
|
1905
|
-
q("#railOtras").innerHTML = `<li class="${SECCION === "ciudades" ? "aqui" : ""}" data-s="ciudades" role="button"
|
|
1906
|
-
tabindex="0" ${SECCION === "ciudades" ? 'aria-current="page"' : ""}>
|
|
1907
|
-
<span>${t("All cities")}</span><span class="n">${E.ciudades.length}</span></li>`;
|
|
1908
|
-
todos("#rail li,#railOtras li").forEach((li) => {
|
|
1909
|
-
const abre = () => {
|
|
1910
|
-
document.body.classList.remove("enGuia");
|
|
1911
|
-
SECCION = li.dataset.s ?? "mapa";
|
|
1912
|
-
pinta();
|
|
1913
|
-
};
|
|
1914
|
-
li.onclick = abre;
|
|
1915
|
-
li.onkeydown = (event) => {
|
|
1916
|
-
if (event.key !== "Enter" && event.key !== " ") return;
|
|
1917
|
-
event.preventDefault();
|
|
1918
|
-
abre();
|
|
1919
|
-
};
|
|
1920
|
-
});
|
|
1921
|
-
q("#dondeDatos").textContent = corto(E.datos);
|
|
1922
|
-
const cs = q("#ciudades");
|
|
1923
|
-
if (E.ciudades.length > 1) {
|
|
1924
|
-
cs.innerHTML = `<select id="cambiaCiudad" aria-label="${t("which city this hall manages")}"
|
|
1925
|
-
title="${t("which city this hall manages")}">
|
|
1926
|
-
${E.ciudades.map(
|
|
1927
|
-
(c) => `<option value="${esc2(c.ruta)}" ${c.actual ? "selected" : ""}>
|
|
1928
|
-
${esc2(c.nombre)}</option>`
|
|
1929
|
-
).join("")}</select>`;
|
|
1930
|
-
q("#cambiaCiudad").onchange = (ev) => {
|
|
1931
|
-
const ruta = ev.target.value;
|
|
1932
|
-
location.href = "/?PASE=" + encodeURIComponent(PASE) + "&city=" + encodeURIComponent(ruta);
|
|
1933
|
-
};
|
|
1934
|
-
} else {
|
|
1935
|
-
cs.innerHTML = "";
|
|
1936
|
-
}
|
|
1937
|
-
const nueva = q("#alWizard");
|
|
1938
|
-
nueva.onclick = async (ev) => {
|
|
1939
|
-
ev.preventDefault();
|
|
1940
|
-
const dicho = await pregunta({
|
|
1941
|
-
titulo: t("Start another city"),
|
|
1942
|
-
cuerpo: [
|
|
1943
|
-
t(
|
|
1944
|
-
"A city is a place with its own seat, its own houses and its own map. Yours stay where they are."
|
|
1945
|
-
)
|
|
1946
|
-
],
|
|
1947
|
-
campos: [
|
|
1948
|
-
{
|
|
1949
|
-
id: "name",
|
|
1950
|
-
etiqueta: t("What is it called?"),
|
|
1951
|
-
pista: t("home, clients, the lab \u2014 whatever you would say out loud"),
|
|
1952
|
-
requerido: true
|
|
1953
|
-
}
|
|
1954
|
-
],
|
|
1955
|
-
aceptar: t("Create it")
|
|
1956
|
-
});
|
|
1957
|
-
const name = dicho?.name;
|
|
1958
|
-
if (!name?.trim()) return;
|
|
1959
|
-
const r = await api("/api/ciudades", {
|
|
1960
|
-
method: "POST",
|
|
1961
|
-
body: JSON.stringify({ name })
|
|
1962
|
-
});
|
|
1963
|
-
if (r.error || !r.city) return toast(r.error ?? "Could not create the city", true);
|
|
1964
|
-
location.href = "/?PASE=" + encodeURIComponent(PASE) + "&city=" + encodeURIComponent(r.city);
|
|
1965
|
-
};
|
|
1966
|
-
}
|
|
1967
|
-
var montada = null;
|
|
1968
|
-
function pinta() {
|
|
1969
|
-
montada?.desmonta?.();
|
|
1970
|
-
montada = null;
|
|
1971
|
-
rail();
|
|
1972
|
-
q(".cuerpo").style.padding = "";
|
|
1973
|
-
q("#lienzo").style.maxWidth = "";
|
|
1974
|
-
VISTAS[SECCION]();
|
|
1975
|
-
q(".cuerpo").scrollTop = 0;
|
|
1976
|
-
}
|
|
1977
|
-
var VISTAS = {};
|
|
1978
|
-
var guia = null;
|
|
1979
|
-
VISTAS.bienvenida = () => {
|
|
1980
|
-
document.body.classList.add("enGuia");
|
|
1981
|
-
guia ??= new Bienvenida({
|
|
1982
|
-
api,
|
|
1983
|
-
esc: esc2,
|
|
1984
|
-
aviso: toast,
|
|
1985
|
-
refresca,
|
|
1986
|
-
vete: (seccion) => {
|
|
1987
|
-
document.body.classList.remove("enGuia");
|
|
1988
|
-
SECCION = seccion;
|
|
1989
|
-
pinta();
|
|
1990
|
-
},
|
|
1991
|
-
ciudad: E.city_name,
|
|
1992
|
-
yo: E.yo,
|
|
1993
|
-
datos: corto(E.datos)
|
|
1994
|
-
});
|
|
1995
|
-
guia.monta(q("#lienzo"));
|
|
1996
|
-
montada = guia;
|
|
1997
|
-
};
|
|
1998
|
-
VISTAS.ciudades = () => {
|
|
1999
|
-
const filas = E.ciudades.map(
|
|
2000
|
-
(c) => `
|
|
2001
|
-
<div class="fila ${c.actual ? "on" : ""}">
|
|
2002
|
-
<span class="et">${esc2(c.nombre)}${c.actual ? ' <b class="de">\xB7 open now</b>' : ""}</span>
|
|
2003
|
-
<span class="de">${c.agentes ?? 0} house${(c.agentes ?? 0) === 1 ? "" : "s"} of its own</span>
|
|
2004
|
-
<span class="de mono">${esc2(corto(c.ruta))}</span>
|
|
204
|
+
</li>`}function Je(){return[...L].reverse().find(e=>!!e.thread&&(e.kind==="committee.opened"||e.kind==="conversation.user"&&!we(e)))?.thread||""}function Qe(e,n){let t=e?d?.deliberations?.find(v=>v.id===e):void 0,a=e?L.filter(v=>v.thread===e):[],i=a.find(v=>v.kind==="committee.opened");if(!t&&!i){n.innerHTML="",n.hidden=!0;return}let c=i?.details.find(v=>v.startsWith("Invited:"))?.slice(8).split(",").map(v=>v.trim()).filter(Boolean)||[],o=t?.participants||c,r=t?.received??new Set(a.filter(v=>v.kind==="committee.position.submitted").map(v=>v.actor)).size,p=t?.total??o.length,u=a.at(-1)?.phase||t?.status||"starting",h=["seat",...o];n.hidden=!1,n.innerHTML=`<div class="livePeople">${h.map(v=>`<span class="livePerson${d?.avatars?.[v]?" conCara":""}" title="${l(v==="seat"?"seat \xB7 chair":`${v} \xB7 ${Oe(v)||"member"}`)}" style="--actor-hue:${De(v)}">${Ne(v,v==="seat")}</span>`).join("")}</div><span class="liveContextText"><b>${s("seat moderates")}</b> \xB7 ${l(r)}/${l(p)} positions \xB7 ${l(u)}</span>`}function we(e){if(e.kind!=="conversation.user")return!1;let n=e.summary.trimStart();return n.startsWith("[Agents City authenticated local bus]")||n.startsWith("<channel")&&n.slice(0,500).includes("plugin:city:city-bus")}function Ke(e){return e.kind==="work.command.started"||e.kind==="work.command.completed"||e.kind==="runtime.turn.started"||e.kind==="runtime.turn.completed"||e.kind==="runtime.gateway.ready"||e.kind==="runtime.session.started"||e.kind==="runtime.session.ended"}function Oe(e){return e==="seat"?"chair":d?.skills?.[e]?.role||""}function Ne(e,n){let t=d?.avatars?.[e];return t&&t.startsWith("data:image/svg+xml;base64,")?`<img class="liveCara" src="${l(t)}" alt="">`:n?Be("chair"):l(Ze(e))}function Ze(e){let n=e.split(/[-_.\s]+/).filter(Boolean);return(n.length>1?n.slice(0,2).map(a=>a[0]).join(""):e.slice(0,2)).toUpperCase()}function De(e){let n=17;for(let t of e)n=(n*31+t.charCodeAt(0))%360;return n}function Xe(e){if(e.length<=520)return e;let n=e.slice(0,520).lastIndexOf(" ");return e.slice(0,n>360?n:520).trimEnd()+"\u2026"}function et(e){return e.kind==="conversation.user"?e.actor==="seat"?"Question":"Brief received":e.kind==="conversation.agent.commentary"?"Working note":e.kind==="conversation.agent"?"Response":e.kind==="committee.opened"?"Question to the committee":e.kind==="committee.position.submitted"?"Position sealed":e.kind==="committee.positions.revealed"?"Blind round complete":e.kind==="committee.position.revealed"?"Position":e.kind==="committee.synthesis.published"?"Chair synthesis":e.kind==="committee.floor.requested"?"Requests the floor":e.kind==="committee.floor.granted"?"Floor granted":e.kind==="committee.floor.denied"?"Floor denied":e.kind==="committee.floor.spoke"?"Intervention":e.kind==="committee.decision.recorded"?"Decision":e.kind.startsWith("committee.verification.")?"Verification":e.kind==="committee.closed"?"Committee closed":e.title}function Be(e){return e==="chair"?'<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 17h16l-1.4-9-4.1 3L12 5l-2.5 6-4.1-3L4 17Zm1 2h14"/></svg>':e.includes("question")||e==="committee.opened"||e==="conversation.user"?'<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 5h14v10H9l-4 4V5Zm5 4a2 2 0 1 1 3.4 1.4c-.9.6-1.4 1-1.4 2.1M12 14.7v.1"/></svg>':e.includes("decision")||e.includes("verification")||e==="committee.closed"?'<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m5 12 4 4L19 6"/></svg>':e.includes("floor")?'<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 11V5a1.5 1.5 0 0 1 3 0v5-7a1.5 1.5 0 0 1 3 0v7-5a1.5 1.5 0 0 1 3 0v7l1-2a1.5 1.5 0 0 1 2.6 1.5L17 18a4 4 0 0 1-3.5 2H10a5 5 0 0 1-5-5v-3.5a1.5 1.5 0 0 1 3 0V13"/></svg>':e.includes("command")||e.includes("work.")?'<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m6 8 4 4-4 4m6 0h6"/></svg>':'<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 6h14v12H5zM8 10h8m-8 4h5"/></svg>'}function ze(){let e=(d.agents??[]).length;m("#railCiudad").innerHTML=`<span class="railEtiqueta">${s("you are in")}</span>
|
|
205
|
+
<b>${l(d.city_name)}</b>
|
|
206
|
+
<span class="railDe">${l(d.domain)} \xB7 ${x(e,"{n} house","{n} houses")}</span>`,m("#rail").innerHTML=Ye.filter(([a])=>a!=="ciudades").map(([a,i])=>{let c=a==="demos"?"":a==="gente"?String(Object.keys(d.skills).length):a==="committee"?String(d.deliberations.length):a==="recepcion"?String(d.reception?.pending??0):a==="red"?String(d.roads.length):a==="barrios"?String(d.parcelas.length):"";return`<li class="${a===S?"aqui":""}" data-s="${a}" role="button"
|
|
207
|
+
tabindex="0" ${a===S?'aria-current="page"':""}>
|
|
208
|
+
<span>${l(s(i))}</span>${c?`<span class="n">${c}</span>`:""}</li>`}).join(""),m("#railOtras").innerHTML=`<li class="${S==="ciudades"?"aqui":""}" data-s="ciudades" role="button"
|
|
209
|
+
tabindex="0" ${S==="ciudades"?'aria-current="page"':""}>
|
|
210
|
+
<span>${s("All cities")}</span><span class="n">${d.ciudades.length}</span></li>`,f("#rail li,#railOtras li").forEach(a=>{let i=()=>{document.body.classList.remove("enGuia"),S=a.dataset.s??"mapa",C()};a.onclick=i,a.onkeydown=c=>{c.key!=="Enter"&&c.key!==" "||(c.preventDefault(),i())}}),m("#dondeDatos").textContent=N(d.datos);let n=m("#ciudades");d.ciudades.length>1?(n.innerHTML=`<select id="cambiaCiudad" aria-label="${s("which city this hall manages")}"
|
|
211
|
+
title="${s("which city this hall manages")}">
|
|
212
|
+
${d.ciudades.map(a=>`<option value="${l(a.ruta)}" ${a.actual?"selected":""}>
|
|
213
|
+
${l(a.nombre)}</option>`).join("")}</select>`,m("#cambiaCiudad").onchange=a=>{let i=a.target.value;location.href="/?PASE="+encodeURIComponent(F)+"&city="+encodeURIComponent(i)}):n.innerHTML="";let t=m("#alWizard");t.onclick=async a=>{a.preventDefault();let c=(await I({titulo:s("Start another city"),cuerpo:[s("A city is a place with its own seat, its own houses and its own map. Yours stay where they are.")],campos:[{id:"name",etiqueta:s("What is it called?"),pista:s("home, clients, the lab \u2014 whatever you would say out loud"),requerido:!0}],aceptar:s("Create it")}))?.name;if(!c?.trim())return;let o=await b("/api/ciudades",{method:"POST",body:JSON.stringify({name:c})});if(o.error||!o.city)return g(o.error??"Could not create the city",!0);location.href="/?PASE="+encodeURIComponent(F)+"&city="+encodeURIComponent(o.city)}}var ie=null;function C(){ie?.desmonta?.(),ie=null,ze(),m(".cuerpo").style.padding="",m("#lienzo").style.maxWidth="",T[S](),m(".cuerpo").scrollTop=0}var T={},me=null;T.bienvenida=()=>{document.body.classList.add("enGuia"),me??=new ee({api:b,esc:l,aviso:g,refresca:k,vete:e=>{document.body.classList.remove("enGuia"),S=e,C()},ciudad:d.city_name,yo:d.yo,datos:N(d.datos)}),me.monta(m("#lienzo")),ie=me};T.ciudades=()=>{let e=d.ciudades.map(a=>`
|
|
214
|
+
<div class="fila ${a.actual?"on":""}">
|
|
215
|
+
<span class="et">${l(a.nombre)}${a.actual?' <b class="de">\xB7 open now</b>':""}</span>
|
|
216
|
+
<span class="de">${a.agentes??0} house${(a.agentes??0)===1?"":"s"} of its own</span>
|
|
217
|
+
<span class="de mono">${l(N(a.ruta))}</span>
|
|
2005
218
|
<span style="margin-left:auto;display:flex;gap:6px">
|
|
2006
|
-
${
|
|
2007
|
-
<button class="bt mini archiva" data-ciudad="${
|
|
2008
|
-
data-nombre="${
|
|
219
|
+
${a.actual?"":`<a class="bt mini" href="?city=${encodeURIComponent(a.ruta)}">open</a>
|
|
220
|
+
<button class="bt mini archiva" data-ciudad="${l(a.ruta)}"
|
|
221
|
+
data-nombre="${l(a.nombre)}">archive</button>`}
|
|
2009
222
|
</span>
|
|
2010
|
-
</div>`
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
<h1 style="margin-top:6px">${t("Your cities")}</h1>
|
|
2014
|
-
<p class="prosa" style="margin-top:8px">${t(`One person, several autonomous cities: each has
|
|
223
|
+
</div>`).join("");m("#lienzo").innerHTML=`<div><span class="sub">cities</span>
|
|
224
|
+
<h1 style="margin-top:6px">${s("Your cities")}</h1>
|
|
225
|
+
<p class="prosa" style="margin-top:8px">${s(`One person, several autonomous cities: each has
|
|
2015
226
|
its own identity, domain, chair, <b>its own houses</b> and its own roads. A house is not
|
|
2016
227
|
shared \u2014 it stands inside the city that owns it, with its workspace and mounts under that
|
|
2017
228
|
city's folder \u2014 so two cities can each have a <code class="mono">docs</code> house and
|
|
2018
229
|
they are two different workers. They share nothing unless you build a road between
|
|
2019
230
|
them.`)}</p></div>
|
|
2020
|
-
<div class="lista"><div class="filas">${
|
|
231
|
+
<div class="lista"><div class="filas">${e}</div></div>
|
|
2021
232
|
<div class="campos" style="max-width:420px">
|
|
2022
|
-
<div class="campo"><label>${
|
|
2023
|
-
<input type="text" id="nuevaCiudad" placeholder="${
|
|
233
|
+
<div class="campo"><label>${s("Start another city")}</label>
|
|
234
|
+
<input type="text" id="nuevaCiudad" placeholder="${s("client-a, research, the book\u2026")}"></div>
|
|
2024
235
|
<button class="bt ppal" id="creaCiudad">Create it</button>
|
|
2025
236
|
</div>
|
|
2026
237
|
<div class="peligro">
|
|
2027
|
-
<h3>${
|
|
2028
|
-
<p class="prosa">${
|
|
2029
|
-
|
|
2030
|
-
{ city: `<b>${esc2(E.city_name)}</b>` }
|
|
2031
|
-
)}</p>
|
|
2032
|
-
<button class="bt malo" id="reiniciaCiudad">${t("Start over\u2026")}</button>
|
|
238
|
+
<h3>${s("Start this city over")}</h3>
|
|
239
|
+
<p class="prosa">${s("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.",{city:`<b>${l(d.city_name)}</b>`})}</p>
|
|
240
|
+
<button class="bt malo" id="reiniciaCiudad">${s("Start over\u2026")}</button>
|
|
2033
241
|
</div>
|
|
2034
242
|
<p class="pista">Archiving <b>moves</b> a city into
|
|
2035
243
|
<code class="mono">.backups/</code> \u2014 nothing is deleted, and you can bring it back with
|
|
2036
244
|
<code class="mono">mv</code>. The city you are standing in, and the last one you own,
|
|
2037
|
-
cannot be archived.</p
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
if (!nombre) {
|
|
2043
|
-
toast("Give it a name", true);
|
|
2044
|
-
return;
|
|
2045
|
-
}
|
|
2046
|
-
const r = await api("/api/ciudades", {
|
|
2047
|
-
method: "POST",
|
|
2048
|
-
body: JSON.stringify({ name: nombre })
|
|
2049
|
-
});
|
|
2050
|
-
if (!r.ok) {
|
|
2051
|
-
toast(r.error ?? "Could not create it", true);
|
|
2052
|
-
return;
|
|
2053
|
-
}
|
|
2054
|
-
toast(`${nombre} created`);
|
|
2055
|
-
await refresca();
|
|
2056
|
-
};
|
|
2057
|
-
const reiniciar = document.getElementById("reiniciaCiudad");
|
|
2058
|
-
if (reiniciar)
|
|
2059
|
-
reiniciar.onclick = async () => {
|
|
2060
|
-
const previa = await api("/api/ciudad-reinicia", {
|
|
2061
|
-
method: "POST",
|
|
2062
|
-
body: "{}"
|
|
2063
|
-
});
|
|
2064
|
-
if (!previa.preview) {
|
|
2065
|
-
toast(previa.error ?? "Could not read what a reset would do", true);
|
|
2066
|
-
return;
|
|
2067
|
-
}
|
|
2068
|
-
const p = previa.preview;
|
|
2069
|
-
const dicho = await pregunta({
|
|
2070
|
-
titulo: t("Start over {city}?", { city: p.city }),
|
|
2071
|
-
cuerpo: [
|
|
2072
|
-
t("You lose:") + " " + p.loses.join(", ") + ".",
|
|
2073
|
-
t("Right now that is {agents} house(s), {roads} road(s) and {acts} committee act(s).", {
|
|
2074
|
-
agents: p.agents,
|
|
2075
|
-
roads: p.roads,
|
|
2076
|
-
acts: p.deliberations
|
|
2077
|
-
}),
|
|
2078
|
-
t("You keep:") + " " + p.keeps.join(", ") + "."
|
|
2079
|
-
],
|
|
2080
|
-
campos: [{ id: "nombre", etiqueta: t("Type the city\u2019s name to confirm"), requerido: true }],
|
|
2081
|
-
exige: p.city,
|
|
2082
|
-
aceptar: t("Start over"),
|
|
2083
|
-
peligro: true
|
|
2084
|
-
});
|
|
2085
|
-
if (dicho === null) return;
|
|
2086
|
-
const r = await api(
|
|
2087
|
-
"/api/ciudad-reinicia",
|
|
2088
|
-
{ method: "POST", body: JSON.stringify({ confirm: p.city }) }
|
|
2089
|
-
);
|
|
2090
|
-
if (!r.ok) {
|
|
2091
|
-
toast(r.error ?? "Could not reset it", true);
|
|
2092
|
-
return;
|
|
2093
|
-
}
|
|
2094
|
-
toast(`${p.city} starts over \u2014 the old one is at ${r.backup ?? "its backup"}`);
|
|
2095
|
-
primeraCarga = true;
|
|
2096
|
-
await refresca();
|
|
2097
|
-
};
|
|
2098
|
-
todos(".archiva").forEach((boton) => {
|
|
2099
|
-
boton.onclick = async () => {
|
|
2100
|
-
const nombre = boton.dataset.nombre ?? "";
|
|
2101
|
-
const vale = await confirma(
|
|
2102
|
-
t("Archive {city}?", { city: nombre }),
|
|
2103
|
-
[t("It moves into .backups. Nothing is deleted, and you can put it back by hand.")],
|
|
2104
|
-
{ aceptar: t("Archive it") }
|
|
2105
|
-
);
|
|
2106
|
-
if (!vale) return;
|
|
2107
|
-
const r = await api(
|
|
2108
|
-
"/api/ciudad-archiva",
|
|
2109
|
-
{ method: "POST", body: JSON.stringify({ city: boton.dataset.ciudad ?? "" }) }
|
|
2110
|
-
);
|
|
2111
|
-
if (!r.ok) {
|
|
2112
|
-
toast(r.error ?? "Could not archive it", true);
|
|
2113
|
-
return;
|
|
2114
|
-
}
|
|
2115
|
-
toast(`${nombre} archived \u2014 it is in ${r.backup ?? ".backups"}`);
|
|
2116
|
-
await refresca();
|
|
2117
|
-
};
|
|
2118
|
-
});
|
|
2119
|
-
};
|
|
2120
|
-
var demos = null;
|
|
2121
|
-
VISTAS.demos = () => {
|
|
2122
|
-
demos ??= new Demos({ api, esc: esc2, pinta: renderActividad });
|
|
2123
|
-
q("#lienzo").innerHTML = '<div id="demoHueco"></div>';
|
|
2124
|
-
demos.monta(q("#demoHueco"));
|
|
2125
|
-
montada = demos;
|
|
2126
|
-
};
|
|
2127
|
-
VISTAS.resumen = () => {
|
|
2128
|
-
const mia = E.tarjetas.find((t2) => t2.user === E.yo);
|
|
2129
|
-
const sinUnidad = E.parcelas.filter((p) => p.unidad === "none" || p.unidad === "mine").length;
|
|
2130
|
-
const agentes = E.agents ?? [];
|
|
2131
|
-
const tareas = [
|
|
2132
|
-
{
|
|
2133
|
-
ok: !!mia,
|
|
2134
|
-
txt: mia ? `Your seat is taken \u2014 you are <b>${esc2(mia.agent)}</b> in <b>${esc2(E.domain)}</b>` : "Take your seat: the work domain and your role in it",
|
|
2135
|
-
ir: "puesto"
|
|
2136
|
-
},
|
|
2137
|
-
{
|
|
2138
|
-
ok: agentes.length > 0,
|
|
2139
|
-
txt: agentes.length === 0 ? "Build the <b>houses</b> of this city \u2014 one per worker, each whole: its kind, its role, and everything it works on" : `<b>${agentes.length}</b> house${agentes.length === 1 ? "" : "s"} in this city: ${esc2(
|
|
2140
|
-
agentes.map((a) => a.name).join(", ")
|
|
2141
|
-
)}`,
|
|
2142
|
-
ir: "gente"
|
|
2143
|
-
},
|
|
2144
|
-
{
|
|
2145
|
-
ok: agentes.length === 0 || agentes.some((a) => (a.mounts ?? []).length),
|
|
2146
|
-
txt: agentes.some((a) => (a.mounts ?? []).length) ? "Every house works on something real" : "Give a house something to work on \u2014 a repository, a worktree, a folder of documents",
|
|
2147
|
-
ir: "gente"
|
|
2148
|
-
},
|
|
2149
|
-
{
|
|
2150
|
-
ok: !!(mia && mia.goals_defined),
|
|
2151
|
-
txt: mia && mia.goals_defined ? `Your goal: <b>${esc2(mia.objetivo ? mia.objetivo.title : "")}</b>` : "Set <b>one goal</b>, with the command that measures it",
|
|
2152
|
-
ir: "puesto"
|
|
2153
|
-
},
|
|
2154
|
-
{
|
|
2155
|
-
ok: E.parcelas.length === 0 || sinUnidad === 0,
|
|
2156
|
-
txt: E.parcelas.length === 0 ? "Districts appear on the map once agents have houses" : sinUnidad ? `<b>${sinUnidad}</b> of ${E.parcelas.length} houses sit in no real district \u2014 assign them` : "Every house has its district",
|
|
2157
|
-
ir: "barrios"
|
|
2158
|
-
}
|
|
2159
|
-
];
|
|
2160
|
-
const empezando = !mia || agentes.length === 0;
|
|
2161
|
-
const sesionArriba = E.tmux.includes(E.sesion);
|
|
2162
|
-
q("#lienzo").innerHTML = `
|
|
2163
|
-
<div><span class="sub">${empezando ? "welcome" : "your city"}</span>
|
|
2164
|
-
<h1 style="margin-top:6px">${empezando ? "Let\u2019s build " + esc2(E.city_name) : esc2(E.city_name)}</h1>
|
|
2165
|
-
<p class="de mono" style="margin-top:5px">${esc2(E.address)}</p>
|
|
2166
|
-
${empezando ? `<p class="prosa" style="margin-top:8px">A city is a chair and the agents behind
|
|
245
|
+
cannot be archived.</p>`,re();let n=m("#nuevaCiudad");m("#creaCiudad").onclick=async()=>{let a=n.value.trim();if(!a){g("Give it a name",!0);return}let i=await b("/api/ciudades",{method:"POST",body:JSON.stringify({name:a})});if(!i.ok){g(i.error??"Could not create it",!0);return}g(`${a} created`),await k()};let t=document.getElementById("reiniciaCiudad");t&&(t.onclick=async()=>{let a=await b("/api/ciudad-reinicia",{method:"POST",body:"{}"});if(!a.preview){g(a.error??"Could not read what a reset would do",!0);return}let i=a.preview;if(await I({titulo:s("Start over {city}?",{city:i.city}),cuerpo:[s("You lose:")+" "+i.loses.join(", ")+".",s("Right now that is {agents} house(s), {roads} road(s) and {acts} committee act(s).",{agents:i.agents,roads:i.roads,acts:i.deliberations}),s("You keep:")+" "+i.keeps.join(", ")+"."],campos:[{id:"nombre",etiqueta:s("Type the city\u2019s name to confirm"),requerido:!0}],exige:i.city,aceptar:s("Start over"),peligro:!0})===null)return;let o=await b("/api/ciudad-reinicia",{method:"POST",body:JSON.stringify({confirm:i.city})});if(!o.ok){g(o.error??"Could not reset it",!0);return}g(`${i.city} starts over \u2014 the old one is at ${o.backup??"its backup"}`),be=!0,await k()}),f(".archiva").forEach(a=>{a.onclick=async()=>{let i=a.dataset.nombre??"";if(!await Z(s("Archive {city}?",{city:i}),[s("It moves into .backups. Nothing is deleted, and you can put it back by hand.")],{aceptar:s("Archive it")}))return;let o=await b("/api/ciudad-archiva",{method:"POST",body:JSON.stringify({city:a.dataset.ciudad??""})});if(!o.ok){g(o.error??"Could not archive it",!0);return}g(`${i} archived \u2014 it is in ${o.backup??".backups"}`),await k()}})};var he=null;T.demos=()=>{he??=new X({api:b,esc:l,pinta:Ie}),m("#lienzo").innerHTML='<div id="demoHueco"></div>',he.monta(m("#demoHueco")),ie=he};T.resumen=()=>{let e=d.tarjetas.find(r=>r.user===d.yo),n=d.parcelas.filter(r=>r.unidad==="none"||r.unidad==="mine").length,t=d.agents??[],a=[{ok:!!e,txt:e?`Your seat is taken \u2014 you are <b>${l(e.agent)}</b> in <b>${l(d.domain)}</b>`:"Take your seat: the work domain and your role in it",ir:"puesto"},{ok:t.length>0,txt:t.length===0?"Build the <b>houses</b> of this city \u2014 one per worker, each whole: its kind, its role, and everything it works on":`<b>${t.length}</b> house${t.length===1?"":"s"} in this city: ${l(t.map(r=>r.name).join(", "))}`,ir:"gente"},{ok:t.length===0||t.some(r=>(r.mounts??[]).length),txt:t.some(r=>(r.mounts??[]).length)?"Every house works on something real":"Give a house something to work on \u2014 a repository, a worktree, a folder of documents",ir:"gente"},{ok:!!(e&&e.goals_defined),txt:e&&e.goals_defined?`Your goal: <b>${l(e.objetivo?e.objetivo.title:"")}</b>`:"Set <b>one goal</b>, with the command that measures it",ir:"puesto"},{ok:d.parcelas.length===0||n===0,txt:d.parcelas.length===0?"Districts appear on the map once agents have houses":n?`<b>${n}</b> of ${d.parcelas.length} houses sit in no real district \u2014 assign them`:"Every house has its district",ir:"barrios"}],i=!e||t.length===0,c=d.tmux.includes(d.sesion);m("#lienzo").innerHTML=`
|
|
246
|
+
<div><span class="sub">${i?"welcome":"your city"}</span>
|
|
247
|
+
<h1 style="margin-top:6px">${i?"Let\u2019s build "+l(d.city_name):l(d.city_name)}</h1>
|
|
248
|
+
<p class="de mono" style="margin-top:5px">${l(d.address)}</p>
|
|
249
|
+
${i?`<p class="prosa" style="margin-top:8px">A city is a chair and the agents behind
|
|
2167
250
|
it. Two steps and it is alive: take your seat \u2014 the work domain and your role in
|
|
2168
251
|
it \u2014 then add the agents, each one asked for whole: what kind of work it does,
|
|
2169
252
|
its role, everything it works on, the engine that runs it and the skills it
|
|
2170
253
|
starts with. Everything below is a plain file in
|
|
2171
|
-
<code class="mono">${
|
|
2172
|
-
counted with <code class="mono">${
|
|
254
|
+
<code class="mono">${l(N(d.datos))}</code> you can also edit by hand.</p>`:`<p class="prosa" style="margin-top:8px">Domain: <b>${l(d.domain)}</b>. Growth is
|
|
255
|
+
counted with <code class="mono">${l(d.grow||"nothing yet")}</code>.</p>`}</div>
|
|
2173
256
|
<div class="cifras">
|
|
2174
|
-
<div class="cifra"><b>${Object.keys(
|
|
2175
|
-
<div class="cifra"><b>${
|
|
2176
|
-
<div class="cifra"><b>${
|
|
2177
|
-
<div class="cifra"><b>${
|
|
2178
|
-
<div class="cifra"><b>${Object.values(
|
|
257
|
+
<div class="cifra"><b>${Object.keys(d.skills).length}</b><span>${s("repo agents")}</span></div>
|
|
258
|
+
<div class="cifra"><b>${d.parcelas.length}</b><span>houses</span></div>
|
|
259
|
+
<div class="cifra"><b>${d.roads.length}</b><span>roads</span></div>
|
|
260
|
+
<div class="cifra"><b>${d.deliberations.length}</b><span>${s("committee acts")}</span></div>
|
|
261
|
+
<div class="cifra"><b>${Object.values(d.skills).reduce((r,p)=>r+p.skills.length,0)}</b><span>${s("skills recognised")}</span></div>
|
|
2179
262
|
</div>
|
|
2180
263
|
<div class="luces">
|
|
2181
|
-
<span class="luz ${
|
|
2182
|
-
<span class="luz ${
|
|
2183
|
-
<span class="luz ${
|
|
2184
|
-
<span class="luz ${
|
|
264
|
+
<span class="luz ${d.tarjetas.length?"on":""}">${s("data repo")}</span>
|
|
265
|
+
<span class="luz ${d.gh?"on":"neutra"}">github${d.gh?"":" \u2014 optional"}</span>
|
|
266
|
+
<span class="luz ${d.plugin?"on":""}">${s("plugin installed")}</span>
|
|
267
|
+
<span class="luz ${c?"on":"neutra"}">tmux session${c?" up":""}</span>
|
|
2185
268
|
</div>
|
|
2186
|
-
<div><span class="sub">${
|
|
2187
|
-
<div class="tareas" style="margin-top:9px">${
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
${t2.ok ? "" : `<button class="bt mini ir" data-ir="${t2.ir}">go</button>`}</div>`
|
|
2191
|
-
).join("")}
|
|
269
|
+
<div><span class="sub">${s("what is left")}</span>
|
|
270
|
+
<div class="tareas" style="margin-top:9px">${a.map(r=>`
|
|
271
|
+
<div class="tarea ${r.ok?"hecha":""}">${r.txt}
|
|
272
|
+
${r.ok?"":`<button class="bt mini ir" data-ir="${r.ir}">go</button>`}</div>`).join("")}
|
|
2192
273
|
</div></div>
|
|
2193
274
|
<div><span class="sub">work</span>
|
|
2194
275
|
<div style="display:flex;flex-direction:column;gap:9px;margin-top:9px">
|
|
2195
|
-
<div class="orden"><span class="et2">${
|
|
2196
|
-
<code>${
|
|
2197
|
-
${
|
|
276
|
+
<div class="orden"><span class="et2">${s("your day")}</span>
|
|
277
|
+
<code>${c?"tmux attach -t "+l(d.sesion):"one window per folder, an agent in each"}</code>
|
|
278
|
+
${c?`<button class="bt mini" data-copia="tmux attach -t ${l(d.sesion)}">copy</button>`:`<button class="bt mini ppal" id="abreSesion">${s("open my session")}</button>`}
|
|
2198
279
|
</div>
|
|
2199
280
|
<div class="orden"><span class="et2">the map</span>
|
|
2200
|
-
<code>${
|
|
2201
|
-
<button class="bt mini ${
|
|
2202
|
-
</div></div
|
|
2203
|
-
enlaza();
|
|
2204
|
-
const b = document.querySelector("#abreSesion");
|
|
2205
|
-
if (b)
|
|
2206
|
-
b.onclick = async () => {
|
|
2207
|
-
b.disabled = true;
|
|
2208
|
-
b.textContent = "building\u2026";
|
|
2209
|
-
const r = await api("/api/sesion", {
|
|
2210
|
-
method: "POST",
|
|
2211
|
-
body: JSON.stringify({ user: E.yo })
|
|
2212
|
-
});
|
|
2213
|
-
await espera(1500);
|
|
2214
|
-
await refresca();
|
|
2215
|
-
toast(
|
|
2216
|
-
r.ok && r.attach ? `Session built \u2014 attach with: ${r.attach}` : "Could not build it",
|
|
2217
|
-
!r.ok
|
|
2218
|
-
);
|
|
2219
|
-
};
|
|
2220
|
-
};
|
|
2221
|
-
VISTAS.mapa = () => {
|
|
2222
|
-
if (E.mapa) {
|
|
2223
|
-
q(".cuerpo").style.padding = "0";
|
|
2224
|
-
q("#lienzo").style.maxWidth = "none";
|
|
2225
|
-
const mapUrl = new URL(E.mapa, location.href);
|
|
2226
|
-
mapUrl.searchParams.set("embed", "1");
|
|
2227
|
-
mapUrl.searchParams.set("parent_origin", location.origin);
|
|
2228
|
-
q("#lienzo").innerHTML = `<iframe id="cityMapFrame" src="${esc2(mapUrl.toString())}" title="the map"
|
|
281
|
+
<code>${d.mapa?l(d.mapa):"not drawn yet"}</code>
|
|
282
|
+
<button class="bt mini ${d.mapa?"":"ppal"}" data-ir="mapa">${d.mapa?"open":"draw it"}</button></div>
|
|
283
|
+
</div></div>`,re();let o=document.querySelector("#abreSesion");o&&(o.onclick=async()=>{o.disabled=!0,o.textContent="building\u2026";let r=await b("/api/sesion",{method:"POST",body:JSON.stringify({user:d.yo})});await Re(1500),await k(),g(r.ok&&r.attach?`Session built \u2014 attach with: ${r.attach}`:"Could not build it",!r.ok)})};T.mapa=()=>{if(d.mapa){m(".cuerpo").style.padding="0",m("#lienzo").style.maxWidth="none";let n=new URL(d.mapa,location.href);n.searchParams.set("embed","1"),n.searchParams.set("parent_origin",location.origin),m("#lienzo").innerHTML=`<iframe id="cityMapFrame" src="${l(n.toString())}" title="the map"
|
|
2229
284
|
allow="fullscreen" allowfullscreen
|
|
2230
|
-
style="width:100%;height:calc(100vh - 2px);border:0;display:block"></iframe>`;
|
|
2231
|
-
const frame = q("#cityMapFrame");
|
|
2232
|
-
frame.onload = () => {
|
|
2233
|
-
frame.dataset.ready = "1";
|
|
2234
|
-
const origin = new URL(frame.src, location.href).origin;
|
|
2235
|
-
frame.contentWindow?.postMessage(
|
|
2236
|
-
{
|
|
2237
|
-
protocol: MAP_ACTIVITY_PROTOCOL,
|
|
2238
|
-
type: "map.config",
|
|
2239
|
-
roads: E.roads.map((r) => ({ name: r.name, address: r.address })),
|
|
2240
|
-
agents: (E.agents ?? []).map((a) => ({ name: a.name, kind: a.kind })),
|
|
2241
|
-
avatars: E.avatars ?? {}
|
|
2242
|
-
},
|
|
2243
|
-
origin
|
|
2244
|
-
);
|
|
2245
|
-
frame.contentWindow?.postMessage(
|
|
2246
|
-
{ type: "agents-city-map-theme/1", theme: temaActual() },
|
|
2247
|
-
origin
|
|
2248
|
-
);
|
|
2249
|
-
if (pendingMapActivity) {
|
|
2250
|
-
const event = pendingMapActivity;
|
|
2251
|
-
pendingMapActivity = null;
|
|
2252
|
-
enviaActividadAlMapa(event);
|
|
2253
|
-
}
|
|
2254
|
-
};
|
|
2255
|
-
return;
|
|
2256
|
-
}
|
|
2257
|
-
const cuantos = (E.agents ?? []).length;
|
|
2258
|
-
q("#lienzo").innerHTML = `<div><span class="sub">the map</span>
|
|
285
|
+
style="width:100%;height:calc(100vh - 2px);border:0;display:block"></iframe>`;let t=m("#cityMapFrame");t.onload=()=>{t.dataset.ready="1";let a=new URL(t.src,location.href).origin;if(t.contentWindow?.postMessage({protocol:te,type:"map.config",roads:d.roads.map(i=>({name:i.name,address:i.address})),agents:(d.agents??[]).map(i=>({name:i.name,kind:i.kind})),avatars:d.avatars??{}},a),t.contentWindow?.postMessage({type:"agents-city-map-theme/1",theme:lt()},a),se){let i=se;se=null,Pe(i)}};return}let e=(d.agents??[]).length;m("#lienzo").innerHTML=`<div><span class="sub">the map</span>
|
|
2259
286
|
<h1 style="margin-top:6px">Your city, drawn</h1>
|
|
2260
|
-
<p class="prosa" style="margin-top:8px">${
|
|
287
|
+
<p class="prosa" style="margin-top:8px">${e?`<b>${e}</b> agent${e===1?"":"s"} live here, and the map shows
|
|
2261
288
|
them as houses that grow with the work they do \u2014 with the town hall in the middle
|
|
2262
|
-
when a committee is sitting, and a gate for every road out
|
|
289
|
+
when a committee is sitting, and a gate for every road out.`:`The map draws one house per agent. There are none yet, so it would be an empty
|
|
2263
290
|
plot \u2014 build one from <b>Houses</b> first.`}</p>
|
|
2264
291
|
<p class="prosa">The map is its own little server, so it is not running until you say so.
|
|
2265
292
|
The first time takes about a minute: it builds the front end and seeds a local database.
|
|
2266
293
|
After that it opens instantly, and it is yours \u2014 nothing is uploaded anywhere.</p></div>
|
|
2267
|
-
<div><button class="bt ppal" id="arrancaMapa">${
|
|
2268
|
-
<p class="cargando" id="mapaEspera" style="display:none">baking the map \u2014 first time takes a minute</p
|
|
2269
|
-
|
|
2270
|
-
q("#arrancaMapa").disabled = true;
|
|
2271
|
-
q("#mapaEspera").style.display = "";
|
|
2272
|
-
await api("/api/mapa", { method: "POST", body: "{}" });
|
|
2273
|
-
for (let i = 0; i < 60; i++) {
|
|
2274
|
-
await espera(3e3);
|
|
2275
|
-
E = await api("/api/estado");
|
|
2276
|
-
if (E.mapa) {
|
|
2277
|
-
pinta();
|
|
2278
|
-
return;
|
|
2279
|
-
}
|
|
2280
|
-
}
|
|
2281
|
-
toast("It did not come up \u2014 run ./bin/city by hand and look at its output", true);
|
|
2282
|
-
pinta();
|
|
2283
|
-
};
|
|
2284
|
-
};
|
|
2285
|
-
VISTAS.puesto = () => {
|
|
2286
|
-
void puesto();
|
|
2287
|
-
};
|
|
2288
|
-
async function puesto() {
|
|
2289
|
-
const mia = E.tarjetas.find((t2) => t2.user === E.yo);
|
|
2290
|
-
q("#lienzo").innerHTML = `<div><span class="sub">${t("my seat")}</span>
|
|
2291
|
-
<h1 style="margin-top:6px">${esc2(E.yo)}</h1>
|
|
294
|
+
<div><button class="bt ppal" id="arrancaMapa">${e?"Draw my city":"Draw it anyway"}</button></div>
|
|
295
|
+
<p class="cargando" id="mapaEspera" style="display:none">baking the map \u2014 first time takes a minute</p>`,m("#arrancaMapa").onclick=async()=>{m("#arrancaMapa").disabled=!0,m("#mapaEspera").style.display="",await b("/api/mapa",{method:"POST",body:"{}"});for(let n=0;n<60;n++)if(await Re(3e3),d=await b("/api/estado"),d.mapa){C();return}g("It did not come up \u2014 run ./bin/city by hand and look at its output",!0),C()}};T.puesto=()=>{tt()};async function tt(){let e=d.tarjetas.find(h=>h.user===d.yo);m("#lienzo").innerHTML=`<div><span class="sub">${s("my seat")}</span>
|
|
296
|
+
<h1 style="margin-top:6px">${l(d.yo)}</h1>
|
|
2292
297
|
<p class="prosa" style="margin-top:8px">This is your chair: the work domain, your
|
|
2293
298
|
role inside it, and one goal. The seat stays the boss even when its
|
|
2294
299
|
professional role is blank. Saving writes your card \u2014
|
|
2295
|
-
<code class="mono">${
|
|
300
|
+
<code class="mono">${l(N(d.datos))}/${l(d.yo)}.md</code> \u2014 the same file
|
|
2296
301
|
every other door writes, and it never touches your roster.</p></div>
|
|
2297
|
-
<div><span class="sub">${
|
|
2298
|
-
<p class="cargando">${
|
|
302
|
+
<div><span class="sub">${s("work domain")}</span><div class="rolejilla" id="domains" style="margin-top:9px">
|
|
303
|
+
<p class="cargando">${s("reading the domain packs")}</p></div></div>
|
|
2299
304
|
<div><span class="sub">role</span><div class="rolejilla" id="roles" style="margin-top:9px">
|
|
2300
|
-
<p class="cargando">${
|
|
2301
|
-
<div><span class="sub">${
|
|
305
|
+
<p class="cargando">${s("reading the role files")}</p></div></div>
|
|
306
|
+
<div><span class="sub">${s("the agents")}</span>
|
|
2302
307
|
<p class="prosa" style="margin-top:8px">Who works in this city \u2014 and what each
|
|
2303
|
-
one works on \u2014 lives in <b>${
|
|
308
|
+
one works on \u2014 lives in <b>${s("Agents & skills")}</b>, where an agent is asked for
|
|
2304
309
|
whole: its kind, its role, its repositories and document folders, its engine
|
|
2305
310
|
and its skills. One place, so the terminal and this page cannot disagree.
|
|
2306
|
-
<button class="bt" type="button" data-ir="gente" style="margin-left:8px">${
|
|
2307
|
-
<div><span class="sub">${
|
|
311
|
+
<button class="bt" type="button" data-ir="gente" style="margin-left:8px">${s("Open the roster")}</button></p></div>
|
|
312
|
+
<div><span class="sub">${s("one goal \u2014 optional")}</span><div class="campos" id="meta" style="margin-top:9px"></div></div>
|
|
2308
313
|
<div style="display:flex;gap:10px;align-items:center">
|
|
2309
|
-
<button class="bt ppal" id="guardaPuesto">${
|
|
2310
|
-
<span class="cargando" id="puestoEstado" style="display:none">writing</span></div>`;
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
<
|
|
2319
|
-
<h3>${esc2(r.name)}</h3><span class="de">${esc2(r.id)} \xB7 ${esc2(r.trade.toLowerCase())}</span>
|
|
2320
|
-
<p>${esc2(r.summary)}</p></button>`
|
|
2321
|
-
).join("");
|
|
2322
|
-
todos("#roles .rol").forEach((b) => {
|
|
2323
|
-
b.onclick = () => {
|
|
2324
|
-
rol = b.dataset.r ?? "";
|
|
2325
|
-
pintaRoles();
|
|
2326
|
-
};
|
|
2327
|
-
});
|
|
2328
|
-
};
|
|
2329
|
-
const cargaRoles = async () => {
|
|
2330
|
-
rolesR = (await api("/api/roles?domain=" + encodeURIComponent(domain))).roles;
|
|
2331
|
-
if (!rolesR.some((r) => r.id === rol)) rol = "";
|
|
2332
|
-
pintaRoles();
|
|
2333
|
-
};
|
|
2334
|
-
const pintaDominios = () => {
|
|
2335
|
-
q("#domains").innerHTML = domainsR.map(
|
|
2336
|
-
(d) => `
|
|
2337
|
-
<button class="rol ${d.id === domain ? "on" : ""}" data-d="${esc2(d.id)}">
|
|
2338
|
-
<h3>${esc2(d.name)}</h3><span class="de">${esc2(d.id)}</span>
|
|
2339
|
-
<p>${esc2(d.summary)}</p></button>`
|
|
2340
|
-
).join("");
|
|
2341
|
-
todos("#domains .rol").forEach((b) => {
|
|
2342
|
-
b.onclick = () => {
|
|
2343
|
-
domain = b.dataset.d ?? "software";
|
|
2344
|
-
pintaDominios();
|
|
2345
|
-
void cargaRoles();
|
|
2346
|
-
};
|
|
2347
|
-
});
|
|
2348
|
-
};
|
|
2349
|
-
pintaDominios();
|
|
2350
|
-
await cargaRoles();
|
|
2351
|
-
const o = mia && mia.objetivo || {};
|
|
2352
|
-
q("#meta").innerHTML = `
|
|
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")}">
|
|
314
|
+
<button class="bt ppal" id="guardaPuesto">${s("Save my seat")}</button>
|
|
315
|
+
<span class="cargando" id="puestoEstado" style="display:none">writing</span></div>`;let n=(await b("/api/domains")).domains,t=d.domain||d.kind||"software",a=e?e.role:"",i=[],c=()=>{m("#roles").innerHTML=i.map(h=>`
|
|
316
|
+
<button class="rol ${h.id===a?"on":""}" data-r="${l(h.id)}">
|
|
317
|
+
<h3>${l(h.name)}</h3><span class="de">${l(h.id)} \xB7 ${l(h.trade.toLowerCase())}</span>
|
|
318
|
+
<p>${l(h.summary)}</p></button>`).join(""),f("#roles .rol").forEach(h=>{h.onclick=()=>{a=h.dataset.r??"",c()}})},o=async()=>{i=(await b("/api/roles?domain="+encodeURIComponent(t))).roles,i.some(h=>h.id===a)||(a=""),c()},r=()=>{m("#domains").innerHTML=n.map(h=>`
|
|
319
|
+
<button class="rol ${h.id===t?"on":""}" data-d="${l(h.id)}">
|
|
320
|
+
<h3>${l(h.name)}</h3><span class="de">${l(h.id)}</span>
|
|
321
|
+
<p>${l(h.summary)}</p></button>`).join(""),f("#domains .rol").forEach(h=>{h.onclick=()=>{t=h.dataset.d??"software",r(),o()}})};r(),await o();let p=e&&e.objetivo||{};m("#meta").innerHTML=`
|
|
322
|
+
<div class="campo"><label>${s("The goal, in one line")}</label>
|
|
323
|
+
<input type="text" id="g_title" value="${l(p.title)}" placeholder="${s("Concrete enough to argue with \u2014 empty skips it")}">
|
|
2355
324
|
</div>
|
|
2356
325
|
<div class="dos">
|
|
2357
|
-
<div class="campo"><label>${
|
|
2358
|
-
<input type="text" id="g_signal" value="${
|
|
2359
|
-
<div class="campo"><label>${
|
|
2360
|
-
<input type="text" class="mono" id="g_command" value="${
|
|
2361
|
-
placeholder="${
|
|
326
|
+
<div class="campo"><label>${s("How it is measured")}</label>
|
|
327
|
+
<input type="text" id="g_signal" value="${l(p.signal)}"></div>
|
|
328
|
+
<div class="campo"><label>${s("The command that returns it, if a command can")}</label>
|
|
329
|
+
<input type="text" class="mono" id="g_command" value="${l(p.command)}"
|
|
330
|
+
placeholder="${s("empty for a qualitative goal")}"></div>
|
|
2362
331
|
</div>
|
|
2363
|
-
<div class="campo"><label>${
|
|
2364
|
-
<input type="text" id="g_manual" value="${
|
|
2365
|
-
placeholder="${
|
|
332
|
+
<div class="campo"><label>${s("\u2026or who judges it, and how often")}</label>
|
|
333
|
+
<input type="text" id="g_manual" value="${l(p.manual)}"
|
|
334
|
+
placeholder="${s("in prose: the architect reads the AGENTS.md files on Fridays")}">
|
|
2366
335
|
<p class="pista">A goal judged by a person is a real goal \u2014 plenty of quality
|
|
2367
336
|
is prose, not a number. Used when the command is empty.</p></div>
|
|
2368
337
|
<div class="dos">
|
|
2369
|
-
<div class="campo"><label>${
|
|
2370
|
-
<input type="text" class="mono" id="g_baseline" value="${
|
|
2371
|
-
<div class="campo"><label>${
|
|
2372
|
-
<input type="text" class="mono" id="g_target" value="${
|
|
338
|
+
<div class="campo"><label>${s("What it returns today")}</label>
|
|
339
|
+
<input type="text" class="mono" id="g_baseline" value="${l(p.baseline)}"></div>
|
|
340
|
+
<div class="campo"><label>${s("Where it has to get to")}</label>
|
|
341
|
+
<input type="text" class="mono" id="g_target" value="${l(p.target)}"></div>
|
|
2373
342
|
</div>
|
|
2374
|
-
<div class="campo" style="max-width:240px"><label>${
|
|
2375
|
-
<input type="text" id="g_by" value="${
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
if (!rol) {
|
|
2379
|
-
toast("Pick a role \u2014 it is the name your seat answers to", true);
|
|
2380
|
-
return;
|
|
2381
|
-
}
|
|
2382
|
-
q("#puestoEstado").style.display = "";
|
|
2383
|
-
const r = await api("/api/ficha", {
|
|
2384
|
-
method: "POST",
|
|
2385
|
-
body: JSON.stringify({
|
|
2386
|
-
user: E.yo,
|
|
2387
|
-
domain,
|
|
2388
|
-
role: rol,
|
|
2389
|
-
objetivo: {
|
|
2390
|
-
title: val("#g_title"),
|
|
2391
|
-
signal: val("#g_signal"),
|
|
2392
|
-
command: val("#g_command"),
|
|
2393
|
-
manual: val("#g_manual"),
|
|
2394
|
-
baseline: val("#g_baseline"),
|
|
2395
|
-
target: val("#g_target"),
|
|
2396
|
-
by: val("#g_by") || "this quarter"
|
|
2397
|
-
}
|
|
2398
|
-
})
|
|
2399
|
-
});
|
|
2400
|
-
await refresca();
|
|
2401
|
-
toast(
|
|
2402
|
-
r.ok ? `Card written \u2014 you are ${r.user ?? E.yo}` : r.error ?? "It could not write",
|
|
2403
|
-
!r.ok
|
|
2404
|
-
);
|
|
2405
|
-
};
|
|
2406
|
-
}
|
|
2407
|
-
VISTAS.barrios = () => {
|
|
2408
|
-
const unidades = E.unidades.map((u) => ({ ...u }));
|
|
2409
|
-
const lab = new Set(E.lab);
|
|
2410
|
-
const porRepo = {};
|
|
2411
|
-
for (const p of E.parcelas) {
|
|
2412
|
-
(porRepo[p.repo] = porRepo[p.repo] ?? []).push({
|
|
2413
|
-
ruta: p.ruta,
|
|
2414
|
-
unidad: p.unidad,
|
|
2415
|
-
nombre: p.nombre
|
|
2416
|
-
});
|
|
2417
|
-
}
|
|
2418
|
-
const slug = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 20);
|
|
2419
|
-
const render = () => {
|
|
2420
|
-
const idsValidos = /* @__PURE__ */ new Set([...unidades.map((u) => u.id), "none"]);
|
|
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
|
|
343
|
+
<div class="campo" style="max-width:240px"><label>${s("By when")}</label>
|
|
344
|
+
<input type="text" id="g_by" value="${l(p.by??"this quarter")}"></div>`;let u=h=>m(h).value.trim();m("#guardaPuesto").onclick=async()=>{if(!a){g("Pick a role \u2014 it is the name your seat answers to",!0);return}m("#puestoEstado").style.display="";let h=await b("/api/ficha",{method:"POST",body:JSON.stringify({user:d.yo,domain:t,role:a,objetivo:{title:u("#g_title"),signal:u("#g_signal"),command:u("#g_command"),manual:u("#g_manual"),baseline:u("#g_baseline"),target:u("#g_target"),by:u("#g_by")||"this quarter"}})});await k(),g(h.ok?`Card written \u2014 you are ${h.user??d.yo}`:h.error??"It could not write",!h.ok)}}T.barrios=()=>{let e=d.unidades.map(c=>({...c})),n=new Set(d.lab),t={};for(let c of d.parcelas)(t[c.repo]=t[c.repo]??[]).push({ruta:c.ruta,unidad:c.unidad,nombre:c.nombre});let a=c=>c.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,"").slice(0,20),i=()=>{let c=new Set([...e.map(o=>o.id),"none"]);m("#lienzo").innerHTML=`<div><span class="sub">${s("districts & houses")}</span>
|
|
345
|
+
<h1 style="margin-top:6px">${s("The modelling no tool can do for you")}</h1>
|
|
346
|
+
<p class="prosa" style="margin-top:8px">${s(`A house is <b>not a repo</b> \u2014 it is a
|
|
2424
347
|
parcel, a slice of one serving a single business unit. Split the interesting
|
|
2425
348
|
repos, give every house its district, and the map can say
|
|
2426
349
|
\u201Cthis change touches banking\u201D instead of \u201Cthis change touches src/lib\u201D.`)}</p></div>
|
|
@@ -2428,987 +351,208 @@ VISTAS.barrios = () => {
|
|
|
2428
351
|
<div><span class="sub">districts</span>
|
|
2429
352
|
<div class="tabla" id="unidades" style="margin-top:9px"></div>
|
|
2430
353
|
<div style="display:flex;gap:9px;margin-top:9px">
|
|
2431
|
-
<input type="text" id="nuevaU" placeholder="${
|
|
354
|
+
<input type="text" id="nuevaU" placeholder="${s("another district\u2026")}" style="flex:1">
|
|
2432
355
|
<button class="bt" id="masU">Add</button></div></div>
|
|
2433
356
|
|
|
2434
357
|
<div><span class="sub">houses</span><div id="parcelas" style="margin-top:9px"></div></div>
|
|
2435
358
|
|
|
2436
359
|
<div style="display:flex;gap:10px;align-items:center">
|
|
2437
|
-
<button class="bt ppal" id="guardaBarrios">${
|
|
360
|
+
<button class="bt ppal" id="guardaBarrios">${s("Save districts & houses")}</button>
|
|
2438
361
|
<span style="font-size:12px;color:var(--tinta3);font-style:italic">
|
|
2439
|
-
writes units.yml and parcels.yml \u2014 reseed the map to see it drawn</span></div
|
|
2440
|
-
q("#unidades").innerHTML = unidades.map(
|
|
2441
|
-
(u, i) => `
|
|
362
|
+
writes units.yml and parcels.yml \u2014 reseed the map to see it drawn</span></div>`,m("#unidades").innerHTML=e.map((o,r)=>`
|
|
2442
363
|
<div class="tr">
|
|
2443
|
-
<input type="text" value="${
|
|
2444
|
-
<span class="id">${
|
|
2445
|
-
<span class="pips" data-i="${
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
<
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
<div class="repoCab"><span>${esc2(repo)}</span>
|
|
2456
|
-
<button class="bt mini" data-split="${esc2(repo)}">split</button>
|
|
2457
|
-
<label class="lab"><input type="checkbox" data-lab="${esc2(repo)}"
|
|
2458
|
-
${lab.has(repo) ? "checked" : ""}> lab</label></div>
|
|
2459
|
-
${(porRepo[repo] ?? []).map(
|
|
2460
|
-
(f, j) => `
|
|
364
|
+
<input type="text" value="${l(o.name)}" data-i="${r}" style="flex:1">
|
|
365
|
+
<span class="id">${l(o.id)}</span>
|
|
366
|
+
<span class="pips" data-i="${r}">${d.paleta.map(p=>`
|
|
367
|
+
<button class="pip ${p.hex===o.color?"on":""}" data-c="${p.hex}"
|
|
368
|
+
style="background:#${p.hex}" title="${l(p.nombre)}"></button>`).join("")}</span>
|
|
369
|
+
<button class="x" data-x="${r}">\xD7</button></div>`).join("")||'<div class="tr" style="color:var(--tinta3);font-size:12.5px">No districts yet \u2014 everything sits in \u201Cno unit\u201D.</div>',m("#parcelas").innerHTML=Object.keys(t).sort().map(o=>`
|
|
370
|
+
<div class="repoBloque" data-repo="${l(o)}">
|
|
371
|
+
<div class="repoCab"><span>${l(o)}</span>
|
|
372
|
+
<button class="bt mini" data-split="${l(o)}">split</button>
|
|
373
|
+
<label class="lab"><input type="checkbox" data-lab="${l(o)}"
|
|
374
|
+
${n.has(o)?"checked":""}> lab</label></div>
|
|
375
|
+
${(t[o]??[]).map((r,p)=>`
|
|
2461
376
|
<div class="parcela">
|
|
2462
|
-
<input type="text" value="${
|
|
2463
|
-
<input type="text" class="mono" value="${
|
|
377
|
+
<input type="text" value="${l(r.nombre)}" data-k="nombre" data-r="${l(o)}" data-j="${p}" title="name on the map">
|
|
378
|
+
<input type="text" class="mono" value="${l(r.ruta)}" data-k="ruta" data-r="${l(o)}" data-j="${p}"
|
|
2464
379
|
placeholder="glob inside the repo \u2014 empty = the whole repo">
|
|
2465
|
-
<select data-k="unidad" data-r="${
|
|
2466
|
-
${[...
|
|
2467
|
-
|
|
2468
|
-
<option value="${esc2(id)}" ${(idsValidos.has(f.unidad) ? f.unidad : "none") === id ? "selected" : ""}>${esc2(id)}</option>`
|
|
2469
|
-
).join("")}
|
|
380
|
+
<select data-k="unidad" data-r="${l(o)}" data-j="${p}">
|
|
381
|
+
${[...e.map(u=>u.id),"none"].map(u=>`
|
|
382
|
+
<option value="${l(u)}" ${(c.has(r.unidad)?r.unidad:"none")===u?"selected":""}>${l(u)}</option>`).join("")}
|
|
2470
383
|
</select>
|
|
2471
|
-
${(
|
|
2472
|
-
</div>`
|
|
2473
|
-
).join("")}
|
|
2474
|
-
|
|
2475
|
-
).join("") || '<p class="prosa">No houses yet. Pick your folders in <b>My seat</b> and each becomes one.</p>';
|
|
2476
|
-
todos("#unidades input[data-i]").forEach((inp) => {
|
|
2477
|
-
inp.oninput = () => {
|
|
2478
|
-
const u = unidades[Number(inp.dataset.i)];
|
|
2479
|
-
if (!u) return;
|
|
2480
|
-
u.name = inp.value;
|
|
2481
|
-
u.id = slug(inp.value) || u.id;
|
|
2482
|
-
const idEl = inp.closest(".tr")?.querySelector(".id");
|
|
2483
|
-
if (idEl) idEl.textContent = u.id;
|
|
2484
|
-
};
|
|
2485
|
-
});
|
|
2486
|
-
todos("#unidades .pip").forEach((b) => {
|
|
2487
|
-
b.onclick = () => {
|
|
2488
|
-
const grupo = b.closest(".pips");
|
|
2489
|
-
const u = unidades[Number(grupo?.dataset.i)];
|
|
2490
|
-
if (u) {
|
|
2491
|
-
u.color = b.dataset.c ?? u.color;
|
|
2492
|
-
render();
|
|
2493
|
-
}
|
|
2494
|
-
};
|
|
2495
|
-
});
|
|
2496
|
-
todos("#unidades .x[data-x]").forEach((b) => {
|
|
2497
|
-
b.onclick = () => {
|
|
2498
|
-
unidades.splice(Number(b.dataset.x), 1);
|
|
2499
|
-
render();
|
|
2500
|
-
};
|
|
2501
|
-
});
|
|
2502
|
-
q("#masU").onclick = () => {
|
|
2503
|
-
const n = q("#nuevaU").value.trim();
|
|
2504
|
-
if (!n) return;
|
|
2505
|
-
const usados = unidades.map((u) => u.color);
|
|
2506
|
-
const libre = E.paleta.find((p) => !usados.includes(p.hex)) ?? E.paleta[0];
|
|
2507
|
-
unidades.push({ id: slug(n), name: n, color: libre ? libre.hex : "c8b48a" });
|
|
2508
|
-
render();
|
|
2509
|
-
};
|
|
2510
|
-
q("#nuevaU").onkeydown = (e) => {
|
|
2511
|
-
if (e.key === "Enter") q("#masU").click();
|
|
2512
|
-
};
|
|
2513
|
-
todos("#parcelas [data-k]").forEach((el) => {
|
|
2514
|
-
el.onchange = () => {
|
|
2515
|
-
const fila = porRepo[el.dataset.r ?? ""]?.[Number(el.dataset.j)];
|
|
2516
|
-
if (!fila) return;
|
|
2517
|
-
const k = el.dataset.k;
|
|
2518
|
-
fila[k] = el.value;
|
|
2519
|
-
};
|
|
2520
|
-
});
|
|
2521
|
-
todos("#parcelas [data-split]").forEach((b) => {
|
|
2522
|
-
b.onclick = () => {
|
|
2523
|
-
const r = b.dataset.split ?? "";
|
|
2524
|
-
(porRepo[r] = porRepo[r] ?? []).push({
|
|
2525
|
-
ruta: "",
|
|
2526
|
-
unidad: "none",
|
|
2527
|
-
nombre: `${r} \xB7 new slice`
|
|
2528
|
-
});
|
|
2529
|
-
render();
|
|
2530
|
-
};
|
|
2531
|
-
});
|
|
2532
|
-
todos("#parcelas [data-quita]").forEach((b) => {
|
|
2533
|
-
b.onclick = () => {
|
|
2534
|
-
porRepo[b.dataset.quita ?? ""]?.splice(Number(b.dataset.j), 1);
|
|
2535
|
-
render();
|
|
2536
|
-
};
|
|
2537
|
-
});
|
|
2538
|
-
todos("#parcelas [data-lab]").forEach((c) => {
|
|
2539
|
-
c.onchange = () => {
|
|
2540
|
-
const r = c.dataset.lab ?? "";
|
|
2541
|
-
if (c.checked) lab.add(r);
|
|
2542
|
-
else lab.delete(r);
|
|
2543
|
-
};
|
|
2544
|
-
});
|
|
2545
|
-
q("#guardaBarrios").onclick = async () => {
|
|
2546
|
-
const r1 = await api("/api/unidades", {
|
|
2547
|
-
method: "POST",
|
|
2548
|
-
body: JSON.stringify({ unidades })
|
|
2549
|
-
});
|
|
2550
|
-
const r2 = await api("/api/parcelas", {
|
|
2551
|
-
method: "POST",
|
|
2552
|
-
body: JSON.stringify({ repos: porRepo, lab: [...lab] })
|
|
2553
|
-
});
|
|
2554
|
-
await refresca();
|
|
2555
|
-
const mal = r1.error ?? r2.error;
|
|
2556
|
-
toast(mal ?? "Districts and houses written", !!mal);
|
|
2557
|
-
};
|
|
2558
|
-
};
|
|
2559
|
-
render();
|
|
2560
|
-
};
|
|
2561
|
-
VISTAS.red = () => {
|
|
2562
|
-
const conectadas = new Set(E.roads.map((r) => r.id));
|
|
2563
|
-
const locales = E.ciudades.filter((c) => !c.actual && c.id && !conectadas.has(c.id));
|
|
2564
|
-
const invitation = JSON.stringify(E.invitation);
|
|
2565
|
-
q("#lienzo").innerHTML = `<div><span class="sub">roads</span>
|
|
2566
|
-
<h1 style="margin-top:6px">${t("Cities this one may reach")}</h1>
|
|
384
|
+
${(t[o]??[]).length>1?`<button class="x" data-quita="${l(o)}" data-j="${p}">\xD7</button>`:"<span></span>"}
|
|
385
|
+
</div>`).join("")}
|
|
386
|
+
</div>`).join("")||'<p class="prosa">No houses yet. Pick your folders in <b>My seat</b> and each becomes one.</p>',f("#unidades input[data-i]").forEach(o=>{o.oninput=()=>{let r=e[Number(o.dataset.i)];if(!r)return;r.name=o.value,r.id=a(o.value)||r.id;let p=o.closest(".tr")?.querySelector(".id");p&&(p.textContent=r.id)}}),f("#unidades .pip").forEach(o=>{o.onclick=()=>{let r=o.closest(".pips"),p=e[Number(r?.dataset.i)];p&&(p.color=o.dataset.c??p.color,i())}}),f("#unidades .x[data-x]").forEach(o=>{o.onclick=()=>{e.splice(Number(o.dataset.x),1),i()}}),m("#masU").onclick=()=>{let o=m("#nuevaU").value.trim();if(!o)return;let r=e.map(u=>u.color),p=d.paleta.find(u=>!r.includes(u.hex))??d.paleta[0];e.push({id:a(o),name:o,color:p?p.hex:"c8b48a"}),i()},m("#nuevaU").onkeydown=o=>{o.key==="Enter"&&m("#masU").click()},f("#parcelas [data-k]").forEach(o=>{o.onchange=()=>{let r=t[o.dataset.r??""]?.[Number(o.dataset.j)];if(!r)return;let p=o.dataset.k;r[p]=o.value}}),f("#parcelas [data-split]").forEach(o=>{o.onclick=()=>{let r=o.dataset.split??"";(t[r]=t[r]??[]).push({ruta:"",unidad:"none",nombre:`${r} \xB7 new slice`}),i()}}),f("#parcelas [data-quita]").forEach(o=>{o.onclick=()=>{t[o.dataset.quita??""]?.splice(Number(o.dataset.j),1),i()}}),f("#parcelas [data-lab]").forEach(o=>{o.onchange=()=>{let r=o.dataset.lab??"";o.checked?n.add(r):n.delete(r)}}),m("#guardaBarrios").onclick=async()=>{let o=await b("/api/unidades",{method:"POST",body:JSON.stringify({unidades:e})}),r=await b("/api/parcelas",{method:"POST",body:JSON.stringify({repos:t,lab:[...n]})});await k();let p=o.error??r.error;g(p??"Districts and houses written",!!p)}};i()};T.red=()=>{let e=new Set(d.roads.map(a=>a.id)),n=d.ciudades.filter(a=>!a.actual&&a.id&&!e.has(a.id)),t=JSON.stringify(d.invitation);m("#lienzo").innerHTML=`<div><span class="sub">roads</span>
|
|
387
|
+
<h1 style="margin-top:6px">${s("Cities this one may reach")}</h1>
|
|
2567
388
|
<p class="prosa" style="margin-top:8px">A road joins city seats. It may stay on
|
|
2568
389
|
this machine or continue over the remote bus; the city sees one explicit
|
|
2569
390
|
connection either way.</p></div>
|
|
2570
|
-
<div class="gente">${
|
|
2571
|
-
|
|
2572
|
-
<span class="
|
|
2573
|
-
<
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
<
|
|
2583
|
-
|
|
2584
|
-
<button class="bt mini" data-copia="${esc2(invitation)}">copy</button>
|
|
2585
|
-
</div>`;
|
|
2586
|
-
enlaza();
|
|
2587
|
-
todos("[data-road-open]").forEach((b) => {
|
|
2588
|
-
b.onclick = async () => {
|
|
2589
|
-
const r = await api("/api/roads", {
|
|
2590
|
-
method: "POST",
|
|
2591
|
-
body: JSON.stringify({ action: "connect", target: b.dataset.roadOpen })
|
|
2592
|
-
});
|
|
2593
|
-
if (r.error) return toast(r.error, true);
|
|
2594
|
-
await refresca();
|
|
2595
|
-
toast("Road open at both local cities");
|
|
2596
|
-
};
|
|
2597
|
-
});
|
|
2598
|
-
todos("[data-road-close]").forEach((b) => {
|
|
2599
|
-
b.onclick = async () => {
|
|
2600
|
-
const r = await api("/api/roads", {
|
|
2601
|
-
method: "POST",
|
|
2602
|
-
body: JSON.stringify({ action: "disconnect", target: b.dataset.roadClose })
|
|
2603
|
-
});
|
|
2604
|
-
if (r.error) return toast(r.error, true);
|
|
2605
|
-
await refresca();
|
|
2606
|
-
toast("Road closed");
|
|
2607
|
-
};
|
|
2608
|
-
});
|
|
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>
|
|
391
|
+
<div class="gente">${d.roads.map(a=>`<div class="persona"><h3>${l(a.name)}</h3>
|
|
392
|
+
<span class="ag">${l(a.address)}</span>
|
|
393
|
+
<span class="rp">${a.local?"local road":"remote road"}</span>
|
|
394
|
+
<button class="bt mini" data-road-close="${l(a.id)}">close</button></div>`).join("")||`<p class="prosa">${s("No roads yet. This city is isolated on purpose.")}</p>`}</div>
|
|
395
|
+
<div><span class="sub">${s("other cities on this machine")}</span>
|
|
396
|
+
<div class="gente" style="margin-top:9px">${n.map(a=>`<div class="persona"><h3>${l(a.nombre)}</h3>
|
|
397
|
+
<span class="ag">${l(a.slug??a.ruta)}</span>
|
|
398
|
+
<button class="bt mini ppal" data-road-open="${l(a.id)}">open road</button></div>`).join("")||`<p class="prosa">${s("No unconnected local cities.")}</p>`}</div></div>
|
|
399
|
+
<div class="orden"><span class="et2">${s("remote invitation \xB7 public, no token")}</span>
|
|
400
|
+
<code>${l(t)}</code>
|
|
401
|
+
<button class="bt mini" data-copia="${l(t)}">copy</button>
|
|
402
|
+
</div>`,re(),f("[data-road-open]").forEach(a=>{a.onclick=async()=>{let i=await b("/api/roads",{method:"POST",body:JSON.stringify({action:"connect",target:a.dataset.roadOpen})});if(i.error)return g(i.error,!0);await k(),g("Road open at both local cities")}}),f("[data-road-close]").forEach(a=>{a.onclick=async()=>{let i=await b("/api/roads",{method:"POST",body:JSON.stringify({action:"disconnect",target:a.dataset.roadClose})});if(i.error)return g(i.error,!0);await k(),g("Road closed")}})};T.recepcion=()=>{m("#lienzo").innerHTML=`<div><span class="sub">${s("reception")}</span>
|
|
403
|
+
<h1 style="margin-top:6px">${s("Messages wait for you, not your agents")}</h1>
|
|
404
|
+
<p class="prosa" style="margin-top:8px">${s("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.")}</p></div>
|
|
2616
405
|
<div class="recModo" id="recModo">
|
|
2617
|
-
<div><span class="et2">${
|
|
2618
|
-
<p>${
|
|
2619
|
-
<button class="bt mini" type="button" data-rec-open-config>${
|
|
406
|
+
<div><span class="et2">${s("routing mode")}</span><b>${s("Manual review")}</b>
|
|
407
|
+
<p>${s("Every message needs a person before it reaches a city.")}</p></div>
|
|
408
|
+
<button class="bt mini" type="button" data-rec-open-config>${s("Configure Auto")}</button>
|
|
2620
409
|
</div>
|
|
2621
410
|
<div id="recConfig" hidden></div>
|
|
2622
|
-
<div id="recLista"><p class="prosa">${
|
|
2623
|
-
|
|
2624
|
-
}
|
|
2625
|
-
|
|
2626
|
-
|
|
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>
|
|
411
|
+
<div id="recLista"><p class="prosa">${s("Reading your reception\u2026")}</p></div>`,W()};async function W(){let e=m("#recLista"),n;try{n=await b("/api/reception")}catch{e.innerHTML=`<div class="recError">${s("Could not read your reception")}</div>`;return}if(n.error){e.innerHTML=`<div class="recError">${l(n.error)}</div>`;return}d.reception={pending:n.summary.pending,pendingBytes:n.summary.pendingBytes,routingMode:n.settings.routingMode,reviewPolicy:n.settings.reviewPolicy,routerProfile:n.settings.routerProfile,autoAvailable:n.settings.autoAvailable};let t=new Map(n.settings.autoRules.map(u=>[u.cityId,u.keywords])),a=m("#recModo");a.innerHTML=`<div><span class="et2">${s("routing mode")}</span>
|
|
412
|
+
<b>${n.settings.routingMode==="auto"?s("Automatic routing"):s("Manual review")}</b>
|
|
413
|
+
<p>${n.settings.routingMode==="auto"?s("Only one clear, low-risk rule match can leave the human queue automatically."):s("Every message needs a person before it reaches a city.")}</p></div>
|
|
414
|
+
<button class="bt mini" type="button" data-rec-open-config>${s("Configure Auto")}</button>`;let i=m("#recConfig");i.innerHTML=`<form class="recAutoConfig" data-rec-config>
|
|
415
|
+
<fieldset><legend>${s("Routing policy")}</legend>
|
|
2655
416
|
<label><input type="radio" name="routing_mode" value="manual"
|
|
2656
|
-
${
|
|
417
|
+
${n.settings.routingMode==="manual"?"checked":""}> ${s("Keep human review")}</label>
|
|
2657
418
|
<label><input type="radio" name="routing_mode" value="auto"
|
|
2658
|
-
${
|
|
419
|
+
${n.settings.routingMode==="auto"?"checked":""}> ${s("Use the local rule router")}</label>
|
|
2659
420
|
</fieldset>
|
|
2660
|
-
<div class="recReglas"><p>${
|
|
2661
|
-
${
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
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>
|
|
421
|
+
<div class="recReglas"><p>${s("Write comma-separated words or phrases for each destination. Empty cities are never selected.")}</p>
|
|
422
|
+
${n.cities.map(u=>`<label><span><b>${l(u.name)}</b><small>${l(u.address)}</small></span>
|
|
423
|
+
<input type="text" name="rule-${l(u.id)}" maxlength="1200"
|
|
424
|
+
data-rule-city="${l(u.id)}" value="${l((t.get(u.id)??[]).join(", "))}"
|
|
425
|
+
placeholder="${s("e.g. contract, privacy, legal review")}"></label>`).join("")}</div>
|
|
426
|
+
<p class="recAutoAviso">${s("Suspicious, unmatched, or ambiguous text always waits for you. Auto never executes, answers, or opens links.")}</p>
|
|
427
|
+
<button class="bt ppal" type="submit">${s("Save routing policy")}</button>
|
|
428
|
+
</form>`,m("[data-rec-open-config]",a).onclick=()=>{i.hidden=!i.hidden},ze();let c=n.connections.length?`<section class="recConexiones"><div class="recSeccion"><b>${s("New encrypted message")}</b>
|
|
429
|
+
<span>${n.outbox.queued?x(n.outbox.queued,"message queued","messages queued"):s("Messages leave from this computer with end-to-end encryption.")}</span></div>
|
|
2676
430
|
<form class="recCompose" data-rec-send>
|
|
2677
|
-
<label class="recComposePersona" for="rec-send-connection"><span>${
|
|
431
|
+
<label class="recComposePersona" for="rec-send-connection"><span>${s("Write to")}</span>
|
|
2678
432
|
<select id="rec-send-connection" name="connection" required>
|
|
2679
|
-
${
|
|
2680
|
-
(conexion) => `<option value="${esc2(conexion.id)}">${esc2(conexion.name)}</option>`
|
|
2681
|
-
).join("")}
|
|
433
|
+
${n.connections.map(u=>`<option value="${l(u.id)}">${l(u.name)}</option>`).join("")}
|
|
2682
434
|
</select>
|
|
2683
435
|
</label>
|
|
2684
|
-
<label class="recComposeTexto" for="rec-send-text"><span>${
|
|
436
|
+
<label class="recComposeTexto" for="rec-send-text"><span>${s("Message")}</span>
|
|
2685
437
|
<textarea id="rec-send-text" name="text" rows="3" maxlength="11500" required
|
|
2686
|
-
placeholder="${
|
|
438
|
+
placeholder="${s("Your message will stop in their private reception.")}"></textarea>
|
|
2687
439
|
</label>
|
|
2688
|
-
<button class="bt ppal" type="submit">${
|
|
2689
|
-
</form></section
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
<
|
|
2704
|
-
|
|
2705
|
-
|
|
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>
|
|
440
|
+
<button class="bt ppal" type="submit">${s("Send securely")}</button>
|
|
441
|
+
</form></section>`:"",o=n.messages.length?"":`<div class="recVacia"><b>${s("Reception clear")}</b>
|
|
442
|
+
<p>${s("No remote message is waiting for a decision.")}</p></div>`,r=n.cities.map(u=>`<label class="recDestino"><input type="checkbox" name="destination"
|
|
443
|
+
value="${l(u.id)}"><span><b>${l(u.name)}</b>
|
|
444
|
+
<small>${l(u.address)}</small></span></label>`).join(""),p=n.messages.length?`<div class="recResumen"><b>${n.summary.pending}</b>
|
|
445
|
+
<span>${x(n.summary.pending,"message waiting","messages waiting")}</span>
|
|
446
|
+
<small>${ot(n.summary.pendingBytes)} \xB7 ${s("local only")}</small></div>
|
|
447
|
+
<div class="recMensajes">${n.messages.map(u=>`<article class="recMensaje">
|
|
448
|
+
<header><div><span class="recOrigen">${l(u.fromName)}</span>
|
|
449
|
+
<span>${l(st(u.receivedAt))}</span></div>
|
|
450
|
+
<span class="recSeguro">${u.kind==="rejection"?s("Reply to a rejected message"):s("No agent has read this")}</span></header>
|
|
451
|
+
<pre class="recTexto">${l(u.text)}</pre>
|
|
452
|
+
${u.kind==="rejection"?`<div class="recAvisoAccion">
|
|
453
|
+
<button class="bt mini" type="button" data-rec-dismiss="${l(u.id)}">${s("Dismiss")}</button>
|
|
454
|
+
</div>`:`<div class="recAcciones">
|
|
455
|
+
<form data-rec-route="${l(u.id)}">
|
|
456
|
+
<fieldset><legend>${s("Send to")}</legend><div class="recDestinos">${r}</div></fieldset>
|
|
457
|
+
<button class="bt ppal" type="submit" disabled>${s("Route to selected cities")}</button>
|
|
2712
458
|
</form>
|
|
2713
|
-
<details><summary>${
|
|
2714
|
-
<form data-rec-reject="${
|
|
2715
|
-
<label for="rec-reason-${
|
|
2716
|
-
<input id="rec-reason-${
|
|
459
|
+
<details><summary>${s("Reject and reply with a reason")}</summary>
|
|
460
|
+
<form data-rec-reject="${l(u.id)}">
|
|
461
|
+
<label for="rec-reason-${l(u.id)}">${s("Reason sent back securely")}</label>
|
|
462
|
+
<input id="rec-reason-${l(u.id)}" type="text" name="reason"
|
|
2717
463
|
maxlength="500" required
|
|
2718
|
-
placeholder="${
|
|
2719
|
-
<button class="bt malo" type="submit">${
|
|
464
|
+
placeholder="${s("Reason for rejecting this message")}">
|
|
465
|
+
<button class="bt malo" type="submit">${s("Reject and send reason")}</button>
|
|
2720
466
|
</form>
|
|
2721
467
|
</details>
|
|
2722
468
|
</div>`}
|
|
2723
|
-
</article>`
|
|
2724
|
-
|
|
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
|
-
}
|
|
2864
|
-
VISTAS.committee = () => {
|
|
2865
|
-
q("#lienzo").innerHTML = `<div><span class="sub">committee</span>
|
|
2866
|
-
<h1 style="margin-top:6px">${t("Decisions with a visible chain of custody")}</h1>
|
|
469
|
+
</article>`).join("")}</div>`:"";e.innerHTML=`${o}${p}${c}`,at()}function at(){m("[data-rec-config]").onsubmit=async e=>{e.preventDefault();let n=e.currentTarget,t=m("button[type=submit]",n),a=m("input[name=routing_mode]:checked",n).value,i=f("[data-rule-city]",n).map(o=>({city_id:o.dataset.ruleCity,keywords:o.value.split(",").map(r=>r.trim()).filter(Boolean)})).filter(o=>o.keywords.length);t.disabled=!0;let c=await b("/api/reception",{method:"POST",body:JSON.stringify({action:"configure",routing_mode:a,rules:i})});if(!c.ok){t.disabled=!1,g(c.error??s("Could not save the routing policy"),!0);return}g(s("Routing policy saved.")),await W()},f("[data-rec-send]").forEach(e=>{e.onsubmit=async n=>{n.preventDefault();let t=m("button[type=submit]",e),a=m("textarea[name=text]",e),i=m("select[name=connection]",e).value;if(!i||!a.value.trim())return;t.disabled=!0;let c=await b("/api/reception",{method:"POST",body:JSON.stringify({action:"send",connection_id:i,text:a.value})});if(!c.ok){t.disabled=!1,g(c.error??s("Could not queue the message"),!0);return}a.value="",g(s("Message queued on this computer.")),await W()}}),f("[data-rec-dismiss]").forEach(e=>{e.onclick=async()=>{e.disabled=!0;let n=await b("/api/reception",{method:"POST",body:JSON.stringify({action:"reject",message_id:e.dataset.recDismiss,reason:"Dismissed response"})});if(!n.ok){e.disabled=!1,g(n.error??s("Could not dismiss the response"),!0);return}await W()}}),f("[data-rec-route]").forEach(e=>{let n=m("button[type=submit]",e),t=()=>{n.disabled=!e.querySelector("input:checked")};e.onchange=t,e.onsubmit=async a=>{a.preventDefault();let i=f("input[name=destination]:checked",e).map(o=>o.value);if(!i.length)return;n.disabled=!0;let c=await b("/api/reception",{method:"POST",body:JSON.stringify({action:"route",message_id:e.dataset.recRoute,destinations:i})});if(!c.ok){n.disabled=!1,g(c.error??s("Could not route the message"),!0);return}g(s("Message routed. Only the selected cities can now read it.")),await W()}}),f("[data-rec-reject]").forEach(e=>{e.onsubmit=async n=>{n.preventDefault();let t=m("button[type=submit]",e),a=m("input[name=reason]",e).value.trim();if(!a)return;t.disabled=!0;let i=await b("/api/reception",{method:"POST",body:JSON.stringify({action:"reject",message_id:e.dataset.recReject,reason:a})});if(!i.ok){t.disabled=!1,g(i.error??s("Could not reject the message"),!0);return}g(s("Message rejected. Your reason is queued for encrypted delivery.")),await W()}})}function st(e){let n=new Date(e);return Number.isNaN(n.getTime())?e:n.toLocaleString(D()==="es"?"es-ES":"en-GB",{dateStyle:"medium",timeStyle:"short"})}function ot(e){return e<1024?`${e} B`:e<1024*1024?`${(e/1024).toFixed(1)} KB`:`${(e/(1024*1024)).toFixed(1)} MB`}T.committee=()=>{m("#lienzo").innerHTML=`<div><span class="sub">committee</span>
|
|
470
|
+
<h1 style="margin-top:6px">${s("Decisions with a visible chain of custody")}</h1>
|
|
2867
471
|
<p class="prosa" style="margin-top:8px">The seat selects relevant repo agents,
|
|
2868
472
|
gathers isolated positions, controls the floor, decides and assigns an
|
|
2869
473
|
independent verification. This view is read-only; the vendor-neutral CLI
|
|
2870
474
|
drives the protocol.</p></div>
|
|
2871
475
|
<div class="orden"><span class="et2">start</span>
|
|
2872
|
-
<code>${
|
|
476
|
+
<code>${s("agents-city committee schema open")}</code>
|
|
2873
477
|
<button class="bt mini" data-copia="agents-city committee schema open">copy</button>
|
|
2874
478
|
</div>
|
|
2875
|
-
<div class="gente">${
|
|
2876
|
-
|
|
2877
|
-
<
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
<span class="
|
|
2881
|
-
|
|
2882
|
-
${
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
</div>`
|
|
2886
|
-
).join("") || `<p class="prosa">${t("No committee acts yet. Open one only when the seat needs specialised evidence.")}</p>`}</div>`;
|
|
2887
|
-
enlaza();
|
|
2888
|
-
};
|
|
2889
|
-
function opciones2(valores, actual) {
|
|
2890
|
-
return opciones(valores, actual, esc2);
|
|
2891
|
-
}
|
|
2892
|
-
function caraRPG(uri) {
|
|
2893
|
-
return uri?.startsWith("data:image/svg+xml;base64,") ? `<img class="rpgCara" src="${esc2(uri)}" alt="">` : "";
|
|
2894
|
-
}
|
|
2895
|
-
function barra(ancho, clase = "") {
|
|
2896
|
-
const pct = Math.round(Math.max(0, Math.min(1, ancho)) * 100);
|
|
2897
|
-
return `<span class="barra${clase ? " " + clase : ""}"><span class="llena" style="width:${pct}%"></span></span>`;
|
|
2898
|
-
}
|
|
2899
|
-
VISTAS.gente = () => {
|
|
2900
|
-
const mia = E.tarjetas.find((t2) => t2.user === E.yo);
|
|
2901
|
-
const agentes = E.agents ?? [];
|
|
2902
|
-
const fichaDe = (a) => {
|
|
2903
|
-
const motor = nivelDeMotor(a.model);
|
|
2904
|
-
const esfuerzo = NIVEL_ESFUERZO[a.effort] ?? 0;
|
|
2905
|
-
const g = a.growth;
|
|
2906
|
-
const crecido = Math.min(1, Math.log2(g.floors + 1) / 8);
|
|
2907
|
-
const skills = E.skills[a.name]?.skills ?? [];
|
|
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
|
-
);
|
|
2914
|
-
const opcionesRuntime = opciones2(RUNTIMES, a.runtime);
|
|
2915
|
-
const montajes = a.mounts ?? [];
|
|
2916
|
-
return `
|
|
479
|
+
<div class="gente">${d.deliberations.map(e=>`<div class="persona">
|
|
480
|
+
<h3>${l(e.question)}</h3>
|
|
481
|
+
<span class="ag">${l(e.status)} \xB7 revision ${e.revision} \xB7
|
|
482
|
+
${e.received}/${e.total} positions</span>
|
|
483
|
+
<span class="rp">${l(e.decision||e.desired_outcome)}</span>
|
|
484
|
+
<span class="de">${l(e.participants.join(", ")||"no participants")}
|
|
485
|
+
${e.contributors.length?` \xB7 decisive ${l(e.contributors.join(", "))}`:""}
|
|
486
|
+
${e.verifier?` \xB7 verifier ${l(e.verifier)} ${l(e.verification)}`:""}</span>
|
|
487
|
+
<code class="mono">${l(N(e.act))}</code>
|
|
488
|
+
</div>`).join("")||`<p class="prosa">${s("No committee acts yet. Open one only when the seat needs specialised evidence.")}</p>`}</div>`,re()};function ge(e,n){return z(e,n,l)}function Ae(e){return e?.startsWith("data:image/svg+xml;base64,")?`<img class="rpgCara" src="${l(e)}" alt="">`:""}function ve(e,n=""){let t=Math.round(Math.max(0,Math.min(1,e))*100);return`<span class="barra${n?" "+n:""}"><span class="llena" style="width:${t}%"></span></span>`}T.gente=()=>{let e=d.tarjetas.find(o=>o.user===d.yo),n=d.agents??[],t=o=>{let r=Me(o.model),p=de[o.effort]??0,u=o.growth,h=Math.min(1,Math.log2(u.floors+1)/8),v=d.skills[o.name]?.skills??[],w=J(o.runtime),y=ge([""].concat(w.modelos),o.model)+`<option value="__otro__">${s("custom\u2026")}</option>`,$=ge([""].concat(Q),w.esfuerzo?o.effort:""),le=ge(G,o.runtime),R=o.mounts??[];return`
|
|
2917
489
|
<div class="fichaRPG">
|
|
2918
|
-
<div class="rpgCab">${
|
|
2919
|
-
<div><h3>${
|
|
2920
|
-
<span class="rpgTags"><span class="kindChip ${
|
|
2921
|
-
<span class="rpgRol">${
|
|
2922
|
-
<span class="rpgHogar">${
|
|
490
|
+
<div class="rpgCab">${Ae(o.avatar)}
|
|
491
|
+
<div><h3>${l(o.name)}</h3>
|
|
492
|
+
<span class="rpgTags"><span class="kindChip ${l(o.kind)}">${l(o.kind)}</span>
|
|
493
|
+
<span class="rpgRol">${l(o.role)}</span>
|
|
494
|
+
<span class="rpgHogar">${o.legacy?"repo":`workspace \xB7 ${R.length} ${R.length===1?"mount":"mounts"}`}</span></span></div>
|
|
2923
495
|
<span class="rpgBotones">
|
|
2924
|
-
<button class="rpgMini rpgDado" type="button" data-agente="${
|
|
2925
|
-
title="${
|
|
2926
|
-
<button class="rpgMini rpgIns" type="button" data-agente="${
|
|
2927
|
-
data-file="CLAUDE.md" title="${
|
|
2928
|
-
<button class="rpgMini rpgIns" type="button" data-agente="${
|
|
2929
|
-
data-file="AGENTS.md" title="${
|
|
496
|
+
<button class="rpgMini rpgDado" type="button" data-agente="${l(o.slug)}"
|
|
497
|
+
title="${s("Reroll this agent's face \u2014 deterministic, persisted on the card")}">\u{1F3B2}</button>
|
|
498
|
+
<button class="rpgMini rpgIns" type="button" data-agente="${l(o.slug)}"
|
|
499
|
+
data-file="CLAUDE.md" title="${s("Instructions the Claude runtime reads")}">CLAUDE.md</button>
|
|
500
|
+
<button class="rpgMini rpgIns" type="button" data-agente="${l(o.slug)}"
|
|
501
|
+
data-file="AGENTS.md" title="${s("Instructions Codex, OpenCode and Kimi read")}">AGENTS.md</button>
|
|
2930
502
|
</span>
|
|
2931
503
|
</div>
|
|
2932
|
-
<div class="rpgFila"><label>${
|
|
2933
|
-
${
|
|
2934
|
-
<select class="rpgSel" data-agente="${
|
|
2935
|
-
${
|
|
2936
|
-
<div class="rpgFila"><label>${
|
|
2937
|
-
${
|
|
2938
|
-
<select class="rpgSel" data-agente="${
|
|
2939
|
-
${
|
|
2940
|
-
${
|
|
2941
|
-
<div class="rpgFila"><label>${
|
|
2942
|
-
<span class="rpgDato"><span class="cliDot ${
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
${montajes.length ? montajes.map(
|
|
2953
|
-
(m) => `<code class="mono" title="${esc2(m.target)}">${esc2(m.label)}${m.fixed ? "" : `<button class="rpgQuitar rpgDesmonta" type="button"
|
|
2954
|
-
data-agente="${esc2(a.slug)}" data-mount="${esc2(m.label)}"
|
|
2955
|
-
title="Stop this agent working on ${esc2(m.target)}">\xD7</button>`}</code>`
|
|
2956
|
-
).join(" ") : '<span class="rpgDato">nothing mounted yet</span>'}
|
|
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)}"
|
|
504
|
+
<div class="rpgFila"><label>${s("engine")}</label>
|
|
505
|
+
${ve(r.ancho,r.defecto?"defecto":"")}
|
|
506
|
+
<select class="rpgSel" data-agente="${l(o.slug)}" data-campo="model">
|
|
507
|
+
${y}</select></div>
|
|
508
|
+
<div class="rpgFila"><label>${s("effort")}</label>
|
|
509
|
+
${ve(p/5,p?"":"defecto")}
|
|
510
|
+
<select class="rpgSel" data-agente="${l(o.slug)}" data-campo="effort"
|
|
511
|
+
${w.esfuerzo?"":`disabled title="${s("This CLI has no effort setting.")}"`}>
|
|
512
|
+
${$}</select></div>
|
|
513
|
+
<div class="rpgFila"><label>${s("provider")}</label>
|
|
514
|
+
<span class="rpgDato"><span class="cliDot ${o.cli.connected?"on":o.cli.installed?"idle":"off"}" title="${l(o.cli.connected?"connected: this agent\u2019s window is alive right now":o.cli.installed?"installed on this machine, not in use by this agent":`${o.cli.binary} is not installed`)}"></span>${l(s(o.cli.connected?"connected":o.cli.installed?"idle":"missing"))} \xB7 ${l(o.cli.binary)}
|
|
515
|
+
<button class="rpgMini rpgTest" type="button" data-agente="${l(o.slug)}"
|
|
516
|
+
title="${s("Run the engine for real: --version, and the login state on Claude")}">test</button></span>
|
|
517
|
+
<select class="rpgSel" data-agente="${l(o.slug)}" data-campo="runtime">
|
|
518
|
+
${le}</select></div>
|
|
519
|
+
<div class="rpgSkills"><label>${s("works on")} \xB7 ${R.length}</label>
|
|
520
|
+
${R.length?R.map(M=>`<code class="mono" title="${l(M.target)}">${l(M.label)}${M.fixed?"":`<button class="rpgQuitar rpgDesmonta" type="button"
|
|
521
|
+
data-agente="${l(o.slug)}" data-mount="${l(M.label)}"
|
|
522
|
+
title="Stop this agent working on ${l(M.target)}">\xD7</button>`}</code>`).join(" "):'<span class="rpgDato">nothing mounted yet</span>'}
|
|
523
|
+
${o.legacy?`<span class="rpgDato">${s("a legacy repo agent works on its own repo")}</span>`:`<button class="rpgMini rpgMonta" type="button" data-agente="${l(o.slug)}"
|
|
2958
524
|
title="A repo, a worktree or a folder of documents \u2014 this agent works on all of them">+ folder</button>`}</div>
|
|
2959
|
-
<div class="rpgFila"><label>${
|
|
2960
|
-
${
|
|
2961
|
-
<span class="rpgDato">${
|
|
2962
|
-
<div class="rpgSkills"><label>${
|
|
2963
|
-
${
|
|
2964
|
-
|
|
2965
|
-
data-skill="${esc2(s.dir)}" title="${t("Remove this skill from the agent's home")}">\xD7</button>` : ""}</code>`
|
|
2966
|
-
).join(" ") : '<span class="rpgDato">none discovered</span>'}
|
|
525
|
+
<div class="rpgFila"><label>${s("growth")}</label>
|
|
526
|
+
${ve(h)}
|
|
527
|
+
<span class="rpgDato">${u.floors|0} floors \xB7 ${u.bricks|0} bricks \xB7 ${u.activity30|0}/30d</span></div>
|
|
528
|
+
<div class="rpgSkills"><label>${s("skills")} \xB7 ${v.length}</label>
|
|
529
|
+
${v.length?v.map(M=>`<code class="mono" title="${l(M.description)}">${l(M.name)}${M.removable&&M.dir?`<button class="rpgQuitar" type="button" data-agente="${l(o.slug)}"
|
|
530
|
+
data-skill="${l(M.dir)}" title="${s("Remove this skill from the agent's home")}">\xD7</button>`:""}</code>`).join(" "):'<span class="rpgDato">none discovered</span>'}
|
|
2967
531
|
<label class="rpgMini rpgSubir"
|
|
2968
|
-
title="${
|
|
2969
|
-
+ zip<input type="file" accept=".zip" data-agente="${
|
|
2970
|
-
</div
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
<h1 style="margin-top:6px">${t("Who lives in {city}", { city: esc2(E.city_name) })}</h1>
|
|
2974
|
-
<p class="prosa" style="margin-top:8px">${t(`<b>A house is where an agent lives and works</b>,
|
|
532
|
+
title="${s("Install a skill zip into this agent's own home \u2014 the Claude runtime reads skills; other engines ignore them")}">
|
|
533
|
+
+ zip<input type="file" accept=".zip" data-agente="${l(o.slug)}"></label></div>
|
|
534
|
+
</div>`};m("#lienzo").innerHTML=`<div><span class="sub">the houses of ${l(d.city_name)}</span>
|
|
535
|
+
<h1 style="margin-top:6px">${s("Who lives in {city}",{city:l(d.city_name)})}</h1>
|
|
536
|
+
<p class="prosa" style="margin-top:8px">${s(`<b>A house is where an agent lives and works</b>,
|
|
2975
537
|
and many houses are a city \u2014 it is the same thing the map draws, growing with what that
|
|
2976
538
|
agent actually does. The card and the CLI call them <code class="mono">agents</code>;
|
|
2977
539
|
here you see their houses.`)}</p>
|
|
2978
|
-
<p class="prosa">${
|
|
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
|
|
540
|
+
<p class="prosa">${s("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.",{donde:`<code class="mono">${l(N(d.datos))}/agents/</code>`})} Every agent is whole here: the kind of work
|
|
2982
541
|
it does, its role, everything it works on \u2014 any number of repositories and document
|
|
2983
542
|
folders at once \u2014 the engine and effort that run it, and the skills in its own home.</p>
|
|
2984
543
|
<p class="prosa">Every number is real: growth from what the agent actually produced,
|
|
2985
544
|
skills from live read-only recognition. Changes persist to the card and apply the next
|
|
2986
545
|
time the session opens.</p>
|
|
2987
|
-
<button class="rpgAlta" type="button" id="altaAgente">${
|
|
546
|
+
<button class="rpgAlta" type="button" id="altaAgente">${s("+ Build a house")}</button></div>
|
|
2988
547
|
<div class="fichasRPG">
|
|
2989
548
|
<div class="fichaRPG rpgSeat">
|
|
2990
|
-
<div class="rpgCab">${
|
|
549
|
+
<div class="rpgCab">${Ae(d.avatars?.seat)}
|
|
2991
550
|
<div><h3>seat</h3>
|
|
2992
551
|
<span class="rpgTags"><span class="kindChip coordinator">chair</span>
|
|
2993
|
-
<span class="rpgRol">${
|
|
552
|
+
<span class="rpgRol">${l(e?.role??"not configured")}</span></span></div>
|
|
2994
553
|
</div>
|
|
2995
|
-
<div class="rpgFila"><span class="rpgDato">${
|
|
2996
|
-
<div class="rpgFila"><span class="rpgDato">${
|
|
554
|
+
<div class="rpgFila"><span class="rpgDato">${l(d.address)}</span></div>
|
|
555
|
+
<div class="rpgFila"><span class="rpgDato">${l(e?.objetivo?.title??"No goal yet")}</span></div>
|
|
2997
556
|
</div>
|
|
2998
|
-
${
|
|
2999
|
-
</div>`;
|
|
3000
|
-
const guardaAgente = async (carga, mensaje) => {
|
|
3001
|
-
let r;
|
|
3002
|
-
try {
|
|
3003
|
-
r = await api("/api/agente", { method: "POST", body: JSON.stringify(carga) });
|
|
3004
|
-
} catch (e) {
|
|
3005
|
-
toast(String(e), true);
|
|
3006
|
-
pinta();
|
|
3007
|
-
return;
|
|
3008
|
-
}
|
|
3009
|
-
if (!r.ok || !r.agent) {
|
|
3010
|
-
toast(r.error || "Could not save", true);
|
|
3011
|
-
pinta();
|
|
3012
|
-
return;
|
|
3013
|
-
}
|
|
3014
|
-
E.agents = (E.agents ?? []).map((x) => x.slug === r.agent.slug ? r.agent : x);
|
|
3015
|
-
if (E.avatars) E.avatars[r.agent.name] = r.agent.avatar;
|
|
3016
|
-
toast(mensaje);
|
|
3017
|
-
pinta();
|
|
3018
|
-
};
|
|
3019
|
-
todos(".rpgSel").forEach((sel) => {
|
|
3020
|
-
sel.onchange = async () => {
|
|
3021
|
-
let valor = sel.value;
|
|
3022
|
-
if (valor === "__otro__") {
|
|
3023
|
-
const dicho = await pregunta({
|
|
3024
|
-
titulo: t("Which engine?"),
|
|
3025
|
-
cuerpo: [
|
|
3026
|
-
t("An alias the CLI resolves when the window opens. Anything it accepts works here.")
|
|
3027
|
-
],
|
|
3028
|
-
campos: [
|
|
3029
|
-
{ id: "alias", etiqueta: t("Model alias"), pista: "claude-opus-5", requerido: true }
|
|
3030
|
-
],
|
|
3031
|
-
aceptar: t("Use it")
|
|
3032
|
-
});
|
|
3033
|
-
const propio = dicho?.alias ?? null;
|
|
3034
|
-
if (propio === null) {
|
|
3035
|
-
pinta();
|
|
3036
|
-
return;
|
|
3037
|
-
}
|
|
3038
|
-
valor = propio.trim().toLowerCase();
|
|
3039
|
-
}
|
|
3040
|
-
await guardaAgente(
|
|
3041
|
-
{ agent: sel.dataset.agente ?? "", [sel.dataset.campo ?? "model"]: valor },
|
|
3042
|
-
"Saved \u2014 applies next session"
|
|
3043
|
-
);
|
|
3044
|
-
};
|
|
3045
|
-
});
|
|
3046
|
-
todos(".rpgDado").forEach((boton) => {
|
|
3047
|
-
boton.onclick = () => guardaAgente(
|
|
3048
|
-
{ agent: boton.dataset.agente ?? "", avatar: Math.random().toString(36).slice(2, 8) },
|
|
3049
|
-
"New look \u2014 same identity, everywhere, forever"
|
|
3050
|
-
);
|
|
3051
|
-
});
|
|
3052
|
-
todos(".rpgTest").forEach((boton) => {
|
|
3053
|
-
boton.onclick = async () => {
|
|
3054
|
-
boton.disabled = true;
|
|
3055
|
-
try {
|
|
3056
|
-
const r = await api("/api/motor", {
|
|
3057
|
-
method: "POST",
|
|
3058
|
-
body: JSON.stringify({ agent: boton.dataset.agente ?? "" })
|
|
3059
|
-
});
|
|
3060
|
-
if (r.error) {
|
|
3061
|
-
toast(r.error, true);
|
|
3062
|
-
return;
|
|
3063
|
-
}
|
|
3064
|
-
const partes = [r.version, r.detail].filter(Boolean).join(" \xB7 ");
|
|
3065
|
-
toast(
|
|
3066
|
-
r.ok ? `${r.binary} works \u2014 ${partes}` : `${r.binary}: ${r.detail || "failed"}`,
|
|
3067
|
-
!r.ok
|
|
3068
|
-
);
|
|
3069
|
-
} catch (e) {
|
|
3070
|
-
toast(String(e), true);
|
|
3071
|
-
} finally {
|
|
3072
|
-
boton.disabled = false;
|
|
3073
|
-
}
|
|
3074
|
-
};
|
|
3075
|
-
});
|
|
3076
|
-
todos(".rpgQuitar").forEach((boton) => {
|
|
3077
|
-
boton.onclick = async () => {
|
|
3078
|
-
const nombre = boton.dataset.skill ?? "";
|
|
3079
|
-
const vale = await confirma(
|
|
3080
|
-
t("Remove the skill {skill}?", { skill: nombre }),
|
|
3081
|
-
[t("It is deleted from this agent\u2019s own home. Nothing outside that folder is touched.")],
|
|
3082
|
-
{ aceptar: t("Remove it"), peligro: true }
|
|
3083
|
-
);
|
|
3084
|
-
if (!vale) return;
|
|
3085
|
-
const r = await api("/api/skill", {
|
|
3086
|
-
method: "POST",
|
|
3087
|
-
body: JSON.stringify({ agent: boton.dataset.agente ?? "", remove: nombre })
|
|
3088
|
-
});
|
|
3089
|
-
if (!r.ok) {
|
|
3090
|
-
toast(r.error || "Could not remove", true);
|
|
3091
|
-
return;
|
|
3092
|
-
}
|
|
3093
|
-
toast(`Skill ${nombre} removed`);
|
|
3094
|
-
void refresca();
|
|
3095
|
-
};
|
|
3096
|
-
});
|
|
3097
|
-
const alta = document.getElementById("altaAgente");
|
|
3098
|
-
if (alta)
|
|
3099
|
-
alta.onclick = async () => {
|
|
3100
|
-
const forma = new FormularioDeCasa({ api, esc: esc2, aviso: toast, yo: E.yo });
|
|
3101
|
-
let hecho = null;
|
|
3102
|
-
await pregunta({
|
|
3103
|
-
titulo: t("Build a house"),
|
|
3104
|
-
cuerpo: [
|
|
3105
|
-
t(
|
|
3106
|
-
"One worker, its own window, its own corner of your disk. Everything here can be changed afterwards."
|
|
3107
|
-
)
|
|
3108
|
-
],
|
|
3109
|
-
contenido: '<div id="altaCasa"></div>',
|
|
3110
|
-
aceptar: t("Build it"),
|
|
3111
|
-
ancho: 640,
|
|
3112
|
-
enlaza: (raiz, cierra) => {
|
|
3113
|
-
const hueco = raiz.querySelector("#altaCasa");
|
|
3114
|
-
if (hueco) forma.monta(hueco);
|
|
3115
|
-
const boton = raiz.querySelector('[data-dlg="si"]');
|
|
3116
|
-
if (!boton) return;
|
|
3117
|
-
boton.onclick = async () => {
|
|
3118
|
-
boton.disabled = true;
|
|
3119
|
-
hecho = await forma.guarda();
|
|
3120
|
-
boton.disabled = false;
|
|
3121
|
-
if (hecho) cierra({});
|
|
3122
|
-
};
|
|
3123
|
-
}
|
|
3124
|
-
});
|
|
3125
|
-
if (!hecho) return;
|
|
3126
|
-
toast(t("{name} has a house now", { name: hecho.nombre }));
|
|
3127
|
-
void refresca();
|
|
3128
|
-
};
|
|
3129
|
-
const montaje = async (carga, hecho) => {
|
|
3130
|
-
try {
|
|
3131
|
-
const r = await api("/api/montaje", {
|
|
3132
|
-
method: "POST",
|
|
3133
|
-
body: JSON.stringify(carga)
|
|
3134
|
-
});
|
|
3135
|
-
if (!r.ok) {
|
|
3136
|
-
toast(r.error || "Could not change the mounts", true);
|
|
3137
|
-
return;
|
|
3138
|
-
}
|
|
3139
|
-
toast(hecho);
|
|
3140
|
-
void refresca();
|
|
3141
|
-
} catch (e) {
|
|
3142
|
-
toast(String(e), true);
|
|
3143
|
-
}
|
|
3144
|
-
};
|
|
3145
|
-
todos(".rpgMonta").forEach((boton) => {
|
|
3146
|
-
boton.onclick = async () => {
|
|
3147
|
-
const elegidas = [];
|
|
3148
|
-
const exp = new Explorador(
|
|
3149
|
-
{ api, esc: esc2 },
|
|
3150
|
-
(ruta) => {
|
|
3151
|
-
const ya = elegidas.indexOf(ruta);
|
|
3152
|
-
if (ya >= 0) elegidas.splice(ya, 1);
|
|
3153
|
-
else elegidas.push(ruta);
|
|
3154
|
-
},
|
|
3155
|
-
() => elegidas
|
|
3156
|
-
);
|
|
3157
|
-
const dicho = await pregunta({
|
|
3158
|
-
titulo: t("What else does it work on?"),
|
|
3159
|
-
cuerpo: [
|
|
3160
|
-
t(
|
|
3161
|
-
"A repository, a worktree, a folder of documents, one exact file. It is linked, never copied."
|
|
3162
|
-
)
|
|
3163
|
-
],
|
|
3164
|
-
contenido: '<div id="montaExp"></div>',
|
|
3165
|
-
aceptar: t("Mount it"),
|
|
3166
|
-
ancho: 620,
|
|
3167
|
-
enlaza: (raiz) => {
|
|
3168
|
-
const hueco = raiz.querySelector("#montaExp");
|
|
3169
|
-
if (hueco) exp.monta(hueco);
|
|
3170
|
-
}
|
|
3171
|
-
});
|
|
3172
|
-
if (dicho === null || !elegidas.length) return;
|
|
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();
|
|
3185
|
-
};
|
|
3186
|
-
});
|
|
3187
|
-
todos(".rpgDesmonta").forEach((boton) => {
|
|
3188
|
-
boton.onclick = async () => {
|
|
3189
|
-
const etiqueta = boton.dataset.mount ?? "";
|
|
3190
|
-
const vale = await confirma(
|
|
3191
|
-
t("Stop this agent working on {what}?", { what: etiqueta }),
|
|
3192
|
-
[t("The link goes. The folder it points at stays exactly where it is.")],
|
|
3193
|
-
{ aceptar: t("Unmount it") }
|
|
3194
|
-
);
|
|
3195
|
-
if (!vale) return;
|
|
3196
|
-
void montaje(
|
|
3197
|
-
{ agent: boton.dataset.agente ?? "", remove: etiqueta },
|
|
3198
|
-
`${etiqueta} unmounted \u2014 the folder itself is untouched`
|
|
3199
|
-
);
|
|
3200
|
-
};
|
|
3201
|
-
});
|
|
3202
|
-
todos(".rpgIns").forEach((boton) => {
|
|
3203
|
-
boton.onclick = () => void abreInstrucciones(boton.dataset.agente ?? "", boton.dataset.file ?? "");
|
|
3204
|
-
});
|
|
3205
|
-
todos(".rpgSubir input").forEach((entrada) => {
|
|
3206
|
-
entrada.onchange = async () => {
|
|
3207
|
-
const archivo = entrada.files?.[0];
|
|
3208
|
-
if (!archivo) return;
|
|
3209
|
-
entrada.disabled = true;
|
|
3210
|
-
try {
|
|
3211
|
-
const zip = await aBase64(archivo);
|
|
3212
|
-
const nombre = archivo.name.replace(/\.zip$/i, "").toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
3213
|
-
const r = await api("/api/skill", {
|
|
3214
|
-
method: "POST",
|
|
3215
|
-
body: JSON.stringify({ agent: entrada.dataset.agente ?? "", name: nombre, zip })
|
|
3216
|
-
});
|
|
3217
|
-
if (!r.ok) {
|
|
3218
|
-
toast(r.error || "Could not install", true);
|
|
3219
|
-
return;
|
|
3220
|
-
}
|
|
3221
|
-
toast(`Skill ${r.skill} installed \u2014 the Claude runtime reads it next session`);
|
|
3222
|
-
void refresca();
|
|
3223
|
-
} catch (e) {
|
|
3224
|
-
toast(String(e), true);
|
|
3225
|
-
} finally {
|
|
3226
|
-
entrada.disabled = false;
|
|
3227
|
-
entrada.value = "";
|
|
3228
|
-
}
|
|
3229
|
-
};
|
|
3230
|
-
});
|
|
3231
|
-
};
|
|
3232
|
-
function aBase64(archivo) {
|
|
3233
|
-
return new Promise((listo, falla) => {
|
|
3234
|
-
const lector = new FileReader();
|
|
3235
|
-
lector.onerror = () => falla(lector.error);
|
|
3236
|
-
lector.onload = () => listo(String(lector.result).split(",", 2)[1] ?? "");
|
|
3237
|
-
lector.readAsDataURL(archivo);
|
|
3238
|
-
});
|
|
3239
|
-
}
|
|
3240
|
-
var editorAbierto = null;
|
|
3241
|
-
async function abreInstrucciones(agente, archivo) {
|
|
3242
|
-
const r = await api(`/api/instrucciones?agent=${encodeURIComponent(agente)}&file=${encodeURIComponent(archivo)}`);
|
|
3243
|
-
if (r.error) {
|
|
3244
|
-
toast(r.error, true);
|
|
3245
|
-
return;
|
|
3246
|
-
}
|
|
3247
|
-
editorAbierto = { agent: agente, file: archivo };
|
|
3248
|
-
q("#edTitulo").textContent = `${agente} \xB7 ${archivo}`;
|
|
3249
|
-
q("#edLector").textContent = `read by ${r.reader ?? "?"}${r.exists ? "" : " \xB7 new file"}`;
|
|
3250
|
-
q("#edRuta").textContent = r.home ? `${r.home}/${archivo}` : "";
|
|
3251
|
-
q("#edTexto").value = r.content ?? "";
|
|
3252
|
-
q("#editorIns").hidden = false;
|
|
3253
|
-
q("#edTexto").focus();
|
|
3254
|
-
}
|
|
3255
|
-
document.getElementById("edCerrar")?.addEventListener("click", () => {
|
|
3256
|
-
q("#editorIns").hidden = true;
|
|
3257
|
-
editorAbierto = null;
|
|
3258
|
-
});
|
|
3259
|
-
document.getElementById("edGuardar")?.addEventListener("click", async () => {
|
|
3260
|
-
if (!editorAbierto) return;
|
|
3261
|
-
const r = await api("/api/instrucciones", {
|
|
3262
|
-
method: "POST",
|
|
3263
|
-
body: JSON.stringify({ ...editorAbierto, content: q("#edTexto").value })
|
|
3264
|
-
});
|
|
3265
|
-
if (!r.ok) {
|
|
3266
|
-
toast(r.error || "Could not save", true);
|
|
3267
|
-
return;
|
|
3268
|
-
}
|
|
3269
|
-
toast("Saved");
|
|
3270
|
-
q("#editorIns").hidden = true;
|
|
3271
|
-
editorAbierto = null;
|
|
3272
|
-
});
|
|
3273
|
-
function enlaza() {
|
|
3274
|
-
todos("[data-copia]").forEach((b) => {
|
|
3275
|
-
b.onclick = async () => {
|
|
3276
|
-
try {
|
|
3277
|
-
await navigator.clipboard.writeText(b.dataset.copia ?? "");
|
|
3278
|
-
toast("Copied");
|
|
3279
|
-
} catch {
|
|
3280
|
-
toast("Could not copy \u2014 select it by hand", true);
|
|
3281
|
-
}
|
|
3282
|
-
};
|
|
3283
|
-
});
|
|
3284
|
-
todos("[data-ir]").forEach((b) => {
|
|
3285
|
-
b.onclick = () => {
|
|
3286
|
-
SECCION = b.dataset.ir ?? "mapa";
|
|
3287
|
-
pinta();
|
|
3288
|
-
};
|
|
3289
|
-
});
|
|
3290
|
-
}
|
|
3291
|
-
function arrastreDelRail() {
|
|
3292
|
-
const app = document.getElementById("app");
|
|
3293
|
-
const asa = document.getElementById("liveResize");
|
|
3294
|
-
const rail2 = document.getElementById("livePanel");
|
|
3295
|
-
if (!app || !asa || !rail2) return;
|
|
3296
|
-
const CLAVE = "hall-live-width";
|
|
3297
|
-
const aplica = (px) => {
|
|
3298
|
-
if (px === null) app.style.removeProperty("--live-ancho");
|
|
3299
|
-
else app.style.setProperty("--live-ancho", `${Math.round(px)}px`);
|
|
3300
|
-
};
|
|
3301
|
-
const tope = (px) => Math.max(340, Math.min(px, Math.max(420, window.innerWidth * 0.6)));
|
|
3302
|
-
try {
|
|
3303
|
-
const guardado = Number(localStorage.getItem(CLAVE));
|
|
3304
|
-
if (guardado > 0) aplica(tope(guardado));
|
|
3305
|
-
} catch {
|
|
3306
|
-
}
|
|
3307
|
-
asa.addEventListener("pointerdown", (e) => {
|
|
3308
|
-
e.preventDefault();
|
|
3309
|
-
const desdeX = e.clientX;
|
|
3310
|
-
const desdeAncho = rail2.getBoundingClientRect().width;
|
|
3311
|
-
app.classList.add("liveResizing");
|
|
3312
|
-
asa.setPointerCapture(e.pointerId);
|
|
3313
|
-
const mueve = (ev) => aplica(tope(desdeAncho + (desdeX - ev.clientX)));
|
|
3314
|
-
const suelta = () => {
|
|
3315
|
-
app.classList.remove("liveResizing");
|
|
3316
|
-
asa.removeEventListener("pointermove", mueve);
|
|
3317
|
-
asa.removeEventListener("pointerup", suelta);
|
|
3318
|
-
asa.removeEventListener("pointercancel", suelta);
|
|
3319
|
-
try {
|
|
3320
|
-
localStorage.setItem(CLAVE, String(Math.round(rail2.getBoundingClientRect().width)));
|
|
3321
|
-
} catch {
|
|
3322
|
-
}
|
|
3323
|
-
};
|
|
3324
|
-
asa.addEventListener("pointermove", mueve);
|
|
3325
|
-
asa.addEventListener("pointerup", suelta);
|
|
3326
|
-
asa.addEventListener("pointercancel", suelta);
|
|
3327
|
-
});
|
|
3328
|
-
asa.addEventListener("dblclick", () => {
|
|
3329
|
-
aplica(null);
|
|
3330
|
-
try {
|
|
3331
|
-
localStorage.removeItem(CLAVE);
|
|
3332
|
-
} catch {
|
|
3333
|
-
}
|
|
3334
|
-
});
|
|
3335
|
-
}
|
|
3336
|
-
window.addEventListener("message", (message) => {
|
|
3337
|
-
if (!isMapNavMessage(message.data)) return;
|
|
3338
|
-
const frame = document.querySelector("#cityMapFrame");
|
|
3339
|
-
if (!frame || message.source !== frame.contentWindow) return;
|
|
3340
|
-
if (message.origin !== new URL(frame.src, location.href).origin) return;
|
|
3341
|
-
const view = message.data.view;
|
|
3342
|
-
if (view !== "committee" && view !== "red") return;
|
|
3343
|
-
SECCION = view;
|
|
3344
|
-
pinta();
|
|
3345
|
-
});
|
|
3346
|
-
function temaActual() {
|
|
3347
|
-
const elegido = document.documentElement.getAttribute("data-tema");
|
|
3348
|
-
if (elegido) return elegido === "claro" ? "light" : "dark";
|
|
3349
|
-
return "light";
|
|
3350
|
-
}
|
|
3351
|
-
function tema() {
|
|
3352
|
-
const boton = document.getElementById("temaBoton");
|
|
3353
|
-
const guardado = (() => {
|
|
3354
|
-
try {
|
|
3355
|
-
return localStorage.getItem("hall-tema") ?? "";
|
|
3356
|
-
} catch {
|
|
3357
|
-
return "";
|
|
3358
|
-
}
|
|
3359
|
-
})();
|
|
3360
|
-
const oscuroDelSistema = false;
|
|
3361
|
-
const pon = (elegido) => {
|
|
3362
|
-
const raiz = document.documentElement;
|
|
3363
|
-
if (elegido) raiz.setAttribute("data-tema", elegido);
|
|
3364
|
-
else raiz.removeAttribute("data-tema");
|
|
3365
|
-
const claro = elegido ? elegido === "claro" : !oscuroDelSistema;
|
|
3366
|
-
if (boton) boton.textContent = claro ? "\u263E Night" : "\u2600 Day";
|
|
3367
|
-
const frame = document.querySelector("#cityMapFrame");
|
|
3368
|
-
frame?.contentWindow?.postMessage(
|
|
3369
|
-
{ type: "agents-city-map-theme/1", theme: claro ? "light" : "dark" },
|
|
3370
|
-
new URL(frame.src, location.href).origin
|
|
3371
|
-
);
|
|
3372
|
-
};
|
|
3373
|
-
pon(guardado);
|
|
3374
|
-
if (boton)
|
|
3375
|
-
boton.onclick = () => {
|
|
3376
|
-
const claroAhora = document.documentElement.getAttribute("data-tema") ? document.documentElement.getAttribute("data-tema") === "claro" : !oscuroDelSistema;
|
|
3377
|
-
const elegido = claroAhora ? "oscuro" : "claro";
|
|
3378
|
-
try {
|
|
3379
|
-
localStorage.setItem("hall-tema", elegido);
|
|
3380
|
-
} catch {
|
|
3381
|
-
}
|
|
3382
|
-
pon(elegido);
|
|
3383
|
-
};
|
|
3384
|
-
}
|
|
3385
|
-
function interruptorDeIdioma() {
|
|
3386
|
-
const boton = document.getElementById("idiomaBoton");
|
|
3387
|
-
if (!boton) return;
|
|
3388
|
-
const pon = () => {
|
|
3389
|
-
document.documentElement.lang = idioma();
|
|
3390
|
-
boton.textContent = idioma() === "es" ? "EN" : "ES";
|
|
3391
|
-
todos("[data-i18n]").forEach((el) => {
|
|
3392
|
-
if (!el.dataset.en) el.dataset.en = (el.textContent ?? "").trim();
|
|
3393
|
-
el.textContent = t(el.dataset.en);
|
|
3394
|
-
});
|
|
3395
|
-
};
|
|
3396
|
-
pon();
|
|
3397
|
-
boton.onclick = () => {
|
|
3398
|
-
ponIdioma(idioma() === "es" ? "en" : "es");
|
|
3399
|
-
pon();
|
|
3400
|
-
void refresca();
|
|
3401
|
-
};
|
|
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
|
-
);
|
|
3411
|
-
arrastreDelRail();
|
|
3412
|
-
interruptorDeIdioma();
|
|
3413
|
-
tema();
|
|
3414
|
-
void refresca();
|
|
557
|
+
${n.map(t).join("")}
|
|
558
|
+
</div>`;let a=async(o,r)=>{let p;try{p=await b("/api/agente",{method:"POST",body:JSON.stringify(o)})}catch(u){g(String(u),!0),C();return}if(!p.ok||!p.agent){g(p.error||"Could not save",!0),C();return}d.agents=(d.agents??[]).map(u=>u.slug===p.agent.slug?p.agent:u),d.avatars&&(d.avatars[p.agent.name]=p.agent.avatar),g(r),C()};f(".rpgSel").forEach(o=>{o.onchange=async()=>{let r=o.value;if(r==="__otro__"){let u=(await I({titulo:s("Which engine?"),cuerpo:[s("An alias the CLI resolves when the window opens. Anything it accepts works here.")],campos:[{id:"alias",etiqueta:s("Model alias"),pista:"claude-opus-5",requerido:!0}],aceptar:s("Use it")}))?.alias??null;if(u===null){C();return}r=u.trim().toLowerCase()}await a({agent:o.dataset.agente??"",[o.dataset.campo??"model"]:r},"Saved \u2014 applies next session")}}),f(".rpgDado").forEach(o=>{o.onclick=()=>a({agent:o.dataset.agente??"",avatar:Math.random().toString(36).slice(2,8)},"New look \u2014 same identity, everywhere, forever")}),f(".rpgTest").forEach(o=>{o.onclick=async()=>{o.disabled=!0;try{let r=await b("/api/motor",{method:"POST",body:JSON.stringify({agent:o.dataset.agente??""})});if(r.error){g(r.error,!0);return}let p=[r.version,r.detail].filter(Boolean).join(" \xB7 ");g(r.ok?`${r.binary} works \u2014 ${p}`:`${r.binary}: ${r.detail||"failed"}`,!r.ok)}catch(r){g(String(r),!0)}finally{o.disabled=!1}}}),f(".rpgQuitar").forEach(o=>{o.onclick=async()=>{let r=o.dataset.skill??"";if(!await Z(s("Remove the skill {skill}?",{skill:r}),[s("It is deleted from this agent\u2019s own home. Nothing outside that folder is touched.")],{aceptar:s("Remove it"),peligro:!0}))return;let u=await b("/api/skill",{method:"POST",body:JSON.stringify({agent:o.dataset.agente??"",remove:r})});if(!u.ok){g(u.error||"Could not remove",!0);return}g(`Skill ${r} removed`),k()}});let i=document.getElementById("altaAgente");i&&(i.onclick=async()=>{let o=new _({api:b,esc:l,aviso:g,yo:d.yo}),r=null;await I({titulo:s("Build a house"),cuerpo:[s("One worker, its own window, its own corner of your disk. Everything here can be changed afterwards.")],contenido:'<div id="altaCasa"></div>',aceptar:s("Build it"),ancho:640,enlaza:(p,u)=>{let h=p.querySelector("#altaCasa");h&&o.monta(h);let v=p.querySelector('[data-dlg="si"]');v&&(v.onclick=async()=>{v.disabled=!0,r=await o.guarda(),v.disabled=!1,r&&u({})})}}),r&&(g(s("{name} has a house now",{name:r.nombre})),k())});let c=async(o,r)=>{try{let p=await b("/api/montaje",{method:"POST",body:JSON.stringify(o)});if(!p.ok){g(p.error||"Could not change the mounts",!0);return}g(r),k()}catch(p){g(String(p),!0)}};f(".rpgMonta").forEach(o=>{o.onclick=async()=>{let r=[],p=new B({api:b,esc:l},y=>{let $=r.indexOf(y);$>=0?r.splice($,1):r.push(y)},()=>r);if(await I({titulo:s("What else does it work on?"),cuerpo:[s("A repository, a worktree, a folder of documents, one exact file. It is linked, never copied.")],contenido:'<div id="montaExp"></div>',aceptar:s("Mount it"),ancho:620,enlaza:y=>{let $=y.querySelector("#montaExp");$&&p.monta($)}})===null||!r.length)return;let h=o.dataset.agente??"",w=(await Promise.all(r.map(y=>b("/api/montaje",{method:"POST",body:JSON.stringify({agent:h,add:y})}).then($=>$.ok?"":`${y}: ${$.error??"could not mount"}`).catch($=>`${y}: ${String($)}`)))).filter(Boolean);g(w.length?w.join("; "):s("Mounted"),w.length>0),k()}}),f(".rpgDesmonta").forEach(o=>{o.onclick=async()=>{let r=o.dataset.mount??"";await Z(s("Stop this agent working on {what}?",{what:r}),[s("The link goes. The folder it points at stays exactly where it is.")],{aceptar:s("Unmount it")})&&c({agent:o.dataset.agente??"",remove:r},`${r} unmounted \u2014 the folder itself is untouched`)}}),f(".rpgIns").forEach(o=>{o.onclick=()=>void it(o.dataset.agente??"",o.dataset.file??"")}),f(".rpgSubir input").forEach(o=>{o.onchange=async()=>{let r=o.files?.[0];if(r){o.disabled=!0;try{let p=await nt(r),u=r.name.replace(/\.zip$/i,"").toLowerCase().replace(/[^a-z0-9-]+/g,"-").replace(/^-+|-+$/g,""),h=await b("/api/skill",{method:"POST",body:JSON.stringify({agent:o.dataset.agente??"",name:u,zip:p})});if(!h.ok){g(h.error||"Could not install",!0);return}g(`Skill ${h.skill} installed \u2014 the Claude runtime reads it next session`),k()}catch(p){g(String(p),!0)}finally{o.disabled=!1,o.value=""}}}})};function nt(e){return new Promise((n,t)=>{let a=new FileReader;a.onerror=()=>t(a.error),a.onload=()=>n(String(a.result).split(",",2)[1]??""),a.readAsDataURL(e)})}var V=null;async function it(e,n){let t=await b(`/api/instrucciones?agent=${encodeURIComponent(e)}&file=${encodeURIComponent(n)}`);if(t.error){g(t.error,!0);return}V={agent:e,file:n},m("#edTitulo").textContent=`${e} \xB7 ${n}`,m("#edLector").textContent=`read by ${t.reader??"?"}${t.exists?"":" \xB7 new file"}`,m("#edRuta").textContent=t.home?`${t.home}/${n}`:"",m("#edTexto").value=t.content??"",m("#editorIns").hidden=!1,m("#edTexto").focus()}document.getElementById("edCerrar")?.addEventListener("click",()=>{m("#editorIns").hidden=!0,V=null});document.getElementById("edGuardar")?.addEventListener("click",async()=>{if(!V)return;let e=await b("/api/instrucciones",{method:"POST",body:JSON.stringify({...V,content:m("#edTexto").value})});if(!e.ok){g(e.error||"Could not save",!0);return}g("Saved"),m("#editorIns").hidden=!0,V=null});function re(){f("[data-copia]").forEach(e=>{e.onclick=async()=>{try{await navigator.clipboard.writeText(e.dataset.copia??""),g("Copied")}catch{g("Could not copy \u2014 select it by hand",!0)}}}),f("[data-ir]").forEach(e=>{e.onclick=()=>{S=e.dataset.ir??"mapa",C()}})}function rt(){let e=document.getElementById("app"),n=document.getElementById("liveResize"),t=document.getElementById("livePanel");if(!e||!n||!t)return;let a="hall-live-width",i=o=>{o===null?e.style.removeProperty("--live-ancho"):e.style.setProperty("--live-ancho",`${Math.round(o)}px`)},c=o=>Math.max(340,Math.min(o,Math.max(420,window.innerWidth*.6)));try{let o=Number(localStorage.getItem(a));o>0&&i(c(o))}catch{}n.addEventListener("pointerdown",o=>{o.preventDefault();let r=o.clientX,p=t.getBoundingClientRect().width;e.classList.add("liveResizing"),n.setPointerCapture(o.pointerId);let u=v=>i(c(p+(r-v.clientX))),h=()=>{e.classList.remove("liveResizing"),n.removeEventListener("pointermove",u),n.removeEventListener("pointerup",h),n.removeEventListener("pointercancel",h);try{localStorage.setItem(a,String(Math.round(t.getBoundingClientRect().width)))}catch{}};n.addEventListener("pointermove",u),n.addEventListener("pointerup",h),n.addEventListener("pointercancel",h)}),n.addEventListener("dblclick",()=>{i(null);try{localStorage.removeItem(a)}catch{}})}window.addEventListener("message",e=>{if(!Le(e.data))return;let n=document.querySelector("#cityMapFrame");if(!n||e.source!==n.contentWindow||e.origin!==new URL(n.src,location.href).origin)return;let t=e.data.view;t!=="committee"&&t!=="red"||(S=t,C())});function lt(){let e=document.documentElement.getAttribute("data-tema");return e?e==="claro"?"light":"dark":"light"}function ct(){let e=document.getElementById("temaBoton"),n=(()=>{try{return localStorage.getItem("hall-tema")??""}catch{return""}})(),t=!1,a=i=>{let c=document.documentElement;i?c.setAttribute("data-tema",i):c.removeAttribute("data-tema");let o=i?i==="claro":!t;e&&(e.textContent=o?"\u263E Night":"\u2600 Day");let r=document.querySelector("#cityMapFrame");r?.contentWindow?.postMessage({type:"agents-city-map-theme/1",theme:o?"light":"dark"},new URL(r.src,location.href).origin)};a(n),e&&(e.onclick=()=>{let c=(document.documentElement.getAttribute("data-tema")?document.documentElement.getAttribute("data-tema")==="claro":!t)?"oscuro":"claro";try{localStorage.setItem("hall-tema",c)}catch{}a(c)})}function dt(){let e=document.getElementById("idiomaBoton");if(!e)return;let n=()=>{document.documentElement.lang=D(),e.textContent=D()==="es"?"EN":"ES",f("[data-i18n]").forEach(t=>{t.dataset.en||(t.dataset.en=(t.textContent??"").trim()),t.textContent=s(t.dataset.en)})};n(),e.onclick=()=>{Te(D()==="es"?"en":"es"),n(),k()}}window.addEventListener("error",e=>ne("uncaught error",{mensaje:e.message,fichero:e.filename,linea:e.lineno}));window.addEventListener("unhandledrejection",e=>ne("unhandled rejection",String(e.reason)));rt();dt();ct();k();
|