voicecc 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +6 -0
- package/README.md +48 -0
- package/bin/voicecc.js +39 -0
- package/dashboard/dist/assets/index-BXemFrMp.css +1 -0
- package/dashboard/dist/assets/index-dAYfRls7.js +11 -0
- package/dashboard/dist/audio-processor.js +126 -0
- package/dashboard/dist/index.html +13 -0
- package/dashboard/routes/auth.ts +119 -0
- package/dashboard/routes/browser-call.ts +87 -0
- package/dashboard/routes/claude-md.ts +50 -0
- package/dashboard/routes/conversations.ts +203 -0
- package/dashboard/routes/integrations.ts +154 -0
- package/dashboard/routes/mcp-servers.ts +198 -0
- package/dashboard/routes/settings.ts +64 -0
- package/dashboard/routes/tunnel.ts +66 -0
- package/dashboard/routes/twilio.ts +120 -0
- package/dashboard/routes/voice.ts +48 -0
- package/dashboard/routes/webrtc.ts +85 -0
- package/dashboard/server.ts +130 -0
- package/dashboard/tsconfig.json +13 -0
- package/init/CLAUDE.md +18 -0
- package/package.json +59 -0
- package/run.ts +68 -0
- package/scripts/postinstall.js +228 -0
- package/services/browser-call-manager.ts +106 -0
- package/services/device-pairing.ts +176 -0
- package/services/env.ts +88 -0
- package/services/tunnel.ts +204 -0
- package/services/twilio-manager.ts +126 -0
- package/sidecar/assets/startup.pcm +0 -0
- package/sidecar/audio-adapter.ts +60 -0
- package/sidecar/audio-capture.ts +220 -0
- package/sidecar/browser-audio-playback.test.ts +149 -0
- package/sidecar/browser-audio.ts +147 -0
- package/sidecar/browser-server.ts +331 -0
- package/sidecar/chime.test.ts +69 -0
- package/sidecar/chime.ts +54 -0
- package/sidecar/claude-session.ts +295 -0
- package/sidecar/endpointing.ts +163 -0
- package/sidecar/index.ts +83 -0
- package/sidecar/local-audio.ts +126 -0
- package/sidecar/mic-vpio +0 -0
- package/sidecar/mic-vpio.swift +484 -0
- package/sidecar/mock-tts-server-tagged.mjs +132 -0
- package/sidecar/narration.ts +204 -0
- package/sidecar/scripts/generate-startup-audio.py +79 -0
- package/sidecar/session-lock.ts +123 -0
- package/sidecar/sherpa-onnx-node.d.ts +4 -0
- package/sidecar/stt.ts +199 -0
- package/sidecar/tts-server.py +193 -0
- package/sidecar/tts.ts +481 -0
- package/sidecar/twilio-audio.ts +338 -0
- package/sidecar/twilio-server.ts +436 -0
- package/sidecar/types.ts +210 -0
- package/sidecar/vad.ts +101 -0
- package/sidecar/voice-loop-bugs.test.ts +522 -0
- package/sidecar/voice-session.ts +523 -0
- package/skills/voice/SKILL.md +26 -0
- package/tsconfig.json +22 -0
package/README.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Voice CC
|
|
2
|
+
|
|
3
|
+
A Claude Code plugin for hands-free voice interaction with local speech-to-text, text-to-speech, and voice activity detection.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
### Prerequisites
|
|
8
|
+
|
|
9
|
+
- macOS with Apple Silicon (M1/M2/M3/M4)
|
|
10
|
+
- Node.js 18+
|
|
11
|
+
- Python 3.10+
|
|
12
|
+
- Homebrew
|
|
13
|
+
|
|
14
|
+
### Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
# 1. Install system dependencies
|
|
18
|
+
xcode-select --install
|
|
19
|
+
brew install espeak-ng cloudflared
|
|
20
|
+
|
|
21
|
+
# 2. Install Voice CC
|
|
22
|
+
npm install -g voicecc
|
|
23
|
+
|
|
24
|
+
# 3. Start the dashboard
|
|
25
|
+
voicecc
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The postinstall script handles sox, the Whisper model, Python venv, and TTS dependencies automatically.
|
|
29
|
+
|
|
30
|
+
## How It Works
|
|
31
|
+
|
|
32
|
+
The voice loop runs locally with zero external API calls except to Claude:
|
|
33
|
+
|
|
34
|
+
1. **Mic capture**: VPIO (macOS Voice Processing IO) records 16kHz mono PCM with echo cancellation
|
|
35
|
+
2. **Voice activity detection**: Silero VAD v5 detects speech segments
|
|
36
|
+
3. **Speech-to-text**: sherpa-onnx (Whisper ONNX model) transcribes audio locally
|
|
37
|
+
4. **Endpointing**: VAD silence-based turn detection
|
|
38
|
+
5. **Claude inference**: Transcript sent to Claude Code Agent SDK session with streaming response
|
|
39
|
+
6. **Narration**: Claude's response stripped of markdown and split into sentences
|
|
40
|
+
7. **Text-to-speech**: Kokoro-82M via mlx-audio on Apple Silicon GPU (~8x realtime)
|
|
41
|
+
8. **Speaker playback**: Audio output through VPIO at 24kHz with echo cancellation
|
|
42
|
+
|
|
43
|
+
## Troubleshooting
|
|
44
|
+
|
|
45
|
+
- **"sox not found"**: Install sox with `brew install sox`
|
|
46
|
+
- **"espeak not installed"**: Install espeak-ng with `brew install espeak-ng`
|
|
47
|
+
- **tts-server.py not ready**: Ensure the Python venv is set up correctly
|
|
48
|
+
- **Mic permission denied**: Grant microphone permissions to your terminal or IDE
|
package/bin/voicecc.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CLI entry point for the voicecc command.
|
|
5
|
+
*
|
|
6
|
+
* Resolves the package install directory and spawns `tsx run.ts`
|
|
7
|
+
* with inherited stdio so the dashboard server runs in the foreground.
|
|
8
|
+
*
|
|
9
|
+
* Responsibilities:
|
|
10
|
+
* - Resolve the package root from this script's location
|
|
11
|
+
* - Spawn tsx with run.ts in the correct working directory
|
|
12
|
+
* - Forward signals (SIGINT, SIGTERM) so Ctrl+C stops the server cleanly
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { spawn } from "node:child_process";
|
|
16
|
+
import { dirname, join } from "node:path";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// CONSTANTS
|
|
21
|
+
// ============================================================================
|
|
22
|
+
|
|
23
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
24
|
+
const PKG_ROOT = join(__dirname, "..");
|
|
25
|
+
const TSX_BIN = join(PKG_ROOT, "node_modules", ".bin", "tsx");
|
|
26
|
+
|
|
27
|
+
// ============================================================================
|
|
28
|
+
// MAIN ENTRYPOINT
|
|
29
|
+
// ============================================================================
|
|
30
|
+
|
|
31
|
+
const child = spawn(TSX_BIN, ["run.ts"], {
|
|
32
|
+
cwd: PKG_ROOT,
|
|
33
|
+
stdio: "inherit",
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
process.on("SIGINT", () => child.kill("SIGINT"));
|
|
37
|
+
process.on("SIGTERM", () => child.kill("SIGTERM"));
|
|
38
|
+
|
|
39
|
+
child.on("exit", (code) => process.exit(code ?? 1));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@import"https://fonts.googleapis.com/css2?family=IBM+Plex+Serif:ital,wght@0,100..700;1,100..700&display=swap";:root{--bg-main: #ffffff;--bg-sidebar: #f7f7f8;--bg-surface: #ffffff;--bg-hover: #f1f3f5;--bg-active: #e9ecef;--border-color: #eaeaea;--text-primary: #111827;--text-secondary: #6b7280;--accent-color: #2ea043;--btn-primary-bg: #111111;--btn-primary-text: #ffffff;--btn-primary-hover: #333333;--btn-secondary-bg: #ffffff;--btn-secondary-text: #111827;--btn-secondary-border: #eaeaea;--btn-secondary-hover: #f9fafb}@media(prefers-color-scheme:dark){:root{--bg-main: #0a0a0a;--bg-sidebar: #111111;--bg-surface: #171717;--bg-hover: #262626;--bg-active: #333333;--border-color: #333333;--text-primary: #f3f4f6;--text-secondary: #9ca3af;--accent-color: #3fb950;--btn-primary-bg: #ececec;--btn-primary-text: #111111;--btn-primary-hover: #ffffff;--btn-secondary-bg: #111111;--btn-secondary-text: #f3f4f6;--btn-secondary-border: #333333;--btn-secondary-hover: #1f1f1f}}body.dark{--bg-main: #0a0a0a;--bg-sidebar: #111111;--bg-surface: #171717;--bg-hover: #262626;--bg-active: #333333;--border-color: #333333;--text-primary: #f3f4f6;--text-secondary: #9ca3af;--accent-color: #3fb950;--btn-primary-bg: #ececec;--btn-primary-text: #111111;--btn-primary-hover: #ffffff;--btn-secondary-bg: #111111;--btn-secondary-text: #f3f4f6;--btn-secondary-border: #333333;--btn-secondary-hover: #1f1f1f}body.light{--bg-main: #ffffff;--bg-sidebar: #f7f7f8;--bg-surface: #ffffff;--bg-hover: #f1f3f5;--bg-active: #e9ecef;--border-color: #eaeaea;--text-primary: #111827;--text-secondary: #6b7280;--accent-color: #2ea043;--btn-primary-bg: #111111;--btn-primary-text: #ffffff;--btn-primary-hover: #333333;--btn-secondary-bg: #ffffff;--btn-secondary-text: #111827;--btn-secondary-border: #eaeaea;--btn-secondary-hover: #f9fafb}*{margin:0;padding:0;box-sizing:border-box}body{font-family:-apple-system,BlinkMacSystemFont,Inter,Segoe UI,Roboto,sans-serif;background:var(--bg-main);color:var(--text-primary);height:100vh;overflow:hidden}#root{height:100vh}.sidebar{width:240px;background:var(--bg-sidebar);border-right:1px solid var(--border-color);display:flex;flex-direction:column;flex-shrink:0;height:100vh}.sidebar-nav{flex:1;padding:16px 12px;display:flex;flex-direction:column;overflow:hidden}.sidebar-item{display:flex;align-items:center;gap:8px;margin-bottom:2px;padding:8px 12px;font-size:13px;font-weight:500;color:var(--text-secondary);background:transparent;border:none;border-radius:0;text-align:left;cursor:pointer;font-family:inherit;transition:all .15s ease;flex-shrink:0}.sidebar-item:hover{color:var(--text-primary);background:var(--bg-hover)}.sidebar-item.active{color:var(--accent-color);background:#2ea0431a;font-weight:600}.sidebar-footer{padding:12px 0}.sidebar-section-label{padding:16px 24px 8px;font-size:11px;font-weight:600;color:var(--text-secondary);text-transform:uppercase;letter-spacing:.5px}.sidebar-conversation{display:block;margin-bottom:2px;padding:8px 12px;font-size:13px;color:var(--text-secondary);background:transparent;border:none;border-radius:0;text-align:left;cursor:pointer;font-family:inherit;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex-shrink:0}.sidebar-conversation:hover{color:var(--text-primary);background:var(--bg-hover)}.sidebar-conversation.active{color:var(--text-primary);background:var(--bg-active);font-weight:500}.btn-start-voice{display:block;width:calc(100% - 24px);margin:0 12px 8px;padding:8px 12px;font-size:13px;font-weight:500;font-family:inherit;color:var(--btn-primary-text);background:var(--btn-primary-bg);border:none;border-radius:0;cursor:pointer;text-align:center;transition:all .2s}.btn-start-voice:hover{background:var(--btn-primary-hover)}.btn-start-voice:disabled{opacity:.5;cursor:not-allowed}.btn-browser-call{display:block;width:calc(100% - 24px);margin:0 12px 16px;padding:8px 12px;font-size:13px;font-weight:500;font-family:inherit;color:var(--btn-secondary-text);background:var(--btn-secondary-bg);border:1px solid var(--btn-secondary-border);border-radius:0;cursor:pointer;text-align:center;transition:all .2s}.btn-browser-call:hover{background:var(--btn-secondary-hover)}.btn-browser-call:disabled{opacity:.5;cursor:not-allowed}.browser-call-wrap{display:block;width:100%}.browser-call-tooltip{display:none;position:absolute;left:50%;transform:translate(-50%);bottom:calc(100% + 6px);padding:4px 10px;font-size:12px;color:var(--text-primary);background:var(--bg-surface);border:1px solid var(--border-color);white-space:nowrap;pointer-events:none;z-index:10}.browser-call-wrap:hover .browser-call-tooltip{display:block}.main{flex:1;display:flex;flex-direction:column;overflow:hidden}.page{display:none;flex:1;flex-direction:column;overflow:hidden}.page.active{display:flex}.page-header{background:var(--bg-main);padding:24px 32px 16px;display:flex;align-items:center;justify-content:space-between;flex-shrink:0}.page-header h1{font-size:24px;font-weight:600;color:var(--text-primary);letter-spacing:-.5px}.toolbar{display:flex;align-items:center;gap:10px}.toolbar button{background:var(--btn-primary-bg);color:#fff;border:none;padding:5px 14px;border-radius:0;font-size:12px;cursor:pointer}.toolbar button:hover{background:var(--btn-primary-hover)}.toolbar button:disabled{background:var(--text-secondary);cursor:default}#status{font-size:12px;color:var(--text-secondary)}.warning{background:#3e2f00;border-bottom:1px solid #665500;padding:8px 16px;font-size:12px;color:#cca700;display:none;flex-shrink:0}.warning.visible{display:block}.warning code{background:#2a2000;padding:1px 4px;border-radius:0}#editor{flex:1;width:100%;background:var(--bg-surface);color:var(--text-primary);border:none;padding:16px 20px;font-family:SF Mono,Fira Code,Cascadia Code,monospace;font-size:13px;line-height:1.6;resize:none;outline:none;tab-size:2}.settings-panel{background:var(--bg-surface);border:1px solid var(--border-color);border-radius:0;padding:24px;margin-bottom:24px;box-shadow:0 1px 3px #0000000d}.settings-panel h2{font-size:13px;font-weight:600;color:var(--text-primary);margin-bottom:12px}.settings-row{display:flex;align-items:center;margin-bottom:8px;gap:10px}.settings-row label{font-size:12px;color:var(--text-secondary);width:200px;flex-shrink:0}.settings-row input{flex:1;max-width:340px;background:var(--bg-main);border:1px solid var(--border-color);color:var(--text-primary);padding:8px 12px;font-size:13px;border-radius:0;outline:none;transition:border-color .2s}.settings-row input:focus{border-color:var(--btn-primary-bg)}.settings-actions{margin-top:12px;display:flex;align-items:center;gap:10px}.settings-actions button{background:var(--btn-primary-bg);color:var(--btn-primary-text);border:none;padding:8px 16px;border-radius:0;font-size:13px;font-weight:500;cursor:pointer;transition:all .2s}.settings-actions button:hover{background:var(--btn-primary-hover)}.settings-actions button:disabled{background:var(--text-secondary);cursor:default}.settings-status{font-size:12px;color:var(--text-secondary)}.integrations-panel{background:var(--bg-surface);border:1px solid var(--border-color);border-radius:0;padding:24px;margin-bottom:24px;box-shadow:0 1px 3px #0000000d}.integrations-panel h2{font-size:13px;font-weight:600;color:var(--text-primary);margin-bottom:12px}.btn-integration{display:inline-flex;flex-direction:column;align-items:center;justify-content:center;width:120px;height:120px;background:var(--bg-surface);border:1px solid var(--border-color);border-radius:0;color:var(--text-primary);font-size:12px;cursor:pointer;font-family:inherit;gap:6px}.btn-integration:hover{background:#37373d;border-color:var(--text-secondary)}.integration-dot{width:8px;height:8px;border-radius:50%;background:#666}.integration-dot.running{background:#2ea043}.mcp-panel{background:var(--bg-surface);border:1px solid var(--border-color);border-radius:0;padding:24px;margin-bottom:24px;box-shadow:0 1px 3px #0000000d}.mcp-panel h2{font-size:13px;font-weight:600;color:var(--text-primary);margin-bottom:12px}.mcp-server-list{display:flex;flex-direction:column;gap:6px}.mcp-server-row{display:flex;align-items:center;gap:10px;padding:6px 10px;background:var(--bg-surface);border:1px solid var(--border-color);border-radius:0;font-size:12px}.mcp-dot{width:8px;height:8px;border-radius:50%;background:#666;flex-shrink:0}.mcp-dot.connected{background:#2ea043}.mcp-dot.failed{background:#d73a49}.mcp-dot.needs-auth{background:#d29922}.mcp-server-name{color:var(--text-primary);font-weight:600;min-width:100px}.mcp-server-url{color:var(--text-secondary);font-family:SF Mono,Fira Code,monospace;font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}.mcp-server-badge{font-size:10px;padding:1px 6px;border-radius:0;background:#333;color:var(--text-secondary);border:1px solid var(--border-color);flex-shrink:0}.mcp-server-status{font-size:11px;flex-shrink:0}.mcp-server-status.connected{color:#2ea043}.mcp-server-status.failed{color:#d73a49}button.mcp-server-status.needs-auth{color:#d29922;cursor:pointer;background:none;border:1px solid #d29922;border-radius:0;padding:1px 6px;font-size:11px}button.mcp-server-status.needs-auth:hover{background:#d2992226}.mcp-loading,.mcp-error{font-size:12px;color:var(--text-secondary);padding:4px 0}.mcp-error{color:#d73a49}.mcp-delete-wrap{position:relative;flex-shrink:0}.mcp-delete-btn{background:none;border:none;color:var(--text-secondary);font-size:14px;cursor:pointer;padding:0 4px;line-height:1}.mcp-delete-btn:hover{color:#d73a49}.mcp-delete-btn.disabled{cursor:not-allowed;opacity:.4}.mcp-delete-btn.disabled:hover{color:var(--text-secondary)}.mcp-delete-tooltip{display:none;position:absolute;bottom:calc(100% + 6px);right:0;background:var(--bg-surface);border:1px solid var(--border-color);color:var(--text-secondary);font-size:11px;padding:4px 8px;white-space:nowrap;z-index:10;pointer-events:none}.mcp-delete-wrap:hover .mcp-delete-tooltip{display:block}.mcp-add-btn{background:var(--btn-primary-bg);color:var(--btn-primary-text);border:none;padding:6px 14px;font-size:12px;font-weight:500;cursor:pointer;white-space:nowrap;flex-shrink:0}.mcp-add-btn:hover{opacity:.85}.mcp-add-scope-row{display:flex;align-items:center;gap:6px;margin-bottom:16px}.mcp-add-scope-btn{background:var(--bg-surface);border:1px solid var(--border-color);color:var(--text-secondary);padding:3px 10px;font-size:11px;cursor:pointer}.mcp-add-scope-btn.active{background:var(--btn-primary-bg);color:var(--btn-primary-text);border-color:transparent}.mcp-add-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px}.mcp-add-card{background:var(--bg-surface);border:1px solid var(--border-color);padding:14px;display:flex;flex-direction:column;gap:6px}.mcp-add-card.installed{opacity:.55}.mcp-add-card-header{display:flex;align-items:center;justify-content:space-between}.mcp-add-card-name{font-size:13px;font-weight:600;color:var(--text-primary);text-transform:capitalize}.mcp-add-card-badge{font-size:10px;padding:1px 6px;background:#2ea043;color:#fff;font-weight:500}.mcp-add-card-badge.needs-auth{background:#d29922}.mcp-add-card-desc{font-size:11px;color:var(--text-secondary);line-height:1.4;margin:0}.mcp-add-card-footer{display:flex;align-items:center;justify-content:space-between;margin-top:auto;gap:8px}.mcp-add-card-url{font-size:10px;color:var(--text-secondary);font-family:SF Mono,Fira Code,monospace;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}.mcp-add-card-btn{background:var(--btn-primary-bg);color:var(--btn-primary-text);border:none;padding:3px 12px;font-size:11px;font-weight:500;cursor:pointer;flex-shrink:0}.mcp-add-card-btn:hover{opacity:.85}.mcp-add-card-btn.auth{background:none;border:1px solid #d29922;color:#d29922}.mcp-add-card-btn.auth:hover{background:#d2992226;opacity:1}.mcp-add-card-btn:disabled{opacity:.5;cursor:not-allowed}.mcp-add-card-btn.delete{background:none;border:1px solid #d73a49;color:#d73a49}.mcp-add-card-btn.delete:hover{background:#d73a4926;opacity:1}.mcp-add-card-btn.delete.disabled{opacity:.4;cursor:not-allowed}.mcp-add-card-btn.delete.disabled:hover{background:none}.toast{position:fixed;bottom:20px;right:20px;background:#6e3630;border:1px solid #d73a49;color:#f0f0f0;font-size:12px;padding:10px 14px;display:flex;align-items:center;gap:10px;z-index:200;max-width:400px}.toast-close{background:none;border:none;color:#f0f0f0;font-size:14px;cursor:pointer;padding:0 2px;line-height:1;opacity:.7}.toast-close:hover{opacity:1}.modal-overlay{display:none;position:fixed;inset:0;background:#0009;z-index:100;justify-content:center;align-items:center}.modal-overlay.visible{display:flex}.modal{background:var(--bg-surface);border:1px solid var(--border-color);border-radius:0;width:560px;max-height:80vh;overflow-y:auto;padding:20px 24px}.modal h2{font-size:15px;font-weight:600;color:var(--text-primary);margin-bottom:16px}.modal-close{float:right;background:none;border:none;color:var(--text-secondary);font-size:18px;cursor:pointer;padding:0 4px;line-height:1}.modal-close:hover{color:var(--text-primary)}.setup-step{margin-bottom:16px}.setup-step-number{display:inline-block;width:20px;height:20px;background:var(--btn-primary-bg);color:var(--btn-primary-text);font-size:11px;font-weight:600;text-align:center;line-height:20px;border-radius:50%;margin-right:8px;flex-shrink:0}.setup-step-title{font-size:13px;font-weight:600;color:var(--text-primary);margin-bottom:4px}.setup-step-desc{font-size:12px;color:var(--text-secondary);line-height:1.5;margin-left:28px}.setup-step-desc code{background:var(--bg-surface);padding:1px 5px;border-radius:0;font-family:SF Mono,Fira Code,monospace;font-size:11px;color:var(--accent-color)}.setup-step-desc a{color:var(--accent-color);text-decoration:none}.setup-step-desc a:hover{text-decoration:underline}.setup-paste-row{display:flex;gap:8px;margin:8px 0 0 28px;align-items:center}.setup-paste-row input{flex:1;background:var(--bg-surface);border:1px solid var(--border-color);color:var(--text-primary);padding:5px 8px;font-size:12px;font-family:SF Mono,Fira Code,monospace;border-radius:0;outline:none}.setup-paste-row input:focus{border-color:var(--btn-primary-bg)}.setup-paste-row button{background:var(--btn-primary-bg);color:var(--btn-primary-text);border:none;padding:5px 10px;border-radius:0;font-size:11px;cursor:pointer;white-space:nowrap}.setup-paste-row button:hover{background:var(--btn-primary-hover)}.setup-paste-row button.applied{background:#2ea043}.setup-divider{border:none;border-top:1px solid var(--border-color);margin:16px 0}.conversation-messages{flex:1;overflow-y:auto;padding:16px 20px}.msg{margin-bottom:24px;max-width:100%}.msg-header{display:flex;align-items:center;gap:8px;margin-bottom:6px}.msg-role{font-size:13px;font-weight:600;color:var(--text-primary)}.msg-time{font-size:11px;color:var(--text-secondary)}.msg-content{background:var(--bg-surface);border:1px solid var(--border-color);border-radius:0;padding:12px 16px;font-size:13px;line-height:1.6;color:var(--text-primary);white-space:pre-wrap}.msg-user .msg-content{background:var(--bg-hover)}.conversation-empty{color:var(--text-secondary);font-size:13px;padding:20px}.qr-container{display:flex;justify-content:center;margin:16px 0}.qr-container img,.qr-container svg,.qr-container table{border-radius:0}.call-url{font-size:12px;color:var(--text-secondary);word-break:break-all;margin:8px 0 4px}.call-url a{color:#3794ff;text-decoration:none}.call-url a:hover{text-decoration:underline}.pairing-code{font-size:36px;font-weight:700;letter-spacing:12px;color:var(--text-primary);font-family:SF Mono,Fira Code,monospace;margin:20px 0 8px;text-align:center}.pairing-countdown{font-size:12px;color:var(--text-secondary);text-align:center;margin-bottom:16px}.btn-regenerate{background:#333;border:1px solid var(--border-color);color:var(--text-primary);padding:6px 16px;font-size:12px;cursor:pointer;border-radius:0;font-family:inherit}.btn-regenerate:hover{background:var(--border-color)}.call-container{width:100%;max-width:400px;padding:32px 24px;text-align:center;margin:0 auto;min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center}.call-container h1{font-size:20px;font-weight:600;color:var(--text-primary);margin-bottom:8px}.subtitle{font-size:13px;color:var(--text-secondary);margin-bottom:32px}.call-state{text-align:center}.pin-inputs{display:flex;gap:8px;justify-content:center;margin-bottom:20px}.pin-inputs input{width:44px;height:56px;text-align:center;font-size:24px;font-weight:600;font-family:inherit;background:var(--bg-surface);border:2px solid var(--border-color);border-radius:0;color:var(--text-primary);outline:none}.pin-inputs input:focus{border-color:var(--btn-primary-bg)}.btn{display:inline-block;padding:12px 32px;border:none;border-radius:0;font-size:14px;font-weight:600;font-family:inherit;cursor:pointer;transition:opacity .15s}.btn:hover{opacity:.85}.btn:disabled{opacity:.4;cursor:not-allowed}.btn-pair{background:var(--btn-primary-bg);color:#fff}.btn-call{background:#2ea043;color:#fff;font-size:16px;padding:14px 48px}.btn-hangup{background:#d73a49;color:#fff;font-size:16px;padding:14px 48px}.error-msg{color:#f85149;font-size:13px;margin-top:12px;min-height:18px}.status-msg{color:var(--text-secondary);font-size:13px;margin-top:12px}.pulse-ring{display:inline-block;width:80px;height:80px;border-radius:50%;background:#2ea0434d;margin-bottom:24px;position:relative}.pulse-ring:before{content:"";position:absolute;inset:20px;border-radius:50%;background:#2ea043}.pulse-ring:after{content:"";position:absolute;inset:0;border-radius:50%;border:2px solid #2ea043;animation:pulse 2s ease-out infinite}@keyframes pulse{0%{transform:scale(1);opacity:.6}to{transform:scale(1.6);opacity:0}}.call-label{font-size:15px;color:#2ea043;margin-bottom:24px}
|