@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.
- package/dist/cli.js +1097 -260
- package/dist/cli.js.map +1 -1
- package/package.json +3 -2
- package/pnpm-workspace.yaml +2 -0
- package/src/app/App.tsx +94 -53
- package/src/app/index.ts +1 -2
- package/src/components/AiConsole.tsx +171 -73
- package/src/components/StatusHeader.tsx +36 -36
- package/src/components/index.ts +8 -4
- package/src/components/sprite/BuddyAvatar.tsx +49 -0
- package/src/components/sprite/CountdownClockSprite.tsx +146 -0
- package/src/components/sprite/ProjectileThrowSprite.tsx +86 -0
- package/src/components/tool/createCountdownTool.ts +32 -0
- package/src/components/tool/createInteractionTool.ts +67 -0
- package/src/components/tool/createSessionInfoTool.ts +29 -0
- package/src/components/tool/index.ts +4 -0
- package/src/hooks/globalKeyboard.ts +146 -0
- package/src/hooks/index.ts +5 -7
- package/src/hooks/useActivityMonitor.ts +61 -24
- package/src/hooks/useAiAgent.ts +200 -165
- package/src/hooks/useBroadcaster.ts +55 -47
- package/src/hooks/useScanner.ts +59 -55
- package/src/hooks/useTcpSync.ts +166 -145
- package/src/net/broadcast.ts +21 -21
- package/src/net/index.ts +1 -2
- package/src/page/LeavePage.tsx +85 -0
- package/src/{views → page}/MainMenu.tsx +32 -28
- package/src/page/NicknamePrompt.tsx +62 -0
- package/src/{views → page}/RoomScanner.tsx +4 -1
- package/src/page/Session.tsx +364 -0
- package/src/page/index.ts +5 -0
- package/src/storage/apiKey.ts +36 -0
- package/src/types.ts +8 -0
- package/src/components/AvatarDisplay.tsx +0 -18
- package/src/components/BuddyAvatar.tsx +0 -32
- package/src/hooks/useCountdown.ts +0 -42
- package/src/views/Session.tsx +0 -127
- package/src/views/index.ts +0 -4
package/dist/cli.js
CHANGED
|
@@ -9,7 +9,7 @@ var __export = (target, all) => {
|
|
|
9
9
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
-
// src/
|
|
12
|
+
// src/page/MainMenu.tsx
|
|
13
13
|
import { Box, Text, useInput } from "ink";
|
|
14
14
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
15
15
|
function MainMenu(props) {
|
|
@@ -46,22 +46,202 @@ function MainMenu(props) {
|
|
|
46
46
|
] });
|
|
47
47
|
}
|
|
48
48
|
var init_MainMenu = __esm({
|
|
49
|
-
"src/
|
|
49
|
+
"src/page/MainMenu.tsx"() {
|
|
50
50
|
"use strict";
|
|
51
51
|
}
|
|
52
52
|
});
|
|
53
53
|
|
|
54
|
+
// src/page/NicknamePrompt.tsx
|
|
55
|
+
import { useMemo, useState } from "react";
|
|
56
|
+
import os from "os";
|
|
57
|
+
import { Box as Box2, Text as Text2, useInput as useInput2 } from "ink";
|
|
58
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
59
|
+
function defaultNick() {
|
|
60
|
+
try {
|
|
61
|
+
return os.userInfo().username || os.hostname();
|
|
62
|
+
} catch {
|
|
63
|
+
return os.hostname();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function NicknamePrompt(props) {
|
|
67
|
+
const initial = useMemo(() => defaultNick(), []);
|
|
68
|
+
const [nickname, setNickname] = useState(initial);
|
|
69
|
+
const [touched, setTouched] = useState(false);
|
|
70
|
+
useInput2((input, key) => {
|
|
71
|
+
if (key.escape) props.onExit();
|
|
72
|
+
if (key.return) {
|
|
73
|
+
const name = nickname.trim();
|
|
74
|
+
if (!name) return;
|
|
75
|
+
props.onSubmit(name);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (key.backspace || key.delete) {
|
|
79
|
+
setTouched(true);
|
|
80
|
+
setNickname((v) => v.slice(0, -1));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (key.ctrl || key.meta) return;
|
|
84
|
+
if (!input) return;
|
|
85
|
+
if (input === " ") return;
|
|
86
|
+
setTouched(true);
|
|
87
|
+
setNickname((v) => v + input);
|
|
88
|
+
});
|
|
89
|
+
const hint = touched ? "" : " (\u56DE\u8F66\u786E\u8BA4\uFF0C\u53EF\u76F4\u63A5\u7528\u9ED8\u8BA4\u503C)";
|
|
90
|
+
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", padding: 1, children: [
|
|
91
|
+
/* @__PURE__ */ jsx2(Text2, { color: "gray", children: "\u6B22\u8FCE\u6765\u5230 TermBuddy" }),
|
|
92
|
+
/* @__PURE__ */ jsx2(Box2, { marginTop: 1, children: /* @__PURE__ */ jsxs2(Text2, { children: [
|
|
93
|
+
"\u8BF7\u8F93\u5165\u4F60\u7684\u6635\u79F0\uFF1A",
|
|
94
|
+
/* @__PURE__ */ jsx2(Text2, { color: "cyan", children: nickname || "" }),
|
|
95
|
+
/* @__PURE__ */ jsx2(Text2, { color: "gray", children: hint })
|
|
96
|
+
] }) }),
|
|
97
|
+
/* @__PURE__ */ jsx2(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx2(Text2, { color: "gray", children: "\u6309 Esc \u9000\u51FA\u3002" }) })
|
|
98
|
+
] });
|
|
99
|
+
}
|
|
100
|
+
var init_NicknamePrompt = __esm({
|
|
101
|
+
"src/page/NicknamePrompt.tsx"() {
|
|
102
|
+
"use strict";
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// src/hooks/globalKeyboard.ts
|
|
107
|
+
import { spawn } from "child_process";
|
|
108
|
+
function emitKeydown() {
|
|
109
|
+
for (const listener of listeners) {
|
|
110
|
+
try {
|
|
111
|
+
listener();
|
|
112
|
+
} catch {
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async function tryStartUiohook() {
|
|
117
|
+
try {
|
|
118
|
+
const mod = await import("uiohook-napi");
|
|
119
|
+
const uIOhook = mod.uIOhook ?? mod.default?.uIOhook;
|
|
120
|
+
if (!uIOhook) return null;
|
|
121
|
+
const onKeydown = () => emitKeydown();
|
|
122
|
+
uIOhook.on("keydown", onKeydown);
|
|
123
|
+
uIOhook.start();
|
|
124
|
+
stopBackend = () => {
|
|
125
|
+
uIOhook.removeListener?.("keydown", onKeydown);
|
|
126
|
+
uIOhook.stop();
|
|
127
|
+
};
|
|
128
|
+
return "uiohook";
|
|
129
|
+
} catch {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function tryStartXinput() {
|
|
134
|
+
if (process.platform !== "linux") return Promise.resolve(null);
|
|
135
|
+
if (!process.env.DISPLAY) return Promise.resolve(null);
|
|
136
|
+
return new Promise((resolve) => {
|
|
137
|
+
let resolved = false;
|
|
138
|
+
const child = spawn("xinput", ["test-xi2", "--root"], {
|
|
139
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
140
|
+
});
|
|
141
|
+
const resolveOnce = (value) => {
|
|
142
|
+
if (resolved) return;
|
|
143
|
+
resolved = true;
|
|
144
|
+
resolve(value);
|
|
145
|
+
};
|
|
146
|
+
child.once("error", () => {
|
|
147
|
+
resolveOnce(null);
|
|
148
|
+
});
|
|
149
|
+
resolveOnce("xinput");
|
|
150
|
+
let buf = "";
|
|
151
|
+
child.stdout?.setEncoding("utf8");
|
|
152
|
+
child.stdout?.on("data", (chunk) => {
|
|
153
|
+
buf += chunk;
|
|
154
|
+
while (true) {
|
|
155
|
+
const idx = buf.indexOf("\n");
|
|
156
|
+
if (idx === -1) break;
|
|
157
|
+
const line = buf.slice(0, idx);
|
|
158
|
+
buf = buf.slice(idx + 1);
|
|
159
|
+
if (/KeyPress/.test(line)) emitKeydown();
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
stopBackend = () => {
|
|
163
|
+
child.stdout?.removeAllListeners();
|
|
164
|
+
child.removeAllListeners();
|
|
165
|
+
child.kill();
|
|
166
|
+
};
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
async function ensureGlobalKeyboard() {
|
|
170
|
+
if (started) return backend;
|
|
171
|
+
if (starting) return starting;
|
|
172
|
+
starting = (async () => {
|
|
173
|
+
const uiohook = await tryStartUiohook();
|
|
174
|
+
if (uiohook) return uiohook;
|
|
175
|
+
return await tryStartXinput();
|
|
176
|
+
})();
|
|
177
|
+
backend = await starting;
|
|
178
|
+
started = backend !== null;
|
|
179
|
+
if (!started) stopBackend = null;
|
|
180
|
+
starting = null;
|
|
181
|
+
return backend;
|
|
182
|
+
}
|
|
183
|
+
function stopIfIdle() {
|
|
184
|
+
if (listeners.size > 0) return;
|
|
185
|
+
if (!started) return;
|
|
186
|
+
started = false;
|
|
187
|
+
backend = null;
|
|
188
|
+
const stop = stopBackend;
|
|
189
|
+
stopBackend = null;
|
|
190
|
+
try {
|
|
191
|
+
stop?.();
|
|
192
|
+
} catch {
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
function subscribeGlobalKeydown(listener) {
|
|
196
|
+
listeners.add(listener);
|
|
197
|
+
return () => {
|
|
198
|
+
listeners.delete(listener);
|
|
199
|
+
stopIfIdle();
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
var backend, started, starting, stopBackend, listeners;
|
|
203
|
+
var init_globalKeyboard = __esm({
|
|
204
|
+
"src/hooks/globalKeyboard.ts"() {
|
|
205
|
+
"use strict";
|
|
206
|
+
backend = null;
|
|
207
|
+
started = false;
|
|
208
|
+
starting = null;
|
|
209
|
+
stopBackend = null;
|
|
210
|
+
listeners = /* @__PURE__ */ new Set();
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
|
|
54
214
|
// src/hooks/useActivityMonitor.ts
|
|
55
|
-
import { useEffect, useRef, useState } from "react";
|
|
56
|
-
import { useInput as
|
|
215
|
+
import { useCallback, useEffect, useRef, useState as useState2 } from "react";
|
|
216
|
+
import { useInput as useInput3 } from "ink";
|
|
57
217
|
function useActivityMonitor(options) {
|
|
58
218
|
const idleAfterMs = options?.idleAfterMs ?? 1500;
|
|
59
|
-
const [state, setState] =
|
|
219
|
+
const [state, setState] = useState2("IDLE");
|
|
60
220
|
const lastActivityRef = useRef(Date.now());
|
|
61
|
-
|
|
221
|
+
const markActive = useCallback(() => {
|
|
62
222
|
lastActivityRef.current = Date.now();
|
|
63
223
|
setState("TYPING");
|
|
224
|
+
}, []);
|
|
225
|
+
useInput3(() => {
|
|
226
|
+
markActive();
|
|
64
227
|
});
|
|
228
|
+
useEffect(() => {
|
|
229
|
+
const rawSource = options?.source ?? process.env.TERMBUDDY_ACTIVITY_SOURCE ?? "ink";
|
|
230
|
+
const source = rawSource === "xinput" ? "keyboard" : rawSource;
|
|
231
|
+
if (source !== "keyboard") return;
|
|
232
|
+
let cancelled = false;
|
|
233
|
+
let unsub = null;
|
|
234
|
+
void (async () => {
|
|
235
|
+
const ok = await ensureGlobalKeyboard();
|
|
236
|
+
if (cancelled) return;
|
|
237
|
+
if (!ok) return;
|
|
238
|
+
unsub = subscribeGlobalKeydown(markActive);
|
|
239
|
+
})();
|
|
240
|
+
return () => {
|
|
241
|
+
cancelled = true;
|
|
242
|
+
unsub?.();
|
|
243
|
+
};
|
|
244
|
+
}, [markActive, options?.source]);
|
|
65
245
|
useEffect(() => {
|
|
66
246
|
const id = setInterval(() => {
|
|
67
247
|
const delta = Date.now() - lastActivityRef.current;
|
|
@@ -74,23 +254,165 @@ function useActivityMonitor(options) {
|
|
|
74
254
|
var init_useActivityMonitor = __esm({
|
|
75
255
|
"src/hooks/useActivityMonitor.ts"() {
|
|
76
256
|
"use strict";
|
|
257
|
+
init_globalKeyboard();
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// src/components/tool/createCountdownTool.ts
|
|
262
|
+
import { tool } from "langchain";
|
|
263
|
+
function createCountdownTool(options) {
|
|
264
|
+
return tool(
|
|
265
|
+
async (input) => {
|
|
266
|
+
const minutes = Number(input.minutes);
|
|
267
|
+
if (!Number.isFinite(minutes) || minutes <= 0) return "\u5012\u8BA1\u65F6\u5206\u949F\u6570\u65E0\u6548\u3002";
|
|
268
|
+
options.onStartCountdown?.(minutes);
|
|
269
|
+
return `\u5DF2\u5F00\u59CB\u5012\u8BA1\u65F6 ${minutes} \u5206\u949F\u3002`;
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
name: "start_countdown",
|
|
273
|
+
description: "\u5F00\u59CB\u4E00\u4E2A\u4E13\u6CE8\u5012\u8BA1\u65F6\uFF08\u5206\u949F\uFF09\u3002",
|
|
274
|
+
schema: {
|
|
275
|
+
type: "object",
|
|
276
|
+
properties: {
|
|
277
|
+
minutes: {
|
|
278
|
+
type: "integer",
|
|
279
|
+
minimum: 1,
|
|
280
|
+
maximum: 180,
|
|
281
|
+
description: "\u5012\u8BA1\u65F6\u5206\u949F\u6570"
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
required: ["minutes"],
|
|
285
|
+
additionalProperties: false
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
var init_createCountdownTool = __esm({
|
|
291
|
+
"src/components/tool/createCountdownTool.ts"() {
|
|
292
|
+
"use strict";
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// src/components/tool/createInteractionTool.ts
|
|
297
|
+
import { tool as tool2 } from "langchain";
|
|
298
|
+
function normalizeKind(raw) {
|
|
299
|
+
if (typeof raw !== "string") return null;
|
|
300
|
+
const upper = raw.toUpperCase().trim();
|
|
301
|
+
if (upper === "ROSE" || upper === "POOP" || upper === "HAMMER") return upper;
|
|
302
|
+
const lower = raw.toLowerCase();
|
|
303
|
+
for (const item of KIND_ALIASES) {
|
|
304
|
+
if (item.keys.some((k) => lower.includes(k))) return item.kind;
|
|
305
|
+
}
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
function normalizeDirection(raw) {
|
|
309
|
+
if (typeof raw !== "string") return null;
|
|
310
|
+
const upper = raw.toUpperCase().trim();
|
|
311
|
+
if (upper === "LEFT_TO_RIGHT" || upper === "RIGHT_TO_LEFT") return upper;
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
function createInteractionTool(options) {
|
|
315
|
+
return tool2(
|
|
316
|
+
async (input) => {
|
|
317
|
+
const kind = normalizeKind(input.kind ?? "") ?? "ROSE";
|
|
318
|
+
const direction = normalizeDirection(input.direction) ?? "LEFT_TO_RIGHT";
|
|
319
|
+
options.onThrow?.(kind, direction);
|
|
320
|
+
const msg = (input.message ?? "").trim();
|
|
321
|
+
return msg ? `\u5DF2\u6295\u63B7 ${kind}\uFF1A${msg}` : `\u5DF2\u6295\u63B7 ${kind}\u3002`;
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
name: "throw_projectile",
|
|
325
|
+
description: "\u548C\u540C\u684C\u4E92\u52A8\uFF1A\u6295\u63B7\u4E00\u4E2A\u5C0F\u7269\u54C1\uFF08\u{1F339}/\u{1F4A9}/\u{1F528}\uFF09\u3002",
|
|
326
|
+
schema: {
|
|
327
|
+
type: "object",
|
|
328
|
+
properties: {
|
|
329
|
+
kind: {
|
|
330
|
+
type: "string",
|
|
331
|
+
description: "\u6295\u63B7\u7269\u7C7B\u578B\uFF08ROSE/POOP/HAMMER\uFF0C\u6216\u4EFB\u610F\u63CF\u8FF0\u5982\u201C\u73AB\u7470/\u9524\u5B50/\u{1F4A9}\u201D\uFF09"
|
|
332
|
+
},
|
|
333
|
+
direction: {
|
|
334
|
+
type: "string",
|
|
335
|
+
enum: ["LEFT_TO_RIGHT", "RIGHT_TO_LEFT"],
|
|
336
|
+
description: "\u98DE\u884C\u65B9\u5411"
|
|
337
|
+
},
|
|
338
|
+
message: { type: "string", description: "\u9644\u5E26\u4E00\u53E5\u8BDD\uFF08\u53EF\u9009\uFF09" }
|
|
339
|
+
},
|
|
340
|
+
required: [],
|
|
341
|
+
additionalProperties: false
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
var KIND_ALIASES;
|
|
347
|
+
var init_createInteractionTool = __esm({
|
|
348
|
+
"src/components/tool/createInteractionTool.ts"() {
|
|
349
|
+
"use strict";
|
|
350
|
+
KIND_ALIASES = [
|
|
351
|
+
{ kind: "ROSE", keys: ["rose", "\u82B1", "\u73AB\u7470", "\u{1F339}", "love"] },
|
|
352
|
+
{ kind: "POOP", keys: ["poop", "\u5C4E", "\u{1F4A9}", "\u5927\u4FBF"] },
|
|
353
|
+
{ kind: "HAMMER", keys: ["hammer", "\u9524", "\u{1F528}", "\u6572", "\u6253"] }
|
|
354
|
+
];
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
// src/components/tool/createSessionInfoTool.ts
|
|
359
|
+
import { tool as tool3 } from "langchain";
|
|
360
|
+
function createSessionInfoTool(options) {
|
|
361
|
+
return tool3(
|
|
362
|
+
async () => {
|
|
363
|
+
return JSON.stringify(
|
|
364
|
+
{
|
|
365
|
+
localName: options.localName,
|
|
366
|
+
peerName: options.peerName
|
|
367
|
+
},
|
|
368
|
+
null,
|
|
369
|
+
2
|
|
370
|
+
);
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
name: "session_info",
|
|
374
|
+
description: "\u83B7\u53D6\u5F53\u524D\u4F1A\u8BDD\u4E0A\u4E0B\u6587\uFF08\u672C\u5730\u6635\u79F0\u3001\u540C\u684C\u6635\u79F0\uFF09\u3002",
|
|
375
|
+
schema: {
|
|
376
|
+
type: "object",
|
|
377
|
+
properties: {},
|
|
378
|
+
additionalProperties: false
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
var init_createSessionInfoTool = __esm({
|
|
384
|
+
"src/components/tool/createSessionInfoTool.ts"() {
|
|
385
|
+
"use strict";
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
// src/components/tool/index.ts
|
|
390
|
+
var init_tool = __esm({
|
|
391
|
+
"src/components/tool/index.ts"() {
|
|
392
|
+
"use strict";
|
|
393
|
+
init_createCountdownTool();
|
|
394
|
+
init_createInteractionTool();
|
|
395
|
+
init_createSessionInfoTool();
|
|
77
396
|
}
|
|
78
397
|
});
|
|
79
398
|
|
|
80
399
|
// src/hooks/useAiAgent.ts
|
|
81
|
-
import { useCallback, useEffect as useEffect2, useRef as useRef2, useState as
|
|
82
|
-
import { createAgent
|
|
400
|
+
import { useCallback as useCallback2, useEffect as useEffect2, useRef as useRef2, useState as useState3 } from "react";
|
|
401
|
+
import { createAgent } from "langchain";
|
|
402
|
+
import { ChatOpenAI } from "@langchain/openai";
|
|
83
403
|
function contentToText(content) {
|
|
84
404
|
if (typeof content === "string") return content;
|
|
85
405
|
if (!content) return "";
|
|
86
406
|
if (Array.isArray(content)) {
|
|
87
407
|
return content.map((part) => {
|
|
88
408
|
if (typeof part === "string") return part;
|
|
89
|
-
if (typeof part === "object" && part && "text" in part)
|
|
409
|
+
if (typeof part === "object" && part && "text" in part)
|
|
410
|
+
return String(part.text ?? "");
|
|
90
411
|
return "";
|
|
91
412
|
}).join("");
|
|
92
413
|
}
|
|
93
|
-
if (typeof content === "object" && "text" in content)
|
|
414
|
+
if (typeof content === "object" && "text" in content)
|
|
415
|
+
return String(content.text ?? "");
|
|
94
416
|
return String(content);
|
|
95
417
|
}
|
|
96
418
|
function lastAiText(messages) {
|
|
@@ -110,20 +432,22 @@ function createSystemPrompt(context) {
|
|
|
110
432
|
"\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
433
|
"\u4F60\u53EF\u4EE5\u4F7F\u7528\u5DE5\u5177\u6765\u64CD\u63A7\u5E94\u7528\u529F\u80FD\uFF08\u4F8B\u5982\u5012\u8BA1\u65F6\uFF09\u3002",
|
|
112
434
|
"\u5982\u679C\u7528\u6237\u63D0\u5230\u201C\u5012\u8BA1\u65F6/\u4E13\u6CE8/\u8BA1\u65F6/countdown\u201D\uFF0C\u4F18\u5148\u8C03\u7528 start_countdown\u3002",
|
|
435
|
+
"\u5982\u679C\u7528\u6237\u63D0\u5230\u201C\u4E92\u52A8/\u6254/\u6295\u63B7/throw\u201D\uFF0C\u4F18\u5148\u8C03\u7528 throw_projectile\u3002",
|
|
113
436
|
`\u5F53\u524D\u4E0A\u4E0B\u6587\uFF1A\u6211\u53EB ${context.localName}\uFF1B\u540C\u684C\u53EB ${context.peerName}\u3002`
|
|
114
437
|
].join("\n");
|
|
115
438
|
}
|
|
116
439
|
function useAiAgent(options) {
|
|
117
|
-
const [lines, setLines] =
|
|
118
|
-
const [busy, setBusy] =
|
|
440
|
+
const [lines, setLines] = useState3([]);
|
|
441
|
+
const [busy, setBusy] = useState3(false);
|
|
119
442
|
const agentRef = useRef2(null);
|
|
120
443
|
const agentInitRef = useRef2(null);
|
|
444
|
+
const agentKeyRef = useRef2(null);
|
|
121
445
|
const stateRef = useRef2({ messages: [] });
|
|
122
446
|
const abortRef = useRef2(null);
|
|
123
|
-
const append =
|
|
447
|
+
const append = useCallback2((line) => {
|
|
124
448
|
setLines((prev) => [...prev, line]);
|
|
125
449
|
}, []);
|
|
126
|
-
const updateLine =
|
|
450
|
+
const updateLine = useCallback2((at, text) => {
|
|
127
451
|
setLines((prev) => {
|
|
128
452
|
const idx = prev.findIndex((l) => l.at === at);
|
|
129
453
|
if (idx === -1) return prev;
|
|
@@ -132,67 +456,65 @@ function useAiAgent(options) {
|
|
|
132
456
|
return next;
|
|
133
457
|
});
|
|
134
458
|
}, []);
|
|
135
|
-
const ensureAgent =
|
|
136
|
-
|
|
459
|
+
const ensureAgent = useCallback2(async () => {
|
|
460
|
+
const apiKey = (options.apiKey ?? "").trim();
|
|
461
|
+
if (!apiKey) throw new Error("missing_api_key");
|
|
462
|
+
if (agentRef.current && agentKeyRef.current === apiKey)
|
|
463
|
+
return agentRef.current;
|
|
464
|
+
agentRef.current = null;
|
|
465
|
+
agentInitRef.current = null;
|
|
466
|
+
agentKeyRef.current = apiKey;
|
|
467
|
+
stateRef.current.messages = [];
|
|
137
468
|
agentInitRef.current ??= (async () => {
|
|
138
|
-
const startCountdown =
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
);
|
|
469
|
+
const startCountdown = createCountdownTool({
|
|
470
|
+
onStartCountdown: options.onStartCountdown
|
|
471
|
+
});
|
|
472
|
+
const sessionInfo = createSessionInfoTool({
|
|
473
|
+
localName: options.localName,
|
|
474
|
+
peerName: options.peerName
|
|
475
|
+
});
|
|
476
|
+
const interaction = createInteractionTool({
|
|
477
|
+
onThrow: options.onThrowProjectile
|
|
478
|
+
});
|
|
479
|
+
const llm = new ChatOpenAI({
|
|
480
|
+
model: "deepseek-chat",
|
|
481
|
+
configuration: {
|
|
482
|
+
baseURL: "https://api.deepseek.com"
|
|
168
483
|
},
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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,
|
|
484
|
+
apiKey,
|
|
485
|
+
temperature: 0.1,
|
|
486
|
+
maxTokens: 1e3,
|
|
179
487
|
timeout: 3e4
|
|
180
488
|
});
|
|
181
489
|
return createAgent({
|
|
182
|
-
llm,
|
|
183
|
-
tools: [startCountdown, sessionInfo],
|
|
184
|
-
|
|
490
|
+
model: llm,
|
|
491
|
+
tools: [startCountdown, interaction, sessionInfo],
|
|
492
|
+
systemPrompt: createSystemPrompt({
|
|
493
|
+
localName: options.localName,
|
|
494
|
+
peerName: options.peerName
|
|
495
|
+
}),
|
|
185
496
|
name: "ghost"
|
|
186
497
|
});
|
|
187
498
|
})();
|
|
188
499
|
agentRef.current = await agentInitRef.current;
|
|
189
500
|
return agentRef.current;
|
|
190
|
-
}, [
|
|
191
|
-
|
|
501
|
+
}, [
|
|
502
|
+
options.apiKey,
|
|
503
|
+
options.localName,
|
|
504
|
+
options.onStartCountdown,
|
|
505
|
+
options.onThrowProjectile,
|
|
506
|
+
options.peerName
|
|
507
|
+
]);
|
|
508
|
+
const ask = useCallback2(
|
|
192
509
|
async (text) => {
|
|
193
|
-
|
|
194
|
-
const aiAt =
|
|
195
|
-
|
|
510
|
+
const userAt = Date.now();
|
|
511
|
+
const aiAt = userAt + 1;
|
|
512
|
+
const toolAt = userAt + 2;
|
|
513
|
+
setLines([
|
|
514
|
+
{ kind: "user", text: `> ${text}`, at: userAt },
|
|
515
|
+
{ kind: "ai", text: "\u2026", at: aiAt },
|
|
516
|
+
{ kind: "system", text: "", at: toolAt }
|
|
517
|
+
]);
|
|
196
518
|
abortRef.current?.abort();
|
|
197
519
|
abortRef.current = new AbortController();
|
|
198
520
|
setBusy(true);
|
|
@@ -200,7 +522,7 @@ function useAiAgent(options) {
|
|
|
200
522
|
const agent = await ensureAgent();
|
|
201
523
|
const stream = await agent.stream(
|
|
202
524
|
{
|
|
203
|
-
messages: [
|
|
525
|
+
messages: [{ role: "user", content: text }]
|
|
204
526
|
},
|
|
205
527
|
{
|
|
206
528
|
streamMode: "values",
|
|
@@ -210,12 +532,20 @@ function useAiAgent(options) {
|
|
|
210
532
|
for await (const chunk of stream) {
|
|
211
533
|
const messages = chunk?.messages ?? [];
|
|
212
534
|
if (messages.length > 0) stateRef.current.messages = messages;
|
|
535
|
+
const latest = messages.at(-1);
|
|
536
|
+
if (latest?.tool_calls?.length) {
|
|
537
|
+
const names = latest.tool_calls.map((tc) => tc?.name).filter(Boolean).join(", ");
|
|
538
|
+
if (names) updateLine(toolAt, `Calling tools: ${names}`);
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
213
541
|
const t = lastAiText(messages);
|
|
214
542
|
if (t !== null) updateLine(aiAt, t);
|
|
215
543
|
}
|
|
216
544
|
} catch (e) {
|
|
217
545
|
const msg = e instanceof Error ? e.message : String(e);
|
|
218
|
-
|
|
546
|
+
if (msg === "missing_api_key")
|
|
547
|
+
updateLine(aiAt, "\u8BF7\u5148\u5728 AI Console \u8F93\u5165 DeepSeek API Key\u3002");
|
|
548
|
+
else updateLine(aiAt, `\uFF08AI \u51FA\u9519\uFF09${msg}`);
|
|
219
549
|
} finally {
|
|
220
550
|
setBusy(false);
|
|
221
551
|
}
|
|
@@ -230,6 +560,7 @@ function useAiAgent(options) {
|
|
|
230
560
|
var init_useAiAgent = __esm({
|
|
231
561
|
"src/hooks/useAiAgent.ts"() {
|
|
232
562
|
"use strict";
|
|
563
|
+
init_tool();
|
|
233
564
|
}
|
|
234
565
|
});
|
|
235
566
|
|
|
@@ -245,7 +576,7 @@ var init_constants = __esm({
|
|
|
245
576
|
});
|
|
246
577
|
|
|
247
578
|
// src/net/broadcast.ts
|
|
248
|
-
import
|
|
579
|
+
import os2 from "os";
|
|
249
580
|
function ipv4ToInt(ip) {
|
|
250
581
|
return ip.split(".").map((n) => Number.parseInt(n, 10)).reduce((acc, n) => (acc << 8 | n & 255) >>> 0, 0);
|
|
251
582
|
}
|
|
@@ -254,7 +585,7 @@ function intToIpv4(n) {
|
|
|
254
585
|
}
|
|
255
586
|
function getBroadcastTargets() {
|
|
256
587
|
const out = /* @__PURE__ */ new Set(["255.255.255.255"]);
|
|
257
|
-
const ifaces =
|
|
588
|
+
const ifaces = os2.networkInterfaces();
|
|
258
589
|
for (const entries of Object.values(ifaces)) {
|
|
259
590
|
if (!entries) continue;
|
|
260
591
|
for (const e of entries) {
|
|
@@ -328,51 +659,8 @@ var init_useBroadcaster = __esm({
|
|
|
328
659
|
}
|
|
329
660
|
});
|
|
330
661
|
|
|
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
662
|
// src/hooks/useScanner.ts
|
|
375
|
-
import { useEffect as
|
|
663
|
+
import { useEffect as useEffect4, useState as useState4 } from "react";
|
|
376
664
|
import dgram2 from "dgram";
|
|
377
665
|
function safeParse(msg) {
|
|
378
666
|
try {
|
|
@@ -388,7 +676,7 @@ function safeParse(msg) {
|
|
|
388
676
|
function useScanner(options) {
|
|
389
677
|
const staleAfterMs = options?.staleAfterMs ?? 3500;
|
|
390
678
|
const [rooms, setRooms] = useState4([]);
|
|
391
|
-
|
|
679
|
+
useEffect4(() => {
|
|
392
680
|
const socket = dgram2.createSocket("udp4");
|
|
393
681
|
socket.on("error", () => {
|
|
394
682
|
});
|
|
@@ -413,7 +701,9 @@ function useScanner(options) {
|
|
|
413
701
|
});
|
|
414
702
|
const prune = setInterval(() => {
|
|
415
703
|
const now = Date.now();
|
|
416
|
-
setRooms(
|
|
704
|
+
setRooms(
|
|
705
|
+
(prev) => prev.filter((r) => now - r.lastSeenAt <= staleAfterMs)
|
|
706
|
+
);
|
|
417
707
|
}, 500);
|
|
418
708
|
return () => {
|
|
419
709
|
clearInterval(prune);
|
|
@@ -430,20 +720,24 @@ var init_useScanner = __esm({
|
|
|
430
720
|
});
|
|
431
721
|
|
|
432
722
|
// src/hooks/useTcpSync.ts
|
|
433
|
-
import { useCallback as useCallback3, useEffect as
|
|
723
|
+
import { useCallback as useCallback3, useEffect as useEffect5, useRef as useRef3, useState as useState5 } from "react";
|
|
434
724
|
import net from "net";
|
|
435
725
|
function writePacket(socket, packet) {
|
|
436
726
|
socket.write(`${JSON.stringify(packet)}
|
|
437
727
|
`, "utf8");
|
|
438
728
|
}
|
|
439
729
|
function useTcpSync(options) {
|
|
440
|
-
const [status, setStatus] = useState5(
|
|
730
|
+
const [status, setStatus] = useState5(
|
|
731
|
+
options.role === "host" ? "waiting" : "connecting"
|
|
732
|
+
);
|
|
441
733
|
const [listenPort, setListenPort] = useState5(void 0);
|
|
442
734
|
const [peerName, setPeerName] = useState5(void 0);
|
|
443
|
-
const [remoteState, setRemoteState] = useState5(
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
const
|
|
735
|
+
const [remoteState, setRemoteState] = useState5(
|
|
736
|
+
void 0
|
|
737
|
+
);
|
|
738
|
+
const socketRef = useRef3(null);
|
|
739
|
+
const lastSeenRef = useRef3(Date.now());
|
|
740
|
+
const heartbeatRef = useRef3(null);
|
|
447
741
|
const cleanupSocket = useCallback3(() => {
|
|
448
742
|
if (heartbeatRef.current) clearInterval(heartbeatRef.current);
|
|
449
743
|
heartbeatRef.current = null;
|
|
@@ -477,7 +771,8 @@ function useTcpSync(options) {
|
|
|
477
771
|
else setPeerName(packet.hostName);
|
|
478
772
|
}
|
|
479
773
|
if (packet.type === "status") setRemoteState(packet.state);
|
|
480
|
-
if (packet.type === "ping")
|
|
774
|
+
if (packet.type === "ping")
|
|
775
|
+
writePacket(s, { type: "pong", sentAt: Date.now() });
|
|
481
776
|
if (packet.type === "pong") {
|
|
482
777
|
}
|
|
483
778
|
} catch {
|
|
@@ -514,7 +809,7 @@ function useTcpSync(options) {
|
|
|
514
809
|
},
|
|
515
810
|
[cleanupSocket, options]
|
|
516
811
|
);
|
|
517
|
-
|
|
812
|
+
useEffect5(() => {
|
|
518
813
|
if (options.role === "host") {
|
|
519
814
|
const server = net.createServer((socket2) => {
|
|
520
815
|
attachSocket(socket2);
|
|
@@ -531,9 +826,12 @@ function useTcpSync(options) {
|
|
|
531
826
|
};
|
|
532
827
|
}
|
|
533
828
|
setStatus("connecting");
|
|
534
|
-
const socket = net.createConnection(
|
|
535
|
-
|
|
536
|
-
|
|
829
|
+
const socket = net.createConnection(
|
|
830
|
+
{ host: options.hostIp, port: options.tcpPort },
|
|
831
|
+
() => {
|
|
832
|
+
attachSocket(socket);
|
|
833
|
+
}
|
|
834
|
+
);
|
|
537
835
|
socket.on("error", () => {
|
|
538
836
|
setStatus("disconnected");
|
|
539
837
|
setRemoteState("OFFLINE");
|
|
@@ -564,22 +862,21 @@ var init_hooks = __esm({
|
|
|
564
862
|
init_useActivityMonitor();
|
|
565
863
|
init_useAiAgent();
|
|
566
864
|
init_useBroadcaster();
|
|
567
|
-
init_useCountdown();
|
|
568
865
|
init_useScanner();
|
|
569
866
|
init_useTcpSync();
|
|
570
867
|
}
|
|
571
868
|
});
|
|
572
869
|
|
|
573
|
-
// src/
|
|
574
|
-
import { useMemo } from "react";
|
|
575
|
-
import { Box as
|
|
576
|
-
import { jsx as
|
|
870
|
+
// src/page/RoomScanner.tsx
|
|
871
|
+
import { useMemo as useMemo2 } from "react";
|
|
872
|
+
import { Box as Box3, Text as Text3, useInput as useInput4 } from "ink";
|
|
873
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
577
874
|
function RoomScanner(props) {
|
|
578
875
|
const rooms = useScanner();
|
|
579
|
-
const sortedRooms =
|
|
876
|
+
const sortedRooms = useMemo2(() => {
|
|
580
877
|
return [...rooms].sort((a, b) => b.lastSeenAt - a.lastSeenAt);
|
|
581
878
|
}, [rooms]);
|
|
582
|
-
|
|
879
|
+
useInput4((input, key) => {
|
|
583
880
|
if (key.escape || input === "b") props.onBack();
|
|
584
881
|
if (input === "q") props.onExit();
|
|
585
882
|
const index = Number.parseInt(input, 10);
|
|
@@ -588,61 +885,212 @@ function RoomScanner(props) {
|
|
|
588
885
|
if (!room) return;
|
|
589
886
|
props.onSelectRoom(room);
|
|
590
887
|
});
|
|
591
|
-
return /* @__PURE__ */
|
|
592
|
-
/* @__PURE__ */
|
|
593
|
-
/* @__PURE__ */
|
|
888
|
+
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", padding: 1, children: [
|
|
889
|
+
/* @__PURE__ */ jsxs3(Text3, { children: [
|
|
890
|
+
/* @__PURE__ */ jsx3(Text3, { color: "yellow", children: "\u6B63\u5728\u626B\u63CF\u5C40\u57DF\u7F51..." }),
|
|
594
891
|
" (\u6309 ",
|
|
595
|
-
/* @__PURE__ */
|
|
892
|
+
/* @__PURE__ */ jsx3(Text3, { color: "cyan", children: "b" }),
|
|
596
893
|
" \u8FD4\u56DE,",
|
|
597
894
|
" ",
|
|
598
|
-
/* @__PURE__ */
|
|
895
|
+
/* @__PURE__ */ jsx3(Text3, { color: "cyan", children: "q" }),
|
|
599
896
|
" \u9000\u51FA)"
|
|
600
897
|
] }),
|
|
601
|
-
/* @__PURE__ */
|
|
602
|
-
/* @__PURE__ */
|
|
898
|
+
/* @__PURE__ */ jsx3(Box3, { flexDirection: "column", marginTop: 1, children: sortedRooms.length === 0 ? /* @__PURE__ */ jsx3(Text3, { color: "gray", children: "\u6682\u65E0\u623F\u95F4\u5E7F\u64AD\u3002" }) : sortedRooms.map((room, i) => /* @__PURE__ */ jsxs3(Text3, { children: [
|
|
899
|
+
/* @__PURE__ */ jsxs3(Text3, { color: "cyan", children: [
|
|
603
900
|
"[",
|
|
604
901
|
i + 1,
|
|
605
902
|
"]"
|
|
606
903
|
] }),
|
|
607
904
|
" ",
|
|
608
|
-
room.roomName,
|
|
609
|
-
" \u2014 ",
|
|
610
905
|
room.hostName,
|
|
611
|
-
"
|
|
612
|
-
|
|
613
|
-
"
|
|
614
|
-
|
|
906
|
+
" ",
|
|
907
|
+
/* @__PURE__ */ jsx3(Text3, { color: "gray", children: "@" }),
|
|
908
|
+
" ",
|
|
909
|
+
/* @__PURE__ */ jsxs3(Text3, { color: "gray", children: [
|
|
910
|
+
room.ip,
|
|
911
|
+
":",
|
|
912
|
+
room.tcpPort
|
|
913
|
+
] })
|
|
615
914
|
] }, `${room.ip}:${room.tcpPort}`)) })
|
|
616
915
|
] });
|
|
617
916
|
}
|
|
618
917
|
var init_RoomScanner = __esm({
|
|
619
|
-
"src/
|
|
918
|
+
"src/page/RoomScanner.tsx"() {
|
|
620
919
|
"use strict";
|
|
621
920
|
init_hooks();
|
|
622
921
|
}
|
|
623
922
|
});
|
|
624
923
|
|
|
924
|
+
// src/page/LeavePage.tsx
|
|
925
|
+
import { useMemo as useMemo3 } from "react";
|
|
926
|
+
import { Box as Box4, Text as Text4, useInput as useInput5 } from "ink";
|
|
927
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
928
|
+
function formatDuration(ms) {
|
|
929
|
+
const totalSeconds = Math.max(0, Math.floor(ms / 1e3));
|
|
930
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
931
|
+
const minutes = Math.floor(totalSeconds % 3600 / 60);
|
|
932
|
+
const seconds = totalSeconds % 60;
|
|
933
|
+
if (hours > 0) return `${hours}\u5C0F\u65F6${minutes}\u5206${seconds}\u79D2`;
|
|
934
|
+
if (minutes > 0) return `${minutes}\u5206${seconds}\u79D2`;
|
|
935
|
+
return `${seconds}\u79D2`;
|
|
936
|
+
}
|
|
937
|
+
function LeavePage(props) {
|
|
938
|
+
useInput5((input, key) => {
|
|
939
|
+
if (key.escape || input === "q") props.onExit();
|
|
940
|
+
if (key.return || input === " ") props.onBack();
|
|
941
|
+
});
|
|
942
|
+
const sessionLabel = useMemo3(
|
|
943
|
+
() => formatDuration(props.stats.sessionDurationMs),
|
|
944
|
+
[props.stats.sessionDurationMs]
|
|
945
|
+
);
|
|
946
|
+
const connectedLabel = useMemo3(
|
|
947
|
+
() => formatDuration(props.stats.connectedDurationMs),
|
|
948
|
+
[props.stats.connectedDurationMs]
|
|
949
|
+
);
|
|
950
|
+
return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", padding: 1, alignItems: "center", children: [
|
|
951
|
+
/* @__PURE__ */ jsxs4(
|
|
952
|
+
Box4,
|
|
953
|
+
{
|
|
954
|
+
flexDirection: "column",
|
|
955
|
+
marginTop: 1,
|
|
956
|
+
borderStyle: "round",
|
|
957
|
+
paddingX: 2,
|
|
958
|
+
borderColor: "gray",
|
|
959
|
+
children: [
|
|
960
|
+
/* @__PURE__ */ jsx4(Text4, { color: "white", bold: true, children: props.stats.peerName ? `\u4E0E ${props.stats.peerName} \u7684\u540C\u9891\u8BB0\u5F55` : "\u672C\u6B21\u4E13\u6CE8\u8BB0\u5F55" }),
|
|
961
|
+
/* @__PURE__ */ jsxs4(Box4, { marginTop: 1, flexDirection: "column", gap: 1, children: [
|
|
962
|
+
/* @__PURE__ */ jsxs4(Box4, { justifyContent: "space-between", width: 30, children: [
|
|
963
|
+
/* @__PURE__ */ jsx4(Text4, { children: "\u2328\uFE0F \u952E\u76D8\u6572\u51FB" }),
|
|
964
|
+
/* @__PURE__ */ jsx4(Text4, { color: "yellow", children: props.stats.keyPresses })
|
|
965
|
+
] }),
|
|
966
|
+
/* @__PURE__ */ jsxs4(Box4, { justifyContent: "space-between", width: 30, children: [
|
|
967
|
+
/* @__PURE__ */ jsx4(Text4, { children: "\u23F1\uFE0F \u603B\u5171\u65F6\u957F" }),
|
|
968
|
+
/* @__PURE__ */ jsx4(Text4, { color: "green", children: sessionLabel })
|
|
969
|
+
] }),
|
|
970
|
+
/* @__PURE__ */ jsxs4(Box4, { justifyContent: "space-between", width: 30, children: [
|
|
971
|
+
/* @__PURE__ */ jsx4(Text4, { children: "\u{1F517} \u8FDE\u7EBF\u65F6\u957F" }),
|
|
972
|
+
/* @__PURE__ */ jsx4(Text4, { color: "blue", children: connectedLabel })
|
|
973
|
+
] })
|
|
974
|
+
] })
|
|
975
|
+
]
|
|
976
|
+
}
|
|
977
|
+
),
|
|
978
|
+
/* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsxs4(Text4, { color: "gray", children: [
|
|
979
|
+
"\u6309 ",
|
|
980
|
+
/* @__PURE__ */ jsx4(Text4, { color: "white", children: "Enter" }),
|
|
981
|
+
" \u8FD4\u56DE\u83DC\u5355\uFF0C\u6216",
|
|
982
|
+
" ",
|
|
983
|
+
/* @__PURE__ */ jsx4(Text4, { color: "red", children: "q" }),
|
|
984
|
+
" \u9000\u51FA\u7A0B\u5E8F"
|
|
985
|
+
] }) })
|
|
986
|
+
] });
|
|
987
|
+
}
|
|
988
|
+
var init_LeavePage = __esm({
|
|
989
|
+
"src/page/LeavePage.tsx"() {
|
|
990
|
+
"use strict";
|
|
991
|
+
}
|
|
992
|
+
});
|
|
993
|
+
|
|
994
|
+
// src/storage/apiKey.ts
|
|
995
|
+
import fs from "fs/promises";
|
|
996
|
+
import path from "path";
|
|
997
|
+
async function readJsonFile(filePath) {
|
|
998
|
+
try {
|
|
999
|
+
const raw = await fs.readFile(filePath, "utf8");
|
|
1000
|
+
const parsed = JSON.parse(raw);
|
|
1001
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
1002
|
+
return parsed;
|
|
1003
|
+
} catch {
|
|
1004
|
+
return null;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
async function ensureDirForFile(filePath) {
|
|
1008
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
1009
|
+
}
|
|
1010
|
+
async function loadStoredApiKey() {
|
|
1011
|
+
const absolute = path.resolve(process.cwd(), KEY_RELATIVE_PATH);
|
|
1012
|
+
const json = await readJsonFile(absolute);
|
|
1013
|
+
const key = (json?.apiKey ?? "").trim();
|
|
1014
|
+
return key.length > 0 ? key : null;
|
|
1015
|
+
}
|
|
1016
|
+
async function saveStoredApiKey(apiKey) {
|
|
1017
|
+
const absolute = path.resolve(process.cwd(), KEY_RELATIVE_PATH);
|
|
1018
|
+
await ensureDirForFile(absolute);
|
|
1019
|
+
const payload = { apiKey };
|
|
1020
|
+
await fs.writeFile(absolute, `${JSON.stringify(payload, null, 2)}
|
|
1021
|
+
`, "utf8");
|
|
1022
|
+
}
|
|
1023
|
+
var KEY_RELATIVE_PATH;
|
|
1024
|
+
var init_apiKey = __esm({
|
|
1025
|
+
"src/storage/apiKey.ts"() {
|
|
1026
|
+
"use strict";
|
|
1027
|
+
KEY_RELATIVE_PATH = path.join("src", "assets", "key.json");
|
|
1028
|
+
}
|
|
1029
|
+
});
|
|
1030
|
+
|
|
625
1031
|
// src/components/AiConsole.tsx
|
|
626
|
-
import { useMemo as
|
|
627
|
-
import { Box as
|
|
628
|
-
import { jsx as
|
|
1032
|
+
import { useEffect as useEffect6, useMemo as useMemo4, useState as useState6 } from "react";
|
|
1033
|
+
import { Box as Box5, Text as Text5, useInput as useInput6 } from "ink";
|
|
1034
|
+
import { Fragment, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
629
1035
|
function AiConsole(props) {
|
|
630
1036
|
const [input, setInput] = useState6("");
|
|
1037
|
+
const [apiKey, setApiKey] = useState6(null);
|
|
1038
|
+
const [keyDraft, setKeyDraft] = useState6("");
|
|
1039
|
+
const [keyStatus, setKeyStatus] = useState6("loading");
|
|
1040
|
+
useEffect6(() => {
|
|
1041
|
+
let cancelled = false;
|
|
1042
|
+
void (async () => {
|
|
1043
|
+
const stored = await loadStoredApiKey();
|
|
1044
|
+
if (cancelled) return;
|
|
1045
|
+
if (stored) {
|
|
1046
|
+
setApiKey(stored);
|
|
1047
|
+
setKeyStatus("ready");
|
|
1048
|
+
} else {
|
|
1049
|
+
setKeyStatus("missing");
|
|
1050
|
+
}
|
|
1051
|
+
})();
|
|
1052
|
+
return () => {
|
|
1053
|
+
cancelled = true;
|
|
1054
|
+
};
|
|
1055
|
+
}, []);
|
|
631
1056
|
const agent = useAiAgent({
|
|
632
1057
|
localName: props.localName,
|
|
633
1058
|
peerName: props.peerName,
|
|
634
|
-
onStartCountdown: props.onStartCountdown
|
|
1059
|
+
onStartCountdown: props.onStartCountdown,
|
|
1060
|
+
onThrowProjectile: props.onThrowProjectile,
|
|
1061
|
+
apiKey: apiKey ?? void 0
|
|
635
1062
|
});
|
|
636
|
-
const helpLine =
|
|
637
|
-
() => "\u793A\u4F8B\uFF1A\u5012\u8BA1\u65F620\u5206\u949F /
|
|
1063
|
+
const helpLine = useMemo4(
|
|
1064
|
+
() => "\u793A\u4F8B\uFF1A\u5012\u8BA1\u65F620\u5206\u949F / \u804A\u4F1A\u5929 / \u548C\u522B\u4EBA\u4E92\u52A8\u4E00\u4E0B",
|
|
638
1065
|
[]
|
|
639
1066
|
);
|
|
640
|
-
|
|
1067
|
+
useInput6(
|
|
641
1068
|
(ch, key) => {
|
|
642
1069
|
if (key.escape) {
|
|
643
1070
|
props.onClose();
|
|
644
1071
|
return;
|
|
645
1072
|
}
|
|
1073
|
+
if (keyStatus !== "ready") {
|
|
1074
|
+
if (key.return) {
|
|
1075
|
+
const draft = keyDraft.trim();
|
|
1076
|
+
if (!draft) return;
|
|
1077
|
+
setKeyStatus("saving");
|
|
1078
|
+
void (async () => {
|
|
1079
|
+
await saveStoredApiKey(draft);
|
|
1080
|
+
setApiKey(draft);
|
|
1081
|
+
setKeyDraft("");
|
|
1082
|
+
setKeyStatus("ready");
|
|
1083
|
+
})();
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
if (key.backspace || key.delete) {
|
|
1087
|
+
setKeyDraft((s) => s.slice(0, -1));
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
if (key.ctrl || key.meta) return;
|
|
1091
|
+
if (ch) setKeyDraft((s) => s + ch);
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
646
1094
|
if (key.return) {
|
|
647
1095
|
const line = input.trim();
|
|
648
1096
|
setInput("");
|
|
@@ -659,73 +1107,265 @@ function AiConsole(props) {
|
|
|
659
1107
|
},
|
|
660
1108
|
{ isActive: true }
|
|
661
1109
|
);
|
|
662
|
-
const lines = agent.lines.slice(-
|
|
663
|
-
return /* @__PURE__ */
|
|
664
|
-
/* @__PURE__ */
|
|
665
|
-
/* @__PURE__ */
|
|
666
|
-
/* @__PURE__ */
|
|
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}`))
|
|
1110
|
+
const lines = agent.lines.filter((l) => l.text.trim().length > 0).slice(-6);
|
|
1111
|
+
return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", borderStyle: "round", paddingX: 1, paddingY: 0, children: [
|
|
1112
|
+
/* @__PURE__ */ jsxs5(Box5, { justifyContent: "space-between", marginBottom: 0, children: [
|
|
1113
|
+
/* @__PURE__ */ jsx5(Text5, { color: "cyan", children: "AI Console" }),
|
|
1114
|
+
/* @__PURE__ */ jsx5(Text5, { color: "gray", children: keyStatus === "saving" ? "Saving\u2026" : agent.busy ? "Thinking\u2026" : "Esc Close" })
|
|
672
1115
|
] }),
|
|
673
|
-
/* @__PURE__ */
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
1116
|
+
/* @__PURE__ */ jsx5(Box5, { flexDirection: "column", children: keyStatus === "loading" ? /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "Checking API Key..." }) : keyStatus === "missing" || keyStatus === "saving" ? /* @__PURE__ */ jsxs5(Text5, { color: "yellow", children: [
|
|
1117
|
+
"Setup: Enter DeepSeek API Key (saves to",
|
|
1118
|
+
" ",
|
|
1119
|
+
/* @__PURE__ */ jsx5(Text5, { color: "cyan", children: "src/assets/key.json" }),
|
|
1120
|
+
")"
|
|
1121
|
+
] }) : lines.length === 0 ? /* @__PURE__ */ jsx5(Text5, { color: "gray", children: helpLine }) : null }),
|
|
1122
|
+
/* @__PURE__ */ jsx5(Box5, { flexDirection: "column", marginTop: 0, minHeight: 6, children: keyStatus === "ready" ? /* @__PURE__ */ jsx5(Fragment, { children: lines.map((l, i) => /* @__PURE__ */ jsxs5(
|
|
1123
|
+
Text5,
|
|
1124
|
+
{
|
|
1125
|
+
color: l.kind === "user" ? "yellow" : "white",
|
|
1126
|
+
wrap: "truncate-end",
|
|
1127
|
+
children: [
|
|
1128
|
+
l.kind === "user" ? "> " : "",
|
|
1129
|
+
l.text
|
|
1130
|
+
]
|
|
1131
|
+
},
|
|
1132
|
+
`${l.kind}:${l.at}:${i}`
|
|
1133
|
+
)) }) : /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "Please enter API Key to proceed." }) }),
|
|
1134
|
+
/* @__PURE__ */ jsxs5(
|
|
1135
|
+
Box5,
|
|
1136
|
+
{
|
|
1137
|
+
marginTop: 0,
|
|
1138
|
+
borderStyle: "single",
|
|
1139
|
+
borderTop: true,
|
|
1140
|
+
borderBottom: false,
|
|
1141
|
+
borderLeft: false,
|
|
1142
|
+
borderRight: false,
|
|
1143
|
+
children: [
|
|
1144
|
+
/* @__PURE__ */ jsxs5(Text5, { color: "green", children: [
|
|
1145
|
+
">",
|
|
1146
|
+
" "
|
|
1147
|
+
] }),
|
|
1148
|
+
keyStatus === "ready" ? /* @__PURE__ */ jsx5(Text5, { children: input }) : /* @__PURE__ */ jsx5(Text5, { children: keyDraft.length === 0 ? "" : "*".repeat(Math.min(64, keyDraft.length)) })
|
|
1149
|
+
]
|
|
1150
|
+
}
|
|
1151
|
+
)
|
|
680
1152
|
] });
|
|
681
1153
|
}
|
|
682
1154
|
var init_AiConsole = __esm({
|
|
683
1155
|
"src/components/AiConsole.tsx"() {
|
|
684
1156
|
"use strict";
|
|
685
1157
|
init_hooks();
|
|
1158
|
+
init_apiKey();
|
|
686
1159
|
}
|
|
687
1160
|
});
|
|
688
1161
|
|
|
689
|
-
// src/components/
|
|
690
|
-
import { Box as
|
|
691
|
-
import { jsx as
|
|
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";
|
|
1162
|
+
// src/components/sprite/BuddyAvatar.tsx
|
|
1163
|
+
import { Box as Box6, Text as Text6 } from "ink";
|
|
1164
|
+
import { jsx as jsx6 } from "react/jsx-runtime";
|
|
701
1165
|
function BuddyAvatar(props) {
|
|
702
|
-
const
|
|
703
|
-
|
|
1166
|
+
const sprite = SPRITES[props.state];
|
|
1167
|
+
if (props.variant === "compact") {
|
|
1168
|
+
return /* @__PURE__ */ jsx6(Box6, { marginTop: props.marginTop ?? 1, children: /* @__PURE__ */ jsx6(Text6, { color: sprite.color, children: sprite.compact }) });
|
|
1169
|
+
}
|
|
1170
|
+
return /* @__PURE__ */ jsx6(Box6, { flexDirection: "column", marginTop: props.marginTop ?? 1, children: sprite.frames.map((line, i) => /* @__PURE__ */ jsx6(Text6, { color: sprite.color, children: line }, `${props.state}:${i}`)) });
|
|
704
1171
|
}
|
|
705
|
-
var
|
|
1172
|
+
var SPRITES;
|
|
706
1173
|
var init_BuddyAvatar = __esm({
|
|
707
|
-
"src/components/BuddyAvatar.tsx"() {
|
|
1174
|
+
"src/components/sprite/BuddyAvatar.tsx"() {
|
|
708
1175
|
"use strict";
|
|
709
|
-
|
|
1176
|
+
SPRITES = {
|
|
710
1177
|
TYPING: {
|
|
711
1178
|
color: "green",
|
|
712
|
-
|
|
1179
|
+
compact: "( >_<)===3",
|
|
1180
|
+
frames: [" /\\_/\\ ", "( >_<) ", " /|_|\\\\ ", " / \\\\ "]
|
|
713
1181
|
},
|
|
714
1182
|
IDLE: {
|
|
715
1183
|
color: "yellow",
|
|
716
|
-
|
|
1184
|
+
compact: "( -.-)Zzz",
|
|
1185
|
+
frames: [" /\\_/\\ ", "( -.-) ", " /|_|\\\\ ", " / \\\\ "]
|
|
717
1186
|
},
|
|
718
1187
|
OFFLINE: {
|
|
719
1188
|
color: "gray",
|
|
720
|
-
|
|
1189
|
+
compact: "( x_x)",
|
|
1190
|
+
frames: [" /\\_/\\ ", "( x_x) ", " /|_|\\\\ ", " / \\\\ "]
|
|
721
1191
|
}
|
|
722
1192
|
};
|
|
723
1193
|
}
|
|
724
1194
|
});
|
|
725
1195
|
|
|
1196
|
+
// src/components/sprite/CountdownClockSprite.tsx
|
|
1197
|
+
import { useMemo as useMemo5 } from "react";
|
|
1198
|
+
import { Box as Box7, Text as Text7 } from "ink";
|
|
1199
|
+
import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1200
|
+
function countdownClockTypeFromMinutes(minutes) {
|
|
1201
|
+
if (minutes <= 10) return "SHORT";
|
|
1202
|
+
if (minutes <= 30) return "MEDIUM";
|
|
1203
|
+
return "LONG";
|
|
1204
|
+
}
|
|
1205
|
+
function clamp01(n) {
|
|
1206
|
+
if (n <= 0) return 0;
|
|
1207
|
+
if (n >= 1) return 1;
|
|
1208
|
+
return n;
|
|
1209
|
+
}
|
|
1210
|
+
function handFromProgress(progress01) {
|
|
1211
|
+
const idx = Math.round(clamp01(progress01) * 7);
|
|
1212
|
+
const dirs = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"];
|
|
1213
|
+
return dirs[idx];
|
|
1214
|
+
}
|
|
1215
|
+
function renderClockFace(hand) {
|
|
1216
|
+
const lines = [
|
|
1217
|
+
" .---. ",
|
|
1218
|
+
" / \\ ",
|
|
1219
|
+
"| \u2022 |",
|
|
1220
|
+
" \\ / ",
|
|
1221
|
+
" '---' "
|
|
1222
|
+
].map((s) => s.split(""));
|
|
1223
|
+
const center = { r: 2, c: 4 };
|
|
1224
|
+
const handMap = {
|
|
1225
|
+
N: { r: 1, c: 4, ch: "|" },
|
|
1226
|
+
NE: { r: 1, c: 5, ch: "/" },
|
|
1227
|
+
E: { r: 2, c: 5, ch: "-" },
|
|
1228
|
+
SE: { r: 3, c: 5, ch: "\\" },
|
|
1229
|
+
S: { r: 3, c: 4, ch: "|" },
|
|
1230
|
+
SW: { r: 3, c: 3, ch: "/" },
|
|
1231
|
+
W: { r: 2, c: 3, ch: "-" },
|
|
1232
|
+
NW: { r: 1, c: 3, ch: "\\" }
|
|
1233
|
+
};
|
|
1234
|
+
const tip = handMap[hand];
|
|
1235
|
+
lines[center.r][center.c] = "\u2022";
|
|
1236
|
+
lines[tip.r][tip.c] = tip.ch;
|
|
1237
|
+
return lines.map((row) => row.join(""));
|
|
1238
|
+
}
|
|
1239
|
+
function renderCompactClockFace(hand) {
|
|
1240
|
+
const lines = [" .---. ", "| \u2022 |", " '---' "].map((s) => s.split(""));
|
|
1241
|
+
const center = { r: 1, c: 3 };
|
|
1242
|
+
const handMap = {
|
|
1243
|
+
N: { r: 0, c: 3, ch: "|" },
|
|
1244
|
+
NE: { r: 0, c: 4, ch: "/" },
|
|
1245
|
+
E: { r: 1, c: 5, ch: "-" },
|
|
1246
|
+
SE: { r: 2, c: 4, ch: "\\" },
|
|
1247
|
+
S: { r: 2, c: 3, ch: "|" },
|
|
1248
|
+
SW: { r: 2, c: 2, ch: "/" },
|
|
1249
|
+
W: { r: 1, c: 1, ch: "-" },
|
|
1250
|
+
NW: { r: 0, c: 2, ch: "\\" }
|
|
1251
|
+
};
|
|
1252
|
+
const tip = handMap[hand];
|
|
1253
|
+
lines[center.r][center.c] = "\u2022";
|
|
1254
|
+
lines[tip.r][tip.c] = tip.ch;
|
|
1255
|
+
return lines.map((row) => row.join(""));
|
|
1256
|
+
}
|
|
1257
|
+
function CountdownClockSprite(props) {
|
|
1258
|
+
const type = props.type ?? (typeof props.minutes === "number" ? countdownClockTypeFromMinutes(props.minutes) : "MEDIUM");
|
|
1259
|
+
const progress01 = useMemo5(() => {
|
|
1260
|
+
if (typeof props.totalSeconds !== "number" || props.totalSeconds <= 0 || props.remainingSeconds === null || typeof props.remainingSeconds !== "number") {
|
|
1261
|
+
return null;
|
|
1262
|
+
}
|
|
1263
|
+
return clamp01(props.remainingSeconds / props.totalSeconds);
|
|
1264
|
+
}, [props.remainingSeconds, props.totalSeconds]);
|
|
1265
|
+
const style = TYPE_STYLE[type];
|
|
1266
|
+
const hand = handFromProgress(progress01 ?? 1);
|
|
1267
|
+
const caption = props.label ?? style.label;
|
|
1268
|
+
if (props.variant === "COMPACT") {
|
|
1269
|
+
const face2 = renderCompactClockFace(hand);
|
|
1270
|
+
return /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", children: [
|
|
1271
|
+
face2.map((line, i) => /* @__PURE__ */ jsx7(Text7, { color: style.color, children: line }, `clock:compact:${type}:${hand}:${i}`)),
|
|
1272
|
+
props.showLabel === false ? null : /* @__PURE__ */ jsx7(Text7, { color: "gray", children: caption ?? " " })
|
|
1273
|
+
] });
|
|
1274
|
+
}
|
|
1275
|
+
const face = renderClockFace(hand);
|
|
1276
|
+
return /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", children: [
|
|
1277
|
+
face.map((line, i) => /* @__PURE__ */ jsx7(Text7, { color: style.color, children: line }, `clock:${type}:${hand}:${i}`)),
|
|
1278
|
+
props.showLabel === false ? null : /* @__PURE__ */ jsx7(Text7, { color: "gray", children: caption ?? " " })
|
|
1279
|
+
] });
|
|
1280
|
+
}
|
|
1281
|
+
var TYPE_STYLE;
|
|
1282
|
+
var init_CountdownClockSprite = __esm({
|
|
1283
|
+
"src/components/sprite/CountdownClockSprite.tsx"() {
|
|
1284
|
+
"use strict";
|
|
1285
|
+
TYPE_STYLE = {
|
|
1286
|
+
SHORT: { color: "green", label: "Sprint" },
|
|
1287
|
+
MEDIUM: { color: "cyan", label: "Focus" },
|
|
1288
|
+
LONG: { color: "magenta", label: "Deep" }
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
1291
|
+
});
|
|
1292
|
+
|
|
1293
|
+
// src/components/sprite/ProjectileThrowSprite.tsx
|
|
1294
|
+
import { useEffect as useEffect7, useMemo as useMemo6, useState as useState7 } from "react";
|
|
1295
|
+
import { Box as Box8, Text as Text8 } from "ink";
|
|
1296
|
+
import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
1297
|
+
function clamp012(n) {
|
|
1298
|
+
if (n <= 0) return 0;
|
|
1299
|
+
if (n >= 1) return 1;
|
|
1300
|
+
return n;
|
|
1301
|
+
}
|
|
1302
|
+
function renderTrack(width, pos, glyph) {
|
|
1303
|
+
const w = Math.max(8, Math.floor(width));
|
|
1304
|
+
const innerWidth = w - 2;
|
|
1305
|
+
if (pos < 0) return `|${new Array(innerWidth).fill("\xB7").join("")}|`;
|
|
1306
|
+
const clampedPos = Math.max(0, Math.min(innerWidth - 1, Math.floor(pos)));
|
|
1307
|
+
const track = new Array(innerWidth).fill("\xB7");
|
|
1308
|
+
track[clampedPos] = glyph;
|
|
1309
|
+
return `|${track.join("")}|`;
|
|
1310
|
+
}
|
|
1311
|
+
function ProjectileThrowSprite(props) {
|
|
1312
|
+
const direction = props.direction ?? "LEFT_TO_RIGHT";
|
|
1313
|
+
const width = props.width ?? 28;
|
|
1314
|
+
const durationMs = props.durationMs ?? 700;
|
|
1315
|
+
const [autoProgress, setAutoProgress] = useState7(null);
|
|
1316
|
+
const progress = typeof props.progress === "number" ? props.progress : autoProgress;
|
|
1317
|
+
useEffect7(() => {
|
|
1318
|
+
if (props.shotId === void 0) return;
|
|
1319
|
+
const startedAt = Date.now();
|
|
1320
|
+
setAutoProgress(0);
|
|
1321
|
+
const handle = setInterval(() => {
|
|
1322
|
+
const elapsed = Date.now() - startedAt;
|
|
1323
|
+
const next = clamp012(elapsed / Math.max(1, durationMs));
|
|
1324
|
+
setAutoProgress(next);
|
|
1325
|
+
if (next >= 1) {
|
|
1326
|
+
clearInterval(handle);
|
|
1327
|
+
props.onDone?.();
|
|
1328
|
+
}
|
|
1329
|
+
}, 33);
|
|
1330
|
+
return () => clearInterval(handle);
|
|
1331
|
+
}, [durationMs, props.onDone, props.shotId]);
|
|
1332
|
+
const projectile = PROJECTILES[props.kind];
|
|
1333
|
+
const track = useMemo6(() => {
|
|
1334
|
+
if (progress === null || !Number.isFinite(progress)) {
|
|
1335
|
+
return renderTrack(width, -1, " ");
|
|
1336
|
+
}
|
|
1337
|
+
const innerWidth = Math.max(8, Math.floor(width)) - 2;
|
|
1338
|
+
const rawPos = clamp012(progress) * (innerWidth - 1);
|
|
1339
|
+
const pos = direction === "LEFT_TO_RIGHT" ? rawPos : innerWidth - 1 - rawPos;
|
|
1340
|
+
return renderTrack(width, pos, projectile.glyph);
|
|
1341
|
+
}, [direction, progress, projectile.glyph, width]);
|
|
1342
|
+
return /* @__PURE__ */ jsxs7(Box8, { children: [
|
|
1343
|
+
props.leftLabel ? /* @__PURE__ */ jsxs7(Text8, { color: "gray", children: [
|
|
1344
|
+
props.leftLabel,
|
|
1345
|
+
" "
|
|
1346
|
+
] }) : null,
|
|
1347
|
+
/* @__PURE__ */ jsx8(Text8, { color: projectile.color, children: track }),
|
|
1348
|
+
props.rightLabel ? /* @__PURE__ */ jsxs7(Text8, { color: "gray", children: [
|
|
1349
|
+
" ",
|
|
1350
|
+
props.rightLabel
|
|
1351
|
+
] }) : null
|
|
1352
|
+
] });
|
|
1353
|
+
}
|
|
1354
|
+
var PROJECTILES;
|
|
1355
|
+
var init_ProjectileThrowSprite = __esm({
|
|
1356
|
+
"src/components/sprite/ProjectileThrowSprite.tsx"() {
|
|
1357
|
+
"use strict";
|
|
1358
|
+
PROJECTILES = {
|
|
1359
|
+
ROSE: { glyph: "\u{1F339}", color: "magenta" },
|
|
1360
|
+
POOP: { glyph: "\u{1F4A9}", color: "yellow" },
|
|
1361
|
+
HAMMER: { glyph: "\u{1F528}", color: "cyan" }
|
|
1362
|
+
};
|
|
1363
|
+
}
|
|
1364
|
+
});
|
|
1365
|
+
|
|
726
1366
|
// src/components/StatusHeader.tsx
|
|
727
|
-
import { Box as
|
|
728
|
-
import { jsx as
|
|
1367
|
+
import { Box as Box9, Text as Text9 } from "ink";
|
|
1368
|
+
import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
729
1369
|
function statusText(status) {
|
|
730
1370
|
switch (status) {
|
|
731
1371
|
case "waiting":
|
|
@@ -740,16 +1380,10 @@ function statusText(status) {
|
|
|
740
1380
|
}
|
|
741
1381
|
function StatusHeader(props) {
|
|
742
1382
|
const st = statusText(props.status);
|
|
743
|
-
return /* @__PURE__ */
|
|
744
|
-
/* @__PURE__ */
|
|
745
|
-
|
|
746
|
-
|
|
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
|
-
] });
|
|
1383
|
+
return /* @__PURE__ */ jsx9(Box9, { children: /* @__PURE__ */ jsxs8(Box9, { children: [
|
|
1384
|
+
/* @__PURE__ */ jsx9(Text9, { color: st.color, children: st.label }),
|
|
1385
|
+
props.role === "host" ? /* @__PURE__ */ jsx9(Text9, { color: "gray", children: props.tcpPort ? ` \u2014 TCP :${props.tcpPort}` : "" }) : /* @__PURE__ */ jsx9(Text9, { color: "gray", children: props.hostIp && props.tcpPort ? ` \u2014 ${props.hostIp}:${props.tcpPort}` : "" })
|
|
1386
|
+
] }) });
|
|
753
1387
|
}
|
|
754
1388
|
var init_StatusHeader = __esm({
|
|
755
1389
|
"src/components/StatusHeader.tsx"() {
|
|
@@ -762,21 +1396,31 @@ var init_components = __esm({
|
|
|
762
1396
|
"src/components/index.ts"() {
|
|
763
1397
|
"use strict";
|
|
764
1398
|
init_AiConsole();
|
|
765
|
-
init_AvatarDisplay();
|
|
766
1399
|
init_BuddyAvatar();
|
|
1400
|
+
init_CountdownClockSprite();
|
|
1401
|
+
init_ProjectileThrowSprite();
|
|
767
1402
|
init_StatusHeader();
|
|
768
1403
|
}
|
|
769
1404
|
});
|
|
770
1405
|
|
|
771
|
-
// src/
|
|
772
|
-
import { useCallback as useCallback4, useEffect as
|
|
773
|
-
import { Box as
|
|
774
|
-
import { jsx as
|
|
1406
|
+
// src/page/Session.tsx
|
|
1407
|
+
import { useCallback as useCallback4, useEffect as useEffect8, useMemo as useMemo7, useRef as useRef4, useState as useState8 } from "react";
|
|
1408
|
+
import { Box as Box10, Text as Text10, useInput as useInput7 } from "ink";
|
|
1409
|
+
import { Fragment as Fragment2, jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
1410
|
+
function formatMMSS(totalSeconds) {
|
|
1411
|
+
const m = Math.floor(totalSeconds / 60);
|
|
1412
|
+
const s = totalSeconds % 60;
|
|
1413
|
+
return `${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
|
|
1414
|
+
}
|
|
775
1415
|
function Session(props) {
|
|
776
|
-
const roomName =
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
1416
|
+
const roomName = useMemo7(
|
|
1417
|
+
() => `${props.localName}'s Room`,
|
|
1418
|
+
[props.localName]
|
|
1419
|
+
);
|
|
1420
|
+
const [showAi, setShowAi] = useState8(false);
|
|
1421
|
+
const [countdown, setCountdown] = useState8(null);
|
|
1422
|
+
const [shots, setShots] = useState8([]);
|
|
1423
|
+
const tcpOptions = useMemo7(() => {
|
|
780
1424
|
return props.role === "host" ? { role: "host", localName: props.localName } : {
|
|
781
1425
|
role: "client",
|
|
782
1426
|
localName: props.localName,
|
|
@@ -792,7 +1436,7 @@ function Session(props) {
|
|
|
792
1436
|
props.role === "client" ? props.hostName : ""
|
|
793
1437
|
]);
|
|
794
1438
|
const tcp = useTcpSync(tcpOptions);
|
|
795
|
-
const broadcasterOptions =
|
|
1439
|
+
const broadcasterOptions = useMemo7(() => {
|
|
796
1440
|
return props.role === "host" ? {
|
|
797
1441
|
enabled: true,
|
|
798
1442
|
hostName: props.localName,
|
|
@@ -805,89 +1449,272 @@ function Session(props) {
|
|
|
805
1449
|
const remoteActivity = tcp.remoteState ?? "OFFLINE";
|
|
806
1450
|
const onToggleAi = useCallback4(() => setShowAi((v) => !v), []);
|
|
807
1451
|
const onCloseAi = useCallback4(() => setShowAi(false), []);
|
|
808
|
-
|
|
1452
|
+
const sessionStartAtRef = useRef4(Date.now());
|
|
1453
|
+
const connectedStartAtRef = useRef4(null);
|
|
1454
|
+
const connectedTotalMsRef = useRef4(0);
|
|
1455
|
+
const keyPressesRef = useRef4(0);
|
|
1456
|
+
const useGlobalKeyboardRef = useRef4(false);
|
|
1457
|
+
const countKeyPress = useCallback4(() => {
|
|
1458
|
+
keyPressesRef.current += 1;
|
|
1459
|
+
}, []);
|
|
1460
|
+
useInput7(
|
|
1461
|
+
() => {
|
|
1462
|
+
if (!useGlobalKeyboardRef.current) countKeyPress();
|
|
1463
|
+
},
|
|
1464
|
+
{ isActive: true }
|
|
1465
|
+
);
|
|
1466
|
+
useEffect8(() => {
|
|
1467
|
+
const raw = process.env.TERMBUDDY_ACTIVITY_SOURCE ?? "ink";
|
|
1468
|
+
const source = raw === "xinput" ? "keyboard" : raw;
|
|
1469
|
+
if (source !== "keyboard") return;
|
|
1470
|
+
let cancelled = false;
|
|
1471
|
+
let unsub = null;
|
|
1472
|
+
void (async () => {
|
|
1473
|
+
const ok = await ensureGlobalKeyboard();
|
|
1474
|
+
if (cancelled) return;
|
|
1475
|
+
if (!ok) return;
|
|
1476
|
+
useGlobalKeyboardRef.current = true;
|
|
1477
|
+
unsub = subscribeGlobalKeydown(countKeyPress);
|
|
1478
|
+
})();
|
|
1479
|
+
return () => {
|
|
1480
|
+
cancelled = true;
|
|
1481
|
+
unsub?.();
|
|
1482
|
+
useGlobalKeyboardRef.current = false;
|
|
1483
|
+
};
|
|
1484
|
+
}, [countKeyPress]);
|
|
1485
|
+
useEffect8(() => {
|
|
1486
|
+
if (tcp.status === "connected") {
|
|
1487
|
+
if (connectedStartAtRef.current === null) {
|
|
1488
|
+
connectedStartAtRef.current = Date.now();
|
|
1489
|
+
}
|
|
1490
|
+
return;
|
|
1491
|
+
}
|
|
1492
|
+
if (connectedStartAtRef.current !== null) {
|
|
1493
|
+
connectedTotalMsRef.current += Date.now() - connectedStartAtRef.current;
|
|
1494
|
+
connectedStartAtRef.current = null;
|
|
1495
|
+
}
|
|
1496
|
+
}, [tcp.status]);
|
|
1497
|
+
const finishAndLeave = useCallback4(() => {
|
|
1498
|
+
const endedAt = Date.now();
|
|
1499
|
+
let connectedDurationMs = connectedTotalMsRef.current;
|
|
1500
|
+
if (connectedStartAtRef.current !== null) {
|
|
1501
|
+
connectedDurationMs += endedAt - connectedStartAtRef.current;
|
|
1502
|
+
}
|
|
1503
|
+
const stats = {
|
|
1504
|
+
keyPresses: keyPressesRef.current,
|
|
1505
|
+
sessionDurationMs: endedAt - sessionStartAtRef.current,
|
|
1506
|
+
connectedDurationMs,
|
|
1507
|
+
startedAt: sessionStartAtRef.current,
|
|
1508
|
+
endedAt,
|
|
1509
|
+
peerName: tcp.peerName
|
|
1510
|
+
};
|
|
1511
|
+
props.onLeave(stats);
|
|
1512
|
+
}, [props, tcp.peerName]);
|
|
1513
|
+
const startCountdown = useCallback4((minutes) => {
|
|
1514
|
+
const totalSeconds = Math.max(1, Math.floor(minutes * 60));
|
|
1515
|
+
const endsAt = Date.now() + totalSeconds * 1e3;
|
|
1516
|
+
const type = minutes <= 10 ? "SHORT" : minutes <= 30 ? "MEDIUM" : "LONG";
|
|
1517
|
+
setCountdown({
|
|
1518
|
+
minutes,
|
|
1519
|
+
totalSeconds,
|
|
1520
|
+
endsAt,
|
|
1521
|
+
remainingSeconds: totalSeconds,
|
|
1522
|
+
type
|
|
1523
|
+
});
|
|
1524
|
+
}, []);
|
|
1525
|
+
const throwProjectile = useCallback4(
|
|
1526
|
+
(kind, direction) => {
|
|
1527
|
+
const id = Date.now() + Math.floor(Math.random() * 1e3);
|
|
1528
|
+
setShots((prev) => [...prev, { id, kind, direction }]);
|
|
1529
|
+
},
|
|
1530
|
+
[]
|
|
1531
|
+
);
|
|
1532
|
+
useInput7(
|
|
809
1533
|
(input, key) => {
|
|
810
|
-
if (input === "q")
|
|
1534
|
+
if (input === "q") finishAndLeave();
|
|
811
1535
|
if (input === "/" && !key.ctrl && !key.meta) onToggleAi();
|
|
812
1536
|
},
|
|
813
1537
|
{ isActive: !showAi }
|
|
814
1538
|
);
|
|
815
|
-
|
|
1539
|
+
useInput7(
|
|
1540
|
+
(input) => {
|
|
1541
|
+
if (input === "x") setCountdown(null);
|
|
1542
|
+
},
|
|
1543
|
+
{ isActive: !showAi && countdown !== null }
|
|
1544
|
+
);
|
|
1545
|
+
const buddyName = props.role === "host" ? tcp.peerName ?? "Waiting..." : props.hostName ?? "Host";
|
|
816
1546
|
const localState = localActivity.state;
|
|
817
1547
|
const localLabel = props.role === "host" ? `${props.localName} (Host)` : `${props.localName} (Client)`;
|
|
818
|
-
|
|
1548
|
+
useEffect8(() => {
|
|
819
1549
|
if (tcp.status !== "connected") return;
|
|
820
1550
|
tcp.sendStatus(localState);
|
|
821
1551
|
}, [localState, tcp.status, tcp.sendStatus]);
|
|
822
|
-
|
|
823
|
-
|
|
1552
|
+
useEffect8(() => {
|
|
1553
|
+
if (!countdown) return;
|
|
1554
|
+
const endsAt = countdown.endsAt;
|
|
1555
|
+
const handle = setInterval(() => {
|
|
1556
|
+
const remaining = Math.max(0, Math.ceil((endsAt - Date.now()) / 1e3));
|
|
1557
|
+
setCountdown((prev) => {
|
|
1558
|
+
if (!prev) return prev;
|
|
1559
|
+
if (prev.endsAt !== endsAt) return prev;
|
|
1560
|
+
if (remaining <= 0) return null;
|
|
1561
|
+
if (prev.remainingSeconds === remaining) return prev;
|
|
1562
|
+
return { ...prev, remainingSeconds: remaining };
|
|
1563
|
+
});
|
|
1564
|
+
}, 250);
|
|
1565
|
+
return () => clearInterval(handle);
|
|
1566
|
+
}, [countdown?.endsAt]);
|
|
1567
|
+
return /* @__PURE__ */ jsxs9(Box10, { flexDirection: "column", padding: 1, children: [
|
|
1568
|
+
/* @__PURE__ */ jsx10(
|
|
824
1569
|
StatusHeader,
|
|
825
1570
|
{
|
|
826
1571
|
role: props.role,
|
|
827
1572
|
status: tcp.status,
|
|
828
1573
|
hostIp: props.role === "client" ? props.hostIp : void 0,
|
|
829
|
-
tcpPort: props.role === "client" ? props.tcpPort : tcp.listenPort
|
|
830
|
-
countdownLabel: countdown.label
|
|
1574
|
+
tcpPort: props.role === "client" ? props.tcpPort : tcp.listenPort
|
|
831
1575
|
}
|
|
832
1576
|
),
|
|
833
|
-
/* @__PURE__ */
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
1577
|
+
/* @__PURE__ */ jsxs9(
|
|
1578
|
+
Box10,
|
|
1579
|
+
{
|
|
1580
|
+
flexDirection: "row",
|
|
1581
|
+
alignItems: "center",
|
|
1582
|
+
justifyContent: "space-between",
|
|
1583
|
+
marginTop: 1,
|
|
1584
|
+
gap: 2,
|
|
1585
|
+
children: [
|
|
1586
|
+
/* @__PURE__ */ jsxs9(Box10, { flexDirection: "column", alignItems: "center", minWidth: 20, children: [
|
|
1587
|
+
countdown ? /* @__PURE__ */ jsxs9(Box10, { flexDirection: "column", alignItems: "center", marginBottom: 0, children: [
|
|
1588
|
+
/* @__PURE__ */ jsx10(Text10, { color: "gray", children: formatMMSS(countdown.remainingSeconds) }),
|
|
1589
|
+
/* @__PURE__ */ jsx10(
|
|
1590
|
+
CountdownClockSprite,
|
|
1591
|
+
{
|
|
1592
|
+
variant: "COMPACT",
|
|
1593
|
+
type: countdown.type,
|
|
1594
|
+
minutes: countdown.minutes,
|
|
1595
|
+
totalSeconds: countdown.totalSeconds,
|
|
1596
|
+
remainingSeconds: countdown.remainingSeconds,
|
|
1597
|
+
showLabel: false
|
|
1598
|
+
}
|
|
1599
|
+
)
|
|
1600
|
+
] }) : /* @__PURE__ */ jsx10(Box10, { height: 4 }),
|
|
1601
|
+
/* @__PURE__ */ jsx10(BuddyAvatar, { state: localState, marginTop: 0 }),
|
|
1602
|
+
/* @__PURE__ */ jsx10(Text10, { color: "cyan", children: localLabel })
|
|
1603
|
+
] }),
|
|
1604
|
+
/* @__PURE__ */ jsx10(
|
|
1605
|
+
Box10,
|
|
1606
|
+
{
|
|
1607
|
+
flexDirection: "column",
|
|
1608
|
+
alignItems: "center",
|
|
1609
|
+
flexGrow: 1,
|
|
1610
|
+
minWidth: 40,
|
|
1611
|
+
children: /* @__PURE__ */ jsxs9(Box10, { flexDirection: "column", width: "100%", alignItems: "center", children: [
|
|
1612
|
+
shots.map((s) => /* @__PURE__ */ jsx10(
|
|
1613
|
+
ProjectileThrowSprite,
|
|
1614
|
+
{
|
|
1615
|
+
kind: s.kind,
|
|
1616
|
+
direction: s.direction,
|
|
1617
|
+
shotId: s.id,
|
|
1618
|
+
width: 36,
|
|
1619
|
+
onDone: () => setShots((prev) => prev.filter((x) => x.id !== s.id))
|
|
1620
|
+
},
|
|
1621
|
+
String(s.id)
|
|
1622
|
+
)),
|
|
1623
|
+
shots.length === 0 ? /* @__PURE__ */ jsx10(Box10, { height: 1 }) : null
|
|
1624
|
+
] })
|
|
1625
|
+
}
|
|
1626
|
+
),
|
|
1627
|
+
/* @__PURE__ */ jsxs9(Box10, { flexDirection: "column", alignItems: "center", minWidth: 20, children: [
|
|
1628
|
+
/* @__PURE__ */ jsx10(Box10, { height: 4 }),
|
|
1629
|
+
/* @__PURE__ */ jsx10(BuddyAvatar, { state: remoteActivity, marginTop: 0 }),
|
|
1630
|
+
/* @__PURE__ */ jsx10(Text10, { color: "magenta", children: buddyName })
|
|
1631
|
+
] })
|
|
1632
|
+
]
|
|
1633
|
+
}
|
|
1634
|
+
),
|
|
1635
|
+
!showAi ? /* @__PURE__ */ jsx10(Box10, { marginTop: 1, justifyContent: "center", children: /* @__PURE__ */ jsxs9(Text10, { color: "gray", children: [
|
|
844
1636
|
"\u6309 ",
|
|
845
|
-
/* @__PURE__ */
|
|
846
|
-
" \u53EC\u5524 AI Console\uFF0C\u6309
|
|
847
|
-
|
|
848
|
-
"
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
1637
|
+
/* @__PURE__ */ jsx10(Text10, { color: "cyan", children: "/" }),
|
|
1638
|
+
" \u53EC\u5524 AI Console\uFF0C\u6309",
|
|
1639
|
+
" ",
|
|
1640
|
+
/* @__PURE__ */ jsx10(Text10, { color: "cyan", children: "q" }),
|
|
1641
|
+
" \u7ED3\u675F\u672C\u6B21\u966A\u4F34\u3002",
|
|
1642
|
+
countdown ? /* @__PURE__ */ jsxs9(Fragment2, { children: [
|
|
1643
|
+
" ",
|
|
1644
|
+
/* @__PURE__ */ jsxs9(Text10, { color: "gray", children: [
|
|
1645
|
+
"(\u5012\u8BA1\u65F6\u4E2D\uFF1A\u6309 ",
|
|
1646
|
+
/* @__PURE__ */ jsx10(Text10, { color: "cyan", children: "x" }),
|
|
1647
|
+
" \u53D6\u6D88)"
|
|
1648
|
+
] })
|
|
1649
|
+
] }) : null
|
|
1650
|
+
] }) }) : null,
|
|
1651
|
+
showAi ? /* @__PURE__ */ jsx10(
|
|
1652
|
+
Box10,
|
|
852
1653
|
{
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
1654
|
+
marginTop: 1,
|
|
1655
|
+
width: "100%",
|
|
1656
|
+
flexDirection: "row",
|
|
1657
|
+
justifyContent: "center",
|
|
1658
|
+
children: /* @__PURE__ */ jsx10(Box10, { width: 64, children: /* @__PURE__ */ jsx10(
|
|
1659
|
+
AiConsole,
|
|
1660
|
+
{
|
|
1661
|
+
onClose: onCloseAi,
|
|
1662
|
+
onStartCountdown: startCountdown,
|
|
1663
|
+
onThrowProjectile: throwProjectile,
|
|
1664
|
+
localName: props.localName,
|
|
1665
|
+
peerName: tcp.peerName ?? (props.role === "client" ? props.hostName : void 0) ?? "Buddy"
|
|
1666
|
+
}
|
|
1667
|
+
) })
|
|
857
1668
|
}
|
|
858
|
-
)
|
|
1669
|
+
) : null
|
|
859
1670
|
] });
|
|
860
1671
|
}
|
|
861
1672
|
var init_Session = __esm({
|
|
862
|
-
"src/
|
|
1673
|
+
"src/page/Session.tsx"() {
|
|
863
1674
|
"use strict";
|
|
864
1675
|
init_components();
|
|
865
1676
|
init_hooks();
|
|
1677
|
+
init_globalKeyboard();
|
|
866
1678
|
}
|
|
867
1679
|
});
|
|
868
1680
|
|
|
869
|
-
// src/
|
|
870
|
-
var
|
|
871
|
-
"src/
|
|
1681
|
+
// src/page/index.ts
|
|
1682
|
+
var init_page = __esm({
|
|
1683
|
+
"src/page/index.ts"() {
|
|
872
1684
|
"use strict";
|
|
873
1685
|
init_MainMenu();
|
|
1686
|
+
init_NicknamePrompt();
|
|
874
1687
|
init_RoomScanner();
|
|
1688
|
+
init_LeavePage();
|
|
875
1689
|
init_Session();
|
|
876
1690
|
}
|
|
877
1691
|
});
|
|
878
1692
|
|
|
879
1693
|
// src/app/App.tsx
|
|
880
|
-
import { useCallback as useCallback5, useMemo as
|
|
881
|
-
import
|
|
1694
|
+
import { useCallback as useCallback5, useMemo as useMemo8, useState as useState9 } from "react";
|
|
1695
|
+
import os3 from "os";
|
|
882
1696
|
import { useApp } from "ink";
|
|
883
|
-
import { jsx as
|
|
1697
|
+
import { jsx as jsx11 } from "react/jsx-runtime";
|
|
884
1698
|
function App() {
|
|
885
1699
|
const { exit } = useApp();
|
|
886
|
-
const [view, setView] =
|
|
887
|
-
const
|
|
1700
|
+
const [view, setView] = useState9({ name: "NICKNAME" });
|
|
1701
|
+
const [nickname, setNickname] = useState9(null);
|
|
1702
|
+
const localName = useMemo8(() => nickname ?? os3.hostname(), [nickname]);
|
|
888
1703
|
const goMenu = useCallback5(() => setView({ name: "MENU" }), []);
|
|
1704
|
+
if (view.name === "NICKNAME") {
|
|
1705
|
+
return /* @__PURE__ */ jsx11(
|
|
1706
|
+
NicknamePrompt,
|
|
1707
|
+
{
|
|
1708
|
+
onExit: () => exit(),
|
|
1709
|
+
onSubmit: (name) => {
|
|
1710
|
+
setNickname(name);
|
|
1711
|
+
setView({ name: "MENU" });
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
);
|
|
1715
|
+
}
|
|
889
1716
|
if (view.name === "MENU") {
|
|
890
|
-
return /* @__PURE__ */
|
|
1717
|
+
return /* @__PURE__ */ jsx11(
|
|
891
1718
|
MainMenu,
|
|
892
1719
|
{
|
|
893
1720
|
onHost: () => setView({ name: "SESSION", role: "host" }),
|
|
@@ -896,8 +1723,11 @@ function App() {
|
|
|
896
1723
|
}
|
|
897
1724
|
);
|
|
898
1725
|
}
|
|
1726
|
+
if (view.name === "LEAVE") {
|
|
1727
|
+
return /* @__PURE__ */ jsx11(LeavePage, { stats: view.stats, onBack: goMenu, onExit: () => exit() });
|
|
1728
|
+
}
|
|
899
1729
|
if (view.name === "SCANNING") {
|
|
900
|
-
return /* @__PURE__ */
|
|
1730
|
+
return /* @__PURE__ */ jsx11(
|
|
901
1731
|
RoomScanner,
|
|
902
1732
|
{
|
|
903
1733
|
onBack: goMenu,
|
|
@@ -914,14 +1744,21 @@ function App() {
|
|
|
914
1744
|
);
|
|
915
1745
|
}
|
|
916
1746
|
if (view.name === "SESSION" && view.role === "host") {
|
|
917
|
-
return /* @__PURE__ */
|
|
1747
|
+
return /* @__PURE__ */ jsx11(
|
|
1748
|
+
Session,
|
|
1749
|
+
{
|
|
1750
|
+
localName,
|
|
1751
|
+
role: "host",
|
|
1752
|
+
onLeave: (stats) => setView({ name: "LEAVE", stats })
|
|
1753
|
+
}
|
|
1754
|
+
);
|
|
918
1755
|
}
|
|
919
|
-
return /* @__PURE__ */
|
|
1756
|
+
return /* @__PURE__ */ jsx11(
|
|
920
1757
|
Session,
|
|
921
1758
|
{
|
|
922
1759
|
localName,
|
|
923
1760
|
role: "client",
|
|
924
|
-
|
|
1761
|
+
onLeave: (stats) => setView({ name: "LEAVE", stats }),
|
|
925
1762
|
hostIp: view.hostIp,
|
|
926
1763
|
tcpPort: view.tcpPort,
|
|
927
1764
|
roomName: view.roomName,
|
|
@@ -932,7 +1769,7 @@ function App() {
|
|
|
932
1769
|
var init_App = __esm({
|
|
933
1770
|
"src/app/App.tsx"() {
|
|
934
1771
|
"use strict";
|
|
935
|
-
|
|
1772
|
+
init_page();
|
|
936
1773
|
}
|
|
937
1774
|
});
|
|
938
1775
|
|
|
@@ -950,8 +1787,8 @@ var init_app = __esm({
|
|
|
950
1787
|
|
|
951
1788
|
// src/cli.tsx
|
|
952
1789
|
process.env.NODE_ENV ??= "production";
|
|
953
|
-
var
|
|
1790
|
+
var React9 = await import("react");
|
|
954
1791
|
var { render } = await import("ink");
|
|
955
1792
|
var { App: App2 } = await Promise.resolve().then(() => (init_app(), app_exports));
|
|
956
|
-
render(
|
|
1793
|
+
render(React9.createElement(App2));
|
|
957
1794
|
//# sourceMappingURL=cli.js.map
|