@three333/termbuddy 0.1.0 → 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.
Files changed (38) hide show
  1. package/dist/cli.js +1097 -260
  2. package/dist/cli.js.map +1 -1
  3. package/package.json +3 -2
  4. package/pnpm-workspace.yaml +2 -0
  5. package/src/app/App.tsx +94 -53
  6. package/src/app/index.ts +1 -2
  7. package/src/components/AiConsole.tsx +171 -73
  8. package/src/components/StatusHeader.tsx +36 -36
  9. package/src/components/index.ts +8 -4
  10. package/src/components/sprite/BuddyAvatar.tsx +49 -0
  11. package/src/components/sprite/CountdownClockSprite.tsx +146 -0
  12. package/src/components/sprite/ProjectileThrowSprite.tsx +86 -0
  13. package/src/components/tool/createCountdownTool.ts +32 -0
  14. package/src/components/tool/createInteractionTool.ts +67 -0
  15. package/src/components/tool/createSessionInfoTool.ts +29 -0
  16. package/src/components/tool/index.ts +4 -0
  17. package/src/hooks/globalKeyboard.ts +146 -0
  18. package/src/hooks/index.ts +5 -7
  19. package/src/hooks/useActivityMonitor.ts +61 -24
  20. package/src/hooks/useAiAgent.ts +200 -165
  21. package/src/hooks/useBroadcaster.ts +55 -47
  22. package/src/hooks/useScanner.ts +59 -55
  23. package/src/hooks/useTcpSync.ts +166 -145
  24. package/src/net/broadcast.ts +21 -21
  25. package/src/net/index.ts +1 -2
  26. package/src/page/LeavePage.tsx +85 -0
  27. package/src/{views → page}/MainMenu.tsx +32 -28
  28. package/src/page/NicknamePrompt.tsx +62 -0
  29. package/src/{views → page}/RoomScanner.tsx +4 -1
  30. package/src/page/Session.tsx +364 -0
  31. package/src/page/index.ts +5 -0
  32. package/src/storage/apiKey.ts +36 -0
  33. package/src/types.ts +8 -0
  34. package/src/components/AvatarDisplay.tsx +0 -18
  35. package/src/components/BuddyAvatar.tsx +0 -32
  36. package/src/hooks/useCountdown.ts +0 -42
  37. package/src/views/Session.tsx +0 -127
  38. package/src/views/index.ts +0 -4
@@ -1,153 +1,174 @@
1
- import {useCallback, useEffect, useRef, useState} from 'react';
2
- import net from 'node:net';
3
- import type {ActivityState, ConnectionStatus, TcpPacket} from '../protocol.js';
4
- import {TCP_DEFAULT_PORT} from '../constants.js';
5
-
6
- type HostOptions = {role: 'host'; localName: string; port?: number};
7
- type ClientOptions = {role: 'client'; localName: string; hostIp: string; tcpPort: number; hostName?: string};
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
+ import net from "node:net";
3
+ import type {
4
+ ActivityState,
5
+ ConnectionStatus,
6
+ TcpPacket,
7
+ } from "../protocol.js";
8
+ import { TCP_DEFAULT_PORT } from "../constants.js";
9
+
10
+ type HostOptions = { role: "host"; localName: string; port?: number };
11
+ type ClientOptions = {
12
+ role: "client";
13
+ localName: string;
14
+ hostIp: string;
15
+ tcpPort: number;
16
+ hostName?: string;
17
+ };
8
18
  type Options = HostOptions | ClientOptions;
9
19
 
10
20
  function writePacket(socket: net.Socket, packet: TcpPacket) {
11
- socket.write(`${JSON.stringify(packet)}\n`, 'utf8');
21
+ socket.write(`${JSON.stringify(packet)}\n`, "utf8");
12
22
  }
13
23
 
