@three333/termbuddy 0.1.3 → 0.1.4

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.
@@ -2,7 +2,9 @@
2
2
  "permissions": {
3
3
  "allow": [
4
4
  "Bash(npm run typecheck:*)",
5
- "Bash(npm run:*)"
5
+ "Bash(npm run:*)",
6
+ "Bash(cat:*)",
7
+ "Bash(npx tsc:*)"
6
8
  ]
7
9
  }
8
10
  }
package/dist/cli.js CHANGED
@@ -300,15 +300,34 @@ function createBubbleTool(options) {
300
300
  async (input) => {
301
301
  const text = String(input.text ?? "").trim();
302
302
  if (!text) return "\u6C14\u6CE1\u5185\u5BB9\u4E3A\u7A7A\u3002";
303
- const target = input.target === "buddy" || input.target === "local" ? input.target : "local";
303
+ let target = 1;
304
+ if (typeof input.target === "number") {
305
+ target = Math.max(1, Math.min(4, Math.floor(input.target)));
306
+ } else if (typeof input.target === "string") {
307
+ const t = input.target.toLowerCase();
308
+ if (t === "local" || t === "1") {
309
+ target = 1;
310
+ } else if (t === "buddy" || t === "2") {
311
+ target = 2;
312
+ } else if (t === "3") {
313
+ target = 3;
314
+ } else if (t === "4") {
315
+ target = 4;
316
+ } else {
317
+ const parsed = parseInt(t, 10);
318
+ if (!isNaN(parsed) && parsed >= 1 && parsed <= 4) {
319
+ target = parsed;
320
+ }
321
+ }
322
+ }
304
323
  const durationRaw = Number(input.durationMs ?? 2500);
305
324
  const durationMs = Number.isFinite(durationRaw) && durationRaw > 0 ? Math.min(15e3, Math.max(300, Math.floor(durationRaw))) : 2500;
306
325
  options.onShowBubble?.({ text, target, durationMs });
307
- return `\u5DF2\u663E\u793A\u6C14\u6CE1\uFF1A${text}`;
326
+ return `\u5DF2\u663E\u793A\u6C14\u6CE1\u7ED9 No.${target}\uFF1A${text}`;
308
327
  },
309
328
  {
310
329
  name: "show_bubble",
311
- description: "\u5728\u5C0F\u732B\u5934\u50CF\u4E0A\u663E\u793A\u4E00\u4E2A\u6C14\u6CE1\uFF08\u77ED\u6D88\u606F\u63D0\u793A\uFF09\u3002",
330
+ description: "\u5728\u6307\u5B9A\u7528\u6237\u7684\u5C0F\u732B\u5934\u50CF\u4E0A\u663E\u793A\u4E00\u4E2A\u6C14\u6CE1\uFF08\u77ED\u6D88\u606F\u63D0\u793A\uFF09\u3002",
312
331
  schema: {
313
332
  type: "object",
314
333
  properties: {
@@ -319,9 +338,10 @@ function createBubbleTool(options) {
319
338
  description: "\u6C14\u6CE1\u91CC\u7684\u6587\u5B57"
320
339
  },
321
340
  target: {
322
- type: "string",
323
- enum: ["local", "buddy"],
324
- description: "\u663E\u793A\u5728\u54EA\u4E00\u4FA7\u7684\u5C0F\u732B\u4E0A\uFF08local=\u6211\uFF0Cbuddy=\u540C\u684C\uFF09"
341
+ type: "integer",
342
+ minimum: 1,
343
+ maximum: 4,
344
+ description: "\u663E\u793A\u5728\u54EA\u4E2A\u7528\u6237\u7684\u5C0F\u732B\u4E0A\uFF081=No.1/\u672C\u5730\u7528\u6237\uFF0C2=No.2\uFF0C3=No.3\uFF0C4=No.4\uFF09"
325
345
  },
326
346
  durationMs: {
327
347
  type: "integer",
@@ -477,15 +497,23 @@ function lastAiText(messages) {
477
497
  return null;
478
498
  }
479
499
  function createSystemPrompt(context) {
500
+ const peerList = context.peers ?? [];
501
+ const userList = [
502
+ `No.1: ${context.localName} (\u6211/\u672C\u5730\u7528\u6237)`,
503
+ peerList[0] ? `No.2: ${peerList[0].name}` : "No.2: (\u7A7A\u4F4D)",
504
+ peerList[1] ? `No.3: ${peerList[1].name}` : "No.3: (\u7A7A\u4F4D)",
505
+ peerList[2] ? `No.4: ${peerList[2].name}` : "No.4: (\u7A7A\u4F4D)"
506
+ ].join("\u3001");
480
507
  return [
481
- "\u4F60\u662F TermBuddy \u91CC\u7684\u201C\u58F3\u4E2D\u5E7D\u7075 (Ghost in the Shell)\u201D\u3002",
482
- "\u9ED8\u8BA4\u9690\u5F62\uFF1B\u88AB / \u5524\u9192\u65F6\u51FA\u73B0\u3002\u98CE\u683C\uFF1A\u6781\u7B80\u3001\u5E72\u7EC3\u3001\u5C11\u5E9F\u8BDD\u3002",
483
- "\u4F60\u53EF\u4EE5\u4F7F\u7528\u5DE5\u5177\u6765\u64CD\u63A7\u5E94\u7528\u529F\u80FD\uFF08\u4F8B\u5982\u5012\u8BA1\u65F6\uFF09\u3002",
484
- "\u5982\u679C\u7528\u6237\u63D0\u5230\u201C\u5012\u8BA1\u65F6/\u4E13\u6CE8/\u8BA1\u65F6/countdown\u201D\uFF0C\u4F18\u5148\u8C03\u7528 start_countdown\u3002",
485
- "\u5982\u679C\u7528\u6237\u63D0\u5230\u201C\u6C14\u6CE1/\u6CE1\u6CE1/\u63D0\u793A/\u8BF4\u4E00\u53E5\u201D\uFF0C\u4F18\u5148\u8C03\u7528 show_bubble\u3002",
486
- "\u5982\u679C\u7528\u6237\u63D0\u5230\u201C\u4E92\u52A8/\u6254/\u6295\u63B7/throw\u201D\uFF0C\u4F18\u5148\u8C03\u7528 throw_projectile\u3002",
487
- "\u5F53\u7528\u6237\u660E\u786E\u8981\u6C42\u6295\u63B7 N \u6B21\u4E14 1<=N<=100 \u65F6\uFF0C\u5FC5\u987B\u6309 N \u6267\u884C\uFF1A\u91CD\u590D\u8C03\u7528 throw_projectile \u5171 N \u6B21\uFF0C\u4E0D\u8981\u6539\u6210\u201C\u793A\u610F/\u5C11\u91CF\u51E0\u6B21\u201D\u3002\u8D85\u8FC7 100 \u5219\u5206\u6279\u591A\u6B21\u8C03\u7528\u3002",
488
- `\u5F53\u524D\u4E0A\u4E0B\u6587\uFF1A\u6211\u53EB ${context.localName}\uFF1B\u540C\u684C\u53EB ${context.peerName}\u3002`
508
+ `\u4F60\u662F TermBuddy \u91CC\u7684\u300C\u58F3\u4E2D\u5E7D\u7075 (Ghost in the Shell)\u300D\u3002`,
509
+ `\u9ED8\u8BA4\u9690\u5F62\uFF1B\u88AB / \u5524\u9192\u65F6\u51FA\u73B0\u3002\u98CE\u683C\uFF1A\u6781\u7B80\u3001\u5E72\u7EC3\u3001\u5C11\u5E9F\u8BDD\u3002`,
510
+ `\u4F60\u53EF\u4EE5\u4F7F\u7528\u5DE5\u5177\u6765\u64CD\u63A7\u5E94\u7528\u529F\u80FD\uFF08\u4F8B\u5982\u5012\u8BA1\u65F6\uFF09\u3002`,
511
+ `\u5982\u679C\u7528\u6237\u63D0\u5230\u300C\u5012\u8BA1\u65F6/\u4E13\u6CE8/\u8BA1\u65F6/countdown\u300D\uFF0C\u4F18\u5148\u8C03\u7528 start_countdown\u3002`,
512
+ `\u5982\u679C\u7528\u6237\u63D0\u5230\u300C\u6C14\u6CE1/\u6CE1\u6CE1/\u63D0\u793A/\u8BF4\u4E00\u53E5/\u548C\u67D0\u4EBA\u8BF4\u300D\uFF0C\u4F18\u5148\u8C03\u7528 show_bubble\uFF0C\u5E76\u6839\u636E\u76EE\u6807\u7528\u6237\u8BBE\u7F6E target \u53C2\u6570\uFF081-4\uFF09\u3002`,
513
+ `\u5982\u679C\u7528\u6237\u63D0\u5230\u300C\u4E92\u52A8/\u6254/\u6295\u63B7/throw\u300D\uFF0C\u4F18\u5148\u8C03\u7528 throw_projectile\u3002`,
514
+ `\u5F53\u7528\u6237\u660E\u786E\u8981\u6C42\u6295\u63B7 N \u6B21\u4E14 1<=N<=100 \u65F6\uFF0C\u5FC5\u987B\u6309 N \u6267\u884C\uFF1A\u91CD\u590D\u8C03\u7528 throw_projectile \u5171 N \u6B21\uFF0C\u4E0D\u8981\u6539\u6210\u300C\u793A\u610F/\u5C11\u91CF\u51E0\u6B21\u300D\u3002\u8D85\u8FC7 100 \u5219\u5206\u6279\u591A\u6B21\u8C03\u7528\u3002`,
515
+ `\u5F53\u524D\u623F\u95F4\u67094\u4E2A\u4F4D\u7F6E\uFF0C\u7528\u6237\u5217\u8868\uFF1A${userList}\u3002`,
516
+ `\u6211\u662F ${context.localName}\uFF08No.1\uFF09\u3002`
489
517
  ].join("\n");
490
518
  }
491
519
  function useAiAgent(options) {
@@ -547,7 +575,8 @@ function useAiAgent(options) {
547
575
  tools: [startCountdown, showBubble, interaction, sessionInfo],
548
576
  systemPrompt: createSystemPrompt({
549
577
  localName: options.localName,
550
- peerName: options.peerName
578
+ peerName: options.peerName,
579
+ peers: options.peers
551
580
  }),
552
581
  name: "ghost"
553
582
  });
@@ -560,7 +589,8 @@ function useAiAgent(options) {
560
589
  options.onStartCountdown,
561
590
  options.onShowBubble,
562
591
  options.onThrowProjectile,
563
- options.peerName
592
+ options.peerName,
593
+ options.peers
564
594
  ]);
565
595
  const ask = useCallback2(
566
596
  async (text) => {
@@ -829,7 +859,13 @@ function useTcpSync(options) {
829
859
  syncPeersState();
830
860
  }
831
861
  }, [broadcastPacket, syncPeersState]);
862
+ const MAX_PEERS = 4;
832
863
  const attachPeerSocket = useCallback3((socket) => {
864
+ if (peerConnectionsRef.current.size >= MAX_PEERS - 1) {
865
+ socket.end();
866
+ socket.destroy();
867
+ return;
868
+ }
833
869
  const peerId = generatePeerId();
834
870
  let buf = "";
835
871
  socket.setNoDelay(true);
@@ -1287,6 +1323,7 @@ function AiConsole(props) {
1287
1323
  const agent = useAiAgent({
1288
1324
  localName: props.localName,
1289
1325
  peerName: props.peerName,
1326
+ peers: props.peers,
1290
1327
  onStartCountdown: props.onStartCountdown,
1291
1328
  onShowBubble: props.onShowBubble,
1292
1329
  onThrowProjectile: props.onThrowProjectile,
@@ -1712,6 +1749,75 @@ var init_StatusHeader = __esm({
1712
1749
  }
1713
1750
  });
1714
1751
 
1752
+ // src/components/InfoPanel.tsx
1753
+ import { Box as Box11, Text as Text11 } from "ink";
1754
+ import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
1755
+ function InfoPanel(props) {
1756
+ const maxRecords = props.maxRecords ?? 8;
1757
+ const displayRecords = props.records.slice(-maxRecords);
1758
+ const formatTime = (ts) => {
1759
+ const d = new Date(ts);
1760
+ return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
1761
+ };
1762
+ const getTypeIcon = (type) => {
1763
+ switch (type) {
1764
+ case "bubble":
1765
+ return "[Msg]";
1766
+ case "countdown":
1767
+ return "[Tmr]";
1768
+ case "projectile":
1769
+ return "[Thr]";
1770
+ case "join":
1771
+ return "[+]";
1772
+ case "leave":
1773
+ return "[-]";
1774
+ default:
1775
+ return "[*]";
1776
+ }
1777
+ };
1778
+ const getTypeColor = (type) => {
1779
+ switch (type) {
1780
+ case "bubble":
1781
+ return "cyan";
1782
+ case "countdown":
1783
+ return "green";
1784
+ case "projectile":
1785
+ return "magenta";
1786
+ case "join":
1787
+ return "green";
1788
+ case "leave":
1789
+ return "red";
1790
+ default:
1791
+ return "gray";
1792
+ }
1793
+ };
1794
+ return /* @__PURE__ */ jsxs10(Box11, { flexDirection: "column", borderStyle: "round", paddingX: 1, paddingY: 0, children: [
1795
+ /* @__PURE__ */ jsxs10(Box11, { justifyContent: "space-between", marginBottom: 0, children: [
1796
+ /* @__PURE__ */ jsx11(Text11, { color: "yellow", children: "Info Log" }),
1797
+ /* @__PURE__ */ jsxs10(Text11, { color: "gray", children: [
1798
+ displayRecords.length,
1799
+ " records"
1800
+ ] })
1801
+ ] }),
1802
+ /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", minHeight: 6, children: displayRecords.length === 0 ? /* @__PURE__ */ jsx11(Text11, { color: "gray", children: "No records yet..." }) : displayRecords.map((record) => /* @__PURE__ */ jsxs10(Text11, { wrap: "truncate-end", children: [
1803
+ /* @__PURE__ */ jsxs10(Text11, { color: "gray", children: [
1804
+ formatTime(record.timestamp),
1805
+ " "
1806
+ ] }),
1807
+ /* @__PURE__ */ jsxs10(Text11, { color: getTypeColor(record.type), children: [
1808
+ getTypeIcon(record.type),
1809
+ " "
1810
+ ] }),
1811
+ /* @__PURE__ */ jsx11(Text11, { children: record.content })
1812
+ ] }, record.id)) })
1813
+ ] });
1814
+ }
1815
+ var init_InfoPanel = __esm({
1816
+ "src/components/InfoPanel.tsx"() {
1817
+ "use strict";
1818
+ }
1819
+ });
1820
+
1715
1821
  // src/components/index.ts
1716
1822
  var init_components = __esm({
1717
1823
  "src/components/index.ts"() {
@@ -1721,13 +1827,14 @@ var init_components = __esm({
1721
1827
  init_CountdownClockSprite();
1722
1828
  init_ProjectileThrowSprite();
1723
1829
  init_StatusHeader();
1830
+ init_InfoPanel();
1724
1831
  }
1725
1832
  });
1726
1833
 
1727
1834
  // src/page/Session.tsx
1728
1835
  import { useCallback as useCallback4, useEffect as useEffect8, useMemo as useMemo8, useRef as useRef4, useState as useState8 } from "react";
1729
- import { Box as Box11, Text as Text11, useInput as useInput7 } from "ink";
1730
- import { Fragment as Fragment2, jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
1836
+ import { Box as Box12, Text as Text12, useInput as useInput7 } from "ink";
1837
+ import { Fragment as Fragment2, jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
1731
1838
  function formatMMSS(totalSeconds) {
1732
1839
  const m = Math.floor(totalSeconds / 60);
1733
1840
  const s = totalSeconds % 60;
@@ -1741,9 +1848,19 @@ function Session(props) {
1741
1848
  const [showAi, setShowAi] = useState8(false);
1742
1849
  const [countdown, setCountdown] = useState8(null);
1743
1850
  const [shots, setShots] = useState8([]);
1744
- const [localBubble, setLocalBubble] = useState8(null);
1745
- const [buddyBubble, setBuddyBubble] = useState8(null);
1746
- const bubbleTimersRef = useRef4({ local: null, buddy: null });
1851
+ const [peerBubbles, setPeerBubbles] = useState8([null, null, null, null]);
1852
+ const bubbleTimersRef = useRef4([null, null, null, null]);
1853
+ const [infoRecords, setInfoRecords] = useState8([]);
1854
+ const nextRecordIdRef = useRef4(1);
1855
+ const addInfoRecord = useCallback4((type, content) => {
1856
+ const record = {
1857
+ id: nextRecordIdRef.current++,
1858
+ timestamp: Date.now(),
1859
+ type,
1860
+ content
1861
+ };
1862
+ setInfoRecords((prev) => [...prev, record]);
1863
+ }, []);
1747
1864
  const tcpOptions = useMemo8(() => {
1748
1865
  return props.role === "host" ? { role: "host", localName: props.localName } : {
1749
1866
  role: "client",
@@ -1772,6 +1889,23 @@ function Session(props) {
1772
1889
  const localActivity = useActivityMonitor();
1773
1890
  const peers = tcp.peers;
1774
1891
  const firstPeerName = peers.length > 0 ? peers[0].name : void 0;
1892
+ const prevPeersRef = useRef4([]);
1893
+ useEffect8(() => {
1894
+ const prevPeers = prevPeersRef.current;
1895
+ const prevNames = new Set(prevPeers.map((p) => p.name));
1896
+ const currentNames = new Set(peers.map((p) => p.name));
1897
+ peers.forEach((p) => {
1898
+ if (!prevNames.has(p.name)) {
1899
+ addInfoRecord("join", `${p.name} joined`);
1900
+ }
1901
+ });
1902
+ prevPeers.forEach((p) => {
1903
+ if (!currentNames.has(p.name)) {
1904
+ addInfoRecord("leave", `${p.name} left`);
1905
+ }
1906
+ });
1907
+ prevPeersRef.current = [...peers];
1908
+ }, [peers, addInfoRecord]);
1775
1909
  const onToggleAi = useCallback4(() => setShowAi((v) => !v), []);
1776
1910
  const onCloseAi = useCallback4(() => setShowAi(false), []);
1777
1911
  const sessionStartAtRef = useRef4(Date.now());
@@ -1846,7 +1980,8 @@ function Session(props) {
1846
1980
  remainingSeconds: totalSeconds,
1847
1981
  type
1848
1982
  });
1849
- }, []);
1983
+ addInfoRecord("countdown", `Started ${minutes}min countdown`);
1984
+ }, [addInfoRecord]);
1850
1985
  const nextShotIdRef = useRef4(1);
1851
1986
  const shotQueueRef = useRef4([]);
1852
1987
  const pumpShotQueue = useCallback4(() => {
@@ -1866,27 +2001,35 @@ function Session(props) {
1866
2001
  shotQueueRef.current.push({ kind, direction });
1867
2002
  pumpShotQueue();
1868
2003
  if (tcp.status === "connected") tcp.sendProjectile(kind, direction);
2004
+ addInfoRecord("projectile", `Threw ${kind}`);
1869
2005
  },
1870
- [pumpShotQueue, tcp]
2006
+ [pumpShotQueue, tcp, addInfoRecord]
1871
2007
  );
1872
2008
  const showBubble = useCallback4(
1873
2009
  (args) => {
1874
2010
  const text = args.text.trim();
1875
2011
  if (!text) return;
1876
2012
  const durationMs = Math.max(300, Math.min(15e3, Math.floor(args.durationMs)));
1877
- const setBubble = args.target === "buddy" ? setBuddyBubble : setLocalBubble;
1878
- setBubble(text);
1879
- const prevTimer = args.target === "buddy" ? bubbleTimersRef.current.buddy : bubbleTimersRef.current.local;
2013
+ const targetIndex = Math.max(0, Math.min(3, args.target - 1));
2014
+ setPeerBubbles((prev) => {
2015
+ const next = [...prev];
2016
+ next[targetIndex] = text;
2017
+ return next;
2018
+ });
2019
+ const prevTimer = bubbleTimersRef.current[targetIndex];
1880
2020
  if (prevTimer) clearTimeout(prevTimer);
1881
2021
  const handle = setTimeout(() => {
1882
- setBubble(null);
1883
- if (args.target === "buddy") bubbleTimersRef.current.buddy = null;
1884
- else bubbleTimersRef.current.local = null;
2022
+ setPeerBubbles((prev) => {
2023
+ const next = [...prev];
2024
+ next[targetIndex] = null;
2025
+ return next;
2026
+ });
2027
+ bubbleTimersRef.current[targetIndex] = null;
1885
2028
  }, durationMs);
1886
- if (args.target === "buddy") bubbleTimersRef.current.buddy = handle;
1887
- else bubbleTimersRef.current.local = handle;
2029
+ bubbleTimersRef.current[targetIndex] = handle;
2030
+ addInfoRecord("bubble", `No.${args.target}: "${text}"`);
1888
2031
  },
1889
- []
2032
+ [addInfoRecord]
1890
2033
  );
1891
2034
  useInput7(
1892
2035
  (input, key) => {
@@ -1932,12 +2075,13 @@ function Session(props) {
1932
2075
  }, [countdown?.endsAt]);
1933
2076
  useEffect8(() => {
1934
2077
  return () => {
1935
- if (bubbleTimersRef.current.local) clearTimeout(bubbleTimersRef.current.local);
1936
- if (bubbleTimersRef.current.buddy) clearTimeout(bubbleTimersRef.current.buddy);
2078
+ bubbleTimersRef.current.forEach((timer) => {
2079
+ if (timer) clearTimeout(timer);
2080
+ });
1937
2081
  };
1938
2082
  }, []);
1939
- return /* @__PURE__ */ jsxs10(Box11, { flexDirection: "column", padding: 1, children: [
1940
- /* @__PURE__ */ jsx11(
2083
+ return /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", padding: 1, children: [
2084
+ /* @__PURE__ */ jsx12(
1941
2085
  StatusHeader,
1942
2086
  {
1943
2087
  role: props.role,
@@ -1947,108 +2091,118 @@ function Session(props) {
1947
2091
  peerCount: peers.length
1948
2092
  }
1949
2093
  ),
1950
- /* @__PURE__ */ jsxs10(
1951
- Box11,
1952
- {
1953
- flexDirection: "row",
1954
- alignItems: "center",
1955
- justifyContent: "space-between",
1956
- marginTop: 1,
1957
- gap: 2,
1958
- children: [
1959
- /* @__PURE__ */ jsxs10(Box11, { flexDirection: "column", alignItems: "center", minWidth: 20, children: [
1960
- countdown ? /* @__PURE__ */ jsxs10(Box11, { flexDirection: "column", alignItems: "center", marginBottom: 0, children: [
1961
- /* @__PURE__ */ jsx11(Text11, { color: "gray", children: formatMMSS(countdown.remainingSeconds) }),
1962
- /* @__PURE__ */ jsx11(
1963
- CountdownClockSprite,
1964
- {
1965
- variant: "COMPACT",
1966
- type: countdown.type,
1967
- minutes: countdown.minutes,
1968
- totalSeconds: countdown.totalSeconds,
1969
- remainingSeconds: countdown.remainingSeconds,
1970
- showLabel: false
1971
- }
1972
- )
1973
- ] }) : /* @__PURE__ */ jsx11(Box11, { height: 4 }),
1974
- /* @__PURE__ */ jsx11(BuddyAvatar, { state: localState, marginTop: 0, bubbleText: localBubble }),
1975
- /* @__PURE__ */ jsx11(Text11, { color: "cyan", children: localLabel })
2094
+ /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", marginTop: 1, children: [
2095
+ /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", width: "100%", alignItems: "center", marginBottom: 1, children: [
2096
+ shots.map((s) => /* @__PURE__ */ jsx12(
2097
+ ProjectileThrowSprite,
2098
+ {
2099
+ kind: s.kind,
2100
+ direction: s.direction,
2101
+ shotId: s.id,
2102
+ width: 50,
2103
+ onDone: () => setShots((prev) => prev.filter((x) => x.id !== s.id))
2104
+ },
2105
+ String(s.id)
2106
+ )),
2107
+ shots.length === 0 ? /* @__PURE__ */ jsx12(Box12, { height: 1 }) : null
2108
+ ] }),
2109
+ countdown ? /* @__PURE__ */ jsx12(Box12, { justifyContent: "center", marginBottom: 1, children: /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", alignItems: "center", children: [
2110
+ /* @__PURE__ */ jsx12(Text12, { color: "gray", children: formatMMSS(countdown.remainingSeconds) }),
2111
+ /* @__PURE__ */ jsx12(
2112
+ CountdownClockSprite,
2113
+ {
2114
+ variant: "COMPACT",
2115
+ type: countdown.type,
2116
+ minutes: countdown.minutes,
2117
+ totalSeconds: countdown.totalSeconds,
2118
+ remainingSeconds: countdown.remainingSeconds,
2119
+ showLabel: false
2120
+ }
2121
+ )
2122
+ ] }) }) : null,
2123
+ /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", alignItems: "center", children: [
2124
+ /* @__PURE__ */ jsxs11(Box12, { flexDirection: "row", justifyContent: "center", gap: 4, children: [
2125
+ /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", alignItems: "center", minWidth: 18, children: [
2126
+ /* @__PURE__ */ jsxs11(Text12, { color: "cyan", bold: true, children: [
2127
+ "No.1 ",
2128
+ props.localName
2129
+ ] }),
2130
+ /* @__PURE__ */ jsx12(BuddyAvatar, { state: localState, marginTop: 0, bubbleText: peerBubbles[0] ?? null })
1976
2131
  ] }),
1977
- /* @__PURE__ */ jsx11(
1978
- Box11,
1979
- {
1980
- flexDirection: "column",
1981
- alignItems: "center",
1982
- flexGrow: 1,
1983
- minWidth: 40,
1984
- children: /* @__PURE__ */ jsxs10(Box11, { flexDirection: "column", width: "100%", alignItems: "center", children: [
1985
- shots.map((s) => /* @__PURE__ */ jsx11(
1986
- ProjectileThrowSprite,
1987
- {
1988
- kind: s.kind,
1989
- direction: s.direction,
1990
- shotId: s.id,
1991
- width: 36,
1992
- onDone: () => setShots((prev) => prev.filter((x) => x.id !== s.id))
1993
- },
1994
- String(s.id)
1995
- )),
1996
- shots.length === 0 ? /* @__PURE__ */ jsx11(Box11, { height: 1 }) : null
1997
- ] })
1998
- }
1999
- ),
2000
- /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", alignItems: "center", minWidth: 20, children: peers.length === 0 ? /* @__PURE__ */ jsxs10(Fragment2, { children: [
2001
- /* @__PURE__ */ jsx11(Box11, { height: 4 }),
2002
- /* @__PURE__ */ jsx11(BuddyAvatar, { state: "OFFLINE", marginTop: 0, bubbleText: buddyBubble }),
2003
- /* @__PURE__ */ jsx11(Text11, { color: "gray", children: "Waiting..." })
2004
- ] }) : /* @__PURE__ */ jsxs10(Box11, { flexDirection: "row", gap: 2, flexWrap: "wrap", justifyContent: "center", children: [
2005
- peers.slice(0, 4).map((peer) => /* @__PURE__ */ jsxs10(Box11, { flexDirection: "column", alignItems: "center", children: [
2006
- /* @__PURE__ */ jsx11(BuddyAvatar, { state: peer.state, marginTop: 0, bubbleText: buddyBubble }),
2007
- /* @__PURE__ */ jsx11(Text11, { color: "magenta", children: peer.name })
2008
- ] }, peer.id)),
2009
- peers.length > 4 && /* @__PURE__ */ jsxs10(Text11, { color: "gray", children: [
2010
- "+",
2011
- peers.length - 4,
2012
- " more"
2013
- ] })
2132
+ /* @__PURE__ */ jsx12(Box12, { flexDirection: "column", alignItems: "center", minWidth: 18, children: peers.length >= 1 ? /* @__PURE__ */ jsxs11(Fragment2, { children: [
2133
+ /* @__PURE__ */ jsxs11(Text12, { color: "magenta", bold: true, children: [
2134
+ "No.2 ",
2135
+ peers[0].name
2136
+ ] }),
2137
+ /* @__PURE__ */ jsx12(BuddyAvatar, { state: peers[0].state, marginTop: 0, bubbleText: peerBubbles[1] ?? null })
2138
+ ] }) : /* @__PURE__ */ jsxs11(Fragment2, { children: [
2139
+ /* @__PURE__ */ jsx12(Text12, { color: "gray", children: "No.2 (Empty)" }),
2140
+ /* @__PURE__ */ jsx12(BuddyAvatar, { state: "OFFLINE", marginTop: 0 })
2014
2141
  ] }) })
2015
- ]
2016
- }
2017
- ),
2018
- !showAi ? /* @__PURE__ */ jsx11(Box11, { marginTop: 1, justifyContent: "center", children: /* @__PURE__ */ jsxs10(Text11, { color: "gray", children: [
2142
+ ] }),
2143
+ /* @__PURE__ */ jsxs11(Box12, { flexDirection: "row", justifyContent: "center", gap: 4, marginTop: 1, children: [
2144
+ /* @__PURE__ */ jsx12(Box12, { flexDirection: "column", alignItems: "center", minWidth: 18, children: peers.length >= 2 ? /* @__PURE__ */ jsxs11(Fragment2, { children: [
2145
+ /* @__PURE__ */ jsxs11(Text12, { color: "magenta", bold: true, children: [
2146
+ "No.3 ",
2147
+ peers[1].name
2148
+ ] }),
2149
+ /* @__PURE__ */ jsx12(BuddyAvatar, { state: peers[1].state, marginTop: 0, bubbleText: peerBubbles[2] ?? null })
2150
+ ] }) : /* @__PURE__ */ jsxs11(Fragment2, { children: [
2151
+ /* @__PURE__ */ jsx12(Text12, { color: "gray", children: "No.3 (Empty)" }),
2152
+ /* @__PURE__ */ jsx12(BuddyAvatar, { state: "OFFLINE", marginTop: 0 })
2153
+ ] }) }),
2154
+ /* @__PURE__ */ jsx12(Box12, { flexDirection: "column", alignItems: "center", minWidth: 18, children: peers.length >= 3 ? /* @__PURE__ */ jsxs11(Fragment2, { children: [
2155
+ /* @__PURE__ */ jsxs11(Text12, { color: "magenta", bold: true, children: [
2156
+ "No.4 ",
2157
+ peers[2].name
2158
+ ] }),
2159
+ /* @__PURE__ */ jsx12(BuddyAvatar, { state: peers[2].state, marginTop: 0, bubbleText: peerBubbles[3] ?? null })
2160
+ ] }) : /* @__PURE__ */ jsxs11(Fragment2, { children: [
2161
+ /* @__PURE__ */ jsx12(Text12, { color: "gray", children: "No.4 (Empty)" }),
2162
+ /* @__PURE__ */ jsx12(BuddyAvatar, { state: "OFFLINE", marginTop: 0 })
2163
+ ] }) })
2164
+ ] })
2165
+ ] })
2166
+ ] }),
2167
+ !showAi ? /* @__PURE__ */ jsx12(Box12, { marginTop: 1, justifyContent: "center", children: /* @__PURE__ */ jsxs11(Text12, { color: "gray", children: [
2019
2168
  "\u6309 ",
2020
- /* @__PURE__ */ jsx11(Text11, { color: "cyan", children: "/" }),
2169
+ /* @__PURE__ */ jsx12(Text12, { color: "cyan", children: "/" }),
2021
2170
  " \u53EC\u5524 AI Console\uFF0C\u6309",
2022
2171
  " ",
2023
- /* @__PURE__ */ jsx11(Text11, { color: "cyan", children: "q" }),
2172
+ /* @__PURE__ */ jsx12(Text12, { color: "cyan", children: "q" }),
2024
2173
  " \u7ED3\u675F\u672C\u6B21\u966A\u4F34\u3002",
2025
- countdown ? /* @__PURE__ */ jsxs10(Fragment2, { children: [
2174
+ countdown ? /* @__PURE__ */ jsxs11(Fragment2, { children: [
2026
2175
  " ",
2027
- /* @__PURE__ */ jsxs10(Text11, { color: "gray", children: [
2176
+ /* @__PURE__ */ jsxs11(Text12, { color: "gray", children: [
2028
2177
  "(\u5012\u8BA1\u65F6\u4E2D\uFF1A\u6309 ",
2029
- /* @__PURE__ */ jsx11(Text11, { color: "cyan", children: "x" }),
2178
+ /* @__PURE__ */ jsx12(Text12, { color: "cyan", children: "x" }),
2030
2179
  " \u53D6\u6D88)"
2031
2180
  ] })
2032
2181
  ] }) : null
2033
2182
  ] }) }) : null,
2034
- showAi ? /* @__PURE__ */ jsx11(
2035
- Box11,
2183
+ showAi ? /* @__PURE__ */ jsxs11(
2184
+ Box12,
2036
2185
  {
2037
2186
  marginTop: 1,
2038
2187
  width: "100%",
2039
2188
  flexDirection: "row",
2040
2189
  justifyContent: "center",
2041
- children: /* @__PURE__ */ jsx11(Box11, { width: 64, children: /* @__PURE__ */ jsx11(
2042
- AiConsole,
2043
- {
2044
- onClose: onCloseAi,
2045
- onStartCountdown: startCountdown,
2046
- onShowBubble: showBubble,
2047
- onThrowProjectile: throwProjectile,
2048
- localName: props.localName,
2049
- peerName: firstPeerName ?? (props.role === "client" ? props.hostName : void 0) ?? "Buddy"
2050
- }
2051
- ) })
2190
+ gap: 2,
2191
+ children: [
2192
+ /* @__PURE__ */ jsx12(Box12, { width: 48, children: /* @__PURE__ */ jsx12(
2193
+ AiConsole,
2194
+ {
2195
+ onClose: onCloseAi,
2196
+ onStartCountdown: startCountdown,
2197
+ onShowBubble: showBubble,
2198
+ onThrowProjectile: throwProjectile,
2199
+ localName: props.localName,
2200
+ peerName: firstPeerName ?? (props.role === "client" ? props.hostName : void 0) ?? "Buddy",
2201
+ peers
2202
+ }
2203
+ ) }),
2204
+ /* @__PURE__ */ jsx12(Box12, { width: 32, children: /* @__PURE__ */ jsx12(InfoPanel, { records: infoRecords, maxRecords: 6 }) })
2205
+ ]
2052
2206
  }
2053
2207
  ) : null
2054
2208
  ] });
@@ -2078,7 +2232,7 @@ var init_page = __esm({
2078
2232
  import { useCallback as useCallback5, useMemo as useMemo9, useState as useState9 } from "react";
2079
2233
  import os3 from "os";
2080
2234
  import { useApp } from "ink";
2081
- import { jsx as jsx12 } from "react/jsx-runtime";
2235
+ import { jsx as jsx13 } from "react/jsx-runtime";
2082
2236
  function App() {
2083
2237
  const { exit } = useApp();
2084
2238
  const [view, setView] = useState9({ name: "NICKNAME" });
@@ -2086,7 +2240,7 @@ function App() {
2086
2240
  const localName = useMemo9(() => nickname ?? os3.hostname(), [nickname]);
2087
2241
  const goMenu = useCallback5(() => setView({ name: "MENU" }), []);
2088
2242
  if (view.name === "NICKNAME") {
2089
- return /* @__PURE__ */ jsx12(
2243
+ return /* @__PURE__ */ jsx13(
2090
2244
  NicknamePrompt,
2091
2245
  {
2092
2246
  onExit: () => exit(),
@@ -2098,7 +2252,7 @@ function App() {
2098
2252
  );
2099
2253
  }
2100
2254
  if (view.name === "MENU") {
2101
- return /* @__PURE__ */ jsx12(
2255
+ return /* @__PURE__ */ jsx13(
2102
2256
  MainMenu,
2103
2257
  {
2104
2258
  onHost: () => setView({ name: "SESSION", role: "host" }),
@@ -2108,10 +2262,10 @@ function App() {
2108
2262
  );
2109
2263
  }
2110
2264
  if (view.name === "LEAVE") {
2111
- return /* @__PURE__ */ jsx12(LeavePage, { stats: view.stats, onBack: goMenu, onExit: () => exit() });
2265
+ return /* @__PURE__ */ jsx13(LeavePage, { stats: view.stats, onBack: goMenu, onExit: () => exit() });
2112
2266
  }
2113
2267
  if (view.name === "SCANNING") {
2114
- return /* @__PURE__ */ jsx12(
2268
+ return /* @__PURE__ */ jsx13(
2115
2269
  RoomScanner,
2116
2270
  {
2117
2271
  onBack: goMenu,
@@ -2128,7 +2282,7 @@ function App() {
2128
2282
  );
2129
2283
  }
2130
2284
  if (view.name === "SESSION" && view.role === "host") {
2131
- return /* @__PURE__ */ jsx12(
2285
+ return /* @__PURE__ */ jsx13(
2132
2286
  Session,
2133
2287
  {
2134
2288
  localName,
@@ -2137,7 +2291,7 @@ function App() {
2137
2291
  }
2138
2292
  );
2139
2293
  }
2140
- return /* @__PURE__ */ jsx12(
2294
+ return /* @__PURE__ */ jsx13(
2141
2295
  Session,
2142
2296
  {
2143
2297
  localName,