agents-city 0.3.0-beta.21 → 0.3.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 +310 -70
- package/README.md +297 -69
- package/bin/agents-city.js +3 -0
- package/bin/doctor +3 -0
- package/bin/hall.html +164 -24
- package/bin/navegador.mjs +415 -0
- package/bin/serve.py +383 -127
- package/bin/shortcut +3 -0
- package/bin/test +5 -2
- package/bin/test-actualiza.py +130 -0
- package/bin/test-atajos.py +301 -0
- package/bin/test-busca.py +216 -0
- package/bin/test-cage.py +170 -2
- package/bin/test-card.py +2 -2
- package/bin/test-cities.py +45 -0
- package/bin/test-contracts.py +12 -5
- package/bin/test-doctor.py +33 -0
- package/bin/test-navegador.py +164 -0
- package/bin/test-seat.py +245 -25
- package/bin/test-serve.py +214 -9
- package/bin/test-workspace.py +63 -0
- package/bin/testlib.py +23 -0
- package/bin/update +3 -0
- package/city/web/dist/city.js +47 -47
- package/city/web/dist/index.html +1 -1
- package/city/web/dist-hall/hall.js +2193 -174
- package/city/web/src/bienvenida.ts +686 -0
- package/city/web/src/es.ts +180 -0
- package/city/web/src/hall.ts +520 -168
- package/city/web/src/idioma.ts +86 -0
- package/city/web/src/main.ts +27 -0
- package/city/web/src/motores.ts +54 -0
- package/docs/agents-first.md +8 -1
- package/docs/security.md +46 -12
- package/docs/testing.md +1 -1
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/channel/bus.js +1 -1
- package/plugin/channel/bus.ts +1 -1
- package/plugin/channel/runtime/codex.ts +1 -1
- package/plugin/channel/runtime-gateway.js +1 -1
- package/plugin/scripts/actualiza.py +198 -0
- package/plugin/scripts/atajos.py +506 -0
- package/plugin/scripts/busca.py +436 -0
- package/plugin/scripts/cage.py +266 -26
- package/plugin/scripts/capabilities.py +17 -10
- package/plugin/scripts/card.py +10 -0
- package/plugin/scripts/cities.py +34 -0
- package/plugin/scripts/city-session.sh +33 -7
- package/plugin/scripts/doctor.py +122 -0
- package/plugin/scripts/find-repos.sh +12 -105
- package/plugin/scripts/read-card.py +6 -2
- package/plugin/scripts/report.py +5 -6
- package/plugin/scripts/reset.py +50 -14
- package/plugin/scripts/seat.py +445 -103
- package/plugin/scripts/workspace.py +197 -0
package/city/web/src/hall.ts
CHANGED
|
@@ -11,7 +11,17 @@
|
|
|
11
11
|
* ever serves this file.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import
|
|
14
|
+
import './es'; // the Spanish dictionary registers itself on import
|
|
15
|
+
import { idioma, plural, ponIdioma, t as _ } from './idioma';
|
|
16
|
+
import {
|
|
17
|
+
ESFUERZOS,
|
|
18
|
+
MODELOS,
|
|
19
|
+
NIVEL_ESFUERZO,
|
|
20
|
+
RUNTIMES,
|
|
21
|
+
nivelDeMotor,
|
|
22
|
+
opciones as opcionesDe,
|
|
23
|
+
} from './motores';
|
|
24
|
+
import { Bienvenida } from './bienvenida';
|
|
15
25
|
import {
|
|
16
26
|
ActivityEvent,
|
|
17
27
|
MAP_ACTIVITY_PROTOCOL,
|
|
@@ -64,6 +74,8 @@ interface Ciudad {
|
|
|
64
74
|
slug?: string;
|
|
65
75
|
id?: string;
|
|
66
76
|
actual: boolean;
|
|
77
|
+
/** How many agents live in it. An agent belongs to exactly one city. */
|
|
78
|
+
agentes?: number;
|
|
67
79
|
}
|
|
68
80
|
interface Road {
|
|
69
81
|
id: string;
|
|
@@ -105,7 +117,9 @@ interface FichaAgente {
|
|
|
105
117
|
avatar: string;
|
|
106
118
|
cli: { binary: string; installed: boolean; connected: boolean };
|
|
107
119
|
legacy: boolean;
|
|
108
|
-
|
|
120
|
+
/** What this agent actually works on — repos, worktrees, document folders.
|
|
121
|
+
* `fixed` is a legacy repo agent's own repo, which is not a mount it chose. */
|
|
122
|
+
mounts: { label: string; target: string; fixed?: boolean }[];
|
|
109
123
|
growth: { floors: number; bricks: number; activity30: number; signal: string };
|
|
110
124
|
}
|
|
111
125
|
interface Deliberation {
|
|
@@ -172,6 +186,8 @@ interface Estado {
|
|
|
172
186
|
agents?: FichaAgente[];
|
|
173
187
|
/** True only for the packaged Aurora demo: unlocks the replay controls. */
|
|
174
188
|
demo?: boolean;
|
|
189
|
+
/** One line when a newer release is published, '' otherwise. */
|
|
190
|
+
update?: string;
|
|
175
191
|
}
|
|
176
192
|
interface RolInfo {
|
|
177
193
|
id: string;
|
|
@@ -184,6 +200,16 @@ interface DomainInfo {
|
|
|
184
200
|
name: string;
|
|
185
201
|
summary: string;
|
|
186
202
|
}
|
|
203
|
+
/** What a reset would do, as the server computes it — never guessed here. */
|
|
204
|
+
interface Efectos {
|
|
205
|
+
city: string;
|
|
206
|
+
backup: string;
|
|
207
|
+
agents: number;
|
|
208
|
+
roads: number;
|
|
209
|
+
deliberations: number;
|
|
210
|
+
keeps: string[];
|
|
211
|
+
loses: string[];
|
|
212
|
+
}
|
|
187
213
|
interface RepoLocal {
|
|
188
214
|
nombre: string;
|
|
189
215
|
ruta: string;
|
|
@@ -260,15 +286,35 @@ const SECCIONES: Array<[string, string]> = [
|
|
|
260
286
|
['resumen', 'Overview'],
|
|
261
287
|
['mapa', 'The map'],
|
|
262
288
|
['puesto', 'My seat'],
|
|
263
|
-
['barrios', 'Districts
|
|
289
|
+
['barrios', 'Districts'],
|
|
264
290
|
['red', 'Roads'],
|
|
265
291
|
['committee', 'Committee'],
|
|
266
|
-
['gente', '
|
|
292
|
+
['gente', 'Houses'],
|
|
293
|
+
['ciudades', 'Cities'],
|
|
267
294
|
];
|
|
268
295
|
|
|
296
|
+
/** True until the person navigates: the landing section is decided once, from
|
|
297
|
+
* the state, and never yanked out from under them afterwards. */
|
|
298
|
+
let primeraCarga = true;
|
|
299
|
+
|
|
269
300
|
async function refresca(): Promise<void> {
|
|
270
301
|
E = await api<Estado>('/api/estado');
|
|
302
|
+
if (primeraCarga) {
|
|
303
|
+
primeraCarga = false;
|
|
304
|
+
// A city with no seat or no agents has nothing to draw, and "Not drawn
|
|
305
|
+
// yet" is a dead end as a first impression. Start where the next step is.
|
|
306
|
+
const mia = E.tarjetas.find((t) => t.user === E.yo);
|
|
307
|
+
// Never land on a dead end. Three cases, in order of what the person needs:
|
|
308
|
+
// nothing set up -> the guide, which asks the questions
|
|
309
|
+
// set up, no map yet -> the overview, which says what is left and how
|
|
310
|
+
// set up, map running -> the map, which is the point of the product
|
|
311
|
+
// Landing on the map view without a map was how "Not drawn yet" became the
|
|
312
|
+
// first thing an owner saw every single morning.
|
|
313
|
+
if (!mia || (E.agents ?? []).length === 0) SECCION = 'bienvenida';
|
|
314
|
+
else if (!E.mapa) SECCION = 'resumen';
|
|
315
|
+
}
|
|
271
316
|
pinta();
|
|
317
|
+
pintaAvisoDeVersion();
|
|
272
318
|
pintaDemoControles();
|
|
273
319
|
sincronizaActividad(E.live_bus);
|
|
274
320
|
}
|
|
@@ -279,6 +325,16 @@ async function refresca(): Promise<void> {
|
|
|
279
325
|
* whose map was still baking missed it — these buttons are the way back. Real
|
|
280
326
|
* cities never see them, and the server refuses them anyway.
|
|
281
327
|
*/
|
|
328
|
+
/** A newer release, said once and quietly: the server already decided whether
|
|
329
|
+
* there is one (cached for a day), so this only renders what it was told. */
|
|
330
|
+
function pintaAvisoDeVersion(): void {
|
|
331
|
+
const caja = document.getElementById('avisoVersion');
|
|
332
|
+
if (!caja) return;
|
|
333
|
+
const texto = (E.update ?? '').trim();
|
|
334
|
+
caja.textContent = texto;
|
|
335
|
+
caja.hidden = !texto;
|
|
336
|
+
}
|
|
337
|
+
|
|
282
338
|
function pintaDemoControles(): void {
|
|
283
339
|
const header = document.querySelector<HTMLElement>('#livePanel header');
|
|
284
340
|
const previa = document.getElementById('demoControles');
|
|
@@ -692,22 +748,36 @@ function icono(kind: string): string {
|
|
|
692
748
|
}
|
|
693
749
|
|
|
694
750
|
function rail(): void {
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
751
|
+
// Everything above the divider belongs to ONE city — its seat, its agents,
|
|
752
|
+
// its map. Saying so here is the difference between a menu and a place: an
|
|
753
|
+
// agent's workspace and mounts live inside this city's folder, and a flat
|
|
754
|
+
// list of "Cities" and "Agents" as siblings hid that completely.
|
|
755
|
+
const cuantos = (E.agents ?? []).length;
|
|
756
|
+
q('#railCiudad').innerHTML = `<span class="railEtiqueta">${_('you are in')}</span>
|
|
757
|
+
<b>${esc(E.city_name)}</b>
|
|
758
|
+
<span class="railDe">${esc(E.domain)} · ${plural(cuantos, '{n} house', '{n} houses')}</span>`;
|
|
759
|
+
q('#rail').innerHTML = SECCIONES.filter(([id]) => id !== 'ciudades')
|
|
760
|
+
.map(([id, et]) => {
|
|
761
|
+
const n =
|
|
762
|
+
id === 'gente'
|
|
763
|
+
? String(Object.keys(E.skills).length)
|
|
764
|
+
: id === 'committee'
|
|
765
|
+
? String(E.deliberations.length)
|
|
766
|
+
: id === 'red'
|
|
767
|
+
? String(E.roads.length)
|
|
768
|
+
: id === 'barrios'
|
|
769
|
+
? String(E.parcelas.length)
|
|
770
|
+
: '';
|
|
771
|
+
return `<li class="${id === SECCION ? 'aqui' : ''}" data-s="${id}">
|
|
772
|
+
<span>${esc(_(et))}</span>${n ? `<span class="n">${n}</span>` : ''}</li>`;
|
|
773
|
+
})
|
|
774
|
+
.join('');
|
|
775
|
+
q('#railOtras').innerHTML =
|
|
776
|
+
`<li class="${SECCION === 'ciudades' ? 'aqui' : ''}" data-s="ciudades">
|
|
777
|
+
<span>${_('All cities')}</span><span class="n">${E.ciudades.length}</span></li>`;
|
|
778
|
+
todos<HTMLElement>('#rail li,#railOtras li').forEach((li) => {
|
|
710
779
|
li.onclick = () => {
|
|
780
|
+
document.body.classList.remove('enGuia');
|
|
711
781
|
SECCION = li.dataset.s ?? 'mapa';
|
|
712
782
|
pinta();
|
|
713
783
|
};
|
|
@@ -754,17 +824,185 @@ function pinta(): void {
|
|
|
754
824
|
const VISTAS: Record<string, () => void> = {};
|
|
755
825
|
|
|
756
826
|
// ── overview ─────────────────────────────────────────────────────────────────
|
|
827
|
+
/** The guided first run. Built once and kept, so a half-finished answer
|
|
828
|
+
* survives a repaint. */
|
|
829
|
+
let guia: Bienvenida | null = null;
|
|
830
|
+
|
|
831
|
+
VISTAS.bienvenida = () => {
|
|
832
|
+
document.body.classList.add('enGuia');
|
|
833
|
+
guia ??= new Bienvenida({
|
|
834
|
+
api,
|
|
835
|
+
esc,
|
|
836
|
+
aviso: toast,
|
|
837
|
+
refresca,
|
|
838
|
+
vete: (seccion: string) => {
|
|
839
|
+
document.body.classList.remove('enGuia');
|
|
840
|
+
SECCION = seccion;
|
|
841
|
+
pinta();
|
|
842
|
+
},
|
|
843
|
+
ciudad: E.city_name,
|
|
844
|
+
yo: E.yo,
|
|
845
|
+
datos: corto(E.datos),
|
|
846
|
+
});
|
|
847
|
+
void guia.pinta(q<HTMLElement>('#lienzo'));
|
|
848
|
+
};
|
|
849
|
+
|
|
850
|
+
VISTAS.ciudades = () => {
|
|
851
|
+
const filas = E.ciudades
|
|
852
|
+
.map(
|
|
853
|
+
(c) => `
|
|
854
|
+
<div class="fila ${c.actual ? 'on' : ''}">
|
|
855
|
+
<span class="et">${esc(c.nombre)}${c.actual ? ' <b class="de">· open now</b>' : ''}</span>
|
|
856
|
+
<span class="de">${c.agentes ?? 0} house${(c.agentes ?? 0) === 1 ? '' : 's'} of its own</span>
|
|
857
|
+
<span class="de mono">${esc(corto(c.ruta))}</span>
|
|
858
|
+
<span style="margin-left:auto;display:flex;gap:6px">
|
|
859
|
+
${
|
|
860
|
+
c.actual
|
|
861
|
+
? ''
|
|
862
|
+
: `<a class="bt mini" href="?city=${encodeURIComponent(c.ruta)}">open</a>
|
|
863
|
+
<button class="bt mini archiva" data-ciudad="${esc(c.ruta)}"
|
|
864
|
+
data-nombre="${esc(c.nombre)}">archive</button>`
|
|
865
|
+
}
|
|
866
|
+
</span>
|
|
867
|
+
</div>`,
|
|
868
|
+
)
|
|
869
|
+
.join('');
|
|
870
|
+
q('#lienzo').innerHTML = `<div><span class="sub">cities</span>
|
|
871
|
+
<h1 style="margin-top:6px">${_('Your cities')}</h1>
|
|
872
|
+
<p class="prosa" style="margin-top:8px">One person, several autonomous cities: each has
|
|
873
|
+
its own identity, domain, chair, <b>its own houses</b> and its own roads. A house is not
|
|
874
|
+
shared — it stands inside the city that owns it, with its workspace and mounts under that
|
|
875
|
+
city's folder — so two cities can each have a <code class="mono">docs</code> house and
|
|
876
|
+
they are two different workers. They share nothing unless you build a road between
|
|
877
|
+
them.</p></div>
|
|
878
|
+
<div class="lista"><div class="filas">${filas}</div></div>
|
|
879
|
+
<div class="campos" style="max-width:420px">
|
|
880
|
+
<div class="campo"><label>${_('Start another city')}</label>
|
|
881
|
+
<input type="text" id="nuevaCiudad" placeholder="client-a, research, the book…"></div>
|
|
882
|
+
<button class="bt ppal" id="creaCiudad">Create it</button>
|
|
883
|
+
</div>
|
|
884
|
+
<div class="peligro">
|
|
885
|
+
<h3>${_('Start this city over')}</h3>
|
|
886
|
+
<p class="prosa">Takes <b>${esc(E.city_name)}</b> back to its first day: no seat, no
|
|
887
|
+
agents, no committee history, no map. Your repositories and document folders are
|
|
888
|
+
<b>never touched</b> — only the city that points at them.</p>
|
|
889
|
+
<button class="bt malo" id="reiniciaCiudad">${_('Start over…')}</button>
|
|
890
|
+
</div>
|
|
891
|
+
<p class="pista">Archiving <b>moves</b> a city into
|
|
892
|
+
<code class="mono">.backups/</code> — nothing is deleted, and you can bring it back with
|
|
893
|
+
<code class="mono">mv</code>. The city you are standing in, and the last one you own,
|
|
894
|
+
cannot be archived.</p>`;
|
|
895
|
+
enlaza();
|
|
896
|
+
const campo = q<HTMLInputElement>('#nuevaCiudad');
|
|
897
|
+
q<HTMLButtonElement>('#creaCiudad').onclick = async () => {
|
|
898
|
+
const nombre = campo.value.trim();
|
|
899
|
+
if (!nombre) {
|
|
900
|
+
toast('Give it a name', true);
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
const r = await api<Respuesta>('/api/ciudades', {
|
|
904
|
+
method: 'POST',
|
|
905
|
+
body: JSON.stringify({ name: nombre }),
|
|
906
|
+
});
|
|
907
|
+
if (!r.ok) {
|
|
908
|
+
toast(r.error ?? 'Could not create it', true);
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
toast(`${nombre} created`);
|
|
912
|
+
await refresca();
|
|
913
|
+
};
|
|
914
|
+
const reiniciar = document.getElementById('reiniciaCiudad');
|
|
915
|
+
if (reiniciar)
|
|
916
|
+
reiniciar.onclick = async () => {
|
|
917
|
+
// Two round-trips on purpose: the first asks the server what this would
|
|
918
|
+
// actually do, and the person reads it before typing anything. "Are you
|
|
919
|
+
// sure?" is not information; a list of what disappears is.
|
|
920
|
+
const previa = await api<{ preview?: Efectos; error?: string }>('/api/ciudad-reinicia', {
|
|
921
|
+
method: 'POST',
|
|
922
|
+
body: '{}',
|
|
923
|
+
});
|
|
924
|
+
if (!previa.preview) {
|
|
925
|
+
toast(previa.error ?? 'Could not read what a reset would do', true);
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
const p = previa.preview;
|
|
929
|
+
const texto =
|
|
930
|
+
`Start over ${p.city}?\n\nYou lose:\n` +
|
|
931
|
+
p.loses.map((l) => ` · ${l}`).join('\n') +
|
|
932
|
+
`\n\nRight now that is ${p.agents} agent(s), ${p.roads} road(s) and ` +
|
|
933
|
+
`${p.deliberations} committee act(s).\n\nYou keep:\n` +
|
|
934
|
+
p.keeps.map((l) => ` · ${l}`).join('\n') +
|
|
935
|
+
`\n\nType the city's name to confirm:`;
|
|
936
|
+
const escrito = window.prompt(texto, '');
|
|
937
|
+
if (escrito === null) return;
|
|
938
|
+
if (escrito.trim() !== p.city) {
|
|
939
|
+
toast('That is not the name — nothing was changed', true);
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
const r = await api<{ ok?: boolean; backup?: string; error?: string }>(
|
|
943
|
+
'/api/ciudad-reinicia',
|
|
944
|
+
{ method: 'POST', body: JSON.stringify({ confirm: p.city }) },
|
|
945
|
+
);
|
|
946
|
+
if (!r.ok) {
|
|
947
|
+
toast(r.error ?? 'Could not reset it', true);
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
toast(`${p.city} starts over — the old one is at ${r.backup ?? 'its backup'}`);
|
|
951
|
+
primeraCarga = true; // land wherever a fresh city should land: the guide
|
|
952
|
+
await refresca();
|
|
953
|
+
};
|
|
954
|
+
todos<HTMLButtonElement>('.archiva').forEach((boton) => {
|
|
955
|
+
boton.onclick = async () => {
|
|
956
|
+
const nombre = boton.dataset.nombre ?? '';
|
|
957
|
+
if (!window.confirm(`Archive ${nombre}? It moves into .backups — nothing is deleted.`))
|
|
958
|
+
return;
|
|
959
|
+
const r = await api<{ ok?: boolean; backup?: string; error?: string }>(
|
|
960
|
+
'/api/ciudad-archiva',
|
|
961
|
+
{ method: 'POST', body: JSON.stringify({ city: boton.dataset.ciudad ?? '' }) },
|
|
962
|
+
);
|
|
963
|
+
if (!r.ok) {
|
|
964
|
+
toast(r.error ?? 'Could not archive it', true);
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
toast(`${nombre} archived — it is in ${r.backup ?? '.backups'}`);
|
|
968
|
+
await refresca();
|
|
969
|
+
};
|
|
970
|
+
});
|
|
971
|
+
};
|
|
972
|
+
|
|
757
973
|
VISTAS.resumen = () => {
|
|
758
974
|
const mia = E.tarjetas.find((t) => t.user === E.yo);
|
|
759
975
|
const sinUnidad = E.parcelas.filter((p) => p.unidad === 'none' || p.unidad === 'mine').length;
|
|
976
|
+
const agentes = E.agents ?? [];
|
|
977
|
+
// The order somebody actually does it in: take the chair, say who works
|
|
978
|
+
// here, say what for. The old list asked for "folders" — the vocabulary of
|
|
979
|
+
// the model before agents existed — and never mentioned the roster at all,
|
|
980
|
+
// so a new city could look finished with nobody in it.
|
|
760
981
|
const tareas = [
|
|
761
982
|
{
|
|
762
983
|
ok: !!mia,
|
|
763
984
|
txt: mia
|
|
764
|
-
? `Your
|
|
765
|
-
: 'Take your seat: role
|
|
985
|
+
? `Your seat is taken — you are <b>${esc(mia.agent)}</b> in <b>${esc(E.domain)}</b>`
|
|
986
|
+
: 'Take your seat: the work domain and your role in it',
|
|
766
987
|
ir: 'puesto',
|
|
767
988
|
},
|
|
989
|
+
{
|
|
990
|
+
ok: agentes.length > 0,
|
|
991
|
+
txt:
|
|
992
|
+
agentes.length === 0
|
|
993
|
+
? 'Build the <b>houses</b> of this city — one per worker, each whole: its kind, its role, and everything it works on'
|
|
994
|
+
: `<b>${agentes.length}</b> house${agentes.length === 1 ? '' : 's'} in this city: ${esc(
|
|
995
|
+
agentes.map((a) => a.name).join(', '),
|
|
996
|
+
)}`,
|
|
997
|
+
ir: 'gente',
|
|
998
|
+
},
|
|
999
|
+
{
|
|
1000
|
+
ok: agentes.length === 0 || agentes.some((a) => (a.mounts ?? []).length),
|
|
1001
|
+
txt: agentes.some((a) => (a.mounts ?? []).length)
|
|
1002
|
+
? 'Every house works on something real'
|
|
1003
|
+
: 'Give a house something to work on — a repository, a worktree, a folder of documents',
|
|
1004
|
+
ir: 'gente',
|
|
1005
|
+
},
|
|
768
1006
|
{
|
|
769
1007
|
ok: !!(mia && mia.goals_defined),
|
|
770
1008
|
txt:
|
|
@@ -774,23 +1012,33 @@ VISTAS.resumen = () => {
|
|
|
774
1012
|
ir: 'puesto',
|
|
775
1013
|
},
|
|
776
1014
|
{
|
|
777
|
-
ok: E.parcelas.length
|
|
1015
|
+
ok: E.parcelas.length === 0 || sinUnidad === 0,
|
|
778
1016
|
txt:
|
|
779
1017
|
E.parcelas.length === 0
|
|
780
|
-
? '
|
|
1018
|
+
? 'Districts appear on the map once agents have houses'
|
|
781
1019
|
: sinUnidad
|
|
782
1020
|
? `<b>${sinUnidad}</b> of ${E.parcelas.length} houses sit in no real district — assign them`
|
|
783
1021
|
: 'Every house has its district',
|
|
784
1022
|
ir: 'barrios',
|
|
785
1023
|
},
|
|
786
1024
|
];
|
|
1025
|
+
const empezando = !mia || agentes.length === 0;
|
|
787
1026
|
const sesionArriba = E.tmux.includes(E.sesion);
|
|
788
1027
|
q('#lienzo').innerHTML = `
|
|
789
|
-
<div><span class="sub"
|
|
790
|
-
<h1 style="margin-top:6px">${esc(E.city_name)}</h1>
|
|
1028
|
+
<div><span class="sub">${empezando ? 'welcome' : 'your city'}</span>
|
|
1029
|
+
<h1 style="margin-top:6px">${empezando ? 'Let’s build ' + esc(E.city_name) : esc(E.city_name)}</h1>
|
|
791
1030
|
<p class="de mono" style="margin-top:5px">${esc(E.address)}</p>
|
|
792
|
-
|
|
793
|
-
|
|
1031
|
+
${
|
|
1032
|
+
empezando
|
|
1033
|
+
? `<p class="prosa" style="margin-top:8px">A city is a chair and the agents behind
|
|
1034
|
+
it. Two steps and it is alive: take your seat — the work domain and your role in
|
|
1035
|
+
it — then add the agents, each one asked for whole: what kind of work it does,
|
|
1036
|
+
its role, everything it works on, the engine that runs it and the skills it
|
|
1037
|
+
starts with. Everything below is a plain file in
|
|
1038
|
+
<code class="mono">${esc(corto(E.datos))}</code> you can also edit by hand.</p>`
|
|
1039
|
+
: `<p class="prosa" style="margin-top:8px">Domain: <b>${esc(E.domain)}</b>. Growth is
|
|
1040
|
+
counted with <code class="mono">${esc(E.grow || 'nothing yet')}</code>.</p>`
|
|
1041
|
+
}</div>
|
|
794
1042
|
<div class="cifras">
|
|
795
1043
|
<div class="cifra"><b>${Object.keys(E.skills).length}</b><span>repo agents</span></div>
|
|
796
1044
|
<div class="cifra"><b>${E.parcelas.length}</b><span>houses</span></div>
|
|
@@ -882,6 +1130,12 @@ VISTAS.mapa = () => {
|
|
|
882
1130
|
},
|
|
883
1131
|
origin,
|
|
884
1132
|
);
|
|
1133
|
+
// The map loads after the theme was decided, so tell it now as well —
|
|
1134
|
+
// otherwise a light Hall frames a night map until the next toggle.
|
|
1135
|
+
frame.contentWindow?.postMessage(
|
|
1136
|
+
{ type: 'agents-city-map-theme/1', theme: temaActual() },
|
|
1137
|
+
origin,
|
|
1138
|
+
);
|
|
885
1139
|
if (pendingMapActivity) {
|
|
886
1140
|
const event = pendingMapActivity;
|
|
887
1141
|
pendingMapActivity = null;
|
|
@@ -890,12 +1144,23 @@ VISTAS.mapa = () => {
|
|
|
890
1144
|
};
|
|
891
1145
|
return;
|
|
892
1146
|
}
|
|
1147
|
+
const cuantos = (E.agents ?? []).length;
|
|
893
1148
|
q('#lienzo').innerHTML = `<div><span class="sub">the map</span>
|
|
894
|
-
<h1 style="margin-top:6px">
|
|
895
|
-
<p class="prosa" style="margin-top:8px"
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
1149
|
+
<h1 style="margin-top:6px">Your city, drawn</h1>
|
|
1150
|
+
<p class="prosa" style="margin-top:8px">${
|
|
1151
|
+
cuantos
|
|
1152
|
+
? `<b>${cuantos}</b> agent${cuantos === 1 ? '' : 's'} live here, and the map shows
|
|
1153
|
+
them as houses that grow with the work they do — with the town hall in the middle
|
|
1154
|
+
when a committee is sitting, and a gate for every road out.`
|
|
1155
|
+
: `The map draws one house per agent. There are none yet, so it would be an empty
|
|
1156
|
+
plot — build one from <b>Houses</b> first.`
|
|
1157
|
+
}</p>
|
|
1158
|
+
<p class="prosa">The map is its own little server, so it is not running until you say so.
|
|
1159
|
+
The first time takes about a minute: it builds the front end and seeds a local database.
|
|
1160
|
+
After that it opens instantly, and it is yours — nothing is uploaded anywhere.</p></div>
|
|
1161
|
+
<div><button class="bt ppal" id="arrancaMapa">${
|
|
1162
|
+
cuantos ? 'Draw my city' : 'Draw it anyway'
|
|
1163
|
+
}</button></div>
|
|
899
1164
|
<p class="cargando" id="mapaEspera" style="display:none">baking the map — first time takes a minute</p>`;
|
|
900
1165
|
q<HTMLButtonElement>('#arrancaMapa').onclick = async () => {
|
|
901
1166
|
q<HTMLButtonElement>('#arrancaMapa').disabled = true;
|
|
@@ -923,22 +1188,21 @@ async function puesto(): Promise<void> {
|
|
|
923
1188
|
const mia = E.tarjetas.find((t) => t.user === E.yo);
|
|
924
1189
|
q('#lienzo').innerHTML = `<div><span class="sub">my seat</span>
|
|
925
1190
|
<h1 style="margin-top:6px">${esc(E.yo)}</h1>
|
|
926
|
-
<p class="prosa" style="margin-top:8px">
|
|
927
|
-
|
|
928
|
-
|
|
1191
|
+
<p class="prosa" style="margin-top:8px">This is your chair: the work domain, your
|
|
1192
|
+
role inside it, and one goal. The seat stays the boss even when its
|
|
1193
|
+
professional role is blank. Saving writes your card —
|
|
929
1194
|
<code class="mono">${esc(corto(E.datos))}/${esc(E.yo)}.md</code> — the same file
|
|
930
|
-
every other door writes.</p></div>
|
|
1195
|
+
every other door writes, and it never touches your roster.</p></div>
|
|
931
1196
|
<div><span class="sub">work domain</span><div class="rolejilla" id="domains" style="margin-top:9px">
|
|
932
1197
|
<p class="cargando">reading the domain packs</p></div></div>
|
|
933
1198
|
<div><span class="sub">role</span><div class="rolejilla" id="roles" style="margin-top:9px">
|
|
934
1199
|
<p class="cargando">reading the role files</p></div></div>
|
|
935
|
-
<div><span class="sub">
|
|
936
|
-
<p class="
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
<div id="agentRoles" style="margin-top:9px"></div></div>
|
|
1200
|
+
<div><span class="sub">the agents</span>
|
|
1201
|
+
<p class="prosa" style="margin-top:8px">Who works in this city — and what each
|
|
1202
|
+
one works on — lives in <b>Agents & skills</b>, where an agent is asked for
|
|
1203
|
+
whole: its kind, its role, its repositories and document folders, its engine
|
|
1204
|
+
and its skills. One place, so the terminal and this page cannot disagree.
|
|
1205
|
+
<button class="bt" type="button" data-ir="gente" style="margin-left:8px">Open the roster</button></p></div>
|
|
942
1206
|
<div><span class="sub">one goal — optional</span><div class="campos" id="meta" style="margin-top:9px"></div></div>
|
|
943
1207
|
<div style="display:flex;gap:10px;align-items:center">
|
|
944
1208
|
<button class="bt ppal" id="guardaPuesto">Save my seat</button>
|
|
@@ -992,84 +1256,9 @@ async function puesto(): Promise<void> {
|
|
|
992
1256
|
pintaDominios();
|
|
993
1257
|
await cargaRoles();
|
|
994
1258
|
|
|
995
|
-
//
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
api<{ roles: RepoRoleOption[] }>('/api/roles?scope=agent'),
|
|
999
|
-
]);
|
|
1000
|
-
let { repos } = reposResponse;
|
|
1001
|
-
const agentRoleOptions = agentRolesResponse.roles;
|
|
1002
|
-
const sel = new Set<string>(
|
|
1003
|
-
mia && mia.repos.length ? mia.repos : repos.filter((r) => r.mio).map((r) => r.nombre),
|
|
1004
|
-
);
|
|
1005
|
-
const assignedRoles: Record<string, string> = { ...(mia?.repo_roles || {}) };
|
|
1006
|
-
const pintaRolesAgentes = (): void => {
|
|
1007
|
-
q('#agentRoles').innerHTML = renderRepoRoleEditor(sel, assignedRoles, agentRoleOptions, esc);
|
|
1008
|
-
todos<HTMLSelectElement>('#agentRoles [data-repo-role]').forEach((select) => {
|
|
1009
|
-
select.onchange = () => {
|
|
1010
|
-
assignedRoles[select.dataset.repoRole ?? ''] = select.value || 'blank';
|
|
1011
|
-
};
|
|
1012
|
-
});
|
|
1013
|
-
};
|
|
1014
|
-
let filtro = '';
|
|
1015
|
-
const pintaCarpetas = (): void => {
|
|
1016
|
-
const vis = repos.filter((r) => !filtro || r.nombre.toLowerCase().includes(filtro));
|
|
1017
|
-
q('#carpetas').innerHTML = `<div class="lista">
|
|
1018
|
-
<div class="barra">
|
|
1019
|
-
<input class="mono" id="fl" placeholder="filter ${repos.length} repos…" value="${esc(filtro)}">
|
|
1020
|
-
<button class="mini2" id="rescan" title="the index caches for a day — this rebuilds it now">rescan disk</button>
|
|
1021
|
-
<button class="mini2" id="nada">none</button>
|
|
1022
|
-
<span class="de" style="font-family:var(--mono);font-size:11px;color:var(--tinta3)">${sel.size} picked</span>
|
|
1023
|
-
</div>
|
|
1024
|
-
<div class="filas">${
|
|
1025
|
-
vis
|
|
1026
|
-
.map(
|
|
1027
|
-
(r) => `
|
|
1028
|
-
<div class="fila ${sel.has(r.nombre) ? 'on' : ''}" data-n="${esc(r.nombre)}">
|
|
1029
|
-
<span class="caja">${sel.has(r.nombre) ? '[x]' : '[ ]'}</span>
|
|
1030
|
-
<span class="et">${esc(r.nombre)}</span>
|
|
1031
|
-
<span class="de">${esc(r.cuando || '—')} ${esc(corto(r.ruta))}</span></div>`,
|
|
1032
|
-
)
|
|
1033
|
-
.join('') ||
|
|
1034
|
-
'<div class="fila"><span class="de" style="margin:0">nothing matches</span></div>'
|
|
1035
|
-
}
|
|
1036
|
-
</div></div>
|
|
1037
|
-
<p class="pista" style="margin-top:6px;font-size:11.5px;color:var(--tinta3);font-style:italic">
|
|
1038
|
-
Ticked by your commits this year. Owning <b>no</b> folders is a real answer for a
|
|
1039
|
-
cross-cutting role — your city is simply empty, and your work is the roads.</p>`;
|
|
1040
|
-
const fl = q<HTMLInputElement>('#fl');
|
|
1041
|
-
fl.oninput = () => {
|
|
1042
|
-
filtro = fl.value.toLowerCase();
|
|
1043
|
-
pintaCarpetas();
|
|
1044
|
-
const f2 = q<HTMLInputElement>('#fl');
|
|
1045
|
-
f2.focus();
|
|
1046
|
-
f2.setSelectionRange(f2.value.length, f2.value.length);
|
|
1047
|
-
};
|
|
1048
|
-
q<HTMLButtonElement>('#rescan').onclick = async () => {
|
|
1049
|
-
const b = q<HTMLButtonElement>('#rescan');
|
|
1050
|
-
b.textContent = 'scanning…';
|
|
1051
|
-
b.disabled = true;
|
|
1052
|
-
({ repos } = await api<{ repos: RepoLocal[] }>(
|
|
1053
|
-
'/api/misrepos?refresh=1&user=' + encodeURIComponent(E.yo),
|
|
1054
|
-
));
|
|
1055
|
-
pintaCarpetas();
|
|
1056
|
-
toast(`${repos.length} repos on this disk`);
|
|
1057
|
-
};
|
|
1058
|
-
q<HTMLButtonElement>('#nada').onclick = () => {
|
|
1059
|
-
vis.forEach((r) => sel.delete(r.nombre));
|
|
1060
|
-
pintaCarpetas();
|
|
1061
|
-
};
|
|
1062
|
-
todos<HTMLElement>('#carpetas .fila[data-n]').forEach((f) => {
|
|
1063
|
-
f.onclick = () => {
|
|
1064
|
-
const n = f.dataset.n ?? '';
|
|
1065
|
-
if (sel.has(n)) sel.delete(n);
|
|
1066
|
-
else sel.add(n);
|
|
1067
|
-
pintaCarpetas();
|
|
1068
|
-
};
|
|
1069
|
-
});
|
|
1070
|
-
pintaRolesAgentes();
|
|
1071
|
-
};
|
|
1072
|
-
pintaCarpetas();
|
|
1259
|
+
// The roster is not asked for here any more: it lives in one place,
|
|
1260
|
+
// the agents view, so this page and the terminal cannot write different
|
|
1261
|
+
// cities. What this page owns is the chair — domain, role, goal.
|
|
1073
1262
|
|
|
1074
1263
|
// goal
|
|
1075
1264
|
const o: Partial<Objetivo> = (mia && mia.objetivo) || {};
|
|
@@ -1111,8 +1300,6 @@ async function puesto(): Promise<void> {
|
|
|
1111
1300
|
user: E.yo,
|
|
1112
1301
|
domain,
|
|
1113
1302
|
role: rol,
|
|
1114
|
-
repos: [...sel],
|
|
1115
|
-
repo_roles: selectedRepoRoles(sel, assignedRoles),
|
|
1116
1303
|
objetivo: {
|
|
1117
1304
|
title: val('#g_title'),
|
|
1118
1305
|
signal: val('#g_signal'),
|
|
@@ -1417,36 +1604,9 @@ VISTAS.committee = () => {
|
|
|
1417
1604
|
};
|
|
1418
1605
|
|
|
1419
1606
|
// ── agents and skills ────────────────────────────────────────────────────────
|
|
1420
|
-
/** The
|
|
1421
|
-
* the owner's Claude default — unknown here, so the bar says "default" rather
|
|
1422
|
-
* than inventing a number. */
|
|
1423
|
-
function nivelDeMotor(model: string): { ancho: number; texto: string; defecto: boolean } {
|
|
1424
|
-
const alias = model.toLowerCase();
|
|
1425
|
-
if (!alias) return { ancho: 0.5, texto: 'default', defecto: true };
|
|
1426
|
-
for (const [clave, ancho] of [
|
|
1427
|
-
['fable', 1],
|
|
1428
|
-
['opus', 0.8],
|
|
1429
|
-
['sonnet', 0.55],
|
|
1430
|
-
['haiku', 0.35],
|
|
1431
|
-
] as const) {
|
|
1432
|
-
if (alias.includes(clave)) return { ancho, texto: alias, defecto: false };
|
|
1433
|
-
}
|
|
1434
|
-
return { ancho: 0.6, texto: alias, defecto: false };
|
|
1435
|
-
}
|
|
1436
|
-
|
|
1437
|
-
const NIVEL_ESFUERZO: Record<string, number> = { low: 1, medium: 2, high: 3, xhigh: 4, max: 5 };
|
|
1438
|
-
const MODELOS = ['haiku', 'sonnet', 'opus', 'fable'];
|
|
1439
|
-
|
|
1440
|
-
/** `<option>`s from known values plus the current one when it is off the list:
|
|
1441
|
-
* a card key must never be held hostage by a curated dropdown. */
|
|
1607
|
+
/** The shared `<option>` builder, with this page's escaper bound in. */
|
|
1442
1608
|
function opciones(valores: string[], actual: string): string {
|
|
1443
|
-
return valores
|
|
1444
|
-
.concat(actual && !valores.includes(actual) ? [actual] : [])
|
|
1445
|
-
.map(
|
|
1446
|
-
(v) =>
|
|
1447
|
-
`<option value="${esc(v)}"${v === actual ? ' selected' : ''}>${esc(v || 'default')}</option>`,
|
|
1448
|
-
)
|
|
1449
|
-
.join('');
|
|
1609
|
+
return opcionesDe(valores, actual, esc);
|
|
1450
1610
|
}
|
|
1451
1611
|
|
|
1452
1612
|
/** The one sheet-avatar guard: a data URI or nothing. */
|
|
@@ -1476,15 +1636,16 @@ VISTAS.gente = () => {
|
|
|
1476
1636
|
// the alias is resolved by the CLI at launch, not by this list.
|
|
1477
1637
|
const opcionesModelo =
|
|
1478
1638
|
opciones([''].concat(MODELOS), a.model) + '<option value="__otro__">custom…</option>';
|
|
1479
|
-
const opcionesEsfuerzo = opciones([''].concat(
|
|
1480
|
-
const opcionesRuntime = opciones(
|
|
1639
|
+
const opcionesEsfuerzo = opciones([''].concat(ESFUERZOS), a.effort);
|
|
1640
|
+
const opcionesRuntime = opciones(RUNTIMES, a.runtime);
|
|
1641
|
+
const montajes = a.mounts ?? [];
|
|
1481
1642
|
return `
|
|
1482
1643
|
<div class="fichaRPG">
|
|
1483
1644
|
<div class="rpgCab">${caraRPG(a.avatar)}
|
|
1484
1645
|
<div><h3>${esc(a.name)}</h3>
|
|
1485
1646
|
<span class="rpgTags"><span class="kindChip ${esc(a.kind)}">${esc(a.kind)}</span>
|
|
1486
1647
|
<span class="rpgRol">${esc(a.role)}</span>
|
|
1487
|
-
<span class="rpgHogar">${a.legacy ? 'repo' : `workspace · ${
|
|
1648
|
+
<span class="rpgHogar">${a.legacy ? 'repo' : `workspace · ${montajes.length} ${montajes.length === 1 ? 'mount' : 'mounts'}`}</span></span></div>
|
|
1488
1649
|
<span class="rpgBotones">
|
|
1489
1650
|
<button class="rpgMini rpgDado" type="button" data-agente="${esc(a.slug)}"
|
|
1490
1651
|
title="Reroll this agent's face — deterministic, persisted on the card">🎲</button>
|
|
@@ -1494,17 +1655,17 @@ VISTAS.gente = () => {
|
|
|
1494
1655
|
data-file="AGENTS.md" title="Instructions Codex, OpenCode and Kimi read">AGENTS.md</button>
|
|
1495
1656
|
</span>
|
|
1496
1657
|
</div>
|
|
1497
|
-
<div class="rpgFila"><label
|
|
1658
|
+
<div class="rpgFila"><label>${_('engine')}</label>
|
|
1498
1659
|
${barra(motor.ancho, motor.defecto ? 'defecto' : '')}
|
|
1499
1660
|
<select class="rpgSel" data-agente="${esc(a.slug)}" data-campo="model"
|
|
1500
1661
|
${esClaude ? '' : 'disabled title="This window runs its own CLI; its model lives in its own flags"'}>
|
|
1501
1662
|
${opcionesModelo}</select></div>
|
|
1502
|
-
<div class="rpgFila"><label
|
|
1663
|
+
<div class="rpgFila"><label>${_('effort')}</label>
|
|
1503
1664
|
${barra(esfuerzo / 5, esfuerzo ? '' : 'defecto')}
|
|
1504
1665
|
<select class="rpgSel" data-agente="${esc(a.slug)}" data-campo="effort"
|
|
1505
1666
|
${esClaude ? '' : 'disabled title="This window runs its own CLI; its effort lives in its own flags"'}>
|
|
1506
1667
|
${opcionesEsfuerzo}</select></div>
|
|
1507
|
-
<div class="rpgFila"><label
|
|
1668
|
+
<div class="rpgFila"><label>${_('provider')}</label>
|
|
1508
1669
|
<span class="rpgDato"><span class="cliDot ${
|
|
1509
1670
|
a.cli.connected ? 'on' : a.cli.installed ? 'idle' : 'off'
|
|
1510
1671
|
}" title="${esc(
|
|
@@ -1514,16 +1675,39 @@ VISTAS.gente = () => {
|
|
|
1514
1675
|
? 'installed on this machine, not in use by this agent'
|
|
1515
1676
|
: `${a.cli.binary} is not installed`,
|
|
1516
1677
|
)}"></span>${esc(
|
|
1517
|
-
a.cli.connected ? 'connected' : a.cli.installed ? 'idle' : 'missing',
|
|
1678
|
+
_(a.cli.connected ? 'connected' : a.cli.installed ? 'idle' : 'missing'),
|
|
1518
1679
|
)} · ${esc(a.cli.binary)}
|
|
1519
1680
|
<button class="rpgMini rpgTest" type="button" data-agente="${esc(a.slug)}"
|
|
1520
1681
|
title="Run the engine for real: --version, and the login state on Claude">test</button></span>
|
|
1521
1682
|
<select class="rpgSel" data-agente="${esc(a.slug)}" data-campo="runtime">
|
|
1522
1683
|
${opcionesRuntime}</select></div>
|
|
1523
|
-
<div class="
|
|
1684
|
+
<div class="rpgSkills"><label>${_('works on')} · ${montajes.length}</label>
|
|
1685
|
+
${
|
|
1686
|
+
montajes.length
|
|
1687
|
+
? montajes
|
|
1688
|
+
.map(
|
|
1689
|
+
(m) =>
|
|
1690
|
+
`<code class="mono" title="${esc(m.target)}">${esc(m.label)}${
|
|
1691
|
+
m.fixed
|
|
1692
|
+
? ''
|
|
1693
|
+
: `<button class="rpgQuitar rpgDesmonta" type="button"
|
|
1694
|
+
data-agente="${esc(a.slug)}" data-mount="${esc(m.label)}"
|
|
1695
|
+
title="Stop this agent working on ${esc(m.target)}">×</button>`
|
|
1696
|
+
}</code>`,
|
|
1697
|
+
)
|
|
1698
|
+
.join(' ')
|
|
1699
|
+
: '<span class="rpgDato">nothing mounted yet</span>'
|
|
1700
|
+
}
|
|
1701
|
+
${
|
|
1702
|
+
a.legacy
|
|
1703
|
+
? '<span class="rpgDato">a legacy repo agent works on its own repo</span>'
|
|
1704
|
+
: `<button class="rpgMini rpgMonta" type="button" data-agente="${esc(a.slug)}"
|
|
1705
|
+
title="A repo, a worktree or a folder of documents — this agent works on all of them">+ folder</button>`
|
|
1706
|
+
}</div>
|
|
1707
|
+
<div class="rpgFila"><label>${_('growth')}</label>
|
|
1524
1708
|
${barra(crecido)}
|
|
1525
1709
|
<span class="rpgDato">${g.floors | 0} floors · ${g.bricks | 0} bricks · ${g.activity30 | 0}/30d</span></div>
|
|
1526
|
-
<div class="rpgSkills"><label
|
|
1710
|
+
<div class="rpgSkills"><label>${_('skills')} · ${skills.length}</label>
|
|
1527
1711
|
${
|
|
1528
1712
|
skills.length
|
|
1529
1713
|
? skills
|
|
@@ -1545,13 +1729,22 @@ VISTAS.gente = () => {
|
|
|
1545
1729
|
</div>`;
|
|
1546
1730
|
};
|
|
1547
1731
|
|
|
1548
|
-
q('#lienzo').innerHTML = `<div><span class="sub">
|
|
1549
|
-
<h1 style="margin-top:6px"
|
|
1550
|
-
<p class="prosa" style="margin-top:8px">
|
|
1551
|
-
|
|
1552
|
-
agent actually
|
|
1553
|
-
|
|
1554
|
-
|
|
1732
|
+
q('#lienzo').innerHTML = `<div><span class="sub">the houses of ${esc(E.city_name)}</span>
|
|
1733
|
+
<h1 style="margin-top:6px">${_('Who lives in {city}', { city: esc(E.city_name) })}</h1>
|
|
1734
|
+
<p class="prosa" style="margin-top:8px"><b>A house is where an agent lives and works</b>,
|
|
1735
|
+
and many houses are a city — it is the same thing the map draws, growing with what that
|
|
1736
|
+
agent actually does. The card and the CLI call them <code class="mono">agents</code>;
|
|
1737
|
+
here you see their houses.</p>
|
|
1738
|
+
<p class="prosa">They belong to <b>this</b> city and only
|
|
1739
|
+
to it: each one's workspace and its mounts live inside
|
|
1740
|
+
<code class="mono">${esc(corto(E.datos))}/agents/</code>, so another city has its own
|
|
1741
|
+
people even if you give them the same names. Every agent is whole here: the kind of work
|
|
1742
|
+
it does, its role, everything it works on — any number of repositories and document
|
|
1743
|
+
folders at once — the engine and effort that run it, and the skills in its own home.</p>
|
|
1744
|
+
<p class="prosa">Every number is real: growth from what the agent actually produced,
|
|
1745
|
+
skills from live read-only recognition. Changes persist to the card and apply the next
|
|
1746
|
+
time the session opens.</p>
|
|
1747
|
+
<button class="rpgAlta" type="button" id="altaAgente">${_('+ Build a house')}</button></div>
|
|
1555
1748
|
<div class="fichasRPG">
|
|
1556
1749
|
<div class="fichaRPG rpgSeat">
|
|
1557
1750
|
<div class="rpgCab">${caraRPG(E.avatars?.['seat'])}
|
|
@@ -1672,6 +1865,71 @@ VISTAS.gente = () => {
|
|
|
1672
1865
|
};
|
|
1673
1866
|
});
|
|
1674
1867
|
|
|
1868
|
+
// Adding an agent from the web: the roster the wizard builds question by
|
|
1869
|
+
// question, reachable by somebody who never opens a terminal.
|
|
1870
|
+
const alta = document.getElementById('altaAgente');
|
|
1871
|
+
if (alta)
|
|
1872
|
+
alta.onclick = async () => {
|
|
1873
|
+
const nombre = window.prompt("The agent's name:", '');
|
|
1874
|
+
if (nombre === null || !nombre.trim()) return;
|
|
1875
|
+
const clase =
|
|
1876
|
+
window.prompt('What kind of work does it do? code, knowledge or coordinator', 'code') ?? '';
|
|
1877
|
+
if (!clase.trim()) return;
|
|
1878
|
+
const rol = window.prompt('Its role (its specialty, not authority):', 'blank') ?? '';
|
|
1879
|
+
if (!rol.trim()) return;
|
|
1880
|
+
try {
|
|
1881
|
+
const r = await api<{ ok?: boolean; error?: string }>('/api/agentes', {
|
|
1882
|
+
method: 'POST',
|
|
1883
|
+
body: JSON.stringify({ name: nombre.trim(), kind: clase.trim(), role: rol.trim() }),
|
|
1884
|
+
});
|
|
1885
|
+
if (!r.ok) {
|
|
1886
|
+
toast(r.error || 'Could not add the agent', true);
|
|
1887
|
+
return;
|
|
1888
|
+
}
|
|
1889
|
+
toast(`${nombre.trim()} has a house now — give it something to work on`);
|
|
1890
|
+
void refresca();
|
|
1891
|
+
} catch (e) {
|
|
1892
|
+
toast(String(e), true);
|
|
1893
|
+
}
|
|
1894
|
+
};
|
|
1895
|
+
|
|
1896
|
+
// Mounts: what an agent works on, added and removed where it is read.
|
|
1897
|
+
const montaje = async (carga: Record<string, string>, hecho: string) => {
|
|
1898
|
+
try {
|
|
1899
|
+
const r = await api<{ ok?: boolean; error?: string }>('/api/montaje', {
|
|
1900
|
+
method: 'POST',
|
|
1901
|
+
body: JSON.stringify(carga),
|
|
1902
|
+
});
|
|
1903
|
+
if (!r.ok) {
|
|
1904
|
+
toast(r.error || 'Could not change the mounts', true);
|
|
1905
|
+
return;
|
|
1906
|
+
}
|
|
1907
|
+
toast(hecho);
|
|
1908
|
+
void refresca();
|
|
1909
|
+
} catch (e) {
|
|
1910
|
+
toast(String(e), true);
|
|
1911
|
+
}
|
|
1912
|
+
};
|
|
1913
|
+
todos<HTMLButtonElement>('.rpgMonta').forEach((boton) => {
|
|
1914
|
+
boton.onclick = () => {
|
|
1915
|
+
const ruta = window.prompt('A repo, a worktree or a folder of documents:', '~/');
|
|
1916
|
+
if (ruta === null || !ruta.trim()) return;
|
|
1917
|
+
void montaje({ agent: boton.dataset.agente ?? '', add: ruta.trim() }, 'Mounted');
|
|
1918
|
+
};
|
|
1919
|
+
});
|
|
1920
|
+
todos<HTMLButtonElement>('.rpgDesmonta').forEach((boton) => {
|
|
1921
|
+
boton.onclick = () => {
|
|
1922
|
+
const etiqueta = boton.dataset.mount ?? '';
|
|
1923
|
+
if (!window.confirm(`Stop this agent working on ${etiqueta}?`)) return;
|
|
1924
|
+
// Unmounting removes a symlink and a card key. What it points at is not
|
|
1925
|
+
// touched — say so, because "remove" reads as "delete my folder".
|
|
1926
|
+
void montaje(
|
|
1927
|
+
{ agent: boton.dataset.agente ?? '', remove: etiqueta },
|
|
1928
|
+
`${etiqueta} unmounted — the folder itself is untouched`,
|
|
1929
|
+
);
|
|
1930
|
+
};
|
|
1931
|
+
});
|
|
1932
|
+
|
|
1675
1933
|
// The instruction editors: one click, the modal opens on that agent's file.
|
|
1676
1934
|
todos<HTMLButtonElement>('.rpgIns').forEach((boton) => {
|
|
1677
1935
|
boton.onclick = () =>
|
|
@@ -1861,7 +2119,101 @@ window.addEventListener('message', (message) => {
|
|
|
1861
2119
|
pinta();
|
|
1862
2120
|
});
|
|
1863
2121
|
|
|
2122
|
+
/**
|
|
2123
|
+
* Night or day. The city's own light is night — an isometric map at 3am — so
|
|
2124
|
+
* that stays the default; day is the palette of agentscity.net, so the tool on
|
|
2125
|
+
* your machine and the site are visibly one product.
|
|
2126
|
+
*
|
|
2127
|
+
* Three states, not two: no stored choice means "follow this machine", and only
|
|
2128
|
+
* an explicit click stamps the root element and outranks the system setting.
|
|
2129
|
+
* The map lives in an iframe of its own, so it is told as well — a light Hall
|
|
2130
|
+
* around a night map would read as two products in one window.
|
|
2131
|
+
*/
|
|
2132
|
+
/** Which skin is showing right now: the stamped choice, or this machine's. */
|
|
2133
|
+
function temaActual(): 'light' | 'dark' {
|
|
2134
|
+
const elegido = document.documentElement.getAttribute('data-tema');
|
|
2135
|
+
if (elegido) return elegido === 'claro' ? 'light' : 'dark';
|
|
2136
|
+
// Day is THE default, full stop: a desktop set to dark does not decide this
|
|
2137
|
+
// for the owner. Only the switch does, and it is remembered.
|
|
2138
|
+
return 'light';
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2141
|
+
function tema(): void {
|
|
2142
|
+
const boton = document.getElementById('temaBoton');
|
|
2143
|
+
const guardado = ((): string => {
|
|
2144
|
+
try {
|
|
2145
|
+
return localStorage.getItem('hall-tema') ?? '';
|
|
2146
|
+
} catch {
|
|
2147
|
+
return ''; // a browser with site data blocked still gets a working page
|
|
2148
|
+
}
|
|
2149
|
+
})();
|
|
2150
|
+
const oscuroDelSistema = false; // daylight is the default; only the switch changes it
|
|
2151
|
+
|
|
2152
|
+
const pon = (elegido: string): void => {
|
|
2153
|
+
const raiz = document.documentElement;
|
|
2154
|
+
if (elegido) raiz.setAttribute('data-tema', elegido);
|
|
2155
|
+
else raiz.removeAttribute('data-tema');
|
|
2156
|
+
const claro = elegido ? elegido === 'claro' : !oscuroDelSistema;
|
|
2157
|
+
// The button says where a click TAKES you, not where you are: a control
|
|
2158
|
+
// labelled with the state you can already see is a riddle.
|
|
2159
|
+
if (boton) boton.textContent = claro ? '☾ Night' : '☀ Day';
|
|
2160
|
+
const frame = document.querySelector<HTMLIFrameElement>('#cityMapFrame');
|
|
2161
|
+
frame?.contentWindow?.postMessage(
|
|
2162
|
+
{ type: 'agents-city-map-theme/1', theme: claro ? 'light' : 'dark' },
|
|
2163
|
+
new URL(frame.src, location.href).origin,
|
|
2164
|
+
);
|
|
2165
|
+
};
|
|
2166
|
+
|
|
2167
|
+
pon(guardado);
|
|
2168
|
+
if (boton)
|
|
2169
|
+
boton.onclick = () => {
|
|
2170
|
+
const claroAhora = document.documentElement.getAttribute('data-tema')
|
|
2171
|
+
? document.documentElement.getAttribute('data-tema') === 'claro'
|
|
2172
|
+
: !oscuroDelSistema;
|
|
2173
|
+
const elegido = claroAhora ? 'oscuro' : 'claro';
|
|
2174
|
+
try {
|
|
2175
|
+
localStorage.setItem('hall-tema', elegido);
|
|
2176
|
+
} catch {
|
|
2177
|
+
/* not being able to remember it is not a reason to refuse the switch */
|
|
2178
|
+
}
|
|
2179
|
+
pon(elegido);
|
|
2180
|
+
};
|
|
2181
|
+
}
|
|
2182
|
+
|
|
2183
|
+
/**
|
|
2184
|
+
* Español or English, on a switch next to the theme.
|
|
2185
|
+
*
|
|
2186
|
+
* The two READMEs shipped bilingual from the first commit and the product did
|
|
2187
|
+
* not, which is backwards: documentation is read once and the interface every
|
|
2188
|
+
* day. The page starts in this browser's language and the button says which one
|
|
2189
|
+
* a click takes you to, the same way the theme button does.
|
|
2190
|
+
*/
|
|
2191
|
+
function interruptorDeIdioma(): void {
|
|
2192
|
+
const boton = document.getElementById('idiomaBoton');
|
|
2193
|
+
if (!boton) return;
|
|
2194
|
+
const pon = (): void => {
|
|
2195
|
+
document.documentElement.lang = idioma();
|
|
2196
|
+
// The label is the language you would GET, not the one you are reading.
|
|
2197
|
+
boton.textContent = idioma() === 'es' ? 'EN' : 'ES';
|
|
2198
|
+
// The chrome that lives in the HTML file rather than in a view: its English
|
|
2199
|
+
// is kept on the element the first time through, so switching back and
|
|
2200
|
+
// forth translates from the source every time instead of from itself.
|
|
2201
|
+
todos<HTMLElement>('[data-i18n]').forEach((el) => {
|
|
2202
|
+
if (!el.dataset.en) el.dataset.en = (el.textContent ?? '').trim();
|
|
2203
|
+
el.textContent = _(el.dataset.en);
|
|
2204
|
+
});
|
|
2205
|
+
};
|
|
2206
|
+
pon();
|
|
2207
|
+
boton.onclick = () => {
|
|
2208
|
+
ponIdioma(idioma() === 'es' ? 'en' : 'es');
|
|
2209
|
+
pon();
|
|
2210
|
+
void refresca();
|
|
2211
|
+
};
|
|
2212
|
+
}
|
|
2213
|
+
|
|
1864
2214
|
arrastreDelRail();
|
|
2215
|
+
interruptorDeIdioma();
|
|
2216
|
+
tema();
|
|
1865
2217
|
void refresca();
|
|
1866
2218
|
|
|
1867
2219
|
export {};
|