chesspal 0.4.0 → 0.5.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 +37 -8
- package/dist/cli/chat-hook.d.ts +4 -1
- package/dist/cli/chat-hook.js +12 -5
- package/dist/cli/cli.js +3 -3
- package/dist/cli/command-line.js +9 -9
- package/dist/commands/game/index.d.ts +1 -1
- package/dist/commands/game/index.js +1 -1
- package/dist/commands/game/new.d.ts +2 -0
- package/dist/commands/game/new.js +4 -6
- package/dist/commands/index.d.ts +1 -1
- package/dist/commands/index.js +1 -1
- package/dist/commands/registry/context.d.ts +5 -0
- package/dist/commands/registry/context.js +2 -1
- package/dist/commands/setup/init.d.ts +4 -1
- package/dist/commands/setup/init.js +30 -10
- package/dist/core/review.d.ts +2 -2
- package/dist/core/review.js +7 -3
- package/dist/host-files/host-files.d.ts +10 -6
- package/dist/host-files/host-files.js +57 -9
- package/dist/host-files/index.d.ts +1 -1
- package/dist/host-files/index.js +1 -1
- package/dist/page/app/app.js +13 -9
- package/dist/page/app/phase.js +4 -0
- package/dist/page/dialogs/settings.js +6 -7
- package/dist/page/guide/en.md +3 -3
- package/dist/page/guide/it.md +3 -3
- package/dist/page/profile/settings.js +2 -0
- package/dist/page/table/action.js +19 -7
- package/dist/page/table/window.js +7 -5
- package/dist/page/talk/talk.css +13 -0
- package/dist/page/talk/talk.js +3 -3
- package/dist/page/text.js +4 -2
- package/dist/server/bell/antigravity.d.ts +10 -0
- package/dist/server/bell/antigravity.js +75 -0
- package/dist/server/bell/bell.d.ts +22 -5
- package/dist/server/bell/bell.js +67 -13
- package/dist/server/bell/index.d.ts +2 -1
- package/dist/server/bell/index.js +3 -1
- package/dist/server/bell/interrupted.d.ts +2 -0
- package/dist/server/bell/interrupted.js +56 -0
- package/dist/server/mcp/caller.js +26 -1
- package/dist/server/page/game.js +6 -1
- package/dist/server/page/page.js +3 -2
- package/dist/server/page/table.d.ts +9 -0
- package/dist/server/page/table.js +13 -2
- package/dist/server/table/hosts/chats.d.ts +4 -2
- package/dist/server/table/hosts/chats.js +23 -10
- package/dist/server/table/hosts/find.d.ts +6 -0
- package/dist/server/table/hosts/find.js +19 -1
- package/dist/server/table/hosts/hooks.d.ts +5 -2
- package/dist/server/table/hosts/hooks.js +6 -3
- package/dist/server/table/hosts/hosts.d.ts +19 -10
- package/dist/server/table/hosts/hosts.js +34 -16
- package/dist/server/table/hosts/models.d.ts +19 -0
- package/dist/server/table/hosts/models.js +95 -0
- package/dist/server/table/hosts/play.d.ts +4 -1
- package/dist/server/table/hosts/play.js +17 -8
- package/dist/server/table/hosts/seat.js +2 -0
- package/dist/server/table/table.js +1 -1
- package/package.json +1 -1
package/dist/page/app/app.js
CHANGED
|
@@ -43,7 +43,7 @@ import { AiNote, AiWords, Compose, PointSquare, TalkFaces } from "../talk/talk.j
|
|
|
43
43
|
import { textFor } from "../text.js";
|
|
44
44
|
import { anchorName, useAnchor } from "./anchor.js";
|
|
45
45
|
import { useBrowseKeys, useFoldKeys, useLegendKey, useMapKey, useWheelSteps } from "./keys.js";
|
|
46
|
-
import { phaseOf, verdictOf } from "./phase.js";
|
|
46
|
+
import { helpsShown, phaseOf, verdictOf } from "./phase.js";
|
|
47
47
|
import { ask, post, refusedText, sendMessage, sendMove, useServed } from "./served.js";
|
|
48
48
|
import { tabTitle, useTabTitle } from "./title.js";
|
|
49
49
|
/**
|
|
@@ -87,8 +87,9 @@ export function App({ start }) {
|
|
|
87
87
|
setChatToReview(false);
|
|
88
88
|
setDialog(first ? "chat" : "new-game");
|
|
89
89
|
};
|
|
90
|
+
// In analysis the chat is who answers on the game: its window asks who reviews it.
|
|
90
91
|
const askChat = () => {
|
|
91
|
-
setChatToReview(
|
|
92
|
+
setChatToReview(served?.analysis !== undefined);
|
|
92
93
|
setDialog("chat");
|
|
93
94
|
};
|
|
94
95
|
const closeChat = () => {
|
|
@@ -130,7 +131,7 @@ export function App({ start }) {
|
|
|
130
131
|
apply(settingsFrom(served.settings));
|
|
131
132
|
}
|
|
132
133
|
}, [profileId]);
|
|
133
|
-
// The AI as the host names it (Claude, Codex), also where the page sends the player to write; the last known while the page is offline.
|
|
134
|
+
// The AI as the host names it (Claude, Codex, Gemini), also where the page sends the player to write; the last known while the page is offline.
|
|
134
135
|
const name = served?.opponent ?? text.ai;
|
|
135
136
|
// Under it, the model and effort the AI played its last move with (D-82).
|
|
136
137
|
const signed = served?.aiModel === undefined ? undefined : signature(served.aiModel.model, served.aiModel.effort);
|
|
@@ -207,14 +208,15 @@ export function App({ start }) {
|
|
|
207
208
|
const shown = useMemo(() => past ?? (pending === undefined ? live : withMove(live, pending)), [live, pending, past]);
|
|
208
209
|
// A square lit from the AI's words goes off with the position: a touch keeps it lit until then, a mouse until it leaves the word.
|
|
209
210
|
useEffect(() => setPointed(noSquares), [shown]);
|
|
210
|
-
//
|
|
211
|
+
// The player sees all of Chesspal's facts, whatever the AI's level (D-79): in a game with the helps on, always in analysis (D-114).
|
|
211
212
|
const analysing = studied !== undefined;
|
|
213
|
+
const helped = helpsShown(phase, settings.helps);
|
|
212
214
|
// Seen from the player's side (D-91): theirs in a game, in analysis the side the game names theirs, none when it names neither.
|
|
213
215
|
const you = analysing ? playerIn(studiedSides(live.record.tags, served?.profile.name ?? "", playedAs, text.sides, served?.profile.accounts)) : game?.ai === undefined ? undefined : game.ai === "white" ? "black" : "white";
|
|
214
|
-
const helps = useMemo(() =>
|
|
215
|
-
// Who the AI saw ahead, the coach's
|
|
216
|
+
const helps = useMemo(() => helped ? helpsOf(shown, you) : undefined, [shown, helped, you]);
|
|
217
|
+
// Who the AI saw ahead, the coach's in a game and the analyst's in analysis, for the bar and the chart (D-90).
|
|
216
218
|
const drawnOnBoard = live.outcome.over && live.outcome.result === "1/2-1/2";
|
|
217
|
-
const standings = useMemo(() =>
|
|
219
|
+
const standings = useMemo(() => helped ? standingsOf(plies, drawnOnBoard) : undefined, [plies, drawnOnBoard, helped]);
|
|
218
220
|
useMapKey(helps !== undefined, setMapping);
|
|
219
221
|
// A new move or a new game ends what the player was doing on the one before: the move sent, the look back, the choice of a promotion. The same game
|
|
220
222
|
// sent again, for the talk, the coach's words or the settings, leaves them where they are (Zoe); so does a review written on the game in analysis.
|
|
@@ -297,7 +299,7 @@ export function App({ start }) {
|
|
|
297
299
|
const header = (_jsx(Header, { text: text, kind: kind, verdict: verdict, ...(!ended && (offer === "ai" || declined) ? { who: name } : {}), actions: actions, onNewGame: served === undefined ? undefined : askNewGame,
|
|
298
300
|
// Between games the new one stands out in the player's card (D-77): in the header only when the talk, and the card with it, is folded away.
|
|
299
301
|
standOut: studied !== undefined || (between && folds.talk !== "open"), onStudy: served === undefined || going ? undefined : () => setDialog("study"), flipped: flipped, onFlip: () => setFlipped(!flipped), sound: sound, onSound: () => change({ ...settings, sound: !sound }), onTheme: switchTheme, onSettings: () => setSetting(true), onGuide: () => setDialog("guide"), ...(served === undefined ? {} : { profile: served.profile, onProfile: () => setDialog("profiles") }), ...(served?.update === undefined || served.table === undefined || offline ? {} : { update: { version: served.update.version, installing: served.update.installing === true }, onUpdate: () => setDialog("update") }) }));
|
|
300
|
-
const panel = _jsx(SettingsPanel, { open: setting, settings: settings, onChange: change, onClose: () => setSetting(false), text: text });
|
|
302
|
+
const panel = _jsx(SettingsPanel, { open: setting, settings: settings, analysing: analysing, onChange: change, onClose: () => setSetting(false), text: text });
|
|
301
303
|
// The guide opens before the page has its data too.
|
|
302
304
|
const guide = _jsx(GuideDialog, { open: dialog === "guide", onClose: () => setDialog(undefined), text: text });
|
|
303
305
|
if (problem !== undefined) {
|
|
@@ -367,6 +369,8 @@ export function App({ start }) {
|
|
|
367
369
|
// Where to wake the chat, under the AI's card (over the board when the talk is folded); the button that starts the clocks, under the player's (Zoe).
|
|
368
370
|
// On the table, what its chat is doing stands there, and in a game with no chat at it a line that opens the chat's window (D-107).
|
|
369
371
|
const table = offline ? undefined : served.table;
|
|
372
|
+
// In analysis the chat that answers stands over the box: its name opens its window, as on the AI's card in a game.
|
|
373
|
+
const chatName = table === undefined ? undefined : _jsx(AiNameButton, { name: name, table: table, onOpen: askChat, text: text });
|
|
370
374
|
const atTable = table === undefined ? null : tableNote({ table, name, inGame: going, onOpen: askChat, text });
|
|
371
375
|
const asleepNote = atTable !== null
|
|
372
376
|
? atTable
|
|
@@ -473,7 +477,7 @@ export function App({ start }) {
|
|
|
473
477
|
? (_jsxs(_Fragment, { children: [_jsxs("section", { class: "ai-block", "aria-label": sides[ai].name, children: [studiedCard(ai, "ai"), asleepNote, _jsx(AiNote, { ...(reviewed === undefined ? {} : { ply: reviewed }), ...(shownOpening === undefined ? {} : { opening: shownOpening }), ...(at === total && closing !== undefined ? { closing } : {}), ...(at === 0 && summary !== undefined ? { summary } : {}), ...(reviewing ? { reviewing: true } : {}), ...(count === undefined ? {} : { count }), ...(completeIt === undefined ? {} : { onComplete: completeIt }), name: analyst, by: annotator, mood: mood, ...(voiced ? { voice: text } : {}), text: text })] }), _jsxs("section", { class: "you-block", "aria-label": sides[player].name, children: [_jsx("div", { class: "chat", children: _jsx(Compose, { lines: analysisTalk(talk, studied.name), newestFirst: false, spent: 0, draft: draft, onDraft: (words) => {
|
|
474
478
|
setDraft(words);
|
|
475
479
|
setUnsent(undefined);
|
|
476
|
-
}, onSend: () => sendWords({ game: studied.name, at }), canMove: false, name: name, problem: unsent, about: { ...(at === 0 ? {} : { ply: plies[at - 1] }), onShow: goTo }, text: text }) }), studiedCard(player, "you")] })] }))
|
|
480
|
+
}, onSend: () => sendWords({ game: studied.name, at }), canMove: false, name: name, problem: unsent, about: { ...(at === 0 ? {} : { ply: plies[at - 1] }), onShow: goTo }, ...(chatName === undefined ? {} : { to: chatName }), text: text }) }), studiedCard(player, "you")] })] }))
|
|
477
481
|
: (_jsxs(_Fragment, { children: [_jsxs("section", { class: "ai-block", "aria-label": name, children: [_jsx(PlayerCard, { place: "ai", picture: thinker(48), name: table === undefined ? name : _jsx(AiNameButton, { name: name, table: table, onOpen: askChat, text: text }), signed: signed, side: idle ? undefined : ai, ...(idle ? {} : { level: text.start.levels[level] }), taken: material.taken[ai], ahead: ahead(ai), clock: clockOf(ai), text: text }), asleepNote, _jsx(AiWords, { words: aiWords(plies, ai, talk), newestFirst: !flipped, ...(voiced ? { voice: text } : {}) })] }), _jsxs("section", { class: "you-block", "aria-label": profile.name, children: [_jsx(Compose, { lines: said, newestFirst: flipped,
|
|
478
482
|
// Between games, and before the first, the player writes freely, a line at a time, as the AI does (D-74, D-77).
|
|
479
483
|
spent: going ? playerSpent(plies, ai, talk) : 0, draft: draft, onDraft: (words) => {
|
package/dist/page/app/phase.js
CHANGED
|
@@ -14,6 +14,10 @@ export function phaseOf(analysing, game, over, sawEnd) {
|
|
|
14
14
|
}
|
|
15
15
|
return sawEnd ? "over" : "idle";
|
|
16
16
|
}
|
|
17
|
+
/** Whether the board shows the helps: always in analysis, where the player uses every tool there is (D-114); in a game, with the helps `switched` on. */
|
|
18
|
+
export function helpsShown(phase, switched) {
|
|
19
|
+
return phase === "analysis" || (phase === "going" && switched);
|
|
20
|
+
}
|
|
17
21
|
/** What the header says of the game, with the kind that gives it a colour and an icon: in analysis, none going on, how it ended, or the draw offered. */
|
|
18
22
|
export function verdictOf(phase, how, winner, offer, declined, text) {
|
|
19
23
|
if (phase === "analysis") {
|
|
@@ -3,8 +3,8 @@ import { useEffect, useRef, useState } from "preact/hooks";
|
|
|
3
3
|
import { Piece } from "../pieces/pieces.js";
|
|
4
4
|
import { looks, pieceSets } from "../profile/settings.js";
|
|
5
5
|
import { hasVoice, onVoices } from "../talk/voice.js";
|
|
6
|
-
/** The panel of the settings, open while `open` is true: every change goes to `onChange` at once. */
|
|
7
|
-
export function SettingsPanel({ open, settings, onChange, onClose, text }) {
|
|
6
|
+
/** The panel of the settings, open while `open` is true: every change goes to `onChange` at once; `analysing` while a game is in analysis. */
|
|
7
|
+
export function SettingsPanel({ open, settings, analysing, onChange, onClose, text }) {
|
|
8
8
|
const dialog = useRef(null);
|
|
9
9
|
const voiced = useVoiced(text.language);
|
|
10
10
|
useEffect(() => {
|
|
@@ -17,7 +17,7 @@ export function SettingsPanel({ open, settings, onChange, onClose, text }) {
|
|
|
17
17
|
}
|
|
18
18
|
}, [open]);
|
|
19
19
|
const set = (key, value) => onChange({ ...settings, [key]: value });
|
|
20
|
-
return (_jsxs("dialog", { ref: dialog, class: "dialog", "aria-label": text.settings, onClose: onClose, onClick: (event) => event.target === dialog.current && onClose(), children: [_jsxs("header", { children: [_jsx("h2", { children: text.settings }), _jsx("button", { class: "icon-button", type: "button", title: text.close, "aria-label": text.close, onClick: onClose, children: _jsx("svg", { class: "icon", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { d: "M6 6L18 18M18 6L6 18" }) }) })] }), _jsxs("div", { class: "setting-columns", children: [_jsxs(Section, { title: text.look, icon: lookIcon, children: [_jsxs("div", { class: "setting-group", children: [_jsx("p", { class: "setting-label", children: text.themeChoice }), _jsx(Segments, { label: text.themeChoice, children: ["system", "light", "dark"].map((theme) => _jsx(Choice, { on: settings.theme === theme, onPick: () => set("theme", theme), children: text.themes[theme] }, theme)) })] }), _jsxs("div", { class: "setting-group", children: [_jsxs("p", { class: "setting-label", children: [text.boardSkin, " ", _jsx("b", { children: nameOf(settings.board, text) })] }), _jsx(Swatches, { look: "board", label: text.boardSkin, settings: settings, onPick: (id) => set("board", id), text: text })] }), _jsxs("div", { class: "setting-group", children: [_jsx("p", { class: "setting-label", children: text.pieceSet }), _jsx("div", { class: "piece-sets", role: "radiogroup", "aria-label": text.pieceSet, children: pieceSets.map((pieceSet) => (_jsxs(Choice, { on: settings.set === pieceSet, onPick: () => set("set", pieceSet), children: [_jsxs("span", { class: "sample", children: [_jsx(Piece, { letter: "N", set: pieceSet }), _jsx(Piece, { letter: "n", set: pieceSet })] }), nameOf(pieceSet, text)] }, pieceSet))) })] }), _jsxs("div", { class: "setting-group setting-pair", children: [_jsxs("div", { children: [_jsx("p", { class: "setting-label", children: text.pieceColours }), _jsx(Swatches, { look: "pieces", label: text.pieceColours, settings: settings, onPick: (id) => set("pieces", id), text: text })] }), _jsxs("div", { children: [_jsx("p", { class: "setting-label", children: text.marksColour }), _jsx(Swatches, { look: "marks", label: text.marksColour, settings: settings, onPick: (id) => set("marks", id), text: text })] })] })] }), _jsxs("div", { class: "setting-column", children: [_jsxs(Section, { title: text.play, icon: playIcon, children: [_jsx(Switch, { on: settings.dots, onSwitch: (on) => set("dots", on), children: text.dots }), _jsx(Switch, { on: settings.squareNames, onSwitch: (on) => set("squareNames", on), children: text.squareNames }), _jsx(Switch, { on: settings.explain, onSwitch: (on) => set("explain", on), children: text.explain }), _jsx(Switch, { on: settings.book, onSwitch: (on) => set("book", on), children: text.bookBadges }), _jsx(Switch, { on: settings.helps, onSwitch: (on) => set("helps", on), children: text.helps }), _jsx(Switch, { on: settings.sound, onSwitch: (on) => set("sound", on), children: text.sounds }), _jsx(Switch, { on: settings.voice, onSwitch: (on) => set("voice", on), ...(voiced ? {} : {
|
|
20
|
+
return (_jsxs("dialog", { ref: dialog, class: "dialog", "aria-label": text.settings, onClose: onClose, onClick: (event) => event.target === dialog.current && onClose(), children: [_jsxs("header", { children: [_jsx("h2", { children: text.settings }), _jsx("button", { class: "icon-button", type: "button", title: text.close, "aria-label": text.close, onClick: onClose, children: _jsx("svg", { class: "icon", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { d: "M6 6L18 18M18 6L6 18" }) }) })] }), _jsxs("div", { class: "setting-columns", children: [_jsxs(Section, { title: text.look, icon: lookIcon, children: [_jsxs("div", { class: "setting-group", children: [_jsx("p", { class: "setting-label", children: text.themeChoice }), _jsx(Segments, { label: text.themeChoice, children: ["system", "light", "dark"].map((theme) => _jsx(Choice, { on: settings.theme === theme, onPick: () => set("theme", theme), children: text.themes[theme] }, theme)) })] }), _jsxs("div", { class: "setting-group", children: [_jsxs("p", { class: "setting-label", children: [text.boardSkin, " ", _jsx("b", { children: nameOf(settings.board, text) })] }), _jsx(Swatches, { look: "board", label: text.boardSkin, settings: settings, onPick: (id) => set("board", id), text: text })] }), _jsxs("div", { class: "setting-group", children: [_jsx("p", { class: "setting-label", children: text.pieceSet }), _jsx("div", { class: "piece-sets", role: "radiogroup", "aria-label": text.pieceSet, children: pieceSets.map((pieceSet) => (_jsxs(Choice, { on: settings.set === pieceSet, onPick: () => set("set", pieceSet), children: [_jsxs("span", { class: "sample", children: [_jsx(Piece, { letter: "N", set: pieceSet }), _jsx(Piece, { letter: "n", set: pieceSet })] }), nameOf(pieceSet, text)] }, pieceSet))) })] }), _jsxs("div", { class: "setting-group setting-pair", children: [_jsxs("div", { children: [_jsx("p", { class: "setting-label", children: text.pieceColours }), _jsx(Swatches, { look: "pieces", label: text.pieceColours, settings: settings, onPick: (id) => set("pieces", id), text: text })] }), _jsxs("div", { children: [_jsx("p", { class: "setting-label", children: text.marksColour }), _jsx(Swatches, { look: "marks", label: text.marksColour, settings: settings, onPick: (id) => set("marks", id), text: text })] })] })] }), _jsxs("div", { class: "setting-column", children: [_jsxs(Section, { title: text.play, icon: playIcon, children: [_jsx(Switch, { on: settings.dots, onSwitch: (on) => set("dots", on), children: text.dots }), _jsx(Switch, { on: settings.squareNames, onSwitch: (on) => set("squareNames", on), children: text.squareNames }), _jsx(Switch, { on: settings.explain, onSwitch: (on) => set("explain", on), children: text.explain }), _jsx(Switch, { on: settings.book, onSwitch: (on) => set("book", on), children: text.bookBadges }), _jsx(Switch, { on: settings.helps, onSwitch: (on) => set("helps", on), ...(analysing ? { locked: { on: true, why: text.helpsInAnalysis } } : {}), children: text.helps }), _jsx(Switch, { on: settings.sound, onSwitch: (on) => set("sound", on), children: text.sounds }), _jsx(Switch, { on: settings.voice, onSwitch: (on) => set("voice", on), ...(voiced ? {} : { locked: { on: false, why: text.noVoice } }), children: text.voice })] }), _jsx(Section, { title: text.languageChoice, icon: languageIcon, children: _jsx(Segments, { label: text.languageChoice, children: ["auto", "it", "en"].map((language) => _jsx(Choice, { on: settings.language === language, onPick: () => set("language", language), children: text.languages[language] }, language)) }) })] })] })] }));
|
|
21
21
|
}
|
|
22
22
|
// The sections' line icons: a palette, a pawn, a globe.
|
|
23
23
|
const lookIcon = (_jsxs(_Fragment, { children: [_jsx("path", { d: "M12 3a9 9 0 1 0 0 18c1.2 0 1.8-.8 1.8-1.7 0-1.2-1-1.6-1-2.6 0-1 .8-1.7 1.8-1.7H17a4 4 0 0 0 4-4C21 6.5 17 3 12 3Z" }), _jsx("circle", { cx: "7.5", cy: "11", r: "1.2" }), _jsx("circle", { cx: "10", cy: "7", r: "1.2" }), _jsx("circle", { cx: "14.5", cy: "7", r: "1.2" })] }));
|
|
@@ -36,10 +36,9 @@ function Segments({ label, children }) {
|
|
|
36
36
|
function Choice({ on, onPick, children }) {
|
|
37
37
|
return (_jsx("button", { type: "button", role: "radio", "aria-checked": on, onClick: onPick, children: children }));
|
|
38
38
|
}
|
|
39
|
-
/** A switch; one
|
|
40
|
-
function Switch({ on, onSwitch,
|
|
41
|
-
|
|
42
|
-
return (_jsxs("label", { class: "switch", title: unavailable, children: [_jsx("span", { children: children }), _jsx("input", { type: "checkbox", role: "switch", checked: on && usable, disabled: !usable, title: unavailable, onChange: (event) => onSwitch(event.currentTarget.checked) })] }));
|
|
39
|
+
/** A switch; one `locked` here holds its value, disabled, and its tooltip says why. */
|
|
40
|
+
function Switch({ on, onSwitch, locked, children }) {
|
|
41
|
+
return (_jsxs("label", { class: "switch", title: locked?.why, children: [_jsx("span", { children: children }), _jsx("input", { type: "checkbox", role: "switch", checked: locked?.on ?? on, disabled: locked !== undefined, title: locked?.why, onChange: (event) => onSwitch(event.currentTarget.checked) })] }));
|
|
43
42
|
}
|
|
44
43
|
/** Whether the browser has a voice for the language, looked at again whenever its voices change. */
|
|
45
44
|
function useVoiced(language) {
|
package/dist/page/guide/en.md
CHANGED
|
@@ -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 **
|
|
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,14 +37,14 @@ 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
|
|
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
|
|
44
44
|
|
|
45
45
|

|
|
46
46
|
|
|
47
|
-
- With **"The AI teaches as you play"**, in the settings, the **Helps** card shows the pieces in danger, the pins and the pawns, and the AI, as your coach, gives you an idea.
|
|
47
|
+
- With **"The AI teaches as you play"**, in the settings, the **Helps** card shows the pieces in danger, the pins and the pawns, and the AI, as your coach, gives you an idea. In analysis the card and its marks are always there.
|
|
48
48
|
- The AI draws **coloured arrows** on the board: the **?** beside the moves explains every mark.
|
|
49
49
|
- In the review, a **badge** on a move gives the AI's judgment: a great move, an inaccuracy, a blunder.
|
|
50
50
|
|
package/dist/page/guide/it.md
CHANGED
|
@@ -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 **
|
|
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,14 +37,14 @@ 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
|
|
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
|
|
44
44
|
|
|
45
45
|

|
|
46
46
|
|
|
47
|
-
- Con **«L'AI ti insegna mentre giochi»**, nelle impostazioni, la scheda **Aiuti** mostra i pezzi in pericolo, le inchiodature e i pedoni, e l'AI, da allenatore, ti dà un'idea.
|
|
47
|
+
- Con **«L'AI ti insegna mentre giochi»**, nelle impostazioni, la scheda **Aiuti** mostra i pezzi in pericolo, le inchiodature e i pedoni, e l'AI, da allenatore, ti dà un'idea. Nell'analisi la scheda e i suoi segni ci sono sempre.
|
|
48
48
|
- L'AI disegna **frecce colorate** sulla scacchiera: il **?** accanto alle mosse spiega ogni segno.
|
|
49
49
|
- Nel ripasso, un **bollino** su una mossa dice il giudizio dell'AI: un'ottima mossa, un'imprecisione, un errore grave.
|
|
50
50
|
|
|
@@ -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
|
-
/**
|
|
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
|
|
19
|
-
return
|
|
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
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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/talk/talk.css
CHANGED
|
@@ -266,6 +266,14 @@
|
|
|
266
266
|
color: var(--accent);
|
|
267
267
|
}
|
|
268
268
|
|
|
269
|
+
/* In analysis the chat that answers, over the box as its addressee: its name opens the chat's window, as on the AI's card in a game */
|
|
270
|
+
.sent + .ai-name {
|
|
271
|
+
align-self: flex-start;
|
|
272
|
+
margin: 0 0 6px 12px;
|
|
273
|
+
color: var(--soft);
|
|
274
|
+
font-size: 0.875rem;
|
|
275
|
+
}
|
|
276
|
+
|
|
269
277
|
/* 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 */
|
|
270
278
|
.says,
|
|
271
279
|
.sent {
|
|
@@ -328,6 +336,11 @@
|
|
|
328
336
|
cursor: pointer;
|
|
329
337
|
}
|
|
330
338
|
|
|
339
|
+
/* In the game's review the mark ends the title's line */
|
|
340
|
+
.review .head > .skip-reading {
|
|
341
|
+
margin-left: auto;
|
|
342
|
+
}
|
|
343
|
+
|
|
331
344
|
.skip-reading .icon {
|
|
332
345
|
display: block;
|
|
333
346
|
width: 16px;
|
package/dist/page/talk/talk.js
CHANGED
|
@@ -72,7 +72,7 @@ export function Legend({ review, words }) {
|
|
|
72
72
|
* move takes them with it. 200 characters from one move of the AI's to the next: once they are written, the box goes until its move (D-54). In analysis the
|
|
73
73
|
* AI's answers are in the same thread, each by a small Torretta, and each line of the player's names the move it was written on (D-101).
|
|
74
74
|
*/
|
|
75
|
-
export function Compose({ lines, newestFirst, spent, draft, onDraft, onSend, canMove, name, problem, about, text }) {
|
|
75
|
+
export function Compose({ lines, newestFirst, spent, draft, onDraft, onSend, canMove, name, problem, about, to, text }) {
|
|
76
76
|
const list = useRef(null);
|
|
77
77
|
useOnlyWhole(list, lines.map((line) => line.text));
|
|
78
78
|
const left = talkLimits.line - spent;
|
|
@@ -87,7 +87,7 @@ export function Compose({ lines, newestFirst, spent, draft, onDraft, onSend, can
|
|
|
87
87
|
};
|
|
88
88
|
return (_jsxs(_Fragment, { children: [_jsx("ol", { ref: list, class: "sent", "aria-live": "polite", children: inOrder(lines.map((line, index) => (_jsxs("li", { class: line.who === "ai" ? "from-ai" : undefined, style: about === undefined ? aged(lines.length - 1 - index) : reread(index, lastAnswer), children: [line.who === "ai"
|
|
89
89
|
? (_jsxs(_Fragment, { children: [_jsx(Torretta, { size: 24 }), _jsx("p", { class: "say", children: _jsx(Marked, { text: line.text }) })] }))
|
|
90
|
-
: (_jsxs("p", { class: "said", children: [about === undefined || line.shown?.move === undefined ? null : _jsx(ShownMove, { move: line.shown.move, onShow: () => about.onShow(line.shown.at), text: text }), line.text] })), index === lastWritten ? _jsx(Status, { heard: line.heard, name: name, text: text }) : null] }, index))), newestFirst) }), left <= 0 ? null : (_jsxs("form", { class: about === undefined ? "compose" : "compose asking", onSubmit: (event) => {
|
|
90
|
+
: (_jsxs("p", { class: "said", children: [about === undefined || line.shown?.move === undefined ? null : _jsx(ShownMove, { move: line.shown.move, onShow: () => about.onShow(line.shown.at), text: text }), line.text] })), index === lastWritten ? _jsx(Status, { heard: line.heard, name: name, text: text }) : null] }, index))), newestFirst) }), to, left <= 0 ? null : (_jsxs("form", { class: about === undefined ? "compose" : "compose asking", onSubmit: (event) => {
|
|
91
91
|
event.preventDefault();
|
|
92
92
|
submit();
|
|
93
93
|
}, children: [_jsx("p", { class: "limit", role: "status", children: problem ?? "" }), _jsx("textarea", { rows: 2, maxLength: left, placeholder: prompt, "aria-label": prompt, value: draft, onInput: (event) => onDraft(event.currentTarget.value), onKeyDown: (event) => {
|
|
@@ -135,7 +135,7 @@ function squareIn(word) {
|
|
|
135
135
|
/** The AI's word on the whole game (D-69), in the Torretta's bubble at the start of the game: a paragraph, and its points as a list. */
|
|
136
136
|
function GameReview({ by, summary, skipping, text }) {
|
|
137
137
|
const [lead = "", ...points] = summary.split(/\s*•\s+/);
|
|
138
|
-
return (_jsxs("section", { class: "say review", "aria-label": text.reviewBy(by), children: [
|
|
138
|
+
return (_jsxs("section", { class: "say review", "aria-label": text.reviewBy(by), children: [_jsxs("p", { class: "head", children: [_jsx("svg", { class: "icon", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { d: "M12 4V20M8 20H16M5 7H19M5 7L2.5 13A2.5 2 0 0 0 7.5 13ZM19 7L16.5 13A2.5 2 0 0 0 21.5 13Z" }) }), text.reviewBy(by), skipping] }), lead === "" ? null : (_jsx("p", { children: _jsx(Marked, { text: lead }) })), points.length === 0 ? null : (_jsx("ul", { children: points.map((point, index) => (_jsx("li", { children: _jsx(Marked, { text: point }) }, index))) }))] }));
|
|
139
139
|
}
|
|
140
140
|
function RailPlayer({ place, picture, name, side, moves, unread = false }) {
|
|
141
141
|
return (_jsxs("div", { class: `rail-player ${place}${moves ? " moving" : ""}`, children: [_jsx("span", { class: `face${unread ? " unread" : ""}`, children: picture }), _jsxs("span", { class: "rail-name", children: [name, " ", side === undefined ? null : _jsxs("span", { class: "note", children: ["\u00B7 ", side] })] })] }));
|
package/dist/page/text.js
CHANGED
|
@@ -30,6 +30,7 @@ const english = {
|
|
|
30
30
|
sounds: "Sounds",
|
|
31
31
|
voice: "Read the AI's words aloud",
|
|
32
32
|
noVoice: "This browser has no voice for this language: add one in your system's settings",
|
|
33
|
+
helpsInAnalysis: "In analysis the helps are always on: the switch counts in a game",
|
|
33
34
|
skipReading: "Stop reading this aloud",
|
|
34
35
|
saidMove: (move) => moveInWords(englishMoveWords, move),
|
|
35
36
|
newGame: "New game",
|
|
@@ -181,7 +182,7 @@ const english = {
|
|
|
181
182
|
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.`,
|
|
182
183
|
noChat: "No chat at the table: choose one",
|
|
183
184
|
change: "Change the chat or its model",
|
|
184
|
-
install: (name) => `To play with ${name}, install Claude Code or
|
|
185
|
+
install: (name) => `To play with ${name}, install Claude Code, Codex or Antigravity, then open this page again.`,
|
|
185
186
|
chat: "Chat",
|
|
186
187
|
model: "Model",
|
|
187
188
|
effort: "Effort",
|
|
@@ -468,6 +469,7 @@ const italian = {
|
|
|
468
469
|
sounds: "Suoni",
|
|
469
470
|
voice: "Leggi ad alta voce le parole dell'AI",
|
|
470
471
|
noVoice: "Questo browser non ha una voce per questa lingua: aggiungila nelle impostazioni del sistema",
|
|
472
|
+
helpsInAnalysis: "Nell'analisi gli aiuti sono sempre accesi: l'interruttore vale in partita",
|
|
471
473
|
skipReading: "Smetti di leggere questo a voce",
|
|
472
474
|
saidMove: (move) => moveInWords(italianMoveWords, move),
|
|
473
475
|
newGame: "Nuova partita",
|
|
@@ -619,7 +621,7 @@ const italian = {
|
|
|
619
621
|
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.`,
|
|
620
622
|
noChat: "Nessuna chat al tavolo: scegline una",
|
|
621
623
|
change: "Cambia la chat o il modello",
|
|
622
|
-
install: (name) => `Per giocare con ${name}, installa Claude Code o
|
|
624
|
+
install: (name) => `Per giocare con ${name}, installa Claude Code, Codex o Antigravity, poi apri di nuovo questa pagina.`,
|
|
623
625
|
chat: "Chat",
|
|
624
626
|
model: "Modello",
|
|
625
627
|
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
|
+
}
|
|
@@ -11,14 +11,17 @@ export declare class Bell {
|
|
|
11
11
|
private busy;
|
|
12
12
|
private woken;
|
|
13
13
|
private hooked;
|
|
14
|
+
private stopWatch;
|
|
14
15
|
private readonly news;
|
|
15
16
|
private readonly changed;
|
|
17
|
+
private readonly stopped;
|
|
16
18
|
/**
|
|
17
19
|
* `news` says whether a ring no hook heard is still worth waking the chat for; `changed` is told when what the page shows of the chat may change: it
|
|
18
|
-
* starts or stops listening, the doorbell wakes it, its turn ends
|
|
20
|
+
* starts or stops listening, the doorbell wakes it, its turn ends; `stopped` whether the host says the chat was stopped in its turn (Esc), which no
|
|
21
|
+
* hook tells.
|
|
19
22
|
*/
|
|
20
|
-
constructor(news?: () => boolean, changed?: () => void);
|
|
21
|
-
/** Whether the chat is in a turn, as far as the doorbell knows: a chat stopped with Esc stays in it until its next tool. */
|
|
23
|
+
constructor(news?: () => boolean, changed?: () => void, stopped?: () => boolean);
|
|
24
|
+
/** Whether the chat is in a turn, as far as the doorbell knows: a chat stopped with Esc stays in it until its next tool, or until a ring finds it stopped. */
|
|
22
25
|
get inTurn(): boolean;
|
|
23
26
|
/** Whether the doorbell woke the chat in the turn it is in. */
|
|
24
27
|
get woke(): boolean;
|
|
@@ -35,6 +38,7 @@ export declare class Bell {
|
|
|
35
38
|
/**
|
|
36
39
|
* The player acted on the page: the hook waiting hears it now, or the next one as soon as it comes. In a host that runs no hook nothing tells the end of
|
|
37
40
|
* a turn: an act the chat waits for finds it out of its turn, and nobody hears the player, unless the chat `mayBeAtWork` on the AI's move meanwhile.
|
|
41
|
+
* A chat stopped in its turn ends no turn either: while the ring waits, the doorbell looks every `stopCheck` whether the host says so.
|
|
38
42
|
*/
|
|
39
43
|
ring(mayBeAtWork?: boolean): void;
|
|
40
44
|
/**
|
|
@@ -45,7 +49,12 @@ export declare class Bell {
|
|
|
45
49
|
/** The chat left the table (D-104): its process ended, and nobody is in a turn or waits for the bell; a ring no hook heard is no longer news for anyone. */
|
|
46
50
|
chatLeft(): void;
|
|
47
51
|
private tellIf;
|
|
52
|
+
/** A ring waits for a chat in its turn: a chat stopped there, which nobody hears, is out of it now, and the page says where to wake it. */
|
|
53
|
+
private watchForStop;
|
|
54
|
+
private waitsInTurn;
|
|
48
55
|
}
|
|
56
|
+
/** How often the doorbell looks whether a chat a ring waits for was stopped in its turn. */
|
|
57
|
+
export declare const stopCheck = 2000;
|
|
49
58
|
/**
|
|
50
59
|
* Notes in the data folder the port a chat's page took off its folder's usual ports, one file per page, for the hook to find it there; then forgets, off
|
|
51
60
|
* the caller's clock, the notes of its folder whose page is gone: nobody listens at the port, or another folder's page does. A page that answers late
|
|
@@ -56,11 +65,19 @@ export declare function notePagePort(home: string, folder: string, port: number,
|
|
|
56
65
|
export declare function forgetPagePort(home: string, folder: string, port: number): void;
|
|
57
66
|
/** What Codex's Stop hook answers to wake the chat: its turn goes on with the doorbell's line. */
|
|
58
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";
|
|
59
72
|
/**
|
|
60
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
|
|
61
|
-
* Codex's Stop
|
|
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.
|
|
62
77
|
*/
|
|
63
|
-
export declare function listen(
|
|
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;
|
|
64
81
|
/**
|
|
65
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.
|
|
66
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.
|