14
24
  export function useTcpSync(options: Options): {
15
- status: ConnectionStatus;
16
- listenPort?: number;
17
- peerName?: string;
18
- remoteState?: ActivityState;
19
- sendStatus: (state: ActivityState) => void;
25
+ status: ConnectionStatus;
26
+ listenPort?: number;
27
+ peerName?: string;
28
+ remoteState?: ActivityState;
29
+ sendStatus: (state: ActivityState) => void;
20
30
  } {
21
- const [status, setStatus] = useState<ConnectionStatus>(options.role === 'host' ? 'waiting' : 'connecting');
22
- const [listenPort, setListenPort] = useState<number | undefined>(undefined);
23
- const [peerName, setPeerName] = useState<string | undefined>(undefined);
24
- const [remoteState, setRemoteState] = useState<ActivityState | undefined>(undefined);
25
-
26
- const socketRef = useRef<net.Socket | null>(null);
27
- const lastSeenRef = useRef<number>(Date.now());
28
- const heartbeatRef = useRef<NodeJS.Timeout | null>(null);
29
-
30
- const cleanupSocket = useCallback(() => {
31
- if (heartbeatRef.current) clearInterval(heartbeatRef.current);
32
- heartbeatRef.current = null;
33
-
34
- const s = socketRef.current;
35
- socketRef.current = null;
36
- if (s && !s.destroyed) s.destroy();
37
- }, []);
38
-
39
- const attachSocket = useCallback(
40
- (s: net.Socket) => {
41
- cleanupSocket();
42
- socketRef.current = s;
43
- lastSeenRef.current = Date.now();
44
-
45
- setStatus('connected');
46
- setRemoteState('IDLE');
47
-
48
- let buf = '';
49
- s.setNoDelay(true);
50
- s.setEncoding('utf8');
51
-
52
- const onData = (chunk: string) => {
53
- buf += chunk;
54
- while (true) {
55
- const idx = buf.indexOf('\n');
56
- if (idx === -1) break;
57
- const line = buf.slice(0, idx).trim();
58
- buf = buf.slice(idx + 1);
59
- if (!line) continue;
60
- try {
61
- const packet = JSON.parse(line) as TcpPacket;
62
- lastSeenRef.current = Date.now();
63
- if (packet.type === 'hello') {
64
- if (options.role === 'host') setPeerName(packet.clientName);
65
- else setPeerName(packet.hostName);
66
- }
67
- if (packet.type === 'status') setRemoteState(packet.state);
68
- if (packet.type === 'ping') writePacket(s, {type: 'pong', sentAt: Date.now()});
69
- if (packet.type === 'pong') {
70
- // no-op
71
- }
72
- } catch {
73
- // ignore
74
- }
75
- }
76
- };
77
-
78
- s.on('data', onData);
79
- s.on('close', () => {
80
- setStatus(options.role === 'host' ? 'waiting' : 'disconnected');
81
- setRemoteState('OFFLINE');
82
- cleanupSocket();
83
- });
84
- s.on('error', () => {
85
- setStatus(options.role === 'host' ? 'waiting' : 'disconnected');
86
- setRemoteState('OFFLINE');
87
- });
88
-
89
- // Hello handshake.
90
- writePacket(s, {
91
- type: 'hello',
92
- hostName: options.role === 'host' ? options.localName : options.hostName ?? 'Host',
93
- clientName: options.role === 'client' ? options.localName : 'Client',
94
- sentAt: Date.now()
95
- });
96
-
97
- heartbeatRef.current = setInterval(() => {
98
- const sock = socketRef.current;
99
- if (!sock || sock.destroyed) return;
100
- writePacket(sock, {type: 'ping', sentAt: Date.now()});
101
- const age = Date.now() - lastSeenRef.current;
102
- if (age > 6000) {
103
- setStatus('disconnected');
104
- setRemoteState('OFFLINE');
105
- cleanupSocket();
106
- }
107
- }, 2000);
108
- },
109
- [cleanupSocket, options]
110
- );
111
-
112
- useEffect(() => {
113
- if (options.role === 'host') {
114
- const server = net.createServer((socket) => {
115
- attachSocket(socket);
116
- });
117
-
118
- server.on('error', () => {});
119
-
120
- server.listen(options.port ?? TCP_DEFAULT_PORT, () => {
121
- const address = server.address();
122
- if (address && typeof address === 'object') setListenPort(address.port);
123
- });
124
-
125
- return () => {
126
- cleanupSocket();
127
- server.close();
128
- };
129
- }
130
-
131
- setStatus('connecting');
132
- const socket = net.createConnection({host: options.hostIp, port: options.tcpPort}, () => {
133
- attachSocket(socket);
134
- });
135
- socket.on('error', () => {
136
- setStatus('disconnected');
137
- setRemoteState('OFFLINE');
138
- });
139
-
140
- return () => {
141
- socket.destroy();
142
- cleanupSocket();
143
- };
144
- }, [attachSocket, cleanupSocket, options]);
145
-
146
- const sendStatus = useCallback((state: ActivityState) => {
147
- const socket = socketRef.current;
148
- if (!socket || socket.destroyed) return;
149
- writePacket(socket, {type: 'status', state, sentAt: Date.now()});
150
- }, []);
151
-
152
- return {status, listenPort, peerName, remoteState, sendStatus};
31
+ const [status, setStatus] = useState<ConnectionStatus>(
32
+ options.role === "host" ? "waiting" : "connecting"
33
+ );
34
+ const [listenPort, setListenPort] = useState<number | undefined>(undefined);
35
+ const [peerName, setPeerName] = useState<string | undefined>(undefined);
36
+ const [remoteState, setRemoteState] = useState<ActivityState | undefined>(
37
+ undefined
38
+ );
39
+
40
+ const socketRef = useRef<net.Socket | null>(null);
41
+ const lastSeenRef = useRef<number>(Date.now());
42
+ const heartbeatRef = useRef<NodeJS.Timeout | null>(null);
43
+
44
+ const cleanupSocket = useCallback(() => {
45
+ if (heartbeatRef.current) clearInterval(heartbeatRef.current);
46
+ heartbeatRef.current = null;
47
+
48
+ const s = socketRef.current;
49
+ socketRef.current = null;
50
+ if (s && !s.destroyed) s.destroy();
51
+ }, []);
52
+
53
+ const attachSocket = useCallback(
54
+ (s: net.Socket) => {
55
+ cleanupSocket();
56
+ socketRef.current = s;
57
+ lastSeenRef.current = Date.now();
58
+
59
+ setStatus("connected");
60
+ setRemoteState("IDLE");
61
+
62
+ let buf = "";
63
+ s.setNoDelay(true);
64
+ s.setEncoding("utf8");
65
+
66
+ const onData = (chunk: string) => {
67
+ buf += chunk;
68
+ while (true) {
69
+ const idx = buf.indexOf("\n");
70
+ if (idx === -1) break;
71
+ const line = buf.slice(0, idx).trim();
72
+ buf = buf.slice(idx + 1);
73
+ if (!line) continue;
74
+ try {
75
+ const packet = JSON.parse(line) as TcpPacket;
76
+ lastSeenRef.current = Date.now();
77
+ if (packet.type === "hello") {
78
+ if (options.role === "host") setPeerName(packet.clientName);
79
+ else setPeerName(packet.hostName);
80
+ }
81
+ if (packet.type === "status") setRemoteState(packet.state);
82
+ if (packet.type === "ping")
83
+ writePacket(s, { type: "pong", sentAt: Date.now() });
84
+ if (packet.type === "pong") {
85
+ // no-op
86
+ }
87
+ } catch {
88
+ // ignore
89
+ }
90
+ }
91
+ };
92
+
93
+ s.on("data", onData);
94
+ s.on("close", () => {
95
+ setStatus(options.role === "host" ? "waiting" : "disconnected");
96
+ setRemoteState("OFFLINE");
97
+ cleanupSocket();
98
+ });
99
+ s.on("error", () => {
100
+ setStatus(options.role === "host" ? "waiting" : "disconnected");
101
+ setRemoteState("OFFLINE");
102
+ });
103
+
104
+ // Hello handshake.
105
+ writePacket(s, {
106
+ type: "hello",
107
+ hostName:
108
+ options.role === "host"
109
+ ? options.localName
110
+ : options.hostName ?? "Host",
111
+ clientName: options.role === "client" ? options.localName : "Client",
112
+ sentAt: Date.now(),
113
+ });
114
+
115
+ heartbeatRef.current = setInterval(() => {
116
+ const sock = socketRef.current;
117
+ if (!sock || sock.destroyed) return;
118
+ writePacket(sock, { type: "ping", sentAt: Date.now() });
119
+ const age = Date.now() - lastSeenRef.current;
120
+ if (age > 6000) {
121
+ setStatus("disconnected");
122
+ setRemoteState("OFFLINE");
123
+ cleanupSocket();
124
+ }
125
+ }, 2000);
126
+ },
127
+ [cleanupSocket, options]
128
+ );
129
+
130
+ useEffect(() => {
131
+ if (options.role === "host") {
132
+ const server = net.createServer((socket) => {
133
+ attachSocket(socket);
134
+ });
135
+
136
+ server.on("error", () => {});
137
+
138
+ server.listen(options.port ?? TCP_DEFAULT_PORT, () => {
139
+ const address = server.address();
140
+ if (address && typeof address === "object") setListenPort(address.port);
141
+ });
142
+
143
+ return () => {
144
+ cleanupSocket();
145
+ server.close();
146
+ };
147
+ }
148
+
149
+ setStatus("connecting");
150
+ const socket = net.createConnection(
151
+ { host: options.hostIp, port: options.tcpPort },
152
+ () => {
153
+ attachSocket(socket);
154
+ }
155
+ );
156
+ socket.on("error", () => {
157
+ setStatus("disconnected");
158
+ setRemoteState("OFFLINE");
159
+ });
160
+
161
+ return () => {
162
+ socket.destroy();
163
+ cleanupSocket();
164
+ };
165
+ }, [attachSocket, cleanupSocket, options]);
166
+
167
+ const sendStatus = useCallback((state: ActivityState) => {
168
+ const socket = socketRef.current;
169
+ if (!socket || socket.destroyed) return;
170
+ writePacket(socket, { type: "status", state, sentAt: Date.now() });
171
+ }, []);
172
+
173
+ return { status, listenPort, peerName, remoteState, sendStatus };
153
174
  }
