chesspal 0.3.4 → 0.3.6

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.
Files changed (104) hide show
  1. package/README.md +1 -0
  2. package/dist/cli/cli.js +10 -2
  3. package/dist/cli/command-line.js +17 -9
  4. package/dist/cli/icon.d.ts +2 -0
  5. package/dist/cli/icon.js +11 -1
  6. package/dist/commands/analysis/analyse.d.ts +2 -2
  7. package/dist/commands/analysis/analyse.js +3 -3
  8. package/dist/commands/answers/agents.d.ts +10 -2
  9. package/dist/commands/answers/agents.js +30 -7
  10. package/dist/commands/answers/index.d.ts +1 -1
  11. package/dist/commands/answers/index.js +1 -1
  12. package/dist/commands/answers/lines.js +3 -2
  13. package/dist/commands/answers/summary.d.ts +2 -1
  14. package/dist/commands/answers/view.d.ts +1 -1
  15. package/dist/commands/answers/view.js +2 -2
  16. package/dist/commands/game/look.d.ts +7 -3
  17. package/dist/commands/game/look.js +9 -4
  18. package/dist/commands/game/new.js +1 -1
  19. package/dist/commands/game/ponder.js +3 -3
  20. package/dist/commands/game/position.d.ts +2 -1
  21. package/dist/commands/game/position.js +9 -4
  22. package/dist/commands/game/status.d.ts +3 -1
  23. package/dist/commands/game/status.js +19 -9
  24. package/dist/commands/profiles/past-games.d.ts +7 -2
  25. package/dist/commands/profiles/past-games.js +26 -10
  26. package/dist/commands/talk/ai-say.js +1 -1
  27. package/dist/games/analysis.d.ts +7 -4
  28. package/dist/games/analysis.js +5 -3
  29. package/dist/games/index.d.ts +1 -1
  30. package/dist/games/index.js +1 -1
  31. package/dist/games/thoughts.d.ts +12 -4
  32. package/dist/games/thoughts.js +27 -10
  33. package/dist/home/index.d.ts +1 -0
  34. package/dist/home/index.js +1 -0
  35. package/dist/home/same-path.d.ts +2 -0
  36. package/dist/home/same-path.js +5 -0
  37. package/dist/host-files/host-files.js +8 -3
  38. package/dist/page/app/anchor.js +15 -11
  39. package/dist/page/app/app.js +30 -17
  40. package/dist/page/app/served.js +23 -36
  41. package/dist/page/app/watch.js +97 -0
  42. package/dist/page/avatars/torretta.css +108 -0
  43. package/dist/page/board/board.css +7 -6
  44. package/dist/page/board/marks.js +2 -2
  45. package/dist/page/board/pointer.js +2 -2
  46. package/dist/page/dialogs/analyses.js +9 -7
  47. package/dist/page/dialogs/rows.js +72 -0
  48. package/dist/page/dialogs/site-games.js +3 -1
  49. package/dist/page/dialogs/studied.js +11 -6
  50. package/dist/page/dialogs/study.js +2 -2
  51. package/dist/page/game/game.css +51 -10
  52. package/dist/page/game/move-list.js +7 -5
  53. package/dist/page/guide/en.md +1 -1
  54. package/dist/page/guide/it.md +1 -1
  55. package/dist/page/helps/helps.css +5 -5
  56. package/dist/page/helps/helps.js +3 -181
  57. package/dist/page/helps/rows.js +16 -0
  58. package/dist/page/helps/strategy.js +105 -0
  59. package/dist/page/helps/tactics.js +69 -0
  60. package/dist/page/layout/header.js +49 -6
  61. package/dist/page/layout/layout.css +17 -2
  62. package/dist/page/layout/window.js +4 -4
  63. package/dist/page/lead/lead.js +41 -4
  64. package/dist/page/sounds/sounds.js +1 -1
  65. package/dist/page/style.css +1 -0
  66. package/dist/page/talk/talk.css +0 -109
  67. package/dist/page/text.js +10 -0
  68. package/dist/page/theme.css +13 -0
  69. package/dist/profiles/profiles.js +5 -1
  70. package/dist/server/bell/bell.d.ts +0 -2
  71. package/dist/server/bell/bell.js +17 -7
  72. package/dist/server/bell/index.d.ts +1 -1
  73. package/dist/server/bell/index.js +1 -1
  74. package/dist/server/page/chat.d.ts +5 -1
  75. package/dist/server/page/chat.js +5 -1
  76. package/dist/server/page/game.js +18 -3
  77. package/dist/server/page/table.d.ts +2 -1
  78. package/dist/server/page/table.js +4 -2
  79. package/dist/server/page/update.d.ts +1 -1
  80. package/dist/server/page/update.js +4 -4
  81. package/dist/server/table/hosts/chats.d.ts +5 -0
  82. package/dist/server/table/hosts/chats.js +11 -4
  83. package/dist/server/table/hosts/play.d.ts +3 -1
  84. package/dist/server/table/hosts/play.js +17 -5
  85. package/dist/server/table/icon/index.d.ts +2 -0
  86. package/dist/server/table/icon/index.js +16 -2
  87. package/dist/server/table/index.d.ts +1 -1
  88. package/dist/server/table/index.js +1 -1
  89. package/dist/server/table/link.js +3 -1
  90. package/dist/server/table/table.js +2 -2
  91. package/dist/sites/index.d.ts +1 -1
  92. package/dist/sites/index.js +1 -1
  93. package/dist/sites/{chesscom.d.ts → ratings/chesscom.d.ts} +2 -2
  94. package/dist/sites/{chesscom.js → ratings/chesscom.js} +1 -1
  95. package/dist/sites/{choice.d.ts → ratings/choice.d.ts} +1 -1
  96. package/dist/sites/{choice.js → ratings/choice.js} +1 -1
  97. package/dist/sites/ratings/index.d.ts +1 -0
  98. package/dist/sites/ratings/index.js +2 -0
  99. package/dist/sites/{lichess.d.ts → ratings/lichess.d.ts} +2 -2
  100. package/dist/sites/{lichess.js → ratings/lichess.js} +1 -1
  101. package/dist/sites/{sites.d.ts → ratings/ratings.d.ts} +2 -2
  102. package/dist/sites/{sites.js → ratings/ratings.js} +1 -1
  103. package/instructions/start.md +1 -1
  104. package/package.json +1 -1
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime";
2
2
  // Who is ahead as the AI estimates it (D-90): a two-colour bar in the game's card, with the helps on and in analysis, and in analysis a chart of the game,
