diff --git a/.gitignore b/.gitignore index 2828485cecf3a0d277bb6268add2b4775486b215..5195488756ec35ac35f576cacc9eca0605d48cfc 100644 --- a/.gitignore +++ b/.gitignore @@ -8,11 +8,11 @@ VersperClaw # Prebuilt native binaries (tracked in claude-code/ reference) vendor/ -# Friend VRM desktop (Tauri) build artifacts -friend/frontend/node_modules/ -friend/frontend/dist/ -friend/frontend/src-tauri/target/ -friend/frontend/src-tauri/gen/ +# Friend VRM frontend (Vite + Tauri) build artifacts +src/components/friend/frontend/node_modules/ +src/components/friend/frontend/dist/ +src/components/friend/frontend/src-tauri/target/ +src/components/friend/frontend/src-tauri/gen/ # Python cache __pycache__/ diff --git a/scripts/build.ts b/scripts/build.ts index d03d2723da96e318ff63217931ea313b5b26f076..71ab97d2afa542f6105b0f943bbb86bd85b8fcda 100644 --- a/scripts/build.ts +++ b/scripts/build.ts @@ -118,6 +118,36 @@ const outfile = compile : dev ? './VersperClaw' : './cli' + +// ── Pre-step: build Friend VRM frontend ─────────────────────────────────────── +function buildFriendFrontend(): boolean { + const frontendDir = join(process.cwd(), 'src', 'components', 'friend', 'frontend') + const distIndex = join(frontendDir, 'dist', 'index.html') + if (existsSync(distIndex)) { + console.log('Friend frontend already built, skipping.') + return true + } + console.log('Building Friend VRM frontend...') + const proc = Bun.spawnSync({ + cmd: ['npm', 'run', 'build'], + cwd: frontendDir, + stdout: 'inherit', + stderr: 'inherit', + }) + if (proc.exitCode !== 0) { + console.error('Friend frontend build failed.') + return false + } + console.log('Friend frontend built.') + return true +} + +if (!buildFriendFrontend()) { + process.exit(1) +} + +// ───────────────────────────────────────────────────────────────────────────── + const buildTime = new Date().toISOString() const version = dev ? getDevVersion(pkg.version) : pkg.version diff --git a/src/commands.ts b/src/commands.ts index 9b2d1376fc41bfb329d369125a71ef4e9a8a8ab6..242ff46a4134f0bbadabddc25215f183fc492826 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -44,6 +44,7 @@ import skills from './commands/skills/index.js' import status from './commands/status/index.js' import tasks from './commands/tasks/index.js' import feishu from './commands/feishu/index.js' +import friend from './commands/friend/index.js' import goals from './commands/goal/index.js' import telegram from './commands/telegram/index.js' import teleport from './commands/teleport/index.js' @@ -309,6 +310,7 @@ const COMMANDS = memoize((): Command[] => [ theme, feedback, feishu, + friend, goals, review, ultrareview, diff --git a/src/commands/friend/friend.tsx b/src/commands/friend/friend.tsx index de69a80afc4393ea89f8e7d29fb59b80c556513e..e1cf320ec302bff131dbca7678de3c72a88b1e46 100644 --- a/src/commands/friend/friend.tsx +++ b/src/commands/friend/friend.tsx @@ -4,13 +4,26 @@ * Ink-based terminal UI that shows status and controls for the * Friend desktop pet feature. */ -import React, { useState, useEffect } from 'react' +import React, { useState } from 'react' import { Box, Text, useInput } from '../../ink.js' import { getPrefs, updatePrefs } from '../../friend/prefs.js' -import type { LocalJSXCommandOnDone, CommandResultDisplay } from '../../commands.js' +import { + launchTauri, + stopTauri, + getTauriProcess, +} from '../../friend/tauri-launcher.js' +import type { LocalJSXCommandOnDone, CommandResultDisplay } from '../../types/command.js' +const FRIEND_FRONTEND_DIR = '../../components/friend/frontend' const FRIEND_URL = 'http://127.0.0.1:3456/friend/' +function logger() { + return { + info: (msg: string) => console.log(`[Friend] ${msg}`), + warn: (msg: string) => console.warn(`[Friend] ${msg}`), + } +} + type Page = 'status' | 'help' export async function call( @@ -20,22 +33,31 @@ export async function call( ): Promise { const trimmed = args?.trim().toLowerCase() - // Non-interactive modes if (trimmed === 'start') { updatePrefs({ enabled: true }) + const alreadyRunning = !!getTauriProcess() + if (alreadyRunning) { + return ( + + Friend is already running. + Open {FRIEND_URL} in your browser. + + ) + } + launchTauri(FRIEND_FRONTEND_DIR, logger()) return ( - Friend companion enabled. - Start the server with: - bun run src/server/index.ts --port 3456 - Then open {FRIEND_URL} in a browser (or Tauri will launch automatically). + Starting Friend VRM companion... + A Tauri window will open. + Also available at: {FRIEND_URL} ) } if (trimmed === 'stop') { updatePrefs({ enabled: false }) - return Friend companion disabled. + stopTauri(logger()) + return Friend companion stopped. } if (trimmed === 'status') { @@ -43,7 +65,6 @@ export async function call( return } - // Interactive mode (no args or unrecognized) return } @@ -60,17 +81,17 @@ function FriendManager({ onDone }: { onDone: LocalJSXCommandOnDone }) { - {page === 'status' ? '🄵 Friend VRM Desktop Pet' : 'Help'} + Friend VRM Desktop Pet - {page === 'status' && } + Usage: - /friend Interactive status & controls - /friend start Enable & launch friend - /friend stop Disable friend + /friend Interactive status & controls + /friend start Launch friend window + /friend stop Stop friend window /friend status Quick status overview @@ -90,6 +111,7 @@ function FriendStatus({ }) { const statusDot = prefs.enabled ? '●' : '○' const statusColor = prefs.enabled ? 'green' : 'gray' + const running = !!getTauriProcess() return ( @@ -101,6 +123,12 @@ function FriendStatus({ {!compact && ( <> + {running && ( + + Window: + Running + + )} URL: {FRIEND_URL} @@ -121,4 +149,4 @@ function FriendStatus({ )} ) -} +} \ No newline at end of file diff --git a/friend/frontend/src/App.tsx b/src/components/friend/frontend/App.tsx similarity index 100% rename from friend/frontend/src/App.tsx rename to src/components/friend/frontend/App.tsx diff --git a/friend/frontend/src/api.ts b/src/components/friend/frontend/api.ts similarity index 100% rename from friend/frontend/src/api.ts rename to src/components/friend/frontend/api.ts diff --git a/friend/frontend/src/assets/lip-sync-profile.json b/src/components/friend/frontend/assets/lip-sync-profile.json similarity index 100% rename from friend/frontend/src/assets/lip-sync-profile.json rename to src/components/friend/frontend/assets/lip-sync-profile.json diff --git a/friend/frontend/src/components/ChatInput.tsx b/src/components/friend/frontend/components/ChatInput.tsx similarity index 100% rename from friend/frontend/src/components/ChatInput.tsx rename to src/components/friend/frontend/components/ChatInput.tsx diff --git a/friend/frontend/src/components/HistoryPanel.tsx b/src/components/friend/frontend/components/HistoryPanel.tsx similarity index 100% rename from friend/frontend/src/components/HistoryPanel.tsx rename to src/components/friend/frontend/components/HistoryPanel.tsx diff --git a/friend/frontend/src/components/MoodIndicator.tsx b/src/components/friend/frontend/components/MoodIndicator.tsx similarity index 100% rename from friend/frontend/src/components/MoodIndicator.tsx rename to src/components/friend/frontend/components/MoodIndicator.tsx diff --git a/friend/frontend/src/components/ResizeHandles.tsx b/src/components/friend/frontend/components/ResizeHandles.tsx similarity index 100% rename from friend/frontend/src/components/ResizeHandles.tsx rename to src/components/friend/frontend/components/ResizeHandles.tsx diff --git a/friend/frontend/src/components/SettingsPanel.tsx b/src/components/friend/frontend/components/SettingsPanel.tsx similarity index 100% rename from friend/frontend/src/components/SettingsPanel.tsx rename to src/components/friend/frontend/components/SettingsPanel.tsx diff --git a/friend/frontend/src/components/TextBubble.tsx b/src/components/friend/frontend/components/TextBubble.tsx similarity index 100% rename from friend/frontend/src/components/TextBubble.tsx rename to src/components/friend/frontend/components/TextBubble.tsx diff --git a/friend/frontend/src/components/VRMScene.tsx b/src/components/friend/frontend/components/VRMScene.tsx similarity index 100% rename from friend/frontend/src/components/VRMScene.tsx rename to src/components/friend/frontend/components/VRMScene.tsx diff --git a/friend/frontend/src/emote.ts b/src/components/friend/frontend/emote.ts similarity index 100% rename from friend/frontend/src/emote.ts rename to src/components/friend/frontend/emote.ts diff --git a/friend/frontend/src/hooks/usePassThrough.ts b/src/components/friend/frontend/hooks/usePassThrough.ts similarity index 100% rename from friend/frontend/src/hooks/usePassThrough.ts rename to src/components/friend/frontend/hooks/usePassThrough.ts diff --git a/friend/frontend/index.html b/src/components/friend/frontend/index.html similarity index 97% rename from friend/frontend/index.html rename to src/components/friend/frontend/index.html index 00c9c2f8bb2a96448e83a4f100f6d528a7c44072..151506ba72564b3831aa87095c95a94353b58677 100644 --- a/friend/frontend/index.html +++ b/src/components/friend/frontend/index.html @@ -96,7 +96,7 @@
- + diff --git a/friend/frontend/src/lip-sync.ts b/src/components/friend/frontend/lip-sync.ts similarity index 100% rename from friend/frontend/src/lip-sync.ts rename to src/components/friend/frontend/lip-sync.ts diff --git a/friend/frontend/src/main.tsx b/src/components/friend/frontend/main.tsx similarity index 100% rename from friend/frontend/src/main.tsx rename to src/components/friend/frontend/main.tsx diff --git a/friend/frontend/src/mixamo-loader.ts b/src/components/friend/frontend/mixamo-loader.ts similarity index 100% rename from friend/frontend/src/mixamo-loader.ts rename to src/components/friend/frontend/mixamo-loader.ts diff --git a/friend/frontend/src/mmd-parser.d.ts b/src/components/friend/frontend/mmd-parser.d.ts similarity index 100% rename from friend/frontend/src/mmd-parser.d.ts rename to src/components/friend/frontend/mmd-parser.d.ts diff --git a/friend/frontend/src/motion-controller.ts b/src/components/friend/frontend/motion-controller.ts similarity index 100% rename from friend/frontend/src/motion-controller.ts rename to src/components/friend/frontend/motion-controller.ts diff --git a/friend/frontend/package-lock.json b/src/components/friend/frontend/package-lock.json similarity index 100% rename from friend/frontend/package-lock.json rename to src/components/friend/frontend/package-lock.json diff --git a/friend/frontend/package.json b/src/components/friend/frontend/package.json similarity index 100% rename from friend/frontend/package.json rename to src/components/friend/frontend/package.json diff --git a/friend/frontend/public/akimbo.vrma b/src/components/friend/frontend/public/akimbo.vrma similarity index 100% rename from friend/frontend/public/akimbo.vrma rename to src/components/friend/frontend/public/akimbo.vrma diff --git a/friend/frontend/public/angry.fbx b/src/components/friend/frontend/public/angry.fbx similarity index 100% rename from friend/frontend/public/angry.fbx rename to src/components/friend/frontend/public/angry.fbx diff --git a/friend/frontend/public/angryPump.fbx b/src/components/friend/frontend/public/angryPump.fbx similarity index 100% rename from friend/frontend/public/angryPump.fbx rename to src/components/friend/frontend/public/angryPump.fbx diff --git a/friend/frontend/public/excited.fbx b/src/components/friend/frontend/public/excited.fbx similarity index 100% rename from friend/frontend/public/excited.fbx rename to src/components/friend/frontend/public/excited.fbx diff --git a/friend/frontend/public/greeting.fbx b/src/components/friend/frontend/public/greeting.fbx similarity index 100% rename from friend/frontend/public/greeting.fbx rename to src/components/friend/frontend/public/greeting.fbx diff --git a/friend/frontend/public/happy.fbx b/src/components/friend/frontend/public/happy.fbx similarity index 100% rename from friend/frontend/public/happy.fbx rename to src/components/friend/frontend/public/happy.fbx diff --git a/friend/frontend/public/idle_loop.vrma b/src/components/friend/frontend/public/idle_loop.vrma similarity index 100% rename from friend/frontend/public/idle_loop.vrma rename to src/components/friend/frontend/public/idle_loop.vrma diff --git a/friend/frontend/public/jile.vmd b/src/components/friend/frontend/public/jile.vmd similarity index 100% rename from friend/frontend/public/jile.vmd rename to src/components/friend/frontend/public/jile.vmd diff --git a/friend/frontend/public/love.vmd b/src/components/friend/frontend/public/love.vmd similarity index 100% rename from friend/frontend/public/love.vmd rename to src/components/friend/frontend/public/love.vmd diff --git a/friend/frontend/public/playFingers.vrma b/src/components/friend/frontend/public/playFingers.vrma similarity index 100% rename from friend/frontend/public/playFingers.vrma rename to src/components/friend/frontend/public/playFingers.vrma diff --git a/friend/frontend/public/point.fbx b/src/components/friend/frontend/public/point.fbx similarity index 100% rename from friend/frontend/public/point.fbx rename to src/components/friend/frontend/public/point.fbx diff --git a/friend/frontend/public/salute.fbx b/src/components/friend/frontend/public/salute.fbx similarity index 100% rename from friend/frontend/public/salute.fbx rename to src/components/friend/frontend/public/salute.fbx diff --git a/friend/frontend/public/scratchHead.vrma b/src/components/friend/frontend/public/scratchHead.vrma similarity index 100% rename from friend/frontend/public/scratchHead.vrma rename to src/components/friend/frontend/public/scratchHead.vrma diff --git a/friend/frontend/public/shy.fbx b/src/components/friend/frontend/public/shy.fbx similarity index 100% rename from friend/frontend/public/shy.fbx rename to src/components/friend/frontend/public/shy.fbx diff --git a/friend/frontend/public/stretch.vrma b/src/components/friend/frontend/public/stretch.vrma similarity index 100% rename from friend/frontend/public/stretch.vrma rename to src/components/friend/frontend/public/stretch.vrma diff --git a/friend/frontend/src-tauri/Cargo.lock b/src/components/friend/frontend/src-tauri/Cargo.lock similarity index 100% rename from friend/frontend/src-tauri/Cargo.lock rename to src/components/friend/frontend/src-tauri/Cargo.lock diff --git a/friend/frontend/src-tauri/Cargo.toml b/src/components/friend/frontend/src-tauri/Cargo.toml similarity index 100% rename from friend/frontend/src-tauri/Cargo.toml rename to src/components/friend/frontend/src-tauri/Cargo.toml diff --git a/friend/frontend/src-tauri/build.rs b/src/components/friend/frontend/src-tauri/build.rs similarity index 100% rename from friend/frontend/src-tauri/build.rs rename to src/components/friend/frontend/src-tauri/build.rs diff --git a/friend/frontend/src-tauri/capabilities/default.json b/src/components/friend/frontend/src-tauri/capabilities/default.json similarity index 100% rename from friend/frontend/src-tauri/capabilities/default.json rename to src/components/friend/frontend/src-tauri/capabilities/default.json diff --git a/friend/frontend/src-tauri/icons/128x128.png b/src/components/friend/frontend/src-tauri/icons/128x128.png similarity index 100% rename from friend/frontend/src-tauri/icons/128x128.png rename to src/components/friend/frontend/src-tauri/icons/128x128.png diff --git a/friend/frontend/src-tauri/icons/256x256.png b/src/components/friend/frontend/src-tauri/icons/256x256.png similarity index 100% rename from friend/frontend/src-tauri/icons/256x256.png rename to src/components/friend/frontend/src-tauri/icons/256x256.png diff --git a/friend/frontend/src-tauri/icons/32x32.png b/src/components/friend/frontend/src-tauri/icons/32x32.png similarity index 100% rename from friend/frontend/src-tauri/icons/32x32.png rename to src/components/friend/frontend/src-tauri/icons/32x32.png diff --git a/friend/frontend/src-tauri/src/lib.rs b/src/components/friend/frontend/src-tauri/src/lib.rs similarity index 100% rename from friend/frontend/src-tauri/src/lib.rs rename to src/components/friend/frontend/src-tauri/src/lib.rs diff --git a/friend/frontend/src-tauri/src/main.rs b/src/components/friend/frontend/src-tauri/src/main.rs similarity index 100% rename from friend/frontend/src-tauri/src/main.rs rename to src/components/friend/frontend/src-tauri/src/main.rs diff --git a/friend/frontend/src-tauri/tauri.conf.json b/src/components/friend/frontend/src-tauri/tauri.conf.json similarity index 100% rename from friend/frontend/src-tauri/tauri.conf.json rename to src/components/friend/frontend/src-tauri/tauri.conf.json diff --git a/friend/frontend/src/three-helpers.ts b/src/components/friend/frontend/three-helpers.ts similarity index 100% rename from friend/frontend/src/three-helpers.ts rename to src/components/friend/frontend/three-helpers.ts diff --git a/friend/frontend/tsconfig.json b/src/components/friend/frontend/tsconfig.json similarity index 95% rename from friend/frontend/tsconfig.json rename to src/components/friend/frontend/tsconfig.json index d75087f5a6a5e1e9c9a4751528a27aeeab220294..5cdd4e20dc8f50fae915e988821be58cb25e2fad 100644 --- a/friend/frontend/tsconfig.json +++ b/src/components/friend/frontend/tsconfig.json @@ -16,5 +16,5 @@ "noUnusedParameters": false, "noFallthroughCasesInSwitch": true }, - "include": ["src"] + "include": ["."] } diff --git a/friend/frontend/vite.config.ts b/src/components/friend/frontend/vite.config.ts similarity index 94% rename from friend/frontend/vite.config.ts rename to src/components/friend/frontend/vite.config.ts index ee00c1882e292a11e561f4e3d56664698bb8816d..df106fdb4a0d350d2c97d2fad09dbed7f0d2a0d3 100644 --- a/friend/frontend/vite.config.ts +++ b/src/components/friend/frontend/vite.config.ts @@ -3,7 +3,7 @@ import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], - base: '/friend/', + base: './', server: { port: 1420, strictPort: false, diff --git a/friend/frontend/src/vmd-loader.ts b/src/components/friend/frontend/vmd-loader.ts similarity index 100% rename from friend/frontend/src/vmd-loader.ts rename to src/components/friend/frontend/vmd-loader.ts diff --git a/friend/frontend/src/vrm-ik-handler.ts b/src/components/friend/frontend/vrm-ik-handler.ts similarity index 100% rename from friend/frontend/src/vrm-ik-handler.ts rename to src/components/friend/frontend/vrm-ik-handler.ts diff --git a/friend/frontend/src/web-file-picker.ts b/src/components/friend/frontend/web-file-picker.ts similarity index 100% rename from friend/frontend/src/web-file-picker.ts rename to src/components/friend/frontend/web-file-picker.ts diff --git a/src/friend/tauri-launcher.ts b/src/friend/tauri-launcher.ts index 818835a507692e79c2592e554c5133a3c73b81f4..7f173060ec40b1d1b10262da178978da73afd8f8 100644 --- a/src/friend/tauri-launcher.ts +++ b/src/friend/tauri-launcher.ts @@ -4,76 +4,77 @@ * Builds and launches the VRM desktop pet as a native Tauri window * (transparent, always-on-top) instead of a browser tab. */ -import { spawn, execSync } from 'node:child_process'; +import { spawn } from 'node:child_process'; import path from 'node:path'; import { existsSync } from 'node:fs'; let tauriProcess: ReturnType | null = null; +/** + * Launch the Friend Tauri desktop window. + * Uses the release binary (built via `npx tauri build`). + * The binary loads the pre-built frontend from frontend/dist/. + */ export function launchTauri(appDir: string, log: { info: (msg: string) => void; warn: (msg: string) => void }) { - const tauriDir = path.join(appDir, 'src-tauri'); - const tauriConfPath = path.join(tauriDir, 'tauri.conf.json'); + const releaseBinary = path.join(appDir, 'src-tauri', 'target', 'release', 'versperclaw-friend') + const debugBinary = path.join(appDir, 'src-tauri', 'target', 'debug', 'versperclaw-friend') + + const binary = existsSync(releaseBinary) + ? releaseBinary + : existsSync(debugBinary) + ? debugBinary + : null - // Verify the Tauri project exists - if (!existsSync(tauriConfPath)) { - log.warn(`Friend: Tauri project not found at ${tauriDir}. Falling back to browser mode.`); - return; + if (!binary) { + log.warn(`Friend: Tauri binary not found. Run \`cd ${appDir} && npx tauri build\` first.`) + log.warn(`Looking for: ${releaseBinary}`) + return } - log.info(`Starting Friend Tauri desktop window from ${appDir}`); + log.info(`Starting Friend desktop window from ${binary}`) - // Run `npx tauri dev` — Bun's npx equivalent auto-fetches the CLI - tauriProcess = spawn('npx', ['tauri', 'dev'], { + tauriProcess = spawn(binary, [], { cwd: appDir, stdio: 'pipe', - shell: true, - env: { - ...process.env, - // Tell Tauri dev server to skip its own Vite and load from VersperClaw - TAURI_DEV_HOST: '127.0.0.1', - TAURI_DEV_PORT: '3456', - }, - }); + detached: true, + }) tauriProcess.stdout?.on('data', (data: Buffer) => { for (const line of data.toString().split('\n').filter(Boolean)) { - log.info(line); + log.info(line) } - }); + }) tauriProcess.stderr?.on('data', (data: Buffer) => { for (const line of data.toString().split('\n').filter(Boolean)) { - log.info(line); + log.info(line) } - }); + }) tauriProcess.on('error', (err: Error) => { - log.warn(`Friend Tauri error: ${err.message}`); - tauriProcess = null; - }); + log.warn(`Friend Tauri error: ${err.message}`) + tauriProcess = null + }) tauriProcess.on('exit', (code: number | null) => { - log.info(`Friend Tauri exited (code: ${code})`); - tauriProcess = null; - }); + log.info(`Friend Tauri exited (code: ${code})`) + tauriProcess = null + }) } export function stopTauri(log: { info: (msg: string) => void }) { if (tauriProcess) { - log.info('Stopping Friend Tauri window...'); - const proc = tauriProcess; - tauriProcess = null; + log.info('Stopping Friend Tauri window...') + const proc = tauriProcess + tauriProcess = null - // Send SIGTERM first - proc.kill('SIGTERM'); - - // Force kill after 3s if still alive + proc.kill('SIGTERM') setTimeout(() => { - try { if (proc && !proc.killed) proc.kill('SIGKILL'); } catch { /* ignore */ } - }, 3000); + try { if (!proc.killed) proc.kill('SIGKILL') } catch { /* ignore */ } + }, 3000) } } export function getTauriProcess(): ReturnType | null { - return tauriProcess; -} + return tauriProcess +} \ No newline at end of file diff --git a/src/server/index.ts b/src/server/index.ts index 2de89f4dcd449c4a4f488257f57b62353c328738..dcfb4856d6900c4f05977df9cea4c680cb8881f4 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -439,7 +439,7 @@ export function startServer(port = PORT, host = HOST) { // Try launching Tauri desktop window first const _srcDir = path.dirname(fileURLToPath(import.meta.url)) - const friendFrontendDir = path.resolve(_srcDir, '..', '..', 'friend', 'frontend') + const friendFrontendDir = path.resolve(_srcDir, '..', '..', 'src', 'components', 'friend', 'frontend') launchTauri(friendFrontendDir, { info: (msg: string) => console.log(`[Friend] ${msg}`), warn: (msg: string) => console.warn(`[Friend] ${msg}`), diff --git a/src/server/staticFriend.ts b/src/server/staticFriend.ts index fdd8c1889e15108a454fa2bce82b378a2ccd6db9..93661742e0e23a7e628ccf9606b839b43f1f64fc 100644 --- a/src/server/staticFriend.ts +++ b/src/server/staticFriend.ts @@ -1,7 +1,7 @@ /** * Static file serving for the Friend VRM frontend. * - * Serves the built Vite/React frontend from friend/frontend/dist/ at /friend/*. + * Serves the built Vite/React frontend from src/components/friend/frontend/dist/ at /friend/*. * All asset files (VRM, FBX, VRMA, VMD, MP3) are in the dist root and served * under /friend/. */ @@ -70,8 +70,8 @@ export async function handleFriendStaticRequest(req: Request, url: URL): Promise async function resolveFriendDistDir(): Promise { const _srcDir = path.dirname(fileURLToPath(import.meta.url)) - // src/server/ -> ../../friend/frontend/dist - const candidate = path.resolve(_srcDir, '..', '..', 'friend', 'frontend', 'dist') + // src/server/ -> ../../src/components/friend/frontend/dist + const candidate = path.resolve(_srcDir, '..', '..', 'src', 'components', 'friend', 'frontend', 'dist') try { const stat = await fs.stat(path.join(candidate, 'index.html')) if (stat.isFile()) { diff --git a/src/tools/FriendEmotionTool.ts b/src/tools/FriendEmotionTool.ts index 87366d98b2fe59cb93b2956e8ccd49a2dde19681..c2acc7581c57ea3019e4920aeda783805a051b79 100644 --- a/src/tools/FriendEmotionTool.ts +++ b/src/tools/FriendEmotionTool.ts @@ -109,4 +109,48 @@ export const FriendEmotionTool = buildTool({ }], }; }, + mapToolResultToToolResultBlockParam(output: Output, toolUseID: string) { + // If the tool already returned a ToolResultBlockParam-like object, pass it through + if (output && (output as any).content) { + return { + tool_use_id: toolUseID, + type: 'tool_result' as const, + content: (output as any).content, + } + } + + // Fallback: produce a simple text block summarizing the result + const text = + output && 'emotion' in output + ? `Avatar emotion set to ${(output as any).emotion}.${ + (output as any).moodDelta !== undefined + ? ` Your mood ${(output as any).moodDelta > 0 ? '+' : ''}${(output as any).moodDelta} → ${(output as any).moodIndex}%` + : '' + }` + : JSON.stringify(output) + + return { + tool_use_id: toolUseID, + type: 'tool_result' as const, + content: [{ type: 'text' as const, text }], + } + }, + // Consistent with other tools: set a reasonable persistence threshold + maxResultSizeChars: 100_000, + renderToolUseMessage(input: Partial) { + const emotion = (input as any)?.emotion + return emotion ? `Set avatar emotion to ${emotion}` : 'Set avatar emotion' + }, + renderToolResultMessage(output: Output) { + if (!output) return null + const mood = output.moodDelta !== undefined ? ` Your mood ${output.moodDelta > 0 ? '+' : ''}${output.moodDelta} → ${output.moodIndex}%` : '' + return `Avatar emotion set to ${output.emotion}.${mood}` + }, + extractSearchText(output: Output) { + if (!output) return '' + return `Avatar emotion: ${output.emotion}${output.moodDelta !== undefined ? ` moodDelta:${output.moodDelta} moodIndex:${output.moodIndex}` : ''}` + }, + isResultTruncated() { + return false + }, } satisfies ToolDef); diff --git a/src/tools/FriendScreenObserveTool.ts b/src/tools/FriendScreenObserveTool.ts index b2e5ca1783bdaeeeb35b3e9b00b024d504da0146..5af2daa26a78ae878017f051ced4fb201a11e4ac 100644 --- a/src/tools/FriendScreenObserveTool.ts +++ b/src/tools/FriendScreenObserveTool.ts @@ -117,4 +117,51 @@ export const FriendScreenObserveTool = buildTool({ }], }; }, + mapToolResultToToolResultBlockParam(output: Output | { content: unknown }, toolUseID: string) { + if (output && (output as any).content) { + return { + tool_use_id: toolUseID, + type: 'tool_result' as const, + content: (output as any).content, + } + } + + const text = output && 'imagePath' in output && (output as any).imagePath + ? `Screenshot saved. Use the read tool to view: ${(output as any).imagePath}` + : JSON.stringify(output) + + return { + tool_use_id: toolUseID, + type: 'tool_result' as const, + content: [{ type: 'text' as const, text }], + } + }, + // Mirror common tool APIs + maxResultSizeChars: 100_000, + renderToolUseMessage() { + return 'Capture desktop screenshot' + }, + renderToolResultMessage(output: Output | { content?: unknown }) { + if (!output) return null + if ((output as any).content) { + // If the mapped block was passed through, try to extract text + const content = (output as any).content + if (typeof content === 'string') return content + if (Array.isArray(content)) { + const t = content.find((b: any) => b?.type === 'text') + return t?.text ?? JSON.stringify(content) + } + return JSON.stringify(content) + } + if ('imagePath' in (output as any) && (output as any).imagePath) { + return `Screenshot saved: ${(output as any).imagePath}` + } + return JSON.stringify(output) + }, + extractSearchText(output: Output) { + return output && output.imagePath ? `screenshot:${output.imagePath}` : '' + }, + isResultTruncated() { + return false + }, } satisfies ToolDef); diff --git a/tsconfig.json b/tsconfig.json index a97c78c4113d5b40ebf95f5df8e4b104ec823615..c046d805093ddacc888269550994147588720fae 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,5 +19,6 @@ "skipLibCheck": true, "noFallthroughCasesInSwitch": true }, - "include": ["src", "scripts", "env.d.ts"] + "include": ["src", "scripts", "env.d.ts"], + "exclude": ["**/src-tauri/target"] }