@@ -1,32 +1,32 @@
1
- import os from 'node:os';
1
+ import os from "node:os";
2
2
 
3
3
  function ipv4ToInt(ip: string) {
4
- return ip
5
- .split('.')
6
- .map((n) => Number.parseInt(n, 10))
7
- .reduce((acc, n) => ((acc << 8) | (n & 255)) >>> 0, 0);
4
+ return ip
5
+ .split(".")
6
+ .map((n) => Number.parseInt(n, 10))
7
+ .reduce((acc, n) => ((acc << 8) | (n & 255)) >>> 0, 0);
8
8
  }
9
9
 
10
10
  function intToIpv4(n: number) {
11
- return [24, 16, 8, 0].map((shift) => String((n >>> shift) & 255)).join('.');
11
+ return [24, 16, 8, 0].map((shift) => String((n >>> shift) & 255)).join(".");
12
12
  }
13
13
 
14
14
  export function getBroadcastTargets(): string[] {
15
- const out = new Set<string>(['255.255.255.255']);
15
+ const out = new Set<string>(["255.255.255.255"]);
16
16
 
17
- const ifaces = os.networkInterfaces();
18
- for (const entries of Object.values(ifaces)) {
19
- if (!entries) continue;
20
- for (const e of entries) {
21
- if (e.family !== 'IPv4') continue;
22
- if (e.internal) continue;
23
- if (!e.address || !e.netmask) continue;
24
- const ip = ipv4ToInt(e.address);
25
- const mask = ipv4ToInt(e.netmask);
26
- const broadcast = (ip | (~mask >>> 0)) >>> 0;
27
- out.add(intToIpv4(broadcast));
28
- }
29
- }
17
+ const ifaces = os.networkInterfaces();
18
+ for (const entries of Object.values(ifaces)) {
19
+ if (!entries) continue;
20
+ for (const e of entries) {
21
+ if (e.family !== "IPv4") continue;
22
+ if (e.internal) continue;
23
+ if (!e.address || !e.netmask) continue;
24
+ const ip = ipv4ToInt(e.address);
25
+ const mask = ipv4ToInt(e.netmask);
26
+ const broadcast = (ip | (~mask >>> 0)) >>> 0;
27
+ out.add(intToIpv4(broadcast));
28
+ }
29
+ }
30
30
 
31
- return [...out];
31
+ return [...out];
32
32
  }
