agents-city 0.1.0 → 0.2.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.
@@ -1,3 +1,393 @@
1
+ // city/web/src/bienvenida.ts
2
+ var CLASES = [
3
+ [
4
+ "code",
5
+ "It writes code",
6
+ "Its house grows with the pull requests it merges. Mounts repositories and worktrees."
7
+ ],
8
+ [
9
+ "knowledge",
10
+ "It keeps knowledge",
11
+ "Its house grows with the documents it writes. Needs no git at all."
12
+ ],
13
+ ["coordinator", "It coordinates", "Its house grows with the decisions it records."]
14
+ ];
15
+ var PASOS = ["Welcome", "The work", "Your chair", "The agents", "Ready"];
16
+ function casita(color) {
17
+ return `<svg viewBox="0 0 120 108" role="img" aria-label="an isometric house" class="bvCasa">
18
+ <polygon points="60,10 112,40 60,70 8,40" fill="${color}" opacity=".95"/>
19
+ <polygon points="8,40 60,70 60,100 8,70" fill="${color}" opacity=".62"/>
20
+ <polygon points="112,40 60,70 60,100 112,70" fill="${color}" opacity=".40"/>
21
+ </svg>`;
22
+ }
23
+ var Bienvenida = class {
24
+ constructor(p) {
25
+ this.p = p;
26
+ }
27
+ paso = 0;
28
+ dominios = [];
29
+ roles = [];
30
+ rolesDeAgente = [];
31
+ repos = [];
32
+ dominio = "";
33
+ rol = "";
34
+ roster = [];
35
+ enCurso = null;
36
+ guardando = false;
37
+ buscandoRepos = false;
38
+ reposListos = false;
39
+ lienzo = null;
40
+ /** Draw the current step into `lienzo`. The only entry point. */
41
+ async pinta(lienzo) {
42
+ this.lienzo = lienzo;
43
+ if (!this.dominios.length) {
44
+ lienzo.innerHTML = '<p class="cargando">reading the domain packs</p>';
45
+ const [d, ra] = await Promise.all([
46
+ this.p.api("/api/domains"),
47
+ this.p.api("/api/roles?scope=agent")
48
+ ]);
49
+ this.dominios = d.domains;
50
+ this.rolesDeAgente = ra.roles;
51
+ }
52
+ lienzo.innerHTML = `<div class="bv">${this.cabecera()}${await this.cuerpo()}</div>`;
53
+ this.enlaza(lienzo);
54
+ }
55
+ cabecera() {
56
+ const puntos = PASOS.map(
57
+ (nombre, i) => `<span class="bvPunto ${i === this.paso ? "aqui" : i < this.paso ? "hecho" : ""}"
58
+ title="${this.p.esc(nombre)}"><i></i>${this.p.esc(nombre)}</span>`
59
+ ).join("");
60
+ return `<div class="bvPasos">${puntos}</div>`;
61
+ }
62
+ async cuerpo() {
63
+ switch (this.paso) {
64
+ case 0:
65
+ return this.pantallaHola();
66
+ case 1:
67
+ return this.pantallaDominio();
68
+ case 2:
69
+ return await this.pantallaRol();
70
+ case 3:
71
+ return this.pantallaAgentes();
72
+ default:
73
+ return this.pantallaFinal();
74
+ }
75
+ }
76
+ // ── 0. what this is ────────────────────────────────────────────────────────
77
+ pantallaHola() {
78
+ return `
79
+ <div class="bvHola">
80
+ <div class="bvArte">${casita("#7048e8")}${casita("#45b9a7")}${casita("#f2674a")}</div>
81
+ <div>
82
+ <span class="sub">welcome</span>
83
+ <h1>A city is you, and the agents who work with you</h1>
84
+ <p class="prosa">You take the chair. Each <b>agent</b> is one worker with its own
85
+ window, its own role and its own corner of your disk \u2014 a repository, three of them,
86
+ a folder of documents, whatever it actually works on. They never talk to each other
87
+ behind your back: you chair, they answer.</p>
88
+ <p class="prosa">Everything lives as plain files in
89
+ <code class="mono">${this.p.esc(this.p.datos)}</code>. Nothing leaves this machine,
90
+ there is no account, and you can edit any of it by hand afterwards.</p>
91
+ <p class="prosa"><b>Four questions and you are working.</b> You can skip any of them
92
+ and change everything later.</p>
93
+ <div class="bvBotones">
94
+ <button class="bt ppal" data-bv="siguiente">Let\u2019s build it</button>
95
+ <button class="bt" data-bv="salir">I\u2019ll set it up myself</button>
96
+ </div>
97
+ </div>
98
+ </div>`;
99
+ }
100
+ // ── 1. the domain ─────────────────────────────────────────────────────────
101
+ pantallaDominio() {
102
+ const tarjetas = this.dominios.map(
103
+ (d) => `
104
+ <button class="bvOpcion ${this.dominio === d.id ? "elegida" : ""}" data-bv="dominio"
105
+ data-id="${this.p.esc(d.id)}">
106
+ <b>${this.p.esc(d.name)}</b><span>${this.p.esc(d.summary)}</span></button>`
107
+ ).join("");
108
+ return `
109
+ <div class="bvPaso">
110
+ <span class="sub">question 1 of 4</span>
111
+ <h1>What kind of work happens here?</h1>
112
+ <p class="prosa">It decides the vocabulary, the roles you will be offered and what
113
+ counts as evidence in a decision. A clinic does not ship pull requests, and a law
114
+ firm does not measure story points. Pick the closest one \u2014 you can change it later.</p>
115
+ <div class="bvRejilla">${tarjetas}</div>
116
+ <div class="bvBotones">
117
+ <button class="bt" data-bv="atras">Back</button>
118
+ <button class="bt ppal" data-bv="siguiente" ${this.dominio ? "" : "disabled"}>Next</button>
119
+ </div>
120
+ </div>`;
121
+ }
122
+ // ── 2. your chair ─────────────────────────────────────────────────────────
123
+ async pantallaRol() {
124
+ if (!this.roles.length || this.roles[0]?.id === void 0) {
125
+ const r = await this.p.api(
126
+ "/api/roles?domain=" + encodeURIComponent(this.dominio)
127
+ );
128
+ this.roles = r.roles;
129
+ }
130
+ const tarjetas = this.roles.map(
131
+ (r) => `
132
+ <button class="bvOpcion ${this.rol === r.id ? "elegida" : ""}" data-bv="rol"
133
+ data-id="${this.p.esc(r.id)}">
134
+ <b>${this.p.esc(r.name)}</b><span>${this.p.esc(r.summary || r.trade)}</span></button>`
135
+ ).join("");
136
+ return `
137
+ <div class="bvPaso">
138
+ <span class="sub">question 2 of 4</span>
139
+ <h1>And what are you, here?</h1>
140
+ <p class="prosa">You chair this city whatever you answer \u2014 this is your
141
+ <b>speciality</b>, not your authority. It shapes the perspective you bring to a
142
+ decision and the knowledge files the city writes for you. <b>Blank</b> is a real
143
+ answer: it means no preset knowledge.</p>
144
+ <div class="bvRejilla">${tarjetas}</div>
145
+ <div class="bvBotones">
146
+ <button class="bt" data-bv="atras">Back</button>
147
+ <button class="bt ppal" data-bv="siguiente" ${this.rol ? "" : "disabled"}>Next</button>
148
+ </div>
149
+ </div>`;
150
+ }
151
+ // ── 3. the agents ─────────────────────────────────────────────────────────
152
+ pantallaAgentes() {
153
+ this.buscaReposUnaVez();
154
+ if (!this.enCurso) return this.listaDeAgentes();
155
+ return this.formularioDeAgente(this.enCurso);
156
+ }
157
+ buscaReposUnaVez() {
158
+ if (this.reposListos || this.buscandoRepos) return;
159
+ this.buscandoRepos = true;
160
+ void this.p.api("/api/misrepos?user=" + encodeURIComponent(this.p.yo)).then((r) => {
161
+ this.repos = (r.repos ?? []).slice(0, 200);
162
+ }).catch(() => {
163
+ this.repos = [];
164
+ }).finally(() => {
165
+ this.buscandoRepos = false;
166
+ this.reposListos = true;
167
+ if (this.lienzo && this.paso === 3) void this.pinta(this.lienzo);
168
+ });
169
+ }
170
+ listaDeAgentes() {
171
+ const fichas = this.roster.map(
172
+ (a) => `
173
+ <div class="bvFicha">
174
+ <b>${this.p.esc(a.nombre)}</b>
175
+ <span>${this.p.esc(a.clase)} \xB7 ${this.p.esc(a.rol)}</span>
176
+ <span class="de">${a.montajes.length ? this.p.esc(a.montajes.map((m) => m.split("/").pop()).join(", ")) : "nothing mounted yet"}</span>
177
+ </div>`
178
+ ).join("");
179
+ return `
180
+ <div class="bvPaso">
181
+ <span class="sub">question 3 of 4</span>
182
+ <h1>${this.roster.length ? "Who else works here?" : "Who works with you?"}</h1>
183
+ <p class="prosa">An <b>agent</b> is one worker: it gets its own window, its own role
184
+ and its own corner of your disk. It is <b>not</b> a repository \u2014 one agent can answer
185
+ for three services and a folder of documents at once, and an agent whose work is
186
+ documents needs no git anywhere.</p>
187
+ ${this.roster.length ? `<div class="bvFichas">${fichas}</div>` : ""}
188
+ <div class="bvBotones">
189
+ <button class="bt ppal" data-bv="nuevo">${this.roster.length ? "Add another agent" : "Add the first agent"}</button>
190
+ <button class="bt" data-bv="siguiente">${this.roster.length ? "That is everyone" : "Skip \u2014 just me for now"}</button>
191
+ </div>
192
+ </div>`;
193
+ }
194
+ formularioDeAgente(a) {
195
+ const clases = CLASES.map(
196
+ ([id, titulo, porque]) => `
197
+ <button class="bvOpcion ${a.clase === id ? "elegida" : ""}" data-bv="clase" data-id="${id}">
198
+ <b>${titulo}</b><span>${porque}</span></button>`
199
+ ).join("");
200
+ const roles = this.rolesDeAgente.map(
201
+ (r) => `<option value="${this.p.esc(r.id)}"${r.id === a.rol ? " selected" : ""}>${this.p.esc(
202
+ r.name
203
+ )}</option>`
204
+ ).join("");
205
+ const sugeridos = this.repos.filter((r) => r.mio).slice(0, 12).map(
206
+ (r) => `<button class="bvChip ${a.montajes.includes(r.ruta) ? "elegida" : ""}"
207
+ data-bv="monta" data-ruta="${this.p.esc(r.ruta)}">${this.p.esc(r.nombre)}</button>`
208
+ ).join("");
209
+ return `
210
+ <div class="bvPaso">
211
+ <span class="sub">a new agent</span>
212
+ <h1>Tell me about this one</h1>
213
+ <div class="campo"><label>What do you call it?</label>
214
+ <input type="text" id="bvNombre" value="${this.p.esc(a.nombre)}"
215
+ placeholder="urgencias, api, the handbook \u2014 whatever you would say out loud"></div>
216
+ <label class="bvEtiqueta">What kind of work does it do?</label>
217
+ <div class="bvRejilla bvTres">${clases}</div>
218
+ <div class="campo"><label>Its role \u2014 its speciality, never authority</label>
219
+ <select id="bvRol">${roles}</select></div>
220
+ <label class="bvEtiqueta">What does it work on?</label>
221
+ <p class="pista">Pick any of your repositories, and add folders by hand. As many as
222
+ you like \u2014 this is the part that makes it an agent and not a repo.</p>
223
+ ${sugeridos ? `<div class="bvChips">${sugeridos}</div>` : this.buscandoRepos ? '<p class="cargando">looking through your disk for repositories\u2026</p>' : '<p class="pista">No repositories found on this disk \u2014 add folders by path below.</p>'}
224
+ <div class="campo"><label>\u2026or a path, and Enter</label>
225
+ <input type="text" id="bvRuta" placeholder="~/documents/handbook"></div>
226
+ ${a.montajes.length ? `<p class="pista">Working on: <b>${this.p.esc(a.montajes.join(", "))}</b></p>` : ""}
227
+ <div class="bvBotones">
228
+ <button class="bt" data-bv="cancela">Cancel</button>
229
+ <button class="bt ppal" data-bv="guarda" ${this.guardando ? "disabled" : ""}>${this.guardando ? "adding\u2026" : "Add this agent"}</button>
230
+ </div>
231
+ </div>`;
232
+ }
233
+ // ── 4. done ───────────────────────────────────────────────────────────────
234
+ pantallaFinal() {
235
+ return `
236
+ <div class="bvPaso">
237
+ <span class="sub">ready</span>
238
+ <h1>${this.p.esc(this.p.ciudad)} is alive</h1>
239
+ <p class="prosa">Your seat is written and ${this.roster.length ? `<b>${this.roster.length}</b> agent${this.roster.length === 1 ? "" : "s"} live here` : "the city is waiting for its first agent"}. Everything you just answered is a plain file you can read and edit.</p>
240
+ <p class="prosa">What people usually do next:</p>
241
+ <div class="bvSiguientes">
242
+ <button class="bt ppal" data-bv="sesion">Open my session</button>
243
+ <button class="bt" data-bv="ir-gente">See the agents</button>
244
+ <button class="bt" data-bv="ir-mapa">Draw the map</button>
245
+ <button class="bt" data-bv="ir-puesto">Set a goal</button>
246
+ </div>
247
+ <p class="pista">A goal is optional, and the city works without one \u2014 but a round with
248
+ no goal is a status report, because there is nothing to argue against.</p>
249
+ </div>`;
250
+ }
251
+ // ── wiring ────────────────────────────────────────────────────────────────
252
+ enlaza(lienzo) {
253
+ const repinta = () => void this.pinta(lienzo);
254
+ lienzo.querySelectorAll("[data-bv]").forEach((el) => {
255
+ el.onclick = async (evento) => {
256
+ evento.preventDefault();
257
+ const que = el.dataset.bv;
258
+ if (que === "salir") return this.p.vete("resumen");
259
+ if (que === "atras") {
260
+ this.paso = Math.max(0, this.paso - 1);
261
+ return repinta();
262
+ }
263
+ if (que === "dominio") {
264
+ this.dominio = el.dataset.id ?? "";
265
+ this.roles = [];
266
+ this.rol = "";
267
+ return repinta();
268
+ }
269
+ if (que === "rol") {
270
+ this.rol = el.dataset.id ?? "";
271
+ return repinta();
272
+ }
273
+ if (que === "nuevo") {
274
+ this.enCurso = { nombre: "", clase: "code", rol: "blank", montajes: [] };
275
+ return repinta();
276
+ }
277
+ if (que === "clase" && this.enCurso) {
278
+ this.enCurso.clase = el.dataset.id ?? "code";
279
+ this.recogeFormulario(lienzo);
280
+ return repinta();
281
+ }
282
+ if (que === "monta" && this.enCurso) {
283
+ const ruta2 = el.dataset.ruta ?? "";
284
+ const ya = this.enCurso.montajes.indexOf(ruta2);
285
+ if (ya >= 0) this.enCurso.montajes.splice(ya, 1);
286
+ else this.enCurso.montajes.push(ruta2);
287
+ this.recogeFormulario(lienzo);
288
+ return repinta();
289
+ }
290
+ if (que === "cancela") {
291
+ this.enCurso = null;
292
+ return repinta();
293
+ }
294
+ if (que === "guarda") return void await this.guardaAgente(lienzo);
295
+ if (que === "siguiente") return void await this.avanza(lienzo);
296
+ if (que === "sesion") return void await this.abreSesion();
297
+ if (que === "ir-gente") return this.p.vete("gente");
298
+ if (que === "ir-mapa") return this.p.vete("mapa");
299
+ if (que === "ir-puesto") return this.p.vete("puesto");
300
+ };
301
+ });
302
+ const ruta = lienzo.querySelector("#bvRuta");
303
+ if (ruta)
304
+ ruta.onkeydown = (e) => {
305
+ if (e.key !== "Enter" || !this.enCurso) return;
306
+ e.preventDefault();
307
+ const valor = ruta.value.trim();
308
+ if (valor) this.enCurso.montajes.push(valor);
309
+ this.recogeFormulario(lienzo);
310
+ void this.pinta(lienzo);
311
+ };
312
+ lienzo.querySelector("#bvNombre")?.focus();
313
+ }
314
+ /** Keep what is typed when a click forces a repaint. */
315
+ recogeFormulario(lienzo) {
316
+ if (!this.enCurso) return;
317
+ const nombre = lienzo.querySelector("#bvNombre");
318
+ const rol = lienzo.querySelector("#bvRol");
319
+ if (nombre) this.enCurso.nombre = nombre.value;
320
+ if (rol) this.enCurso.rol = rol.value;
321
+ }
322
+ async guardaAgente(lienzo) {
323
+ this.recogeFormulario(lienzo);
324
+ const a = this.enCurso;
325
+ if (!a) return;
326
+ if (!a.nombre.trim()) {
327
+ this.p.aviso("Give it a name \u2014 it is how you will call it in its window", true);
328
+ return;
329
+ }
330
+ this.guardando = true;
331
+ await this.pinta(lienzo);
332
+ try {
333
+ await this.aseguraFicha();
334
+ const r = await this.p.api("/api/agentes", {
335
+ method: "POST",
336
+ body: JSON.stringify({ name: a.nombre.trim(), kind: a.clase, role: a.rol })
337
+ });
338
+ if (!r.ok || !r.agent) {
339
+ this.p.aviso(r.error || "Could not add that agent", true);
340
+ return;
341
+ }
342
+ for (const ruta of a.montajes) {
343
+ const m = await this.p.api("/api/montaje", {
344
+ method: "POST",
345
+ body: JSON.stringify({ agent: r.agent, add: ruta })
346
+ });
347
+ if (!m.ok) this.p.aviso(`${ruta}: ${m.error ?? "could not mount"}`, true);
348
+ }
349
+ this.roster.push({ ...a, nombre: a.nombre.trim() });
350
+ this.enCurso = null;
351
+ this.p.aviso(`${a.nombre.trim()} joined the city`);
352
+ await this.p.refresca();
353
+ } catch (e) {
354
+ this.p.aviso(String(e), true);
355
+ } finally {
356
+ this.guardando = false;
357
+ await this.pinta(lienzo);
358
+ }
359
+ }
360
+ async aseguraFicha() {
361
+ await this.p.api("/api/ficha", {
362
+ method: "POST",
363
+ body: JSON.stringify({ domain: this.dominio || "software", role: this.rol || "blank" })
364
+ });
365
+ }
366
+ async avanza(lienzo) {
367
+ const escribeLaFicha = this.paso === 2;
368
+ this.paso = Math.min(PASOS.length - 1, this.paso + 1);
369
+ await this.pinta(lienzo);
370
+ if (escribeLaFicha) {
371
+ await this.aseguraFicha();
372
+ await this.p.refresca();
373
+ }
374
+ }
375
+ async abreSesion() {
376
+ try {
377
+ const r = await this.p.api("/api/sesion", {
378
+ method: "POST",
379
+ body: JSON.stringify({ user: this.p.yo })
380
+ });
381
+ this.p.aviso(
382
+ r.ok && r.attach ? `Session built \u2014 attach with: ${r.attach}` : r.error ?? "Could not",
383
+ !r.ok
384
+ );
385
+ } catch (e) {
386
+ this.p.aviso(String(e), true);
387
+ }
388
+ }
389
+ };
390
+
1
391
  // city/web/src/activity.ts
