agents-city 0.4.0 → 0.5.2
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 +240 -11
- package/README.md +238 -11
- package/benchmarks/latency/fake-claude-cli.mjs +2 -1
- package/benchmarks/reception/README.md +16 -0
- package/benchmarks/reception/run.mjs +84 -0
- package/bin/agents-city.js +1 -0
- package/bin/connect +5 -0
- package/bin/hall.html +117 -18
- package/bin/navegador.mjs +170 -1
- package/bin/serve.py +201 -101
- package/bin/test +15 -3
- package/bin/test-arnes.py +192 -0
- package/bin/test-cage.py +9 -0
- package/bin/test-channel.py +178 -12
- package/bin/test-claude-runtime.py +132 -4
- package/bin/test-connect-client.mjs +609 -0
- package/bin/test-connect.py +52 -0
- package/bin/test-contracts.py +214 -0
- package/bin/test-diario.py +185 -0
- package/bin/test-doctor.py +31 -0
- package/bin/test-i18n.py +189 -0
- package/bin/test-navegador.py +61 -0
- package/bin/test-seat.py +78 -20
- package/bin/test-security.py +2 -0
- package/bin/test-serve.py +366 -0
- package/city/web/dist-hall/hall.js +789 -214
- package/city/web/src/bienvenida.ts +14 -13
- package/city/web/src/casa.ts +143 -56
- package/city/web/src/demo.ts +61 -31
- package/city/web/src/es.ts +187 -27
- package/city/web/src/explorador.ts +70 -43
- package/city/web/src/hall.ts +520 -103
- package/city/web/src/idioma.ts +21 -1
- package/city/web/src/motores.ts +48 -1
- package/city/web/src/vista.ts +58 -0
- package/demo/graba.py +5 -10
- package/docs/managed-connect.md +237 -0
- package/docs/security.md +41 -2
- package/package.json +8 -4
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/channel/adapter-prompts.ts +10 -0
- package/plugin/channel/adapter.js +78 -31
- package/plugin/channel/agents_city_hybrid_crypto_bg.wasm +0 -0
- package/plugin/channel/bus.js +109 -65
- package/plugin/channel/bus.ts +11 -1
- package/plugin/channel/client.js +67 -30
- package/plugin/channel/delivery-queue.ts +160 -21
- package/plugin/channel/hub/remote-roads.ts +32 -1
- package/plugin/channel/hub/road-controller.ts +116 -16
- package/plugin/channel/kinsh_vodozemac_wasm_bg.wasm +0 -0
- package/plugin/channel/licenses/CONNECT_CLIENT_THIRD_PARTY_NOTICES.md +21 -0
- package/plugin/channel/licenses/HYBRID_CRYPTO_THIRD_PARTY_NOTICES.md +12 -0
- package/plugin/channel/licenses/hybrid-crypto-Apache-2.0.txt +201 -0
- package/plugin/channel/licenses/keyring-MIT.txt +21 -0
- package/plugin/channel/licenses/vodozemac-Apache-2.0.txt +201 -0
- package/plugin/channel/local-hub.js +2080 -97
- package/plugin/channel/local-hub.ts +5 -4
- package/plugin/channel/managed-connect/bridge.ts +208 -0
- package/plugin/channel/managed-connect/cli.ts +416 -0
- package/plugin/channel/managed-connect/device.ts +15 -0
- package/plugin/channel/managed-connect/local-cities.ts +94 -0
- package/plugin/channel/managed-connect/person-message.ts +74 -0
- package/plugin/channel/managed-connect/reception-bridge.ts +341 -0
- package/plugin/channel/managed-connect/relay-session.ts +7 -0
- package/plugin/channel/managed-connect/storage.ts +653 -0
- package/plugin/channel/managed-connect/transport.ts +87 -0
- package/plugin/channel/managed-connect-cli.js +4777 -0
- package/plugin/channel/managed-connect-cli.ts +7 -0
- package/plugin/channel/managed-connect-client.d.ts +261 -0
- package/plugin/channel/managed-connect-client.js +6572 -0
- package/plugin/channel/managed-connect-client.manifest.json +38 -0
- package/plugin/channel/package-lock.json +123 -105
- package/plugin/channel/package.json +4 -4
- package/plugin/channel/protocol.ts +4 -0
- package/plugin/channel/reception.ts +896 -0
- package/plugin/channel/road-cli.ts +1 -1
- package/plugin/channel/runtime/arnes.json +189 -0
- package/plugin/channel/runtime/arnes.ts +47 -0
- package/plugin/channel/runtime/claude.ts +18 -0
- package/plugin/channel/runtime/codex-config.ts +51 -1
- package/plugin/channel/runtime/codex.ts +31 -11
- package/plugin/channel/runtime/kimi.ts +6 -4
- package/plugin/channel/runtime-files.ts +39 -5
- package/plugin/channel/runtime-gateway.js +426 -100
- package/plugin/channel/runtime-gateway.ts +12 -0
- package/plugin/channel/trust/agents-city-sandbox-roots.json +66 -0
- package/plugin/scripts/arnes.py +271 -0
- package/plugin/scripts/busca.py +46 -10
- package/plugin/scripts/cage.py +5 -0
- package/plugin/scripts/city-session.sh +144 -28
- package/plugin/scripts/crecimiento.py +4 -1
- package/plugin/scripts/demos.py +58 -43
- package/plugin/scripts/desinstala.py +36 -21
- package/plugin/scripts/diario.py +119 -0
- package/plugin/scripts/doctor.py +90 -15
- package/plugin/scripts/read-card.py +48 -8
- package/plugin/scripts/reception.py +665 -0
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import { plural, t as _ } from './idioma';
|
|
19
|
-
import {
|
|
19
|
+
import { FormularioDeCasa } from './casa';
|
|
20
20
|
import type { DatosDeCasa, Rol } from './casa';
|
|
21
21
|
|
|
22
22
|
export type { Rol } from './casa';
|
|
@@ -46,7 +46,7 @@ const PASOS = ['Welcome', 'The work', 'Your chair', 'The houses', 'Ready'];
|
|
|
46
46
|
/** A house, drawn rather than shipped: the same isometric shape the map and the
|
|
47
47
|
* desktop icon use, so the first thing somebody sees is already the product. */
|
|
48
48
|
function casita(color: string): string {
|
|
49
|
-
return `<svg viewBox="0 0 120 108" role="img" aria-label="an isometric house" class="bvCasa">
|
|
49
|
+
return `<svg viewBox="0 0 120 108" role="img" aria-label="${_('an isometric house')}" class="bvCasa">
|
|
50
50
|
<polygon points="60,10 112,40 60,70 8,40" fill="${color}" opacity=".95"/>
|
|
51
51
|
<polygon points="8,40 60,70 60,100 8,70" fill="${color}" opacity=".62"/>
|
|
52
52
|
<polygon points="112,40 60,70 60,100 112,70" fill="${color}" opacity=".40"/>
|
|
@@ -67,7 +67,18 @@ export class Bienvenida {
|
|
|
67
67
|
|
|
68
68
|
constructor(private readonly p: Puerta) {}
|
|
69
69
|
|
|
70
|
-
/**
|
|
70
|
+
/**
|
|
71
|
+
* The view contract, so the Hall's dispatcher treats every section the same.
|
|
72
|
+
*
|
|
73
|
+
* The painting itself is async — the first one asks the server for the domain
|
|
74
|
+
* packs — and a router cannot wait on a view. It does not need to: the guide
|
|
75
|
+
* draws a loading line first and fills itself in.
|
|
76
|
+
*/
|
|
77
|
+
monta(host: HTMLElement): void {
|
|
78
|
+
void this.pinta(host);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Draw the current step into `lienzo`. */
|
|
71
82
|
async pinta(lienzo: HTMLElement): Promise<void> {
|
|
72
83
|
this.lienzo = lienzo;
|
|
73
84
|
if (!this.dominios.length) {
|
|
@@ -243,16 +254,6 @@ export class Bienvenida {
|
|
|
243
254
|
</div>`;
|
|
244
255
|
}
|
|
245
256
|
|
|
246
|
-
/**
|
|
247
|
-
* The places on this disk worth offering for this house, ranked.
|
|
248
|
-
*
|
|
249
|
-
* With nothing typed it is what you touched recently, because on a machine
|
|
250
|
-
* with three hundred clones a list is not an answer. What is typed matches
|
|
251
|
-
* the name and the path both — people look for `rankia` and for `Documents`
|
|
252
|
-
* with equal frequency, and only one of those is a repository name. A house
|
|
253
|
-
* that keeps knowledge sees document folders first, because that is the one
|
|
254
|
-
* kind of agent the old picker could not help at all.
|
|
255
|
-
*/
|
|
256
257
|
// ── 4. done ───────────────────────────────────────────────────────────────
|
|
257
258
|
private pantallaFinal(): string {
|
|
258
259
|
return `
|
package/city/web/src/casa.ts
CHANGED
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
|
|
14
14
|
import { plural, t as _ } from './idioma';
|
|
15
15
|
import { Explorador } from './explorador';
|
|
16
|
-
import { ESFUERZOS,
|
|
16
|
+
import { ESFUERZOS, RUNTIMES, motorDe, opciones } from './motores';
|
|
17
|
+
import { Montada } from './vista';
|
|
17
18
|
|
|
18
19
|
export interface Rol {
|
|
19
20
|
id: string;
|
|
@@ -57,7 +58,7 @@ export const CLASES: Array<[string, string, string]> = [
|
|
|
57
58
|
['coordinator', 'It coordinates', 'Its house grows with the decisions it records.'],
|
|
58
59
|
];
|
|
59
60
|
|
|
60
|
-
export class FormularioDeCasa {
|
|
61
|
+
export class FormularioDeCasa extends Montada {
|
|
61
62
|
readonly datos: DatosDeCasa = {
|
|
62
63
|
nombre: '',
|
|
63
64
|
clase: 'code',
|
|
@@ -68,40 +69,70 @@ export class FormularioDeCasa {
|
|
|
68
69
|
esfuerzo: '',
|
|
69
70
|
};
|
|
70
71
|
private roles: Rol[] = [];
|
|
71
|
-
private
|
|
72
|
-
|
|
72
|
+
private rolesPedidos = false;
|
|
73
73
|
constructor(
|
|
74
74
|
private readonly p: Puerta,
|
|
75
75
|
roles: Rol[] = [],
|
|
76
76
|
) {
|
|
77
|
+
super();
|
|
77
78
|
this.roles = roles;
|
|
78
79
|
}
|
|
79
80
|
|
|
80
|
-
|
|
81
|
-
monta(host: HTMLElement): void {
|
|
82
|
-
this.host = host;
|
|
81
|
+
override monta(host: HTMLElement): void {
|
|
83
82
|
void this.aseguraRoles();
|
|
84
|
-
|
|
85
|
-
this.
|
|
83
|
+
super.monta(host);
|
|
84
|
+
// The picker is a view of its own, living in a hole this one leaves for it.
|
|
86
85
|
const hueco = host.querySelector<HTMLElement>('.casaExp');
|
|
87
86
|
if (hueco) this.explora().monta(hueco);
|
|
88
87
|
}
|
|
89
88
|
|
|
90
|
-
|
|
89
|
+
protected override repinta(): void {
|
|
90
|
+
// A repaint here rebuilds the hole too, so the picker has to be remounted
|
|
91
|
+
// into the new one — which is what `monta` does.
|
|
91
92
|
if (this.host) this.monta(this.host);
|
|
92
93
|
}
|
|
93
94
|
|
|
95
|
+
/** Asked once, ever. `monta` runs on every repaint, so a guard on "did we get
|
|
96
|
+
* any roles back" would refetch — and repaint, and refetch — forever the day
|
|
97
|
+
* the catalogue comes back empty. */
|
|
94
98
|
private async aseguraRoles(): Promise<void> {
|
|
95
|
-
if (this.
|
|
99
|
+
if (this.rolesPedidos) return;
|
|
100
|
+
this.rolesPedidos = true;
|
|
96
101
|
try {
|
|
97
102
|
const r = await this.p.api<{ roles: Rol[] }>('/api/roles?scope=agent');
|
|
98
103
|
this.roles = r.roles ?? [];
|
|
99
|
-
|
|
104
|
+
// Only the one control that changed. Rebuilding the whole form because a
|
|
105
|
+
// dropdown filled in is how the name got lost in the first place, and it
|
|
106
|
+
// also took the folder picker down mid-walk.
|
|
107
|
+
this.pintaRoles();
|
|
100
108
|
} catch {
|
|
101
109
|
this.roles = []; // the role list is a convenience; blank is always valid
|
|
102
110
|
}
|
|
103
111
|
}
|
|
104
112
|
|
|
113
|
+
/** The role list, once it arrives, without touching anything else. */
|
|
114
|
+
private pintaRoles(): void {
|
|
115
|
+
const sel = this.host?.querySelector<HTMLSelectElement>('#bvRol');
|
|
116
|
+
if (!sel || !this.roles.length) return;
|
|
117
|
+
sel.innerHTML = this.opcionesDeRol();
|
|
118
|
+
sel.value = this.datos.rol;
|
|
119
|
+
// A select that answers to nothing is a select that silently keeps the
|
|
120
|
+
// first option no matter what the person picks.
|
|
121
|
+
sel.onchange = () => this.recoge();
|
|
122
|
+
this.recoge();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private opcionesDeRol(): string {
|
|
126
|
+
return this.roles
|
|
127
|
+
.map(
|
|
128
|
+
(r) =>
|
|
129
|
+
`<option value="${this.p.esc(r.id)}"${
|
|
130
|
+
r.id === this.datos.rol ? ' selected' : ''
|
|
131
|
+
}>${this.p.esc(r.name)}</option>`,
|
|
132
|
+
)
|
|
133
|
+
.join('');
|
|
134
|
+
}
|
|
135
|
+
|
|
105
136
|
/** The folder picker, built once so walking survives a repaint. */
|
|
106
137
|
private explorador: Explorador | null = null;
|
|
107
138
|
|
|
@@ -113,14 +144,17 @@ export class FormularioDeCasa {
|
|
|
113
144
|
const ya = this.datos.montajes.indexOf(ruta);
|
|
114
145
|
if (ya >= 0) this.datos.montajes.splice(ya, 1);
|
|
115
146
|
else this.datos.montajes.push(ruta);
|
|
116
|
-
|
|
147
|
+
// Only the strip of chosen chips changes here. Repainting the whole
|
|
148
|
+
// form tore the picker down and rebuilt it mid-click — one tick
|
|
149
|
+
// rendered two thousand rows twice, and lost the person's place.
|
|
150
|
+
this.pintaElegidas();
|
|
117
151
|
},
|
|
118
152
|
() => this.datos.montajes,
|
|
119
153
|
);
|
|
120
154
|
return this.explorador;
|
|
121
155
|
}
|
|
122
156
|
|
|
123
|
-
html(): string {
|
|
157
|
+
protected html(): string {
|
|
124
158
|
const a = this.datos;
|
|
125
159
|
const esc = this.p.esc;
|
|
126
160
|
const clases = CLASES.map(
|
|
@@ -135,7 +169,7 @@ export class FormularioDeCasa {
|
|
|
135
169
|
`<option value="${esc(r.id)}"${r.id === a.rol ? ' selected' : ''}>${esc(r.name)}</option>`,
|
|
136
170
|
)
|
|
137
171
|
.join('');
|
|
138
|
-
const
|
|
172
|
+
const motor = motorDe(a.runtime);
|
|
139
173
|
return `
|
|
140
174
|
<div class="campo"><label>${_('What do you call it?')}</label>
|
|
141
175
|
<input type="text" id="bvNombre" value="${esc(a.nombre)}"
|
|
@@ -146,52 +180,91 @@ export class FormularioDeCasa {
|
|
|
146
180
|
<select id="bvRol">${roles}</select></div>
|
|
147
181
|
<label class="bvEtiqueta">${_('What runs it?')}</label>
|
|
148
182
|
<p class="pista">${_(
|
|
149
|
-
'Leave all three on default and it runs the way you do.
|
|
183
|
+
'Leave all three on default and it runs the way you do. Whatever you set here is written once on the card, and the launcher hands it to whichever CLI runs this house.',
|
|
150
184
|
)}</p>
|
|
151
185
|
<div class="bvMotor">
|
|
152
186
|
<div class="campo"><label>${_('provider')}</label>
|
|
153
187
|
<select id="bvRuntime">${opciones(RUNTIMES, a.runtime, esc, 'claude')}</select></div>
|
|
154
188
|
<div class="campo"><label>${_('engine')}</label>
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
189
|
+
${
|
|
190
|
+
motor.modelos.length
|
|
191
|
+
? `<select id="bvModelo">${opciones(
|
|
192
|
+
[''].concat(motor.modelos),
|
|
193
|
+
a.modelo,
|
|
194
|
+
esc,
|
|
195
|
+
_('default'),
|
|
196
|
+
)}</select>`
|
|
197
|
+
: `<input type="text" id="bvModelo" value="${esc(a.modelo)}"
|
|
198
|
+
placeholder="${_('default')}" autocomplete="off" spellcheck="false">`
|
|
199
|
+
}</div>
|
|
161
200
|
<div class="campo"><label>${_('effort')}</label>
|
|
162
|
-
<select id="bvEsfuerzo" ${
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
201
|
+
<select id="bvEsfuerzo" ${motor.esfuerzo ? '' : 'disabled'}
|
|
202
|
+
title="${motor.esfuerzo ? '' : _('This CLI has no effort setting.')}">${opciones(
|
|
203
|
+
[''].concat(ESFUERZOS),
|
|
204
|
+
motor.esfuerzo ? a.esfuerzo : '',
|
|
205
|
+
esc,
|
|
206
|
+
_('default'),
|
|
207
|
+
)}</select></div>
|
|
168
208
|
</div>
|
|
209
|
+
<p class="pista">${esc(_(motor.pista))}</p>
|
|
169
210
|
<label class="bvEtiqueta">${_('What does it work on?')}</label>
|
|
170
211
|
<p class="pista">${_(`Walk your disk and pick whatever you like: a repository, a
|
|
171
212
|
worktree, a folder of documents, one exact file. As many as you want. Nothing is
|
|
172
213
|
copied — each one is linked into this agent's own home.`)}</p>
|
|
173
214
|
<div class="casaExp"></div>
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
215
|
+
<div id="casaElegidas">${this.elegidas()}</div>`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** The mounts chosen so far, as chips you can take back off. Rendered on its
|
|
219
|
+
* own so ticking a row does not rebuild the picker underneath the cursor. */
|
|
220
|
+
private elegidas(): string {
|
|
221
|
+
const esc = this.p.esc;
|
|
222
|
+
const montajes = this.datos.montajes;
|
|
223
|
+
if (!montajes.length)
|
|
224
|
+
return `<p class="pista">${_('Nothing chosen yet — an agent with no mounts is fine too.')}</p>`;
|
|
225
|
+
return `<div class="expElegidas"><label class="bvEtiqueta">${_('Working on')}</label>
|
|
226
|
+
${montajes
|
|
227
|
+
.map(
|
|
228
|
+
(m) => `<button type="button" class="bvChip elegida" data-bv="quita"
|
|
229
|
+
data-ruta="${esc(m)}" title="${esc(m)}">${esc(
|
|
230
|
+
m.split('/').pop() || m,
|
|
231
|
+
)}<i class="expQuita">✕</i></button>`,
|
|
232
|
+
)
|
|
233
|
+
.join('')}</div>`;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
private pintaElegidas(): void {
|
|
237
|
+
const hueco = this.host?.querySelector<HTMLElement>('#casaElegidas');
|
|
238
|
+
if (!hueco) return;
|
|
239
|
+
hueco.innerHTML = this.elegidas();
|
|
240
|
+
this.enlaza(hueco);
|
|
187
241
|
}
|
|
188
242
|
|
|
189
|
-
|
|
243
|
+
protected enlaza(raiz: HTMLElement): void {
|
|
244
|
+
// Every field writes through as it is typed.
|
|
245
|
+
//
|
|
246
|
+
// `recoge()` used to be the only sync, called at the moments somebody
|
|
247
|
+
// remembered to call it — and a repaint from anywhere else redrew the
|
|
248
|
+
// inputs from a state that had never heard of what was in them. The roles
|
|
249
|
+
// list arrives a few hundred milliseconds after this form opens, which is
|
|
250
|
+
// exactly while a person is typing the name: the answer landed, the form
|
|
251
|
+
// repainted, the name was gone, and then "Build it" said to give it one.
|
|
252
|
+
//
|
|
253
|
+
// Now nothing can lose it, because there is no window in which the field
|
|
254
|
+
// and the state disagree.
|
|
255
|
+
raiz
|
|
256
|
+
.querySelectorAll<HTMLInputElement | HTMLSelectElement>(
|
|
257
|
+
'#bvNombre,#bvRol,#bvModelo,#bvEsfuerzo',
|
|
258
|
+
)
|
|
259
|
+
.forEach((el) => {
|
|
260
|
+
el.oninput = () => this.recoge();
|
|
261
|
+
el.onchange = () => this.recoge();
|
|
262
|
+
});
|
|
190
263
|
raiz.querySelectorAll<HTMLElement>('[data-bv]').forEach((el) => {
|
|
191
264
|
el.onclick = (evento) => {
|
|
192
265
|
evento.preventDefault();
|
|
193
266
|
const que = el.dataset.bv;
|
|
194
|
-
this.recoge(
|
|
267
|
+
this.recoge();
|
|
195
268
|
if (que === 'clase') this.datos.clase = el.dataset.id ?? 'code';
|
|
196
269
|
else if (que === 'quita') {
|
|
197
270
|
const ya = this.datos.montajes.indexOf(el.dataset.ruta ?? '');
|
|
@@ -201,13 +274,20 @@ export class FormularioDeCasa {
|
|
|
201
274
|
};
|
|
202
275
|
});
|
|
203
276
|
raiz.querySelector<HTMLSelectElement>('#bvRuntime')?.addEventListener('change', () => {
|
|
204
|
-
this.recoge(
|
|
277
|
+
this.recoge();
|
|
205
278
|
this.repinta();
|
|
206
279
|
});
|
|
207
280
|
}
|
|
208
281
|
|
|
209
|
-
/** Keep what is typed when a click forces a repaint.
|
|
210
|
-
|
|
282
|
+
/** Keep what is typed when a click forces a repaint.
|
|
283
|
+
*
|
|
284
|
+
* Always from the form's own root, never from whatever element the click
|
|
285
|
+
* came through: the chosen-chips strip re-uses this wiring, and reading the
|
|
286
|
+
* fields relative to that strip would find none of them and quietly throw
|
|
287
|
+
* away everything the person had typed. */
|
|
288
|
+
private recoge(): void {
|
|
289
|
+
const raiz = this.host;
|
|
290
|
+
if (!raiz) return;
|
|
211
291
|
const nombre = raiz.querySelector<HTMLInputElement>('#bvNombre');
|
|
212
292
|
const rol = raiz.querySelector<HTMLSelectElement>('#bvRol');
|
|
213
293
|
if (nombre) this.datos.nombre = nombre.value;
|
|
@@ -217,8 +297,8 @@ export class FormularioDeCasa {
|
|
|
217
297
|
['#bvModelo', 'modelo'],
|
|
218
298
|
['#bvEsfuerzo', 'esfuerzo'],
|
|
219
299
|
] as const) {
|
|
220
|
-
const sel = raiz.querySelector<HTMLSelectElement>(id);
|
|
221
|
-
if (sel) this.datos[campo] = sel.value;
|
|
300
|
+
const sel = raiz.querySelector<HTMLSelectElement | HTMLInputElement>(id);
|
|
301
|
+
if (sel) this.datos[campo] = sel.value.trim();
|
|
222
302
|
}
|
|
223
303
|
}
|
|
224
304
|
|
|
@@ -231,7 +311,7 @@ export class FormularioDeCasa {
|
|
|
231
311
|
* another's job just to save a round trip.
|
|
232
312
|
*/
|
|
233
313
|
async guarda(): Promise<{ slug: string; nombre: string } | null> {
|
|
234
|
-
|
|
314
|
+
this.recoge();
|
|
235
315
|
const a = this.datos;
|
|
236
316
|
const nombre = a.nombre.trim();
|
|
237
317
|
if (!nombre) {
|
|
@@ -247,13 +327,20 @@ export class FormularioDeCasa {
|
|
|
247
327
|
this.p.aviso(r.error || _('Could not add that agent'), true);
|
|
248
328
|
return null;
|
|
249
329
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
330
|
+
// The mounts are independent of each other; awaiting them one at a time
|
|
331
|
+
// put six serialised round trips on the critical path of "Build it".
|
|
332
|
+
const montados = await Promise.all(
|
|
333
|
+
a.montajes.map((ruta) =>
|
|
334
|
+
this.p
|
|
335
|
+
.api<{ ok?: boolean; error?: string }>('/api/montaje', {
|
|
336
|
+
method: 'POST',
|
|
337
|
+
body: JSON.stringify({ agent: r.agent, add: ruta }),
|
|
338
|
+
})
|
|
339
|
+
.then((m) => (m.ok ? '' : `${ruta}: ${m.error ?? _('could not mount')}`))
|
|
340
|
+
.catch((e) => `${ruta}: ${String(e)}`),
|
|
341
|
+
),
|
|
342
|
+
);
|
|
343
|
+
for (const fallo of montados.filter(Boolean)) this.p.aviso(fallo, true);
|
|
257
344
|
const motor: Record<string, string> = {};
|
|
258
345
|
if (a.runtime) motor.runtime = a.runtime;
|
|
259
346
|
if (a.modelo) motor.model = a.modelo;
|
package/city/web/src/demo.ts
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
import type { ActivityEvent } from './activity';
|
|
21
21
|
import { t as _ } from './idioma';
|
|
22
|
+
import { Montada } from './vista';
|
|
22
23
|
|
|
23
24
|
export interface Ficha {
|
|
24
25
|
id: string;
|
|
@@ -50,7 +51,7 @@ const VELOCIDADES: Array<[string, number]> = [
|
|
|
50
51
|
['4×', 350],
|
|
51
52
|
];
|
|
52
53
|
|
|
53
|
-
export class Demos {
|
|
54
|
+
export class Demos extends Montada {
|
|
54
55
|
private catalogo: Ficha[] = [];
|
|
55
56
|
private cargando = false;
|
|
56
57
|
private elegida: Grabacion | null = null;
|
|
@@ -58,19 +59,19 @@ export class Demos {
|
|
|
58
59
|
private corriendo = false;
|
|
59
60
|
private velocidad = 1450;
|
|
60
61
|
private reloj: number | null = null;
|
|
61
|
-
private host: HTMLElement | null = null;
|
|
62
62
|
|
|
63
|
-
constructor(private readonly p: PuertaDemo) {
|
|
63
|
+
constructor(private readonly p: PuertaDemo) {
|
|
64
|
+
super();
|
|
65
|
+
}
|
|
64
66
|
|
|
65
|
-
monta(host: HTMLElement): void {
|
|
66
|
-
this.host = host;
|
|
67
|
+
override monta(host: HTMLElement): void {
|
|
67
68
|
if (!this.catalogo.length && !this.cargando) void this.lee();
|
|
68
|
-
|
|
69
|
+
super.monta(host);
|
|
69
70
|
}
|
|
70
71
|
|
|
71
|
-
/** Stop the clock.
|
|
72
|
-
*
|
|
73
|
-
|
|
72
|
+
/** Stop the clock. The router calls this when the section is left: a timer
|
|
73
|
+
* still firing into a detached node is a leak with a soundtrack. */
|
|
74
|
+
desmonta(): void {
|
|
74
75
|
this.corriendo = false;
|
|
75
76
|
if (this.reloj !== null) {
|
|
76
77
|
clearTimeout(this.reloj);
|
|
@@ -78,14 +79,10 @@ export class Demos {
|
|
|
78
79
|
}
|
|
79
80
|
}
|
|
80
81
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
this.
|
|
84
|
-
|
|
85
|
-
if (this.elegida) {
|
|
86
|
-
const lista = this.host.querySelector<HTMLElement>('#demoLista');
|
|
87
|
-
if (lista) lista.scrollTop = lista.scrollHeight;
|
|
88
|
-
}
|
|
82
|
+
protected override repinta(): void {
|
|
83
|
+
super.repinta();
|
|
84
|
+
const lista = this.elegida ? this.host?.querySelector<HTMLElement>('#demoLista') : null;
|
|
85
|
+
if (lista) lista.scrollTop = lista.scrollHeight;
|
|
89
86
|
}
|
|
90
87
|
|
|
91
88
|
private async lee(): Promise<void> {
|
|
@@ -102,7 +99,7 @@ export class Demos {
|
|
|
102
99
|
}
|
|
103
100
|
|
|
104
101
|
private async abre(id: string): Promise<void> {
|
|
105
|
-
this.
|
|
102
|
+
this.desmonta();
|
|
106
103
|
this.hasta = 0;
|
|
107
104
|
this.elegida = null;
|
|
108
105
|
this.repinta();
|
|
@@ -128,19 +125,47 @@ export class Demos {
|
|
|
128
125
|
if (this.reloj !== null) clearTimeout(this.reloj);
|
|
129
126
|
this.reloj = window.setTimeout(() => {
|
|
130
127
|
if (!this.corriendo || !this.elegida) return;
|
|
128
|
+
const evento = this.elegida.eventos[this.hasta];
|
|
131
129
|
this.hasta += 1;
|
|
132
|
-
|
|
130
|
+
const final = this.hasta >= this.elegida.eventos.length;
|
|
131
|
+
if (final) {
|
|
133
132
|
this.hasta = this.elegida.eventos.length;
|
|
134
133
|
this.corriendo = false;
|
|
135
|
-
this.repinta();
|
|
136
|
-
return;
|
|
137
134
|
}
|
|
138
|
-
|
|
139
|
-
|
|
135
|
+
// A turn arriving is one more turn on the list. Repainting the player
|
|
136
|
+
// rebuilt every turn so far from zero, rebound the four speed buttons and
|
|
137
|
+
// forced a layout — twenty-two times, to append twenty-two lines. The
|
|
138
|
+
// full repaint is kept for the transitions that actually change the
|
|
139
|
+
// controls: play, pause, replay, and the end.
|
|
140
|
+
if (evento && !final) this.anade(evento);
|
|
141
|
+
else this.repinta();
|
|
142
|
+
if (!final) this.siguiente();
|
|
140
143
|
}, this.velocidad);
|
|
141
144
|
}
|
|
142
145
|
|
|
143
|
-
|
|
146
|
+
/** One more turn on screen, without taking the screen down. */
|
|
147
|
+
private anade(evento: ActivityEvent): void {
|
|
148
|
+
const lista = this.host?.querySelector<HTMLElement>('#demoLista');
|
|
149
|
+
if (!lista || !this.elegida) return this.repinta();
|
|
150
|
+
const vacia = lista.querySelector('.liveEmpty');
|
|
151
|
+
if (vacia) lista.innerHTML = '';
|
|
152
|
+
lista.insertAdjacentHTML('beforeend', this.p.pinta(evento));
|
|
153
|
+
lista.scrollTop = lista.scrollHeight;
|
|
154
|
+
this.marcador();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** The counter and the bar, which are the only other things a turn moves. */
|
|
158
|
+
private marcador(): void {
|
|
159
|
+
if (!this.host || !this.elegida) return;
|
|
160
|
+
const total = this.elegida.eventos.length;
|
|
161
|
+
const hecho = Math.min(this.hasta, total);
|
|
162
|
+
const cuenta = this.host.querySelector<HTMLElement>('.demoCuenta');
|
|
163
|
+
if (cuenta) cuenta.textContent = _('{done} of {total}', { done: hecho, total });
|
|
164
|
+
const barra = this.host.querySelector<HTMLElement>('.demoBarra i');
|
|
165
|
+
if (barra) barra.style.width = `${total ? (hecho / total) * 100 : 0}%`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
protected html(): string {
|
|
144
169
|
if (this.elegida) return this.reproductor(this.elegida);
|
|
145
170
|
if (this.cargando) return `<p class="cargando">${_('reading the demo shelf')}</p>`;
|
|
146
171
|
if (!this.catalogo.length)
|
|
@@ -185,9 +210,14 @@ export class Demos {
|
|
|
185
210
|
<button type="button" class="bt ppal" data-demo="${this.corriendo ? 'pausa' : 'play'}">${
|
|
186
211
|
this.corriendo ? `❚❚ ${_('Pause')}` : acabado ? `↺ ${_('Replay')}` : `▶ ${_('Play')}`
|
|
187
212
|
}</button>
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
213
|
+
${
|
|
214
|
+
// Once it has finished, the primary button already says Replay and
|
|
215
|
+
// does exactly this. Two identical controls side by side is a person
|
|
216
|
+
// wondering which one is the real one.
|
|
217
|
+
hecho && !acabado
|
|
218
|
+
? `<button type="button" class="bt" data-demo="replay">↺ ${_('Replay')}</button>`
|
|
219
|
+
: ''
|
|
220
|
+
}
|
|
191
221
|
<span class="demoVel">${VELOCIDADES.map(
|
|
192
222
|
([etiqueta, ms]) =>
|
|
193
223
|
`<button type="button" class="demoVelBoton ${
|
|
@@ -205,7 +235,7 @@ export class Demos {
|
|
|
205
235
|
</div>`;
|
|
206
236
|
}
|
|
207
237
|
|
|
208
|
-
|
|
238
|
+
protected enlaza(raiz: HTMLElement): void {
|
|
209
239
|
raiz.querySelectorAll<HTMLElement>('[data-demo]').forEach((el) => {
|
|
210
240
|
el.onclick = (evento) => {
|
|
211
241
|
evento.preventDefault();
|
|
@@ -214,7 +244,7 @@ export class Demos {
|
|
|
214
244
|
void this.abre(el.dataset.id ?? '');
|
|
215
245
|
break;
|
|
216
246
|
case 'atras':
|
|
217
|
-
this.
|
|
247
|
+
this.desmonta();
|
|
218
248
|
this.elegida = null;
|
|
219
249
|
this.hasta = 0;
|
|
220
250
|
this.repinta();
|
|
@@ -223,11 +253,11 @@ export class Demos {
|
|
|
223
253
|
this.arranca();
|
|
224
254
|
break;
|
|
225
255
|
case 'pausa':
|
|
226
|
-
this.
|
|
256
|
+
this.desmonta();
|
|
227
257
|
this.repinta();
|
|
228
258
|
break;
|
|
229
259
|
case 'replay':
|
|
230
|
-
this.
|
|
260
|
+
this.desmonta();
|
|
231
261
|
this.hasta = 0;
|
|
232
262
|
this.arranca();
|
|
233
263
|
break;
|