@thebattlearena/corner 0.8.0 → 0.9.1-beta.70
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/README.md +38 -5
- package/fonts/battle-arena-icons/BattleArenaIcons.ttf +0 -0
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/hooks/icons.generated.ts +2 -0
- package/plugin/hooks/pane.tsx +16 -2
- package/plugin/hooks/register.tsx +62 -33
- package/plugin/hooks/ring-io.ts +59 -1
- package/plugin/hooks/ring-pane.tsx +125 -35
- package/plugin/hooks/ring.ts +73 -38
- package/plugin/hooks/sparring.ts +139 -0
- package/plugin/icons/rudo-amber-2x1.png +0 -0
- package/plugin/icons/rudo-amber-4x2.png +0 -0
- package/plugin/icons/rudo-amber-5x2.png +0 -0
- package/plugin/icons/rudo-amber-6x2.png +0 -0
- package/plugin/icons/rudo-grey-2x1.png +0 -0
- package/plugin/icons/rudo-grey-4x2.png +0 -0
- package/plugin/icons/rudo-grey-5x2.png +0 -0
- package/plugin/icons/rudo-grey-6x2.png +0 -0
- package/plugin/icons/rudo-pink-2x1.png +0 -0
- package/plugin/icons/rudo-pink-4x2.png +0 -0
- package/plugin/icons/rudo-pink-5x2.png +0 -0
- package/plugin/icons/rudo-pink-6x2.png +0 -0
- package/plugin/icons/rudo-pink-dim-2x1.png +0 -0
- package/plugin/icons/rudo-pink-dim-4x2.png +0 -0
- package/plugin/icons/rudo-pink-dim-5x2.png +0 -0
- package/plugin/icons/rudo-pink-dim-6x2.png +0 -0
- package/plugin/icons/rudo-red-2x1.png +0 -0
- package/plugin/icons/rudo-red-4x2.png +0 -0
- package/plugin/icons/rudo-red-5x2.png +0 -0
- package/plugin/icons/rudo-red-6x2.png +0 -0
- package/plugin/tests/ring-pane.test.tsx +154 -0
- package/plugin/tests/ring.test.ts +13 -4
- package/plugin/tests/sparring.test.ts +204 -0
- package/plugin/types/index.d.ts +25 -1
- package/ring/.claude-plugin/plugin.json +1 -1
- package/ring/agents/rudo.md +95 -0
- package/ring/scripts/chronicle.mjs +24 -0
- package/ring/scripts/config.mjs +47 -1
- package/ring/scripts/fight.mjs +238 -0
- package/ring/scripts/judges-card.mjs +53 -9
- package/ring/scripts/live-hook.mjs +42 -15
- package/ring/scripts/live.mjs +96 -1
- package/ring/scripts/mask-guard.mjs +184 -6
- package/ring/scripts/monitors.mjs +2 -1
- package/ring/scripts/play-by-play.mjs +39 -9
- package/ring/scripts/round-bell.mjs +10 -6
- package/ring/scripts/sparring-chronicle.mjs +470 -0
- package/ring/scripts/sparring.mjs +1161 -0
- package/ring/skills/play-by-play/SKILL.md +49 -3
- package/ring/skills/play-by-play/templates/chronicle.html +7 -0
- package/ring/skills/referee/SKILL.md +46 -2
- package/ring/skills/sparring/SKILL.md +408 -0
- package/ring/skills/sparring/references/host-signal.md +86 -0
- package/ring/skills/sparring/references/lessons.md +50 -0
- package/ring/skills/sparring/templates/pr-body.md +34 -0
- package/ring/skills/sparring/templates/rudo-brief.md +47 -0
- package/ring/skills/sparring/templates/sparring-card.md +33 -0
- package/ring/skills/weigh-in/SKILL.md +21 -3
package/README.md
CHANGED
|
@@ -351,10 +351,25 @@ Se instala **junto con `battlearena`**, con el mismo comando y del mismo marketp
|
|
|
351
351
|
| `/ring:referee close` | Gates finales, `validateCommand` opcional sobre la punta exacta, cuerpo del PR |
|
|
352
352
|
| `/ring:no-surrender` | Modo noche: fija el `/goal` y deja lo que es del humano anotado, sin bloquearse |
|
|
353
353
|
| `/ring:title-defense <pr> [--max-rounds N]` | Defiende el PR ante la review de Copilot: cataloga cada hallazgo, lo corrige por el mismo ciclo y re-pide review hasta quedar limpio |
|
|
354
|
+
| `/ring:sparring <idea>` | Borrador rápido y feo de una funcionalidad sólo para probar si vale: PR en draft que el CI del proyecto puede desplegar |
|
|
354
355
|
| `/ring:play-by-play <pr>` | Narra el combate como crónica deportiva con la hora real de cada golpe y la publica como página privada, siempre en el mismo link |
|
|
355
356
|
|
|
356
|
-
Agentes: `ring:wrestler` (ejecutor, `effort: medium`)
|
|
357
|
-
`effort: high`, en modo rango, concepto o lote).
|
|
357
|
+
Agentes: `ring:wrestler` (ejecutor, `effort: medium`), `ring:cornerman` (revisor de sólo lectura,
|
|
358
|
+
`effort: high`, en modo rango, concepto o lote) y `ring:rudo` (el del sparring, `effort: medium`).
|
|
359
|
+
|
|
360
|
+
**Sparring** (`/ring:sparring`): un rudo por repo escribe en su worktree, dentro de un timebox de
|
|
361
|
+
3 h como máximo, lo mínimo para que el juez pruebe una hipótesis de negocio. Un solo sparring
|
|
362
|
+
puede abarcar back y front: una hipótesis, una ficha (`~/.claude/ring/sparrings/<slug>/`) y un
|
|
363
|
+
veredicto, con un worktree, una rama y un PR por repo, y lo que uno le entrega al otro (un SDK con
|
|
364
|
+
`npm pack`) en su carpeta `tools/`; se suspenden las reglas de
|
|
365
|
+
estilo y de tests, nunca las de operación en vivo (F), secretos ni base de datos (sólo aditiva,
|
|
366
|
+
nunca contra producción). El PR en draft desde `sparring/<slug>` con las etiquetas `sparring` y
|
|
367
|
+
`do-not-merge` es el evento: el CI del proyecto lo atrapa y despliega su preview si quiere, y el
|
|
368
|
+
ring la lee de la API de Deployments de GitHub (`sparring.previewEnvironment`). Las credenciales
|
|
369
|
+
viajan sólo por nombre en un bloque `<!-- sparring:credentials … -->` y el proyecto las resuelve
|
|
370
|
+
de su almacén de secretos; se asumen rotadas al terminar. El juez da el veredicto (KO, revancha o
|
|
371
|
+
descartado); con KO, `/ring:referee start <pr> --from-sparring <slug>` refactoriza esa misma
|
|
372
|
+
rama. El contrato para el proyecto está en `ring/skills/sparring/references/host-signal.md`.
|
|
358
373
|
|
|
359
374
|
**Config del repo** (`.claude/ring.json`, opcional): `gates` (typecheck, tests por ruta con
|
|
360
375
|
`{paths}`, lint), `validateCommand`, `rulebook` (por defecto el genérico del plugin),
|
|
@@ -376,6 +391,13 @@ rebotes, los commits, las revisiones de Copilot, los eventos del PR y las corrid
|
|
|
376
391
|
inventa una hora: lo que no tiene registro se dice como hueco. Es privada; tú la compartes desde su
|
|
377
392
|
menú. Más adelante `ring` podrá mandar el link de la crónica al dashboard de Battle Arena; hoy no se
|
|
378
393
|
conecta al colector.
|
|
394
|
+
Cuando la pelea empieza en un sparring, la crónica arranca ahí (`/ring:play-by-play --sparring
|
|
395
|
+
<slug>`) y la cuenta a dos de tres caídas: el rudo contra el juez, la defensa de los técnicos y el
|
|
396
|
+
árbitro (cada combate abierto `--from-sparring`). Es una sola página con un solo link, guardado en
|
|
397
|
+
`sparring.json`; `sparring.mjs mark` fecha el vestidor, la defensa, la campana, la máscara y «sin
|
|
398
|
+
equipo» (`no-team`: no hay segunda caída). Tras el KO, `sparring.mjs vestidor <slug>` corre en cada
|
|
399
|
+
repo sus gates propios (typecheck, lint y `build`, que `config.mjs` detecta de `scripts.build`) y
|
|
400
|
+
sólo con todos en verde fecha el vestidor.
|
|
379
401
|
|
|
380
402
|
**El contrato es del juez**: si un concepto agrega o cambia un endpoint del spec de la API
|
|
381
403
|
(`apiSpec`) o un modelo de base de datos (`dbModels`: entidades, schema, migraciones), el trabajo se
|
|
@@ -423,7 +445,16 @@ feed en vivo o, en combates de antes, de `gh pr list --head ring/<pr>/round-<n>`
|
|
|
423
445
|
round). Un escritor puede mandar la tabla de la ficha con `fields` (`live.mjs emit --field
|
|
424
446
|
rama=feat/x`).
|
|
425
447
|
|
|
426
|
-
**
|
|
448
|
+
**Los sparrings en Ringside.** Cada sparring activo (sin veredicto `ko` ni `discard` y tocado en las
|
|
449
|
+
últimas 12 h) lleva su propia sección «▸ Sparring <slug>» tras la del combate, con su tira de
|
|
450
|
+
árbitro, rudo (la máscara, en rosa) y juez: el rudo se enciende mientras escribe, el juez cuando hay
|
|
451
|
+
una pregunta abierta. Desplegada, su cabecera dice la ronda (veredictos de `history` + 1), el último
|
|
452
|
+
veredicto y cada repo con su PR. Salen los que registró la sesión (`sparring.mjs` registra a la que
|
|
453
|
+
lo corre, salvo `list`) o, si no registró ninguno, los que trabajan en su carpeta; el feed vive en
|
|
454
|
+
`~/.claude/ring/sparrings/<slug>/live.jsonl` y lo escriben `sparring.mjs` y `live-hook.mjs` (Agent
|
|
455
|
+
`ring:rudo` o «Rudo: …», AskUserQuestion) cuando la sesión no tiene combate propio.
|
|
456
|
+
|
|
457
|
+
**La fuente de íconos.** `fonts/battle-arena-icons/BattleArenaIcons.ttf`: 59 glifos en el área de
|
|
427
458
|
uso privado suplementaria A (U+FBA00–U+FBA85, lejos de los de Nerd Fonts), con nombre de glifo igual
|
|
428
459
|
al id de `src/codepoints.json`. UPM 1000; un ícono de una celda avanza 600 y uno de dos, 1200 (la
|
|
429
460
|
celda de una mono típica es 0.6 em), centrados en vertical en y = 340. 31 vienen de Lucide (ISC,
|
|
@@ -476,7 +507,7 @@ ejemplo, el de agents-studio-be (su batería de campañas, que además abre la v
|
|
|
476
507
|
|
|
477
508
|
- `id` (minúsculas, dígitos, `_`, `-`; nunca el de un actor del ring) y `label`. Una entrada
|
|
478
509
|
inválida se ignora con un aviso en `config.mjs show`; nunca tumba `mask-guard`.
|
|
479
|
-
- `icon` (opcional): cualquiera de los
|
|
510
|
+
- `icon` (opcional): cualquiera de los 59 nombres de la fuente Battle Arena Icons; uno que no exista,
|
|
480
511
|
o ninguno, deja las dos primeras letras del `label` (los de puntos, acciones y medios son de la
|
|
481
512
|
interfaz de Ringside y no tienen imagen, sólo glifo):
|
|
482
513
|
- marca: `arena`
|
|
@@ -540,7 +571,9 @@ ejemplo, el de agents-studio-be (su batería de campañas, que además abre la v
|
|
|
540
571
|
|
|
541
572
|
**`mask-guard`** (hook `PreToolUse` sobre Bash) bloquea **siempre** `git add -A/./--all`,
|
|
542
573
|
`git stash pop`, `git reset --hard` con el árbol sucio y, si `forbidAiTrailers` está activo, los
|
|
543
|
-
commits con trailers de atribución de IA (los coautores humanos pasan)
|
|
574
|
+
commits con trailers de atribución de IA (los coautores humanos pasan); y en un sparring, commitear
|
|
575
|
+
`.env.sparring`, mezclar una rama `sparring/*` o empujarla a otra rama, abrir su PR sin draft o sin
|
|
576
|
+
`do-not-merge`, y sacarlo de draft o mezclarlo. **Sólo mientras hay un
|
|
544
577
|
combate activo en ese repo** bloquea además lo que decide el humano: merges (y `git pull` que no sea
|
|
545
578
|
`--ff-only`/`--rebase`), cambios de base, cerrar o sacar de draft PRs, `gh pr review`, pedir review
|
|
546
579
|
a Copilot, force push, push a una rama protegida, comandos pesados fuera de `one-at-a-time` y
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thebattlearena/corner",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1-beta.70",
|
|
4
4
|
"description": "Corner: tu esquina de Battle Arena en Claude Code (el plugin `battlearena`). Hooks de tracking, objetivo del turno, plan de batalla /battlearena:conquer, Ringside (el panel de tu día), el ring y la radio. Zero-dependency.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "battlearena",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Corner, tu esquina de Battle Arena en Claude Code: tus sesiones al colector, el objetivo del turno, el plan de batalla /battlearena:conquer y Ringside, el panel de tu día junto al chat.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Battle Arena"
|
|
@@ -11,6 +11,7 @@ export const FONT_ICONS = {
|
|
|
11
11
|
'campana': { codepoint: 0xfba13, cells: 1, group: 'actores', tints: ['amber','red'], png: true },
|
|
12
12
|
'copilot': { codepoint: 0xfba14, cells: 1, group: 'actores', tints: ['purple','red','amber'], png: true },
|
|
13
13
|
'juez': { codepoint: 0xfba15, cells: 1, group: 'actores', tints: ['orange','red','amber'], png: true },
|
|
14
|
+
'rudo': { codepoint: 0xfba16, cells: 1, group: 'actores', tints: ['pink','red','amber'], png: true },
|
|
14
15
|
'activo': { codepoint: 0xfba20, cells: 1, group: 'estados', tints: [], png: true },
|
|
15
16
|
'en-la-esquina': { codepoint: 0xfba21, cells: 1, group: 'estados', tints: [], png: true },
|
|
16
17
|
'golpe': { codepoint: 0xfba22, cells: 1, group: 'estados', tints: [], png: true },
|
|
@@ -98,6 +99,7 @@ export const ACTOR_COLOR = {
|
|
|
98
99
|
campana: 'amber',
|
|
99
100
|
copilot: 'purple',
|
|
100
101
|
juez: 'orange',
|
|
102
|
+
rudo: 'pink',
|
|
101
103
|
} as const;
|
|
102
104
|
|
|
103
105
|
/** Las cajas de ícono con PNG (columnas × filas). */
|
package/plugin/hooks/pane.tsx
CHANGED
|
@@ -50,8 +50,8 @@ import {
|
|
|
50
50
|
unlinkedMeetingMinutes,
|
|
51
51
|
} from './meetings';
|
|
52
52
|
import { outsideMinutes, planMinutes, workedMinutes } from './close';
|
|
53
|
-
import type { RingPaneModel } from './ring-pane';
|
|
54
|
-
import { pointBadge, ringHost, RingRow } from './ring-pane';
|
|
53
|
+
import type { RingPaneModel, SparringPaneModel } from './ring-pane';
|
|
54
|
+
import { pointBadge, ringHost, RingRow, SparringRow } from './ring-pane';
|
|
55
55
|
import { ic } from './icons';
|
|
56
56
|
import type { TerminalCaps } from './terminal-caps';
|
|
57
57
|
import { labelLinks } from './label-links';
|
|
@@ -83,6 +83,8 @@ export type PaneModel = {
|
|
|
83
83
|
) => void;
|
|
84
84
|
/** El combate activo del ring en este repo; sin él, la sección no se pinta. */
|
|
85
85
|
ring?: RingPaneModel | null;
|
|
86
|
+
/** Los sparrings activos de la sesión: una sección cada uno, tras la del combate. */
|
|
87
|
+
sparrings?: SparringPaneModel[];
|
|
86
88
|
/** El tema de la persona es claro: el panel se queda sin el fondo navy. */
|
|
87
89
|
lightTheme: boolean;
|
|
88
90
|
/** Lo que sabe dibujar la terminal; sin dato, todo (Ghostty). */
|
|
@@ -1017,6 +1019,14 @@ const NextLine = (el: Elements, next: MyDay, ms: number) => {
|
|
|
1017
1019
|
);
|
|
1018
1020
|
};
|
|
1019
1021
|
|
|
1022
|
+
/** Una sección por sparring activo, tras la del combate; nada sin sparrings. */
|
|
1023
|
+
const Sparrings = (el: Elements, m: PaneModel, width: number) =>
|
|
1024
|
+
m.sparrings?.length ? (
|
|
1025
|
+
<el.Box key="sparrings" flexDirection="column" gap={1}>
|
|
1026
|
+
{m.sparrings.map((s) => SparringRow(el, s, width))}
|
|
1027
|
+
</el.Box>
|
|
1028
|
+
) : null;
|
|
1029
|
+
|
|
1020
1030
|
/**
|
|
1021
1031
|
* La vista «Hoy». Desde las 16:00, sin plan de hoy y con el de mañana listo, muestra el de mañana
|
|
1022
1032
|
* (sus puntos y la fecha arriba); con plan de hoy, agrega una línea con el de mañana.
|
|
@@ -1046,6 +1056,7 @@ const Today = (el: Elements, day: MyDay, m: PaneModel, columns: number) => {
|
|
|
1046
1056
|
</Box>
|
|
1047
1057
|
)}
|
|
1048
1058
|
{m.ring && !host && RingRow(el, m.ring, columns - GUTTER)}
|
|
1059
|
+
{Sparrings(el, m, columns - GUTTER)}
|
|
1049
1060
|
{shown === next && <Text dimColor>Hoy</Text>}
|
|
1050
1061
|
{Emergents(el, day, m, columns)}
|
|
1051
1062
|
{Complements(el, day, m, columns)}
|
|
@@ -1147,6 +1158,9 @@ export function renderPane($: EngineInterface, e: RenderEvent, m: PaneModel) {
|
|
|
1147
1158
|
m.ring &&
|
|
1148
1159
|
m.tab === 'hoy' &&
|
|
1149
1160
|
RingRow(el, m.ring, columns - GUTTER)}
|
|
1161
|
+
{(!day || closing) &&
|
|
1162
|
+
m.tab === 'hoy' &&
|
|
1163
|
+
Sparrings(el, m, columns - GUTTER)}
|
|
1150
1164
|
{riskDay && Risks(el, riskDay.risks, m, columns)}
|
|
1151
1165
|
{day &&
|
|
1152
1166
|
m.tab === 'hoy' &&
|
|
@@ -27,7 +27,8 @@ import {
|
|
|
27
27
|
} from './api';
|
|
28
28
|
import { renderPane } from './pane';
|
|
29
29
|
import { deriveRing } from './ring';
|
|
30
|
-
import { readIconFont, readRing } from './ring-io';
|
|
30
|
+
import { readIconFont, readRing, readSparrings } from './ring-io';
|
|
31
|
+
import { deriveSparring } from './sparring';
|
|
31
32
|
import { iconSource, loadIconBytes } from './icon-files';
|
|
32
33
|
import { ic, setIconFont } from './icons';
|
|
33
34
|
import { terminalCaps } from './terminal-caps';
|
|
@@ -55,6 +56,8 @@ const RING_TICK_MS = 1_000;
|
|
|
55
56
|
const RING_READ_EVERY = 2;
|
|
56
57
|
/** Clave de la fila «Revisión #N» en `opened`. */
|
|
57
58
|
const RING_ROW = 'ring';
|
|
59
|
+
/** El prefijo de un sparring: su fila en `opened` y sus llaves (`sparring-<slug>-chip-rudo`). */
|
|
60
|
+
const sparringScope = (slug: string) => `sparring-${slug}`;
|
|
58
61
|
|
|
59
62
|
const EMPTY: Snapshot = {
|
|
60
63
|
day: null,
|
|
@@ -100,7 +103,7 @@ const isBandHidden = atom(
|
|
|
100
103
|
|
|
101
104
|
export const ring = atom(
|
|
102
105
|
{ plugin: 'battlearena', key: 'ring' } as const,
|
|
103
|
-
{ bout: null, readAt: 0 } as RingSnapshot,
|
|
106
|
+
{ bout: null, sparrings: [], readAt: 0 } as RingSnapshot,
|
|
104
107
|
);
|
|
105
108
|
export const ringChip = atom(
|
|
106
109
|
{ plugin: 'battlearena', key: 'ringChip' } as const,
|
|
@@ -154,12 +157,13 @@ async function refreshRing($: EngineInterface): Promise<void> {
|
|
|
154
157
|
const refs = [...(day?.items ?? []), ...(next?.items ?? [])].flatMap(
|
|
155
158
|
(item) => item.brief?.refs ?? [],
|
|
156
159
|
);
|
|
157
|
-
const [bout, iconFont] = await Promise.all([
|
|
160
|
+
const [bout, sparrings, iconFont] = await Promise.all([
|
|
158
161
|
readRing($, refs),
|
|
162
|
+
readSparrings($),
|
|
159
163
|
readIconFont($),
|
|
160
164
|
]);
|
|
161
165
|
const readAt = await $.clock.now();
|
|
162
|
-
await update($, ring, () => ({ bout, readAt, iconFont }));
|
|
166
|
+
await update($, ring, () => ({ bout, sparrings, readAt, iconFont }));
|
|
163
167
|
}
|
|
164
168
|
|
|
165
169
|
/**
|
|
@@ -169,10 +173,13 @@ async function refreshRing($: EngineInterface): Promise<void> {
|
|
|
169
173
|
async function ringBeat($: EngineInterface, beat: number): Promise<void> {
|
|
170
174
|
if (!(await isPaneShown($))) return;
|
|
171
175
|
if (beat % RING_READ_EVERY === 0) await refreshRing($);
|
|
172
|
-
const { bout } = await read($, ring);
|
|
173
|
-
if (!bout) return;
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
+
const { bout, sparrings = [] } = await read($, ring);
|
|
177
|
+
if (!bout && !sparrings.length) return;
|
|
178
|
+
const now = await $.clock.now();
|
|
179
|
+
const running =
|
|
180
|
+
(bout && deriveRing(bout, now).running) ||
|
|
181
|
+
sparrings.some((s) => deriveSparring(s, now).running);
|
|
182
|
+
if (running) await update($, ringTick, (n) => n + 1);
|
|
176
183
|
await probeChipImage($);
|
|
177
184
|
}
|
|
178
185
|
|
|
@@ -416,40 +423,62 @@ export const register: Register = (on) => {
|
|
|
416
423
|
});
|
|
417
424
|
|
|
418
425
|
on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
|
|
419
|
-
const { bout, iconFont = false } = await read($, ring);
|
|
426
|
+
const { bout, sparrings = [], iconFont = false } = await read($, ring);
|
|
420
427
|
const caps = terminalCaps(await $.env.get('TERM_PROGRAM'));
|
|
421
428
|
// Los símbolos de todo Ringside van con la fuente de íconos si está instalada.
|
|
422
429
|
setIconFont(iconFont, caps.glyphsFit);
|
|
423
430
|
const now = await $.clock.now();
|
|
431
|
+
// Lo que comparten las secciones del ring (el combate y cada sparring); cada una con su fila en
|
|
432
|
+
// `opened` y su ficha abierta en `ringChip` (la de un sparring, con su prefijo: una a la vez).
|
|
433
|
+
const section = async (row: string, scope?: string) => {
|
|
434
|
+
const key = (id: string) => (scope ? `${scope}/${id}` : id);
|
|
435
|
+
const prefix = scope ? `${scope}/` : null;
|
|
436
|
+
const chip = await read($, ringChip);
|
|
437
|
+
return {
|
|
438
|
+
scope,
|
|
439
|
+
iconFont,
|
|
440
|
+
iconDir: iconDirOf($),
|
|
441
|
+
imagesDraw: (await read($, ringImages)).draws,
|
|
442
|
+
onProbe: (shape: typeof chipShape) => {
|
|
443
|
+
chipShape = shape;
|
|
444
|
+
},
|
|
445
|
+
light: await isLightTheme($),
|
|
446
|
+
isTerminal: e.surface === 'terminal',
|
|
447
|
+
tick: await read($, ringTick),
|
|
448
|
+
now,
|
|
449
|
+
isOpen: (await read($, opened)).includes(row),
|
|
450
|
+
onRow: () =>
|
|
451
|
+
void update($, opened, (list) =>
|
|
452
|
+
list.includes(row) ? list.filter((x) => x !== row) : [...list, row],
|
|
453
|
+
).then(() => probeChipImage($)),
|
|
454
|
+
selected: prefix
|
|
455
|
+
? chip?.startsWith(prefix)
|
|
456
|
+
? chip.slice(prefix.length)
|
|
457
|
+
: null
|
|
458
|
+
: chip,
|
|
459
|
+
onChip: (id: string) =>
|
|
460
|
+
void update($, ringChip, (current) =>
|
|
461
|
+
current === key(id) ? null : key(id),
|
|
462
|
+
),
|
|
463
|
+
onOpen: (command: string) =>
|
|
464
|
+
void $.command
|
|
465
|
+
.run({ command })
|
|
466
|
+
.catch(() => $.ui.toast(`Escribe /${command}`)),
|
|
467
|
+
};
|
|
468
|
+
};
|
|
424
469
|
const ringModel = bout && {
|
|
425
|
-
|
|
426
|
-
iconDir: iconDirOf($),
|
|
427
|
-
imagesDraw: (await read($, ringImages)).draws,
|
|
428
|
-
onProbe: (shape: typeof chipShape) => {
|
|
429
|
-
chipShape = shape;
|
|
430
|
-
},
|
|
431
|
-
light: await isLightTheme($),
|
|
432
|
-
isTerminal: e.surface === 'terminal',
|
|
470
|
+
...(await section(RING_ROW)),
|
|
433
471
|
view: deriveRing(bout, now),
|
|
434
|
-
tick: await read($, ringTick),
|
|
435
|
-
now,
|
|
436
|
-
isOpen: (await read($, opened)).includes(RING_ROW),
|
|
437
|
-
onRow: () =>
|
|
438
|
-
void update($, opened, (list) =>
|
|
439
|
-
list.includes(RING_ROW)
|
|
440
|
-
? list.filter((x) => x !== RING_ROW)
|
|
441
|
-
: [...list, RING_ROW],
|
|
442
|
-
).then(() => probeChipImage($)),
|
|
443
|
-
selected: await read($, ringChip),
|
|
444
|
-
onChip: (id: string) =>
|
|
445
|
-
void update($, ringChip, (current) => (current === id ? null : id)),
|
|
446
|
-
onOpen: (command: string) =>
|
|
447
|
-
void $.command
|
|
448
|
-
.run({ command })
|
|
449
|
-
.catch(() => $.ui.toast(`Escribe /${command}`)),
|
|
450
472
|
};
|
|
473
|
+
const sparringModels = await Promise.all(
|
|
474
|
+
sparrings.map(async (s) => ({
|
|
475
|
+
...(await section(sparringScope(s.slug), sparringScope(s.slug))),
|
|
476
|
+
view: deriveSparring(s, now),
|
|
477
|
+
})),
|
|
478
|
+
);
|
|
451
479
|
return renderPane($, e, {
|
|
452
480
|
ring: ringModel,
|
|
481
|
+
sparrings: sparringModels,
|
|
453
482
|
lightTheme: await isLightTheme($),
|
|
454
483
|
caps,
|
|
455
484
|
onOpenUrl: (url) =>
|
package/plugin/hooks/ring-io.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { EngineInterface } from 'claude-code';
|
|
2
2
|
|
|
3
|
-
import type { RingBout } from '../types';
|
|
3
|
+
import type { RingBout, RingSparring } from '../types';
|
|
4
4
|
import {
|
|
5
5
|
bellsOf,
|
|
6
6
|
defenseOf,
|
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
refersTo,
|
|
10
10
|
slugFromRemote,
|
|
11
11
|
} from './ring';
|
|
12
|
+
import type { SparringFile } from './sparring';
|
|
13
|
+
import { sparringOf, sparringsFor } from './sparring';
|
|
12
14
|
|
|
13
15
|
/**
|
|
14
16
|
* Lee el combate activo del repo de la sesión desde los archivos del plugin `ring`
|
|
@@ -233,6 +235,62 @@ export async function readRing(
|
|
|
233
235
|
}
|
|
234
236
|
}
|
|
235
237
|
|
|
238
|
+
/** Los archivos de un sparring que cuentan como «tocarlo». */
|
|
239
|
+
const SPARRING_FILES = ['sparring.json', 'card.md', 'live.jsonl'];
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Los sparrings activos que muestra esta sesión (`~/.claude/ring/sparrings/<slug>/`): los que ella
|
|
243
|
+
* registró o, sin ninguno, los que trabajan en su carpeta (ver `sparringsFor`), cada uno con su feed.
|
|
244
|
+
* Vacío sin ring o sin sparrings; nunca falla.
|
|
245
|
+
*/
|
|
246
|
+
export async function readSparrings(
|
|
247
|
+
$: EngineInterface,
|
|
248
|
+
): Promise<RingSparring[]> {
|
|
249
|
+
try {
|
|
250
|
+
const [root, home, sessionId, now] = await Promise.all([
|
|
251
|
+
$.session.root(),
|
|
252
|
+
ringHome($),
|
|
253
|
+
$.session.id().catch(() => null),
|
|
254
|
+
$.clock.now(),
|
|
255
|
+
]);
|
|
256
|
+
if (!home) return [];
|
|
257
|
+
const base = `${home}/sparrings`;
|
|
258
|
+
const entries = await $.fs.list(base).catch(() => []);
|
|
259
|
+
const found = await Promise.all(
|
|
260
|
+
entries
|
|
261
|
+
.filter((entry) => entry.kind === 'dir')
|
|
262
|
+
.map(async (entry) => {
|
|
263
|
+
const dir = `${base}/${entry.name}`;
|
|
264
|
+
const [state, files] = await Promise.all([
|
|
265
|
+
readJson<SparringFile>($, `${dir}/sparring.json`),
|
|
266
|
+
$.fs.list(dir).catch(() => []),
|
|
267
|
+
]);
|
|
268
|
+
const touched = Math.max(
|
|
269
|
+
0,
|
|
270
|
+
...files
|
|
271
|
+
.filter((f) => SPARRING_FILES.includes(f.name))
|
|
272
|
+
.map((f) => f.mtimeMs),
|
|
273
|
+
);
|
|
274
|
+
return state ? { dir, state, touched } : null;
|
|
275
|
+
}),
|
|
276
|
+
);
|
|
277
|
+
const shown = sparringsFor(
|
|
278
|
+
found.filter((s) => s !== null),
|
|
279
|
+
{ sessionId, root, now },
|
|
280
|
+
);
|
|
281
|
+
return await Promise.all(
|
|
282
|
+
shown.map(async ({ dir, state }) =>
|
|
283
|
+
sparringOf(
|
|
284
|
+
state,
|
|
285
|
+
parseEvents(await $.fs.read(`${dir}/live.jsonl`).catch(() => ''), []),
|
|
286
|
+
),
|
|
287
|
+
),
|
|
288
|
+
);
|
|
289
|
+
} catch {
|
|
290
|
+
return [];
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
236
294
|
/**
|
|
237
295
|
* ¿Está instalada la fuente de íconos? Lo dice `iconFont: true` en `~/.claude/battle-arena/config.json`,
|
|
238
296
|
* que escribe el instalador al copiarla: un mod no puede preguntarle a la terminal por sus fuentes.
|