agents-city 0.3.0-beta.21 → 0.3.0
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 +310 -70
- package/README.md +297 -69
- package/bin/agents-city.js +3 -0
- package/bin/doctor +3 -0
- package/bin/hall.html +164 -24
- package/bin/navegador.mjs +415 -0
- package/bin/serve.py +383 -127
- package/bin/shortcut +3 -0
- package/bin/test +5 -2
- package/bin/test-actualiza.py +130 -0
- package/bin/test-atajos.py +301 -0
- package/bin/test-busca.py +216 -0
- package/bin/test-cage.py +170 -2
- package/bin/test-card.py +2 -2
- package/bin/test-cities.py +45 -0
- package/bin/test-contracts.py +12 -5
- package/bin/test-doctor.py +33 -0
- package/bin/test-navegador.py +164 -0
- package/bin/test-seat.py +245 -25
- package/bin/test-serve.py +214 -9
- package/bin/test-workspace.py +63 -0
- package/bin/testlib.py +23 -0
- package/bin/update +3 -0
- package/city/web/dist/city.js +47 -47
- package/city/web/dist/index.html +1 -1
- package/city/web/dist-hall/hall.js +2193 -174
- package/city/web/src/bienvenida.ts +686 -0
- package/city/web/src/es.ts +180 -0
- package/city/web/src/hall.ts +520 -168
- package/city/web/src/idioma.ts +86 -0
- package/city/web/src/main.ts +27 -0
- package/city/web/src/motores.ts +54 -0
- package/docs/agents-first.md +8 -1
- package/docs/security.md +46 -12
- package/docs/testing.md +1 -1
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/channel/bus.js +1 -1
- package/plugin/channel/bus.ts +1 -1
- package/plugin/channel/runtime/codex.ts +1 -1
- package/plugin/channel/runtime-gateway.js +1 -1
- package/plugin/scripts/actualiza.py +198 -0
- package/plugin/scripts/atajos.py +506 -0
- package/plugin/scripts/busca.py +436 -0
- package/plugin/scripts/cage.py +266 -26
- package/plugin/scripts/capabilities.py +17 -10
- package/plugin/scripts/card.py +10 -0
- package/plugin/scripts/cities.py +34 -0
- package/plugin/scripts/city-session.sh +33 -7
- package/plugin/scripts/doctor.py +122 -0
- package/plugin/scripts/find-repos.sh +12 -105
- package/plugin/scripts/read-card.py +6 -2
- package/plugin/scripts/report.py +5 -6
- package/plugin/scripts/reset.py +50 -14
- package/plugin/scripts/seat.py +445 -103
- package/plugin/scripts/workspace.py +197 -0
|
@@ -1,131 +1,1620 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
// city/web/src/idioma.ts
|
|
2
|
+
var ES = {};
|
|
3
|
+
function clave(fuente) {
|
|
4
|
+
return fuente.replace(/\s+/g, " ").trim();
|
|
5
|
+
}
|
|
6
|
+
function anota(pares) {
|
|
7
|
+
for (const [fuente, texto] of Object.entries(pares)) ES[clave(fuente)] = texto;
|
|
8
|
+
}
|
|
9
|
+
var ELEGIDO = "";
|
|
10
|
+
function idioma() {
|
|
11
|
+
if (ELEGIDO === "es" || ELEGIDO === "en") return ELEGIDO;
|
|
12
|
+
try {
|
|
13
|
+
const guardado = localStorage.getItem("hall-idioma");
|
|
14
|
+
if (guardado === "es" || guardado === "en") {
|
|
15
|
+
ELEGIDO = guardado;
|
|
16
|
+
return ELEGIDO;
|
|
17
|
+
}
|
|
18
|
+
} catch {
|
|
19
|
+
}
|
|
20
|
+
const suyo = (navigator.language || "en").toLowerCase();
|
|
21
|
+
ELEGIDO = suyo.startsWith("es") ? "es" : "en";
|
|
22
|
+
return ELEGIDO;
|
|
23
|
+
}
|
|
24
|
+
function ponIdioma(cual) {
|
|
25
|
+
ELEGIDO = cual;
|
|
26
|
+
try {
|
|
27
|
+
localStorage.setItem("hall-idioma", cual);
|
|
28
|
+
} catch {
|
|
29
|
+
}
|
|
30
|
+
document.documentElement.lang = cual;
|
|
31
|
+
}
|
|
32
|
+
function t(fuente, valores) {
|
|
33
|
+
const texto = idioma() === "es" ? ES[clave(fuente)] ?? fuente : fuente;
|
|
34
|
+
if (!valores) return texto;
|
|
35
|
+
return texto.replace(
|
|
36
|
+
/\{(\w+)\}/g,
|
|
37
|
+
(_, clave2) => valores[clave2] === void 0 ? `{${clave2}}` : String(valores[clave2])
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
function plural(n, una, varias) {
|
|
41
|
+
return t(n === 1 ? una : varias, { n: String(n) });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// city/web/src/es.ts
|
|
45
|
+
anota({
|
|
46
|
+
// ── the rail ──────────────────────────────────────────────────────────────
|
|
47
|
+
Overview: "Resumen",
|
|
48
|
+
"The map": "El mapa",
|
|
49
|
+
"My seat": "Mi asiento",
|
|
50
|
+
Districts: "Distritos",
|
|
51
|
+
Roads: "Carreteras",
|
|
52
|
+
Committee: "Comit\xE9",
|
|
53
|
+
Houses: "Casas",
|
|
54
|
+
"All cities": "Todas las ciudades",
|
|
55
|
+
"you are in": "est\xE1s en",
|
|
56
|
+
"{n} house": "{n} casa",
|
|
57
|
+
"{n} houses": "{n} casas",
|
|
58
|
+
"Create another city \u2192": "Crear otra ciudad \u2192",
|
|
59
|
+
// ── the guide ─────────────────────────────────────────────────────────────
|
|
60
|
+
Welcome: "Bienvenida",
|
|
61
|
+
"The work": "El trabajo",
|
|
62
|
+
"Your chair": "Tu silla",
|
|
63
|
+
"The houses": "Las casas",
|
|
64
|
+
Ready: "Listo",
|
|
65
|
+
"A city is you, and the houses around you": "Una ciudad eres t\xFA y las casas que te rodean",
|
|
66
|
+
"Let\u2019s build it": "Vamos a construirla",
|
|
67
|
+
"I\u2019ll set it up myself": "Ya lo configuro yo",
|
|
68
|
+
"What kind of work happens here?": "\xBFQu\xE9 clase de trabajo se hace aqu\xED?",
|
|
69
|
+
"And what are you, here?": "\xBFY t\xFA qu\xE9 eres aqu\xED?",
|
|
70
|
+
"Who lives in your city?": "\xBFQui\xE9n vive en tu ciudad?",
|
|
71
|
+
"Who else lives here?": "\xBFQui\xE9n m\xE1s vive aqu\xED?",
|
|
72
|
+
"Build the first house": "Construye la primera casa",
|
|
73
|
+
"Add another house": "A\xF1ade otra casa",
|
|
74
|
+
"That is everyone": "Ya est\xE1n todos",
|
|
75
|
+
"Skip \u2014 just me for now": "S\xE1ltalo \u2014 de momento solo yo",
|
|
76
|
+
"a new house": "una casa nueva",
|
|
77
|
+
"Who lives in it?": "\xBFQui\xE9n vive en ella?",
|
|
78
|
+
"What do you call it?": "\xBFC\xF3mo la llamas?",
|
|
79
|
+
"What kind of work does it do?": "\xBFQu\xE9 clase de trabajo hace?",
|
|
80
|
+
"It writes code": "Escribe c\xF3digo",
|
|
81
|
+
"It keeps knowledge": "Guarda conocimiento",
|
|
82
|
+
"It coordinates": "Coordina",
|
|
83
|
+
"What does it work on?": "\xBFSobre qu\xE9 trabaja?",
|
|
84
|
+
"Build it": "Constr\xFAyela",
|
|
85
|
+
"building\u2026": "construyendo\u2026",
|
|
86
|
+
Cancel: "Cancelar",
|
|
87
|
+
Back: "Atr\xE1s",
|
|
88
|
+
Next: "Siguiente",
|
|
89
|
+
"question 1 of 4": "pregunta 1 de 4",
|
|
90
|
+
"question 2 of 4": "pregunta 2 de 4",
|
|
91
|
+
"question 3 of 4": "pregunta 3 de 4",
|
|
92
|
+
"Open my session": "Abrir mi sesi\xF3n",
|
|
93
|
+
"See the houses": "Ver las casas",
|
|
94
|
+
"Draw the map": "Dibujar el mapa",
|
|
95
|
+
"Set a goal": "Poner un objetivo",
|
|
96
|
+
"{city} is alive": "{city} est\xE1 viva",
|
|
97
|
+
// ── the houses ────────────────────────────────────────────────────────────
|
|
98
|
+
"the houses of {city}": "las casas de {city}",
|
|
99
|
+
"Who lives in {city}": "Qui\xE9n vive en {city}",
|
|
100
|
+
"+ Build a house": "+ Construir una casa",
|
|
101
|
+
"+ folder": "+ carpeta",
|
|
102
|
+
"+ zip": "+ zip",
|
|
103
|
+
test: "probar",
|
|
104
|
+
engine: "motor",
|
|
105
|
+
effort: "esfuerzo",
|
|
106
|
+
provider: "proveedor",
|
|
107
|
+
growth: "crecimiento",
|
|
108
|
+
"works on": "trabaja sobre",
|
|
109
|
+
skills: "skills",
|
|
110
|
+
"nothing mounted yet": "a\xFAn sin nada montado",
|
|
111
|
+
"none discovered": "ninguna descubierta",
|
|
112
|
+
connected: "conectado",
|
|
113
|
+
idle: "inactivo",
|
|
114
|
+
missing: "no instalado",
|
|
115
|
+
default: "por defecto",
|
|
116
|
+
"custom\u2026": "otro\u2026",
|
|
117
|
+
// ── cities ────────────────────────────────────────────────────────────────
|
|
118
|
+
cities: "ciudades",
|
|
119
|
+
"Your cities": "Tus ciudades",
|
|
120
|
+
"Start another city": "Empezar otra ciudad",
|
|
121
|
+
"Create it": "Crearla",
|
|
122
|
+
open: "abrir",
|
|
123
|
+
archive: "archivar",
|
|
124
|
+
"\xB7 open now": "\xB7 abierta ahora",
|
|
125
|
+
"{n} house of its own": "{n} casa propia",
|
|
126
|
+
"{n} houses of its own": "{n} casas propias",
|
|
127
|
+
"Start this city over": "Empezar esta ciudad de cero",
|
|
128
|
+
"Start over\u2026": "Empezar de cero\u2026",
|
|
129
|
+
// ── the map ───────────────────────────────────────────────────────────────
|
|
130
|
+
"the map": "el mapa",
|
|
131
|
+
"Your city, drawn": "Tu ciudad, dibujada",
|
|
132
|
+
"Draw my city": "Dibujar mi ciudad",
|
|
133
|
+
"Draw it anyway": "Dibujarla igualmente",
|
|
134
|
+
"baking the map \u2014 first time takes a minute": "horneando el mapa \u2014 la primera vez tarda un minuto",
|
|
135
|
+
// ── shared ────────────────────────────────────────────────────────────────
|
|
136
|
+
Saved: "Guardado",
|
|
137
|
+
Copied: "Copiado",
|
|
138
|
+
"reading your city": "leyendo tu ciudad",
|
|
139
|
+
"Could not save": "No se pudo guardar"
|
|
140
|
+
});
|
|
141
|
+
anota({
|
|
142
|
+
// ── the chrome that lives in the page itself ──────────────────────────────
|
|
143
|
+
"town hall": "ayuntamiento",
|
|
144
|
+
"Manage one autonomous city: its seat, repo agents, roads and map. Everything here is a plain file you can also inspect by hand.": "Gobierna una ciudad aut\xF3noma: su asiento, sus agentes, sus carreteras y su mapa. Todo lo que hay aqu\xED es un fichero de texto que tambi\xE9n puedes abrir a mano.",
|
|
145
|
+
// ── looking through the disk ──────────────────────────────────────────────
|
|
146
|
+
"search your disk \u2014 a name, or part of a path": "busca en tu disco \u2014 un nombre, o parte de una ruta",
|
|
147
|
+
"Look through the disk again \u2014 for something cloned a minute ago": "Vuelve a mirar el disco \u2014 por si acabas de clonar algo",
|
|
148
|
+
"Search again": "Buscar otra vez",
|
|
149
|
+
"searching\u2026": "buscando\u2026",
|
|
150
|
+
"looking through your disk\u2026": "mirando en tu disco\u2026",
|
|
151
|
+
"{shown} of {total} places match": "{shown} de {total} sitios encajan",
|
|
152
|
+
"{total} places found \u2014 type to narrow it down": "{total} sitios encontrados \u2014 escribe para acotar",
|
|
153
|
+
"Nothing matches that. Add it by path below.": "Nada encaja con eso. A\xF1\xE1delo por ruta aqu\xED abajo.",
|
|
154
|
+
"Nothing found on this disk \u2014 add folders by path below.": "No hay nada en este disco \u2014 a\xF1ade carpetas por ruta aqu\xED abajo.",
|
|
155
|
+
"\u2026or a path, and Enter": "\u2026o una ruta, y Enter",
|
|
156
|
+
// ── what runs an agent ────────────────────────────────────────────────────
|
|
157
|
+
"Its role \u2014 its speciality, never authority": "Su rol \u2014 su especialidad, nunca su autoridad",
|
|
158
|
+
"What runs it?": "\xBFQu\xE9 lo mueve?",
|
|
159
|
+
"Leave all three on default and it runs the way you do. Model and effort are Claude\u2019s to read; another CLI carries its own flags.": "D\xE9jalos los tres por defecto y funcionar\xE1 como t\xFA. El modelo y el esfuerzo los lee Claude; otra CLI lleva sus propias opciones."
|
|
160
|
+
});
|
|
161
|
+
anota({
|
|
162
|
+
// ── the guide, in prose ───────────────────────────────────────────────────
|
|
163
|
+
"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.",
|
|
164
|
+
"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.",
|
|
165
|
+
"<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.",
|
|
166
|
+
"It decides the vocabulary, the roles you will be offered and what counts as evidence in a decision. A clinic does not ship pull requests, and a law firm does not measure story points. Pick the closest one \u2014 you can change it later.": "Decide el vocabulario, los roles que se te ofrecen y qu\xE9 cuenta como prueba en una decisi\xF3n. Una cl\xEDnica no entrega pull requests, y un despacho no mide story points. Elige el m\xE1s cercano \u2014 luego se cambia.",
|
|
167
|
+
"You chair this city whatever you answer \u2014 this is your <b>speciality</b>, not your authority. It shapes the perspective you bring to a decision and the knowledge files the city writes for you. <b>Blank</b> is a real answer: it means no preset knowledge.": "Presides esta ciudad respondas lo que respondas \u2014 esto es tu <b>especialidad</b>, no tu autoridad. Da forma a la perspectiva con la que entras en una decisi\xF3n y a los ficheros de conocimiento que la ciudad escribe para ti. <b>En blanco</b> es una respuesta de verdad: significa sin conocimiento previo.",
|
|
168
|
+
"Every house holds one worker \u2014 an <b>agent</b> \u2014 with its own window, its own role and its own corner of your disk. A house is <b>not</b> a repository: one can answer for three services and a folder of documents at once, and a house whose work is documents needs no git anywhere.": "Cada casa aloja a un trabajador \u2014 un <b>agente</b> \u2014 con su ventana, su rol y su rinc\xF3n de tu disco. Una casa <b>no</b> es un repositorio: una sola puede responder por tres servicios y una carpeta de documentos a la vez, y una casa cuyo trabajo son documentos no necesita git en ninguna parte.",
|
|
169
|
+
"Everything found on this disk: <b>repositories</b>, the <b>worktrees</b> an isolated agent works in, and <b>folders of documents</b> with no git at all. Pick as many as you like \u2014 this is the part that makes it an agent and not a repo.": "Todo lo que hay en este disco: <b>repositorios</b>, los <b>worktrees</b> en los que trabaja un agente aislado, y <b>carpetas de documentos</b> sin git ninguno. Elige las que quieras \u2014 esta es la parte que lo convierte en un agente y no en un repo.",
|
|
170
|
+
"Working on: {que}": "Trabaja sobre: {que}",
|
|
171
|
+
"Your seat is written and {cuantas}. Everything you just answered is a plain file you can read and edit.": "Tu asiento queda escrito y {cuantas}. Todo lo que acabas de responder es un fichero de texto que puedes leer y editar.",
|
|
172
|
+
"<b>{n}</b> house stands around it": "a su alrededor hay <b>{n}</b> casa",
|
|
173
|
+
"<b>{n}</b> houses stand around it": "a su alrededor hay <b>{n}</b> casas",
|
|
174
|
+
"the city is waiting for its first house": "la ciudad espera su primera casa",
|
|
175
|
+
"What people usually do next:": "Lo que suele hacerse ahora:",
|
|
176
|
+
"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.",
|
|
177
|
+
// ── what a house does for a living ────────────────────────────────────────
|
|
178
|
+
"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.",
|
|
179
|
+
"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.",
|
|
180
|
+
"Its house grows with the decisions it records.": "Su casa crece con las decisiones que deja escritas.",
|
|
181
|
+
"urgencias, api, the handbook \u2014 whatever you would say out loud": "urgencias, api, el manual \u2014 como lo llamar\xEDas en voz alta"
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// city/web/src/motores.ts
|
|
185
|
+
var RUNTIMES = ["claude", "codex", "opencode", "kimi"];
|
|
186
|
+
var MODELOS = ["haiku", "sonnet", "opus", "fable"];
|
|
187
|
+
var NIVEL_ESFUERZO = {
|
|
188
|
+
low: 1,
|
|
189
|
+
medium: 2,
|
|
190
|
+
high: 3,
|
|
191
|
+
xhigh: 4,
|
|
192
|
+
max: 5
|
|
193
|
+
};
|
|
194
|
+
var ESFUERZOS = Object.keys(NIVEL_ESFUERZO);
|
|
195
|
+
function opciones(valores, actual, esc2, porDefecto = "default") {
|
|
196
|
+
return valores.concat(actual && !valores.includes(actual) ? [actual] : []).map(
|
|
197
|
+
(v) => `<option value="${esc2(v)}"${v === actual ? " selected" : ""}>${esc2(v || porDefecto)}</option>`
|
|
198
|
+
).join("");
|
|
199
|
+
}
|
|
200
|
+
function nivelDeMotor(model) {
|
|
201
|
+
const alias = model.toLowerCase();
|
|
202
|
+
if (!alias) return { ancho: 0.5, texto: "default", defecto: true };
|
|
203
|
+
for (const [clave2, ancho] of [
|
|
204
|
+
["fable", 1],
|
|
205
|
+
["opus", 0.8],
|
|
206
|
+
["sonnet", 0.55],
|
|
207
|
+
["haiku", 0.35]
|
|
208
|
+
]) {
|
|
209
|
+
if (alias.includes(clave2)) return { ancho, texto: alias, defecto: false };
|
|
210
|
+
}
|
|
211
|
+
return { ancho: 0.6, texto: alias, defecto: false };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// city/web/src/bienvenida.ts
|
|
215
|
+
var CLASES = [
|
|
216
|
+
[
|
|
217
|
+
"code",
|
|
218
|
+
"It writes code",
|
|
219
|
+
"Its house grows with the pull requests it merges. Mounts repositories and worktrees."
|
|
220
|
+
],
|
|
221
|
+
[
|
|
222
|
+
"knowledge",
|
|
223
|
+
"It keeps knowledge",
|
|
224
|
+
"Its house grows with the documents it writes. Needs no git at all."
|
|
225
|
+
],
|
|
226
|
+
["coordinator", "It coordinates", "Its house grows with the decisions it records."]
|
|
227
|
+
];
|
|
228
|
+
var CLASE_CORTA = { repo: "git", worktree: "wt", docs: "doc" };
|
|
229
|
+
var PASOS = ["Welcome", "The work", "Your chair", "The houses", "Ready"];
|
|
230
|
+
function casita(color) {
|
|
231
|
+
return `<svg viewBox="0 0 120 108" role="img" aria-label="an isometric house" class="bvCasa">
|
|
232
|
+
<polygon points="60,10 112,40 60,70 8,40" fill="${color}" opacity=".95"/>
|
|
233
|
+
<polygon points="8,40 60,70 60,100 8,70" fill="${color}" opacity=".62"/>
|
|
234
|
+
<polygon points="112,40 60,70 60,100 112,70" fill="${color}" opacity=".40"/>
|
|
235
|
+
</svg>`;
|
|
236
|
+
}
|
|
237
|
+
var Bienvenida = class {
|
|
238
|
+
constructor(p) {
|
|
239
|
+
this.p = p;
|
|
240
|
+
}
|
|
241
|
+
paso = 0;
|
|
242
|
+
dominios = [];
|
|
243
|
+
roles = [];
|
|
244
|
+
rolesDeAgente = [];
|
|
245
|
+
repos = [];
|
|
246
|
+
dominio = "";
|
|
247
|
+
rol = "";
|
|
248
|
+
roster = [];
|
|
249
|
+
enCurso = null;
|
|
250
|
+
guardando = false;
|
|
251
|
+
buscandoRepos = false;
|
|
252
|
+
reposListos = false;
|
|
253
|
+
/** What is typed in the mount search box, kept across repaints. */
|
|
254
|
+
filtro = "";
|
|
255
|
+
lienzo = null;
|
|
256
|
+
/** Draw the current step into `lienzo`. The only entry point. */
|
|
257
|
+
async pinta(lienzo) {
|
|
258
|
+
this.lienzo = lienzo;
|
|
259
|
+
if (!this.dominios.length) {
|
|
260
|
+
lienzo.innerHTML = '<p class="cargando">reading the domain packs</p>';
|
|
261
|
+
const [d, ra] = await Promise.all([
|
|
262
|
+
this.p.api("/api/domains"),
|
|
263
|
+
this.p.api("/api/roles?scope=agent")
|
|
264
|
+
]);
|
|
265
|
+
this.dominios = d.domains;
|
|
266
|
+
this.rolesDeAgente = ra.roles;
|
|
267
|
+
}
|
|
268
|
+
lienzo.innerHTML = `<div class="bv">${this.cabecera()}${await this.cuerpo()}</div>`;
|
|
269
|
+
this.enlaza(lienzo);
|
|
270
|
+
}
|
|
271
|
+
cabecera() {
|
|
272
|
+
const puntos = PASOS.map(
|
|
273
|
+
(nombre, i) => `<span class="bvPunto ${i === this.paso ? "aqui" : i < this.paso ? "hecho" : ""}"
|
|
274
|
+
title="${this.p.esc(t(nombre))}"><i></i>${this.p.esc(t(nombre))}</span>`
|
|
275
|
+
).join("");
|
|
276
|
+
return `<div class="bvPasos">${puntos}</div>`;
|
|
277
|
+
}
|
|
278
|
+
async cuerpo() {
|
|
279
|
+
switch (this.paso) {
|
|
280
|
+
case 0:
|
|
281
|
+
return this.pantallaHola();
|
|
282
|
+
case 1:
|
|
283
|
+
return this.pantallaDominio();
|
|
284
|
+
case 2:
|
|
285
|
+
return await this.pantallaRol();
|
|
286
|
+
case 3:
|
|
287
|
+
return this.pantallaAgentes();
|
|
288
|
+
default:
|
|
289
|
+
return this.pantallaFinal();
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
// ── 0. what this is ────────────────────────────────────────────────────────
|
|
293
|
+
pantallaHola() {
|
|
294
|
+
return `
|
|
295
|
+
<div class="bvHola">
|
|
296
|
+
<div class="bvArte">${casita("#7048e8")}${casita("#45b9a7")}${casita("#f2674a")}</div>
|
|
297
|
+
<div>
|
|
298
|
+
<span class="sub">welcome</span>
|
|
299
|
+
<h1>${t("A city is you, and the houses around you")}</h1>
|
|
300
|
+
<p class="prosa">${t(`You take the chair. Around it, one <b>house</b> per worker \u2014 an
|
|
301
|
+
<b>agent</b> \u2014 each with its own window, its own role and its own corner of your
|
|
302
|
+
disk: a repository, three of them, a folder of documents, whatever it actually
|
|
303
|
+
works on. The map draws them as houses that grow with the work done in them, and
|
|
304
|
+
many houses are a city. They never talk to each other behind your back: you chair,
|
|
305
|
+
they answer.`)}</p>
|
|
306
|
+
<p class="prosa">${t("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>
|
|
307
|
+
<p class="prosa">${t("<b>Four questions and you are working.</b> You can skip any of them and change everything later.")}</p>
|
|
308
|
+
<div class="bvBotones">
|
|
309
|
+
<button class="bt ppal" data-bv="siguiente">${t("Let\u2019s build it")}</button>
|
|
310
|
+
<button class="bt" data-bv="salir">${t("I\u2019ll set it up myself")}</button>
|
|
311
|
+
</div>
|
|
312
|
+
</div>
|
|
313
|
+
</div>`;
|
|
314
|
+
}
|
|
315
|
+
// ── 1. the domain ─────────────────────────────────────────────────────────
|
|
316
|
+
pantallaDominio() {
|
|
317
|
+
const tarjetas = this.dominios.map(
|
|
318
|
+
(d) => `
|
|
319
|
+
<button class="bvOpcion ${this.dominio === d.id ? "elegida" : ""}" data-bv="dominio"
|
|
320
|
+
data-id="${this.p.esc(d.id)}">
|
|
321
|
+
<b>${this.p.esc(d.name)}</b><span>${this.p.esc(d.summary)}</span></button>`
|
|
322
|
+
).join("");
|
|
323
|
+
return `
|
|
324
|
+
<div class="bvPaso">
|
|
325
|
+
<span class="sub">${t("question 1 of 4")}</span>
|
|
326
|
+
<h1>${t("What kind of work happens here?")}</h1>
|
|
327
|
+
<p class="prosa">${t(`It decides the vocabulary, the roles you will be offered and what
|
|
328
|
+
counts as evidence in a decision. A clinic does not ship pull requests, and a law
|
|
329
|
+
firm does not measure story points. Pick the closest one \u2014 you can change it later.`)}</p>
|
|
330
|
+
<div class="bvRejilla">${tarjetas}</div>
|
|
331
|
+
<div class="bvBotones">
|
|
332
|
+
<button class="bt" data-bv="atras">${t("Back")}</button>
|
|
333
|
+
<button class="bt ppal" data-bv="siguiente" ${this.dominio ? "" : "disabled"}>${t("Next")}</button>
|
|
334
|
+
</div>
|
|
335
|
+
</div>`;
|
|
336
|
+
}
|
|
337
|
+
// ── 2. your chair ─────────────────────────────────────────────────────────
|
|
338
|
+
async pantallaRol() {
|
|
339
|
+
if (!this.roles.length || this.roles[0]?.id === void 0) {
|
|
340
|
+
const r = await this.p.api(
|
|
341
|
+
"/api/roles?domain=" + encodeURIComponent(this.dominio)
|
|
342
|
+
);
|
|
343
|
+
this.roles = r.roles;
|
|
344
|
+
}
|
|
345
|
+
const tarjetas = this.roles.map(
|
|
346
|
+
(r) => `
|
|
347
|
+
<button class="bvOpcion ${this.rol === r.id ? "elegida" : ""}" data-bv="rol"
|
|
348
|
+
data-id="${this.p.esc(r.id)}">
|
|
349
|
+
<b>${this.p.esc(r.name)}</b><span>${this.p.esc(r.summary || r.trade)}</span></button>`
|
|
350
|
+
).join("");
|
|
351
|
+
return `
|
|
352
|
+
<div class="bvPaso">
|
|
353
|
+
<span class="sub">${t("question 2 of 4")}</span>
|
|
354
|
+
<h1>${t("And what are you, here?")}</h1>
|
|
355
|
+
<p class="prosa">${t(`You chair this city whatever you answer \u2014 this is your
|
|
356
|
+
<b>speciality</b>, not your authority. It shapes the perspective you bring to a
|
|
357
|
+
decision and the knowledge files the city writes for you. <b>Blank</b> is a real
|
|
358
|
+
answer: it means no preset knowledge.`)}</p>
|
|
359
|
+
<div class="bvRejilla">${tarjetas}</div>
|
|
360
|
+
<div class="bvBotones">
|
|
361
|
+
<button class="bt" data-bv="atras">${t("Back")}</button>
|
|
362
|
+
<button class="bt ppal" data-bv="siguiente" ${this.rol ? "" : "disabled"}>Next</button>
|
|
363
|
+
</div>
|
|
364
|
+
</div>`;
|
|
365
|
+
}
|
|
366
|
+
// ── 3. the agents ─────────────────────────────────────────────────────────
|
|
367
|
+
pantallaAgentes() {
|
|
368
|
+
this.buscaReposUnaVez();
|
|
369
|
+
if (!this.enCurso) return this.listaDeAgentes();
|
|
370
|
+
return this.formularioDeAgente(this.enCurso);
|
|
371
|
+
}
|
|
372
|
+
buscaReposUnaVez(otraVez = false) {
|
|
373
|
+
if (this.buscandoRepos || this.reposListos && !otraVez) return;
|
|
374
|
+
this.buscandoRepos = true;
|
|
375
|
+
void this.p.api(
|
|
376
|
+
"/api/misrepos?user=" + encodeURIComponent(this.p.yo) + (otraVez ? "&refresh=1" : "")
|
|
377
|
+
).then((r) => {
|
|
378
|
+
this.repos = r.repos ?? [];
|
|
379
|
+
}).catch(() => {
|
|
380
|
+
this.repos = [];
|
|
381
|
+
}).finally(() => {
|
|
382
|
+
this.buscandoRepos = false;
|
|
383
|
+
this.reposListos = true;
|
|
384
|
+
if (this.lienzo && this.paso === 3) void this.pinta(this.lienzo);
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
listaDeAgentes() {
|
|
388
|
+
const fichas = this.roster.map(
|
|
389
|
+
(a) => `
|
|
390
|
+
<div class="bvFicha">
|
|
391
|
+
<b>${this.p.esc(a.nombre)}</b>
|
|
392
|
+
<span>${this.p.esc(a.clase)} \xB7 ${this.p.esc(a.rol)}</span>
|
|
393
|
+
<span class="de">${a.montajes.length ? this.p.esc(a.montajes.map((m) => m.split("/").pop()).join(", ")) : t("nothing mounted yet")}</span>
|
|
394
|
+
</div>`
|
|
395
|
+
).join("");
|
|
396
|
+
return `
|
|
397
|
+
<div class="bvPaso">
|
|
398
|
+
<span class="sub">${t("question 3 of 4")}</span>
|
|
399
|
+
<h1>${t(this.roster.length ? "Who else lives here?" : "Who lives in your city?")}</h1>
|
|
400
|
+
<p class="prosa">${t(`Every house holds one worker \u2014 an <b>agent</b> \u2014 with its own window,
|
|
401
|
+
its own role and its own corner of your disk. A house is <b>not</b> a repository: one
|
|
402
|
+
can answer for three services and a folder of documents at once, and a house whose
|
|
403
|
+
work is documents needs no git anywhere.`)}</p>
|
|
404
|
+
${this.roster.length ? `<div class="bvFichas">${fichas}</div>` : ""}
|
|
405
|
+
<div class="bvBotones">
|
|
406
|
+
<button class="bt ppal" data-bv="nuevo">${t(
|
|
407
|
+
this.roster.length ? "Add another house" : "Build the first house"
|
|
408
|
+
)}</button>
|
|
409
|
+
<button class="bt" data-bv="siguiente">${t(
|
|
410
|
+
this.roster.length ? "That is everyone" : "Skip \u2014 just me for now"
|
|
411
|
+
)}</button>
|
|
412
|
+
</div>
|
|
413
|
+
</div>`;
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* The places on this disk worth offering for this house, ranked.
|
|
417
|
+
*
|
|
418
|
+
* With nothing typed it is what you touched recently, because on a machine
|
|
419
|
+
* with three hundred clones a list is not an answer. What is typed matches
|
|
420
|
+
* the name and the path both — people look for `rankia` and for `Documents`
|
|
421
|
+
* with equal frequency, and only one of those is a repository name. A house
|
|
422
|
+
* that keeps knowledge sees document folders first, because that is the one
|
|
423
|
+
* kind of agent the old picker could not help at all.
|
|
424
|
+
*/
|
|
425
|
+
buscaEnElDisco(a) {
|
|
426
|
+
const q2 = this.filtro.trim().toLowerCase();
|
|
427
|
+
const quiereDocs = a.clase === "knowledge";
|
|
428
|
+
const puntua = (r) => (r.clase === "docs" ? quiereDocs ? -2 : 1 : 0) + (r.mio ? -1 : 0);
|
|
429
|
+
const encaja = q2 ? this.repos.filter(
|
|
430
|
+
(r) => r.nombre.toLowerCase().includes(q2) || r.ruta.toLowerCase().includes(q2)
|
|
431
|
+
) : this.repos.filter((r) => r.mio);
|
|
432
|
+
const elegidas = this.repos.filter((r) => a.montajes.includes(r.ruta) && !encaja.includes(r));
|
|
433
|
+
return elegidas.concat(encaja).sort((x, y) => puntua(x) - puntua(y)).slice(0, 40);
|
|
434
|
+
}
|
|
435
|
+
formularioDeAgente(a) {
|
|
436
|
+
const clases = CLASES.map(
|
|
437
|
+
([id, titulo, porque]) => `
|
|
438
|
+
<button class="bvOpcion ${a.clase === id ? "elegida" : ""}" data-bv="clase" data-id="${id}">
|
|
439
|
+
<b>${t(titulo)}</b><span>${t(porque)}</span></button>`
|
|
440
|
+
).join("");
|
|
441
|
+
const roles = this.rolesDeAgente.map(
|
|
442
|
+
(r) => `<option value="${this.p.esc(r.id)}"${r.id === a.rol ? " selected" : ""}>${this.p.esc(
|
|
443
|
+
r.name
|
|
444
|
+
)}</option>`
|
|
445
|
+
).join("");
|
|
446
|
+
const esClaude = (a.runtime || "claude") === "claude";
|
|
447
|
+
const encontrados = this.buscaEnElDisco(a);
|
|
448
|
+
const sugeridos = encontrados.map(
|
|
449
|
+
(r) => `<button class="bvChip cl-${this.p.esc(r.clase)} ${a.montajes.includes(r.ruta) ? "elegida" : ""}"
|
|
450
|
+
data-bv="monta" data-ruta="${this.p.esc(r.ruta)}"
|
|
451
|
+
title="${this.p.esc(r.ruta)}${r.cuando ? ` \xB7 ${this.p.esc(r.cuando)}` : ""}"
|
|
452
|
+
><i class="bvClase">${this.p.esc(CLASE_CORTA[r.clase] ?? r.clase)}</i>${this.p.esc(
|
|
453
|
+
r.nombre
|
|
454
|
+
)}</button>`
|
|
455
|
+
).join("");
|
|
456
|
+
return `
|
|
457
|
+
<div class="bvPaso">
|
|
458
|
+
<span class="sub">${t("a new house")}</span>
|
|
459
|
+
<h1>${t("Who lives in it?")}</h1>
|
|
460
|
+
<div class="campo"><label>${t("What do you call it?")}</label>
|
|
461
|
+
<input type="text" id="bvNombre" value="${this.p.esc(a.nombre)}"
|
|
462
|
+
placeholder="${t("urgencias, api, the handbook \u2014 whatever you would say out loud")}"></div>
|
|
463
|
+
<label class="bvEtiqueta">${t("What kind of work does it do?")}</label>
|
|
464
|
+
<div class="bvRejilla bvTres">${clases}</div>
|
|
465
|
+
<div class="campo"><label>${t("Its role \u2014 its speciality, never authority")}</label>
|
|
466
|
+
<select id="bvRol">${roles}</select></div>
|
|
467
|
+
<label class="bvEtiqueta">${t("What runs it?")}</label>
|
|
468
|
+
<p class="pista">${t(
|
|
469
|
+
"Leave all three on default and it runs the way you do. Model and effort are Claude\u2019s to read; another CLI carries its own flags."
|
|
470
|
+
)}</p>
|
|
471
|
+
<div class="bvMotor">
|
|
472
|
+
<div class="campo"><label>${t("provider")}</label>
|
|
473
|
+
<select id="bvRuntime">${opciones(RUNTIMES, a.runtime, this.p.esc, "claude")}</select></div>
|
|
474
|
+
<div class="campo"><label>${t("engine")}</label>
|
|
475
|
+
<select id="bvModelo" ${esClaude ? "" : "disabled"}>${opciones(
|
|
476
|
+
[""].concat(MODELOS),
|
|
477
|
+
a.modelo,
|
|
478
|
+
this.p.esc,
|
|
479
|
+
t("default")
|
|
480
|
+
)}</select></div>
|
|
481
|
+
<div class="campo"><label>${t("effort")}</label>
|
|
482
|
+
<select id="bvEsfuerzo" ${esClaude ? "" : "disabled"}>${opciones(
|
|
483
|
+
[""].concat(ESFUERZOS),
|
|
484
|
+
a.esfuerzo,
|
|
485
|
+
this.p.esc,
|
|
486
|
+
t("default")
|
|
487
|
+
)}</select></div>
|
|
488
|
+
</div>
|
|
489
|
+
<label class="bvEtiqueta">${t("What does it work on?")}</label>
|
|
490
|
+
<p class="pista">${t(`Everything found on this disk: <b>repositories</b>, the
|
|
491
|
+
<b>worktrees</b> an isolated agent works in, and <b>folders of documents</b> with no git
|
|
492
|
+
at all. Pick as many as you like \u2014 this is the part that makes it an agent and not a
|
|
493
|
+
repo.`)}</p>
|
|
494
|
+
<div class="bvBuscador">
|
|
495
|
+
<input type="search" id="bvBusca" value="${this.p.esc(this.filtro)}"
|
|
496
|
+
placeholder="${t("search your disk \u2014 a name, or part of a path")}">
|
|
497
|
+
<button class="bt bvMini" data-bv="rebusca" ${this.buscandoRepos ? "disabled" : ""}
|
|
498
|
+
title="${t("Look through the disk again \u2014 for something cloned a minute ago")}">${t(
|
|
499
|
+
this.buscandoRepos ? "searching\u2026" : "Search again"
|
|
500
|
+
)}</button>
|
|
501
|
+
</div>
|
|
502
|
+
${sugeridos ? `<div class="bvChips">${sugeridos}</div>
|
|
503
|
+
<p class="pista">${this.filtro ? t("{shown} of {total} places match", {
|
|
504
|
+
shown: String(encontrados.length),
|
|
505
|
+
total: String(this.repos.length)
|
|
506
|
+
}) : t("{total} places found \u2014 type to narrow it down", {
|
|
507
|
+
total: String(this.repos.length)
|
|
508
|
+
})}</p>` : this.buscandoRepos ? `<p class="cargando">${t("looking through your disk\u2026")}</p>` : this.filtro ? `<p class="pista">${t("Nothing matches that. Add it by path below.")}</p>` : `<p class="pista">${t("Nothing found on this disk \u2014 add folders by path below.")}</p>`}
|
|
509
|
+
<div class="campo"><label>${t("\u2026or a path, and Enter")}</label>
|
|
510
|
+
<input type="text" id="bvRuta" placeholder="~/documents/handbook"></div>
|
|
511
|
+
${a.montajes.length ? `<p class="pista">${t("Working on: {que}", { que: `<b>${this.p.esc(a.montajes.join(", "))}</b>` })}</p>` : ""}
|
|
512
|
+
<div class="bvBotones">
|
|
513
|
+
<button class="bt" data-bv="cancela">${t("Cancel")}</button>
|
|
514
|
+
<button class="bt ppal" data-bv="guarda" ${this.guardando ? "disabled" : ""}>${t(
|
|
515
|
+
this.guardando ? "building\u2026" : "Build it"
|
|
516
|
+
)}</button>
|
|
517
|
+
</div>
|
|
518
|
+
</div>`;
|
|
519
|
+
}
|
|
520
|
+
// ── 4. done ───────────────────────────────────────────────────────────────
|
|
521
|
+
pantallaFinal() {
|
|
522
|
+
return `
|
|
523
|
+
<div class="bvPaso">
|
|
524
|
+
<span class="sub">${t("Ready").toLowerCase()}</span>
|
|
525
|
+
<h1>${t("{city} is alive", { city: this.p.esc(this.p.ciudad) })}</h1>
|
|
526
|
+
<p class="prosa">${t(
|
|
527
|
+
"Your seat is written and {cuantas}. Everything you just answered is a plain file you can read and edit.",
|
|
528
|
+
{
|
|
529
|
+
cuantas: this.roster.length ? plural(
|
|
530
|
+
this.roster.length,
|
|
531
|
+
"<b>{n}</b> house stands around it",
|
|
532
|
+
"<b>{n}</b> houses stand around it"
|
|
533
|
+
) : t("the city is waiting for its first house")
|
|
534
|
+
}
|
|
535
|
+
)}</p>
|
|
536
|
+
<p class="prosa">${t("What people usually do next:")}</p>
|
|
537
|
+
<div class="bvSiguientes">
|
|
538
|
+
<button class="bt ppal" data-bv="sesion">${t("Open my session")}</button>
|
|
539
|
+
<button class="bt" data-bv="ir-gente">${t("See the houses")}</button>
|
|
540
|
+
<button class="bt" data-bv="ir-mapa">${t("Draw the map")}</button>
|
|
541
|
+
<button class="bt" data-bv="ir-puesto">${t("Set a goal")}</button>
|
|
542
|
+
</div>
|
|
543
|
+
<p class="pista">${t(`A goal is optional, and the city works without one \u2014 but a round with
|
|
544
|
+
no goal is a status report, because there is nothing to argue against.`)}</p>
|
|
545
|
+
</div>`;
|
|
546
|
+
}
|
|
547
|
+
// ── wiring ────────────────────────────────────────────────────────────────
|
|
548
|
+
enlaza(lienzo) {
|
|
549
|
+
const repinta = () => void this.pinta(lienzo);
|
|
550
|
+
lienzo.querySelectorAll("[data-bv]").forEach((el) => {
|
|
551
|
+
el.onclick = async (evento) => {
|
|
552
|
+
evento.preventDefault();
|
|
553
|
+
const que = el.dataset.bv;
|
|
554
|
+
if (que === "salir") return this.p.vete("resumen");
|
|
555
|
+
if (que === "atras") {
|
|
556
|
+
this.paso = Math.max(0, this.paso - 1);
|
|
557
|
+
return repinta();
|
|
558
|
+
}
|
|
559
|
+
if (que === "dominio") {
|
|
560
|
+
this.dominio = el.dataset.id ?? "";
|
|
561
|
+
this.roles = [];
|
|
562
|
+
this.rol = "";
|
|
563
|
+
return repinta();
|
|
564
|
+
}
|
|
565
|
+
if (que === "rol") {
|
|
566
|
+
this.rol = el.dataset.id ?? "";
|
|
567
|
+
return repinta();
|
|
568
|
+
}
|
|
569
|
+
if (que === "nuevo") {
|
|
570
|
+
this.enCurso = {
|
|
571
|
+
nombre: "",
|
|
572
|
+
clase: "code",
|
|
573
|
+
rol: "blank",
|
|
574
|
+
montajes: [],
|
|
575
|
+
runtime: "",
|
|
576
|
+
modelo: "",
|
|
577
|
+
esfuerzo: ""
|
|
578
|
+
};
|
|
579
|
+
return repinta();
|
|
580
|
+
}
|
|
581
|
+
if (que === "clase" && this.enCurso) {
|
|
582
|
+
this.enCurso.clase = el.dataset.id ?? "code";
|
|
583
|
+
this.recogeFormulario(lienzo);
|
|
584
|
+
return repinta();
|
|
585
|
+
}
|
|
586
|
+
if (que === "monta" && this.enCurso) {
|
|
587
|
+
const ruta2 = el.dataset.ruta ?? "";
|
|
588
|
+
const ya = this.enCurso.montajes.indexOf(ruta2);
|
|
589
|
+
if (ya >= 0) this.enCurso.montajes.splice(ya, 1);
|
|
590
|
+
else this.enCurso.montajes.push(ruta2);
|
|
591
|
+
this.recogeFormulario(lienzo);
|
|
592
|
+
return repinta();
|
|
593
|
+
}
|
|
594
|
+
if (que === "rebusca") {
|
|
595
|
+
this.recogeFormulario(lienzo);
|
|
596
|
+
this.buscaReposUnaVez(true);
|
|
597
|
+
return repinta();
|
|
598
|
+
}
|
|
599
|
+
if (que === "cancela") {
|
|
600
|
+
this.enCurso = null;
|
|
601
|
+
return repinta();
|
|
602
|
+
}
|
|
603
|
+
if (que === "guarda") return void await this.guardaAgente(lienzo);
|
|
604
|
+
if (que === "siguiente") return void await this.avanza(lienzo);
|
|
605
|
+
if (que === "sesion") return void await this.abreSesion();
|
|
606
|
+
if (que === "ir-gente") return this.p.vete("gente");
|
|
607
|
+
if (que === "ir-mapa") return this.p.vete("mapa");
|
|
608
|
+
if (que === "ir-puesto") return this.p.vete("puesto");
|
|
609
|
+
};
|
|
610
|
+
});
|
|
611
|
+
const busca = lienzo.querySelector("#bvBusca");
|
|
612
|
+
if (busca) {
|
|
613
|
+
busca.oninput = () => {
|
|
614
|
+
this.filtro = busca.value;
|
|
615
|
+
const donde = busca.selectionStart ?? busca.value.length;
|
|
616
|
+
void this.pinta(lienzo).then(() => {
|
|
617
|
+
const otra = lienzo.querySelector("#bvBusca");
|
|
618
|
+
if (!otra) return;
|
|
619
|
+
otra.focus();
|
|
620
|
+
otra.setSelectionRange(donde, donde);
|
|
621
|
+
});
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
const ruta = lienzo.querySelector("#bvRuta");
|
|
625
|
+
if (ruta)
|
|
626
|
+
ruta.onkeydown = (e) => {
|
|
627
|
+
if (e.key !== "Enter" || !this.enCurso) return;
|
|
628
|
+
e.preventDefault();
|
|
629
|
+
const valor = ruta.value.trim();
|
|
630
|
+
if (valor) this.enCurso.montajes.push(valor);
|
|
631
|
+
this.recogeFormulario(lienzo);
|
|
632
|
+
void this.pinta(lienzo);
|
|
633
|
+
};
|
|
634
|
+
lienzo.querySelector("#bvRuntime")?.addEventListener("change", () => {
|
|
635
|
+
this.recogeFormulario(lienzo);
|
|
636
|
+
void this.pinta(lienzo);
|
|
637
|
+
});
|
|
638
|
+
if (!busca?.value) lienzo.querySelector("#bvNombre")?.focus();
|
|
639
|
+
}
|
|
640
|
+
/** Keep what is typed when a click forces a repaint. */
|
|
641
|
+
recogeFormulario(lienzo) {
|
|
642
|
+
if (!this.enCurso) return;
|
|
643
|
+
const nombre = lienzo.querySelector("#bvNombre");
|
|
644
|
+
const rol = lienzo.querySelector("#bvRol");
|
|
645
|
+
if (nombre) this.enCurso.nombre = nombre.value;
|
|
646
|
+
if (rol) this.enCurso.rol = rol.value;
|
|
647
|
+
for (const [id, campo] of [
|
|
648
|
+
["#bvRuntime", "runtime"],
|
|
649
|
+
["#bvModelo", "modelo"],
|
|
650
|
+
["#bvEsfuerzo", "esfuerzo"]
|
|
651
|
+
]) {
|
|
652
|
+
const sel = lienzo.querySelector(id);
|
|
653
|
+
if (sel) this.enCurso[campo] = sel.value;
|
|
654
|
+
}
|
|
655
|
+
const busca = lienzo.querySelector("#bvBusca");
|
|
656
|
+
if (busca) this.filtro = busca.value;
|
|
657
|
+
}
|
|
658
|
+
async guardaAgente(lienzo) {
|
|
659
|
+
this.recogeFormulario(lienzo);
|
|
660
|
+
const a = this.enCurso;
|
|
661
|
+
if (!a) return;
|
|
662
|
+
if (!a.nombre.trim()) {
|
|
663
|
+
this.p.aviso("Give it a name \u2014 it is how you will call it in its window", true);
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
this.guardando = true;
|
|
667
|
+
await this.pinta(lienzo);
|
|
668
|
+
try {
|
|
669
|
+
await this.aseguraFicha();
|
|
670
|
+
const r = await this.p.api("/api/agentes", {
|
|
671
|
+
method: "POST",
|
|
672
|
+
body: JSON.stringify({ name: a.nombre.trim(), kind: a.clase, role: a.rol })
|
|
673
|
+
});
|
|
674
|
+
if (!r.ok || !r.agent) {
|
|
675
|
+
this.p.aviso(r.error || "Could not add that agent", true);
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
for (const ruta of a.montajes) {
|
|
679
|
+
const m = await this.p.api("/api/montaje", {
|
|
680
|
+
method: "POST",
|
|
681
|
+
body: JSON.stringify({ agent: r.agent, add: ruta })
|
|
682
|
+
});
|
|
683
|
+
if (!m.ok) this.p.aviso(`${ruta}: ${m.error ?? "could not mount"}`, true);
|
|
684
|
+
}
|
|
685
|
+
const motor = {};
|
|
686
|
+
if (a.runtime) motor.runtime = a.runtime;
|
|
687
|
+
if (a.modelo) motor.model = a.modelo;
|
|
688
|
+
if (a.esfuerzo) motor.effort = a.esfuerzo;
|
|
689
|
+
if (Object.keys(motor).length) {
|
|
690
|
+
const m = await this.p.api("/api/agente", {
|
|
691
|
+
method: "POST",
|
|
692
|
+
body: JSON.stringify({ agent: r.agent, ...motor })
|
|
693
|
+
});
|
|
694
|
+
if (!m.ok) this.p.aviso(m.error ?? "Could not set its engine", true);
|
|
695
|
+
}
|
|
696
|
+
this.roster.push({ ...a, nombre: a.nombre.trim() });
|
|
697
|
+
this.enCurso = null;
|
|
698
|
+
this.p.aviso(`${a.nombre.trim()} joined the city`);
|
|
699
|
+
await this.p.refresca();
|
|
700
|
+
} catch (e) {
|
|
701
|
+
this.p.aviso(String(e), true);
|
|
702
|
+
} finally {
|
|
703
|
+
this.guardando = false;
|
|
704
|
+
await this.pinta(lienzo);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
async aseguraFicha() {
|
|
708
|
+
await this.p.api("/api/ficha", {
|
|
709
|
+
method: "POST",
|
|
710
|
+
body: JSON.stringify({ domain: this.dominio || "software", role: this.rol || "blank" })
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
async avanza(lienzo) {
|
|
714
|
+
const escribeLaFicha = this.paso === 2;
|
|
715
|
+
this.paso = Math.min(PASOS.length - 1, this.paso + 1);
|
|
716
|
+
await this.pinta(lienzo);
|
|
717
|
+
if (escribeLaFicha) {
|
|
718
|
+
await this.aseguraFicha();
|
|
719
|
+
await this.p.refresca();
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
async abreSesion() {
|
|
723
|
+
try {
|
|
724
|
+
const r = await this.p.api("/api/sesion", {
|
|
725
|
+
method: "POST",
|
|
726
|
+
body: JSON.stringify({ user: this.p.yo })
|
|
727
|
+
});
|
|
728
|
+
this.p.aviso(
|
|
729
|
+
r.ok && r.attach ? `Session built \u2014 attach with: ${r.attach}` : r.error ?? "Could not",
|
|
730
|
+
!r.ok
|
|
731
|
+
);
|
|
732
|
+
} catch (e) {
|
|
733
|
+
this.p.aviso(String(e), true);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
};
|
|
737
|
+
|
|
738
|
+
// city/web/src/activity.ts
|
|
739
|
+
var MAP_ACTIVITY_PROTOCOL = "agents-city-map-activity/1";
|
|
740
|
+
function isMapNavMessage(value) {
|
|
741
|
+
if (!value || typeof value !== "object") return false;
|
|
742
|
+
const message = value;
|
|
743
|
+
return message.protocol === MAP_ACTIVITY_PROTOCOL && message.type === "map.nav" && typeof message.view === "string";
|
|
744
|
+
}
|
|
745
|
+
function isActivityEvent(value) {
|
|
746
|
+
if (!value || typeof value !== "object") return false;
|
|
747
|
+
const event = value;
|
|
748
|
+
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);
|
|
749
|
+
}
|
|
750
|
+
function isPresenceEvent(event) {
|
|
751
|
+
return event.kind === "conversation.user" || event.kind === "conversation.agent" || event.kind === "runtime.session.started" || event.kind === "runtime.session.ended";
|
|
752
|
+
}
|
|
753
|
+
function isSpeechEvent(event) {
|
|
754
|
+
if (event.kind === "conversation.agent" || event.kind === "conversation.agent.commentary") {
|
|
755
|
+
return Boolean(event.summary.trim());
|
|
756
|
+
}
|
|
757
|
+
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";
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// city/web/src/hall.ts
|
|
761
|
+
var PASE = window.PASE;
|
|
762
|
+
var CIUDAD = new URLSearchParams(location.search).get("city") ?? "";
|
|
763
|
+
async function api(ruta, opts) {
|
|
764
|
+
let u = ruta + (ruta.includes("?") ? "&" : "?") + "PASE=" + encodeURIComponent(PASE);
|
|
765
|
+
if (CIUDAD) u += "&city=" + encodeURIComponent(CIUDAD);
|
|
766
|
+
const r = await fetch(u, {
|
|
767
|
+
headers: { "X-City-Pase": PASE, "Content-Type": "application/json" },
|
|
768
|
+
...opts
|
|
769
|
+
});
|
|
770
|
+
return r.json();
|
|
771
|
+
}
|
|
772
|
+
function q(sel, raiz = document) {
|
|
773
|
+
const el = raiz.querySelector(sel);
|
|
774
|
+
if (!el) throw new Error(`no element matches ${sel}`);
|
|
775
|
+
return el;
|
|
776
|
+
}
|
|
777
|
+
function todos(sel, raiz = document) {
|
|
778
|
+
return [...raiz.querySelectorAll(sel)];
|
|
779
|
+
}
|
|
780
|
+
function esc(s) {
|
|
781
|
+
return String(s ?? "").replace(
|
|
782
|
+
/[&<>"']/g,
|
|
783
|
+
(c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]
|
|
784
|
+
);
|
|
785
|
+
}
|
|
786
|
+
var corto = (p) => p.replace(E.casa, "~");
|
|
787
|
+
var toastTimer = 0;
|
|
788
|
+
function toast(msg, mal = false) {
|
|
789
|
+
const t2 = q("#toast");
|
|
790
|
+
t2.textContent = msg;
|
|
791
|
+
t2.className = "ver" + (mal ? " mal" : "");
|
|
792
|
+
clearTimeout(toastTimer);
|
|
793
|
+
toastTimer = window.setTimeout(() => {
|
|
794
|
+
t2.className = "";
|
|
795
|
+
}, 3200);
|
|
796
|
+
}
|
|
797
|
+
var espera = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
798
|
+
var E;
|
|
799
|
+
var SECCION = "mapa";
|
|
800
|
+
var SECCIONES = [
|
|
801
|
+
["resumen", "Overview"],
|
|
802
|
+
["mapa", "The map"],
|
|
803
|
+
["puesto", "My seat"],
|
|
804
|
+
["barrios", "Districts"],
|
|
805
|
+
["red", "Roads"],
|
|
806
|
+
["committee", "Committee"],
|
|
807
|
+
["gente", "Houses"],
|
|
808
|
+
["ciudades", "Cities"]
|
|
809
|
+
];
|
|
810
|
+
var primeraCarga = true;
|
|
811
|
+
async function refresca() {
|
|
812
|
+
E = await api("/api/estado");
|
|
813
|
+
if (primeraCarga) {
|
|
814
|
+
primeraCarga = false;
|
|
815
|
+
const mia = E.tarjetas.find((t2) => t2.user === E.yo);
|
|
816
|
+
if (!mia || (E.agents ?? []).length === 0) SECCION = "bienvenida";
|
|
817
|
+
else if (!E.mapa) SECCION = "resumen";
|
|
818
|
+
}
|
|
819
|
+
pinta();
|
|
820
|
+
pintaAvisoDeVersion();
|
|
821
|
+
pintaDemoControles();
|
|
822
|
+
sincronizaActividad(E.live_bus);
|
|
823
|
+
}
|
|
824
|
+
function pintaAvisoDeVersion() {
|
|
825
|
+
const caja = document.getElementById("avisoVersion");
|
|
826
|
+
if (!caja) return;
|
|
827
|
+
const texto = (E.update ?? "").trim();
|
|
828
|
+
caja.textContent = texto;
|
|
829
|
+
caja.hidden = !texto;
|
|
830
|
+
}
|
|
831
|
+
function pintaDemoControles() {
|
|
832
|
+
const header = document.querySelector("#livePanel header");
|
|
833
|
+
const previa = document.getElementById("demoControles");
|
|
834
|
+
if (!header || !E.demo) {
|
|
835
|
+
previa?.remove();
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
if (previa) return;
|
|
839
|
+
const caja = document.createElement("div");
|
|
840
|
+
caja.id = "demoControles";
|
|
841
|
+
caja.innerHTML = `<span>guided committee</span><button id="demoReplay" type="button" title="Play the guided committee from the top">\u27F3 replay</button><button id="demoPausa" type="button" title="Pause or resume mid-scene">\u23F8 pause</button>`;
|
|
842
|
+
header.appendChild(caja);
|
|
843
|
+
const pausa = q("#demoPausa", caja);
|
|
844
|
+
const manda = (action) => api("/api/demo", {
|
|
845
|
+
method: "POST",
|
|
846
|
+
body: JSON.stringify({ action })
|
|
847
|
+
});
|
|
848
|
+
q("#demoReplay", caja).onclick = async () => {
|
|
849
|
+
const r = await manda("restart");
|
|
850
|
+
pausa.textContent = "\u23F8 pause";
|
|
851
|
+
if (!r.ok) toast("Could not restart the demo", true);
|
|
852
|
+
};
|
|
853
|
+
pausa.onclick = async () => {
|
|
854
|
+
const antes = pausa.textContent?.includes("resume") ?? false;
|
|
855
|
+
const r = await manda(antes ? "resume" : "pause");
|
|
856
|
+
if (!r.running) {
|
|
857
|
+
pausa.textContent = "\u23F8 pause";
|
|
858
|
+
toast("Nothing playing \u2014 hit replay", true);
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
pausa.textContent = r.paused ? "\u25B6 resume" : "\u23F8 pause";
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
var liveSocket = null;
|
|
865
|
+
var liveUrl = "";
|
|
866
|
+
var liveEvents = [];
|
|
867
|
+
var liveConnected = false;
|
|
868
|
+
var liveRetry = 0;
|
|
869
|
+
var liveSelectedThread = null;
|
|
870
|
+
var liveFilterTouched = false;
|
|
871
|
+
var liveShowWork = false;
|
|
872
|
+
var pendingMapActivity = null;
|
|
873
|
+
function sincronizaActividad(endpoint) {
|
|
874
|
+
if (!endpoint?.online || !endpoint.url) {
|
|
875
|
+
liveConnected = false;
|
|
876
|
+
pintaActividad();
|
|
877
|
+
programaDescubrimiento();
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
if (endpoint.url === liveUrl && liveSocket && (liveSocket.readyState === WebSocket.OPEN || liveSocket.readyState === WebSocket.CONNECTING)) {
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
const anterior = liveSocket;
|
|
884
|
+
if (anterior) {
|
|
885
|
+
anterior.onclose = null;
|
|
886
|
+
anterior.close();
|
|
887
|
+
}
|
|
888
|
+
liveUrl = endpoint.url;
|
|
889
|
+
const socket = new WebSocket(endpoint.url);
|
|
890
|
+
liveSocket = socket;
|
|
891
|
+
liveConnected = false;
|
|
892
|
+
pintaActividad();
|
|
893
|
+
socket.onopen = () => {
|
|
894
|
+
if (liveSocket !== socket) return;
|
|
895
|
+
liveConnected = true;
|
|
896
|
+
liveRetry = 0;
|
|
897
|
+
pintaActividad();
|
|
898
|
+
};
|
|
899
|
+
socket.onmessage = (message) => {
|
|
900
|
+
if (liveSocket !== socket) return;
|
|
901
|
+
let value;
|
|
902
|
+
try {
|
|
903
|
+
value = JSON.parse(String(message.data));
|
|
904
|
+
} catch {
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
if (value.type === "activity.state") {
|
|
908
|
+
liveEvents = Array.isArray(value.events) ? value.events.filter(isActivityEvent) : [];
|
|
909
|
+
liveEvents.sort((a, b) => a.seq - b.seq);
|
|
910
|
+
if (liveSelectedThread === null) liveSelectedThread = newestConversationThread();
|
|
911
|
+
pintaActividad(true);
|
|
912
|
+
} else if (value.type === "activity.event" && isActivityEvent(value.event)) {
|
|
913
|
+
const event = value.event;
|
|
914
|
+
if (!liveEvents.some((item) => item.id === event.id)) liveEvents.push(event);
|
|
915
|
+
liveEvents = liveEvents.sort((a, b) => a.seq - b.seq).slice(-200);
|
|
916
|
+
if (!liveFilterTouched && event.kind === "committee.opened" && event.thread) {
|
|
917
|
+
liveSelectedThread = event.thread;
|
|
918
|
+
}
|
|
919
|
+
pintaActividad(true);
|
|
920
|
+
enviaActividadAlMapa(event);
|
|
921
|
+
}
|
|
922
|
+
};
|
|
923
|
+
socket.onclose = () => {
|
|
924
|
+
if (liveSocket !== socket) return;
|
|
925
|
+
liveSocket = null;
|
|
926
|
+
liveConnected = false;
|
|
927
|
+
pintaActividad();
|
|
928
|
+
programaDescubrimiento();
|
|
929
|
+
};
|
|
930
|
+
socket.onerror = () => {
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
function enviaActividadAlMapa(event) {
|
|
934
|
+
const esComite = event.kind.startsWith("committee.");
|
|
935
|
+
if (!isSpeechEvent(event) && !isPresenceEvent(event) && !esComite) return;
|
|
936
|
+
const frame = document.querySelector("#cityMapFrame");
|
|
937
|
+
if (!frame) return;
|
|
938
|
+
if (frame.dataset.ready !== "1") {
|
|
939
|
+
pendingMapActivity = event;
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
const origin = new URL(frame.src, location.href).origin;
|
|
943
|
+
frame.contentWindow?.postMessage(
|
|
944
|
+
{ protocol: MAP_ACTIVITY_PROTOCOL, type: "activity.event", event },
|
|
945
|
+
origin
|
|
946
|
+
);
|
|
947
|
+
}
|
|
948
|
+
function programaDescubrimiento() {
|
|
949
|
+
if (liveRetry) return;
|
|
950
|
+
liveRetry = window.setTimeout(async () => {
|
|
951
|
+
liveRetry = 0;
|
|
952
|
+
try {
|
|
953
|
+
sincronizaActividad(await api("/api/live"));
|
|
954
|
+
} catch {
|
|
955
|
+
programaDescubrimiento();
|
|
956
|
+
}
|
|
957
|
+
}, 1500);
|
|
958
|
+
}
|
|
959
|
+
function pintaActividad(seguir = false) {
|
|
960
|
+
const dot = document.querySelector("#liveDot");
|
|
961
|
+
const state = document.querySelector("#liveState");
|
|
962
|
+
const list = document.querySelector("#liveEvents");
|
|
963
|
+
const filter = document.querySelector("#liveFilter");
|
|
964
|
+
const context = document.querySelector("#liveContext");
|
|
965
|
+
const workToggle = document.querySelector("#liveWorkToggle");
|
|
966
|
+
if (!dot || !state || !list || !filter || !context || !workToggle) return;
|
|
967
|
+
dot.classList.toggle("on", liveConnected);
|
|
968
|
+
state.textContent = liveConnected ? "websocket live" : liveUrl ? "reconnecting" : "session offline";
|
|
969
|
+
const opened = /* @__PURE__ */ new Map();
|
|
970
|
+
for (const event of liveEvents) {
|
|
971
|
+
if (event.thread && (event.kind === "committee.opened" || event.kind === "conversation.user") && !esPromptDeTransporte(event) && !opened.has(event.thread)) {
|
|
972
|
+
opened.set(event.thread, event.summary);
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
filter.innerHTML = '<option value="">all conversations</option>' + [...opened.entries()].reverse().map(
|
|
976
|
+
([thread, question]) => `<option value="${esc(thread)}">${esc(question.slice(0, 54) || thread)}</option>`
|
|
977
|
+
).join("");
|
|
978
|
+
const selected = liveSelectedThread || "";
|
|
979
|
+
filter.value = [...filter.options].some((option) => option.value === selected) ? selected : "";
|
|
980
|
+
filter.onchange = () => {
|
|
981
|
+
liveSelectedThread = filter.value;
|
|
982
|
+
liveFilterTouched = true;
|
|
983
|
+
pintaActividad(true);
|
|
984
|
+
};
|
|
985
|
+
workToggle.classList.toggle("on", liveShowWork);
|
|
986
|
+
workToggle.setAttribute("aria-pressed", String(liveShowWork));
|
|
987
|
+
workToggle.textContent = liveShowWork ? "hide work" : "show work";
|
|
988
|
+
workToggle.onclick = () => {
|
|
989
|
+
liveShowWork = !liveShowWork;
|
|
990
|
+
pintaActividad(true);
|
|
991
|
+
};
|
|
992
|
+
pintaContexto(filter.value, context);
|
|
993
|
+
const scoped = filter.value ? liveEvents.filter((event) => event.thread === filter.value) : liveEvents;
|
|
994
|
+
const visible = scoped.filter(
|
|
995
|
+
(event) => !esPromptDeTransporte(event) && (liveShowWork || !esRuidoDeTrabajo(event) || event.tone === "error")
|
|
996
|
+
);
|
|
997
|
+
if (!visible.length) {
|
|
998
|
+
list.innerHTML = `<li class="liveEmpty">${liveConnected ? "The bus is live. Questions, positions and moderated replies will appear here." : "Start the city session. Its visible conversation will appear here as it happens."}</li>`;
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
const wasNearBottom = list.scrollHeight - list.scrollTop - list.clientHeight < 90;
|
|
1002
|
+
list.innerHTML = visible.map(renderActividad).join("");
|
|
1003
|
+
if (seguir || wasNearBottom) list.scrollTop = list.scrollHeight;
|
|
1004
|
+
}
|
|
1005
|
+
function renderActividad(event) {
|
|
1006
|
+
const when = new Date(event.at);
|
|
1007
|
+
const time = Number.isNaN(when.getTime()) ? "" : when.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
|
1008
|
+
const side = event.role === "chair" ? "chair" : event.role === "member" ? "member" : "system";
|
|
1009
|
+
const role = rolOperativo(event.actor);
|
|
1010
|
+
const route = event.target && !["committee", "seat"].includes(event.target) ? `to ${event.target}` : event.target === "committee" ? "to the committee" : "";
|
|
1011
|
+
const summary = event.summary.trim();
|
|
1012
|
+
const compact = resumenVisible(summary);
|
|
1013
|
+
const long = compact !== summary;
|
|
1014
|
+
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>${esc(detail)}</li>`).join("")}</ul></details>` : "";
|
|
1015
|
+
const full = long ? `<details class="liveFull"><summary>read full message</summary><p>${esc(summary)}</p></details>` : "";
|
|
1016
|
+
return `<li class="liveTurn ${side} ${esc(event.tone)}">
|
|
1017
|
+
<div class="liveAvatar${E?.avatars?.[event.actor] ? " conCara" : ""}"
|
|
1018
|
+
style="--actor-hue:${tonoActor(event.actor)}" aria-hidden="true">
|
|
1019
|
+
${cara(event.actor, event.role === "chair")}
|
|
11
1020
|
</div>
|
|
12
1021
|
<article class="liveBubble">
|
|
13
|
-
<div class="liveMeta"><span class="liveActor">${
|
|
14
|
-
${
|
|
15
|
-
${
|
|
16
|
-
<div class="liveHeading">${
|
|
17
|
-
<p>${
|
|
1022
|
+
<div class="liveMeta"><span class="liveActor">${esc(event.actor)}</span>
|
|
1023
|
+
${role ? `<span class="liveRole">${esc(role)}</span>` : ""}
|
|
1024
|
+
${route ? `<span>${esc(route)}</span>` : ""}<span class="liveWhen">${esc(time)}</span></div>
|
|
1025
|
+
<div class="liveHeading">${icono(event.kind)}<h3>${esc(tituloVisible(event))}</h3></div>
|
|
1026
|
+
<p>${esc(compact)}</p>${full}${details}
|
|
18
1027
|
</article>
|
|
19
|
-
</li
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
1028
|
+
</li>`;
|
|
1029
|
+
}
|
|
1030
|
+
function newestConversationThread() {
|
|
1031
|
+
return [...liveEvents].reverse().find(
|
|
1032
|
+
(event) => Boolean(event.thread) && (event.kind === "committee.opened" || event.kind === "conversation.user" && !esPromptDeTransporte(event))
|
|
1033
|
+
)?.thread || "";
|
|
1034
|
+
}
|
|
1035
|
+
function pintaContexto(thread, container) {
|
|
1036
|
+
const deliberation = thread ? E?.deliberations?.find((item) => item.id === thread) : void 0;
|
|
1037
|
+
const conversation = thread ? liveEvents.filter((event) => event.thread === thread) : [];
|
|
1038
|
+
const opened = conversation.find((event) => event.kind === "committee.opened");
|
|
1039
|
+
if (!deliberation && !opened) {
|
|
1040
|
+
container.innerHTML = "";
|
|
1041
|
+
container.hidden = true;
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
const invited = opened?.details.find((detail) => detail.startsWith("Invited:"))?.slice("Invited:".length).split(",").map((actor) => actor.trim()).filter(Boolean) || [];
|
|
1045
|
+
const participants = deliberation?.participants || invited;
|
|
1046
|
+
const received = deliberation?.received ?? new Set(
|
|
1047
|
+
conversation.filter((event) => event.kind === "committee.position.submitted").map((event) => event.actor)
|
|
1048
|
+
).size;
|
|
1049
|
+
const total = deliberation?.total ?? participants.length;
|
|
1050
|
+
const status = conversation.at(-1)?.phase || deliberation?.status || "starting";
|
|
1051
|
+
const actors = ["seat", ...participants];
|
|
1052
|
+
container.hidden = false;
|
|
1053
|
+
container.innerHTML = `<div class="livePeople">${actors.map(
|
|
1054
|
+
(actor) => `<span class="livePerson${E?.avatars?.[actor] ? " conCara" : ""}" title="${esc(
|
|
1055
|
+
actor === "seat" ? "seat \xB7 chair" : `${actor} \xB7 ${rolOperativo(actor) || "member"}`
|
|
1056
|
+
)}" style="--actor-hue:${tonoActor(actor)}">${cara(actor, actor === "seat")}</span>`
|
|
1057
|
+
).join("")}</div><span class="liveContextText"><b>seat moderates</b> \xB7 ${esc(
|
|
1058
|
+
received
|
|
1059
|
+
)}/${esc(total)} positions \xB7 ${esc(status)}</span>`;
|
|
1060
|
+
}
|
|
1061
|
+
function esPromptDeTransporte(event) {
|
|
1062
|
+
if (event.kind !== "conversation.user") return false;
|
|
1063
|
+
const summary = event.summary.trimStart();
|
|
1064
|
+
return summary.startsWith("[Agents City authenticated local bus]") || summary.startsWith("<channel") && summary.slice(0, 500).includes("plugin:city:city-bus");
|
|
1065
|
+
}
|
|
1066
|
+
function esRuidoDeTrabajo(event) {
|
|
1067
|
+
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";
|
|
1068
|
+
}
|
|
1069
|
+
function rolOperativo(actor) {
|
|
1070
|
+
if (actor === "seat") return "chair";
|
|
1071
|
+
return E?.skills?.[actor]?.role || "";
|
|
1072
|
+
}
|
|
1073
|
+
function cara(actor, esChair) {
|
|
1074
|
+
const uri = E?.avatars?.[actor];
|
|
1075
|
+
if (uri && uri.startsWith("data:image/svg+xml;base64,")) {
|
|
1076
|
+
return `<img class="liveCara" src="${esc(uri)}" alt="">`;
|
|
1077
|
+
}
|
|
1078
|
+
return esChair ? icono("chair") : esc(iniciales(actor));
|
|
1079
|
+
}
|
|
1080
|
+
function iniciales(actor) {
|
|
1081
|
+
const parts = actor.split(/[-_.\s]+/).filter(Boolean);
|
|
1082
|
+
const initials = parts.length > 1 ? parts.slice(0, 2).map((part) => part[0]).join("") : actor.slice(0, 2);
|
|
1083
|
+
return initials.toUpperCase();
|
|
1084
|
+
}
|
|
1085
|
+
function tonoActor(actor) {
|
|
1086
|
+
let hash = 17;
|
|
1087
|
+
for (const character of actor) hash = (hash * 31 + character.charCodeAt(0)) % 360;
|
|
1088
|
+
return hash;
|
|
1089
|
+
}
|
|
1090
|
+
function resumenVisible(summary) {
|
|
1091
|
+
if (summary.length <= 520) return summary;
|
|
1092
|
+
const boundary = summary.slice(0, 520).lastIndexOf(" ");
|
|
1093
|
+
return summary.slice(0, boundary > 360 ? boundary : 520).trimEnd() + "\u2026";
|
|
1094
|
+
}
|
|
1095
|
+
function tituloVisible(event) {
|
|
1096
|
+
if (event.kind === "conversation.user")
|
|
1097
|
+
return event.actor === "seat" ? "Question" : "Brief received";
|
|
1098
|
+
if (event.kind === "conversation.agent.commentary") return "Working note";
|
|
1099
|
+
if (event.kind === "conversation.agent") return "Response";
|
|
1100
|
+
if (event.kind === "committee.opened") return "Question to the committee";
|
|
1101
|
+
if (event.kind === "committee.position.submitted") return "Position sealed";
|
|
1102
|
+
if (event.kind === "committee.positions.revealed") return "Blind round complete";
|
|
1103
|
+
if (event.kind === "committee.position.revealed") return "Position";
|
|
1104
|
+
if (event.kind === "committee.synthesis.published") return "Chair synthesis";
|
|
1105
|
+
if (event.kind === "committee.floor.requested") return "Requests the floor";
|
|
1106
|
+
if (event.kind === "committee.floor.granted") return "Floor granted";
|
|
1107
|
+
if (event.kind === "committee.floor.denied") return "Floor denied";
|
|
1108
|
+
if (event.kind === "committee.floor.spoke") return "Intervention";
|
|
1109
|
+
if (event.kind === "committee.decision.recorded") return "Decision";
|
|
1110
|
+
if (event.kind.startsWith("committee.verification.")) return "Verification";
|
|
1111
|
+
if (event.kind === "committee.closed") return "Committee closed";
|
|
1112
|
+
return event.title;
|
|
1113
|
+
}
|
|
1114
|
+
function icono(kind) {
|
|
1115
|
+
if (kind === "chair") {
|
|
1116
|
+
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>';
|
|
1117
|
+
}
|
|
1118
|
+
if (kind.includes("question") || kind === "committee.opened" || kind === "conversation.user") {
|
|
1119
|
+
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>';
|
|
1120
|
+
}
|
|
1121
|
+
if (kind.includes("decision") || kind.includes("verification") || kind === "committee.closed") {
|
|
1122
|
+
return '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m5 12 4 4L19 6"/></svg>';
|
|
1123
|
+
}
|
|
1124
|
+
if (kind.includes("floor")) {
|
|
1125
|
+
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>';
|
|
1126
|
+
}
|
|
1127
|
+
if (kind.includes("command") || kind.includes("work.")) {
|
|
1128
|
+
return '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m6 8 4 4-4 4m6 0h6"/></svg>';
|
|
1129
|
+
}
|
|
1130
|
+
return '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 6h14v12H5zM8 10h8m-8 4h5"/></svg>';
|
|
1131
|
+
}
|
|
1132
|
+
function rail() {
|
|
1133
|
+
const cuantos = (E.agents ?? []).length;
|
|
1134
|
+
q("#railCiudad").innerHTML = `<span class="railEtiqueta">${t("you are in")}</span>
|
|
1135
|
+
<b>${esc(E.city_name)}</b>
|
|
1136
|
+
<span class="railDe">${esc(E.domain)} \xB7 ${plural(cuantos, "{n} house", "{n} houses")}</span>`;
|
|
1137
|
+
q("#rail").innerHTML = SECCIONES.filter(([id]) => id !== "ciudades").map(([id, et]) => {
|
|
1138
|
+
const n = id === "gente" ? String(Object.keys(E.skills).length) : id === "committee" ? String(E.deliberations.length) : id === "red" ? String(E.roads.length) : id === "barrios" ? String(E.parcelas.length) : "";
|
|
1139
|
+
return `<li class="${id === SECCION ? "aqui" : ""}" data-s="${id}">
|
|
1140
|
+
<span>${esc(t(et))}</span>${n ? `<span class="n">${n}</span>` : ""}</li>`;
|
|
1141
|
+
}).join("");
|
|
1142
|
+
q("#railOtras").innerHTML = `<li class="${SECCION === "ciudades" ? "aqui" : ""}" data-s="ciudades">
|
|
1143
|
+
<span>${t("All cities")}</span><span class="n">${E.ciudades.length}</span></li>`;
|
|
1144
|
+
todos("#rail li,#railOtras li").forEach((li) => {
|
|
1145
|
+
li.onclick = () => {
|
|
1146
|
+
document.body.classList.remove("enGuia");
|
|
1147
|
+
SECCION = li.dataset.s ?? "mapa";
|
|
1148
|
+
pinta();
|
|
1149
|
+
};
|
|
1150
|
+
});
|
|
1151
|
+
q("#dondeDatos").textContent = corto(E.datos);
|
|
1152
|
+
const cs = q("#ciudades");
|
|
1153
|
+
if (E.ciudades.length > 1) {
|
|
1154
|
+
cs.innerHTML = `<select id="cambiaCiudad" title="which city this hall manages">
|
|
1155
|
+
${E.ciudades.map(
|
|
1156
|
+
(c) => `<option value="${esc(c.ruta)}" ${c.actual ? "selected" : ""}>
|
|
1157
|
+
${esc(c.nombre)}</option>`
|
|
1158
|
+
).join("")}</select>`;
|
|
1159
|
+
q("#cambiaCiudad").onchange = (ev) => {
|
|
1160
|
+
const ruta = ev.target.value;
|
|
1161
|
+
location.href = "/?PASE=" + encodeURIComponent(PASE) + "&city=" + encodeURIComponent(ruta);
|
|
1162
|
+
};
|
|
1163
|
+
} else {
|
|
1164
|
+
cs.innerHTML = "";
|
|
1165
|
+
}
|
|
1166
|
+
const nueva = q("#alWizard");
|
|
1167
|
+
nueva.onclick = async (ev) => {
|
|
1168
|
+
ev.preventDefault();
|
|
1169
|
+
const name = window.prompt("Name of the new city");
|
|
1170
|
+
if (!name?.trim()) return;
|
|
1171
|
+
const r = await api("/api/ciudades", {
|
|
1172
|
+
method: "POST",
|
|
1173
|
+
body: JSON.stringify({ name })
|
|
1174
|
+
});
|
|
1175
|
+
if (r.error || !r.city) return toast(r.error ?? "Could not create the city", true);
|
|
1176
|
+
location.href = "/?PASE=" + encodeURIComponent(PASE) + "&city=" + encodeURIComponent(r.city);
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
1179
|
+
function pinta() {
|
|
1180
|
+
rail();
|
|
1181
|
+
q(".cuerpo").style.padding = "";
|
|
1182
|
+
q("#lienzo").style.maxWidth = "";
|
|
1183
|
+
VISTAS[SECCION]();
|
|
1184
|
+
q(".cuerpo").scrollTop = 0;
|
|
1185
|
+
}
|
|
1186
|
+
var VISTAS = {};
|
|
1187
|
+
var guia = null;
|
|
1188
|
+
VISTAS.bienvenida = () => {
|
|
1189
|
+
document.body.classList.add("enGuia");
|
|
1190
|
+
guia ??= new Bienvenida({
|
|
1191
|
+
api,
|
|
1192
|
+
esc,
|
|
1193
|
+
aviso: toast,
|
|
1194
|
+
refresca,
|
|
1195
|
+
vete: (seccion) => {
|
|
1196
|
+
document.body.classList.remove("enGuia");
|
|
1197
|
+
SECCION = seccion;
|
|
1198
|
+
pinta();
|
|
1199
|
+
},
|
|
1200
|
+
ciudad: E.city_name,
|
|
1201
|
+
yo: E.yo,
|
|
1202
|
+
datos: corto(E.datos)
|
|
1203
|
+
});
|
|
1204
|
+
void guia.pinta(q("#lienzo"));
|
|
1205
|
+
};
|
|
1206
|
+
VISTAS.ciudades = () => {
|
|
1207
|
+
const filas = E.ciudades.map(
|
|
1208
|
+
(c) => `
|
|
1209
|
+
<div class="fila ${c.actual ? "on" : ""}">
|
|
1210
|
+
<span class="et">${esc(c.nombre)}${c.actual ? ' <b class="de">\xB7 open now</b>' : ""}</span>
|
|
1211
|
+
<span class="de">${c.agentes ?? 0} house${(c.agentes ?? 0) === 1 ? "" : "s"} of its own</span>
|
|
1212
|
+
<span class="de mono">${esc(corto(c.ruta))}</span>
|
|
1213
|
+
<span style="margin-left:auto;display:flex;gap:6px">
|
|
1214
|
+
${c.actual ? "" : `<a class="bt mini" href="?city=${encodeURIComponent(c.ruta)}">open</a>
|
|
1215
|
+
<button class="bt mini archiva" data-ciudad="${esc(c.ruta)}"
|
|
1216
|
+
data-nombre="${esc(c.nombre)}">archive</button>`}
|
|
1217
|
+
</span>
|
|
1218
|
+
</div>`
|
|
1219
|
+
).join("");
|
|
1220
|
+
q("#lienzo").innerHTML = `<div><span class="sub">cities</span>
|
|
1221
|
+
<h1 style="margin-top:6px">${t("Your cities")}</h1>
|
|
1222
|
+
<p class="prosa" style="margin-top:8px">One person, several autonomous cities: each has
|
|
1223
|
+
its own identity, domain, chair, <b>its own houses</b> and its own roads. A house is not
|
|
1224
|
+
shared \u2014 it stands inside the city that owns it, with its workspace and mounts under that
|
|
1225
|
+
city's folder \u2014 so two cities can each have a <code class="mono">docs</code> house and
|
|
1226
|
+
they are two different workers. They share nothing unless you build a road between
|
|
1227
|
+
them.</p></div>
|
|
1228
|
+
<div class="lista"><div class="filas">${filas}</div></div>
|
|
1229
|
+
<div class="campos" style="max-width:420px">
|
|
1230
|
+
<div class="campo"><label>${t("Start another city")}</label>
|
|
1231
|
+
<input type="text" id="nuevaCiudad" placeholder="client-a, research, the book\u2026"></div>
|
|
1232
|
+
<button class="bt ppal" id="creaCiudad">Create it</button>
|
|
1233
|
+
</div>
|
|
1234
|
+
<div class="peligro">
|
|
1235
|
+
<h3>${t("Start this city over")}</h3>
|
|
1236
|
+
<p class="prosa">Takes <b>${esc(E.city_name)}</b> back to its first day: no seat, no
|
|
1237
|
+
agents, no committee history, no map. Your repositories and document folders are
|
|
1238
|
+
<b>never touched</b> \u2014 only the city that points at them.</p>
|
|
1239
|
+
<button class="bt malo" id="reiniciaCiudad">${t("Start over\u2026")}</button>
|
|
1240
|
+
</div>
|
|
1241
|
+
<p class="pista">Archiving <b>moves</b> a city into
|
|
1242
|
+
<code class="mono">.backups/</code> \u2014 nothing is deleted, and you can bring it back with
|
|
1243
|
+
<code class="mono">mv</code>. The city you are standing in, and the last one you own,
|
|
1244
|
+
cannot be archived.</p>`;
|
|
1245
|
+
enlaza();
|
|
1246
|
+
const campo = q("#nuevaCiudad");
|
|
1247
|
+
q("#creaCiudad").onclick = async () => {
|
|
1248
|
+
const nombre = campo.value.trim();
|
|
1249
|
+
if (!nombre) {
|
|
1250
|
+
toast("Give it a name", true);
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
1253
|
+
const r = await api("/api/ciudades", {
|
|
1254
|
+
method: "POST",
|
|
1255
|
+
body: JSON.stringify({ name: nombre })
|
|
1256
|
+
});
|
|
1257
|
+
if (!r.ok) {
|
|
1258
|
+
toast(r.error ?? "Could not create it", true);
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
toast(`${nombre} created`);
|
|
1262
|
+
await refresca();
|
|
1263
|
+
};
|
|
1264
|
+
const reiniciar = document.getElementById("reiniciaCiudad");
|
|
1265
|
+
if (reiniciar)
|
|
1266
|
+
reiniciar.onclick = async () => {
|
|
1267
|
+
const previa = await api("/api/ciudad-reinicia", {
|
|
1268
|
+
method: "POST",
|
|
1269
|
+
body: "{}"
|
|
1270
|
+
});
|
|
1271
|
+
if (!previa.preview) {
|
|
1272
|
+
toast(previa.error ?? "Could not read what a reset would do", true);
|
|
1273
|
+
return;
|
|
1274
|
+
}
|
|
1275
|
+
const p = previa.preview;
|
|
1276
|
+
const texto = `Start over ${p.city}?
|
|
1277
|
+
|
|
1278
|
+
You lose:
|
|
1279
|
+
` + p.loses.map((l) => ` \xB7 ${l}`).join("\n") + `
|
|
1280
|
+
|
|
1281
|
+
Right now that is ${p.agents} agent(s), ${p.roads} road(s) and ${p.deliberations} committee act(s).
|
|
1282
|
+
|
|
1283
|
+
You keep:
|
|
1284
|
+
` + p.keeps.map((l) => ` \xB7 ${l}`).join("\n") + `
|
|
1285
|
+
|
|
1286
|
+
Type the city's name to confirm:`;
|
|
1287
|
+
const escrito = window.prompt(texto, "");
|
|
1288
|
+
if (escrito === null) return;
|
|
1289
|
+
if (escrito.trim() !== p.city) {
|
|
1290
|
+
toast("That is not the name \u2014 nothing was changed", true);
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1293
|
+
const r = await api(
|
|
1294
|
+
"/api/ciudad-reinicia",
|
|
1295
|
+
{ method: "POST", body: JSON.stringify({ confirm: p.city }) }
|
|
1296
|
+
);
|
|
1297
|
+
if (!r.ok) {
|
|
1298
|
+
toast(r.error ?? "Could not reset it", true);
|
|
1299
|
+
return;
|
|
1300
|
+
}
|
|
1301
|
+
toast(`${p.city} starts over \u2014 the old one is at ${r.backup ?? "its backup"}`);
|
|
1302
|
+
primeraCarga = true;
|
|
1303
|
+
await refresca();
|
|
1304
|
+
};
|
|
1305
|
+
todos(".archiva").forEach((boton) => {
|
|
1306
|
+
boton.onclick = async () => {
|
|
1307
|
+
const nombre = boton.dataset.nombre ?? "";
|
|
1308
|
+
if (!window.confirm(`Archive ${nombre}? It moves into .backups \u2014 nothing is deleted.`))
|
|
1309
|
+
return;
|
|
1310
|
+
const r = await api(
|
|
1311
|
+
"/api/ciudad-archiva",
|
|
1312
|
+
{ method: "POST", body: JSON.stringify({ city: boton.dataset.ciudad ?? "" }) }
|
|
1313
|
+
);
|
|
1314
|
+
if (!r.ok) {
|
|
1315
|
+
toast(r.error ?? "Could not archive it", true);
|
|
1316
|
+
return;
|
|
1317
|
+
}
|
|
1318
|
+
toast(`${nombre} archived \u2014 it is in ${r.backup ?? ".backups"}`);
|
|
1319
|
+
await refresca();
|
|
1320
|
+
};
|
|
1321
|
+
});
|
|
1322
|
+
};
|
|
1323
|
+
VISTAS.resumen = () => {
|
|
1324
|
+
const mia = E.tarjetas.find((t2) => t2.user === E.yo);
|
|
1325
|
+
const sinUnidad = E.parcelas.filter((p) => p.unidad === "none" || p.unidad === "mine").length;
|
|
1326
|
+
const agentes = E.agents ?? [];
|
|
1327
|
+
const tareas = [
|
|
1328
|
+
{
|
|
1329
|
+
ok: !!mia,
|
|
1330
|
+
txt: mia ? `Your seat is taken \u2014 you are <b>${esc(mia.agent)}</b> in <b>${esc(E.domain)}</b>` : "Take your seat: the work domain and your role in it",
|
|
1331
|
+
ir: "puesto"
|
|
1332
|
+
},
|
|
1333
|
+
{
|
|
1334
|
+
ok: agentes.length > 0,
|
|
1335
|
+
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: ${esc(
|
|
1336
|
+
agentes.map((a) => a.name).join(", ")
|
|
1337
|
+
)}`,
|
|
1338
|
+
ir: "gente"
|
|
1339
|
+
},
|
|
1340
|
+
{
|
|
1341
|
+
ok: agentes.length === 0 || agentes.some((a) => (a.mounts ?? []).length),
|
|
1342
|
+
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",
|
|
1343
|
+
ir: "gente"
|
|
1344
|
+
},
|
|
1345
|
+
{
|
|
1346
|
+
ok: !!(mia && mia.goals_defined),
|
|
1347
|
+
txt: mia && mia.goals_defined ? `Your goal: <b>${esc(mia.objetivo ? mia.objetivo.title : "")}</b>` : "Set <b>one goal</b>, with the command that measures it",
|
|
1348
|
+
ir: "puesto"
|
|
1349
|
+
},
|
|
1350
|
+
{
|
|
1351
|
+
ok: E.parcelas.length === 0 || sinUnidad === 0,
|
|
1352
|
+
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",
|
|
1353
|
+
ir: "barrios"
|
|
1354
|
+
}
|
|
1355
|
+
];
|
|
1356
|
+
const empezando = !mia || agentes.length === 0;
|
|
1357
|
+
const sesionArriba = E.tmux.includes(E.sesion);
|
|
1358
|
+
q("#lienzo").innerHTML = `
|
|
1359
|
+
<div><span class="sub">${empezando ? "welcome" : "your city"}</span>
|
|
1360
|
+
<h1 style="margin-top:6px">${empezando ? "Let\u2019s build " + esc(E.city_name) : esc(E.city_name)}</h1>
|
|
1361
|
+
<p class="de mono" style="margin-top:5px">${esc(E.address)}</p>
|
|
1362
|
+
${empezando ? `<p class="prosa" style="margin-top:8px">A city is a chair and the agents behind
|
|
1363
|
+
it. Two steps and it is alive: take your seat \u2014 the work domain and your role in
|
|
1364
|
+
it \u2014 then add the agents, each one asked for whole: what kind of work it does,
|
|
1365
|
+
its role, everything it works on, the engine that runs it and the skills it
|
|
1366
|
+
starts with. Everything below is a plain file in
|
|
1367
|
+
<code class="mono">${esc(corto(E.datos))}</code> you can also edit by hand.</p>` : `<p class="prosa" style="margin-top:8px">Domain: <b>${esc(E.domain)}</b>. Growth is
|
|
1368
|
+
counted with <code class="mono">${esc(E.grow || "nothing yet")}</code>.</p>`}</div>
|
|
28
1369
|
<div class="cifras">
|
|
29
|
-
<div class="cifra"><b>${Object.keys(
|
|
30
|
-
<div class="cifra"><b>${
|
|
31
|
-
<div class="cifra"><b>${
|
|
32
|
-
<div class="cifra"><b>${
|
|
33
|
-
<div class="cifra"><b>${Object.values(
|
|
1370
|
+
<div class="cifra"><b>${Object.keys(E.skills).length}</b><span>repo agents</span></div>
|
|
1371
|
+
<div class="cifra"><b>${E.parcelas.length}</b><span>houses</span></div>
|
|
1372
|
+
<div class="cifra"><b>${E.roads.length}</b><span>roads</span></div>
|
|
1373
|
+
<div class="cifra"><b>${E.deliberations.length}</b><span>committee acts</span></div>
|
|
1374
|
+
<div class="cifra"><b>${Object.values(E.skills).reduce((n, a) => n + a.skills.length, 0)}</b><span>skills recognised</span></div>
|
|
34
1375
|
</div>
|
|
35
1376
|
<div class="luces">
|
|
36
|
-
<span class="luz ${
|
|
37
|
-
<span class="luz ${
|
|
38
|
-
<span class="luz ${
|
|
39
|
-
<span class="luz ${
|
|
1377
|
+
<span class="luz ${E.tarjetas.length ? "on" : ""}">data repo</span>
|
|
1378
|
+
<span class="luz ${E.gh ? "on" : "neutra"}">github${E.gh ? "" : " \u2014 optional"}</span>
|
|
1379
|
+
<span class="luz ${E.plugin ? "on" : ""}">plugin installed</span>
|
|
1380
|
+
<span class="luz ${sesionArriba ? "on" : "neutra"}">tmux session${sesionArriba ? " up" : ""}</span>
|
|
40
1381
|
</div>
|
|
41
1382
|
<div><span class="sub">what is left</span>
|
|
42
|
-
<div class="tareas" style="margin-top:9px">${
|
|
43
|
-
|
|
44
|
-
|
|
1383
|
+
<div class="tareas" style="margin-top:9px">${tareas.map(
|
|
1384
|
+
(t2) => `
|
|
1385
|
+
<div class="tarea ${t2.ok ? "hecha" : ""}">${t2.txt}
|
|
1386
|
+
${t2.ok ? "" : `<button class="bt mini ir" data-ir="${t2.ir}">go</button>`}</div>`
|
|
1387
|
+
).join("")}
|
|
45
1388
|
</div></div>
|
|
46
1389
|
<div><span class="sub">work</span>
|
|
47
1390
|
<div style="display:flex;flex-direction:column;gap:9px;margin-top:9px">
|
|
48
1391
|
<div class="orden"><span class="et2">your day</span>
|
|
49
|
-
<code>${
|
|
50
|
-
${
|
|
1392
|
+
<code>${sesionArriba ? "tmux attach -t " + esc(E.sesion) : "one window per folder, an agent in each"}</code>
|
|
1393
|
+
${sesionArriba ? `<button class="bt mini" data-copia="tmux attach -t ${esc(E.sesion)}">copy</button>` : '<button class="bt mini ppal" id="abreSesion">open my session</button>'}
|
|
51
1394
|
</div>
|
|
52
1395
|
<div class="orden"><span class="et2">the map</span>
|
|
53
|
-
<code>${
|
|
54
|
-
<button class="bt mini ${
|
|
55
|
-
</div></div
|
|
1396
|
+
<code>${E.mapa ? esc(E.mapa) : "not drawn yet"}</code>
|
|
1397
|
+
<button class="bt mini ${E.mapa ? "" : "ppal"}" data-ir="mapa">${E.mapa ? "open" : "draw it"}</button></div>
|
|
1398
|
+
</div></div>`;
|
|
1399
|
+
enlaza();
|
|
1400
|
+
const b = document.querySelector("#abreSesion");
|
|
1401
|
+
if (b)
|
|
1402
|
+
b.onclick = async () => {
|
|
1403
|
+
b.disabled = true;
|
|
1404
|
+
b.textContent = "building\u2026";
|
|
1405
|
+
const r = await api("/api/sesion", {
|
|
1406
|
+
method: "POST",
|
|
1407
|
+
body: JSON.stringify({ user: E.yo })
|
|
1408
|
+
});
|
|
1409
|
+
await espera(1500);
|
|
1410
|
+
await refresca();
|
|
1411
|
+
toast(
|
|
1412
|
+
r.ok && r.attach ? `Session built \u2014 attach with: ${r.attach}` : "Could not build it",
|
|
1413
|
+
!r.ok
|
|
1414
|
+
);
|
|
1415
|
+
};
|
|
1416
|
+
};
|
|
1417
|
+
VISTAS.mapa = () => {
|
|
1418
|
+
if (E.mapa) {
|
|
1419
|
+
q(".cuerpo").style.padding = "0";
|
|
1420
|
+
q("#lienzo").style.maxWidth = "none";
|
|
1421
|
+
const mapUrl = new URL(E.mapa, location.href);
|
|
1422
|
+
mapUrl.searchParams.set("embed", "1");
|
|
1423
|
+
mapUrl.searchParams.set("parent_origin", location.origin);
|
|
1424
|
+
q("#lienzo").innerHTML = `<iframe id="cityMapFrame" src="${esc(mapUrl.toString())}" title="the map"
|
|
56
1425
|
allow="fullscreen" allowfullscreen
|
|
57
|
-
style="width:100%;height:calc(100vh - 2px);border:0;display:block"></iframe>`;
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
1426
|
+
style="width:100%;height:calc(100vh - 2px);border:0;display:block"></iframe>`;
|
|
1427
|
+
const frame = q("#cityMapFrame");
|
|
1428
|
+
frame.onload = () => {
|
|
1429
|
+
frame.dataset.ready = "1";
|
|
1430
|
+
const origin = new URL(frame.src, location.href).origin;
|
|
1431
|
+
frame.contentWindow?.postMessage(
|
|
1432
|
+
{
|
|
1433
|
+
protocol: MAP_ACTIVITY_PROTOCOL,
|
|
1434
|
+
type: "map.config",
|
|
1435
|
+
roads: E.roads.map((r) => ({ name: r.name, address: r.address })),
|
|
1436
|
+
agents: (E.agents ?? []).map((a) => ({ name: a.name, kind: a.kind })),
|
|
1437
|
+
avatars: E.avatars ?? {}
|
|
1438
|
+
},
|
|
1439
|
+
origin
|
|
1440
|
+
);
|
|
1441
|
+
frame.contentWindow?.postMessage(
|
|
1442
|
+
{ type: "agents-city-map-theme/1", theme: temaActual() },
|
|
1443
|
+
origin
|
|
1444
|
+
);
|
|
1445
|
+
if (pendingMapActivity) {
|
|
1446
|
+
const event = pendingMapActivity;
|
|
1447
|
+
pendingMapActivity = null;
|
|
1448
|
+
enviaActividadAlMapa(event);
|
|
1449
|
+
}
|
|
1450
|
+
};
|
|
1451
|
+
return;
|
|
1452
|
+
}
|
|
1453
|
+
const cuantos = (E.agents ?? []).length;
|
|
1454
|
+
q("#lienzo").innerHTML = `<div><span class="sub">the map</span>
|
|
1455
|
+
<h1 style="margin-top:6px">Your city, drawn</h1>
|
|
1456
|
+
<p class="prosa" style="margin-top:8px">${cuantos ? `<b>${cuantos}</b> agent${cuantos === 1 ? "" : "s"} live here, and the map shows
|
|
1457
|
+
them as houses that grow with the work they do \u2014 with the town hall in the middle
|
|
1458
|
+
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
|
|
1459
|
+
plot \u2014 build one from <b>Houses</b> first.`}</p>
|
|
1460
|
+
<p class="prosa">The map is its own little server, so it is not running until you say so.
|
|
1461
|
+
The first time takes about a minute: it builds the front end and seeds a local database.
|
|
1462
|
+
After that it opens instantly, and it is yours \u2014 nothing is uploaded anywhere.</p></div>
|
|
1463
|
+
<div><button class="bt ppal" id="arrancaMapa">${cuantos ? "Draw my city" : "Draw it anyway"}</button></div>
|
|
1464
|
+
<p class="cargando" id="mapaEspera" style="display:none">baking the map \u2014 first time takes a minute</p>`;
|
|
1465
|
+
q("#arrancaMapa").onclick = async () => {
|
|
1466
|
+
q("#arrancaMapa").disabled = true;
|
|
1467
|
+
q("#mapaEspera").style.display = "";
|
|
1468
|
+
await api("/api/mapa", { method: "POST", body: "{}" });
|
|
1469
|
+
for (let i = 0; i < 60; i++) {
|
|
1470
|
+
await espera(3e3);
|
|
1471
|
+
E = await api("/api/estado");
|
|
1472
|
+
if (E.mapa) {
|
|
1473
|
+
pinta();
|
|
1474
|
+
return;
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
toast("It did not come up \u2014 run ./bin/city by hand and look at its output", true);
|
|
1478
|
+
pinta();
|
|
1479
|
+
};
|
|
1480
|
+
};
|
|
1481
|
+
VISTAS.puesto = () => {
|
|
1482
|
+
void puesto();
|
|
1483
|
+
};
|
|
1484
|
+
async function puesto() {
|
|
1485
|
+
const mia = E.tarjetas.find((t2) => t2.user === E.yo);
|
|
1486
|
+
q("#lienzo").innerHTML = `<div><span class="sub">my seat</span>
|
|
1487
|
+
<h1 style="margin-top:6px">${esc(E.yo)}</h1>
|
|
1488
|
+
<p class="prosa" style="margin-top:8px">This is your chair: the work domain, your
|
|
1489
|
+
role inside it, and one goal. The seat stays the boss even when its
|
|
1490
|
+
professional role is blank. Saving writes your card \u2014
|
|
1491
|
+
<code class="mono">${esc(corto(E.datos))}/${esc(E.yo)}.md</code> \u2014 the same file
|
|
1492
|
+
every other door writes, and it never touches your roster.</p></div>
|
|
70
1493
|
<div><span class="sub">work domain</span><div class="rolejilla" id="domains" style="margin-top:9px">
|
|
71
1494
|
<p class="cargando">reading the domain packs</p></div></div>
|
|
72
1495
|
<div><span class="sub">role</span><div class="rolejilla" id="roles" style="margin-top:9px">
|
|
73
1496
|
<p class="cargando">reading the role files</p></div></div>
|
|
74
|
-
<div><span class="sub">
|
|
75
|
-
<p class="
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
<div id="agentRoles" style="margin-top:9px"></div></div>
|
|
1497
|
+
<div><span class="sub">the agents</span>
|
|
1498
|
+
<p class="prosa" style="margin-top:8px">Who works in this city \u2014 and what each
|
|
1499
|
+
one works on \u2014 lives in <b>Agents & skills</b>, where an agent is asked for
|
|
1500
|
+
whole: its kind, its role, its repositories and document folders, its engine
|
|
1501
|
+
and its skills. One place, so the terminal and this page cannot disagree.
|
|
1502
|
+
<button class="bt" type="button" data-ir="gente" style="margin-left:8px">Open the roster</button></p></div>
|
|
81
1503
|
<div><span class="sub">one goal \u2014 optional</span><div class="campos" id="meta" style="margin-top:9px"></div></div>
|
|
82
1504
|
<div style="display:flex;gap:10px;align-items:center">
|
|
83
1505
|
<button class="bt ppal" id="guardaPuesto">Save my seat</button>
|
|
84
|
-
<span class="cargando" id="puestoEstado" style="display:none">writing</span></div>`;
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
<
|
|
94
|
-
<
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
1506
|
+
<span class="cargando" id="puestoEstado" style="display:none">writing</span></div>`;
|
|
1507
|
+
const domainsR = (await api("/api/domains")).domains;
|
|
1508
|
+
let domain = E.domain || E.kind || "software";
|
|
1509
|
+
let rol = mia ? mia.role : "";
|
|
1510
|
+
let rolesR = [];
|
|
1511
|
+
const pintaRoles = () => {
|
|
1512
|
+
q("#roles").innerHTML = rolesR.map(
|
|
1513
|
+
(r) => `
|
|
1514
|
+
<button class="rol ${r.id === rol ? "on" : ""}" data-r="${esc(r.id)}">
|
|
1515
|
+
<h3>${esc(r.name)}</h3><span class="de">${esc(r.id)} \xB7 ${esc(r.trade.toLowerCase())}</span>
|
|
1516
|
+
<p>${esc(r.summary)}</p></button>`
|
|
1517
|
+
).join("");
|
|
1518
|
+
todos("#roles .rol").forEach((b) => {
|
|
1519
|
+
b.onclick = () => {
|
|
1520
|
+
rol = b.dataset.r ?? "";
|
|
1521
|
+
pintaRoles();
|
|
1522
|
+
};
|
|
1523
|
+
});
|
|
1524
|
+
};
|
|
1525
|
+
const cargaRoles = async () => {
|
|
1526
|
+
rolesR = (await api("/api/roles?domain=" + encodeURIComponent(domain))).roles;
|
|
1527
|
+
if (!rolesR.some((r) => r.id === rol)) rol = "";
|
|
1528
|
+
pintaRoles();
|
|
1529
|
+
};
|
|
1530
|
+
const pintaDominios = () => {
|
|
1531
|
+
q("#domains").innerHTML = domainsR.map(
|
|
1532
|
+
(d) => `
|
|
1533
|
+
<button class="rol ${d.id === domain ? "on" : ""}" data-d="${esc(d.id)}">
|
|
1534
|
+
<h3>${esc(d.name)}</h3><span class="de">${esc(d.id)}</span>
|
|
1535
|
+
<p>${esc(d.summary)}</p></button>`
|
|
1536
|
+
).join("");
|
|
1537
|
+
todos("#domains .rol").forEach((b) => {
|
|
1538
|
+
b.onclick = () => {
|
|
1539
|
+
domain = b.dataset.d ?? "software";
|
|
1540
|
+
pintaDominios();
|
|
1541
|
+
void cargaRoles();
|
|
1542
|
+
};
|
|
1543
|
+
});
|
|
1544
|
+
};
|
|
1545
|
+
pintaDominios();
|
|
1546
|
+
await cargaRoles();
|
|
1547
|
+
const o = mia && mia.objetivo || {};
|
|
1548
|
+
q("#meta").innerHTML = `
|
|
106
1549
|
<div class="campo"><label>The goal, in one line</label>
|
|
107
|
-
<input type="text" id="g_title" value="${
|
|
1550
|
+
<input type="text" id="g_title" value="${esc(o.title)}" placeholder="Concrete enough to argue with \u2014 empty skips it">
|
|
108
1551
|
</div>
|
|
109
1552
|
<div class="dos">
|
|
110
1553
|
<div class="campo"><label>How it is measured</label>
|
|
111
|
-
<input type="text" id="g_signal" value="${
|
|
1554
|
+
<input type="text" id="g_signal" value="${esc(o.signal)}"></div>
|
|
112
1555
|
<div class="campo"><label>The command that returns it, if a command can</label>
|
|
113
|
-
<input type="text" class="mono" id="g_command" value="${
|
|
1556
|
+
<input type="text" class="mono" id="g_command" value="${esc(o.command)}"
|
|
114
1557
|
placeholder="empty for a qualitative goal"></div>
|
|
115
1558
|
</div>
|
|
116
1559
|
<div class="campo"><label>\u2026or who judges it, and how often</label>
|
|
117
|
-
<input type="text" id="g_manual" value="${
|
|
1560
|
+
<input type="text" id="g_manual" value="${esc(o.manual)}"
|
|
118
1561
|
placeholder="in prose: the architect reads the AGENTS.md files on Fridays">
|
|
119
1562
|
<p class="pista">A goal judged by a person is a real goal \u2014 plenty of quality
|
|
120
1563
|
is prose, not a number. Used when the command is empty.</p></div>
|
|
121
1564
|
<div class="dos">
|
|
122
1565
|
<div class="campo"><label>What it returns today</label>
|
|
123
|
-
<input type="text" class="mono" id="g_baseline" value="${
|
|
1566
|
+
<input type="text" class="mono" id="g_baseline" value="${esc(o.baseline)}"></div>
|
|
124
1567
|
<div class="campo"><label>Where it has to get to</label>
|
|
125
|
-
<input type="text" class="mono" id="g_target" value="${
|
|
1568
|
+
<input type="text" class="mono" id="g_target" value="${esc(o.target)}"></div>
|
|
126
1569
|
</div>
|
|
127
1570
|
<div class="campo" style="max-width:240px"><label>By when</label>
|
|
128
|
-
<input type="text" id="g_by" value="${
|
|
1571
|
+
<input type="text" id="g_by" value="${esc(o.by ?? "this quarter")}"></div>`;
|
|
1572
|
+
const val = (id) => q(id).value.trim();
|
|
1573
|
+
q("#guardaPuesto").onclick = async () => {
|
|
1574
|
+
if (!rol) {
|
|
1575
|
+
toast("Pick a role \u2014 it is the name your seat answers to", true);
|
|
1576
|
+
return;
|
|
1577
|
+
}
|
|
1578
|
+
q("#puestoEstado").style.display = "";
|
|
1579
|
+
const r = await api("/api/ficha", {
|
|
1580
|
+
method: "POST",
|
|
1581
|
+
body: JSON.stringify({
|
|
1582
|
+
user: E.yo,
|
|
1583
|
+
domain,
|
|
1584
|
+
role: rol,
|
|
1585
|
+
objetivo: {
|
|
1586
|
+
title: val("#g_title"),
|
|
1587
|
+
signal: val("#g_signal"),
|
|
1588
|
+
command: val("#g_command"),
|
|
1589
|
+
manual: val("#g_manual"),
|
|
1590
|
+
baseline: val("#g_baseline"),
|
|
1591
|
+
target: val("#g_target"),
|
|
1592
|
+
by: val("#g_by") || "this quarter"
|
|
1593
|
+
}
|
|
1594
|
+
})
|
|
1595
|
+
});
|
|
1596
|
+
await refresca();
|
|
1597
|
+
toast(
|
|
1598
|
+
r.ok ? `Card written \u2014 you are ${r.user ?? E.yo}` : r.error ?? "It could not write",
|
|
1599
|
+
!r.ok
|
|
1600
|
+
);
|
|
1601
|
+
};
|
|
1602
|
+
}
|
|
1603
|
+
VISTAS.barrios = () => {
|
|
1604
|
+
const unidades = E.unidades.map((u) => ({ ...u }));
|
|
1605
|
+
const lab = new Set(E.lab);
|
|
1606
|
+
const porRepo = {};
|
|
1607
|
+
for (const p of E.parcelas) {
|
|
1608
|
+
(porRepo[p.repo] = porRepo[p.repo] ?? []).push({
|
|
1609
|
+
ruta: p.ruta,
|
|
1610
|
+
unidad: p.unidad,
|
|
1611
|
+
nombre: p.nombre
|
|
1612
|
+
});
|
|
1613
|
+
}
|
|
1614
|
+
const slug = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 20);
|
|
1615
|
+
const render = () => {
|
|
1616
|
+
const idsValidos = /* @__PURE__ */ new Set([...unidades.map((u) => u.id), "none"]);
|
|
1617
|
+
q("#lienzo").innerHTML = `<div><span class="sub">districts & houses</span>
|
|
129
1618
|
<h1 style="margin-top:6px">The modelling no tool can do for you</h1>
|
|
130
1619
|
<p class="prosa" style="margin-top:8px">A house is <b>not a repo</b> \u2014 it is a
|
|
131
1620
|
parcel, a slice of one serving a single business unit. Split the interesting
|
|
@@ -143,47 +1632,179 @@ function Z(e,a){return Object.fromEntries([...e].sort().map(i=>[i,a[i]||"blank"]
|
|
|
143
1632
|
<div style="display:flex;gap:10px;align-items:center">
|
|
144
1633
|
<button class="bt ppal" id="guardaBarrios">Save districts & houses</button>
|
|
145
1634
|
<span style="font-size:12px;color:var(--tinta3);font-style:italic">
|
|
146
|
-
writes units.yml and parcels.yml \u2014 reseed the map to see it drawn</span></div
|
|
1635
|
+
writes units.yml and parcels.yml \u2014 reseed the map to see it drawn</span></div>`;
|
|
1636
|
+
q("#unidades").innerHTML = unidades.map(
|
|
1637
|
+
(u, i) => `
|
|
147
1638
|
<div class="tr">
|
|
148
|
-
<input type="text" value="${
|
|
149
|
-
<span class="id">${
|
|
150
|
-
<span class="pips" data-i="${
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
<
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
1639
|
+
<input type="text" value="${esc(u.name)}" data-i="${i}" style="flex:1">
|
|
1640
|
+
<span class="id">${esc(u.id)}</span>
|
|
1641
|
+
<span class="pips" data-i="${i}">${E.paleta.map(
|
|
1642
|
+
(p) => `
|
|
1643
|
+
<button class="pip ${p.hex === u.color ? "on" : ""}" data-c="${p.hex}"
|
|
1644
|
+
style="background:#${p.hex}" title="${esc(p.nombre)}"></button>`
|
|
1645
|
+
).join("")}</span>
|
|
1646
|
+
<button class="x" data-x="${i}">\xD7</button></div>`
|
|
1647
|
+
).join("") || '<div class="tr" style="color:var(--tinta3);font-size:12.5px">No districts yet \u2014 everything sits in \u201Cno unit\u201D.</div>';
|
|
1648
|
+
q("#parcelas").innerHTML = Object.keys(porRepo).sort().map(
|
|
1649
|
+
(repo) => `
|
|
1650
|
+
<div class="repoBloque" data-repo="${esc(repo)}">
|
|
1651
|
+
<div class="repoCab"><span>${esc(repo)}</span>
|
|
1652
|
+
<button class="bt mini" data-split="${esc(repo)}">split</button>
|
|
1653
|
+
<label class="lab"><input type="checkbox" data-lab="${esc(repo)}"
|
|
1654
|
+
${lab.has(repo) ? "checked" : ""}> lab</label></div>
|
|
1655
|
+
${(porRepo[repo] ?? []).map(
|
|
1656
|
+
(f, j) => `
|
|
160
1657
|
<div class="parcela">
|
|
161
|
-
<input type="text" value="${
|
|
162
|
-
<input type="text" class="mono" value="${
|
|
1658
|
+
<input type="text" value="${esc(f.nombre)}" data-k="nombre" data-r="${esc(repo)}" data-j="${j}" title="name on the map">
|
|
1659
|
+
<input type="text" class="mono" value="${esc(f.ruta)}" data-k="ruta" data-r="${esc(repo)}" data-j="${j}"
|
|
163
1660
|
placeholder="glob inside the repo \u2014 empty = the whole repo">
|
|
164
|
-
<select data-k="unidad" data-r="${
|
|
165
|
-
${[...
|
|
166
|
-
|
|
1661
|
+
<select data-k="unidad" data-r="${esc(repo)}" data-j="${j}">
|
|
1662
|
+
${[...unidades.map((u) => u.id), "none"].map(
|
|
1663
|
+
(id) => `
|
|
1664
|
+
<option value="${esc(id)}" ${(idsValidos.has(f.unidad) ? f.unidad : "none") === id ? "selected" : ""}>${esc(id)}</option>`
|
|
1665
|
+
).join("")}
|
|
167
1666
|
</select>
|
|
168
|
-
${(
|
|
169
|
-
</div>`
|
|
170
|
-
|
|
1667
|
+
${(porRepo[repo] ?? []).length > 1 ? `<button class="x" data-quita="${esc(repo)}" data-j="${j}">\xD7</button>` : "<span></span>"}
|
|
1668
|
+
</div>`
|
|
1669
|
+
).join("")}
|
|
1670
|
+
</div>`
|
|
1671
|
+
).join("") || '<p class="prosa">No houses yet. Pick your folders in <b>My seat</b> and each becomes one.</p>';
|
|
1672
|
+
todos("#unidades input[data-i]").forEach((inp) => {
|
|
1673
|
+
inp.oninput = () => {
|
|
1674
|
+
const u = unidades[Number(inp.dataset.i)];
|
|
1675
|
+
if (!u) return;
|
|
1676
|
+
u.name = inp.value;
|
|
1677
|
+
u.id = slug(inp.value) || u.id;
|
|
1678
|
+
const idEl = inp.closest(".tr")?.querySelector(".id");
|
|
1679
|
+
if (idEl) idEl.textContent = u.id;
|
|
1680
|
+
};
|
|
1681
|
+
});
|
|
1682
|
+
todos("#unidades .pip").forEach((b) => {
|
|
1683
|
+
b.onclick = () => {
|
|
1684
|
+
const grupo = b.closest(".pips");
|
|
1685
|
+
const u = unidades[Number(grupo?.dataset.i)];
|
|
1686
|
+
if (u) {
|
|
1687
|
+
u.color = b.dataset.c ?? u.color;
|
|
1688
|
+
render();
|
|
1689
|
+
}
|
|
1690
|
+
};
|
|
1691
|
+
});
|
|
1692
|
+
todos("#unidades .x[data-x]").forEach((b) => {
|
|
1693
|
+
b.onclick = () => {
|
|
1694
|
+
unidades.splice(Number(b.dataset.x), 1);
|
|
1695
|
+
render();
|
|
1696
|
+
};
|
|
1697
|
+
});
|
|
1698
|
+
q("#masU").onclick = () => {
|
|
1699
|
+
const n = q("#nuevaU").value.trim();
|
|
1700
|
+
if (!n) return;
|
|
1701
|
+
const usados = unidades.map((u) => u.color);
|
|
1702
|
+
const libre = E.paleta.find((p) => !usados.includes(p.hex)) ?? E.paleta[0];
|
|
1703
|
+
unidades.push({ id: slug(n), name: n, color: libre ? libre.hex : "c8b48a" });
|
|
1704
|
+
render();
|
|
1705
|
+
};
|
|
1706
|
+
q("#nuevaU").onkeydown = (e) => {
|
|
1707
|
+
if (e.key === "Enter") q("#masU").click();
|
|
1708
|
+
};
|
|
1709
|
+
todos("#parcelas [data-k]").forEach((el) => {
|
|
1710
|
+
el.onchange = () => {
|
|
1711
|
+
const fila = porRepo[el.dataset.r ?? ""]?.[Number(el.dataset.j)];
|
|
1712
|
+
if (!fila) return;
|
|
1713
|
+
const k = el.dataset.k;
|
|
1714
|
+
fila[k] = el.value;
|
|
1715
|
+
};
|
|
1716
|
+
});
|
|
1717
|
+
todos("#parcelas [data-split]").forEach((b) => {
|
|
1718
|
+
b.onclick = () => {
|
|
1719
|
+
const r = b.dataset.split ?? "";
|
|
1720
|
+
(porRepo[r] = porRepo[r] ?? []).push({
|
|
1721
|
+
ruta: "",
|
|
1722
|
+
unidad: "none",
|
|
1723
|
+
nombre: `${r} \xB7 new slice`
|
|
1724
|
+
});
|
|
1725
|
+
render();
|
|
1726
|
+
};
|
|
1727
|
+
});
|
|
1728
|
+
todos("#parcelas [data-quita]").forEach((b) => {
|
|
1729
|
+
b.onclick = () => {
|
|
1730
|
+
porRepo[b.dataset.quita ?? ""]?.splice(Number(b.dataset.j), 1);
|
|
1731
|
+
render();
|
|
1732
|
+
};
|
|
1733
|
+
});
|
|
1734
|
+
todos("#parcelas [data-lab]").forEach((c) => {
|
|
1735
|
+
c.onchange = () => {
|
|
1736
|
+
const r = c.dataset.lab ?? "";
|
|
1737
|
+
if (c.checked) lab.add(r);
|
|
1738
|
+
else lab.delete(r);
|
|
1739
|
+
};
|
|
1740
|
+
});
|
|
1741
|
+
q("#guardaBarrios").onclick = async () => {
|
|
1742
|
+
const r1 = await api("/api/unidades", {
|
|
1743
|
+
method: "POST",
|
|
1744
|
+
body: JSON.stringify({ unidades })
|
|
1745
|
+
});
|
|
1746
|
+
const r2 = await api("/api/parcelas", {
|
|
1747
|
+
method: "POST",
|
|
1748
|
+
body: JSON.stringify({ repos: porRepo, lab: [...lab] })
|
|
1749
|
+
});
|
|
1750
|
+
await refresca();
|
|
1751
|
+
const mal = r1.error ?? r2.error;
|
|
1752
|
+
toast(mal ?? "Districts and houses written", !!mal);
|
|
1753
|
+
};
|
|
1754
|
+
};
|
|
1755
|
+
render();
|
|
1756
|
+
};
|
|
1757
|
+
VISTAS.red = () => {
|
|
1758
|
+
const conectadas = new Set(E.roads.map((r) => r.id));
|
|
1759
|
+
const locales = E.ciudades.filter((c) => !c.actual && c.id && !conectadas.has(c.id));
|
|
1760
|
+
const invitation = JSON.stringify(E.invitation);
|
|
1761
|
+
q("#lienzo").innerHTML = `<div><span class="sub">roads</span>
|
|
171
1762
|
<h1 style="margin-top:6px">Cities this one may reach</h1>
|
|
172
1763
|
<p class="prosa" style="margin-top:8px">A road joins city seats. It may stay on
|
|
173
1764
|
this machine or continue over the remote bus; the city sees one explicit
|
|
174
1765
|
connection either way.</p></div>
|
|
175
|
-
<div class="gente">${
|
|
176
|
-
|
|
177
|
-
<span class="
|
|
178
|
-
<
|
|
1766
|
+
<div class="gente">${E.roads.map(
|
|
1767
|
+
(r) => `<div class="persona"><h3>${esc(r.name)}</h3>
|
|
1768
|
+
<span class="ag">${esc(r.address)}</span>
|
|
1769
|
+
<span class="rp">${r.local ? "local road" : "remote road"}</span>
|
|
1770
|
+
<button class="bt mini" data-road-close="${esc(r.id)}">close</button></div>`
|
|
1771
|
+
).join("") || '<p class="prosa">No roads yet. This city is isolated on purpose.</p>'}</div>
|
|
179
1772
|
<div><span class="sub">other cities on this machine</span>
|
|
180
|
-
<div class="gente" style="margin-top:9px">${
|
|
181
|
-
|
|
182
|
-
<
|
|
1773
|
+
<div class="gente" style="margin-top:9px">${locales.map(
|
|
1774
|
+
(c) => `<div class="persona"><h3>${esc(c.nombre)}</h3>
|
|
1775
|
+
<span class="ag">${esc(c.slug ?? c.ruta)}</span>
|
|
1776
|
+
<button class="bt mini ppal" data-road-open="${esc(c.id)}">open road</button></div>`
|
|
1777
|
+
).join("") || '<p class="prosa">No unconnected local cities.</p>'}</div></div>
|
|
183
1778
|
<div class="orden"><span class="et2">remote invitation \xB7 public, no token</span>
|
|
184
|
-
<code>${
|
|
185
|
-
<button class="bt mini" data-copia="${
|
|
186
|
-
</div
|
|
1779
|
+
<code>${esc(invitation)}</code>
|
|
1780
|
+
<button class="bt mini" data-copia="${esc(invitation)}">copy</button>
|
|
1781
|
+
</div>`;
|
|
1782
|
+
enlaza();
|
|
1783
|
+
todos("[data-road-open]").forEach((b) => {
|
|
1784
|
+
b.onclick = async () => {
|
|
1785
|
+
const r = await api("/api/roads", {
|
|
1786
|
+
method: "POST",
|
|
1787
|
+
body: JSON.stringify({ action: "connect", target: b.dataset.roadOpen })
|
|
1788
|
+
});
|
|
1789
|
+
if (r.error) return toast(r.error, true);
|
|
1790
|
+
await refresca();
|
|
1791
|
+
toast("Road open at both local cities");
|
|
1792
|
+
};
|
|
1793
|
+
});
|
|
1794
|
+
todos("[data-road-close]").forEach((b) => {
|
|
1795
|
+
b.onclick = async () => {
|
|
1796
|
+
const r = await api("/api/roads", {
|
|
1797
|
+
method: "POST",
|
|
1798
|
+
body: JSON.stringify({ action: "disconnect", target: b.dataset.roadClose })
|
|
1799
|
+
});
|
|
1800
|
+
if (r.error) return toast(r.error, true);
|
|
1801
|
+
await refresca();
|
|
1802
|
+
toast("Road closed");
|
|
1803
|
+
};
|
|
1804
|
+
});
|
|
1805
|
+
};
|
|
1806
|
+
VISTAS.committee = () => {
|
|
1807
|
+
q("#lienzo").innerHTML = `<div><span class="sub">committee</span>
|
|
187
1808
|
<h1 style="margin-top:6px">Decisions with a visible chain of custody</h1>
|
|
188
1809
|
<p class="prosa" style="margin-top:8px">The seat selects relevant repo agents,
|
|
189
1810
|
gathers isolated positions, controls the floor, decides and assigns an
|
|
@@ -193,72 +1814,470 @@ function Z(e,a){return Object.fromEntries([...e].sort().map(i=>[i,a[i]||"blank"]
|
|
|
193
1814
|
<code>agents-city committee schema open</code>
|
|
194
1815
|
<button class="bt mini" data-copia="agents-city committee schema open">copy</button>
|
|
195
1816
|
</div>
|
|
196
|
-
<div class="gente">${
|
|
197
|
-
|
|
198
|
-
<
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
<span class="
|
|
202
|
-
|
|
203
|
-
${
|
|
204
|
-
|
|
205
|
-
|
|
1817
|
+
<div class="gente">${E.deliberations.map(
|
|
1818
|
+
(act) => `<div class="persona">
|
|
1819
|
+
<h3>${esc(act.question)}</h3>
|
|
1820
|
+
<span class="ag">${esc(act.status)} \xB7 revision ${act.revision} \xB7
|
|
1821
|
+
${act.received}/${act.total} positions</span>
|
|
1822
|
+
<span class="rp">${esc(act.decision || act.desired_outcome)}</span>
|
|
1823
|
+
<span class="de">${esc(act.participants.join(", ") || "no participants")}
|
|
1824
|
+
${act.contributors.length ? ` \xB7 decisive ${esc(act.contributors.join(", "))}` : ""}
|
|
1825
|
+
${act.verifier ? ` \xB7 verifier ${esc(act.verifier)} ${esc(act.verification)}` : ""}</span>
|
|
1826
|
+
<code class="mono">${esc(corto(act.act))}</code>
|
|
1827
|
+
</div>`
|
|
1828
|
+
).join("") || '<p class="prosa">No committee acts yet. Open one only when the seat needs specialised evidence.</p>'}</div>`;
|
|
1829
|
+
enlaza();
|
|
1830
|
+
};
|
|
1831
|
+
function opciones2(valores, actual) {
|
|
1832
|
+
return opciones(valores, actual, esc);
|
|
1833
|
+
}
|
|
1834
|
+
function caraRPG(uri) {
|
|
1835
|
+
return uri?.startsWith("data:image/svg+xml;base64,") ? `<img class="rpgCara" src="${esc(uri)}" alt="">` : "";
|
|
1836
|
+
}
|
|
1837
|
+
function barra(ancho, clase = "") {
|
|
1838
|
+
const pct = Math.round(Math.max(0, Math.min(1, ancho)) * 100);
|
|
1839
|
+
return `<span class="barra${clase ? " " + clase : ""}"><span class="llena" style="width:${pct}%"></span></span>`;
|
|
1840
|
+
}
|
|
1841
|
+
VISTAS.gente = () => {
|
|
1842
|
+
const mia = E.tarjetas.find((t2) => t2.user === E.yo);
|
|
1843
|
+
const agentes = E.agents ?? [];
|
|
1844
|
+
const fichaDe = (a) => {
|
|
1845
|
+
const motor = nivelDeMotor(a.model);
|
|
1846
|
+
const esfuerzo = NIVEL_ESFUERZO[a.effort] ?? 0;
|
|
1847
|
+
const g = a.growth;
|
|
1848
|
+
const crecido = Math.min(1, Math.log2(g.floors + 1) / 8);
|
|
1849
|
+
const skills = E.skills[a.name]?.skills ?? [];
|
|
1850
|
+
const esClaude = a.runtime === "claude";
|
|
1851
|
+
const opcionesModelo = opciones2([""].concat(MODELOS), a.model) + '<option value="__otro__">custom\u2026</option>';
|
|
1852
|
+
const opcionesEsfuerzo = opciones2([""].concat(ESFUERZOS), a.effort);
|
|
1853
|
+
const opcionesRuntime = opciones2(RUNTIMES, a.runtime);
|
|
1854
|
+
const montajes = a.mounts ?? [];
|
|
1855
|
+
return `
|
|
206
1856
|
<div class="fichaRPG">
|
|
207
|
-
<div class="rpgCab">${
|
|
208
|
-
<div><h3>${
|
|
209
|
-
<span class="rpgTags"><span class="kindChip ${
|
|
210
|
-
<span class="rpgRol">${
|
|
211
|
-
<span class="rpgHogar">${
|
|
1857
|
+
<div class="rpgCab">${caraRPG(a.avatar)}
|
|
1858
|
+
<div><h3>${esc(a.name)}</h3>
|
|
1859
|
+
<span class="rpgTags"><span class="kindChip ${esc(a.kind)}">${esc(a.kind)}</span>
|
|
1860
|
+
<span class="rpgRol">${esc(a.role)}</span>
|
|
1861
|
+
<span class="rpgHogar">${a.legacy ? "repo" : `workspace \xB7 ${montajes.length} ${montajes.length === 1 ? "mount" : "mounts"}`}</span></span></div>
|
|
212
1862
|
<span class="rpgBotones">
|
|
213
|
-
<button class="rpgMini rpgDado" type="button" data-agente="${
|
|
1863
|
+
<button class="rpgMini rpgDado" type="button" data-agente="${esc(a.slug)}"
|
|
214
1864
|
title="Reroll this agent's face \u2014 deterministic, persisted on the card">\u{1F3B2}</button>
|
|
215
|
-
<button class="rpgMini rpgIns" type="button" data-agente="${
|
|
1865
|
+
<button class="rpgMini rpgIns" type="button" data-agente="${esc(a.slug)}"
|
|
216
1866
|
data-file="CLAUDE.md" title="Instructions the Claude runtime reads">CLAUDE.md</button>
|
|
217
|
-
<button class="rpgMini rpgIns" type="button" data-agente="${
|
|
1867
|
+
<button class="rpgMini rpgIns" type="button" data-agente="${esc(a.slug)}"
|
|
218
1868
|
data-file="AGENTS.md" title="Instructions Codex, OpenCode and Kimi read">AGENTS.md</button>
|
|
219
1869
|
</span>
|
|
220
1870
|
</div>
|
|
221
|
-
<div class="rpgFila"><label
|
|
222
|
-
${
|
|
223
|
-
<select class="rpgSel" data-agente="${
|
|
224
|
-
${
|
|
225
|
-
${
|
|
226
|
-
<div class="rpgFila"><label
|
|
227
|
-
${
|
|
228
|
-
<select class="rpgSel" data-agente="${
|
|
229
|
-
${
|
|
230
|
-
${
|
|
231
|
-
<div class="rpgFila"><label
|
|
232
|
-
<span class="rpgDato"><span class="cliDot ${
|
|
233
|
-
|
|
1871
|
+
<div class="rpgFila"><label>${t("engine")}</label>
|
|
1872
|
+
${barra(motor.ancho, motor.defecto ? "defecto" : "")}
|
|
1873
|
+
<select class="rpgSel" data-agente="${esc(a.slug)}" data-campo="model"
|
|
1874
|
+
${esClaude ? "" : 'disabled title="This window runs its own CLI; its model lives in its own flags"'}>
|
|
1875
|
+
${opcionesModelo}</select></div>
|
|
1876
|
+
<div class="rpgFila"><label>${t("effort")}</label>
|
|
1877
|
+
${barra(esfuerzo / 5, esfuerzo ? "" : "defecto")}
|
|
1878
|
+
<select class="rpgSel" data-agente="${esc(a.slug)}" data-campo="effort"
|
|
1879
|
+
${esClaude ? "" : 'disabled title="This window runs its own CLI; its effort lives in its own flags"'}>
|
|
1880
|
+
${opcionesEsfuerzo}</select></div>
|
|
1881
|
+
<div class="rpgFila"><label>${t("provider")}</label>
|
|
1882
|
+
<span class="rpgDato"><span class="cliDot ${a.cli.connected ? "on" : a.cli.installed ? "idle" : "off"}" title="${esc(
|
|
1883
|
+
a.cli.connected ? "connected: this agent\u2019s window is alive right now" : a.cli.installed ? "installed on this machine, not in use by this agent" : `${a.cli.binary} is not installed`
|
|
1884
|
+
)}"></span>${esc(
|
|
1885
|
+
t(a.cli.connected ? "connected" : a.cli.installed ? "idle" : "missing")
|
|
1886
|
+
)} \xB7 ${esc(a.cli.binary)}
|
|
1887
|
+
<button class="rpgMini rpgTest" type="button" data-agente="${esc(a.slug)}"
|
|
234
1888
|
title="Run the engine for real: --version, and the login state on Claude">test</button></span>
|
|
235
|
-
<select class="rpgSel" data-agente="${
|
|
236
|
-
${
|
|
237
|
-
<div class="
|
|
238
|
-
${
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
1889
|
+
<select class="rpgSel" data-agente="${esc(a.slug)}" data-campo="runtime">
|
|
1890
|
+
${opcionesRuntime}</select></div>
|
|
1891
|
+
<div class="rpgSkills"><label>${t("works on")} \xB7 ${montajes.length}</label>
|
|
1892
|
+
${montajes.length ? montajes.map(
|
|
1893
|
+
(m) => `<code class="mono" title="${esc(m.target)}">${esc(m.label)}${m.fixed ? "" : `<button class="rpgQuitar rpgDesmonta" type="button"
|
|
1894
|
+
data-agente="${esc(a.slug)}" data-mount="${esc(m.label)}"
|
|
1895
|
+
title="Stop this agent working on ${esc(m.target)}">\xD7</button>`}</code>`
|
|
1896
|
+
).join(" ") : '<span class="rpgDato">nothing mounted yet</span>'}
|
|
1897
|
+
${a.legacy ? '<span class="rpgDato">a legacy repo agent works on its own repo</span>' : `<button class="rpgMini rpgMonta" type="button" data-agente="${esc(a.slug)}"
|
|
1898
|
+
title="A repo, a worktree or a folder of documents \u2014 this agent works on all of them">+ folder</button>`}</div>
|
|
1899
|
+
<div class="rpgFila"><label>${t("growth")}</label>
|
|
1900
|
+
${barra(crecido)}
|
|
1901
|
+
<span class="rpgDato">${g.floors | 0} floors \xB7 ${g.bricks | 0} bricks \xB7 ${g.activity30 | 0}/30d</span></div>
|
|
1902
|
+
<div class="rpgSkills"><label>${t("skills")} \xB7 ${skills.length}</label>
|
|
1903
|
+
${skills.length ? skills.map(
|
|
1904
|
+
(s) => `<code class="mono" title="${esc(s.description)}">${esc(s.name)}${s.removable && s.dir ? `<button class="rpgQuitar" type="button" data-agente="${esc(a.slug)}"
|
|
1905
|
+
data-skill="${esc(s.dir)}" title="Remove this skill from the agent's home">\xD7</button>` : ""}</code>`
|
|
1906
|
+
).join(" ") : '<span class="rpgDato">none discovered</span>'}
|
|
243
1907
|
<label class="rpgMini rpgSubir"
|
|
244
1908
|
title="Install a skill zip into this agent's own home \u2014 the Claude runtime reads skills; other engines ignore them">
|
|
245
|
-
+ zip<input type="file" accept=".zip" data-agente="${
|
|
246
|
-
</div
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
1909
|
+
+ zip<input type="file" accept=".zip" data-agente="${esc(a.slug)}"></label></div>
|
|
1910
|
+
</div>`;
|
|
1911
|
+
};
|
|
1912
|
+
q("#lienzo").innerHTML = `<div><span class="sub">the houses of ${esc(E.city_name)}</span>
|
|
1913
|
+
<h1 style="margin-top:6px">${t("Who lives in {city}", { city: esc(E.city_name) })}</h1>
|
|
1914
|
+
<p class="prosa" style="margin-top:8px"><b>A house is where an agent lives and works</b>,
|
|
1915
|
+
and many houses are a city \u2014 it is the same thing the map draws, growing with what that
|
|
1916
|
+
agent actually does. The card and the CLI call them <code class="mono">agents</code>;
|
|
1917
|
+
here you see their houses.</p>
|
|
1918
|
+
<p class="prosa">They belong to <b>this</b> city and only
|
|
1919
|
+
to it: each one's workspace and its mounts live inside
|
|
1920
|
+
<code class="mono">${esc(corto(E.datos))}/agents/</code>, so another city has its own
|
|
1921
|
+
people even if you give them the same names. Every agent is whole here: the kind of work
|
|
1922
|
+
it does, its role, everything it works on \u2014 any number of repositories and document
|
|
1923
|
+
folders at once \u2014 the engine and effort that run it, and the skills in its own home.</p>
|
|
1924
|
+
<p class="prosa">Every number is real: growth from what the agent actually produced,
|
|
1925
|
+
skills from live read-only recognition. Changes persist to the card and apply the next
|
|
1926
|
+
time the session opens.</p>
|
|
1927
|
+
<button class="rpgAlta" type="button" id="altaAgente">${t("+ Build a house")}</button></div>
|
|
253
1928
|
<div class="fichasRPG">
|
|
254
1929
|
<div class="fichaRPG rpgSeat">
|
|
255
|
-
<div class="rpgCab">${
|
|
1930
|
+
<div class="rpgCab">${caraRPG(E.avatars?.["seat"])}
|
|
256
1931
|
<div><h3>seat</h3>
|
|
257
1932
|
<span class="rpgTags"><span class="kindChip coordinator">chair</span>
|
|
258
|
-
<span class="rpgRol">${
|
|
1933
|
+
<span class="rpgRol">${esc(mia?.role ?? "not configured")}</span></span></div>
|
|
259
1934
|
</div>
|
|
260
|
-
<div class="rpgFila"><span class="rpgDato">${
|
|
261
|
-
<div class="rpgFila"><span class="rpgDato">${
|
|
1935
|
+
<div class="rpgFila"><span class="rpgDato">${esc(E.address)}</span></div>
|
|
1936
|
+
<div class="rpgFila"><span class="rpgDato">${esc(mia?.objetivo?.title ?? "No goal yet")}</span></div>
|
|
262
1937
|
</div>
|
|
263
|
-
${
|
|
264
|
-
</div>`;
|
|
1938
|
+
${agentes.map(fichaDe).join("")}
|
|
1939
|
+
</div>`;
|
|
1940
|
+
const guardaAgente = async (carga, mensaje) => {
|
|
1941
|
+
let r;
|
|
1942
|
+
try {
|
|
1943
|
+
r = await api("/api/agente", { method: "POST", body: JSON.stringify(carga) });
|
|
1944
|
+
} catch (e) {
|
|
1945
|
+
toast(String(e), true);
|
|
1946
|
+
pinta();
|
|
1947
|
+
return;
|
|
1948
|
+
}
|
|
1949
|
+
if (!r.ok || !r.agent) {
|
|
1950
|
+
toast(r.error || "Could not save", true);
|
|
1951
|
+
pinta();
|
|
1952
|
+
return;
|
|
1953
|
+
}
|
|
1954
|
+
E.agents = (E.agents ?? []).map((x) => x.slug === r.agent.slug ? r.agent : x);
|
|
1955
|
+
if (E.avatars) E.avatars[r.agent.name] = r.agent.avatar;
|
|
1956
|
+
toast(mensaje);
|
|
1957
|
+
pinta();
|
|
1958
|
+
};
|
|
1959
|
+
todos(".rpgSel").forEach((sel) => {
|
|
1960
|
+
sel.onchange = async () => {
|
|
1961
|
+
let valor = sel.value;
|
|
1962
|
+
if (valor === "__otro__") {
|
|
1963
|
+
const propio = window.prompt("Model alias (resolved by the CLI at launch):", "");
|
|
1964
|
+
if (propio === null) {
|
|
1965
|
+
pinta();
|
|
1966
|
+
return;
|
|
1967
|
+
}
|
|
1968
|
+
valor = propio.trim().toLowerCase();
|
|
1969
|
+
}
|
|
1970
|
+
await guardaAgente(
|
|
1971
|
+
{ agent: sel.dataset.agente ?? "", [sel.dataset.campo ?? "model"]: valor },
|
|
1972
|
+
"Saved \u2014 applies next session"
|
|
1973
|
+
);
|
|
1974
|
+
};
|
|
1975
|
+
});
|
|
1976
|
+
todos(".rpgDado").forEach((boton) => {
|
|
1977
|
+
boton.onclick = () => guardaAgente(
|
|
1978
|
+
{ agent: boton.dataset.agente ?? "", avatar: Math.random().toString(36).slice(2, 8) },
|
|
1979
|
+
"New look \u2014 same identity, everywhere, forever"
|
|
1980
|
+
);
|
|
1981
|
+
});
|
|
1982
|
+
todos(".rpgTest").forEach((boton) => {
|
|
1983
|
+
boton.onclick = async () => {
|
|
1984
|
+
boton.disabled = true;
|
|
1985
|
+
try {
|
|
1986
|
+
const r = await api("/api/motor", {
|
|
1987
|
+
method: "POST",
|
|
1988
|
+
body: JSON.stringify({ agent: boton.dataset.agente ?? "" })
|
|
1989
|
+
});
|
|
1990
|
+
if (r.error) {
|
|
1991
|
+
toast(r.error, true);
|
|
1992
|
+
return;
|
|
1993
|
+
}
|
|
1994
|
+
const partes = [r.version, r.detail].filter(Boolean).join(" \xB7 ");
|
|
1995
|
+
toast(
|
|
1996
|
+
r.ok ? `${r.binary} works \u2014 ${partes}` : `${r.binary}: ${r.detail || "failed"}`,
|
|
1997
|
+
!r.ok
|
|
1998
|
+
);
|
|
1999
|
+
} catch (e) {
|
|
2000
|
+
toast(String(e), true);
|
|
2001
|
+
} finally {
|
|
2002
|
+
boton.disabled = false;
|
|
2003
|
+
}
|
|
2004
|
+
};
|
|
2005
|
+
});
|
|
2006
|
+
todos(".rpgQuitar").forEach((boton) => {
|
|
2007
|
+
boton.onclick = async () => {
|
|
2008
|
+
const nombre = boton.dataset.skill ?? "";
|
|
2009
|
+
if (!window.confirm(`Remove the skill ${nombre} from this agent's home?`)) return;
|
|
2010
|
+
const r = await api("/api/skill", {
|
|
2011
|
+
method: "POST",
|
|
2012
|
+
body: JSON.stringify({ agent: boton.dataset.agente ?? "", remove: nombre })
|
|
2013
|
+
});
|
|
2014
|
+
if (!r.ok) {
|
|
2015
|
+
toast(r.error || "Could not remove", true);
|
|
2016
|
+
return;
|
|
2017
|
+
}
|
|
2018
|
+
toast(`Skill ${nombre} removed`);
|
|
2019
|
+
void refresca();
|
|
2020
|
+
};
|
|
2021
|
+
});
|
|
2022
|
+
const alta = document.getElementById("altaAgente");
|
|
2023
|
+
if (alta)
|
|
2024
|
+
alta.onclick = async () => {
|
|
2025
|
+
const nombre = window.prompt("The agent's name:", "");
|
|
2026
|
+
if (nombre === null || !nombre.trim()) return;
|
|
2027
|
+
const clase = window.prompt("What kind of work does it do? code, knowledge or coordinator", "code") ?? "";
|
|
2028
|
+
if (!clase.trim()) return;
|
|
2029
|
+
const rol = window.prompt("Its role (its specialty, not authority):", "blank") ?? "";
|
|
2030
|
+
if (!rol.trim()) return;
|
|
2031
|
+
try {
|
|
2032
|
+
const r = await api("/api/agentes", {
|
|
2033
|
+
method: "POST",
|
|
2034
|
+
body: JSON.stringify({ name: nombre.trim(), kind: clase.trim(), role: rol.trim() })
|
|
2035
|
+
});
|
|
2036
|
+
if (!r.ok) {
|
|
2037
|
+
toast(r.error || "Could not add the agent", true);
|
|
2038
|
+
return;
|
|
2039
|
+
}
|
|
2040
|
+
toast(`${nombre.trim()} has a house now \u2014 give it something to work on`);
|
|
2041
|
+
void refresca();
|
|
2042
|
+
} catch (e) {
|
|
2043
|
+
toast(String(e), true);
|
|
2044
|
+
}
|
|
2045
|
+
};
|
|
2046
|
+
const montaje = async (carga, hecho) => {
|
|
2047
|
+
try {
|
|
2048
|
+
const r = await api("/api/montaje", {
|
|
2049
|
+
method: "POST",
|
|
2050
|
+
body: JSON.stringify(carga)
|
|
2051
|
+
});
|
|
2052
|
+
if (!r.ok) {
|
|
2053
|
+
toast(r.error || "Could not change the mounts", true);
|
|
2054
|
+
return;
|
|
2055
|
+
}
|
|
2056
|
+
toast(hecho);
|
|
2057
|
+
void refresca();
|
|
2058
|
+
} catch (e) {
|
|
2059
|
+
toast(String(e), true);
|
|
2060
|
+
}
|
|
2061
|
+
};
|
|
2062
|
+
todos(".rpgMonta").forEach((boton) => {
|
|
2063
|
+
boton.onclick = () => {
|
|
2064
|
+
const ruta = window.prompt("A repo, a worktree or a folder of documents:", "~/");
|
|
2065
|
+
if (ruta === null || !ruta.trim()) return;
|
|
2066
|
+
void montaje({ agent: boton.dataset.agente ?? "", add: ruta.trim() }, "Mounted");
|
|
2067
|
+
};
|
|
2068
|
+
});
|
|
2069
|
+
todos(".rpgDesmonta").forEach((boton) => {
|
|
2070
|
+
boton.onclick = () => {
|
|
2071
|
+
const etiqueta = boton.dataset.mount ?? "";
|
|
2072
|
+
if (!window.confirm(`Stop this agent working on ${etiqueta}?`)) return;
|
|
2073
|
+
void montaje(
|
|
2074
|
+
{ agent: boton.dataset.agente ?? "", remove: etiqueta },
|
|
2075
|
+
`${etiqueta} unmounted \u2014 the folder itself is untouched`
|
|
2076
|
+
);
|
|
2077
|
+
};
|
|
2078
|
+
});
|
|
2079
|
+
todos(".rpgIns").forEach((boton) => {
|
|
2080
|
+
boton.onclick = () => void abreInstrucciones(boton.dataset.agente ?? "", boton.dataset.file ?? "");
|
|
2081
|
+
});
|
|
2082
|
+
todos(".rpgSubir input").forEach((entrada) => {
|
|
2083
|
+
entrada.onchange = async () => {
|
|
2084
|
+
const archivo = entrada.files?.[0];
|
|
2085
|
+
if (!archivo) return;
|
|
2086
|
+
entrada.disabled = true;
|
|
2087
|
+
try {
|
|
2088
|
+
const zip = await aBase64(archivo);
|
|
2089
|
+
const nombre = archivo.name.replace(/\.zip$/i, "").toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
2090
|
+
const r = await api("/api/skill", {
|
|
2091
|
+
method: "POST",
|
|
2092
|
+
body: JSON.stringify({ agent: entrada.dataset.agente ?? "", name: nombre, zip })
|
|
2093
|
+
});
|
|
2094
|
+
if (!r.ok) {
|
|
2095
|
+
toast(r.error || "Could not install", true);
|
|
2096
|
+
return;
|
|
2097
|
+
}
|
|
2098
|
+
toast(`Skill ${r.skill} installed \u2014 the Claude runtime reads it next session`);
|
|
2099
|
+
void refresca();
|
|
2100
|
+
} catch (e) {
|
|
2101
|
+
toast(String(e), true);
|
|
2102
|
+
} finally {
|
|
2103
|
+
entrada.disabled = false;
|
|
2104
|
+
entrada.value = "";
|
|
2105
|
+
}
|
|
2106
|
+
};
|
|
2107
|
+
});
|
|
2108
|
+
};
|
|
2109
|
+
function aBase64(archivo) {
|
|
2110
|
+
return new Promise((listo, falla) => {
|
|
2111
|
+
const lector = new FileReader();
|
|
2112
|
+
lector.onerror = () => falla(lector.error);
|
|
2113
|
+
lector.onload = () => listo(String(lector.result).split(",", 2)[1] ?? "");
|
|
2114
|
+
lector.readAsDataURL(archivo);
|
|
2115
|
+
});
|
|
2116
|
+
}
|
|
2117
|
+
var editorAbierto = null;
|
|
2118
|
+
async function abreInstrucciones(agente, archivo) {
|
|
2119
|
+
const r = await api(`/api/instrucciones?agent=${encodeURIComponent(agente)}&file=${encodeURIComponent(archivo)}`);
|
|
2120
|
+
if (r.error) {
|
|
2121
|
+
toast(r.error, true);
|
|
2122
|
+
return;
|
|
2123
|
+
}
|
|
2124
|
+
editorAbierto = { agent: agente, file: archivo };
|
|
2125
|
+
q("#edTitulo").textContent = `${agente} \xB7 ${archivo}`;
|
|
2126
|
+
q("#edLector").textContent = `read by ${r.reader ?? "?"}${r.exists ? "" : " \xB7 new file"}`;
|
|
2127
|
+
q("#edRuta").textContent = r.home ? `${r.home}/${archivo}` : "";
|
|
2128
|
+
q("#edTexto").value = r.content ?? "";
|
|
2129
|
+
q("#editorIns").hidden = false;
|
|
2130
|
+
q("#edTexto").focus();
|
|
2131
|
+
}
|
|
2132
|
+
document.getElementById("edCerrar")?.addEventListener("click", () => {
|
|
2133
|
+
q("#editorIns").hidden = true;
|
|
2134
|
+
editorAbierto = null;
|
|
2135
|
+
});
|
|
2136
|
+
document.getElementById("edGuardar")?.addEventListener("click", async () => {
|
|
2137
|
+
if (!editorAbierto) return;
|
|
2138
|
+
const r = await api("/api/instrucciones", {
|
|
2139
|
+
method: "POST",
|
|
2140
|
+
body: JSON.stringify({ ...editorAbierto, content: q("#edTexto").value })
|
|
2141
|
+
});
|
|
2142
|
+
if (!r.ok) {
|
|
2143
|
+
toast(r.error || "Could not save", true);
|
|
2144
|
+
return;
|
|
2145
|
+
}
|
|
2146
|
+
toast("Saved");
|
|
2147
|
+
q("#editorIns").hidden = true;
|
|
2148
|
+
editorAbierto = null;
|
|
2149
|
+
});
|
|
2150
|
+
function enlaza() {
|
|
2151
|
+
todos("[data-copia]").forEach((b) => {
|
|
2152
|
+
b.onclick = async () => {
|
|
2153
|
+
try {
|
|
2154
|
+
await navigator.clipboard.writeText(b.dataset.copia ?? "");
|
|
2155
|
+
toast("Copied");
|
|
2156
|
+
} catch {
|
|
2157
|
+
toast("Could not copy \u2014 select it by hand", true);
|
|
2158
|
+
}
|
|
2159
|
+
};
|
|
2160
|
+
});
|
|
2161
|
+
todos("[data-ir]").forEach((b) => {
|
|
2162
|
+
b.onclick = () => {
|
|
2163
|
+
SECCION = b.dataset.ir ?? "mapa";
|
|
2164
|
+
pinta();
|
|
2165
|
+
};
|
|
2166
|
+
});
|
|
2167
|
+
}
|
|
2168
|
+
function arrastreDelRail() {
|
|
2169
|
+
const app = document.getElementById("app");
|
|
2170
|
+
const asa = document.getElementById("liveResize");
|
|
2171
|
+
const rail2 = document.getElementById("livePanel");
|
|
2172
|
+
if (!app || !asa || !rail2) return;
|
|
2173
|
+
const CLAVE = "hall-live-width";
|
|
2174
|
+
const aplica = (px) => {
|
|
2175
|
+
if (px === null) app.style.removeProperty("--live-ancho");
|
|
2176
|
+
else app.style.setProperty("--live-ancho", `${Math.round(px)}px`);
|
|
2177
|
+
};
|
|
2178
|
+
const tope = (px) => Math.max(340, Math.min(px, Math.max(420, window.innerWidth * 0.6)));
|
|
2179
|
+
try {
|
|
2180
|
+
const guardado = Number(localStorage.getItem(CLAVE));
|
|
2181
|
+
if (guardado > 0) aplica(tope(guardado));
|
|
2182
|
+
} catch {
|
|
2183
|
+
}
|
|
2184
|
+
asa.addEventListener("pointerdown", (e) => {
|
|
2185
|
+
e.preventDefault();
|
|
2186
|
+
const desdeX = e.clientX;
|
|
2187
|
+
const desdeAncho = rail2.getBoundingClientRect().width;
|
|
2188
|
+
app.classList.add("liveResizing");
|
|
2189
|
+
asa.setPointerCapture(e.pointerId);
|
|
2190
|
+
const mueve = (ev) => aplica(tope(desdeAncho + (desdeX - ev.clientX)));
|
|
2191
|
+
const suelta = () => {
|
|
2192
|
+
app.classList.remove("liveResizing");
|
|
2193
|
+
asa.removeEventListener("pointermove", mueve);
|
|
2194
|
+
asa.removeEventListener("pointerup", suelta);
|
|
2195
|
+
asa.removeEventListener("pointercancel", suelta);
|
|
2196
|
+
try {
|
|
2197
|
+
localStorage.setItem(CLAVE, String(Math.round(rail2.getBoundingClientRect().width)));
|
|
2198
|
+
} catch {
|
|
2199
|
+
}
|
|
2200
|
+
};
|
|
2201
|
+
asa.addEventListener("pointermove", mueve);
|
|
2202
|
+
asa.addEventListener("pointerup", suelta);
|
|
2203
|
+
asa.addEventListener("pointercancel", suelta);
|
|
2204
|
+
});
|
|
2205
|
+
asa.addEventListener("dblclick", () => {
|
|
2206
|
+
aplica(null);
|
|
2207
|
+
try {
|
|
2208
|
+
localStorage.removeItem(CLAVE);
|
|
2209
|
+
} catch {
|
|
2210
|
+
}
|
|
2211
|
+
});
|
|
2212
|
+
}
|
|
2213
|
+
window.addEventListener("message", (message) => {
|
|
2214
|
+
if (!isMapNavMessage(message.data)) return;
|
|
2215
|
+
const frame = document.querySelector("#cityMapFrame");
|
|
2216
|
+
if (!frame || message.source !== frame.contentWindow) return;
|
|
2217
|
+
if (message.origin !== new URL(frame.src, location.href).origin) return;
|
|
2218
|
+
const view = message.data.view;
|
|
2219
|
+
if (view !== "committee" && view !== "red") return;
|
|
2220
|
+
SECCION = view;
|
|
2221
|
+
pinta();
|
|
2222
|
+
});
|
|
2223
|
+
function temaActual() {
|
|
2224
|
+
const elegido = document.documentElement.getAttribute("data-tema");
|
|
2225
|
+
if (elegido) return elegido === "claro" ? "light" : "dark";
|
|
2226
|
+
return "light";
|
|
2227
|
+
}
|
|
2228
|
+
function tema() {
|
|
2229
|
+
const boton = document.getElementById("temaBoton");
|
|
2230
|
+
const guardado = (() => {
|
|
2231
|
+
try {
|
|
2232
|
+
return localStorage.getItem("hall-tema") ?? "";
|
|
2233
|
+
} catch {
|
|
2234
|
+
return "";
|
|
2235
|
+
}
|
|
2236
|
+
})();
|
|
2237
|
+
const oscuroDelSistema = false;
|
|
2238
|
+
const pon = (elegido) => {
|
|
2239
|
+
const raiz = document.documentElement;
|
|
2240
|
+
if (elegido) raiz.setAttribute("data-tema", elegido);
|
|
2241
|
+
else raiz.removeAttribute("data-tema");
|
|
2242
|
+
const claro = elegido ? elegido === "claro" : !oscuroDelSistema;
|
|
2243
|
+
if (boton) boton.textContent = claro ? "\u263E Night" : "\u2600 Day";
|
|
2244
|
+
const frame = document.querySelector("#cityMapFrame");
|
|
2245
|
+
frame?.contentWindow?.postMessage(
|
|
2246
|
+
{ type: "agents-city-map-theme/1", theme: claro ? "light" : "dark" },
|
|
2247
|
+
new URL(frame.src, location.href).origin
|
|
2248
|
+
);
|
|
2249
|
+
};
|
|
2250
|
+
pon(guardado);
|
|
2251
|
+
if (boton)
|
|
2252
|
+
boton.onclick = () => {
|
|
2253
|
+
const claroAhora = document.documentElement.getAttribute("data-tema") ? document.documentElement.getAttribute("data-tema") === "claro" : !oscuroDelSistema;
|
|
2254
|
+
const elegido = claroAhora ? "oscuro" : "claro";
|
|
2255
|
+
try {
|
|
2256
|
+
localStorage.setItem("hall-tema", elegido);
|
|
2257
|
+
} catch {
|
|
2258
|
+
}
|
|
2259
|
+
pon(elegido);
|
|
2260
|
+
};
|
|
2261
|
+
}
|
|
2262
|
+
function interruptorDeIdioma() {
|
|
2263
|
+
const boton = document.getElementById("idiomaBoton");
|
|
2264
|
+
if (!boton) return;
|
|
2265
|
+
const pon = () => {
|
|
2266
|
+
document.documentElement.lang = idioma();
|
|
2267
|
+
boton.textContent = idioma() === "es" ? "EN" : "ES";
|
|
2268
|
+
todos("[data-i18n]").forEach((el) => {
|
|
2269
|
+
if (!el.dataset.en) el.dataset.en = (el.textContent ?? "").trim();
|
|
2270
|
+
el.textContent = t(el.dataset.en);
|
|
2271
|
+
});
|
|
2272
|
+
};
|
|
2273
|
+
pon();
|
|
2274
|
+
boton.onclick = () => {
|
|
2275
|
+
ponIdioma(idioma() === "es" ? "en" : "es");
|
|
2276
|
+
pon();
|
|
2277
|
+
void refresca();
|
|
2278
|
+
};
|
|
2279
|
+
}
|
|
2280
|
+
arrastreDelRail();
|
|
2281
|
+
interruptorDeIdioma();
|
|
2282
|
+
tema();
|
|
2283
|
+
void refresca();
|