@thebattlearena/corner 0.8.0 → 0.9.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/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
|
@@ -14,7 +14,8 @@ import { blend, tint } from './colors';
|
|
|
14
14
|
import { iconSource } from './icon-files';
|
|
15
15
|
import { tintedLink } from './label-links';
|
|
16
16
|
import type { Elements } from './pane';
|
|
17
|
-
import type { ChipLook, ChipView, RingView } from './ring';
|
|
17
|
+
import type { ChipLook, ChipView, RingView, StripView } from './ring';
|
|
18
|
+
import type { SparringView } from './sparring';
|
|
18
19
|
import type { RingHealth } from '../types';
|
|
19
20
|
import {
|
|
20
21
|
clockTime,
|
|
@@ -31,9 +32,14 @@ import {
|
|
|
31
32
|
truncateEnd,
|
|
32
33
|
} from './ring';
|
|
33
34
|
|
|
34
|
-
/**
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Lo que Ringside necesita para pintar una sección del ring (diseño «F · Panel del ring», lámina
|
|
37
|
+
* Cuerpo): un combate o un sparring, con su tira de fichas.
|
|
38
|
+
*/
|
|
39
|
+
export type StripPaneModel = {
|
|
40
|
+
view: StripView;
|
|
41
|
+
/** Prefijo de las llaves y del grupo del realce: cada sección tiene el suyo (`ring` el combate). */
|
|
42
|
+
scope?: string;
|
|
37
43
|
/** La fuente de íconos está instalada: glifos en vez de sus dos letras. */
|
|
38
44
|
iconFont: boolean;
|
|
39
45
|
/** Carpeta de los PNG de las fichas (`<plugin>/icons`). */
|
|
@@ -66,6 +72,14 @@ export type RingPaneModel = {
|
|
|
66
72
|
onOpen: (command: string) => void;
|
|
67
73
|
};
|
|
68
74
|
|
|
75
|
+
/** El combate activo: su cabecera lleva el PR, los rounds, la campana y la crónica. */
|
|
76
|
+
export type RingPaneModel = StripPaneModel & { view: RingView };
|
|
77
|
+
|
|
78
|
+
/** Un sparring: su cabecera lleva la ronda, el último veredicto y sus repos con su PR. */
|
|
79
|
+
export type SparringPaneModel = StripPaneModel & { view: SparringView };
|
|
80
|
+
|
|
81
|
+
const scopeOf = (m: StripPaneModel): string => m.scope ?? 'ring';
|
|
82
|
+
|
|
69
83
|
const AMBER = 'warning';
|
|
70
84
|
const PURPLE = '#8B5CF6';
|
|
71
85
|
const RED = '#E5484D';
|
|
@@ -161,7 +175,7 @@ export const ringHost = (items: DayItem[], view: RingView): DayItem | null =>
|
|
|
161
175
|
* alguien trabaja, ■ ámbar si te toca decidir, 👊N rojo si Copilot dejó hilos; nada en reposo.
|
|
162
176
|
*/
|
|
163
177
|
export const pointBadge = (
|
|
164
|
-
view:
|
|
178
|
+
view: StripView,
|
|
165
179
|
tick: number,
|
|
166
180
|
iconFont = false,
|
|
167
181
|
): { text: string; color: string } | null => {
|
|
@@ -182,7 +196,7 @@ export const pointBadge = (
|
|
|
182
196
|
};
|
|
183
197
|
|
|
184
198
|
/** El `Image` de la superficie, cuando se puede usar: terminal y sin que el engine lo haya negado. */
|
|
185
|
-
const imageOf = (el: Elements, m:
|
|
199
|
+
const imageOf = (el: Elements, m: StripPaneModel, probing = false) =>
|
|
186
200
|
m.isTerminal &&
|
|
187
201
|
(probing ? m.imagesDraw !== false : m.imagesDraw === true) &&
|
|
188
202
|
'Image' in el
|
|
@@ -216,19 +230,26 @@ const InlineIcon = (
|
|
|
216
230
|
|
|
217
231
|
const Glyph = (
|
|
218
232
|
{ Text }: Elements,
|
|
219
|
-
m:
|
|
233
|
+
m: StripPaneModel,
|
|
220
234
|
icon: Icon,
|
|
221
235
|
color?: string,
|
|
222
236
|
) => <Text color={color}>{iconText(icon, m.iconFont)}</Text>;
|
|
223
237
|
|
|
238
|
+
/** La marca de la cabecera: ■ ámbar si te toca, el spinner si alguien trabaja, ⁝ en reposo. */
|
|
239
|
+
const headerMark = (m: StripPaneModel) =>
|
|
240
|
+
m.view.waiting
|
|
241
|
+
? { text: ic('■'), color: AMBER }
|
|
242
|
+
: {
|
|
243
|
+
text: m.view.running ? spinnerFrame(m.tick) : '⁝',
|
|
244
|
+
color: fills(m.light).current,
|
|
245
|
+
};
|
|
246
|
+
|
|
224
247
|
/** (a) Cabecera: spinner · #PR · rounds y su barra; a la derecha la última campana y la crónica. */
|
|
225
248
|
const Header = (el: Elements, m: RingPaneModel, width: number) => {
|
|
226
249
|
const { Box, Link, Text } = el;
|
|
227
250
|
const { view } = m;
|
|
228
251
|
const tone = fills(m.light);
|
|
229
|
-
const mark =
|
|
230
|
-
? { text: ic('■'), color: AMBER }
|
|
231
|
-
: { text: view.running ? spinnerFrame(m.tick) : '⁝', color: tone.current };
|
|
252
|
+
const mark = headerMark(m);
|
|
232
253
|
const cells =
|
|
233
254
|
view.rounds &&
|
|
234
255
|
roundCells(view.rounds.fought, view.rounds.total, ROUND_CELLS);
|
|
@@ -274,6 +295,45 @@ const Header = (el: Elements, m: RingPaneModel, width: number) => {
|
|
|
274
295
|
);
|
|
275
296
|
};
|
|
276
297
|
|
|
298
|
+
/**
|
|
299
|
+
* (a) Cabecera de un sparring: marca · ronda; a la derecha el último veredicto; debajo, un renglón
|
|
300
|
+
* por repo con su PR (borrador) o «sin PR».
|
|
301
|
+
*/
|
|
302
|
+
const SparringHeader = (el: Elements, m: SparringPaneModel, width: number) => {
|
|
303
|
+
const { Box, Text } = el;
|
|
304
|
+
const { view } = m;
|
|
305
|
+
const mark = headerMark(m);
|
|
306
|
+
return (
|
|
307
|
+
<Box flexDirection="column" width={width}>
|
|
308
|
+
<Box justifyContent="space-between" width={width}>
|
|
309
|
+
<Box gap={1}>
|
|
310
|
+
<Text color={mark.color} bold>
|
|
311
|
+
{mark.text}
|
|
312
|
+
</Text>
|
|
313
|
+
<Text bold>{`ronda ${view.round}`}</Text>
|
|
314
|
+
</Box>
|
|
315
|
+
<Text dimColor wrap="truncate-end">
|
|
316
|
+
{view.verdict
|
|
317
|
+
? `último veredicto: ${view.verdict.call}`
|
|
318
|
+
: 'sin veredicto'}
|
|
319
|
+
</Text>
|
|
320
|
+
</Box>
|
|
321
|
+
{view.repos.map((repo) => (
|
|
322
|
+
<Box key={`${scopeOf(m)}-repo-${repo.repo}`} gap={1} width={width}>
|
|
323
|
+
<Text dimColor wrap="truncate-end">
|
|
324
|
+
{repo.repo}
|
|
325
|
+
</Text>
|
|
326
|
+
{repo.pr && repo.url ? (
|
|
327
|
+
tintedLink(el, repo.url, `#${repo.pr}`, { color: PALETTE.blue })
|
|
328
|
+
) : (
|
|
329
|
+
<Text dimColor>sin PR</Text>
|
|
330
|
+
)}
|
|
331
|
+
</Box>
|
|
332
|
+
))}
|
|
333
|
+
</Box>
|
|
334
|
+
);
|
|
335
|
+
};
|
|
336
|
+
|
|
277
337
|
/** La barra de acento a la izquierda de una tarjeta. */
|
|
278
338
|
const Accent = ({ Text }: Elements, color: string) => (
|
|
279
339
|
<Text color={color}>▌</Text>
|
|
@@ -283,7 +343,7 @@ const Accent = ({ Text }: Elements, color: string) => (
|
|
|
283
343
|
* (b) La línea en vivo: tarjeta a todo lo ancho con acento a la izquierda. Trabajando, azul;
|
|
284
344
|
* Copilot golpeó, rojo; te toca decidir, ámbar lleno con el tiempo; en reposo, desde cuándo.
|
|
285
345
|
*/
|
|
286
|
-
const LiveCard = (el: Elements, m:
|
|
346
|
+
const LiveCard = (el: Elements, m: StripPaneModel, width: number) => {
|
|
287
347
|
const { Box, Text } = el;
|
|
288
348
|
const live = m.view.live;
|
|
289
349
|
const tone = fills(m.light);
|
|
@@ -462,8 +522,8 @@ export const tileLook = (
|
|
|
462
522
|
* des-invierte (probado en Ghostty con un diagnóstico de cuatro variantes; esta y la de Box con llave
|
|
463
523
|
* invierten la ficha entera).
|
|
464
524
|
*/
|
|
465
|
-
export const tileHover = (chip: ChipView) => ({
|
|
466
|
-
scope:
|
|
525
|
+
export const tileHover = (chip: ChipView, scope = 'ring') => ({
|
|
526
|
+
scope: `${scope}-tile-${chip.id}`,
|
|
467
527
|
inverse: true,
|
|
468
528
|
bold: true,
|
|
469
529
|
});
|
|
@@ -471,7 +531,7 @@ export const tileHover = (chip: ChipView) => ({
|
|
|
471
531
|
/** El ícono de una ficha: imagen sólo si la terminal las dibuja; si no, el glifo centrado. */
|
|
472
532
|
const ChipIcon = (
|
|
473
533
|
el: Elements,
|
|
474
|
-
m:
|
|
534
|
+
m: StripPaneModel,
|
|
475
535
|
chip: ChipView,
|
|
476
536
|
size: { cols: number; rows: number },
|
|
477
537
|
look: ChipLook,
|
|
@@ -483,7 +543,7 @@ const ChipIcon = (
|
|
|
483
543
|
if (Image && chip.icon.png && hasShape(zone))
|
|
484
544
|
return (
|
|
485
545
|
<Image
|
|
486
|
-
key={
|
|
546
|
+
key={`${scopeOf(m)}-icon-${chip.id}`}
|
|
487
547
|
source={iconSource(m.iconDir, chipPng(chip, color, look, zone))}
|
|
488
548
|
columns={zone.cols}
|
|
489
549
|
rows={zone.rows}
|
|
@@ -497,7 +557,7 @@ const ChipIcon = (
|
|
|
497
557
|
<Text
|
|
498
558
|
color={tile.glyph}
|
|
499
559
|
backgroundColor={tile.fill}
|
|
500
|
-
hover={tileHover(chip)}
|
|
560
|
+
hover={tileHover(chip, scopeOf(m))}
|
|
501
561
|
bold
|
|
502
562
|
>
|
|
503
563
|
{marked && <Text color={tile.hex}>▸</Text>}
|
|
@@ -514,9 +574,10 @@ const ChipIcon = (
|
|
|
514
574
|
* una fila cada uno, el del ícono y el del indicador: un botón no puede tener dos renglones). La
|
|
515
575
|
* abierta lleva el relleno más fuerte y ▾ debajo.
|
|
516
576
|
*/
|
|
517
|
-
const Chips = (el: Elements, m:
|
|
577
|
+
const Chips = (el: Elements, m: StripPaneModel, width: number) => {
|
|
518
578
|
const { Box, Button, Text } = el;
|
|
519
579
|
const tone = fills(m.light);
|
|
580
|
+
const scope = scopeOf(m);
|
|
520
581
|
const size = chipSize(width, m.view.chips.length);
|
|
521
582
|
return (
|
|
522
583
|
<Box columnGap={size.gap} justifyContent="center" width={width}>
|
|
@@ -535,7 +596,7 @@ const Chips = (el: Elements, m: RingPaneModel, width: number) => {
|
|
|
535
596
|
? COLORS.delivered
|
|
536
597
|
: tile.dot
|
|
537
598
|
: tile.hex;
|
|
538
|
-
const hover = tileHover(chip);
|
|
599
|
+
const hover = tileHover(chip, scope);
|
|
539
600
|
const health = healthIndicator(chip, m.tick, m.iconFont);
|
|
540
601
|
// Toda la ficha es UN botón: cada renglón es un botón `plain` con el mismo clic, su texto mide
|
|
541
602
|
// el ancho de la ficha y lleva su mismo fondo (ningún renglón se ve distinto en reposo), y el
|
|
@@ -546,7 +607,7 @@ const Chips = (el: Elements, m: RingPaneModel, width: number) => {
|
|
|
546
607
|
</Button>
|
|
547
608
|
);
|
|
548
609
|
const indicator = row(
|
|
549
|
-
|
|
610
|
+
`${scope}-chip-${chip.id}`,
|
|
550
611
|
<Text
|
|
551
612
|
color={health?.color ?? indicatorColor}
|
|
552
613
|
backgroundColor={tile.fill}
|
|
@@ -557,24 +618,27 @@ const Chips = (el: Elements, m: RingPaneModel, width: number) => {
|
|
|
557
618
|
</Text>,
|
|
558
619
|
);
|
|
559
620
|
const top = row(
|
|
560
|
-
|
|
621
|
+
`${scope}-chip-${chip.id}-top`,
|
|
561
622
|
<Text backgroundColor={tile.fill} hover={hover}>
|
|
562
623
|
{' '.repeat(size.box)}
|
|
563
624
|
</Text>,
|
|
564
625
|
);
|
|
565
626
|
const icon = image
|
|
566
627
|
? ChipIcon(el, m, chip, size, look)
|
|
567
|
-
: row(
|
|
628
|
+
: row(
|
|
629
|
+
`${scope}-chip-${chip.id}-icon`,
|
|
630
|
+
ChipIcon(el, m, chip, size, look),
|
|
631
|
+
);
|
|
568
632
|
return (
|
|
569
633
|
<Box
|
|
570
|
-
key={
|
|
634
|
+
key={`${scope}-actor-${chip.id}`}
|
|
571
635
|
flexDirection="column"
|
|
572
636
|
width={size.box}
|
|
573
637
|
flexShrink={0}
|
|
574
638
|
>
|
|
575
639
|
{/* Todas iguales: margen, ícono e indicador, con el mismo alto en todo estado. */}
|
|
576
640
|
<Box
|
|
577
|
-
key={
|
|
641
|
+
key={`${scope}-tile-${chip.id}`}
|
|
578
642
|
flexDirection="column"
|
|
579
643
|
backgroundColor={tile.fill}
|
|
580
644
|
width={size.box}
|
|
@@ -599,14 +663,14 @@ const Chips = (el: Elements, m: RingPaneModel, width: number) => {
|
|
|
599
663
|
const OpenAction = (
|
|
600
664
|
{ Button, Text }: Elements,
|
|
601
665
|
chip: ChipView,
|
|
602
|
-
m:
|
|
666
|
+
m: StripPaneModel,
|
|
603
667
|
) => {
|
|
604
668
|
const open = chip.open;
|
|
605
669
|
if (!open) return null;
|
|
606
670
|
if ('command' in open && open.canRun)
|
|
607
671
|
return (
|
|
608
672
|
<Button
|
|
609
|
-
key={
|
|
673
|
+
key={`${scopeOf(m)}-open-${chip.id}`}
|
|
610
674
|
label={`abrir ${ic('↗')}`}
|
|
611
675
|
variant="primary"
|
|
612
676
|
onPress={() => m.onOpen(open.command)}
|
|
@@ -657,7 +721,7 @@ const rowsOf = (chip: ChipView): [string, string][] => {
|
|
|
657
721
|
const Detail = (
|
|
658
722
|
el: Elements,
|
|
659
723
|
chip: ChipView,
|
|
660
|
-
m:
|
|
724
|
+
m: StripPaneModel,
|
|
661
725
|
width: number,
|
|
662
726
|
) => {
|
|
663
727
|
const { Box, Text } = el;
|
|
@@ -713,7 +777,7 @@ const Detail = (
|
|
|
713
777
|
{rows.length > 0 && (
|
|
714
778
|
<Box flexDirection="column" marginTop={1}>
|
|
715
779
|
{rows.map(([key, value]) => (
|
|
716
|
-
<Box key={
|
|
780
|
+
<Box key={`${scopeOf(m)}-field-${key}`} width={inner}>
|
|
717
781
|
<Box width={LABEL_WIDTH} flexShrink={0}>
|
|
718
782
|
<Text dimColor>{key}</Text>
|
|
719
783
|
</Box>
|
|
@@ -758,8 +822,13 @@ const Detail = (
|
|
|
758
822
|
);
|
|
759
823
|
};
|
|
760
824
|
|
|
761
|
-
/** El bloque desplegado: cabecera, línea en vivo, fichas y la ficha abierta. */
|
|
762
|
-
const Block =
|
|
825
|
+
/** El bloque desplegado: cabecera (la de un combate o la de un sparring), línea en vivo, fichas y la ficha abierta. */
|
|
826
|
+
const Block = <M extends StripPaneModel>(
|
|
827
|
+
el: Elements,
|
|
828
|
+
m: M,
|
|
829
|
+
width: number,
|
|
830
|
+
header: (el: Elements, m: M, width: number) => JSX.Element,
|
|
831
|
+
) => {
|
|
763
832
|
const { Box } = el;
|
|
764
833
|
const chip = m.view.chips.find((c) => c.id === m.selected);
|
|
765
834
|
const inner = width - 2;
|
|
@@ -772,7 +841,7 @@ const Block = (el: Elements, m: RingPaneModel, width: number) => {
|
|
|
772
841
|
paddingTop={1}
|
|
773
842
|
width={width}
|
|
774
843
|
>
|
|
775
|
-
{
|
|
844
|
+
{header(el, m, inner)}
|
|
776
845
|
{LiveCard(el, m, inner)}
|
|
777
846
|
{Chips(el, m, inner)}
|
|
778
847
|
{chip && Detail(el, chip, m, inner)}
|
|
@@ -792,32 +861,53 @@ export const RingRow = (
|
|
|
792
861
|
m: RingPaneModel,
|
|
793
862
|
width: number,
|
|
794
863
|
indent = 0,
|
|
864
|
+
) => Section(el, m, width, indent, `Revisión #${m.view.pr}`, Header);
|
|
865
|
+
|
|
866
|
+
/**
|
|
867
|
+
* La sección de un sparring activo, como la de un combate: «▸ Sparring <slug>» en su banda y,
|
|
868
|
+
* desplegada, su ronda, su último veredicto, sus repos con su PR y la tira de árbitro, rudo y juez.
|
|
869
|
+
*/
|
|
870
|
+
export const SparringRow = (
|
|
871
|
+
el: Elements,
|
|
872
|
+
m: SparringPaneModel,
|
|
873
|
+
width: number,
|
|
874
|
+
) => Section(el, m, width, 0, `Sparring ${m.view.slug}`, SparringHeader);
|
|
875
|
+
|
|
876
|
+
/** El título con banda (indicador y tiempo de hoy a la derecha) y, desplegado, el bloque. */
|
|
877
|
+
const Section = <M extends StripPaneModel>(
|
|
878
|
+
el: Elements,
|
|
879
|
+
m: M,
|
|
880
|
+
width: number,
|
|
881
|
+
indent: number,
|
|
882
|
+
title: string,
|
|
883
|
+
header: (el: Elements, m: M, width: number) => JSX.Element,
|
|
795
884
|
) => {
|
|
796
885
|
const { Box, Button, Text } = el;
|
|
797
886
|
const { view } = m;
|
|
887
|
+
const scope = scopeOf(m);
|
|
798
888
|
const tone = fills(m.light);
|
|
799
889
|
const minutes = Math.round(view.todayMs / 60_000);
|
|
800
890
|
const badge = pointBadge(view, m.tick, m.iconFont);
|
|
801
891
|
return (
|
|
802
|
-
<Box key=
|
|
892
|
+
<Box key={`${scope}-section`} flexDirection="column" width={width}>
|
|
803
893
|
<Box
|
|
804
|
-
key=
|
|
894
|
+
key={`${scope}-title`}
|
|
805
895
|
backgroundColor={tone.band}
|
|
806
896
|
justifyContent="space-between"
|
|
807
897
|
paddingLeft={Math.max(1, indent)}
|
|
808
898
|
paddingRight={1}
|
|
809
899
|
width={width}
|
|
810
900
|
>
|
|
811
|
-
<Button key=
|
|
901
|
+
<Button key={`${scope}-row`} plain onPress={() => m.onRow()}>
|
|
812
902
|
<Text color={COLORS.blue}>{`${ic(m.isOpen ? '▾' : '▸')} `}</Text>
|
|
813
|
-
<Text bold>{
|
|
903
|
+
<Text bold>{title}</Text>
|
|
814
904
|
</Button>
|
|
815
905
|
<Text>
|
|
816
906
|
{badge && <Text color={badge.color}>{`${badge.text} `}</Text>}
|
|
817
907
|
<Text dimColor>{duration(minutes) || ''}</Text>
|
|
818
908
|
</Text>
|
|
819
909
|
</Box>
|
|
820
|
-
{m.isOpen && Block(el, m, width)}
|
|
910
|
+
{m.isOpen && Block(el, m, width, header)}
|
|
821
911
|
</Box>
|
|
822
912
|
);
|
|
823
913
|
};
|
package/plugin/hooks/ring.ts
CHANGED
|
@@ -83,15 +83,11 @@ export const lookOf = (chip: ChipView): ChipLook => {
|
|
|
83
83
|
return 'quiet';
|
|
84
84
|
};
|
|
85
85
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
rounds: { fought: number; total: number } | null;
|
|
92
|
-
/** La última round-bell: su PR si se sabe, si no su número de round. */
|
|
93
|
-
bell: { pr: number | null; url: string | null; round: number } | null;
|
|
94
|
-
chronicleUrl: string | null;
|
|
86
|
+
/**
|
|
87
|
+
* Lo que comparten un combate y un sparring: la tira de fichas, la línea en vivo y si alguien
|
|
88
|
+
* trabaja o te espera. Sale de `deriveStrip`.
|
|
89
|
+
*/
|
|
90
|
+
export type StripView = {
|
|
95
91
|
/** Alguien trabaja ahora (gira el spinner). */
|
|
96
92
|
running: boolean;
|
|
97
93
|
/** Una decisión espera al humano (el spinner se vuelve ■ ámbar). */
|
|
@@ -115,8 +111,21 @@ export type RingView = {
|
|
|
115
111
|
health: 'warn' | 'fail' | null;
|
|
116
112
|
};
|
|
117
113
|
|
|
118
|
-
|
|
119
|
-
|
|
114
|
+
export type RingView = StripView & {
|
|
115
|
+
slug: string;
|
|
116
|
+
pr: number;
|
|
117
|
+
prUrl: string;
|
|
118
|
+
/** Rounds de la defensa del título; `null` mientras no hay. */
|
|
119
|
+
rounds: { fought: number; total: number } | null;
|
|
120
|
+
/** La última round-bell: su PR si se sabe, si no su número de round. */
|
|
121
|
+
bell: { pr: number | null; url: string | null; round: number } | null;
|
|
122
|
+
chronicleUrl: string | null;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
export type ActorChip = { id: RingActorId & IconName; name: string };
|
|
126
|
+
|
|
127
|
+
/** Los actores de un combate, en el orden de la tira. */
|
|
128
|
+
export const CORE_CHIPS: ActorChip[] = [
|
|
120
129
|
{ id: 'arbitro', name: 'árbitro' },
|
|
121
130
|
{ id: 'luchador', name: 'luchador' },
|
|
122
131
|
{ id: 'esquinero', name: 'esquinero' },
|
|
@@ -125,7 +134,17 @@ export const CORE_CHIPS: { id: RingActorId & IconName; name: string }[] = [
|
|
|
125
134
|
{ id: 'juez', name: 'juez' },
|
|
126
135
|
];
|
|
127
136
|
|
|
128
|
-
|
|
137
|
+
/** Los de un sparring: el árbitro lo abre, el rudo escribe el borrador y el juez dicta. */
|
|
138
|
+
export const SPARRING_CHIPS: ActorChip[] = [
|
|
139
|
+
{ id: 'arbitro', name: 'árbitro' },
|
|
140
|
+
{ id: 'rudo', name: 'rudo' },
|
|
141
|
+
{ id: 'juez', name: 'juez' },
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
/** Todos los actores del ring: ningún monitor puede llamarse así. */
|
|
145
|
+
const CORE_IDS: string[] = [
|
|
146
|
+
...new Set([...CORE_CHIPS, ...SPARRING_CHIPS].map((chip) => chip.id)),
|
|
147
|
+
];
|
|
129
148
|
const STATES = ['active', 'waiting', 'done', 'failed'];
|
|
130
149
|
const OPEN_STATES = ['active', 'waiting'];
|
|
131
150
|
/** Un «activo» sin cierre después de esto se da por perdido (la sesión murió a medio trabajo). */
|
|
@@ -497,13 +516,22 @@ const todayOf = (events: RingEvent[], now: number, running: boolean) => {
|
|
|
497
516
|
return (running ? now : Math.max(...today)) - Math.min(...today);
|
|
498
517
|
};
|
|
499
518
|
|
|
500
|
-
/**
|
|
501
|
-
|
|
519
|
+
/**
|
|
520
|
+
* Los eventos de un feed en vivo → la tira de fichas (los actores dados y los monitores), la línea en
|
|
521
|
+
* vivo y quién trabaja. Lo comparten el combate y el sparring.
|
|
522
|
+
*/
|
|
523
|
+
export const deriveStrip = (
|
|
524
|
+
events: RingEvent[],
|
|
525
|
+
actors: ActorChip[],
|
|
526
|
+
monitors: RingMonitor[],
|
|
527
|
+
commands: string[],
|
|
528
|
+
now: number,
|
|
529
|
+
): StripView => {
|
|
502
530
|
const bySource = new Map<string, RingEvent[]>();
|
|
503
|
-
for (const e of
|
|
531
|
+
for (const e of events)
|
|
504
532
|
bySource.set(sourceOf(e), [...(bySource.get(sourceOf(e)) ?? []), e]);
|
|
505
533
|
const chips = [
|
|
506
|
-
...
|
|
534
|
+
...actors.map((chip) =>
|
|
507
535
|
chipOf(
|
|
508
536
|
{
|
|
509
537
|
id: chip.id,
|
|
@@ -516,7 +544,7 @@ export const deriveRing = (bout: RingBout, now: number): RingView => {
|
|
|
516
544
|
null,
|
|
517
545
|
),
|
|
518
546
|
),
|
|
519
|
-
...
|
|
547
|
+
...monitors.map((m) =>
|
|
520
548
|
chipOf(
|
|
521
549
|
{
|
|
522
550
|
id: m.id,
|
|
@@ -526,7 +554,7 @@ export const deriveRing = (bout: RingBout, now: number): RingView => {
|
|
|
526
554
|
},
|
|
527
555
|
bySource.get(`monitor:${m.id}`) ?? [],
|
|
528
556
|
now,
|
|
529
|
-
openFor(m,
|
|
557
|
+
openFor(m, commands),
|
|
530
558
|
),
|
|
531
559
|
),
|
|
532
560
|
];
|
|
@@ -549,26 +577,7 @@ export const deriveRing = (bout: RingBout, now: number): RingView => {
|
|
|
549
577
|
: lookOf(lead) === 'hit'
|
|
550
578
|
? 'hit'
|
|
551
579
|
: 'idle';
|
|
552
|
-
const bellEvent = [...bout.events]
|
|
553
|
-
.reverse()
|
|
554
|
-
.find((e) => e.actor === 'campana' && e.pr);
|
|
555
|
-
const hasBell = bout.bells > 0 || !!bellEvent;
|
|
556
580
|
return {
|
|
557
|
-
slug: bout.slug,
|
|
558
|
-
pr: bout.pr,
|
|
559
|
-
prUrl: `https://github.com/${bout.slug}/pull/${bout.pr}`,
|
|
560
|
-
rounds: bout.defense && {
|
|
561
|
-
fought: bout.defense.fought,
|
|
562
|
-
total: bout.defense.limit,
|
|
563
|
-
},
|
|
564
|
-
bell: hasBell
|
|
565
|
-
? {
|
|
566
|
-
pr: bellEvent?.pr ?? bout.bellPr?.pr ?? null,
|
|
567
|
-
url: bellEvent?.url ?? bout.bellPr?.url ?? null,
|
|
568
|
-
round: bout.bells,
|
|
569
|
-
}
|
|
570
|
-
: null,
|
|
571
|
-
chronicleUrl: bout.playByPlayUrl,
|
|
572
581
|
running: working.length > 0,
|
|
573
582
|
waiting,
|
|
574
583
|
live: lead?.label
|
|
@@ -584,7 +593,7 @@ export const deriveRing = (bout: RingBout, now: number): RingView => {
|
|
|
584
593
|
severity: lead.severity,
|
|
585
594
|
}
|
|
586
595
|
: null,
|
|
587
|
-
todayMs: todayOf(
|
|
596
|
+
todayMs: todayOf(events, now, working.length > 0),
|
|
588
597
|
chips,
|
|
589
598
|
health: chips.some((c) => c.health?.level === 'fail')
|
|
590
599
|
? 'fail'
|
|
@@ -593,3 +602,29 @@ export const deriveRing = (bout: RingBout, now: number): RingView => {
|
|
|
593
602
|
: null,
|
|
594
603
|
};
|
|
595
604
|
};
|
|
605
|
+
|
|
606
|
+
/** El combate → lo que pinta el panel: cabecera, línea en vivo y la tira de chips. */
|
|
607
|
+
export const deriveRing = (bout: RingBout, now: number): RingView => {
|
|
608
|
+
const bellEvent = [...bout.events]
|
|
609
|
+
.reverse()
|
|
610
|
+
.find((e) => e.actor === 'campana' && e.pr);
|
|
611
|
+
const hasBell = bout.bells > 0 || !!bellEvent;
|
|
612
|
+
return {
|
|
613
|
+
slug: bout.slug,
|
|
614
|
+
pr: bout.pr,
|
|
615
|
+
prUrl: `https://github.com/${bout.slug}/pull/${bout.pr}`,
|
|
616
|
+
rounds: bout.defense && {
|
|
617
|
+
fought: bout.defense.fought,
|
|
618
|
+
total: bout.defense.limit,
|
|
619
|
+
},
|
|
620
|
+
bell: hasBell
|
|
621
|
+
? {
|
|
622
|
+
pr: bellEvent?.pr ?? bout.bellPr?.pr ?? null,
|
|
623
|
+
url: bellEvent?.url ?? bout.bellPr?.url ?? null,
|
|
624
|
+
round: bout.bells,
|
|
625
|
+
}
|
|
626
|
+
: null,
|
|
627
|
+
chronicleUrl: bout.playByPlayUrl,
|
|
628
|
+
...deriveStrip(bout.events, CORE_CHIPS, bout.monitors, bout.commands, now),
|
|
629
|
+
};
|
|
630
|
+
};
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import type { RingEvent, RingSparring, RingSparringRepo } from '../types';
|
|
2
|
+
import type { StripView } from './ring';
|
|
3
|
+
import { deriveStrip, SPARRING_CHIPS } from './ring';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Los sparrings del plugin `ring` en Ringside: de `sparring.json` y su `live.jsonl` sale una sección
|
|
7
|
+
* parecida a la de un combate, con su tira de actores (árbitro, rudo, juez), sus repos con su PR, la
|
|
8
|
+
* ronda y el último veredicto. Todo aquí es puro.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** Los veredictos que cierran un sparring: después ya no se pinta. */
|
|
12
|
+
export const FINAL_VERDICTS = ['ko', 'discard'];
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Un sparring sin tocar en este tiempo ya no es «activo» aunque nadie haya dictado: el timebox de un
|
|
16
|
+
* sparring es de 3 h como mucho, y el juez lo prueba el mismo día. Tocarlo = cualquier escritura de
|
|
17
|
+
* `sparring.json`, `card.md` o `live.jsonl` (un comando del sparring, el rudo, una pregunta).
|
|
18
|
+
*/
|
|
19
|
+
export const SPARRING_FRESH_MS = 12 * 3_600_000;
|
|
20
|
+
|
|
21
|
+
/** Lo que Ringside lee de `sparring.json`; todo opcional porque un archivo malo no tumba el panel. */
|
|
22
|
+
export type SparringFile = {
|
|
23
|
+
slug?: unknown;
|
|
24
|
+
verdict?: { call?: unknown; note?: unknown } | null;
|
|
25
|
+
history?: unknown;
|
|
26
|
+
repos?: unknown;
|
|
27
|
+
sessions?: unknown;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** Un sparring tal como se encontró: su archivo y cuándo se tocó por última vez (ms). */
|
|
31
|
+
export type FoundSparring = { state: SparringFile; touched: number };
|
|
32
|
+
|
|
33
|
+
const text = (value: unknown): string | null =>
|
|
34
|
+
typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
35
|
+
|
|
36
|
+
/** ¿Sigue en pelea? Sin veredicto final y tocado dentro de `SPARRING_FRESH_MS`. */
|
|
37
|
+
export const isActiveSparring = (
|
|
38
|
+
state: SparringFile,
|
|
39
|
+
touched: number,
|
|
40
|
+
now: number,
|
|
41
|
+
): boolean =>
|
|
42
|
+
!!text(state.slug) &&
|
|
43
|
+
!FINAL_VERDICTS.includes(String(state.verdict?.call ?? '')) &&
|
|
44
|
+
now - touched < SPARRING_FRESH_MS;
|
|
45
|
+
|
|
46
|
+
const reposOf = (state: SparringFile) =>
|
|
47
|
+
Array.isArray(state.repos)
|
|
48
|
+
? (state.repos as Record<string, unknown>[]).filter(
|
|
49
|
+
(entry) => !!entry && typeof entry === 'object',
|
|
50
|
+
)
|
|
51
|
+
: [];
|
|
52
|
+
|
|
53
|
+
const sessionIds = (state: SparringFile): string[] =>
|
|
54
|
+
Array.isArray(state.sessions)
|
|
55
|
+
? (state.sessions as { id?: unknown }[])
|
|
56
|
+
.map((session) => session?.id)
|
|
57
|
+
.filter((id): id is string => typeof id === 'string')
|
|
58
|
+
: [];
|
|
59
|
+
|
|
60
|
+
const within = (dir: string, path: string): boolean =>
|
|
61
|
+
path === dir || path.startsWith(`${dir.replace(/\/+$/, '')}/`);
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Qué sparrings activos muestra esta sesión: los que ella registró (corrió `sparring.mjs` en ellos);
|
|
65
|
+
* si no registró ninguno, los que trabajan en su carpeta (algún `repos[].worktree` la contiene). Del
|
|
66
|
+
* más reciente al más viejo.
|
|
67
|
+
*/
|
|
68
|
+
export const sparringsFor = <T extends FoundSparring>(
|
|
69
|
+
found: T[],
|
|
70
|
+
{
|
|
71
|
+
sessionId,
|
|
72
|
+
root,
|
|
73
|
+
now,
|
|
74
|
+
}: { sessionId: string | null; root: string; now: number },
|
|
75
|
+
): T[] => {
|
|
76
|
+
const active = found
|
|
77
|
+
.filter((s) => isActiveSparring(s.state, s.touched, now))
|
|
78
|
+
.sort((a, b) => b.touched - a.touched);
|
|
79
|
+
const own = active.filter(
|
|
80
|
+
(s) => !!sessionId && sessionIds(s.state).includes(sessionId),
|
|
81
|
+
);
|
|
82
|
+
if (own.length) return own;
|
|
83
|
+
return active.filter((s) =>
|
|
84
|
+
reposOf(s.state).some(
|
|
85
|
+
(entry) =>
|
|
86
|
+
typeof entry.worktree === 'string' && within(entry.worktree, root),
|
|
87
|
+
),
|
|
88
|
+
);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/** La ronda que se pelea: los veredictos dictados + 1 (los de antes de `history`, por su veredicto). */
|
|
92
|
+
export const roundOf = (state: SparringFile): number => {
|
|
93
|
+
const history = Array.isArray(state.history)
|
|
94
|
+
? state.history.length
|
|
95
|
+
: state.verdict
|
|
96
|
+
? 1
|
|
97
|
+
: 0;
|
|
98
|
+
return history + 1;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/** `sparring.json` + los eventos de su feed → lo que lleva el snapshot. */
|
|
102
|
+
export const sparringOf = (
|
|
103
|
+
state: SparringFile,
|
|
104
|
+
events: RingEvent[],
|
|
105
|
+
): RingSparring => {
|
|
106
|
+
const call = text(state.verdict?.call);
|
|
107
|
+
return {
|
|
108
|
+
slug: text(state.slug) ?? '',
|
|
109
|
+
repos: reposOf(state).map(
|
|
110
|
+
(entry): RingSparringRepo => ({
|
|
111
|
+
repo: text(entry.repo) ?? '?',
|
|
112
|
+
pr: typeof entry.pr === 'number' ? entry.pr : null,
|
|
113
|
+
url: text(entry.url),
|
|
114
|
+
}),
|
|
115
|
+
),
|
|
116
|
+
round: roundOf(state),
|
|
117
|
+
verdict: call ? { call, note: text(state.verdict?.note) ?? '' } : null,
|
|
118
|
+
events,
|
|
119
|
+
};
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export type SparringView = StripView & {
|
|
123
|
+
slug: string;
|
|
124
|
+
repos: RingSparringRepo[];
|
|
125
|
+
round: number;
|
|
126
|
+
verdict: { call: string; note: string } | null;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
/** El sparring → lo que pinta su sección: la tira de árbitro, rudo y juez, sus repos y la ronda. */
|
|
130
|
+
export const deriveSparring = (
|
|
131
|
+
sparring: RingSparring,
|
|
132
|
+
now: number,
|
|
133
|
+
): SparringView => ({
|
|
134
|
+
slug: sparring.slug,
|
|
135
|
+
repos: sparring.repos,
|
|
136
|
+
round: sparring.round,
|
|
137
|
+
verdict: sparring.verdict,
|
|
138
|
+
...deriveStrip(sparring.events, SPARRING_CHIPS, [], [], now),
|
|
139
|
+
});
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|