3
3
  // as chess.com draws them, that goes to the move clicked. The page draws the AI's figures, never computes them (D-6, D-19).
4
+ import { useRef, useState } from "preact/hooks";
4
5
  import { FoldButton, useCardFold } from "../game/card-fold.js";
5
6
  import { chartBadges, sideAhead } from "./view.js";
6
7
  /**
@@ -15,10 +16,14 @@ export function LeadBar({ standing, shown, text }) {
15
16
  }
16
17
  /**
17
18
  * The chart of the game in analysis, in a card that folds: White's share as the white area, the moves marked by a badge as dots, kept whole inside its
18
- * edges, the move shown as a line.
19
+ * edges, the move shown as a line, and under the mouse a fainter line on the move a click would show. A finger or the mouse drawn along it runs through the
20
+ * game.
19
21
  */
20
22
  export function LeadChart({ plies, standings, shown, onShow, text }) {
21
23
  const [folded, fold] = useCardFold("chart");
24
+ const [pointed, setPointed] = useState(undefined);
25
+ // A finger or the pressed mouse on the chart: where it went down, whether it runs through the game yet, and the move it showed last.
26
+ const pressed = useRef(undefined);
22
27
  const width = Math.max(plies.length, 1);
23
28
  const heightOf = (share) => 1 - share;
24
29
  const first = standings[0];
@@ -34,11 +39,43 @@ export function LeadChart({ plies, standings, shown, onShow, text }) {
34
39
  const at = standings.find((standing) => standing.plies === index + 1);
35
40
  return badge === undefined || at === undefined || !chartBadges.has(badge) ? [] : [{ badge, at }];
36
41
  });
37
- const go = (event) => {
42
+ // The move at the pointer: how many moves the board would show.
43
+ const plyAt = (event) => {
38
44
  const box = event.currentTarget.getBoundingClientRect();
39
- onShow(Math.round(((event.clientX - box.left) / box.width) * width));
45
+ return Math.min(width, Math.max(0, Math.round(((event.clientX - box.left) / box.width) * width)));
40
46
  };
41
- return (_jsxs("section", { class: `card lead-card${folded ? " folded" : ""}`, children: [_jsx("h3", { class: "card-title", children: _jsxs(FoldButton, { folded: folded, onFold: fold, text: text, children: [_jsx(ChartIcon, {}), text.lead.chart] }) }), folded ? null : (_jsxs("div", { class: "lead-chart", role: "img", "aria-label": text.lead.chartHint, title: text.lead.chartHint, onClick: go, children: [_jsxs("svg", { viewBox: `0 0 ${width} 2`, preserveAspectRatio: "none", "aria-hidden": "true", children: [_jsx("path", { class: "white-area", d: area }), last.plies < width ? _jsx("rect", { class: "pending", x: last.plies, y: "0", width: width - last.plies, height: "2" }) : null, _jsx("path", { class: "even", d: `M0 1 L${width} 1`, "vector-effect": "non-scaling-stroke" })] }), _jsx("span", { class: "shown", style: { left: inside((shown / width) * 100, 2) }, "aria-hidden": "true" }), dots.map(({ badge, at }) => (_jsx("span", { class: `dot ${badge}`, style: { left: inside((at.plies / width) * 100, 6), top: inside((heightOf(at.share) / 2) * 100, 6) }, "aria-hidden": "true" }, at.plies)))] }))] }));
47
+ const showAt = (at) => {
48
+ if (pressed.current !== undefined && pressed.current.showed !== at) {
49
+ pressed.current.showed = at;
50
+ onShow(at);
51
+ }
52
+ };
53
+ // The mouse shows its move as it goes down; a finger once it runs across, as one that runs up or down scrolls the page, or as it lifts, a tap.
54
+ const press = (event) => {
55
+ event.currentTarget.setPointerCapture(event.pointerId);
56
+ pressed.current = { x: event.clientX, y: event.clientY, along: event.pointerType === "mouse" };
57
+ setPointed(undefined);
58
+ if (pressed.current.along) {
59
+ showAt(plyAt(event));
60
+ }
61
+ };
62
+ const point = (event) => {
63
+ const press = pressed.current;
64
+ if (press === undefined) {
65
+ setPointed(event.pointerType === "mouse" ? plyAt(event) : undefined);
66
+ return;
67
+ }
68
+ const across = Math.abs(event.clientX - press.x);
69
+ press.along ||= across > 8 && across > Math.abs(event.clientY - press.y);
70
+ if (press.along) {
71
+ showAt(plyAt(event));
72
+ }
73
+ };
74
+ const lift = (event) => {
75
+ showAt(plyAt(event));
76
+ pressed.current = undefined;
77
+ };
78
+ return (_jsxs("section", { class: `card lead-card${folded ? " folded" : ""}`, children: [_jsx("h3", { class: "card-title", children: _jsxs(FoldButton, { folded: folded, onFold: fold, text: text, children: [_jsx(ChartIcon, {}), text.lead.chart] }) }), folded ? null : (_jsxs("div", { class: "lead-chart", role: "img", "aria-label": text.lead.chartHint, title: text.lead.chartHint, onPointerDown: press, onPointerMove: point, onPointerUp: lift, onPointerCancel: () => void (pressed.current = undefined), onPointerLeave: () => setPointed(undefined), children: [_jsxs("svg", { viewBox: `0 0 ${width} 2`, preserveAspectRatio: "none", "aria-hidden": "true", children: [_jsx("path", { class: "white-area", d: area }), last.plies < width ? _jsx("rect", { class: "pending", x: last.plies, y: "0", width: width - last.plies, height: "2" }) : null, _jsx("path", { class: "even", d: `M0 1 L${width} 1`, "vector-effect": "non-scaling-stroke" })] }), pointed === undefined || pointed === shown ? null : _jsx("span", { class: "shown ghost", style: { left: inside((pointed / width) * 100, 2) }, "aria-hidden": "true" }), _jsx("span", { class: "shown", style: { left: inside((shown / width) * 100, 2) }, "aria-hidden": "true" }), dots.map(({ badge, at }) => (_jsx("span", { class: `dot ${badge}`, style: { left: inside((at.plies / width) * 100, 6), top: inside((heightOf(at.share) / 2) * 100, 6) }, "aria-hidden": "true" }, at.plies)))] }))] }));
42
79
  }
