chesspal 0.4.1 → 0.6.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.
Files changed (57) hide show
  1. package/README.md +37 -8
  2. package/dist/cli/chat-hook.d.ts +4 -1
  3. package/dist/cli/chat-hook.js +12 -5
  4. package/dist/cli/cli.js +3 -3
  5. package/dist/cli/command-line.js +9 -9
  6. package/dist/commands/game/new.js +3 -5
  7. package/dist/commands/registry/context.d.ts +5 -0
  8. package/dist/commands/registry/context.js +2 -1
  9. package/dist/commands/setup/init.d.ts +4 -1
  10. package/dist/commands/setup/init.js +30 -10
  11. package/dist/core/review.d.ts +2 -2
  12. package/dist/core/review.js +7 -3
  13. package/dist/host-files/host-files.d.ts +10 -6
  14. package/dist/host-files/host-files.js +57 -9
  15. package/dist/host-files/index.d.ts +1 -1
  16. package/dist/host-files/index.js +1 -1
  17. package/dist/page/app/app.js +112 -25
  18. package/dist/page/app/keys.js +16 -1
  19. package/dist/page/app/title.js +6 -3
  20. package/dist/page/board/board.css +6 -0
  21. package/dist/page/game/column.js +3 -3
  22. package/dist/page/game/game.css +121 -1
  23. package/dist/page/game/move-list.js +48 -10
  24. package/dist/page/game/variation.js +65 -0
  25. package/dist/page/guide/en.md +3 -2
  26. package/dist/page/guide/it.md +3 -2
  27. package/dist/page/profile/settings.js +2 -0
  28. package/dist/page/table/action.js +19 -7
  29. package/dist/page/table/window.js +7 -5
  30. package/dist/page/text.js +4 -2
  31. package/dist/server/bell/antigravity.d.ts +10 -0
  32. package/dist/server/bell/antigravity.js +75 -0
  33. package/dist/server/bell/bell.d.ts +10 -2
  34. package/dist/server/bell/bell.js +33 -10
  35. package/dist/server/bell/index.d.ts +1 -1
  36. package/dist/server/bell/index.js +2 -1
  37. package/dist/server/mcp/caller.js +26 -1
  38. package/dist/server/page/game.js +4 -3
  39. package/dist/server/page/page.js +1 -0
  40. package/dist/server/page/table.d.ts +14 -2
  41. package/dist/server/page/table.js +17 -5
  42. package/dist/server/table/hosts/chats.d.ts +4 -2
  43. package/dist/server/table/hosts/chats.js +24 -11
  44. package/dist/server/table/hosts/find.d.ts +6 -0
  45. package/dist/server/table/hosts/find.js +19 -1
  46. package/dist/server/table/hosts/hooks.d.ts +5 -2
  47. package/dist/server/table/hosts/hooks.js +6 -3
  48. package/dist/server/table/hosts/hosts.d.ts +19 -10
  49. package/dist/server/table/hosts/hosts.js +34 -16
  50. package/dist/server/table/hosts/models.d.ts +19 -0
  51. package/dist/server/table/hosts/models.js +95 -0
  52. package/dist/server/table/hosts/play.d.ts +7 -2
  53. package/dist/server/table/hosts/play.js +22 -12
  54. package/dist/server/table/hosts/seat.d.ts +5 -1
  55. package/dist/server/table/hosts/seat.js +1 -1
  56. package/dist/server/table/table.js +1 -1
  57. package/package.json +1 -1
@@ -0,0 +1,65 @@
1
+ import { stepped } from "./browse.js";
2
+ /** Whether the board shows a move of the line, not the game. */
3
+ export function onLine(line) {
4
+ return line !== undefined && line.cursor > 0;
5
+ }
6
+ /**
7
+ * The line after the player moves `san` where the board stands, and how many of the game's moves the board shows by then: on the game, its own next move
8
+ * steps forward and the line's first move goes back into the line; any other starts a new line. On the line, its own next move steps forward and any other
9
+ * cuts the line there and goes on with it.
10
+ */
11
+ export function afterMove(line, game, shown, san) {
12
+ if (line !== undefined && line.cursor > 0) {
13
+ const moves = line.moves[line.cursor] === san ? line.moves : [...line.moves.slice(0, line.cursor), san];
14
+ return { line: { at: line.at, moves, cursor: line.cursor + 1 }, shown: line.at };
15
+ }
16
+ if (game[shown] === san) {
17
+ return { line: line === undefined ? undefined : { ...line, cursor: 0 }, shown: shown + 1 };
18
+ }
19
+ if (line?.at === shown && line.moves[0] === san) {
20
+ return { line: { ...line, cursor: 1 }, shown };
21
+ }
22
+ return { line: { at: shown, moves: [san], cursor: 1 }, shown };
23
+ }
24
+ /** The line after a step back or forward through it: back from its first move the board is on the game again, where the line leaves it. */
25
+ export function stepLine(line, step) {
26
+ return { ...line, cursor: stepped(step, line.cursor, line.moves.length) };
27
+ }
28
+ /** The line with the board back on the game, the line kept faded. */
29
+ export function leaveLine(line) {
30
+ return line === undefined ? undefined : { ...line, cursor: 0 };
31
+ }
32
+ /** The record of the position the board shows: the game's moves up to the line, then the line's up to its cursor. */
33
+ export function lineRecord(record, line) {
34
+ const moves = [...record.moves.slice(0, line.at).map(({ san }) => san), ...line.moves.slice(0, line.cursor)];
35
+ return { tags: record.tags, moves: moves.map((san) => ({ san })) };
36
+ }
37
+ /** The line's moves numbered from the position it leaves the game at, given by its move number and the side to move. */
38
+ export function linePlies(line, start) {
39
+ let { moveNumber: number, turn: side } = start;
40
+ return line.moves.map((san) => {
41
+ const ply = { san, number, side };
42
+ number += side === "black" ? 1 : 0;
43
+ side = side === "white" ? "black" : "white";
44
+ return ply;
45
+ });
46
+ }
47
+ /** The moves written as a PGN writes them, 12...Kf8 13.Nxe5 Bxe2: the number before White's moves, and before the first when it is Black's. */
48
+ export function lineText(plies) {
49
+ return plies.map((ply, index) => `${moveNumberOf(ply, index === 0)}${ply.san}`).join(" ");
50
+ }
51
+ /** The number written before a move of a line, 13. or 12..., none before Black's but the first. */
52
+ export function moveNumberOf(ply, first) {
53
+ return ply.side === "white" ? `${ply.number}.` : first ? `${ply.number}...` : "";
54
+ }
55
+ /** Whether the board goes from one place to another by more than one move, which shows the position at once instead of sliding a move. */
56
+ export function isJump(from, to) {
57
+ if (from.cursor === 0 && to.cursor === 0) {
58
+ return Math.abs(from.at - to.at) > 1;
59
+ }
60
+ if (from.cursor > 0 && to.cursor > 0) {
61
+ return from.at !== to.at || Math.abs(from.cursor - to.cursor) > 1;
62
+ }
63
+ const [game, line] = from.cursor === 0 ? [from, to] : [to, from];
64
+ return !(game.at === line.at && line.cursor === 1);
65
+ }
@@ -18,7 +18,7 @@ No chess engine plays for the AI: every move and every judgment is its own. Ches
18
18
 
