@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.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/views/MainMenu.tsx","../src/hooks/useActivityMonitor.ts","../src/hooks/useAiAgent.ts","../src/constants.ts","../src/net/broadcast.ts","../src/net/index.ts","../src/hooks/useBroadcaster.ts","../src/hooks/useCountdown.ts","../src/hooks/useScanner.ts","../src/hooks/useTcpSync.ts","../src/hooks/index.ts","../src/views/RoomScanner.tsx","../src/components/AiConsole.tsx","../src/components/AvatarDisplay.tsx","../src/components/BuddyAvatar.tsx","../src/components/StatusHeader.tsx","../src/components/index.ts","../src/views/Session.tsx","../src/views/index.ts","../src/app/App.tsx","../src/app/index.ts","../src/cli.tsx"],"sourcesContent":["import React from 'react';\nimport {Box, Text, useInput} from 'ink';\n\nexport function MainMenu(props: {onHost: () => void; onJoin: () => void; onExit: () => void}) {\n\tuseInput((input, key) => {\n\t\tif (key.escape || input === 'q') props.onExit();\n\t\tif (input === '1') props.onHost();\n\t\tif (input === '2') props.onJoin();\n\t});\n\n\treturn (\n\t\t<Box flexDirection=\"column\" padding={1}>\n\t\t\t<Text>\n\t\t\t\t{String.raw`\n████████╗███████╗██████╗ ███╗ ███╗██████╗ ██╗ ██╗██████╗ ██████╗ ██╗ ██╗\n╚══██╔══╝██╔════╝██╔══██╗████╗ ████║██╔══██╗██║ ██║██╔══██╗██╔══██╗╚██╗ ██╔╝\n ██║ █████╗ ██████╔╝██╔████╔██║██████╔╝██║ ██║██║ ██║██║ ██║ ╚████╔╝ \n ██║ ██╔══╝ ██╔══██╗██║╚██╔╝██║██╔══██╗██║ ██║██║ ██║██║ ██║ ╚██╔╝ \n ██║ ███████╗██║ ██║██║ ╚═╝ ██║██████╔╝╚██████╔╝██████╔╝██████╔╝ ██║ \n ╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚═╝ \n`}\n\t\t\t</Text>\n\t\t\t<Box flexDirection=\"column\" marginTop={1}>\n\t\t\t\t<Text>Terminal Body Doubling — 极简 / 极客 / 私密</Text>\n\t\t\t\t<Text> </Text>\n\t\t\t\t<Text>\n\t\t\t\t\t<Text color=\"cyan\">[1]</Text> 建房 (Host)\n\t\t\t\t</Text>\n\t\t\t\t<Text>\n\t\t\t\t\t<Text color=\"cyan\">[2]</Text> 加入 (Join)\n\t\t\t\t</Text>\n\t\t\t\t<Text>\n\t\t\t\t\t<Text color=\"cyan\">[q]</Text> 退出\n\t\t\t\t</Text>\n\t\t\t</Box>\n\t\t</Box>\n\t);\n}\n","import {useEffect, useRef, useState} from 'react';\nimport {useInput} from 'ink';\nimport type {ActivityState} from '../protocol.js';\n\nexport function useActivityMonitor(options?: {idleAfterMs?: number}): {state: ActivityState} {\n\tconst idleAfterMs = options?.idleAfterMs ?? 1500;\n\tconst [state, setState] = useState<ActivityState>('IDLE');\n\n\tconst lastActivityRef = useRef<number>(Date.now());\n\n\tuseInput(() => {\n\t\tlastActivityRef.current = Date.now();\n\t\tsetState('TYPING');\n\t});\n\n\tuseEffect(() => {\n\t\tconst id = setInterval(() => {\n\t\t\tconst delta = Date.now() - lastActivityRef.current;\n\t\t\tif (delta >= idleAfterMs) setState('IDLE');\n\t\t}, 200);\n\t\treturn () => clearInterval(id);\n\t}, [idleAfterMs]);\n\n\treturn {state};\n}\n","import {useCallback, useEffect, useRef, useState} from 'react';\nimport {createAgent, initChatModel, tool} from 'langchain';\n\ntype LineKind = 'user' | 'ai' | 'system';\nexport type AiLine = {kind: LineKind; text: string; at: number};\n\nfunction contentToText(content: unknown): string {\n\tif (typeof content === 'string') return content;\n\tif (!content) return '';\n\tif (Array.isArray(content)) {\n\t\treturn content\n\t\t\t.map((part) => {\n\t\t\t\tif (typeof part === 'string') return part;\n\t\t\t\tif (typeof part === 'object' && part && 'text' in part) return String((part as any).text ?? '');\n\t\t\t\treturn '';\n\t\t\t})\n\t\t\t.join('');\n\t}\n\tif (typeof content === 'object' && 'text' in (content as any)) return String((content as any).text ?? '');\n\treturn String(content);\n}\n\nfunction lastAiText(messages: unknown[]): string | null {\n\tfor (let i = messages.length - 1; i >= 0; i--) {\n\t\tconst m: any = messages[i];\n\t\tconst type = typeof m?.getType === 'function' ? m.getType() : typeof m?._getType === 'function' ? m._getType() : m?.type;\n\t\tif (type === 'ai') {\n\t\t\tconst t = contentToText(m?.content);\n\t\t\treturn t || '';\n\t\t}\n\t}\n\treturn null;\n}\n\nfunction createSystemPrompt(context: {localName: string; peerName: string}) {\n\treturn [\n\t\t'你是 TermBuddy 里的“壳中幽灵 (Ghost in the Shell)”。',\n\t\t'默认隐形;被 / 唤醒时出现。风格:极简、干练、少废话。',\n\t\t'你可以使用工具来操控应用功能(例如倒计时)。',\n\t\t'如果用户提到“倒计时/专注/计时/countdown”,优先调用 start_countdown。',\n\t\t`当前上下文:我叫 ${context.localName};同桌叫 ${context.peerName}。`\n\t].join('\\n');\n}\n\nexport function useAiAgent(options: {\n\tlocalName: string;\n\tpeerName: string;\n\tonStartCountdown?: (minutes: number) => void;\n}) {\n\tconst [lines, setLines] = useState<AiLine[]>([]);\n\tconst [busy, setBusy] = useState(false);\n\n\tconst agentRef = useRef<Awaited<ReturnType<typeof createAgent>> | null>(null);\n\tconst agentInitRef = useRef<Promise<Awaited<ReturnType<typeof createAgent>>> | null>(null);\n\tconst stateRef = useRef<{messages: unknown[]}>({messages: []});\n\tconst abortRef = useRef<AbortController | null>(null);\n\n\tconst append = useCallback((line: AiLine) => {\n\t\tsetLines((prev) => [...prev, line]);\n\t}, []);\n\n\tconst updateLine = useCallback((at: number, text: string) => {\n\t\tsetLines((prev) => {\n\t\t\tconst idx = prev.findIndex((l) => l.at === at);\n\t\t\tif (idx === -1) return prev;\n\t\t\tconst next = [...prev];\n\t\t\tnext[idx] = {...next[idx], text};\n\t\t\treturn next;\n\t\t});\n\t}, []);\n\n\tconst ensureAgent = useCallback(async () => {\n\t\tif (agentRef.current) return agentRef.current;\n\t\tagentInitRef.current ??= (async () => {\n\t\t\tconst startCountdown = tool(\n\t\t\t\tasync (input: {minutes: number}) => {\n\t\t\t\t\tconst minutes = Number(input.minutes);\n\t\t\t\t\tif (!Number.isFinite(minutes) || minutes <= 0) return '倒计时分钟数无效。';\n\t\t\t\t\toptions.onStartCountdown?.(minutes);\n\t\t\t\t\treturn `已开始倒计时 ${minutes} 分钟。`;\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tname: 'start_countdown',\n\t\t\t\t\tdescription: '开始一个专注倒计时(分钟)。',\n\t\t\t\t\tschema: {\n\t\t\t\t\t\ttype: 'object',\n\t\t\t\t\t\tproperties: {\n\t\t\t\t\t\t\tminutes: {type: 'integer', minimum: 1, maximum: 180, description: '倒计时分钟数'}\n\t\t\t\t\t\t},\n\t\t\t\t\t\trequired: ['minutes'],\n\t\t\t\t\t\tadditionalProperties: false\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t);\n\n\t\t\tconst sessionInfo = tool(\n\t\t\t\tasync () => {\n\t\t\t\t\treturn JSON.stringify(\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tlocalName: options.localName,\n\t\t\t\t\t\t\tpeerName: options.peerName\n\t\t\t\t\t\t},\n\t\t\t\t\t\tnull,\n\t\t\t\t\t\t2\n\t\t\t\t\t);\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tname: 'session_info',\n\t\t\t\t\tdescription: '获取当前会话上下文(本地昵称、同桌昵称)。',\n\t\t\t\t\tschema: {type: 'object', properties: {}, additionalProperties: false}\n\t\t\t\t}\n\t\t\t);\n\n\t\t\tconst modelId = process.env.TERMBUDDY_MODEL ?? 'openai:gpt-4o-mini';\n\t\t\tconst llm = await initChatModel(modelId, {\n\t\t\t\ttemperature: 0.2,\n\t\t\t\tmaxTokens: 800,\n\t\t\t\ttimeout: 30_000\n\t\t\t});\n\n\t\t\treturn createAgent({\n\t\t\t\tllm,\n\t\t\t\ttools: [startCountdown, sessionInfo],\n\t\t\t\tprompt: createSystemPrompt({localName: options.localName, peerName: options.peerName}),\n\t\t\t\tname: 'ghost'\n\t\t\t});\n\t\t})();\n\n\t\tagentRef.current = await agentInitRef.current;\n\t\treturn agentRef.current;\n\t}, [options.localName, options.onStartCountdown, options.peerName]);\n\n\tconst ask = useCallback(\n\t\tasync (text: string) => {\n\t\t\tappend({kind: 'user', text: `> ${text}`, at: Date.now()});\n\n\t\t\tconst aiAt = Date.now() + 1;\n\t\t\tappend({kind: 'ai', text: '…', at: aiAt});\n\n\t\t\tabortRef.current?.abort();\n\t\t\tabortRef.current = new AbortController();\n\n\t\t\tsetBusy(true);\n\t\t\ttry {\n\t\t\t\tconst agent = await ensureAgent();\n\t\t\t\tconst stream = await agent.stream(\n\t\t\t\t\t{\n\t\t\t\t\t\tmessages: [...stateRef.current.messages, {role: 'user', content: text}]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tstreamMode: 'values',\n\t\t\t\t\t\tsignal: abortRef.current.signal\n\t\t\t\t\t} as any\n\t\t\t\t);\n\n\t\t\t\tfor await (const chunk of stream as any) {\n\t\t\t\t\tconst messages = (chunk?.messages ?? []) as unknown[];\n\t\t\t\t\tif (messages.length > 0) stateRef.current.messages = messages;\n\t\t\t\t\tconst t = lastAiText(messages);\n\t\t\t\t\tif (t !== null) updateLine(aiAt, t);\n\t\t\t\t}\n\t\t\t} catch (e) {\n\t\t\t\tconst msg = e instanceof Error ? e.message : String(e);\n\t\t\t\tupdateLine(aiAt, `(AI 出错)${msg}`);\n\t\t\t} finally {\n\t\t\t\tsetBusy(false);\n\t\t\t}\n\t\t},\n\t\t[append, ensureAgent, updateLine]\n\t);\n\n\tuseEffect(() => {\n\t\treturn () => abortRef.current?.abort();\n\t}, []);\n\n\treturn {lines, ask, busy};\n}\n","export const UDP_PORT = 45888;\nexport const TCP_DEFAULT_PORT = 45999;\n\nexport const DISCOVERY_VERSION = 1;\nexport const APP_NAME = 'TermBuddy';\n","import os from 'node:os';\n\nfunction ipv4ToInt(ip: string) {\n\treturn ip\n\t\t.split('.')\n\t\t.map((n) => Number.parseInt(n, 10))\n\t\t.reduce((acc, n) => ((acc << 8) | (n & 255)) >>> 0, 0);\n}\n\nfunction intToIpv4(n: number) {\n\treturn [24, 16, 8, 0].map((shift) => String((n >>> shift) & 255)).join('.');\n}\n\nexport function getBroadcastTargets(): string[] {\n\tconst out = new Set<string>(['255.255.255.255']);\n\n\tconst ifaces = os.networkInterfaces();\n\tfor (const entries of Object.values(ifaces)) {\n\t\tif (!entries) continue;\n\t\tfor (const e of entries) {\n\t\t\tif (e.family !== 'IPv4') continue;\n\t\t\tif (e.internal) continue;\n\t\t\tif (!e.address || !e.netmask) continue;\n\t\t\tconst ip = ipv4ToInt(e.address);\n\t\t\tconst mask = ipv4ToInt(e.netmask);\n\t\t\tconst broadcast = (ip | (~mask >>> 0)) >>> 0;\n\t\t\tout.add(intToIpv4(broadcast));\n\t\t}\n\t}\n\n\treturn [...out];\n}\n","export {getBroadcastTargets} from './broadcast.js';\n\n","import {useEffect} from 'react';\nimport dgram from 'node:dgram';\nimport {UDP_PORT, DISCOVERY_VERSION} from '../constants.js';\nimport type {DiscoveryPacket} from '../protocol.js';\nimport {getBroadcastTargets} from '../net/index.js';\n\ntype Options =\n\t| {enabled: false}\n\t| {enabled: true; hostName: string; roomName: string; tcpPort?: number | null; intervalMs?: number};\n\nexport function useBroadcaster(options: Options) {\n\tconst depKey = options.enabled\n\t\t? `${options.hostName}|${options.roomName}|${options.tcpPort ?? ''}|${options.intervalMs ?? 1000}`\n\t\t: 'disabled';\n\n\tuseEffect(() => {\n\t\tif (!options.enabled) return;\n\t\tif (!options.tcpPort) return;\n\n\t\tconst socket = dgram.createSocket('udp4');\n\t\tsocket.on('error', () => {});\n\n\t\tsocket.bind(() => {\n\t\t\tsocket.setBroadcast(true);\n\t\t});\n\n\t\tconst targets = getBroadcastTargets();\n\n\t\tconst send = () => {\n\t\t\tconst packet: DiscoveryPacket = {\n\t\t\t\ttype: 'termbuddy_discovery',\n\t\t\t\tversion: DISCOVERY_VERSION,\n\t\t\t\thostName: options.hostName,\n\t\t\t\troomName: options.roomName,\n\t\t\t\ttcpPort: options.tcpPort!,\n\t\t\t\tsentAt: Date.now()\n\t\t\t};\n\t\t\tconst msg = Buffer.from(JSON.stringify(packet));\n\t\t\tfor (const address of targets) {\n\t\t\t\tsocket.send(msg, UDP_PORT, address);\n\t\t\t}\n\t\t};\n\n\t\tsend();\n\t\tconst id = setInterval(send, options.intervalMs ?? 1000);\n\n\t\treturn () => {\n\t\t\tclearInterval(id);\n\t\t\tsocket.close();\n\t\t};\n\t}, [depKey]);\n}\n","import {useCallback, useEffect, useRef, useState} from 'react';\n\nfunction formatMMSS(totalSeconds: number) {\n\tconst m = Math.floor(totalSeconds / 60);\n\tconst s = totalSeconds % 60;\n\treturn `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;\n}\n\nexport function useCountdown() {\n\tconst [remainingSeconds, setRemainingSeconds] = useState<number | null>(null);\n\tconst timerRef = useRef<NodeJS.Timeout | null>(null);\n\n\tconst start = useCallback((minutes: number) => {\n\t\tconst seconds = Math.max(1, Math.floor(minutes * 60));\n\t\tsetRemainingSeconds(seconds);\n\t\tif (timerRef.current) clearInterval(timerRef.current);\n\t\ttimerRef.current = setInterval(() => {\n\t\t\tsetRemainingSeconds((prev) => {\n\t\t\t\tif (prev === null) return null;\n\t\t\t\tif (prev <= 1) return null;\n\t\t\t\treturn prev - 1;\n\t\t\t});\n\t\t}, 1000);\n\t}, []);\n\n\tuseEffect(() => {\n\t\tif (remainingSeconds !== null) return;\n\t\tif (timerRef.current) clearInterval(timerRef.current);\n\t\ttimerRef.current = null;\n\t}, [remainingSeconds]);\n\n\tuseEffect(() => {\n\t\treturn () => {\n\t\t\tif (timerRef.current) clearInterval(timerRef.current);\n\t\t};\n\t}, []);\n\n\treturn {\n\t\tstart,\n\t\tlabel: remainingSeconds === null ? null : formatMMSS(remainingSeconds)\n\t};\n}\n","import {useEffect, useState} from 'react';\nimport dgram from 'node:dgram';\nimport {UDP_PORT, DISCOVERY_VERSION} from '../constants.js';\nimport type {DiscoveryPacket} from '../protocol.js';\nimport type {DiscoveredRoom} from '../types.js';\n\nfunction safeParse(msg: Buffer): DiscoveryPacket | null {\n\ttry {\n\t\tconst parsed = JSON.parse(msg.toString('utf8')) as DiscoveryPacket;\n\t\tif (parsed?.type !== 'termbuddy_discovery') return null;\n\t\tif (parsed?.version !== DISCOVERY_VERSION) return null;\n\t\tif (!parsed.hostName || !parsed.roomName || !parsed.tcpPort) return null;\n\t\treturn parsed;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nexport function useScanner(options?: {staleAfterMs?: number}): DiscoveredRoom[] {\n\tconst staleAfterMs = options?.staleAfterMs ?? 3500;\n\tconst [rooms, setRooms] = useState<DiscoveredRoom[]>([]);\n\n\tuseEffect(() => {\n\t\tconst socket = dgram.createSocket('udp4');\n\t\tsocket.on('error', () => {});\n\n\t\tsocket.on('message', (msg, rinfo) => {\n\t\t\tconst packet = safeParse(msg);\n\t\t\tif (!packet) return;\n\n\t\t\tconst now = Date.now();\n\t\t\tsetRooms((prev) => {\n\t\t\t\tconst key = `${rinfo.address}:${packet.tcpPort}`;\n\t\t\t\tconst next = prev.filter((r) => `${r.ip}:${r.tcpPort}` !== key);\n\t\t\t\tnext.push({\n\t\t\t\t\tip: rinfo.address,\n\t\t\t\t\thostName: packet.hostName,\n\t\t\t\t\troomName: packet.roomName,\n\t\t\t\t\ttcpPort: packet.tcpPort,\n\t\t\t\t\tlastSeenAt: now\n\t\t\t\t});\n\t\t\t\treturn next;\n\t\t\t});\n\t\t});\n\n\t\tsocket.bind(UDP_PORT, () => {});\n\n\t\tconst prune = setInterval(() => {\n\t\t\tconst now = Date.now();\n\t\t\tsetRooms((prev) => prev.filter((r) => now - r.lastSeenAt <= staleAfterMs));\n\t\t}, 500);\n\n\t\treturn () => {\n\t\t\tclearInterval(prune);\n\t\t\tsocket.close();\n\t\t};\n\t}, [staleAfterMs]);\n\n\treturn rooms;\n}\n","import {useCallback, useEffect, useRef, useState} from 'react';\nimport net from 'node:net';\nimport type {ActivityState, ConnectionStatus, TcpPacket} from '../protocol.js';\nimport {TCP_DEFAULT_PORT} from '../constants.js';\n\ntype HostOptions = {role: 'host'; localName: string; port?: number};\ntype ClientOptions = {role: 'client'; localName: string; hostIp: string; tcpPort: number; hostName?: string};\ntype Options = HostOptions | ClientOptions;\n\nfunction writePacket(socket: net.Socket, packet: TcpPacket) {\n\tsocket.write(`${JSON.stringify(packet)}\\n`, 'utf8');\n}\n\nexport function useTcpSync(options: Options): {\n\tstatus: ConnectionStatus;\n\tlistenPort?: number;\n\tpeerName?: string;\n\tremoteState?: ActivityState;\n\tsendStatus: (state: ActivityState) => void;\n} {\n\tconst [status, setStatus] = useState<ConnectionStatus>(options.role === 'host' ? 'waiting' : 'connecting');\n\tconst [listenPort, setListenPort] = useState<number | undefined>(undefined);\n\tconst [peerName, setPeerName] = useState<string | undefined>(undefined);\n\tconst [remoteState, setRemoteState] = useState<ActivityState | undefined>(undefined);\n\n\tconst socketRef = useRef<net.Socket | null>(null);\n\tconst lastSeenRef = useRef<number>(Date.now());\n\tconst heartbeatRef = useRef<NodeJS.Timeout | null>(null);\n\n\tconst cleanupSocket = useCallback(() => {\n\t\tif (heartbeatRef.current) clearInterval(heartbeatRef.current);\n\t\theartbeatRef.current = null;\n\n\t\tconst s = socketRef.current;\n\t\tsocketRef.current = null;\n\t\tif (s && !s.destroyed) s.destroy();\n\t}, []);\n\n\tconst attachSocket = useCallback(\n\t\t(s: net.Socket) => {\n\t\t\tcleanupSocket();\n\t\t\tsocketRef.current = s;\n\t\t\tlastSeenRef.current = Date.now();\n\n\t\t\tsetStatus('connected');\n\t\t\tsetRemoteState('IDLE');\n\n\t\t\tlet buf = '';\n\t\t\ts.setNoDelay(true);\n\t\t\ts.setEncoding('utf8');\n\n\t\t\tconst onData = (chunk: string) => {\n\t\t\t\tbuf += chunk;\n\t\t\t\twhile (true) {\n\t\t\t\t\tconst idx = buf.indexOf('\\n');\n\t\t\t\t\tif (idx === -1) break;\n\t\t\t\t\tconst line = buf.slice(0, idx).trim();\n\t\t\t\t\tbuf = buf.slice(idx + 1);\n\t\t\t\t\tif (!line) continue;\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst packet = JSON.parse(line) as TcpPacket;\n\t\t\t\t\t\tlastSeenRef.current = Date.now();\n\t\t\t\t\t\tif (packet.type === 'hello') {\n\t\t\t\t\t\t\tif (options.role === 'host') setPeerName(packet.clientName);\n\t\t\t\t\t\t\telse setPeerName(packet.hostName);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (packet.type === 'status') setRemoteState(packet.state);\n\t\t\t\t\t\tif (packet.type === 'ping') writePacket(s, {type: 'pong', sentAt: Date.now()});\n\t\t\t\t\t\tif (packet.type === 'pong') {\n\t\t\t\t\t\t\t// no-op\n\t\t\t\t\t\t}\n\t\t\t\t\t} catch {\n\t\t\t\t\t\t// ignore\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t};\n\n\t\t\ts.on('data', onData);\n\t\t\ts.on('close', () => {\n\t\t\t\tsetStatus(options.role === 'host' ? 'waiting' : 'disconnected');\n\t\t\t\tsetRemoteState('OFFLINE');\n\t\t\t\tcleanupSocket();\n\t\t\t});\n\t\t\ts.on('error', () => {\n\t\t\t\tsetStatus(options.role === 'host' ? 'waiting' : 'disconnected');\n\t\t\t\tsetRemoteState('OFFLINE');\n\t\t\t});\n\n\t\t\t// Hello handshake.\n\t\t\twritePacket(s, {\n\t\t\t\ttype: 'hello',\n\t\t\t\thostName: options.role === 'host' ? options.localName : options.hostName ?? 'Host',\n\t\t\t\tclientName: options.role === 'client' ? options.localName : 'Client',\n\t\t\t\tsentAt: Date.now()\n\t\t\t});\n\n\t\t\theartbeatRef.current = setInterval(() => {\n\t\t\t\tconst sock = socketRef.current;\n\t\t\t\tif (!sock || sock.destroyed) return;\n\t\t\t\twritePacket(sock, {type: 'ping', sentAt: Date.now()});\n\t\t\t\tconst age = Date.now() - lastSeenRef.current;\n\t\t\t\tif (age > 6000) {\n\t\t\t\t\tsetStatus('disconnected');\n\t\t\t\t\tsetRemoteState('OFFLINE');\n\t\t\t\t\tcleanupSocket();\n\t\t\t\t}\n\t\t\t}, 2000);\n\t\t},\n\t\t[cleanupSocket, options]\n\t);\n\n\tuseEffect(() => {\n\t\tif (options.role === 'host') {\n\t\t\tconst server = net.createServer((socket) => {\n\t\t\t\tattachSocket(socket);\n\t\t\t});\n\n\t\t\tserver.on('error', () => {});\n\n\t\t\tserver.listen(options.port ?? TCP_DEFAULT_PORT, () => {\n\t\t\t\tconst address = server.address();\n\t\t\t\tif (address && typeof address === 'object') setListenPort(address.port);\n\t\t\t});\n\n\t\t\treturn () => {\n\t\t\t\tcleanupSocket();\n\t\t\t\tserver.close();\n\t\t\t};\n\t\t}\n\n\t\tsetStatus('connecting');\n\t\tconst socket = net.createConnection({host: options.hostIp, port: options.tcpPort}, () => {\n\t\t\tattachSocket(socket);\n\t\t});\n\t\tsocket.on('error', () => {\n\t\t\tsetStatus('disconnected');\n\t\t\tsetRemoteState('OFFLINE');\n\t\t});\n\n\t\treturn () => {\n\t\t\tsocket.destroy();\n\t\t\tcleanupSocket();\n\t\t};\n\t}, [attachSocket, cleanupSocket, options]);\n\n\tconst sendStatus = useCallback((state: ActivityState) => {\n\t\tconst socket = socketRef.current;\n\t\tif (!socket || socket.destroyed) return;\n\t\twritePacket(socket, {type: 'status', state, sentAt: Date.now()});\n\t}, []);\n\n\treturn {status, listenPort, peerName, remoteState, sendStatus};\n}\n","export {useActivityMonitor} from './useActivityMonitor.js';\nexport {useAiAgent} from './useAiAgent.js';\nexport {useBroadcaster} from './useBroadcaster.js';\nexport {useCountdown} from './useCountdown.js';\nexport {useScanner} from './useScanner.js';\nexport {useTcpSync} from './useTcpSync.js';\n\n","import React, {useMemo} from 'react';\nimport {Box, Text, useInput} from 'ink';\nimport {useScanner} from '../hooks/index.js';\nimport type {DiscoveredRoom} from '../types.js';\n\nexport function RoomScanner(props: {\n\tonSelectRoom: (room: DiscoveredRoom) => void;\n\tonBack: () => void;\n\tonExit: () => void;\n}) {\n\tconst rooms = useScanner();\n\n\tconst sortedRooms = useMemo(() => {\n\t\treturn [...rooms].sort((a, b) => b.lastSeenAt - a.lastSeenAt);\n\t}, [rooms]);\n\n\tuseInput((input, key) => {\n\t\tif (key.escape || input === 'b') props.onBack();\n\t\tif (input === 'q') props.onExit();\n\n\t\tconst index = Number.parseInt(input, 10);\n\t\tif (Number.isNaN(index)) return;\n\t\tconst room = sortedRooms[index - 1];\n\t\tif (!room) return;\n\t\tprops.onSelectRoom(room);\n\t});\n\n\treturn (\n\t\t<Box flexDirection=\"column\" padding={1}>\n\t\t\t<Text>\n\t\t\t\t<Text color=\"yellow\">正在扫描局域网...</Text> (按 <Text color=\"cyan\">b</Text> 返回,{' '}\n\t\t\t\t<Text color=\"cyan\">q</Text> 退出)\n\t\t\t</Text>\n\t\t\t<Box flexDirection=\"column\" marginTop={1}>\n\t\t\t\t{sortedRooms.length === 0 ? (\n\t\t\t\t\t<Text color=\"gray\">暂无房间广播。</Text>\n\t\t\t\t) : (\n\t\t\t\t\tsortedRooms.map((room, i) => (\n\t\t\t\t\t\t<Text key={`${room.ip}:${room.tcpPort}`}>\n\t\t\t\t\t\t\t<Text color=\"cyan\">[{i + 1}]</Text> {room.roomName} — {room.hostName} @ {room.ip}:{room.tcpPort}\n\t\t\t\t\t\t</Text>\n\t\t\t\t\t))\n\t\t\t\t)}\n\t\t\t</Box>\n\t\t</Box>\n\t);\n}\n","import React, {useMemo, useState} from 'react';\nimport {Box, Text, useInput} from 'ink';\nimport {useAiAgent} from '../hooks/index.js';\n\nexport function AiConsole(props: {\n\tonClose: () => void;\n\tonStartCountdown: (minutes: number) => void;\n\tlocalName: string;\n\tpeerName: string;\n}) {\n\tconst [input, setInput] = useState('');\n\tconst agent = useAiAgent({\n\t\tlocalName: props.localName,\n\t\tpeerName: props.peerName,\n\t\tonStartCountdown: props.onStartCountdown\n\t});\n\n\tconst helpLine = useMemo(\n\t\t() => '示例:倒计时20分钟 / countdown 20 / 问个技术问题',\n\t\t[]\n\t);\n\n\tuseInput(\n\t\t(ch, key) => {\n\t\t\tif (key.escape) {\n\t\t\t\tprops.onClose();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (key.return) {\n\t\t\t\tconst line = input.trim();\n\t\t\t\tsetInput('');\n\t\t\t\tif (!line) return;\n\t\t\t\tvoid agent.ask(line);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (key.backspace || key.delete) {\n\t\t\t\tsetInput((s) => s.slice(0, -1));\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (key.ctrl || key.meta) return;\n\t\t\tif (ch) setInput((s) => s + ch);\n\t\t},\n\t\t{isActive: true}\n\t);\n\n\tconst lines = agent.lines.slice(-12);\n\n\treturn (\n\t\t<Box flexDirection=\"column\" borderStyle=\"round\" paddingX={1} paddingY={0}>\n\t\t\t<Box justifyContent=\"space-between\">\n\t\t\t\t<Text color=\"cyan\">AI Console</Text>\n\t\t\t\t<Text color=\"gray\">{agent.busy ? 'Thinking…' : 'Esc 关闭'}</Text>\n\t\t\t</Box>\n\n\t\t\t<Box flexDirection=\"column\" marginTop={1}>\n\t\t\t\t<Text color=\"gray\">{helpLine}</Text>\n\t\t\t</Box>\n\n\t\t\t<Box flexDirection=\"column\" marginTop={1}>\n\t\t\t\t{lines.length === 0 ? <Text color=\"gray\">(幽灵还在壳里…)</Text> : null}\n\t\t\t\t{lines.map((l, i) => (\n\t\t\t\t\t<Text key={`${l.kind}:${l.at}:${i}`} color={l.kind === 'user' ? 'yellow' : 'white'}>\n\t\t\t\t\t\t{l.text}\n\t\t\t\t\t</Text>\n\t\t\t\t))}\n\t\t\t</Box>\n\n\t\t\t<Box marginTop={1}>\n\t\t\t\t<Text color=\"green\">{'>'} </Text>\n\t\t\t\t<Text>{input}</Text>\n\t\t\t</Box>\n\t\t</Box>\n\t);\n}\n","import React from 'react';\nimport {Box, Text} from 'ink';\nimport type {ActivityState} from '../protocol.js';\n\nconst AVATAR: Record<ActivityState, {text: string; color?: string}> = {\n\tTYPING: {text: '( >_<)===3', color: 'green'},\n\tIDLE: {text: '( -.-)Zzz', color: 'yellow'},\n\tOFFLINE: {text: '( x_x)', color: 'gray'}\n};\n\nexport function AvatarDisplay(props: {state: ActivityState}) {\n\tconst avatar = AVATAR[props.state];\n\treturn (\n\t\t<Box marginTop={1}>\n\t\t\t<Text color={avatar.color}>{avatar.text}</Text>\n\t\t</Box>\n\t);\n}\n","import React from 'react';\nimport {Box, Text} from 'ink';\nimport type {ActivityState} from '../protocol.js';\n\nconst FRAMES: Record<ActivityState, {color?: string; lines: string[]}> = {\n\tTYPING: {\n\t\tcolor: 'green',\n\t\tlines: [' /\\\\_/\\\\ ', '( >_<) ', ' /|_|\\\\\\\\ ', ' / \\\\\\\\ ']\n\t},\n\tIDLE: {\n\t\tcolor: 'yellow',\n\t\tlines: [' /\\\\_/\\\\ ', '( -.-) ', ' /|_|\\\\\\\\ ', ' / \\\\\\\\ ']\n\t},\n\tOFFLINE: {\n\t\tcolor: 'gray',\n\t\tlines: [' /\\\\_/\\\\ ', '( x_x) ', ' /|_|\\\\\\\\ ', ' / \\\\\\\\ ']\n\t}\n};\n\nexport function BuddyAvatar(props: {state: ActivityState}) {\n\tconst frame = FRAMES[props.state];\n\treturn (\n\t\t<Box flexDirection=\"column\" marginTop={1}>\n\t\t\t{frame.lines.map((line, i) => (\n\t\t\t\t<Text key={`${props.state}:${i}`} color={frame.color}>\n\t\t\t\t\t{line}\n\t\t\t\t</Text>\n\t\t\t))}\n\t\t</Box>\n\t);\n}\n\n","import React from 'react';\nimport {Box, Text} from 'ink';\nimport type {ConnectionStatus} from '../protocol.js';\n\nfunction statusText(status: ConnectionStatus) {\n\tswitch (status) {\n\t\tcase 'waiting':\n\t\t\treturn {label: 'Waiting', color: 'yellow'};\n\t\tcase 'connecting':\n\t\t\treturn {label: 'Connecting', color: 'yellow'};\n\t\tcase 'connected':\n\t\t\treturn {label: 'Connected via TCP', color: 'green'};\n\t\tcase 'disconnected':\n\t\t\treturn {label: 'Disconnected', color: 'red'};\n\t}\n}\n\nexport function StatusHeader(props: {\n\trole: 'host' | 'client';\n\tstatus: ConnectionStatus;\n\thostIp?: string;\n\ttcpPort?: number;\n\tcountdownLabel?: string | null;\n}) {\n\tconst st = statusText(props.status);\n\treturn (\n\t\t<Box justifyContent=\"space-between\">\n\t\t\t<Box>\n\t\t\t\t<Text color={st.color}>{st.label}</Text>\n\t\t\t\t{props.role === 'host' ? (\n\t\t\t\t\t<Text color=\"gray\">{props.tcpPort ? ` — TCP :${props.tcpPort}` : ''}</Text>\n\t\t\t\t) : (\n\t\t\t\t\t<Text color=\"gray\">\n\t\t\t\t\t\t{props.hostIp && props.tcpPort ? ` — ${props.hostIp}:${props.tcpPort}` : ''}\n\t\t\t\t\t</Text>\n\t\t\t\t)}\n\t\t\t</Box>\n\t\t\t<Box>\n\t\t\t\t{props.countdownLabel ? <Text color=\"cyan\">Focus {props.countdownLabel}</Text> : <Text> </Text>}\n\t\t\t</Box>\n\t\t</Box>\n\t);\n}\n","export {AiConsole} from './AiConsole.js';\nexport {AvatarDisplay} from './AvatarDisplay.js';\nexport {BuddyAvatar} from './BuddyAvatar.js';\nexport {StatusHeader} from './StatusHeader.js';\n","import React, {useCallback, useEffect, useMemo, useState} from 'react';\nimport {Box, Text, useInput} from 'ink';\nimport type {ActivityState} from '../protocol.js';\nimport {AiConsole, BuddyAvatar, StatusHeader} from '../components/index.js';\nimport {useActivityMonitor, useBroadcaster, useCountdown, useTcpSync} from '../hooks/index.js';\n\nexport function Session(\n\tprops:\n\t\t| {role: 'host'; localName: string; onExit: () => void}\n\t\t| {\n\t\t\t\trole: 'client';\n\t\t\t\tlocalName: string;\n\t\t\t\tonExit: () => void;\n\t\t\t\thostIp: string;\n\t\t\t\ttcpPort: number;\n\t\t\t\troomName: string;\n\t\t\t\thostName: string;\n\t\t }\n) {\n\tconst roomName = useMemo(() => `${props.localName}'s Room`, [props.localName]);\n\n\tconst [showAi, setShowAi] = useState(false);\n\tconst countdown = useCountdown();\n\n\tconst tcpOptions = useMemo(() => {\n\t\treturn props.role === 'host'\n\t\t\t? ({role: 'host', localName: props.localName} as const)\n\t\t\t: ({\n\t\t\t\t\trole: 'client',\n\t\t\t\t\tlocalName: props.localName,\n\t\t\t\t\thostIp: props.hostIp,\n\t\t\t\t\ttcpPort: props.tcpPort,\n\t\t\t\t\thostName: props.hostName\n\t\t\t } as const);\n\t}, [\n\t\tprops.role,\n\t\tprops.localName,\n\t\tprops.role === 'client' ? props.hostIp : '',\n\t\tprops.role === 'client' ? props.tcpPort : 0,\n\t\tprops.role === 'client' ? props.hostName : ''\n\t]);\n\n\tconst tcp = useTcpSync(tcpOptions);\n\n\tconst broadcasterOptions = useMemo(() => {\n\t\treturn props.role === 'host'\n\t\t\t? ({\n\t\t\t\t\tenabled: true,\n\t\t\t\t\thostName: props.localName,\n\t\t\t\t\troomName,\n\t\t\t\t\ttcpPort: tcp.listenPort\n\t\t\t } as const)\n\t\t\t: ({enabled: false} as const);\n\t}, [props.role, props.localName, roomName, tcp.listenPort]);\n\n\tuseBroadcaster(broadcasterOptions);\n\n\tconst localActivity = useActivityMonitor();\n\n\tconst remoteActivity: ActivityState = tcp.remoteState ?? 'OFFLINE';\n\n\tconst onToggleAi = useCallback(() => setShowAi((v) => !v), []);\n\tconst onCloseAi = useCallback(() => setShowAi(false), []);\n\n\tuseInput(\n\t\t(input, key) => {\n\t\t\tif (input === 'q') props.onExit();\n\t\t\tif (input === '/' && !key.ctrl && !key.meta) onToggleAi();\n\t\t},\n\t\t{isActive: !showAi}\n\t);\n\n\tconst buddyName =\n\t\tprops.role === 'host'\n\t\t\t? tcp.peerName ?? 'Waiting...'\n\t\t\t: `${props.hostName ?? 'Host'} (${props.roomName ?? 'Room'})`;\n\n\tconst localState = localActivity.state;\n\tconst localLabel = props.role === 'host' ? `${props.localName} (Host)` : `${props.localName} (Client)`;\n\n\t// Sync local activity state to peer.\n\tuseEffect(() => {\n\t\tif (tcp.status !== 'connected') return;\n\t\ttcp.sendStatus(localState);\n\t}, [localState, tcp.status, tcp.sendStatus]);\n\n\treturn (\n\t\t<Box flexDirection=\"column\" padding={1}>\n\t\t\t<StatusHeader\n\t\t\t\trole={props.role}\n\t\t\t\tstatus={tcp.status}\n\t\t\t\thostIp={props.role === 'client' ? props.hostIp : undefined}\n\t\t\t\ttcpPort={props.role === 'client' ? props.tcpPort : tcp.listenPort}\n\t\t\t\tcountdownLabel={countdown.label}\n\t\t\t/>\n\n\t\t\t<Box flexDirection=\"row\" gap={4} marginTop={1}>\n\t\t\t\t<Box flexDirection=\"column\" width=\"50%\">\n\t\t\t\t\t<Text color=\"cyan\">{localLabel}</Text>\n\t\t\t\t\t<BuddyAvatar state={localState} />\n\t\t\t\t</Box>\n\n\t\t\t\t<Box flexDirection=\"column\" width=\"50%\">\n\t\t\t\t\t<Text color=\"magenta\">{buddyName}</Text>\n\t\t\t\t\t<BuddyAvatar state={remoteActivity} />\n\t\t\t\t</Box>\n\t\t\t</Box>\n\n\t\t\t<Box marginTop={1}>\n\t\t\t\t<Text color=\"gray\">\n\t\t\t\t\t按 <Text color=\"cyan\">/</Text> 召唤 AI Console,按 <Text color=\"cyan\">q</Text> 返回菜单。\n\t\t\t\t</Text>\n\t\t\t</Box>\n\n\t\t\t{showAi ? (\n\t\t\t\t<Box marginTop={1}>\n\t\t\t\t\t<AiConsole\n\t\t\t\t\t\tonClose={onCloseAi}\n\t\t\t\t\t\tonStartCountdown={countdown.start}\n\t\t\t\t\t\tlocalName={props.localName}\n\t\t\t\t\t\tpeerName={tcp.peerName ?? (props.role === 'client' ? props.hostName : undefined) ?? 'Buddy'}\n\t\t\t\t\t/>\n\t\t\t\t</Box>\n\t\t\t) : null}\n\t\t</Box>\n\t);\n}\n","export {MainMenu} from './MainMenu.js';\nexport {RoomScanner} from './RoomScanner.js';\nexport {Session} from './Session.js';\n\n","import React, {useCallback, useMemo, useState} from 'react';\nimport os from 'node:os';\nimport {useApp} from 'ink';\nimport {MainMenu, RoomScanner, Session} from '../views/index.js';\n\ntype View =\n\t| {name: 'MENU'}\n\t| {name: 'SCANNING'}\n\t| {name: 'SESSION'; role: 'host'}\n\t| {name: 'SESSION'; role: 'client'; hostIp: string; tcpPort: number; roomName: string; hostName: string};\n\nexport function App() {\n\tconst {exit} = useApp();\n\tconst [view, setView] = useState<View>({name: 'MENU'});\n\n\tconst localName = useMemo(() => os.hostname(), []);\n\n\tconst goMenu = useCallback(() => setView({name: 'MENU'}), []);\n\n\tif (view.name === 'MENU') {\n\t\treturn (\n\t\t\t<MainMenu\n\t\t\t\tonHost={() => setView({name: 'SESSION', role: 'host'})}\n\t\t\t\tonJoin={() => setView({name: 'SCANNING'})}\n\t\t\t\tonExit={() => exit()}\n\t\t\t/>\n\t\t);\n\t}\n\n\tif (view.name === 'SCANNING') {\n\t\treturn (\n\t\t\t<RoomScanner\n\t\t\t\tonBack={goMenu}\n\t\t\t\tonExit={() => exit()}\n\t\t\t\tonSelectRoom={(room) =>\n\t\t\t\t\tsetView({\n\t\t\t\t\t\tname: 'SESSION',\n\t\t\t\t\t\trole: 'client',\n\t\t\t\t\t\thostIp: room.ip,\n\t\t\t\t\t\ttcpPort: room.tcpPort,\n\t\t\t\t\t\troomName: room.roomName,\n\t\t\t\t\t\thostName: room.hostName\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t/>\n\t\t);\n\t}\n\n\tif (view.name === 'SESSION' && view.role === 'host') {\n\t\treturn <Session localName={localName} role=\"host\" onExit={goMenu} />;\n\t}\n\n\treturn (\n\t\t<Session\n\t\t\tlocalName={localName}\n\t\t\trole=\"client\"\n\t\t\tonExit={goMenu}\n\t\t\thostIp={view.hostIp}\n\t\t\ttcpPort={view.tcpPort}\n\t\t\troomName={view.roomName}\n\t\t\thostName={view.hostName}\n\t\t/>\n\t);\n}\n","export {App} from './App.js';\n\n","process.env.NODE_ENV ??= 'production';\n\nconst React = await import('react');\nconst {render} = await import('ink');\nconst {App} = await import('./app/index.js');\n\nrender(React.createElement(App));\n"],"mappings":"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","names":["useInput","useEffect","useRef","useState","useEffect","useCallback","useEffect","useRef","useState","useEffect","useState","dgram","useCallback","useEffect","useRef","useState","socket","Box","Text","useInput","jsx","jsxs","useMemo","useState","Box","Text","useInput","jsx","jsxs","Box","Text","jsx","Box","Text","jsx","Box","Text","jsx","jsxs","useCallback","useEffect","useMemo","useState","Box","Text","useInput","jsx","jsxs","useCallback","useMemo","useState","os","jsx","React","App"]}
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@three333/termbuddy",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "type": "module",
6
+ "bin": {
7
+ "termbuddy": "dist/cli.js"
8
+ },
9
+ "scripts": {
10
+ "dev": "tsup --watch",
11
+ "build": "tsup",
12
+ "start": "node dist/cli.js"
13
+ },
14
+ "dependencies": {
15
+ "@langchain/core": "^1.1.8",
16
+ "@langchain/openai": "^1.2.0",
17
+ "ink": "^6.6.0",
18
+ "langchain": "^1.2.3",
19
+ "react": "^19.2.3"
20
+ },
21
+ "devDependencies": {
22
+ "@types/node": "^25.0.3",
23
+ "@types/react": "^19.2.7",
24
+ "ts-node": "^10.9.2",
25
+ "tsup": "^8.5.1",
26
+ "typescript": "^5.9.3"
27
+ }
28
+ }
@@ -0,0 +1,64 @@
1
+ import React, {useCallback, useMemo, useState} from 'react';
2
+ import os from 'node:os';
3
+ import {useApp} from 'ink';
4
+ import {MainMenu, RoomScanner, Session} from '../views/index.js';
5
+
6
+ type View =
7
+ | {name: 'MENU'}
8
+ | {name: 'SCANNING'}
9
+ | {name: 'SESSION'; role: 'host'}
10
+ | {name: 'SESSION'; role: 'client'; hostIp: string; tcpPort: number; roomName: string; hostName: string};
11
+
12
+ export function App() {
13
+ const {exit} = useApp();
14
+ const [view, setView] = useState<View>({name: 'MENU'});
15
+
16
+ const localName = useMemo(() => os.hostname(), []);
17
+
18
+ const goMenu = useCallback(() => setView({name: 'MENU'}), []);
19
+
20
+ if (view.name === 'MENU') {
21
+ return (
22
+ <MainMenu
23
+ onHost={() => setView({name: 'SESSION', role: 'host'})}
24
+ onJoin={() => setView({name: 'SCANNING'})}
25
+ onExit={() => exit()}
26
+ />
27
+ );
28
+ }
29
+
30
+ if (view.name === 'SCANNING') {
31
+ return (
32
+ <RoomScanner
33
+ onBack={goMenu}
34
+ onExit={() => exit()}
35
+ onSelectRoom={(room) =>
36
+ setView({
37
+ name: 'SESSION',
38
+ role: 'client',
39
+ hostIp: room.ip,
40
+ tcpPort: room.tcpPort,
41
+ roomName: room.roomName,
42
+ hostName: room.hostName
43
+ })
44
+ }
45
+ />
46
+ );
47
+ }
48
+
49
+ if (view.name === 'SESSION' && view.role === 'host') {
50
+ return <Session localName={localName} role="host" onExit={goMenu} />;
51
+ }
52
+
53
+ return (
54
+ <Session
55
+ localName={localName}
56
+ role="client"
57
+ onExit={goMenu}
58
+ hostIp={view.hostIp}
59
+ tcpPort={view.tcpPort}
60
+ roomName={view.roomName}
61
+ hostName={view.hostName}
62
+ />
63
+ );
64
+ }
@@ -0,0 +1,2 @@
1
+ export {App} from './App.js';
2
+
package/src/cli.tsx ADDED
@@ -0,0 +1,7 @@
1
+ process.env.NODE_ENV ??= 'production';
2
+
3
+ const React = await import('react');
4
+ const {render} = await import('ink');
5
+ const {App} = await import('./app/index.js');
6
+
7
+ render(React.createElement(App));
@@ -0,0 +1,77 @@
1
+ import React, {useMemo, useState} from 'react';
2
+ import {Box, Text, useInput} from 'ink';
3
+ import {useAiAgent} from '../hooks/index.js';
4
+
5
+ export function AiConsole(props: {
6
+ onClose: () => void;
7
+ onStartCountdown: (minutes: number) => void;
8
+ localName: string;
9
+ peerName: string;
10
+ }) {
11
+ const [input, setInput] = useState('');
12
+ const agent = useAiAgent({
13
+ localName: props.localName,
14
+ peerName: props.peerName,
15
+ onStartCountdown: props.onStartCountdown
16
+ });
17
+
18
+ const helpLine = useMemo(
19
+ () => '示例:倒计时20分钟 / countdown 20 / 问个技术问题',
20
+ []
21
+ );
22
+
23
+ useInput(
24
+ (ch, key) => {
25
+ if (key.escape) {
26
+ props.onClose();
27
+ return;
28
+ }
29
+
30
+ if (key.return) {
31
+ const line = input.trim();
32
+ setInput('');
33
+ if (!line) return;
34
+ void agent.ask(line);
35
+ return;
36
+ }
37
+
38
+ if (key.backspace || key.delete) {
39
+ setInput((s) => s.slice(0, -1));
40
+ return;
41
+ }
42
+
43
+ if (key.ctrl || key.meta) return;
44
+ if (ch) setInput((s) => s + ch);
45
+ },
46
+ {isActive: true}
47
+ );
48
+
49
+ const lines = agent.lines.slice(-12);
50
+
51
+ return (
52
+ <Box flexDirection="column" borderStyle="round" paddingX={1} paddingY={0}>
53
+ <Box justifyContent="space-between">
54
+ <Text color="cyan">AI Console</Text>
55
+ <Text color="gray">{agent.busy ? 'Thinking…' : 'Esc 关闭'}</Text>
56
+ </Box>
57
+
58
+ <Box flexDirection="column" marginTop={1}>
59
+ <Text color="gray">{helpLine}</Text>
60
+ </Box>
61
+
62
+ <Box flexDirection="column" marginTop={1}>
63
+ {lines.length === 0 ? <Text color="gray">(幽灵还在壳里…)</Text> : null}
64
+ {lines.map((l, i) => (
65
+ <Text key={`${l.kind}:${l.at}:${i}`} color={l.kind === 'user' ? 'yellow' : 'white'}>
66
+ {l.text}
67
+ </Text>
68
+ ))}
69
+ </Box>
70
+
71
+ <Box marginTop={1}>
72
+ <Text color="green">{'>'} </Text>
73
+ <Text>{input}</Text>
74
+ </Box>
75
+ </Box>
76
+ );
77
+ }
@@ -0,0 +1,18 @@
1
+ import React from 'react';
2
+ import {Box, Text} from 'ink';
3
+ import type {ActivityState} from '../protocol.js';
4
+
5
+ const AVATAR: Record<ActivityState, {text: string; color?: string}> = {
6
+ TYPING: {text: '( >_<)===3', color: 'green'},
7
+ IDLE: {text: '( -.-)Zzz', color: 'yellow'},
8
+ OFFLINE: {text: '( x_x)', color: 'gray'}
9
+ };
10
+
11
+ export function AvatarDisplay(props: {state: ActivityState}) {
12
+ const avatar = AVATAR[props.state];
13
+ return (
14
+ <Box marginTop={1}>
15
+ <Text color={avatar.color}>{avatar.text}</Text>
16
+ </Box>
17
+ );
18
+ }
@@ -0,0 +1,32 @@
1
+ import React from 'react';
2
+ import {Box, Text} from 'ink';
3
+ import type {ActivityState} from '../protocol.js';
4
+
5
+ const FRAMES: Record<ActivityState, {color?: string; lines: string[]}> = {
6
+ TYPING: {
7
+ color: 'green',
8
+ lines: [' /\\_/\\ ', '( >_<) ', ' /|_|\\\\ ', ' / \\\\ ']
9
+ },
10
+ IDLE: {
11
+ color: 'yellow',
12
+ lines: [' /\\_/\\ ', '( -.-) ', ' /|_|\\\\ ', ' / \\\\ ']
13
+ },
14
+ OFFLINE: {
15
+ color: 'gray',
16
+ lines: [' /\\_/\\ ', '( x_x) ', ' /|_|\\\\ ', ' / \\\\ ']
17
+ }
18
+ };
19
+
20
+ export function BuddyAvatar(props: {state: ActivityState}) {
21
+ const frame = FRAMES[props.state];
22
+ return (
23
+ <Box flexDirection="column" marginTop={1}>
24
+ {frame.lines.map((line, i) => (
25
+ <Text key={`${props.state}:${i}`} color={frame.color}>
26
+ {line}
27
+ </Text>
28
+ ))}
29
+ </Box>
30
+ );
31
+ }
32
+
@@ -0,0 +1,43 @@
1
+ import React from 'react';
2
+ import {Box, Text} from 'ink';
3
+ import type {ConnectionStatus} from '../protocol.js';
4
+
5
+ function statusText(status: ConnectionStatus) {
6
+ switch (status) {
7
+ case 'waiting':
8
+ return {label: 'Waiting', color: 'yellow'};
9
+ case 'connecting':
10
+ return {label: 'Connecting', color: 'yellow'};
11
+ case 'connected':
12
+ return {label: 'Connected via TCP', color: 'green'};
13
+ case 'disconnected':
14
+ return {label: 'Disconnected', color: 'red'};
15
+ }
16
+ }
17
+
18
+ export function StatusHeader(props: {
19
+ role: 'host' | 'client';
20
+ status: ConnectionStatus;
21
+ hostIp?: string;
22
+ tcpPort?: number;
23
+ countdownLabel?: string | null;
24
+ }) {
25
+ const st = statusText(props.status);
26
+ return (
27
+ <Box justifyContent="space-between">
28
+ <Box>
29
+ <Text color={st.color}>{st.label}</Text>
30
+ {props.role === 'host' ? (
31
+ <Text color="gray">{props.tcpPort ? ` — TCP :${props.tcpPort}` : ''}</Text>
32
+ ) : (
33
+ <Text color="gray">
34
+ {props.hostIp && props.tcpPort ? ` — ${props.hostIp}:${props.tcpPort}` : ''}
35
+ </Text>
36
+ )}
37
+ </Box>
38
+ <Box>
39
+ {props.countdownLabel ? <Text color="cyan">Focus {props.countdownLabel}</Text> : <Text> </Text>}
40
+ </Box>
41
+ </Box>
42
+ );
43
+ }
@@ -0,0 +1,4 @@
1
+ export {AiConsole} from './AiConsole.js';
2
+ export {AvatarDisplay} from './AvatarDisplay.js';
3
+ export {BuddyAvatar} from './BuddyAvatar.js';
4
+ export {StatusHeader} from './StatusHeader.js';
@@ -0,0 +1,5 @@
1
+ export const UDP_PORT = 45888;
2
+ export const TCP_DEFAULT_PORT = 45999;
3
+
4
+ export const DISCOVERY_VERSION = 1;
5
+ export const APP_NAME = 'TermBuddy';
@@ -0,0 +1,7 @@
1
+ export {useActivityMonitor} from './useActivityMonitor.js';
2
+ export {useAiAgent} from './useAiAgent.js';
3
+ export {useBroadcaster} from './useBroadcaster.js';
4
+ export {useCountdown} from './useCountdown.js';
5
+ export {useScanner} from './useScanner.js';
6
+ export {useTcpSync} from './useTcpSync.js';
7
+
@@ -0,0 +1,25 @@
1
+ import {useEffect, useRef, useState} from 'react';
2
+ import {useInput} from 'ink';
3
+ import type {ActivityState} from '../protocol.js';
4
+
5
+ export function useActivityMonitor(options?: {idleAfterMs?: number}): {state: ActivityState} {
6
+ const idleAfterMs = options?.idleAfterMs ?? 1500;
7
+ const [state, setState] = useState<ActivityState>('IDLE');
8
+
9
+ const lastActivityRef = useRef<number>(Date.now());
10
+
11
+ useInput(() => {
12
+ lastActivityRef.current = Date.now();
13
+ setState('TYPING');
14
+ });
15
+
16
+ useEffect(() => {
17
+ const id = setInterval(() => {
18
+ const delta = Date.now() - lastActivityRef.current;
19
+ if (delta >= idleAfterMs) setState('IDLE');
20
+ }, 200);
21
+ return () => clearInterval(id);
22
+ }, [idleAfterMs]);
23
+
24
+ return {state};
25
+ }
@@ -0,0 +1,177 @@
1
+ import {useCallback, useEffect, useRef, useState} from 'react';
2
+ import {createAgent, initChatModel, tool} from 'langchain';
3
+
4
+ type LineKind = 'user' | 'ai' | 'system';
5
+ export type AiLine = {kind: LineKind; text: string; at: number};
6
+
7
+ function contentToText(content: unknown): string {
8
+ if (typeof content === 'string') return content;
9
+ if (!content) return '';
10
+ if (Array.isArray(content)) {
11
+ return content
12
+ .map((part) => {
13
+ if (typeof part === 'string') return part;
14
+ if (typeof part === 'object' && part && 'text' in part) return String((part as any).text ?? '');
15
+ return '';
16
+ })
17
+ .join('');
18
+ }
19
+ if (typeof content === 'object' && 'text' in (content as any)) return String((content as any).text ?? '');
20
+ return String(content);
21
+ }
22
+
23
+ function lastAiText(messages: unknown[]): string | null {
24
+ for (let i = messages.length - 1; i >= 0; i--) {
25
+ const m: any = messages[i];
26
+ const type = typeof m?.getType === 'function' ? m.getType() : typeof m?._getType === 'function' ? m._getType() : m?.type;
27
+ if (type === 'ai') {
28
+ const t = contentToText(m?.content);
29
+ return t || '';
30
+ }
31
+ }
32
+ return null;
33
+ }
34
+
35
+ function createSystemPrompt(context: {localName: string; peerName: string}) {
36
+ return [
37
+ '你是 TermBuddy 里的“壳中幽灵 (Ghost in the Shell)”。',
38
+ '默认隐形;被 / 唤醒时出现。风格:极简、干练、少废话。',
39
+ '你可以使用工具来操控应用功能(例如倒计时)。',
40
+ '如果用户提到“倒计时/专注/计时/countdown”,优先调用 start_countdown。',
41
+ `当前上下文:我叫 ${context.localName};同桌叫 ${context.peerName}。`
42
+ ].join('\n');
43
+ }
44
+
45
+ export function useAiAgent(options: {
46
+ localName: string;
47
+ peerName: string;
48
+ onStartCountdown?: (minutes: number) => void;
49
+ }) {
50
+ const [lines, setLines] = useState<AiLine[]>([]);
51
+ const [busy, setBusy] = useState(false);
52
+
53
+ const agentRef = useRef<Awaited<ReturnType<typeof createAgent>> | null>(null);
54
+ const agentInitRef = useRef<Promise<Awaited<ReturnType<typeof createAgent>>> | null>(null);
55
+ const stateRef = useRef<{messages: unknown[]}>({messages: []});
56
+ const abortRef = useRef<AbortController | null>(null);
57
+
58
+ const append = useCallback((line: AiLine) => {
59
+ setLines((prev) => [...prev, line]);
60
+ }, []);
61
+
62
+ const updateLine = useCallback((at: number, text: string) => {
63
+ setLines((prev) => {
64
+ const idx = prev.findIndex((l) => l.at === at);
65
+ if (idx === -1) return prev;
66
+ const next = [...prev];
67
+ next[idx] = {...next[idx], text};
68
+ return next;
69
+ });
70
+ }, []);
71
+
72
+ const ensureAgent = useCallback(async () => {
73
+ if (agentRef.current) return agentRef.current;
74
+ agentInitRef.current ??= (async () => {
75
+ const startCountdown = tool(
76
+ async (input: {minutes: number}) => {
77
+ const minutes = Number(input.minutes);
78
+ if (!Number.isFinite(minutes) || minutes <= 0) return '倒计时分钟数无效。';
79
+ options.onStartCountdown?.(minutes);
80
+ return `已开始倒计时 ${minutes} 分钟。`;
81
+ },
82
+ {
83
+ name: 'start_countdown',
84
+ description: '开始一个专注倒计时(分钟)。',
85
+ schema: {
86
+ type: 'object',
87
+ properties: {
88
+ minutes: {type: 'integer', minimum: 1, maximum: 180, description: '倒计时分钟数'}
89
+ },
90
+ required: ['minutes'],
91
+ additionalProperties: false
92
+ }
93
+ }
94
+ );
95
+
96
+ const sessionInfo = tool(
97
+ async () => {
98
+ return JSON.stringify(
99
+ {
100
+ localName: options.localName,
101
+ peerName: options.peerName
102
+ },
103
+ null,
104
+ 2
105
+ );
106
+ },
107
+ {
108
+ name: 'session_info',
109
+ description: '获取当前会话上下文(本地昵称、同桌昵称)。',
110
+ schema: {type: 'object', properties: {}, additionalProperties: false}
111
+ }
112
+ );
113
+
114
+ const modelId = process.env.TERMBUDDY_MODEL ?? 'openai:gpt-4o-mini';
115
+ const llm = await initChatModel(modelId, {
116
+ temperature: 0.2,
117
+ maxTokens: 800,
118
+ timeout: 30_000
119
+ });
120
+
121
+ return createAgent({
122
+ llm,
123
+ tools: [startCountdown, sessionInfo],
124
+ prompt: createSystemPrompt({localName: options.localName, peerName: options.peerName}),
125
+ name: 'ghost'
126
+ });
127
+ })();
128
+
129
+ agentRef.current = await agentInitRef.current;
130
+ return agentRef.current;
131
+ }, [options.localName, options.onStartCountdown, options.peerName]);
132
+
133
+ const ask = useCallback(
134
+ async (text: string) => {
135
+ append({kind: 'user', text: `> ${text}`, at: Date.now()});
136
+
137
+ const aiAt = Date.now() + 1;
138
+ append({kind: 'ai', text: '…', at: aiAt});
139
+
140
+ abortRef.current?.abort();
141
+ abortRef.current = new AbortController();
142
+
143
+ setBusy(true);
144
+ try {
145
+ const agent = await ensureAgent();
146
+ const stream = await agent.stream(
147
+ {
148
+ messages: [...stateRef.current.messages, {role: 'user', content: text}]
149
+ },
150
+ {
151
+ streamMode: 'values',
152
+ signal: abortRef.current.signal
153
+ } as any
154
+ );
155
+
156
+ for await (const chunk of stream as any) {
157
+ const messages = (chunk?.messages ?? []) as unknown[];
158
+ if (messages.length > 0) stateRef.current.messages = messages;
159
+ const t = lastAiText(messages);
160
+ if (t !== null) updateLine(aiAt, t);
161
+ }
162
+ } catch (e) {
163
+ const msg = e instanceof Error ? e.message : String(e);
164
+ updateLine(aiAt, `(AI 出错)${msg}`);
165
+ } finally {
166
+ setBusy(false);
167
+ }
168
+ },
169
+ [append, ensureAgent, updateLine]
170
+ );
171
+
172
+ useEffect(() => {
173
+ return () => abortRef.current?.abort();
174
+ }, []);
175
+
176
+ return {lines, ask, busy};
177
+ }