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.
- package/.claude-plugin/marketplace.json +1 -1
- package/bin/hall.html +103 -54
- package/bin/navegador.mjs +59 -1
- package/bin/serve.py +21 -0
- package/bin/test-navegador.py +8 -1
- package/bin/test-seat.py +1 -1
- package/bin/test-serve.py +15 -9
- package/city/web/dist-hall/hall.js +514 -13
- package/city/web/src/bienvenida.ts +520 -0
- package/city/web/src/hall.ts +159 -13
- 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/cities.py +34 -0
|
@@ -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
|
|
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
|
|
451
|
-
txt: E.parcelas.length === 0 ? "
|
|
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"
|
|
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
|
-
|
|
461
|
-
|
|
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:
|
|
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
|
|
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" :
|
|
1342
|
-
if (boton) boton.textContent = claro ? "\
|
|
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" :
|
|
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);
|