beast-agent 2.2.1 → 2.3.1

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,293 @@ async function renderSkillsPane() {
1891
1894
 
1892
1895
  /* ---------------- sesli yanıt (TTS) ---------------- */
1893
1896
 
1894
- async function renderTtsPane() {
1895
- const pane = $('#tab-tts');
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
+ }
2100
+
2101
+ async function renderTtsPane() { const pane = $('#tab-tts');
1896
2102
  if (!pane) return;
2103
+ const edgeOpts = [
2104
+ ['tr-TR-AhmetNeural', 'Ahmet — Türkçe (erkek)'],
2105
+ ['tr-TR-EmelNeural', 'Emel — Türkçe (kadın)'],
2106
+ ['en-US-GuyNeural', 'Guy — English (male)'],
2107
+ ['en-US-AriaNeural', 'Aria — English (female)'],
2108
+ ['de-DE-KatjaNeural', 'Katja — Deutsch'],
2109
+ ['ar-SA-HamedNeural', 'Hamed — العربية'],
2110
+ ]
2111
+ .map(([v, n]) => `<option value="${v}">${n}</option>`)
2112
+ .join('');
1897
2113
  pane.innerHTML =
1898
2114
  '<h2>' + _t('tts_h2') + '</h2><div class="sub">' + _t('tts_sub') + '</div>' +
1899
2115
  `<div class="form-grid" style="grid-template-columns:auto 1fr 1fr;align-items:center;margin-top:10px">
1900
2116
  <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>
2117
+ <label style="grid-column:1">${_t('tts_engine')}</label>
2118
+ <select id="ttsEngine" class="inp" style="grid-column:2/4">
2119
+ <option value="edge">Edge TTS yerel & ücretsiz (Ahmet/Emel…)</option>
2120
+ <option value="openai">OpenAI-uyumlu API (tts-1, ses: alloy…)</option>
2121
+ </select>
2122
+ <label style="grid-column:1">${_t('tts_edge_voice')}</label>
2123
+ <select id="ttsEdgeVoice" class="inp" style="grid-column:2/4">${edgeOpts}</select>
2124
+ <label class="lock-row" style="grid-column:1/4"><input type="checkbox" id="ttsChatAuto" /><span>${_t('tts_chat_auto')}</span></label>
2125
+ <span id="ttsOpenaiWrap" style="display:contents">
2126
+ <input id="ttsUrl" class="inp" placeholder="https://api.openai.com/v1" autocomplete="off" />
2127
+ <input id="ttsKey" class="inp" type="password" placeholder="API Key" autocomplete="off" />
2128
+ <input id="ttsModel" class="inp" placeholder="tts-1" autocomplete="off" />
2129
+ <input id="ttsVoice" class="inp" placeholder="ses: alloy" autocomplete="off" />
2130
+ </span>
2131
+ <button id="ttsSave" class="btn ghost" style="grid-column:1">${_t('tts_save')}</button>
2132
+ <button id="ttsTest" class="btn ghost" style="grid-column:2;justify-self:start">${_t('tts_test')}</button>
1906
2133
  </div>` +
1907
2134
  '<div class="sub" style="margin-top:8px">' + _t('tts_note') + '</div>';
1908
2135
 
2136
+ const syncEngineUi = () => {
2137
+ const isEdge = $('#ttsEngine').value === 'edge';
2138
+ $('#ttsEdgeVoice').disabled = !isEdge;
2139
+ $('#ttsOpenaiWrap').style.opacity = isEdge ? '0.35' : '1';
2140
+ $('#ttsOpenaiWrap').querySelectorAll('input').forEach((i) => (i.disabled = isEdge));
2141
+ };
2142
+
1909
2143
  try {
1910
2144
  const tts = await beast.waGetTts();
1911
2145
  $('#ttsOn').checked = !!tts.enabled;
2146
+ $('#ttsEngine').value = tts.engine === 'openai' ? 'openai' : 'edge';
2147
+ $('#ttsEdgeVoice').value = tts.edgeVoice || 'tr-TR-AhmetNeural';
2148
+ $('#ttsChatAuto').checked = !!tts.chatAutoSpeak;
1912
2149
  $('#ttsUrl').value = tts.baseUrl || '';
1913
2150
  $('#ttsKey').value = tts.key || '';
1914
2151
  $('#ttsModel').value = tts.model || 'tts-1';
1915
2152
  $('#ttsVoice').value = tts.voice || 'alloy';
1916
2153
  } catch {}
2154
+ syncEngineUi();
2155
+ $('#ttsEngine').addEventListener('change', syncEngineUi);
1917
2156
  $('#ttsSave').addEventListener('click', async () => {
1918
2157
  await beast.waSetTts({
1919
2158
  enabled: $('#ttsOn').checked,
2159
+ engine: $('#ttsEngine').value,
2160
+ edgeVoice: $('#ttsEdgeVoice').value,
2161
+ chatAutoSpeak: $('#ttsChatAuto').checked,
1920
2162
  baseUrl: $('#ttsUrl').value.trim(),
1921
2163
  key: $('#ttsKey').value.trim(),
1922
2164
  model: $('#ttsModel').value.trim(),
1923
2165
  voice: $('#ttsVoice').value.trim(),
1924
2166
  });
1925
- toast($('#ttsOn').checked ? 'TTS açık cevaplar sesli de gider' : 'TTS kapalı');
2167
+ ttsCfgCache = null; // ayar önbelleğini tazele
2168
+ /* hoparlör düğmesiyle TEK doğruluk kaynağı senkronu — asıl seslendirmeme bug'ı */
2169
+ chatTtsOn = $('#ttsChatAuto').checked;
2170
+ if (els.ttsBtn) els.ttsBtn.classList.toggle('on', chatTtsOn);
2171
+ toast($('#ttsOn').checked ? 'TTS açık — cevaplar sesli' : 'TTS kapalı');
2172
+ });
2173
+ $('#ttsTest').addEventListener('click', async () => {
2174
+ /* motor + IPC + çalma zincirini uçtan uca dener — sorun neredeyse görünür */
2175
+ toast('TTS test ediliyor…');
2176
+ const r = await beast.ttsSpeak('Merhaba kanka, Edge TTS testi. Ben Beast.').catch((e) => ({ ok: false, error: String((e && e.message) || e) }));
2177
+ if (!(r && r.ok)) { toast('TTS test HATA: ' + ((r && r.error) || '?')); return; }
2178
+ try {
2179
+ await playTtsB64(r);
2180
+ toast('TTS test OK — ses geliyor');
2181
+ } catch (e) {
2182
+ toast('TTS test çalma hatası: ' + String((e && e.message) || e));
2183
+ }
1926
2184
  });
1927
2185
  }
1928
2186
 
@@ -4420,10 +4678,17 @@ function onEvent(ev) {
4420
4678
  if (shown && ev.width) document.body.style.setProperty('--bw', ev.width + 'px');
4421
4679
  document.body.classList.toggle('phone-mode', !!ev.phone);
4422
4680
  if (els.bbPhone) els.bbPhone.classList.toggle('on', !!ev.phone);
4681
+ /* MOBİL ÖNİZLEME: telefon silueti çerçevesi + düğme durumu + cihaz seçimi */
4682
+ document.body.classList.toggle('mobile-preview', !!ev.mobile);
4683
+ if (els.bbMobile) els.bbMobile.classList.toggle('on', !!ev.mobile);
4684
+ if (ev.device) {
4685
+ document.body.dataset.pfDevice = ev.device;
4686
+ if (els.bbDevice) els.bbDevice.value = ev.device;
4687
+ }
4688
+ renderPhoneFrame(ev.phoneRect || null);
4423
4689
  els.browserBar.hidden = !shown;
4424
4690
  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);
4691
+ /* terminal artık ALT dock — tarayıcıyla birlikte yaşar, kapatılmaz */
4427
4692
  /* #19 tarayıcı açılınca paralel ajan konsolu (sağ panel) yerini bırakır;
4428
4693
  kapanınca önceki durumuna döner — istenirse railBtn ile elle açılır.
4429
4694
  GİZLİ ajan gezinmeleri (shown=false, wasShown=false) rail'e DOKUNMAZ —
@@ -4485,10 +4750,20 @@ function onEvent(ev) {
4485
4750
  break;
4486
4751
  case 'done':
4487
4752
  closeChatToolGroup();
4488
- /* iptal/durdurma sebebini SOHBETE yaz — sebepsiz durdurma yok */
4489
- if (ev.aborted) addStopNote(ev.reason);
4753
+ /* iptal sebebini SOHBETE yaz — ANCAK sesle kesilen turlarda not DÜŞÜLMEZ
4754
+ ("■ durdurma" hayaleti olmasın; handsfree'in kendi iş akışı) */
4755
+ if (ev.aborted) {
4756
+ if (!/eller serbest/i.test(String(ev.reason || ''))) addStopNote(ev.reason);
4757
+ ttsQueueReset();
4758
+ }
4490
4759
  setBusy(false);
4491
- setStatus(''); /* son durum balonu ("düşünüyor…" vb.) yapışmasın */
4760
+ setStatus('');
4761
+ /* TTS (eski hal): cevap bitince TAMAMINI seslendir */
4762
+ if (!ev.aborted && (!ev.sessionId || String(ev.sessionId) === String(activeId))) {
4763
+ const mdEl = [...els.msgs.querySelectorAll('.msg-assistant .md')].pop();
4764
+ const replyText = mdEl ? mdEl.innerText.trim() : '';
4765
+ if (replyText) void speakText(replyText);
4766
+ }
4492
4767
  /* cevabın SONU görünsün: markdown son render sonrası iki kez en alta kilitle */
4493
4768
  scrollDown(true);
4494
4769
  setTimeout(() => scrollDown(true), 80);
@@ -4498,6 +4773,7 @@ function onEvent(ev) {
4498
4773
  break;
4499
4774
  case 'error':
4500
4775
  closeChatToolGroup();
4776
+ ttsQueueReset(); // hata — konuşma kuyruğunu da temizle
4501
4777
  setBusy(false);
4502
4778
  setStatus('');
4503
4779
  addErrorBubble(ev.error);
@@ -4559,17 +4835,28 @@ function termSetShell(s) {
4559
4835
  if (changed && termOpen && termBannerDone) termLine('t-sys', 'Kabuk: ' + TERM_SHELLS[s].label);
4560
4836
  }
4561
4837
 
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 {}
4838
+ function termSetHeight(h) {
4839
+ const v = Math.max(120, Math.min(Math.round(h), Math.round(window.innerHeight * 0.7)));
4840
+ document.body.style.setProperty('--th', v + 'px');
4841
+ try { localStorage.setItem('beast.termH', String(v)); } catch {}
4842
+ /* native tarayıcı view'ına alt payı bildir — view yüksekliğini kısar */
4843
+ beast.browserSetBottomInset(termOpen ? v : 0).catch(() => {});
4565
4844
  }
4566
4845
 
4567
4846
  function termSetOpen(v) {
4847
+ /* BEAST CODE modunda terminal HEP AÇIK: kapatma denemeleri IDE modunda yok sayılır */
4848
+ if (!v && ideModeOn()) {
4849
+ toast('Beast Code modunda terminal açık kalır');
4850
+ return;
4851
+ }
4568
4852
  termOpen = !!v;
4569
4853
  els.termPanel.hidden = !v;
4570
4854
  els.termResize.hidden = !v;
4571
4855
  document.body.classList.toggle('term-open', v);
4572
4856
  termSetShell(termShell);
4857
+ /* alt dock: tarayıcı view'ı terminal payını boşaltır */
4858
+ const th = parseInt(getComputedStyle(document.body).getPropertyValue('--th')) || 180;
4859
+ beast.browserSetBottomInset(v ? th : 0).catch(() => {});
4573
4860
  if (v) {
4574
4861
  els.termInput.focus();
4575
4862
  termScroll(true);
@@ -4611,6 +4898,56 @@ function termShortTool(name, args) {
4611
4898
  } catch { return ''; }
4612
4899
  }
4613
4900
 
4901
+ /* ---------------- MOBİL ÖNİZLEME: telefon silueti ----------------
4902
+ main, WebContentsView'ı cihaz EKRANI boyutuna küçültür (phoneRect olayıyla
4903
+ gelir); çerçeve TEK YUVARLAK HALKA (border 12px, radius 42 — üst/alt köşeler
4904
+ birebir aynı) DOM'da çizilir; home çubuğu alt kenar içindedir. */
4905
+ const PF_BEZEL = 12;
4906
+
4907
+ function renderPhoneFrame(rect) {
4908
+ const pk = document.getElementById('bbDevice');
4909
+ let f = document.getElementById('phoneFrame');
4910
+ if (!rect) {
4911
+ if (f) f.style.display = 'none';
4912
+ if (pk) pk.style.display = 'none';
4913
+ return;
4914
+ }
4915
+ if (!f) {
4916
+ f = document.createElement('div');
4917
+ f.id = 'phoneFrame';
4918
+ f.innerHTML = '<div class="pf-home"></div>';
4919
+ document.body.appendChild(f);
4920
+ }
4921
+ f.style.display = 'block';
4922
+ f.style.left = (rect.x - PF_BEZEL) + 'px';
4923
+ f.style.top = (rect.y - PF_BEZEL) + 'px';
4924
+ f.style.width = (rect.width + PF_BEZEL * 2) + 'px';
4925
+ f.style.height = (rect.height + PF_BEZEL * 2) + 'px';
4926
+ /* cihaz seçici: çerçevenin altında ortalanmış */
4927
+ if (pk) {
4928
+ const frameBottom = rect.y + rect.height + PF_BEZEL;
4929
+ pk.hidden = false;
4930
+ pk.style.display = 'block';
4931
+ pk.style.top = (frameBottom + 12) + 'px';
4932
+ pk.style.left = Math.max(8, rect.x + rect.width / 2 - pk.offsetWidth / 2) + 'px';
4933
+ }
4934
+ }
4935
+
4936
+ /* ajan dev sunucu başlattı mı? (expo/metro/vite/next) — mobil önizleme
4937
+ açıksa oraya otomatik gidilir, kapatılsa da son URL hatırlanır */
4938
+ let devSeenUrl = '';
4939
+ 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})/;
4940
+ function devServerSeen(text) {
4941
+ const m = DEV_URL_RE_R.exec(String(text || ''));
4942
+ if (!m) return;
4943
+ const port = Number(m[1]);
4944
+ if (port === 19000 || port === 19001 || port === 19002) return; // expo native/log portları
4945
+ const url = m[0].replace(/\/+$/, '') + '/';
4946
+ if (devSeenUrl === url) return;
4947
+ devSeenUrl = url;
4948
+ if (document.body.classList.contains('mobile-preview')) beast.browserNavigate(url).catch(() => {});
4949
+ }
4950
+
4614
4951
  /* ajan araç etkinliğini terminale akıt (tüm oturumlar: ana sohbet + paralel ajanlar + cron) */
4615
4952
  function termAgentEvent(ev) {
4616
4953
  if (ev.type === 'tool-start') {
@@ -4618,6 +4955,8 @@ function termAgentEvent(ev) {
4618
4955
  const args = termShortTool(ev.name, ev.args);
4619
4956
  termLine('t-agent', sid + '▸ ' + ev.name + (args ? ': ' + args : ''));
4620
4957
  } else if (ev.type === 'tool-end') {
4958
+ /* mobil önizleme: dev sunucu çıktısı yakala ("Local: http://localhost:8081" vb.) */
4959
+ devServerSeen(ev.result);
4621
4960
  const shellish = /bash|shell|powershell|cmd|command|script|terminal/i.test(String(ev.name || ''));
4622
4961
  const out = String(ev.result || '').replace(/\s+$/, '');
4623
4962
  const cap = shellish ? 1600 : 240;
@@ -4929,6 +5268,7 @@ async function saveVisible(v) {
4929
5268
  async function sendCurrent() {
4930
5269
  const text = els.input.value.trim();
4931
5270
  if ((!text && !pending.length) || !activeId) return;
5271
+ ttsQueueReset(); // yeni tur — eski seslendirme kuyruğu temizlenir
4932
5272
  /* #25 /clear artık main'e gider: oturum kayıtları GERÇEKTEN silinir
4933
5273
  (engine.clearMessages) ve 'clear' olayıyla ekran da temizlenir */
4934
5274
  if (text === '/screenshot') {
@@ -5170,7 +5510,10 @@ async function init() {
5170
5510
  refreshAgentsPane();
5171
5511
 
5172
5512
  els.sendBtn.addEventListener('click', sendCurrent);
5173
- els.stopBtn.addEventListener('click', () => activeId && beast.interrupt(activeId));
5513
+ els.stopBtn.addEventListener('click', () => {
5514
+ ttsQueueReset(); // ■ = konuşan ajan da susturulur
5515
+ if (activeId) beast.interrupt(activeId);
5516
+ });
5174
5517
 
5175
5518
  els.input.addEventListener('keydown', (e) => {
5176
5519
  if (!els.slashMenu.hidden) {
@@ -5282,68 +5625,88 @@ async function init() {
5282
5625
  els.fileInput.value = '';
5283
5626
  });
5284
5627
 
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'));
5628
+ /* ---- ELLER SERBEST KONUŞMA (Hermes portu handsfree.js) ----
5629
+ Tek tık: sürekli dinleme döngüsü. Konuş, susunca cümle kendiliğinden
5630
+ kesilir yazıya çevrilir OTOMATİK GÖNDERİLİR → cevap bitince mikrofon
5631
+ yeniden açılır. "dur/yeter/goodbye" döngüyü bitirir. Tekrar tık (dinlerken)
5632
+ = şimdi gönder; düşünürken tık = modu kapat. Üç panelde aynı düzenek:
5633
+ chat, Beast Code, Beast Studio. */
5634
+ const hfBlobToDataUrl = (b) =>
5635
+ new Promise((res) => {
5636
+ const fr = new FileReader();
5637
+ fr.onload = () => res(fr.result);
5638
+ fr.readAsDataURL(b);
5328
5639
  });
5640
+ /* STT çağrıları main'deki öncelikli sıraya gider: final (priority 1)
5641
+ önizlemelerin (0) önüne geçer — whisper CPU'da yavaş olsa bile
5642
+ konuşanın cevabı önce transkribe edilir */
5643
+ let hfSttChain = Promise.resolve();
5644
+ const hfTranscribe = (blob, isStale, priority) => {
5645
+ const run = async () => {
5646
+ if (isStale && isStale()) throw new Error('stale');
5647
+ const b64 = await hfBlobToDataUrl(blob);
5648
+ /* arayüz dili = Whisper dili: TR ise Türkçe, EN ise İngilizce algılar */
5649
+ const uiLang = (window.I18N && window.I18N.lang) || 'tr';
5650
+ const r = await beast.sttTranscribe(b64, uiLang, priority).catch(() => ({ ok: false, error: 'ipc' }));
5651
+ if (r && r.ok && r.text) return String(r.text);
5652
+ throw new Error((r && r.error) || 'stt-fail');
5653
+ };
5654
+ const p = hfSttChain.then(run, run);
5655
+ hfSttChain = p.then(() => {}, () => {});
5656
+ return p;
5329
5657
  };
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
- });
5658
+ if (window.BeastHandsFree) {
5659
+ const mkHF = (cfg) => { if (cfg.btn) window.BeastHandsFree.createPanel(cfg); };
5660
+ /* DOĞAL KONUŞMA: barge tetiklenince TTS anında kesilir → mikrofon temizlenir */
5661
+ const cutTts = () => {
5662
+ try { if (ttsAudioEl) ttsAudioEl.pause(); } catch {}
5663
+ const st = window.BeastHandsFree && window.BeastHandsFree.ttsState;
5664
+ if (st) st.playing = false;
5665
+ };
5666
+ mkHF({
5667
+ btn: els.micBtn,
5668
+ isBusy: () => busy,
5669
+ submit: async (text) => { els.input.value = text; await sendCurrent(); },
5670
+ transcribe: hfTranscribe,
5671
+ draft: (text) => {
5672
+ els.input.value = text;
5673
+ els.input.dispatchEvent(new Event('input'));
5674
+ autosize();
5675
+ },
5676
+ interrupt: () => { if (activeId) beast.interrupt(activeId, 'eller serbest: konuşunca tur kesildi').catch(() => {}); },
5677
+ cutTts,
5678
+ status: (m) => setStatus(m),
5679
+ statusHide: () => setStatus(''),
5680
+ toast,
5681
+ t: _t,
5682
+ });
5683
+ mkHF({
5684
+ btn: els.bcMic,
5685
+ isBusy: () => bcRunning,
5686
+ submit: async (text) => { els.bcInput.value = text; bcRunCurrent(); },
5687
+ transcribe: hfTranscribe,
5688
+ draft: (text) => { els.bcInput.value = text; bcInputResize(); },
5689
+ interrupt: () => { beast.beastcodeStop().catch(() => {}); },
5690
+ cutTts,
5691
+ status: (m) => bcStatusShow(m),
5692
+ statusHide: () => bcStatusHide(),
5693
+ toast,
5694
+ t: _t,
5695
+ });
5696
+ mkHF({
5697
+ btn: els.stMic,
5698
+ isBusy: () => stRunning,
5699
+ submit: async (text) => { els.stInput.value = text; stRunCurrent(); },
5700
+ transcribe: hfTranscribe,
5701
+ draft: (text) => { els.stInput.value = text; stInputResize(); },
5702
+ interrupt: () => { beast.studioStop().catch(() => {}); },
5703
+ cutTts,
5704
+ status: (m) => stStatusShow(m),
5705
+ statusHide: () => stStatusHide(),
5706
+ toast,
5707
+ t: _t,
5708
+ });
5709
+ }
5347
5710
 
5348
5711
  beast.onWaEvent(onWaEvent);
5349
5712
  beast.onTgEvent(onTgEvent);
@@ -5400,8 +5763,25 @@ async function init() {
5400
5763
  beast.browserPhone(on).catch(() => {});
5401
5764
  });
5402
5765
  }
5766
+ if (els.bbMobile) {
5767
+ els.bbMobile.addEventListener('click', () => {
5768
+ /* MOBİL ÖNİZLEME: telefon silueti içinde canlı dev-server önizlemesi */
5769
+ const on = !document.body.classList.contains('mobile-preview');
5770
+ beast.browserMobileSet(on).catch(() => {});
5771
+ });
5772
+ }
5773
+ if (els.bbDevice) {
5774
+ els.bbDevice.addEventListener('change', () => {
5775
+ beast.browserDeviceSet(els.bbDevice.value).catch(() => {});
5776
+ });
5777
+ }
5403
5778
  els.bbClose.addEventListener('click', () => {
5404
5779
  document.body.classList.remove('browser-open');
5780
+ /* mobil önizleme KESİN kapansın: main tarafı zaten kapatıyor, DOM tarafı da
5781
+ beklemeden temizlenir (siluet + cihaz seçici + mod düğmesi) */
5782
+ document.body.classList.remove('mobile-preview');
5783
+ if (els.bbMobile) els.bbMobile.classList.remove('on');
5784
+ renderPhoneFrame(null);
5405
5785
  els.browserBar.hidden = true;
5406
5786
  els.bbResize.hidden = true;
5407
5787
  els.browserBtn.classList.remove('on');
@@ -5472,7 +5852,7 @@ async function init() {
5472
5852
  });
5473
5853
 
5474
5854
  /* terminal paneli — olay bağlama */
5475
- try { termSetWidth(parseInt(localStorage.getItem('beast.termW')) || 520); } catch {}
5855
+ try { termSetHeight(parseInt(localStorage.getItem('beast.termH')) || 180); } catch {}
5476
5856
  if (els.termCBtn) els.termCBtn.addEventListener('click', () => termToggle('cmd'));
5477
5857
  if (els.termClose) els.termClose.addEventListener('click', () => termSetOpen(false));
5478
5858
  if (els.termClear) els.termClear.addEventListener('click', () => { els.termOut.innerHTML = ''; });
@@ -5495,18 +5875,19 @@ async function init() {
5495
5875
  }
5496
5876
  }
5497
5877
  });
5498
- /* terminal sürükle-boyutlandır */
5878
+ /* terminal sürükle-boyutlandır (alt dock: üst kenardan dikey sürükle) */
5499
5879
  let trz = null;
5500
- const twNow = () => parseInt(getComputedStyle(document.body).getPropertyValue('--tw')) || 520;
5880
+ const thNow = () => parseInt(getComputedStyle(document.body).getPropertyValue('--th')) || 180;
5501
5881
  if (els.termResize) els.termResize.addEventListener('mousedown', (e) => {
5502
5882
  e.preventDefault();
5503
- trz = { sx: e.clientX, sw: twNow() };
5883
+ trz = { sy: e.clientY, sh: thNow() };
5504
5884
  document.body.classList.add('term-dragging');
5505
5885
  });
5506
5886
  document.addEventListener('mousemove', (e) => {
5507
5887
  if (!trz) return;
5508
- const w = Math.max(340, Math.min(trz.sw - (e.clientX - trz.sx), window.innerWidth - 340));
5509
- termSetWidth(w);
5888
+ /* yukarı çek = yükseklik artar; aşağı çek = azalır */
5889
+ const h = Math.max(120, Math.min(trz.sh + (trz.sy - e.clientY), Math.round(window.innerHeight * 0.7)));
5890
+ termSetHeight(h);
5510
5891
  });
5511
5892
  document.addEventListener('mouseup', () => {
5512
5893
  if (!trz) return;
@@ -6258,9 +6639,13 @@ async function setIdeMode(on) {
6258
6639
  if (!on && document.body.classList.contains('browser-open')) {
6259
6640
  try { beast.toggleBrowser(); } catch {}
6260
6641
  }
6642
+ /* ALT TERMINAL: yalnız Beast Code'a özgü — Agent/Studio moduna dönüşte kapanır */
6643
+ if (!on && termOpen) termSetOpen(false);
6261
6644
  if (on) {
6262
6645
  ideSplitRestore();
6263
6646
  setEditorHidden(localStorage.getItem('beast.editorHidden') === '1');
6647
+ /* BEAST CODE modunda terminal HEP AÇIK — mod girilirken otomatik açılır */
6648
+ if (!termOpen) termSetOpen(true);
6264
6649
  await loadIdeTree();
6265
6650
  bcBanner();
6266
6651
  codeGutterRender(); /* dosya açık olmasa bile rakamlar görünür */
@@ -6291,6 +6676,8 @@ async function setStudioMode(on) {
6291
6676
  if (on && ideModeOn()) await setIdeMode(false); /* IDE açıkken Studio'ya geçiş — IDE kapanır */
6292
6677
  document.body.classList.toggle('studio-mode', !!on);
6293
6678
  if (els.studioBtn) els.studioBtn.classList.toggle('on', !!on);
6679
+ /* ALT TERMINAL: yalnız Beast Code'a özgü — Studio'ya geçişte kapanır */
6680
+ if (on && termOpen) termSetOpen(false);
6294
6681
  const brandSub = document.querySelector('#brand .brand-sub');
6295
6682
  if (brandSub) brandSub.textContent = on ? 'Studio' : 'Agent';
6296
6683
  /* klasör konsolunun en üstü: Studio modunda "BEAST STUDIO" yazar */
@@ -7091,6 +7478,8 @@ $('#filePreview').addEventListener('click', async () => {
7091
7478
  const r = await beast.idePreview().catch(() => null);
7092
7479
  if (r && r.ok) {
7093
7480
  if (ideModeOn() === false) setIdeMode(true);
7481
+ /* mobil proje: npm run web + telefon silueti — kullanıcıya net geri bildirim */
7482
+ if (r.mobile) toast('Mobil önizleme: dev sunucu başlatıldı — telefon siluetine bak');
7094
7483
  } else {
7095
7484
  toast((r && r.error) || 'preview açılamadı');
7096
7485
  }