beast-agent 2.44.13 → 2.45.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).
@@ -4135,6 +4136,26 @@ function ensureBrowser() {
4135
4136
  if (/^https?:\/\//i.test(url)) wc.loadURL(url).catch(() => {});
4136
4137
  return { action: 'deny' };
4137
4138
  });
4139
+ /* JS DİYALOGLARI (alert/confirm/prompt/beforeunload): CDP ile OTOMATİK
4140
+ onaylanır/kapatılır — aksi halde renderer kilitlenir ve TÜM tarayıcı
4141
+ araçları askıda kalır (executeJavaScript diyalog kapanana dek dönmez). */
4142
+ try {
4143
+ if (!wc.debugger.isAttached()) wc.debugger.attach('1.3');
4144
+ wc.debugger.sendCommand('Page.enable').catch(() => {});
4145
+ wc.debugger.on('message', (_ev, method, params) => {
4146
+ if (method === 'Page.javascriptDialogOpening') {
4147
+ browser.lastDialog = {
4148
+ type: String((params && params.type) || 'alert'),
4149
+ message: String((params && params.message) || ''),
4150
+ at: Date.now(),
4151
+ };
4152
+ try { wc.debugger.sendCommand('Page.handleJavaScriptDialog', { accept: true }).catch(() => {}); } catch {}
4153
+ blog('dialog', browser.lastDialog.type + ': ' + browser.lastDialog.message.slice(0, 80));
4154
+ }
4155
+ });
4156
+ } catch (e) {
4157
+ waLog('tarayıcı diyalog köprüsü kurulamadı: ' + String((e && e.message) || e).slice(0, 120));
4158
+ }
4138
4159
  // Tarayıcı kapalıyken olayların UI'ı geri açmamasını garantile
4139
4160
  const notify = (extra) => {
4140
4161
  if (browser.open && win && !win.isDestroyed()) {
@@ -4251,6 +4272,61 @@ async function browserNavigate(raw, signal, ctx) {
4251
4272
  });
4252
4273
  }
4253
4274
 
