chesspal 0.3.0 → 0.3.1
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/dist/commands/context.d.ts +2 -0
- package/dist/page/app/app.js +27 -11
- package/dist/page/dialogs/profiles.js +2 -2
- package/dist/page/layout/layout.css +0 -26
- package/dist/page/style.css +1 -1
- package/dist/page/table/table.css +123 -0
- package/dist/page/table/window.js +130 -0
- package/dist/page/talk/chat.css +13 -2
- package/dist/page/talk/talk.js +2 -2
- package/dist/page/talk/voice.js +35 -9
- package/dist/page/text.js +52 -4
- package/dist/server/page/chat.d.ts +5 -0
- package/dist/server/page/chat.js +9 -1
- package/dist/server/page/page.d.ts +4 -0
- package/dist/server/page/page.js +7 -2
- package/dist/server/page/table.d.ts +34 -15
- package/dist/server/page/table.js +126 -18
- package/dist/server/table/hosts/chats.js +6 -3
- package/dist/server/table/hosts/play.d.ts +8 -4
- package/dist/server/table/hosts/play.js +36 -8
- package/dist/server/table/hosts/seat.d.ts +13 -5
- package/dist/server/table/hosts/seat.js +21 -15
- package/dist/server/table/link.js +1 -0
- package/package.json +1 -1
- package/dist/page/table/opener.css +0 -65
- package/dist/page/table/opener.js +0 -59
package/dist/page/text.js
CHANGED
|
@@ -5,12 +5,31 @@ export function textFor(languages) {
|
|
|
5
5
|
}
|
|
6
6
|
const englishNames = { K: "king", Q: "queen", R: "rook", B: "bishop", N: "knight", P: "pawn" };
|
|
7
7
|
const italianNames = { K: "re", Q: "donna", R: "torre", B: "alfiere", N: "cavallo", P: "pedone" };
|
|
8
|
+
const englishMoveWords = {
|
|
9
|
+
names: englishNames,
|
|
10
|
+
castles: { short: "castles short", long: "castles long" },
|
|
11
|
+
from: "from",
|
|
12
|
+
to: "to",
|
|
13
|
+
takes: "takes",
|
|
14
|
+
promotes: "promotes to",
|
|
15
|
+
check: { check: "check", mate: "checkmate" },
|
|
16
|
+
};
|
|
17
|
+
const italianMoveWords = {
|
|
18
|
+
names: italianNames,
|
|
19
|
+
castles: { short: "arrocco corto", long: "arrocco lungo" },
|
|
20
|
+
from: "da",
|
|
21
|
+
to: "in",
|
|
22
|
+
takes: "prende",
|
|
23
|
+
promotes: "promuove a",
|
|
24
|
+
check: { check: "scacco", mate: "scacco matto" },
|
|
25
|
+
};
|
|
8
26
|
const english = {
|
|
9
27
|
language: "en",
|
|
10
28
|
flip: "Flip",
|
|
11
29
|
theme: "Light or dark theme",
|
|
12
30
|
sounds: "Sounds",
|
|
13
31
|
voice: "Read the AI's words aloud",
|
|
32
|
+
saidMove: (move) => moveInWords(englishMoveWords, move),
|
|
14
33
|
newGame: "New game",
|
|
15
34
|
offerDraw: "Offer a draw",
|
|
16
35
|
offerDrawWhen: "Offer a draw: on your turn, once each side has moved",
|
|
@@ -134,15 +153,25 @@ const english = {
|
|
|
134
153
|
asleep: (name) => `${name} is not listening: write a message, such as "hi", in the window where you opened ${name}, not here. Its clock waits until it answers.`,
|
|
135
154
|
asleepInAnalysis: (name) => `${name} is not listening: write to it in the window where you opened it, not here.`,
|
|
136
155
|
table: {
|
|
137
|
-
|
|
138
|
-
|
|
156
|
+
title: "Who are you playing?",
|
|
157
|
+
choose: "Choose the chat to play with: it opens in a window of its own, to keep open.",
|
|
158
|
+
restartHint: "Choose the model and effort, then press Claude Code or Codex: the chat starts again, clean, in the same window, once it has ended its answer. The game stays as it is.",
|
|
159
|
+
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.`,
|
|
160
|
+
noChat: "No chat at the table: choose one",
|
|
161
|
+
change: "Change the chat or its model",
|
|
139
162
|
install: (name) => `To play with ${name}, install Claude Code or Codex, then open this page again.`,
|
|
140
163
|
model: "Model",
|
|
141
164
|
effort: "Effort",
|
|
142
165
|
asHost: (host) => `as set in ${host}`,
|
|
143
166
|
opening: "Opening the chat… In the terminal, if it asks whether you trust the folder, choose Yes with the arrow keys and press Enter.",
|
|
144
167
|
again: (name) => `${name} starts again with a clean chat for the new game…`,
|
|
168
|
+
restart: "The chat starts again, clean…",
|
|
169
|
+
waiting: (name) => `${name} starts again once it has ended its answer…`,
|
|
170
|
+
restartChanged: "Starts again, clean, with the new choices",
|
|
171
|
+
restartNow: "Start again now: it cuts the answer it is giving",
|
|
172
|
+
cancel: "Do not start again",
|
|
145
173
|
failed: "The terminal did not open: type the command below in a terminal.",
|
|
174
|
+
notRestarted: "The chat did not start again: close its window, then open another from here.",
|
|
146
175
|
typeIt: "Or type in a terminal:",
|
|
147
176
|
},
|
|
148
177
|
commandHere: (name) => `/clear and the other commands go in the window where you opened ${name}, not here.`,
|
|
@@ -383,6 +412,7 @@ const italian = {
|
|
|
383
412
|
theme: "Tema chiaro o scuro",
|
|
384
413
|
sounds: "Suoni",
|
|
385
414
|
voice: "Leggi ad alta voce le parole dell'AI",
|
|
415
|
+
saidMove: (move) => moveInWords(italianMoveWords, move),
|
|
386
416
|
newGame: "Nuova partita",
|
|
387
417
|
offerDraw: "Proponi patta",
|
|
388
418
|
offerDrawWhen: "Proponi patta: nel tuo turno, dopo una mossa a testa",
|
|
@@ -506,15 +536,25 @@ const italian = {
|
|
|
506
536
|
asleep: (name) => `${name} non ti sta ascoltando: scrivi un messaggio, per esempio "ciao", nella finestra dove hai aperto ${name}, non qui. Il suo orologio aspetta che risponda.`,
|
|
507
537
|
asleepInAnalysis: (name) => `${name} non ti sta ascoltando: scrivigli nella finestra dove l'hai aperto, non qui.`,
|
|
508
538
|
table: {
|
|
509
|
-
|
|
510
|
-
|
|
539
|
+
title: "Contro chi giochi?",
|
|
540
|
+
choose: "Scegli la chat con cui giocare: parte in una finestra a parte, da tenere aperta.",
|
|
541
|
+
restartHint: "Scegli modello e ragionamento, poi premi Claude Code o Codex: la chat riparte pulita nella stessa finestra, appena ha finito di rispondere. La partita resta com'è.",
|
|
542
|
+
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.`,
|
|
543
|
+
noChat: "Nessuna chat al tavolo: scegline una",
|
|
544
|
+
change: "Cambia la chat o il modello",
|
|
511
545
|
install: (name) => `Per giocare con ${name}, installa Claude Code o Codex, poi apri di nuovo questa pagina.`,
|
|
512
546
|
model: "Modello",
|
|
513
547
|
effort: "Ragionamento",
|
|
514
548
|
asHost: (host) => `come è impostato in ${host}`,
|
|
515
549
|
opening: "Apro la chat… Nel terminale, se ti chiede se ti fidi della cartella, scegli Yes con le frecce e premi Invio.",
|
|
516
550
|
again: (name) => `${name} riparte con una chat pulita per la nuova partita…`,
|
|
551
|
+
restart: "La chat riparte pulita…",
|
|
552
|
+
waiting: (name) => `${name} riparte appena ha finito di rispondere…`,
|
|
553
|
+
restartChanged: "Riparte pulita con le nuove scelte",
|
|
554
|
+
restartNow: "Riparti adesso: interrompe la risposta in corso",
|
|
555
|
+
cancel: "Non ripartire",
|
|
517
556
|
failed: "Il terminale non si è aperto: scrivi il comando qui sotto in un terminale.",
|
|
557
|
+
notRestarted: "La chat non è ripartita: chiudi la sua finestra, poi aprine un'altra da qui.",
|
|
518
558
|
typeIt: "Oppure scrivi in un terminale:",
|
|
519
559
|
},
|
|
520
560
|
commandHere: (name) => `/clear e gli altri comandi vanno scritti nella finestra dove hai aperto ${name}, non qui.`,
|
|
@@ -811,6 +851,14 @@ function italianCulprit(by) {
|
|
|
811
851
|
function italianWith(preposition, letter) {
|
|
812
852
|
return italianThe[letter].replace(/^il /, `${preposition}l `).replace(/^la /, `${preposition}lla `).replace(/^l'/, `${preposition}ll'`);
|
|
813
853
|
}
|
|
854
|
+
/** A move in words: "knight from b takes d7, check", "cavallo in f3", "arrocco corto". */
|
|
855
|
+
function moveInWords(words, move) {
|
|
856
|
+
const promotion = "castles" in move || move.promotion === undefined ? undefined : `${words.promotes} ${words.names[move.promotion]}`;
|
|
857
|
+
const played = "castles" in move
|
|
858
|
+
? words.castles[move.castles]
|
|
859
|
+
: [words.names[move.piece], ...(move.from === undefined ? [] : [words.from, move.from]), move.takes ? words.takes : words.to, move.square].join(" ");
|
|
860
|
+
return [played, promotion, move.check === undefined ? undefined : words.check[move.check]].filter((part) => part !== undefined).join(", ");
|
|
861
|
+
}
|
|
814
862
|
/** The words with their first letter capital, to open a sentence. */
|
|
815
863
|
function capital(words) {
|
|
816
864
|
return words.charAt(0).toUpperCase() + words.slice(1);
|
|
@@ -8,6 +8,7 @@ export interface ChatSeen {
|
|
|
8
8
|
readonly given: ReadonlySet<string>;
|
|
9
9
|
readonly seen: string | undefined;
|
|
10
10
|
readonly positionSeen: SeenPosition | null | undefined;
|
|
11
|
+
readonly positionFirstSeen: SeenPosition | undefined;
|
|
11
12
|
host(): string | undefined;
|
|
12
13
|
}
|
|
13
14
|
/** The chat as the page reads it, in the chat's own process: read from its context at each request, so always as the chat left it. */
|
|
@@ -39,6 +40,10 @@ export declare const chatCopy: z.ZodObject<{
|
|
|
39
40
|
started: z.ZodOptional<z.ZodString>;
|
|
40
41
|
plies: z.ZodInt;
|
|
41
42
|
}, z.core.$strip>>>;
|
|
43
|
+
positionFirstSeen: z.ZodOptional<z.ZodObject<{
|
|
44
|
+
started: z.ZodOptional<z.ZodString>;
|
|
45
|
+
plies: z.ZodInt;
|
|
46
|
+
}, z.core.$strip>>;
|
|
42
47
|
given: z.ZodArray<z.ZodString>;
|
|
43
48
|
host: z.ZodOptional<z.ZodString>;
|
|
44
49
|
aiModel: z.ZodOptional<z.ZodObject<{
|
package/dist/server/page/chat.js
CHANGED
|
@@ -13,6 +13,9 @@ export function chatSeenOf(chat) {
|
|
|
13
13
|
get positionSeen() {
|
|
14
14
|
return chat.positionSeen;
|
|
15
15
|
},
|
|
16
|
+
get positionFirstSeen() {
|
|
17
|
+
return chat.positionFirstSeen;
|
|
18
|
+
},
|
|
16
19
|
host: () => chat.host?.(),
|
|
17
20
|
};
|
|
18
21
|
}
|
|
@@ -47,6 +50,7 @@ export const chatAttach = z.object({
|
|
|
47
50
|
host: z.string().optional(),
|
|
48
51
|
outdated: z.boolean(),
|
|
49
52
|
});
|
|
53
|
+
const seenPosition = z.object({ started: z.string().optional(), plies: z.int().min(0) });
|
|
50
54
|
/**
|
|
51
55
|
* What the chat attached to the table tells it after each tool, and when the page has news no file holds (`news`): what it saw, the words it was given,
|
|
52
56
|
* its host, and the AI's model and host, which the table notes for a game that ends on the page (D-82, D-97).
|
|
@@ -54,7 +58,8 @@ export const chatAttach = z.object({
|
|
|
54
58
|
export const chatCopy = z.object({
|
|
55
59
|
token: z.string(),
|
|
56
60
|
seen: z.string().optional(),
|
|
57
|
-
positionSeen:
|
|
61
|
+
positionSeen: seenPosition.nullable().optional(),
|
|
62
|
+
positionFirstSeen: seenPosition.optional(),
|
|
58
63
|
given: z.array(z.string()),
|
|
59
64
|
host: z.string().optional(),
|
|
60
65
|
aiModel: z.object({ model: z.string(), effort: z.string().optional() }).optional(),
|
|
@@ -74,6 +79,9 @@ export function attachedSeen(attached) {
|
|
|
74
79
|
get positionSeen() {
|
|
75
80
|
return attached()?.copy?.positionSeen;
|
|
76
81
|
},
|
|
82
|
+
get positionFirstSeen() {
|
|
83
|
+
return attached()?.copy?.positionFirstSeen;
|
|
84
|
+
},
|
|
77
85
|
host: () => attached()?.copy?.host ?? attached()?.chat.host,
|
|
78
86
|
};
|
|
79
87
|
}
|
|
@@ -69,6 +69,10 @@ export interface Served {
|
|
|
69
69
|
readonly at: number;
|
|
70
70
|
readonly how: Opening;
|
|
71
71
|
} | undefined;
|
|
72
|
+
/** The host the page asked the chat to start again with while it was at work (D-107). */
|
|
73
|
+
restartWanted?: {
|
|
74
|
+
readonly host: string;
|
|
75
|
+
} | undefined;
|
|
72
76
|
}
|
|
73
77
|
/**
|
|
74
78
|
* The chat has read the game as it is now, at the end of a tool's call: the doorbell wakes it only for what changes it after; and, when the tool `showed` it
|
package/dist/server/page/page.js
CHANGED
|
@@ -17,7 +17,7 @@ import { sendFile, sendHtml } from "./files.js";
|
|
|
17
17
|
import { answerGame, chatCalls } from "./game.js";
|
|
18
18
|
import { json, send } from "./http.js";
|
|
19
19
|
import { answerProfiles, readRatingsOf } from "./profiles.js";
|
|
20
|
-
import { answerTable } from "./table.js";
|
|
20
|
+
import { answerTable, restartWhenAtRest } from "./table.js";
|
|
21
21
|
/**
|
|
22
22
|
* Serves the page of the context's folder on 127.0.0.1, on a port of its own when free, else on any free one, where the doorbell cannot find it; the
|
|
23
23
|
* table's on its own ports alone, which keep two tables apart. The page reads the chat from the context in the chat's process, and on the table from
|
|
@@ -33,7 +33,11 @@ export async function startPage(context, options = {}) {
|
|
|
33
33
|
context: pageContext(context),
|
|
34
34
|
chat,
|
|
35
35
|
opponent: options.opponent ?? (() => aiNameOf(chat.host())),
|
|
36
|
-
|
|
36
|
+
// A start again asked while the chat was at work comes once it has ended its answer (D-107).
|
|
37
|
+
bell: new Bell(() => hasNews(context, chat), () => {
|
|
38
|
+
restartWhenAtRest(served);
|
|
39
|
+
tellWatchers(watchers);
|
|
40
|
+
}),
|
|
37
41
|
watchers,
|
|
38
42
|
port: 0,
|
|
39
43
|
outdated: outdatedHostFiles(folder, context.platform).length > 0,
|
|
@@ -77,6 +81,7 @@ export function chatHasRead(context, showed) {
|
|
|
77
81
|
catch {
|
|
78
82
|
context.positionSeen = null;
|
|
79
83
|
}
|
|
84
|
+
context.positionFirstSeen ??= context.positionSeen ?? undefined;
|
|
80
85
|
// The chat on another position may now be thinking on its move: the page hears it, which no file tells when no bell woke the chat.
|
|
81
86
|
if (!sameSeen(before, context.positionSeen)) {
|
|
82
87
|
context.heard?.();
|
|
@@ -9,31 +9,50 @@ export interface ChatHost {
|
|
|
9
9
|
readonly command: string;
|
|
10
10
|
}
|
|
11
11
|
/**
|
|
12
|
-
* How the table opens a chat: the hosts it offers, and a terminal with one of them, whether one opened;
|
|
13
|
-
* and how it is asked to start it again.
|
|
12
|
+
* How the table opens a chat: the hosts it offers, and a terminal with one of them, whether one opened; the chat `chesspal play` holds in a folder, with
|
|
13
|
+
* the model and effort it started with, and how it is asked to start it again.
|
|
14
14
|
*/
|
|
15
15
|
export interface TableChats {
|
|
16
16
|
hosts(): readonly ChatHost[];
|
|
17
17
|
open(host: string): Promise<boolean>;
|
|
18
|
-
|
|
19
|
-
relaunch(folder: string): boolean;
|
|
18
|
+
held(folder: string): HeldChat | undefined;
|
|
19
|
+
relaunch(folder: string, host?: string): boolean;
|
|
20
20
|
}
|
|
21
|
-
/**
|
|
22
|
-
export
|
|
23
|
-
|
|
21
|
+
/** The chat `chesspal play` holds: the model and effort its host started with, none for the host's own. */
|
|
22
|
+
export interface HeldChat {
|
|
23
|
+
readonly model?: string | undefined;
|
|
24
|
+
readonly effort?: string | undefined;
|
|
25
|
+
}
|
|
26
|
+
/** 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). */
|
|
27
|
+
export type Opening = "chat" | "again" | "restart";
|
|
28
|
+
/** What the table's page shows of its chat (D-104, D-107). */
|
|
29
|
+
export interface TableShown {
|
|
30
|
+
/** Whether a chat is at the table, and its host among those offered, when it says one. */
|
|
31
|
+
readonly chat: boolean;
|
|
32
|
+
readonly host?: string;
|
|
33
|
+
/** Whether `chesspal play` holds the chat, which the page can then start again; one opened by hand it cannot. With the model and effort it started with. */
|
|
34
|
+
readonly held: boolean;
|
|
35
|
+
readonly started?: HeldChat;
|
|
36
|
+
readonly hosts: readonly ChatHost[];
|
|
37
|
+
readonly opening?: Opening;
|
|
38
|
+
/** A start again asked while the chat was at work, waiting for it to end its answer, or to be started at once or let go from the page. */
|
|
39
|
+
readonly waiting?: true;
|
|
40
|
+
}
|
|
41
|
+
/** What the table's page shows of its chat: whether one is at the table, with which host, the hosts it can open one in, and one opening. */
|
|
24
42
|
export declare function tableShown(served: Served): {
|
|
25
|
-
table?:
|
|
26
|
-
chat: boolean;
|
|
27
|
-
hosts: readonly ChatHost[];
|
|
28
|
-
opening?: Opening;
|
|
29
|
-
};
|
|
43
|
+
table?: TableShown;
|
|
30
44
|
};
|
|
31
45
|
/** Answers the table's own routes, under /api/table/; false for any other, and for every request to a page a chat serves itself. */
|
|
32
46
|
export declare function answerTable(request: IncomingMessage, response: ServerResponse, url: URL, served: Served): boolean;
|
|
33
47
|
/**
|
|
34
48
|
* 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
|
|
35
|
-
* has seen a game; one that only greeted the player stays.
|
|
49
|
+
* 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.
|
|
36
50
|
*/
|
|
37
51
|
export declare function chatToRelaunch(served: Served): boolean;
|
|
38
|
-
/**
|
|
39
|
-
|
|
52
|
+
/**
|
|
53
|
+
* Asks `chesspal play` to start the chat at the table again: once the new game has started, or with another model or host in its place (D-107). Whether
|
|
54
|
+
* it was asked; the pages then show it opening.
|
|
55
|
+
*/
|
|
56
|
+
export declare function relaunchChat(served: Served, how?: "again" | "restart", host?: string): boolean;
|
|
57
|
+
/** Starts the chat again as the page asked while it was at work, now that it has ended its answer; a chat gone meanwhile takes the request with it. */
|
|
58
|
+
export declare function restartWhenAtRest(served: Served): void;
|
|
@@ -1,15 +1,28 @@
|
|
|
1
|
+
import { samePosition } from "../../commands/index.js";
|
|
1
2
|
import { gameInAnalysis } from "../../games/index.js";
|
|
2
3
|
import { gameFolder } from "../../profiles/index.js";
|
|
3
4
|
import { tellWatchers } from "./events.js";
|
|
4
5
|
import { bodyOf, isJson, jsonOf, runRoute, sendJson } from "./http.js";
|
|
5
|
-
/** What the table's page shows of its chat: whether one is at the table, the hosts it can open one in, and one opening. */
|
|
6
|
+
/** What the table's page shows of its chat: whether one is at the table, with which host, the hosts it can open one in, and one opening. */
|
|
6
7
|
export function tableShown(served) {
|
|
7
|
-
const { table } = served;
|
|
8
|
+
const { table, attached } = served;
|
|
8
9
|
if (table === undefined) {
|
|
9
10
|
return {};
|
|
10
11
|
}
|
|
12
|
+
const host = hostAtTable(served);
|
|
13
|
+
const held = heldChat(served);
|
|
11
14
|
const now = opening(served);
|
|
12
|
-
return {
|
|
15
|
+
return {
|
|
16
|
+
table: {
|
|
17
|
+
chat: attached !== undefined,
|
|
18
|
+
...(host === undefined ? {} : { host }),
|
|
19
|
+
held: held !== undefined,
|
|
20
|
+
...(held === undefined ? {} : { started: held }),
|
|
21
|
+
hosts: table.chats?.hosts() ?? [],
|
|
22
|
+
...(now === undefined ? {} : { opening: now }),
|
|
23
|
+
...(served.restartWanted === undefined ? {} : { waiting: true }),
|
|
24
|
+
},
|
|
25
|
+
};
|
|
13
26
|
}
|
|
14
27
|
/** Answers the table's own routes, under /api/table/; false for any other, and for every request to a page a chat serves itself. */
|
|
15
28
|
export function answerTable(request, response, url, served) {
|
|
@@ -25,30 +38,54 @@ export function answerTable(request, response, url, served) {
|
|
|
25
38
|
else if (path === "/api/table/chat") {
|
|
26
39
|
runRoute(response, () => openChat(request, response, served));
|
|
27
40
|
}
|
|
41
|
+
else if (path === "/api/table/restart") {
|
|
42
|
+
runRoute(response, () => restartChat(request, response, served));
|
|
43
|
+
}
|
|
44
|
+
else if (path === "/api/table/waiting") {
|
|
45
|
+
runRoute(response, () => endWaiting(request, response, served));
|
|
46
|
+
}
|
|
28
47
|
else {
|
|
29
|
-
sendJson(response, 404, { error: `${path} is no route of the table's: it answers POST /api/table/chat and /api/table/quit.` });
|
|
48
|
+
sendJson(response, 404, { error: `${path} is no route of the table's: it answers POST /api/table/chat, /api/table/restart, /api/table/waiting and /api/table/quit.` });
|
|
30
49
|
}
|
|
31
50
|
return true;
|
|
32
51
|
}
|
|
33
52
|
/**
|
|
34
53
|
* 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
|
|
35
|
-
* has seen a game; one that only greeted the player stays.
|
|
54
|
+
* 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.
|
|
36
55
|
*/
|
|
37
56
|
export function chatToRelaunch(served) {
|
|
38
|
-
const {
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
return attached !== undefined && stopped && !analysing && served.chat.positionSeen?.started !== undefined && served.table?.chats?.holds(attached.chat.folder) === true;
|
|
57
|
+
const { positionSeen, positionFirstSeen } = served.chat;
|
|
58
|
+
const played = positionSeen?.started !== undefined && (positionFirstSeen === undefined || !samePosition(positionSeen, positionFirstSeen));
|
|
59
|
+
return played && heldChat(served) !== undefined && chatAtRest(served);
|
|
42
60
|
}
|
|
43
|
-
/**
|
|
44
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Asks `chesspal play` to start the chat at the table again: once the new game has started, or with another model or host in its place (D-107). Whether
|
|
63
|
+
* it was asked; the pages then show it opening.
|
|
64
|
+
*/
|
|
65
|
+
export function relaunchChat(served, how = "again", host) {
|
|
45
66
|
const folder = served.attached?.chat.folder;
|
|
46
|
-
|
|
67
|
+
// Another host may first ask whether it trusts the table's folder: the pages say so, as for a chat opened.
|
|
68
|
+
const switching = how === "restart" && host !== undefined && host !== hostAtTable(served);
|
|
69
|
+
if (folder === undefined || served.table?.chats?.relaunch(folder, host) !== true) {
|
|
47
70
|
return false;
|
|
48
71
|
}
|
|
49
|
-
noteOpening(served, "
|
|
72
|
+
noteOpening(served, switching ? "chat" : how);
|
|
50
73
|
return true;
|
|
51
74
|
}
|
|
75
|
+
/** Starts the chat again as the page asked while it was at work, now that it has ended its answer; a chat gone meanwhile takes the request with it. */
|
|
76
|
+
export function restartWhenAtRest(served) {
|
|
77
|
+
const wanted = served.restartWanted;
|
|
78
|
+
if (wanted === undefined) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (heldChat(served) === undefined) {
|
|
82
|
+
served.restartWanted = undefined;
|
|
83
|
+
}
|
|
84
|
+
else if (chatAtRest(served)) {
|
|
85
|
+
served.restartWanted = undefined;
|
|
86
|
+
relaunchChat(served, "restart", wanted.host);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
52
89
|
// A terminal takes a few seconds to open and its chat to attach: a second click meanwhile would open a second chat, which serves a page of its own.
|
|
53
90
|
const openingFor = 30_000;
|
|
54
91
|
/**
|
|
@@ -56,13 +93,12 @@ const openingFor = 30_000;
|
|
|
56
93
|
* opening. Whether it opened; else the page shows the command to type.
|
|
57
94
|
*/
|
|
58
95
|
async function openChat(request, response, served) {
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
if (request.headers.origin !== `http://127.0.0.1:${served.port}` && request.headers.origin !== `http://localhost:${served.port}`) {
|
|
96
|
+
const offered = hostOffered(served, jsonOf(await bodyOf(request))?.host);
|
|
97
|
+
if (!fromTablePage(request, served)) {
|
|
62
98
|
sendJson(response, 403, { error: "Only the table's own page opens a chat." });
|
|
63
99
|
}
|
|
64
100
|
else if (offered === undefined || served.table?.chats === undefined) {
|
|
65
|
-
sendJson(response, 400, { error:
|
|
101
|
+
sendJson(response, 400, { error: hostsOffered(served) });
|
|
66
102
|
}
|
|
67
103
|
else if (served.attached !== undefined || opening(served) !== undefined) {
|
|
68
104
|
sendJson(response, 409, { error: "A chat is at the table, or opening: play there." });
|
|
@@ -77,6 +113,78 @@ async function openChat(request, response, served) {
|
|
|
77
113
|
tellWatchers(served.watchers);
|
|
78
114
|
}
|
|
79
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* Starts the chat `chesspal play` holds again, clean, with the host the page names and the model and effort it keeps for it (D-107): at once when it is at
|
|
118
|
+
* rest, else once it has ended its answer. Asked by the table's own page alone; a chat opened by hand changes its model with /model.
|
|
119
|
+
*/
|
|
120
|
+
async function restartChat(request, response, served) {
|
|
121
|
+
const offered = hostOffered(served, jsonOf(await bodyOf(request))?.host);
|
|
122
|
+
if (!fromTablePage(request, served)) {
|
|
123
|
+
sendJson(response, 403, { error: "Only the table's own page starts the chat again." });
|
|
124
|
+
}
|
|
125
|
+
else if (offered === undefined) {
|
|
126
|
+
sendJson(response, 400, { error: hostsOffered(served) });
|
|
127
|
+
}
|
|
128
|
+
else if (heldChat(served) === undefined) {
|
|
129
|
+
sendJson(response, 409, { error: "No chat the table opened is at it: open one from the page, or change the model with /model in a chat opened by hand." });
|
|
130
|
+
}
|
|
131
|
+
else if (chatAtRest(served)) {
|
|
132
|
+
sendJson(response, 200, { restarting: relaunchChat(served, "restart", offered.name) });
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
served.restartWanted = { host: offered.name };
|
|
136
|
+
tellWatchers(served.watchers);
|
|
137
|
+
sendJson(response, 200, { waiting: true });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Ends a start again the page asked while the chat was at work (D-107): `now` starts it at once, cutting the answer it gives, as after an Esc, when it would
|
|
142
|
+
* never end; else the request is let go. Asked by the table's own page alone.
|
|
143
|
+
*/
|
|
144
|
+
async function endWaiting(request, response, served) {
|
|
145
|
+
const now = jsonOf(await bodyOf(request))?.now;
|
|
146
|
+
const wanted = served.restartWanted;
|
|
147
|
+
if (!fromTablePage(request, served)) {
|
|
148
|
+
sendJson(response, 403, { error: "Only the table's own page ends a start again that waits." });
|
|
149
|
+
}
|
|
150
|
+
else if (typeof now !== "boolean") {
|
|
151
|
+
sendJson(response, 400, { error: "Say now: true to start the chat again at once, cutting its answer, or now: false to let the request go." });
|
|
152
|
+
}
|
|
153
|
+
else if (wanted === undefined) {
|
|
154
|
+
sendJson(response, 409, { error: "No start again waits: ask for one from the chat's window." });
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
served.restartWanted = undefined;
|
|
158
|
+
const restarting = now && heldChat(served) !== undefined && relaunchChat(served, "restart", wanted.host);
|
|
159
|
+
tellWatchers(served.watchers);
|
|
160
|
+
sendJson(response, 200, now ? { restarting } : { waiting: false });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// The browser marks a request of the table's own page with its origin.
|
|
164
|
+
function fromTablePage(request, served) {
|
|
165
|
+
return request.headers.origin === `http://127.0.0.1:${served.port}` || request.headers.origin === `http://localhost:${served.port}`;
|
|
166
|
+
}
|
|
167
|
+
function hostOffered(served, host) {
|
|
168
|
+
return served.table?.chats?.hosts().find((one) => one.name === host && one.installed);
|
|
169
|
+
}
|
|
170
|
+
function hostsOffered(served) {
|
|
171
|
+
return `Name a host the table offers and this computer has: ${served.table?.chats?.hosts().filter((one) => one.installed).map((one) => one.name).join(", ") || "none"}.`;
|
|
172
|
+
}
|
|
173
|
+
// The host of the chat at the table, among those offered: it says its name as its MCP client does, «claude-code» or «codex-mcp-client».
|
|
174
|
+
function hostAtTable(served) {
|
|
175
|
+
const said = served.chat.host()?.toLowerCase() ?? "";
|
|
176
|
+
return served.attached === undefined ? undefined : served.table?.chats?.hosts().find((one) => said.includes(one.name))?.name;
|
|
177
|
+
}
|
|
178
|
+
// The chat at the table when `chesspal play` holds it, which the table can start again: the model and effort it started with.
|
|
179
|
+
function heldChat(served) {
|
|
180
|
+
const { attached } = served;
|
|
181
|
+
return attached === undefined ? undefined : served.table?.chats?.held(attached.chat.folder);
|
|
182
|
+
}
|
|
183
|
+
// The chat waits for the doorbell, out of a turn, and no analyst of it is at work: starting it again loses nothing.
|
|
184
|
+
function chatAtRest(served) {
|
|
185
|
+
const { bell } = served;
|
|
186
|
+
return bell.listening && !bell.inTurn && gameInAnalysis(gameFolder(served.context.home()))?.analyst === undefined;
|
|
187
|
+
}
|
|
80
188
|
// The pages show a chat opening from now until it attaches; one that has not by then lets them open another. The opening noted.
|
|
81
189
|
function noteOpening(served, how) {
|
|
82
190
|
const opened = { at: served.context.now().getTime(), how };
|
|
@@ -98,5 +206,5 @@ function opening(served) {
|
|
|
98
206
|
if (chatOpened !== undefined && context.now().getTime() - chatOpened.at < openingFor) {
|
|
99
207
|
return chatOpened.how;
|
|
100
208
|
}
|
|
101
|
-
return served.table?.chats?.
|
|
209
|
+
return served.table?.chats?.held(context.folder) === undefined ? undefined : "chat";
|
|
102
210
|
}
|
|
@@ -2,7 +2,7 @@ import { tableFolder } from "../../../home/index.js";
|
|
|
2
2
|
import { samePath } from "../../bell.js";
|
|
3
3
|
import { programPath } from "./find.js";
|
|
4
4
|
import { hostChoices, hostNames, isHostName } from "./hosts.js";
|
|
5
|
-
import { askAgain,
|
|
5
|
+
import { askAgain, seatHeld } from "./seat.js";
|
|
6
6
|
import { openTerminal } from "./terminal.js";
|
|
7
7
|
/**
|
|
8
8
|
* The chats of a real table: the hosts installed, looked for again once a while has passed, since the player may install one with the page open; a
|
|
@@ -20,8 +20,11 @@ export function terminalChats(context, chesspal = [process.execPath, ...process.
|
|
|
20
20
|
},
|
|
21
21
|
open: async (host) => isHostName(host) && await openTerminal(tableFolder(context.home()), [...chesspal, "play", host], context.platform, env),
|
|
22
22
|
// Only a chat in the table's folder can be the one chesspal play holds.
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
held: (folder) => {
|
|
24
|
+
const seat = samePath(folder, tableFolder(context.home())) ? seatHeld(folder, context.now()) : undefined;
|
|
25
|
+
return seat === undefined ? undefined : { model: seat.model, effort: seat.effort };
|
|
26
|
+
},
|
|
27
|
+
relaunch: (folder, host) => samePath(folder, tableFolder(context.home())) && askAgain(folder, context.now(), host === undefined ? {} : { host }),
|
|
25
28
|
};
|
|
26
29
|
}
|
|
27
30
|
// Looking on the PATH takes a few milliseconds, and the page asks at every change of the game.
|
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
import type { Context } from "../../../commands/index.ts";
|
|
2
|
-
import { type HostName } from "./hosts.ts";
|
|
3
|
-
/**
|
|
2
|
+
import { type HostChoice, type HostName } from "./hosts.ts";
|
|
3
|
+
/**
|
|
4
|
+
* What `chesspal play` starts: the host's program, its arguments, folder and environment, whether it needs cmd, as a .cmd shim does on Windows, and the
|
|
5
|
+
* model and effort it is given, which the seat notes for the page (D-107).
|
|
6
|
+
*/
|
|
4
7
|
export interface HostLaunch {
|
|
5
8
|
readonly program: string;
|
|
6
9
|
readonly args: readonly string[];
|
|
7
10
|
readonly folder: string;
|
|
8
11
|
readonly env: NodeJS.ProcessEnv;
|
|
9
12
|
readonly shell: boolean;
|
|
13
|
+
readonly choice: HostChoice;
|
|
10
14
|
}
|
|
11
15
|
/**
|
|
12
|
-
* Runs the host in the table's folder, in this terminal, until it ends: its exit code; started again, clean, when the table asks for a new game (D-57)
|
|
13
|
-
* Ctrl+C is the host's: this process ignores it, or the terminal would close with the chat.
|
|
16
|
+
* Runs the host in the table's folder, in this terminal, until it ends: its exit code; started again, clean, when the table asks for a new game (D-57),
|
|
17
|
+
* or for another model or host, which then takes its place (D-107). Ctrl+C is the host's: this process ignores it, or the terminal would close with the chat.
|
|
14
18
|
*/
|
|
15
19
|
export declare function playHost(context: Context, host: HostName): Promise<number>;
|
|
16
20
|
/**
|
|
@@ -7,12 +7,12 @@ import { tableFolder } from "../../../home/index.js";
|
|
|
7
7
|
import { setUpHosts } from "../../../host-files/index.js";
|
|
8
8
|
import { activeProfile, gameFolder, languageIn, profileSettings } from "../../../profiles/index.js";
|
|
9
9
|
import { programPath } from "./find.js";
|
|
10
|
-
import { choiceIn, hostArgs, startMessage } from "./hosts.js";
|
|
10
|
+
import { choiceIn, hostArgs, isHostName, startMessage } from "./hosts.js";
|
|
11
11
|
import { askedAgain, leaveSeat, refreshSeat, seatTimes, takeSeat } from "./seat.js";
|
|
12
12
|
import { cmdQuoted } from "./terminal.js";
|
|
13
13
|
/**
|
|
14
|
-
* Runs the host in the table's folder, in this terminal, until it ends: its exit code; started again, clean, when the table asks for a new game (D-57)
|
|
15
|
-
* Ctrl+C is the host's: this process ignores it, or the terminal would close with the chat.
|
|
14
|
+
* Runs the host in the table's folder, in this terminal, until it ends: its exit code; started again, clean, when the table asks for a new game (D-57),
|
|
15
|
+
* or for another model or host, which then takes its place (D-107). Ctrl+C is the host's: this process ignores it, or the terminal would close with the chat.
|
|
16
16
|
*/
|
|
17
17
|
export async function playHost(context, host) {
|
|
18
18
|
const folder = tableFolder(context.home());
|
|
@@ -24,24 +24,44 @@ export async function playHost(context, host) {
|
|
|
24
24
|
process.exit(129);
|
|
25
25
|
});
|
|
26
26
|
const terminal = savedTerminal(context.platform);
|
|
27
|
+
let playing = host;
|
|
28
|
+
let next = host;
|
|
29
|
+
// The host before one asked in its place, until that one has started: it goes on when the other does not start.
|
|
30
|
+
let before;
|
|
27
31
|
let refreshed = Date.now();
|
|
28
32
|
const asked = () => {
|
|
29
33
|
if (Date.now() - refreshed >= seatTimes.refresh) {
|
|
30
34
|
refreshed = Date.now();
|
|
31
35
|
refreshSeat(folder, process.pid);
|
|
32
36
|
}
|
|
33
|
-
|
|
37
|
+
const again = askedAgain(folder, process.pid);
|
|
38
|
+
next = again?.host !== undefined && isHostName(again.host) ? again.host : playing;
|
|
39
|
+
return again !== undefined;
|
|
34
40
|
};
|
|
35
41
|
try {
|
|
36
42
|
for (;;) {
|
|
37
43
|
// A request left from a second click while the host before was ending is not for this one.
|
|
38
44
|
askedAgain(folder, process.pid);
|
|
39
|
-
const ran = await
|
|
40
|
-
|
|
45
|
+
const ran = await started(context, playing, asked).catch((error) => {
|
|
46
|
+
if (before === undefined) {
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
process.stderr.write(`${error.message} ${hostTitle[before]} goes on.\n`);
|
|
50
|
+
return undefined;
|
|
41
51
|
});
|
|
52
|
+
if (ran === undefined) {
|
|
53
|
+
playing = before ?? playing;
|
|
54
|
+
before = undefined;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
before = undefined;
|
|
42
58
|
if (!ran.again) {
|
|
43
59
|
return ran.code;
|
|
44
60
|
}
|
|
61
|
+
if (next !== playing) {
|
|
62
|
+
before = playing;
|
|
63
|
+
playing = next;
|
|
64
|
+
}
|
|
45
65
|
terminal.restore();
|
|
46
66
|
process.stdout.write(cleanScreen);
|
|
47
67
|
}
|
|
@@ -98,11 +118,19 @@ export function hostLaunch(context, host, env = process.env, exists) {
|
|
|
98
118
|
const childEnv = { ...env, ...Object.fromEntries(sessionMarks.map((name) => [name, undefined])), ...effort };
|
|
99
119
|
const script = npmScript(host, found, context.platform, exists ?? existsSync);
|
|
100
120
|
if (script !== undefined) {
|
|
101
|
-
return { program: process.execPath, args: [script, ...args], folder, env: childEnv, shell: false };
|
|
121
|
+
return { program: process.execPath, args: [script, ...args], folder, env: childEnv, shell: false, choice };
|
|
102
122
|
}
|
|
103
|
-
return { program: found, args, folder, env: childEnv, shell: context.platform === "win32" && /\.(cmd|bat)$/i.test(found) };
|
|
123
|
+
return { program: found, args, folder, env: childEnv, shell: context.platform === "win32" && /\.(cmd|bat)$/i.test(found), choice };
|
|
104
124
|
}
|
|
105
125
|
const hostTitle = { claude: "Claude Code", codex: "Codex" };
|
|
126
|
+
// Runs the host as runHost does, the seat noting the model and effort it is given; one not installed, or that could not start, fails saying so.
|
|
127
|
+
async function started(context, host, again) {
|
|
128
|
+
const launch = hostLaunch(context, host);
|
|
129
|
+
takeSeat(launch.folder, { pid: process.pid, host, ...launch.choice });
|
|
130
|
+
return runHost(launch, again, context.platform).catch((error) => {
|
|
131
|
+
throw new Error(`${hostTitle[host]} did not start: ${error.message}`);
|
|
132
|
+
});
|
|
133
|
+
}
|
|
106
134
|
// A host that has not ended this long after it was asked is killed.
|
|
107
135
|
const killAfter = 3_000;
|
|
108
136
|
// A host ended in the middle leaves the modes it set: colours, scroll region, synchronized output, bracketed paste, focus and mouse reports, kitty's
|