prompt2pwn 0.0.0-stage → 0.1.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/README.md CHANGED
@@ -1,3 +1,20 @@
1
- # Temporary Holding Version
1
+ # p2p — ChatPvP
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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
+ };