2
392
  var MAP_ACTIVITY_PROTOCOL = "agents-city-map-activity/1";
3
393
  function isMapNavMessage(value) {
@@ -67,10 +457,17 @@ var SECCIONES = [
67
457
  ["barrios", "Districts & houses"],
68
458
  ["red", "Roads"],
69
459
  ["committee", "Committee"],
70
- ["gente", "Agents & skills"]
460
+ ["gente", "Agents & skills"],
461
+ ["ciudades", "Cities"]
71
462
  ];
463
+ var primeraCarga = true;
72
464
  async function refresca() {
73
465
  E = await api("/api/estado");
466
+ if (primeraCarga) {
467
+ primeraCarga = false;
468
+ const mia = E.tarjetas.find((t) => t.user === E.yo);
469
+ if (!mia || (E.agents ?? []).length === 0) SECCION = "bienvenida";
470
+ }
74
471
  pinta();
75
472
  pintaAvisoDeVersion();
76
473
  pintaDemoControles();
@@ -392,6 +789,7 @@ function rail() {
392
789
  }).join("");
393
790
  todos("#rail li").forEach((li) => {
394
791
  li.onclick = () => {
792
+ document.body.classList.remove("enGuia");
395
793
  SECCION = li.dataset.s ?? "mapa";
396
794
  pinta();
397
795
  };
@@ -432,33 +830,136 @@ function pinta() {
432
830
  q(".cuerpo").scrollTop = 0;
433
831
  }
434
832
  var VISTAS = {};
833
+ var guia = null;
834
+ VISTAS.bienvenida = () => {
835
+ document.body.classList.add("enGuia");
836
+ guia ??= new Bienvenida({
837
+ api,
838
+ esc,
839
+ aviso: toast,
840
+ refresca,
841
+ vete: (seccion) => {
842
+ document.body.classList.remove("enGuia");
843
+ SECCION = seccion;
844
+ pinta();
845
+ },
846
+ ciudad: E.city_name,
847
+ yo: E.yo,
848
+ datos: corto(E.datos)
849
+ });
850
+ void guia.pinta(q("#lienzo"));
851
+ };
852
+ VISTAS.ciudades = () => {
853
+ const filas = E.ciudades.map(
854
+ (c) => `
855
+ <div class="fila ${c.actual ? "on" : ""}">
856
+ <span class="et">${esc(c.nombre)}${c.actual ? ' <b class="de">\xB7 open now</b>' : ""}</span>
857
+ <span class="de mono">${esc(corto(c.ruta))}</span>
858
+ <span style="margin-left:auto;display:flex;gap:6px">
859
+ ${c.actual ? "" : `<a class="bt mini" href="?city=${encodeURIComponent(c.ruta)}">open</a>
860
+ <button class="bt mini archiva" data-ciudad="${esc(c.ruta)}"
861
+ data-nombre="${esc(c.nombre)}">archive</button>`}
862
+ </span>
863
+ </div>`
864
+ ).join("");
865
+ q("#lienzo").innerHTML = `<div><span class="sub">cities</span>
866
+ <h1 style="margin-top:6px">Your cities</h1>
867
+ <p class="prosa" style="margin-top:8px">One person, several autonomous cities: each has
868
+ its own identity, domain, chair, agents and roads. They share nothing unless you build a
869
+ road between them. The one you have open is marked.</p></div>
870
+ <div class="lista"><div class="filas">${filas}</div></div>
871
+ <div class="campos" style="max-width:420px">
872
+ <div class="campo"><label>Start another city</label>
873
+ <input type="text" id="nuevaCiudad" placeholder="client-a, research, the book\u2026"></div>
874
+ <button class="bt ppal" id="creaCiudad">Create it</button>
875
+ </div>
876
+ <p class="pista">Archiving <b>moves</b> a city into
877
+ <code class="mono">.backups/</code> \u2014 nothing is deleted, and you can bring it back with
878
+ <code class="mono">mv</code>. The city you are standing in, and the last one you own,
879
+ cannot be archived.</p>`;
880
+ enlaza();
881
+ const campo = q("#nuevaCiudad");
882
+ q("#creaCiudad").onclick = async () => {
883
+ const nombre = campo.value.trim();
884
+ if (!nombre) {
885
+ toast("Give it a name", true);
886
+ return;
887
+ }
888
+ const r = await api("/api/ciudades", {
889
+ method: "POST",
890
+ body: JSON.stringify({ name: nombre })
891
+ });
892
+ if (!r.ok) {
893
+ toast(r.error ?? "Could not create it", true);
894
+ return;
895
+ }
896
+ toast(`${nombre} created`);
897
+ await refresca();
898
+ };
899
+ todos(".archiva").forEach((boton) => {
900
+ boton.onclick = async () => {
901
+ const nombre = boton.dataset.nombre ?? "";
902
+ if (!window.confirm(`Archive ${nombre}? It moves into .backups \u2014 nothing is deleted.`))
903
+ return;
904
+ const r = await api(
905
+ "/api/ciudad-archiva",
906
+ { method: "POST", body: JSON.stringify({ city: boton.dataset.ciudad ?? "" }) }
907
+ );
908
+ if (!r.ok) {
909
+ toast(r.error ?? "Could not archive it", true);
910
+ return;
911
+ }
912
+ toast(`${nombre} archived \u2014 it is in ${r.backup ?? ".backups"}`);
913
+ await refresca();
914
+ };
915
+ });
916
+ };
435
917
  VISTAS.resumen = () => {
436
918
  const mia = E.tarjetas.find((t) => t.user === E.yo);
437
919
  const sinUnidad = E.parcelas.filter((p) => p.unidad === "none" || p.unidad === "mine").length;
920
+ const agentes = E.agents ?? [];
438
921
  const tareas = [
439
922
  {
440
923
  ok: !!mia,
441
- txt: mia ? `Your card exists \u2014 you are <b>${esc(mia.agent)}</b>` : "Take your seat: role, folders, one goal",
924
+ 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",
442
925
  ir: "puesto"
443
926
  },
927
+ {
928
+ ok: agentes.length > 0,
929
+ txt: agentes.length === 0 ? "Add the <b>agents</b> that work in this city \u2014 each one whole: its kind, its role, and everything it works on" : `<b>${agentes.length}</b> agent${agentes.length === 1 ? "" : "s"} in this city: ${esc(
930
+ agentes.map((a) => a.name).join(", ")
931
+ )}`,
932
+ ir: "gente"
933
+ },
934
+ {
935
+ ok: agentes.length === 0 || agentes.some((a) => (a.mounts ?? []).length),
936
+ txt: agentes.some((a) => (a.mounts ?? []).length) ? "Your agents work on real repositories and folders" : "Give an agent something to work on \u2014 a repository, a worktree, a folder of documents",
937
+ ir: "gente"
938
+ },
444
939
  {
445
940
  ok: !!(mia && mia.goals_defined),
446
941
  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",
447
942
  ir: "puesto"
448
943
  },
449
944
  {
450
- ok: E.parcelas.length > 0 && sinUnidad === 0,
451
- txt: E.parcelas.length === 0 ? "No houses yet \u2014 pick your folders and they appear" : sinUnidad ? `<b>${sinUnidad}</b> of ${E.parcelas.length} houses sit in no real district \u2014 assign them` : "Every house has its district",
945
+ ok: E.parcelas.length === 0 || sinUnidad === 0,
946
+ 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",
452
947
  ir: "barrios"
453
948
  }
454
949
  ];
950
+ const empezando = !mia || agentes.length === 0;
455
951
  const sesionArriba = E.tmux.includes(E.sesion);
456
952
  q("#lienzo").innerHTML = `
457
- <div><span class="sub">your city</span>
458
- <h1 style="margin-top:6px">${esc(E.city_name)}</h1>
953
+ <div><span class="sub">${empezando ? "welcome" : "your city"}</span>
954
+ <h1 style="margin-top:6px">${empezando ? "Let\u2019s build " + esc(E.city_name) : esc(E.city_name)}</h1>
459
955
  <p class="de mono" style="margin-top:5px">${esc(E.address)}</p>
460
- <p class="prosa" style="margin-top:8px">Domain: <b>${esc(E.domain)}</b>. Growth is
461
- counted with <code class="mono">${esc(E.grow || "nothing yet")}</code>.</p></div>
956
+ ${empezando ? `<p class="prosa" style="margin-top:8px">A city is a chair and the agents behind
957
+ it. Two steps and it is alive: take your seat \u2014 the work domain and your role in
958
+ it \u2014 then add the agents, each one asked for whole: what kind of work it does,
959
+ its role, everything it works on, the engine that runs it and the skills it
960
+ starts with. Everything below is a plain file in
961
+ <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
962
+ counted with <code class="mono">${esc(E.grow || "nothing yet")}</code>.</p>`}</div>
462
963
  <div class="cifras">
463
964
  <div class="cifra"><b>${Object.keys(E.skills).length}</b><span>repo agents</span></div>
464
965
  <div class="cifra"><b>${E.parcelas.length}</b><span>houses</span></div>
@@ -1322,7 +1823,7 @@ window.addEventListener("message", (message) => {
1322
1823
  function temaActual() {
1323
1824
  const elegido = document.documentElement.getAttribute("data-tema");
1324
1825
  if (elegido) return elegido === "claro" ? "light" : "dark";
1325
- return window.matchMedia?.("(prefers-color-scheme: light)").matches ? "light" : "dark";
1826
+ return window.matchMedia?.("(prefers-color-scheme: dark)").matches ? "dark" : "light";
1326
1827
  }
1327
1828
  function tema() {
1328
1829
  const boton = document.getElementById("temaBoton");
@@ -1333,13 +1834,13 @@ function tema() {
1333
1834
  return "";
1334
1835
  }
1335
1836
  })();
1336
- const claroDelSistema = window.matchMedia?.("(prefers-color-scheme: light)").matches ?? false;
1837
+ const oscuroDelSistema = window.matchMedia?.("(prefers-color-scheme: dark)").matches ?? false;
1337
1838
  const pon = (elegido) => {
1338
1839
  const raiz = document.documentElement;
1339
1840
  if (elegido) raiz.setAttribute("data-tema", elegido);
1340
1841
  else raiz.removeAttribute("data-tema");
1341
- const claro = elegido ? elegido === "claro" : claroDelSistema;
1342
- if (boton) boton.textContent = claro ? "\u2600 Day" : "\u263E Night";
1842
+ const claro = elegido ? elegido === "claro" : !oscuroDelSistema;
1843
+ if (boton) boton.textContent = claro ? "\u263E Night" : "\u2600 Day";
1343
1844
  const frame = document.querySelector("#cityMapFrame");
1344
1845
  frame?.contentWindow?.postMessage(
1345
1846
  { type: "agents-city-map-theme/1", theme: claro ? "light" : "dark" },
@@ -1349,7 +1850,7 @@ function tema() {
1349
1850
  pon(guardado);
1350
1851
  if (boton)
1351
1852
  boton.onclick = () => {
1352
- const claroAhora = document.documentElement.getAttribute("data-tema") ? document.documentElement.getAttribute("data-tema") === "claro" : claroDelSistema;
1853
+ const claroAhora = document.documentElement.getAttribute("data-tema") ? document.documentElement.getAttribute("data-tema") === "claro" : !oscuroDelSistema;
1353
1854
  const elegido = claroAhora ? "oscuro" : "claro";
1354
1855
  try {
1355
1856
  localStorage.setItem("hall-tema", elegido);