@three333/termbuddy 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/dist/cli.js +957 -0
- package/dist/cli.js.map +1 -0
- package/package.json +28 -0
- package/src/app/App.tsx +64 -0
- package/src/app/index.ts +2 -0
- package/src/cli.tsx +7 -0
- package/src/components/AiConsole.tsx +77 -0
- package/src/components/AvatarDisplay.tsx +18 -0
- package/src/components/BuddyAvatar.tsx +32 -0
- package/src/components/StatusHeader.tsx +43 -0
- package/src/components/index.ts +4 -0
- package/src/constants.ts +5 -0
- package/src/hooks/index.ts +7 -0
- package/src/hooks/useActivityMonitor.ts +25 -0
- package/src/hooks/useAiAgent.ts +177 -0
- package/src/hooks/useBroadcaster.ts +52 -0
- package/src/hooks/useCountdown.ts +42 -0
- package/src/hooks/useScanner.ts +60 -0
- package/src/hooks/useTcpSync.ts +153 -0
- package/src/net/broadcast.ts +32 -0
- package/src/net/index.ts +2 -0
- package/src/protocol.ts +18 -0
- package/src/types.ts +8 -0
- package/src/views/MainMenu.tsx +38 -0
- package/src/views/RoomScanner.tsx +47 -0
- package/src/views/Session.tsx +127 -0
- package/src/views/index.ts +4 -0
- package/tsconfig.json +14 -0
- package/tsup.config.ts +15 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,957 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// src/views/MainMenu.tsx
|
|
13
|
+
import { Box, Text, useInput } from "ink";
|
|
14
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
15
|
+
function MainMenu(props) {
|
|
16
|
+
useInput((input, key) => {
|
|
17
|
+
if (key.escape || input === "q") props.onExit();
|
|
18
|
+
if (input === "1") props.onHost();
|
|
19
|
+
if (input === "2") props.onJoin();
|
|
20
|
+
});
|
|
21
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", padding: 1, children: [
|
|
22
|
+
/* @__PURE__ */ jsx(Text, { children: String.raw`
|
|
23
|
+
████████╗███████╗██████╗ ███╗ ███╗██████╗ ██╗ ██╗██████╗ ██████╗ ██╗ ██╗
|
|
24
|
+
╚══██╔══╝██╔════╝██╔══██╗████╗ ████║██╔══██╗██║ ██║██╔══██╗██╔══██╗╚██╗ ██╔╝
|
|
25
|
+
██║ █████╗ ██████╔╝██╔████╔██║██████╔╝██║ ██║██║ ██║██║ ██║ ╚████╔╝
|
|
26
|
+
██║ ██╔══╝ ██╔══██╗██║╚██╔╝██║██╔══██╗██║ ██║██║ ██║██║ ██║ ╚██╔╝
|
|
27
|
+
██║ ███████╗██║ ██║██║ ╚═╝ ██║██████╔╝╚██████╔╝██████╔╝██████╔╝ ██║
|
|
28
|
+
╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚═╝
|
|
29
|
+
` }),
|
|
30
|
+
/* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
|
|
31
|
+
/* @__PURE__ */ jsx(Text, { children: "Terminal Body Doubling \u2014 \u6781\u7B80 / \u6781\u5BA2 / \u79C1\u5BC6" }),
|
|
32
|
+
/* @__PURE__ */ jsx(Text, { children: " " }),
|
|
33
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
34
|
+
/* @__PURE__ */ jsx(Text, { color: "cyan", children: "[1]" }),
|
|
35
|
+
" \u5EFA\u623F (Host)"
|
|
36
|
+
] }),
|
|
37
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
38
|
+
/* @__PURE__ */ jsx(Text, { color: "cyan", children: "[2]" }),
|
|
39
|
+
" \u52A0\u5165 (Join)"
|
|
40
|
+
] }),
|
|
41
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
42
|
+
/* @__PURE__ */ jsx(Text, { color: "cyan", children: "[q]" }),
|
|
43
|
+
" \u9000\u51FA"
|
|
44
|
+
] })
|
|
45
|
+
] })
|
|
46
|
+
] });
|
|
47
|
+
}
|
|
48
|
+
var init_MainMenu = __esm({
|
|
49
|
+
"src/views/MainMenu.tsx"() {
|
|
50
|
+
"use strict";
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// src/hooks/useActivityMonitor.ts
|
|
55
|
+
import { useEffect, useRef, useState } from "react";
|
|
56
|
+
import { useInput as useInput2 } from "ink";
|
|
57
|
+
function useActivityMonitor(options) {
|
|
58
|
+
const idleAfterMs = options?.idleAfterMs ?? 1500;
|
|
59
|
+
const [state, setState] = useState("IDLE");
|
|
60
|
+
const lastActivityRef = useRef(Date.now());
|
|
61
|
+
useInput2(() => {
|
|
62
|
+
lastActivityRef.current = Date.now();
|
|
63
|
+
setState("TYPING");
|
|
64
|
+
});
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
const id = setInterval(() => {
|
|
67
|
+
const delta = Date.now() - lastActivityRef.current;
|
|
68
|
+
if (delta >= idleAfterMs) setState("IDLE");
|
|
69
|
+
}, 200);
|
|
70
|
+
return () => clearInterval(id);
|
|
71
|
+
}, [idleAfterMs]);
|
|
72
|
+
return { state };
|
|
73
|
+
}
|
|
74
|
+
var init_useActivityMonitor = __esm({
|
|
75
|
+
"src/hooks/useActivityMonitor.ts"() {
|
|
76
|
+
"use strict";
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// src/hooks/useAiAgent.ts
|
|
81
|
+
import { useCallback, useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
|
|
82
|
+
import { createAgent, initChatModel, tool } from "langchain";
|
|
83
|
+
function contentToText(content) {
|
|
84
|
+
if (typeof content === "string") return content;
|
|
85
|
+
if (!content) return "";
|
|
86
|
+
if (Array.isArray(content)) {
|
|
87
|
+
return content.map((part) => {
|
|
88
|
+
if (typeof part === "string") return part;
|
|
89
|
+
if (typeof part === "object" && part && "text" in part) return String(part.text ?? "");
|
|
90
|
+
return "";
|
|
91
|
+
}).join("");
|
|
92
|
+
}
|
|
93
|
+
if (typeof content === "object" && "text" in content) return String(content.text ?? "");
|
|
94
|
+
return String(content);
|
|
95
|
+
}
|
|
96
|
+
function lastAiText(messages) {
|
|
97
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
98
|
+
const m = messages[i];
|
|
99
|
+
const type = typeof m?.getType === "function" ? m.getType() : typeof m?._getType === "function" ? m._getType() : m?.type;
|
|
100
|
+
if (type === "ai") {
|
|
101
|
+
const t = contentToText(m?.content);
|
|
102
|
+
return t || "";
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
function createSystemPrompt(context) {
|
|
108
|
+
return [
|
|
109
|
+
"\u4F60\u662F TermBuddy \u91CC\u7684\u201C\u58F3\u4E2D\u5E7D\u7075 (Ghost in the Shell)\u201D\u3002",
|
|
110
|
+
"\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",
|
|
111
|
+
"\u4F60\u53EF\u4EE5\u4F7F\u7528\u5DE5\u5177\u6765\u64CD\u63A7\u5E94\u7528\u529F\u80FD\uFF08\u4F8B\u5982\u5012\u8BA1\u65F6\uFF09\u3002",
|
|
112
|
+
"\u5982\u679C\u7528\u6237\u63D0\u5230\u201C\u5012\u8BA1\u65F6/\u4E13\u6CE8/\u8BA1\u65F6/countdown\u201D\uFF0C\u4F18\u5148\u8C03\u7528 start_countdown\u3002",
|
|
113
|
+
`\u5F53\u524D\u4E0A\u4E0B\u6587\uFF1A\u6211\u53EB ${context.localName}\uFF1B\u540C\u684C\u53EB ${context.peerName}\u3002`
|
|
114
|
+
].join("\n");
|
|
115
|
+
}
|
|
116
|
+
function useAiAgent(options) {
|
|
117
|
+
const [lines, setLines] = useState2([]);
|
|
118
|
+
const [busy, setBusy] = useState2(false);
|
|
119
|
+
const agentRef = useRef2(null);
|
|
120
|
+
const agentInitRef = useRef2(null);
|
|
121
|
+
const stateRef = useRef2({ messages: [] });
|
|
122
|
+
const abortRef = useRef2(null);
|
|
123
|
+
const append = useCallback((line) => {
|
|
124
|
+
setLines((prev) => [...prev, line]);
|
|
125
|
+
}, []);
|
|
126
|
+
const updateLine = useCallback((at, text) => {
|
|
127
|
+
setLines((prev) => {
|
|
128
|
+
const idx = prev.findIndex((l) => l.at === at);
|
|
129
|
+
if (idx === -1) return prev;
|
|
130
|
+
const next = [...prev];
|
|
131
|
+
next[idx] = { ...next[idx], text };
|
|
132
|
+
return next;
|
|
133
|
+
});
|
|
134
|
+
}, []);
|
|
135
|
+
const ensureAgent = useCallback(async () => {
|
|
136
|
+
if (agentRef.current) return agentRef.current;
|
|
137
|
+
agentInitRef.current ??= (async () => {
|
|
138
|
+
const startCountdown = tool(
|
|
139
|
+
async (input) => {
|
|
140
|
+
const minutes = Number(input.minutes);
|
|
141
|
+
if (!Number.isFinite(minutes) || minutes <= 0) return "\u5012\u8BA1\u65F6\u5206\u949F\u6570\u65E0\u6548\u3002";
|
|
142
|
+
options.onStartCountdown?.(minutes);
|
|
143
|
+
return `\u5DF2\u5F00\u59CB\u5012\u8BA1\u65F6 ${minutes} \u5206\u949F\u3002`;
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
name: "start_countdown",
|
|
147
|
+
description: "\u5F00\u59CB\u4E00\u4E2A\u4E13\u6CE8\u5012\u8BA1\u65F6\uFF08\u5206\u949F\uFF09\u3002",
|
|
148
|
+
schema: {
|
|
149
|
+
type: "object",
|
|
150
|
+
properties: {
|
|
151
|
+
minutes: { type: "integer", minimum: 1, maximum: 180, description: "\u5012\u8BA1\u65F6\u5206\u949F\u6570" }
|
|
152
|
+
},
|
|
153
|
+
required: ["minutes"],
|
|
154
|
+
additionalProperties: false
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
);
|
|
158
|
+
const sessionInfo = tool(
|
|
159
|
+
async () => {
|
|
160
|
+
return JSON.stringify(
|
|
161
|
+
{
|
|
162
|
+
localName: options.localName,
|
|
163
|
+
peerName: options.peerName
|
|
164
|
+
},
|
|
165
|
+
null,
|
|
166
|
+
2
|
|
167
|
+
);
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
name: "session_info",
|
|
171
|
+
description: "\u83B7\u53D6\u5F53\u524D\u4F1A\u8BDD\u4E0A\u4E0B\u6587\uFF08\u672C\u5730\u6635\u79F0\u3001\u540C\u684C\u6635\u79F0\uFF09\u3002",
|
|
172
|
+
schema: { type: "object", properties: {}, additionalProperties: false }
|
|
173
|
+
}
|
|
174
|
+
);
|
|
175
|
+
const modelId = process.env.TERMBUDDY_MODEL ?? "openai:gpt-4o-mini";
|
|
176
|
+
const llm = await initChatModel(modelId, {
|
|
177
|
+
temperature: 0.2,
|
|
178
|
+
maxTokens: 800,
|
|
179
|
+
timeout: 3e4
|
|
180
|
+
});
|
|
181
|
+
return createAgent({
|
|
182
|
+
llm,
|
|
183
|
+
tools: [startCountdown, sessionInfo],
|
|
184
|
+
prompt: createSystemPrompt({ localName: options.localName, peerName: options.peerName }),
|
|
185
|
+
name: "ghost"
|
|
186
|
+
});
|
|
187
|
+
})();
|
|
188
|
+
agentRef.current = await agentInitRef.current;
|
|
189
|
+
return agentRef.current;
|
|
190
|
+
}, [options.localName, options.onStartCountdown, options.peerName]);
|
|
191
|
+
const ask = useCallback(
|
|
192
|
+
async (text) => {
|
|
193
|
+
append({ kind: "user", text: `> ${text}`, at: Date.now() });
|
|
194
|
+
const aiAt = Date.now() + 1;
|
|
195
|
+
append({ kind: "ai", text: "\u2026", at: aiAt });
|
|
196
|
+
abortRef.current?.abort();
|
|
197
|
+
abortRef.current = new AbortController();
|
|
198
|
+
setBusy(true);
|
|
199
|
+
try {
|
|
200
|
+
const agent = await ensureAgent();
|
|
201
|
+
const stream = await agent.stream(
|
|
202
|
+
{
|
|
203
|
+
messages: [...stateRef.current.messages, { role: "user", content: text }]
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
streamMode: "values",
|
|
207
|
+
signal: abortRef.current.signal
|
|
208
|
+
}
|
|
209
|
+
);
|
|
210
|
+
for await (const chunk of stream) {
|
|
211
|
+
const messages = chunk?.messages ?? [];
|
|
212
|
+
if (messages.length > 0) stateRef.current.messages = messages;
|
|
213
|
+
const t = lastAiText(messages);
|
|
214
|
+
if (t !== null) updateLine(aiAt, t);
|
|
215
|
+
}
|
|
216
|
+
} catch (e) {
|
|
217
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
218
|
+
updateLine(aiAt, `\uFF08AI \u51FA\u9519\uFF09${msg}`);
|
|
219
|
+
} finally {
|
|
220
|
+
setBusy(false);
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
[append, ensureAgent, updateLine]
|
|
224
|
+
);
|
|
225
|
+
useEffect2(() => {
|
|
226
|
+
return () => abortRef.current?.abort();
|
|
227
|
+
}, []);
|
|
228
|
+
return { lines, ask, busy };
|
|
229
|
+
}
|
|
230
|
+
var init_useAiAgent = __esm({
|
|
231
|
+
"src/hooks/useAiAgent.ts"() {
|
|
232
|
+
"use strict";
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// src/constants.ts
|
|
237
|
+
var UDP_PORT, TCP_DEFAULT_PORT, DISCOVERY_VERSION;
|
|
238
|
+
var init_constants = __esm({
|
|
239
|
+
"src/constants.ts"() {
|
|
240
|
+
"use strict";
|
|
241
|
+
UDP_PORT = 45888;
|
|
242
|
+
TCP_DEFAULT_PORT = 45999;
|
|
243
|
+
DISCOVERY_VERSION = 1;
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// src/net/broadcast.ts
|
|
248
|
+
import os from "os";
|
|
249
|
+
function ipv4ToInt(ip) {
|
|
250
|
+
return ip.split(".").map((n) => Number.parseInt(n, 10)).reduce((acc, n) => (acc << 8 | n & 255) >>> 0, 0);
|
|
251
|
+
}
|
|
252
|
+
function intToIpv4(n) {
|
|
253
|
+
return [24, 16, 8, 0].map((shift) => String(n >>> shift & 255)).join(".");
|
|
254
|
+
}
|
|
255
|
+
function getBroadcastTargets() {
|
|
256
|
+
const out = /* @__PURE__ */ new Set(["255.255.255.255"]);
|
|
257
|
+
const ifaces = os.networkInterfaces();
|
|
258
|
+
for (const entries of Object.values(ifaces)) {
|
|
259
|
+
if (!entries) continue;
|
|
260
|
+
for (const e of entries) {
|
|
261
|
+
if (e.family !== "IPv4") continue;
|
|
262
|
+
if (e.internal) continue;
|
|
263
|
+
if (!e.address || !e.netmask) continue;
|
|
264
|
+
const ip = ipv4ToInt(e.address);
|
|
265
|
+
const mask = ipv4ToInt(e.netmask);
|
|
266
|
+
const broadcast = (ip | ~mask >>> 0) >>> 0;
|
|
267
|
+
out.add(intToIpv4(broadcast));
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return [...out];
|
|
271
|
+
}
|
|
272
|
+
var init_broadcast = __esm({
|
|
273
|
+
"src/net/broadcast.ts"() {
|
|
274
|
+
"use strict";
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// src/net/index.ts
|
|
279
|
+
var init_net = __esm({
|
|
280
|
+
"src/net/index.ts"() {
|
|
281
|
+
"use strict";
|
|
282
|
+
init_broadcast();
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// src/hooks/useBroadcaster.ts
|
|
287
|
+
import { useEffect as useEffect3 } from "react";
|
|
288
|
+
import dgram from "dgram";
|
|
289
|
+
function useBroadcaster(options) {
|
|
290
|
+
const depKey = options.enabled ? `${options.hostName}|${options.roomName}|${options.tcpPort ?? ""}|${options.intervalMs ?? 1e3}` : "disabled";
|
|
291
|
+
useEffect3(() => {
|
|
292
|
+
if (!options.enabled) return;
|
|
293
|
+
if (!options.tcpPort) return;
|
|
294
|
+
const socket = dgram.createSocket("udp4");
|
|
295
|
+
socket.on("error", () => {
|
|
296
|
+
});
|
|
297
|
+
socket.bind(() => {
|
|
298
|
+
socket.setBroadcast(true);
|
|
299
|
+
});
|
|
300
|
+
const targets = getBroadcastTargets();
|
|
301
|
+
const send = () => {
|
|
302
|
+
const packet = {
|
|
303
|
+
type: "termbuddy_discovery",
|
|
304
|
+
version: DISCOVERY_VERSION,
|
|
305
|
+
hostName: options.hostName,
|
|
306
|
+
roomName: options.roomName,
|
|
307
|
+
tcpPort: options.tcpPort,
|
|
308
|
+
sentAt: Date.now()
|
|
309
|
+
};
|
|
310
|
+
const msg = Buffer.from(JSON.stringify(packet));
|
|
311
|
+
for (const address of targets) {
|
|
312
|
+
socket.send(msg, UDP_PORT, address);
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
send();
|
|
316
|
+
const id = setInterval(send, options.intervalMs ?? 1e3);
|
|
317
|
+
return () => {
|
|
318
|
+
clearInterval(id);
|
|
319
|
+
socket.close();
|
|
320
|
+
};
|
|
321
|
+
}, [depKey]);
|
|
322
|
+
}
|
|
323
|
+
var init_useBroadcaster = __esm({
|
|
324
|
+
"src/hooks/useBroadcaster.ts"() {
|
|
325
|
+
"use strict";
|
|
326
|
+
init_constants();
|
|
327
|
+
init_net();
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
// src/hooks/useCountdown.ts
|
|
332
|
+
import { useCallback as useCallback2, useEffect as useEffect4, useRef as useRef3, useState as useState3 } from "react";
|
|
333
|
+
function formatMMSS(totalSeconds) {
|
|
334
|
+
const m = Math.floor(totalSeconds / 60);
|
|
335
|
+
const s = totalSeconds % 60;
|
|
336
|
+
return `${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
|
|
337
|
+
}
|
|
338
|
+
function useCountdown() {
|
|
339
|
+
const [remainingSeconds, setRemainingSeconds] = useState3(null);
|
|
340
|
+
const timerRef = useRef3(null);
|
|
341
|
+
const start = useCallback2((minutes) => {
|
|
342
|
+
const seconds = Math.max(1, Math.floor(minutes * 60));
|
|
343
|
+
setRemainingSeconds(seconds);
|
|
344
|
+
if (timerRef.current) clearInterval(timerRef.current);
|
|
345
|
+
timerRef.current = setInterval(() => {
|
|
346
|
+
setRemainingSeconds((prev) => {
|
|
347
|
+
if (prev === null) return null;
|
|
348
|
+
if (prev <= 1) return null;
|
|
349
|
+
return prev - 1;
|
|
350
|
+
});
|
|
351
|
+
}, 1e3);
|
|
352
|
+
}, []);
|
|
353
|
+
useEffect4(() => {
|
|
354
|
+
if (remainingSeconds !== null) return;
|
|
355
|
+
if (timerRef.current) clearInterval(timerRef.current);
|
|
356
|
+
timerRef.current = null;
|
|
357
|
+
}, [remainingSeconds]);
|
|
358
|
+
useEffect4(() => {
|
|
359
|
+
return () => {
|
|
360
|
+
if (timerRef.current) clearInterval(timerRef.current);
|
|
361
|
+
};
|
|
362
|
+
}, []);
|
|
363
|
+
return {
|
|
364
|
+
start,
|
|
365
|
+
label: remainingSeconds === null ? null : formatMMSS(remainingSeconds)
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
var init_useCountdown = __esm({
|
|
369
|
+
"src/hooks/useCountdown.ts"() {
|
|
370
|
+
"use strict";
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
// src/hooks/useScanner.ts
|
|
375
|
+
import { useEffect as useEffect5, useState as useState4 } from "react";
|
|
376
|
+
import dgram2 from "dgram";
|
|
377
|
+
function safeParse(msg) {
|
|
378
|
+
try {
|
|
379
|
+
const parsed = JSON.parse(msg.toString("utf8"));
|
|
380
|
+
if (parsed?.type !== "termbuddy_discovery") return null;
|
|
381
|
+
if (parsed?.version !== DISCOVERY_VERSION) return null;
|
|
382
|
+
if (!parsed.hostName || !parsed.roomName || !parsed.tcpPort) return null;
|
|
383
|
+
return parsed;
|
|
384
|
+
} catch {
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
function useScanner(options) {
|
|
389
|
+
const staleAfterMs = options?.staleAfterMs ?? 3500;
|
|
390
|
+
const [rooms, setRooms] = useState4([]);
|
|
391
|
+
useEffect5(() => {
|
|
392
|
+
const socket = dgram2.createSocket("udp4");
|
|
393
|
+
socket.on("error", () => {
|
|
394
|
+
});
|
|
395
|
+
socket.on("message", (msg, rinfo) => {
|
|
396
|
+
const packet = safeParse(msg);
|
|
397
|
+
if (!packet) return;
|
|
398
|
+
const now = Date.now();
|
|
399
|
+
setRooms((prev) => {
|
|
400
|
+
const key = `${rinfo.address}:${packet.tcpPort}`;
|
|
401
|
+
const next = prev.filter((r) => `${r.ip}:${r.tcpPort}` !== key);
|
|
402
|
+
next.push({
|
|
403
|
+
ip: rinfo.address,
|
|
404
|
+
hostName: packet.hostName,
|
|
405
|
+
roomName: packet.roomName,
|
|
406
|
+
tcpPort: packet.tcpPort,
|
|
407
|
+
lastSeenAt: now
|
|
408
|
+
});
|
|
409
|
+
return next;
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
socket.bind(UDP_PORT, () => {
|
|
413
|
+
});
|
|
414
|
+
const prune = setInterval(() => {
|
|
415
|
+
const now = Date.now();
|
|
416
|
+
setRooms((prev) => prev.filter((r) => now - r.lastSeenAt <= staleAfterMs));
|
|
417
|
+
}, 500);
|
|
418
|
+
return () => {
|
|
419
|
+
clearInterval(prune);
|
|
420
|
+
socket.close();
|
|
421
|
+
};
|
|
422
|
+
}, [staleAfterMs]);
|
|
423
|
+
return rooms;
|
|
424
|
+
}
|
|
425
|
+
var init_useScanner = __esm({
|
|
426
|
+
"src/hooks/useScanner.ts"() {
|
|
427
|
+
"use strict";
|
|
428
|
+
init_constants();
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
// src/hooks/useTcpSync.ts
|
|
433
|
+
import { useCallback as useCallback3, useEffect as useEffect6, useRef as useRef4, useState as useState5 } from "react";
|
|
434
|
+
import net from "net";
|
|
435
|
+
function writePacket(socket, packet) {
|
|
436
|
+
socket.write(`${JSON.stringify(packet)}
|
|
437
|
+
`, "utf8");
|
|
438
|
+
}
|
|
439
|
+
function useTcpSync(options) {
|
|
440
|
+
const [status, setStatus] = useState5(options.role === "host" ? "waiting" : "connecting");
|
|
441
|
+
const [listenPort, setListenPort] = useState5(void 0);
|
|
442
|
+
const [peerName, setPeerName] = useState5(void 0);
|
|
443
|
+
const [remoteState, setRemoteState] = useState5(void 0);
|
|
444
|
+
const socketRef = useRef4(null);
|
|
445
|
+
const lastSeenRef = useRef4(Date.now());
|
|
446
|
+
const heartbeatRef = useRef4(null);
|
|
447
|
+
const cleanupSocket = useCallback3(() => {
|
|
448
|
+
if (heartbeatRef.current) clearInterval(heartbeatRef.current);
|
|
449
|
+
heartbeatRef.current = null;
|
|
450
|
+
const s = socketRef.current;
|
|
451
|
+
socketRef.current = null;
|
|
452
|
+
if (s && !s.destroyed) s.destroy();
|
|
453
|
+
}, []);
|
|
454
|
+
const attachSocket = useCallback3(
|
|
455
|
+
(s) => {
|
|
456
|
+
cleanupSocket();
|
|
457
|
+
socketRef.current = s;
|
|
458
|
+
lastSeenRef.current = Date.now();
|
|
459
|
+
setStatus("connected");
|
|
460
|
+
setRemoteState("IDLE");
|
|
461
|
+
let buf = "";
|
|
462
|
+
s.setNoDelay(true);
|
|
463
|
+
s.setEncoding("utf8");
|
|
464
|
+
const onData = (chunk) => {
|
|
465
|
+
buf += chunk;
|
|
466
|
+
while (true) {
|
|
467
|
+
const idx = buf.indexOf("\n");
|
|
468
|
+
if (idx === -1) break;
|
|
469
|
+
const line = buf.slice(0, idx).trim();
|
|
470
|
+
buf = buf.slice(idx + 1);
|
|
471
|
+
if (!line) continue;
|
|
472
|
+
try {
|
|
473
|
+
const packet = JSON.parse(line);
|
|
474
|
+
lastSeenRef.current = Date.now();
|
|
475
|
+
if (packet.type === "hello") {
|
|
476
|
+
if (options.role === "host") setPeerName(packet.clientName);
|
|
477
|
+
else setPeerName(packet.hostName);
|
|
478
|
+
}
|
|
479
|
+
if (packet.type === "status") setRemoteState(packet.state);
|
|
480
|
+
if (packet.type === "ping") writePacket(s, { type: "pong", sentAt: Date.now() });
|
|
481
|
+
if (packet.type === "pong") {
|
|
482
|
+
}
|
|
483
|
+
} catch {
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
s.on("data", onData);
|
|
488
|
+
s.on("close", () => {
|
|
489
|
+
setStatus(options.role === "host" ? "waiting" : "disconnected");
|
|
490
|
+
setRemoteState("OFFLINE");
|
|
491
|
+
cleanupSocket();
|
|
492
|
+
});
|
|
493
|
+
s.on("error", () => {
|
|
494
|
+
setStatus(options.role === "host" ? "waiting" : "disconnected");
|
|
495
|
+
setRemoteState("OFFLINE");
|
|
496
|
+
});
|
|
497
|
+
writePacket(s, {
|
|
498
|
+
type: "hello",
|
|
499
|
+
hostName: options.role === "host" ? options.localName : options.hostName ?? "Host",
|
|
500
|
+
clientName: options.role === "client" ? options.localName : "Client",
|
|
501
|
+
sentAt: Date.now()
|
|
502
|
+
});
|
|
503
|
+
heartbeatRef.current = setInterval(() => {
|
|
504
|
+
const sock = socketRef.current;
|
|
505
|
+
if (!sock || sock.destroyed) return;
|
|
506
|
+
writePacket(sock, { type: "ping", sentAt: Date.now() });
|
|
507
|
+
const age = Date.now() - lastSeenRef.current;
|
|
508
|
+
if (age > 6e3) {
|
|
509
|
+
setStatus("disconnected");
|
|
510
|
+
setRemoteState("OFFLINE");
|
|
511
|
+
cleanupSocket();
|
|
512
|
+
}
|
|
513
|
+
}, 2e3);
|
|
514
|
+
},
|
|
515
|
+
[cleanupSocket, options]
|
|
516
|
+
);
|
|
517
|
+
useEffect6(() => {
|
|
518
|
+
if (options.role === "host") {
|
|
519
|
+
const server = net.createServer((socket2) => {
|
|
520
|
+
attachSocket(socket2);
|
|
521
|
+
});
|
|
522
|
+
server.on("error", () => {
|
|
523
|
+
});
|
|
524
|
+
server.listen(options.port ?? TCP_DEFAULT_PORT, () => {
|
|
525
|
+
const address = server.address();
|
|
526
|
+
if (address && typeof address === "object") setListenPort(address.port);
|
|
527
|
+
});
|
|
528
|
+
return () => {
|
|
529
|
+
cleanupSocket();
|
|
530
|
+
server.close();
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
setStatus("connecting");
|
|
534
|
+
const socket = net.createConnection({ host: options.hostIp, port: options.tcpPort }, () => {
|
|
535
|
+
attachSocket(socket);
|
|
536
|
+
});
|
|
537
|
+
socket.on("error", () => {
|
|
538
|
+
setStatus("disconnected");
|
|
539
|
+
setRemoteState("OFFLINE");
|
|
540
|
+
});
|
|
541
|
+
return () => {
|
|
542
|
+
socket.destroy();
|
|
543
|
+
cleanupSocket();
|
|
544
|
+
};
|
|
545
|
+
}, [attachSocket, cleanupSocket, options]);
|
|
546
|
+
const sendStatus = useCallback3((state) => {
|
|
547
|
+
const socket = socketRef.current;
|
|
548
|
+
if (!socket || socket.destroyed) return;
|
|
549
|
+
writePacket(socket, { type: "status", state, sentAt: Date.now() });
|
|
550
|
+
}, []);
|
|
551
|
+
return { status, listenPort, peerName, remoteState, sendStatus };
|
|
552
|
+
}
|
|
553
|
+
var init_useTcpSync = __esm({
|
|
554
|
+
"src/hooks/useTcpSync.ts"() {
|
|
555
|
+
"use strict";
|
|
556
|
+
init_constants();
|
|
557
|
+
}
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
// src/hooks/index.ts
|
|
561
|
+
var init_hooks = __esm({
|
|
562
|
+
"src/hooks/index.ts"() {
|
|
563
|
+
"use strict";
|
|
564
|
+
init_useActivityMonitor();
|
|
565
|
+
init_useAiAgent();
|
|
566
|
+
init_useBroadcaster();
|
|
567
|
+
init_useCountdown();
|
|
568
|
+
init_useScanner();
|
|
569
|
+
init_useTcpSync();
|
|
570
|
+
}
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
// src/views/RoomScanner.tsx
|
|
574
|
+
import { useMemo } from "react";
|
|
575
|
+
import { Box as Box2, Text as Text2, useInput as useInput3 } from "ink";
|
|
576
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
577
|
+
function RoomScanner(props) {
|
|
578
|
+
const rooms = useScanner();
|
|
579
|
+
const sortedRooms = useMemo(() => {
|
|
580
|
+
return [...rooms].sort((a, b) => b.lastSeenAt - a.lastSeenAt);
|
|
581
|
+
}, [rooms]);
|
|
582
|
+
useInput3((input, key) => {
|
|
583
|
+
if (key.escape || input === "b") props.onBack();
|
|
584
|
+
if (input === "q") props.onExit();
|
|
585
|
+
const index = Number.parseInt(input, 10);
|
|
586
|
+
if (Number.isNaN(index)) return;
|
|
587
|
+
const room = sortedRooms[index - 1];
|
|
588
|
+
if (!room) return;
|
|
589
|
+
props.onSelectRoom(room);
|
|
590
|
+
});
|
|
591
|
+
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", padding: 1, children: [
|
|
592
|
+
/* @__PURE__ */ jsxs2(Text2, { children: [
|
|
593
|
+
/* @__PURE__ */ jsx2(Text2, { color: "yellow", children: "\u6B63\u5728\u626B\u63CF\u5C40\u57DF\u7F51..." }),
|
|
594
|
+
" (\u6309 ",
|
|
595
|
+
/* @__PURE__ */ jsx2(Text2, { color: "cyan", children: "b" }),
|
|
596
|
+
" \u8FD4\u56DE,",
|
|
597
|
+
" ",
|
|
598
|
+
/* @__PURE__ */ jsx2(Text2, { color: "cyan", children: "q" }),
|
|
599
|
+
" \u9000\u51FA)"
|
|
600
|
+
] }),
|
|
601
|
+
/* @__PURE__ */ jsx2(Box2, { flexDirection: "column", marginTop: 1, children: sortedRooms.length === 0 ? /* @__PURE__ */ jsx2(Text2, { color: "gray", children: "\u6682\u65E0\u623F\u95F4\u5E7F\u64AD\u3002" }) : sortedRooms.map((room, i) => /* @__PURE__ */ jsxs2(Text2, { children: [
|
|
602
|
+
/* @__PURE__ */ jsxs2(Text2, { color: "cyan", children: [
|
|
603
|
+
"[",
|
|
604
|
+
i + 1,
|
|
605
|
+
"]"
|
|
606
|
+
] }),
|
|
607
|
+
" ",
|
|
608
|
+
room.roomName,
|
|
609
|
+
" \u2014 ",
|
|
610
|
+
room.hostName,
|
|
611
|
+
" @ ",
|
|
612
|
+
room.ip,
|
|
613
|
+
":",
|
|
614
|
+
room.tcpPort
|
|
615
|
+
] }, `${room.ip}:${room.tcpPort}`)) })
|
|
616
|
+
] });
|
|
617
|
+
}
|
|
618
|
+
var init_RoomScanner = __esm({
|
|
619
|
+
"src/views/RoomScanner.tsx"() {
|
|
620
|
+
"use strict";
|
|
621
|
+
init_hooks();
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
// src/components/AiConsole.tsx
|
|
626
|
+
import { useMemo as useMemo2, useState as useState6 } from "react";
|
|
627
|
+
import { Box as Box3, Text as Text3, useInput as useInput4 } from "ink";
|
|
628
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
629
|
+
function AiConsole(props) {
|
|
630
|
+
const [input, setInput] = useState6("");
|
|
631
|
+
const agent = useAiAgent({
|
|
632
|
+
localName: props.localName,
|
|
633
|
+
peerName: props.peerName,
|
|
634
|
+
onStartCountdown: props.onStartCountdown
|
|
635
|
+
});
|
|
636
|
+
const helpLine = useMemo2(
|
|
637
|
+
() => "\u793A\u4F8B\uFF1A\u5012\u8BA1\u65F620\u5206\u949F / countdown 20 / \u95EE\u4E2A\u6280\u672F\u95EE\u9898",
|
|
638
|
+
[]
|
|
639
|
+
);
|
|
640
|
+
useInput4(
|
|
641
|
+
(ch, key) => {
|
|
642
|
+
if (key.escape) {
|
|
643
|
+
props.onClose();
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
if (key.return) {
|
|
647
|
+
const line = input.trim();
|
|
648
|
+
setInput("");
|
|
649
|
+
if (!line) return;
|
|
650
|
+
void agent.ask(line);
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
if (key.backspace || key.delete) {
|
|
654
|
+
setInput((s) => s.slice(0, -1));
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
if (key.ctrl || key.meta) return;
|
|
658
|
+
if (ch) setInput((s) => s + ch);
|
|
659
|
+
},
|
|
660
|
+
{ isActive: true }
|
|
661
|
+
);
|
|
662
|
+
const lines = agent.lines.slice(-12);
|
|
663
|
+
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", borderStyle: "round", paddingX: 1, paddingY: 0, children: [
|
|
664
|
+
/* @__PURE__ */ jsxs3(Box3, { justifyContent: "space-between", children: [
|
|
665
|
+
/* @__PURE__ */ jsx3(Text3, { color: "cyan", children: "AI Console" }),
|
|
666
|
+
/* @__PURE__ */ jsx3(Text3, { color: "gray", children: agent.busy ? "Thinking\u2026" : "Esc \u5173\u95ED" })
|
|
667
|
+
] }),
|
|
668
|
+
/* @__PURE__ */ jsx3(Box3, { flexDirection: "column", marginTop: 1, children: /* @__PURE__ */ jsx3(Text3, { color: "gray", children: helpLine }) }),
|
|
669
|
+
/* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", marginTop: 1, children: [
|
|
670
|
+
lines.length === 0 ? /* @__PURE__ */ jsx3(Text3, { color: "gray", children: "\uFF08\u5E7D\u7075\u8FD8\u5728\u58F3\u91CC\u2026\uFF09" }) : null,
|
|
671
|
+
lines.map((l, i) => /* @__PURE__ */ jsx3(Text3, { color: l.kind === "user" ? "yellow" : "white", children: l.text }, `${l.kind}:${l.at}:${i}`))
|
|
672
|
+
] }),
|
|
673
|
+
/* @__PURE__ */ jsxs3(Box3, { marginTop: 1, children: [
|
|
674
|
+
/* @__PURE__ */ jsxs3(Text3, { color: "green", children: [
|
|
675
|
+
">",
|
|
676
|
+
" "
|
|
677
|
+
] }),
|
|
678
|
+
/* @__PURE__ */ jsx3(Text3, { children: input })
|
|
679
|
+
] })
|
|
680
|
+
] });
|
|
681
|
+
}
|
|
682
|
+
var init_AiConsole = __esm({
|
|
683
|
+
"src/components/AiConsole.tsx"() {
|
|
684
|
+
"use strict";
|
|
685
|
+
init_hooks();
|
|
686
|
+
}
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
// src/components/AvatarDisplay.tsx
|
|
690
|
+
import { Box as Box4, Text as Text4 } from "ink";
|
|
691
|
+
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
692
|
+
var init_AvatarDisplay = __esm({
|
|
693
|
+
"src/components/AvatarDisplay.tsx"() {
|
|
694
|
+
"use strict";
|
|
695
|
+
}
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
// src/components/BuddyAvatar.tsx
|
|
699
|
+
import { Box as Box5, Text as Text5 } from "ink";
|
|
700
|
+
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
701
|
+
function BuddyAvatar(props) {
|
|
702
|
+
const frame = FRAMES[props.state];
|
|
703
|
+
return /* @__PURE__ */ jsx5(Box5, { flexDirection: "column", marginTop: 1, children: frame.lines.map((line, i) => /* @__PURE__ */ jsx5(Text5, { color: frame.color, children: line }, `${props.state}:${i}`)) });
|
|
704
|
+
}
|
|
705
|
+
var FRAMES;
|
|
706
|
+
var init_BuddyAvatar = __esm({
|
|
707
|
+
"src/components/BuddyAvatar.tsx"() {
|
|
708
|
+
"use strict";
|
|
709
|
+
FRAMES = {
|
|
710
|
+
TYPING: {
|
|
711
|
+
color: "green",
|
|
712
|
+
lines: [" /\\_/\\ ", "( >_<) ", " /|_|\\\\ ", " / \\\\ "]
|
|
713
|
+
},
|
|
714
|
+
IDLE: {
|
|
715
|
+
color: "yellow",
|
|
716
|
+
lines: [" /\\_/\\ ", "( -.-) ", " /|_|\\\\ ", " / \\\\ "]
|
|
717
|
+
},
|
|
718
|
+
OFFLINE: {
|
|
719
|
+
color: "gray",
|
|
720
|
+
lines: [" /\\_/\\ ", "( x_x) ", " /|_|\\\\ ", " / \\\\ "]
|
|
721
|
+
}
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
// src/components/StatusHeader.tsx
|
|
727
|
+
import { Box as Box6, Text as Text6 } from "ink";
|
|
728
|
+
import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
729
|
+
function statusText(status) {
|
|
730
|
+
switch (status) {
|
|
731
|
+
case "waiting":
|
|
732
|
+
return { label: "Waiting", color: "yellow" };
|
|
733
|
+
case "connecting":
|
|
734
|
+
return { label: "Connecting", color: "yellow" };
|
|
735
|
+
case "connected":
|
|
736
|
+
return { label: "Connected via TCP", color: "green" };
|
|
737
|
+
case "disconnected":
|
|
738
|
+
return { label: "Disconnected", color: "red" };
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
function StatusHeader(props) {
|
|
742
|
+
const st = statusText(props.status);
|
|
743
|
+
return /* @__PURE__ */ jsxs4(Box6, { justifyContent: "space-between", children: [
|
|
744
|
+
/* @__PURE__ */ jsxs4(Box6, { children: [
|
|
745
|
+
/* @__PURE__ */ jsx6(Text6, { color: st.color, children: st.label }),
|
|
746
|
+
props.role === "host" ? /* @__PURE__ */ jsx6(Text6, { color: "gray", children: props.tcpPort ? ` \u2014 TCP :${props.tcpPort}` : "" }) : /* @__PURE__ */ jsx6(Text6, { color: "gray", children: props.hostIp && props.tcpPort ? ` \u2014 ${props.hostIp}:${props.tcpPort}` : "" })
|
|
747
|
+
] }),
|
|
748
|
+
/* @__PURE__ */ jsx6(Box6, { children: props.countdownLabel ? /* @__PURE__ */ jsxs4(Text6, { color: "cyan", children: [
|
|
749
|
+
"Focus ",
|
|
750
|
+
props.countdownLabel
|
|
751
|
+
] }) : /* @__PURE__ */ jsx6(Text6, { children: " " }) })
|
|
752
|
+
] });
|
|
753
|
+
}
|
|
754
|
+
var init_StatusHeader = __esm({
|
|
755
|
+
"src/components/StatusHeader.tsx"() {
|
|
756
|
+
"use strict";
|
|
757
|
+
}
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
// src/components/index.ts
|
|
761
|
+
var init_components = __esm({
|
|
762
|
+
"src/components/index.ts"() {
|
|
763
|
+
"use strict";
|
|
764
|
+
init_AiConsole();
|
|
765
|
+
init_AvatarDisplay();
|
|
766
|
+
init_BuddyAvatar();
|
|
767
|
+
init_StatusHeader();
|
|
768
|
+
}
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
// src/views/Session.tsx
|
|
772
|
+
import { useCallback as useCallback4, useEffect as useEffect7, useMemo as useMemo3, useState as useState7 } from "react";
|
|
773
|
+
import { Box as Box7, Text as Text7, useInput as useInput5 } from "ink";
|
|
774
|
+
import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
775
|
+
function Session(props) {
|
|
776
|
+
const roomName = useMemo3(() => `${props.localName}'s Room`, [props.localName]);
|
|
777
|
+
const [showAi, setShowAi] = useState7(false);
|
|
778
|
+
const countdown = useCountdown();
|
|
779
|
+
const tcpOptions = useMemo3(() => {
|
|
780
|
+
return props.role === "host" ? { role: "host", localName: props.localName } : {
|
|
781
|
+
role: "client",
|
|
782
|
+
localName: props.localName,
|
|
783
|
+
hostIp: props.hostIp,
|
|
784
|
+
tcpPort: props.tcpPort,
|
|
785
|
+
hostName: props.hostName
|
|
786
|
+
};
|
|
787
|
+
}, [
|
|
788
|
+
props.role,
|
|
789
|
+
props.localName,
|
|
790
|
+
props.role === "client" ? props.hostIp : "",
|
|
791
|
+
props.role === "client" ? props.tcpPort : 0,
|
|
792
|
+
props.role === "client" ? props.hostName : ""
|
|
793
|
+
]);
|
|
794
|
+
const tcp = useTcpSync(tcpOptions);
|
|
795
|
+
const broadcasterOptions = useMemo3(() => {
|
|
796
|
+
return props.role === "host" ? {
|
|
797
|
+
enabled: true,
|
|
798
|
+
hostName: props.localName,
|
|
799
|
+
roomName,
|
|
800
|
+
tcpPort: tcp.listenPort
|
|
801
|
+
} : { enabled: false };
|
|
802
|
+
}, [props.role, props.localName, roomName, tcp.listenPort]);
|
|
803
|
+
useBroadcaster(broadcasterOptions);
|
|
804
|
+
const localActivity = useActivityMonitor();
|
|
805
|
+
const remoteActivity = tcp.remoteState ?? "OFFLINE";
|
|
806
|
+
const onToggleAi = useCallback4(() => setShowAi((v) => !v), []);
|
|
807
|
+
const onCloseAi = useCallback4(() => setShowAi(false), []);
|
|
808
|
+
useInput5(
|
|
809
|
+
(input, key) => {
|
|
810
|
+
if (input === "q") props.onExit();
|
|
811
|
+
if (input === "/" && !key.ctrl && !key.meta) onToggleAi();
|
|
812
|
+
},
|
|
813
|
+
{ isActive: !showAi }
|
|
814
|
+
);
|
|
815
|
+
const buddyName = props.role === "host" ? tcp.peerName ?? "Waiting..." : `${props.hostName ?? "Host"} (${props.roomName ?? "Room"})`;
|
|
816
|
+
const localState = localActivity.state;
|
|
817
|
+
const localLabel = props.role === "host" ? `${props.localName} (Host)` : `${props.localName} (Client)`;
|
|
818
|
+
useEffect7(() => {
|
|
819
|
+
if (tcp.status !== "connected") return;
|
|
820
|
+
tcp.sendStatus(localState);
|
|
821
|
+
}, [localState, tcp.status, tcp.sendStatus]);
|
|
822
|
+
return /* @__PURE__ */ jsxs5(Box7, { flexDirection: "column", padding: 1, children: [
|
|
823
|
+
/* @__PURE__ */ jsx7(
|
|
824
|
+
StatusHeader,
|
|
825
|
+
{
|
|
826
|
+
role: props.role,
|
|
827
|
+
status: tcp.status,
|
|
828
|
+
hostIp: props.role === "client" ? props.hostIp : void 0,
|
|
829
|
+
tcpPort: props.role === "client" ? props.tcpPort : tcp.listenPort,
|
|
830
|
+
countdownLabel: countdown.label
|
|
831
|
+
}
|
|
832
|
+
),
|
|
833
|
+
/* @__PURE__ */ jsxs5(Box7, { flexDirection: "row", gap: 4, marginTop: 1, children: [
|
|
834
|
+
/* @__PURE__ */ jsxs5(Box7, { flexDirection: "column", width: "50%", children: [
|
|
835
|
+
/* @__PURE__ */ jsx7(Text7, { color: "cyan", children: localLabel }),
|
|
836
|
+
/* @__PURE__ */ jsx7(BuddyAvatar, { state: localState })
|
|
837
|
+
] }),
|
|
838
|
+
/* @__PURE__ */ jsxs5(Box7, { flexDirection: "column", width: "50%", children: [
|
|
839
|
+
/* @__PURE__ */ jsx7(Text7, { color: "magenta", children: buddyName }),
|
|
840
|
+
/* @__PURE__ */ jsx7(BuddyAvatar, { state: remoteActivity })
|
|
841
|
+
] })
|
|
842
|
+
] }),
|
|
843
|
+
/* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsxs5(Text7, { color: "gray", children: [
|
|
844
|
+
"\u6309 ",
|
|
845
|
+
/* @__PURE__ */ jsx7(Text7, { color: "cyan", children: "/" }),
|
|
846
|
+
" \u53EC\u5524 AI Console\uFF0C\u6309 ",
|
|
847
|
+
/* @__PURE__ */ jsx7(Text7, { color: "cyan", children: "q" }),
|
|
848
|
+
" \u8FD4\u56DE\u83DC\u5355\u3002"
|
|
849
|
+
] }) }),
|
|
850
|
+
showAi ? /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx7(
|
|
851
|
+
AiConsole,
|
|
852
|
+
{
|
|
853
|
+
onClose: onCloseAi,
|
|
854
|
+
onStartCountdown: countdown.start,
|
|
855
|
+
localName: props.localName,
|
|
856
|
+
peerName: tcp.peerName ?? (props.role === "client" ? props.hostName : void 0) ?? "Buddy"
|
|
857
|
+
}
|
|
858
|
+
) }) : null
|
|
859
|
+
] });
|
|
860
|
+
}
|
|
861
|
+
var init_Session = __esm({
|
|
862
|
+
"src/views/Session.tsx"() {
|
|
863
|
+
"use strict";
|
|
864
|
+
init_components();
|
|
865
|
+
init_hooks();
|
|
866
|
+
}
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
// src/views/index.ts
|
|
870
|
+
var init_views = __esm({
|
|
871
|
+
"src/views/index.ts"() {
|
|
872
|
+
"use strict";
|
|
873
|
+
init_MainMenu();
|
|
874
|
+
init_RoomScanner();
|
|
875
|
+
init_Session();
|
|
876
|
+
}
|
|
877
|
+
});
|
|
878
|
+
|
|
879
|
+
// src/app/App.tsx
|
|
880
|
+
import { useCallback as useCallback5, useMemo as useMemo4, useState as useState8 } from "react";
|
|
881
|
+
import os2 from "os";
|
|
882
|
+
import { useApp } from "ink";
|
|
883
|
+
import { jsx as jsx8 } from "react/jsx-runtime";
|
|
884
|
+
function App() {
|
|
885
|
+
const { exit } = useApp();
|
|
886
|
+
const [view, setView] = useState8({ name: "MENU" });
|
|
887
|
+
const localName = useMemo4(() => os2.hostname(), []);
|
|
888
|
+
const goMenu = useCallback5(() => setView({ name: "MENU" }), []);
|
|
889
|
+
if (view.name === "MENU") {
|
|
890
|
+
return /* @__PURE__ */ jsx8(
|
|
891
|
+
MainMenu,
|
|
892
|
+
{
|
|
893
|
+
onHost: () => setView({ name: "SESSION", role: "host" }),
|
|
894
|
+
onJoin: () => setView({ name: "SCANNING" }),
|
|
895
|
+
onExit: () => exit()
|
|
896
|
+
}
|
|
897
|
+
);
|
|
898
|
+
}
|
|
899
|
+
if (view.name === "SCANNING") {
|
|
900
|
+
return /* @__PURE__ */ jsx8(
|
|
901
|
+
RoomScanner,
|
|
902
|
+
{
|
|
903
|
+
onBack: goMenu,
|
|
904
|
+
onExit: () => exit(),
|
|
905
|
+
onSelectRoom: (room) => setView({
|
|
906
|
+
name: "SESSION",
|
|
907
|
+
role: "client",
|
|
908
|
+
hostIp: room.ip,
|
|
909
|
+
tcpPort: room.tcpPort,
|
|
910
|
+
roomName: room.roomName,
|
|
911
|
+
hostName: room.hostName
|
|
912
|
+
})
|
|
913
|
+
}
|
|
914
|
+
);
|
|
915
|
+
}
|
|
916
|
+
if (view.name === "SESSION" && view.role === "host") {
|
|
917
|
+
return /* @__PURE__ */ jsx8(Session, { localName, role: "host", onExit: goMenu });
|
|
918
|
+
}
|
|
919
|
+
return /* @__PURE__ */ jsx8(
|
|
920
|
+
Session,
|
|
921
|
+
{
|
|
922
|
+
localName,
|
|
923
|
+
role: "client",
|
|
924
|
+
onExit: goMenu,
|
|
925
|
+
hostIp: view.hostIp,
|
|
926
|
+
tcpPort: view.tcpPort,
|
|
927
|
+
roomName: view.roomName,
|
|
928
|
+
hostName: view.hostName
|
|
929
|
+
}
|
|
930
|
+
);
|
|
931
|
+
}
|
|
932
|
+
var init_App = __esm({
|
|
933
|
+
"src/app/App.tsx"() {
|
|
934
|
+
"use strict";
|
|
935
|
+
init_views();
|
|
936
|
+
}
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
// src/app/index.ts
|
|
940
|
+
var app_exports = {};
|
|
941
|
+
__export(app_exports, {
|
|
942
|
+
App: () => App
|
|
943
|
+
});
|
|
944
|
+
var init_app = __esm({
|
|
945
|
+
"src/app/index.ts"() {
|
|
946
|
+
"use strict";
|
|
947
|
+
init_App();
|
|
948
|
+
}
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
// src/cli.tsx
|
|
952
|
+
process.env.NODE_ENV ??= "production";
|
|
953
|
+
var React5 = await import("react");
|
|
954
|
+
var { render } = await import("ink");
|
|
955
|
+
var { App: App2 } = await Promise.resolve().then(() => (init_app(), app_exports));
|
|
956
|
+
render(React5.createElement(App2));
|
|
957
|
+
//# sourceMappingURL=cli.js.map
|