4275
+ /* JS diyalogları (alert/confirm/prompt) CDP ile OTOMATİK yönetilir;
4276
+ bu yardımcı çağrı SIRASINDA oluşan diyaloğu yanıta ekler. */
4277
+ function browserDialogNote(sinceAt) {
4278
+ const d = browser.lastDialog;
4279
+ if (!d || !d.at || d.at <= (sinceAt || 0)) return {};
4280
+ const verb = d.type === 'alert' || d.type === 'prompt' ? 'kapatıldı' : 'onaylandı';
4281
+ return {
4282
+ dialog: { type: d.type, message: d.message },
4283
+ dialogNote: 'JS diyaloğu otomatik ' + verb + ': "' + String(d.message || '').slice(0, 120) + '"',
4284
+ };
4285
+ }
4286
+
4287
+ /* SPA/DOM oturması: readyState complete + içerik uzunluğu İKİ turdur sabitse
4288
+ dön (en çok maxMs). Böylece hidrasyon bitmeden snapshot/okuma yapılmaz. */
4289
+ async function browserSettle(wc, signal, maxMs = 4000) {
4290
+ const t0 = Date.now();
4291
+ let lastLen = -1;
4292
+ let stable = 0;
4293
+ while (Date.now() - t0 < maxMs) {
4294
+ if (signal && signal.aborted) return;
4295
+ let info = null;
4296
+ try {
4297
+ info = await wc.executeJavaScript(
4298
+ '({rs: document.readyState, len: (document.body ? ((document.body.innerText || "").trim().length) : 0)})',
4299
+ true
4300
+ );
4301
+ } catch {
4302
+ return;
4303
+ }
4304
+ if (!info) return;
4305
+ const rs = String(info.rs || '');
4306
+ const len = Number(info.len) || 0;
4307
+ if (rs === 'complete') {
4308
+ if (len > 0 && len === lastLen) stable++;
4309
+ else stable = 0;
4310
+ if (stable >= 2) return;
4311
+ /* boş/az metinli sayfa (canvas/SPA shell): yine de kısa bir hidrasyon penceresi bırak */
4312
+ if (len === 0 && Date.now() - t0 > 1500) return;
4313
+ }
4314
+ lastLen = len;
4315
+ await new Promise((r) => setTimeout(r, 300));
4316
+ }
4317
+ }
4318
+
4319
+ /* snapshot al (hatayı yut) — navigate/act yanıtlarına gömmek için */
4320
+ async function browserSnapshotNow(wc) {
4321
+ try {
4322
+ const raw = await wc.executeJavaScript(BROWSER_SNAPSHOT_JS, true);
4323
+ const obj = JSON.parse(raw);
4324
+ return obj && typeof obj.count === 'number' ? obj : null;
4325
+ } catch {
4326
+ return null;
4327
+ }
4328
+ }
4329
+
4254
4330
  async function browserNavigateNow(raw, signal, ctx) {
4255
4331
  let url = String(raw || '').trim();
4256
4332
  if (!url) return { ok: false, error: 'boş adres' };
@@ -4262,11 +4338,27 @@ async function browserNavigateNow(raw, signal, ctx) {
4262
4338
  }
4263
4339
  setBrowserOpenForAgent();
4264
4340
  const wc = browser.view.webContents;
4341
+ const dlgAt = browser.lastDialog ? browser.lastDialog.at : 0;
4342
+ let failInfo = null;
4343
+ let timedOut = false;
4265
4344
  await new Promise((resolve) => {
4266
4345
  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(); };
4346
+ const finish = () => {
4347
+ if (settled) return;
4348
+ settled = true;
4349
+ clearTimeout(timer);
4350
+ cleanup();
4351
+ resolve();
4352
+ };
4353
+ const timer = setTimeout(() => { timedOut = true; finish(); }, 25000);
4354
+ /* did-fail-load: ANA kare hatasını YAKALA ve rapora koy. kod -3 (ABORTED)
4355
+ yönlendirme/SPA geçişlerinde normaldir; alt kare hataları sayılmaz. */
4356
+ const onFail = (_e, code, desc, validatedURL, isMainFrame) => {
4357
+ if (isMainFrame === false) return;
4358
+ if (code === -3) return;
4359
+ failInfo = { code, desc: String(desc || ''), url: String(validatedURL || url) };
4360
+ finish();
4361
+ };
4270
4362
  function cleanup() {
4271
4363
  clearTimeout(timer);
4272
4364
  wc.removeListener('did-finish-load', finish);
@@ -4274,24 +4366,44 @@ async function browserNavigateNow(raw, signal, ctx) {
4274
4366
  }
4275
4367
  wc.once('did-finish-load', finish);
4276
4368
  wc.on('did-fail-load', onFail);
4277
- wc.loadURL(url).catch(() => {});
4369
+ wc.loadURL(url).catch((e) => {
4370
+ failInfo = { code: null, desc: String((e && e.message) || e) };
4371
+ finish();
4372
+ });
4278
4373
  });
4279
4374
  let title = '';
4280
4375
  let finalUrl = url;
4281
4376
  try { title = wc.getTitle(); finalUrl = wc.getURL() || url; } catch {}
4282
4377
  browserEmit({ open: true, width: browserShownWidth(browserW()), url: finalUrl });
4283
4378
  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 {}
4379
+ /* YÜKLEME HATASI: artık "açıldı" denmez — kod/açıklama + çözüm ipucu döner */
4380
+ if (failInfo) {
4381
+ const codeTxt = failInfo.code != null ? ' kod ' + failInfo.code : '';
4382
+ const hint = /ERR_NAME_NOT_RESOLVED|ERR_INTERNET_DISCONNECTED/i.test(failInfo.desc)
4383
+ ? ' (DNS/ağ: internet bağlantısını ve adresi kontrol et)'
4384
+ : /ERR_CONNECTION_REFUSED|ERR_CONNECTION_TIMED_OUT|ERR_EMPTY_RESPONSE/i.test(failInfo.desc)
4385
+ ? ' (sunucu yanıt vermiyor — adres/port doğru mu?)'
4386
+ : '';
4387
+ return {
4388
+ ok: false,
4389
+ error: 'sayfa yüklenemedi' + codeTxt + ': ' + failInfo.desc + hint,
4390
+ url: failInfo.url || finalUrl,
4391
+ ...browserDialogNote(dlgAt),
4392
+ };
4393
+ }
4394
+ /* SPA hidrasyonu: içerik oturmadan snapshot alınmaz (timeout'ta bekleme yok) */
4395
+ await browserSettle(wc, signal, timedOut ? 0 : 4000);
4396
+ const snap = await browserSnapshotNow(wc);
4397
+ let stillLoading = false;
4398
+ try { stillLoading = wc.isLoading(); } catch {}
4291
4399
  return {
4292
4400
  ok: true,
4401
+ ...(timedOut
4402
+ ? { warning: 'yükleme 25 sn içinde tamamlanmadı — sayfa kısmen yüklü olabilir; browser_wait/browser_read ile kontrol et', loading: stillLoading }
4403
+ : {}),
4293
4404
  url: finalUrl,
4294
4405
  title,
4406
+ ...browserDialogNote(dlgAt),
4295
4407
  ...(snap
4296
4408
  ? { 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
4409
  : { note: 'sayfa acildi — etkileşimli elemanlar için browser_snapshot al' }),
@@ -4691,21 +4803,70 @@ async function browserSearchNow(query, signal, ctx) {
4691
4803
  }
4692
4804
  }
4693
4805
 
4694
- async function browserScreenshot(signal) {
4806
+ /* Ekran görüntüsü üzerine ref numaralarını ÇİZ: vision modeli kutuları ve
4807
+ numaraları görür, koordinatı tahmin etmek yerine okur. Ref'ler snapshot ile
4808
+ AYNI numaralandırmayı kullanır (snapshot hemen önce alınır). */
4809
+ async function annotateBrowserShot(img, snap) {
4810
+ const lib = canvasLib();
4811
+ if (!lib || !snap || !snap.boxes) return null;
4812
+ const im = await lib.loadImage(img.toPNG());
4813
+ const canvas = lib.createCanvas(im.width, im.height);
4814
+ const ctx = canvas.getContext('2d');
4815
+ ctx.drawImage(im, 0, 0);
4816
+ const vw = Number(snap.vw) || im.width;
4817
+ const k = im.width / Math.max(1, vw);
4818
+ let drawn = 0;
4819
+ for (const [ref, b] of Object.entries(snap.boxes)) {
4820
+ const x = Number(b[0]) * k;
4821
+ const y = Number(b[1]) * k;
4822
+ const w = Number(b[2]) * k;
4823
+ const h = Number(b[3]) * k;
4824
+ if (!(w >= 6 && h >= 6)) continue;
4825
+ if (y > im.height || x > im.width || y + h < 0 || x + w < 0) continue;
4826
+ ctx.strokeStyle = 'rgba(0,190,255,0.95)';
4827
+ ctx.lineWidth = 1.5;
4828
+ ctx.strokeRect(x + 0.5, y + 0.5, Math.max(2, w), Math.max(2, h));
4829
+ const label = String(ref);
4830
+ ctx.font = 'bold 13px Segoe UI, sans-serif';
4831
+ const tw = Math.ceil(ctx.measureText(label).width);
4832
+ const by = y - 16 < 0 ? y + 1 : y - 16;
4833
+ ctx.fillStyle = 'rgba(0,0,0,0.78)';
4834
+ ctx.fillRect(x, by, tw + 8, 16);
4835
+ ctx.fillStyle = '#00d8ff';
4836
+ ctx.fillText(label, x + 4, by + 12.5);
4837
+ if (++drawn >= 90) break;
4838
+ }
4839
+ return canvas.toBuffer('image/jpeg', 78);
4840
+ }
4841
+
4842
+ async function browserScreenshot(signal, opts) {
4695
4843
  if (!browser.view || !browser.open) return { ok: false, error: 'tarayıcı açık değil' };
4696
4844
  const wc = browser.view.webContents;
4845
+ const annotate = !(opts && opts.annotate === false);
4846
+ const dlgAt = browser.lastDialog ? browser.lastDialog.at : 0;
4697
4847
  try {
4848
+ /* ref numaraları görselle eşleşsin: önce taze snapshot */
4849
+ let snap = null;
4850
+ if (annotate) snap = await browserSnapshotNow(wc);
4698
4851
  const img = await wc.capturePage();
4699
4852
  let out = img;
4700
4853
  const sz = img.getSize();
4701
4854
  const maxW = 1000; // vision bütçesi için küçült
4702
4855
  if (sz.width > maxW) out = img.resize({ width: maxW });
4703
- const jpeg = out.toJPEG(72);
4856
+ let jpeg = null;
4857
+ if (annotate) {
4858
+ try { jpeg = await annotateBrowserShot(out, snap); } catch {}
4859
+ }
4860
+ if (!jpeg || !jpeg.length) jpeg = out.toJPEG(72);
4704
4861
  if (!jpeg || !jpeg.length) return { ok: false, error: 'görüntü alınamadı' };
4705
4862
  return {
4706
4863
  ok: true,
4707
- note: 'ekran görüntüsü bir sonraki adımda sana gösterilecek',
4864
+ note: snap
4865
+ ? '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'
4866
+ : 'ekran görüntüsü bir sonraki adımda sana gösterilecek',
4708
4867
  __injectImage: 'data:image/jpeg;base64,' + jpeg.toString('base64'),
4868
+ ...(snap ? { snapshot: snap.snapshot, refCount: snap.count } : {}),
4869
+ ...browserDialogNote(dlgAt),
4709
4870
  url: wc.getURL(),
4710
4871
  title: wc.getTitle(),
4711
4872
  bytes: jpeg.length,
@@ -4754,6 +4915,7 @@ const BROWSER_SNAPSHOT_JS = `(function(){
4754
4915
  window.__beMap={};
4755
4916
  const seen=new Set();
4756
4917
  const lines=[];
4918
+ const boxes={};
4757
4919
  let i=0;
4758
4920
  function add(e){
4759
4921
  if(!e||seen.has(e)) return;
@@ -4761,6 +4923,8 @@ const BROWSER_SNAPSHOT_JS = `(function(){
4761
4923
  if(!__vis(e)||e.disabled||e.getAttribute('aria-disabled')==='true') return;
4762
4924
  i++;
4763
4925
  window.__beMap[i]=e;
4926
+ const rb=e.getBoundingClientRect();
4927
+ boxes[i]=[Math.round(rb.left),Math.round(rb.top),Math.round(rb.width),Math.round(rb.height)];
4764
4928
  const tag=e.tagName.toLowerCase();
4765
4929
  const type=(e.getAttribute&&e.getAttribute('type'))||'';
4766
4930
  let s='['+i+'] <'+tag+(type?' type='+type:'')+'>';
@@ -4776,7 +4940,7 @@ const BROWSER_SNAPSHOT_JS = `(function(){
4776
4940
  lines.push('--- SAYFA ---');
4777
4941
  }
4778
4942
  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')});
4943
+ return JSON.stringify({count:i,title:document.title,url:location.href,snapshot:lines.join('\\n'),boxes:boxes,vw:window.innerWidth,vh:window.innerHeight});
4780
4944
  })()`;
4781
4945
 
4782
4946
  function browserActionJs(kind, args) {
@@ -4953,6 +5117,10 @@ async function browserSnapshot(signal) {
4953
5117
  async function browserAct(kind, args, signal) {
4954
5118
  if (!browser.view || !browser.open) return { ok: false, error: 'tarayıcı açık değil' };
4955
5119
  const wc = browser.view.webContents;
5120
+ const dlgAt = browser.lastDialog ? browser.lastDialog.at : 0;
5121
+ /* DOĞRULAMA: eylem öncesi görsel parmak izi (yalnız anlamlı eylemlerde) */
5122
+ const verify = ['click', 'type', 'select', 'press'].includes(kind) && !(args && args.verify === false);
5123
+ const sigBefore = verify ? await pageSignature(wc) : null;
4956
5124
  try {
4957
5125
  const raw = await wc.executeJavaScript(browserActionJs(kind, args || {}), true);
4958
5126
  let obj = {};
@@ -4969,6 +5137,20 @@ async function browserAct(kind, args, signal) {
4969
5137
  wc.on('did-navigate', onNav);
4970
5138
  });
4971
5139
  }
5140
+ /* SPA: sayfa değiştiyse DOM oturmadan snapshot alınmaz */
5141
+ if (navigated) await browserSettle(wc, signal, 2500);
5142
+
5143
+ /* DOĞRULAMA: eylem sonrası değişim oranı — "tıkladım ama bir şey olmadı" tuzağını yakalar */
5144
+ let changed = null;
5145
+ let changeRatio = null;
5146
+ if (sigBefore) {
5147
+ const sigAfter = await pageSignature(wc);
5148
+ const ratio = computeruse.signatureDiff(sigBefore, sigAfter);
5149
+ if (ratio != null) {
5150
+ changed = ratio >= 0.02;
5151
+ changeRatio = Number(ratio.toFixed(3));
5152
+ }
5153
+ }
4972
5154
 
4973
5155
  blog(
4974
5156
  kind,
@@ -4981,12 +5163,7 @@ async function browserAct(kind, args, signal) {
4981
5163
  );
4982
5164
 
4983
5165
  /* 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 {}
5166
+ const freshSnap = await browserSnapshotNow(wc);
4990
5167
 
4991
5168
  return {
4992
5169
  ok: true,
@@ -4995,6 +5172,16 @@ async function browserAct(kind, args, signal) {
4995
5172
  url: wc.getURL(),
4996
5173
  title: wc.getTitle(),
4997
5174
  navigated,
5175
+ ...(changed != null ? { changed, changeRatio } : {}),
5176
+ ...(changed === false && ['click', 'type', 'select'].includes(kind)
5177
+ ? {
5178
+ warning:
5179
+ 'görünür değişiklik yok (changeRatio ' + changeRatio + ') — eylem etkisiz olabilir: ' +
5180
+ 'ref bayat olabilir (browser_snapshot al ve güncel ref ile tekrar dene), öğe kapalı/gizli olabilir ' +
5181
+ 'ya da tıklama sayfa tarafından engellenmiş olabilir. browser_wait ile kısa bekleme dene.',
5182
+ }
5183
+ : {}),
5184
+ ...browserDialogNote(dlgAt),
4998
5185
  ...(freshSnap ? { snapshot: freshSnap.snapshot, refCount: freshSnap.count } : {}),
4999
5186
  ...(navigated
5000
5187
  ? { note: 'sayfa degisti — yanıtta güncel snapshot var; refler eskiyse yeni browser_snapshot al' }
@@ -5008,6 +5195,61 @@ async function browserAct(kind, args, signal) {
5008
5195
  }
5009
5196
  }
5010
5197
 
5198
+ /* BEKLE: selector/text/ref görünene (gone:true ise kaybolana) kadar ya da ms
5199
+ kadar bekle. SPA'lar, gecikmeli açılan menüler ve ağ yavaşlığı için —
5200
+ körlemesine tekrar denemek yerine deterministik bekleme. */
5201
+ async function browserWait(args, signal) {
5202
+ if (!browser.view || !browser.open) return { ok: false, error: 'tarayıcı açık değil' };
5203
+ const wc = browser.view.webContents;
5204
+ const a = args || {};
5205
+ const timeout = Math.max(200, Math.min(30000, Number(a.timeout_ms) || 10000));
5206
+ const ms = Math.max(0, Math.min(10000, Number(a.ms) || 0));
5207
+ const hasCond = a.selector != null || a.text != null || a.ref != null;
5208
+ if (!hasCond) {
5209
+ if (!ms) return { ok: false, error: 'selector, text, ref ya da ms ver' };
5210
+ await new Promise((r) => setTimeout(r, ms));
5211
+ return { ok: true, waited_ms: ms, url: wc.getURL(), title: wc.getTitle() };
5212
+ }
5213
+ const gone = !!a.gone;
5214
+ const t0 = Date.now();
5215
+ /* ms verildiyse koşul taramasından ÖNCE sabit bekleme (extra delay) */
5216
+ if (ms) await new Promise((r) => setTimeout(r, ms));
5217
+ const refJs = a.ref != null ? `!!__resolveRef(${JSON.stringify(Number(a.ref))})` : '';
5218
+ const selJs = a.selector != null ? `(function(){try{var el=document.querySelector(${JSON.stringify(String(a.selector))});return !!(el&&__vis(el));}catch(e){return false;}})()` : '';
5219
+ const txtJs = a.text != null ? `((document.body?document.body.innerText:'').toLowerCase().includes(${JSON.stringify(String(a.text).toLowerCase())}))` : '';
5220
+ const probe = `(function(){${BROWSER_JS_HELPERS};try{return !!(${refJs || selJs || txtJs});}catch(e){return false;}})()`;
5221
+ while (Date.now() - t0 < timeout) {
5222
+ if (signal && signal.aborted) return { ok: false, error: 'bekleme iptal edildi' };
5223
+ let found = false;
5224
+ try { found = !!(await wc.executeJavaScript(probe, true)); } catch { found = false; }
5225
+ if (gone ? !found : found) {
5226
+ return {
5227
+ ok: true,
5228
+ waited_ms: Date.now() - t0,
5229
+ ...(gone ? { gone: true } : { found: true }),
5230
+ ...(a.selector != null ? { selector: String(a.selector) } : {}),
5231
+ ...(a.text != null ? { text: String(a.text).slice(0, 60) } : {}),
5232
+ ...(a.ref != null ? { ref: Number(a.ref) } : {}),
5233
+ url: wc.getURL(),
5234
+ title: wc.getTitle(),
5235
+ note: 'koşul sağlandı — şimdi eyleme geç; ref lazımsa browser_snapshot ile güncel ref al',
5236
+ };
5237
+ }
5238
+ await new Promise((r) => setTimeout(r, 300));
5239
+ }
5240
+ return {
5241
+ ok: false,
5242
+ error:
5243
+ 'bekleme zaman aşımı (' + timeout + ' ms): ' +
5244
+ (a.selector != null ? 'selector bulunamadı: ' + String(a.selector).slice(0, 60)
5245
+ : a.text != null ? 'metin bulunamadı: ' + String(a.text).slice(0, 60)
5246
+ : 'ref mevcut değil: ' + Number(a.ref)) +
5247
+ (gone ? ' (hâlâ görünüyor)' : ''),
5248
+ url: wc.getURL(),
5249
+ title: wc.getTitle(),
5250
+ };
5251
+ }
5252
+
5011
5253
  /* oturum açılışında --hidden ile başlarsa pencere gösterme, tepside yaşa */
5012
5254
  const startHidden =
5013
5255
  process.argv.includes('--hidden') ||
@@ -7985,6 +8227,36 @@ function termSend(ev) {
7985
8227
  if (win && !win.isDestroyed()) win.webContents.send('agent:event', ev);
7986
8228
  }
7987
8229
 
8230
+ /* TERMİNAL ÇIKTI COALESCE: alt süreçten gelen data chunk'ları 80ms'de bir TEK
8231
+ 'term-batch' IPC'sinde birleştirilir. Aksi halde saniyede yüzlerce IPC +
8232
+ renderer'da satır satır DOM/layout = "Yanıt vermiyor" donması. */
8233
+ const TERM_BATCH_MS = 80;
8234
+ const TERM_BATCH_BYTES = 48 * 1024;
8235
+ const termBatches = new Map(); // id -> { timer, bytes, events: [{stream,text}] }
8236
+
8237
+ function termQueue(id, stream, text) {
8238
+ const key = String(id || termShellId || '');
8239
+ let b = termBatches.get(key);
8240
+ if (!b) {
8241
+ b = { timer: null, bytes: 0, events: [] };
8242
+ termBatches.set(key, b);
8243
+ }
8244
+ b.events.push({ stream, chunk: String(text) });
8245
+ b.bytes += String(text).length;
8246
+ if (b.bytes >= TERM_BATCH_BYTES) return termFlush(key); // tampon taştı — hemen boşalt
8247
+ if (!b.timer) b.timer = setTimeout(() => termFlush(key), TERM_BATCH_MS);
8248
+ }
8249
+
8250
+ function termFlush(id) {
8251
+ const key = String(id || '');
8252
+ const b = termBatches.get(key);
8253
+ if (!b) return;
8254
+ if (b.timer) { clearTimeout(b.timer); b.timer = null; }
8255
+ termBatches.delete(key);
8256
+ if (!b.events.length) return;
8257
+ termSend({ type: 'term-batch', id: key, events: b.events });
8258
+ }
8259
+
7988
8260
  /* Kalıcı kabuğu başlat: cmd /q /k — girdi pipe'ından satır satır okur,
7989
8261
  prompt yazmaz. chcp 65001 → Türkçe yollar (Masaüstü vb.) doğru çözülür. */
7990
8262
  function termShellSpawn() {
@@ -8009,14 +8281,15 @@ function termShellSpawn() {
8009
8281
  if (termForwarded > TERM_FWD_CAP) {
8010
8282
  if (!termCapNotified) {
8011
8283
  termCapNotified = true;
8284
+ termFlush(termShellId);
8012
8285
  termSend({ type: 'term-out', id: termShellId, stream: 'out', chunk: '\n[beast] çıktı çok büyük — iletim durduruldu (komut sürüyor)\n' });
8013
8286
  }
8014
8287
  return;
8015
8288
  }
8016
- termSend({ type: 'term-out', id: termShellId, stream: 'out', chunk: String(d) });
8289
+ termQueue(termShellId, 'out', String(d));
8017
8290
  });
8018
8291
  child.stderr.on('data', (d) => {
8019
- termSend({ type: 'term-out', id: termShellId, stream: 'err', chunk: String(d) });
8292
+ termQueue(termShellId, 'err', String(d));
8020
8293
  });
8021
8294
  child.on('exit', () => {
8022
8295
  if (termChild === child) termChild = null;
@@ -8041,6 +8314,7 @@ ipcMain.handle('terminal:run', (_e, payload) => {
8041
8314
  if (!termChild || !termChild.stdin.writable) termShellSpawn();
8042
8315
  if (!termChild) return { ok: false, error: 'kabuk başlatılamadı' };
8043
8316
  const id = 't' + Date.now().toString(36) + ++termShellSeq;
8317
+ termFlush(termShellId); // önceki komuttan kalan çıktı sırayı bozmasın
8044
8318
  termShellId = id;
8045
8319
  termForwarded = 0;
8046
8320
  termCapNotified = false;
@@ -8073,12 +8347,14 @@ ipcMain.handle('terminal:run', (_e, payload) => {
8073
8347
  }
8074
8348
  child.stdout.setEncoding('utf8');
8075
8349
  child.stderr.setEncoding('utf8');
8076
- child.stdout.on('data', (d) => termSend({ type: 'term-out', id, stream: 'out', chunk: String(d) }));
8077
- child.stderr.on('data', (d) => termSend({ type: 'term-out', id, stream: 'err', chunk: String(d) }));
8350
+ child.stdout.on('data', (d) => termQueue(id, 'out', String(d)));
8351
+ child.stderr.on('data', (d) => termQueue(id, 'err', String(d)));
8078
8352
  child.on('error', (err) => {
8353
+ termFlush(id);
8079
8354
  termSend({ type: 'term-end', id, code: -1, error: String((err && err.message) || err) });
8080
8355
  });
8081
8356
  child.on('close', (code) => {
8357
+ termFlush(id);
8082
8358
  termSend({ type: 'term-end', id, code: code == null ? -1 : code });
8083
8359
  });
8084
8360
  return { ok: true, id };
@@ -8092,6 +8368,7 @@ ipcMain.handle('terminal:stop', () => {
8092
8368
  try { termChild.kill(); } catch {}
8093
8369
  termChild = null;
8094
8370
  }
8371
+ termFlush(termShellId);
8095
8372
  termSend({ type: 'term-end', id: termShellId, code: 130, error: 'komut durduruldu — kalıcı CMD yeniden hazır' });
8096
8373
  termShellId = null;
8097
8374
  return { ok: true };
@@ -13042,8 +13319,131 @@ ipcMain.handle('email:send', (_e, msg) => emailSend(msg || {}));
13042
13319
 
13043
13320
  /* ---------- ekran görüntüsü ---------- */
13044
13321
 
13045
- /* Ana ekrandan JPEG dataURL yakalar (computer_use ve screen:capture ortak) */
13046
- async function captureScreenDataUrl() {
13322
+ /* Son ekran görüntüsünün kaynak ekran sınırları + görüntü boyutu — computer_act
13323
+ koordinat dönüşümü buradan yapılır (görüntü 1280px'e ölçeklenir). */
13324
+ let lastScreenCapture = null;
13325
+
13326
+ /* Canvas (görüntü üzerine çizim) — yoksa annotasyon sessizce atlanır */
13327
+ let _canvasLib;
13328
+ function canvasLib() {
13329
+ if (_canvasLib !== undefined) return _canvasLib;
13330
+ try {
13331
+ _canvasLib = require('@napi-rs/canvas');
13332
+ } catch {
13333
+ _canvasLib = null;
13334
+ }
13335
+ return _canvasLib;
13336
+ }
13337
+
13338
+ /* Gerçek imleç konumu (400 ms önbellek) — computer_look işaretçisi için */
13339
+ let _cursorCache = { at: 0, x: 0, y: 0 };
13340
+ function cursorPosition() {
13341
+ return new Promise((resolve) => {
13342
+ if (Date.now() - _cursorCache.at < 400) return resolve({ x: _cursorCache.x, y: _cursorCache.y });
13343
+ let out = '';
13344
+ try {
13345
+ const p = spawn(
13346
+ 'powershell.exe',
13347
+ ['-NoProfile', '-NonInteractive', '-Command', 'Add-Type -AssemblyName System.Windows.Forms; $p=[System.Windows.Forms.Cursor]::Position; "$($p.X),$($p.Y)"'],
13348
+ { windowsHide: true }
13349
+ );
13350
+ const t = setTimeout(() => { try { p.kill(); } catch {} resolve(null); }, 4000);
13351
+ p.stdout.on('data', (d) => { out += String(d); });
13352
+ p.on('error', () => { clearTimeout(t); resolve(null); });
13353
+ p.on('close', () => {
13354
+ clearTimeout(t);
13355
+ const m = /(\d+)\s*,\s*(\d+)/.exec(out);
13356
+ if (m) {
13357
+ _cursorCache = { at: Date.now(), x: Number(m[1]), y: Number(m[2]) };
13358
+ resolve({ x: _cursorCache.x, y: _cursorCache.y });
13359
+ } else resolve(null);
13360
+ });
13361
+ } catch {
13362
+ resolve(null);
13363
+ }
13364
+ });
13365
+ }
13366
+
13367
+ /* Ekran görüntüsü üzerine 128px ızgara + imleç işareti çiz: model koordinatı
13368
+ görselden okuyabilir. Izgara çizgileri yarı saydam — içeriği boğmaz. */
13369
+ async function annotateScreenShot(img) {
13370
+ const lib = canvasLib();
13371
+ if (!lib) return null;
13372
+ const im = await lib.loadImage(img.toPNG());
13373
+ const canvas = lib.createCanvas(im.width, im.height);
13374
+ const ctx = canvas.getContext('2d');
13375
+ ctx.drawImage(im, 0, 0);
13376
+ const step = 128;
13377
+ ctx.lineWidth = 1;
13378
+ ctx.font = 'bold 12px Segoe UI, sans-serif';
13379
+ for (let x = step; x < im.width; x += step) {
13380
+ ctx.strokeStyle = 'rgba(0,200,255,0.28)';
13381
+ ctx.beginPath();
13382
+ ctx.moveTo(x + 0.5, 0);
13383
+ ctx.lineTo(x + 0.5, im.height);
13384
+ ctx.stroke();
13385
+ const t = String(x);
13386
+ const tw = ctx.measureText(t).width;
13387
+ ctx.fillStyle = 'rgba(0,0,0,0.62)';
13388
+ ctx.fillRect(x + 1, 1, tw + 7, 15);
13389
+ ctx.fillStyle = '#00d8ff';
13390
+ ctx.fillText(t, x + 4, 12.5);
13391
+ }
13392
+ for (let y = step; y < im.height; y += step) {
13393
+ ctx.strokeStyle = 'rgba(0,200,255,0.28)';
13394
+ ctx.beginPath();
13395
+ ctx.moveTo(0, y + 0.5);
13396
+ ctx.lineTo(im.width, y + 0.5);
13397
+ ctx.stroke();
13398
+ const t = String(y);
13399
+ const tw = ctx.measureText(t).width;
13400
+ ctx.fillStyle = 'rgba(0,0,0,0.62)';
13401
+ ctx.fillRect(1, y + 1, tw + 7, 15);
13402
+ ctx.fillStyle = '#00d8ff';
13403
+ ctx.fillText(t, 4, y + 12.5);
13404
+ }
13405
+ try {
13406
+ /* imleç sorgusu en çok 1.5 sn bekletir — look hızlı kalmalı */
13407
+ const cur = await Promise.race([
13408
+ cursorPosition(),
13409
+ new Promise((r) => setTimeout(() => r(null), 1500)),
13410
+ ]);
13411
+ const cap = lastScreenCapture;
13412
+ if (cur && cap && cap.w > 0 && cap.h > 0) {
13413
+ const kx = cap.imgW / cap.w;
13414
+ const ky = cap.imgH / cap.h;
13415
+ const cx = (cur.x - cap.x) * kx;
13416
+ const cy = (cur.y - cap.y) * ky;
13417
+ if (cx >= -24 && cy >= -24 && cx <= im.width + 24 && cy <= im.height + 24) {
13418
+ ctx.strokeStyle = '#ff3b5c';
13419
+ ctx.lineWidth = 2;
13420
+ ctx.beginPath();
13421
+ ctx.arc(cx, cy, 14, 0, Math.PI * 2);
13422
+ ctx.stroke();
13423
+ ctx.beginPath();
13424
+ ctx.moveTo(cx - 22, cy);
13425
+ ctx.lineTo(cx + 22, cy);
13426
+ ctx.moveTo(cx, cy - 22);
13427
+ ctx.lineTo(cx, cy + 22);
13428
+ ctx.stroke();
13429
+ const label = 'imlec (' + Math.round(cx) + ',' + Math.round(cy) + ')';
13430
+ const tw = ctx.measureText(label).width;
13431
+ const bx = Math.min(Math.max(2, cx + 18), Math.max(2, im.width - tw - 12));
13432
+ const by = Math.min(Math.max(16, cy - 34), im.height - 6);
13433
+ ctx.fillStyle = 'rgba(0,0,0,0.78)';
13434
+ ctx.fillRect(bx - 3, by - 13, tw + 8, 16);
13435
+ ctx.fillStyle = '#ff8ba0';
13436
+ ctx.fillText(label, bx, by);
13437
+ }
13438
+ }
13439
+ } catch {}
13440
+ return canvas.toBuffer('image/jpeg', 78);
13441
+ }
13442
+
13443
+ /* Ana ekrandan JPEG dataURL yakalar (computer_use ve screen:capture ortak).
13444
+ opts.annotate: ızgara + imleç işareti (yalnız ajan computer_look kullanır;
13445
+ OCR/komut yolları düz görüntü ister). */
13446
+ async function captureScreenDataUrl(opts) {
13047
13447
  try {
13048
13448
  const sources = await desktopCapturer.getSources({
13049
13449
  types: ['screen'],
@@ -13057,7 +13457,22 @@ async function captureScreenDataUrl() {
13057
13457
  if (a.width * a.height > b.width * b.height) best = s;
13058
13458
  }
13059
13459
  const img = best.thumbnail.resize({ width: 1280 });
13060
- const jpeg = img.toJPEG(72);
13460
+ /* KAYNAK EKRAN SINIRLARI: ajan görüntüyü 1280px tabanlı okur; act çağrısı
13461
+ bu sınırlara göre gerçek ekran koordinatına çevrilir (çok monitörde ofset dahil) */
13462
+ try {
13463
+ const all = screen.getAllDisplays();
13464
+ const disp = all.find((d) => String(d.id) === String(best.display_id)) || screen.getPrimaryDisplay();
13465
+ const b = (disp && disp.bounds) || { x: 0, y: 0, width: 1280, height: 720 };
13466
+ const size = img.getSize();
13467
+ lastScreenCapture = { x: b.x, y: b.y, w: b.width, h: b.height, imgW: size.width, imgH: size.height };
13468
+ } catch {
13469
+ lastScreenCapture = null;
13470
+ }
13471
+ let jpeg = null;
13472
+ if (opts && opts.annotate) {
13473
+ try { jpeg = await annotateScreenShot(img); } catch {}
13474
+ }
13475
+ if (!jpeg || !jpeg.length) jpeg = img.toJPEG(72);
13061
13476
  if (!jpeg || !jpeg.length) return null;
13062
13477
  return 'data:image/jpeg;base64,' + jpeg.toString('base64');
13063
13478
  } catch {
@@ -13065,6 +13480,87 @@ async function captureScreenDataUrl() {
13065
13480
  }
13066
13481
  }
13067
13482
 
13483
+ /* ---- görsel parmak izi: aksiyon "işe yaradı mı" (verify) ---- */
13484
+ function imageSignature(img) {
13485
+ try {
13486
+ if (!img) return null;
13487
+ const small = img.getSize().width > 64 ? img.resize({ width: 64 }) : img;
13488
+ const sz = small.getSize();
13489
+ return computeruse.bitmapSignature(small.toBitmap(), sz.width, sz.height);
13490
+ } catch {
13491
+ return null;
13492
+ }
13493
+ }
13494
+
13495
+ async function pageSignature(wc) {
13496
+ try {
13497
+ return imageSignature(await wc.capturePage());
13498
+ } catch {
13499
+ return null;
13500
+ }
13501
+ }
13502
+
13503
+ /* Ekran değişim imzası — küçük thumbnail ile hızlı (computer_act verify) */
13504
+ async function screenSignature() {
13505
+ try {
13506
+ const sources = await desktopCapturer.getSources({ types: ['screen'], thumbnailSize: { width: 128, height: 72 } });
13507
+ if (!sources.length) return null;
13508
+ let best = sources[0];
13509
+ for (const s of sources) {
13510
+ const a = s.thumbnail.getSize();
13511
+ const b = best.thumbnail.getSize();
13512
+ if (a.width * a.height > b.width * b.height) best = s;
13513
+ }
13514
+ return imageSignature(best.thumbnail);
13515
+ } catch {
13516
+ return null;
13517
+ }
13518
+ }
13519
+
13520
+ const CU_VERIFY_OPS = new Set(['click', 'dblclick', 'rightclick', 'type', 'key', 'drag']);
13521
+
13522
+ /* computer_act köprüsü: model 1280px tabanlı görüntü koordinatı verir —
13523
+ gerçek ekran koordinatına ölçekle (1280'den geniş her ekranda tıklamalar
13524
+ yanlış yere gidiyordu). Aksiyon sonrası görsel değişimi de raporlar. */
13525
+ async function computerActScaled(op, args) {
13526
+ const a = { ...(args || {}) };
13527
+ let cap = lastScreenCapture;
13528
+ if (!cap) {
13529
+ try {
13530
+ const b = screen.getPrimaryDisplay().bounds;
13531
+ const imgH = Math.max(1, Math.round((1280 * b.height) / b.width));
13532
+ cap = { x: b.x, y: b.y, w: b.width, h: b.height, imgW: 1280, imgH };
13533
+ } catch {
13534
+ cap = null;
13535
+ }
13536
+ }
13537
+ const opName = String(op || '').toLowerCase();
13538
+ const scaled = cap && cap.imgW > 0 ? computeruse.scaleArgs(a, cap) : a;
13539
+ const verify = CU_VERIFY_OPS.has(opName) && a.verify !== false;
13540
+ const before = verify ? await screenSignature() : null;
13541
+ let r;
13542
+ try {
13543
+ r = await computeruse.act(opName, scaled);
13544
+ } catch (e) {
13545
+ return { ok: false, error: String((e && e.message) || e) };
13546
+ }
13547
+ if (before && r && r.ok) {
13548
+ await new Promise((res) => setTimeout(res, 350)); /* UI tepkisi otursun */
13549
+ const after = await screenSignature();
13550
+ const ratio = computeruse.signatureDiff(before, after);
13551
+ if (ratio != null) {
13552
+ r.changed = ratio >= 0.02;
13553
+ r.changeRatio = Number(ratio.toFixed(3));
13554
+ if (!r.changed && ['click', 'dblclick', 'rightclick', 'type'].includes(opName)) {
13555
+ r.warning =
13556
+ 'ekranda görünür değişiklik yok — aksiyon etkisiz olabilir (yanlış pencere/hedef ya da odak kaybı). ' +
13557
+ 'computer_look ile durumu kontrol et; focus ile pencereyi öne getirip tekrar dene.';
13558
+ }
13559
+ }
13560
+ }
13561
+ return r;
13562
+ }
13563
+
13068
13564
  ipcMain.handle('screen:capture', async () => {
13069
13565
  const image = await captureScreenDataUrl();
13070
13566
  return image ? { ok: true, image } : { ok: false, error: 'ekran görüntüsü alınamadı' };