beast-agent 1.4.2 → 1.5.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.
package/src/main.js CHANGED
@@ -863,10 +863,15 @@ function stopEverything() {
863
863
  }
864
864
 
865
865
  function resumeServices() {
866
- if (!servicesPaused) return;
866
+ if (!servicesPaused) {
867
+ /* /stop kapısı burada da kalkar — /start VE kullanıcının kendi mesajı canlandırır */
868
+ try { engine.clearStop(); } catch {}
869
+ return;
870
+ }
867
871
  try { cron.init({ onFire: cronFire }); } catch {}
868
872
  try { watchers.start({ onTrigger: watcherFire }); } catch {}
869
873
  try { startEventBus(); } catch {}
874
+ try { engine.clearStop(); } catch {}
870
875
  servicesPaused = false;
871
876
  }
872
877
 
@@ -1100,7 +1105,7 @@ async function tryWaSlash(jid, rawText, senderNum, payload0) {
1100
1105
  const stopped = stopEverything();
1101
1106
  out =
1102
1107
  `*Durdu* — ${stopped} koşan iş kesildi.\n` +
1103
- `Paralel ajanlar ve bekleyen kuyruklar temizlendi. Cron, izleyici ve olay merkezi çalışmaya devam ediyor.\n` +
1108
+ `Sürüyen sorgular, akıştaki cevaplar ve ajan faaliyetleri ANINDA kesildi; ajan yeni sorgu da açamaz.\n` +
1104
1109
  `Devam için bir şeyler yaz ya da /start`;
1105
1110
  } else if (cmd === 'start') {
1106
1111
  if (servicesPaused) {
@@ -2032,7 +2037,7 @@ async function processWaMessage(jid, payload, senderNum, requeues = 0) {
2032
2037
  text: waEcho.join('\n'),
2033
2038
  });
2034
2039
  }
2035
- engine.send(sid, { text: text.slice(0, 8000), attachments });
2040
+ engine.send(sid, { text: text.slice(0, 8000), attachments }, { userAction: true });
2036
2041
  }
2037
2042
 
2038
2043
  function ensureWa() {
@@ -2241,7 +2246,7 @@ async function processTgMessage(chatId, payload, requeues = 0) {
2241
2246
  text += `\n[NOT: Bu kişi SAHİP DEĞİL, misafirdir. Sahibin ayarlarını/verilerini değiştirme; kalıcı hafızaya misafire özel bilgi yazma.]`;
2242
2247
  }
2243
2248
  text += `\n${String(payload.text || '').slice(0, 6000)}`;
2244
- engine.send(sid, { text: text.slice(0, 8000), attachments: [] });
2249
+ engine.send(sid, { text: text.slice(0, 8000), attachments: [] }, { userAction: true });
2245
2250
  }
2246
2251
 
2247
2252
  async function sendTgSafe(chatId, text) {
@@ -2459,7 +2464,7 @@ async function processDcMessage(channelId, payload) {
2459
2464
  text += `\n[NOT: Bu kişi SAHİP DEĞİL, misafirdir. Sahibin ayarlarını/verilerini değiştirme; kalıcı hafızaya misafire özel bilgi yazma.]`;
2460
2465
  }
2461
2466
  text += `\n${String(payload.text || '').slice(0, 6000)}`;
2462
- engine.send(sid, { text: text.slice(0, 8000), attachments: [] });
2467
+ engine.send(sid, { text: text.slice(0, 8000), attachments: [] }, { userAction: true });
2463
2468
  }
2464
2469
 
2465
2470
  async function sendDcSafe(channelId, text) {
@@ -2572,6 +2577,11 @@ function reloadBackend() {
2572
2577
  if (win && !win.isDestroyed()) win.webContents.send('agent:event', ev);
2573
2578
  flushDesktopOnDone(ev); /* biriken desktop mesajlarını sıraya bas */
2574
2579
  bcFlushOnDone(ev); /* Beast Code kuyruğunu iş bitiminde boşalt */
2580
+ /* BC canlı önizleme: ajan bir dev server başlattıysa adresi yakala —
2581
+ preview butonu ve otomatik açılış DAİMA bu sunucuyu öncelikli kullanır */
2582
+ if (ev.type === 'bc-preview' && /^https?:\/\/(?:localhost|127\.0\.0\.1)(?::\d{2,5})?/i.test(String(ev.url || ''))) {
2583
+ bcLastServerUrl = String(ev.url);
2584
+ }
2575
2585
  /* WA canlı iş takibi: oturum bir WhatsApp sohbetine bağlıysa araç
2576
2586
  hareketlerini (terminal, web, dosya…) kısa satırla bildir.
2577
2587
  Spam olmasın: sohbet başına en az 7 sn'de bir tek satır. */
@@ -2693,6 +2703,32 @@ function reloadBackend() {
2693
2703
  })();
2694
2704
  }
2695
2705
  }
2706
+ /* CRON → TÜM AKTİF ENTEGRASYONLAR: cron işi bittiğinde cevap (ya da
2707
+ hata) sahibin bağlı olduğu her kanala yansıtılır — WA + Telegram +
2708
+ Discord. Aynı kanal hem cron oturumuna bağlıysa TEK cevap alır. */
2709
+ if ((ev.type === 'done' || ev.type === 'error') && cronAnswerPending.has(String(ev.sessionId))) {
2710
+ const cjob = cronAnswerPending.get(String(ev.sessionId));
2711
+ cronAnswerPending.delete(String(ev.sessionId));
2712
+ if (!ev.aborted) {
2713
+ (async () => {
2714
+ try {
2715
+ let txt = '';
2716
+ if (ev.type === 'error') {
2717
+ txt = '⚠️ [cron: ' + String((cjob && cjob.name) || 'görev') + ']\nHata: ' + String(ev.error || '').slice(0, 200);
2718
+ } else {
2719
+ const s = engine.openSession(ev.sessionId);
2720
+ const lastA = [...s.messages].reverse().find((m) => m.role === 'assistant' && m.content);
2721
+ txt = typeof (lastA && lastA.content) === 'string' ? lastA.content : '';
2722
+ if (txt.trim()) txt = '⏰ [cron: ' + String((cjob && cjob.name) || 'görev') + ']\n' + txt;
2723
+ }
2724
+ if (!txt.trim()) return;
2725
+ for (const m of cronMirrorTargets(String(ev.sessionId))) {
2726
+ try { await m.send(txt); } catch {}
2727
+ }
2728
+ } catch {}
2729
+ })();
2730
+ }
2731
+ }
2696
2732
  },
2697
2733
  });
2698
2734
  return engine.publicState();
