beast-agent 2.44.14 → 2.46.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
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const { app, BrowserWindow, WebContentsView, ipcMain, shell, dialog, Tray, Menu, nativeImage, desktopCapturer, session, net: electronNet, clipboard, Notification } = require('electron');
3
+ const { app, BrowserWindow, WebContentsView, ipcMain, shell, dialog, Tray, Menu, nativeImage, desktopCapturer, screen, session, net: electronNet, clipboard, Notification } = require('electron');
4
4
  const path = require('path');
5
5
  const fs = require('fs');
6
6
  const http = require('http');
@@ -2292,7 +2292,7 @@ function botToolSet(cfg) {
2292
2292
  ]);
2293
2293
  if (s.web_search) { set.add('web_search'); set.add('http_fetch'); set.add('webfetch'); set.add('deep_search'); }
2294
2294
  if (s.browser) {
2295
- for (const t of ['browser_open', 'browser_read', 'browser_screenshot', 'browser_snapshot', 'browser_click', 'browser_type', 'browser_press', 'browser_scroll', 'browser_select', 'ocr_read', 'computer_look']) set.add(t);
2295
+ for (const t of ['browser_open', 'browser_read', 'browser_screenshot', 'browser_snapshot', 'browser_click', 'browser_type', 'browser_press', 'browser_scroll', 'browser_select', 'browser_wait', 'ocr_read', 'computer_look']) set.add(t);
2296
2296
  }
2297
2297
  if (s.email) { set.add('email_list'); set.add('email_read'); set.add('email_send'); }
