agentgui 1.0.370 → 1.0.372

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.md CHANGED
@@ -31,7 +31,7 @@ static/js/websocket-manager.js WebSocket connection handling
31
31
  static/js/ui-components.js UI component helpers
32
32
  static/js/syntax-highlighter.js Code syntax highlighting
33
33
  static/js/voice.js Voice input/output
34
- static/js/features.js Feature flags
34
+ static/js/features.js View toggle, drag-drop upload, model progress indicator
35
35
  static/templates/ 31 HTML template fragments for event rendering
36
36
  ```
37
37
 
@@ -89,282 +89,26 @@ Server broadcasts:
89
89
  - `streaming_complete` - Execution finished
90
90
  - `streaming_error` - Execution failed
91
91
  - `conversation_created`, `conversation_updated`, `conversation_deleted`
92
- - `tts_setup_progress` - Windows pocket-tts setup progress (step, status, message)
92
+ - `model_download_progress` - Voice model download progress
93
+ - `voice_list` - Available TTS voices
93
94
 
94
- ## Pocket-TTS Windows Setup (Reliability for Slow/Bad Internet)
95
+ ## Voice Model Download
95
96
 
96
- On Windows, text-to-speech uses pocket-tts which requires Python and pip install. The setup process is now resilient to slow/unreliable connections:
97
+ Speech models (~470MB total) are downloaded automatically on server startup. No credentials required.
97
98
 
98
- ### Features
99
- - **Extended timeouts**: 120s for pip install (accommodates slow connections)
100
- - **Retry logic**: 3 attempts with exponential backoff (1s, 2s delays)
101
- - **Progress reporting**: Real-time updates via WebSocket to UI
102
- - **Partial install cleanup**: Failed venvs are removed to allow retry
103
- - **Installation verification**: Binary validation via `--version` check
104
- - **Concurrent waiting**: Multiple simultaneous requests wait for single setup (600s timeout)
99
+ ### Download Sources (fallback chain)
100
+ 1. **GitHub LFS** (primary): `https://github.com/AnEntrypoint/models` - LFS-tracked ONNX files via `media.githubusercontent.com`, small files via `raw.githubusercontent.com`
101
+ 2. **HuggingFace** (fallback): `onnx-community/whisper-base` for STT, `AnEntrypoint/sttttsmodels` for TTS
105
102
 