@@ -2711,8 +2747,7 @@ function falloutResume() {
2711
2747
  if (!f.enabled || f.autoResume === false) return;
2712
2748
  const st = JSON.parse(fs.readFileSync(FALLOUT_CRASH_FILE, 'utf8'));
2713
2749
  if (!st || !st.sessionId) return;
2714
- const exists = engine.listSessions().some((s) => s.id === st.sessionId);
2715
- const sid = exists ? st.sessionId : engine.createSession().id;
2750
+ const sid = reuseOrLatestSession(st.sessionId);
2716
2751
  const when = st.at ? new Date(st.at).toLocaleString('tr-TR') : '?';
2717
2752
  setTimeout(() => {
2718
2753
  try {
@@ -2759,7 +2794,8 @@ function whereWasISummary() {
2759
2794
  function maybeRunWhereWasI() {
2760
2795
  try {
2761
2796
  const cfg = settings.whereWasI || {};
2762
- if (cfg.enabled === false) return;
2797
+ /* DEFAULT KAPALI: yalnız Ayarlar → Maliyet · Limit'ten açıldıysa göster */
2798
+ if (cfg.enabled !== true) return;
2763
2799
  const sum = whereWasISummary();
2764
2800
  if (!sum) return;
2765
2801
  settingsLog(`nerede-kaldım: ${String(sum.text).split('\n')[0]}`);
@@ -2855,6 +2891,7 @@ app.whenReady().then(() => {
2855
2891
  cron.init({ onFire: cronFire });
2856
2892
  watchers.start({ onTrigger: watcherFire });
2857
2893
  startEventBus();
2894
+ ideWatchStart(); // soldaki dosya ağacı canlı izlemede
2858
2895
  maybeRunWhereWasI();
2859
2896
  falloutResume();
2860
2897
  startAutoUpdater(); // #3 sessiz güncelleme
@@ -2938,18 +2975,57 @@ function showWin() {
2938
2975
 
2939
2976
  const BROWSER_TOOLBAR_H = 46;
2940
2977
  const BROWSER_START_URL = 'https://www.google.com/';
2978
+ /* TELEFON MODU: mobil UA + dar dock → sitelerin mobil versiyonu canlı izlenir;
2979
+ Expo/Metro dev sunucularında otomatik devreye girer */
2980
+ const PHONE_UA =
2981
+ 'Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/' +
2982
+ process.versions.chrome +
2983
+ ' Mobile Safari/537.36';
2941
2984
  /* Tarayıcı state: visible=false → ajanlar GİZLİ kullanır (headless);
2942
2985
  göz ikonuyla görünür mod açılır. open=view aktif, visible=panelde görünürlük */
2943
- const browser = { view: null, open: false, visible: false, width: 0, attached: false, started: false };
2986
+ const browser = { view: null, open: false, visible: false, width: 0, attached: false, started: false, phone: false, desktopUA: '' };
2944
2987
 
2945
2988
  function browserEmit(payload) {
2946
- if (win && !win.isDestroyed()) win.webContents.send('agent:event', { type: 'browser', visible: browser.visible, ...payload });
2989
+ if (win && !win.isDestroyed()) win.webContents.send('agent:event', { type: 'browser', visible: browser.visible, phone: browser.phone, ...payload });
2947
2990
  }
2948
2991
 
2949
2992
  function browserWidthFor(w) {
2950
2993
  return Math.max(380, Math.min(800, Math.floor(w * 0.46)));
2951
2994
  }
2952
2995
 
2996
+ function browserShownWidth(w) {
2997
+ /* telefon modunda dock genişliği KORUNUR — 390x844 telefon ekranı
2998
+ görüntünün ORTASINDA, cihaz çerçevesiyle gösterilir (DevTools gibi) */
2999
+ return Math.min(browser.width, Math.max(320, w - 320));
3000
+ }
3001
+
3002
+ /* telefon modu görüntü parametreleri: mobil ekran emülasyonu + sığma ölçeği */
3003
+ function applyPhoneEmulation() {
3004
+ try {
3005
+ const wc = browser.view && browser.view.webContents;
3006
+ if (!wc) return;
3007
+ if (!browser.phone) {
3008
+ wc.disableDeviceEmulation();
3009
+ return;
3010
+ }
3011
+ let w = 800;
3012
+ let h = 900;
3013
+ try { [w, h] = win.getContentSize(); } catch {}
3014
+ const viewW = browserShownWidth(w);
3015
+ const viewH = Math.max(240, h - BROWSER_TOOLBAR_H - 8);
3016
+ const PW = 390;
3017
+ const PH = 844;
3018
+ const scale = Math.max(0.3, Math.min(1, (viewW - 20) / PW, (viewH - 12) / PH));
3019
+ wc.enableDeviceEmulation({
3020
+ screenPosition: 'mobile',
3021
+ screenOrientation: { type: 'portraitPrimary', angle: 0 },
3022
+ viewSize: { width: PW, height: PH },
3023
+ deviceScaleFactor: 2,
3024
+ scale,
3025
+ });
3026
+ } catch {}
3027
+ }
3028
+
2953
3029
  function layoutBrowser() {
2954
3030
  if (!win || win.isDestroyed()) return;
2955
3031
  const [w, h] = win.getContentSize();
@@ -2960,7 +3036,7 @@ function layoutBrowser() {
2960
3036
  const saved = Math.round(Number(settings.browserWidth) || 0);
2961
3037
  browser.width = saved >= 300 ? saved : browserWidthFor(w);
2962
3038
  }
2963
- const shownWidth = Math.min(browser.width, Math.max(320, w - 320));
3039
+ const shownWidth = browserShownWidth(w);
2964
3040
  if (!browser.view || !browser.open) return;
2965
3041
  const view = browser.view;
2966
3042
  if (!browser.attached) {
@@ -2972,6 +3048,7 @@ function layoutBrowser() {
2972
3048
  try {
2973
3049
  view.setBounds({ x: Math.max(0, w - shownWidth), y: BROWSER_TOOLBAR_H, width: shownWidth, height: Math.max(0, h - BROWSER_TOOLBAR_H) });
2974
3050
  view.setVisible(browser.open && browser.visible);
3051
+ if (browser.phone) applyPhoneEmulation(); // pencere boyutlanınca telefon ölçeği tazelensin
2975
3052
  } catch {}
2976
3053
  }
2977
3054
 
@@ -2998,6 +3075,8 @@ function ensureBrowser() {
2998
3075
  const bses = session.fromPartition('persist:browser');
2999
3076
  if (bses && bses.setUserAgent) bses.setUserAgent(chromeUA, 'tr-TR,tr;q=0.9,en;q=0.8');
3000
3077
  wc.setUserAgent(chromeUA);
3078
+ browser.desktopUA = chromeUA;
3079
+ if (browser.phone) wc.setUserAgent(PHONE_UA); // telefon modu açıkken mobil UA ile doğ
3001
3080
  } catch {}
3002
3081
  wc.setWindowOpenHandler(({ url }) => {
3003
3082
  if (/^https?:\/\//i.test(url)) wc.loadURL(url).catch(() => {});
@@ -3022,6 +3101,7 @@ function ensureBrowser() {
3022
3101
  browser.view = null;
3023
3102
  });
3024
3103
  browser.view = view;
3104
+ if (browser.phone) applyPhoneEmulation(); // tarayıcı telefon modunda doğarsa emülasyonu uygula
3025
3105
  return view;
3026
3106
  }
3027
3107
 
@@ -3923,6 +4003,8 @@ function createWindow() {
3923
4003
  /* ---------------- IPC ---------------- */
3924
4004
 
3925
4005
  ipcMain.handle('app:state', () => engine.publicState());
4006
+ /* Ayarlar penceresi altında küçük sürüm etiketi (ör. v1.4.2) */
4007
+ ipcMain.handle('app:version', () => beastVersion());
3926
4008
 
3927
4009
  /* #3 log sistemi: ayarlar → Log sekmesinde görüntülenir */
3928
4010
  ipcMain.handle('logs:get', () => {
@@ -4295,7 +4377,7 @@ function handleGlobalStopStart(sessionId, cmd) {
4295
4377
  const n = stopEverything();
4296
4378
  reply =
4297
4379
  `\u25A0 **Durdu** — ${n} koşan iş kesildi.\n` +
4298
- 'Paralel ajanlar ve bekleyen kuyruklar temizlendi. Cron, izleyici ve olay merkezi çalışmaya devam ediyor.\n' +
4380
+ 'Sürüyen sorgular, akıştaki cevaplar ve ajan faaliyetleri ANINDA kesildi; ajan yeni sorgu da açamaz.\n' +
4299
4381
  'Devam için bir şeyler yaz ya da `/start`';
4300
4382
  } else {
4301
4383
  const wasPaused = servicesPaused;
@@ -4392,7 +4474,7 @@ async function flushDesktop(sessionId) {
4392
4474
  if (!mergedAtts && Array.isArray(m.attachments)) mergedAtts = m.attachments;
4393
4475
  }
4394
4476
  if (!mergedText.trim() && !mergedAtts) return;
4395
- engine.send(sid, mergedAtts ? { text: mergedText, attachments: mergedAtts } : mergedText);
4477
+ engine.send(sid, mergedAtts ? { text: mergedText, attachments: mergedAtts } : mergedText, { userAction: true });
4396
4478
  }
4397
4479
 
4398
4480
  /* agent işi bitince biriken masaüstü mesajlarını göndere bastır.
@@ -4627,13 +4709,13 @@ ipcMain.handle('model:set', (_e, sel) => {
4627
4709
 
4628
4710
  /* Paralel ajan geçmişini TOPLUCA sil (rail başlığındaki çöp ikonu) */
4629
4711
  ipcMain.handle('agents:clearAll', () => {
4630
- try {
4631
- const removed = engine.clearAllBgJobs();
4632
- return { ok: true, removed };
4633
- } catch (e) {
4634
- return { ok: false, error: String((e && e.message) || e) };
4635
- }
4636
- });
4712
+ try {
4713
+ const removed = engine.clearAllBgJobs();
4714
+ return { ok: true, removed };
4715
+ } catch (e) {
4716
+ return { ok: false, error: String((e && e.message) || e) };
4717
+ }
4718
+ });
4637
4719
 
4638
4720
  ipcMain.handle('model:role', (_e, map) => {
4639
4721
  const roleModels = {};
@@ -4840,8 +4922,12 @@ ipcMain.handle('setup:skip', () => {
4840
4922
  return { ok: true };
4841
4923
  });
4842
4924
 
4843
- /* #5 "nerede kaldım" */
4844
- ipcMain.handle('wherewasi:get', () => whereWasISummary());
4925
+ /* #5 "nerede kaldım": get → gerçek ayar durumu (checkbox doğru görünsün) */
4926
+ ipcMain.handle('wherewasi:get', () => {
4927
+ const cfg = settings.whereWasI || {};
4928
+ const sum = whereWasISummary();
4929
+ return { enabled: cfg.enabled === true, ...(sum || {}) };
4930
+ });
4845
4931
  ipcMain.handle('wherewasi:set', (_e, cfg) => {
4846
4932
  settings.whereWasI = { enabled: !!(cfg && cfg.enabled) };
4847
4933
  saveSettings();
@@ -5121,18 +5207,86 @@ function cronEmit() {
5121
5207
  }
5122
5208
  }
5123
5209
 
5210
+ /* Cron cevabı bekleme haritası: sid -> job. İş 'done' olunca cevap
5211
+ SAHİBİN bağlı olduğu TÜM AKTİF entegrasyonlara yansıtılır (kullanıcı
5212
+ hangi kanaldan ajanla iletişimde belli değil). */
5213
+ const cronAnswerPending = new Map();
5214
+
5215
+ /* OTOMATİK YENİ SOHBET YOK: cron/izleyici/fallout tetiklendiğinde oturum
5216
+ seçimi — kayıtlı id geçerliyse O, değilse EN GÜNCEL oturum kullanılır.
5217
+ Hiç oturum yoksa (ilk kurulum) yeni açılır. Böylece soldaki sohbet
5218
+ geçmişine "+ Yeni Sohbet" olmadan hayalet sohbetler düşmez. */
5219
+ function reuseOrLatestSession(preferredId) {
5220
+ const sid = String(preferredId || '');
5221
+ if (sid) {
5222
+ try {
5223
+ if (engine._load(sid)) return sid;
5224
+ } catch {}
5225
+ }
5226
+ try {
5227
+ const list = engine.listSessions(); // updatedAt'e göre yeni→eski sıralı
5228
+ if (list.length) return String(list[0].id);
5229
+ } catch {}
5230
+ return engine.createSession().id;
5231
+ }
5232
+
5233
+ /* Yansıtma hedefleri: bağlı WA (owner numarası), Telegram ve Discord
5234
+ (owner işaretli kayıt; tek kayıt varsa o). Cron oturumunun KENDİ
5235
+ kanalına yansıtmayız — cevabı zaten kendi akışından alır (çift yok). */
5236
+ function tgOwnerIds() {
5237
+ const list = settings.tgAllow || [];
5238
+ const objs = list.filter((e) => e && typeof e === 'object' && e.id && e.id !== '*');
5239
+ const owner = objs.find((e) => e.owner) || (objs.length === 1 ? objs[0] : null);
5240
+ return owner ? [String(owner.id)] : [];
5241
+ }
5242
+ function dcOwnerIds() {
5243
+ const list = settings.dcAllow || [];
5244
+ const objs = list.filter((e) => e && typeof e === 'object' && e.id && e.id !== '*');
5245
+ const owner = objs.find((e) => e.owner) || (objs.length === 1 ? objs[0] : null);
5246
+ return owner ? [String(owner.id)] : [];
5247
+ }
5248
+ function cronMirrorTargets(cronSid) {
5249
+ const out = [];
5250
+ try {
5251
+ if (wa && wa.connected) {
5252
+ const own = waOwnerNum();
5253
+ if (own) {
5254
+ const jid = own + '@s.whatsapp.net';
5255
+ const bound = [...waChats.entries()].some(([j, s]) => j === jid && String(s) === String(cronSid));
5256
+ if (!bound) out.push({ kind: 'wa', send: (t) => sendWaSafe(jid, t) });
5257
+ }
5258
+ }
5259
+ } catch {}
5260
+ try {
5261
+ if (tg && tg.connected) {
5262
+ for (const id of tgOwnerIds()) {
5263
+ const bound = [...tgChats.entries()].some(([c, s]) => String(c) === String(id) && String(s) === String(cronSid));
5264
+ if (!bound) out.push({ kind: 'tg', send: (t) => sendTgSafe(id, t) });
5265
+ }
5266
+ }
5267
+ } catch {}
5268
+ try {
5269
+ if (dc && dc.connected) {
5270
+ for (const id of dcOwnerIds()) {
5271
+ const bound = [...dcChats.entries()].some(([c, s]) => String(c) === String(id) && String(s) === String(cronSid));
5272
+ if (!bound) out.push({ kind: 'dc', send: (t) => sendDcSafe(id, t) });
5273
+ }
5274
+ }
5275
+ } catch {}
5276
+ return out;
5277
+ }
5278
+
5124
5279
  function cronFire(job) {
5125
5280
  try {
5126
- let sid = job.sessionId;
5127
- const exists = sid && engine.listSessions().some((s) => s.id === sid);
5128
- if (!exists) {
5129
- const s = engine.createSession();
5130
- sid = s.id;
5281
+ const sid = reuseOrLatestSession(job.sessionId);
5282
+ if (sid !== String(job.sessionId || '')) {
5131
5283
  cron.update(job.id, { sessionId: sid });
5132
5284
  }
5133
- engine.send(sid, {
5285
+ cronAnswerPending.set(String(sid), job);
5286
+ const sent = engine.send(sid, {
5134
5287
  text: `[cron: ${job.name}]\n${job.prompt}`,
5135
5288
  });
5289
+ if (!sent) cronAnswerPending.delete(String(sid)); // gönderilemedi — bayat bekleme bırakma
5136
5290
  } catch {}
5137
5291
  cronEmit();
5138
5292
  }
@@ -5141,10 +5295,8 @@ function cronFire(job) {
5141
5295
  function watcherFire(w, value) {
5142
5296
  try {
5143
5297
  watcherLog(`tetiklendi id=${w.id} name="${w.name}" kind=${w.kind} op=${w.op} value=${value}`);
5144
- let sid = w.sessionId;
5145
- const exists = sid && engine.listSessions().some((s) => s.id === sid);
5146
- if (!exists) {
5147
- sid = engine.createSession().id;
5298
+ const sid = reuseOrLatestSession(w.sessionId);
5299
+ if (sid !== String(w.sessionId || '')) {
5148
5300
  watchers.patch(w.id, { sessionId: sid });
5149
5301
  }
5150
5302
  const target =
@@ -5274,9 +5426,35 @@ ipcMain.handle('browser:setWidth', (_e, wpx) => {
5274
5426
  settings.browserWidth = browser.width;
5275
5427
  saveSettings();
5276
5428
  layoutBrowser();
5277
- browserEmit({ open: true, width: browser.width });
5429
+ browserEmit({ open: true, width: browserShownWidth(w) });
5278
5430
  return { ok: true, width: browser.width };
5279
5431
  });
5432
+
5433
+ /* TELEFON MODU: mobil UA + dar dock (≈430px) → aynı sitenin mobil versiyonu.
5434
+ Kapatınca masaüstü UA + kayıtlı genişlik geri gelir. UA değişimi için sayfa
5435
+ taze yüklenir. Expo/Metro dev sunucularında otomatik açılır. */
5436
+ function setBrowserPhone(on) {
5437
+ const want = !!on;
5438
+ if (browser.phone === want) return { ok: true, phone: want };
5439
+ browser.phone = want;
5440
+ try {
5441
+ const wc = browser.view && browser.view.webContents;
5442
+ if (wc) {
5443
+ wc.setUserAgent(browser.phone ? PHONE_UA : browser.desktopUA || PHONE_UA);
5444
+ let url = '';
5445
+ try { url = wc.getURL(); } catch {}
5446
+ if (url && /^https?:/i.test(url)) wc.loadURL(url).catch(() => {});
5447
+ }
5448
+ } catch {}
5449
+ applyPhoneEmulation(); // açık: 390x844 cihaz ortalanır — kapalı: emülasyon kapanır
5450
+ if (browser.open && win && !win.isDestroyed()) {
5451
+ layoutBrowser();
5452
+ const [w] = win.getContentSize();
5453
+ browserEmit({ open: true, width: browserShownWidth(w) });
5454
+ }
5455
+ return { ok: true, phone: browser.phone };
5456
+ }
5457
+ ipcMain.handle('browser:phone', (_e, on) => setBrowserPhone(on));
5280
5458
  ipcMain.handle('browser:screenshot', async () => {
5281
5459
  const r = await browserScreenshot();
5282
5460
  if (!r.ok) return { ok: false, error: r.error };
@@ -5287,31 +5465,95 @@ ipcMain.handle('browser:screenshot', async () => {
5287
5465
  Sağ dock paneli renderer'da; main sadece PowerShell komutlarını
5288
5466
  çalıştırır ve çıktıyı canlı akıtır. Terminal ile tarayıcı aynı
5289
5467
  dock'u paylaştığı için ikisi aynı anda açık kalamaz. */
5290
- let termChild = null;
5468
+ let termChild = null; /* KALICI CMD oturumu — cd/set değişkenleri komutlar arasında KORUNUR */
5469
+ let termShellCwd = ''; /* izlenen çalışma klasörü (komut sonundaki marker'dan) */
5470
+ let termShellId = null;
5471
+ let termShellSeq = 0;
5472
+ let termForwarded = 0;
5473
+ let termCapNotified = false;
5291
5474
  const TERM_FWD_CAP = 1024 * 1024; /* iletilecek çıktı üst sınırı (1 MB) */
5475
+ const TERM_MARKER = '__BEAST_EOF__';
5292
5476
 
5293
5477
  function termSend(ev) {
5294
5478
  if (win && !win.isDestroyed()) win.webContents.send('agent:event', ev);
5295
5479
  }
5296
5480
 
5481
+ /* Kalıcı kabuğu başlat: cmd /q /k — girdi pipe'ından satır satır okur,
5482
+ prompt yazmaz. chcp 65001 → Türkçe yollar (Masaüstü vb.) doğru çözülür. */
5483
+ function termShellSpawn() {
5484
+ const cwd =
5485
+ termShellCwd ||
5486
+ (engine && engine.workspace) ||
5487
+ settings.workspace ||
5488
+ app.getPath('home');
5489
+ const child = spawn('cmd.exe', ['/q', '/k'], {
5490
+ cwd,
5491
+ windowsHide: true,
5492
+ stdio: ['pipe', 'pipe', 'pipe'],
5493
+ });
5494
+ termShellCwd = cwd;
5495
+ termChild = child;
5496
+ termForwarded = 0;
5497
+ termCapNotified = false;
5498
+ child.stdout.setEncoding('utf8');
5499
+ child.stderr.setEncoding('utf8');
5500
+ child.stdout.on('data', (d) => {
5501
+ termForwarded += String(d).length;
5502
+ if (termForwarded > TERM_FWD_CAP) {
5503
+ if (!termCapNotified) {
5504
+ termCapNotified = true;
5505
+ termSend({ type: 'term-out', id: termShellId, stream: 'out', chunk: '\n[beast] çıktı çok büyük — iletim durduruldu (komut sürüyor)\n' });
5506
+ }
5507
+ return;
5508
+ }
5509
+ termSend({ type: 'term-out', id: termShellId, stream: 'out', chunk: String(d) });
5510
+ });
5511
+ child.stderr.on('data', (d) => {
5512
+ termSend({ type: 'term-out', id: termShellId, stream: 'err', chunk: String(d) });
5513
+ });
5514
+ child.on('exit', () => {
5515
+ if (termChild === child) termChild = null;
5516
+ });
5517
+ /* UTF-8 kod sayfası: Türkçe karakterli klasörler/çıktılar bozulmadan akar.
5518
+ (cmd her stdin satırını bir komut işler — bu ilk satır chcp olur) */
5519
+ try { child.stdin.write('chcp 65001 > nul\r\n'); } catch {}
5520
+ }
5521
+
5297
5522
  ipcMain.handle('terminal:toggle', () => {
5298
5523
  if (browser.open) setBrowserOpen(false);
5299
- return { ok: true, cwd: (engine && engine.workspace) || settings.workspace || app.getPath('home') };
5524
+ return { ok: true, cwd: termShellCwd || (engine && engine.workspace) || settings.workspace || app.getPath('home') };
5300
5525
  });
5301
5526
 
5302
5527
  ipcMain.handle('terminal:run', (_e, payload) => {
5303
5528
  const cmd = String((payload && payload.cmd) || '').trim();
5304
5529
  const shell = String((payload && payload.shell) || 'cmd');
5305
5530
  if (!cmd) return { ok: false, error: 'boş komut' };
5306
- if (termChild) return { ok: false, error: 'önceki komut sürüyor ile durdurabilirsin' };
5307
- const cwd = (engine && engine.workspace) || settings.workspace || app.getPath('home');
5531
+ /* kalıcı CMD: komut AYNI kabuğa yazılır cd/set kalıcıdır; komutlar sıraya girer */
5532
+ if (shell === 'cmd') {
5533
+ if (!termChild || !termChild.stdin.writable) termShellSpawn();
5534
+ if (!termChild) return { ok: false, error: 'kabuk başlatılamadı' };
5535
+ const id = 't' + Date.now().toString(36) + ++termShellSeq;
5536
+ termShellId = id;
5537
+ termForwarded = 0;
5538
+ termCapNotified = false;
5539
+ try {
5540
+ /* İKİ AYRI satır: cmd her satırı SIRAYLA işler — ikinci satırdaki %CD%
5541
+ ancak ilk komut BİTİNCE okunur/genişletilir → doğru (yeni) klasör gelir.
5542
+ Aynı satıra & echo yazsaydık %CD% eski klasörü verirdi. */
5543
+ termChild.stdin.write(cmd + '\r\n' + 'echo ' + TERM_MARKER + '%CD%' + TERM_MARKER + '\r\n');
5544
+ } catch (e) {
5545
+ return { ok: false, error: String((e && e.message) || e) };
5546
+ }
5547
+ return { ok: true, id };
5548
+ }
5549
+ /* geriye dönük uyumluluk — UI artık yalnız CMD gönderir */
5550
+ const cwd = termShellCwd || (engine && engine.workspace) || settings.workspace || app.getPath('home');
5308
5551
  const id = 't' + Date.now().toString(36);
5309
5552
  let file, args;
5310
5553
  if (shell === 'cmd') {
5311
5554
  file = 'cmd.exe';
5312
5555
  args = ['/d', '/s', '/c', cmd];
5313
5556
  } else {
5314
- /* geriye dönük uyumluluk — UI artık yalnız CMD gönderir */
5315
5557
  file = 'powershell.exe';
5316
5558
  args = ['-NoProfile', '-NoLogo', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', cmd];
5317
5559
  }
@@ -5321,38 +5563,29 @@ ipcMain.handle('terminal:run', (_e, payload) => {
5321
5563
  } catch (e) {
5322
5564
  return { ok: false, error: String((e && e.message) || e) };
5323
5565
  }
5324
- termChild = child;
5325
5566
  child.stdout.setEncoding('utf8');
5326
5567
  child.stderr.setEncoding('utf8');
5327
- let forwarded = 0;
5328
- let capNotified = false;
5329
- const push = (stream, d) => {
5330
- if (forwarded > TERM_FWD_CAP) {
5331
- if (!capNotified) {
5332
- capNotified = true;
5333
- termSend({ type: 'term-out', id, stream, chunk: '\n[beast] çıktı çok büyük — iletim durduruldu (komut sürüyor)\n' });
5334
- }
5335
- return;
5336
- }
5337
- forwarded += String(d).length;
5338
- termSend({ type: 'term-out', id, stream, chunk: String(d) });
5339
- };
5340
- child.stdout.on('data', (d) => push('out', d));
5341
- child.stderr.on('data', (d) => push('err', d));
5568
+ child.stdout.on('data', (d) => termSend({ type: 'term-out', id, stream: 'out', chunk: String(d) }));
5569
+ child.stderr.on('data', (d) => termSend({ type: 'term-out', id, stream: 'err', chunk: String(d) }));
5342
5570
  child.on('error', (err) => {
5343
- if (termChild === child) termChild = null;
5344
5571
  termSend({ type: 'term-end', id, code: -1, error: String((err && err.message) || err) });
5345
5572
  });
5346
5573
  child.on('close', (code) => {
5347
- if (termChild === child) termChild = null;
5348
5574
  termSend({ type: 'term-end', id, code: code == null ? -1 : code });
5349
5575
  });
5350
5576
  return { ok: true, id };
5351
5577
  });
5352
5578
 
5353
5579
  ipcMain.handle('terminal:stop', () => {
5354
- if (!termChild) return { ok: false };
5355
- try { spawn('taskkill', ['/pid', String(termChild.pid), '/T', '/F'], { windowsHide: true }); } catch {}
5580
+ /* ■: süren komutu (tüm alt süreçleriyle) kes; KALICI kabuk yenilenir
5581
+ son bilinen klasör korunur, cd geçmişi kaybolmaz */
5582
+ if (termChild) {
5583
+ try { spawn('taskkill', ['/pid', String(termChild.pid), '/T', '/F'], { windowsHide: true }); } catch {}
5584
+ try { termChild.kill(); } catch {}
5585
+ termChild = null;
5586
+ }
5587
+ termSend({ type: 'term-end', id: termShellId, code: 130, error: 'komut durduruldu — kalıcı CMD yeniden hazır' });
5588
+ termShellId = null;
5356
5589
  return { ok: true };
5357
5590
  });
5358
5591
 
@@ -5405,13 +5638,16 @@ function bcPanelEvent(sid, ev) {
5405
5638
  const BC_DEBOUNCE_MS = 900;
5406
5639
  const bcQueue = new Map(); /* klasör yolu → { timer, msgs[] } */
5407
5640
 
5408
- function bcQueuePush(ws, text) {
5641
+ function bcQueuePush(ws, text, attachments) {
5409
5642
  let q = bcQueue.get(ws);
5410
5643
  if (!q) {
5411
5644
  q = { timer: null, msgs: [] };
5412
5645
  bcQueue.set(ws, q);
5413
5646
  }
5414
- q.msgs.push({ text });
5647
+ q.msgs.push({
5648
+ text,
5649
+ attachments: Array.isArray(attachments) && attachments.length ? attachments : undefined,
5650
+ });
5415
5651
  return q;
5416
5652
  }
5417
5653
 
@@ -5421,8 +5657,12 @@ function bcFlush(folder) {
5421
5657
  const s = bcGetSession(folder); /* oturum yoksa oluşturur */
5422
5658
  if (engine.isBusy(s.id)) return; /* hâlâ çalışıyor — done/error eventini bekle */
5423
5659
  let merged = '';
5424
- for (const m of q.msgs) if (m.text) merged += (merged ? '\n' : '') + m.text;
5425
- if (!merged.trim()) {
5660
+ let mergedAtts = null;
5661
+ for (const m of q.msgs) {
5662
+ if (m.text) merged += (merged ? '\n' : '') + m.text;
5663
+ if (!mergedAtts && Array.isArray(m.attachments) && m.attachments.length) mergedAtts = m.attachments;
5664
+ }
5665
+ if (!merged.trim() && !mergedAtts) {
5426
5666
  bcQueue.delete(folder);
5427
5667
  clearTimeout(q.timer);
5428
5668
  return;
@@ -5430,7 +5670,8 @@ function bcFlush(folder) {
5430
5670
  s.workspace = folder;
5431
5671
  s.bcCode = true;
5432
5672
  engine.cache.set(s.id, s);
5433
- if (engine.send(s.id, merged)) {
5673
+ const payload = mergedAtts ? { text: merged, attachments: mergedAtts } : merged;
5674
+ if (engine.send(s.id, payload, { userAction: true })) {
5434
5675
  bcQueue.delete(folder);
5435
5676
  clearTimeout(q.timer);
5436
5677
  } else {
@@ -5461,7 +5702,8 @@ function bcFlushOnDone(ev) {
5461
5702
 
5462
5703
  ipcMain.handle('beastcode:send', async (_e, payload) => {
5463
5704
  const text = String((payload && payload.msg) || '').trim();
5464
- if (!text) return { ok: false, error: 'boş mesaj' };
5705
+ const attachments = Array.isArray(payload && payload.attachments) ? payload.attachments : [];
5706
+ if (!text && !attachments.length) return { ok: false, error: 'boş mesaj' };
5465
5707
  if (!engine) return { ok: false, error: 'ajan hazır değil' };
5466
5708
  const ws = ideRoot();
5467
5709
  const modeM = /^\/(plan|build|auto)\b/i.exec(text);
@@ -5489,18 +5731,36 @@ ipcMain.handle('beastcode:send', async (_e, payload) => {
5489
5731
  }
5490
5732
  if (busy) {
5491
5733
  /* ajan çalışıyor → kuyruğa al; iş bitince (done/error) toplu gider */
5492
- const q = bcQueuePush(ws, text);
5734
+ const q = bcQueuePush(ws, text, attachments);
5493
5735
  return { ok: true, queued: true, count: q.msgs.length, sessionId: s.id };
5494
5736
  }
5495
5737
  /* boşta: kısa pencere — hızlı ard arda mesajlar tek işte birleşir */
5496
- const q = bcQueuePush(ws, text);
5738
+ const q = bcQueuePush(ws, text, attachments);
5497
5739
  clearTimeout(q.timer);
5498
5740
  q.timer = setTimeout(() => { try { bcFlush(ws); } catch {} }, BC_DEBOUNCE_MS);
5499
5741
  return { ok: true, sessionId: s.id, pending: true };
5500
5742
  });
5501
5743
 
5502
- ipcMain.handle('beastcode:stop', async () => {
5503
- const ws = ideRoot();
5744
+ /* BC görev listesi (ID'li) + TEK TUŞ GERİ ALMA: bir görev maddesinin
5745
+ değişiklikleri, madde başlamadan önceki kod tabanına döndürülür */
5746
+ ipcMain.handle('bc:todos', (_e, payload) => {
5747
+ try {
5748
+ return engine.todoUndoInfo(String((payload && payload.sessionId) || ''));
5749
+ } catch (e) {
5750
+ return { ok: false, error: String((e && e.message) || e) };
5751
+ }
5752
+ });
5753
+ ipcMain.handle('bc:undo', (_e, payload) => {
5754
+ try {
5755
+ const sid = String((payload && payload.sessionId) || '');
5756
+ const todoId = String((payload && payload.todoId) || '');
5757
+ return todoId === 'last' ? engine.undoLastTodo(sid) : engine.undoTodo(sid, todoId);
5758
+ } catch (e) {
5759
+ return { ok: false, error: String((e && e.message) || e) };
5760
+ }
5761
+ });
5762
+
5763
+ ipcMain.handle('beastcode:stop', async () => { const ws = ideRoot();
5504
5764
  const sid = bcSessions.get(ws);
5505
5765
  /* durdurma: bekleyen kuyruğu da boşalt (kullanıcı vazgeçti) */
5506
5766
  const q = bcQueue.get(ws);
@@ -5527,6 +5787,7 @@ ipcMain.handle('beastcode:new', async () => {
5527
5787
  }
5528
5788
  if (sid) {
5529
5789
  try { engine.deleteSession(sid); } catch {}
5790
+ bcLastServerUrl = ''; /* yeni oturum — eski dev server adresi geçersiz */
5530
5791
  bcSessions.delete(ws);
5531
5792
  }
5532
5793
  return { ok: true };
@@ -5727,6 +5988,7 @@ ipcMain.handle('ide:setroot', async () => {
5727
5988
  if (r.canceled || !r.filePaths || !r.filePaths[0]) return { ok: false, canceled: true };
5728
5989
  settings.ideRoot = r.filePaths[0];
5729
5990
  saveSettings();
5991
+ ideWatchStart(); // yeni kökte izleme yeniden kurulur
5730
5992
  return { ok: true, root: settings.ideRoot };
5731
5993
  } catch (e) {
5732
5994
  return { ok: false, error: String((e && e.message) || e) };
@@ -5741,6 +6003,56 @@ function ideSafe(rel) {
5741
6003
  return p;
5742
6004
  }
5743
6005
 
6006
+ /* ---------- IDE DOSYA AĞACI CANLI İZLEME ----------
6007
+ Soldaki klasör paneli ELLE yenilemeden güncellensin: workspace kökü
6008
+ fs.watch (recursive) ile izlenir; node_modules/.git gürültüsü elenir,
6009
+ 500ms debounce ile renderer'a 'ide-tree-changed' düşer. Ajan dışında
6010
+ (kullanıcı kaydı, git, harici program) değişen dosyalar da yakalanır. */
6011
+ let ideWatcher = null;
6012
+ let ideWatchTimer = null;
6013
+ let ideWatchRoot = '';
6014
+ function ideWatchStart() {
6015
+ const root = ideRoot();
6016
+ if (ideWatcher && ideWatchRoot === root) return;
6017
+ ideWatchStop();
6018
+ /* ev dizininin KÖKÜNÜ izlemek AppData gürültüsü yüzünden paneli sürekli
6019
+ yeniler — özel klasör seçiliyken (ideRoot/workspace) izleme aktiftir */
6020
+ if (root === app.getPath('home')) return;
6021
+ try {
6022
+ fs.accessSync(root); // kök yoksa izleme kurma
6023
+ } catch {
6024
+ return;
6025
+ }
6026
+ ideWatchRoot = root;
6027
+ try {
6028
+ ideWatcher = fs.watch(root, { recursive: true }, (_evType, fname) => {
6029
+ const f = String(fname || '').replace(/\\/g, '/');
6030
+ if (/^(node_modules|\.git|dist|\.next|\.nuxt)(\/|$)/i.test(f)) return;
6031
+ if (ideWatchTimer) return;
6032
+ ideWatchTimer = setTimeout(() => {
6033
+ ideWatchTimer = null;
6034
+ try {
6035
+ if (win && !win.isDestroyed()) {
6036
+ win.webContents.send('agent:event', { type: 'ide-tree-changed' });
6037
+ }
6038
+ } catch {}
6039
+ }, 500);
6040
+ });
6041
+ ideWatcher.on('error', () => {
6042
+ ideWatchStop();
6043
+ /* kök klasör silinip yeniden yaratıldıysa kısa süre sonra tekrar dene */
6044
+ setTimeout(() => { try { ideWatchStart(); } catch {} }, 3000);
6045
+ });
6046
+ try { log.info('ide', 'ağaç izleme açık: ' + root); } catch {}
6047
+ } catch {}
6048
+ }
6049
+ function ideWatchStop() {
6050
+ if (ideWatcher) { try { ideWatcher.close(); } catch {} }
6051
+ ideWatcher = null;
6052
+ ideWatchRoot = '';
6053
+ if (ideWatchTimer) { clearTimeout(ideWatchTimer); ideWatchTimer = null; }
6054
+ }
6055
+
5744
6056
  ipcMain.handle('ide:tree', (_e, rel) => {
5745
6057
  const p = ideSafe(rel);
5746
6058
  if (!p) return { ok: false, error: 'geçersiz yol' };
@@ -5818,25 +6130,102 @@ ipcMain.handle('ide:delete', async (_e, rel) => {
5818
6130
  });
5819
6131
 
5820
6132
  /* Sağ tık menüsü: HTML dosyasını dahili tarayıcıda GÖRÜNÜR aç */
5821
- ipcMain.handle('ide:previewFile', (_e, rel) => {
6133
+ /* ---------- BC DAHİLİ STATİK SUNUCU ----------
6134
+ Beast Code çıktısı ASLA file:// ile açılmaz: ES modülleri, fetch, ServiceWorker
6135
+ ve "clean URL" yolları file://'da çalışmaz. Her preview http://127.0.0.1 üzerinden
6136
+ servis edilir; ajan kendi dev sunucusunu başlattıysa o adres önceliklidir. */
6137
+ let bcLastServerUrl = ''; /* ajanın başlattığı dev server adresi (bc-preview'dan yakalanır) */
6138
+ const bcStatic = { server: null, root: '', base: '' };
6139
+ const BC_MIME = {
6140
+ '.html': 'text/html; charset=utf-8', '.htm': 'text/html; charset=utf-8',
6141
+ '.js': 'text/javascript; charset=utf-8', '.mjs': 'text/javascript; charset=utf-8',
6142
+ '.css': 'text/css; charset=utf-8', '.json': 'application/json; charset=utf-8',
6143
+ '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif',
6144
+ '.svg': 'image/svg+xml', '.ico': 'image/x-icon', '.webp': 'image/webp', '.avif': 'image/avif',
6145
+ '.woff': 'font/woff', '.woff2': 'font/woff2', '.ttf': 'font/ttf', '.otf': 'font/otf',
6146
+ '.txt': 'text/plain; charset=utf-8', '.md': 'text/plain; charset=utf-8',
6147
+ '.map': 'application/json', '.wasm': 'application/wasm', '.pdf': 'application/pdf',
6148
+ '.mp3': 'audio/mpeg', '.wav': 'audio/wav', '.mp4': 'video/mp4', '.webm': 'video/webm',
6149
+ '.xml': 'application/xml', '.csv': 'text/csv',
6150
+ };
6151
+ function bcStaticHandler(req, res) {
6152
+ try {
6153
+ let rel = decodeURIComponent(String((req && req.url) || '/').split('?')[0]);
6154
+ rel = rel.replace(/^\/+/, '');
6155
+ const fp = path.resolve(bcStatic.root, rel);
6156
+ if (fp !== bcStatic.root && !fp.startsWith(bcStatic.root + path.sep)) {
6157
+ res.writeHead(403); res.end('forbidden'); return;
6158
+ }
6159
+ const isFile = (p) => { try { return fs.statSync(p).isFile() ? p : null; } catch { return null; } };
6160
+ let hit = isFile(fp);
6161
+ if (!hit) hit = isFile(path.join(fp, 'index.html')); /* dizin → index.html */
6162
+ if (!hit) hit = isFile(fp + '.html'); /* clean URL: /hakkinda → hakkinda.html */
6163
+ if (!hit) { res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' }); res.end('404 — /' + rel); return; }
6164
+ const ext = path.extname(hit).toLowerCase();
6165
+ res.writeHead(200, { 'Content-Type': BC_MIME[ext] || 'application/octet-stream', 'Cache-Control': 'no-store' });
6166
+ fs.createReadStream(hit).pipe(res);
6167
+ } catch (e) {
6168
+ try { res.writeHead(500); res.end('server error'); } catch {}
6169
+ }
6170
+ }
6171
+ function bcStaticStart(root) {
6172
+ return new Promise((resolve) => {
6173
+ const r = path.resolve(String(root || ''));
6174
+ if (bcStatic.server && bcStatic.root === r) { resolve(bcStatic.base); return; }
6175
+ if (bcStatic.server) { try { bcStatic.server.close(); } catch {} bcStatic.server = null; }
6176
+ const srv = http.createServer(bcStaticHandler);
6177
+ srv.on('error', () => resolve(''));
6178
+ srv.listen(0, '127.0.0.1', () => {
6179
+ bcStatic.server = srv;
6180
+ bcStatic.root = r;
6181
+ bcStatic.base = 'http://127.0.0.1:' + srv.address().port;
6182
+ try { log.info('bc', 'statik sunucu: ' + bcStatic.base + ' → ' + r); } catch {}
6183
+ resolve(bcStatic.base);
6184
+ });
6185
+ });
6186
+ }
6187
+ /* Preview'a basılınca BC oturumuna SESSİZ bağlam enjeksiyonu (observe — tur
6188
+ AÇMAZ, maliyet yok): ajan dahili sunucunun çalıştığını bilir ve statik
6189
+ dosyalar için kendi sunucusunu başlatma denemez → çakışma biter */
6190
+ function bcTellServe(staticBase) {
6191
+ try {
6192
+ const ws = ideRoot();
6193
+ const sid = bcSessions.get(ws);
6194
+ if (!sid) return;
6195
+ engine.observe(sid,
6196
+ '[PREVIEW] Kullanıcı önizlemeyi açtı — DAHİLİ STATİK SUNUCU şu adreste ÇALIŞIYOR: ' + staticBase + '\n' +
6197
+ 'Statik dosyalar için KENDİ sunucunu BAŞLATMA; üretilen siteyi bu adres üzerinden değerlendir.\n' +
6198
+ 'Yalnızca gerçek dev-server/build gerekiyorsa (React/Vite/Next/Expo: npm run dev, expo start) ' +
6199
+ 'kendi sunucunu BLOKLAMADAN arka planda başlat ve çalışan adresi yaz.'
6200
+ );
6201
+ } catch {}
6202
+ }
6203
+
6204
+ ipcMain.handle('ide:previewFile', async (_e, rel) => {
5822
6205
  try {
5823
6206
  const p = ideSafe(rel);
5824
6207
  if (!p) return { ok: false, error: 'geçersiz yol' };
5825
6208
  if (!/\.html?$/i.test(p)) return { ok: false, error: 'önizleme yalnız .html/.htm dosyaları için' };
6209
+ const base = await bcStaticStart(ideRoot());
6210
+ if (!base) return { ok: false, error: 'statik sunucu başlatılamadı' };
6211
+ const root = ideRoot();
6212
+ const relPath = path.relative(root, p).replace(/\\/g, '/');
6213
+ const url = base + '/' + relPath;
5826
6214
  setBrowserOpen(true, true);
5827
- const url = 'file:///' + p.replace(/\\/g, '/');
5828
6215
  browser.view.webContents.loadURL(url).catch(() => {});
5829
6216
  browserEmit({ open: true, width: browser.width, url });
5830
- return { ok: true };
6217
+ return { ok: true, url };
5831
6218
  } catch (e) {
5832
6219
  return { ok: false, error: String((e && e.message) || e) };
5833
6220
  }
5834
6221
  });
5835
6222
 
5836
- /* PREVIEW: workspace kökündeki entry sayfayı (index.html ilk *.html) sağdaki
5837
- dahili tarayıcıda aç. file:// yükleme browserNavigate'i BYPASS eder o https
5838
- olmayan adresi aramaya çevirir. */
5839
- ipcMain.handle('ide:preview', () => { try {
6223
+ /* PREVIEW: workspace kökündeki siteyi DAİMA sunucudan
6224
+ 1) ajan bir dev server başlattıysa onun adresi, 2) yoksa dahili statik sunucu.
6225
+ file:// ASLA kullanılmaz (JS/clean URL kırılmaları). Ayrıca ajana SUNUCU
6226
+ komutu düşer: uygulama sunucu istiyorsa kendisi başlatıp adresi yazar. */
6227
+ ipcMain.handle('ide:preview', async () => {
6228
+ try {
5840
6229
  const root = ideRoot();
5841
6230
  const pick = (name) => {
5842
6231
  const p = path.join(root, name);
@@ -5851,12 +6240,19 @@ ipcMain.handle('ide:preview', () => { try {
5851
6240
  entry = htmls.length ? path.join(root, htmls.sort()[0]) : null;
5852
6241
  }
5853
6242
  if (!entry) return { ok: false, error: 'workspace kökünde index.html yok — önce agent\'a siteyi yazdır' };
5854
- /* forceVisible: preview'a basınca tarayıcı ikonuna basmaya gerek kalmasın
5855
- dahili tarayıcı otomatik ve GÖRÜNÜR açılır */
6243
+ /* forceVisible: preview'a basınca tarayıcı ikonuna basmaya gerek kalmasın */
5856
6244
  setBrowserOpen(true, true);
5857
- const url = 'file:///' + entry.replace(/\\/g, '/');
6245
+ let url = '';
6246
+ if (bcLastServerUrl) {
6247
+ url = bcLastServerUrl; /* ajanın kendi dev sunucusu öncelikli */
6248
+ } else {
6249
+ const base = await bcStaticStart(root);
6250
+ if (!base) return { ok: false, error: 'statik sunucu başlatılamadı' };
6251
+ url = base + '/';
6252
+ }
5858
6253
  browser.view.webContents.loadURL(url).catch(() => {});
5859
6254
  browserEmit({ open: true, width: browser.width, url });
6255
+ if (!bcLastServerUrl) bcTellServe(url.replace(/\/$/, ''));
5860
6256
  return { ok: true, url };
5861
6257
  } catch (e) {
5862
6258
  return { ok: false, error: String((e && e.message) || e) };
@@ -5864,20 +6260,44 @@ ipcMain.handle('ide:preview', () => { try {
5864
6260
  });
5865
6261
 
5866
6262
  /* BEAST CODE otomatik canlı önizleme: iş bitince üretilen site/app dahili
5867
- tarayıcıda GÖRÜNÜR açılır. Yalnız localhost + file:// kabul edilir —
5868
- dış adresler güvenlik için reddedilir. */
5869
- ipcMain.handle('ide:previewUrl', (_e, url) => {
6263
+ tarayıcıda GÖRÜNÜR açılır. Yalnız localhost kabul edilir; file:// gelen
6264
+ eser adresi DAHİLİ STATİK SUNUCUYA çevrilir — dosyadan ASLA açılmaz. */
6265
+ ipcMain.handle('ide:previewUrl', async (_e, url) => {
5870
6266
  try {
5871
6267
  const u = String(url || '');
5872
- const okUrl =
5873
- /^https?:\/\/(?:localhost|127\.0\.0\.1)(?::\d{2,5})?(?:\/|$)/i.test(u) ||
5874
- /^file:\/\/\//i.test(u);
5875
- if (!okUrl) return { ok: false, error: 'yalnız localhost/file:// adresleri önizlenebilir' };
5876
- /* forceVisible: preview'a basmaya gerek kalmasın — otomatik ve GÖRÜNÜR açılır */
6268
+ let target = '';
6269
+ if (/^https?:\/\/(?:localhost|127\.0\.0\.1)(?::\d{2,5})?(?:\/|$)/i.test(u)) {
6270
+ target = u;
6271
+ } else if (/^file:\/\/\//i.test(u)) {
6272
+ const raw = decodeURIComponent(u.replace(/^file:\/\/\//, '')).replace(/\/$/, '');
6273
+ const fp = path.resolve(raw);
6274
+ const root = ideRoot();
6275
+ const rel = path.relative(root, fp).replace(/\\/g, '/');
6276
+ let base;
6277
+ if (rel && !rel.startsWith('..')) {
6278
+ base = await bcStaticStart(root);
6279
+ if (!base) return { ok: false, error: 'statik sunucu başlatılamadı' };
6280
+ target = base + '/' + rel;
6281
+ } else {
6282
+ /* workspace dışı eser — dosyanın kendi klasörü kök alınır */
6283
+ base = await bcStaticStart(path.dirname(fp));
6284
+ if (!base) return { ok: false, error: 'statik sunucu başlatılamadı' };
6285
+ target = base + '/' + path.basename(fp);
6286
+ }
6287
+ } else {
6288
+ return { ok: false, error: 'yalnız localhost adresleri önizlenebilir' };
6289
+ }
6290
+ /* Expo/Metro dev sunucusu → telefon modu OTOMATİK (mobil uygulama canlı önizleme) */
6291
+ if (/^https?:\/\/(?:localhost|127\.0\.0\.1):(8081|19000|19001|19002|3000|5173)\//i.test(target)) {
6292
+ if (!browser.phone) setBrowserPhone(true);
6293
+ }
6294
+ /* forceVisible: otomatik ve GÖRÜNÜR açılır */
5877
6295
  setBrowserOpen(true, true);
5878
- browser.view.webContents.loadURL(u).catch(() => {});
5879
- browserEmit({ open: true, width: browser.width, url: u });
5880
- return { ok: true, url: u };
6296
+ browser.view.webContents.loadURL(target).catch(() => {});
6297
+ let wNow = 0;
6298
+ try { wNow = win.getContentSize()[0]; } catch {}
6299
+ browserEmit({ open: true, width: browserShownWidth(wNow), url: target });
6300
+ return { ok: true, url: target };
5881
6301
  } catch (e) {
5882
6302
  return { ok: false, error: String((e && e.message) || e) };
5883
6303
  }