package/src/net/index.ts CHANGED
@@ -1,2 +1 @@
1
- export {getBroadcastTargets} from './broadcast.js';
2
-
1
+ export { getBroadcastTargets } from "./broadcast.js";
@@ -0,0 +1,85 @@
1
+ import React, { useMemo } from "react";
2
+ import { Box, Text, useInput } from "ink";
3
+ import type { LeaveStats } from "../types.js";
4
+
5
+ function formatDuration(ms: number) {
6
+ const totalSeconds = Math.max(0, Math.floor(ms / 1000));
7
+ const hours = Math.floor(totalSeconds / 3600);
8
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
9
+ const seconds = totalSeconds % 60;
10
+
11
+ if (hours > 0) return `${hours}小时${minutes}分${seconds}秒`;
12
+ if (minutes > 0) return `${minutes}分${seconds}秒`;
13
+ return `${seconds}秒`;
14
+ }
15
+
16
+ export function LeavePage(props: {
17
+ stats: LeaveStats;
18
+ onBack: () => void;
19
+ onExit: () => void;
20
+ }) {
21
+ useInput((input, key) => {
22
+ if (key.escape || input === "q") props.onExit();
23
+ if (key.return || input === " ") props.onBack();
24
+ });
25
+
26
+ const sessionLabel = useMemo(
27
+ () => formatDuration(props.stats.sessionDurationMs),
28
+ [props.stats.sessionDurationMs]
29
+ );
30
+ const connectedLabel = useMemo(
31
+ () => formatDuration(props.stats.connectedDurationMs),
32
+ [props.stats.connectedDurationMs]
33
+ );
34
+
35
+ return (
36
+ <Box flexDirection="column" padding={1} alignItems="center">
37
+ {/* <Text color="cyan">
38
+ {String.raw`
39
+ ____ _ _
40
+ / ___| ___ ___ | | __ _| |_| |
41
+ \___ \ / _ \ _ \ | | / _' | __| |
42
+ ___) | __/ __/ | |__| (_| | |_|_|
43
+ |____/ \___|\___|___|_____\__,_|\__(_)
44
+ |_____|
45
+ `}
46
+ </Text> */}
47
+
48
+ <Box
49
+ flexDirection="column"
50
+ marginTop={1}
51
+ borderStyle="round"
52
+ paddingX={2}
53
+ borderColor="gray"
54
+ >
55
+ <Text color="white" bold>
56
+ {props.stats.peerName
57
+ ? `与 ${props.stats.peerName} 的同频记录`
58
+ : "本次专注记录"}
59
+ </Text>
60
+
61
+ <Box marginTop={1} flexDirection="column" gap={1}>
62
+ <Box justifyContent="space-between" width={30}>
63
+ <Text>⌨️ 键盘敲击</Text>
64
+ <Text color="yellow">{props.stats.keyPresses}</Text>
65
+ </Box>
66
+ <Box justifyContent="space-between" width={30}>
67
+ <Text>⏱️ 总共时长</Text>
68
+ <Text color="green">{sessionLabel}</Text>
69
+ </Box>
70
+ <Box justifyContent="space-between" width={30}>
71
+ <Text>🔗 连线时长</Text>
72
+ <Text color="blue">{connectedLabel}</Text>
73
+ </Box>
74
+ </Box>
75
+ </Box>
76
+
77
+ <Box marginTop={1}>
78
+ <Text color="gray">
79
+ 按 <Text color="white">Enter</Text> 返回菜单,或{" "}
80
+ <Text color="red">q</Text> 退出程序
81
+ </Text>
82
+ </Box>
83
+ </Box>
84
+ );
85
+ }
@@ -1,17 +1,21 @@
1
- import React from 'react';
2
- import {Box, Text, useInput} from 'ink';
1
+ import React from "react";
2
+ import { Box, Text, useInput } from "ink";
3
3
 
