prompt2pwn 0.0.0-stage → 0.1.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 +19 -2
- package/dist/App-AQGN5XOX.js +624 -0
- package/dist/chunk-SZ6ZA65E.js +3508 -0
- package/dist/index.js +793 -0
- package/package.json +41 -4
- package/skills/prompt2pwn/SKILL.md +174 -0
package/README.md
CHANGED
|
@@ -1,3 +1,20 @@
|
|
|
1
|
-
#
|
|
1
|
+
# p2p — ChatPvP
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Play ChatPvP from your terminal. Requires Node.js 24 or newer and a deployment URL from your game host.
|
|
4
|
+
|
|
5
|
+
Run without a global install:
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npx prompt2pwn --target <url>
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or install the `p2p` command:
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
npm install --global prompt2pwn
|
|
15
|
+
p2p --target <url>
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Replace `<url>` with the deployment origin. In the client, enter `/login` and follow the GitHub authorization instructions, then choose your player handle. `/help` lists commands; `/quit` exits.
|
|
19
|
+
|
|
20
|
+
To connect an MCP client as the same player, use `/mcp` or read the saved bearer with `p2p token --target <url>`. Keep that bearer private. Detailed agent setup instructions are included in [skills/prompt2pwn/SKILL.md](skills/prompt2pwn/SKILL.md).
|
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ago,
|
|
3
|
+
compareEventIds,
|
|
4
|
+
cooldownSeconds,
|
|
5
|
+
countdown,
|
|
6
|
+
displayWidth,
|
|
7
|
+
formatDuration,
|
|
8
|
+
formatTokens,
|
|
9
|
+
gauge,
|
|
10
|
+
holderLabel,
|
|
11
|
+
rowTemplate,
|
|
12
|
+
sanitize,
|
|
13
|
+
shortModel,
|
|
14
|
+
spinnerGlyph,
|
|
15
|
+
stateBadge,
|
|
16
|
+
winnerLabel
|
|
17
|
+
} from "./chunk-SZ6ZA65E.js";
|
|
18
|
+
|
|
19
|
+
// src/ui/App.tsx
|
|
20
|
+
import { Box as Box4, Text as Text6, useInput, useStdin } from "ink";
|
|
21
|
+
import { useRef } from "react";
|
|
22
|
+
|
|
23
|
+
// src/model/local-lines.ts
|
|
24
|
+
function mergeLines(lines, local) {
|
|
25
|
+
const pending = [...local].sort((a, b) => a.seq - b.seq);
|
|
26
|
+
const merged = [];
|
|
27
|
+
const emit = (line) => merged.push({ ...line, key: `local:${line.seq}` });
|
|
28
|
+
for (const line of lines) {
|
|
29
|
+
for (let i = 0; i < pending.length; ) {
|
|
30
|
+
const next = pending[i];
|
|
31
|
+
if (next && (next.anchor === null || compareEventIds(next.anchor, line.id) < 0)) {
|
|
32
|
+
emit(next);
|
|
33
|
+
pending.splice(i, 1);
|
|
34
|
+
} else i += 1;
|
|
35
|
+
}
|
|
36
|
+
merged.push(line);
|
|
37
|
+
}
|
|
38
|
+
for (const line of pending) emit(line);
|
|
39
|
+
return merged;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// src/ui/InputBox.tsx
|
|
43
|
+
import { Text } from "ink";
|
|
44
|
+
|
|
45
|
+
// src/ui/layout.ts
|
|
46
|
+
var segmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
|
|
47
|
+
var graphemes = (text) => Array.from(segmenter.segment(text), (segment) => segment.segment);
|
|
48
|
+
function fitRow(text, columns) {
|
|
49
|
+
const flat = text.replace(/\n/g, " ");
|
|
50
|
+
if (columns <= 0) return "";
|
|
51
|
+
if (displayWidth(flat) <= columns) return flat;
|
|
52
|
+
let out = "";
|
|
53
|
+
let width = 0;
|
|
54
|
+
for (const grapheme of graphemes(flat)) {
|
|
55
|
+
const next = displayWidth(grapheme);
|
|
56
|
+
if (width + next + 1 > columns) break;
|
|
57
|
+
out += grapheme;
|
|
58
|
+
width += next;
|
|
59
|
+
}
|
|
60
|
+
return `${out}\u2026`;
|
|
61
|
+
}
|
|
62
|
+
function cells(content) {
|
|
63
|
+
const out = [];
|
|
64
|
+
let position = 0;
|
|
65
|
+
for (const segment of content)
|
|
66
|
+
for (const grapheme of graphemes(segment.text)) {
|
|
67
|
+
out.push({ grapheme, width: displayWidth(grapheme), style: segment.style, position });
|
|
68
|
+
position += 1;
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
function pushCell(segments, cell) {
|
|
73
|
+
const last = segments.at(-1);
|
|
74
|
+
if (last && last.style === cell.style) last.text += cell.grapheme;
|
|
75
|
+
else
|
|
76
|
+
segments.push(
|
|
77
|
+
cell.style ? { text: cell.grapheme, style: cell.style } : { text: cell.grapheme }
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
function wrapContent(content, firstWidth, width) {
|
|
81
|
+
const rows = [];
|
|
82
|
+
let current = { position: 0, segments: [] };
|
|
83
|
+
let used = 0;
|
|
84
|
+
let limit = firstWidth;
|
|
85
|
+
const flush = (position) => {
|
|
86
|
+
rows.push(current);
|
|
87
|
+
current = { position, segments: [] };
|
|
88
|
+
used = 0;
|
|
89
|
+
limit = width;
|
|
90
|
+
};
|
|
91
|
+
for (const cell of cells(content)) {
|
|
92
|
+
if (cell.grapheme === "\n") {
|
|
93
|
+
flush(cell.position + 1);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (used + cell.width > limit && (used > 0 || limit < width)) flush(cell.position);
|
|
97
|
+
if (limit <= 0) continue;
|
|
98
|
+
pushCell(current.segments, cell);
|
|
99
|
+
used += cell.width;
|
|
100
|
+
}
|
|
101
|
+
rows.push(current);
|
|
102
|
+
return rows;
|
|
103
|
+
}
|
|
104
|
+
function layoutLine(line, options) {
|
|
105
|
+
const { columns } = options;
|
|
106
|
+
if (columns <= 0) return [];
|
|
107
|
+
const template = rowTemplate(line, options);
|
|
108
|
+
const prefixText = template.prefix.map((segment) => segment.text).join("");
|
|
109
|
+
let prefix = template.prefix;
|
|
110
|
+
let prefixWidth = displayWidth(prefixText);
|
|
111
|
+
if (prefixWidth > columns) {
|
|
112
|
+
const fitted = fitRow(prefixText, columns);
|
|
113
|
+
const style = template.prefix[0]?.style;
|
|
114
|
+
prefix = [style ? { text: fitted, style } : { text: fitted }];
|
|
115
|
+
prefixWidth = displayWidth(fitted);
|
|
116
|
+
}
|
|
117
|
+
const firstWidth = columns - prefixWidth;
|
|
118
|
+
const indentWidth = firstWidth >= 1 ? prefixWidth : 0;
|
|
119
|
+
const wrapped = wrapContent(template.content, firstWidth, columns - indentWidth);
|
|
120
|
+
const starts = template.parts ? partStarts(template.content) : null;
|
|
121
|
+
return wrapped.map(({ position, segments }, index) => {
|
|
122
|
+
const row = {
|
|
123
|
+
key: template.key,
|
|
124
|
+
position,
|
|
125
|
+
segments: index === 0 ? [...prefix, ...segments] : indentWidth > 0 ? [{ text: " ".repeat(indentWidth) }, ...segments] : segments
|
|
126
|
+
};
|
|
127
|
+
if (!starts) return row;
|
|
128
|
+
const part = starts.findLastIndex((start) => start <= position);
|
|
129
|
+
return { ...row, part, position: position - (starts[part] ?? 0) };
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
function partStarts(content) {
|
|
133
|
+
const starts = [0];
|
|
134
|
+
graphemes(content.map((segment) => segment.text).join("")).forEach((grapheme, index) => {
|
|
135
|
+
if (grapheme === "\n") starts.push(index + 1);
|
|
136
|
+
});
|
|
137
|
+
return starts;
|
|
138
|
+
}
|
|
139
|
+
var layoutRows = (lines, options) => lines.flatMap((line) => layoutLine(line, options));
|
|
140
|
+
var maxOffset = (rowCount, viewportRows) => Math.max(0, rowCount - viewportRows);
|
|
141
|
+
var anchorText = (line) => line?.kind === "agent" ? line.text : null;
|
|
142
|
+
var partText = (rows, key, part) => rows.filter((row) => row.key === key && row.part === part).flatMap((row) => row.segments.map((segment) => segment.text)).join("");
|
|
143
|
+
function rebasePart(position, before, after) {
|
|
144
|
+
if (before === after) return position;
|
|
145
|
+
const old = graphemes(before);
|
|
146
|
+
const next = graphemes(after);
|
|
147
|
+
let same = 0;
|
|
148
|
+
while (same < old.length && same < next.length && old[same] === next[same]) same += 1;
|
|
149
|
+
return position < same ? position : Math.max(same, position + next.length - old.length);
|
|
150
|
+
}
|
|
151
|
+
function anchorAt(rows, lines, offset) {
|
|
152
|
+
const row = rows[offset];
|
|
153
|
+
if (!row) return null;
|
|
154
|
+
if (row.part !== void 0)
|
|
155
|
+
return {
|
|
156
|
+
key: row.key,
|
|
157
|
+
part: row.part,
|
|
158
|
+
position: row.position,
|
|
159
|
+
text: partText(rows, row.key, row.part)
|
|
160
|
+
};
|
|
161
|
+
return {
|
|
162
|
+
key: row.key,
|
|
163
|
+
position: row.position,
|
|
164
|
+
text: anchorText(lines.find((line) => line.key === row.key))
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function offsetForAnchor(rows, lines, anchor) {
|
|
168
|
+
const line = lines.find((candidate) => candidate.key === anchor.key);
|
|
169
|
+
if (!line) return null;
|
|
170
|
+
let { position } = anchor;
|
|
171
|
+
if (anchor.part !== void 0) {
|
|
172
|
+
if (anchor.text !== null)
|
|
173
|
+
position = rebasePart(position, anchor.text, partText(rows, anchor.key, anchor.part));
|
|
174
|
+
} else {
|
|
175
|
+
const text = anchorText(line);
|
|
176
|
+
if (anchor.text !== null && text !== null && text !== anchor.text && text.endsWith(anchor.text))
|
|
177
|
+
position += graphemes(text).length - graphemes(anchor.text).length;
|
|
178
|
+
}
|
|
179
|
+
let found = null;
|
|
180
|
+
for (const [index, row] of rows.entries()) {
|
|
181
|
+
if (row.key !== anchor.key || row.part !== anchor.part) continue;
|
|
182
|
+
if (row.position <= position) found = index;
|
|
183
|
+
else if (found === null) return index;
|
|
184
|
+
}
|
|
185
|
+
return found;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// src/ui/InputBox.tsx
|
|
189
|
+
import { jsx } from "react/jsx-runtime";
|
|
190
|
+
var clip = (text, width) => {
|
|
191
|
+
let out = "";
|
|
192
|
+
for (const cluster of graphemes(text)) {
|
|
193
|
+
if (displayWidth(out + cluster) > width) break;
|
|
194
|
+
out += cluster;
|
|
195
|
+
}
|
|
196
|
+
return out;
|
|
197
|
+
};
|
|
198
|
+
var safe = (text) => sanitize(text).replace(/\n/g, " ");
|
|
199
|
+
function inputRow({
|
|
200
|
+
input,
|
|
201
|
+
columns,
|
|
202
|
+
maxChars,
|
|
203
|
+
placeholder,
|
|
204
|
+
showCounter = true
|
|
205
|
+
}) {
|
|
206
|
+
if (columns <= 0) return [];
|
|
207
|
+
const count = [...input.text].length;
|
|
208
|
+
const counter = showCounter ? clip(`${count}/${maxChars ?? "\u2013"}`, columns) : "";
|
|
209
|
+
const available = columns - displayWidth(counter);
|
|
210
|
+
const display = safe(input.text);
|
|
211
|
+
const position = safe([...input.text].slice(0, input.cursor).join("")).length;
|
|
212
|
+
const clusters = graphemes(display);
|
|
213
|
+
let index = 0;
|
|
214
|
+
let offset = 0;
|
|
215
|
+
while (index < clusters.length) {
|
|
216
|
+
const cluster = clusters[index] ?? "";
|
|
217
|
+
if (offset + cluster.length > position) break;
|
|
218
|
+
offset += cluster.length;
|
|
219
|
+
index++;
|
|
220
|
+
}
|
|
221
|
+
const cursor = clusters[index] ?? " ";
|
|
222
|
+
const cursorWidth = displayWidth(cursor) <= available ? displayWidth(cursor) : 0;
|
|
223
|
+
const prefix = clip(" > ", available - cursorWidth);
|
|
224
|
+
const gap = available - cursorWidth - displayWidth(prefix) >= 2 ? 2 : 0;
|
|
225
|
+
const contentWidth = available - cursorWidth - displayWidth(prefix) - gap;
|
|
226
|
+
let before = "";
|
|
227
|
+
let after = "";
|
|
228
|
+
if (!input.text) before = clip(safe(placeholder), contentWidth);
|
|
229
|
+
else {
|
|
230
|
+
for (let i = index - 1; i >= 0; i--) {
|
|
231
|
+
const candidate = (clusters[i] ?? "") + before;
|
|
232
|
+
if (displayWidth(candidate) > contentWidth) break;
|
|
233
|
+
before = candidate;
|
|
234
|
+
}
|
|
235
|
+
after = clip(clusters.slice(index + 1).join(""), contentWidth - displayWidth(before));
|
|
236
|
+
}
|
|
237
|
+
const body = [
|
|
238
|
+
{ key: "prefix", text: prefix },
|
|
239
|
+
{ key: "before", text: before, dim: input.text === "" }
|
|
240
|
+
];
|
|
241
|
+
if (cursorWidth > 0) body.push({ key: "cursor", text: cursor, inverse: true });
|
|
242
|
+
body.push({ key: "after", text: after });
|
|
243
|
+
const used = body.reduce((sum, segment) => sum + displayWidth(segment.text), 0);
|
|
244
|
+
body.push({ key: "gap", text: " ".repeat(Math.max(0, available - used)) });
|
|
245
|
+
const red = maxChars !== null && count > maxChars - 50;
|
|
246
|
+
body.push({ key: "counter", text: counter, red, dim: !red });
|
|
247
|
+
return body;
|
|
248
|
+
}
|
|
249
|
+
function InputBox(props) {
|
|
250
|
+
return /* @__PURE__ */ jsx(Text, { wrap: "truncate", children: inputRow(props).map((segment) => /* @__PURE__ */ jsx(
|
|
251
|
+
Text,
|
|
252
|
+
{
|
|
253
|
+
inverse: segment.inverse,
|
|
254
|
+
dimColor: segment.dim,
|
|
255
|
+
color: segment.red ? "red" : void 0,
|
|
256
|
+
children: segment.text
|
|
257
|
+
},
|
|
258
|
+
segment.key
|
|
259
|
+
)) });
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// src/ui/Picker.tsx
|
|
263
|
+
import { Box, Text as Text2 } from "ink";
|
|
264
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
265
|
+
function pickerRows(picker, columns, viewportRows) {
|
|
266
|
+
const { step, choices, options } = picker;
|
|
267
|
+
const hint = step === "handle" ? "type a handle, Enter to confirm \xB7 Tab next step \xB7 Ctrl-L logout \xB7 Ctrl-C quit" : step === "emoji" ? "\u2190\u2191\u2193\u2192 move \xB7 Enter choose \xB7 Tab next step \xB7 \xD7 seated \xB7 Ctrl-L logout \xB7 Ctrl-C quit" : "\u2190\u2192 move \xB7 Enter claim \xB7 Tab next step \xB7 Ctrl-L logout \xB7 Ctrl-C quit";
|
|
268
|
+
const fit = (text) => fitRow(sanitize(text), columns);
|
|
269
|
+
const rows = [
|
|
270
|
+
[
|
|
271
|
+
{
|
|
272
|
+
text: fit(`onboarding \xB7 step ${["handle", "emoji", "color"].indexOf(step) + 1}/3 ${step}`)
|
|
273
|
+
}
|
|
274
|
+
],
|
|
275
|
+
[
|
|
276
|
+
{
|
|
277
|
+
text: fit(
|
|
278
|
+
`handle ${choices.handle ?? "\u2013"} \xB7 emoji ${choices.emoji ?? "\u2013"} \xB7 color ${choices.color ?? "\u2013"}`
|
|
279
|
+
)
|
|
280
|
+
}
|
|
281
|
+
],
|
|
282
|
+
[{ text: fit(hint) }],
|
|
283
|
+
// Slice 047 (045's follow-up): what a seated preference means in this run's room.
|
|
284
|
+
...step === "emoji" ? [[{ text: fit("your emoji here is this one if free, else the first free one") }]] : [],
|
|
285
|
+
[{ text: fit(picker.message) }]
|
|
286
|
+
];
|
|
287
|
+
const fixedRows = rows.length;
|
|
288
|
+
if (options && step !== "handle") {
|
|
289
|
+
const width = step === "emoji" ? 4 : 12;
|
|
290
|
+
const perRow = Math.max(1, Math.floor(columns / width));
|
|
291
|
+
const values = step === "emoji" ? options.emoji : options.colors;
|
|
292
|
+
const focus = step === "emoji" ? picker.emojiFocus : picker.colorFocus;
|
|
293
|
+
const gridRows = Math.max(0, viewportRows - fixedRows);
|
|
294
|
+
const start = Math.max(0, Math.floor(focus / perRow) - gridRows + 1) * perRow;
|
|
295
|
+
for (let i = start; i < values.length && rows.length < viewportRows; i += perRow) {
|
|
296
|
+
const cells2 = [];
|
|
297
|
+
for (let j = i; j < Math.min(i + perRow, values.length); j++) {
|
|
298
|
+
const value = values[j] ?? "";
|
|
299
|
+
const marker = focus === j ? ">" : " ";
|
|
300
|
+
if (step === "emoji")
|
|
301
|
+
cells2.push({
|
|
302
|
+
text: fit(`${marker}${value}${options.takenEmoji.includes(value) ? "\xD7" : " "}`),
|
|
303
|
+
dim: options.takenEmoji.includes(value)
|
|
304
|
+
});
|
|
305
|
+
else if (columns < width) cells2.push({ text: fit(`${marker}\u2588\u2588 ${value} `), color: value });
|
|
306
|
+
else
|
|
307
|
+
cells2.push(
|
|
308
|
+
{ text: marker },
|
|
309
|
+
{ text: "\u2588\u2588", color: value },
|
|
310
|
+
{ text: ` ${sanitize(value)} ` }
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
rows.push(cells2);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return rows.slice(0, Math.max(0, viewportRows));
|
|
317
|
+
}
|
|
318
|
+
function Picker({
|
|
319
|
+
picker,
|
|
320
|
+
columns,
|
|
321
|
+
viewportRows
|
|
322
|
+
}) {
|
|
323
|
+
return /* @__PURE__ */ jsx2(Box, { flexDirection: "column", height: viewportRows, overflow: "hidden", children: pickerRows(picker, columns, viewportRows).map((cells2, row) => {
|
|
324
|
+
const fixed = picker.step === "emoji" ? ["step", "choices", "hint", "note", "message"] : ["step", "choices", "hint", "message"];
|
|
325
|
+
const key = fixed[row] ?? cells2.map((cell) => cell.text).join("");
|
|
326
|
+
let column = 0;
|
|
327
|
+
return /* @__PURE__ */ jsx2(Box, { height: 1, children: /* @__PURE__ */ jsx2(Text2, { wrap: "truncate", children: cells2.map((cell) => {
|
|
328
|
+
const start = column;
|
|
329
|
+
column += cell.text.length;
|
|
330
|
+
return /* @__PURE__ */ jsx2(Text2, { dimColor: cell.dim, color: cell.color, children: cell.text }, `${start}`);
|
|
331
|
+
}) }) }, key);
|
|
332
|
+
}) });
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// src/ui/QueuePanel.tsx
|
|
336
|
+
import { Box as Box2, Text as Text3 } from "ink";
|
|
337
|
+
import { Fragment, jsx as jsx3, jsxs } from "react/jsx-runtime";
|
|
338
|
+
function chromeRow(left, right, fill, columns) {
|
|
339
|
+
if (columns <= 1) return fitRow(left, columns);
|
|
340
|
+
const body = fitRow(left, columns - 1);
|
|
341
|
+
return body + fill.repeat(columns - 1 - displayWidth(body)) + right;
|
|
342
|
+
}
|
|
343
|
+
function queuePanelRows({
|
|
344
|
+
panel,
|
|
345
|
+
nowMs,
|
|
346
|
+
columns
|
|
347
|
+
}) {
|
|
348
|
+
const eta = panel ? countdown(panel.nextTurnEta, nowMs) : "\u2013";
|
|
349
|
+
const separator = chromeRow(`\u251C\u2500 next turn ${eta} `, "\u2524", "\u2500", columns);
|
|
350
|
+
const items = (panel?.queue ?? []).map(
|
|
351
|
+
({ author, submittedAt, chars }) => `${sanitize(author.emoji)} @${sanitize(author.handle)} ${ago(submittedAt, nowMs)} ${chars}ch`
|
|
352
|
+
);
|
|
353
|
+
const queue = fitRow(
|
|
354
|
+
`queued ${items.length}${items.length > 0 ? `: ${items.join(" \xB7 ")}` : ""}`,
|
|
355
|
+
columns
|
|
356
|
+
);
|
|
357
|
+
const indicators = fitRow(
|
|
358
|
+
[
|
|
359
|
+
...(panel?.probing ?? []).map((handle) => `\u{1F50D} @${sanitize(handle)} probing\u2026`),
|
|
360
|
+
...(panel?.typing ?? []).map((handle) => `\u270E @${sanitize(handle)} typing`)
|
|
361
|
+
].join(" \xB7 "),
|
|
362
|
+
columns
|
|
363
|
+
);
|
|
364
|
+
return [separator, queue, indicators];
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// src/ui/StatusLine.tsx
|
|
368
|
+
import { Text as Text4 } from "ink";
|
|
369
|
+
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
370
|
+
function statusRow({
|
|
371
|
+
panel,
|
|
372
|
+
holder,
|
|
373
|
+
result,
|
|
374
|
+
authors,
|
|
375
|
+
connection,
|
|
376
|
+
columns,
|
|
377
|
+
play,
|
|
378
|
+
nowMs = 0
|
|
379
|
+
}) {
|
|
380
|
+
const models = panel ? `${shortModel(sanitize(panel.model.current))} \u2192 ${shortModel(sanitize(panel.model.next))}` : "\u2013 \u2192 \u2013";
|
|
381
|
+
const context = panel ? `ctx ${gauge(panel.contextTokens, panel.compactAtTokens)} ${formatTokens(panel.contextTokens)}/${formatTokens(panel.compactAtTokens)}` : "ctx \u2013";
|
|
382
|
+
const identity = play?.authStatus ?? (!play?.snapshot ? "\u2013" : !play.token ? "/login to play" : play.snapshot.me ? `@${sanitize(play.snapshot.me.handle)}` : "not onboarded");
|
|
383
|
+
const seconds = cooldownSeconds(play?.cooldownUntil ?? null, nowMs);
|
|
384
|
+
const cd = play?.token && play.snapshot?.me && seconds > 0 ? formatDuration(seconds) : "\u2013";
|
|
385
|
+
const dm = play?.token && play.snapshot?.me && play.dmsLeft !== null ? `${play.dmsLeft}/${play.snapshot.config.dm.perWindow}` : "\u2013";
|
|
386
|
+
const standing = result ? `\u{1F3C6} ${winnerLabel(result.winner, authors)}` : `\u{1F451} ${holderLabel(holder, authors)}`;
|
|
387
|
+
const text = ` ${identity} ${models} \xB7 ${context} \xB7 cd ${cd} \xB7 dm ${dm} \xB7 ${standing}${connection === "reconnecting" ? " \xB7 reconnecting\u2026" : ""}`;
|
|
388
|
+
return fitRow(text, columns);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// src/ui/Transcript.tsx
|
|
392
|
+
import { Box as Box3, Text as Text5 } from "ink";
|
|
393
|
+
import { useImperativeHandle, useMemo, useState } from "react";
|
|
394
|
+
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
395
|
+
var clamp = (value, max) => Math.min(Math.max(0, value), max);
|
|
396
|
+
function Transcript({
|
|
397
|
+
lines,
|
|
398
|
+
authors,
|
|
399
|
+
viewportRows,
|
|
400
|
+
columns,
|
|
401
|
+
timeZone,
|
|
402
|
+
spinner,
|
|
403
|
+
requestOlder,
|
|
404
|
+
ref
|
|
405
|
+
}) {
|
|
406
|
+
const rows = useMemo(
|
|
407
|
+
() => layoutRows(lines, { columns, timeZone, spinner, authors }),
|
|
408
|
+
[lines, columns, timeZone, spinner, authors]
|
|
409
|
+
);
|
|
410
|
+
const [view, setView] = useState({ follow: true, anchor: null, fallback: 0 });
|
|
411
|
+
const bottom = maxOffset(rows.length, viewportRows);
|
|
412
|
+
const anchored = view.anchor === null ? null : offsetForAnchor(rows, lines, view.anchor);
|
|
413
|
+
const offset = view.follow ? bottom : clamp(anchored ?? view.fallback, bottom);
|
|
414
|
+
const step = Math.max(1, viewportRows - 1);
|
|
415
|
+
const moveTo = (target) => {
|
|
416
|
+
const next = clamp(target, bottom);
|
|
417
|
+
if (next === bottom) setView({ follow: true, anchor: null, fallback: next });
|
|
418
|
+
else setView({ follow: false, anchor: anchorAt(rows, lines, next), fallback: next });
|
|
419
|
+
};
|
|
420
|
+
useImperativeHandle(ref, () => ({
|
|
421
|
+
navigate(key) {
|
|
422
|
+
if (viewportRows <= 0) return;
|
|
423
|
+
if (key === "pageUp" || key === "up") {
|
|
424
|
+
if (offset === 0) requestOlder();
|
|
425
|
+
moveTo(offset - (key === "pageUp" ? step : 1));
|
|
426
|
+
} else if (key === "pageDown") moveTo(offset + step);
|
|
427
|
+
else if (key === "down") moveTo(offset + 1);
|
|
428
|
+
else if (key === "home") moveTo(0);
|
|
429
|
+
else if (key === "end") moveTo(bottom);
|
|
430
|
+
}
|
|
431
|
+
}));
|
|
432
|
+
if (viewportRows <= 0) return null;
|
|
433
|
+
const seen = /* @__PURE__ */ new Map();
|
|
434
|
+
const visible = rows.slice(offset, offset + viewportRows).map((row) => {
|
|
435
|
+
const base = `${row.key}:${row.position}`;
|
|
436
|
+
const repeat = seen.get(base) ?? 0;
|
|
437
|
+
seen.set(base, repeat + 1);
|
|
438
|
+
return { key: repeat === 0 ? base : `${base}:${repeat}`, row };
|
|
439
|
+
});
|
|
440
|
+
return /* @__PURE__ */ jsx5(Box3, { flexDirection: "column", height: viewportRows, overflow: "hidden", children: visible.map(({ key, row }) => {
|
|
441
|
+
let column = 0;
|
|
442
|
+
return /* @__PURE__ */ jsx5(Text5, { wrap: "truncate", children: row.segments.map((segment) => {
|
|
443
|
+
const start = column;
|
|
444
|
+
column += segment.text.length;
|
|
445
|
+
return /* @__PURE__ */ jsx5(Text5, { color: segment.style?.color, dimColor: segment.style?.dim, children: segment.text }, `${start}`);
|
|
446
|
+
}) }, key);
|
|
447
|
+
}) });
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// src/ui/useSession.ts
|
|
451
|
+
import { useEffect, useState as useState2 } from "react";
|
|
452
|
+
function useSession(session, intervalMs, clock) {
|
|
453
|
+
const [model, setModel] = useState2(() => session.model);
|
|
454
|
+
useEffect(() => {
|
|
455
|
+
let timer = null;
|
|
456
|
+
let active = true;
|
|
457
|
+
const unsubscribe = session.subscribe(() => {
|
|
458
|
+
if (timer !== null) return;
|
|
459
|
+
timer = clock.setTimeout(() => {
|
|
460
|
+
timer = null;
|
|
461
|
+
if (active) setModel(session.model);
|
|
462
|
+
}, intervalMs);
|
|
463
|
+
});
|
|
464
|
+
setModel((current) => current === session.model ? current : session.model);
|
|
465
|
+
return () => {
|
|
466
|
+
active = false;
|
|
467
|
+
unsubscribe();
|
|
468
|
+
if (timer !== null) clock.clearTimeout(timer);
|
|
469
|
+
};
|
|
470
|
+
}, [session, intervalMs, clock]);
|
|
471
|
+
return model;
|
|
472
|
+
}
|
|
473
|
+
function useSpinner(active, clock, intervalMs = 100) {
|
|
474
|
+
const [tick, setTick] = useState2(0);
|
|
475
|
+
useEffect(() => {
|
|
476
|
+
if (!active) return;
|
|
477
|
+
let timer = null;
|
|
478
|
+
const schedule = () => {
|
|
479
|
+
timer = clock.setTimeout(() => {
|
|
480
|
+
setTick((current) => current + 1);
|
|
481
|
+
schedule();
|
|
482
|
+
}, intervalMs);
|
|
483
|
+
};
|
|
484
|
+
schedule();
|
|
485
|
+
return () => {
|
|
486
|
+
if (timer !== null) clock.clearTimeout(timer);
|
|
487
|
+
};
|
|
488
|
+
}, [active, clock, intervalMs]);
|
|
489
|
+
return spinnerGlyph(tick);
|
|
490
|
+
}
|
|
491
|
+
function useTerminalSize(terminal) {
|
|
492
|
+
const [size, setSize] = useState2({ columns: terminal.columns, rows: terminal.rows });
|
|
493
|
+
useEffect(
|
|
494
|
+
() => terminal.subscribe(() => setSize({ columns: terminal.columns, rows: terminal.rows })),
|
|
495
|
+
[terminal]
|
|
496
|
+
);
|
|
497
|
+
return size;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// src/ui/App.tsx
|
|
501
|
+
import { jsx as jsx6, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
502
|
+
var RENDER_INTERVAL_MS = 33;
|
|
503
|
+
var CHROME_ROWS = 7;
|
|
504
|
+
function headerRow(model, columns) {
|
|
505
|
+
const viewers = model.panel ? String(model.panel.viewers) : "\u2013";
|
|
506
|
+
const state = stateBadge(
|
|
507
|
+
model.snapshot?.state ?? null,
|
|
508
|
+
model.completion,
|
|
509
|
+
model.snapshot?.pauseReasons ?? []
|
|
510
|
+
);
|
|
511
|
+
const room = model.snapshot?.room.title ?? model.pinnedRoom ?? null;
|
|
512
|
+
const segment = room === null ? "" : `\xB7 ${fitRow(room, 24)} `;
|
|
513
|
+
return chromeRow(`\u256D\u2500 p2p \xB7 ChatPvP ${segment}${state} ${viewers} watching `, "\u256E", "\u2500", columns);
|
|
514
|
+
}
|
|
515
|
+
var footerRow = (columns) => chromeRow("\u2570", "\u256F", "\u2500", columns);
|
|
516
|
+
function App({ session, clock, timeZone, terminal, onExit }) {
|
|
517
|
+
const model = useSession(session, RENDER_INTERVAL_MS, clock);
|
|
518
|
+
const { columns, rows } = useTerminalSize(terminal);
|
|
519
|
+
const lines = mergeLines(model.lines, model.historyLoaded ? model.localLines : []);
|
|
520
|
+
const thinking = lines.some(
|
|
521
|
+
(line) => (line.kind === "agent" || line.kind === "dm_agent") && line.status === "thinking"
|
|
522
|
+
);
|
|
523
|
+
const transcript = useRef(null);
|
|
524
|
+
const spinner = useSpinner(thinking, clock);
|
|
525
|
+
const { isRawModeSupported } = useStdin();
|
|
526
|
+
useInput(
|
|
527
|
+
(input, key) => {
|
|
528
|
+
if (key.ctrl && input === "c") {
|
|
529
|
+
onExit?.();
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
if (session.model.picker) {
|
|
533
|
+
if (key.ctrl && input === "l") {
|
|
534
|
+
void session.logout();
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
const action = key.return ? "enter" : key.tab ? "tab" : key.leftArrow ? "left" : key.rightArrow ? "right" : key.upArrow ? "up" : key.downArrow ? "down" : key.backspace ? "backspace" : key.delete ? "delete" : key.home ? "home" : key.end ? "end" : null;
|
|
538
|
+
if (action) void session.pickerKey(action, columns);
|
|
539
|
+
else if (!key.ctrl && !key.meta && !key.escape && !key.pageUp && !key.pageDown && input)
|
|
540
|
+
void session.pickerKey("insert", columns, input);
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
if (key.pageUp) transcript.current?.navigate("pageUp");
|
|
544
|
+
else if (key.pageDown) transcript.current?.navigate("pageDown");
|
|
545
|
+
else if (key.upArrow) transcript.current?.navigate("up");
|
|
546
|
+
else if (key.downArrow) transcript.current?.navigate("down");
|
|
547
|
+
else if (key.home || key.end) {
|
|
548
|
+
const direction = key.home ? "home" : "end";
|
|
549
|
+
if (session.model.input.text) session.moveCursor(direction);
|
|
550
|
+
else transcript.current?.navigate(direction);
|
|
551
|
+
} else if (key.leftArrow) session.moveCursor(-1);
|
|
552
|
+
else if (key.rightArrow) session.moveCursor(1);
|
|
553
|
+
else if (key.backspace) session.backspace();
|
|
554
|
+
else if (key.delete) session.delete();
|
|
555
|
+
else if (session.model.browserOffer && !session.model.input.text && !key.ctrl && !key.meta && (key.return || ["y", "Y", "n", "N"].includes(input)))
|
|
556
|
+
session.answerBrowser(!key.return && input.toLowerCase() === "y");
|
|
557
|
+
else if (key.return) void session.submit();
|
|
558
|
+
else if (!key.ctrl && !key.meta && !key.escape && !key.tab && input) {
|
|
559
|
+
session.dismissBrowser();
|
|
560
|
+
session.insert(input);
|
|
561
|
+
}
|
|
562
|
+
},
|
|
563
|
+
{ isActive: isRawModeSupported }
|
|
564
|
+
);
|
|
565
|
+
if (rows < 1 || columns < 1) return null;
|
|
566
|
+
const nowMs = clock.now() + (model.snapshot?.serverOffsetMs ?? 0);
|
|
567
|
+
const viewportRows = Math.max(0, rows - CHROME_ROWS);
|
|
568
|
+
const [separator, queue, indicators] = queuePanelRows({ panel: model.panel, nowMs, columns });
|
|
569
|
+
const status = statusRow({
|
|
570
|
+
panel: model.panel,
|
|
571
|
+
holder: model.holder,
|
|
572
|
+
result: model.lines.findLast((line) => line.kind === "results"),
|
|
573
|
+
authors: model.authors,
|
|
574
|
+
connection: model.connection,
|
|
575
|
+
columns,
|
|
576
|
+
play: model,
|
|
577
|
+
nowMs: clock.now()
|
|
578
|
+
});
|
|
579
|
+
const bottom = [
|
|
580
|
+
{ name: "separator", text: separator, dim: true },
|
|
581
|
+
{ name: "queue", text: queue, dim: false },
|
|
582
|
+
{ name: "indicators", text: indicators, dim: false },
|
|
583
|
+
{ name: "footer", text: footerRow(columns), dim: true },
|
|
584
|
+
{ name: "input", text: "", dim: false },
|
|
585
|
+
{ name: "status", text: status, dim: false }
|
|
586
|
+
].slice(0, Math.max(0, rows - 1 - viewportRows));
|
|
587
|
+
return /* @__PURE__ */ jsxs2(Box4, { flexDirection: "column", width: columns, height: rows, overflow: "hidden", children: [
|
|
588
|
+
/* @__PURE__ */ jsx6(Box4, { height: 1, children: /* @__PURE__ */ jsx6(Text6, { wrap: "truncate", dimColor: true, children: headerRow(model, columns) }) }),
|
|
589
|
+
/* @__PURE__ */ jsx6(
|
|
590
|
+
Transcript,
|
|
591
|
+
{
|
|
592
|
+
ref: transcript,
|
|
593
|
+
lines,
|
|
594
|
+
authors: model.authors,
|
|
595
|
+
viewportRows: model.picker ? 0 : viewportRows,
|
|
596
|
+
columns,
|
|
597
|
+
timeZone,
|
|
598
|
+
spinner,
|
|
599
|
+
requestOlder: session.requestOlder
|
|
600
|
+
}
|
|
601
|
+
),
|
|
602
|
+
model.picker && /* @__PURE__ */ jsx6(Picker, { picker: model.picker, columns, viewportRows }),
|
|
603
|
+
bottom.map((row) => (
|
|
604
|
+
// A fixed-height box keeps an empty row (no indicators) on screen.
|
|
605
|
+
/* @__PURE__ */ jsx6(Box4, { height: 1, children: row.name === "input" ? /* @__PURE__ */ jsx6(
|
|
606
|
+
InputBox,
|
|
607
|
+
{
|
|
608
|
+
input: model.picker?.input ?? model.input,
|
|
609
|
+
showCounter: !model.picker,
|
|
610
|
+
columns,
|
|
611
|
+
maxChars: model.snapshot?.config.prompt.maxChars ?? null,
|
|
612
|
+
placeholder: model.picker ? "handle\u2026" : !model.token ? "/login to play" : !model.snapshot?.me ? "not onboarded" : "prompt\u2026"
|
|
613
|
+
}
|
|
614
|
+
) : /* @__PURE__ */ jsx6(Text6, { wrap: "truncate", dimColor: row.dim, children: row.text }) }, row.name)
|
|
615
|
+
))
|
|
616
|
+
] });
|
|
617
|
+
}
|
|
618
|
+
export {
|
|
619
|
+
App,
|
|
620
|
+
CHROME_ROWS,
|
|
621
|
+
RENDER_INTERVAL_MS,
|
|
622
|
+
footerRow,
|
|
623
|
+
headerRow
|
|
624
|
+
};
|