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/README.es.md
CHANGED
|
@@ -556,6 +556,27 @@ se escriben en el registro de actividad. El token de observador rota con el hub,
|
|
|
556
556
|
sólo acepta un origen de este ordenador y es de sólo lectura: el navegador no
|
|
557
557
|
puede dirigir el comité. `Ctrl-c` detiene el Hall.
|
|
558
558
|
|
|
559
|
+
**Demos**, en el rail, reproduce un comité entero sin montar nada: una historia
|
|
560
|
+
por dominio de trabajo — un estudio, una clínica, un despacho — con play, pausa,
|
|
561
|
+
repetir y velocidad. Lo que reproduce son *grabaciones*: `demo/graba.py` ejecuta
|
|
562
|
+
cada historia sobre el bus local real, a través de la máquina de estados real del
|
|
563
|
+
comité, y guarda el flujo exacto de eventos que vio un espectador; el Hall los
|
|
564
|
+
reproduce con el mismo renderizador que usa el rail en vivo. Lo dice en pantalla,
|
|
565
|
+
porque una demo que finge estar en vivo es justo la que este producto no debe
|
|
566
|
+
publicar. Para lanzar una en vivo desde terminal: `agents-city demo --domain
|
|
567
|
+
software`.
|
|
568
|
+
|
|
569
|
+
Regenera las grabaciones tras editar `demo/stories.py`:
|
|
570
|
+
|
|
571
|
+
```bash
|
|
572
|
+
demo/graba.py # todas las historias
|
|
573
|
+
demo/graba.py medicina # sólo una
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
La suite de demo falla cuando una grabación deja de corresponderse con la
|
|
577
|
+
historia que dice ser, así que una obsoleta es un build en rojo y no un navegador
|
|
578
|
+
reproduciendo en silencio el comité del mes pasado.
|
|
579
|
+
|
|
559
580
|
Bajo la marca hay dos botones: **día/noche** e **ES/EN**. El Hall habla español e
|
|
560
581
|
inglés, arranca en el idioma del navegador y recuerda la elección explícita. Las
|
|
561
582
|
traducciones se indexan por la frase en inglés, así que lo que aún no esté
|
|
@@ -1843,9 +1864,17 @@ CITY_SETTLE=0 CITY_STAGGER=0 agents-city seat --city producto
|
|
|
1843
1864
|
Lee `.git/config` y `HEAD` directamente en vez de lanzar `git` una vez por
|
|
1844
1865
|
repositorio, así que un escaneo completo termina mientras alguien mira la
|
|
1845
1866
|
pantalla, y funciona allí donde funcione Python: macOS, Linux y Windows por
|
|
1846
|
-
igual.
|
|
1847
|
-
|
|
1848
|
-
|
|
1867
|
+
igual. Este índice es el que usa la **terminal**: `seat --repos`, y el lanzador
|
|
1868
|
+
resolviendo el `repo@rama` de una ficha a una ruta de esta máquina.
|
|
1869
|
+
`plugin/scripts/find-repos.sh` es un envoltorio fino para quien lo llama desde
|
|
1870
|
+
shell, y sólo imprime la mitad git.
|
|
1871
|
+
|
|
1872
|
+
El **Hall** no lo usa, y a propósito: allí, elegir sobre qué trabaja un agente es
|
|
1873
|
+
un explorador de carpetas. Recorres tu disco y coges lo que quieras — un
|
|
1874
|
+
repositorio, un worktree, una carpeta de documentos, un fichero exacto — y no se
|
|
1875
|
+
te adelanta nada ni se filtra nada. Una lista adivinada sólo puede ofrecerte lo
|
|
1876
|
+
que sabía buscar, y es una lista más que leer antes de hacer aquello a lo que
|
|
1877
|
+
venías.
|
|
1849
1878
|
|
|
1850
1879
|
El índice se cachea un día en `$XDG_CACHE_HOME/agents-city/lugares.tsv` o
|
|
1851
1880
|
`~/.cache/agents-city/lugares.tsv`. Tanto el formulario de casas del Hall como el
|
|
@@ -2099,10 +2128,30 @@ agents-city seat --repos
|
|
|
2099
2128
|
```
|
|
2100
2129
|
|
|
2101
2130
|
La detección exige `.git` (directorio o fichero de worktree) y un remote `origin`.
|
|
2102
|
-
Si acabas de cambiar las raíces,
|
|
2103
|
-
casas, o ejecuta `plugin/scripts/busca.py --refresh`. `AGENTS_CITY_ORG` puede estar filtrando el repo;
|
|
2131
|
+
Si acabas de cambiar las raíces, ejecuta `plugin/scripts/busca.py --refresh`. `AGENTS_CITY_ORG` puede estar filtrando el repo;
|
|
2104
2132
|
déjala vacía para indexar todos los remotes.
|
|
2105
2133
|
|
|
2134
|
+
### Desinstalarlo del todo
|
|
2135
|
+
|
|
2136
|
+
```bash
|
|
2137
|
+
agents-city uninstall # dice exactamente qué se iría; no borra nada
|
|
2138
|
+
agents-city uninstall --yes # lo hace
|
|
2139
|
+
agents-city uninstall --keep-cities --yes # desconecta la máquina, conserva las ciudades
|
|
2140
|
+
agents-city uninstall --npm --yes # y quita también el paquete global
|
|
2141
|
+
```
|
|
2142
|
+
|
|
2143
|
+
Cierra todas las sesiones, halls y mapas que el producto arrancó, quita los
|
|
2144
|
+
accesos directos del escritorio y el registro del plugin de Claude, y borra
|
|
2145
|
+
`~/.agents-city` (tus ciudades, su estado y sus copias), `~/.config/agents-city`,
|
|
2146
|
+
`~/.cache/agents-city` y `~/.claude/channels/city-bus` — más el token del bus en
|
|
2147
|
+
el Keychain de macOS.
|
|
2148
|
+
|
|
2149
|
+
Nunca toca tus repositorios, tus worktrees ni tus carpetas de documentos. La casa
|
|
2150
|
+
de un agente guarda *enlaces* a eso, y lo único que se va es el enlace.
|
|
2151
|
+
|
|
2152
|
+
`reset` responde a la otra pregunta: vacía una ciudad, deja una copia y conserva
|
|
2153
|
+
la instalación, para cuando piensas seguir usándolo.
|
|
2154
|
+
|
|
2106
2155
|
### GitHub no muestra repos privados u organizaciones
|
|
2107
2156
|
|
|
2108
2157
|
```bash
|
package/README.md
CHANGED
|
@@ -550,6 +550,26 @@ spectator token rotates with the hub, accepts only an origin on this computer,
|
|
|
550
550
|
and is read-only: the browser cannot direct the committee. `Ctrl-c` stops the
|
|
551
551
|
Hall.
|
|
552
552
|
|
|
553
|
+
**Demos** in the rail plays a whole committee without setting anything up: one
|
|
554
|
+
story per work domain — a studio, a clinic, a law firm — with play, pause,
|
|
555
|
+
replay and speed. What it plays are *recordings*: `demo/graba.py` runs each story
|
|
556
|
+
over the real local bus, through the real committee state machine, and keeps the
|
|
557
|
+
exact event stream a spectator saw; the Hall replays those events through the
|
|
558
|
+
same renderer the live rail uses. It says so on screen, because a demo that
|
|
559
|
+
pretends to be live is the one kind this product must not ship. To run one live
|
|
560
|
+
in a terminal instead: `agents-city demo --domain software`.
|
|
561
|
+
|
|
562
|
+
Regenerate the recordings after editing `demo/stories.py`:
|
|
563
|
+
|
|
564
|
+
```bash
|
|
565
|
+
demo/graba.py # every story
|
|
566
|
+
demo/graba.py medicina # just one
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
The demo suite fails when a recording no longer matches the story it claims to
|
|
570
|
+
be, so a stale one is a red build rather than a browser quietly playing last
|
|
571
|
+
month's committee.
|
|
572
|
+
|
|
553
573
|
Two buttons sit under the brand: **day/night**, and **ES/EN**. The Hall speaks
|
|
554
574
|
Spanish and English, starting in the browser's own language and remembering an
|
|
555
575
|
explicit choice. Translations are keyed by the English sentence, so anything not
|
|
@@ -1819,9 +1839,17 @@ CITY_SETTLE=0 CITY_STAGGER=0 agents-city seat --city product
|
|
|
1819
1839
|
|
|
1820
1840
|
It reads `.git/config` and `HEAD` directly rather than running `git` once per
|
|
1821
1841
|
repository, so a full scan finishes while somebody is looking at the screen, and
|
|
1822
|
-
it runs anywhere Python does — macOS, Linux and Windows alike.
|
|
1823
|
-
|
|
1824
|
-
|
|
1842
|
+
it runs anywhere Python does — macOS, Linux and Windows alike. This index is what
|
|
1843
|
+
the **terminal** uses: `seat --repos`, and the launcher resolving a card's
|
|
1844
|
+
`repo@branch` to a path on this machine. `plugin/scripts/find-repos.sh` is a thin
|
|
1845
|
+
shim over it for shell callers, printing the git half only.
|
|
1846
|
+
|
|
1847
|
+
The **Hall** does not use it, and deliberately: choosing what an agent works on
|
|
1848
|
+
is a folder picker there. You walk your disk and take what you want — a
|
|
1849
|
+
repository, a worktree, a folder of documents, one exact file — and nothing is
|
|
1850
|
+
offered in advance or filtered out. A guessed list can only ever offer what it
|
|
1851
|
+
knew how to look for, and it is one more list to read before you can do the thing
|
|
1852
|
+
you came to do.
|
|
1825
1853
|
|
|
1826
1854
|
The index is cached for one day at `$XDG_CACHE_HOME/agents-city/lugares.tsv` or
|
|
1827
1855
|
`~/.cache/agents-city/lugares.tsv`. Both the Hall's house form and the seat offer
|
|
@@ -2065,10 +2093,30 @@ agents-city seat --repos
|
|
|
2065
2093
|
```
|
|
2066
2094
|
|
|
2067
2095
|
Discovery requires `.git` (a directory or worktree file) and an `origin` remote.
|
|
2068
|
-
If roots just changed,
|
|
2069
|
-
`plugin/scripts/busca.py --refresh`. `AGENTS_CITY_ORG` may be filtering the repo;
|
|
2096
|
+
If roots just changed, run `plugin/scripts/busca.py --refresh`. `AGENTS_CITY_ORG` may be filtering the repo;
|
|
2070
2097
|
leave it empty to index every remote.
|
|
2071
2098
|
|
|
2099
|
+
### Removing it completely
|
|
2100
|
+
|
|
2101
|
+
```bash
|
|
2102
|
+
agents-city uninstall # says exactly what would go; removes nothing
|
|
2103
|
+
agents-city uninstall --yes # goes through with it
|
|
2104
|
+
agents-city uninstall --keep-cities --yes # unwire the machine, keep the cities
|
|
2105
|
+
agents-city uninstall --npm --yes # and remove the global package too
|
|
2106
|
+
```
|
|
2107
|
+
|
|
2108
|
+
It closes every session, hall and map the product started, removes the desktop
|
|
2109
|
+
shortcuts and the Claude plugin registration, and deletes `~/.agents-city` (your
|
|
2110
|
+
cities, their state and their backups), `~/.config/agents-city`,
|
|
2111
|
+
`~/.cache/agents-city` and `~/.claude/channels/city-bus` — plus the bus token in
|
|
2112
|
+
the macOS Keychain.
|
|
2113
|
+
|
|
2114
|
+
It never touches your repositories, your worktrees or your document folders. An
|
|
2115
|
+
agent's home holds *links* to those, and a link is all that goes.
|
|
2116
|
+
|
|
2117
|
+
`reset` is the other question: it empties one city, keeps a backup and leaves the
|
|
2118
|
+
install in place, for when you mean to keep using it.
|
|
2119
|
+
|
|
2072
2120
|
### GitHub does not show private repos or organisations
|
|
2073
2121
|
|
|
2074
2122
|
```bash
|
package/bin/agents-city.js
CHANGED
|
@@ -30,6 +30,10 @@ const ORDENES = {
|
|
|
30
30
|
benchmark: { que: ['bin/benchmark'], di: 'offline stress/governance or opt-in live runtime latency' },
|
|
31
31
|
logs: { que: ['bin/logs'], di: 'read or follow visible activity and operational diagnostics' },
|
|
32
32
|
reset: { que: ['bin/reset'], di: 'reset one city to onboarding, recoverably' },
|
|
33
|
+
uninstall: {
|
|
34
|
+
que: ['bin/uninstall'],
|
|
35
|
+
di: 'remove everything this wrote on this machine (previews first)',
|
|
36
|
+
},
|
|
33
37
|
skills: { que: ['bin/skills'], di: 'recognise the skills installed in each repo' },
|
|
34
38
|
city: { que: ['bin/city'], di: 'draw your city' },
|
|
35
39
|
shortcut: { que: ['bin/shortcut'], di: 'put a city on your desktop: icon, name, double-click' },
|
package/bin/hall.html
CHANGED
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
--hud:"Chakra Petch",system-ui,sans-serif; --texto:"Newsreader",Georgia,serif;
|
|
32
32
|
--mono:"IBM Plex Mono",ui-monospace,monospace;
|
|
33
33
|
--sombra:0 18px 40px -30px #1d222945;
|
|
34
|
+
--velo:#1d222959; --sombra-alta:0 24px 60px -20px #1d222966;
|
|
34
35
|
|
|
35
36
|
/* Night, declared once: the city's own light, an isometric map at 3am. The
|
|
36
37
|
two rules below only MAP these; a palette written twice drifts twice. */
|
|
@@ -41,6 +42,7 @@
|
|
|
41
42
|
--noche-panel:#101720; --noche-burbuja:#171f2a; --noche-burbuja-mia:#27291f;
|
|
42
43
|
--noche-borde-acento:#65552d; --noche-sobre-lampara:#1a1408;
|
|
43
44
|
--noche-lampara-viva:#ffd469;
|
|
45
|
+
--noche-velo:#05080d99; --noche-sombra-alta:0 24px 60px -18px #000000aa;
|
|
44
46
|
--noche-chip-luz:36% 30%; --noche-chip-tinta:#f3eee4;
|
|
45
47
|
--noche-tono-palabra:#aa93ff; --noche-tono-evidencia:#61ace9;
|
|
46
48
|
--noche-tono-verificacion:#50ccb1;
|
|
@@ -60,6 +62,7 @@
|
|
|
60
62
|
--tono-palabra:var(--noche-tono-palabra); --tono-evidencia:var(--noche-tono-evidencia);
|
|
61
63
|
--tono-verificacion:var(--noche-tono-verificacion);
|
|
62
64
|
--sombra:var(--noche-sombra);
|
|
65
|
+
--velo:var(--noche-velo); --sombra-alta:var(--noche-sombra-alta);
|
|
63
66
|
}
|
|
64
67
|
/* No `prefers-color-scheme` rule on purpose: daylight is THE default, not a
|
|
65
68
|
default that a machine set to dark quietly overrides. Night is a choice made
|
|
@@ -349,15 +352,97 @@ body.enGuia #app{grid-template-columns:250px minmax(0,1fr)}
|
|
|
349
352
|
font:11px var(--mono);padding:5px 10px;border-radius:999px;cursor:pointer}
|
|
350
353
|
.bvChip:hover{border-color:var(--lampara)}
|
|
351
354
|
.bvChip.elegida{border-color:var(--lampara);background:var(--burbuja-mia);color:var(--tinta)}
|
|
355
|
+
.dlgFondo{position:fixed;inset:0;z-index:60;display:flex;align-items:center;
|
|
356
|
+
justify-content:center;padding:24px;background:var(--velo);backdrop-filter:blur(2px)}
|
|
357
|
+
.dlg{background:var(--suelo2);border:1px solid var(--linea2);border-radius:8px;
|
|
358
|
+
box-shadow:var(--sombra-alta);padding:22px 24px;width:100%;max-width:460px;
|
|
359
|
+
max-height:86vh;overflow:auto}
|
|
360
|
+
.dlg h2{font:600 17px var(--hud);color:var(--tinta);margin:0 0 10px}
|
|
361
|
+
.dlgProsa{font:13px/1.55 var(--hud);color:var(--tinta2);margin:0 0 10px}
|
|
362
|
+
.dlg .campo{margin-bottom:12px}
|
|
363
|
+
.dlg .campo label{display:block;font:600 10px var(--hud);letter-spacing:.14em;
|
|
364
|
+
text-transform:uppercase;color:var(--tinta3);margin-bottom:5px}
|
|
365
|
+
.dlg .campo input,.dlg .campo select{width:100%;background:var(--suelo);
|
|
366
|
+
border:1px solid var(--linea2);border-radius:4px;padding:8px 10px;
|
|
367
|
+
font-family:var(--hud);font-size:13px;color:var(--tinta)}
|
|
368
|
+
.dlg .campo input:focus,.dlg .campo select:focus{outline:none;border-color:var(--lampara)}
|
|
369
|
+
.dlgBotones{display:flex;gap:8px;justify-content:flex-end;margin-top:16px}
|
|
370
|
+
.dlgBotones .bt[disabled]{opacity:.45;cursor:not-allowed}
|
|
352
371
|
.bvClase{font:600 9px var(--mono);letter-spacing:.08em;text-transform:uppercase;
|
|
353
372
|
opacity:.55;font-style:normal;margin-right:6px}
|
|
354
373
|
.bvChip.cl-docs .bvClase{color:var(--verde);opacity:.85}
|
|
355
374
|
.bvChip.cl-worktree .bvClase{color:var(--lampara);opacity:.85}
|
|
356
|
-
.bvBuscador{display:flex;gap:8px;align-items:center;margin:4px 0 10px}
|
|
357
|
-
.bvBuscador input{flex:1;background:var(--suelo);border:1px solid var(--linea2);
|
|
358
|
-
border-radius:4px;padding:8px 10px;font-family:var(--hud);font-size:13px;color:var(--tinta)}
|
|
359
|
-
.bvBuscador input:focus{outline:none;border-color:var(--lampara)}
|
|
360
375
|
.bt.bvMini{padding:7px 12px;font-size:11px;white-space:nowrap}
|
|
376
|
+
.exp{border:1px solid var(--linea2);border-radius:5px;background:var(--suelo);
|
|
377
|
+
padding:8px;margin-bottom:10px}
|
|
378
|
+
.expAtajos{display:flex;flex-wrap:wrap;gap:5px;margin-bottom:7px}
|
|
379
|
+
.expAtajo{border:1px solid var(--linea);background:var(--suelo3);color:var(--tinta2);
|
|
380
|
+
font:11px var(--hud);padding:3px 9px;border-radius:999px;cursor:pointer}
|
|
381
|
+
.expAtajo:hover,.expAtajo.aqui{border-color:var(--lampara);color:var(--lampara)}
|
|
382
|
+
.expBarra{display:flex;gap:7px;align-items:center;margin-bottom:7px}
|
|
383
|
+
.expSubir{border:1px solid var(--linea2);background:var(--suelo3);color:var(--tinta2);
|
|
384
|
+
font:12px var(--mono);width:26px;height:26px;border-radius:4px;cursor:pointer;flex:none}
|
|
385
|
+
.expSubir:hover:not([disabled]){border-color:var(--lampara);color:var(--lampara)}
|
|
386
|
+
.expSubir[disabled]{opacity:.35;cursor:default}
|
|
387
|
+
.expMigas{display:flex;flex-wrap:wrap;align-items:center;gap:1px;overflow:hidden;
|
|
388
|
+
font:11px var(--mono);color:var(--tinta3)}
|
|
389
|
+
.expMigas i{opacity:.4;font-style:normal}
|
|
390
|
+
.expMiga{border:0;background:none;color:var(--tinta2);font:11px var(--mono);
|
|
391
|
+
padding:1px 3px;cursor:pointer;border-radius:3px}
|
|
392
|
+
.expMiga:hover{color:var(--lampara);background:var(--suelo3)}
|
|
393
|
+
.expLista{max-height:240px;overflow:auto;border-top:1px solid var(--linea);
|
|
394
|
+
border-bottom:1px solid var(--linea)}
|
|
395
|
+
.expFila{display:flex;align-items:center;gap:6px;border-bottom:1px solid var(--linea)}
|
|
396
|
+
.expFila:last-child{border-bottom:0}
|
|
397
|
+
.expFila.puesta{background:var(--burbuja-mia)}
|
|
398
|
+
.expNombre{flex:1;display:flex;align-items:center;gap:7px;border:0;background:none;
|
|
399
|
+
text-align:left;font:12px var(--hud);color:var(--tinta);padding:6px 8px;cursor:pointer;
|
|
400
|
+
overflow:hidden;white-space:nowrap;text-overflow:ellipsis}
|
|
401
|
+
.expNombre[disabled]{cursor:default;color:var(--tinta2)}
|
|
402
|
+
.expNombre:hover:not([disabled]){color:var(--lampara)}
|
|
403
|
+
.expIcono{font-style:normal;color:var(--tinta3);font-size:10px;width:8px;flex:none}
|
|
404
|
+
.expFila.fich .expNombre{font-family:var(--mono);font-size:11px}
|
|
405
|
+
.expMarca{font:600 9px var(--mono);letter-spacing:.06em;text-transform:uppercase;
|
|
406
|
+
color:var(--lampara);opacity:.8;font-style:normal;flex:none}
|
|
407
|
+
.expMas{border:0;background:none;color:var(--tinta3);font:13px var(--mono);
|
|
408
|
+
width:30px;height:28px;cursor:pointer;flex:none}
|
|
409
|
+
.expMas:hover{color:var(--lampara)}
|
|
410
|
+
.expFila.puesta .expMas{color:var(--lampara)}
|
|
411
|
+
.expTomaAqui{margin-top:8px;width:100%}
|
|
412
|
+
.expElegidas{margin:8px 0 4px;display:flex;flex-wrap:wrap;gap:6px;align-items:center}
|
|
413
|
+
.expElegidas .bvEtiqueta{width:100%;margin:0 0 2px}
|
|
414
|
+
.expQuita{font-style:normal;margin-left:7px;opacity:.6}
|
|
415
|
+
.pista.malo{color:var(--carta)}
|
|
416
|
+
.demoRejilla{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));
|
|
417
|
+
gap:12px;margin:18px 0 14px}
|
|
418
|
+
.demoCarta{display:flex;flex-direction:column;gap:5px;text-align:left;cursor:pointer;
|
|
419
|
+
background:var(--suelo2);border:1px solid var(--linea2);border-radius:7px;
|
|
420
|
+
padding:15px 16px;box-shadow:var(--sombra)}
|
|
421
|
+
.demoCarta:hover{border-color:var(--lampara)}
|
|
422
|
+
.demoDominio{font:600 10px var(--hud);letter-spacing:.14em;text-transform:uppercase;
|
|
423
|
+
color:var(--lampara)}
|
|
424
|
+
.demoCarta b{font:600 15px/1.35 var(--hud);color:var(--tinta)}
|
|
425
|
+
.demoCiudad{font:12px var(--texto);color:var(--tinta2)}
|
|
426
|
+
.demoReparto{font:10px var(--mono);color:var(--tinta3);overflow:hidden;
|
|
427
|
+
white-space:nowrap;text-overflow:ellipsis}
|
|
428
|
+
.demoPlay{margin-top:6px;font:600 11px var(--mono);color:var(--tinta2)}
|
|
429
|
+
.demoCarta:hover .demoPlay{color:var(--lampara)}
|
|
430
|
+
.demoCab{display:flex;gap:14px;align-items:flex-start;margin-bottom:12px}
|
|
431
|
+
.demoCab h2{font:600 17px var(--hud);color:var(--tinta);margin:2px 0 0}
|
|
432
|
+
.demoMandos{display:flex;gap:9px;align-items:center;flex-wrap:wrap;margin-bottom:9px}
|
|
433
|
+
.demoVel{display:inline-flex;border:1px solid var(--linea2);border-radius:999px;
|
|
434
|
+
overflow:hidden;background:var(--suelo2)}
|
|
435
|
+
.demoVelBoton{border:0;background:none;color:var(--tinta3);font:11px var(--mono);
|
|
436
|
+
padding:5px 10px;cursor:pointer}
|
|
437
|
+
.demoVelBoton:hover{color:var(--tinta)}
|
|
438
|
+
.demoVelBoton.aqui{background:var(--burbuja-mia);color:var(--lampara)}
|
|
439
|
+
.demoCuenta{font:11px var(--mono);color:var(--tinta3);margin-left:auto}
|
|
440
|
+
.demoBarra{height:3px;background:var(--linea);border-radius:2px;overflow:hidden;
|
|
441
|
+
margin-bottom:12px}
|
|
442
|
+
.demoBarra i{display:block;height:100%;background:var(--lampara);
|
|
443
|
+
transition:width .35s ease}
|
|
444
|
+
.demoPlayer .liveLista{max-height:calc(100vh - 300px);border:1px solid var(--linea);
|
|
445
|
+
border-radius:7px;background:var(--panel);padding:16px}
|
|
361
446
|
.bvMotor{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-bottom:6px}
|
|
362
447
|
.bvMotor .campo{margin:0}
|
|
363
448
|
@media (max-width:640px){.bvMotor{grid-template-columns:1fr}}
|
|
@@ -456,7 +541,9 @@ body.enGuia #app{grid-template-columns:250px minmax(0,1fr)}
|
|
|
456
541
|
stroke-linecap:round;stroke-linejoin:round}
|
|
457
542
|
.liveContextText{min-width:0;color:var(--tinta3);font:10px/1.35 var(--hud)}
|
|
458
543
|
.liveContextText b{color:var(--tinta2);font-weight:600}
|
|
459
|
-
|
|
544
|
+
/* The demo player replays into a list of its own, and must look identical:
|
|
545
|
+
it is the same renderer, so it gets the same rules rather than a copy. */
|
|
546
|
+
#liveEvents,.liveLista{list-style:none;margin:0;padding:16px 16px 26px;overflow-y:auto;flex:1;
|
|
460
547
|
display:flex;flex-direction:column;gap:13px;scrollbar-color:var(--linea2) transparent}
|
|
461
548
|
.liveEmpty{margin:auto;padding:30px 18px;text-align:center;color:var(--tinta3);font-size:13px;line-height:1.55}
|
|
462
549
|
.liveTurn{--tono:var(--tinta3);display:grid;grid-template-columns:31px minmax(0,1fr);
|
package/bin/navegador.mjs
CHANGED
|
@@ -292,23 +292,32 @@ async function main() {
|
|
|
292
292
|
await new Promise(r => setTimeout(r, 900));
|
|
293
293
|
(await espera('[data-bv="nuevo"]'))?.click();
|
|
294
294
|
await new Promise(r => setTimeout(r, 400));
|
|
295
|
-
const busca = await espera('#bvBusca');
|
|
296
295
|
const motores = ['#bvRuntime', '#bvModelo', '#bvEsfuerzo'].map(s => !!document.querySelector(s));
|
|
297
296
|
const opcionesMotor = [...(document.querySelector('#bvModelo')?.options ?? [])].map(o => o.value);
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
297
|
+
// The picker walks: it must list a folder, and going into one must change
|
|
298
|
+
// where it says you are. Nothing is offered before you ask for it.
|
|
299
|
+
await espera('.exp .expLista .expFila');
|
|
300
|
+
const filas = document.querySelectorAll('.expFila').length;
|
|
301
|
+
// Picking a row records it — no scan happened, nothing was suggested.
|
|
302
|
+
document.querySelector('.expFila .expMas')?.click();
|
|
303
|
+
await new Promise(r => setTimeout(r, 300));
|
|
304
|
+
const elegidas = document.querySelectorAll('.expElegidas .bvChip').length;
|
|
305
|
+
// And going into a folder must change where it says you are.
|
|
306
|
+
const dondeAntes = document.querySelector('.expMigas')?.textContent ?? '';
|
|
307
|
+
[...document.querySelectorAll('.expFila.dir .expNombre')][0]?.click();
|
|
308
|
+
for (let i = 0; i < 40; i++) {
|
|
309
|
+
if ((document.querySelector('.expMigas')?.textContent ?? '') !== dondeAntes) break;
|
|
310
|
+
await new Promise(r => setTimeout(r, 250));
|
|
305
311
|
}
|
|
306
|
-
|
|
307
|
-
|
|
312
|
+
const dondeDespues = document.querySelector('.expMigas')?.textContent ?? '';
|
|
313
|
+
return { motores, opcionesMotor, filas, dondeAntes, dondeDespues, elegidas,
|
|
314
|
+
sube: !!document.querySelector('.expSubir'),
|
|
315
|
+
tomaAqui: !!document.querySelector('.expTomaAqui') };
|
|
308
316
|
})()`);
|
|
309
317
|
comprueba(
|
|
310
|
-
'the house form
|
|
311
|
-
|
|
318
|
+
'the house form walks the disk — nothing offered, you pick what you want',
|
|
319
|
+
(casa?.filas ?? 0) > 0 && !!casa?.sube && !!casa?.tomaAqui &&
|
|
320
|
+
casa.dondeDespues !== casa.dondeAntes && (casa?.elegidas ?? 0) > 0,
|
|
312
321
|
JSON.stringify(casa),
|
|
313
322
|
);
|
|
314
323
|
comprueba(
|
|
@@ -363,6 +372,126 @@ async function main() {
|
|
|
363
372
|
JSON.stringify(ciudades),
|
|
364
373
|
);
|
|
365
374
|
|
|
375
|
+
// The app's own dialogs. Every one of these was a `window.prompt` box, and
|
|
376
|
+
// a headless browser answers those with null — which is how three dead
|
|
377
|
+
// buttons could have passed a browser suite that only counted clicks.
|
|
378
|
+
const dlg = await cdp.evalua(`(async () => {
|
|
379
|
+
const espera = async (sel, n = 40) => {
|
|
380
|
+
for (let i = 0; i < n; i++) {
|
|
381
|
+
const el = document.querySelector(sel);
|
|
382
|
+
if (el) return el;
|
|
383
|
+
await new Promise(r => setTimeout(r, 250));
|
|
384
|
+
}
|
|
385
|
+
return null;
|
|
386
|
+
};
|
|
387
|
+
const nueva = document.getElementById('alWizard');
|
|
388
|
+
nueva?.click();
|
|
389
|
+
const caja = await espera('.dlgFondo .dlg');
|
|
390
|
+
if (!caja) return { falta: true };
|
|
391
|
+
const acepta = caja.querySelector('[data-dlg="si"]');
|
|
392
|
+
const campo = caja.querySelector('input[data-campo="name"]');
|
|
393
|
+
const bloqueado = !!acepta?.disabled;
|
|
394
|
+
if (campo) {
|
|
395
|
+
campo.value = 'x';
|
|
396
|
+
campo.dispatchEvent(new Event('input', { bubbles: true }));
|
|
397
|
+
await new Promise(r => setTimeout(r, 100));
|
|
398
|
+
}
|
|
399
|
+
const libre = !acepta?.disabled;
|
|
400
|
+
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
|
|
401
|
+
await new Promise(r => setTimeout(r, 200));
|
|
402
|
+
return {
|
|
403
|
+
titulo: caja.querySelector('h2')?.textContent?.trim() ?? '',
|
|
404
|
+
campo: !!campo, bloqueado, libre,
|
|
405
|
+
cerrado: !document.querySelector('.dlgFondo'),
|
|
406
|
+
};
|
|
407
|
+
})()`);
|
|
408
|
+
comprueba(
|
|
409
|
+
'creating a city asks in the app, not through a browser prompt box',
|
|
410
|
+
!!dlg && !dlg.falta && dlg.campo && dlg.bloqueado && dlg.libre && dlg.cerrado,
|
|
411
|
+
JSON.stringify(dlg),
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
// Building a house from the houses view: the guide's form, in a dialog.
|
|
415
|
+
const alta = await cdp.evalua(`(async () => {
|
|
416
|
+
const ir = [...document.querySelectorAll('nav li')].find(l => /houses|casas/i.test(l.textContent));
|
|
417
|
+
ir?.click();
|
|
418
|
+
await new Promise(r => setTimeout(r, 700));
|
|
419
|
+
document.getElementById('altaAgente')?.click();
|
|
420
|
+
for (let i = 0; i < 40; i++) {
|
|
421
|
+
if (document.querySelector('.dlgFondo .exp .expFila')) break;
|
|
422
|
+
await new Promise(r => setTimeout(r, 250));
|
|
423
|
+
}
|
|
424
|
+
const caja = document.querySelector('.dlgFondo .dlg');
|
|
425
|
+
const hay = s => !!caja?.querySelector(s);
|
|
426
|
+
const fuera = {
|
|
427
|
+
nombre: hay('#bvNombre'), clase: hay('[data-bv="clase"]'), rol: hay('#bvRol'),
|
|
428
|
+
motor: hay('#bvRuntime') && hay('#bvModelo') && hay('#bvEsfuerzo'),
|
|
429
|
+
explora: hay('.exp .expLista'),
|
|
430
|
+
};
|
|
431
|
+
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
|
|
432
|
+
await new Promise(r => setTimeout(r, 200));
|
|
433
|
+
return fuera;
|
|
434
|
+
})()`);
|
|
435
|
+
comprueba(
|
|
436
|
+
'and building a house opens the same form the guide uses, not three prompts',
|
|
437
|
+
!!alta && alta.nombre && alta.clase && alta.rol && alta.motor && alta.explora,
|
|
438
|
+
JSON.stringify(alta),
|
|
439
|
+
);
|
|
440
|
+
|
|
441
|
+
// The demo shelf: three cards, and a player that actually advances.
|
|
442
|
+
const demo = await cdp.evalua(`(async () => {
|
|
443
|
+
const espera = async (sel, n = 40) => {
|
|
444
|
+
for (let i = 0; i < n; i++) {
|
|
445
|
+
const el = document.querySelector(sel);
|
|
446
|
+
if (el) return el;
|
|
447
|
+
await new Promise(r => setTimeout(r, 250));
|
|
448
|
+
}
|
|
449
|
+
return null;
|
|
450
|
+
};
|
|
451
|
+
const ir = [...document.querySelectorAll('nav li')].find(l => /demos/i.test(l.textContent));
|
|
452
|
+
if (!ir) return { falta: true };
|
|
453
|
+
ir.click();
|
|
454
|
+
const carta = await espera('.demoCarta');
|
|
455
|
+
const cartas = document.querySelectorAll('.demoCarta').length;
|
|
456
|
+
carta?.click();
|
|
457
|
+
await espera('.demoPlayer');
|
|
458
|
+
// Fastest speed, so the check does not sit here for half a minute.
|
|
459
|
+
document.querySelector('[data-demo="vel"][data-ms="350"]')?.click();
|
|
460
|
+
await new Promise(r => setTimeout(r, 200));
|
|
461
|
+
const alPrincipio = document.querySelectorAll('#demoLista .liveTurn').length;
|
|
462
|
+
for (let i = 0; i < 60; i++) {
|
|
463
|
+
if (document.querySelectorAll('#demoLista .liveTurn').length > alPrincipio + 1) break;
|
|
464
|
+
await new Promise(r => setTimeout(r, 250));
|
|
465
|
+
}
|
|
466
|
+
const jugando = document.querySelectorAll('#demoLista .liveTurn').length;
|
|
467
|
+
document.querySelector('[data-demo="pausa"]')?.click();
|
|
468
|
+
await new Promise(r => setTimeout(r, 700));
|
|
469
|
+
const enPausa = document.querySelectorAll('#demoLista .liveTurn').length;
|
|
470
|
+
await new Promise(r => setTimeout(r, 700));
|
|
471
|
+
const sigueEnPausa = document.querySelectorAll('#demoLista .liveTurn').length;
|
|
472
|
+
document.querySelector('[data-demo="replay"]')?.click();
|
|
473
|
+
await new Promise(r => setTimeout(r, 150));
|
|
474
|
+
const trasReplay = document.querySelectorAll('#demoLista .liveTurn').length;
|
|
475
|
+
document.querySelector('[data-demo="pausa"]')?.click();
|
|
476
|
+
return { cartas, alPrincipio, jugando, enPausa, sigueEnPausa, trasReplay,
|
|
477
|
+
dice: (document.querySelector('.demoPlayer .pista')?.textContent ?? '') };
|
|
478
|
+
})()`);
|
|
479
|
+
comprueba(
|
|
480
|
+
'the demo shelf offers one story per domain and plays it turn by turn',
|
|
481
|
+
(demo?.cartas ?? 0) === 3 && demo.jugando > demo.alPrincipio,
|
|
482
|
+
JSON.stringify(demo),
|
|
483
|
+
);
|
|
484
|
+
comprueba(
|
|
485
|
+
'and pause holds, and replay starts it over',
|
|
486
|
+
demo?.sigueEnPausa === demo?.enPausa && (demo?.trasReplay ?? 99) < (demo?.enPausa ?? 0),
|
|
487
|
+
JSON.stringify(demo),
|
|
488
|
+
);
|
|
489
|
+
comprueba(
|
|
490
|
+
'and it says out loud that it is a recording, not a live committee',
|
|
491
|
+
/recording|grabaci/i.test(demo?.dice ?? ''),
|
|
492
|
+
JSON.stringify(demo?.dice),
|
|
493
|
+
);
|
|
494
|
+
|
|
366
495
|
// Spanish and English, on a switch. The READMEs shipped bilingual and the
|
|
367
496
|
// product did not; a language button that does not actually change the page
|
|
368
497
|
// is worse than none at all.
|