19
19
  ### Getting started
20
20
 
21
- 1. Click the **AI's name**, at the top left, and choose its chat among those on your computer: **Claude Code** or **Codex**. It opens in a window of its own: keep it open while you play. The first time it asks some questions. Whether you trust the folder: **Yes** (in Claude Code No is chosen already: go down to Yes with the arrow keys, then Enter). With Codex, about the hooks, Chesspal's two helpers: **Trust all and continue**. On Windows Codex also asks whether to set up its sandbox, the protected space it works in: choose the first option, and yes when Windows asks for permission.
21
+ 1. Click the **AI's name**, at the top left, and choose its chat among those on your computer: **Claude Code**, **Codex** or **Antigravity**. It opens in a window of its own: keep it open while you play. The first time it asks some questions. Whether you trust the folder: **Yes** (in Claude Code No is chosen already: go down to Yes with the arrow keys, then Enter). With Codex, about the hooks, Chesspal's two helpers: **Trust all and continue**. On Windows Codex also asks whether to set up its sandbox, the protected space it works in: choose the first option, and yes when Windows asks for permission.
22
22
  2. Press **New game**, choose your colour, the clock and the AI's level, and press **Start**.
23
23
  3. Move: the AI answers on the board, and with a few words under its name.
24
24
 
@@ -37,7 +37,7 @@ No chess engine plays for the AI: every move and every judgment is its own. Ches
37
37
 
38
38
  - You talk with the AI in the **box under its name**, a few words with each move: it reads them with your move. You can also write to it in its window. With Codex, if you write there while it waits for your move, press **Esc** after Enter: else it reads your message only after your move.
39
39
  - A **move written** in the chat counts too: `e4`, `Nf3`, with the English letters of the pieces (K Q R B N), or in words.
40
- - **The AI's name** opens the choice of its chat: Claude Code or Codex, the model (which AI: some are faster, others stronger) and how much it reasons. The more it reasons, the slower it plays. If you change them, the chat starts afresh in the same window: the AI forgets what you talked about, and the game stays as it is. In analysis the name stands over the box where you write to it.
40
+ - **The AI's name** opens the choice of its chat: Claude Code, Codex or Antigravity, the model (which AI: some are faster, others stronger) and how much it reasons. The more it reasons, the slower it plays. If you change them, the chat starts afresh in the same window: the AI forgets what you talked about, and the game stays as it is. In analysis the name stands over the box where you write to it.
41
41
  - At each **new game**, the chat the page opened starts afresh by itself, once the AI has stopped thinking: a long chat makes the AI slower.
42
42
 
43
43
  ## Helps and marks
@@ -53,6 +53,7 @@ No chess engine plays for the AI: every move and every judgment is its own. Ches
53
53
  ![The bar that steps through the moves, and the judgments on the moves](analysis)
54
54
 
55
55
  - When the game is over, press **Analyse the game**: the AI goes over it and puts its judgments and notes on the moves. Read them by stepping through the moves with the arrows at the foot of the column, or by clicking a move.
56
+ - In analysis, **move the pieces**, White's and Black's, from any move: a variation starts, with a coloured edge round the board. Click a move of the game to go back to it; the ✕, or Esc, closes the variation.
56
57
  - **Analyses**, in the header, opens your analyses, and loads a game played elsewhere: paste its PGN, or choose a .pgn file.
57
58
  - With your names on Lichess and Chess.com in your profile, **Load a game** lists your latest games there: click one to analyse it.
58
59
  - Ask the AI about the game in the box under its name.
@@ -18,7 +18,7 @@ Nessun motore di scacchi gioca al posto dell'AI: ogni mossa e ogni giudizio sono
18
18
 
19
19
  ### Per cominciare
20
20
 
