agents-city 0.3.0 → 0.4.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 +54 -5
- package/README.md +53 -5
- package/bin/agents-city.js +4 -0
- package/bin/hall.html +92 -5
- package/bin/navegador.mjs +141 -12
- package/bin/serve.py +94 -38
- package/bin/test +6 -3
- package/bin/test-demo.py +67 -0
- package/bin/test-desinstala.py +170 -0
- package/bin/test-navegador.py +2 -0
- package/bin/test-serve.py +122 -2
- package/bin/uninstall +5 -0
- package/city/web/dist/city.js +65 -65
- package/city/web/dist/index.html +18 -5
- package/city/web/dist-hall/hall.js +941 -385
- package/city/web/index.html +17 -4
- package/city/web/src/bienvenida.ts +35 -329
- package/city/web/src/casa.ts +282 -0
- package/city/web/src/demo.ts +247 -0
- package/city/web/src/dialogo.ts +178 -0
- package/city/web/src/es.ts +96 -0
- package/city/web/src/explorador.ts +189 -0
- package/city/web/src/hall.ts +157 -54
- package/city/web/src/main.ts +28 -5
- package/demo/graba.py +132 -0
- package/demo/grabaciones/legal.jsonl +22 -0
- package/demo/grabaciones/medicina.jsonl +22 -0
- package/demo/grabaciones/software.jsonl +22 -0
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/scripts/demos.py +107 -0
- package/plugin/scripts/desinstala.py +222 -0
package/city/web/index.html
CHANGED
|
@@ -10,12 +10,24 @@
|
|
|
10
10
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Chakra+Petch:wght@500;600;700&family=Newsreader:opsz,wght@6..72,400;6..72,500&family=IBM+Plex+Mono:wght@400;500&display=swap">
|
|
11
11
|
<style>
|
|
12
12
|
:root{
|
|
13
|
+
/* Day is the base, exactly as in the Hall: the map is framed by it, and a
|
|
14
|
+
light Hall around a night map read as two products in one window. The
|
|
15
|
+
tokens are the Hall's, so the two frames cannot drift apart. Night is
|
|
16
|
+
stamped by the Hall's own switch, never guessed from the OS. */
|
|
17
|
+
--suelo:#f3f0e8; --suelo2:#fbfaf6; --suelo3:#ffffff; --linea:#e2ddd0; --linea2:#cdc6b6;
|
|
18
|
+
--tinta:#1d2229; --tinta2:#515861; --tinta3:#7b8187;
|
|
19
|
+
--nucleo:#8a7247; /* los colores de cada unidad llegan de la API */
|
|
20
|
+
--lampara:#7048e8; --carta:#dd5d3e;
|
|
21
|
+
--insignia-borde:#cdc6b6; --insignia-fondo:#ffffff;
|
|
22
|
+
--hud:"Chakra Petch",system-ui,sans-serif; --texto:"Newsreader",Georgia,serif;
|
|
23
|
+
--mono:"IBM Plex Mono",ui-monospace,monospace;
|
|
24
|
+
}
|
|
25
|
+
:root[data-tema="dark"]{
|
|
13
26
|
--suelo:#131a24; --suelo2:#1b2431; --suelo3:#222d3c; --linea:#26313f; --linea2:#38495f;
|
|
14
27
|
--tinta:#e8e2d4; --tinta2:#9aa7b8; --tinta3:#6b7889;
|
|
15
|
-
--nucleo:#c8b48a;
|
|
28
|
+
--nucleo:#c8b48a;
|
|
16
29
|
--lampara:#f5c451; --carta:#e2604f;
|
|
17
|
-
--
|
|
18
|
-
--mono:"IBM Plex Mono",ui-monospace,monospace;
|
|
30
|
+
--insignia-borde:#4a4127; --insignia-fondo:#241f14;
|
|
19
31
|
}
|
|
20
32
|
*{box-sizing:border-box}
|
|
21
33
|
html,body{height:100%}
|
|
@@ -42,7 +54,8 @@ aside{background:var(--suelo2);border-right:1px solid var(--linea);padding:20px
|
|
|
42
54
|
display:flex;flex-direction:column;gap:18px;overflow-y:auto}
|
|
43
55
|
.badge{display:inline-flex;align-items:center;gap:7px;align-self:flex-start;font-family:var(--hud);
|
|
44
56
|
font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--lampara);
|
|
45
|
-
border:1px solid
|
|
57
|
+
border:1px solid var(--insignia-borde);background:var(--insignia-fondo);
|
|
58
|
+
padding:4px 9px;border-radius:2px}
|
|
46
59
|
.badge::before{content:"";width:5px;height:5px;border-radius:50%;background:var(--lampara)}
|
|
47
60
|
h1{font-size:23px;line-height:1.05;margin:8px 0 5px}
|
|
48
61
|
p.sub{font-size:13px;line-height:1.5;color:var(--tinta2);margin:0}
|
|
@@ -16,28 +16,16 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import { plural, t as _ } from './idioma';
|
|
19
|
-
import {
|
|
19
|
+
import { CLASES, FormularioDeCasa } from './casa';
|
|
20
|
+
import type { DatosDeCasa, Rol } from './casa';
|
|
21
|
+
|
|
22
|
+
export type { Rol } from './casa';
|
|
20
23
|
|
|
21
24
|
export interface Dominio {
|
|
22
25
|
id: string;
|
|
23
26
|
name: string;
|
|
24
27
|
summary: string;
|
|
25
28
|
}
|
|
26
|
-
export interface Rol {
|
|
27
|
-
id: string;
|
|
28
|
-
name: string;
|
|
29
|
-
summary: string;
|
|
30
|
-
trade: string;
|
|
31
|
-
}
|
|
32
|
-
export interface RepoDelDisco {
|
|
33
|
-
nombre: string;
|
|
34
|
-
ruta: string;
|
|
35
|
-
/** repo · worktree · docs — a house can be given any of the three. */
|
|
36
|
-
clase: string;
|
|
37
|
-
cuando: string;
|
|
38
|
-
/** Touched recently, so almost certainly what this person came to pick. */
|
|
39
|
-
mio: boolean;
|
|
40
|
-
}
|
|
41
29
|
|
|
42
30
|
/** What the onboarding needs from the Hall. Passed in rather than imported, so
|
|
43
31
|
* this file cannot quietly grow its own way of talking to the server. */
|
|
@@ -53,38 +41,6 @@ export interface Puerta {
|
|
|
53
41
|
datos: string;
|
|
54
42
|
}
|
|
55
43
|
|
|
56
|
-
interface AgenteNuevo {
|
|
57
|
-
nombre: string;
|
|
58
|
-
clase: string;
|
|
59
|
-
rol: string;
|
|
60
|
-
montajes: string[];
|
|
61
|
-
/** Which CLI runs it, and — on Claude — the engine and how hard it thinks.
|
|
62
|
-
* Empty means the owner's default, which is a real answer and the one most
|
|
63
|
-
* people should give. Asked here because it is a decision somebody has
|
|
64
|
-
* already made in their head while naming the house; making them find three
|
|
65
|
-
* dropdowns afterwards is making them say it twice. */
|
|
66
|
-
runtime: string;
|
|
67
|
-
modelo: string;
|
|
68
|
-
esfuerzo: string;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const CLASES: Array<[string, string, string]> = [
|
|
72
|
-
[
|
|
73
|
-
'code',
|
|
74
|
-
'It writes code',
|
|
75
|
-
'Its house grows with the pull requests it merges. Mounts repositories and worktrees.',
|
|
76
|
-
],
|
|
77
|
-
[
|
|
78
|
-
'knowledge',
|
|
79
|
-
'It keeps knowledge',
|
|
80
|
-
'Its house grows with the documents it writes. Needs no git at all.',
|
|
81
|
-
],
|
|
82
|
-
['coordinator', 'It coordinates', 'Its house grows with the decisions it records.'],
|
|
83
|
-
];
|
|
84
|
-
|
|
85
|
-
/** One glyph per kind of place, so a chip says what it is without a legend. */
|
|
86
|
-
const CLASE_CORTA: Record<string, string> = { repo: 'git', worktree: 'wt', docs: 'doc' };
|
|
87
|
-
|
|
88
44
|
const PASOS = ['Welcome', 'The work', 'Your chair', 'The houses', 'Ready'];
|
|
89
45
|
|
|
90
46
|
/** A house, drawn rather than shipped: the same isometric shape the map and the
|
|
@@ -102,16 +58,11 @@ export class Bienvenida {
|
|
|
102
58
|
private dominios: Dominio[] = [];
|
|
103
59
|
private roles: Rol[] = [];
|
|
104
60
|
private rolesDeAgente: Rol[] = [];
|
|
105
|
-
private repos: RepoDelDisco[] = [];
|
|
106
61
|
private dominio = '';
|
|
107
62
|
private rol = '';
|
|
108
|
-
private roster:
|
|
109
|
-
private enCurso:
|
|
63
|
+
private roster: DatosDeCasa[] = [];
|
|
64
|
+
private enCurso: FormularioDeCasa | null = null;
|
|
110
65
|
private guardando = false;
|
|
111
|
-
private buscandoRepos = false;
|
|
112
|
-
private reposListos = false;
|
|
113
|
-
/** What is typed in the mount search box, kept across repaints. */
|
|
114
|
-
private filtro = '';
|
|
115
66
|
private lienzo: HTMLElement | null = null;
|
|
116
67
|
|
|
117
68
|
constructor(private readonly p: Puerta) {}
|
|
@@ -239,37 +190,22 @@ export class Bienvenida {
|
|
|
239
190
|
|
|
240
191
|
// ── 3. the agents ─────────────────────────────────────────────────────────
|
|
241
192
|
private pantallaAgentes(): string {
|
|
242
|
-
// The repository index is a disk scan — a minute on a big home the first
|
|
243
|
-
// time. Awaiting it here froze the guide on the PREVIOUS screen with no
|
|
244
|
-
// explanation: the person clicks Next and nothing happens. So the step
|
|
245
|
-
// draws immediately and the list arrives when it arrives.
|
|
246
|
-
this.buscaReposUnaVez();
|
|
247
193
|
if (!this.enCurso) return this.listaDeAgentes();
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
})
|
|
264
|
-
.catch(() => {
|
|
265
|
-
this.repos = []; // no index is not a reason to block the flow
|
|
266
|
-
})
|
|
267
|
-
.finally(() => {
|
|
268
|
-
this.buscandoRepos = false;
|
|
269
|
-
this.reposListos = true;
|
|
270
|
-
// Only repaint if this step is still on screen.
|
|
271
|
-
if (this.lienzo && this.paso === 3) void this.pinta(this.lienzo);
|
|
272
|
-
});
|
|
194
|
+
// The form mounts itself into this hole after the step is on screen: it
|
|
195
|
+
// repaints on its own as chips are ticked and the disk index arrives, and
|
|
196
|
+
// that must never take the step's frame down with it.
|
|
197
|
+
return `
|
|
198
|
+
<div class="bvPaso">
|
|
199
|
+
<span class="sub">${_('a new house')}</span>
|
|
200
|
+
<h1>${_('Who lives in it?')}</h1>
|
|
201
|
+
<div id="bvCasa"></div>
|
|
202
|
+
<div class="bvBotones">
|
|
203
|
+
<button class="bt" data-bv="cancela">${_('Cancel')}</button>
|
|
204
|
+
<button class="bt ppal" data-bv="guarda" ${this.guardando ? 'disabled' : ''}>${_(
|
|
205
|
+
this.guardando ? 'building…' : 'Build it',
|
|
206
|
+
)}</button>
|
|
207
|
+
</div>
|
|
208
|
+
</div>`;
|
|
273
209
|
}
|
|
274
210
|
|
|
275
211
|
private listaDeAgentes(): string {
|
|
@@ -317,134 +253,6 @@ export class Bienvenida {
|
|
|
317
253
|
* that keeps knowledge sees document folders first, because that is the one
|
|
318
254
|
* kind of agent the old picker could not help at all.
|
|
319
255
|
*/
|
|
320
|
-
private buscaEnElDisco(a: AgenteNuevo): RepoDelDisco[] {
|
|
321
|
-
const q = this.filtro.trim().toLowerCase();
|
|
322
|
-
const quiereDocs = a.clase === 'knowledge';
|
|
323
|
-
const puntua = (r: RepoDelDisco): number =>
|
|
324
|
-
(r.clase === 'docs' ? (quiereDocs ? -2 : 1) : 0) + (r.mio ? -1 : 0);
|
|
325
|
-
const encaja = q
|
|
326
|
-
? this.repos.filter(
|
|
327
|
-
(r) => r.nombre.toLowerCase().includes(q) || r.ruta.toLowerCase().includes(q),
|
|
328
|
-
)
|
|
329
|
-
: this.repos.filter((r) => r.mio);
|
|
330
|
-
// A mount already chosen stays visible even when the search moves past it,
|
|
331
|
-
// so unticking never means hunting for it again.
|
|
332
|
-
const elegidas = this.repos.filter((r) => a.montajes.includes(r.ruta) && !encaja.includes(r));
|
|
333
|
-
return elegidas
|
|
334
|
-
.concat(encaja)
|
|
335
|
-
.sort((x, y) => puntua(x) - puntua(y))
|
|
336
|
-
.slice(0, 40);
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
private formularioDeAgente(a: AgenteNuevo): string {
|
|
340
|
-
const clases = CLASES.map(
|
|
341
|
-
([id, titulo, porque]) => `
|
|
342
|
-
<button class="bvOpcion ${a.clase === id ? 'elegida' : ''}" data-bv="clase" data-id="${id}">
|
|
343
|
-
<b>${_(titulo)}</b><span>${_(porque)}</span></button>`,
|
|
344
|
-
).join('');
|
|
345
|
-
const roles = this.rolesDeAgente
|
|
346
|
-
.map(
|
|
347
|
-
(r) =>
|
|
348
|
-
`<option value="${this.p.esc(r.id)}"${r.id === a.rol ? ' selected' : ''}>${this.p.esc(
|
|
349
|
-
r.name,
|
|
350
|
-
)}</option>`,
|
|
351
|
-
)
|
|
352
|
-
.join('');
|
|
353
|
-
const esClaude = (a.runtime || 'claude') === 'claude';
|
|
354
|
-
const encontrados = this.buscaEnElDisco(a);
|
|
355
|
-
const sugeridos = encontrados
|
|
356
|
-
.map(
|
|
357
|
-
(r) => `<button class="bvChip cl-${this.p.esc(r.clase)} ${
|
|
358
|
-
a.montajes.includes(r.ruta) ? 'elegida' : ''
|
|
359
|
-
}"
|
|
360
|
-
data-bv="monta" data-ruta="${this.p.esc(r.ruta)}"
|
|
361
|
-
title="${this.p.esc(r.ruta)}${r.cuando ? ` · ${this.p.esc(r.cuando)}` : ''}"
|
|
362
|
-
><i class="bvClase">${this.p.esc(CLASE_CORTA[r.clase] ?? r.clase)}</i>${this.p.esc(
|
|
363
|
-
r.nombre,
|
|
364
|
-
)}</button>`,
|
|
365
|
-
)
|
|
366
|
-
.join('');
|
|
367
|
-
return `
|
|
368
|
-
<div class="bvPaso">
|
|
369
|
-
<span class="sub">${_('a new house')}</span>
|
|
370
|
-
<h1>${_('Who lives in it?')}</h1>
|
|
371
|
-
<div class="campo"><label>${_('What do you call it?')}</label>
|
|
372
|
-
<input type="text" id="bvNombre" value="${this.p.esc(a.nombre)}"
|
|
373
|
-
placeholder="${_('urgencias, api, the handbook — whatever you would say out loud')}"></div>
|
|
374
|
-
<label class="bvEtiqueta">${_('What kind of work does it do?')}</label>
|
|
375
|
-
<div class="bvRejilla bvTres">${clases}</div>
|
|
376
|
-
<div class="campo"><label>${_('Its role — its speciality, never authority')}</label>
|
|
377
|
-
<select id="bvRol">${roles}</select></div>
|
|
378
|
-
<label class="bvEtiqueta">${_('What runs it?')}</label>
|
|
379
|
-
<p class="pista">${_(
|
|
380
|
-
'Leave all three on default and it runs the way you do. Model and effort are Claude’s to read; another CLI carries its own flags.',
|
|
381
|
-
)}</p>
|
|
382
|
-
<div class="bvMotor">
|
|
383
|
-
<div class="campo"><label>${_('provider')}</label>
|
|
384
|
-
<select id="bvRuntime">${opciones(RUNTIMES, a.runtime, this.p.esc, 'claude')}</select></div>
|
|
385
|
-
<div class="campo"><label>${_('engine')}</label>
|
|
386
|
-
<select id="bvModelo" ${esClaude ? '' : 'disabled'}>${opciones(
|
|
387
|
-
[''].concat(MODELOS),
|
|
388
|
-
a.modelo,
|
|
389
|
-
this.p.esc,
|
|
390
|
-
_('default'),
|
|
391
|
-
)}</select></div>
|
|
392
|
-
<div class="campo"><label>${_('effort')}</label>
|
|
393
|
-
<select id="bvEsfuerzo" ${esClaude ? '' : 'disabled'}>${opciones(
|
|
394
|
-
[''].concat(ESFUERZOS),
|
|
395
|
-
a.esfuerzo,
|
|
396
|
-
this.p.esc,
|
|
397
|
-
_('default'),
|
|
398
|
-
)}</select></div>
|
|
399
|
-
</div>
|
|
400
|
-
<label class="bvEtiqueta">${_('What does it work on?')}</label>
|
|
401
|
-
<p class="pista">${_(`Everything found on this disk: <b>repositories</b>, the
|
|
402
|
-
<b>worktrees</b> an isolated agent works in, and <b>folders of documents</b> with no git
|
|
403
|
-
at all. Pick as many as you like — this is the part that makes it an agent and not a
|
|
404
|
-
repo.`)}</p>
|
|
405
|
-
<div class="bvBuscador">
|
|
406
|
-
<input type="search" id="bvBusca" value="${this.p.esc(this.filtro)}"
|
|
407
|
-
placeholder="${_('search your disk — a name, or part of a path')}">
|
|
408
|
-
<button class="bt bvMini" data-bv="rebusca" ${this.buscandoRepos ? 'disabled' : ''}
|
|
409
|
-
title="${_('Look through the disk again — for something cloned a minute ago')}">${_(
|
|
410
|
-
this.buscandoRepos ? 'searching…' : 'Search again',
|
|
411
|
-
)}</button>
|
|
412
|
-
</div>
|
|
413
|
-
${
|
|
414
|
-
sugeridos
|
|
415
|
-
? `<div class="bvChips">${sugeridos}</div>
|
|
416
|
-
<p class="pista">${
|
|
417
|
-
this.filtro
|
|
418
|
-
? _('{shown} of {total} places match', {
|
|
419
|
-
shown: String(encontrados.length),
|
|
420
|
-
total: String(this.repos.length),
|
|
421
|
-
})
|
|
422
|
-
: _('{total} places found — type to narrow it down', {
|
|
423
|
-
total: String(this.repos.length),
|
|
424
|
-
})
|
|
425
|
-
}</p>`
|
|
426
|
-
: this.buscandoRepos
|
|
427
|
-
? `<p class="cargando">${_('looking through your disk…')}</p>`
|
|
428
|
-
: this.filtro
|
|
429
|
-
? `<p class="pista">${_('Nothing matches that. Add it by path below.')}</p>`
|
|
430
|
-
: `<p class="pista">${_('Nothing found on this disk — add folders by path below.')}</p>`
|
|
431
|
-
}
|
|
432
|
-
<div class="campo"><label>${_('…or a path, and Enter')}</label>
|
|
433
|
-
<input type="text" id="bvRuta" placeholder="~/documents/handbook"></div>
|
|
434
|
-
${
|
|
435
|
-
a.montajes.length
|
|
436
|
-
? `<p class="pista">${_('Working on: {que}', { que: `<b>${this.p.esc(a.montajes.join(', '))}</b>` })}</p>`
|
|
437
|
-
: ''
|
|
438
|
-
}
|
|
439
|
-
<div class="bvBotones">
|
|
440
|
-
<button class="bt" data-bv="cancela">${_('Cancel')}</button>
|
|
441
|
-
<button class="bt ppal" data-bv="guarda" ${this.guardando ? 'disabled' : ''}>${_(
|
|
442
|
-
this.guardando ? 'building…' : 'Build it',
|
|
443
|
-
)}</button>
|
|
444
|
-
</div>
|
|
445
|
-
</div>`;
|
|
446
|
-
}
|
|
447
|
-
|
|
448
256
|
// ── 4. done ───────────────────────────────────────────────────────────────
|
|
449
257
|
private pantallaFinal(): string {
|
|
450
258
|
return `
|
|
@@ -498,33 +306,10 @@ export class Bienvenida {
|
|
|
498
306
|
return repinta();
|
|
499
307
|
}
|
|
500
308
|
if (que === 'nuevo') {
|
|
501
|
-
this.enCurso =
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
montajes: [],
|
|
506
|
-
runtime: '',
|
|
507
|
-
modelo: '',
|
|
508
|
-
esfuerzo: '',
|
|
509
|
-
};
|
|
510
|
-
return repinta();
|
|
511
|
-
}
|
|
512
|
-
if (que === 'clase' && this.enCurso) {
|
|
513
|
-
this.enCurso.clase = el.dataset.id ?? 'code';
|
|
514
|
-
this.recogeFormulario(lienzo);
|
|
515
|
-
return repinta();
|
|
516
|
-
}
|
|
517
|
-
if (que === 'monta' && this.enCurso) {
|
|
518
|
-
const ruta = el.dataset.ruta ?? '';
|
|
519
|
-
const ya = this.enCurso.montajes.indexOf(ruta);
|
|
520
|
-
if (ya >= 0) this.enCurso.montajes.splice(ya, 1);
|
|
521
|
-
else this.enCurso.montajes.push(ruta);
|
|
522
|
-
this.recogeFormulario(lienzo);
|
|
523
|
-
return repinta();
|
|
524
|
-
}
|
|
525
|
-
if (que === 'rebusca') {
|
|
526
|
-
this.recogeFormulario(lienzo);
|
|
527
|
-
this.buscaReposUnaVez(true);
|
|
309
|
+
this.enCurso = new FormularioDeCasa(
|
|
310
|
+
{ api: this.p.api, esc: this.p.esc, aviso: this.p.aviso, yo: this.p.yo },
|
|
311
|
+
this.rolesDeAgente,
|
|
312
|
+
);
|
|
528
313
|
return repinta();
|
|
529
314
|
}
|
|
530
315
|
if (que === 'cancela') {
|
|
@@ -539,107 +324,28 @@ export class Bienvenida {
|
|
|
539
324
|
if (que === 'ir-puesto') return this.p.vete('puesto');
|
|
540
325
|
};
|
|
541
326
|
});
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
if (busca) {
|
|
547
|
-
busca.oninput = () => {
|
|
548
|
-
this.filtro = busca.value;
|
|
549
|
-
const donde = busca.selectionStart ?? busca.value.length;
|
|
550
|
-
void this.pinta(lienzo).then(() => {
|
|
551
|
-
const otra = lienzo.querySelector<HTMLInputElement>('#bvBusca');
|
|
552
|
-
if (!otra) return;
|
|
553
|
-
otra.focus();
|
|
554
|
-
otra.setSelectionRange(donde, donde);
|
|
555
|
-
});
|
|
556
|
-
};
|
|
327
|
+
const hueco = lienzo.querySelector<HTMLElement>('#bvCasa');
|
|
328
|
+
if (hueco && this.enCurso) {
|
|
329
|
+
this.enCurso.monta(hueco);
|
|
330
|
+
hueco.querySelector<HTMLInputElement>('#bvNombre')?.focus();
|
|
557
331
|
}
|
|
558
|
-
const ruta = lienzo.querySelector<HTMLInputElement>('#bvRuta');
|
|
559
|
-
if (ruta)
|
|
560
|
-
ruta.onkeydown = (e) => {
|
|
561
|
-
if (e.key !== 'Enter' || !this.enCurso) return;
|
|
562
|
-
e.preventDefault();
|
|
563
|
-
const valor = ruta.value.trim();
|
|
564
|
-
if (valor) this.enCurso.montajes.push(valor);
|
|
565
|
-
this.recogeFormulario(lienzo);
|
|
566
|
-
void this.pinta(lienzo);
|
|
567
|
-
};
|
|
568
|
-
lienzo.querySelector<HTMLSelectElement>('#bvRuntime')?.addEventListener('change', () => {
|
|
569
|
-
this.recogeFormulario(lienzo);
|
|
570
|
-
void this.pinta(lienzo);
|
|
571
|
-
});
|
|
572
|
-
if (!busca?.value) lienzo.querySelector<HTMLInputElement>('#bvNombre')?.focus();
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
/** Keep what is typed when a click forces a repaint. */
|
|
576
|
-
private recogeFormulario(lienzo: HTMLElement): void {
|
|
577
|
-
if (!this.enCurso) return;
|
|
578
|
-
const nombre = lienzo.querySelector<HTMLInputElement>('#bvNombre');
|
|
579
|
-
const rol = lienzo.querySelector<HTMLSelectElement>('#bvRol');
|
|
580
|
-
if (nombre) this.enCurso.nombre = nombre.value;
|
|
581
|
-
if (rol) this.enCurso.rol = rol.value;
|
|
582
|
-
for (const [id, campo] of [
|
|
583
|
-
['#bvRuntime', 'runtime'],
|
|
584
|
-
['#bvModelo', 'modelo'],
|
|
585
|
-
['#bvEsfuerzo', 'esfuerzo'],
|
|
586
|
-
] as const) {
|
|
587
|
-
const sel = lienzo.querySelector<HTMLSelectElement>(id);
|
|
588
|
-
if (sel) this.enCurso[campo] = sel.value;
|
|
589
|
-
}
|
|
590
|
-
const busca = lienzo.querySelector<HTMLInputElement>('#bvBusca');
|
|
591
|
-
if (busca) this.filtro = busca.value;
|
|
592
332
|
}
|
|
593
333
|
|
|
594
334
|
private async guardaAgente(lienzo: HTMLElement): Promise<void> {
|
|
595
|
-
this.
|
|
596
|
-
|
|
597
|
-
if (!a) return;
|
|
598
|
-
if (!a.nombre.trim()) {
|
|
599
|
-
this.p.aviso('Give it a name — it is how you will call it in its window', true);
|
|
600
|
-
return;
|
|
601
|
-
}
|
|
335
|
+
const forma = this.enCurso;
|
|
336
|
+
if (!forma) return;
|
|
602
337
|
this.guardando = true;
|
|
603
338
|
await this.pinta(lienzo);
|
|
604
339
|
try {
|
|
605
340
|
// The seat has to exist before an agent can join it: agents live on the
|
|
606
341
|
// owner's card. Writing it here is why this step can come before the goal.
|
|
607
342
|
await this.aseguraFicha();
|
|
608
|
-
const
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
});
|
|
612
|
-
if (!r.ok || !r.agent) {
|
|
613
|
-
this.p.aviso(r.error || 'Could not add that agent', true);
|
|
614
|
-
return;
|
|
615
|
-
}
|
|
616
|
-
for (const ruta of a.montajes) {
|
|
617
|
-
const m = await this.p.api<{ ok?: boolean; error?: string }>('/api/montaje', {
|
|
618
|
-
method: 'POST',
|
|
619
|
-
body: JSON.stringify({ agent: r.agent, add: ruta }),
|
|
620
|
-
});
|
|
621
|
-
if (!m.ok) this.p.aviso(`${ruta}: ${m.error ?? 'could not mount'}`, true);
|
|
622
|
-
}
|
|
623
|
-
// The engine is a second write by necessity: `/api/agentes` owns who is
|
|
624
|
-
// on the roster and `/api/agente` owns how one is run, and neither is
|
|
625
|
-
// allowed to learn the other's job just to save a round trip.
|
|
626
|
-
const motor: Record<string, string> = {};
|
|
627
|
-
if (a.runtime) motor.runtime = a.runtime;
|
|
628
|
-
if (a.modelo) motor.model = a.modelo;
|
|
629
|
-
if (a.esfuerzo) motor.effort = a.esfuerzo;
|
|
630
|
-
if (Object.keys(motor).length) {
|
|
631
|
-
const m = await this.p.api<{ ok?: boolean; error?: string }>('/api/agente', {
|
|
632
|
-
method: 'POST',
|
|
633
|
-
body: JSON.stringify({ agent: r.agent, ...motor }),
|
|
634
|
-
});
|
|
635
|
-
if (!m.ok) this.p.aviso(m.error ?? 'Could not set its engine', true);
|
|
636
|
-
}
|
|
637
|
-
this.roster.push({ ...a, nombre: a.nombre.trim() });
|
|
343
|
+
const hecho = await forma.guarda();
|
|
344
|
+
if (!hecho) return;
|
|
345
|
+
this.roster.push({ ...forma.datos, nombre: hecho.nombre });
|
|
638
346
|
this.enCurso = null;
|
|
639
|
-
this.p.aviso(
|
|
347
|
+
this.p.aviso(_('{name} joined the city', { name: hecho.nombre }));
|
|
640
348
|
await this.p.refresca();
|
|
641
|
-
} catch (e) {
|
|
642
|
-
this.p.aviso(String(e), true);
|
|
643
349
|
} finally {
|
|
644
350
|
this.guardando = false;
|
|
645
351
|
await this.pinta(lienzo);
|