106
- ### Configuration (lib/windows-pocket-tts-setup.js)
107
- ```javascript
108
- const CONFIG = {
109
- PIP_TIMEOUT: 120000, // 2 minutes
110
- VENV_CREATION_TIMEOUT: 30000, // 30 seconds
111
- MAX_RETRIES: 3, // 3 attempts
112
- RETRY_DELAY_MS: 1000, // 1 second initial
113
- RETRY_BACKOFF_MULTIPLIER: 2, // 2x exponential
114
- };
115
- ```
116
-
117
- ### Network Requirements
118
- - **Minimum**: 50 kbps sustained, < 5s latency, < 10% packet loss
119
- - **Recommended**: 256+ kbps, < 2s latency, < 1% packet loss
120
- - **Expected time on slow connection**: 2-6 minutes with retries
121
-
122
- ### Progress Messages
123
- During TTS setup on first use, WebSocket broadcasts:
124
- ```json
125
- {
126
- "type": "tts_setup_progress",
127
- "step": "detecting-python|creating-venv|installing|verifying",
128
- "status": "in-progress|success|error",
129
- "message": "descriptive status message with retry count if applicable"
130
- }
131
- ```
132
-
133
- ### Recovery Behavior
134
- 1. Network timeout → auto-retry with backoff
135
- 2. Partial venv → auto-cleanup before retry
136
- 3. Failed verification → auto-cleanup and error
137
- 4. Concurrent requests → first starts setup, others wait up to 600s
138
- 5. Interrupted setup → cleanup allows fresh retry
139
-
140
- ### Testing
141
- Setup validates by running pocket-tts binary with `--version` flag to confirm functional installation, not just file existence.
142
-
143
- ## Model Download Fallback Chain Architecture (Task 1C)
144
-
145
- Three-layer resilient fallback for speech models (280MB whisper-base + 197MB TTS). Designed to eliminate single points of failure while maintaining backward compatibility.
146
-
147
- ### Layer 1: IPFS Gateway (Primary)
148
-
149
- Decentralized distribution across three gateways with automatic failover:
150
-
151
- ```
152
- Cloudflare IPFS https://cloudflare-ipfs.com/ipfs/ Priority 1 (99.9% reliable)
153
- dweb.link https://dweb.link/ipfs/ Priority 2 (99% reliable)
154
- Pinata https://gateway.pinata.cloud/ipfs/ Priority 3 (99.5% reliable)
155
- ```
156
-
157
- **Model Distribution**:
158
- - Whisper Base (280MB): `TBD_WHISPER_HASH` → encoder (78.6MB) + decoder (198.9MB) + configs
159
- - TTS Models (197MB): `TBD_TTS_HASH` → mimi_encoder (73MB) + decoders + text_conditioner + flow_lm
160
-
161
- **Characteristics**: 30s timeout per gateway, 2 retries before fallback, SHA-256 per-file verification against IPFS-stored manifest
162
-
163
- ### Layer 2: HuggingFace (Secondary)
164
-
165
- Current working implementation via webtalk package. Proven reliable with region-dependent latency.
166
-
167
- ```
168
- Whisper https://huggingface.co/onnx-community/whisper-base/resolve/main/
169
- TTS https://huggingface.co/datasets/AnEntrypoint/sttttsmodels/resolve/main/tts/
170
- ```
171
-
172
- **Characteristics**: 3 retries with exponential backoff (2^attempt seconds), 30s timeout, file size validation (minBytes thresholds: encoder ≥40MB, decoder ≥100MB, TTS files ≥18-61MB range)
173
-
174
- **Implementation Location**: webtalk/whisper-models.js, webtalk/tts-models.js (unchanged, wrapped by fallback logic)
175
-
176
- ### Layer 3: Local Cache + Fallbacks
177
-
178
- **Primary Cache**: `~/.gmgui/models/` with manifest at `~/.gmgui/models/.manifests.json`
179
-
180
- **Verification Algorithms**:
181
- 1. Size check (minBytes threshold) → corrupted: delete & retry
182
- 2. SHA-256 hash against manifest → mismatch: delete & re-download
183
- 3. ONNX format validation (header check) → invalid: delete & escalate to primary
184
-
185
- **Bundled Models** (future): `agentgui/bundled-models.tar.gz` (~50-80MB) for offline-first deployments
186
-
187
- **Peer-to-Peer** (future): mDNS discovery for LAN sharing across multiple AgentGUI instances
188
-
189
- ### Download Decision Logic
190
-
191
- ```
192
- 1. Check local cache validity → RETURN if valid, record cache_hit metric
193
- 2. TRY PRIMARY (IPFS): attempt 3 gateways sequentially, 2 retries each
194
- - VERIFY size + sha256 → ON SUCCESS: record primary_success, return
195
- 3. TRY SECONDARY (HuggingFace): 3 attempts with exponential backoff
196
- - VERIFY file size → ON SUCCESS: record secondary_success, return
197
- 4. TRY TERTIARY (Bundled): extract tarball if present
198
- - VERIFY extraction → ON SUCCESS: record tertiary_bundled_success, return
199
- 5. TRY TERTIARY (Peer): query mDNS if enabled, fetch from peer
200
- - VERIFY checksum → ON SUCCESS: record tertiary_peer_success, return
201
- 6. FAILURE: record all_layers_exhausted metric, throw error (optional: activate degraded mode)
202
- ```
203
-
204
- ### Metrics Collection
205
-
206
- **Storage**: `~/.gmgui/models/.metrics.json` (append-only, rotated daily)
207
-
208
- **Per-Download Fields**: timestamp, modelType, layer, gateway, status, latency_ms, bytes_downloaded/total, error_type/message
209
-
210
- **Aggregations**: per-layer success rate, per-gateway success rate, avg latency per layer, cache effectiveness
211
-
212
- **Dashboard Endpoints**:
213
- - `GET /api/metrics/downloads` - all metrics
214
- - `GET /api/metrics/downloads/summary` - aggregated stats
215
- - `GET /api/metrics/downloads/health` - per-layer health
216
- - `POST /api/metrics/downloads/reset` - clear history
217
-
218
- ### Cache Invalidation Strategy
219
-
220
- **Version Manifest** (`~/.gmgui/models/.manifests.json`):
221
- ```json
222
- {
223
- "whisper-base": {
224
- "currentVersion": "1.0.0",
225
- "ipfsHash": "QmXXXX...",
226
- "huggingfaceTag": "revision-hash",
227
- "downloadedAt": "ISO8601",
228
- "sha256": { "file": "hash...", ... }
229
- },
230
- "tts-models": { ... }
231
- }
232
- ```
233
-
234
- **Version Mismatch Detection** (on startup + periodic background check):
235
- - Query HuggingFace API HEAD for latest revision
236
- - Query IPFS gateway for latest dag-json manifest
237
- - If new version: log warning, set flag in `/api/status`, prompt user (not auto-download)
238
- - If corrupted: quarantine to `.bak`, mark invalid, trigger auto-download from primary on next request
239
-
240
- **Stale Cache Handling**:
241
- - Max age: 90 days → background check queries IPFS for new hash
242
- - Stale window: 7 days after max age → serve stale if live fetch fails
243
- - Offline degradation: serve even if 365 days old when network down
244
-
245
- **Cleanup Policy**:
246
- - Backup retention: 1 previous version (`.bak`) for 7 days
247
- - Failed downloads: delete `*.tmp` after 1 hour idle
248
- - Old versions: delete if > 90 days old
249
- - Disk threshold: warn if `~/.gmgui/models` exceeds 2GB
250
-
251
- ### Design Rationale
252
-
253
- **Why Three Layers?** IPFS (decentralized, no SPoF) + HuggingFace (proven, existing) + Local (offline-ready, LAN-resilient)
254
-
255
- **Why Metrics First?** Enables data-driven gateway selection, identifies reliability in production, guides timeout/retry tuning
256
-
257
- **Why No Auto-Upgrade?** User controls timing, allows staged rollout, supports version pinning, reduces surprise breakage
258
-
259
- **Why Bundled Models?** Enables air-gapped deployments, reduces network load, supports edge environments with poor connectivity
260
-
261
- ### Implementation Status
262
-
263
- | Phase | Description | Status | File(s) |
264
- |-------|-------------|--------|---------|
265
- | 1 | IPFS gateway discovery | ✅ DONE | `webtalk/ipfs-downloader.js` |
266
- | 2 | 3-layer fallback chain | ✅ DONE | `lib/model-downloader.js` |
267
- | 3 | Metrics collection | ✅ DONE | `lib/model-downloader.js` (JSON storage) |
268
- | 4 | Manifest generation with SHA-256 | ✅ DONE | Generated to `~/.gmgui/models/.manifests.json` |
269
- | 5 | Metrics API endpoints | ✅ DONE | `server.js` (4 endpoints added) |
270
- | 6 | IPFS publishing script | ✅ DONE | `scripts/publish-models-to-ipfs.js` |
271
- | 7 | Database IPFS tables | ✅ EXISTS | `database.js` (ipfs_cids, ipfs_downloads) |
272
- | 8 | Integration into ensureModels | ⏳ TODO | Need to wire into `server.js` |
273
- | 9 | Publish to IPFS (get real CIDs) | ⏳ TODO | Requires Pinata API keys |
274
- | 10 | Update database.js with real CIDs | ⏳ TODO | After publishing |
275
- | 11 | Stale-while-revalidate checks | 📋 FUTURE | Background job |
276
- | 12 | Bundled models | 📋 FUTURE | Tarball creation |
277
- | 13 | Peer-to-peer discovery | 📋 FUTURE | mDNS implementation |
278
-
279
- ### Current Model Inventory
280
-
281
- **Models Downloaded Locally**: `~/.gmgui/models/`
282
-
283
- **Whisper Base** (280.15 MB) - 7 files:
284
- - `config.json` (0.00 MB) - SHA256: `f4d0608f7d918166...`
285
- - `tokenizer.json` (2.37 MB) - SHA256: `27fc476bfe7f1729...`
286
- - `tokenizer_config.json` (0.27 MB) - SHA256: `2e036e4dbacfdeb7...`
287
- - `onnx/encoder_model.onnx` (78.65 MB) - SHA256: `a9f3b752833b49e8...`
288
- - `onnx/decoder_model_merged.onnx` (198.86 MB) - SHA256: `514903744bb1b458...`
289
-
290
- **TTS Models** (189.40 MB) - 6 files:
291
- - `mimi_encoder.onnx` (69.78 MB) - SHA256: `360f050cd0b1e1c9...`
292
- - `flow_lm_main_int8.onnx` (72.81 MB) - SHA256: `fd5cdd7f7ab05f63...`
293
- - `mimi_decoder_int8.onnx` (21.63 MB) - SHA256: `501e16f51cf3fb91...`
294
- - `text_conditioner.onnx` (15.63 MB) - SHA256: `80ea69f46d8153a9...`
295
- - `flow_lm_flow_int8.onnx` (9.50 MB) - SHA256: `8d627d235c44a597...`
296
- - `tokenizer.model` (0.06 MB) - SHA256: `d461765ae1795666...`
297
-
298
- **Manifest Location**: `~/.gmgui/models/.manifests.json` (auto-generated with full SHA-256 hashes)
299
-
300
- ### Next Steps to Complete Task 1C
301
-
302
- #### 1. Publish Models to IPFS (Get Real CIDs)
303
-
304
- ```bash
305
- # Get free Pinata API keys at https://www.pinata.cloud/
306
- export PINATA_API_KEY=your_api_key
307
- export PINATA_SECRET_KEY=your_secret_key
308
-
309
- # Run publishing script
310
- node scripts/publish-models-to-ipfs.js
311
- ```
312
-
313
- This will output real IPFS CIDs for both model sets.
314
-
315
- #### 2. Update database.js with Real CIDs
316
-
317
- Replace placeholder CIDs in `database.js` (lines 389-390):
318
- ```javascript
319
- const WHISPER_CID = 'bafybeidyw252ecy4vs46bbmezrtw325gl2ymdltosmzqgx4edjsc3fbofy'; // PLACEHOLDER
320
- const TTS_CID = 'bafybeidyw252ecy4vs46bbmezrtw325gl2ymdltosmzqgx4edjsc3fbofy'; // PLACEHOLDER
321
- ```
322
-
323
- Update with real CIDs from step 1.
324
-
325
- #### 3. Integrate Fallback Chain
326
-
327
- Modify `server.js` `ensureModelsDownloaded()` (starting line 66) to use the new 3-layer fallback:
328
-
329
- ```javascript
330
- import { downloadWithFallback } from './lib/model-downloader.js';
331
- import { queries } from './database.js';
332
-
333
- // Get IPFS CIDs from database
334
- const whisperCidRecord = queries.getIpfsCidByModel('whisper-base', 'stt');
335
- const ttsCidRecord = queries.getIpfsCidByModel('tts', 'voice');
336
-
337
- // Load manifest
338
- const manifestPath = path.join(modelsDir, '.manifests.json');
339
- const manifests = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
340
-
341
- // For each required file, use fallback chain:
342
- for (const [filename, fileInfo] of Object.entries(manifests['whisper-base'].files)) {
343
- const destPath = path.join(whisperDir, filename);
344
- await downloadWithFallback({
345
- ipfsCid: `${whisperCidRecord.cid}/${filename}`,
346
- huggingfaceUrl: `https://huggingface.co/onnx-community/whisper-base/resolve/main/${filename}`,
347
- destPath,
348
- manifest: fileInfo,
349
- minBytes: fileInfo.size * 0.8,
350
- preferredLayer: 'ipfs'
351
- }, (progress) => {
352
- broadcastModelProgress({ ...progress, file: filename, type: 'whisper' });
353
- });
354
- }
355
- ```
356
-
357
- ### Metrics API Endpoints (Live)
358
-
359
- - `GET /gm/api/metrics/downloads` - All download metrics (last 24 hours)
360
- - `GET /gm/api/metrics/downloads/summary` - Aggregated statistics
361
- - `GET /gm/api/metrics/downloads/health` - Per-layer health status (success rates, latency)
362
- - `POST /gm/api/metrics/downloads/reset` - Clear metrics history
103
+ ### Models
104
+ - **Whisper Base** (~280MB): encoder + decoder ONNX models, tokenizer, config files
105
+ - **TTS Models** (~190MB): mimi encoder/decoder, flow_lm, text_conditioner, tokenizer
363
106
 
364
- ### Architecture Files Created
107
+ ### UI Behavior
108
+ - Voice tab is hidden until models are ready
109
+ - A circular progress indicator appears in the header during download
110
+ - Once models are downloaded, the Voice tab becomes visible
111
+ - Model status is broadcast via WebSocket `model_download_progress` events
365
112
 
366
- - `lib/model-downloader.js` - 3-layer fallback implementation with metrics
367
- - `lib/ipfs-publish.js` - Local IPFS publishing (requires kubo)
368
- - `scripts/publish-models-to-ipfs.js` - Pinata-based publishing (no local IPFS needed)
369
- - `~/.gmgui/models/.manifests.json` - Auto-generated with SHA-256 hashes
370
- - `~/.gmgui/models/.metrics.json` - Download metrics (auto-rotated daily)
113
+ ### Cache Location
114
+ Models are stored at `~/.gmgui/models/` (whisper in `onnx-community/whisper-base/`, TTS in `tts/`).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.370",
3
+ "version": "1.0.372",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",
@@ -30,7 +30,7 @@
30
30
  "fsbrowse": "^0.2.18",
31
31
  "google-auth-library": "^10.5.0",
32
32
  "onnxruntime-node": "1.21.0",
33
- "webtalk": "file:../webtalk",
33
+ "webtalk": "^1.0.31",
34
34
  "ws": "^8.14.2"
35
35
  },
36
36
  "overrides": {
package/server.js CHANGED
@@ -2652,64 +2652,6 @@ const server = http.createServer(async (req, res) => {
2652
2652
  return;
2653
2653
  }
2654
2654
 
2655
- if (pathOnly === '/api/metrics/downloads' && req.method === 'GET') {
2656
- try {
2657
- const { getMetrics } = await import('./lib/model-downloader.js');
2658
- const metrics = getMetrics();
2659
- sendJSON(req, res, 200, { metrics });
2660
- } catch (err) {
2661
- sendJSON(req, res, 500, { error: err.message });
2662
- }
2663
- return;
2664
- }
2665
-
2666
- if (pathOnly === '/api/metrics/downloads/summary' && req.method === 'GET') {
2667
- try {
2668
- const { getMetricsSummary } = await import('./lib/model-downloader.js');
2669
- const summary = getMetricsSummary();
2670
- sendJSON(req, res, 200, summary);
2671
- } catch (err) {
2672
- sendJSON(req, res, 500, { error: err.message });
2673
- }
2674
- return;
2675
- }
2676
-
2677
- if (pathOnly === '/api/metrics/downloads/health' && req.method === 'GET') {
2678
- try {
2679
- const { getMetricsSummary } = await import('./lib/model-downloader.js');
2680
- const summary = getMetricsSummary();
2681
- const health = {
2682
- huggingface: {
2683
- status: summary.huggingface.success > 0 ? 'healthy' : summary.huggingface.error > 0 ? 'degraded' : 'unknown',
2684
- success_rate: summary.huggingface.success + summary.huggingface.error > 0
2685
- ? ((summary.huggingface.success / (summary.huggingface.success + summary.huggingface.error)) * 100).toFixed(2)
2686
- : 0,
2687
- avg_latency_ms: summary.huggingface.avg_latency
2688
- },
2689
- cache: {
2690
- hit_rate: summary.total > 0
2691
- ? ((summary.cache_hits / summary.total) * 100).toFixed(2)
2692
- : 0
2693
- }
2694
- };
2695
- sendJSON(req, res, 200, health);
2696
- } catch (err) {
2697
- sendJSON(req, res, 500, { error: err.message });
2698
- }
2699
- return;
2700
- }
2701
-
2702
- if (pathOnly === '/api/metrics/downloads/reset' && req.method === 'POST') {
2703
- try {
2704
- const { resetMetrics } = await import('./lib/model-downloader.js');
2705
- resetMetrics();
2706
- sendJSON(req, res, 200, { ok: true, message: 'Metrics reset' });
2707
- } catch (err) {
2708
- sendJSON(req, res, 500, { error: err.message });
2709
- }
2710
- return;
2711
- }
2712
-
2713
2655
  if (pathOnly === '/api/speech-status' && req.method === 'POST') {
2714
2656
  const body = await parseBody(req);
2715
2657
  if (body.forceDownload) {
package/static/index.html CHANGED
@@ -2694,6 +2694,26 @@
2694
2694
  .connection-dot.reconnecting { background: #f59e0b; animation: pulse 1s ease-in-out infinite; }
2695
2695
  .connection-dot.downloading { background: #3b82f6; animation: pulse 1s ease-in-out infinite; }
2696
2696
 
2697
+ .model-dl-indicator {
2698
+ display: none;
2699
+ position: relative;
2700
+ width: 24px;
2701
+ height: 24px;
2702
+ cursor: pointer;
2703
+ }
2704
+ .model-dl-indicator.active { display: inline-flex; align-items: center; justify-content: center; }
2705
+ .model-dl-indicator svg { width: 24px; height: 24px; transform: rotate(-90deg); }
2706
+ .model-dl-indicator .track { fill: none; stroke: var(--color-bg-secondary); stroke-width: 3; }
2707
+ .model-dl-indicator .progress { fill: none; stroke: var(--color-primary); stroke-width: 3; stroke-linecap: round; transition: stroke-dashoffset 0.3s ease; }
2708
+ .model-dl-indicator .icon { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); }
2709
+ .model-dl-indicator .icon svg { width: 12px; height: 12px; transform: none; }
2710
+ .model-dl-indicator-tooltip {
2711
+ display: none; position: absolute; top: calc(100% + 6px); right: 0; background: var(--color-bg-primary);
2712
+ border: 1px solid var(--color-border); border-radius: 6px; padding: 8px 12px; font-size: 0.75rem;
2713
+ white-space: nowrap; z-index: 1000; box-shadow: 0 2px 8px rgba(0,0,0,0.15);
2714
+ }
2715
+ .model-dl-indicator:hover .model-dl-indicator-tooltip { display: block; }
2716
+
2697
2717
  .connection-tooltip {
2698
2718
  position: absolute;
2699
2719
  top: 100%;
@@ -2972,6 +2992,11 @@
2972
2992
  <svg viewBox="0 0 24 24" fill="currentColor" stroke="none"><rect x="5" y="5" width="14" height="14" rx="1"></rect></svg>
2973
2993
  </button>
2974
2994
  </div>
2995
+ <div class="model-dl-indicator" id="modelDlIndicator" title="Downloading voice models...">
2996
+ <svg viewBox="0 0 24 24"><circle class="track" cx="12" cy="12" r="10"/><circle class="progress" cx="12" cy="12" r="10" stroke-dasharray="62.83" stroke-dashoffset="62.83"/></svg>
2997
+ <div class="icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/></svg></div>
2998
+ <div class="model-dl-indicator-tooltip" id="modelDlTooltip">Preparing voice models...</div>
2999
+ </div>
2975
3000
  <button class="header-icon-btn agent-auth-btn" id="agentAuthBtn" title="Agent authentication" aria-label="Agent authentication" style="display:none;">
2976
3001
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path></svg>
2977
3002
  <div class="agent-auth-dropdown" id="agentAuthDropdown"></div>
@@ -3000,7 +3025,7 @@
3000
3025
  <div class="view-toggle-bar" id="viewToggleBar">
3001
3026
  <button class="view-toggle-btn active" data-view="chat">Chat</button>
3002
3027
  <button class="view-toggle-btn" data-view="files">Files</button>
3003
- <button class="view-toggle-btn" data-view="voice">Voice</button>
3028
+ <button class="view-toggle-btn" data-view="voice" id="voiceTabBtn" style="display:none;">Voice</button>
3004
3029
  <button class="view-toggle-btn" data-view="terminal" id="terminalTabBtn">Terminal</button>
3005
3030
  </div>
3006
3031
 
@@ -3129,7 +3154,6 @@
3129
3154
  <script defer src="/gm/js/syntax-highlighter.js"></script>
3130
3155
  <script defer src="/gm/js/dialogs.js"></script>
3131
3156
  <script defer src="/gm/js/ui-components.js"></script>
3132
- <script defer src="/gm/js/progress-dialog.js"></script>
3133
3157
  <script defer src="/gm/js/conversations.js"></script>
3134
3158
  <script defer src="/gm/js/client.js"></script>
3135
3159
  <script type="module" src="/gm/js/voice.js"></script>
@@ -1890,19 +1890,14 @@ class AgentGUIClient {
1890
1890
  } else if (status.modelsDownloading) {
1891
1891
  this._modelDownloadProgress = status.modelsProgress || { downloading: true };
1892
1892
  this._modelDownloadInProgress = true;
1893
- setTimeout(function() {
1894
- if (window.__showVoiceDownloadProgress) window.__showVoiceDownloadProgress();
1895
- }, 0);
1896
1893
  } else {
1897
1894
  this._modelDownloadProgress = { done: false };
1898
1895
  this._modelDownloadInProgress = false;
1899
1896
  }
1900
- this._updateVoiceTabState();
1901
1897
  } catch (error) {
1902
1898
  console.error('Failed to check speech status:', error);
1903
1899
  this._modelDownloadProgress = { done: false };
1904
1900
  this._modelDownloadInProgress = false;
1905
- this._updateVoiceTabState();
1906
1901
  }
1907
1902
  }
1908
1903
 
@@ -2044,75 +2039,21 @@ class AgentGUIClient {
2044
2039
 
2045
2040
  _handleModelDownloadProgress(progress) {
2046
2041
  this._modelDownloadProgress = progress;
2047
-
2048
2042
  if (progress.status === 'failed' || progress.error) {
2049
2043
  this._modelDownloadInProgress = false;
2050
2044
  console.error('[Models] Download error:', progress.error || progress.status);
2051
2045
  this._updateConnectionIndicator(this.wsManager?.latency?.quality || 'unknown');
2052
- if (window._voiceProgressDialog) {
2053
- window._voiceProgressDialog.close();
2054
- window._voiceProgressDialog = null;
2055
- }
2056
- const errorMsg = 'Failed to download voice models: ' + (progress.error || 'unknown error');
2057
- if (window.UIDialog) {
2058
- window.UIDialog.alert(errorMsg, 'Download Error');
2059
- } else {
2060
- alert(errorMsg);
2061
- }
2062
2046
  return;
2063
2047
  }
2064
-
2065
2048
  if (progress.done || progress.status === 'completed') {
2066
2049
  this._modelDownloadInProgress = false;
2067
2050
  console.log('[Models] Download complete');
2068
2051
  this._updateConnectionIndicator(this.wsManager?.latency?.quality || 'unknown');
2069
- this._updateVoiceTabState();
2070
- if (window._voiceProgressDialog) {
2071
- window._voiceProgressDialog.update(100, 'Voice models ready!');
2072
- setTimeout(function() {
2073
- if (window._voiceProgressDialog) {
2074
- window._voiceProgressDialog.close();
2075
- window._voiceProgressDialog = null;
2076
- }
2077
- }, 500);
2078
- }
2079
- if (window._voiceTabPendingOpen) {
2080
- window._voiceTabPendingOpen = false;
2081
- var voiceBtn = document.querySelector('[data-view="voice"]');
2082
- if (voiceBtn) voiceBtn.click();
2083
- }
2084
2052
  return;
2085
2053
  }
2086
-
2087
2054
  if (progress.started || progress.downloading || progress.status === 'downloading' || progress.status === 'connecting') {
2088
2055
  this._modelDownloadInProgress = true;
2089
2056
  this._updateConnectionIndicator(this.wsManager?.latency?.quality || 'unknown');
2090
-
2091
- if (!window._voiceProgressDialog && window.__showVoiceDownloadProgress) {
2092
- window.__showVoiceDownloadProgress();
2093
- }
2094
-
2095
- if (window._voiceProgressDialog) {
2096
- let displayText = 'Downloading models...';
2097
-
2098
- if (progress.status === 'connecting') {
2099
- displayText = 'Connecting to ' + (progress.currentGateway || 'gateway') + '...';
2100
- } else if (progress.totalBytes > 0) {
2101
- const downloaded = (progress.bytesDownloaded || 0) / 1024 / 1024;
2102
- const total = progress.totalBytes / 1024 / 1024;
2103
- const speed = progress.downloadSpeed ? (progress.downloadSpeed / 1024 / 1024).toFixed(2) : '0';
2104
- const eta = progress.eta ? Math.ceil(progress.eta) + 's' : '...';
2105
- const retryInfo = progress.retryCount > 0 ? ` (retry ${progress.retryCount})` : '';
2106
-
2107
- displayText = `Downloading ${downloaded.toFixed(1)}MB / ${total.toFixed(1)}MB @ ${speed}MB/s (ETA: ${eta})${retryInfo}`;
2108
- } else if (progress.file) {
2109
- displayText = 'Loading ' + progress.file + '...';
2110
- } else if (progress.completedFiles && progress.totalFiles) {
2111
- displayText = `Downloaded ${progress.completedFiles}/${progress.totalFiles} files`;
2112
- }
2113
-
2114
- window._voiceProgressDialog.update(progress.percentComplete || 0, displayText);
2115
- }
2116
2057
  }
2117
2058
  }
2118
2059
 
@@ -2626,24 +2567,10 @@ class AgentGUIClient {
2626
2567
  };
2627
2568
  }
2628
2569
 
2629
- _handleModelDownloadProgress(data) {
2630
- if (!data) return;
2631
- this._modelDownloadProgress = data;
2632
- if (data.done && data.complete) {
2633
- this._modelDownloadInProgress = false;
2634
- this._hideModelDownloadProgress();
2635
- } else if (data.downloading || data.done === false) {
2636
- this._modelDownloadInProgress = true;
2637
- this._showModelDownloadProgress(data);
2638
- }
2639
- this._updateVoiceTabState();
2640
- }
2641
-
2642
2570
  _handleSTTProgress(data) {
2643
2571
  if (!data) return;
2644
2572
  const transcriptEl = document.getElementById('voiceTranscript');
2645
2573
  if (!transcriptEl) return;
2646
-
2647
2574
  if (data.status === 'transcribing') {
2648
2575
  transcriptEl.textContent = 'Transcribing...';
2649
2576
  transcriptEl.style.color = 'var(--color-text-secondary)';
@@ -2656,61 +2583,6 @@ class AgentGUIClient {
2656
2583
  }
2657
2584
  }
2658
2585
 
2659
- _handleTTSSetupProgress(data) {
2660
- if (!data) return;
2661
- this._showModelDownloadProgress({
2662
- downloading: true,
2663
- message: data.message || 'Setting up TTS...'
2664
- });
2665
- }
2666
-
2667
- _showModelDownloadProgress(progress) {
2668
- let indicator = document.getElementById('modelDownloadIndicator');
2669
- if (!indicator) {
2670
- const header = document.querySelector('.main-header');
2671
- if (!header) return;
2672
-
2673
- indicator = document.createElement('div');
2674
- indicator.id = 'modelDownloadIndicator';
2675
- indicator.style = `
2676
- display: flex;
2677
- align-items: center;
2678
- gap: 0.5rem;
2679
- padding: 0.375rem 0.75rem;
2680
- background: var(--color-bg-secondary);
2681
- border-radius: 0.375rem;
2682
- font-size: 0.875rem;
2683
- color: var(--color-text-secondary);
2684
- `;
2685
-
2686
- const controls = header.querySelector('.header-controls');
2687
- if (controls) {
2688
- controls.insertBefore(indicator, controls.firstChild);
2689
- }
2690
- }
2691
-
2692
- indicator.style.display = 'flex';
2693
- indicator.innerHTML = `
2694
- <span style="display:inline-block;width:1rem;height:1rem;border:2px solid var(--color-border);border-top-color:var(--color-primary);border-radius:50%;animation:spin 1s linear infinite;"></span>
2695
- <span>${progress.message || 'Loading models...'}</span>
2696
- `;
2697
- }
2698
-
2699
- _hideModelDownloadProgress() {
2700
- const indicator = document.getElementById('modelDownloadIndicator');
2701
- if (indicator) {
2702
- indicator.style.display = 'none';
2703
- }
2704
- }
2705
-
2706
- _updateVoiceTabState() {
2707
- const voiceBtn = document.querySelector('[data-view="voice"]');
2708
- if (!voiceBtn) return;
2709
- const isReady = this._modelDownloadProgress?.done === true &&
2710
- this._modelDownloadProgress?.complete === true;
2711
- voiceBtn.title = isReady ? 'Voice' : 'Voice (click to download models)';
2712
- }
2713
-
2714
2586
  /**
2715
2587
  * Cleanup resources
2716
2588
  */