2298
2298
  if (s.run_command) {
@@ -3259,7 +3259,7 @@ function reloadBackend() {
3259
3259
  },
3260
3260
  computer: {
3261
3261
  look: captureScreenDataUrl,
3262
- act: (op, args) => computeruse.act(op, args),
3262
+ act: (op, args) => computerActScaled(op, args),
3263
3263
  },
3264
3264
  ocr: (o) => ocrRead(o),
3265
3265
  email: { list: emailList, read: emailRead, send: emailSend },
@@ -3270,6 +3270,7 @@ function reloadBackend() {
3270
3270
  screenshot: (s) => browserScreenshot(s),
3271
3271
  snapshot: (s) => browserSnapshot(s),
3272
3272
  act: (k, a, s) => browserAct(k, a, s),
3273
+ wait: (a, s) => browserWait(a, s),
3273
3274
  },
3274
3275
  research: {
3275
3276
  /* deep_search'ün "sayfayı GİZLİ tarayıcıda açıp oku" parçası (Electron-only).
@@ -3310,6 +3311,7 @@ function reloadBackend() {
3310
3311
  bcFlushOnDone(ev); /* Beast Code kuyruğunu iş bitiminde boşalt */
3311
3312
  stFlushOnDone(ev); /* Beast Studio kuyruğunu iş bitiminde boşalt */
3312
3313
  sbFlushOnDone(ev); /* Sandbox kuyruğunu iş bitiminde boşalt */
3314
+ mcpChatFlushOnDone(ev); /* MCP sohbet kuyruğunu iş bitiminde boşalt */
3313
3315
  finFlushOnDone(ev); /* Finance trader döngüsü: iş bitince sıradaki turu planla */
3314
3316
  /* BC canlı önizleme: ajan bir dev server başlattıysa adresi yakala —
3315
3317
  preview butonu ve otomatik açılış DAİMA bu sunucuyu öncelikli kullanır */
@@ -4135,6 +4137,26 @@ function ensureBrowser() {
4135
4137
  if (/^https?:\/\//i.test(url)) wc.loadURL(url).catch(() => {});
4136
4138
  return { action: 'deny' };
4137
4139
  });
4140
+ /* JS DİYALOGLARI (alert/confirm/prompt/beforeunload): CDP ile OTOMATİK
4141
+ onaylanır/kapatılır — aksi halde renderer kilitlenir ve TÜM tarayıcı
4142
+ araçları askıda kalır (executeJavaScript diyalog kapanana dek dönmez). */
4143
+ try {
4144
+ if (!wc.debugger.isAttached()) wc.debugger.attach('1.3');
4145
+ wc.debugger.sendCommand('Page.enable').catch(() => {});
4146
+ wc.debugger.on('message', (_ev, method, params) => {
4147
+ if (method === 'Page.javascriptDialogOpening') {
4148
+ browser.lastDialog = {
4149
+ type: String((params && params.type) || 'alert'),
4150
+ message: String((params && params.message) || ''),
4151
+ at: Date.now(),
4152
+ };
4153
+ try { wc.debugger.sendCommand('Page.handleJavaScriptDialog', { accept: true }).catch(() => {}); } catch {}
4154
+ blog('dialog', browser.lastDialog.type + ': ' + browser.lastDialog.message.slice(0, 80));
4155
+ }
4156
+ });
4157
+ } catch (e) {
4158
+ waLog('tarayıcı diyalog köprüsü kurulamadı: ' + String((e && e.message) || e).slice(0, 120));
4159
+ }
4138
4160
  // Tarayıcı kapalıyken olayların UI'ı geri açmamasını garantile
4139
4161
  const notify = (extra) => {
4140
4162
  if (browser.open && win && !win.isDestroyed()) {
@@ -4251,6 +4273,61 @@ async function browserNavigate(raw, signal, ctx) {
4251
4273
  });
4252
4274
  }
4253
4275
 
4276
+ /* JS diyalogları (alert/confirm/prompt) CDP ile OTOMATİK yönetilir;
4277
+ bu yardımcı çağrı SIRASINDA oluşan diyaloğu yanıta ekler. */
4278
+ function browserDialogNote(sinceAt) {
4279
+ const d = browser.lastDialog;
4280
+ if (!d || !d.at || d.at <= (sinceAt || 0)) return {};
4281
+ const verb = d.type === 'alert' || d.type === 'prompt' ? 'kapatıldı' : 'onaylandı';
4282
+ return {
4283
+ dialog: { type: d.type, message: d.message },
4284
+ dialogNote: 'JS diyaloğu otomatik ' + verb + ': "' + String(d.message || '').slice(0, 120) + '"',
4285
+ };
4286
+ }
4287
+
4288
+ /* SPA/DOM oturması: readyState complete + içerik uzunluğu İKİ turdur sabitse
4289
+ dön (en çok maxMs). Böylece hidrasyon bitmeden snapshot/okuma yapılmaz. */
4290
+ async function browserSettle(wc, signal, maxMs = 4000) {
4291
+ const t0 = Date.now();
4292
+ let lastLen = -1;
4293
+ let stable = 0;
4294
+ while (Date.now() - t0 < maxMs) {
4295
+ if (signal && signal.aborted) return;
4296
+ let info = null;
4297
+ try {
4298
+ info = await wc.executeJavaScript(
4299
+ '({rs: document.readyState, len: (document.body ? ((document.body.innerText || "").trim().length) : 0)})',
4300
+ true
4301
+ );
4302
+ } catch {
4303
+ return;
4304
+ }
4305
+ if (!info) return;
4306
+ const rs = String(info.rs || '');
4307
+ const len = Number(info.len) || 0;
4308
+ if (rs === 'complete') {
4309
+ if (len > 0 && len === lastLen) stable++;
4310
+ else stable = 0;
4311
+ if (stable >= 2) return;
4312
+ /* boş/az metinli sayfa (canvas/SPA shell): yine de kısa bir hidrasyon penceresi bırak */
4313
+ if (len === 0 && Date.now() - t0 > 1500) return;
4314
+ }
4315
+ lastLen = len;
4316
+ await new Promise((r) => setTimeout(r, 300));
4317
+ }
4318
+ }
4319
+
4320
+ /* snapshot al (hatayı yut) — navigate/act yanıtlarına gömmek için */
4321
+ async function browserSnapshotNow(wc) {
4322
+ try {
4323
+ const raw = await wc.executeJavaScript(BROWSER_SNAPSHOT_JS, true);
4324
+ const obj = JSON.parse(raw);
4325
+ return obj && typeof obj.count === 'number' ? obj : null;
4326
+ } catch {
4327
+ return null;
4328
+ }
4329
+ }
4330
+
4254
4331
  async function browserNavigateNow(raw, signal, ctx) {
4255
4332
  let url = String(raw || '').trim();
4256
4333
  if (!url) return { ok: false, error: 'boş adres' };
@@ -4262,11 +4339,27 @@ async function browserNavigateNow(raw, signal, ctx) {
4262
4339
  }
4263
4340
  setBrowserOpenForAgent();
4264
4341
  const wc = browser.view.webContents;
4342
+ const dlgAt = browser.lastDialog ? browser.lastDialog.at : 0;
4343
+ let failInfo = null;
4344
+ let timedOut = false;
4265
4345
  await new Promise((resolve) => {
4266
4346
  let settled = false;
4267
- const finish = () => { settled = true; clearTimeout(timer); cleanup(); resolve(); };
4268
- const timer = setTimeout(finish, 25000);
4269
- const onFail = (_e, code, desc) => { if (code !== -3 && !settled) finish(); };
4347
+ const finish = () => {
4348
+ if (settled) return;
4349
+ settled = true;
4350
+ clearTimeout(timer);
4351
+ cleanup();
4352
+ resolve();
4353
+ };
4354
+ const timer = setTimeout(() => { timedOut = true; finish(); }, 25000);
4355
+ /* did-fail-load: ANA kare hatasını YAKALA ve rapora koy. kod -3 (ABORTED)
4356
+ yönlendirme/SPA geçişlerinde normaldir; alt kare hataları sayılmaz. */
4357
+ const onFail = (_e, code, desc, validatedURL, isMainFrame) => {
4358
+ if (isMainFrame === false) return;
4359
+ if (code === -3) return;
4360
+ failInfo = { code, desc: String(desc || ''), url: String(validatedURL || url) };
4361
+ finish();
4362
+ };
4270
4363
  function cleanup() {
4271
4364
  clearTimeout(timer);
4272
4365
  wc.removeListener('did-finish-load', finish);
@@ -4274,24 +4367,44 @@ async function browserNavigateNow(raw, signal, ctx) {
4274
4367
  }
4275
4368
  wc.once('did-finish-load', finish);
4276
4369
  wc.on('did-fail-load', onFail);
4277
- wc.loadURL(url).catch(() => {});
4370
+ wc.loadURL(url).catch((e) => {
4371
+ failInfo = { code: null, desc: String((e && e.message) || e) };
4372
+ finish();
4373
+ });
4278
4374
  });
4279
4375
  let title = '';
4280
4376
  let finalUrl = url;
4281
4377
  try { title = wc.getTitle(); finalUrl = wc.getURL() || url; } catch {}
4282
4378
  browserEmit({ open: true, width: browserShownWidth(browserW()), url: finalUrl });
4283
4379
  flushBrowserStorage(); // oturum çerezleri diske — ani kapanışta kaybolmasın
4284
- /* açılışta snapshot da göm — model ayrı snapshot çağırmadan ref'lere başlar */
4285
- let snap = null;
4286
- try {
4287
- const sraw = await wc.executeJavaScript(BROWSER_SNAPSHOT_JS, true);
4288
- const sobj = JSON.parse(sraw);
4289
- if (sobj && typeof sobj.count === 'number') snap = sobj;
4290
- } catch {}
4380
+ /* YÜKLEME HATASI: artık "açıldı" denmez — kod/açıklama + çözüm ipucu döner */
4381
+ if (failInfo) {
4382
+ const codeTxt = failInfo.code != null ? ' kod ' + failInfo.code : '';
4383
+ const hint = /ERR_NAME_NOT_RESOLVED|ERR_INTERNET_DISCONNECTED/i.test(failInfo.desc)
4384
+ ? ' (DNS/ağ: internet bağlantısını ve adresi kontrol et)'
4385
+ : /ERR_CONNECTION_REFUSED|ERR_CONNECTION_TIMED_OUT|ERR_EMPTY_RESPONSE/i.test(failInfo.desc)
4386
+ ? ' (sunucu yanıt vermiyor — adres/port doğru mu?)'
4387
+ : '';
4388
+ return {
4389
+ ok: false,
4390
+ error: 'sayfa yüklenemedi' + codeTxt + ': ' + failInfo.desc + hint,
4391
+ url: failInfo.url || finalUrl,
4392
+ ...browserDialogNote(dlgAt),
4393
+ };
4394
+ }
4395
+ /* SPA hidrasyonu: içerik oturmadan snapshot alınmaz (timeout'ta bekleme yok) */
4396
+ await browserSettle(wc, signal, timedOut ? 0 : 4000);
4397
+ const snap = await browserSnapshotNow(wc);
4398
+ let stillLoading = false;
4399
+ try { stillLoading = wc.isLoading(); } catch {}
4291
4400
  return {
4292
4401
  ok: true,
4402
+ ...(timedOut
4403
+ ? { warning: 'yükleme 25 sn içinde tamamlanmadı — sayfa kısmen yüklü olabilir; browser_wait/browser_read ile kontrol et', loading: stillLoading }
4404
+ : {}),
4293
4405
  url: finalUrl,
4294
4406
  title,
4407
+ ...browserDialogNote(dlgAt),
4295
4408
  ...(snap
4296
4409
  ? { snapshot: snap.snapshot, refCount: snap.count, note: 'sayfa acildi — güncel snapshot hazır (' + snap.count + ' ref); ref numarasıyla browser_click/browser_type ile devam et' }
4297
4410
  : { note: 'sayfa acildi — etkileşimli elemanlar için browser_snapshot al' }),
@@ -4691,21 +4804,70 @@ async function browserSearchNow(query, signal, ctx) {
4691
4804
  }
4692
4805
  }
4693
4806
 
4694
- async function browserScreenshot(signal) {
4807
+ /* Ekran görüntüsü üzerine ref numaralarını ÇİZ: vision modeli kutuları ve
4808
+ numaraları görür, koordinatı tahmin etmek yerine okur. Ref'ler snapshot ile
4809
+ AYNI numaralandırmayı kullanır (snapshot hemen önce alınır). */
4810
+ async function annotateBrowserShot(img, snap) {
4811
+ const lib = canvasLib();
4812
+ if (!lib || !snap || !snap.boxes) return null;
4813
+ const im = await lib.loadImage(img.toPNG());
4814
+ const canvas = lib.createCanvas(im.width, im.height);
4815
+ const ctx = canvas.getContext('2d');
4816
+ ctx.drawImage(im, 0, 0);
4817
+ const vw = Number(snap.vw) || im.width;
4818
+ const k = im.width / Math.max(1, vw);
4819
+ let drawn = 0;
4820
+ for (const [ref, b] of Object.entries(snap.boxes)) {
4821
+ const x = Number(b[0]) * k;
4822
+ const y = Number(b[1]) * k;
4823
+ const w = Number(b[2]) * k;
4824
+ const h = Number(b[3]) * k;
4825
+ if (!(w >= 6 && h >= 6)) continue;
4826
+ if (y > im.height || x > im.width || y + h < 0 || x + w < 0) continue;
4827
+ ctx.strokeStyle = 'rgba(0,190,255,0.95)';
4828
+ ctx.lineWidth = 1.5;
4829
+ ctx.strokeRect(x + 0.5, y + 0.5, Math.max(2, w), Math.max(2, h));
4830
+ const label = String(ref);
4831
+ ctx.font = 'bold 13px Segoe UI, sans-serif';
4832
+ const tw = Math.ceil(ctx.measureText(label).width);
4833
+ const by = y - 16 < 0 ? y + 1 : y - 16;
4834
+ ctx.fillStyle = 'rgba(0,0,0,0.78)';
4835
+ ctx.fillRect(x, by, tw + 8, 16);
4836
+ ctx.fillStyle = '#00d8ff';
4837
+ ctx.fillText(label, x + 4, by + 12.5);
4838
+ if (++drawn >= 90) break;
4839
+ }
4840
+ return canvas.toBuffer('image/jpeg', 78);
4841
+ }
4842
+
4843
+ async function browserScreenshot(signal, opts) {
4695
4844
  if (!browser.view || !browser.open) return { ok: false, error: 'tarayıcı açık değil' };
4696
4845
  const wc = browser.view.webContents;
4846
+ const annotate = !(opts && opts.annotate === false);
4847
+ const dlgAt = browser.lastDialog ? browser.lastDialog.at : 0;
4697
4848
  try {
4849
+ /* ref numaraları görselle eşleşsin: önce taze snapshot */
4850
+ let snap = null;
4851
+ if (annotate) snap = await browserSnapshotNow(wc);
4698
4852
  const img = await wc.capturePage();
4699
4853
  let out = img;
4700
4854
  const sz = img.getSize();
4701
4855
  const maxW = 1000; // vision bütçesi için küçült
4702
4856
  if (sz.width > maxW) out = img.resize({ width: maxW });
4703
- const jpeg = out.toJPEG(72);
4857
+ let jpeg = null;
4858
+ if (annotate) {
4859
+ try { jpeg = await annotateBrowserShot(out, snap); } catch {}
4860
+ }
4861
+ if (!jpeg || !jpeg.length) jpeg = out.toJPEG(72);
4704
4862
  if (!jpeg || !jpeg.length) return { ok: false, error: 'görüntü alınamadı' };
4705
4863
  return {
4706
4864
  ok: true,
4707
- note: 'ekran görüntüsü bir sonraki adımda sana gösterilecek',
4865
+ note: snap
4866
+ ? 'ekran görüntüsü bir sonraki adımda gösterilecek — üzerindeki numaralar snapshot ref\'leriyle AYNIDIR; tıklamak için o ref\'i kullan'
4867
+ : 'ekran görüntüsü bir sonraki adımda sana gösterilecek',
4708
4868
  __injectImage: 'data:image/jpeg;base64,' + jpeg.toString('base64'),
4869
+ ...(snap ? { snapshot: snap.snapshot, refCount: snap.count } : {}),
4870
+ ...browserDialogNote(dlgAt),
4709
4871
  url: wc.getURL(),
4710
4872
  title: wc.getTitle(),
4711
4873
  bytes: jpeg.length,
@@ -4754,6 +4916,7 @@ const BROWSER_SNAPSHOT_JS = `(function(){
4754
4916
  window.__beMap={};
4755
4917
  const seen=new Set();
4756
4918
  const lines=[];
4919
+ const boxes={};
4757
4920
  let i=0;
4758
4921
  function add(e){
4759
4922
  if(!e||seen.has(e)) return;
@@ -4761,6 +4924,8 @@ const BROWSER_SNAPSHOT_JS = `(function(){
4761
4924
  if(!__vis(e)||e.disabled||e.getAttribute('aria-disabled')==='true') return;
4762
4925
  i++;
4763
4926
  window.__beMap[i]=e;
4927
+ const rb=e.getBoundingClientRect();
4928
+ boxes[i]=[Math.round(rb.left),Math.round(rb.top),Math.round(rb.width),Math.round(rb.height)];
4764
4929
  const tag=e.tagName.toLowerCase();
4765
4930
  const type=(e.getAttribute&&e.getAttribute('type'))||'';
4766
4931
  let s='['+i+'] <'+tag+(type?' type='+type:'')+'>';
@@ -4776,7 +4941,7 @@ const BROWSER_SNAPSHOT_JS = `(function(){
4776
4941
  lines.push('--- SAYFA ---');
4777
4942
  }
4778
4943
  for(const e of document.querySelectorAll(sel)){ if(i>=100) break; add(e); }
4779
- return JSON.stringify({count:i,title:document.title,url:location.href,snapshot:lines.join('\\n')});
4944
+ return JSON.stringify({count:i,title:document.title,url:location.href,snapshot:lines.join('\\n'),boxes:boxes,vw:window.innerWidth,vh:window.innerHeight});
4780
4945
  })()`;
4781
4946
 
4782
4947
  function browserActionJs(kind, args) {
@@ -4953,6 +5118,10 @@ async function browserSnapshot(signal) {
4953
5118
  async function browserAct(kind, args, signal) {
4954
5119
  if (!browser.view || !browser.open) return { ok: false, error: 'tarayıcı açık değil' };
4955
5120
  const wc = browser.view.webContents;
5121
+ const dlgAt = browser.lastDialog ? browser.lastDialog.at : 0;
5122
+ /* DOĞRULAMA: eylem öncesi görsel parmak izi (yalnız anlamlı eylemlerde) */
5123
+ const verify = ['click', 'type', 'select', 'press'].includes(kind) && !(args && args.verify === false);
5124
+ const sigBefore = verify ? await pageSignature(wc) : null;
4956
5125
  try {
4957
5126
  const raw = await wc.executeJavaScript(browserActionJs(kind, args || {}), true);
4958
5127
  let obj = {};
@@ -4969,6 +5138,20 @@ async function browserAct(kind, args, signal) {
4969
5138
  wc.on('did-navigate', onNav);
4970
5139
  });
4971
5140
  }
5141
+ /* SPA: sayfa değiştiyse DOM oturmadan snapshot alınmaz */
5142
+ if (navigated) await browserSettle(wc, signal, 2500);
5143
+
5144
+ /* DOĞRULAMA: eylem sonrası değişim oranı — "tıkladım ama bir şey olmadı" tuzağını yakalar */
5145
+ let changed = null;
5146
+ let changeRatio = null;
5147
+ if (sigBefore) {
5148
+ const sigAfter = await pageSignature(wc);
5149
+ const ratio = computeruse.signatureDiff(sigBefore, sigAfter);
5150
+ if (ratio != null) {
5151
+ changed = ratio >= 0.02;
5152
+ changeRatio = Number(ratio.toFixed(3));
5153
+ }
5154
+ }
4972
5155
 
4973
5156
  blog(
4974
5157
  kind,
@@ -4981,12 +5164,7 @@ async function browserAct(kind, args, signal) {
4981
5164
  );
4982
5165
 
4983
5166
  /* HIZ: eylem cevabına taze snapshot göm — model ayrı browser_snapshot çağırmaz (tur sayısı yarıya iner) */
4984
- let freshSnap = null;
4985
- try {
4986
- const sraw = await wc.executeJavaScript(BROWSER_SNAPSHOT_JS, true);
4987
- const sobj = JSON.parse(sraw);
4988
- if (sobj && typeof sobj.count === 'number') freshSnap = sobj;
4989
- } catch {}
5167
+ const freshSnap = await browserSnapshotNow(wc);
4990
5168
 
4991
5169
  return {
4992
5170
  ok: true,
@@ -4995,6 +5173,16 @@ async function browserAct(kind, args, signal) {
4995
5173
  url: wc.getURL(),
4996
5174
  title: wc.getTitle(),
4997
5175
  navigated,
5176
+ ...(changed != null ? { changed, changeRatio } : {}),
5177
+ ...(changed === false && ['click', 'type', 'select'].includes(kind)
5178
+ ? {
5179
+ warning:
5180
+ 'görünür değişiklik yok (changeRatio ' + changeRatio + ') — eylem etkisiz olabilir: ' +
5181
+ 'ref bayat olabilir (browser_snapshot al ve güncel ref ile tekrar dene), öğe kapalı/gizli olabilir ' +
5182
+ 'ya da tıklama sayfa tarafından engellenmiş olabilir. browser_wait ile kısa bekleme dene.',
5183
+ }
5184
+ : {}),
5185
+ ...browserDialogNote(dlgAt),
4998
5186
  ...(freshSnap ? { snapshot: freshSnap.snapshot, refCount: freshSnap.count } : {}),
4999
5187
  ...(navigated
5000
5188
  ? { note: 'sayfa degisti — yanıtta güncel snapshot var; refler eskiyse yeni browser_snapshot al' }
@@ -5008,6 +5196,61 @@ async function browserAct(kind, args, signal) {
5008
5196
  }
5009
5197
  }
5010
5198
 
5199
+ /* BEKLE: selector/text/ref görünene (gone:true ise kaybolana) kadar ya da ms
5200
+ kadar bekle. SPA'lar, gecikmeli açılan menüler ve ağ yavaşlığı için —
5201
+ körlemesine tekrar denemek yerine deterministik bekleme. */
5202
+ async function browserWait(args, signal) {
5203
+ if (!browser.view || !browser.open) return { ok: false, error: 'tarayıcı açık değil' };
5204
+ const wc = browser.view.webContents;
5205
+ const a = args || {};
5206
+ const timeout = Math.max(200, Math.min(30000, Number(a.timeout_ms) || 10000));
5207
+ const ms = Math.max(0, Math.min(10000, Number(a.ms) || 0));
5208
+ const hasCond = a.selector != null || a.text != null || a.ref != null;
5209
+ if (!hasCond) {
5210
+ if (!ms) return { ok: false, error: 'selector, text, ref ya da ms ver' };
5211
+ await new Promise((r) => setTimeout(r, ms));
5212
+ return { ok: true, waited_ms: ms, url: wc.getURL(), title: wc.getTitle() };
5213
+ }
5214
+ const gone = !!a.gone;
5215
+ const t0 = Date.now();
5216
+ /* ms verildiyse koşul taramasından ÖNCE sabit bekleme (extra delay) */
5217
+ if (ms) await new Promise((r) => setTimeout(r, ms));
5218
+ const refJs = a.ref != null ? `!!__resolveRef(${JSON.stringify(Number(a.ref))})` : '';
5219
+ const selJs = a.selector != null ? `(function(){try{var el=document.querySelector(${JSON.stringify(String(a.selector))});return !!(el&&__vis(el));}catch(e){return false;}})()` : '';
5220
+ const txtJs = a.text != null ? `((document.body?document.body.innerText:'').toLowerCase().includes(${JSON.stringify(String(a.text).toLowerCase())}))` : '';
5221
+ const probe = `(function(){${BROWSER_JS_HELPERS};try{return !!(${refJs || selJs || txtJs});}catch(e){return false;}})()`;
5222
+ while (Date.now() - t0 < timeout) {
5223
+ if (signal && signal.aborted) return { ok: false, error: 'bekleme iptal edildi' };
5224
+ let found = false;
5225
+ try { found = !!(await wc.executeJavaScript(probe, true)); } catch { found = false; }
5226
+ if (gone ? !found : found) {
5227
+ return {
5228
+ ok: true,
5229
+ waited_ms: Date.now() - t0,
5230
+ ...(gone ? { gone: true } : { found: true }),
5231
+ ...(a.selector != null ? { selector: String(a.selector) } : {}),
5232
+ ...(a.text != null ? { text: String(a.text).slice(0, 60) } : {}),
5233
+ ...(a.ref != null ? { ref: Number(a.ref) } : {}),
5234
+ url: wc.getURL(),
5235
+ title: wc.getTitle(),
5236
+ note: 'koşul sağlandı — şimdi eyleme geç; ref lazımsa browser_snapshot ile güncel ref al',
5237
+ };
5238
+ }
5239
+ await new Promise((r) => setTimeout(r, 300));
5240
+ }
5241
+ return {
5242
+ ok: false,
5243
+ error:
5244
+ 'bekleme zaman aşımı (' + timeout + ' ms): ' +
5245
+ (a.selector != null ? 'selector bulunamadı: ' + String(a.selector).slice(0, 60)
5246
+ : a.text != null ? 'metin bulunamadı: ' + String(a.text).slice(0, 60)
5247
+ : 'ref mevcut değil: ' + Number(a.ref)) +
5248
+ (gone ? ' (hâlâ görünüyor)' : ''),
5249
+ url: wc.getURL(),
5250
+ title: wc.getTitle(),
5251
+ };
5252
+ }
5253
+
5011
5254
  /* oturum açılışında --hidden ile başlarsa pencere gösterme, tepside yaşa */
5012
5255
  const startHidden =
5013
5256
  process.argv.includes('--hidden') ||
@@ -6393,6 +6636,211 @@ ipcMain.handle('mcp:refresh', async (_e, name) => {
6393
6636
  }
6394
6637
  });
6395
6638
 
6639
+ /* ---------------- BEAST MCP SOHBETİ (MCP modu SAĞ panel) ----------------
6640
+ MCP moduna özel, ANA SOHBETTEN TAMAMEN AYRI oturum: bgTitle 'Beast MCP'
6641
+ (listSessions gizler, listMcpSessions listeler; geçmiş seçici eski
6642
+ oturumları açabilir). MCP sunucu araçları zaten tüm engine turlarına
6643
+ enjekte edilir; panel yalnız oturum yaşam döngüsünü yönetir.
6644
+ Gönderim main chat disiplininin aynısı: boşta kısa debounce (ard arda
6645
+ mesajlar birleşir), meşgulken steer (koşan tura anında eklenir). */
6646
+ let mcpChatSid = ''; /* aktif MCP sohbet oturumu */
6647
+ let mcpChatFresh = false; /* 'Yeni' istendi: eski sohbete DÖNME, taze aç */
6648
+ const mcpChatQueue = new Map(); /* 'mcp' → { timer, msgs[] } */
6649
+ const MCP_CHAT_DEBOUNCE_MS = 900;
6650
+
6651
+ function mcpChatCreate() {
6652
+ const s = engine._load(engine.createSession().id);
6653
+ s.messages = s.messages || [];
6654
+ s.bgTitle = 'Beast MCP'; /* _view.isBg → ana sohbet geçmişi listesinde gizli */
6655
+ try {
6656
+ fs.appendFileSync(
6657
+ engine._file(s.id),
6658
+ JSON.stringify({ t: 'meta2', bgOf: '', title: 'Beast MCP', at: new Date().toISOString() }) + '\n'
6659
+ );
6660
+ } catch {}
6661
+ engine.cache.set(s.id, s);
6662
+ mcpChatSid = s.id;
6663
+ mcpChatFresh = false;
6664
+ return s;
6665
+ }
6666
+
6667
+ function mcpChatSession() {
6668
+ if (mcpChatSid) {
6669
+ try {
6670
+ const s = engine.cache.get(mcpChatSid) || engine._load(mcpChatSid);
6671
+ if (s && s.bgTitle === 'Beast MCP') return s;
6672
+ } catch {}
6673
+ mcpChatSid = '';
6674
+ }
6675
+ /* 'Yeni' istenmediyse son MCP sohbetine dön: uygulama yeniden açıldığında
6676
+ sohbet kaldığı yerden sürer (geçmiş seçiciden eski oturumlar açılabilir) */
6677
+ if (!mcpChatFresh) {
6678
+ try {
6679
+ const last = engine.listMcpSessions(50).find((it) => it.count > 0);
6680
+ if (last) {
6681
+ const s = engine._load(last.id);
6682
+ if (s && s.bgTitle === 'Beast MCP') {
6683
+ engine.cache.set(s.id, s);
6684
+ mcpChatSid = s.id;
6685
+ return s;
6686
+ }
6687
+ }
6688
+ } catch {}
6689
+ }
6690
+ return mcpChatCreate();
6691
+ }
6692
+
6693
+ /* geçmiş yükü: yalnız metin mesajları (tool çağrıları panele basılmaz) */
6694
+ function mcpChatMessages(s) {
6695
+ const msgs = [];
6696
+ for (const m of s.messages || []) {
6697
+ if (m.tool_calls) continue;
6698
+ const txt = bcMsgText(m);
6699
+ if (!txt) continue;
6700
+ msgs.push({ role: m.role === 'assistant' ? 'assistant' : 'user', text: txt.slice(0, 4000) });
6701
+ }
6702
+ return msgs.slice(-200);
6703
+ }
6704
+
6705
+ /* geçmiş listesi: boş oturumlar gizlenir (aktif olan daima görünür) */
6706
+ function mcpChatHistory() {
6707
+ let items = [];
6708
+ try { items = engine.listMcpSessions(100); } catch {}
6709
+ return items.filter((it) => it.id === mcpChatSid || it.count > 0);
6710
+ }
6711
+
6712
+ function mcpChatQueuePush(text, attachments) {
6713
+ let q = mcpChatQueue.get('mcp');
6714
+ if (!q) {
6715
+ q = { timer: null, msgs: [] };
6716
+ mcpChatQueue.set('mcp', q);
6717
+ }
6718
+ q.msgs.push({
6719
+ text,
6720
+ attachments: Array.isArray(attachments) && attachments.length ? attachments : undefined,
6721
+ });
6722
+ return q;
6723
+ }
6724
+
6725
+ function mcpChatFlush() {
6726
+ const q = mcpChatQueue.get('mcp');
6727
+ if (!q || !q.msgs.length) return;
6728
+ const s = mcpChatSession();
6729
+ if (engine.isBusy(s.id)) return; /* hâlâ çalışıyor — steer yolu mesajı zaten aldı */
6730
+ let merged = '';
6731
+ let mergedAtts = null;
6732
+ for (const m of q.msgs) {
6733
+ if (m.text) merged += (merged ? '\n' : '') + m.text;
6734
+ if (!mergedAtts && Array.isArray(m.attachments) && m.attachments.length) mergedAtts = m.attachments;
6735
+ }
6736
+ mcpChatQueue.delete('mcp');
6737
+ clearTimeout(q.timer);
6738
+ if (!merged.trim() && !mergedAtts) return;
6739
+ try {
6740
+ engine.send(s.id, mergedAtts ? { text: merged, attachments: mergedAtts } : merged, { userAction: true });
6741
+ } catch (e) {
6742
+ /* senkron patlama: panel busy'de kilitlenmesin — hata olayı düş */
6743
+ try {
6744
+ if (win && !win.isDestroyed()) {
6745
+ win.webContents.send('agent:event', { sessionId: s.id, type: 'error', error: String((e && e.message) || e) });
6746
+ }
6747
+ } catch {}
6748
+ }
6749
+ }
6750
+
6751
+ function mcpChatFlushOnDone(ev) {
6752
+ if (!ev || (ev.type !== 'done' && ev.type !== 'error') || !ev.sessionId) return;
6753
+ if (mcpChatQueue.has('mcp') && String(ev.sessionId) === String(mcpChatSid)) {
6754
+ setTimeout(() => { try { mcpChatFlush(); } catch {} }, 150);
6755
+ }
6756
+ }
6757
+
6758
+ ipcMain.handle('mcp:chat:open', async (_e, payload) => {
6759
+ if (!engine) return { ok: false, error: 'ajan hazır değil' };
6760
+ /* bekleyen debounce kuyruğu varsa ÖNCE eski oturuma teslim et (mesaj kaybolmaz) */
6761
+ try {
6762
+ if (mcpChatQueue.has('mcp') && mcpChatSid) mcpChatFlush();
6763
+ } catch {}
6764
+ const q = mcpChatQueue.get('mcp');
6765
+ if (q) {
6766
+ clearTimeout(q.timer);
6767
+ mcpChatQueue.delete('mcp');
6768
+ }
6769
+ const id = String((payload && payload.id) || '');
6770
+ let s;
6771
+ if (id) {
6772
+ try { s = engine._load(id); } catch { return { ok: false, error: 'oturum açılamadı' }; }
6773
+ if (s.bgTitle !== 'Beast MCP') return { ok: false, error: 'bu oturum MCP sohbeti değil' };
6774
+ mcpChatSid = id;
6775
+ mcpChatFresh = false;
6776
+ } else {
6777
+ s = mcpChatSession();
6778
+ }
6779
+ engine.cache.set(s.id, s);
6780
+ return {
6781
+ ok: true,
6782
+ sessionId: s.id,
6783
+ busy: !!engine.isBusy(s.id),
6784
+ messages: mcpChatMessages(s),
6785
+ items: mcpChatHistory(),
6786
+ };
6787
+ });
6788
+
6789
+ ipcMain.handle('mcp:chat:send', async (_e, payload) => {
6790
+ const text = String((payload && payload.msg) || '').trim();
6791
+ const attachments = Array.isArray(payload && payload.attachments) ? payload.attachments : [];
6792
+ if (!text && !attachments.length) return { ok: false, error: 'boş mesaj' };
6793
+ if (!engine) return { ok: false, error: 'ajan hazır değil' };
6794
+ if (!engine.publicState().hasModel) return { ok: false, error: 'model yok — Ayarlar → Provider sekmesinden ekle' };
6795
+ const s = mcpChatSession();
6796
+ if (engine.isBusy(s.id)) {
6797
+ /* MAIN CHAT STEER: koşan tur varken mesaj ANINDA konuşmaya eklenir */
6798
+ engine.send(s.id, attachments.length ? { text, attachments } : text, { userAction: true });
6799
+ return { ok: true, sessionId: s.id, steered: true };
6800
+ }
6801
+ const q = mcpChatQueuePush(text, attachments);
6802
+ clearTimeout(q.timer);
6803
+ q.timer = setTimeout(() => { try { mcpChatFlush(); } catch {} }, MCP_CHAT_DEBOUNCE_MS);
6804
+ return { ok: true, sessionId: s.id, pending: true };
6805
+ });
6806
+
6807
+ ipcMain.handle('mcp:chat:stop', async () => {
6808
+ const q = mcpChatQueue.get('mcp');
6809
+ if (q) {
6810
+ clearTimeout(q.timer);
6811
+ mcpChatQueue.delete('mcp');
6812
+ }
6813
+ const sid = mcpChatSid;
6814
+ const wasBusy = sid ? engine.isBusy(sid) : false;
6815
+ let r = false;
6816
+ if (wasBusy) {
6817
+ try { r = engine.interrupt(sid, 'kullanıcı MCP sohbet panelinden ■ ile durdurdu'); } catch {}
6818
+ }
6819
+ return { ok: true, wasBusy, interrupted: r };
6820
+ });
6821
+
6822
+ ipcMain.handle('mcp:chat:new', async () => {
6823
+ if (!engine) return { ok: false, error: 'ajan hazır değil' };
6824
+ const sid = mcpChatSid;
6825
+ if (sid && engine.isBusy(sid)) return { ok: false, error: 'mesaj sürüyor — önce ■ ile durdur' };
6826
+ const q = mcpChatQueue.get('mcp');
6827
+ if (q) {
6828
+ clearTimeout(q.timer);
6829
+ mcpChatQueue.delete('mcp');
6830
+ }
6831
+ mcpChatSid = '';
6832
+ mcpChatFresh = true; /* sonraki oturum TAZE açılır; eski sohbet geçmişte kalır */
6833
+ return { ok: true, sessionId: '', messages: [], items: mcpChatHistory() };
6834
+ });
6835
+
6836
+ ipcMain.handle('mcp:chat:history', async () => {
6837
+ try {
6838
+ return { ok: true, items: mcpChatHistory() };
6839
+ } catch (e) {
6840
+ return { ok: false, error: String((e && e.message) || e), items: [] };
6841
+ }
6842
+ });
6843
+
6396
6844
  /* ---------------- FALLOUT IPC ---------------- */
6397
6845
 
6398
6846
  function defaultFallout() {
@@ -13077,8 +13525,131 @@ ipcMain.handle('email:send', (_e, msg) => emailSend(msg || {}));
13077
13525
 
13078
13526
  /* ---------- ekran görüntüsü ---------- */
13079
13527
 
13080
- /* Ana ekrandan JPEG dataURL yakalar (computer_use ve screen:capture ortak) */
13081
- async function captureScreenDataUrl() {
13528
+ /* Son ekran görüntüsünün kaynak ekran sınırları + görüntü boyutu — computer_act
13529
+ koordinat dönüşümü buradan yapılır (görüntü 1280px'e ölçeklenir). */
13530
+ let lastScreenCapture = null;
13531
+
13532
+ /* Canvas (görüntü üzerine çizim) — yoksa annotasyon sessizce atlanır */
13533
+ let _canvasLib;
13534
+ function canvasLib() {
13535
+ if (_canvasLib !== undefined) return _canvasLib;
13536
+ try {
13537
+ _canvasLib = require('@napi-rs/canvas');
13538
+ } catch {
13539
+ _canvasLib = null;
13540
+ }
13541
+ return _canvasLib;
13542
+ }
13543
+
13544
+ /* Gerçek imleç konumu (400 ms önbellek) — computer_look işaretçisi için */
13545
+ let _cursorCache = { at: 0, x: 0, y: 0 };
13546
+ function cursorPosition() {
13547
+ return new Promise((resolve) => {
13548
+ if (Date.now() - _cursorCache.at < 400) return resolve({ x: _cursorCache.x, y: _cursorCache.y });
13549
+ let out = '';
13550
+ try {
13551
+ const p = spawn(
13552
+ 'powershell.exe',
13553
+ ['-NoProfile', '-NonInteractive', '-Command', 'Add-Type -AssemblyName System.Windows.Forms; $p=[System.Windows.Forms.Cursor]::Position; "$($p.X),$($p.Y)"'],
13554
+ { windowsHide: true }
13555
+ );
13556
+ const t = setTimeout(() => { try { p.kill(); } catch {} resolve(null); }, 4000);
13557
+ p.stdout.on('data', (d) => { out += String(d); });
13558
+ p.on('error', () => { clearTimeout(t); resolve(null); });
13559
+ p.on('close', () => {
13560
+ clearTimeout(t);
13561
+ const m = /(\d+)\s*,\s*(\d+)/.exec(out);
13562
+ if (m) {
13563
+ _cursorCache = { at: Date.now(), x: Number(m[1]), y: Number(m[2]) };
13564
+ resolve({ x: _cursorCache.x, y: _cursorCache.y });
13565
+ } else resolve(null);
13566
+ });
13567
+ } catch {
13568
+ resolve(null);
13569
+ }
13570
+ });
13571
+ }
13572
+
13573
+ /* Ekran görüntüsü üzerine 128px ızgara + imleç işareti çiz: model koordinatı
13574
+ görselden okuyabilir. Izgara çizgileri yarı saydam — içeriği boğmaz. */
13575
+ async function annotateScreenShot(img) {
13576
+ const lib = canvasLib();
13577
+ if (!lib) return null;
13578
+ const im = await lib.loadImage(img.toPNG());
13579
+ const canvas = lib.createCanvas(im.width, im.height);
13580
+ const ctx = canvas.getContext('2d');
13581
+ ctx.drawImage(im, 0, 0);
13582
+ const step = 128;
13583
+ ctx.lineWidth = 1;
13584
+ ctx.font = 'bold 12px Segoe UI, sans-serif';
13585
+ for (let x = step; x < im.width; x += step) {
13586
+ ctx.strokeStyle = 'rgba(0,200,255,0.28)';
13587
+ ctx.beginPath();
13588
+ ctx.moveTo(x + 0.5, 0);
13589
+ ctx.lineTo(x + 0.5, im.height);
13590
+ ctx.stroke();
13591
+ const t = String(x);
13592
+ const tw = ctx.measureText(t).width;
13593
+ ctx.fillStyle = 'rgba(0,0,0,0.62)';
13594
+ ctx.fillRect(x + 1, 1, tw + 7, 15);
13595
+ ctx.fillStyle = '#00d8ff';
13596
+ ctx.fillText(t, x + 4, 12.5);
13597
+ }
13598
+ for (let y = step; y < im.height; y += step) {
13599
+ ctx.strokeStyle = 'rgba(0,200,255,0.28)';
13600
+ ctx.beginPath();
13601
+ ctx.moveTo(0, y + 0.5);
13602
+ ctx.lineTo(im.width, y + 0.5);
13603
+ ctx.stroke();
13604
+ const t = String(y);
13605
+ const tw = ctx.measureText(t).width;
13606
+ ctx.fillStyle = 'rgba(0,0,0,0.62)';
13607
+ ctx.fillRect(1, y + 1, tw + 7, 15);
13608
+ ctx.fillStyle = '#00d8ff';
13609
+ ctx.fillText(t, 4, y + 12.5);
13610
+ }
13611
+ try {
13612
+ /* imleç sorgusu en çok 1.5 sn bekletir — look hızlı kalmalı */
13613
+ const cur = await Promise.race([
13614
+ cursorPosition(),
13615
+ new Promise((r) => setTimeout(() => r(null), 1500)),
13616
+ ]);
13617
+ const cap = lastScreenCapture;
13618
+ if (cur && cap && cap.w > 0 && cap.h > 0) {
13619
+ const kx = cap.imgW / cap.w;
13620
+ const ky = cap.imgH / cap.h;
13621
+ const cx = (cur.x - cap.x) * kx;
13622
+ const cy = (cur.y - cap.y) * ky;
13623
+ if (cx >= -24 && cy >= -24 && cx <= im.width + 24 && cy <= im.height + 24) {
13624
+ ctx.strokeStyle = '#ff3b5c';
13625
+ ctx.lineWidth = 2;
13626
+ ctx.beginPath();
13627
+ ctx.arc(cx, cy, 14, 0, Math.PI * 2);
13628
+ ctx.stroke();
13629
+ ctx.beginPath();
13630
+ ctx.moveTo(cx - 22, cy);
13631
+ ctx.lineTo(cx + 22, cy);
13632
+ ctx.moveTo(cx, cy - 22);
13633
+ ctx.lineTo(cx, cy + 22);
13634
+ ctx.stroke();
13635
+ const label = 'imlec (' + Math.round(cx) + ',' + Math.round(cy) + ')';
13636
+ const tw = ctx.measureText(label).width;
13637
+ const bx = Math.min(Math.max(2, cx + 18), Math.max(2, im.width - tw - 12));
13638
+ const by = Math.min(Math.max(16, cy - 34), im.height - 6);
13639
+ ctx.fillStyle = 'rgba(0,0,0,0.78)';
13640
+ ctx.fillRect(bx - 3, by - 13, tw + 8, 16);
13641
+ ctx.fillStyle = '#ff8ba0';
13642
+ ctx.fillText(label, bx, by);
13643
+ }
13644
+ }
13645
+ } catch {}
13646
+ return canvas.toBuffer('image/jpeg', 78);
13647
+ }
13648
+
13649
+ /* Ana ekrandan JPEG dataURL yakalar (computer_use ve screen:capture ortak).
13650
+ opts.annotate: ızgara + imleç işareti (yalnız ajan computer_look kullanır;
13651
+ OCR/komut yolları düz görüntü ister). */
13652
+ async function captureScreenDataUrl(opts) {
13082
13653
  try {
13083
13654
  const sources = await desktopCapturer.getSources({
13084
13655
  types: ['screen'],
@@ -13092,7 +13663,22 @@ async function captureScreenDataUrl() {
13092
13663
  if (a.width * a.height > b.width * b.height) best = s;
13093
13664
  }
13094
13665
  const img = best.thumbnail.resize({ width: 1280 });
13095
- const jpeg = img.toJPEG(72);
13666
+ /* KAYNAK EKRAN SINIRLARI: ajan görüntüyü 1280px tabanlı okur; act çağrısı
13667
+ bu sınırlara göre gerçek ekran koordinatına çevrilir (çok monitörde ofset dahil) */
13668
+ try {
13669
+ const all = screen.getAllDisplays();
13670
+ const disp = all.find((d) => String(d.id) === String(best.display_id)) || screen.getPrimaryDisplay();
13671
+ const b = (disp && disp.bounds) || { x: 0, y: 0, width: 1280, height: 720 };
13672
+ const size = img.getSize();
13673
+ lastScreenCapture = { x: b.x, y: b.y, w: b.width, h: b.height, imgW: size.width, imgH: size.height };
13674
+ } catch {
13675
+ lastScreenCapture = null;
13676
+ }
13677
+ let jpeg = null;
13678
+ if (opts && opts.annotate) {
13679
+ try { jpeg = await annotateScreenShot(img); } catch {}
13680
+ }
13681
+ if (!jpeg || !jpeg.length) jpeg = img.toJPEG(72);
13096
13682
  if (!jpeg || !jpeg.length) return null;
13097
13683
  return 'data:image/jpeg;base64,' + jpeg.toString('base64');
13098
13684
  } catch {
@@ -13100,6 +13686,87 @@ async function captureScreenDataUrl() {
13100
13686
  }
13101
13687
  }
13102
13688
 
13689
+ /* ---- görsel parmak izi: aksiyon "işe yaradı mı" (verify) ---- */
13690
+ function imageSignature(img) {
13691
+ try {
13692
+ if (!img) return null;
13693
+ const small = img.getSize().width > 64 ? img.resize({ width: 64 }) : img;
13694
+ const sz = small.getSize();
13695
+ return computeruse.bitmapSignature(small.toBitmap(), sz.width, sz.height);
13696
+ } catch {
13697
+ return null;
13698
+ }
13699
+ }
13700
+
13701
+ async function pageSignature(wc) {
13702
+ try {
13703
+ return imageSignature(await wc.capturePage());
13704
+ } catch {
13705
+ return null;
13706
+ }
13707
+ }
13708
+
13709
+ /* Ekran değişim imzası — küçük thumbnail ile hızlı (computer_act verify) */
13710
+ async function screenSignature() {
13711
+ try {
13712
+ const sources = await desktopCapturer.getSources({ types: ['screen'], thumbnailSize: { width: 128, height: 72 } });
13713
+ if (!sources.length) return null;
13714
+ let best = sources[0];
13715
+ for (const s of sources) {
13716
+ const a = s.thumbnail.getSize();
13717
+ const b = best.thumbnail.getSize();
13718
+ if (a.width * a.height > b.width * b.height) best = s;
13719
+ }
13720
+ return imageSignature(best.thumbnail);
13721
+ } catch {
13722
+ return null;
13723
+ }
13724
+ }
13725
+
13726
+ const CU_VERIFY_OPS = new Set(['click', 'dblclick', 'rightclick', 'type', 'key', 'drag']);
13727
+
13728
+ /* computer_act köprüsü: model 1280px tabanlı görüntü koordinatı verir —
13729
+ gerçek ekran koordinatına ölçekle (1280'den geniş her ekranda tıklamalar
13730
+ yanlış yere gidiyordu). Aksiyon sonrası görsel değişimi de raporlar. */
13731
+ async function computerActScaled(op, args) {
13732
+ const a = { ...(args || {}) };
13733
+ let cap = lastScreenCapture;
13734
+ if (!cap) {
13735
+ try {
13736
+ const b = screen.getPrimaryDisplay().bounds;
13737
+ const imgH = Math.max(1, Math.round((1280 * b.height) / b.width));
13738
+ cap = { x: b.x, y: b.y, w: b.width, h: b.height, imgW: 1280, imgH };
13739
+ } catch {
13740
+ cap = null;
13741
+ }
13742
+ }
13743
+ const opName = String(op || '').toLowerCase();
13744
+ const scaled = cap && cap.imgW > 0 ? computeruse.scaleArgs(a, cap) : a;
13745
+ const verify = CU_VERIFY_OPS.has(opName) && a.verify !== false;
13746
+ const before = verify ? await screenSignature() : null;
13747
+ let r;
13748
+ try {
13749
+ r = await computeruse.act(opName, scaled);
13750
+ } catch (e) {
13751
+ return { ok: false, error: String((e && e.message) || e) };
13752
+ }
13753
+ if (before && r && r.ok) {
13754
+ await new Promise((res) => setTimeout(res, 350)); /* UI tepkisi otursun */
13755
+ const after = await screenSignature();
13756
+ const ratio = computeruse.signatureDiff(before, after);
13757
+ if (ratio != null) {
13758
+ r.changed = ratio >= 0.02;
13759
+ r.changeRatio = Number(ratio.toFixed(3));
13760
+ if (!r.changed && ['click', 'dblclick', 'rightclick', 'type'].includes(opName)) {
13761
+ r.warning =
13762
+ 'ekranda görünür değişiklik yok — aksiyon etkisiz olabilir (yanlış pencere/hedef ya da odak kaybı). ' +
13763
+ 'computer_look ile durumu kontrol et; focus ile pencereyi öne getirip tekrar dene.';
13764
+ }
13765
+ }
13766
+ }
13767
+ return r;
13768
+ }
13769
+
13103
13770
  ipcMain.handle('screen:capture', async () => {
13104
13771
  const image = await captureScreenDataUrl();
13105
13772
  return image ? { ok: true, image } : { ok: false, error: 'ekran görüntüsü alınamadı' };