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
package/city/web/src/hall.ts
CHANGED
|
@@ -15,13 +15,14 @@ import './es'; // the Spanish dictionary registers itself on import
|
|
|
15
15
|
import { FormularioDeCasa } from './casa';
|
|
16
16
|
import { confirma, pregunta } from './dialogo';
|
|
17
17
|
import { Demos } from './demo';
|
|
18
|
+
import type { Vista } from './vista';
|
|
18
19
|
import { Explorador } from './explorador';
|
|
19
20
|
import { idioma, plural, ponIdioma, t as _ } from './idioma';
|
|
20
21
|
import {
|
|
21
22
|
ESFUERZOS,
|
|
22
|
-
MODELOS,
|
|
23
23
|
NIVEL_ESFUERZO,
|
|
24
24
|
RUNTIMES,
|
|
25
|
+
motorDe,
|
|
25
26
|
nivelDeMotor,
|
|
26
27
|
opciones as opcionesDe,
|
|
27
28
|
} from './motores';
|
|
@@ -90,6 +91,44 @@ interface Road {
|
|
|
90
91
|
domain?: string;
|
|
91
92
|
role?: string;
|
|
92
93
|
}
|
|
94
|
+
interface ReceptionSummary {
|
|
95
|
+
pending: number;
|
|
96
|
+
pendingBytes: number;
|
|
97
|
+
routingMode: 'manual' | 'auto';
|
|
98
|
+
reviewPolicy: 'every_message' | 'new_thread';
|
|
99
|
+
routerProfile: string | null;
|
|
100
|
+
autoAvailable: boolean;
|
|
101
|
+
error?: string;
|
|
102
|
+
}
|
|
103
|
+
interface ReceptionState {
|
|
104
|
+
protocol: 'agents-city-reception/1';
|
|
105
|
+
error?: string;
|
|
106
|
+
settings: {
|
|
107
|
+
routingMode: 'manual' | 'auto';
|
|
108
|
+
reviewPolicy: 'every_message' | 'new_thread';
|
|
109
|
+
routerProfile: string | null;
|
|
110
|
+
autoAvailable: boolean;
|
|
111
|
+
autoRules: Array<{ cityId: string; address: string; keywords: string[] }>;
|
|
112
|
+
};
|
|
113
|
+
summary: { pending: number; pendingBytes: number; shown: number };
|
|
114
|
+
messages: Array<{
|
|
115
|
+
id: string;
|
|
116
|
+
from: string;
|
|
117
|
+
fromName: string;
|
|
118
|
+
kind: 'message' | 'rejection';
|
|
119
|
+
inReplyTo: string | null;
|
|
120
|
+
createdAt: string;
|
|
121
|
+
receivedAt: string;
|
|
122
|
+
receivedVia: string;
|
|
123
|
+
text: string;
|
|
124
|
+
connectionId: string | null;
|
|
125
|
+
roadId: string | null;
|
|
126
|
+
agentExposure: false;
|
|
127
|
+
}>;
|
|
128
|
+
cities: Array<{ id: string; name: string; address: string }>;
|
|
129
|
+
connections: Array<{ id: string; roadId: string; name: string; connected: boolean }>;
|
|
130
|
+
outbox: { queued: number };
|
|
131
|
+
}
|
|
93
132
|
interface SkillInfo {
|
|
94
133
|
name: string;
|
|
95
134
|
description: string;
|
|
@@ -179,6 +218,7 @@ interface Estado {
|
|
|
179
218
|
sesion: string;
|
|
180
219
|
ciudades: Ciudad[];
|
|
181
220
|
roads: Road[];
|
|
221
|
+
reception: ReceptionSummary;
|
|
182
222
|
invitation: Invitation;
|
|
183
223
|
skills: Record<string, AgentCapabilities>;
|
|
184
224
|
deliberations: Deliberation[];
|
|
@@ -239,14 +279,52 @@ declare global {
|
|
|
239
279
|
const PASE = window.PASE;
|
|
240
280
|
const CIUDAD = new URLSearchParams(location.search).get('city') ?? '';
|
|
241
281
|
|
|
282
|
+
/**
|
|
283
|
+
* Tell the city's journal something happened here.
|
|
284
|
+
*
|
|
285
|
+
* The page is half of this product and it used to keep its failures to itself:
|
|
286
|
+
* an error became a toast, the toast went away, and a person reporting it had
|
|
287
|
+
* only their memory. This writes into the same file the server writes, so one
|
|
288
|
+
* file answers "what happened" — and `agents-city doctor --report` can hand it
|
|
289
|
+
* to somebody else without them having to have been watching.
|
|
290
|
+
*
|
|
291
|
+
* It never throws and never awaits: a log that can break the thing it is
|
|
292
|
+
* logging, or slow it down, is worse than no log.
|
|
293
|
+
*/
|
|
294
|
+
function anota(que: string, detalle?: unknown, donde?: string): void {
|
|
295
|
+
try {
|
|
296
|
+
let u = '/api/diario?PASE=' + encodeURIComponent(PASE);
|
|
297
|
+
if (CIUDAD) u += '&city=' + encodeURIComponent(CIUDAD);
|
|
298
|
+
void fetch(u, {
|
|
299
|
+
method: 'POST',
|
|
300
|
+
headers: { 'X-City-Pase': PASE, 'Content-Type': 'application/json' },
|
|
301
|
+
body: JSON.stringify({ que, detalle, donde: donde ?? location.hash ?? '' }),
|
|
302
|
+
keepalive: true,
|
|
303
|
+
}).catch(() => {});
|
|
304
|
+
} catch {
|
|
305
|
+
/* the journal is a courtesy, never a dependency */
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
242
309
|
async function api<T>(ruta: string, opts?: RequestInit): Promise<T> {
|
|
243
310
|
let u = ruta + (ruta.includes('?') ? '&' : '?') + 'PASE=' + encodeURIComponent(PASE);
|
|
244
311
|
if (CIUDAD) u += '&city=' + encodeURIComponent(CIUDAD);
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
312
|
+
let r: Response;
|
|
313
|
+
try {
|
|
314
|
+
r = await fetch(u, {
|
|
315
|
+
headers: { 'X-City-Pase': PASE, 'Content-Type': 'application/json' },
|
|
316
|
+
...opts,
|
|
317
|
+
});
|
|
318
|
+
} catch (e) {
|
|
319
|
+
// A request that never came back. This is the one a person cannot report,
|
|
320
|
+
// because nothing on screen says it happened.
|
|
321
|
+
if (ruta !== '/api/diario') anota('fetch failed', String(e), ruta);
|
|
322
|
+
throw e;
|
|
323
|
+
}
|
|
324
|
+
const cuerpo = (await r.json()) as T & { error?: string };
|
|
325
|
+
if (ruta !== '/api/diario' && (!r.ok || cuerpo?.error))
|
|
326
|
+
anota('api refused', { estado: r.status, error: cuerpo?.error }, ruta);
|
|
327
|
+
return cuerpo as T;
|
|
250
328
|
}
|
|
251
329
|
|
|
252
330
|
/** querySelector that refuses to hand back null: a missing element here is a bug
|
|
@@ -291,6 +369,7 @@ const SECCIONES: Array<[string, string]> = [
|
|
|
291
369
|
['mapa', 'The map'],
|
|
292
370
|
['puesto', 'My seat'],
|
|
293
371
|
['barrios', 'Districts'],
|
|
372
|
+
['recepcion', 'Reception'],
|
|
294
373
|
['red', 'Roads'],
|
|
295
374
|
['committee', 'Committee'],
|
|
296
375
|
['gente', 'Houses'],
|
|
@@ -351,9 +430,9 @@ function pintaDemoControles(): void {
|
|
|
351
430
|
const caja = document.createElement('div');
|
|
352
431
|
caja.id = 'demoControles';
|
|
353
432
|
caja.innerHTML =
|
|
354
|
-
`<span
|
|
355
|
-
`<button id="demoReplay" type="button" title="Play the guided committee from the top"
|
|
356
|
-
`<button id="demoPausa" type="button" title="Pause or resume mid-scene"
|
|
433
|
+
`<span>${_('guided committee')}</span>` +
|
|
434
|
+
`<button id="demoReplay" type="button" title="${_('Play the guided committee from the top')}">${_('⟳ replay')}</button>` +
|
|
435
|
+
`<button id="demoPausa" type="button" title="${_('Pause or resume mid-scene')}">${_('⏸ pause')}</button>`;
|
|
357
436
|
header.appendChild(caja);
|
|
358
437
|
const pausa = q<HTMLButtonElement>('#demoPausa', caja);
|
|
359
438
|
const manda = (action: string) =>
|
|
@@ -496,10 +575,10 @@ function pintaActividad(seguir = false): void {
|
|
|
496
575
|
if (!dot || !state || !list || !filter || !context || !workToggle) return;
|
|
497
576
|
dot.classList.toggle('on', liveConnected);
|
|
498
577
|
state.textContent = liveConnected
|
|
499
|
-
? 'websocket live'
|
|
578
|
+
? _('websocket live')
|
|
500
579
|
: liveUrl
|
|
501
|
-
? 'reconnecting'
|
|
502
|
-
: 'session offline';
|
|
580
|
+
? _('reconnecting')
|
|
581
|
+
: _('session offline');
|
|
503
582
|
|
|
504
583
|
const opened = new Map<string, string>();
|
|
505
584
|
for (const event of liveEvents) {
|
|
@@ -513,7 +592,7 @@ function pintaActividad(seguir = false): void {
|
|
|
513
592
|
}
|
|
514
593
|
}
|
|
515
594
|
filter.innerHTML =
|
|
516
|
-
|
|
595
|
+
`<option value="">${_('all conversations')}</option>` +
|
|
517
596
|
[...opened.entries()]
|
|
518
597
|
.reverse()
|
|
519
598
|
.map(
|
|
@@ -530,7 +609,7 @@ function pintaActividad(seguir = false): void {
|
|
|
530
609
|
};
|
|
531
610
|
workToggle.classList.toggle('on', liveShowWork);
|
|
532
611
|
workToggle.setAttribute('aria-pressed', String(liveShowWork));
|
|
533
|
-
workToggle.textContent = liveShowWork ? 'hide work' : 'show work';
|
|
612
|
+
workToggle.textContent = liveShowWork ? _('hide work') : _('show work');
|
|
534
613
|
workToggle.onclick = () => {
|
|
535
614
|
liveShowWork = !liveShowWork;
|
|
536
615
|
pintaActividad(true);
|
|
@@ -548,8 +627,8 @@ function pintaActividad(seguir = false): void {
|
|
|
548
627
|
if (!visible.length) {
|
|
549
628
|
list.innerHTML = `<li class="liveEmpty">${
|
|
550
629
|
liveConnected
|
|
551
|
-
? 'The bus is live. Questions, positions and moderated replies will appear here.'
|
|
552
|
-
: 'Start the city session. Its visible conversation will appear here as it happens.'
|
|
630
|
+
? _('The bus is live. Questions, positions and moderated replies will appear here.')
|
|
631
|
+
: _('Start the city session. Its visible conversation will appear here as it happens.')
|
|
553
632
|
}</li>`;
|
|
554
633
|
return;
|
|
555
634
|
}
|
|
@@ -580,7 +659,7 @@ function renderActividad(event: ActivityEvent): string {
|
|
|
580
659
|
}</summary><ul>${event.details.map((detail) => `<li>${esc(detail)}</li>`).join('')}</ul></details>`
|
|
581
660
|
: '';
|
|
582
661
|
const full = long
|
|
583
|
-
? `<details class="liveFull"><summary
|
|
662
|
+
? `<details class="liveFull"><summary>${_('read full message')}</summary><p>${esc(summary)}</p></details>`
|
|
584
663
|
: '';
|
|
585
664
|
return `<li class="liveTurn ${side} ${esc(event.tone)}">
|
|
586
665
|
<div class="liveAvatar${E?.avatars?.[event.actor] ? ' conCara' : ''}"
|
|
@@ -645,7 +724,7 @@ function pintaContexto(thread: string, container: HTMLElement): void {
|
|
|
645
724
|
actor === 'seat' ? 'seat · chair' : `${actor} · ${rolOperativo(actor) || 'member'}`,
|
|
646
725
|
)}" style="--actor-hue:${tonoActor(actor)}">${cara(actor, actor === 'seat')}</span>`,
|
|
647
726
|
)
|
|
648
|
-
.join('')}</div><span class="liveContextText"><b
|
|
727
|
+
.join('')}</div><span class="liveContextText"><b>${_('seat moderates')}</b> · ${esc(
|
|
649
728
|
received,
|
|
650
729
|
)}/${esc(total)} positions · ${esc(status)}</span>`;
|
|
651
730
|
}
|
|
@@ -770,29 +849,40 @@ function rail(): void {
|
|
|
770
849
|
? String(Object.keys(E.skills).length)
|
|
771
850
|
: id === 'committee'
|
|
772
851
|
? String(E.deliberations.length)
|
|
773
|
-
: id === '
|
|
774
|
-
? String(E.
|
|
775
|
-
: id === '
|
|
776
|
-
? String(E.
|
|
777
|
-
: ''
|
|
778
|
-
|
|
852
|
+
: id === 'recepcion'
|
|
853
|
+
? String(E.reception?.pending ?? 0)
|
|
854
|
+
: id === 'red'
|
|
855
|
+
? String(E.roads.length)
|
|
856
|
+
: id === 'barrios'
|
|
857
|
+
? String(E.parcelas.length)
|
|
858
|
+
: '';
|
|
859
|
+
return `<li class="${id === SECCION ? 'aqui' : ''}" data-s="${id}" role="button"
|
|
860
|
+
tabindex="0" ${id === SECCION ? 'aria-current="page"' : ''}>
|
|
779
861
|
<span>${esc(_(et))}</span>${n ? `<span class="n">${n}</span>` : ''}</li>`;
|
|
780
862
|
})
|
|
781
863
|
.join('');
|
|
782
864
|
q('#railOtras').innerHTML =
|
|
783
|
-
`<li class="${SECCION === 'ciudades' ? 'aqui' : ''}" data-s="ciudades"
|
|
865
|
+
`<li class="${SECCION === 'ciudades' ? 'aqui' : ''}" data-s="ciudades" role="button"
|
|
866
|
+
tabindex="0" ${SECCION === 'ciudades' ? 'aria-current="page"' : ''}>
|
|
784
867
|
<span>${_('All cities')}</span><span class="n">${E.ciudades.length}</span></li>`;
|
|
785
868
|
todos<HTMLElement>('#rail li,#railOtras li').forEach((li) => {
|
|
786
|
-
|
|
869
|
+
const abre = () => {
|
|
787
870
|
document.body.classList.remove('enGuia');
|
|
788
871
|
SECCION = li.dataset.s ?? 'mapa';
|
|
789
872
|
pinta();
|
|
790
873
|
};
|
|
874
|
+
li.onclick = abre;
|
|
875
|
+
li.onkeydown = (event) => {
|
|
876
|
+
if (event.key !== 'Enter' && event.key !== ' ') return;
|
|
877
|
+
event.preventDefault();
|
|
878
|
+
abre();
|
|
879
|
+
};
|
|
791
880
|
});
|
|
792
881
|
q('#dondeDatos').textContent = corto(E.datos);
|
|
793
882
|
const cs = q<HTMLElement>('#ciudades');
|
|
794
883
|
if (E.ciudades.length > 1) {
|
|
795
|
-
cs.innerHTML = `<select id="cambiaCiudad"
|
|
884
|
+
cs.innerHTML = `<select id="cambiaCiudad" aria-label="${_('which city this hall manages')}"
|
|
885
|
+
title="${_('which city this hall manages')}">
|
|
796
886
|
${E.ciudades
|
|
797
887
|
.map(
|
|
798
888
|
(c) => `<option value="${esc(c.ruta)}" ${c.actual ? 'selected' : ''}>
|
|
@@ -837,10 +927,15 @@ function rail(): void {
|
|
|
837
927
|
};
|
|
838
928
|
}
|
|
839
929
|
|
|
930
|
+
/** The view on screen, if it is one that has anything to stop. */
|
|
931
|
+
let montada: Vista | null = null;
|
|
932
|
+
|
|
840
933
|
function pinta(): void {
|
|
841
|
-
//
|
|
842
|
-
//
|
|
843
|
-
|
|
934
|
+
// Whatever was here is being left. The dispatcher used to name one view and
|
|
935
|
+
// one of its teardown methods by hand, which is a line it would have grown
|
|
936
|
+
// again for the next view that owned a timer or a socket.
|
|
937
|
+
montada?.desmonta?.();
|
|
938
|
+
montada = null;
|
|
844
939
|
rail();
|
|
845
940
|
q<HTMLElement>('.cuerpo').style.padding = '';
|
|
846
941
|
q<HTMLElement>('#lienzo').style.maxWidth = '';
|
|
@@ -871,7 +966,8 @@ VISTAS.bienvenida = () => {
|
|
|
871
966
|
yo: E.yo,
|
|
872
967
|
datos: corto(E.datos),
|
|
873
968
|
});
|
|
874
|
-
|
|
969
|
+
guia.monta(q<HTMLElement>('#lienzo'));
|
|
970
|
+
montada = guia;
|
|
875
971
|
};
|
|
876
972
|
|
|
877
973
|
VISTAS.ciudades = () => {
|
|
@@ -896,23 +992,24 @@ VISTAS.ciudades = () => {
|
|
|
896
992
|
.join('');
|
|
897
993
|
q('#lienzo').innerHTML = `<div><span class="sub">cities</span>
|
|
898
994
|
<h1 style="margin-top:6px">${_('Your cities')}</h1>
|
|
899
|
-
<p class="prosa" style="margin-top:8px"
|
|
995
|
+
<p class="prosa" style="margin-top:8px">${_(`One person, several autonomous cities: each has
|
|
900
996
|
its own identity, domain, chair, <b>its own houses</b> and its own roads. A house is not
|
|
901
997
|
shared — it stands inside the city that owns it, with its workspace and mounts under that
|
|
902
998
|
city's folder — so two cities can each have a <code class="mono">docs</code> house and
|
|
903
999
|
they are two different workers. They share nothing unless you build a road between
|
|
904
|
-
them
|
|
1000
|
+
them.`)}</p></div>
|
|
905
1001
|
<div class="lista"><div class="filas">${filas}</div></div>
|
|
906
1002
|
<div class="campos" style="max-width:420px">
|
|
907
1003
|
<div class="campo"><label>${_('Start another city')}</label>
|
|
908
|
-
<input type="text" id="nuevaCiudad" placeholder="client-a, research, the book…"></div>
|
|
1004
|
+
<input type="text" id="nuevaCiudad" placeholder="${_('client-a, research, the book…')}"></div>
|
|
909
1005
|
<button class="bt ppal" id="creaCiudad">Create it</button>
|
|
910
1006
|
</div>
|
|
911
1007
|
<div class="peligro">
|
|
912
1008
|
<h3>${_('Start this city over')}</h3>
|
|
913
|
-
<p class="prosa"
|
|
914
|
-
|
|
915
|
-
|
|
1009
|
+
<p class="prosa">${_(
|
|
1010
|
+
'Takes {city} back to its first day: no seat, no agents, no committee history, no map. Your repositories and document folders are <b>never touched</b> — only the city that points at them.',
|
|
1011
|
+
{ city: `<b>${esc(E.city_name)}</b>` },
|
|
1012
|
+
)}</p>
|
|
916
1013
|
<button class="bt malo" id="reiniciaCiudad">${_('Start over…')}</button>
|
|
917
1014
|
</div>
|
|
918
1015
|
<p class="pista">Archiving <b>moves</b> a city into
|
|
@@ -1009,9 +1106,10 @@ VISTAS.ciudades = () => {
|
|
|
1009
1106
|
let demos: Demos | null = null;
|
|
1010
1107
|
|
|
1011
1108
|
VISTAS.demos = () => {
|
|
1012
|
-
|
|
1109
|
+
demos ??= new Demos({ api, esc, pinta: renderActividad });
|
|
1013
1110
|
q('#lienzo').innerHTML = '<div id="demoHueco"></div>';
|
|
1014
1111
|
demos.monta(q<HTMLElement>('#demoHueco'));
|
|
1112
|
+
montada = demos;
|
|
1015
1113
|
};
|
|
1016
1114
|
|
|
1017
1115
|
VISTAS.resumen = () => {
|
|
@@ -1084,19 +1182,19 @@ VISTAS.resumen = () => {
|
|
|
1084
1182
|
counted with <code class="mono">${esc(E.grow || 'nothing yet')}</code>.</p>`
|
|
1085
1183
|
}</div>
|
|
1086
1184
|
<div class="cifras">
|
|
1087
|
-
<div class="cifra"><b>${Object.keys(E.skills).length}</b><span
|
|
1185
|
+
<div class="cifra"><b>${Object.keys(E.skills).length}</b><span>${_('repo agents')}</span></div>
|
|
1088
1186
|
<div class="cifra"><b>${E.parcelas.length}</b><span>houses</span></div>
|
|
1089
1187
|
<div class="cifra"><b>${E.roads.length}</b><span>roads</span></div>
|
|
1090
|
-
<div class="cifra"><b>${E.deliberations.length}</b><span
|
|
1091
|
-
<div class="cifra"><b>${Object.values(E.skills).reduce((n, a) => n + a.skills.length, 0)}</b><span
|
|
1188
|
+
<div class="cifra"><b>${E.deliberations.length}</b><span>${_('committee acts')}</span></div>
|
|
1189
|
+
<div class="cifra"><b>${Object.values(E.skills).reduce((n, a) => n + a.skills.length, 0)}</b><span>${_('skills recognised')}</span></div>
|
|
1092
1190
|
</div>
|
|
1093
1191
|
<div class="luces">
|
|
1094
|
-
<span class="luz ${E.tarjetas.length ? 'on' : ''}"
|
|
1192
|
+
<span class="luz ${E.tarjetas.length ? 'on' : ''}">${_('data repo')}</span>
|
|
1095
1193
|
<span class="luz ${E.gh ? 'on' : 'neutra'}">github${E.gh ? '' : ' — optional'}</span>
|
|
1096
|
-
<span class="luz ${E.plugin ? 'on' : ''}"
|
|
1194
|
+
<span class="luz ${E.plugin ? 'on' : ''}">${_('plugin installed')}</span>
|
|
1097
1195
|
<span class="luz ${sesionArriba ? 'on' : 'neutra'}">tmux session${sesionArriba ? ' up' : ''}</span>
|
|
1098
1196
|
</div>
|
|
1099
|
-
<div><span class="sub"
|
|
1197
|
+
<div><span class="sub">${_('what is left')}</span>
|
|
1100
1198
|
<div class="tareas" style="margin-top:9px">${tareas
|
|
1101
1199
|
.map(
|
|
1102
1200
|
(t) => `
|
|
@@ -1107,7 +1205,7 @@ VISTAS.resumen = () => {
|
|
|
1107
1205
|
</div></div>
|
|
1108
1206
|
<div><span class="sub">work</span>
|
|
1109
1207
|
<div style="display:flex;flex-direction:column;gap:9px;margin-top:9px">
|
|
1110
|
-
<div class="orden"><span class="et2"
|
|
1208
|
+
<div class="orden"><span class="et2">${_('your day')}</span>
|
|
1111
1209
|
<code>${
|
|
1112
1210
|
sesionArriba
|
|
1113
1211
|
? 'tmux attach -t ' + esc(E.sesion)
|
|
@@ -1116,7 +1214,7 @@ VISTAS.resumen = () => {
|
|
|
1116
1214
|
${
|
|
1117
1215
|
sesionArriba
|
|
1118
1216
|
? `<button class="bt mini" data-copia="tmux attach -t ${esc(E.sesion)}">copy</button>`
|
|
1119
|
-
:
|
|
1217
|
+
: `<button class="bt mini ppal" id="abreSesion">${_('open my session')}</button>`
|
|
1120
1218
|
}
|
|
1121
1219
|
</div>
|
|
1122
1220
|
<div class="orden"><span class="et2">the map</span>
|
|
@@ -1230,26 +1328,26 @@ VISTAS.puesto = () => {
|
|
|
1230
1328
|
|
|
1231
1329
|
async function puesto(): Promise<void> {
|
|
1232
1330
|
const mia = E.tarjetas.find((t) => t.user === E.yo);
|
|
1233
|
-
q('#lienzo').innerHTML = `<div><span class="sub"
|
|
1331
|
+
q('#lienzo').innerHTML = `<div><span class="sub">${_('my seat')}</span>
|
|
1234
1332
|
<h1 style="margin-top:6px">${esc(E.yo)}</h1>
|
|
1235
1333
|
<p class="prosa" style="margin-top:8px">This is your chair: the work domain, your
|
|
1236
1334
|
role inside it, and one goal. The seat stays the boss even when its
|
|
1237
1335
|
professional role is blank. Saving writes your card —
|
|
1238
1336
|
<code class="mono">${esc(corto(E.datos))}/${esc(E.yo)}.md</code> — the same file
|
|
1239
1337
|
every other door writes, and it never touches your roster.</p></div>
|
|
1240
|
-
<div><span class="sub"
|
|
1241
|
-
<p class="cargando"
|
|
1338
|
+
<div><span class="sub">${_('work domain')}</span><div class="rolejilla" id="domains" style="margin-top:9px">
|
|
1339
|
+
<p class="cargando">${_('reading the domain packs')}</p></div></div>
|
|
1242
1340
|
<div><span class="sub">role</span><div class="rolejilla" id="roles" style="margin-top:9px">
|
|
1243
|
-
<p class="cargando"
|
|
1244
|
-
<div><span class="sub"
|
|
1341
|
+
<p class="cargando">${_('reading the role files')}</p></div></div>
|
|
1342
|
+
<div><span class="sub">${_('the agents')}</span>
|
|
1245
1343
|
<p class="prosa" style="margin-top:8px">Who works in this city — and what each
|
|
1246
|
-
one works on — lives in <b
|
|
1344
|
+
one works on — lives in <b>${_('Agents & skills')}</b>, where an agent is asked for
|
|
1247
1345
|
whole: its kind, its role, its repositories and document folders, its engine
|
|
1248
1346
|
and its skills. One place, so the terminal and this page cannot disagree.
|
|
1249
|
-
<button class="bt" type="button" data-ir="gente" style="margin-left:8px"
|
|
1250
|
-
<div><span class="sub"
|
|
1347
|
+
<button class="bt" type="button" data-ir="gente" style="margin-left:8px">${_('Open the roster')}</button></p></div>
|
|
1348
|
+
<div><span class="sub">${_('one goal — optional')}</span><div class="campos" id="meta" style="margin-top:9px"></div></div>
|
|
1251
1349
|
<div style="display:flex;gap:10px;align-items:center">
|
|
1252
|
-
<button class="bt ppal" id="guardaPuesto"
|
|
1350
|
+
<button class="bt ppal" id="guardaPuesto">${_('Save my seat')}</button>
|
|
1253
1351
|
<span class="cargando" id="puestoEstado" style="display:none">writing</span></div>`;
|
|
1254
1352
|
|
|
1255
1353
|
// Domain first, then only the roles relevant inside it. Both are copied into
|
|
@@ -1307,28 +1405,28 @@ async function puesto(): Promise<void> {
|
|
|
1307
1405
|
// goal
|
|
1308
1406
|
const o: Partial<Objetivo> = (mia && mia.objetivo) || {};
|
|
1309
1407
|
q('#meta').innerHTML = `
|
|
1310
|
-
<div class="campo"><label
|
|
1311
|
-
<input type="text" id="g_title" value="${esc(o.title)}" placeholder="Concrete enough to argue with — empty skips it">
|
|
1408
|
+
<div class="campo"><label>${_('The goal, in one line')}</label>
|
|
1409
|
+
<input type="text" id="g_title" value="${esc(o.title)}" placeholder="${_('Concrete enough to argue with — empty skips it')}">
|
|
1312
1410
|
</div>
|
|
1313
1411
|
<div class="dos">
|
|
1314
|
-
<div class="campo"><label
|
|
1412
|
+
<div class="campo"><label>${_('How it is measured')}</label>
|
|
1315
1413
|
<input type="text" id="g_signal" value="${esc(o.signal)}"></div>
|
|
1316
|
-
<div class="campo"><label
|
|
1414
|
+
<div class="campo"><label>${_('The command that returns it, if a command can')}</label>
|
|
1317
1415
|
<input type="text" class="mono" id="g_command" value="${esc(o.command)}"
|
|
1318
|
-
placeholder="empty for a qualitative goal"></div>
|
|
1416
|
+
placeholder="${_('empty for a qualitative goal')}"></div>
|
|
1319
1417
|
</div>
|
|
1320
|
-
<div class="campo"><label
|
|
1418
|
+
<div class="campo"><label>${_('…or who judges it, and how often')}</label>
|
|
1321
1419
|
<input type="text" id="g_manual" value="${esc(o.manual)}"
|
|
1322
|
-
placeholder="in prose: the architect reads the AGENTS.md files on Fridays">
|
|
1420
|
+
placeholder="${_('in prose: the architect reads the AGENTS.md files on Fridays')}">
|
|
1323
1421
|
<p class="pista">A goal judged by a person is a real goal — plenty of quality
|
|
1324
1422
|
is prose, not a number. Used when the command is empty.</p></div>
|
|
1325
1423
|
<div class="dos">
|
|
1326
|
-
<div class="campo"><label
|
|
1424
|
+
<div class="campo"><label>${_('What it returns today')}</label>
|
|
1327
1425
|
<input type="text" class="mono" id="g_baseline" value="${esc(o.baseline)}"></div>
|
|
1328
|
-
<div class="campo"><label
|
|
1426
|
+
<div class="campo"><label>${_('Where it has to get to')}</label>
|
|
1329
1427
|
<input type="text" class="mono" id="g_target" value="${esc(o.target)}"></div>
|
|
1330
1428
|
</div>
|
|
1331
|
-
<div class="campo" style="max-width:240px"><label
|
|
1429
|
+
<div class="campo" style="max-width:240px"><label>${_('By when')}</label>
|
|
1332
1430
|
<input type="text" id="g_by" value="${esc(o.by ?? 'this quarter')}"></div>`;
|
|
1333
1431
|
|
|
1334
1432
|
const val = (id: string): string => q<HTMLInputElement>(id).value.trim();
|
|
@@ -1390,23 +1488,23 @@ VISTAS.barrios = () => {
|
|
|
1390
1488
|
|
|
1391
1489
|
const render = (): void => {
|
|
1392
1490
|
const idsValidos = new Set([...unidades.map((u) => u.id), 'none']);
|
|
1393
|
-
q('#lienzo').innerHTML = `<div><span class="sub"
|
|
1394
|
-
<h1 style="margin-top:6px"
|
|
1395
|
-
<p class="prosa" style="margin-top:8px"
|
|
1491
|
+
q('#lienzo').innerHTML = `<div><span class="sub">${_('districts & houses')}</span>
|
|
1492
|
+
<h1 style="margin-top:6px">${_('The modelling no tool can do for you')}</h1>
|
|
1493
|
+
<p class="prosa" style="margin-top:8px">${_(`A house is <b>not a repo</b> — it is a
|
|
1396
1494
|
parcel, a slice of one serving a single business unit. Split the interesting
|
|
1397
1495
|
repos, give every house its district, and the map can say
|
|
1398
|
-
“this change touches banking” instead of “this change touches src/lib
|
|
1496
|
+
“this change touches banking” instead of “this change touches src/lib”.`)}</p></div>
|
|
1399
1497
|
|
|
1400
1498
|
<div><span class="sub">districts</span>
|
|
1401
1499
|
<div class="tabla" id="unidades" style="margin-top:9px"></div>
|
|
1402
1500
|
<div style="display:flex;gap:9px;margin-top:9px">
|
|
1403
|
-
<input type="text" id="nuevaU" placeholder="another district…" style="flex:1">
|
|
1501
|
+
<input type="text" id="nuevaU" placeholder="${_('another district…')}" style="flex:1">
|
|
1404
1502
|
<button class="bt" id="masU">Add</button></div></div>
|
|
1405
1503
|
|
|
1406
1504
|
<div><span class="sub">houses</span><div id="parcelas" style="margin-top:9px"></div></div>
|
|
1407
1505
|
|
|
1408
1506
|
<div style="display:flex;gap:10px;align-items:center">
|
|
1409
|
-
<button class="bt ppal" id="guardaBarrios"
|
|
1507
|
+
<button class="bt ppal" id="guardaBarrios">${_('Save districts & houses')}</button>
|
|
1410
1508
|
<span style="font-size:12px;color:var(--tinta3);font-style:italic">
|
|
1411
1509
|
writes units.yml and parcels.yml — reseed the map to see it drawn</span></div>`;
|
|
1412
1510
|
|
|
@@ -1562,7 +1660,7 @@ VISTAS.red = () => {
|
|
|
1562
1660
|
const locales = E.ciudades.filter((c) => !c.actual && c.id && !conectadas.has(c.id));
|
|
1563
1661
|
const invitation = JSON.stringify(E.invitation);
|
|
1564
1662
|
q('#lienzo').innerHTML = `<div><span class="sub">roads</span>
|
|
1565
|
-
<h1 style="margin-top:6px"
|
|
1663
|
+
<h1 style="margin-top:6px">${_('Cities this one may reach')}</h1>
|
|
1566
1664
|
<p class="prosa" style="margin-top:8px">A road joins city seats. It may stay on
|
|
1567
1665
|
this machine or continue over the remote bus; the city sees one explicit
|
|
1568
1666
|
connection either way.</p></div>
|
|
@@ -1574,9 +1672,9 @@ VISTAS.red = () => {
|
|
|
1574
1672
|
<span class="rp">${r.local ? 'local road' : 'remote road'}</span>
|
|
1575
1673
|
<button class="bt mini" data-road-close="${esc(r.id)}">close</button></div>`,
|
|
1576
1674
|
)
|
|
1577
|
-
.join('') ||
|
|
1675
|
+
.join('') || `<p class="prosa">${_('No roads yet. This city is isolated on purpose.')}</p>`
|
|
1578
1676
|
}</div>
|
|
1579
|
-
<div><span class="sub"
|
|
1677
|
+
<div><span class="sub">${_('other cities on this machine')}</span>
|
|
1580
1678
|
<div class="gente" style="margin-top:9px">${
|
|
1581
1679
|
locales
|
|
1582
1680
|
.map(
|
|
@@ -1584,9 +1682,9 @@ VISTAS.red = () => {
|
|
|
1584
1682
|
<span class="ag">${esc(c.slug ?? c.ruta)}</span>
|
|
1585
1683
|
<button class="bt mini ppal" data-road-open="${esc(c.id)}">open road</button></div>`,
|
|
1586
1684
|
)
|
|
1587
|
-
.join('') ||
|
|
1685
|
+
.join('') || `<p class="prosa">${_('No unconnected local cities.')}</p>`
|
|
1588
1686
|
}</div></div>
|
|
1589
|
-
<div class="orden"><span class="et2"
|
|
1687
|
+
<div class="orden"><span class="et2">${_('remote invitation · public, no token')}</span>
|
|
1590
1688
|
<code>${esc(invitation)}</code>
|
|
1591
1689
|
<button class="bt mini" data-copia="${esc(invitation)}">copy</button>
|
|
1592
1690
|
</div>`;
|
|
@@ -1615,16 +1713,305 @@ VISTAS.red = () => {
|
|
|
1615
1713
|
});
|
|
1616
1714
|
};
|
|
1617
1715
|
|
|
1716
|
+
// ── owner reception ─────────────────────────────────────────────────────────
|
|
1717
|
+
VISTAS.recepcion = () => {
|
|
1718
|
+
q('#lienzo').innerHTML = `<div><span class="sub">${_('reception')}</span>
|
|
1719
|
+
<h1 style="margin-top:6px">${_('Messages wait for you, not your agents')}</h1>
|
|
1720
|
+
<p class="prosa" style="margin-top:8px">${_(
|
|
1721
|
+
'Remote text stops here as inert text. Read it, reject it with a reason, or choose the cities that should receive it. Until then no model can read it.',
|
|
1722
|
+
)}</p></div>
|
|
1723
|
+
<div class="recModo" id="recModo">
|
|
1724
|
+
<div><span class="et2">${_('routing mode')}</span><b>${_('Manual review')}</b>
|
|
1725
|
+
<p>${_('Every message needs a person before it reaches a city.')}</p></div>
|
|
1726
|
+
<button class="bt mini" type="button" data-rec-open-config>${_('Configure Auto')}</button>
|
|
1727
|
+
</div>
|
|
1728
|
+
<div id="recConfig" hidden></div>
|
|
1729
|
+
<div id="recLista"><p class="prosa">${_('Reading your reception…')}</p></div>`;
|
|
1730
|
+
void cargaRecepcion();
|
|
1731
|
+
};
|
|
1732
|
+
|
|
1733
|
+
async function cargaRecepcion(): Promise<void> {
|
|
1734
|
+
const hueco = q<HTMLElement>('#recLista');
|
|
1735
|
+
let estado: ReceptionState;
|
|
1736
|
+
try {
|
|
1737
|
+
estado = await api<ReceptionState>('/api/reception');
|
|
1738
|
+
} catch {
|
|
1739
|
+
hueco.innerHTML = `<div class="recError">${_('Could not read your reception')}</div>`;
|
|
1740
|
+
return;
|
|
1741
|
+
}
|
|
1742
|
+
if (estado.error) {
|
|
1743
|
+
hueco.innerHTML = `<div class="recError">${esc(estado.error)}</div>`;
|
|
1744
|
+
return;
|
|
1745
|
+
}
|
|
1746
|
+
E.reception = {
|
|
1747
|
+
pending: estado.summary.pending,
|
|
1748
|
+
pendingBytes: estado.summary.pendingBytes,
|
|
1749
|
+
routingMode: estado.settings.routingMode,
|
|
1750
|
+
reviewPolicy: estado.settings.reviewPolicy,
|
|
1751
|
+
routerProfile: estado.settings.routerProfile,
|
|
1752
|
+
autoAvailable: estado.settings.autoAvailable,
|
|
1753
|
+
};
|
|
1754
|
+
const reglas = new Map(estado.settings.autoRules.map((rule) => [rule.cityId, rule.keywords]));
|
|
1755
|
+
const modo = q<HTMLElement>('#recModo');
|
|
1756
|
+
modo.innerHTML = `<div><span class="et2">${_('routing mode')}</span>
|
|
1757
|
+
<b>${estado.settings.routingMode === 'auto' ? _('Automatic routing') : _('Manual review')}</b>
|
|
1758
|
+
<p>${
|
|
1759
|
+
estado.settings.routingMode === 'auto'
|
|
1760
|
+
? _('Only one clear, low-risk rule match can leave the human queue automatically.')
|
|
1761
|
+
: _('Every message needs a person before it reaches a city.')
|
|
1762
|
+
}</p></div>
|
|
1763
|
+
<button class="bt mini" type="button" data-rec-open-config>${_('Configure Auto')}</button>`;
|
|
1764
|
+
const configuracion = q<HTMLElement>('#recConfig');
|
|
1765
|
+
configuracion.innerHTML = `<form class="recAutoConfig" data-rec-config>
|
|
1766
|
+
<fieldset><legend>${_('Routing policy')}</legend>
|
|
1767
|
+
<label><input type="radio" name="routing_mode" value="manual"
|
|
1768
|
+
${estado.settings.routingMode === 'manual' ? 'checked' : ''}> ${_('Keep human review')}</label>
|
|
1769
|
+
<label><input type="radio" name="routing_mode" value="auto"
|
|
1770
|
+
${estado.settings.routingMode === 'auto' ? 'checked' : ''}> ${_('Use the local rule router')}</label>
|
|
1771
|
+
</fieldset>
|
|
1772
|
+
<div class="recReglas"><p>${_('Write comma-separated words or phrases for each destination. Empty cities are never selected.')}</p>
|
|
1773
|
+
${estado.cities
|
|
1774
|
+
.map(
|
|
1775
|
+
(city) => `<label><span><b>${esc(city.name)}</b><small>${esc(city.address)}</small></span>
|
|
1776
|
+
<input type="text" name="rule-${esc(city.id)}" maxlength="1200"
|
|
1777
|
+
data-rule-city="${esc(city.id)}" value="${esc((reglas.get(city.id) ?? []).join(', '))}"
|
|
1778
|
+
placeholder="${_('e.g. contract, privacy, legal review')}"></label>`,
|
|
1779
|
+
)
|
|
1780
|
+
.join('')}</div>
|
|
1781
|
+
<p class="recAutoAviso">${_('Suspicious, unmatched, or ambiguous text always waits for you. Auto never executes, answers, or opens links.')}</p>
|
|
1782
|
+
<button class="bt ppal" type="submit">${_('Save routing policy')}</button>
|
|
1783
|
+
</form>`;
|
|
1784
|
+
q<HTMLButtonElement>('[data-rec-open-config]', modo).onclick = () => {
|
|
1785
|
+
configuracion.hidden = !configuracion.hidden;
|
|
1786
|
+
};
|
|
1787
|
+
rail();
|
|
1788
|
+
const conexiones = estado.connections.length
|
|
1789
|
+
? `<section class="recConexiones"><div class="recSeccion"><b>${_('New encrypted message')}</b>
|
|
1790
|
+
<span>${estado.outbox.queued ? plural(estado.outbox.queued, 'message queued', 'messages queued') : _('Messages leave from this computer with end-to-end encryption.')}</span></div>
|
|
1791
|
+
<form class="recCompose" data-rec-send>
|
|
1792
|
+
<label class="recComposePersona" for="rec-send-connection"><span>${_('Write to')}</span>
|
|
1793
|
+
<select id="rec-send-connection" name="connection" required>
|
|
1794
|
+
${estado.connections
|
|
1795
|
+
.map(
|
|
1796
|
+
(conexion) => `<option value="${esc(conexion.id)}">${esc(conexion.name)}</option>`,
|
|
1797
|
+
)
|
|
1798
|
+
.join('')}
|
|
1799
|
+
</select>
|
|
1800
|
+
</label>
|
|
1801
|
+
<label class="recComposeTexto" for="rec-send-text"><span>${_('Message')}</span>
|
|
1802
|
+
<textarea id="rec-send-text" name="text" rows="3" maxlength="11500" required
|
|
1803
|
+
placeholder="${_('Your message will stop in their private reception.')}"></textarea>
|
|
1804
|
+
</label>
|
|
1805
|
+
<button class="bt ppal" type="submit">${_('Send securely')}</button>
|
|
1806
|
+
</form></section>`
|
|
1807
|
+
: '';
|
|
1808
|
+
const vacia = !estado.messages.length
|
|
1809
|
+
? `<div class="recVacia"><b>${_('Reception clear')}</b>
|
|
1810
|
+
<p>${_('No remote message is waiting for a decision.')}</p></div>`
|
|
1811
|
+
: '';
|
|
1812
|
+
const ciudades = estado.cities
|
|
1813
|
+
.map(
|
|
1814
|
+
(ciudad) => `<label class="recDestino"><input type="checkbox" name="destination"
|
|
1815
|
+
value="${esc(ciudad.id)}"><span><b>${esc(ciudad.name)}</b>
|
|
1816
|
+
<small>${esc(ciudad.address)}</small></span></label>`,
|
|
1817
|
+
)
|
|
1818
|
+
.join('');
|
|
1819
|
+
const pendientes = estado.messages.length
|
|
1820
|
+
? `<div class="recResumen"><b>${estado.summary.pending}</b>
|
|
1821
|
+
<span>${plural(estado.summary.pending, 'message waiting', 'messages waiting')}</span>
|
|
1822
|
+
<small>${formateaBytes(estado.summary.pendingBytes)} · ${_('local only')}</small></div>
|
|
1823
|
+
<div class="recMensajes">${estado.messages
|
|
1824
|
+
.map(
|
|
1825
|
+
(mensaje) => `<article class="recMensaje">
|
|
1826
|
+
<header><div><span class="recOrigen">${esc(mensaje.fromName)}</span>
|
|
1827
|
+
<span>${esc(fechaRecepcion(mensaje.receivedAt))}</span></div>
|
|
1828
|
+
<span class="recSeguro">${mensaje.kind === 'rejection' ? _('Reply to a rejected message') : _('No agent has read this')}</span></header>
|
|
1829
|
+
<pre class="recTexto">${esc(mensaje.text)}</pre>
|
|
1830
|
+
${
|
|
1831
|
+
mensaje.kind === 'rejection'
|
|
1832
|
+
? `<div class="recAvisoAccion">
|
|
1833
|
+
<button class="bt mini" type="button" data-rec-dismiss="${esc(mensaje.id)}">${_('Dismiss')}</button>
|
|
1834
|
+
</div>`
|
|
1835
|
+
: `<div class="recAcciones">
|
|
1836
|
+
<form data-rec-route="${esc(mensaje.id)}">
|
|
1837
|
+
<fieldset><legend>${_('Send to')}</legend><div class="recDestinos">${ciudades}</div></fieldset>
|
|
1838
|
+
<button class="bt ppal" type="submit" disabled>${_('Route to selected cities')}</button>
|
|
1839
|
+
</form>
|
|
1840
|
+
<details><summary>${_('Reject and reply with a reason')}</summary>
|
|
1841
|
+
<form data-rec-reject="${esc(mensaje.id)}">
|
|
1842
|
+
<label for="rec-reason-${esc(mensaje.id)}">${_('Reason sent back securely')}</label>
|
|
1843
|
+
<input id="rec-reason-${esc(mensaje.id)}" type="text" name="reason"
|
|
1844
|
+
maxlength="500" required
|
|
1845
|
+
placeholder="${_('Reason for rejecting this message')}">
|
|
1846
|
+
<button class="bt malo" type="submit">${_('Reject and send reason')}</button>
|
|
1847
|
+
</form>
|
|
1848
|
+
</details>
|
|
1849
|
+
</div>`
|
|
1850
|
+
}
|
|
1851
|
+
</article>`,
|
|
1852
|
+
)
|
|
1853
|
+
.join('')}</div>`
|
|
1854
|
+
: '';
|
|
1855
|
+
hueco.innerHTML = `${vacia}${pendientes}${conexiones}`;
|
|
1856
|
+
enlazaRecepcion();
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
function enlazaRecepcion(): void {
|
|
1860
|
+
q<HTMLFormElement>('[data-rec-config]').onsubmit = async (evento) => {
|
|
1861
|
+
evento.preventDefault();
|
|
1862
|
+
const formulario = evento.currentTarget as HTMLFormElement;
|
|
1863
|
+
const boton = q<HTMLButtonElement>('button[type=submit]', formulario);
|
|
1864
|
+
const routingMode = q<HTMLInputElement>('input[name=routing_mode]:checked', formulario).value;
|
|
1865
|
+
const rules = todos<HTMLInputElement>('[data-rule-city]', formulario)
|
|
1866
|
+
.map((campo) => ({
|
|
1867
|
+
city_id: campo.dataset.ruleCity,
|
|
1868
|
+
keywords: campo.value
|
|
1869
|
+
.split(',')
|
|
1870
|
+
.map((value) => value.trim())
|
|
1871
|
+
.filter(Boolean),
|
|
1872
|
+
}))
|
|
1873
|
+
.filter((rule) => rule.keywords.length);
|
|
1874
|
+
boton.disabled = true;
|
|
1875
|
+
const respuesta = await api<{ ok?: boolean; error?: string }>('/api/reception', {
|
|
1876
|
+
method: 'POST',
|
|
1877
|
+
body: JSON.stringify({ action: 'configure', routing_mode: routingMode, rules }),
|
|
1878
|
+
});
|
|
1879
|
+
if (!respuesta.ok) {
|
|
1880
|
+
boton.disabled = false;
|
|
1881
|
+
toast(respuesta.error ?? _('Could not save the routing policy'), true);
|
|
1882
|
+
return;
|
|
1883
|
+
}
|
|
1884
|
+
toast(_('Routing policy saved.'));
|
|
1885
|
+
await cargaRecepcion();
|
|
1886
|
+
};
|
|
1887
|
+
todos<HTMLFormElement>('[data-rec-send]').forEach((formulario) => {
|
|
1888
|
+
formulario.onsubmit = async (evento) => {
|
|
1889
|
+
evento.preventDefault();
|
|
1890
|
+
const boton = q<HTMLButtonElement>('button[type=submit]', formulario);
|
|
1891
|
+
const campo = q<HTMLTextAreaElement>('textarea[name=text]', formulario);
|
|
1892
|
+
const connectionId = q<HTMLSelectElement>('select[name=connection]', formulario).value;
|
|
1893
|
+
if (!connectionId || !campo.value.trim()) return;
|
|
1894
|
+
boton.disabled = true;
|
|
1895
|
+
const respuesta = await api<{ ok?: boolean; error?: string }>('/api/reception', {
|
|
1896
|
+
method: 'POST',
|
|
1897
|
+
body: JSON.stringify({
|
|
1898
|
+
action: 'send',
|
|
1899
|
+
connection_id: connectionId,
|
|
1900
|
+
text: campo.value,
|
|
1901
|
+
}),
|
|
1902
|
+
});
|
|
1903
|
+
if (!respuesta.ok) {
|
|
1904
|
+
boton.disabled = false;
|
|
1905
|
+
toast(respuesta.error ?? _('Could not queue the message'), true);
|
|
1906
|
+
return;
|
|
1907
|
+
}
|
|
1908
|
+
campo.value = '';
|
|
1909
|
+
toast(_('Message queued on this computer.'));
|
|
1910
|
+
await cargaRecepcion();
|
|
1911
|
+
};
|
|
1912
|
+
});
|
|
1913
|
+
todos<HTMLButtonElement>('[data-rec-dismiss]').forEach((boton) => {
|
|
1914
|
+
boton.onclick = async () => {
|
|
1915
|
+
boton.disabled = true;
|
|
1916
|
+
const respuesta = await api<{ ok?: boolean; error?: string }>('/api/reception', {
|
|
1917
|
+
method: 'POST',
|
|
1918
|
+
body: JSON.stringify({
|
|
1919
|
+
action: 'reject',
|
|
1920
|
+
message_id: boton.dataset.recDismiss,
|
|
1921
|
+
reason: 'Dismissed response',
|
|
1922
|
+
}),
|
|
1923
|
+
});
|
|
1924
|
+
if (!respuesta.ok) {
|
|
1925
|
+
boton.disabled = false;
|
|
1926
|
+
toast(respuesta.error ?? _('Could not dismiss the response'), true);
|
|
1927
|
+
return;
|
|
1928
|
+
}
|
|
1929
|
+
await cargaRecepcion();
|
|
1930
|
+
};
|
|
1931
|
+
});
|
|
1932
|
+
todos<HTMLFormElement>('[data-rec-route]').forEach((formulario) => {
|
|
1933
|
+
const boton = q<HTMLButtonElement>('button[type=submit]', formulario);
|
|
1934
|
+
const actualiza = () => {
|
|
1935
|
+
boton.disabled = !formulario.querySelector<HTMLInputElement>('input:checked');
|
|
1936
|
+
};
|
|
1937
|
+
formulario.onchange = actualiza;
|
|
1938
|
+
formulario.onsubmit = async (evento) => {
|
|
1939
|
+
evento.preventDefault();
|
|
1940
|
+
const destinations = todos<HTMLInputElement>(
|
|
1941
|
+
'input[name=destination]:checked',
|
|
1942
|
+
formulario,
|
|
1943
|
+
).map((campo) => campo.value);
|
|
1944
|
+
if (!destinations.length) return;
|
|
1945
|
+
boton.disabled = true;
|
|
1946
|
+
const respuesta = await api<{ ok?: boolean; error?: string }>('/api/reception', {
|
|
1947
|
+
method: 'POST',
|
|
1948
|
+
body: JSON.stringify({
|
|
1949
|
+
action: 'route',
|
|
1950
|
+
message_id: formulario.dataset.recRoute,
|
|
1951
|
+
destinations,
|
|
1952
|
+
}),
|
|
1953
|
+
});
|
|
1954
|
+
if (!respuesta.ok) {
|
|
1955
|
+
boton.disabled = false;
|
|
1956
|
+
toast(respuesta.error ?? _('Could not route the message'), true);
|
|
1957
|
+
return;
|
|
1958
|
+
}
|
|
1959
|
+
toast(_('Message routed. Only the selected cities can now read it.'));
|
|
1960
|
+
await cargaRecepcion();
|
|
1961
|
+
};
|
|
1962
|
+
});
|
|
1963
|
+
todos<HTMLFormElement>('[data-rec-reject]').forEach((formulario) => {
|
|
1964
|
+
formulario.onsubmit = async (evento) => {
|
|
1965
|
+
evento.preventDefault();
|
|
1966
|
+
const boton = q<HTMLButtonElement>('button[type=submit]', formulario);
|
|
1967
|
+
const reason = q<HTMLInputElement>('input[name=reason]', formulario).value.trim();
|
|
1968
|
+
if (!reason) return;
|
|
1969
|
+
boton.disabled = true;
|
|
1970
|
+
const respuesta = await api<{ ok?: boolean; error?: string }>('/api/reception', {
|
|
1971
|
+
method: 'POST',
|
|
1972
|
+
body: JSON.stringify({
|
|
1973
|
+
action: 'reject',
|
|
1974
|
+
message_id: formulario.dataset.recReject,
|
|
1975
|
+
reason,
|
|
1976
|
+
}),
|
|
1977
|
+
});
|
|
1978
|
+
if (!respuesta.ok) {
|
|
1979
|
+
boton.disabled = false;
|
|
1980
|
+
toast(respuesta.error ?? _('Could not reject the message'), true);
|
|
1981
|
+
return;
|
|
1982
|
+
}
|
|
1983
|
+
toast(_('Message rejected. Your reason is queued for encrypted delivery.'));
|
|
1984
|
+
await cargaRecepcion();
|
|
1985
|
+
};
|
|
1986
|
+
});
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
function fechaRecepcion(value: string): string {
|
|
1990
|
+
const date = new Date(value);
|
|
1991
|
+
return Number.isNaN(date.getTime())
|
|
1992
|
+
? value
|
|
1993
|
+
: date.toLocaleString(idioma() === 'es' ? 'es-ES' : 'en-GB', {
|
|
1994
|
+
dateStyle: 'medium',
|
|
1995
|
+
timeStyle: 'short',
|
|
1996
|
+
});
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
function formateaBytes(bytes: number): string {
|
|
2000
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
2001
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
2002
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
2003
|
+
}
|
|
2004
|
+
|
|
1618
2005
|
// ── committee acts ───────────────────────────────────────────────────────────
|
|
1619
2006
|
VISTAS.committee = () => {
|
|
1620
2007
|
q('#lienzo').innerHTML = `<div><span class="sub">committee</span>
|
|
1621
|
-
<h1 style="margin-top:6px"
|
|
2008
|
+
<h1 style="margin-top:6px">${_('Decisions with a visible chain of custody')}</h1>
|
|
1622
2009
|
<p class="prosa" style="margin-top:8px">The seat selects relevant repo agents,
|
|
1623
2010
|
gathers isolated positions, controls the floor, decides and assigns an
|
|
1624
2011
|
independent verification. This view is read-only; the vendor-neutral CLI
|
|
1625
2012
|
drives the protocol.</p></div>
|
|
1626
2013
|
<div class="orden"><span class="et2">start</span>
|
|
1627
|
-
<code
|
|
2014
|
+
<code>${_('agents-city committee schema open')}</code>
|
|
1628
2015
|
<button class="bt mini" data-copia="agents-city committee schema open">copy</button>
|
|
1629
2016
|
</div>
|
|
1630
2017
|
<div class="gente">${
|
|
@@ -1642,7 +2029,7 @@ VISTAS.committee = () => {
|
|
|
1642
2029
|
</div>`,
|
|
1643
2030
|
)
|
|
1644
2031
|
.join('') ||
|
|
1645
|
-
|
|
2032
|
+
`<p class="prosa">${_('No committee acts yet. Open one only when the seat needs specialised evidence.')}</p>`
|
|
1646
2033
|
}</div>`;
|
|
1647
2034
|
enlaza();
|
|
1648
2035
|
};
|
|
@@ -1675,12 +2062,17 @@ VISTAS.gente = () => {
|
|
|
1675
2062
|
const g = a.growth;
|
|
1676
2063
|
const crecido = Math.min(1, Math.log2(g.floors + 1) / 8);
|
|
1677
2064
|
const skills = E.skills[a.name]?.skills ?? [];
|
|
1678
|
-
const
|
|
2065
|
+
const motorDelAgente = motorDe(a.runtime);
|
|
1679
2066
|
// "custom…" keeps the curated lineup from ever holding the card hostage:
|
|
1680
|
-
// the alias is resolved by the CLI at launch, not by this list.
|
|
2067
|
+
// the alias is resolved by the CLI at launch, not by this list. Where the
|
|
2068
|
+
// names are not ours to list at all, the whole field is that free text.
|
|
1681
2069
|
const opcionesModelo =
|
|
1682
|
-
opciones([''].concat(
|
|
1683
|
-
|
|
2070
|
+
opciones([''].concat(motorDelAgente.modelos), a.model) +
|
|
2071
|
+
`<option value="__otro__">${_('custom…')}</option>`;
|
|
2072
|
+
const opcionesEsfuerzo = opciones(
|
|
2073
|
+
[''].concat(ESFUERZOS),
|
|
2074
|
+
motorDelAgente.esfuerzo ? a.effort : '',
|
|
2075
|
+
);
|
|
1684
2076
|
const opcionesRuntime = opciones(RUNTIMES, a.runtime);
|
|
1685
2077
|
const montajes = a.mounts ?? [];
|
|
1686
2078
|
return `
|
|
@@ -1692,22 +2084,25 @@ VISTAS.gente = () => {
|
|
|
1692
2084
|
<span class="rpgHogar">${a.legacy ? 'repo' : `workspace · ${montajes.length} ${montajes.length === 1 ? 'mount' : 'mounts'}`}</span></span></div>
|
|
1693
2085
|
<span class="rpgBotones">
|
|
1694
2086
|
<button class="rpgMini rpgDado" type="button" data-agente="${esc(a.slug)}"
|
|
1695
|
-
title="Reroll this agent's face — deterministic, persisted on the card">🎲</button>
|
|
2087
|
+
title="${_("Reroll this agent's face — deterministic, persisted on the card")}">🎲</button>
|
|
1696
2088
|
<button class="rpgMini rpgIns" type="button" data-agente="${esc(a.slug)}"
|
|
1697
|
-
data-file="CLAUDE.md" title="Instructions the Claude runtime reads">CLAUDE.md</button>
|
|
2089
|
+
data-file="CLAUDE.md" title="${_('Instructions the Claude runtime reads')}">CLAUDE.md</button>
|
|
1698
2090
|
<button class="rpgMini rpgIns" type="button" data-agente="${esc(a.slug)}"
|
|
1699
|
-
data-file="AGENTS.md" title="Instructions Codex, OpenCode and Kimi read">AGENTS.md</button>
|
|
2091
|
+
data-file="AGENTS.md" title="${_('Instructions Codex, OpenCode and Kimi read')}">AGENTS.md</button>
|
|
1700
2092
|
</span>
|
|
1701
2093
|
</div>
|
|
1702
2094
|
<div class="rpgFila"><label>${_('engine')}</label>
|
|
1703
2095
|
${barra(motor.ancho, motor.defecto ? 'defecto' : '')}
|
|
1704
|
-
<select class="rpgSel" data-agente="${esc(a.slug)}" data-campo="model"
|
|
1705
|
-
${esClaude ? '' : 'disabled title="This window runs its own CLI; its model lives in its own flags"'}>
|
|
2096
|
+
<select class="rpgSel" data-agente="${esc(a.slug)}" data-campo="model">
|
|
1706
2097
|
${opcionesModelo}</select></div>
|
|
1707
2098
|
<div class="rpgFila"><label>${_('effort')}</label>
|
|
1708
2099
|
${barra(esfuerzo / 5, esfuerzo ? '' : 'defecto')}
|
|
1709
2100
|
<select class="rpgSel" data-agente="${esc(a.slug)}" data-campo="effort"
|
|
1710
|
-
${
|
|
2101
|
+
${
|
|
2102
|
+
motorDelAgente.esfuerzo
|
|
2103
|
+
? ''
|
|
2104
|
+
: `disabled title="${_('This CLI has no effort setting.')}"`
|
|
2105
|
+
}>
|
|
1711
2106
|
${opcionesEsfuerzo}</select></div>
|
|
1712
2107
|
<div class="rpgFila"><label>${_('provider')}</label>
|
|
1713
2108
|
<span class="rpgDato"><span class="cliDot ${
|
|
@@ -1722,7 +2117,7 @@ VISTAS.gente = () => {
|
|
|
1722
2117
|
_(a.cli.connected ? 'connected' : a.cli.installed ? 'idle' : 'missing'),
|
|
1723
2118
|
)} · ${esc(a.cli.binary)}
|
|
1724
2119
|
<button class="rpgMini rpgTest" type="button" data-agente="${esc(a.slug)}"
|
|
1725
|
-
title="Run the engine for real: --version, and the login state on Claude">test</button></span>
|
|
2120
|
+
title="${_('Run the engine for real: --version, and the login state on Claude')}">test</button></span>
|
|
1726
2121
|
<select class="rpgSel" data-agente="${esc(a.slug)}" data-campo="runtime">
|
|
1727
2122
|
${opcionesRuntime}</select></div>
|
|
1728
2123
|
<div class="rpgSkills"><label>${_('works on')} · ${montajes.length}</label>
|
|
@@ -1744,7 +2139,7 @@ VISTAS.gente = () => {
|
|
|
1744
2139
|
}
|
|
1745
2140
|
${
|
|
1746
2141
|
a.legacy
|
|
1747
|
-
?
|
|
2142
|
+
? `<span class="rpgDato">${_('a legacy repo agent works on its own repo')}</span>`
|
|
1748
2143
|
: `<button class="rpgMini rpgMonta" type="button" data-agente="${esc(a.slug)}"
|
|
1749
2144
|
title="A repo, a worktree or a folder of documents — this agent works on all of them">+ folder</button>`
|
|
1750
2145
|
}</div>
|
|
@@ -1760,7 +2155,7 @@ VISTAS.gente = () => {
|
|
|
1760
2155
|
`<code class="mono" title="${esc(s.description)}">${esc(s.name)}${
|
|
1761
2156
|
s.removable && s.dir
|
|
1762
2157
|
? `<button class="rpgQuitar" type="button" data-agente="${esc(a.slug)}"
|
|
1763
|
-
data-skill="${esc(s.dir)}" title="Remove this skill from the agent's home">×</button>`
|
|
2158
|
+
data-skill="${esc(s.dir)}" title="${_("Remove this skill from the agent's home")}">×</button>`
|
|
1764
2159
|
: ''
|
|
1765
2160
|
}</code>`,
|
|
1766
2161
|
)
|
|
@@ -1768,21 +2163,21 @@ VISTAS.gente = () => {
|
|
|
1768
2163
|
: '<span class="rpgDato">none discovered</span>'
|
|
1769
2164
|
}
|
|
1770
2165
|
<label class="rpgMini rpgSubir"
|
|
1771
|
-
title="Install a skill zip into this agent's own home — the Claude runtime reads skills; other engines ignore them">
|
|
2166
|
+
title="${_("Install a skill zip into this agent's own home — the Claude runtime reads skills; other engines ignore them")}">
|
|
1772
2167
|
+ zip<input type="file" accept=".zip" data-agente="${esc(a.slug)}"></label></div>
|
|
1773
2168
|
</div>`;
|
|
1774
2169
|
};
|
|
1775
2170
|
|
|
1776
2171
|
q('#lienzo').innerHTML = `<div><span class="sub">the houses of ${esc(E.city_name)}</span>
|
|
1777
2172
|
<h1 style="margin-top:6px">${_('Who lives in {city}', { city: esc(E.city_name) })}</h1>
|
|
1778
|
-
<p class="prosa" style="margin-top:8px"
|
|
2173
|
+
<p class="prosa" style="margin-top:8px">${_(`<b>A house is where an agent lives and works</b>,
|
|
1779
2174
|
and many houses are a city — it is the same thing the map draws, growing with what that
|
|
1780
2175
|
agent actually does. The card and the CLI call them <code class="mono">agents</code>;
|
|
1781
|
-
here you see their houses
|
|
1782
|
-
<p class="prosa"
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
2176
|
+
here you see their houses.`)}</p>
|
|
2177
|
+
<p class="prosa">${_(
|
|
2178
|
+
'They belong to <b>this</b> city and only to it: each one’s workspace and its mounts live inside {donde}, so another city has its own people even if you give them the same names.',
|
|
2179
|
+
{ donde: `<code class="mono">${esc(corto(E.datos))}/agents/</code>` },
|
|
2180
|
+
)} Every agent is whole here: the kind of work
|
|
1786
2181
|
it does, its role, everything it works on — any number of repositories and document
|
|
1787
2182
|
folders at once — the engine and effort that run it, and the skills in its own home.</p>
|
|
1788
2183
|
<p class="prosa">Every number is real: growth from what the agent actually produced,
|
|
@@ -2009,8 +2404,23 @@ VISTAS.gente = () => {
|
|
|
2009
2404
|
},
|
|
2010
2405
|
});
|
|
2011
2406
|
if (dicho === null || !elegidas.length) return;
|
|
2012
|
-
for
|
|
2013
|
-
|
|
2407
|
+
// One refresh and one toast for the whole dialog: mounting five folders
|
|
2408
|
+
// used to cost five serialised posts, five state fetches and five full
|
|
2409
|
+
// repaints of the Hall, for five independent writes.
|
|
2410
|
+
const agente = boton.dataset.agente ?? '';
|
|
2411
|
+
const fallos = await Promise.all(
|
|
2412
|
+
elegidas.map((ruta) =>
|
|
2413
|
+
api<{ ok?: boolean; error?: string }>('/api/montaje', {
|
|
2414
|
+
method: 'POST',
|
|
2415
|
+
body: JSON.stringify({ agent: agente, add: ruta }),
|
|
2416
|
+
})
|
|
2417
|
+
.then((r) => (r.ok ? '' : `${ruta}: ${r.error ?? 'could not mount'}`))
|
|
2418
|
+
.catch((e) => `${ruta}: ${String(e)}`),
|
|
2419
|
+
),
|
|
2420
|
+
);
|
|
2421
|
+
const malos = fallos.filter(Boolean);
|
|
2422
|
+
toast(malos.length ? malos.join('; ') : _('Mounted'), malos.length > 0);
|
|
2423
|
+
void refresca();
|
|
2014
2424
|
};
|
|
2015
2425
|
});
|
|
2016
2426
|
todos<HTMLButtonElement>('.rpgDesmonta').forEach((boton) => {
|
|
@@ -2024,8 +2434,6 @@ VISTAS.gente = () => {
|
|
|
2024
2434
|
{ aceptar: _('Unmount it') },
|
|
2025
2435
|
);
|
|
2026
2436
|
if (!vale) return;
|
|
2027
|
-
// Unmounting removes a symlink and a card key. What it points at is not
|
|
2028
|
-
// touched — say so, because "remove" reads as "delete my folder".
|
|
2029
2437
|
void montaje(
|
|
2030
2438
|
{ agent: boton.dataset.agente ?? '', remove: etiqueta },
|
|
2031
2439
|
`${etiqueta} unmounted — the folder itself is untouched`,
|
|
@@ -2314,6 +2722,15 @@ function interruptorDeIdioma(): void {
|
|
|
2314
2722
|
};
|
|
2315
2723
|
}
|
|
2316
2724
|
|
|
2725
|
+
// Nothing in a browser reports itself. These two are why a person can say "it
|
|
2726
|
+
// just did nothing" and be exactly right.
|
|
2727
|
+
window.addEventListener('error', (e) =>
|
|
2728
|
+
anota('uncaught error', { mensaje: e.message, fichero: e.filename, linea: e.lineno }),
|
|
2729
|
+
);
|
|
2730
|
+
window.addEventListener('unhandledrejection', (e) =>
|
|
2731
|
+
anota('unhandled rejection', String((e as PromiseRejectionEvent).reason)),
|
|
2732
|
+
);
|
|
2733
|
+
|
|
2317
2734
|
arrastreDelRail();
|
|
2318
2735
|
interruptorDeIdioma();
|
|
2319
2736
|
tema();
|