beast-agent 1.4.2 → 1.5.0

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();
@@ -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 = {};
@@ -5121,6 +5203,57 @@ function cronEmit() {
5121
5203
  }
5122
5204
  }
5123
5205
 
5206
+ /* Cron cevabı bekleme haritası: sid -> job. İş 'done' olunca cevap
5207
+ SAHİBİN bağlı olduğu TÜM AKTİF entegrasyonlara yansıtılır (kullanıcı
5208
+ hangi kanaldan ajanla iletişimde belli değil). */
5209
+ const cronAnswerPending = new Map();
5210
+
5211
+ /* Yansıtma hedefleri: bağlı WA (owner numarası), Telegram ve Discord
5212
+ (owner işaretli kayıt; tek kayıt varsa o). Cron oturumunun KENDİ
5213
+ kanalına yansıtmayız — cevabı zaten kendi akışından alır (çift yok). */
5214
+ function tgOwnerIds() {
5215
+ const list = settings.tgAllow || [];
5216
+ const objs = list.filter((e) => e && typeof e === 'object' && e.id && e.id !== '*');
5217
+ const owner = objs.find((e) => e.owner) || (objs.length === 1 ? objs[0] : null);
5218
+ return owner ? [String(owner.id)] : [];
5219
+ }
5220
+ function dcOwnerIds() {
5221
+ const list = settings.dcAllow || [];
5222
+ const objs = list.filter((e) => e && typeof e === 'object' && e.id && e.id !== '*');
5223
+ const owner = objs.find((e) => e.owner) || (objs.length === 1 ? objs[0] : null);
5224
+ return owner ? [String(owner.id)] : [];
5225
+ }
5226
+ function cronMirrorTargets(cronSid) {
5227
+ const out = [];
5228
+ try {
5229
+ if (wa && wa.connected) {
5230
+ const own = waOwnerNum();
5231
+ if (own) {
5232
+ const jid = own + '@s.whatsapp.net';
5233
+ const bound = [...waChats.entries()].some(([j, s]) => j === jid && String(s) === String(cronSid));
5234
+ if (!bound) out.push({ kind: 'wa', send: (t) => sendWaSafe(jid, t) });
5235
+ }
5236
+ }
5237
+ } catch {}
5238
+ try {
5239
+ if (tg && tg.connected) {
5240
+ for (const id of tgOwnerIds()) {
5241
+ const bound = [...tgChats.entries()].some(([c, s]) => String(c) === String(id) && String(s) === String(cronSid));
5242
+ if (!bound) out.push({ kind: 'tg', send: (t) => sendTgSafe(id, t) });
5243
+ }
5244
+ }
5245
+ } catch {}
5246
+ try {
5247
+ if (dc && dc.connected) {
5248
+ for (const id of dcOwnerIds()) {
5249
+ const bound = [...dcChats.entries()].some(([c, s]) => String(c) === String(id) && String(s) === String(cronSid));
5250
+ if (!bound) out.push({ kind: 'dc', send: (t) => sendDcSafe(id, t) });
5251
+ }
5252
+ }
5253
+ } catch {}
5254
+ return out;
5255
+ }
5256
+
5124
5257
  function cronFire(job) {
5125
5258
  try {
5126
5259
  let sid = job.sessionId;
@@ -5130,9 +5263,11 @@ function cronFire(job) {
5130
5263
  sid = s.id;
5131
5264
  cron.update(job.id, { sessionId: sid });
5132
5265
  }
5133
- engine.send(sid, {
5266
+ cronAnswerPending.set(String(sid), job);
5267
+ const sent = engine.send(sid, {
5134
5268
  text: `[cron: ${job.name}]\n${job.prompt}`,
5135
5269
  });
5270
+ if (!sent) cronAnswerPending.delete(String(sid)); // gönderilemedi — bayat bekleme bırakma
5136
5271
  } catch {}
5137
5272
  cronEmit();
5138
5273
  }
@@ -5274,9 +5409,35 @@ ipcMain.handle('browser:setWidth', (_e, wpx) => {
5274
5409
  settings.browserWidth = browser.width;
5275
5410
  saveSettings();
5276
5411
  layoutBrowser();
5277
- browserEmit({ open: true, width: browser.width });
5412
+ browserEmit({ open: true, width: browserShownWidth(w) });
5278
5413
  return { ok: true, width: browser.width };
5279
5414
  });
5415
+
5416
+ /* TELEFON MODU: mobil UA + dar dock (≈430px) → aynı sitenin mobil versiyonu.
5417
+ Kapatınca masaüstü UA + kayıtlı genişlik geri gelir. UA değişimi için sayfa
5418
+ taze yüklenir. Expo/Metro dev sunucularında otomatik açılır. */
5419
+ function setBrowserPhone(on) {
5420
+ const want = !!on;
5421
+ if (browser.phone === want) return { ok: true, phone: want };
5422
+ browser.phone = want;
5423
+ try {
5424
+ const wc = browser.view && browser.view.webContents;
5425
+ if (wc) {
5426
+ wc.setUserAgent(browser.phone ? PHONE_UA : browser.desktopUA || PHONE_UA);
5427
+ let url = '';
5428
+ try { url = wc.getURL(); } catch {}
5429
+ if (url && /^https?:/i.test(url)) wc.loadURL(url).catch(() => {});
5430
+ }
5431
+ } catch {}
5432
+ applyPhoneEmulation(); // açık: 390x844 cihaz ortalanır — kapalı: emülasyon kapanır
5433
+ if (browser.open && win && !win.isDestroyed()) {
5434
+ layoutBrowser();
5435
+ const [w] = win.getContentSize();
5436
+ browserEmit({ open: true, width: browserShownWidth(w) });
5437
+ }
5438
+ return { ok: true, phone: browser.phone };
5439
+ }
5440
+ ipcMain.handle('browser:phone', (_e, on) => setBrowserPhone(on));
5280
5441
  ipcMain.handle('browser:screenshot', async () => {
5281
5442
  const r = await browserScreenshot();
5282
5443
  if (!r.ok) return { ok: false, error: r.error };
@@ -5287,31 +5448,95 @@ ipcMain.handle('browser:screenshot', async () => {
5287
5448
  Sağ dock paneli renderer'da; main sadece PowerShell komutlarını
5288
5449
  çalıştırır ve çıktıyı canlı akıtır. Terminal ile tarayıcı aynı
5289
5450
  dock'u paylaştığı için ikisi aynı anda açık kalamaz. */
5290
- let termChild = null;
5451
+ let termChild = null; /* KALICI CMD oturumu — cd/set değişkenleri komutlar arasında KORUNUR */
5452
+ let termShellCwd = ''; /* izlenen çalışma klasörü (komut sonundaki marker'dan) */
5453
+ let termShellId = null;
5454
+ let termShellSeq = 0;
5455
+ let termForwarded = 0;
5456
+ let termCapNotified = false;
5291
5457
  const TERM_FWD_CAP = 1024 * 1024; /* iletilecek çıktı üst sınırı (1 MB) */
5458
+ const TERM_MARKER = '__BEAST_EOF__';
5292
5459
 
5293
5460
  function termSend(ev) {
5294
5461
  if (win && !win.isDestroyed()) win.webContents.send('agent:event', ev);
5295
5462
  }
5296
5463
 
5464
+ /* Kalıcı kabuğu başlat: cmd /q /k — girdi pipe'ından satır satır okur,
5465
+ prompt yazmaz. chcp 65001 → Türkçe yollar (Masaüstü vb.) doğru çözülür. */
5466
+ function termShellSpawn() {
5467
+ const cwd =
5468
+ termShellCwd ||
5469
+ (engine && engine.workspace) ||
5470
+ settings.workspace ||
5471
+ app.getPath('home');
5472
+ const child = spawn('cmd.exe', ['/q', '/k'], {
5473
+ cwd,
5474
+ windowsHide: true,
5475
+ stdio: ['pipe', 'pipe', 'pipe'],
5476
+ });
5477
+ termShellCwd = cwd;
5478
+ termChild = child;
5479
+ termForwarded = 0;
5480
+ termCapNotified = false;
5481
+ child.stdout.setEncoding('utf8');
5482
+ child.stderr.setEncoding('utf8');
5483
+ child.stdout.on('data', (d) => {
5484
+ termForwarded += String(d).length;
5485
+ if (termForwarded > TERM_FWD_CAP) {
5486
+ if (!termCapNotified) {
5487
+ termCapNotified = true;
5488
+ termSend({ type: 'term-out', id: termShellId, stream: 'out', chunk: '\n[beast] çıktı çok büyük — iletim durduruldu (komut sürüyor)\n' });
5489
+ }
5490
+ return;
5491
+ }
5492
+ termSend({ type: 'term-out', id: termShellId, stream: 'out', chunk: String(d) });
5493
+ });
5494
+ child.stderr.on('data', (d) => {
5495
+ termSend({ type: 'term-out', id: termShellId, stream: 'err', chunk: String(d) });
5496
+ });
5497
+ child.on('exit', () => {
5498
+ if (termChild === child) termChild = null;
5499
+ });
5500
+ /* UTF-8 kod sayfası: Türkçe karakterli klasörler/çıktılar bozulmadan akar.
5501
+ (cmd her stdin satırını bir komut işler — bu ilk satır chcp olur) */
5502
+ try { child.stdin.write('chcp 65001 > nul\r\n'); } catch {}
5503
+ }
5504
+
5297
5505
  ipcMain.handle('terminal:toggle', () => {
5298
5506
  if (browser.open) setBrowserOpen(false);
5299
- return { ok: true, cwd: (engine && engine.workspace) || settings.workspace || app.getPath('home') };
5507
+ return { ok: true, cwd: termShellCwd || (engine && engine.workspace) || settings.workspace || app.getPath('home') };
5300
5508
  });
5301
5509
 
5302
5510
  ipcMain.handle('terminal:run', (_e, payload) => {
5303
5511
  const cmd = String((payload && payload.cmd) || '').trim();
5304
5512
  const shell = String((payload && payload.shell) || 'cmd');
5305
5513
  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');
5514
+ /* kalıcı CMD: komut AYNI kabuğa yazılır cd/set kalıcıdır; komutlar sıraya girer */
5515
+ if (shell === 'cmd') {
5516
+ if (!termChild || !termChild.stdin.writable) termShellSpawn();
5517
+ if (!termChild) return { ok: false, error: 'kabuk başlatılamadı' };
5518
+ const id = 't' + Date.now().toString(36) + ++termShellSeq;
5519
+ termShellId = id;
5520
+ termForwarded = 0;
5521
+ termCapNotified = false;
5522
+ try {
5523
+ /* İKİ AYRI satır: cmd her satırı SIRAYLA işler — ikinci satırdaki %CD%
5524
+ ancak ilk komut BİTİNCE okunur/genişletilir → doğru (yeni) klasör gelir.
5525
+ Aynı satıra & echo yazsaydık %CD% eski klasörü verirdi. */
5526
+ termChild.stdin.write(cmd + '\r\n' + 'echo ' + TERM_MARKER + '%CD%' + TERM_MARKER + '\r\n');
5527
+ } catch (e) {
5528
+ return { ok: false, error: String((e && e.message) || e) };
5529
+ }
5530
+ return { ok: true, id };
5531
+ }
5532
+ /* geriye dönük uyumluluk — UI artık yalnız CMD gönderir */
5533
+ const cwd = termShellCwd || (engine && engine.workspace) || settings.workspace || app.getPath('home');
5308
5534
  const id = 't' + Date.now().toString(36);
5309
5535
  let file, args;
5310
5536
  if (shell === 'cmd') {
5311
5537
  file = 'cmd.exe';
5312
5538
  args = ['/d', '/s', '/c', cmd];
5313
5539
  } else {
5314
- /* geriye dönük uyumluluk — UI artık yalnız CMD gönderir */
5315
5540
  file = 'powershell.exe';
5316
5541
  args = ['-NoProfile', '-NoLogo', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', cmd];
5317
5542
  }
@@ -5321,38 +5546,29 @@ ipcMain.handle('terminal:run', (_e, payload) => {
5321
5546
  } catch (e) {
5322
5547
  return { ok: false, error: String((e && e.message) || e) };
5323
5548
  }
5324
- termChild = child;
5325
5549
  child.stdout.setEncoding('utf8');
5326
5550
  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));
5551
+ child.stdout.on('data', (d) => termSend({ type: 'term-out', id, stream: 'out', chunk: String(d) }));
5552
+ child.stderr.on('data', (d) => termSend({ type: 'term-out', id, stream: 'err', chunk: String(d) }));
5342
5553
  child.on('error', (err) => {
5343
- if (termChild === child) termChild = null;
5344
5554
  termSend({ type: 'term-end', id, code: -1, error: String((err && err.message) || err) });
5345
5555
  });
5346
5556
  child.on('close', (code) => {
5347
- if (termChild === child) termChild = null;
5348
5557
  termSend({ type: 'term-end', id, code: code == null ? -1 : code });
5349
5558
  });
5350
5559
  return { ok: true, id };
5351
5560
  });
5352
5561
 
5353
5562
  ipcMain.handle('terminal:stop', () => {
5354
- if (!termChild) return { ok: false };
5355
- try { spawn('taskkill', ['/pid', String(termChild.pid), '/T', '/F'], { windowsHide: true }); } catch {}
5563
+ /* ■: süren komutu (tüm alt süreçleriyle) kes; KALICI kabuk yenilenir
5564
+ son bilinen klasör korunur, cd geçmişi kaybolmaz */
5565
+ if (termChild) {
5566
+ try { spawn('taskkill', ['/pid', String(termChild.pid), '/T', '/F'], { windowsHide: true }); } catch {}
5567
+ try { termChild.kill(); } catch {}
5568
+ termChild = null;
5569
+ }
5570
+ termSend({ type: 'term-end', id: termShellId, code: 130, error: 'komut durduruldu — kalıcı CMD yeniden hazır' });
5571
+ termShellId = null;
5356
5572
  return { ok: true };
5357
5573
  });
5358
5574
 
@@ -5405,13 +5621,16 @@ function bcPanelEvent(sid, ev) {
5405
5621
  const BC_DEBOUNCE_MS = 900;
5406
5622
  const bcQueue = new Map(); /* klasör yolu → { timer, msgs[] } */
5407
5623
 
5408
- function bcQueuePush(ws, text) {
5624
+ function bcQueuePush(ws, text, attachments) {
5409
5625
  let q = bcQueue.get(ws);
5410
5626
  if (!q) {
5411
5627
  q = { timer: null, msgs: [] };
5412
5628
  bcQueue.set(ws, q);
5413
5629
  }
5414
- q.msgs.push({ text });
5630
+ q.msgs.push({
5631
+ text,
5632
+ attachments: Array.isArray(attachments) && attachments.length ? attachments : undefined,
5633
+ });
5415
5634
  return q;
5416
5635
  }
5417
5636
 
@@ -5421,8 +5640,12 @@ function bcFlush(folder) {
5421
5640
  const s = bcGetSession(folder); /* oturum yoksa oluşturur */
5422
5641
  if (engine.isBusy(s.id)) return; /* hâlâ çalışıyor — done/error eventini bekle */
5423
5642
  let merged = '';
5424
- for (const m of q.msgs) if (m.text) merged += (merged ? '\n' : '') + m.text;
5425
- if (!merged.trim()) {
5643
+ let mergedAtts = null;
5644
+ for (const m of q.msgs) {
5645
+ if (m.text) merged += (merged ? '\n' : '') + m.text;
5646
+ if (!mergedAtts && Array.isArray(m.attachments) && m.attachments.length) mergedAtts = m.attachments;
5647
+ }
5648
+ if (!merged.trim() && !mergedAtts) {
5426
5649
  bcQueue.delete(folder);
5427
5650
  clearTimeout(q.timer);
5428
5651
  return;
@@ -5430,7 +5653,8 @@ function bcFlush(folder) {
5430
5653
  s.workspace = folder;
5431
5654
  s.bcCode = true;
5432
5655
  engine.cache.set(s.id, s);
5433
- if (engine.send(s.id, merged)) {
5656
+ const payload = mergedAtts ? { text: merged, attachments: mergedAtts } : merged;
5657
+ if (engine.send(s.id, payload, { userAction: true })) {
5434
5658
  bcQueue.delete(folder);
5435
5659
  clearTimeout(q.timer);
5436
5660
  } else {
@@ -5461,7 +5685,8 @@ function bcFlushOnDone(ev) {
5461
5685
 
5462
5686
  ipcMain.handle('beastcode:send', async (_e, payload) => {
5463
5687
  const text = String((payload && payload.msg) || '').trim();
5464
- if (!text) return { ok: false, error: 'boş mesaj' };
5688
+ const attachments = Array.isArray(payload && payload.attachments) ? payload.attachments : [];
5689
+ if (!text && !attachments.length) return { ok: false, error: 'boş mesaj' };
5465
5690
  if (!engine) return { ok: false, error: 'ajan hazır değil' };
5466
5691
  const ws = ideRoot();
5467
5692
  const modeM = /^\/(plan|build|auto)\b/i.exec(text);
@@ -5489,18 +5714,36 @@ ipcMain.handle('beastcode:send', async (_e, payload) => {
5489
5714
  }
5490
5715
  if (busy) {
5491
5716
  /* ajan çalışıyor → kuyruğa al; iş bitince (done/error) toplu gider */
5492
- const q = bcQueuePush(ws, text);
5717
+ const q = bcQueuePush(ws, text, attachments);
5493
5718
  return { ok: true, queued: true, count: q.msgs.length, sessionId: s.id };
5494
5719
  }
5495
5720
  /* boşta: kısa pencere — hızlı ard arda mesajlar tek işte birleşir */
5496
- const q = bcQueuePush(ws, text);
5721
+ const q = bcQueuePush(ws, text, attachments);
5497
5722
  clearTimeout(q.timer);
5498
5723
  q.timer = setTimeout(() => { try { bcFlush(ws); } catch {} }, BC_DEBOUNCE_MS);
5499
5724
  return { ok: true, sessionId: s.id, pending: true };
5500
5725
  });
5501
5726
 
5502
- ipcMain.handle('beastcode:stop', async () => {
5503
- const ws = ideRoot();
5727
+ /* BC görev listesi (ID'li) + TEK TUŞ GERİ ALMA: bir görev maddesinin
5728
+ değişiklikleri, madde başlamadan önceki kod tabanına döndürülür */
5729
+ ipcMain.handle('bc:todos', (_e, payload) => {
5730
+ try {
5731
+ return engine.todoUndoInfo(String((payload && payload.sessionId) || ''));
5732
+ } catch (e) {
5733
+ return { ok: false, error: String((e && e.message) || e) };
5734
+ }
5735
+ });
5736
+ ipcMain.handle('bc:undo', (_e, payload) => {
5737
+ try {
5738
+ const sid = String((payload && payload.sessionId) || '');
5739
+ const todoId = String((payload && payload.todoId) || '');
5740
+ return todoId === 'last' ? engine.undoLastTodo(sid) : engine.undoTodo(sid, todoId);
5741
+ } catch (e) {
5742
+ return { ok: false, error: String((e && e.message) || e) };
5743
+ }
5744
+ });
5745
+
5746
+ ipcMain.handle('beastcode:stop', async () => { const ws = ideRoot();
5504
5747
  const sid = bcSessions.get(ws);
5505
5748
  /* durdurma: bekleyen kuyruğu da boşalt (kullanıcı vazgeçti) */
5506
5749
  const q = bcQueue.get(ws);
@@ -5527,6 +5770,7 @@ ipcMain.handle('beastcode:new', async () => {
5527
5770
  }
5528
5771
  if (sid) {
5529
5772
  try { engine.deleteSession(sid); } catch {}
5773
+ bcLastServerUrl = ''; /* yeni oturum — eski dev server adresi geçersiz */
5530
5774
  bcSessions.delete(ws);
5531
5775
  }
5532
5776
  return { ok: true };
@@ -5727,6 +5971,7 @@ ipcMain.handle('ide:setroot', async () => {
5727
5971
  if (r.canceled || !r.filePaths || !r.filePaths[0]) return { ok: false, canceled: true };
5728
5972
  settings.ideRoot = r.filePaths[0];
5729
5973
  saveSettings();
5974
+ ideWatchStart(); // yeni kökte izleme yeniden kurulur
5730
5975
  return { ok: true, root: settings.ideRoot };
5731
5976
  } catch (e) {
5732
5977
  return { ok: false, error: String((e && e.message) || e) };
@@ -5741,6 +5986,56 @@ function ideSafe(rel) {
5741
5986
  return p;
5742
5987
  }
5743
5988
 
5989
+ /* ---------- IDE DOSYA AĞACI CANLI İZLEME ----------
5990
+ Soldaki klasör paneli ELLE yenilemeden güncellensin: workspace kökü
5991
+ fs.watch (recursive) ile izlenir; node_modules/.git gürültüsü elenir,
5992
+ 500ms debounce ile renderer'a 'ide-tree-changed' düşer. Ajan dışında
5993
+ (kullanıcı kaydı, git, harici program) değişen dosyalar da yakalanır. */
5994
+ let ideWatcher = null;
5995
+ let ideWatchTimer = null;
5996
+ let ideWatchRoot = '';
5997
+ function ideWatchStart() {
5998
+ const root = ideRoot();
5999
+ if (ideWatcher && ideWatchRoot === root) return;
6000
+ ideWatchStop();
6001
+ /* ev dizininin KÖKÜNÜ izlemek AppData gürültüsü yüzünden paneli sürekli
6002
+ yeniler — özel klasör seçiliyken (ideRoot/workspace) izleme aktiftir */
6003
+ if (root === app.getPath('home')) return;
6004
+ try {
6005
+ fs.accessSync(root); // kök yoksa izleme kurma
6006
+ } catch {
6007
+ return;
6008
+ }
6009
+ ideWatchRoot = root;
6010
+ try {
6011
+ ideWatcher = fs.watch(root, { recursive: true }, (_evType, fname) => {
6012
+ const f = String(fname || '').replace(/\\/g, '/');
6013
+ if (/^(node_modules|\.git|dist|\.next|\.nuxt)(\/|$)/i.test(f)) return;
6014
+ if (ideWatchTimer) return;
6015
+ ideWatchTimer = setTimeout(() => {
6016
+ ideWatchTimer = null;
6017
+ try {
6018
+ if (win && !win.isDestroyed()) {
6019
+ win.webContents.send('agent:event', { type: 'ide-tree-changed' });
6020
+ }
6021
+ } catch {}
6022
+ }, 500);
6023
+ });
6024
+ ideWatcher.on('error', () => {
6025
+ ideWatchStop();
6026
+ /* kök klasör silinip yeniden yaratıldıysa kısa süre sonra tekrar dene */
6027
+ setTimeout(() => { try { ideWatchStart(); } catch {} }, 3000);
6028
+ });
6029
+ try { log.info('ide', 'ağaç izleme açık: ' + root); } catch {}
6030
+ } catch {}
6031
+ }
6032
+ function ideWatchStop() {
6033
+ if (ideWatcher) { try { ideWatcher.close(); } catch {} }
6034
+ ideWatcher = null;
6035
+ ideWatchRoot = '';
6036
+ if (ideWatchTimer) { clearTimeout(ideWatchTimer); ideWatchTimer = null; }
6037
+ }
6038
+
5744
6039
  ipcMain.handle('ide:tree', (_e, rel) => {
5745
6040
  const p = ideSafe(rel);
5746
6041
  if (!p) return { ok: false, error: 'geçersiz yol' };
@@ -5818,25 +6113,102 @@ ipcMain.handle('ide:delete', async (_e, rel) => {
5818
6113
  });
5819
6114
 
5820
6115
  /* Sağ tık menüsü: HTML dosyasını dahili tarayıcıda GÖRÜNÜR aç */
5821
- ipcMain.handle('ide:previewFile', (_e, rel) => {
6116
+ /* ---------- BC DAHİLİ STATİK SUNUCU ----------
6117
+ Beast Code çıktısı ASLA file:// ile açılmaz: ES modülleri, fetch, ServiceWorker
6118
+ ve "clean URL" yolları file://'da çalışmaz. Her preview http://127.0.0.1 üzerinden
6119
+ servis edilir; ajan kendi dev sunucusunu başlattıysa o adres önceliklidir. */
6120
+ let bcLastServerUrl = ''; /* ajanın başlattığı dev server adresi (bc-preview'dan yakalanır) */
6121
+ const bcStatic = { server: null, root: '', base: '' };
6122
+ const BC_MIME = {
6123
+ '.html': 'text/html; charset=utf-8', '.htm': 'text/html; charset=utf-8',
6124
+ '.js': 'text/javascript; charset=utf-8', '.mjs': 'text/javascript; charset=utf-8',
6125
+ '.css': 'text/css; charset=utf-8', '.json': 'application/json; charset=utf-8',
6126
+ '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif',
6127
+ '.svg': 'image/svg+xml', '.ico': 'image/x-icon', '.webp': 'image/webp', '.avif': 'image/avif',
6128
+ '.woff': 'font/woff', '.woff2': 'font/woff2', '.ttf': 'font/ttf', '.otf': 'font/otf',
6129
+ '.txt': 'text/plain; charset=utf-8', '.md': 'text/plain; charset=utf-8',
6130
+ '.map': 'application/json', '.wasm': 'application/wasm', '.pdf': 'application/pdf',
6131
+ '.mp3': 'audio/mpeg', '.wav': 'audio/wav', '.mp4': 'video/mp4', '.webm': 'video/webm',
6132
+ '.xml': 'application/xml', '.csv': 'text/csv',
6133
+ };
6134
+ function bcStaticHandler(req, res) {
6135
+ try {
6136
+ let rel = decodeURIComponent(String((req && req.url) || '/').split('?')[0]);
6137
+ rel = rel.replace(/^\/+/, '');
6138
+ const fp = path.resolve(bcStatic.root, rel);
6139
+ if (fp !== bcStatic.root && !fp.startsWith(bcStatic.root + path.sep)) {
6140
+ res.writeHead(403); res.end('forbidden'); return;
6141
+ }
6142
+ const isFile = (p) => { try { return fs.statSync(p).isFile() ? p : null; } catch { return null; } };
6143
+ let hit = isFile(fp);
6144
+ if (!hit) hit = isFile(path.join(fp, 'index.html')); /* dizin → index.html */
6145
+ if (!hit) hit = isFile(fp + '.html'); /* clean URL: /hakkinda → hakkinda.html */
6146
+ if (!hit) { res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' }); res.end('404 — /' + rel); return; }
6147
+ const ext = path.extname(hit).toLowerCase();
6148
+ res.writeHead(200, { 'Content-Type': BC_MIME[ext] || 'application/octet-stream', 'Cache-Control': 'no-store' });
6149
+ fs.createReadStream(hit).pipe(res);
6150
+ } catch (e) {
6151
+ try { res.writeHead(500); res.end('server error'); } catch {}
6152
+ }
6153
+ }
6154
+ function bcStaticStart(root) {
6155
+ return new Promise((resolve) => {
6156
+ const r = path.resolve(String(root || ''));
6157
+ if (bcStatic.server && bcStatic.root === r) { resolve(bcStatic.base); return; }
6158
+ if (bcStatic.server) { try { bcStatic.server.close(); } catch {} bcStatic.server = null; }
6159
+ const srv = http.createServer(bcStaticHandler);
6160
+ srv.on('error', () => resolve(''));
6161
+ srv.listen(0, '127.0.0.1', () => {
6162
+ bcStatic.server = srv;
6163
+ bcStatic.root = r;
6164
+ bcStatic.base = 'http://127.0.0.1:' + srv.address().port;
6165
+ try { log.info('bc', 'statik sunucu: ' + bcStatic.base + ' → ' + r); } catch {}
6166
+ resolve(bcStatic.base);
6167
+ });
6168
+ });
6169
+ }
6170
+ /* Preview'a basılınca BC oturumuna SESSİZ bağlam enjeksiyonu (observe — tur
6171
+ AÇMAZ, maliyet yok): ajan dahili sunucunun çalıştığını bilir ve statik
6172
+ dosyalar için kendi sunucusunu başlatma denemez → çakışma biter */
6173
+ function bcTellServe(staticBase) {
6174
+ try {
6175
+ const ws = ideRoot();
6176
+ const sid = bcSessions.get(ws);
6177
+ if (!sid) return;
6178
+ engine.observe(sid,
6179
+ '[PREVIEW] Kullanıcı önizlemeyi açtı — DAHİLİ STATİK SUNUCU şu adreste ÇALIŞIYOR: ' + staticBase + '\n' +
6180
+ 'Statik dosyalar için KENDİ sunucunu BAŞLATMA; üretilen siteyi bu adres üzerinden değerlendir.\n' +
6181
+ 'Yalnızca gerçek dev-server/build gerekiyorsa (React/Vite/Next/Expo: npm run dev, expo start) ' +
6182
+ 'kendi sunucunu BLOKLAMADAN arka planda başlat ve çalışan adresi yaz.'
6183
+ );
6184
+ } catch {}
6185
+ }
6186
+
6187
+ ipcMain.handle('ide:previewFile', async (_e, rel) => {
5822
6188
  try {
5823
6189
  const p = ideSafe(rel);
5824
6190
  if (!p) return { ok: false, error: 'geçersiz yol' };
5825
6191
  if (!/\.html?$/i.test(p)) return { ok: false, error: 'önizleme yalnız .html/.htm dosyaları için' };
6192
+ const base = await bcStaticStart(ideRoot());
6193
+ if (!base) return { ok: false, error: 'statik sunucu başlatılamadı' };
6194
+ const root = ideRoot();
6195
+ const relPath = path.relative(root, p).replace(/\\/g, '/');
6196
+ const url = base + '/' + relPath;
5826
6197
  setBrowserOpen(true, true);
5827
- const url = 'file:///' + p.replace(/\\/g, '/');
5828
6198
  browser.view.webContents.loadURL(url).catch(() => {});
5829
6199
  browserEmit({ open: true, width: browser.width, url });
5830
- return { ok: true };
6200
+ return { ok: true, url };
5831
6201
  } catch (e) {
5832
6202
  return { ok: false, error: String((e && e.message) || e) };
5833
6203
  }
5834
6204
  });
5835
6205
 
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 {
6206
+ /* PREVIEW: workspace kökündeki siteyi DAİMA sunucudan
6207
+ 1) ajan bir dev server başlattıysa onun adresi, 2) yoksa dahili statik sunucu.
6208
+ file:// ASLA kullanılmaz (JS/clean URL kırılmaları). Ayrıca ajana SUNUCU
6209
+ komutu düşer: uygulama sunucu istiyorsa kendisi başlatıp adresi yazar. */
6210
+ ipcMain.handle('ide:preview', async () => {
6211
+ try {
5840
6212
  const root = ideRoot();
5841
6213
  const pick = (name) => {
5842
6214
  const p = path.join(root, name);
@@ -5851,12 +6223,19 @@ ipcMain.handle('ide:preview', () => { try {
5851
6223
  entry = htmls.length ? path.join(root, htmls.sort()[0]) : null;
5852
6224
  }
5853
6225
  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 */
6226
+ /* forceVisible: preview'a basınca tarayıcı ikonuna basmaya gerek kalmasın */
5856
6227
  setBrowserOpen(true, true);
5857
- const url = 'file:///' + entry.replace(/\\/g, '/');
6228
+ let url = '';
6229
+ if (bcLastServerUrl) {
6230
+ url = bcLastServerUrl; /* ajanın kendi dev sunucusu öncelikli */
6231
+ } else {
6232
+ const base = await bcStaticStart(root);
6233
+ if (!base) return { ok: false, error: 'statik sunucu başlatılamadı' };
6234
+ url = base + '/';
6235
+ }
5858
6236
  browser.view.webContents.loadURL(url).catch(() => {});
5859
6237
  browserEmit({ open: true, width: browser.width, url });
6238
+ if (!bcLastServerUrl) bcTellServe(url.replace(/\/$/, ''));
5860
6239
  return { ok: true, url };
5861
6240
  } catch (e) {
5862
6241
  return { ok: false, error: String((e && e.message) || e) };
@@ -5864,20 +6243,44 @@ ipcMain.handle('ide:preview', () => { try {
5864
6243
  });
5865
6244
 
5866
6245
  /* 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) => {
6246
+ tarayıcıda GÖRÜNÜR açılır. Yalnız localhost kabul edilir; file:// gelen
6247
+ eser adresi DAHİLİ STATİK SUNUCUYA çevrilir — dosyadan ASLA açılmaz. */
6248
+ ipcMain.handle('ide:previewUrl', async (_e, url) => {
5870
6249
  try {
5871
6250
  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 */
6251
+ let target = '';
6252
+ if (/^https?:\/\/(?:localhost|127\.0\.0\.1)(?::\d{2,5})?(?:\/|$)/i.test(u)) {
6253
+ target = u;
6254
+ } else if (/^file:\/\/\//i.test(u)) {
6255
+ const raw = decodeURIComponent(u.replace(/^file:\/\/\//, '')).replace(/\/$/, '');
6256
+ const fp = path.resolve(raw);
6257
+ const root = ideRoot();
6258
+ const rel = path.relative(root, fp).replace(/\\/g, '/');
6259
+ let base;
6260
+ if (rel && !rel.startsWith('..')) {
6261
+ base = await bcStaticStart(root);
6262
+ if (!base) return { ok: false, error: 'statik sunucu başlatılamadı' };
6263
+ target = base + '/' + rel;
6264
+ } else {
6265
+ /* workspace dışı eser — dosyanın kendi klasörü kök alınır */
6266
+ base = await bcStaticStart(path.dirname(fp));
6267
+ if (!base) return { ok: false, error: 'statik sunucu başlatılamadı' };
6268
+ target = base + '/' + path.basename(fp);
6269
+ }
6270
+ } else {
6271
+ return { ok: false, error: 'yalnız localhost adresleri önizlenebilir' };
6272
+ }
6273
+ /* Expo/Metro dev sunucusu → telefon modu OTOMATİK (mobil uygulama canlı önizleme) */
6274
+ if (/^https?:\/\/(?:localhost|127\.0\.0\.1):(8081|19000|19001|19002|3000|5173)\//i.test(target)) {
6275
+ if (!browser.phone) setBrowserPhone(true);
6276
+ }
6277
+ /* forceVisible: otomatik ve GÖRÜNÜR açılır */
5877
6278
  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 };
6279
+ browser.view.webContents.loadURL(target).catch(() => {});
6280
+ let wNow = 0;
6281
+ try { wNow = win.getContentSize()[0]; } catch {}
6282
+ browserEmit({ open: true, width: browserShownWidth(wNow), url: target });
6283
+ return { ok: true, url: target };
5881
6284
  } catch (e) {
5882
6285
  return { ok: false, error: String((e && e.message) || e) };
5883
6286
  }