21
- 1. Clicca il **nome dell'AI**, in alto a sinistra, e scegli la chat fra quelle che hai sul computer: **Claude Code** o **Codex**. Si apre in una finestra sua: tienila aperta mentre giochi. La prima volta fa qualche domanda. Se ti fidi della cartella: **Sì** (in Claude Code è già scelto No: scendi su Yes con le frecce, poi Invio). Con Codex, sugli hook, i due aiutanti di Chesspal: **Trust all and continue**. Su Windows Codex chiede anche se preparare la sua sandbox, lo spazio protetto dove lavora: scegli la prima opzione, e sì quando Windows chiede il permesso.
21
+ 1. Clicca il **nome dell'AI**, in alto a sinistra, e scegli la chat fra quelle che hai sul computer: **Claude Code**, **Codex** o **Antigravity**. Si apre in una finestra sua: tienila aperta mentre giochi. La prima volta fa qualche domanda. Se ti fidi della cartella: **Sì** (in Claude Code è già scelto No: scendi su Yes con le frecce, poi Invio). Con Codex, sugli hook, i due aiutanti di Chesspal: **Trust all and continue**. Su Windows Codex chiede anche se preparare la sua sandbox, lo spazio protetto dove lavora: scegli la prima opzione, e sì quando Windows chiede il permesso.
22
22
  2. Premi **Nuova partita**, scegli il colore, l'orologio e il livello dell'AI, e premi **Inizia**.
23
23
  3. Muovi: l'AI risponde sulla scacchiera, e con qualche parola sotto il suo nome.
24
24
 
@@ -37,7 +37,7 @@ Nessun motore di scacchi gioca al posto dell'AI: ogni mossa e ogni giudizio sono
37
37
 
38
38
  - Parli con l'AI nella **casella sotto il suo nome**, qualche parola a ogni mossa: le legge con la tua mossa. Puoi scriverle anche nella sua finestra. Con Codex, se gli scrivi lì mentre aspetta la tua mossa, dopo Invio premi **Esc**: se no legge il messaggio solo dopo la tua mossa.
39
39
  - Anche una **mossa scritta** nella chat vale: `e4`, `Nf3`, con le lettere inglesi dei pezzi (K Q R B N), o a parole.
40
- - **Il nome dell'AI** apre la scelta della chat: Claude Code o Codex, il modello (quale AI: alcune sono più veloci, altre più forti) e quanto ragiona. Più ragiona, più gioca lenta. Se cambi, la chat riparte da capo nella stessa finestra: l'AI dimentica la chiacchierata di prima, la partita resta com'è. Nell'analisi il nome sta sopra la casella dove le scrivi.
40
+ - **Il nome dell'AI** apre la scelta della chat: Claude Code, Codex o Antigravity, il modello (quale AI: alcune sono più veloci, altre più forti) e quanto ragiona. Più ragiona, più gioca lenta. Se cambi, la chat riparte da capo nella stessa finestra: l'AI dimentica la chiacchierata di prima, la partita resta com'è. Nell'analisi il nome sta sopra la casella dove le scrivi.
41
41
  - A ogni **nuova partita** la chat aperta dalla pagina riparte da capo da sola, se l'AI ha finito di pensare: una chat lunga rende l'AI più lenta.
42
42
 
43
43
  ## Gli aiuti e i segni
@@ -53,6 +53,7 @@ Nessun motore di scacchi gioca al posto dell'AI: ogni mossa e ogni giudizio sono
53
53
  ![La barra per scorrere le mosse e i giudizi sulle mosse](analysis)
54
54
 
55
55
  - A partita finita premi **Analizza la partita**: l'AI la ripassa e mette sulle mosse i suoi giudizi e le sue note. Le leggi scorrendo le mosse con le frecce in fondo alla colonna, o cliccando una mossa.
56
+ - Nell'analisi **muovi i pezzi**, bianchi e neri, da qualunque mossa: nasce una variante, con un bordo colorato attorno alla scacchiera. Clicca una mossa della partita per tornarci; la ✕, o Esc, chiude la variante.
56
57
  - **Analisi**, nell'intestazione, apre le tue analisi e carica una partita giocata altrove: incolla il suo PGN, o scegli un file .pgn.
57
58
  - Con i tuoi nomi su Lichess e Chess.com nel profilo, **Carica una partita** elenca le tue ultime partite là: clicca su una per analizzarla.
58
59
  - Chiedi all'AI della partita nella casella sotto il suo nome.
@@ -36,6 +36,8 @@ export function settingsFrom(value, defaults = { theme: "system", sound: true })
36
36
  claudeEffort: word(stored.claudeEffort),
37
37
  codexModel: word(stored.codexModel),
38
38
  codexEffort: word(stored.codexEffort),
39
+ antigravityModel: word(stored.antigravityModel),
40
+ antigravityEffort: word(stored.antigravityEffort),
39
41
  };
40
42
  }
41
43
  /** Remembers the settings in this browser. */
@@ -2,6 +2,7 @@
2
2
  export const choiceKeys = {
3
3
  claude: { model: "claudeModel", effort: "claudeEffort" },
4
4
  codex: { model: "codexModel", effort: "codexEffort" },
5
+ antigravity: { model: "antigravityModel", effort: "antigravityEffort" },
5
6
  };
6
7
  /** The hosts the window offers: those installed whose choices the settings keep. */
