beast-agent 2.2.1 → 2.3.2

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.
@@ -59,6 +59,8 @@ const els = {
59
59
  bbClose: $('#bbClose'),
60
60
  bbResize: $('#bbResize'),
61
61
  bbPhone: $('#bbPhone'),
62
+ bbMobile: $('#bbMobile'),
63
+ bbDevice: $('#bbDevice'),
62
64
  termCBtn: $('#termCBtn'),
63
65
  termPanel: $('#termPanel'),
64
66
  termCwd: $('#termCwd'),
@@ -137,6 +139,7 @@ const els = {
137
139
  setClose: $('#setClose'),
138
140
  setVersion: $('#setVersion'),
139
141
  attachBtn: $('#attachBtn'),
142
+ ttsBtn: $('#ttsBtn'),
140
143
  micBtn: $('#micBtn'),
141
144
  fileInput: $('#fileInput'),
142
145
  fileTasks: $('#fileTasks'),
@@ -1891,38 +1894,329 @@ async function renderSkillsPane() {
1891
1894
 
1892
1895
  /* ---------------- sesli yanıt (TTS) ---------------- */
1893
1896
 
1897
+ /* ---------------- CHAT TTS: ajan yazılarını otomatik seslendirme ----------------
1898
+ Ayarlar → Sesli Yanıt'ta "chat'te otomatik seslendir" açıksa her tur sonunda
1899
+ ajanın son yazısı Edge TTS (veya seçili motor) ile okunur. */
1900
+ let ttsCfgCache = null;
1901
+ let ttsAudioEl = null;
1902
+ let ttsLastSpoken = '';
1903
+ let chatTtsOn = false; // hoparlör düğmesi: chat'te otomatik seslendirme
1904
+
1905
+ async function ttsCfg() {
1906
+ if (!ttsCfgCache) {
1907
+ try { ttsCfgCache = await beast.waGetTts(); } catch { ttsCfgCache = {}; }
1908
+ }
1909
+ return ttsCfgCache || {};
1910
+ }
1911
+
1912
+ /* açılışta TTS tercihini yükle + düğme durumunu senkronla */
1913
+ (async () => {
1914
+ const cfg = await ttsCfg();
1915
+ chatTtsOn = !!cfg.chatAutoSpeak;
1916
+ if (els.ttsBtn) els.ttsBtn.classList.toggle('on', chatTtsOn);
1917
+ })();
1918
+ if (els.ttsBtn) {
1919
+ els.ttsBtn.addEventListener('click', async () => {
1920
+ chatTtsOn = !chatTtsOn;
1921
+ els.ttsBtn.classList.toggle('on', chatTtsOn);
1922
+ /* KAPATMA = ANINDA SUS: çalan cümle durur + kuyruktaki tüm cümleler atılır */
1923
+ if (!chatTtsOn) ttsQueueReset();
1924
+ const cfg = await ttsCfg();
1925
+ ttsCfgCache = { ...cfg, chatAutoSpeak: chatTtsOn };
1926
+ await beast.waSetTts(ttsCfgCache).catch(() => {});
1927
+ if (!cfg.enabled) {
1928
+ toast(chatTtsOn ? 'TTS motoru kapalı — Ayarlar → Sesli Yanıttan etkinleştir' : 'Otomatik seslendirme kapalı');
1929
+ } else {
1930
+ toast(chatTtsOn ? 'Otomatik seslendirme AÇIK' : 'Otomatik seslendirme kapalı');
1931
+ }
1932
+ });
1933
+ }
1934
+
1935
+ async function speakText(text) {
1936
+ try {
1937
+ const cfg = await ttsCfg();
1938
+ /* chatTtsOn tek doğruluk kaynağı: hoparlör düğmesi + ayar paneli + boot yükleme
1939
+ üçü de senkron tutar. Yalnız chat modunda; terminal/BC/Studio asla okunmaz. */
1940
+ if (!cfg.enabled || !chatTtsOn || ideModeOn() || studioModeOn()) return;
1941
+ const clean = speechReadyText(text);
1942
+ if (!clean) return;
1943
+ if (clean === ttsLastSpoken) return; // aynı yazıyı iki kez okuma
1944
+ const r = await beast.ttsSpeak(clean.slice(0, 4000));
1945
+ if (!(r && r.ok && r.audioB64)) {
1946
+ toast('TTS: ' + ((r && r.error) || 'seslendirilemedi'));
1947
+ return;
1948
+ }
1949
+ ttsLastSpoken = clean;
1950
+ /* STT YANKI KÖPRÜSÜ: mikrofon, seslendirme bitene kadar tetiklenmez */
1951
+ const st = window.BeastHandsFree && window.BeastHandsFree.ttsState;
1952
+ if (st) { st.lastText = clean; st.playing = true; }
1953
+ await playTtsB64(r);
1954
+ } catch {}
1955
+ }
1956
+
1957
+ /* base64 mp3 → Blob URL → Audio. data: URL CSP'de engellendiği için blob kullanılır.
1958
+ Promise, çalma BİTTİĞİNDE çözülür (cümle kuyruğu sırayı böyle kurar);
1959
+ kesinti (pause/barge) ttsPlayResolve üzerinden anında çözer. */
1960
+ async function playTtsB64(r) {
1961
+ const bin = atob(r.audioB64);
1962
+ const bytes = new Uint8Array(bin.length);
1963
+ for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
1964
+ const blob = new Blob([bytes], { type: r.mime || 'audio/mpeg' });
1965
+ const url = URL.createObjectURL(blob);
1966
+ if (ttsAudioEl) { try { ttsAudioEl.pause(); } catch {} }
1967
+ return new Promise((resolve) => {
1968
+ let settled = false;
1969
+ const done = () => {
1970
+ if (settled) return;
1971
+ settled = true;
1972
+ ttsPlayResolve = null;
1973
+ try { URL.revokeObjectURL(url); } catch {}
1974
+ const st2 = window.BeastHandsFree && window.BeastHandsFree.ttsState;
1975
+ if (st2) st2.lastEndedAt = Date.now(); // yankı penceresi için
1976
+ setTimeout(() => {
1977
+ const st = window.BeastHandsFree && window.BeastHandsFree.ttsState;
1978
+ if (st) st.playing = false;
1979
+ }, 300);
1980
+ resolve();
1981
+ };
1982
+ ttsPlayResolve = done;
1983
+ ttsAudioEl = new Audio(url);
1984
+ ttsAudioEl.onended = done;
1985
+ ttsAudioEl.onerror = done;
1986
+ const st0 = window.BeastHandsFree && window.BeastHandsFree.ttsState;
1987
+ if (st0) st0.playing = true; // ses başlıyor — mikrofon adaptif zemin moduna geçer
1988
+ ttsAudioEl.play().catch((e) => {
1989
+ const st = window.BeastHandsFree && window.BeastHandsFree.ttsState;
1990
+ if (st) st.playing = false;
1991
+ toast('TTS çalınamadı: ' + String((e && e.message) || e));
1992
+ done();
1993
+ });
1994
+ });
1995
+ }
1996
+
1997
+ /* ---------------- CÜMLE CÜMLE STREAMING TTS ----------------
1998
+ Ajan yazarken tamamlanan her cümle kuyruğa girer ve SIRAYLA okunur —
1999
+ cevabın bitmesi beklenmez. Kesinti (barge/stop) jenerasyonu artırır:
2000
+ eski cümleler anında çöker, yeni cevap temiz başlar. */
2001
+ let ttsQList = []; // seslendirilecek cümleler (sıra)
2002
+ let ttsReady = []; // sentezlenmiş: { text, r } — sırayla çalınır
2003
+ let ttsSynthBusy = false; // tek sentez biriminde PREFETCH: cümle 1 çalarken 2 sentezlenir
2004
+ let ttsQBusy = false; // şu an bir cümle ÇALIYOR mu
2005
+ let ttsQGen = 0;
2006
+ let ttsPending = '';
2007
+ let ttsPlayResolve = null;
2008
+
2009
+ function ttsAutoOn() {
2010
+ return !!chatTtsOn && !ideModeOn() && !studioModeOn();
2011
+ }
2012
+
2013
+ /* openclaw tts-payload politikası: kısa metin okunmaz; kod-ağır metnin yerine
2014
+ "ekranda bıraktım" denir — kod okumak anlamsız ve kafa karıştırır */
2015
+ function speechReadyText(t) {
2016
+ const s = String(t || '').replace(/\s+/g, ' ').trim();
2017
+ if (s.length < 10) return '';
2018
+ const codeChars = (s.match(/[{}();=<>[\]\\]/g) || []).length;
2019
+ if (/```/.test(s) || codeChars / s.length >= 0.3) return 'Kod bloklarını ekranda bıraktım.';
2020
+ return s;
2021
+ }
2022
+
2023
+ function ttsQueueReset() {
2024
+ ttsQList = [];
2025
+ ttsReady = [];
2026
+ ttsPending = '';
2027
+ ttsQGen++;
2028
+ ttsSynthBusy = false; // in-flight sentez sonuçları gen kontrolüyle atılır
2029
+ ttsQBusy = false;
2030
+ try { if (ttsAudioEl) ttsAudioEl.pause(); } catch {}
2031
+ const st = window.BeastHandsFree && window.BeastHandsFree.ttsState;
2032
+ if (st) st.playing = false;
2033
+ if (ttsPlayResolve) { const r = ttsPlayResolve; ttsPlayResolve = null; r(); }
2034
+ }
2035
+
2036
+ function ttsEnqueueSentence(sentence) {
2037
+ ttsQList.push(sentence);
2038
+ ttsKick();
2039
+ }
2040
+
2041
+ /* PREFETCH: çalan cümlenin süresi boyunca SIRADAKİ cümle sentezlenir →
2042
+ cümleler arasında boşluk kalmaz (Edge istek gecikmesi gizlenir) */
2043
+ function ttsKick() {
2044
+ const myGen = ttsQGen;
2045
+ if (ttsSynthBusy) { ttsPump(); return; }
2046
+ if (!ttsQList.length) { ttsPump(); return; }
2047
+ const sentence = ttsQList.shift();
2048
+ ttsSynthBusy = true;
2049
+ beast.ttsSpeak(sentence.slice(0, 800))
2050
+ .then((r) => {
2051
+ ttsSynthBusy = false;
2052
+ if (myGen !== ttsQGen) return;
2053
+ if (r && r.ok && r.audioB64) ttsReady.push({ text: sentence, r });
2054
+ ttsKick();
2055
+ ttsPump();
2056
+ })
2057
+ .catch(() => {
2058
+ ttsSynthBusy = false;
2059
+ if (myGen !== ttsQGen) return;
2060
+ ttsKick();
2061
+ ttsPump();
2062
+ });
2063
+ }
2064
+
2065
+ async function ttsPump() {
2066
+ if (ttsQBusy) return;
2067
+ const myGen = ttsQGen;
2068
+ while (ttsReady.length) {
2069
+ if (myGen !== ttsQGen) return;
2070
+ const item = ttsReady.shift();
2071
+ ttsQBusy = true;
2072
+ try {
2073
+ ttsLastSpoken = item.text;
2074
+ const st = window.BeastHandsFree && window.BeastHandsFree.ttsState;
2075
+ if (st) { st.lastText = item.text; st.playing = true; }
2076
+ await playTtsB64(item.r);
2077
+ } catch {}
2078
+ ttsQBusy = false;
2079
+ if (myGen !== ttsQGen) return;
2080
+ ttsKick(); // çalma bitince sıradaki sentezi hemen başlat
2081
+ }
2082
+ }
2083
+
2084
+ function ttsFeedDelta(delta) {
2085
+ if (!ttsAutoOn()) return;
2086
+ ttsPending += String(delta || '');
2087
+ const parts = ttsPending.split(/(?<=[.!?…])\s+|\n+/);
2088
+ if (parts.length <= 1) return;
2089
+ const done = parts.slice(0, -1).map((s) => speechReadyText(s)).filter(Boolean);
2090
+ ttsPending = parts[parts.length - 1] || '';
2091
+ for (const s of done) ttsEnqueueSentence(s);
2092
+ }
2093
+
2094
+ function ttsFlushTail() {
2095
+ const rest = speechReadyText(ttsPending);
2096
+ ttsPending = '';
2097
+ if (rest) ttsEnqueueSentence(rest);
2098
+ else ttsKick();
2099
+ }
1894
2100
  async function renderTtsPane() {
1895
2101
  const pane = $('#tab-tts');
1896
2102
  if (!pane) return;
2103
+ /* STT DURUMU: model indirildi mi / iniyor mu / hangi motor */
2104
+ let stt = null;
2105
+ try { stt = await beast.sttStatus(); } catch {}
2106
+ const stateText = {
2107
+ ready: 'HAZIR — model yüklü',
2108
+ loading: 'İNİYOR / yükleniyor…',
2109
+ downloaded: 'indirildi — ilk kullanımda yüklenir',
2110
+ partial: 'kısmen indi — devam edecek',
2111
+ missing: 'henüz indirilmedi',
2112
+ cloud: 'bulut motoru — indirme gerekmez',
2113
+ };
2114
+ const sttLine = stt
2115
+ ? stt.engineLabel + ' · ' + (stateText[stt.state] || stt.state) + (stt.mb ? ' · ' + stt.mb + ' MB' : '')
2116
+ : (window.beast ? 'bilinmiyor' : '');
2117
+ const edgeOpts = [
2118
+ ['tr-TR-AhmetNeural', 'Ahmet — Türkçe (erkek)'],
2119
+ ['tr-TR-EmelNeural', 'Emel — Türkçe (kadın)'],
2120
+ ['en-US-GuyNeural', 'Guy — English (male)'],
2121
+ ['en-US-AriaNeural', 'Aria — English (female)'],
2122
+ ['de-DE-KatjaNeural', 'Katja — Deutsch'],
2123
+ ['ar-SA-HamedNeural', 'Hamed — العربية'],
2124
+ ]
2125
+ .map(([v, n]) => `<option value="${v}">${n}</option>`)
2126
+ .join('');
1897
2127
  pane.innerHTML =
1898
2128
  '<h2>' + _t('tts_h2') + '</h2><div class="sub">' + _t('tts_sub') + '</div>' +
2129
+ '<div class="sub" style="margin:10px 0 4px;font-weight:700;color:var(--accent)">STT (Ses → Yazı)</div>' +
2130
+ `<div class="sub" id="sttStatusTxt" style="margin-bottom:8px">${sttLine}</div>` +
2131
+ `<button id="sttDlBtn" class="btn ghost" style="margin-bottom:14px">${_t('stt_download_now')}</button>` +
2132
+ '<div class="sub" style="margin:10px 0 4px;font-weight:700;color:var(--accent)">TTS (Yazı → Ses)</div>' +
1899
2133
  `<div class="form-grid" style="grid-template-columns:auto 1fr 1fr;align-items:center;margin-top:10px">
1900
2134
  <label class="lock-row"><input type="checkbox" id="ttsOn" /><span>${_t('tts_active')}</span></label>
1901
- <input id="ttsUrl" class="inp" placeholder="https://api.openai.com/v1" autocomplete="off" />
1902
- <input id="ttsKey" class="inp" type="password" placeholder="API Key" autocomplete="off" />
1903
- <input id="ttsModel" class="inp" placeholder="tts-1" autocomplete="off" />
1904
- <input id="ttsVoice" class="inp" placeholder="ses: alloy" autocomplete="off" />
1905
- <button id="ttsSave" class="btn ghost">${_t('tts_save')}</button>
2135
+ <label style="grid-column:1">${_t('tts_engine')}</label>
2136
+ <select id="ttsEngine" class="inp" style="grid-column:2/4">
2137
+ <option value="edge">Edge TTS yerel & ücretsiz (Ahmet/Emel…)</option>
2138
+ <option value="openai">OpenAI-uyumlu API (tts-1, ses: alloy…)</option>
2139
+ </select>
2140
+ <label style="grid-column:1">${_t('tts_edge_voice')}</label>
2141
+ <select id="ttsEdgeVoice" class="inp" style="grid-column:2/4">${edgeOpts}</select>
2142
+ <label class="lock-row" style="grid-column:1/4"><input type="checkbox" id="ttsChatAuto" /><span>${_t('tts_chat_auto')}</span></label>
2143
+ <span id="ttsOpenaiWrap" style="display:contents">
2144
+ <input id="ttsUrl" class="inp" placeholder="https://api.openai.com/v1" autocomplete="off" />
2145
+ <input id="ttsKey" class="inp" type="password" placeholder="API Key" autocomplete="off" />
2146
+ <input id="ttsModel" class="inp" placeholder="tts-1" autocomplete="off" />
2147
+ <input id="ttsVoice" class="inp" placeholder="ses: alloy" autocomplete="off" />
2148
+ </span>
2149
+ <button id="ttsSave" class="btn ghost" style="grid-column:1">${_t('tts_save')}</button>
2150
+ <button id="ttsTest" class="btn ghost" style="grid-column:2;justify-self:start">${_t('tts_test')}</button>
1906
2151
  </div>` +
1907
2152
  '<div class="sub" style="margin-top:8px">' + _t('tts_note') + '</div>';
1908
2153
 
2154
+ const syncEngineUi = () => {
2155
+ const isEdge = $('#ttsEngine').value === 'edge';
2156
+ $('#ttsEdgeVoice').disabled = !isEdge;
2157
+ $('#ttsOpenaiWrap').style.opacity = isEdge ? '0.35' : '1';
2158
+ $('#ttsOpenaiWrap').querySelectorAll('input').forEach((i) => (i.disabled = isEdge));
2159
+ };
2160
+
1909
2161
  try {
1910
2162
  const tts = await beast.waGetTts();
1911
2163
  $('#ttsOn').checked = !!tts.enabled;
2164
+ $('#ttsEngine').value = tts.engine === 'openai' ? 'openai' : 'edge';
2165
+ $('#ttsEdgeVoice').value = tts.edgeVoice || 'tr-TR-AhmetNeural';
2166
+ $('#ttsChatAuto').checked = !!tts.chatAutoSpeak;
1912
2167
  $('#ttsUrl').value = tts.baseUrl || '';
1913
2168
  $('#ttsKey').value = tts.key || '';
1914
2169
  $('#ttsModel').value = tts.model || 'tts-1';
1915
2170
  $('#ttsVoice').value = tts.voice || 'alloy';
1916
2171
  } catch {}
2172
+ syncEngineUi();
2173
+ $('#ttsEngine').addEventListener('change', syncEngineUi);
2174
+
2175
+ /* STT durum satırı: yenile + şimdi indir (yüklenirken 3 sn'de bir güncellenir) */
2176
+ const refreshSttStatus = async () => {
2177
+ try {
2178
+ stt = await beast.sttStatus();
2179
+ const el = $('#sttStatusTxt');
2180
+ if (el && stt) el.textContent = stt.engineLabel + ' · ' + (stateText[stt.state] || stt.state) + (stt.mb ? ' · ' + stt.mb + ' MB' : '');
2181
+ return stt;
2182
+ } catch { return null; }
2183
+ };
2184
+ $('#sttDlBtn').addEventListener('click', async () => {
2185
+ await beast.sttPrefetchNow().catch(() => {});
2186
+ toast('STT modeli indiriliyor/yükleniyor…');
2187
+ const poll = setInterval(async () => {
2188
+ const s = await refreshSttStatus();
2189
+ if (s && (s.state === 'ready' || s.state === 'cloud')) { clearInterval(poll); toast('STT hazır'); }
2190
+ }, 3000);
2191
+ });
1917
2192
  $('#ttsSave').addEventListener('click', async () => {
1918
2193
  await beast.waSetTts({
1919
2194
  enabled: $('#ttsOn').checked,
2195
+ engine: $('#ttsEngine').value,
2196
+ edgeVoice: $('#ttsEdgeVoice').value,
2197
+ chatAutoSpeak: $('#ttsChatAuto').checked,
1920
2198
  baseUrl: $('#ttsUrl').value.trim(),
1921
2199
  key: $('#ttsKey').value.trim(),
1922
2200
  model: $('#ttsModel').value.trim(),
1923
2201
  voice: $('#ttsVoice').value.trim(),
1924
2202
  });
1925
- toast($('#ttsOn').checked ? 'TTS açık cevaplar sesli de gider' : 'TTS kapalı');
2203
+ ttsCfgCache = null; // ayar önbelleğini tazele
2204
+ /* hoparlör düğmesiyle TEK doğruluk kaynağı senkronu — asıl seslendirmeme bug'ı */
2205
+ chatTtsOn = $('#ttsChatAuto').checked;
2206
+ if (els.ttsBtn) els.ttsBtn.classList.toggle('on', chatTtsOn);
2207
+ toast($('#ttsOn').checked ? 'TTS açık — cevaplar sesli' : 'TTS kapalı');
2208
+ });
2209
+ $('#ttsTest').addEventListener('click', async () => {
2210
+ /* motor + IPC + çalma zincirini uçtan uca dener — sorun neredeyse görünür */
2211
+ toast('TTS test ediliyor…');
2212
+ const r = await beast.ttsSpeak('Merhaba kanka, Edge TTS testi. Ben Beast.').catch((e) => ({ ok: false, error: String((e && e.message) || e) }));
2213
+ if (!(r && r.ok)) { toast('TTS test HATA: ' + ((r && r.error) || '?')); return; }
2214
+ try {
2215
+ await playTtsB64(r);
2216
+ toast('TTS test OK — ses geliyor');
2217
+ } catch (e) {
2218
+ toast('TTS test çalma hatası: ' + String((e && e.message) || e));
2219
+ }
1926
2220
  });
1927
2221
  }
1928
2222
 
@@ -4420,10 +4714,17 @@ function onEvent(ev) {
4420
4714
  if (shown && ev.width) document.body.style.setProperty('--bw', ev.width + 'px');
4421
4715
  document.body.classList.toggle('phone-mode', !!ev.phone);
4422
4716
  if (els.bbPhone) els.bbPhone.classList.toggle('on', !!ev.phone);
4717
+ /* MOBİL ÖNİZLEME: telefon silueti çerçevesi + düğme durumu + cihaz seçimi */
4718
+ document.body.classList.toggle('mobile-preview', !!ev.mobile);
4719
+ if (els.bbMobile) els.bbMobile.classList.toggle('on', !!ev.mobile);
4720
+ if (ev.device) {
4721
+ document.body.dataset.pfDevice = ev.device;
4722
+ if (els.bbDevice) els.bbDevice.value = ev.device;
4723
+ }
4724
+ renderPhoneFrame(ev.phoneRect || null);
4423
4725
  els.browserBar.hidden = !shown;
4424
4726
  els.bbResize.hidden = !shown;
4425
- /* terminal de sağ dock'u kullanır tarayıcı açılınca yerini bırak */
4426
- if (shown && termOpen) termSetOpen(false);
4727
+ /* terminal artık ALT dock — tarayıcıyla birlikte yaşar, kapatılmaz */
4427
4728
  /* #19 tarayıcı açılınca paralel ajan konsolu (sağ panel) yerini bırakır;
4428
4729
  kapanınca önceki durumuna döner — istenirse railBtn ile elle açılır.
4429
4730
  GİZLİ ajan gezinmeleri (shown=false, wasShown=false) rail'e DOKUNMAZ —
@@ -4485,10 +4786,20 @@ function onEvent(ev) {
4485
4786
  break;
4486
4787
  case 'done':
4487
4788
  closeChatToolGroup();
4488
- /* iptal/durdurma sebebini SOHBETE yaz — sebepsiz durdurma yok */
4489
- if (ev.aborted) addStopNote(ev.reason);
4789
+ /* iptal sebebini SOHBETE yaz — ANCAK sesle kesilen turlarda not DÜŞÜLMEZ
4790
+ ("■ durdurma" hayaleti olmasın; handsfree'in kendi iş akışı) */
4791
+ if (ev.aborted) {
4792
+ if (!/eller serbest/i.test(String(ev.reason || ''))) addStopNote(ev.reason);
4793
+ ttsQueueReset();
4794
+ }
4490
4795
  setBusy(false);
4491
- setStatus(''); /* son durum balonu ("düşünüyor…" vb.) yapışmasın */
4796
+ setStatus('');
4797
+ /* TTS (eski hal): cevap bitince TAMAMINI seslendir */
4798
+ if (!ev.aborted && (!ev.sessionId || String(ev.sessionId) === String(activeId))) {
4799
+ const mdEl = [...els.msgs.querySelectorAll('.msg-assistant .md')].pop();
4800
+ const replyText = mdEl ? mdEl.innerText.trim() : '';
4801
+ if (replyText) void speakText(replyText);
4802
+ }
4492
4803
  /* cevabın SONU görünsün: markdown son render sonrası iki kez en alta kilitle */
4493
4804
  scrollDown(true);
4494
4805
  setTimeout(() => scrollDown(true), 80);
@@ -4498,6 +4809,7 @@ function onEvent(ev) {
4498
4809
  break;
4499
4810
  case 'error':
4500
4811
  closeChatToolGroup();
4812
+ ttsQueueReset(); // hata — konuşma kuyruğunu da temizle
4501
4813
  setBusy(false);
4502
4814
  setStatus('');
4503
4815
  addErrorBubble(ev.error);
@@ -4559,17 +4871,28 @@ function termSetShell(s) {
4559
4871
  if (changed && termOpen && termBannerDone) termLine('t-sys', 'Kabuk: ' + TERM_SHELLS[s].label);
4560
4872
  }
4561
4873
 
4562
- function termSetWidth(w) {
4563
- document.body.style.setProperty('--tw', Math.round(w) + 'px');
4564
- try { localStorage.setItem('beast.termW', String(Math.round(w))); } catch {}
4874
+ function termSetHeight(h) {
4875
+ const v = Math.max(120, Math.min(Math.round(h), Math.round(window.innerHeight * 0.7)));
4876
+ document.body.style.setProperty('--th', v + 'px');
4877
+ try { localStorage.setItem('beast.termH', String(v)); } catch {}
4878
+ /* native tarayıcı view'ına alt payı bildir — view yüksekliğini kısar */
4879
+ beast.browserSetBottomInset(termOpen ? v : 0).catch(() => {});
4565
4880
  }
4566
4881
 
4567
4882
  function termSetOpen(v) {
4883
+ /* BEAST CODE modunda terminal HEP AÇIK: kapatma denemeleri IDE modunda yok sayılır */
4884
+ if (!v && ideModeOn()) {
4885
+ toast('Beast Code modunda terminal açık kalır');
4886
+ return;
4887
+ }
4568
4888
  termOpen = !!v;
4569
4889
  els.termPanel.hidden = !v;
4570
4890
  els.termResize.hidden = !v;
4571
4891
  document.body.classList.toggle('term-open', v);
4572
4892
  termSetShell(termShell);
4893
+ /* alt dock: tarayıcı view'ı terminal payını boşaltır */
4894
+ const th = parseInt(getComputedStyle(document.body).getPropertyValue('--th')) || 180;
4895
+ beast.browserSetBottomInset(v ? th : 0).catch(() => {});
4573
4896
  if (v) {
4574
4897
  els.termInput.focus();
4575
4898
  termScroll(true);
@@ -4611,6 +4934,56 @@ function termShortTool(name, args) {
4611
4934
  } catch { return ''; }
4612
4935
  }
4613
4936
 
4937
+ /* ---------------- MOBİL ÖNİZLEME: telefon silueti ----------------
4938
+ main, WebContentsView'ı cihaz EKRANI boyutuna küçültür (phoneRect olayıyla
4939
+ gelir); çerçeve TEK YUVARLAK HALKA (border 12px, radius 42 — üst/alt köşeler
4940
+ birebir aynı) DOM'da çizilir; home çubuğu alt kenar içindedir. */
4941
+ const PF_BEZEL = 12;
4942
+
4943
+ function renderPhoneFrame(rect) {
4944
+ const pk = document.getElementById('bbDevice');
4945
+ let f = document.getElementById('phoneFrame');
4946
+ if (!rect) {
4947
+ if (f) f.style.display = 'none';
4948
+ if (pk) pk.style.display = 'none';
4949
+ return;
4950
+ }
4951
+ if (!f) {
4952
+ f = document.createElement('div');
4953
+ f.id = 'phoneFrame';
4954
+ f.innerHTML = '<div class="pf-home"></div>';
4955
+ document.body.appendChild(f);
4956
+ }
4957
+ f.style.display = 'block';
4958
+ f.style.left = (rect.x - PF_BEZEL) + 'px';
4959
+ f.style.top = (rect.y - PF_BEZEL) + 'px';
4960
+ f.style.width = (rect.width + PF_BEZEL * 2) + 'px';
4961
+ f.style.height = (rect.height + PF_BEZEL * 2) + 'px';
4962
+ /* cihaz seçici: çerçevenin altında ortalanmış */
4963
+ if (pk) {
4964
+ const frameBottom = rect.y + rect.height + PF_BEZEL;
4965
+ pk.hidden = false;
4966
+ pk.style.display = 'block';
4967
+ pk.style.top = (frameBottom + 12) + 'px';
4968
+ pk.style.left = Math.max(8, rect.x + rect.width / 2 - pk.offsetWidth / 2) + 'px';
4969
+ }
4970
+ }
4971
+
4972
+ /* ajan dev sunucu başlattı mı? (expo/metro/vite/next) — mobil önizleme
4973
+ açıksa oraya otomatik gidilir, kapatılsa da son URL hatırlanır */
4974
+ let devSeenUrl = '';
4975
+ const DEV_URL_RE_R = /https?:\/\/(?:localhost|127\.0\.0\.1|\[::1\]|(?:192\.168|10\.\d+|172\.(?:1[6-9]|2\d|3[01]))\.\d+\.\d+):(\d{2,5})/;
4976
+ function devServerSeen(text) {
4977
+ const m = DEV_URL_RE_R.exec(String(text || ''));
4978
+ if (!m) return;
4979
+ const port = Number(m[1]);
4980
+ if (port === 19000 || port === 19001 || port === 19002) return; // expo native/log portları
4981
+ const url = m[0].replace(/\/+$/, '') + '/';
4982
+ if (devSeenUrl === url) return;
4983
+ devSeenUrl = url;
4984
+ if (document.body.classList.contains('mobile-preview')) beast.browserNavigate(url).catch(() => {});
4985
+ }
4986
+
4614
4987
  /* ajan araç etkinliğini terminale akıt (tüm oturumlar: ana sohbet + paralel ajanlar + cron) */
4615
4988
  function termAgentEvent(ev) {
4616
4989
  if (ev.type === 'tool-start') {
@@ -4618,6 +4991,8 @@ function termAgentEvent(ev) {
4618
4991
  const args = termShortTool(ev.name, ev.args);
4619
4992
  termLine('t-agent', sid + '▸ ' + ev.name + (args ? ': ' + args : ''));
4620
4993
  } else if (ev.type === 'tool-end') {
4994
+ /* mobil önizleme: dev sunucu çıktısı yakala ("Local: http://localhost:8081" vb.) */
4995
+ devServerSeen(ev.result);
4621
4996
  const shellish = /bash|shell|powershell|cmd|command|script|terminal/i.test(String(ev.name || ''));
4622
4997
  const out = String(ev.result || '').replace(/\s+$/, '');
4623
4998
  const cap = shellish ? 1600 : 240;
@@ -4929,6 +5304,7 @@ async function saveVisible(v) {
4929
5304
  async function sendCurrent() {
4930
5305
  const text = els.input.value.trim();
4931
5306
  if ((!text && !pending.length) || !activeId) return;
5307
+ ttsQueueReset(); // yeni tur — eski seslendirme kuyruğu temizlenir
4932
5308
  /* #25 /clear artık main'e gider: oturum kayıtları GERÇEKTEN silinir
4933
5309
  (engine.clearMessages) ve 'clear' olayıyla ekran da temizlenir */
4934
5310
  if (text === '/screenshot') {
@@ -5170,7 +5546,10 @@ async function init() {
5170
5546
  refreshAgentsPane();
5171
5547
 
5172
5548
  els.sendBtn.addEventListener('click', sendCurrent);
5173
- els.stopBtn.addEventListener('click', () => activeId && beast.interrupt(activeId));
5549
+ els.stopBtn.addEventListener('click', () => {
5550
+ ttsQueueReset(); // ■ = konuşan ajan da susturulur
5551
+ if (activeId) beast.interrupt(activeId);
5552
+ });
5174
5553
 
5175
5554
  els.input.addEventListener('keydown', (e) => {
5176
5555
  if (!els.slashMenu.hidden) {
@@ -5282,68 +5661,88 @@ async function init() {
5282
5661
  els.fileInput.value = '';
5283
5662
  });
5284
5663
 
5285
- /* ---- Sesli komut (yerel Whisper): kayıt yazıya çevir → hedef input'a koy ----
5286
- Aynı düzenek chat composer VE Beast Code girişinde çalışır. */
5287
- const setupMicButton = (btn, apply) => {
5288
- if (!btn) return;
5289
- let micRec = null;
5290
- let micChunks = [];
5291
- let micStream = null;
5292
- const blobToDataUrl = (b) =>
5293
- new Promise((res) => {
5294
- const fr = new FileReader();
5295
- fr.onload = () => res(fr.result);
5296
- fr.readAsDataURL(b);
5297
- });
5298
- btn.addEventListener('click', async () => {
5299
- if (micRec && micRec.state === 'recording') {
5300
- micRec.stop();
5301
- return;
5302
- }
5303
- try {
5304
- micStream = await navigator.mediaDevices.getUserMedia({ audio: true });
5305
- } catch {
5306
- toast(_t('mic_denied'));
5307
- return;
5308
- }
5309
- micChunks = [];
5310
- micRec = new MediaRecorder(micStream);
5311
- micRec.ondataavailable = (e) => { if (e.data && e.data.size) micChunks.push(e.data); };
5312
- micRec.onstop = async () => {
5313
- try { micStream.getTracks().forEach((t) => t.stop()); } catch {}
5314
- btn.classList.remove('rec');
5315
- const blob = new Blob(micChunks, { type: micRec.mimeType || 'audio/webm' });
5316
- if (!blob.size) return;
5317
- toast(_t('mic_transcribing'));
5318
- const b64 = await blobToDataUrl(blob);
5319
- /* arayüz dili = Whisper dili: TR ise Türkçe, EN ise İngilizce algılar */
5320
- const uiLang = (window.I18N && window.I18N.lang) || 'tr';
5321
- const r = await beast.sttTranscribe(b64, uiLang).catch(() => ({ ok: false, error: 'ipc' }));
5322
- if (r && r.ok && r.text) apply(r.text);
5323
- else toast(_t('mic_fail'));
5324
- };
5325
- micRec.start();
5326
- btn.classList.add('rec');
5327
- toast(_t('mic_listening'));
5664
+ /* ---- ELLER SERBEST KONUŞMA (Hermes portu handsfree.js) ----
5665
+ Tek tık: sürekli dinleme döngüsü. Konuş, susunca cümle kendiliğinden
5666
+ kesilir yazıya çevrilir OTOMATİK GÖNDERİLİR → cevap bitince mikrofon
5667
+ yeniden açılır. "dur/yeter/goodbye" döngüyü bitirir. Tekrar tık (dinlerken)
5668
+ = şimdi gönder; düşünürken tık = modu kapat. Üç panelde aynı düzenek:
5669
+ chat, Beast Code, Beast Studio. */
5670
+ const hfBlobToDataUrl = (b) =>
5671
+ new Promise((res) => {
5672
+ const fr = new FileReader();
5673
+ fr.onload = () => res(fr.result);
5674
+ fr.readAsDataURL(b);
5328
5675
  });
5676
+ /* STT çağrıları main'deki öncelikli sıraya gider: final (priority 1)
5677
+ önizlemelerin (0) önüne geçer — whisper CPU'da yavaş olsa bile
5678
+ konuşanın cevabı önce transkribe edilir */
5679
+ let hfSttChain = Promise.resolve();
5680
+ const hfTranscribe = (blob, isStale, priority) => {
5681
+ const run = async () => {
5682
+ if (isStale && isStale()) throw new Error('stale');
5683
+ const b64 = await hfBlobToDataUrl(blob);
5684
+ /* arayüz dili = Whisper dili: TR ise Türkçe, EN ise İngilizce algılar */
5685
+ const uiLang = (window.I18N && window.I18N.lang) || 'tr';
5686
+ const r = await beast.sttTranscribe(b64, uiLang, priority).catch(() => ({ ok: false, error: 'ipc' }));
5687
+ if (r && r.ok && r.text) return String(r.text);
5688
+ throw new Error((r && r.error) || 'stt-fail');
5689
+ };
5690
+ const p = hfSttChain.then(run, run);
5691
+ hfSttChain = p.then(() => {}, () => {});
5692
+ return p;
5329
5693
  };
5330
- setupMicButton(els.micBtn, (t) => {
5331
- els.input.value = (els.input.value ? els.input.value.replace(/\s+$/, '') + ' ' : '') + t;
5332
- els.input.focus();
5333
- els.input.dispatchEvent(new Event('input'));
5334
- });
5335
- setupMicButton(els.bcMic, (t) => {
5336
- if (!els.bcInput) return;
5337
- els.bcInput.value = (els.bcInput.value ? els.bcInput.value.replace(/\s+$/, '') + ' ' : '') + t;
5338
- els.bcInput.focus();
5339
- bcInputResize();
5340
- });
5341
- setupMicButton(els.stMic, (t) => {
5342
- if (!els.stInput) return;
5343
- els.stInput.value = (els.stInput.value ? els.stInput.value.replace(/\s+$/, '') + ' ' : '') + t;
5344
- els.stInput.focus();
5345
- stInputResize();
5346
- });
5694
+ if (window.BeastHandsFree) {
5695
+ const mkHF = (cfg) => { if (cfg.btn) window.BeastHandsFree.createPanel(cfg); };
5696
+ /* DOĞAL KONUŞMA: barge tetiklenince TTS anında kesilir → mikrofon temizlenir */
5697
+ const cutTts = () => {
5698
+ try { if (ttsAudioEl) ttsAudioEl.pause(); } catch {}
5699
+ const st = window.BeastHandsFree && window.BeastHandsFree.ttsState;
5700
+ if (st) st.playing = false;
5701
+ };
5702
+ mkHF({
5703
+ btn: els.micBtn,
5704
+ isBusy: () => busy,
5705
+ submit: async (text) => { els.input.value = text; await sendCurrent(); },
5706
+ transcribe: hfTranscribe,
5707
+ draft: (text) => {
5708
+ els.input.value = text;
5709
+ els.input.dispatchEvent(new Event('input'));
5710
+ autosize();
5711
+ },
5712
+ interrupt: () => { if (activeId) beast.interrupt(activeId, 'eller serbest: konuşunca tur kesildi').catch(() => {}); },
5713
+ cutTts,
5714
+ status: (m) => setStatus(m),
5715
+ statusHide: () => setStatus(''),
5716
+ toast,
5717
+ t: _t,
5718
+ });
5719
+ mkHF({
5720
+ btn: els.bcMic,
5721
+ isBusy: () => bcRunning,
5722
+ submit: async (text) => { els.bcInput.value = text; bcRunCurrent(); },
5723
+ transcribe: hfTranscribe,
5724
+ draft: (text) => { els.bcInput.value = text; bcInputResize(); },
5725
+ interrupt: () => { beast.beastcodeStop().catch(() => {}); },
5726
+ cutTts,
5727
+ status: (m) => bcStatusShow(m),
5728
+ statusHide: () => bcStatusHide(),
5729
+ toast,
5730
+ t: _t,
5731
+ });
5732
+ mkHF({
5733
+ btn: els.stMic,
5734
+ isBusy: () => stRunning,
5735
+ submit: async (text) => { els.stInput.value = text; stRunCurrent(); },
5736
+ transcribe: hfTranscribe,
5737
+ draft: (text) => { els.stInput.value = text; stInputResize(); },
5738
+ interrupt: () => { beast.studioStop().catch(() => {}); },
5739
+ cutTts,
5740
+ status: (m) => stStatusShow(m),
5741
+ statusHide: () => stStatusHide(),
5742
+ toast,
5743
+ t: _t,
5744
+ });
5745
+ }
5347
5746
 
5348
5747
  beast.onWaEvent(onWaEvent);
5349
5748
  beast.onTgEvent(onTgEvent);
@@ -5400,8 +5799,25 @@ async function init() {
5400
5799
  beast.browserPhone(on).catch(() => {});
5401
5800
  });
5402
5801
  }
5802
+ if (els.bbMobile) {
5803
+ els.bbMobile.addEventListener('click', () => {
5804
+ /* MOBİL ÖNİZLEME: telefon silueti içinde canlı dev-server önizlemesi */
5805
+ const on = !document.body.classList.contains('mobile-preview');
5806
+ beast.browserMobileSet(on).catch(() => {});
5807
+ });
5808
+ }
5809
+ if (els.bbDevice) {
5810
+ els.bbDevice.addEventListener('change', () => {
5811
+ beast.browserDeviceSet(els.bbDevice.value).catch(() => {});
5812
+ });
5813
+ }
5403
5814
  els.bbClose.addEventListener('click', () => {
5404
5815
  document.body.classList.remove('browser-open');
5816
+ /* mobil önizleme KESİN kapansın: main tarafı zaten kapatıyor, DOM tarafı da
5817
+ beklemeden temizlenir (siluet + cihaz seçici + mod düğmesi) */
5818
+ document.body.classList.remove('mobile-preview');
5819
+ if (els.bbMobile) els.bbMobile.classList.remove('on');
5820
+ renderPhoneFrame(null);
5405
5821
  els.browserBar.hidden = true;
5406
5822
  els.bbResize.hidden = true;
5407
5823
  els.browserBtn.classList.remove('on');
@@ -5472,7 +5888,7 @@ async function init() {
5472
5888
  });
5473
5889
 
5474
5890
  /* terminal paneli — olay bağlama */
5475
- try { termSetWidth(parseInt(localStorage.getItem('beast.termW')) || 520); } catch {}
5891
+ try { termSetHeight(parseInt(localStorage.getItem('beast.termH')) || 180); } catch {}
5476
5892
  if (els.termCBtn) els.termCBtn.addEventListener('click', () => termToggle('cmd'));
5477
5893
  if (els.termClose) els.termClose.addEventListener('click', () => termSetOpen(false));
5478
5894
  if (els.termClear) els.termClear.addEventListener('click', () => { els.termOut.innerHTML = ''; });
@@ -5495,18 +5911,19 @@ async function init() {
5495
5911
  }
5496
5912
  }
5497
5913
  });
5498
- /* terminal sürükle-boyutlandır */
5914
+ /* terminal sürükle-boyutlandır (alt dock: üst kenardan dikey sürükle) */
5499
5915
  let trz = null;
5500
- const twNow = () => parseInt(getComputedStyle(document.body).getPropertyValue('--tw')) || 520;
5916
+ const thNow = () => parseInt(getComputedStyle(document.body).getPropertyValue('--th')) || 180;
5501
5917
  if (els.termResize) els.termResize.addEventListener('mousedown', (e) => {
5502
5918
  e.preventDefault();
5503
- trz = { sx: e.clientX, sw: twNow() };
5919
+ trz = { sy: e.clientY, sh: thNow() };
5504
5920
  document.body.classList.add('term-dragging');
5505
5921
  });
5506
5922
  document.addEventListener('mousemove', (e) => {
5507
5923
  if (!trz) return;
5508
- const w = Math.max(340, Math.min(trz.sw - (e.clientX - trz.sx), window.innerWidth - 340));
5509
- termSetWidth(w);
5924
+ /* yukarı çek = yükseklik artar; aşağı çek = azalır */
5925
+ const h = Math.max(120, Math.min(trz.sh + (trz.sy - e.clientY), Math.round(window.innerHeight * 0.7)));
5926
+ termSetHeight(h);
5510
5927
  });
5511
5928
  document.addEventListener('mouseup', () => {
5512
5929
  if (!trz) return;
@@ -6258,9 +6675,13 @@ async function setIdeMode(on) {
6258
6675
  if (!on && document.body.classList.contains('browser-open')) {
6259
6676
  try { beast.toggleBrowser(); } catch {}
6260
6677
  }
6678
+ /* ALT TERMINAL: yalnız Beast Code'a özgü — Agent/Studio moduna dönüşte kapanır */
6679
+ if (!on && termOpen) termSetOpen(false);
6261
6680
  if (on) {
6262
6681
  ideSplitRestore();
6263
6682
  setEditorHidden(localStorage.getItem('beast.editorHidden') === '1');
6683
+ /* BEAST CODE modunda terminal HEP AÇIK — mod girilirken otomatik açılır */
6684
+ if (!termOpen) termSetOpen(true);
6264
6685
  await loadIdeTree();
6265
6686
  bcBanner();
6266
6687
  codeGutterRender(); /* dosya açık olmasa bile rakamlar görünür */
@@ -6291,6 +6712,8 @@ async function setStudioMode(on) {
6291
6712
  if (on && ideModeOn()) await setIdeMode(false); /* IDE açıkken Studio'ya geçiş — IDE kapanır */
6292
6713
  document.body.classList.toggle('studio-mode', !!on);
6293
6714
  if (els.studioBtn) els.studioBtn.classList.toggle('on', !!on);
6715
+ /* ALT TERMINAL: yalnız Beast Code'a özgü — Studio'ya geçişte kapanır */
6716
+ if (on && termOpen) termSetOpen(false);
6294
6717
  const brandSub = document.querySelector('#brand .brand-sub');
6295
6718
  if (brandSub) brandSub.textContent = on ? 'Studio' : 'Agent';
6296
6719
  /* klasör konsolunun en üstü: Studio modunda "BEAST STUDIO" yazar */
@@ -7091,6 +7514,8 @@ $('#filePreview').addEventListener('click', async () => {
7091
7514
  const r = await beast.idePreview().catch(() => null);
7092
7515
  if (r && r.ok) {
7093
7516
  if (ideModeOn() === false) setIdeMode(true);
7517
+ /* mobil proje: npm run web + telefon silueti — kullanıcıya net geri bildirim */
7518
+ if (r.mobile) toast('Mobil önizleme: dev sunucu başlatıldı — telefon siluetine bak');
7094
7519
  } else {
7095
7520
  toast((r && r.error) || 'preview açılamadı');
7096
7521
  }