43
80
  /** A place along the chart, in percent, kept whole inside its edges by so many pixels. */
44
81
  function inside(percent, pixels) {
@@ -18,7 +18,7 @@ export function endSoundOf(how) {
18
18
  /** Lets the page play sounds: loads the recordings, and since a browser plays none before the player has touched the page, the first touch wakes them. */
19
19
  export function wakeSounds() {
20
20
  loadRecordings();
21
- addEventListener("pointerdown", () => void audio()?.resume(), { once: true });
21
+ window.addEventListener("pointerdown", () => void audio()?.resume(), { once: true });
22
22
  }
23
23
  /** Plays a sound: its recordings together, else its tone; a browser without Web Audio plays none. */
24
24
  export function playSound(sound) {
@@ -3,6 +3,7 @@
3
3
  @import "base.css";
4
4
  @import "layout/layout.css";
5
5
  @import "board/board.css";
6
+ @import "avatars/torretta.css";
6
7
  @import "talk/talk.css";
7
8
  @import "talk/chat.css";
8
9
  @import "table/table.css";
@@ -266,115 +266,6 @@
266
266
  color: var(--accent);
267
267
  }
268
268
 
269
- /* The Torretta, whose eyes blink now and then, and its moods (D-70): a new face that pops in, never fading out, a red light when worried, out when sad, a light that breathes when
270
- thoughtful */
271
- .torretta .antenna {
272
- fill: none;
273
- stroke: var(--torretta-shade);
274
- stroke-width: 4;
275
- stroke-linecap: round;
276
- }
277
-
278
- .torretta .light {
279
- fill: var(--torretta-light);
280
- transition: fill 0.2s;
281
- }
282
-
283
- .torretta.mood-worried .light {
284
- fill: var(--torretta-alarm);
285
- }
286
-
287
- .torretta.mood-thoughtful .light {
288
- animation: ponder 2.4s ease-in-out infinite;
289
- }
290
-
291
- .torretta.mood-sad .light {
292
- fill: var(--torretta-shade);
293
- }
294
-
295
- @keyframes ponder {
296
- 50% {
297
- fill: var(--torretta-ponder);
298
- }
299
- }
300
-
301
- .torretta .expression.changed {
302
- transform-box: fill-box;
303
- transform-origin: center;
304
- animation: face-in 0.16s ease-out;
305
- }
306
-
307
- @keyframes face-in {
308
- from {
309
- transform: scale(0.8);
310
- }
311
- }
312
-
313
- /* Its bust on a player's card: a disc of the player's picture's colour, which cuts what falls out of it */
314
- .torretta.bust {
315
- clip-path: circle(50%);
316
- }
317
-
318
- .torretta .back {
319
- fill: var(--say);
320
- }
321
-
322
- .torretta .shade {
323
- fill: var(--torretta-shade);
324
- }
325
-
326
- .torretta .body {
327
- fill: var(--torretta-body);
328
- }
329
-
330
- .torretta .screen {
331
- fill: var(--torretta-screen);
332
- }
333
-
334
- .torretta .eyes,
335
- .torretta .mouth {
336
- fill: var(--torretta-face);
337
- }
338
-
339
- .torretta .expression .line,
340
- .torretta .brows {
341
- fill: none;
342
- stroke: var(--torretta-face);
343
- stroke-width: 3.4;
344
- stroke-linecap: round;
345
- stroke-linejoin: round;
346
- }
347
-
348
- .torretta .eyes {
349
- transform-box: fill-box;
350
- transform-origin: center;
351
- animation: blink 6s infinite;
352
- }
353
-
354
- @keyframes blink {
355
- 0%,
356
- 95%,
357
- 100% {
358
- transform: scaleY(1);
359
- }
360
-
361
- 97% {
362
- transform: scaleY(0.1);
363
- }
364
- }
365
-
366
- @media (prefers-reduced-motion: reduce) {
367
- .torretta .eyes,
368
- .torretta .expression.changed,
369
- .torretta.mood-thoughtful .light {
370
- animation: none;
371
- }
372
-
373
- .torretta .light {
374
- transition: none;
375
- }
376
- }
377
-
378
269
  /* The bubbles: the AI's under its card, the tail under the Torretta's centre (14 px of padding, 48 px picture), and the player's above their box */
379
270
  .says,
380
271
  .sent {
package/dist/page/text.js CHANGED
@@ -40,6 +40,7 @@ const english = {
40
40
  declineDrawTitle: "Decline: moving declines it too",
41
41
  resign: "Resign",
42
42
  resignGame: "Resign this game?",
43
+ keepPlaying: "Play on",
43
44
  settings: "Settings",
44
45
  close: "Close",
45
46
  look: "Look",
@@ -195,6 +196,10 @@ const english = {
195
196
  typeIt: "Or type in a terminal:",
196
197
  },
197
198
  commandHere: (name) => `/clear and the other commands go in the window where you opened ${name}, not here.`,
199
+ link: {
200
+ going: "A game is going on: finish it, then open the link again.",
201
+ gone: "The game of this link is not here: it was deleted, or it is another profile's.",
202
+ },
198
203
  refused: {
199
204
  "no-game": () => "There is no game going on: press + to start one, or ask for one in the chat.",
200
205
  "ai-to-move": () => "Wait for the AI's move.",
@@ -465,6 +470,7 @@ const italian = {
465
470
  declineDrawTitle: "Rifiuta: anche muovere rifiuta",
466
471
  resign: "Abbandona",
467
472
  resignGame: "Abbandonare la partita?",
473
+ keepPlaying: "Continua",
468
474
  settings: "Impostazioni",
469
475
  close: "Chiudi",
470
476
  look: "Aspetto",
@@ -620,6 +626,10 @@ const italian = {
620
626
  typeIt: "Oppure scrivi in un terminale:",
621
627
  },
622
628
  commandHere: (name) => `/clear e gli altri comandi vanno scritti nella finestra dove hai aperto ${name}, non qui.`,
629
+ link: {
630
+ going: "C'è una partita in corso: finiscila, poi riapri il link.",
631
+ gone: "La partita di questo link non c'è: è stata cancellata, o è di un altro profilo.",
632
+ },
623
633
  refused: {
624
634
  "no-game": () => "Nessuna partita in corso: premi + per iniziarne una, o chiedila nella chat.",
625
635
  "ai-to-move": () => "Aspetta la mossa dell'AI.",
@@ -62,6 +62,19 @@
62
62
  --badge-mistake: #d4650f;
63
63
  --badge-miss: #c2407d;
64
64
  --badge-blunder: #d22c2c;
65
+ /* The kings' badges on a game over, the same in both themes: the winner's gold crown, the mate in red on white, the flag that fell, the draw */
66
+ --king-crown: #e0a526;
67
+ --king-crown-ink: #3b2a00;
68
+ --king-mate: #c81e1e;
69
+ --king-flag: #4a4f57;
70
+ --king-draw: #69727d;
71
+ /* The two sides as the game's column draws them, in the bar and the chart of who is ahead and in the helps' bars and tiles, the same in both themes:
72
+ White, Black, and both, each half 3:1 at least with the tile's green tick; their edge keeps the white half in sight on a light card and the black one
73
+ on a dark card. The board's map and counts keep their own, darker on the squares (Zoe) */
74
+ --side-white: #f5f5f2;
75
+ --side-black: #33383f;
76
+ --side-both: #8a9099;
77
+ --side-edge: color-mix(in srgb, var(--side-both) 60%, transparent);
65
78
  --shadow: 0 1px 2px rgba(29, 39, 51, 0.06), 0 6px 18px rgba(29, 39, 51, 0.06);
66
79
  /* On a light page the board keeps a thin edge in its dark squares colour, and a deeper shadow, so its light squares do not melt into the page */
67
80
  --board-shadow: 0 0 0 1px color-mix(in oklab, var(--dark-square) 55%, transparent), 0 2px 4px rgba(29, 39, 51, 0.1), 0 10px 28px rgba(29, 39, 51, 0.12);
@@ -23,8 +23,12 @@ export function listProfiles(root) {
23
23
  * together make one: the second finds its folder taken.
24
24
  */
25
25
  export function ensureProfile(root, account) {
26
+ // Every command asks, so the common answer comes first: a profile is there.
27
+ if (profileIds(root).length > 0) {
28
+ return;
29
+ }
26
30
  const id = freeId(root, slugOf(account));
27
- if (profileIds(root).length === 0 && claimed(root, id)) {
31
+ if (claimed(root, id)) {
28
32
  fillProfile(root, id, { name: account }, true);
29
33
  }
30
34
  }
@@ -85,5 +85,3 @@ export type BellHeard = {
85
85
  export declare function bellHeard(input: string, ports?: number[]): Promise<BellHeard>;
86
86
  /** Adds a line to the log of the hooks that heard no bell, keeping its last lines; a log that cannot be written never stops the hook. */
87
87
  export declare function noteSilence(folder: string, why: string, now?: Date): void;
88
- /** Whether two paths name the same folder: without regard to case on Windows. */
89
- export declare function samePath(one: string, other: string): boolean;
@@ -5,7 +5,7 @@ import { existsSync, readFileSync } from "node:fs";
5
5
  import { get } from "node:http";
6
6
  import { join, resolve } from "node:path";
7
7
  import { writeAtomically } from "../../games/index.js";
8
- import { dataFolder, tableFolder } from "../../home/index.js";
8
+ import { dataFolder, samePath, tableFolder } from "../../home/index.js";
9
9
  import { analystAgent } from "../../host-files/index.js";
10
10
  import { version } from "../../version.js";
11
11
  /** What the chat hears from the doorbell: one line for whatever the player did, never what, only where to read it. */
@@ -287,15 +287,11 @@ function probeOf(said) {
287
287
  return undefined;
288
288
  }
289
289
  }
290
- /** Whether two paths name the same folder: without regard to case on Windows. */
291
- export function samePath(one, other) {
292
- return process.platform === "win32" ? resolve(one).toLowerCase() === resolve(other).toLowerCase() : resolve(one) === resolve(other);
293
- }
294
290
  /** The body a page of this computer answers with, once it ends; undefined when it does not answer, or not with 200. */
295
291
  function body(port, path, wait) {
296
292
  return new Promise((done) => {
297
293
  // A connection of its own: Node's shared one ends a request idle for five seconds, and the doorbell waits for hours.
298
- const request = get({ host: "127.0.0.1", port, path, agent: false, ...(wait === undefined ? {} : { timeout: wait }) }, (response) => {
294
+ const request = get({ host: "127.0.0.1", port, path, agent: false }, (response) => {
299
295
  let text = "";
300
296
  response.setEncoding("utf8");
301
297
  response.on("data", (chunk) => {
@@ -305,8 +301,22 @@ function body(port, path, wait) {
305
301
  response.on("error", () => done(undefined));
306
302
  });
307
303
  if (wait !== undefined) {
308
- request.on("timeout", () => request.destroy());
304
+ giveUpAfterTurns(request, wait);
309
305
  }
310
306
  request.on("error", () => done(undefined));
311
307
  });
312
308
  }
309
+ /**
310
+ * Ends a request still unanswered after `wait`, counted in ten turns of this process's clock: after a stall (a busy computer, one waking up), a timer
311
+ * fires before the answer waiting is read, and a page of this process, or one that answered meanwhile, would be taken for one that does not answer.
312
+ */
313
+ function giveUpAfterTurns(request, wait) {
314
+ let turns = 0;
315
+ const timer = setInterval(() => {
316
+ turns++;
317
+ if (turns === 10) {
318
+ request.destroy();
319
+ }
320
+ }, wait / 10);
321
+ request.on("close", () => clearInterval(timer));
322
+ }
@@ -1 +1 @@
1
- export { Bell, bellProbe, listen, pageAt, pagePorts, samePath, type TableChat } from "./bell.ts";
1
+ export { Bell, bellProbe, listen, pageAt, pagePorts, type TableChat } from "./bell.ts";
@@ -1,2 +1,2 @@
1
1
  // The doorbell that wakes a chat when the player acts on the page (D-47), and the ports where a folder's page answers.
2
- export { Bell, bellProbe, listen, pageAt, pagePorts, samePath } from "./bell.js";
2
+ export { Bell, bellProbe, listen, pageAt, pagePorts } from "./bell.js";
@@ -20,13 +20,17 @@ export declare function chatSeenOf(chat: Context): ChatSeen;
20
20
  export declare function pageContext(chat: Context): Context;
21
21
  /** The context of a new game started on the page: the page's, with what the chat saw and its host, by which the game before asks for a clean chat (D-57). */
22
22
  export declare function asTheChatSees(context: Context, chat: ChatSeen): Context;
23
- /** What a chat says of itself as it attaches to the table (D-104): its version, its session, its folder, its host, and whether its folder needs init again. */
23
+ /**
24
+ * What a chat says of itself as it attaches to the table (D-104): its version, its session, its folder, its host, whether its folder needs init again,
25
+ * and the seat of the `chesspal play` that started it, if one did.
26
+ */
24
27
  export declare const chatAttach: z.ZodObject<{
25
28
  version: z.ZodString;
26
29
  session: z.ZodNullable<z.ZodString>;
27
30
  folder: z.ZodString;
28
31
  host: z.ZodOptional<z.ZodString>;
29
32
  outdated: z.ZodBoolean;
33
+ seat: z.ZodOptional<z.ZodInt>;
30
34
  }, z.core.$strip>;
31
35
  export type ChatAttach = z.infer<typeof chatAttach>;
32
36
  /**
@@ -42,13 +42,17 @@ export function pageContext(chat) {
42
42
  export function asTheChatSees(context, chat) {
43
43
  return { ...context, host: () => chat.host(), positionSeen: chat.positionSeen };
44
44
  }
45
- /** What a chat says of itself as it attaches to the table (D-104): its version, its session, its folder, its host, and whether its folder needs init again. */
45
+ /**
46
+ * What a chat says of itself as it attaches to the table (D-104): its version, its session, its folder, its host, whether its folder needs init again,
47
+ * and the seat of the `chesspal play` that started it, if one did.
48
+ */
46
49
  export const chatAttach = z.object({
47
50
  version: z.string(),
48
51
  session: z.string().nullable(),
49
52
  folder: z.string(),
50
53
  host: z.string().optional(),
51
54
  outdated: z.boolean(),
55
+ seat: z.int().optional(),
52
56
  });
53
57
  const seenPosition = z.object({ started: z.string().optional(), plies: z.int().min(0) });
54
58
  /**
@@ -1,9 +1,10 @@
1
1
  import { analyseGame, analystOf, answerDraw, bookPlies, ChatNotClear, DrawRefused, endGame, gameAt, gameToStudy, namedGame, newGame, offerDraw, playMove, samePosition, say, shownPosition, startNewGame, } from "../../commands/index.js";
2
2
  import { clockStarted, isReviewed, writePgn } from "../../core/index.js";
3
- import { aiModelOf, archiveName, coachShown, coachWords, drawOfferOf, endOf, gameInAnalysis, heardLines, loadGame, loadTableTalk, playerPgn, ponderingOn, rerunIfChanged, saveGame, startAnalysis, talkLimits, thoughtKey, wakesTheChat, WordsRefused, } from "../../games/index.js";
3
+ import { aiModelOf, archiveName, coachShown, coachWords, drawOfferOf, endOf, gameInAnalysis, heardLines, loadGame, loadTableTalk, playerPgn, ponderingUntil, rerunIfChanged, saveGame, startAnalysis, talkLimits, thoughtKey, wakesTheChat, WordsRefused, } from "../../games/index.js";
4
4
  import { gameFolder, helpsIn } from "../../profiles/index.js";
5
5
  import { version } from "../../version.js";
6
6
  import { asTheChatSees } from "./chat.js";
7
+ import { tellWatchers } from "./events.js";
7
8
  import { bodyOf, bytesOf, isJson, json, jsonOf, runRoute, send, textOf } from "./http.js";
8
9
  import { profilesShown } from "./profiles.js";
9
10
  import { chatToRelaunch, relaunchChat, tableShown } from "./table.js";
@@ -233,6 +234,8 @@ async function studyFromPage(request, response, served) {
233
234
  const last = body?.last === true;
234
235
  // A game a link of the page opens is there to look at: the chat is not asked to go over it.
235
236
  const look = body?.look === true;
237
+ // «Analyse again», the review just deleted: the chat starts the analyst without asking, even after one was stopped (Priya).
238
+ const redo = body?.redo === true;
236
239
  if (pgn === undefined && game === undefined && !last) {
237
240
  send(response, 400, json, JSON.stringify({ error: asked, refusal: "pgn-unreadable" }));
238
241
  return;
@@ -249,7 +252,7 @@ async function studyFromPage(request, response, served) {
249
252
  reviewed = isReviewed(opened.record);
250
253
  brought = !reviewed && !look;
251
254
  // A game not yet reviewed is marked as brought, for status to ask the AI to go over it; one reviewed is ready, and the chat sleeps on.
252
- startAnalysis(gameFolder(served.context.home()), opened.name, served.context.now(), { brought });
255
+ startAnalysis(gameFolder(served.context.home()), opened.name, served.context.now(), { brought, redo });
253
256
  }
254
257
  catch (error) {
255
258
  send(response, 400, json, JSON.stringify({ error: error.message, refusal: "pgn-unreadable" }));
@@ -327,7 +330,19 @@ function thinkingShown(served, current) {
327
330
  const onIt = served.bell.woke || (seen !== undefined && seen !== null && samePosition(seen, position));
328
331
  return served.bell.inTurn && onIt ? { thinking: "move" } : {};
329
332
  }
330
- return ponderingOn(gameFolder(served.context.home()), thoughtKey(current)) ? { thinking: "ponder" } : {};
333
+ const until = ponderingUntil(gameFolder(served.context.home()), thoughtKey(current));
334
+ const left = until === undefined ? 0 : until.getTime() - served.context.now().getTime();
335
+ if (left <= 0) {
336
+ return {};
337
+ }
338
+ signGoesIn(served, left);
339
+ return { thinking: "ponder" };
340
+ }
341
+ // The pages hear when the sign of an agent that stopped pondering without a word goes, which no file tells: one timer a table, the latest.
342
+ const signTimers = new WeakMap();
343
+ function signGoesIn(served, wait) {
344
+ clearTimeout(signTimers.get(served));
345
+ signTimers.set(served, setTimeout(() => tellWatchers(served.watchers), wait + 100).unref());
331
346
  }
332
347
  /**
333
348
  * What the coach wrote that the player may see now, in a game going on with the helps on (D-88): its judgments, those whose note names a move they may still
@@ -19,10 +19,11 @@ export interface TableChats {
19
19
  /** Asks for the chat again: with another host, or `updated`, from the Chesspal just installed (D-108). */
20
20
  relaunch(folder: string, host?: string, updated?: boolean): boolean;
21
21
  }
22
- /** The chat `chesspal play` holds: the model and effort its host started with, none for the host's own. */
22
+ /** The chat `chesspal play` holds: the model and effort its host started with, none for the host's own, and the seat, the play's process. */
23
23
  export interface HeldChat {
24
24
  readonly model?: string | undefined;
25
25
  readonly effort?: string | undefined;
26
+ readonly seat?: number;
26
27
  }
27
28
  /** A chat opening at the table: one the page asked for, the one started again for a new game, or with another model or host (D-107). */
28
29
  export type Opening = "chat" | "again" | "restart";
@@ -72,7 +72,7 @@ export function relaunchChat(served, how = "again", host) {
72
72
  const folder = served.attached?.chat.folder;
73
73
  // Another host may first ask whether it trusts the table's folder: the pages say so, as for a chat opened.
74
74
  const switching = how === "restart" && host !== undefined && host !== hostAtTable(served);
75
- if (folder === undefined || served.table?.chats?.relaunch(folder, host) !== true) {
75
+ if (folder === undefined || heldChat(served) === undefined || served.table?.chats?.relaunch(folder, host) !== true) {
76
76
  return false;
77
77
  }
78
78
  noteOpening(served, switching ? "chat" : how);
@@ -252,9 +252,11 @@ function hostAtTable(served) {
252
252
  return served.attached === undefined ? undefined : served.table?.chats?.hosts().find((one) => said.includes(one.name))?.name;
253
253
  }
254
254
  // The chat at the table when `chesspal play` holds it, which the table can start again: the model and effort it started with.
255
+ // The chat at the table when it is the one `chesspal play` holds: in its folder and started by it, not another opened there by hand.
255
256
  function heldChat(served) {
256
257
  const { attached } = served;
257
- return attached === undefined ? undefined : served.table?.chats?.held(attached.chat.folder);
258
+ const held = attached === undefined ? undefined : served.table?.chats?.held(attached.chat.folder);
259
+ return held === undefined || held.seat !== attached?.chat.seat ? undefined : { model: held.model, effort: held.effort };
258
260
  }
259
261
  // The chat waits for the doorbell, out of a turn, and no analyst of it is at work: starting it again loses nothing.
260
262
  function chatAtRest(served) {
@@ -8,7 +8,7 @@ export interface UpdateShown {
8
8
  readonly installing?: true;
9
9
  readonly installed?: true;
10
10
  }
11
- /** Asks npm for Chesspal's latest version now and once a day after, the pages told when it is newer than this one; how to stop. None in the tests. */
11
+ /** Asks npm for Chesspal's latest version now and every hour after, the pages told when it is newer than this one; how to stop. None in the tests. */
12
12
  export declare function watchForUpdates(served: Served, every?: number): () => void;
13
13
  /** The newer Chesspal the page shows, when npm has one. */
14
14
  export declare function updateShown(served: Served): {
@@ -1,10 +1,10 @@
1
- // A newer Chesspal on npm (D-108): the page's server asks as it starts and once a day after; the table's page offers to install it, a chat's own page says
1
+ // A newer Chesspal on npm (D-108): the page's server asks as it starts and every hour after; the table's page offers to install it, a chat's own page says
2
2
  // the command to type.
3
3
  import { answerTime, askSite } from "../../sites/index.js";
4
4
  import { isOlder, version } from "../../version.js";
5
5
  import { tellWatchers } from "./events.js";
6
- /** Asks npm for Chesspal's latest version now and once a day after, the pages told when it is newer than this one; how to stop. None in the tests. */
7
- export function watchForUpdates(served, every = aDay) {
6
+ /** Asks npm for Chesspal's latest version now and every hour after, the pages told when it is newer than this one; how to stop. None in the tests. */
7
+ export function watchForUpdates(served, every = anHour) {
8
8
  const fetch = served.context.fetch;
9
9
  if (fetch === undefined) {
10
10
  return () => undefined;
@@ -37,4 +37,4 @@ export async function latestVersion(fetch) {
37
37
  return answer?.status === 200 && typeof said === "string" && /^\d+\.\d+\.\d+$/.test(said) ? said : undefined;
38
38
  }
39
39
  const latestUrl = "https://registry.npmjs.org/chesspal/latest";
40
- const aDay = 24 * 60 * 60 * 1000;
40
+ const anHour = 60 * 60 * 1000;
@@ -5,3 +5,8 @@ import type { TableChats } from "../../page/index.ts";
5
5
  * terminal opened with `chesspal play`, run by `chesspal`, the same Node and script as the table's.
6
6
  */
7
7
  export declare function terminalChats(context: Context, chesspal?: readonly string[], env?: NodeJS.ProcessEnv): TableChats;
8
+ /**
9
+ * What the terminal runs: `chesspal play <host>`, with the table's data folder when it has one of its own, as a playtest's: Terminal.app and Windows
10
+ * Terminal start the chat without the table's environment.
11
+ */
12
+ export declare function playCommand(chesspal: readonly string[], host: string, env: NodeJS.ProcessEnv): string[];
@@ -1,5 +1,4 @@
1
- import { tableFolder } from "../../../home/index.js";
2
- import { samePath } from "../../bell/index.js";
1
+ import { samePath, tableFolder } from "../../../home/index.js";
3
2
  import { programPath } from "./find.js";
4
3
  import { hostChoices, hostNames, isHostName } from "./hosts.js";
5
4
  import { askAgain, seatHeld } from "./seat.js";
@@ -18,15 +17,23 @@ export function terminalChats(context, chesspal = [process.execPath, ...process.
18
17
  }
19
18
  return found.hosts;
20
19
  },
21
- open: async (host) => isHostName(host) && await openTerminal(tableFolder(context.home()), [...chesspal, "play", host], context.platform, env),
20
+ open: async (host) => isHostName(host) && await openTerminal(tableFolder(context.home()), playCommand(chesspal, host, env), context.platform, env),
22
21
  // Only a chat in the table's folder can be the one chesspal play holds.
23
22
  held: (folder) => {
24
23
  const seat = samePath(folder, tableFolder(context.home())) ? seatHeld(folder, context.now()) : undefined;
25
- return seat === undefined ? undefined : { model: seat.model, effort: seat.effort };
24
+ return seat === undefined ? undefined : { model: seat.model, effort: seat.effort, seat: seat.pid };
26
25
  },
27
26
  relaunch: (folder, host, updated) => samePath(folder, tableFolder(context.home())) && askAgain(folder, context.now(), { ...(host === undefined ? {} : { host }), ...(updated === true ? { updated } : {}) }),
28
27
  };
29
28
  }
29
+ /**
30
+ * What the terminal runs: `chesspal play <host>`, with the table's data folder when it has one of its own, as a playtest's: Terminal.app and Windows
31
+ * Terminal start the chat without the table's environment.
32
+ */
33
+ export function playCommand(chesspal, host, env) {
34
+ const home = env.CHESSPAL_HOME?.trim();
35
+ return [...chesspal, "play", host, ...(home ? ["--home", home] : [])];
36
+ }
30
37
  // Looking on the PATH takes a few milliseconds, and the page asks at every change of the game.
31
38
  const lookAgain = 10_000;
32
39
  function chatHost(name, platform, env) {
@@ -1,5 +1,5 @@
1
1
  import { type ChildProcess } from "node:child_process";
2
- import type { Context } from "../../../commands/index.ts";
2
+ import { type Context } from "../../../commands/index.ts";
3
3
  import { type HostChoice, type HostName } from "./hosts.ts";
4
4
  /**
5
5
  * What `chesspal play` starts: the host's program, its arguments, folder and environment, whether it needs cmd, as a .cmd shim does on Windows, and the
@@ -12,6 +12,8 @@ export interface HostLaunch {
12
12
  readonly env: NodeJS.ProcessEnv;
13
13
  readonly shell: boolean;
14
14
  readonly choice: HostChoice;
15
+ /** Codex's hooks were there and Chesspal wrote them again: Codex runs them only once the player trusts them again. */
16
+ readonly hooksChanged?: true;
15
17
  }
16
18
  /**
17
19
  * Runs the host in the table's folder, in this terminal, until it ends: its exit code; started again, clean, when the table asks for a new game (D-57),
@@ -3,8 +3,9 @@
3
3
  import { spawn, spawnSync } from "node:child_process";
4
4
  import { existsSync } from "node:fs";
5
5
  import { win32 } from "node:path";
6
+ import { speaksItalian } from "../../../commands/index.js";
6
7
  import { tableFolder } from "../../../home/index.js";
7
- import { setUpHosts } from "../../../host-files/index.js";
8
+ import { codexHooksPath, setUpHosts } from "../../../host-files/index.js";
8
9
  import { activeProfile, gameFolder, languageIn, profileSettings } from "../../../profiles/index.js";
9
10
  import { programPath } from "./find.js";
10
11
  import { choiceIn, hostArgs, isHostName, startMessage } from "./hosts.js";
@@ -120,23 +121,34 @@ export function hostLaunch(context, host, env = process.env, exists) {
120
121
  if (found === undefined) {
121
122
  throw new Error(`${hostTitle[host]} is not installed on this computer, or not on its PATH: install it (${installHint[host]}), then try again.`);
122
123
  }
123
- setUpHosts(folder, context.platform);
124
+ const hooks = codexHooksPath(folder);
125
+ const hadHooks = host === "codex" && existsSync(hooks);
126
+ const hooksChanged = setUpHosts(folder, context.platform).includes(hooks) && hadHooks ? { hooksChanged: true } : {};
124
127
  const choice = choiceIn(profileSettings(home, activeProfile(home).id), host);
125
128
  const args = hostArgs(host, startMessage(languageIn(gameFolder(home), context.language)), choice);
126
129
  // An effort the player set for every chat would win over the one chosen on the page: spawn leaves out a variable that is undefined.
127
130
  const effort = host === "claude" && choice.effort !== undefined ? { CLAUDE_CODE_EFFORT_LEVEL: undefined } : {};
128
- const childEnv = { ...env, ...Object.fromEntries(sessionMarks.map((name) => [name, undefined])), ...effort };
131
+ // The seat this play holds, by which its chat's server tells the table it is the chat held, and not another opened in the same folder.
132
+ const childEnv = { ...env, ...Object.fromEntries(sessionMarks.map((name) => [name, undefined])), ...effort, CHESSPAL_SEAT: String(process.pid) };
129
133
  const script = npmScript(host, found, context.platform, exists ?? existsSync);
130
134
  if (script !== undefined) {
131
- return { program: process.execPath, args: [script, ...args], folder, env: childEnv, shell: false, choice };
135
+ return { program: process.execPath, args: [script, ...args], folder, env: childEnv, shell: false, choice, ...hooksChanged };
132
136
  }
133
- return { program: found, args, folder, env: childEnv, shell: context.platform === "win32" && /\.(cmd|bat)$/i.test(found), choice };
137
+ return { program: found, args, folder, env: childEnv, shell: context.platform === "win32" && /\.(cmd|bat)$/i.test(found), choice, ...hooksChanged };
134
138
  }
135
139
  const hostTitle = { claude: "Claude Code", codex: "Codex" };
140
+ const hooksChangedWords = {
141
+ english: "Chesspal's hooks for Codex have changed: when Codex asks about them, choose Trust all again.",
142
+ italian: "Gli «hook» di Chesspal per Codex sono cambiati: quando Codex te lo chiede, scegli di nuovo Trust all.",
143
+ };
136
144
  // Runs the host as runHost does, the seat noting the model and effort it is given; one not installed, or that could not start, fails saying so.
137
145
  async function started(context, host, again) {
138
146
  const launch = hostLaunch(context, host);
139
147
  takeSeat(launch.folder, { pid: process.pid, host, ...launch.choice });
148
+ // After an update the player is asked about the hooks again: said here, above the host's question, as init says it.
149
+ if (launch.hooksChanged === true) {
150
+ process.stdout.write(`${speaksItalian(context.language) ? hooksChangedWords.italian : hooksChangedWords.english}\n`);
151
+ }
140
152
  return runHost(launch, again, context.platform).catch((error) => {
141
153
  throw new Error(`${hostTitle[host]} did not start: ${error.message}`);
142
154
  });
@@ -25,6 +25,8 @@ export declare function iconAsked(home: string, machine?: string): boolean;
25
25
  export declare function noteIconAsked(home: string, machine?: string): void;
26
26
  /** Notes the icon made on this machine, its command and files, which asks no more there. */
27
27
  export declare function noteIconMade(home: string, command: IconCommand, files: readonly string[], machine?: string): void;
28
+ /** Takes Chesspal's icon off the desktop and the menu of this machine, the files noted when it was made: those it removed. */
29
+ export declare function removeIcons(home: string, machine?: string): string[];
28
30
  /**
29
31
  * Whether the icon made on this machine runs another Node or script than `command`, which a Node manager or an update may have taken away, and is still
30
32
  * there: then it is made again. One the player removed stays removed.