7
8
  export function offeredHosts(table) {
@@ -13,14 +14,21 @@ export function firstHost(table, settings) {
13
14
  const atTable = offered.find((host) => table.chat && host.name === table.host);
14
15
  return (atTable ?? offered.find((host) => host.name === settings.chatHost) ?? offered[0])?.name;
15
16
  }
16
- /** A host's model or effort as the settings keep it: the host's middle one for none, and for a choice it no longer offers. */
17
+ /** The efforts a host offers for one of its models: none for a model that has no effort to choose. */
18
+ export function effortsOf(host, model) {
19
+ return host.modelEfforts?.[model] ?? host.efforts;
20
+ }
21
+ /**
22
+ * A host's model or effort as the settings keep it: the host's middle one for none, and for a choice it no longer offers; the effort among the chosen
23
+ * model's, none for a model without.
24
+ */
17
25
  export function chosen(host, kind, settings) {
18
- const key = choiceKeys[host.name][kind];
19
- return offeredOr(host, kind, settings[key]);
26
+ const keys = choiceKeys[host.name];
27
+ return offered(host, settings[keys.model], settings[keys.effort])[kind];
20
28
  }
21
29
  /** What the chat held started with: the host's middle one where the table does not say, or says one it no longer offers. */
22
30
  export function startedWith(table, host, kind) {
23
- return offeredOr(host, kind, table.started?.[kind]);
31
+ return offered(host, table.started?.model, table.started?.effort)[kind];
24
32
  }
25
33
  /** What the button does with the host selected, none without one: with no chat, open one, to review the game in `review`; else start the chat again, in another host when it is not the one at the table. */
26
34
  export function chatAction(table, host, review) {
@@ -36,7 +44,11 @@ export function chatAction(table, host, review) {
36
44
  export function canAct(table) {
37
45
  return table.opening === undefined && table.waiting === undefined && (!table.chat || table.held);
38
46
  }
39
- function offeredOr(host, kind, value) {
40
- const offered = kind === "model" ? host.models : host.efforts;
41
- return value !== undefined && offered.includes(value) ? value : host[kind];
47
+ // The choice kept to what the host offers, as the table keeps it when it starts the chat.
48
+ function offered(host, model, effort) {
49
+ const kept = model !== undefined && host.models.includes(model) ? model : host.model;
50
+ const efforts = effortsOf(host, kept);
51
+ const middle = efforts.includes(host.effort) ? host.effort : efforts[Math.floor((efforts.length - 1) / 2)];
52
+ const keptEffort = effort !== undefined && efforts.includes(effort) ? effort : middle;
53
+ return keptEffort === undefined ? { model: kept } : { model: kept, effort: keptEffort };
42
54
  }
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "preact/jsx-ru
2
2
  import { useEffect, useState } from "preact/hooks";
3
3
  import { Dialog } from "../dialogs/dialog.js";
4
4
  import { checkIcon } from "../dialogs/profiles.js";
5
- import { canAct, chatAction, choiceKeys, chosen, firstHost, offeredHosts, startedWith } from "./action.js";
5
+ import { canAct, chatAction, choiceKeys, chosen, effortsOf, firstHost, offeredHosts, startedWith } from "./action.js";
6
6
  /** The chat's window: closed once a chat opens or starts again, the page then saying where it stands; `review` when it opened to have a game analysed. */
7
7
  export function ChatDialog({ open, review = false, table, settings, onChange, name, onClose, text }) {
8
8
  return (_jsx(Dialog, { open: open, title: review ? text.table.reviewTitle : text.table.title, onClose: onClose, text: text, children: _jsx(ChatForm, { review: review, table: table, settings: settings, onChange: onChange, name: name, onClose: onClose, text: text }) }));
@@ -41,7 +41,7 @@ export function chatState(table, name, text) {
41
41
  }
42
42
  return table.opening === "chat" ? words.opening : table.opening === "again" ? words.again(name) : table.opening === "restart" ? words.restart : undefined;
43
43
  }
44
- const hostTitles = { claude: "Claude Code", codex: "Codex" };
44
+ const hostTitles = { claude: "Claude Code", codex: "Codex", antigravity: "Antigravity" };
45
45
  /** The ↻ of a chat that starts again. */
46
46
  const restartIcon = (_jsx("svg", { class: "icon in-use", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { d: "M19.5 12a7.5 7.5 0 1 1-2.2-5.3M19.5 4.5v4h-4" }) }));
47
47
  /** The window's form, made again at each opening with the host at the table selected, else the last the button used, else the first. */
@@ -94,17 +94,19 @@ function submitOnEnter(event) {
94
94
  /** A host's model or effort as a row of choices, its middle one chosen until the player chooses; the one the chat at the table `started` with bears a dot. */
95
95
  function Choices({ host, kind, settings, started, onChange, text }) {
96
96
  const key = choiceKeys[host.name][kind];
97
- const offered = kind === "model" ? host.models : host.efforts;
97
+ // The efforts of the model chosen: none, and no row, for a model without.
98
+ const offered = kind === "model" ? host.models : effortsOf(host, chosen(host, "model", settings) ?? host.model);
98
99
  const value = chosen(host, kind, settings);
99
100
  if (offered.length === 0) {
100
101
  return null;
101
102
  }
102
103
  return (_jsxs("fieldset", { children: [_jsx("legend", { children: text.table[kind] }), _jsx("div", { class: "chat-choices", children: offered.map((one) => {
103
104
  const words = one + (one === started ? ` · ${text.table.atTable}` : "");
104
- return (_jsxs("label", { class: one === started ? "level started" : "level", title: words, children: [_jsx("input", { type: "radio", name: `chat-${kind}`, checked: value === one, onChange: () => onChange({ ...settings, [key]: one }), "aria-label": words }), _jsx("span", { children: shortName(one) })] }, one));
105
+ return (_jsxs("label", { class: one === started ? "level started" : "level", title: words, children: [_jsx("input", { type: "radio", name: `chat-${kind}`, checked: value === one, onChange: () => onChange({ ...settings, [key]: one }), "aria-label": words }), _jsx("span", { children: host.name === "codex" ? shortName(one) : one })] }, one));
105
106
  }) })] }));
106
107
  }
107
- // Codex's models without the "gpt-" they all start with, so the choice stays narrow; the title keeps the whole name.
108
+ // Codex's models without the "gpt-" they all start with, so the choice stays narrow; the title keeps the whole name. Another host's keep it: Antigravity's
109
+ // GPT-OSS would read as "oss".
108
110
  function shortName(value) {
109
111
  return value.replace(/^gpt-/, "");
110
112
  }
package/dist/page/text.js CHANGED
@@ -84,6 +84,7 @@ const english = {
84
84
  game: "The game",
85
85
  moves: "Moves",
86
86
  steps: { first: "Start", back: "Previous move", forward: "Next move", last: "Back to the game" },
87
+ variation: { line: "Variation", close: "Close the variation (Esc)", back: (move) => move === undefined ? "Back to the start of the game" : `Back to the game, at ${move}` },
87
88
  noGame: "No game yet: press + to start one, or ask for one in the chat.",
88
89
  foldCard: "Fold the card",
89
90
  openCard: "Open the card",
@@ -182,7 +183,7 @@ const english = {
182
183
  byHand: (name) => `You opened this chat yourself: change ${name}'s model with /model in its window, or close it and open one from here, with the choices below.`,
183
184
  noChat: "No chat at the table: choose one",
184
185
  change: "Change the chat or its model",
185
- install: (name) => `To play with ${name}, install Claude Code or Codex, then open this page again.`,
186
+ install: (name) => `To play with ${name}, install Claude Code, Codex or Antigravity, then open this page again.`,
186
187
  chat: "Chat",
187
188
  model: "Model",
188
189
  effort: "Effort",
@@ -523,6 +524,7 @@ const italian = {
523
524
  game: "La partita",
524
525
  moves: "Mosse",
525
526
  steps: { first: "Inizio", back: "Mossa precedente", forward: "Mossa successiva", last: "Torna alla partita" },
527
+ variation: { line: "Variante", close: "Chiudi la variante (Esc)", back: (move) => move === undefined ? "Torna all'inizio della partita" : `Torna alla partita, a ${move}` },
526
528
  noGame: "Nessuna partita: premi + per iniziarne una, o chiedila nella chat.",
527
529
  foldCard: "Chiudi la scheda",
528
530
  openCard: "Apri la scheda",
@@ -621,7 +623,7 @@ const italian = {
621
623
  byHand: (name) => `Questa chat l'hai aperta tu: cambia il modello di ${name} con /model nella sua finestra, oppure chiudila e aprine una da qui, con le scelte qui sotto.`,
622
624
  noChat: "Nessuna chat al tavolo: scegline una",
623
625
  change: "Cambia la chat o il modello",
624
- install: (name) => `Per giocare con ${name}, installa Claude Code o Codex, poi apri di nuovo questa pagina.`,
626
+ install: (name) => `Per giocare con ${name}, installa Claude Code, Codex o Antigravity, poi apri di nuovo questa pagina.`,
625
627
  chat: "Chat",
626
628
  model: "Modello",
627
629
  effort: "Ragionamento",
@@ -0,0 +1,10 @@
1
+ /** Antigravity's own folder, where its command line keeps its history and its log. */
2
+ export declare function antigravityFolder(userHome?: string): string;
3
+ /**
4
+ * Watches, from now on, whether the player starts a new turn in the conversation: writes in the chat, or stops it with Esc. `started` resolves then, never
5
+ * otherwise; `stop` ends the watch.
6
+ */
7
+ export declare function watchNewTurn(conversation: string, folder?: string, every?: number): {
8
+ started: Promise<void>;
9
+ stop: () => void;
10
+ };
@@ -0,0 +1,75 @@
1
+ // Antigravity's Stop hook holds the chat until it ends: a message the player writes meanwhile waits behind it, and Esc leaves it running. Antigravity's own
2
+ // files tell either: the history of what the player typed, with the conversation it went to, and its log, which notes the conversation stopped.
3
+ import { closeSync, openSync, readSync, statSync } from "node:fs";
4
+ import { homedir } from "node:os";
5
+ import { join } from "node:path";
6
+ /** Antigravity's own folder, where its command line keeps its history and its log. */
7
+ export function antigravityFolder(userHome = homedir()) {
8
+ return join(userHome, ".gemini", "antigravity-cli");
9
+ }
10
+ /**
11
+ * Watches, from now on, whether the player starts a new turn in the conversation: writes in the chat, or stops it with Esc. `started` resolves then, never
12
+ * otherwise; `stop` ends the watch.
13
+ */
14
+ export function watchNewTurn(conversation, folder = antigravityFolder(), every = 500) {
15
+ const history = join(folder, "history.jsonl");
16
+ const log = join(folder, "cli.log");
17
+ const read = { [history]: sizeOf(history), [log]: sizeOf(log) };
18
+ let timer;
19
+ const started = new Promise((done) => {
20
+ timer = setInterval(() => {
21
+ if (wrote(newText(history, read), conversation) || stopped(newText(log, read), conversation)) {
22
+ clearInterval(timer);
23
+ done();
24
+ }
25
+ }, every);
26
+ timer.unref();
27
+ });
28
+ return { started, stop: () => clearInterval(timer) };
29
+ }
30
+ /** Whether these lines of Antigravity's history hold something the player typed in the conversation: a message, or a command such as /clear. */
31
+ function wrote(lines, conversation) {
32
+ return lines.split("\n").some((line) => {
33
+ try {
34
+ return JSON.parse(line).conversationId === conversation;
35
+ }
36
+ catch {
37
+ return false;
38
+ }
39
+ });
40
+ }
41
+ /** Whether these lines of Antigravity's log say the conversation was stopped, as Esc does. */
42
+ function stopped(lines, conversation) {
43
+ return lines.includes(`Cancelling conversation ${conversation}`);
44
+ }
45
+ /** What a file gained since it was last read, as `read` keeps its size; a file that shrank, written anew, is read from its start. */
46
+ function newText(file, read) {
47
+ const size = sizeOf(file);
48
+ const from = size < (read[file] ?? 0) ? 0 : read[file] ?? 0;
49
+ read[file] = size;
50
+ if (size <= from) {
51
+ return "";
52
+ }
53
+ try {
54
+ const descriptor = openSync(file, "r");
55
+ try {
56
+ const buffer = Buffer.alloc(size - from);
57
+ const length = readSync(descriptor, buffer, 0, buffer.length, from);
58
+ return buffer.subarray(0, length).toString("utf8");
59
+ }
60
+ finally {
61
+ closeSync(descriptor);
62
+ }
63
+ }
64
+ catch {
65
+ return "";
66
+ }
67
+ }
68
+ function sizeOf(file) {
69
+ try {
70
+ return statSync(file).size;
71
+ }
72
+ catch {
73
+ return 0;
74
+ }
75
+ }
@@ -65,11 +65,19 @@ export declare function notePagePort(home: string, folder: string, port: number,
65
65
  export declare function forgetPagePort(home: string, folder: string, port: number): void;
66
66
  /** What Codex's Stop hook answers to wake the chat: its turn goes on with the doorbell's line. */
67
67
  export declare const codexBellAnswer: string;
68
+ /** What Antigravity's Stop hook answers to wake the chat: its turn goes on, the doorbell's line given as a system message (D-115). */
69
+ export declare const antigravityBellAnswer: string;
70
+ /** The host whose Stop hook runs `chesspal listen`, as its flag names it: Claude Code with none. */
71
+ export type ListeningHost = "claude" | "codex" | "antigravity";
68
72
  /**
69
73
  * Reads the hook's input, waits for the bell, and ends the process as the host reads it: for Claude Code's asyncRewake, 2 with the line on stderr; for
70
- * Codex's Stop hook, 0 with its answer on stdout, since Windows PowerShell, which runs Codex's hooks, turns an exit code 2 into 1. Else 0, saying nothing.
74
+ * Codex's and Antigravity's Stop hooks, 0 with their answer on stdout, since Windows PowerShell, which runs Codex's hooks, turns an exit code 2 into 1.
75
+ * Else 0, saying nothing, or for Antigravity an empty answer, which lets its chat stop. Antigravity's hook holds the chat: it lets it go as soon as the
76
+ * player writes in the chat or stops it, without the bell.
71
77
  */
72
- export declare function listen(codex?: boolean): Promise<void>;
78
+ export declare function listen(host?: ListeningHost): Promise<void>;
79
+ /** What the hook says as it ends, by its host: the doorbell's line or answer when it rang; else nothing, but for Antigravity, which reads an answer. */
80
+ export declare function hookAnswer(host: ListeningHost, rang: boolean): string | undefined;
73
81
  /**
74
82
  * Waits at the page of the hook's chat until the player moves there: true then; false when there is no page to wait at, or another hook takes its place.
75
83
  * The chat's own page is the one with its session; after /clear, which gives the hook a new one, the only page of its folder.
@@ -1,4 +1,4 @@
1
- // The doorbell for the page (D-47): `chesspal listen`, the Stop hook init writes for Claude Code and Codex, waits at the page of its chat and, when
1
+ // The doorbell for the page (D-47): `chesspal listen`, the Stop hook init writes for Claude Code, Codex and Antigravity, waits at the page of its chat and, when
2
2
  // the player moves or writes there, tells the chat to call status. It never carries the move, the words or the position: status reads them fresh.
3
3
  import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
4
4
  import { dirname, join, resolve } from "node:path";
@@ -6,6 +6,7 @@ import { positionShownNote, writeAtomically } from "../../games/index.js";
6
6
  import { samePath } from "../../home/index.js";
7
7
  import { analystAgent } from "../../host-files/index.js";
8
8
  import { askPage, chatFolder, findChatPage, notedFolder, notedPagePorts, pageNote, probeOf, probeWait } from "../ports/index.js";
9
+ import { watchNewTurn } from "./antigravity.js";
9
10
  /** What the chat hears from the doorbell: one line for whatever the player did, never what, only where to read it. */
10
11
  export const bellLine = "The player acted on the board page: call status to see it.";
11
12
  /**
@@ -185,31 +186,51 @@ export function forgetPagePort(home, folder, port) {
185
186
  }
186
187
  /** What Codex's Stop hook answers to wake the chat: its turn goes on with the doorbell's line. */
187
188
  export const codexBellAnswer = JSON.stringify({ decision: "block", reason: bellLine });
189
+ /** What Antigravity's Stop hook answers to wake the chat: its turn goes on, the doorbell's line given as a system message (D-115). */
190
+ export const antigravityBellAnswer = JSON.stringify({ decision: "continue", reason: bellLine });
188
191
  /**
189
192
  * Reads the hook's input, waits for the bell, and ends the process as the host reads it: for Claude Code's asyncRewake, 2 with the line on stderr; for
190
- * Codex's Stop hook, 0 with its answer on stdout, since Windows PowerShell, which runs Codex's hooks, turns an exit code 2 into 1. Else 0, saying nothing.
193
+ * Codex's and Antigravity's Stop hooks, 0 with their answer on stdout, since Windows PowerShell, which runs Codex's hooks, turns an exit code 2 into 1.
194
+ * Else 0, saying nothing, or for Antigravity an empty answer, which lets its chat stop. Antigravity's hook holds the chat: it lets it go as soon as the
195
+ * player writes in the chat or stops it, without the bell.
191
196
  */
192
- export async function listen(codex = false) {
197
+ export async function listen(host = "claude") {
193
198
  let input = "";
194
199
  for await (const chunk of process.stdin) {
195
200
  input += String(chunk);
196
201
  }
197
- const heard = await bellHeard(input).catch((error) => ({ folder: chatFolder(hookInput(input)), rang: false, why: `failed: ${error.message}` }));
202
+ const conversation = host === "antigravity" ? hookInput(input).session : undefined;
203
+ const turn = conversation === undefined ? undefined : watchNewTurn(conversation);
204
+ const waited = bellHeard(input).catch((error) => ({ folder: chatFolder(hookInput(input)), rang: false, why: `failed: ${error.message}` }));
205
+ const newTurn = turn?.started.then(() => ({ folder: chatFolder(hookInput(input)), rang: false, why: "the player started a new turn" }));
206
+ const heard = await (newTurn === undefined ? waited : Promise.race([waited, newTurn]));
207
+ turn?.stop();
198
208
  // Codex's turn ends with a hook that heard no bell, and the player's next move on the page wakes nobody: the log keeps why.
199
- if (codex && !heard.rang) {
209
+ if (host === "codex" && !heard.rang) {
200
210
  noteSilence(heard.folder, heard.why);
201
211
  }
202
212
  // It ends as soon as it knows: the chat hears the hook only once it ends.
203
- if (!heard.rang) {
213
+ const answer = hookAnswer(host, heard.rang);
214
+ if (answer === undefined) {
204
215
  process.exit(0);
205
216
  }
206
- else if (codex) {
207
- process.stdout.write(`${codexBellAnswer}\n`, () => process.exit(0));
217
+ else if (host === "claude") {
218
+ process.stderr.write(`${answer}\n`, () => process.exit(2));
208
219
  }
209
220
  else {
210
- process.stderr.write(`${bellLine}\n`, () => process.exit(2));
221
+ process.stdout.write(`${answer}\n`, () => process.exit(0));
211
222
  }
212
223
  }
224
+ /** What the hook says as it ends, by its host: the doorbell's line or answer when it rang; else nothing, but for Antigravity, which reads an answer. */
225
+ export function hookAnswer(host, rang) {
226
+ if (host === "antigravity") {
227
+ return rang ? antigravityBellAnswer : "{}";
228
+ }
229
+ if (!rang) {
230
+ return undefined;
231
+ }
232
+ return host === "codex" ? codexBellAnswer : bellLine;
233
+ }
213
234
  /**
214
235
  * Waits at the page of the hook's chat until the player moves there: true then; false when there is no page to wait at, or another hook takes its place.
215
236
  * The chat's own page is the one with its session; after /clear, which gives the hook a new one, the only page of its folder.
@@ -264,11 +285,13 @@ export function noteSilence(folder, why, now = new Date()) {
264
285
  // The log only explains; the hook ends the same.
265
286
  }
266
287
  }
288
+ /** What the hook reads of its host's input: the chat's session, Antigravity's conversation, its folder, and whether its analyst has gone. */
267
289
  function hookInput(input) {
268
290
  try {
269
291
  const parsed = JSON.parse(input);
292
+ const session = typeof parsed.session_id === "string" ? parsed.session_id : parsed.conversationId;
270
293
  return {
271
- ...(typeof parsed.session_id === "string" ? { session: parsed.session_id } : {}),
294
+ ...(typeof session === "string" ? { session } : {}),
272
295
  ...(typeof parsed.cwd === "string" ? { cwd: parsed.cwd } : {}),
273
296
  ...(analystGone(parsed.background_tasks) ? { analystGone: true } : {}),
274
297
  };
@@ -1,2 +1,2 @@
1
- export { Bell, forgetPagePort, listen, notePagePort } from "./bell.ts";
1
+ export { Bell, forgetPagePort, listen, type ListeningHost, notePagePort } from "./bell.ts";
2
2
  export { chatInterrupted } from "./interrupted.ts";
@@ -1,3 +1,4 @@
1
- // The doorbell that wakes a chat when the player acts on the page (D-47), and the notes of the ports a folder's page took off its usual ones.
1
+ // The doorbell that wakes a chat when the player acts on the page (D-47), Antigravity's included (D-115), and the notes of the ports a folder's page took
2
+ // off its usual ones.
2
3
  export { Bell, forgetPagePort, listen, notePagePort } from "./bell.js";
3
4
  export { chatInterrupted } from "./interrupted.js";
@@ -1,5 +1,6 @@
1
1
  // Who makes a tool call (D-81): only what the host ties to that call, never what the AI says of itself nor a setting the chat may have changed.
2
- // Codex sends the model and the effort with each call; Claude Code writes them into the chat's transcript, on the line of the call.
2
+ // Codex sends the model and the effort with each call; Claude Code writes them into the chat's transcript, on the line of the call; Antigravity sends the
3
+ // conversation, whose transcript names the model with its effort on each of the AI's steps (D-115).
3
4
  import { closeSync, openSync, readdirSync, readFileSync, readSync, statSync } from "node:fs";
4
5
  import { homedir } from "node:os";
5
6
  import { basename, dirname, join } from "node:path";
@@ -18,6 +19,13 @@ export async function callerOf(meta, client, folder, wait = 50) {
18
19
  return found;
19
20
  }
20
21
  }
22
+ const conversation = meta?.["antigravity.google/artifacts_dir"];
23
+ if (typeof conversation === "string" && conversation !== "") {
24
+ const found = await antigravityModel(join(conversation, ".system_generated", "logs", "transcript_full.jsonl"), wait);
25
+ if (found !== undefined) {
26
+ return found;
27
+ }
28
+ }
21
29
  return client === undefined ? undefined : { annotator: client.title ?? client.name, hostOnly: true };
22
30
  }
23
31
  /**
@@ -91,6 +99,23 @@ function callerIn(text, call) {
91
99
  }
92
100
  return undefined;
93
101
  }
102
+ /**
103
+ * The model of the AI's last step in an Antigravity conversation, as its transcript names it, the effort after it (gemini-3.8-flash-medium); the step of
104
+ * the call is written after it, but the chat's steps before it had the same model. None when no step names one in time.
105
+ */
106
+ async function antigravityModel(transcript, wait) {
107
+ for (let attempt = 0; attempt < attempts; attempt++) {
108
+ const model = tailOf(transcript).split("\n").reverse().map((line) => objectOf(line)).find((entry) => entry?.type === "PLANNER_RESPONSE" && typeof entry.model === "string")?.model;
109
+ if (typeof model === "string") {
110
+ const [, name, effort] = /^(.+)-(low|medium|high|xhigh|max)$/.exec(model) ?? [];
111
+ return name === undefined ? { annotator: model } : withEffort(name, effort);
112
+ }
113
+ if (attempt < attempts - 1) {
114
+ await new Promise((done) => setTimeout(done, wait));
115
+ }
116
+ }
117
+ return undefined;
118
+ }
94
119
  /**
95
120
  * Who in Codex made a call: the chat, or an agent by the role its session notes first, before its first call (Priya). An agent started with the chat's
96
121
  * own turns sees the opponent's plan, so it counts as the chat (D-88).
@@ -7,7 +7,7 @@ import { asTheChatSees } from "./chat.js";
7
7
  import { tellWatchers } from "./events.js";
8
8
  import { bodyOf, bytesOf, isJson, json, jsonOf, runRoute, send, textOf } from "./http.js";
9
9
  import { profilesShown } from "./profiles.js";
10
- import { analystAtWork, chatPlayedAtLength, chatToRelaunch, chatToWaitFor, refuseWaited, relaunchChat, tableShown } from "./table.js";
10
+ import { analystAtWork, chatForAnalysis, chatPlayedAtLength, chatToRelaunch, chatToWaitFor, refuseWaited, relaunchChat, tableShown } from "./table.js";
11
11
  import { updateShown } from "./update.js";
12
12
  /** Answers the page's requests about the game: true when the request was one. */
13
13
  export function answerGame(request, response, url, served) {
@@ -132,7 +132,8 @@ async function messageFromPage(request, response, served) {
132
132
  /**
133
133
  * A new game from the page, with the sides, the level and the clock the player chose: the sides and the level they leave out are as in the game
134
134
  * before, and no clock means a game without one. One going on goes to the archive unfinished, and the doorbell tells the chat, unless the table starts
135
- * it again. A chat the table holds that must be clear for it, and is at work, has the game wait with the choices kept until it has ended its answer (D-107).
135
+ * it again. A chat the table holds that must be clear for it, or that it started for the analysis, and is at work, has the game wait with the choices kept
136
+ * until it has ended its answer (D-107).
136
137
  */
137
138
  async function newGameFromPage(request, response, served) {
138
139
  const asked = 'Send the new game as JSON: {"ai_colour": "black", "level": "club", "clock": "10+5"}, each one optional; no clock for a game without one.';
@@ -152,7 +153,7 @@ async function newGameFromPage(request, response, served) {
152
153
  }
153
154
  // The game starts from the page, open: its clocks wait for the player's Start there. A chat the table starts again is clean: no /clear is asked.
154
155
  relaunching = chatToRelaunch(served);
155
- if (!relaunching && chatToWaitFor(served) && chatPlayedAtLength(served, before)) {
156
+ if (!relaunching && chatToWaitFor(served) && (chatPlayedAtLength(served, before) || chatForAnalysis(served))) {
156
157
  waitForChat(served, input.data);
157
158
  send(response, 200, json, JSON.stringify({ waiting: true }));
158
159
  return;
@@ -60,6 +60,7 @@ export async function startPage(context, options = {}) {
60
60
  const noted = offPorts ? notePort(served) : undefined;
61
61
  const stopWatchingGame = watchGame(served);
62
62
  const stopAskingNpm = watchForUpdates(served);
63
+ table?.chats?.watch?.(() => tellWatchers(watchers));
63
64
  // A chat's process ends without closing its page: the note goes with it.
64
65
  const forgetPort = () => {
65
66
  if (noted !== undefined) {
@@ -19,12 +19,18 @@ export interface TableChats {
19
19
  held(folder: string): HeldChat | undefined;
20
20
  /** Asks for the chat again: with another host, or `updated`, from the Chesspal just installed (D-108). */
21
21
  relaunch(folder: string, host?: string, updated?: boolean): boolean;
22
+ /** Tells `changed` when the hosts' choices change: Antigravity's models, read from the player's account. */
23
+ watch?(changed: () => void): void;
22
24
  }
23
- /** 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. */
25
+ /**
26
+ * The chat `chesspal play` holds: the model and effort its host started with, none for the host's own, the seat, the play's process, and `analysis` when
27
+ * it started it for the game in analysis on the page.
28
+ */
24
29
  export interface HeldChat {
25
30
  readonly model?: string | undefined;
26
31
  readonly effort?: string | undefined;
27
32
  readonly seat?: number;
33
+ readonly analysis?: true;
28
34
  }
29
35
  /** 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). */
30
36
  export type Opening = "chat" | "again" | "restart";
@@ -67,7 +73,8 @@ export declare function tableShown(served: Served): {
67
73
  export declare function answerTable(request: IncomingMessage, response: ServerResponse, url: URL, served: Served): boolean;
68
74
  /**
69
75
  * Whether a new game starts the chat at the table again, clean (D-57): `chesspal play` holds it, it waits for the bell, no analyst of it is at work, and it
70
- * has seen a game move since it was first shown one; one that only greeted the player stays, as one opened from the chat's window before a new game.
76
+ * has seen a game move since it was first shown one; one that only greeted the player stays, as one opened from the chat's window before a new game. One
77
+ * `chesspal play` started for the analysis starts again all the same, at rest or still opening, or its first turn would analyse a game no longer there.
71
78
  */
72
79
  export declare function chatToRelaunch(served: Served): boolean;
73
80
  /**
@@ -80,6 +87,11 @@ export declare function relaunchChat(served: Served, how?: "again" | "restart",
80
87
  * it, it is at work, and no analyst of it is; with an analyst at work the player is asked for /clear, as with a chat opened by hand.
81
88
  */
82
89
  export declare function chatToWaitFor(served: Served): boolean;
90
+ /**
91
+ * Whether the chat `chesspal play` holds was started for the analysis on the page: the one at the table, or, with none at it yet, the one still opening.
92
+ * A chat opened by hand never is.
93
+ */
94
+ export declare function chatForAnalysis(served: Served): boolean;
83
95
  /**
84
96
  * Whether the chat at the table has played the game `current` at length (D-57), counted from the first position it was shown of it. A chat the table
85
97
  * started again for a game began after it, and new_game takes it for one cleared since: it holds the whole game all the same.