From bb758e9d0f9c2678bc8b237e7734f5b581cdebd3 Mon Sep 17 00:00:00 2001
From: OpenPets Dev
Date: Tue, 16 Jun 2026 18:40:42 +0000
Subject: [PATCH] feat(desktop): add Text-to-Speech Phase 2 and update
docs/registry
- Add configurable TTS providers: system OS voice, OpenAI TTS, ElevenLabs,
Piper (local), and OpenAI-compatible (OpenRouter/LiteLLM/WaveSpeedAI/custom).
- Add secure TTS credential storage, endpoint presets, voice/model/speed UI,
and test/stop controls in Settings.
- Wire TTS to pet/floating-chat renderer via openpets:tts-speak/audio/stop IPC.
- Respect quiet hours; validate cloud endpoints and returned audio buffers.
- Add plugin voice:speak bridge and tts-engine unit tests.
- Update FEATURES.md, FEATURES_OUR_CHANGES.md, README.md, README_OUR_CHANGES.md,
PULL_REQUEST.md, FEATURE_REGISTRY.md, and add docs/pull-requests/PR_TTS_PHASE_2.md.
---
FEATURES.md | 38 +-
FEATURES_OUR_CHANGES.md | 32 +-
FEATURE_REGISTRY.md | 37 +-
PULL_REQUEST.md | 46 +-
README.md | 18 +-
README_OUR_CHANGES.md | 31 +-
apps/desktop/control-center-preload.cjs | 8 +
apps/desktop/pet-preload.cjs | 34 +-
apps/desktop/prompt-window-preload.cjs | 45 +
apps/desktop/scripts/run-tests.mjs | 1 +
apps/desktop/src/agent-setup.ts | 52 +-
apps/desktop/src/app-state.ts | 18 +
apps/desktop/src/i18n/locales/en.ts | 60 +-
apps/desktop/src/i18n/locales/es-419.ts | 29 +-
apps/desktop/src/i18n/locales/ja.ts | 29 +-
apps/desktop/src/i18n/locales/ko.ts | 29 +-
apps/desktop/src/i18n/locales/pt-BR.ts | 29 +-
apps/desktop/src/i18n/locales/zh-Hans.ts | 29 +-
apps/desktop/src/i18n/locales/zh-Hant.ts | 29 +-
apps/desktop/src/logger.ts | 2 +-
apps/desktop/src/openapi-chat.ts | 9 +
apps/desktop/src/plugin-voice.ts | 12 +-
apps/desktop/src/prompt-window.ts | 4 +
apps/desktop/src/renderer/src/main.tsx | 1019 ++++++++++++-----
apps/desktop/src/tray.ts | 2 +-
apps/desktop/src/tts-catalog.ts | 56 +
apps/desktop/src/tts-credentials.ts | 109 ++
apps/desktop/src/tts-engine.ts | 256 +++++
apps/desktop/src/tts-service.ts | 111 ++
apps/desktop/src/windows.ts | 116 +-
apps/desktop/tests/tts-engine.test.ts | 32 +
.../OPENPETS_3_1_0_MOD_NICHE_PULL_REQUESTS.md | 8 +-
32 files changed, 1944 insertions(+), 386 deletions(-)
create mode 100644 apps/desktop/src/tts-catalog.ts
create mode 100644 apps/desktop/src/tts-credentials.ts
create mode 100644 apps/desktop/src/tts-engine.ts
create mode 100644 apps/desktop/src/tts-service.ts
create mode 100644 apps/desktop/tests/tts-engine.test.ts
diff --git a/FEATURES.md b/FEATURES.md
index 8f0ad96b..b8761d0a 100644
--- a/FEATURES.md
+++ b/FEATURES.md
@@ -51,6 +51,23 @@ It combines:
- Safer speech rules for agent-driven bubble content
- Bubble behavior that avoids showing code, logs, URLs, paths, or secrets in normal integration speech
+## Pet Text-to-Speech (Phase 2)
+
+- Settings > **Text-to-Speech** panel controls voice output for the pet and floating chat.
+- **System voice** uses the OS speech engine through the renderer (`window.speechSynthesis`) with optional voice-name matching and a 0.5×–2.0× speed multiplier.
+- **Cloud TTS providers**: OpenAI TTS, ElevenLabs, and an **OpenAI-compatible** preset for OpenRouter, LiteLLM, WaveSpeedAI, or a custom endpoint.
+- **Local TTS provider**: Piper (spawned as a local process) for fully offline speech.
+- Provider credentials are stored with Electron `safeStorage` and fall back to plain local storage when encryption is unavailable (`apps/desktop/src/tts-credentials.ts`).
+- Per-provider model, voice, speed, endpoint preset, and custom endpoint controls.
+- Dynamic voice list fetching for ElevenLabs; static voice lists for OpenAI / OpenAI-compatible.
+- In-app **Test voice** and **Stop** buttons to preview the configured voice.
+- Assistant replies and other speech are spoken through the pet window renderer via a shared TTS service (`apps/desktop/src/tts-service.ts`).
+- Respects quiet-hours: speech is skipped while quiet hours are active.
+- Audio returned by cloud providers is validated (MP3/WAV magic bytes) and played through a renderer `
- This branch also includes a floating chat surface, local memory, Knowledge Store, vanilla MCP tools, and expanded pet sizing. See FEATURES.md, FEATURE_REGISTRY.md, and PULL_REQUEST.md for details.
+ This branch also includes a floating chat surface, local memory, Knowledge Store, MCP Tool Servers, an OpenPets MCP Server panel, a Curated MCP Toolkit, Text-to-Speech (Phase 2), and expanded pet sizing. See FEATURES.md, FEATURE_REGISTRY.md, and PULL_REQUEST.md for details.
@@ -77,10 +77,11 @@ OpenPets is a tray-first desktop companion app. A pet appears on your desktop, k
- **Bundled abilities** - first-party plugins can add ambient check-ins, break nudges, playful pet actions, focus timers, safe little walks, and optional developer notifications.
- **Developer integrations** - advanced setup for Claude Code, OpenCode, Cursor, Pi, and MCP-capable tools when you want coding activity to drive the pet.
- **MCP ready** - any MCP-capable agent can send short safe speech bubbles and reactions through the OpenPets MCP server.
-- **Floating chat** - double-click the pet to open an always-on-top chat window with BYOK OpenAPI-compatible providers, conversation history, and optional MCP tool activation.
+- **Floating chat** - double-click the pet to open an always-on-top chat window with BYOK OpenAPI-compatible providers, conversation history, and optional MCP Tool Server activation.
- **Virtual-pet status** - right-click the pet and open the Virtual Pet submenu to see hunger, energy, happiness, bond, mood, and level. No on-pet HUD overlay, so chat clicks stay unobstructed.
- **Single-click petting** - click the pet once to pet it; the virtual-pet bond and happiness increase.
- **Local memory** - the pet remembers facts, preferences, and notes across sessions.
+- **Text-to-Speech (Phase 2)** - configurable system, OpenAI, ElevenLabs, Piper, or OpenAI-compatible TTS for the pet and floating chat, with voice/model/speed controls and a test preview.
- **Pet-pack friendly** - loads installed animated pet packs and can route a selected agent/project to its own pet window.
- **Privacy-conscious by design** - automatic hook speech is static and local; prompts, code, logs, command output, URLs, paths, and secrets are not shown in bubbles.
@@ -229,15 +230,22 @@ Available MCP tools:
`openpets_say` messages must be short, single-line, and must not look like code, logs, secrets, URLs, or file paths.
+### MCP Tool Servers
+
+The desktop app includes a dedicated **MCP Tool Servers** panel under **Integrations** for the built-in floating chat. Activate tool servers such as filesystem, terminal, memory, fetch-web, sequential-thinking, playwright, git, github, docker, and sqlite so the pet can use them directly. This works independently of whether Claude, Cursor, OpenCode, or Pi are installed.
+
+### OpenPets MCP Server
+
+A central **OpenPets MCP Server** panel under **Integrations** configures the external-agent bridge. It collects command mode (published / bundled / local), node path, and pet routing in one place, and provides a **Test Server** check and a **Copy MCP JSON** button for pasting into Claude Code, Cursor, Codex CLI, or any other MCP host.
+
### Curated MCP toolkit
-The desktop app also includes a manual-but-curated **MCP Toolkit** panel under **Integrations**. It is there to help you build a practical broader stack without stuffing OpenPets full of unsafe one-click third-party installs.
+The desktop app also includes a manual-but-curated **Curated MCP Toolkit** panel under **Integrations**. It is there to help you build a practical broader stack without stuffing OpenPets full of unsafe one-click third-party installs.
-The toolkit now explicitly offers two paths:
+The toolkit offers two paths:
- `Manual Setup` - per-tool guidance, docs links, and copyable snippets
- `Persistent Full Access` - `Install Now` for the supported persistent baseline in hosts such as Claude Code or Codex CLI, plus a matching copyable bundle
-- `Vanilla Chat Tools` - activate MCP tools directly inside the floating chat window for autonomous filesystem, terminal, web, and browser tasks
The current supported `Install Now` baseline covers:
diff --git a/README_OUR_CHANGES.md b/README_OUR_CHANGES.md
index d7965e2f..0c275a53 100644
--- a/README_OUR_CHANGES.md
+++ b/README_OUR_CHANGES.md
@@ -33,14 +33,24 @@ Store files and add manual knowledge entries that the assistant can reference du
- Relevant files are injected into the assistant's system instructions automatically.
- Combined search across stored files and OpenPets memories.
-## New: Vanilla Chat MCP Tools
+## New: MCP Tool Servers (Built-in Chat)
-Activate MCP tools directly inside the floating chat window for autonomous task execution.
+Activate MCP tool servers for the built-in floating chat, independently of external agents.
- Filesystem, Terminal, Memory, Fetch/Web, Sequential Thinking, Playwright, Git, GitHub, Docker, SQLite.
-- Tiered browser in Settings/Integrations: Starter, Terminal & Systems, Advanced.
+- Top-level **MCP Tool Servers** panel in Control Center → Integrations.
+- Tiered browser: Starter, Terminal & Systems, Advanced.
- Tools run through an internal stdio MCP client in the main process.
+## New: OpenPets MCP Server Panel
+
+A central panel in Control Center → Integrations configures the external-agent bridge.
+
+- Command mode selector (published / bundled / local) in one place.
+- Node path override and pet routing in one place.
+- One-click **Test Server** health check.
+- One-click **Copy MCP JSON** for Claude Code, Cursor, Codex CLI, or generic MCP hosts.
+
## New: Pet Scale
- Continuous scale slider from 0.16x to 10x in Settings.
@@ -57,6 +67,16 @@ Activate MCP tools directly inside the floating chat window for autonomous task
- Give your pet a custom system-prompt style character in Settings.
- Toggle whether the default OpenPets behavior instructions are included in every chat.
+## New: Text-to-Speech (Phase 2)
+
+The pet can now speak aloud using a configurable TTS provider.
+
+- Settings > **Text-to-Speech** chooses between system OS voice, OpenAI TTS, ElevenLabs, Piper (local), or an OpenAI-compatible endpoint.
+- Per-provider voice, model, speed, endpoint preset, and API-key storage.
+- Test and Stop buttons preview the voice in Settings.
+- Speech is skipped during quiet hours and uses the same safety boundaries as bubbles.
+- Plugins with the `voice:speak` permission can ask the pet to speak.
+
## New: Multi-Screen Awareness
The pet now stays on the display it is currently near, rather than always snapping to the primary display.
@@ -69,6 +89,9 @@ No additional setup is required beyond the normal OpenPets desktop app. The new
- Double-click the pet → floating chat.
- Right-click the pet → Virtual Pet status and care commands.
- Settings → OpenAPI Chat for model/credential setup.
+- Settings → Text-to-Speech for voice provider and test preview.
- Settings → Memory for memory management.
- Settings → Knowledge Store for file-based knowledge.
-- Integrations → MCP Toolkit for tool activation.
+- Integrations → MCP Tool Servers for built-in chat tool activation.
+- Integrations → OpenPets MCP Server for external-agent bridge configuration.
+- Integrations → Curated MCP Toolkit for optional persistent bundles and copy-paste snippets.
diff --git a/apps/desktop/control-center-preload.cjs b/apps/desktop/control-center-preload.cjs
index 6e630f58..711ff045 100644
--- a/apps/desktop/control-center-preload.cjs
+++ b/apps/desktop/control-center-preload.cjs
@@ -55,6 +55,14 @@ const api = {
getIntegrationsState: (selectedPetId, commandMode) => ipcRenderer.invoke("openpets:agent-setup-snapshot", selectedPetId, commandMode),
runIntegrationAction: (action, selectedPetId, commandMode) => ipcRenderer.invoke("openpets:agent-setup-action", action, selectedPetId, commandMode),
updateIntegrationCommandPaths: (patch) => ipcRenderer.invoke("openpets:agent-setup-command-paths", patch),
+ getOpenPetsMcpServerPreview: (selectedPetId, commandMode) => ipcRenderer.invoke("openpets:openpets-mcp-server-preview", selectedPetId, commandMode),
+ testOpenPetsMcpServer: (selectedPetId, commandMode) => ipcRenderer.invoke("openpets:test-openpets-mcp-server", selectedPetId, commandMode),
+ getTtsSettings: () => ipcRenderer.invoke("openpets:get-tts-settings"),
+ getTtsVoices: (provider) => ipcRenderer.invoke("openpets:get-tts-voices", provider),
+ saveTtsCredential: (provider, credential) => ipcRenderer.invoke("openpets:save-tts-credential", provider, credential),
+ clearTtsCredential: (provider) => ipcRenderer.invoke("openpets:clear-tts-credential", provider),
+ testTtsSpeak: (text) => ipcRenderer.invoke("openpets:test-tts", text),
+ stopTts: () => ipcRenderer.invoke("openpets:tts-stop"),
copyText: (text) => ipcRenderer.invoke("openpets:copy-text", text),
openExternalUrl: (url) => ipcRenderer.invoke("openpets:open-external-url", url),
installMcpToolkit: (target) => ipcRenderer.invoke("openpets:install-mcp-toolkit", target),
diff --git a/apps/desktop/pet-preload.cjs b/apps/desktop/pet-preload.cjs
index 97381ef7..720ba204 100644
--- a/apps/desktop/pet-preload.cjs
+++ b/apps/desktop/pet-preload.cjs
@@ -395,8 +395,11 @@ ipcRenderer.on("openpets:stop-audio", () => {
// --- Plugin TTS ---------------------------------------------------------------
+let currentTtsAudio = null;
+
ipcRenderer.on("openpets:tts-speak", (_event, payload) => {
try {
+ stopCurrentTtsAudio();
if (!payload || typeof payload.text !== "string" || !window.speechSynthesis) return;
const utterance = new SpeechSynthesisUtterance(payload.text.slice(0, 500));
if (typeof payload.rate === "number" && payload.rate >= 0.5 && payload.rate <= 2) utterance.rate = payload.rate;
@@ -408,10 +411,37 @@ ipcRenderer.on("openpets:tts-speak", (_event, payload) => {
} catch { /* tts is best-effort */ }
});
-ipcRenderer.on("openpets:tts-stop", () => {
- try { window.speechSynthesis && window.speechSynthesis.cancel(); } catch { /* noop */ }
+ipcRenderer.on("openpets:tts-audio", (_event, payload) => {
+ try {
+ stopCurrentTtsAudio();
+ if (!payload || !payload.audio || !payload.mimeType) return;
+ const buffer = payload.audio instanceof Uint8Array ? payload.audio : new Uint8Array(payload.audio);
+ const blob = new Blob([buffer], { type: payload.mimeType });
+ const url = URL.createObjectURL(blob);
+ currentTtsAudio = new Audio(url);
+ currentTtsAudio.addEventListener("ended", () => {
+ URL.revokeObjectURL(url);
+ currentTtsAudio = null;
+ });
+ currentTtsAudio.play().catch(() => { /* audio is best-effort */ });
+ } catch { /* tts is best-effort */ }
});
+ipcRenderer.on("openpets:tts-stop", () => {
+ try {
+ stopCurrentTtsAudio();
+ if (window.speechSynthesis) window.speechSynthesis.cancel();
+ } catch { /* noop */ }
+});
+
+function stopCurrentTtsAudio() {
+ if (currentTtsAudio) {
+ try { currentTtsAudio.pause(); } catch { /* noop */ }
+ try { currentTtsAudio.src = ""; } catch { /* noop */ }
+ currentTtsAudio = null;
+ }
+}
+
const installMouseInterop = () => {
lastInteractiveHit = null;
dragging = false;
diff --git a/apps/desktop/prompt-window-preload.cjs b/apps/desktop/prompt-window-preload.cjs
index cf0f5397..a287a3eb 100644
--- a/apps/desktop/prompt-window-preload.cjs
+++ b/apps/desktop/prompt-window-preload.cjs
@@ -15,3 +15,48 @@ const api = {
};
contextBridge.exposeInMainWorld("openPetsPromptWindow", api);
+
+// --- TTS playback -------------------------------------------------------------
+
+let currentTtsAudio = null;
+
+ipcRenderer.on("openpets:tts-speak", (_event, payload) => {
+ try {
+ stopCurrentTtsAudio();
+ if (!payload || typeof payload.text !== "string" || !window.speechSynthesis) return;
+ const utterance = new SpeechSynthesisUtterance(payload.text.slice(0, 500));
+ if (typeof payload.rate === "number" && payload.rate >= 0.5 && payload.rate <= 2) utterance.rate = payload.rate;
+ window.speechSynthesis.speak(utterance);
+ } catch { /* tts is best-effort */ }
+});
+
+ipcRenderer.on("openpets:tts-audio", (_event, payload) => {
+ try {
+ stopCurrentTtsAudio();
+ if (!payload || !payload.audio || !payload.mimeType) return;
+ const buffer = payload.audio instanceof Uint8Array ? payload.audio : new Uint8Array(payload.audio);
+ const blob = new Blob([buffer], { type: payload.mimeType });
+ const url = URL.createObjectURL(blob);
+ currentTtsAudio = new Audio(url);
+ currentTtsAudio.addEventListener("ended", () => {
+ URL.revokeObjectURL(url);
+ currentTtsAudio = null;
+ });
+ currentTtsAudio.play().catch(() => { /* audio is best-effort */ });
+ } catch { /* tts is best-effort */ }
+});
+
+ipcRenderer.on("openpets:tts-stop", () => {
+ try {
+ stopCurrentTtsAudio();
+ if (window.speechSynthesis) window.speechSynthesis.cancel();
+ } catch { /* noop */ }
+});
+
+function stopCurrentTtsAudio() {
+ if (currentTtsAudio) {
+ try { currentTtsAudio.pause(); } catch { /* noop */ }
+ try { currentTtsAudio.src = ""; } catch { /* noop */ }
+ currentTtsAudio = null;
+ }
+}
diff --git a/apps/desktop/scripts/run-tests.mjs b/apps/desktop/scripts/run-tests.mjs
index af7b0dbc..8e421c7a 100644
--- a/apps/desktop/scripts/run-tests.mjs
+++ b/apps/desktop/scripts/run-tests.mjs
@@ -19,6 +19,7 @@ const behaviorTests = [
".test-dist/tests/update-version.test.js",
".test-dist/tests/reaction-animation-mapping.test.js",
".test-dist/tests/zip-safety.test.js",
+ ".test-dist/tests/tts-engine.test.js",
".test-dist/tests/codex-pets.test.js",
".test-dist/tests/claude-memory.test.js",
".test-dist/tests/prompt-memory-extraction.test.js",
diff --git a/apps/desktop/src/agent-setup.ts b/apps/desktop/src/agent-setup.ts
index 05a54873..a41ec9b9 100644
--- a/apps/desktop/src/agent-setup.ts
+++ b/apps/desktop/src/agent-setup.ts
@@ -4,7 +4,7 @@ import { dirname, isAbsolute, join } from "node:path";
import { createRequire } from "node:module";
import { app } from "electron";
-import { buildClaudeMcpGetCommand, buildClaudeMcpPreview, classifyClaudeMcpStatus, createOpenPetsHookSettingsPreview, doctorClaudeHooks, installClaudeHooks, mapAsarPathToUnpacked, uninstallClaudeHooks, type ClaudeCommandSpec, type ClaudeHookDoctorResult, type ClaudeMcpPreview, type OpenPetsCommandMode, type ParsedClaudeMcpEntry } from "@open-pets/claude";
+import { buildClaudeMcpGetCommand, buildClaudeMcpPreview, buildOpenPetsMcpServerCommand, classifyClaudeMcpStatus, createOpenPetsHookSettingsPreview, doctorClaudeHooks, formatCommandForDisplay, installClaudeHooks, mapAsarPathToUnpacked, uninstallClaudeHooks, type ClaudeCommandSpec, type ClaudeHookDoctorResult, type ClaudeMcpPreview, type OpenPetsCommandMode, type ParsedClaudeMcpEntry } from "@open-pets/claude";
import { buildCursorRulesPreview, classifyCursorMcpStatus, executeCursorMcpWrite, getCursorGlobalMcpPath, planCursorMcpInstall, planCursorMcpRemove, planCursorMcpReplace, readCursorMcpConfig, type CursorMcpStatusResult } from "@open-pets/cursor";
import { buildOpenPetsOnlyPreview, type RedactedPreview } from "@open-pets/cursor";
import { doctorOpenCodeGlobalSetup, getGlobalOpenCodeConfigDir, parseOpenCodeConfig, prepareOpenCodeGlobalRemove, prepareOpenCodeGlobalSetup, writePreparedOpenCodeGlobalRemove, writePreparedOpenCodeGlobalSetup } from "@open-pets/opencode";
@@ -607,6 +607,56 @@ function getMcpPackageVersion(): string {
return getWorkspacePackageVersion("@open-pets/mcp");
}
+export interface OpenPetsMcpServerPreview {
+ readonly commandMode: OpenPetsCommandMode;
+ readonly command: string;
+ readonly args: readonly string[];
+ readonly displayCommand: string;
+ readonly mcpJson: {
+ readonly mcpServers: {
+ readonly openpets: {
+ readonly type: "stdio";
+ readonly command: string;
+ readonly args: readonly string[];
+ };
+ };
+ };
+}
+
+export interface OpenPetsMcpServerHealth {
+ readonly ok: boolean;
+ readonly output: string;
+ readonly error?: string;
+}
+
+export function buildOpenPetsMcpServerPreview(selectedPetId: string | undefined, commandMode: OpenPetsCommandMode): OpenPetsMcpServerPreview {
+ const nodeCommand = getPreferredNodeCommand();
+ const { command, args } = buildOpenPetsMcpServerCommand(selectedPetId, commandMode, nodeCommand);
+ const displayCommand = formatCommandForDisplay({ command, args });
+ return {
+ commandMode,
+ command,
+ args,
+ displayCommand,
+ mcpJson: {
+ mcpServers: {
+ openpets: { type: "stdio", command, args },
+ },
+ },
+ };
+}
+
+export async function testOpenPetsMcpServer(selectedPetId: string | undefined, commandMode: OpenPetsCommandMode): Promise {
+ const nodeCommand = getPreferredNodeCommand();
+ const { command, args } = buildOpenPetsMcpServerCommand(selectedPetId, commandMode, nodeCommand);
+ const result = await runCommand({ command, args: [...args, "--version"] });
+ if (result.ok) {
+ return { ok: true, output: result.stdout.trim() || "MCP server responded." };
+ }
+ const errorMessage = result.error || result.stderr.trim() || `Command exited with code ${result.exitCode ?? "unknown"}.`;
+ return { ok: false, output: "", error: errorMessage };
+}
+
function summarizeMemoryMessages(...messages: readonly string[]): string {
const memoryMessages = messages.flatMap((message) => message.match(/Claude (?:OpenPets )?instructions[^.]*\./g) ?? []);
return memoryMessages.length > 0 ? ` ${memoryMessages.join(" ")}` : "";
diff --git a/apps/desktop/src/app-state.ts b/apps/desktop/src/app-state.ts
index 5bdb24ee..049678e5 100644
--- a/apps/desktop/src/app-state.ts
+++ b/apps/desktop/src/app-state.ts
@@ -51,6 +51,12 @@ export interface OpenPetsStateV1 {
readonly opencodeCommandPath?: string;
readonly vanillaChatMcpTools?: readonly string[];
readonly openApiChatBaseInstructionsEnabled?: boolean;
+ readonly ttsProvider?: "system" | "openai" | "elevenlabs" | "piper" | "openai-compatible";
+ readonly ttsVoice?: string;
+ readonly ttsSpeed?: number;
+ readonly ttsModel?: string;
+ readonly ttsEndpointPreset?: "openrouter" | "litellm" | "wavespeedai" | "custom";
+ readonly ttsEndpoint?: string;
};
readonly pets: {
readonly installed: readonly InstalledPetState[];
@@ -407,6 +413,12 @@ function normalizePreferences(value: Partial): O
opencodeCommandPath: normalizeCommandPath(value.opencodeCommandPath),
vanillaChatMcpTools: normalizeVanillaChatMcpTools(value.vanillaChatMcpTools),
openApiChatBaseInstructionsEnabled: typeof value.openApiChatBaseInstructionsEnabled === "boolean" ? value.openApiChatBaseInstructionsEnabled : true,
+ ttsProvider: ["system", "openai", "elevenlabs", "piper", "openai-compatible"].includes(value.ttsProvider as string) ? (value.ttsProvider as OpenPetsStateV1["preferences"]["ttsProvider"]) : "system",
+ ttsVoice: typeof value.ttsVoice === "string" ? value.ttsVoice : undefined,
+ ttsSpeed: typeof value.ttsSpeed === "number" && value.ttsSpeed >= 0.5 && value.ttsSpeed <= 2 ? value.ttsSpeed : 1,
+ ttsModel: typeof value.ttsModel === "string" ? value.ttsModel : undefined,
+ ttsEndpointPreset: ["openrouter", "litellm", "wavespeedai", "custom"].includes(value.ttsEndpointPreset as string) ? (value.ttsEndpointPreset as OpenPetsStateV1["preferences"]["ttsEndpointPreset"]) : "openrouter",
+ ttsEndpoint: typeof value.ttsEndpoint === "string" ? value.ttsEndpoint : undefined,
};
}
@@ -543,6 +555,12 @@ function createDefaultState(): OpenPetsStateV1 {
opencodeCommandPath: undefined,
vanillaChatMcpTools: undefined,
openApiChatBaseInstructionsEnabled: true,
+ ttsProvider: "system",
+ ttsVoice: undefined,
+ ttsSpeed: 1,
+ ttsModel: undefined,
+ ttsEndpointPreset: "openrouter",
+ ttsEndpoint: undefined,
},
pets: {
installed: [builtInPet],
diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts
index 66a05987..0326563b 100644
--- a/apps/desktop/src/i18n/locales/en.ts
+++ b/apps/desktop/src/i18n/locales/en.ts
@@ -8,8 +8,8 @@ export const en = {
// --- Tray menu (main process, src/tray.ts) ---
"tray.updateAvailable": "Update available: {version}...",
"tray.defaultPet": "Default Pet: {name}",
- "tray.showDefaultPet": "Show Default Pet",
- "tray.hideDefaultPet": "Hide Default Pet",
+ "tray.toggleDefaultPet.show": "Show Default Pet",
+ "tray.toggleDefaultPet.hide": "Hide Default Pet",
"tray.pauseAllPets": "Pause All Pets",
"tray.resumeAllPets": "Resume All Pets",
"tray.managePets": "Manage Pets...",
@@ -36,7 +36,7 @@ export const en = {
"pet.status.done": "Done",
"pet.status.oops": "Oops",
"pet.status.hi": "Hi",
- "pet.menu.hidePet": "Hide pet",
+ "pet.menu.hidePet": "Hide Pet",
"pet.menu.closePet": "Close pet",
"pet.menu.openControlCenter": "Open Control Center",
@@ -270,6 +270,42 @@ export const en = {
"settings.plugins.eyebrow": "Plugin Platform",
"settings.plugins.title": "Plugin Permissions & AI",
"settings.plugins.description": "Global gates for what plugins may do. Sensitive capabilities stay off until you enable them here.",
+ "settings.ai.eyebrow": "AI & Chat",
+ "settings.ai.title": "Chat AI",
+ "settings.ai.description": "Model, endpoint, and system instructions used by the built-in floating chat.",
+ "settings.tts.eyebrow": "Voice",
+ "settings.tts.title": "Text-to-Speech",
+ "settings.tts.description": "Voice provider and controls for the pet and floating chat.",
+ "settings.tts.placeholder": "TTS provider settings will appear here in the next build step.",
+ "settings.tts.provider.title": "Provider",
+ "settings.tts.provider.description": "Voice engine used by the pet and floating chat.",
+ "settings.tts.voice.title": "Voice",
+ "settings.tts.voice.description": "Select a voice for the current provider.",
+ "settings.tts.speed.title": "Speed",
+ "settings.tts.speed.description": "Playback speed multiplier.",
+ "settings.tts.model.title": "Model",
+ "settings.tts.model.description": "TTS model ID. Leave blank for the provider default.",
+ "settings.tts.model.placeholder": "provider default",
+ "settings.tts.endpointPreset.title": "Endpoint preset",
+ "settings.tts.endpointPreset.description": "Built-in base URL preset for OpenAI-compatible providers.",
+ "settings.tts.endpoint.title": "Custom endpoint",
+ "settings.tts.endpoint.description": "Full base or speech endpoint URL when the preset is Custom.",
+ "settings.tts.endpoint.placeholder": "https://api.example.com/v1/audio/speech",
+ "settings.tts.credential.title": "API key",
+ "settings.tts.credential.placeholder": "Paste key",
+ "settings.tts.credential.save": "Save",
+ "settings.tts.credential.clear": "Clear",
+ "settings.tts.test.title": "Test voice",
+ "settings.tts.test.description": "Preview the configured voice with a short phrase.",
+ "settings.tts.test.speak": "Speak",
+ "settings.tts.test.stop": "Stop",
+ "settings.tts.toast.providerSaved": "TTS provider saved.",
+ "settings.tts.toast.voiceSaved": "TTS voice saved.",
+ "settings.tts.toast.speedSaved": "TTS speed saved.",
+ "settings.tts.toast.modelSaved": "TTS model saved.",
+ "settings.tts.toast.endpointSaved": "TTS endpoint saved.",
+ "settings.tts.toast.credentialSaved": "TTS credential saved.",
+ "settings.tts.toast.credentialCleared": "TTS credential removed.",
"settings.plugins.audio.title": "Plugins may play sound",
"settings.plugins.audio.description": "Allow plugin chimes, alerts, and bundled sounds.",
"settings.plugins.voice.title": "Plugins may speak (voice)",
@@ -512,6 +548,24 @@ export const en = {
"integrations.busy.removingHooks": "Removing hooks",
"integrations.busy.updatingInstructions": "Updating instructions",
"integrations.busy.savingPath": "Saving path",
+ "integrations.mcpToolServers.name": "MCP Tool Servers",
+ "integrations.mcpToolServers.description": "Choose which built-in MCP tool servers power the floating chat.",
+ "integrations.mcpToolServers.status": "{count} active",
+ "integrations.mcpToolServers.noTools": "None active",
+ "integrations.mcpToolServers.builtInChat": "Built-in Chat",
+ "integrations.mcpToolServers.saved": "Saved {count} tool server(s).",
+ "integrations.openpetsMcpServer.name": "OpenPets MCP Server",
+ "integrations.openpetsMcpServer.description": "External-agent bridge that exposes your pet to Claude Code, Cursor, Codex CLI, and other MCP hosts.",
+ "integrations.openpetsMcpServer.commandModeHelp": "Choose how the OpenPets MCP server is launched for external hosts.",
+ "integrations.openpetsMcpServer.testServer": "Test Server",
+ "integrations.openpetsMcpServer.testBusy": "Testing server…",
+ "integrations.openpetsMcpServer.testOk": "Server responded: {output}",
+ "integrations.openpetsMcpServer.testError": "Server test failed: {error}",
+ "integrations.openpetsMcpServer.copyMcpJson": "Copy MCP JSON",
+ "integrations.openpetsMcpServer.status": "Ready",
+ "integrations.curatedToolkit.name": "Curated MCP Toolkit",
+ "integrations.curatedToolkit.description": "Starter stack for filesystem, Git, browser use, memory, databases, Docker, and terminal-oriented MCP lanes.",
+ "integrations.linkToCentralPanel": "Configure in the OpenPets MCP Server panel above.",
// --- Settings: Language section (renderer) ---
"settings.language.title": "Language",
diff --git a/apps/desktop/src/i18n/locales/es-419.ts b/apps/desktop/src/i18n/locales/es-419.ts
index 46de5d72..befec9e0 100644
--- a/apps/desktop/src/i18n/locales/es-419.ts
+++ b/apps/desktop/src/i18n/locales/es-419.ts
@@ -5,8 +5,8 @@ export const es419: Partial = {
// --- Tray menu (main process, src/tray.ts) ---
"tray.updateAvailable": "Actualización disponible: {version}...",
"tray.defaultPet": "Mascota predeterminada: {name}",
- "tray.showDefaultPet": "Mostrar mascota predeterminada",
- "tray.hideDefaultPet": "Ocultar mascota predeterminada",
+ "tray.toggleDefaultPet.show": "Mostrar mascota predeterminada",
+ "tray.toggleDefaultPet.hide": "Ocultar mascota predeterminada",
"tray.pauseAllPets": "Pausar todas las mascotas",
"tray.resumeAllPets": "Reanudar todas las mascotas",
"tray.managePets": "Administrar mascotas...",
@@ -267,6 +267,13 @@ export const es419: Partial = {
"settings.plugins.eyebrow": "Plataforma de complementos",
"settings.plugins.title": "Permisos de complementos e IA",
"settings.plugins.description": "Controles globales de lo que pueden hacer los complementos. Las funciones sensibles permanecen desactivadas hasta que las habilites aquí.",
+ "settings.ai.eyebrow": "AI & Chat",
+ "settings.ai.title": "Chat AI",
+ "settings.ai.description": "Model, endpoint, and system instructions used by the built-in floating chat.",
+ "settings.tts.eyebrow": "Voice",
+ "settings.tts.title": "Text-to-Speech",
+ "settings.tts.description": "Voice provider and controls for the pet and floating chat.",
+ "settings.tts.placeholder": "TTS provider settings will appear here in the next build step.",
"settings.plugins.audio.title": "Los complementos pueden reproducir sonido",
"settings.plugins.audio.description": "Permite tonos, alertas y sonidos incluidos de los complementos.",
"settings.plugins.voice.title": "Los complementos pueden hablar (voz)",
@@ -504,6 +511,24 @@ export const es419: Partial = {
"integrations.busy.removingHooks": "Quitando hooks",
"integrations.busy.updatingInstructions": "Actualizando instrucciones",
"integrations.busy.savingPath": "Guardando ruta",
+ "integrations.mcpToolServers.name": "MCP Tool Servers",
+ "integrations.mcpToolServers.description": "Choose which built-in MCP tool servers power the floating chat.",
+ "integrations.mcpToolServers.status": "{count} active",
+ "integrations.mcpToolServers.noTools": "None active",
+ "integrations.mcpToolServers.builtInChat": "Built-in Chat",
+ "integrations.mcpToolServers.saved": "Saved {count} tool server(s).",
+ "integrations.openpetsMcpServer.name": "OpenPets MCP Server",
+ "integrations.openpetsMcpServer.description": "External-agent bridge that exposes your pet to Claude Code, Cursor, Codex CLI, and other MCP hosts.",
+ "integrations.openpetsMcpServer.commandModeHelp": "Choose how the OpenPets MCP server is launched for external hosts.",
+ "integrations.openpetsMcpServer.testServer": "Test Server",
+ "integrations.openpetsMcpServer.testBusy": "Testing server…",
+ "integrations.openpetsMcpServer.testOk": "Server responded: {output}",
+ "integrations.openpetsMcpServer.testError": "Server test failed: {error}",
+ "integrations.openpetsMcpServer.copyMcpJson": "Copy MCP JSON",
+ "integrations.openpetsMcpServer.status": "Ready",
+ "integrations.curatedToolkit.name": "Curated MCP Toolkit",
+ "integrations.curatedToolkit.description": "Starter stack for filesystem, Git, browser use, memory, databases, Docker, and terminal-oriented MCP lanes.",
+ "integrations.linkToCentralPanel": "Configure in the OpenPets MCP Server panel above.",
// --- Settings: Language section (renderer) ---
"settings.language.title": "Idioma",
diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts
index c8bcfd92..b2398d53 100644
--- a/apps/desktop/src/i18n/locales/ja.ts
+++ b/apps/desktop/src/i18n/locales/ja.ts
@@ -5,8 +5,8 @@ export const ja: Partial = {
// --- Tray menu (main process, src/tray.ts) ---
"tray.updateAvailable": "アップデートがあります: {version}...",
"tray.defaultPet": "デフォルトのペット: {name}",
- "tray.showDefaultPet": "デフォルトのペットを表示",
- "tray.hideDefaultPet": "デフォルトのペットを非表示",
+ "tray.toggleDefaultPet.show": "デフォルトのペットを表示",
+ "tray.toggleDefaultPet.hide": "デフォルトのペットを非表示",
"tray.pauseAllPets": "すべてのペットを一時停止",
"tray.resumeAllPets": "すべてのペットを再開",
"tray.managePets": "ペットを管理...",
@@ -267,6 +267,13 @@ export const ja: Partial = {
"settings.plugins.eyebrow": "プラグインプラットフォーム",
"settings.plugins.title": "プラグインの権限と AI",
"settings.plugins.description": "プラグインが行える操作の全体的なゲートです。機微な機能はここで有効にするまでオフのままです。",
+ "settings.ai.eyebrow": "AI & Chat",
+ "settings.ai.title": "Chat AI",
+ "settings.ai.description": "Model, endpoint, and system instructions used by the built-in floating chat.",
+ "settings.tts.eyebrow": "Voice",
+ "settings.tts.title": "Text-to-Speech",
+ "settings.tts.description": "Voice provider and controls for the pet and floating chat.",
+ "settings.tts.placeholder": "TTS provider settings will appear here in the next build step.",
"settings.plugins.audio.title": "プラグインによる音の再生を許可",
"settings.plugins.audio.description": "プラグインのチャイム、アラート、付属サウンドを許可します。",
"settings.plugins.voice.title": "プラグインによる音声発話を許可",
@@ -504,6 +511,24 @@ export const ja: Partial = {
"integrations.busy.removingHooks": "フックを削除中",
"integrations.busy.updatingInstructions": "手順を更新中",
"integrations.busy.savingPath": "パスを保存中",
+ "integrations.mcpToolServers.name": "MCP Tool Servers",
+ "integrations.mcpToolServers.description": "Choose which built-in MCP tool servers power the floating chat.",
+ "integrations.mcpToolServers.status": "{count} active",
+ "integrations.mcpToolServers.noTools": "None active",
+ "integrations.mcpToolServers.builtInChat": "Built-in Chat",
+ "integrations.mcpToolServers.saved": "Saved {count} tool server(s).",
+ "integrations.openpetsMcpServer.name": "OpenPets MCP Server",
+ "integrations.openpetsMcpServer.description": "External-agent bridge that exposes your pet to Claude Code, Cursor, Codex CLI, and other MCP hosts.",
+ "integrations.openpetsMcpServer.commandModeHelp": "Choose how the OpenPets MCP server is launched for external hosts.",
+ "integrations.openpetsMcpServer.testServer": "Test Server",
+ "integrations.openpetsMcpServer.testBusy": "Testing server…",
+ "integrations.openpetsMcpServer.testOk": "Server responded: {output}",
+ "integrations.openpetsMcpServer.testError": "Server test failed: {error}",
+ "integrations.openpetsMcpServer.copyMcpJson": "Copy MCP JSON",
+ "integrations.openpetsMcpServer.status": "Ready",
+ "integrations.curatedToolkit.name": "Curated MCP Toolkit",
+ "integrations.curatedToolkit.description": "Starter stack for filesystem, Git, browser use, memory, databases, Docker, and terminal-oriented MCP lanes.",
+ "integrations.linkToCentralPanel": "Configure in the OpenPets MCP Server panel above.",
// --- Settings: Language section (renderer) ---
"settings.language.title": "言語",
diff --git a/apps/desktop/src/i18n/locales/ko.ts b/apps/desktop/src/i18n/locales/ko.ts
index 82d3b26f..1bcf734d 100644
--- a/apps/desktop/src/i18n/locales/ko.ts
+++ b/apps/desktop/src/i18n/locales/ko.ts
@@ -5,8 +5,8 @@ export const ko: Partial = {
// --- Tray menu (main process, src/tray.ts) ---
"tray.updateAvailable": "업데이트 사용 가능: {version}...",
"tray.defaultPet": "기본 펫: {name}",
- "tray.showDefaultPet": "기본 펫 표시",
- "tray.hideDefaultPet": "기본 펫 숨기기",
+ "tray.toggleDefaultPet.show": "기본 펫 표시",
+ "tray.toggleDefaultPet.hide": "기본 펫 숨기기",
"tray.pauseAllPets": "모든 펫 일시정지",
"tray.resumeAllPets": "모든 펫 재개",
"tray.managePets": "펫 관리...",
@@ -267,6 +267,13 @@ export const ko: Partial = {
"settings.plugins.eyebrow": "플러그인 플랫폼",
"settings.plugins.title": "플러그인 권한 및 AI",
"settings.plugins.description": "플러그인이 할 수 있는 작업에 대한 전역 제어입니다. 민감한 기능은 여기에서 활성화하기 전까지 꺼져 있습니다.",
+ "settings.ai.eyebrow": "AI & Chat",
+ "settings.ai.title": "Chat AI",
+ "settings.ai.description": "Model, endpoint, and system instructions used by the built-in floating chat.",
+ "settings.tts.eyebrow": "Voice",
+ "settings.tts.title": "Text-to-Speech",
+ "settings.tts.description": "Voice provider and controls for the pet and floating chat.",
+ "settings.tts.placeholder": "TTS provider settings will appear here in the next build step.",
"settings.plugins.audio.title": "플러그인 소리 재생 허용",
"settings.plugins.audio.description": "플러그인 알림음, 경고음, 번들 사운드를 허용합니다.",
"settings.plugins.voice.title": "플러그인 음성 말하기 허용",
@@ -504,6 +511,24 @@ export const ko: Partial = {
"integrations.busy.removingHooks": "훅 제거 중",
"integrations.busy.updatingInstructions": "지침 업데이트 중",
"integrations.busy.savingPath": "경로 저장 중",
+ "integrations.mcpToolServers.name": "MCP Tool Servers",
+ "integrations.mcpToolServers.description": "Choose which built-in MCP tool servers power the floating chat.",
+ "integrations.mcpToolServers.status": "{count} active",
+ "integrations.mcpToolServers.noTools": "None active",
+ "integrations.mcpToolServers.builtInChat": "Built-in Chat",
+ "integrations.mcpToolServers.saved": "Saved {count} tool server(s).",
+ "integrations.openpetsMcpServer.name": "OpenPets MCP Server",
+ "integrations.openpetsMcpServer.description": "External-agent bridge that exposes your pet to Claude Code, Cursor, Codex CLI, and other MCP hosts.",
+ "integrations.openpetsMcpServer.commandModeHelp": "Choose how the OpenPets MCP server is launched for external hosts.",
+ "integrations.openpetsMcpServer.testServer": "Test Server",
+ "integrations.openpetsMcpServer.testBusy": "Testing server…",
+ "integrations.openpetsMcpServer.testOk": "Server responded: {output}",
+ "integrations.openpetsMcpServer.testError": "Server test failed: {error}",
+ "integrations.openpetsMcpServer.copyMcpJson": "Copy MCP JSON",
+ "integrations.openpetsMcpServer.status": "Ready",
+ "integrations.curatedToolkit.name": "Curated MCP Toolkit",
+ "integrations.curatedToolkit.description": "Starter stack for filesystem, Git, browser use, memory, databases, Docker, and terminal-oriented MCP lanes.",
+ "integrations.linkToCentralPanel": "Configure in the OpenPets MCP Server panel above.",
// --- Settings: Language section (renderer) ---
"settings.language.title": "언어",
diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts
index 8d27ecca..7dfa0274 100644
--- a/apps/desktop/src/i18n/locales/pt-BR.ts
+++ b/apps/desktop/src/i18n/locales/pt-BR.ts
@@ -5,8 +5,8 @@ export const ptBR: Partial = {
// --- Tray menu (main process, src/tray.ts) ---
"tray.updateAvailable": "Atualização disponível: {version}...",
"tray.defaultPet": "Pet padrão: {name}",
- "tray.showDefaultPet": "Mostrar pet padrão",
- "tray.hideDefaultPet": "Ocultar pet padrão",
+ "tray.toggleDefaultPet.show": "Mostrar pet padrão",
+ "tray.toggleDefaultPet.hide": "Ocultar pet padrão",
"tray.pauseAllPets": "Pausar todos os pets",
"tray.resumeAllPets": "Retomar todos os pets",
"tray.managePets": "Gerenciar pets...",
@@ -267,6 +267,13 @@ export const ptBR: Partial = {
"settings.plugins.eyebrow": "Plataforma de plugins",
"settings.plugins.title": "Permissões de plugins e IA",
"settings.plugins.description": "Controles globais do que os plugins podem fazer. Recursos sensíveis ficam desativados até que você os ative aqui.",
+ "settings.ai.eyebrow": "AI & Chat",
+ "settings.ai.title": "Chat AI",
+ "settings.ai.description": "Model, endpoint, and system instructions used by the built-in floating chat.",
+ "settings.tts.eyebrow": "Voice",
+ "settings.tts.title": "Text-to-Speech",
+ "settings.tts.description": "Voice provider and controls for the pet and floating chat.",
+ "settings.tts.placeholder": "TTS provider settings will appear here in the next build step.",
"settings.plugins.audio.title": "Plugins podem reproduzir som",
"settings.plugins.audio.description": "Permitir sinos, alertas e sons incluídos dos plugins.",
"settings.plugins.voice.title": "Plugins podem falar (voz)",
@@ -504,6 +511,24 @@ export const ptBR: Partial = {
"integrations.busy.removingHooks": "Removendo hooks",
"integrations.busy.updatingInstructions": "Atualizando instruções",
"integrations.busy.savingPath": "Salvando caminho",
+ "integrations.mcpToolServers.name": "MCP Tool Servers",
+ "integrations.mcpToolServers.description": "Choose which built-in MCP tool servers power the floating chat.",
+ "integrations.mcpToolServers.status": "{count} active",
+ "integrations.mcpToolServers.noTools": "None active",
+ "integrations.mcpToolServers.builtInChat": "Built-in Chat",
+ "integrations.mcpToolServers.saved": "Saved {count} tool server(s).",
+ "integrations.openpetsMcpServer.name": "OpenPets MCP Server",
+ "integrations.openpetsMcpServer.description": "External-agent bridge that exposes your pet to Claude Code, Cursor, Codex CLI, and other MCP hosts.",
+ "integrations.openpetsMcpServer.commandModeHelp": "Choose how the OpenPets MCP server is launched for external hosts.",
+ "integrations.openpetsMcpServer.testServer": "Test Server",
+ "integrations.openpetsMcpServer.testBusy": "Testing server…",
+ "integrations.openpetsMcpServer.testOk": "Server responded: {output}",
+ "integrations.openpetsMcpServer.testError": "Server test failed: {error}",
+ "integrations.openpetsMcpServer.copyMcpJson": "Copy MCP JSON",
+ "integrations.openpetsMcpServer.status": "Ready",
+ "integrations.curatedToolkit.name": "Curated MCP Toolkit",
+ "integrations.curatedToolkit.description": "Starter stack for filesystem, Git, browser use, memory, databases, Docker, and terminal-oriented MCP lanes.",
+ "integrations.linkToCentralPanel": "Configure in the OpenPets MCP Server panel above.",
// --- Settings: Language section (renderer) ---
"settings.language.title": "Idioma",
diff --git a/apps/desktop/src/i18n/locales/zh-Hans.ts b/apps/desktop/src/i18n/locales/zh-Hans.ts
index 97175bf1..c8343916 100644
--- a/apps/desktop/src/i18n/locales/zh-Hans.ts
+++ b/apps/desktop/src/i18n/locales/zh-Hans.ts
@@ -5,8 +5,8 @@ export const zhHans: Partial = {
// --- Tray menu (main process, src/tray.ts) ---
"tray.updateAvailable": "有可用更新:{version}...",
"tray.defaultPet": "默认宠物:{name}",
- "tray.showDefaultPet": "显示默认宠物",
- "tray.hideDefaultPet": "隐藏默认宠物",
+ "tray.toggleDefaultPet.show": "显示默认宠物",
+ "tray.toggleDefaultPet.hide": "隐藏默认宠物",
"tray.pauseAllPets": "暂停所有宠物",
"tray.resumeAllPets": "恢复所有宠物",
"tray.managePets": "管理宠物...",
@@ -267,6 +267,13 @@ export const zhHans: Partial = {
"settings.plugins.eyebrow": "插件平台",
"settings.plugins.title": "插件权限与 AI",
"settings.plugins.description": "插件行为的全局开关。敏感能力在你于此启用前保持关闭。",
+ "settings.ai.eyebrow": "AI & Chat",
+ "settings.ai.title": "Chat AI",
+ "settings.ai.description": "Model, endpoint, and system instructions used by the built-in floating chat.",
+ "settings.tts.eyebrow": "Voice",
+ "settings.tts.title": "Text-to-Speech",
+ "settings.tts.description": "Voice provider and controls for the pet and floating chat.",
+ "settings.tts.placeholder": "TTS provider settings will appear here in the next build step.",
"settings.plugins.audio.title": "允许插件播放声音",
"settings.plugins.audio.description": "允许插件播放提示音、警报和内置音效。",
"settings.plugins.voice.title": "允许插件说话(语音)",
@@ -504,6 +511,24 @@ export const zhHans: Partial = {
"integrations.busy.removingHooks": "移除钩子中",
"integrations.busy.updatingInstructions": "更新说明中",
"integrations.busy.savingPath": "保存路径中",
+ "integrations.mcpToolServers.name": "MCP Tool Servers",
+ "integrations.mcpToolServers.description": "Choose which built-in MCP tool servers power the floating chat.",
+ "integrations.mcpToolServers.status": "{count} active",
+ "integrations.mcpToolServers.noTools": "None active",
+ "integrations.mcpToolServers.builtInChat": "Built-in Chat",
+ "integrations.mcpToolServers.saved": "Saved {count} tool server(s).",
+ "integrations.openpetsMcpServer.name": "OpenPets MCP Server",
+ "integrations.openpetsMcpServer.description": "External-agent bridge that exposes your pet to Claude Code, Cursor, Codex CLI, and other MCP hosts.",
+ "integrations.openpetsMcpServer.commandModeHelp": "Choose how the OpenPets MCP server is launched for external hosts.",
+ "integrations.openpetsMcpServer.testServer": "Test Server",
+ "integrations.openpetsMcpServer.testBusy": "Testing server…",
+ "integrations.openpetsMcpServer.testOk": "Server responded: {output}",
+ "integrations.openpetsMcpServer.testError": "Server test failed: {error}",
+ "integrations.openpetsMcpServer.copyMcpJson": "Copy MCP JSON",
+ "integrations.openpetsMcpServer.status": "Ready",
+ "integrations.curatedToolkit.name": "Curated MCP Toolkit",
+ "integrations.curatedToolkit.description": "Starter stack for filesystem, Git, browser use, memory, databases, Docker, and terminal-oriented MCP lanes.",
+ "integrations.linkToCentralPanel": "Configure in the OpenPets MCP Server panel above.",
// --- Settings: Language section (renderer) ---
"settings.language.title": "语言",
diff --git a/apps/desktop/src/i18n/locales/zh-Hant.ts b/apps/desktop/src/i18n/locales/zh-Hant.ts
index 088af3d6..588f3ab8 100644
--- a/apps/desktop/src/i18n/locales/zh-Hant.ts
+++ b/apps/desktop/src/i18n/locales/zh-Hant.ts
@@ -5,8 +5,8 @@ export const zhHant: Partial = {
// --- Tray menu (main process, src/tray.ts) ---
"tray.updateAvailable": "有可用更新:{version}...",
"tray.defaultPet": "預設寵物:{name}",
- "tray.showDefaultPet": "顯示預設寵物",
- "tray.hideDefaultPet": "隱藏預設寵物",
+ "tray.toggleDefaultPet.show": "顯示預設寵物",
+ "tray.toggleDefaultPet.hide": "隱藏預設寵物",
"tray.pauseAllPets": "暫停所有寵物",
"tray.resumeAllPets": "恢復所有寵物",
"tray.managePets": "管理寵物...",
@@ -267,6 +267,13 @@ export const zhHant: Partial = {
"settings.plugins.eyebrow": "外掛平台",
"settings.plugins.title": "外掛權限與 AI",
"settings.plugins.description": "控管外掛可執行行為的全域開關。敏感功能在你於此啟用前都會保持關閉。",
+ "settings.ai.eyebrow": "AI & Chat",
+ "settings.ai.title": "Chat AI",
+ "settings.ai.description": "Model, endpoint, and system instructions used by the built-in floating chat.",
+ "settings.tts.eyebrow": "Voice",
+ "settings.tts.title": "Text-to-Speech",
+ "settings.tts.description": "Voice provider and controls for the pet and floating chat.",
+ "settings.tts.placeholder": "TTS provider settings will appear here in the next build step.",
"settings.plugins.audio.title": "允許外掛播放聲音",
"settings.plugins.audio.description": "允許外掛的提示音、警示音與內建音效。",
"settings.plugins.voice.title": "允許外掛說話(語音)",
@@ -504,6 +511,24 @@ export const zhHant: Partial = {
"integrations.busy.removingHooks": "移除 Hooks 中",
"integrations.busy.updatingInstructions": "更新指示中",
"integrations.busy.savingPath": "儲存路徑中",
+ "integrations.mcpToolServers.name": "MCP Tool Servers",
+ "integrations.mcpToolServers.description": "Choose which built-in MCP tool servers power the floating chat.",
+ "integrations.mcpToolServers.status": "{count} active",
+ "integrations.mcpToolServers.noTools": "None active",
+ "integrations.mcpToolServers.builtInChat": "Built-in Chat",
+ "integrations.mcpToolServers.saved": "Saved {count} tool server(s).",
+ "integrations.openpetsMcpServer.name": "OpenPets MCP Server",
+ "integrations.openpetsMcpServer.description": "External-agent bridge that exposes your pet to Claude Code, Cursor, Codex CLI, and other MCP hosts.",
+ "integrations.openpetsMcpServer.commandModeHelp": "Choose how the OpenPets MCP server is launched for external hosts.",
+ "integrations.openpetsMcpServer.testServer": "Test Server",
+ "integrations.openpetsMcpServer.testBusy": "Testing server…",
+ "integrations.openpetsMcpServer.testOk": "Server responded: {output}",
+ "integrations.openpetsMcpServer.testError": "Server test failed: {error}",
+ "integrations.openpetsMcpServer.copyMcpJson": "Copy MCP JSON",
+ "integrations.openpetsMcpServer.status": "Ready",
+ "integrations.curatedToolkit.name": "Curated MCP Toolkit",
+ "integrations.curatedToolkit.description": "Starter stack for filesystem, Git, browser use, memory, databases, Docker, and terminal-oriented MCP lanes.",
+ "integrations.linkToCentralPanel": "Configure in the OpenPets MCP Server panel above.",
// --- Settings: Language section (renderer) ---
"settings.language.title": "語言",
diff --git a/apps/desktop/src/logger.ts b/apps/desktop/src/logger.ts
index bb1e8dae..f61c5df5 100644
--- a/apps/desktop/src/logger.ts
+++ b/apps/desktop/src/logger.ts
@@ -4,7 +4,7 @@ import { appendFile } from "node:fs/promises";
import { dirname, join } from "node:path";
export type LogLevel = "debug" | "info" | "warn" | "error";
-export type LogScope = "app" | "ipc" | "lease" | "pet.default" | "pet.agent" | "pet.window" | "plugin" | "state" | "tray" | "ui";
+export type LogScope = "app" | "ipc" | "lease" | "pet.default" | "pet.agent" | "pet.window" | "plugin" | "state" | "tray" | "tts" | "ui";
type LogFields = Record;
diff --git a/apps/desktop/src/openapi-chat.ts b/apps/desktop/src/openapi-chat.ts
index b8cfbd28..ea18d959 100644
--- a/apps/desktop/src/openapi-chat.ts
+++ b/apps/desktop/src/openapi-chat.ts
@@ -9,6 +9,7 @@ import type { OpenPetsReaction } from "./local-ipc-protocol.js";
import { buildRelevantKnowledgeContext } from "./knowledge-store.js";
import { buildRelevantMemoryContext, capturePromptMemories } from "./openpets-memory.js";
import { getMcpChatClientManager, type McpChatToolDefinition } from "./mcp-chat-client.js";
+import { speakTts } from "./tts-service.js";
type OpenApiChatTransport = "responses" | "chat-completions";
@@ -294,6 +295,12 @@ export async function sendOpenApiChatPrompt(prompt: string): Promise {
+ warn("app", "TTS for assistant response failed", { error: error instanceof Error ? error.message : String(error) });
+ });
+}
+
async function sendOpenApiChatPromptPlain(prompt: string): Promise {
const credential = getRequiredCredential();
const model = getConfiguredOpenApiChatModel();
@@ -385,6 +392,7 @@ async function sendOpenApiChatPromptPlain(prompt: string): Promise {
- const window = getDefaultPetWindowForPlugins();
- if (!window) throw new Error("No pet window is available for speech.");
- speakPetWindowTts(window, text, opts);
+export async function pluginVoiceSpeak(text: string, _opts: { voice?: string; rate?: number }): Promise {
+ await speakTts(text, "pet");
}
export function pluginVoiceStop(): void {
- const window = getDefaultPetWindowForPlugins();
- if (window) stopPetWindowTts(window);
+ stopTts("pet");
}
let listenInProgress = false;
diff --git a/apps/desktop/src/prompt-window.ts b/apps/desktop/src/prompt-window.ts
index 03056c6b..050ef0b2 100644
--- a/apps/desktop/src/prompt-window.ts
+++ b/apps/desktop/src/prompt-window.ts
@@ -138,6 +138,10 @@ export function installPromptWindowHandlers(): void {
});
}
+export function getPromptWindow(): BrowserWindow | null {
+ return promptWindow && !promptWindow.isDestroyed() ? promptWindow : null;
+}
+
export function openPromptWindow(): void {
const existing = promptWindow;
if (existing && !existing.isDestroyed()) {
diff --git a/apps/desktop/src/renderer/src/main.tsx b/apps/desktop/src/renderer/src/main.tsx
index b3a2c83c..7ef60cfa 100644
--- a/apps/desktop/src/renderer/src/main.tsx
+++ b/apps/desktop/src/renderer/src/main.tsx
@@ -26,7 +26,7 @@ type ThemeMode = "system" | "light" | "dark";
type UserSelectableAnimationState = "idle" | "review" | "running" | "waiting" | "waving" | "jumping" | "failed";
type ReactionAnimationOverrides = Record;
type OpenApiChatSettings = { model: string; endpoint: string; defaultEndpoint: string; usingDefaultEndpoint: boolean; hasCredential: boolean; storageMode: "encrypted" | "plain" };
-type SettingsState = { preferences: { openDefaultPetOnLaunch: boolean; locale?: "system" | string; petScale: number; reactionAnimationOverrides?: ReactionAnimationOverrides; openApiChatModel?: string; openApiChatSystemPrompt?: string; openApiChatEndpoint?: string; openApiChatTheme?: ThemeMode; openApiChatBaseInstructionsEnabled?: boolean }; petScaleOptions: PetScaleOption[]; openApiChat: OpenApiChatSettings };
+type SettingsState = { preferences: { openDefaultPetOnLaunch: boolean; locale?: "system" | string; petScale: number; reactionAnimationOverrides?: ReactionAnimationOverrides; openApiChatModel?: string; openApiChatSystemPrompt?: string; openApiChatEndpoint?: string; openApiChatTheme?: ThemeMode; openApiChatBaseInstructionsEnabled?: boolean; ttsProvider?: TtsProviderId; ttsVoice?: string; ttsSpeed?: number; ttsModel?: string; ttsEndpointPreset?: "openrouter" | "litellm" | "wavespeedai" | "custom"; ttsEndpoint?: string }; petScaleOptions: PetScaleOption[]; openApiChat: OpenApiChatSettings };
type LaunchAtLoginState = { supported: boolean; enabled: boolean };
type UpdateStatus = { state: "idle" | "checking" | "available" | "current" | "error"; currentVersion: string; latestVersion?: string; releaseUrl?: string; checkedAt?: number; error?: string };
type DashboardActivity = { messagesSent: number; reactionsSent: number; reactionCounts: Record; perPetActivityCounts: Record; lastActivityAt?: number };
@@ -128,6 +128,14 @@ type ControlCenterApi = {
getIntegrationsState(selectedPetId?: string, commandMode?: "published" | "local" | "bundled"): Promise;
runIntegrationAction(action: AgentSetupAction, selectedPetId?: string, commandMode?: "published" | "local" | "bundled"): Promise;
updateIntegrationCommandPaths(patch: Partial): Promise;
+ getOpenPetsMcpServerPreview(selectedPetId?: string, commandMode?: "published" | "local" | "bundled"): Promise;
+ testOpenPetsMcpServer(selectedPetId?: string, commandMode?: "published" | "local" | "bundled"): Promise;
+ getTtsSettings(): Promise;
+ getTtsVoices(provider: TtsProviderId): Promise;
+ saveTtsCredential(provider: TtsProviderId, credential: string): Promise;
+ clearTtsCredential(provider: TtsProviderId): Promise;
+ testTtsSpeak(text: string): Promise;
+ stopTts(): Promise;
copyText(text: string): Promise;
openExternalUrl(url: string): Promise;
installMcpToolkit(target: McpToolkitPersistentTarget): Promise;
@@ -157,6 +165,13 @@ type CursorSetupPreview = { global: true; configPath: string; mcpEntry: Record }; status: ClaudeCodeStatus; hookStatus: ClaudeHookDoctorResult; memoryStatus: ClaudeOpenPetsMemoryStatus; opencodeStatus: OpenCodeSetupStatus; opencodePreview: OpenCodeSetupPreview; cursorStatus: CursorSetupStatus; cursorPreview: CursorSetupPreview; commandPaths: AgentSetupCommandPaths; busy: boolean; lastAction?: AgentSetupActionResult };
+type OpenPetsMcpServerPreview = { commandMode: "published" | "local" | "bundled"; command: string; args: readonly string[]; displayCommand: string; mcpJson: { mcpServers: { openpets: { type: "stdio"; command: string; args: readonly string[] } } } };
+type OpenPetsMcpServerHealth = { ok: boolean; output: string; error?: string };
+type TtsProviderId = "system" | "openai" | "elevenlabs" | "piper" | "openai-compatible";
+type TtsEndpointPreset = "openrouter" | "litellm" | "wavespeedai" | "custom";
+type TtsVoice = { id: string; label: string };
+type TtsCredentialStatus = { hasCredential: boolean; storageMode: "encrypted" | "plain"; provider: string };
+type TtsSettingsSnapshot = { provider: TtsProviderId; voice: string; speed: number; model: string; endpointPreset: TtsEndpointPreset; endpoint: string; hasCredential: boolean; providers: { id: TtsProviderId; label: string; defaultModel: string; defaultVoice: string; voices: readonly TtsVoice[] }[] };
type StatusTone = keyof typeof statusPillToneClass;
type OpenApiEndpointPreset = {
id: string;
@@ -547,6 +562,21 @@ const BoxIcon = () => (
);
+const WrenchIcon = () => (
+
+);
+
+const ServerIcon = () => (
+
+);
+
const ShieldIcon = () => (