4
- export function MainMenu(props: {onHost: () => void; onJoin: () => void; onExit: () => void}) {
5
- useInput((input, key) => {
6
- if (key.escape || input === 'q') props.onExit();
7
- if (input === '1') props.onHost();
8
- if (input === '2') props.onJoin();
9
- });
4
+ export function MainMenu(props: {
5
+ onHost: () => void;
6
+ onJoin: () => void;
7
+ onExit: () => void;
8
+ }) {
9
+ useInput((input, key) => {
10
+ if (key.escape || input === "q") props.onExit();
11
+ if (input === "1") props.onHost();
12
+ if (input === "2") props.onJoin();
13
+ });
10
14
 
11
- return (
12
- <Box flexDirection="column" padding={1}>
13
- <Text>
14
- {String.raw`
15
+ return (
16
+ <Box flexDirection="column" padding={1}>
17
+ <Text>
18
+ {String.raw`
15
19
  ████████╗███████╗██████╗ ███╗ ███╗██████╗ ██╗ ██╗██████╗ ██████╗ ██╗ ██╗
16
20
  ╚══██╔══╝██╔════╝██╔══██╗████╗ ████║██╔══██╗██║ ██║██╔══██╗██╔══██╗╚██╗ ██╔╝
17
21
  ██║ █████╗ ██████╔╝██╔████╔██║██████╔╝██║ ██║██║ ██║██║ ██║ ╚████╔╝
@@ -19,20 +23,20 @@ export function MainMenu(props: {onHost: () => void; onJoin: () => void; onExit:
19
23
  ██║ ███████╗██║ ██║██║ ╚═╝ ██║██████╔╝╚██████╔╝██████╔╝██████╔╝ ██║
20
24
  ╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚═╝
21
25
  `}
22
- </Text>
23
- <Box flexDirection="column" marginTop={1}>
24
- <Text>Terminal Body Doubling — 极简 / 极客 / 私密</Text>
25
- <Text> </Text>
26
- <Text>
27
- <Text color="cyan">[1]</Text> 建房 (Host)
28
- </Text>
29
- <Text>
30
- <Text color="cyan">[2]</Text> 加入 (Join)
31
- </Text>
32
- <Text>
33
- <Text color="cyan">[q]</Text> 退出
34
- </Text>
35
- </Box>
36
- </Box>
37
- );
26
+ </Text>
27
+ <Box flexDirection="column" marginTop={1}>
28
+ <Text>Terminal Body Doubling — 极简 / 极客 / 私密</Text>
29
+ <Text> </Text>
30
+ <Text>
31
+ <Text color="cyan">[1]</Text> 建房 (Host)
32
+ </Text>
33
+ <Text>
34
+ <Text color="cyan">[2]</Text> 加入 (Join)
35
+ </Text>
36
+ <Text>
37
+ <Text color="cyan">[q]</Text> 退出
38
+ </Text>
39
+ </Box>
40
+ </Box>
41
+ );
38
42
  }
@@ -0,0 +1,62 @@
1
+ import React, { useMemo, useState } from "react";
2
+ import os from "node:os";
3
+ import { Box, Text, useInput } from "ink";
4
+
5
+ function defaultNick(): string {
6
+ try {
7
+ return os.userInfo().username || os.hostname();
8
+ } catch {
9
+ return os.hostname();
10
+ }
11
+ }
12
+
13
+ export function NicknamePrompt(props: {
14
+ onSubmit: (nickname: string) => void;
15
+ onExit: () => void;
16
+ }) {
17
+ const initial = useMemo(() => defaultNick(), []);
18
+ const [nickname, setNickname] = useState(initial);
19
+ const [touched, setTouched] = useState(false);
20
+
21
+ useInput((input, key) => {
22
+ if (key.escape) props.onExit();
23
+
24
+ if (key.return) {
25
+ const name = nickname.trim();
26
+ if (!name) return;
27
+ props.onSubmit(name);
28
+ return;
29
+ }
30
+
31
+ if (key.backspace || key.delete) {
32
+ setTouched(true);
33
+ setNickname((v) => v.slice(0, -1));
34
+ return;
35
+ }
36
+
37
+ if (key.ctrl || key.meta) return;
38
+ if (!input) return;
39
+ if (input === "\t") return;
40
+
41
+ setTouched(true);
42
+ setNickname((v) => v + input);
43
+ });
44
+
45
+ const hint = touched ? "" : " (回车确认,可直接用默认值)";
46
+
47
+ return (
48
+ <Box flexDirection="column" padding={1}>
49
+ <Text color="gray">欢迎来到 TermBuddy</Text>
50
+ <Box marginTop={1}>
51
+ <Text>
52
+ 请输入你的昵称:<Text color="cyan">{nickname || ""}</Text>
53
+ <Text color="gray">{hint}</Text>
54
+ </Text>
55
+ </Box>
56
+ <Box marginTop={1}>
57
+ <Text color="gray">按 Esc 退出。</Text>
58
+ </Box>
59
+ </Box>
60
+ );
61
+ }
62
+
@@ -37,7 +37,10 @@ export function RoomScanner(props: {
37
37
  ) : (
38
38
  sortedRooms.map((room, i) => (
39
39
  <Text key={`${room.ip}:${room.tcpPort}`}>
40
- <Text color="cyan">[{i + 1}]</Text> {room.roomName} {room.hostName} @ {room.ip}:{room.tcpPort}
40
+ <Text color="cyan">[{i + 1}]</Text> {room.hostName} <Text color="gray">@</Text>{' '}
41
+ <Text color="gray">
42
+ {room.ip}:{room.tcpPort}
43
+ </Text>
41
44
  </Text>
42
45
  ))
43
46
  )}