beast-agent 2.40.0 → 2.41.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "beast-agent",
3
3
  "productName": "Beast Agent",
4
- "version": "2.40.0",
4
+ "version": "2.41.0",
5
5
  "description": "Ultra-fast local agent shell for Windows.",
6
6
  "author": "algokodcom (AlgoKod)",
7
7
  "license": "MIT",
@@ -2106,6 +2106,7 @@ class Engine {
2106
2106
  ? 'Bağımsız alt-işleri run_background ile PARALEL ajana devret; işi KENDİN YÜRÜTME — emri ver, takip et, raporla.'
2107
2107
  : 'Bağımsız alt-işleri delegate_task ile devret; kendi başına halledebileceğin işleri devretme.',
2108
2108
  'Güncel/dış bilgi gerekiyorsa web_search kullan (zincir DAHİLİ tarayıcıyla başlar — gerçek Chromium ile Google); hızlı ham metin okuması için webfetch (veya http_fetch) kullan.',
2109
+ 'KULLANICI GITHUB REPO ADRESİ/LİNKİ VERİRSE (github.com/owner/repo) ya da "şu repoyu indir/kur/çalıştır" derse sandbox_repo aracını kullan: action=indir → Beast Sandbox\'a (~/Beast-Sandbox) klonlar; ardından action=kur (bağımlılıklar) ve action=baslat (çalıştır) ile devam et — uzun sürse bile tur KİLİTLENMEZ, çıktı Sandbox ÇALIŞTIR panelinde canlı akar, localhost adresi sağdaki dahili tarayıcıda otomatik açılır. Sonucu kullanıcıya tek cümleyle bildir (repo, komut, çalışıyor mu).',
2109
2110
  'DERİN ARAŞTIRMA: web_search\u2019in sonucu yetersizse/istenen bilgi listede YOKSA aramayı tekrar tekrar deneme yerine deep_search kullan — 1-4 sorgu varyantını (eş anlamlı, Türkçe+İngilizce yazımlar) PARALEL aratır ve ilk sayfaları GİZLİ gerçek Chromium\u2019da açıp tam metin okur (paneli açmaz, kullanıcıyı rahatsız etmez; JS/SPA sayfalar çalışır). Fiyat karşılaştırma, çok kaynaklı araştırma, "her şeyi bul" işleri ve Türkçe sorgularda sonuç zayıfsa doğrudan deep_search seç. read_top=0 verirsen yalnız harmanlanmış sonuç listesi döner.',
2110
2111
  'ARAMA-DİSİPLİN: bir bilgiyi 2-3 denemede bulamazsan TAKILMA — farklı bir açıya/sorguya geç, yine yoksa bulabildiğin kısmi bilgiyle cevap ver ve neyi bulamadığını açıkça söyle. Kapalı/gizli içerik (private profil, login arkası veri) peşinde KOŞMA — bulunamayacağı belliyse hemen vazgeç.',
2111
2112
  'TARAYICI VARSAYILANI (ZORUNLU): kullanıcı "şu siteyi aç", "bunu ara / google\u2019da ara", "şu sayfaya git" gibi bir web isteği verdiğinde HEP DAHİLİ TARAYICIYI KULLAN (browser_open ile aç → açılış yanıtında hazır snapshot gelir → ref numaralarıyla browser_click/browser_type/browser_select ile hareket et; HER eylem yanıtında taze snapshot döner — ayrıca browser_snapshot çağırma, yanıttaki refleri kullan; browser_read ile metin oku; sadece görsel yerleşim/grafik gerekiyorsa browser_screenshot çek). Tarih/saat alanlarını (type=date/time) browser_type ile DÜZ METİN yaz ("15.03.2026", "2026-03-15") — takvimden tıklamaya çalışma, alan programatik ayarlanır. Panel ekranın sağında açılır ve kullanıcı da sayfayı anında görür; JS/login/SPA/dinamik içerik için idealdir. Bu kural TÜM oturumları kapsar — masaüstü sohbeti ve çok kullanıcılı botlar (WhatsApp vb.) dahil.' +
@@ -5010,6 +5011,51 @@ const skills = require('./skills');
5010
5011
  return { ...r, via };
5011
5012
  }
5012
5013
 
5014
+ /* Sandbox köprüsü (main.js hook'ları): ana sohbetteki ajan sandbox_repo
5015
+ aracıyla repo indirir/kurar/çalıştırır — Sandbox paneliyle AYNI makine.
5016
+ Kur/Başlat yönetilen süreçtir: tur kilitlenmez, çıktı panelde akar,
5017
+ localhost adresi sağdaki dahili tarayıcıda açılır. */
5018
+ async _sandboxRepo(args) {
5019
+ const action = String((args && args.action) || '').trim().toLowerCase();
5020
+ const repo = String((args && args.repo) || '').trim();
5021
+ if (action === 'indir') {
5022
+ if (typeof this.sbCloneHook !== 'function') return { ok: false, error: 'sandbox köprüsü yok' };
5023
+ const r = await this.sbCloneHook(repo);
5024
+ if (!r || !r.ok) return r || { ok: false, error: 'klonlanamadı' };
5025
+ let info = null;
5026
+ try { if (typeof this.sbDetectHook === 'function') info = this.sbDetectHook(r.folder); } catch {}
5027
+ return {
5028
+ ok: true,
5029
+ name: r.name,
5030
+ folder: r.folder,
5031
+ detected: info ? { kind: info.kind, label: info.label, install: info.install, run: info.run } : undefined,
5032
+ note:
5033
+ 'Repo "' + r.name + '" → ' + r.folder + ' klasörüne indirildi. Kurmak için sandbox_repo action=kur, çalıştırmak için action=baslat; kullanıcı Sandbox panelindeki Kur/Başlat butonlarını da kullanabilir.',
5034
+ };
5035
+ }
5036
+ if (action !== 'kur' && action !== 'baslat' && action !== 'durdur') {
5037
+ return { ok: false, error: 'action şunlardan biri olmalı: indir | kur | baslat | durdur' };
5038
+ }
5039
+ let folder = '';
5040
+ try { if (typeof this.sbFolderHook === 'function') folder = String(this.sbFolderHook(repo) || ''); } catch {}
5041
+ if (!folder) return { ok: false, error: 'repo bulunamadı — önce sandbox_repo action=indir ile indir ya da Sandbox panelinden bir repo seç' };
5042
+ if (action === 'kur') {
5043
+ if (typeof this.sbInstallHook !== 'function') return { ok: false, error: 'sandbox köprüsü yok' };
5044
+ const r = this.sbInstallHook(folder);
5045
+ if (!r || !r.ok) return r || { ok: false, error: 'kurulum başlatılamadı' };
5046
+ return { ok: true, folder, command: r.cmd, note: 'Kurulum ÇALIŞTIR panelinde başladı: ' + r.cmd + ' — çıktı panelde canlı akar; bitince kullanıcıya bildir, sonra action=baslat.' };
5047
+ }
5048
+ if (action === 'baslat') {
5049
+ if (typeof this.sbStartHook !== 'function') return { ok: false, error: 'sandbox köprüsü yok' };
5050
+ const r = this.sbStartHook(folder, 'run');
5051
+ if (!r || !r.ok) return r || { ok: false, error: 'başlatılamadı' };
5052
+ return { ok: true, folder, command: r.cmd, note: 'Süreç ÇALIŞTIR panelinde başladı: ' + r.cmd + '. Dev server adresi yakalanınca sağdaki dahili tarayıcıda otomatik açılır; durdurmak için action=durdur.' };
5053
+ }
5054
+ if (typeof this.sbStopHook !== 'function') return { ok: false, error: 'sandbox köprüsü yok' };
5055
+ const stopped = !!this.sbStopHook(folder);
5056
+ return { ok: true, folder, wasRunning: stopped, note: stopped ? 'Süreç durduruldu.' : 'Bu repoda çalışan bir süreç yoktu.' };
5057
+ }
5058
+
5013
5059
  async _subExecTool(name, args, signal) {
5014
5060
  try {
5015
5061
  const blocked = this._guardTool(name, args);
@@ -5024,6 +5070,7 @@ const skills = require('./skills');
5024
5070
  });
5025
5071
  }
5026
5072
  if (name === 'ocr_read') return JSON.stringify(await this._ocrRead(args || {}, signal));
5073
+ if (name === 'sandbox_repo') return JSON.stringify(await this._sandboxRepo(args || {}));
5027
5074
  /* opencode general-agent portu: alt-ajan da edit/grep/glob kullanır.
5028
5075
  MT5 (mt5_*) araçları da tools.exec'e geçer (finance işçileri/danışma). */
5029
5076
  if (!(name === 'run_command' || name === 'read_file' || name === 'write_file' || name === 'edit_file' ||
@@ -5565,6 +5612,9 @@ const skills = require('./skills');
5565
5612
  const n = Number((args && args.max_results) || 8);
5566
5613
  return JSON.stringify(await this._webSearchChain(q, n, sessionId, signal));
5567
5614
  }
5615
+ if (name === 'sandbox_repo') {
5616
+ return JSON.stringify(await this._sandboxRepo(args || {}));
5617
+ }
5568
5618
  if (name === 'ocr_read') {
5569
5619
  return JSON.stringify(await this._ocrRead(args || {}, signal, sessionId));
5570
5620
  }
@@ -130,9 +130,10 @@ function build(opts = {}) {
130
130
  user
131
131
  ),
132
132
  prompt: [
133
- 'BEAST SANDBOX MODUNDASIN: GitHub\u2019dan indirilmi\u015f A\u00c7IK KAYNAK bir repo klas\u00f6r\u00fcndesin (Beast-Sandbox). Bu kodun sahibi de\u011filsin \u2014 hazir bir di\u015f proje; repo sahibinin konvansiyonlarina uy, gereksiz yeniden bi\u00e7imlendirme yapma.',
134
- '\u0130\u015fe README + paket bildirim dosyalar\u0131yla ba\u015fla (list/glob); ba\u011f\u0131ml\u0131l\u0131klar kurulu de\u011filse \u00f6nce kurulum komutunu \u00e7al\u0131\u015ft\u0131r (npm install vb.).',
135
- 'UZUN S\u00dcREL\u0130 sunucu/s\u00fcre\u00e7 (npm start, npm run dev, python app.py vb.) bash ile ASLA ba\u015flatma \u2014 tur kilitlenir; kullan\u0131c\u0131 \u00e7al\u0131\u015ft\u0131rmak isterse panel_run arac\u0131yla \u00c7ALI\u015eTIR paneline b\u0131rak (\u00e7\u0131kt\u0131 orada canl\u0131 akar) ve adresi kullan\u0131c\u0131ya bildir. panel_run yaln\u0131z B\u0130R s\u00fcre\u00e7 \u00e7al\u0131\u015ft\u0131r\u0131r.',
133
+ 'BEAST SANDBOX MODUNDASIN: GitHub\u2019dan indirilmi\u015f A\u00c7IK KAYNAK bir repo klas\u00f6r\u00fcndesin (Beast-Sandbox). Bu kodun sahibi de\u011filsin \u2014 haz\u0131r bir d\u0131\u015f proje; repo sahibinin konvansiyonlar\u0131na uy, gereksiz yeniden bi\u00e7imlendirme yapma.',
134
+ 'PANEL: \u00c7ALI\u015eTIR b\u00f6l\u00fcm\u00fcndeki Kur/Ba\u015flat/Dev butonlar\u0131 proje tipini OTOMAT\u0130K alg\u0131lar (package.json scripts + npm/pnpm/yarn/bun kilidi; requirements.txt/pyproject + uv; Cargo.toml; go.mod; statik index.html) ve do\u011fru komutu do\u011frudan \u00e7al\u0131\u015ft\u0131r\u0131r. \u00d6zel komut kutusu ad-hoc komut \u00e7al\u0131\u015ft\u0131r\u0131r; kaydet butonu o komutu repo i\u00e7in kal\u0131c\u0131 ba\u015flatma komutu yapar. Kullan\u0131c\u0131 butonla ba\u015flatt\u0131ysa \u00e7\u0131kt\u0131 panelde canl\u0131 akar, localhost adresi yakalan\u0131nca sa\u011fdaki dahili taray\u0131c\u0131da otomatik a\u00e7\u0131l\u0131r.',
135
+ '\u0130\u015fe README + paket bildirim dosyalar\u0131yla ba\u015fla (list/glob); kullan\u0131c\u0131 Kur demediyse ba\u011f\u0131ml\u0131l\u0131klar\u0131 sen kurabilirsin (npm install, pip install -r requirements.txt vb.). Kur/Ba\u015flat ba\u015far\u0131s\u0131z olursa ger\u00e7ek hatay\u0131 g\u00f6rmek i\u00e7in ayn\u0131 komutu bash ile \u00e7al\u0131\u015ft\u0131r, nedenini bul, gerekiyorsa d\u00fczelt ve kullan\u0131c\u0131ya do\u011fru \u00f6zel komutu s\u00f6yle (kutuya yaz\u0131p kaydetsin).',
136
+ 'UZUN S\u00dcREL\u0130 sunucu/s\u00fcre\u00e7 (npm start, npm run dev, python app.py vb.) bash ile ASLA ba\u015flatma \u2014 tur kilitlenir; kullan\u0131c\u0131 senden isterse panel_run ile ba\u015flat (\u00e7\u0131kt\u0131 panelde akar, adres sa\u011fda a\u00e7\u0131l\u0131r). Repo ba\u015f\u0131na TEK s\u00fcre\u00e7: kullan\u0131c\u0131 zaten ba\u015flatt\u0131ysa panel_run hata verir \u2014 kullan\u0131c\u0131ya \u00f6nce Durdur demesini s\u00f6yle.',
136
137
  ].join('\n'),
137
138
  mode: 'all',
138
139
  native: false,
@@ -1613,6 +1613,22 @@ const definitions = [
1613
1613
  },
1614
1614
  },
1615
1615
  },
1616
+ {
1617
+ type: 'function',
1618
+ function: {
1619
+ name: 'sandbox_repo',
1620
+ description:
1621
+ 'Beast Sandbox yönetimi — kullanıcı GitHub repo adresi/linki verdiğinde ya da indirilmiş bir repoyu kurmak/çalıştırmak istediğinde kullan. action=indir: repoyu ~/Beast-Sandbox klasörüne klonlar (owner/repo ya da tam URL); action=kur: algılanan kurulum komutunu (npm install, pip install -r requirements.txt, uv sync, cargo fetch, go mod download) ÇALIŞTIR panelinde başlatır; action=baslat: algılanan çalıştırma komutunu başlatır (npm run dev, python app.py vb.) — localhost adresi yakalanınca sağdaki dahili tarayıcıda otomatik açılır; action=durdur: repodaki çalışan süreci durdurur. Kur/Başlat turu KİLİTLEMEZ, çıktı panelde canlı akar. repo alanı boşsa panelde son kullanılan repo kullanılır.',
1622
+ parameters: {
1623
+ type: 'object',
1624
+ properties: {
1625
+ action: { type: 'string', enum: ['indir', 'kur', 'baslat', 'durdur'], description: 'Yapılacak işlem' },
1626
+ repo: { type: 'string', description: 'GitHub adresi (owner/repo ya da tam URL) ya da repo adı/klasör yolu; son kullanılan repo için boş bırak' },
1627
+ },
1628
+ required: ['action'],
1629
+ },
1630
+ },
1631
+ },
1616
1632
  {
1617
1633
  type: 'function',
1618
1634
  function: {
package/src/main.js CHANGED
@@ -3474,6 +3474,15 @@ function reloadBackend() {
3474
3474
  /* PANEL RUN köprüsü: ajanın panel_run aracı → ÇALIŞTIR panelindeki yönetilen
3475
3475
  süreç koşucusu (sandbox:run IPC ile aynı makine). */
3476
3476
  engine.sbRunHook = sbRunStartManaged;
3477
+ /* SANDBOX KÖPRÜSÜ: ana sohbetteki ajanın sandbox_repo aracı → panel ile aynı
3478
+ makine (indir/kur/başlat/durdur); kullanıcı chatte repo linki bırakınca
3479
+ ajan kendisi indirip kurabilir. */
3480
+ engine.sbCloneHook = sbCloneRepo;
3481
+ engine.sbFolderHook = sbResolveFolder;
3482
+ engine.sbDetectHook = sbProjectInfoMerged;
3483
+ engine.sbInstallHook = sbInstallRepo;
3484
+ engine.sbStartHook = sbStartRepo;
3485
+ engine.sbStopHook = sbRunStopFolder;
3477
3486
 
3478
3487
  /* SQUEEZE (opsiyonel token sıkıştırma): varsayılan KAPALI; ayar açıksa
3479
3488
  her LLM isteğinin kopyası gönderimden önce sıkıştırılır. */
@@ -10814,6 +10823,7 @@ ipcMain.handle('studio:delete', async (_e, rel) => {
10814
10823
  run_command/read/write araçları repo klasöründe çalışır). Ana sohbet VE
10815
10824
  Code/Studio panellerinden tamamen ayrıdır; ana chat solda yerinde kalır. */
10816
10825
  const sbSessions = new Map(); /* klasör → sessionId */
10826
+ let sbLastFolder = ''; /* panelde seçilen / son indirilen repo — ajan araçları boş repo alanında bunu kullanır */
10817
10827
  const SB_DEBOUNCE_MS = 900;
10818
10828
  const sbQueue = new Map(); /* klasör → { timer, msgs[] } */
10819
10829
 
@@ -10935,6 +10945,29 @@ function sbRepoFromInput(input) {
10935
10945
  return null;
10936
10946
  }
10937
10947
 
10948
+ /* repo adres/klasör çözümleyici: hem panel hem ajan araçları (sandbox_repo)
10949
+ kullanır — tam yol, klasör adı, owner/repo ya da boş ("son repo") kabul eder */
10950
+ function sbResolveFolder(input) {
10951
+ const root = sandboxRoot();
10952
+ const s = String(input || '').trim();
10953
+ if (!s) return sbLastFolder;
10954
+ try {
10955
+ if (fs.existsSync(s) && fs.statSync(s).isDirectory()) return path.resolve(s);
10956
+ const direct = path.join(root, s);
10957
+ if (fs.existsSync(direct) && fs.statSync(direct).isDirectory()) return direct;
10958
+ const name = s.toLowerCase().replace(/[\\/]+$/, '').split(/[\\/]/).pop().replace(/\.git$/i, '');
10959
+ let partial = '';
10960
+ for (const e of fs.readdirSync(root, { withFileTypes: true })) {
10961
+ if (!e.isDirectory()) continue;
10962
+ const n = e.name.toLowerCase();
10963
+ if (n === name) return path.join(root, e.name);
10964
+ if (!partial && n.startsWith(name + '-')) partial = path.join(root, e.name);
10965
+ }
10966
+ if (partial) return partial;
10967
+ } catch {}
10968
+ return sbLastFolder;
10969
+ }
10970
+
10938
10971
  ipcMain.handle('sandbox:list', async () => {
10939
10972
  const root = sandboxRoot();
10940
10973
  const items = [];
@@ -10950,15 +10983,18 @@ ipcMain.handle('sandbox:list', async () => {
10950
10983
  sid = String(known);
10951
10984
  try { busy = engine.isBusy(known); } catch {}
10952
10985
  }
10953
- items.push({ name: e.name, folder, sid, busy });
10986
+ const slot = sbProcs.get(folder);
10987
+ items.push({ name: e.name, folder, sid, busy, running: !!(slot && slot.proc), kind: slot ? slot.kind : '' });
10954
10988
  }
10955
10989
  } catch {}
10956
10990
  return { ok: true, root, items };
10957
10991
  });
10958
10992
 
10959
- ipcMain.handle('sandbox:clone', async (_e, input) => {
10993
+ /* klonlama çekirdeği: hem Sandbox paneli IPC'si hem de ajanın sandbox_repo
10994
+ aracı buradan geçer (engine.sbCloneHook) */
10995
+ function sbCloneRepo(input) {
10960
10996
  const repo = sbRepoFromInput(input);
10961
- if (!repo) return { ok: false, error: 'github.com/owner/repo, owner/repo ya da tam .git adresi gir' };
10997
+ if (!repo) return Promise.resolve({ ok: false, error: 'github.com/owner/repo, owner/repo ya da tam .git adresi gir' });
10962
10998
  const root = sandboxRoot();
10963
10999
  const base = String(repo.name || 'repo').replace(/[\\/:*?"<>|]/g, '_').slice(0, 60) || 'repo';
10964
11000
  let dest = path.join(root, base);
@@ -11019,6 +11055,7 @@ ipcMain.handle('sandbox:clone', async (_e, input) => {
11019
11055
  return;
11020
11056
  }
11021
11057
  log.info('main', 'sandbox: klonlandı ' + repo.url + ' → ' + finalName);
11058
+ sbLastFolder = dest;
11022
11059
  resolve({ ok: true, name: finalName, folder: dest });
11023
11060
  };
11024
11061
  proc.on('error', done);
@@ -11031,7 +11068,9 @@ ipcMain.handle('sandbox:clone', async (_e, input) => {
11031
11068
  }
11032
11069
  }, 600000);
11033
11070
  });
11034
- });
11071
+ }
11072
+
11073
+ ipcMain.handle('sandbox:clone', async (_e, input) => sbCloneRepo(input));
11035
11074
 
11036
11075
  ipcMain.handle('sandbox:send', async (_e, payload) => {
11037
11076
  const text = String((payload && payload.msg) || '').trim();
@@ -11097,6 +11136,8 @@ ipcMain.handle('sandbox:remove', async (_e, payload) => {
11097
11136
  sbLoadMap();
11098
11137
  const sid = sbSessions.get(folder);
11099
11138
  if (sid && engine.isBusy(sid)) return { ok: false, error: 'repo çalışıyor — önce ■ ile durdur' };
11139
+ const slot = sbProcs.get(folder);
11140
+ if (slot && slot.proc) return { ok: false, error: 'repoda süreç çalışıyor — önce ■ Durdur' };
11100
11141
  try { fs.rmSync(folder, { recursive: true, force: true }); } catch (e) {
11101
11142
  return { ok: false, error: String((e && e.message) || e) };
11102
11143
  }
@@ -11105,6 +11146,12 @@ ipcMain.handle('sandbox:remove', async (_e, payload) => {
11105
11146
  sbSessions.delete(folder);
11106
11147
  sbSaveMap();
11107
11148
  }
11149
+ sbProcs.delete(folder);
11150
+ sbLoadCfg();
11151
+ if (sbCfg[folder]) {
11152
+ delete sbCfg[folder];
11153
+ sbSaveCfg();
11154
+ }
11108
11155
  return { ok: true };
11109
11156
  });
11110
11157
 
@@ -11121,6 +11168,7 @@ ipcMain.handle('sandbox:open', async (_e, payload) => {
11121
11168
  const folder = String((payload && payload.folder) || '');
11122
11169
  if (!folder.startsWith(sandboxRoot() + path.sep)) return { ok: false, error: 'geçersiz klasör' };
11123
11170
  if (!engine) return { ok: false, error: 'ajan hazır değil' };
11171
+ sbLastFolder = folder;
11124
11172
  const s = sandboxGetSession(folder);
11125
11173
  s.workspace = folder;
11126
11174
  engine.cache.set(s.id, s);
@@ -11162,25 +11210,154 @@ ipcMain.handle('sandbox:tree', (_e, payload) => {
11162
11210
  }
11163
11211
  });
11164
11212
 
11165
- /* Sandbox ÇALIŞTIRICI: repo klasöründe uzun süreli komut (npm start/dev vb.)
11166
- YÖNETİLEN SÜREÇ — çıktı satır satır panele akar; ■ ile ağaç-kullanı durdurulur.
11167
- Dev server adresi çıktıdan yakalanır → 'Tarayıcıda aç' aktifleşir.
11168
- Aynı fonksiyon engine.sbRunHook olarak da bağlanır — ajanın panel_run
11169
- aracı turu KİLİTLEMEDEN süreç başlatır (Kur/Başlat butonları bunu kullanır). */
11170
- const sbRunProc = { proc: null, folder: '', url: '' };
11213
+ /* Sandbox ÇALIŞTIRICI v2: repo başına YÖNETİLEN SÜREÇLER.
11214
+ Kur/Başlat/Durdur butonları artık deterministik: proje tipi algılanır
11215
+ (package.json, requirements.txt, pyproject.toml, Cargo.toml, go.mod,
11216
+ index.html) ve doğru komut DOĞRUDAN çalıştırılır. Her repo kendi süreç
11217
+ yuvasında koşar — birden çok repo aynı anda çalışabilir; çıktı tamponlanır,
11218
+ repo değiştirilince panel geçmişi yeniden basılır. Dev server adresi
11219
+ çıktıdan yakalanır → sağdaki dahili tarayıcıda açılır. Aynı fonksiyon
11220
+ engine.sbRunHook'tur: ajanın panel_run aracı da buradan geçer. */
11221
+
11222
+ const sbProcs = new Map(); /* klasör → { proc, kind, cmd, url, lines[], startedAt } */
11223
+ const SB_LOG_CAP = 500; /* repo başına tamponlanan çıktı satırı */
11224
+ const SB_CFG_FILE = 'sandbox-config.json';
11171
11225
 
11172
- function sbRunEmit(data) {
11226
+ function sbCfgPath() {
11227
+ return path.join(beastDir(), SB_CFG_FILE);
11228
+ }
11229
+
11230
+ let sbCfgLoaded = false;
11231
+ let sbCfg = {}; /* klasör → { install, run, dev } — kullanıcının kaydettiği özel komutlar */
11232
+
11233
+ function sbLoadCfg() {
11234
+ if (sbCfgLoaded) return sbCfg;
11235
+ sbCfgLoaded = true;
11173
11236
  try {
11174
- if (win && !win.isDestroyed()) win.webContents.send('agent:event', { type: 'sb-run', data: String(data || '') });
11237
+ const raw = JSON.parse(fs.readFileSync(sbCfgPath(), 'utf8'));
11238
+ if (raw && typeof raw === 'object') sbCfg = raw;
11175
11239
  } catch {}
11240
+ return sbCfg;
11241
+ }
11242
+
11243
+ function sbSaveCfg() {
11244
+ try { fs.writeFileSync(sbCfgPath(), JSON.stringify(sbCfg, null, 2)); } catch {}
11245
+ }
11246
+
11247
+ /* embedded python varsa onu kullan — PATH'te python olmasa bile çalışsın */
11248
+ function sbPythonCmd() {
11249
+ const cands = [process.env.BEAST_PYTHON, path.join(beastDir(), 'py', 'python.exe')];
11250
+ for (const c of cands) {
11251
+ try { if (c && fs.existsSync(c)) return '"' + c + '"'; } catch {}
11252
+ }
11253
+ return 'python';
11254
+ }
11255
+
11256
+ /* repo köküne bakıp proje tipini + kur/çalıştır komutlarını çıkarır */
11257
+ function sbProjectInfo(folder) {
11258
+ const out = { kind: 'unknown', label: 'bilinmiyor', name: path.basename(folder), install: '', run: '', dev: '', build: '' };
11259
+ try {
11260
+ const pkgPath = path.join(folder, 'package.json');
11261
+ if (fs.existsSync(pkgPath)) {
11262
+ let pkg = {};
11263
+ try { pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); } catch {}
11264
+ out.name = String(pkg.name || out.name);
11265
+ let pm = 'npm';
11266
+ if (fs.existsSync(path.join(folder, 'pnpm-lock.yaml'))) pm = 'pnpm';
11267
+ else if (fs.existsSync(path.join(folder, 'yarn.lock'))) pm = 'yarn';
11268
+ else if (fs.existsSync(path.join(folder, 'bun.lockb')) || fs.existsSync(path.join(folder, 'bun.lock'))) pm = 'bun';
11269
+ const s = pkg.scripts && typeof pkg.scripts === 'object' ? pkg.scripts : {};
11270
+ out.kind = 'node';
11271
+ out.label = pm;
11272
+ out.install = pm + ' install';
11273
+ out.dev = s.dev ? pm + ' run dev' : '';
11274
+ out.run = s.start ? pm + ' run start' : s.dev ? pm + ' run dev' : s.serve ? pm + ' run serve' : '';
11275
+ out.build = s.build ? pm + ' run build' : '';
11276
+ return out;
11277
+ }
11278
+ const hasPy = ['pyproject.toml', 'requirements.txt', 'setup.py'].some((f) => fs.existsSync(path.join(folder, f)));
11279
+ if (hasPy) {
11280
+ const py = sbPythonCmd();
11281
+ out.kind = 'python';
11282
+ out.label = 'python';
11283
+ if (fs.existsSync(path.join(folder, 'uv.lock'))) out.install = 'uv sync';
11284
+ else if (fs.existsSync(path.join(folder, 'requirements.txt'))) out.install = py + ' -m pip install -r requirements.txt';
11285
+ else out.install = py + ' -m pip install -e .';
11286
+ if (fs.existsSync(path.join(folder, 'manage.py'))) out.run = py + ' manage.py runserver';
11287
+ else if (fs.existsSync(path.join(folder, 'main.py'))) out.run = py + ' main.py';
11288
+ else if (fs.existsSync(path.join(folder, 'app.py'))) out.run = py + ' app.py';
11289
+ else if (fs.existsSync(path.join(folder, 'run.py'))) out.run = py + ' run.py';
11290
+ else if (fs.existsSync(path.join(folder, 'bot.py'))) out.run = py + ' bot.py';
11291
+ out.dev = out.run;
11292
+ return out;
11293
+ }
11294
+ if (fs.existsSync(path.join(folder, 'Cargo.toml'))) {
11295
+ out.kind = 'rust';
11296
+ out.label = 'cargo';
11297
+ out.install = 'cargo fetch';
11298
+ out.run = 'cargo run';
11299
+ out.dev = 'cargo run';
11300
+ return out;
11301
+ }
11302
+ if (fs.existsSync(path.join(folder, 'go.mod'))) {
11303
+ out.kind = 'go';
11304
+ out.label = 'go';
11305
+ out.install = 'go mod download';
11306
+ out.run = 'go run .';
11307
+ out.dev = 'go run .';
11308
+ return out;
11309
+ }
11310
+ if (fs.existsSync(path.join(folder, 'index.html'))) {
11311
+ out.kind = 'static';
11312
+ out.label = 'statik';
11313
+ out.run = sbPythonCmd() + ' -m http.server 8123 --bind 127.0.0.1';
11314
+ out.dev = out.run;
11315
+ return out;
11316
+ }
11317
+ } catch {}
11318
+ return out;
11176
11319
  }
11177
11320
 
11178
- function sbRunStartManaged(folder, cmd) {
11321
+ /* algılanan komutlar + kullanıcının kaydettiği özel komutlar birleşir */
11322
+ function sbProjectInfoMerged(folder) {
11323
+ const info = sbProjectInfo(folder);
11324
+ const c = sbLoadCfg()[folder];
11325
+ if (c && typeof c === 'object') {
11326
+ if (c.install) info.install = String(c.install);
11327
+ if (c.run) info.run = String(c.run);
11328
+ if (c.dev) info.dev = String(c.dev);
11329
+ if (c.install || c.run || c.dev) info.custom = true;
11330
+ }
11331
+ return info;
11332
+ }
11333
+
11334
+ function sbRunEmit(folder, data, kind) {
11335
+ try {
11336
+ if (win && !win.isDestroyed()) {
11337
+ win.webContents.send('agent:event', {
11338
+ type: 'sb-run',
11339
+ folder: String(folder || ''),
11340
+ kind: kind || 'run',
11341
+ data: String(data || ''),
11342
+ });
11343
+ }
11344
+ } catch {}
11345
+ }
11346
+
11347
+ function sbSlotPush(slot, folder, text) {
11348
+ slot.lines.push(String(text || ''));
11349
+ if (slot.lines.length > SB_LOG_CAP) slot.lines.splice(0, slot.lines.length - SB_LOG_CAP);
11350
+ sbRunEmit(folder, text, slot.kind);
11351
+ }
11352
+
11353
+ function sbRunStartManaged(folder, cmd, kind) {
11179
11354
  folder = String(folder || '');
11180
11355
  cmd = String(cmd || '').trim();
11356
+ kind = kind === 'install' ? 'install' : 'run';
11181
11357
  if (!folder.startsWith(sandboxRoot() + path.sep)) return { ok: false, error: 'geçersiz klasör' };
11182
11358
  if (!cmd) return { ok: false, error: 'komut boş' };
11183
- if (sbRunProc.proc) return { ok: false, error: 'ÇALIŞTIR panelinde zaten bir süreç çalışıyor — önce ■ ile durdur' };
11359
+ const cur = sbProcs.get(folder);
11360
+ if (cur && cur.proc) return { ok: false, error: 'Bu repoda zaten bir süreç çalışıyor — önce ■ Durdur' };
11184
11361
  const { spawn } = require('child_process');
11185
11362
  try {
11186
11363
  const proc = spawn('cmd.exe', ['/d', '/s', '/c', cmd], {
@@ -11188,19 +11365,19 @@ function sbRunStartManaged(folder, cmd) {
11188
11365
  windowsHide: true,
11189
11366
  env: { ...process.env, FORCE_COLOR: '0', NO_COLOR: '1' },
11190
11367
  });
11191
- sbRunProc.proc = proc;
11192
- sbRunProc.folder = folder;
11193
- sbRunProc.url = '';
11368
+ const slot = { proc, kind, cmd, url: '', lines: [], startedAt: Date.now() };
11369
+ sbProcs.set(folder, slot);
11194
11370
  const onLine = (buf) => {
11195
11371
  for (const piece of String(buf).split(/\r?\n/)) {
11196
11372
  if (!piece.trim()) continue;
11197
- sbRunEmit(piece);
11373
+ sbSlotPush(slot, folder, piece);
11374
+ if (kind !== 'run') continue;
11198
11375
  /* dev server adresi yakala (sadece localhost) */
11199
11376
  const m = /https?:\/\/(?:localhost|127\.0\.0\.1)(?::\d{2,5})?[^\s'"<>]*/i.exec(piece);
11200
- if (m && !sbRunProc.url) {
11201
- sbRunProc.url = m[0].replace(/[)\].,;:'"]+$/, '');
11377
+ if (m && !slot.url) {
11378
+ slot.url = m[0].replace(/[)\].,;:'"]+$/, '');
11202
11379
  try {
11203
- if (win && !win.isDestroyed()) win.webContents.send('agent:event', { type: 'sb-run-url', url: sbRunProc.url });
11380
+ if (win && !win.isDestroyed()) win.webContents.send('agent:event', { type: 'sb-run-url', folder, url: slot.url });
11204
11381
  } catch {}
11205
11382
  }
11206
11383
  }
@@ -11208,40 +11385,131 @@ function sbRunStartManaged(folder, cmd) {
11208
11385
  proc.stdout.on('data', onLine);
11209
11386
  proc.stderr.on('data', onLine);
11210
11387
  proc.on('close', (code) => {
11211
- sbRunEmit('[süreç bitti' + (code != null ? ' — kod ' + code : '') + ']');
11212
- sbRunProc.proc = null;
11388
+ sbSlotPush(slot, folder, '[süreç bitti' + (code != null ? ' — kod ' + code : '') + ']');
11389
+ slot.proc = null;
11213
11390
  try {
11214
- if (win && !win.isDestroyed()) win.webContents.send('agent:event', { type: 'sb-run-end' });
11391
+ if (win && !win.isDestroyed()) {
11392
+ win.webContents.send('agent:event', { type: 'sb-run-end', folder, code: code == null ? null : Number(code) });
11393
+ }
11215
11394
  } catch {}
11216
11395
  });
11217
- sbRunEmit('▶ ' + cmd + ' (klasör: ' + path.basename(folder) + ')');
11396
+ sbSlotPush(slot, folder, '▶ ' + cmd + ' (klasör: ' + path.basename(folder) + ')');
11218
11397
  return {
11219
11398
  ok: true,
11399
+ cmd,
11400
+ kind,
11220
11401
  note: 'süreç ÇALIŞTIR panelinde başlatıldı — çıktı orada canlı akar; durdurmak için paneldeki ■. Kullanıcıya adres varsa bildir.',
11221
11402
  };
11222
11403
  } catch (e) {
11223
- sbRunProc.proc = null;
11224
11404
  return { ok: false, error: String((e && e.message) || e) };
11225
11405
  }
11226
11406
  }
11227
11407
 
11408
+ function sbRunStopFolder(folder) {
11409
+ const slot = sbProcs.get(String(folder || ''));
11410
+ if (!slot || !slot.proc) return false;
11411
+ const p = slot.proc;
11412
+ slot.proc = null;
11413
+ try {
11414
+ /* cmd.exe /c zinciriyle çocuklar (node/vite) geride kalmasın — ağaç kesimi */
11415
+ if (process.platform === 'win32') {
11416
+ const { exec } = require('child_process');
11417
+ exec('taskkill /pid ' + p.pid + '/T /F', { windowsHide: true });
11418
+ } else p.kill('SIGTERM');
11419
+ } catch {}
11420
+ return true;
11421
+ }
11422
+
11228
11423
  ipcMain.handle('sandbox:run', async (_e, payload) => {
11229
11424
  const folder = String((payload && payload.folder) || '');
11230
11425
  const cmd = String((payload && payload.cmd) || '').trim();
11231
- return sbRunStartManaged(folder, cmd);
11426
+ return sbRunStartManaged(folder, cmd, (payload && payload.kind) || 'run');
11232
11427
  });
11233
11428
 
11234
- ipcMain.handle('sandbox:runstop', async () => {
11235
- const p = sbRunProc.proc;
11236
- if (!p) return { ok: true, wasRunning: false };
11237
- sbRunProc.proc = null;
11238
- try {
11239
- /* cmd.exe /c zinciriyle çocuklar (node/vite) geride kalmasın — ağaç kesimi */
11240
- const { exec } = require('child_process');
11241
- if (process.platform === 'win32') exec('taskkill /pid ' + p.pid + ' /T /F', { windowsHide: true });
11242
- else p.kill('SIGTERM');
11243
- } catch {}
11244
- return { ok: true, wasRunning: true };
11429
+ /* proje tipi + önerilen komutlar (kullanıcı özel komutlarıyla birleşik) */
11430
+ ipcMain.handle('sandbox:detect', async (_e, payload) => {
11431
+ const folder = String((payload && payload.folder) || '');
11432
+ if (!folder.startsWith(sandboxRoot() + path.sep)) return { ok: false, error: 'geçersiz klasör' };
11433
+ return { ok: true, info: sbProjectInfoMerged(folder) };
11434
+ });
11435
+
11436
+ /* Kur: algılanan/kayıtlı kurulum komutunu doğrudan çalıştırır (panel + ajan ortak) */
11437
+ function sbInstallRepo(folder) {
11438
+ folder = String(folder || '');
11439
+ if (!folder.startsWith(sandboxRoot() + path.sep)) return { ok: false, error: 'geçersiz klasör' };
11440
+ const info = sbProjectInfoMerged(folder);
11441
+ if (!info.install) return { ok: false, error: 'Bu repoda kurulum komutu algılanamadı — özel komutu kutuya yazıp ▶ ile çalıştır' };
11442
+ const r = sbRunStartManaged(folder, info.install, 'install');
11443
+ if (r.ok) r.cmd = info.install;
11444
+ return r;
11445
+ }
11446
+
11447
+ ipcMain.handle('sandbox:install', async (_e, payload) => sbInstallRepo(String((payload && payload.folder) || '')));
11448
+
11449
+ /* Başlat/Dev: algılanan/kayıtlı çalıştırma komutunu başlatır (panel + ajan ortak) */
11450
+ function sbStartRepo(folder, mode) {
11451
+ folder = String(folder || '');
11452
+ mode = String(mode || 'run') === 'dev' ? 'dev' : 'run';
11453
+ if (!folder.startsWith(sandboxRoot() + path.sep)) return { ok: false, error: 'geçersiz klasör' };
11454
+ const info = sbProjectInfoMerged(folder);
11455
+ const cmd = mode === 'dev' ? info.dev || info.run : info.run || info.dev;
11456
+ if (!cmd) return { ok: false, error: 'Bu repo için çalıştırma komutu algılanamadı — özel komutu kutuya yazıp ▶ ile çalıştır' };
11457
+ const r = sbRunStartManaged(folder, cmd, 'run');
11458
+ if (r.ok) r.cmd = cmd;
11459
+ return r;
11460
+ }
11461
+
11462
+ ipcMain.handle('sandbox:start', async (_e, payload) => {
11463
+ const folder = String((payload && payload.folder) || '');
11464
+ return sbStartRepo(folder, String((payload && payload.mode) || 'run'));
11465
+ });
11466
+
11467
+ /* seçili repo paneli için: çalışıyor mu + tamponlanmış çıktı */
11468
+ ipcMain.handle('sandbox:procstate', async (_e, payload) => {
11469
+ const folder = String((payload && payload.folder) || '');
11470
+ if (!folder.startsWith(sandboxRoot() + path.sep)) return { ok: false, error: 'geçersiz klasör' };
11471
+ const slot = sbProcs.get(folder);
11472
+ return {
11473
+ ok: true,
11474
+ running: !!(slot && slot.proc),
11475
+ kind: slot ? slot.kind : '',
11476
+ cmd: slot ? slot.cmd : '',
11477
+ url: slot ? slot.url : '',
11478
+ lines: slot ? slot.lines.slice() : [],
11479
+ };
11480
+ });
11481
+
11482
+ /* özel komutları kalıcı kaydet (repo klasörü → install/run/dev) */
11483
+ ipcMain.handle('sandbox:cfg:set', async (_e, payload) => {
11484
+ const folder = String((payload && payload.folder) || '');
11485
+ if (!folder.startsWith(sandboxRoot() + path.sep)) return { ok: false, error: 'geçersiz klasör' };
11486
+ sbLoadCfg();
11487
+ const cur = sbCfg[folder] && typeof sbCfg[folder] === 'object' ? sbCfg[folder] : {};
11488
+ const next = { ...cur };
11489
+ for (const k of ['install', 'run', 'dev']) {
11490
+ if (payload && payload[k] !== undefined) {
11491
+ const v = String(payload[k] || '').trim().slice(0, 500);
11492
+ if (v) next[k] = v;
11493
+ else delete next[k];
11494
+ }
11495
+ }
11496
+ if (Object.keys(next).length) sbCfg[folder] = next;
11497
+ else delete sbCfg[folder];
11498
+ sbSaveCfg();
11499
+ return { ok: true, cfg: sbCfg[folder] || {} };
11500
+ });
11501
+
11502
+ ipcMain.handle('sandbox:runstop', async (_e, payload) => {
11503
+ const folder = String((payload && payload.folder) || '');
11504
+ if (folder) {
11505
+ if (!folder.startsWith(sandboxRoot() + path.sep)) return { ok: false, error: 'geçersiz klasör' };
11506
+ return { ok: true, wasRunning: sbRunStopFolder(folder) };
11507
+ }
11508
+ let any = false;
11509
+ for (const f of Array.from(sbProcs.keys())) {
11510
+ if (sbRunStopFolder(f)) any = true;
11511
+ }
11512
+ return { ok: true, wasRunning: any };
11245
11513
  });
11246
11514
 
11247
11515
  ipcMain.handle('sandbox:openurl', async (_e, url) => {
package/src/preload.js CHANGED
@@ -185,7 +185,12 @@ contextBridge.exposeInMainWorld('beast', {
185
185
  sandboxOpen: (payload) => ipcRenderer.invoke('sandbox:open', payload),
186
186
  sandboxTree: (payload) => ipcRenderer.invoke('sandbox:tree', payload),
187
187
  sandboxRun: (payload) => ipcRenderer.invoke('sandbox:run', payload),
188
- sandboxRunStop: () => ipcRenderer.invoke('sandbox:runstop'),
188
+ sandboxRunStop: (payload) => ipcRenderer.invoke('sandbox:runstop', payload),
189
+ sandboxDetect: (payload) => ipcRenderer.invoke('sandbox:detect', payload),
190
+ sandboxInstall: (payload) => ipcRenderer.invoke('sandbox:install', payload),
191
+ sandboxStart: (payload) => ipcRenderer.invoke('sandbox:start', payload),
192
+ sandboxProcState: (payload) => ipcRenderer.invoke('sandbox:procstate', payload),
193
+ sandboxCfgSet: (payload) => ipcRenderer.invoke('sandbox:cfg:set', payload),
189
194
  sandboxOpenUrl: (url) => ipcRenderer.invoke('sandbox:openurl', url),
190
195
  waStart: () => ipcRenderer.invoke('wa:start'),
191
196
  waStop: () => ipcRenderer.invoke('wa:stop'),
@@ -426,21 +426,23 @@
426
426
  <section id="sbRun">
427
427
  <div id="sbRunHead">
428
428
  <span id="sbRunTitle">ÇALIŞTIR</span>
429
+ <span id="sbRunKind"></span>
429
430
  <span id="sbRunCwd" title="">—</span>
430
431
  </div>
431
432
  <div id="sbRunBtns">
432
- <button id="sbRunInstall" title="Ajana talimat: projeyi kur — doğru komutu ajan bulur"><svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:3px"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>Kur</button>
433
- <button id="sbRunStart" title="Ajana talimat: projeyi çalıştır — uzun süreli süreç ÇALIŞTIR panelinde başlar"><svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor" stroke="none" style="vertical-align:-2px;margin-right:3px"><polygon points="6 3 20 12 6 21 6 3"/></svg>Başlat</button>
434
- <button id="sbRunDev" title="Ajana talimat: geliştirme modunda başlat (dev)"><svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:3px"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>Dev</button>
433
+ <button id="sbRunInstall" title="Kur — algılanan paket yöneticisiyle bağımlılıkları yükle (npm/pip/cargo/go)"><svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:3px"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>Kur</button>
434
+ <button id="sbRunStart" title="Başlat — algılanan komutla çalıştır (sunucu adresi sağdaki tarayıcıda açılır)"><svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor" stroke="none" style="vertical-align:-2px;margin-right:3px"><polygon points="6 3 20 12 6 21 6 3"/></svg>Başlat</button>
435
+ <button id="sbRunDev" title="Geliştirme modunda başlat (npm run dev, vite, expo…)"><svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:3px"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>Dev</button>
435
436
  <button id="sbRunStop" title="Süreci durdur" hidden><svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor" stroke="none" style="vertical-align:-2px;margin-right:3px"><rect x="6" y="6" width="12" height="12" rx="2"/></svg>Durdur</button>
436
437
  </div>
437
438
  <div id="sbRunCustom">
438
439
  <input id="sbRunCmd" placeholder="özel komut: python main.py, cargo run, node index.js…" spellcheck="false" autocomplete="off" />
439
440
  <button id="sbRunGo" title="Çalıştır"><svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor" stroke="none"><polygon points="6 3 20 12 6 21 6 3"/></svg></button>
441
+ <button id="sbRunSave" title="Yazılan komutu bu reponun başlatma komutu olarak kaydet"><svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg></button>
440
442
  </div>
441
443
  <div id="sbRunOut"></div>
442
444
  <div id="sbRunFoot">
443
- <button id="sbRunOpen" title="Ajana talimat: çalışma adresini bulup tarayıcıda açar" disabled><svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:3px"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg><span id="sbRunOpenLabel">Tarayıcıda aç</span></button>
445
+ <button id="sbRunOpen" title="Önizlemeyi sağdaki dahili tarayıcıda aç" disabled><svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:3px"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg><span id="sbRunOpenLabel">Tarayıcıda aç</span></button>
444
446
  <span id="sbRunUrl" title=""></span>
445
447
  </div>
446
448
  </section>
@@ -136,12 +136,14 @@ const els = {
136
136
  sbInput: $('#sbInput'),
137
137
  sbStop: $('#sbStop'),
138
138
  sbRunCwd: $('#sbRunCwd'),
139
+ sbRunKind: $('#sbRunKind'),
139
140
  sbRunInstall: $('#sbRunInstall'),
140
141
  sbRunStart: $('#sbRunStart'),
141
142
  sbRunDev: $('#sbRunDev'),
142
143
  sbRunStop: $('#sbRunStop'),
143
144
  sbRunCmd: $('#sbRunCmd'),
144
145
  sbRunGo: $('#sbRunGo'),
146
+ sbRunSave: $('#sbRunSave'),
145
147
  sbRunOut: $('#sbRunOut'),
146
148
  sbRunOpen: $('#sbRunOpen'),
147
149
  sbRunUrl: $('#sbRunUrl'),
@@ -11964,6 +11966,7 @@ let sbCloning = false;
11964
11966
  const sbTreeState = { cache: new Map(), open: new Set(['']), path: '' };
11965
11967
  let sbRunUrl = '';
11966
11968
  let sbRunRunning = false;
11969
+ let sbRunInfo = null;
11967
11970
 
11968
11971
  function sbModeOn() {
11969
11972
  return document.body.classList.contains('sandbox-mode');
@@ -12095,7 +12098,7 @@ async function loadSandboxList() {
12095
12098
  for (const it of sbItems) {
12096
12099
  const o = document.createElement('option');
12097
12100
  o.value = it.folder;
12098
- o.textContent = it.name + (it.busy ? ' ●' : '');
12101
+ o.textContent = it.name + (it.busy ? ' ●' : '') + (it.running ? ' ▶' : '');
12099
12102
  sel.appendChild(o);
12100
12103
  }
12101
12104
  if (sbFolder && sbItems.some((x) => x.folder === sbFolder)) sel.value = sbFolder;
@@ -12140,6 +12143,31 @@ async function sbSelectCurrent() {
12140
12143
  sbLine('t-err', (r && r.error) || 'oturum açılamadı');
12141
12144
  }
12142
12145
  sbSetBusy(!!(r && r.busy));
12146
+ /* yönetilen süreç durumu: repo değişince arka plandaki süreç + tampon çıktı geri yüklenir */
12147
+ sbRunInfo = null;
12148
+ const st = await beast.sandboxProcState({ folder: sbFolder }).catch(() => null);
12149
+ if (st && st.ok) {
12150
+ sbRunUrl = st.url || '';
12151
+ sbRunRunning = !!st.running;
12152
+ if (els.sbRunStop) els.sbRunStop.hidden = !sbRunRunning;
12153
+ if (els.sbRunOpen) els.sbRunOpen.disabled = !sbRunUrl;
12154
+ if (els.sbRunUrl) els.sbRunUrl.textContent = sbRunUrl || '';
12155
+ for (const l of st.lines || []) sbRunLine(l);
12156
+ }
12157
+ /* proje tipi algıla: butonlar hangi komutu çalıştıracağını bilir */
12158
+ const det = await beast.sandboxDetect({ folder: sbFolder }).catch(() => null);
12159
+ if (det && det.ok && det.info) {
12160
+ sbRunInfo = det.info;
12161
+ if (els.sbRunKind) els.sbRunKind.textContent = det.info.label || '';
12162
+ if (els.sbRunCmd) {
12163
+ els.sbRunCmd.placeholder = det.info.run
12164
+ ? 'özel komut (algılanan: ' + det.info.run + ')…'
12165
+ : 'özel komut: python main.py, cargo run, node index.js…';
12166
+ }
12167
+ if (!sbRunRunning && !(st && (st.lines || []).length)) {
12168
+ sbRunLine('ℹ ' + (det.info.label || 'proje') + ' · kur: ' + (det.info.install || '—') + ' · çalıştır: ' + (det.info.run || '—'), 't-dim');
12169
+ }
12170
+ }
12143
12171
  /* sol konsol: seçilen reponun klasörü — oturum açıldıktan sonra da garanti tazele */
12144
12172
  sbTreeState.cache.clear();
12145
12173
  if (sbModeOn()) renderFileTree().catch(() => {});
@@ -12242,26 +12270,24 @@ function setSandboxMode(on) {
12242
12270
  }
12243
12271
  }
12244
12272
 
12245
- /* ---------- ÇALIŞTIRICI: butonlar KOMUT DEĞİL AJANA TALİMATTIR ----------
12246
- Her proje farklı çalışır; komutu kullanıcı bilemez, AJAN bilir:
12247
- paket bildirim dosyasına + README'ye bakıp doğru komutu bulur, uzun süreli
12248
- süreci panel_run aracıyla ÇALIŞTIR panelinde (turu kilitlemeden) başlatır. */
12249
- function sbInstruct(text) {
12273
+ /* ---------- ÇALIŞTIRICI v2: butonlar DETERMİNİSTİK ----------
12274
+ Proje tipi algılanır (package.json/requirements.txt/pyproject/Cargo.toml/
12275
+ go.mod/index.html) ve Kur/Başlat/Dev butonları algılanan komutu DOĞRUDAN
12276
+ çalıştırır; süreç panelde canlı akar, repo başına ayrı yuvada koşar. */
12277
+ async function sbRunLaunch(mode) {
12250
12278
  if (!sbFolder) { toast('Önce repo indir ya da seç'); return; }
12251
- sbLine('t-cmd', 'sen> ' + text);
12252
- sbSetBusy(true);
12253
- beast.sandboxSend({ folder: sbFolder, msg: text }).then((r) => {
12254
- if (r && r.ok) {
12255
- if (r.sessionId) sbSessionId = r.sessionId;
12256
- if (r.queued) sbLine('t-dim', '⏳ kuyrukta (' + r.count + ') — iş bitince gönderilir');
12257
- } else {
12258
- sbSetBusy(false);
12259
- sbLine('t-err', (r && r.error) || 'gönderilemedi');
12260
- }
12261
- }).catch((e) => {
12262
- sbSetBusy(false);
12263
- sbLine('t-err', String((e && e.message) || e));
12264
- });
12279
+ if (sbRunRunning) { toast('Bu repoda süreç çalışıyor — önce ■ Durdur'); return; }
12280
+ const call = mode === 'install'
12281
+ ? beast.sandboxInstall({ folder: sbFolder })
12282
+ : beast.sandboxStart({ folder: sbFolder, mode: mode === 'dev' ? 'dev' : 'run' });
12283
+ const r = await call.catch((e) => ({ ok: false, error: String((e && e.message) || e) }));
12284
+ if (!r || !r.ok) {
12285
+ sbRunLine('[hata] ' + ((r && r.error) || 'başlatılamadı'), 't-err');
12286
+ return;
12287
+ }
12288
+ sbRunRunning = true;
12289
+ if (els.sbRunStop) els.sbRunStop.hidden = false;
12290
+ sbBadge(sbFolder, true);
12265
12291
  }
12266
12292
 
12267
12293
  function sbRunLine(text, cls) {
@@ -12274,8 +12300,28 @@ function sbRunLine(text, cls) {
12274
12300
  return div;
12275
12301
  }
12276
12302
 
12277
- /* süreç çıktısı akışı: sessionId'siz global olaylar — en üstte yönlendirilir */
12303
+ /* repo listesindeki rozet: ▶ = bu repoda süreç çalışıyor */
12304
+ function sbBadge(folder, on) {
12305
+ if (!els.sbRepo || !folder) return;
12306
+ for (const o of Array.from(els.sbRepo.options)) {
12307
+ if (o.value !== folder) continue;
12308
+ const it = sbItems.find((x) => x.folder === folder);
12309
+ const name = it ? it.name : o.textContent.replace(/ (?:●|▶)+$/g, '');
12310
+ o.textContent = name + (it && it.busy ? ' ●' : '') + (on ? ' ▶' : '');
12311
+ return;
12312
+ }
12313
+ }
12314
+
12315
+ /* süreç çıktısı akışı: klasör etiketli olaylar — seçili repo paneli süzer,
12316
+ arka plandaki repolar yalnızca rozetlerini günceller */
12278
12317
  function sbRunIngest(ev) {
12318
+ const folder = String(ev.folder || '');
12319
+ const mine = !folder || !sbFolder || folder === sbFolder;
12320
+ if (!mine) {
12321
+ if (ev.type === 'sb-run-url') sbBadge(folder, true);
12322
+ if (ev.type === 'sb-run-end') sbBadge(folder, false);
12323
+ return;
12324
+ }
12279
12325
  if (ev.type === 'sb-run') {
12280
12326
  sbRunLine(ev.data);
12281
12327
  return;
@@ -12285,11 +12331,15 @@ function sbRunIngest(ev) {
12285
12331
  sbRunLine('🌐 dev server: ' + sbRunUrl, 't-dim');
12286
12332
  if (els.sbRunOpen) els.sbRunOpen.disabled = !sbRunUrl;
12287
12333
  if (els.sbRunUrl) els.sbRunUrl.textContent = sbRunUrl;
12334
+ sbBadge(folder || sbFolder, true);
12335
+ /* sağdaki dahili tarayıcıda otomatik açılır — sandbox modundayken */
12336
+ if (sbModeOn() && sbRunUrl) beast.sandboxOpenUrl(sbRunUrl).catch(() => {});
12288
12337
  return;
12289
12338
  }
12290
12339
  if (ev.type === 'sb-run-end') {
12291
12340
  sbRunRunning = false;
12292
12341
  if (els.sbRunStop) els.sbRunStop.hidden = true;
12342
+ sbBadge(folder || sbFolder, false);
12293
12343
  }
12294
12344
  }
12295
12345
 
@@ -12303,6 +12353,8 @@ function sbRunExec(cmd) {
12303
12353
  sbRunRunning = false;
12304
12354
  if (els.sbRunStop) els.sbRunStop.hidden = true;
12305
12355
  sbRunLine('[hata] ' + ((r && r.error) || 'çalıştırılamadı'), 't-err');
12356
+ } else {
12357
+ sbBadge(sbFolder, true);
12306
12358
  }
12307
12359
  }).catch((e) => {
12308
12360
  sbRunRunning = false;
@@ -12370,17 +12422,10 @@ if (els.sbStop) {
12370
12422
  }).catch(() => {});
12371
12423
  });
12372
12424
  }
12373
- /* ÇALIŞTIRICI butonları — KOMUT DEĞİL AJANA TALİMAT (komutu ajan bulur):
12374
- uzun süreli süreci panel_run ile ÇALIŞTIR paneline bırakır, tur kilitlenmez */
12375
- if (els.sbRunInstall) els.sbRunInstall.addEventListener('click', () =>
12376
- sbInstruct('Bu projeyi KUR — paket bildirim dosyasına (package.json, requirements.txt, Cargo.toml, go.mod, pom.xml vb.) göre doğru kurulum komutunu bul ve çalıştır (run_command); sonucu 1-2 satırla raporla.')
12377
- );
12378
- if (els.sbRunStart) els.sbRunStart.addEventListener('click', () =>
12379
- sbInstruct('Bu projeyi ÇALIŞTIR — README/package.json scripts\u2019e göre doğru başlatma komutunu bul; uzun süreli sunucu/süreçse panel_run ile ÇALIŞTIR panelinde başlat (tur kilitleme), adresini/çalıştığını kullanıcıya bildir.')
12380
- );
12381
- if (els.sbRunDev) els.sbRunDev.addEventListener('click', () =>
12382
- sbInstruct('Bu projeyi GELİŞTİRME modunda başlat — package.json scripts\u2019e bak (npm run dev, vite, expo start, ng serve vb. hangisi varsa) ve panel_run ile ÇALIŞTIR panelinde başlat; adresini kullanıcıya bildir.')
12383
- );
12425
+ /* ÇALIŞTIRICI butonları: algılanan komutu doğrudan çalıştırır */
12426
+ if (els.sbRunInstall) els.sbRunInstall.addEventListener('click', () => sbRunLaunch('install'));
12427
+ if (els.sbRunStart) els.sbRunStart.addEventListener('click', () => sbRunLaunch('run'));
12428
+ if (els.sbRunDev) els.sbRunDev.addEventListener('click', () => sbRunLaunch('dev'));
12384
12429
  if (els.sbRunGo) els.sbRunGo.addEventListener('click', () => {
12385
12430
  const c = els.sbRunCmd.value.trim();
12386
12431
  if (!c) { toast('Komut yaz'); return; }
@@ -12395,17 +12440,38 @@ if (els.sbRunCmd) {
12395
12440
  }
12396
12441
  });
12397
12442
  }
12443
+ if (els.sbRunSave) els.sbRunSave.addEventListener('click', async () => {
12444
+ if (!sbFolder) { toast('Önce repo seç'); return; }
12445
+ const c = els.sbRunCmd ? els.sbRunCmd.value.trim() : '';
12446
+ if (!c) { toast('Kaydetmek için komut yaz'); return; }
12447
+ const r = await beast.sandboxCfgSet({ folder: sbFolder, run: c }).catch(() => null);
12448
+ if (r && r.ok) {
12449
+ toast('kaydedildi — Başlat/Dev artık bu komutu kullanır');
12450
+ const det = await beast.sandboxDetect({ folder: sbFolder }).catch(() => null);
12451
+ if (det && det.ok && det.info) {
12452
+ sbRunInfo = det.info;
12453
+ if (els.sbRunCmd) els.sbRunCmd.placeholder = 'özel komut (algılanan: ' + (det.info.run || c) + ')…';
12454
+ }
12455
+ } else {
12456
+ toast((r && r.error) || 'kaydedilemedi');
12457
+ }
12458
+ });
12398
12459
  if (els.sbRunStop) {
12399
12460
  els.sbRunStop.addEventListener('click', () => {
12400
- beast.sandboxRunStop().then((r) => {
12401
- if (r && r.ok) sbRunLine('⏹ durduruldu', 't-dim');
12461
+ beast.sandboxRunStop({ folder: sbFolder }).then((r) => {
12462
+ if (r && r.ok) {
12463
+ sbRunLine('⏹ durduruldu', 't-dim');
12464
+ sbRunRunning = false;
12465
+ if (els.sbRunStop) els.sbRunStop.hidden = true;
12466
+ sbBadge(sbFolder, false);
12467
+ }
12402
12468
  }).catch(() => {});
12403
12469
  });
12404
12470
  }
12405
12471
  if (els.sbRunOpen) {
12406
- els.sbRunOpen.addEventListener('click', () => {
12407
- /* Tarayıcıda aç da AJANA TALİMAT: ajan çalışma adresini bulup kendi
12408
- browser_open aracıyla dahili tarayıcıda açar */
12409
- sbInstruct('Bu projenin web arayüzü çalışıyorsa adresini tespit et (çıktılar/README/port taraması) ve browser_open aracıyla DAHİLİ TARAYICIDA aç.')
12472
+ els.sbRunOpen.addEventListener('click', async () => {
12473
+ if (!sbRunUrl) { toast('Adres henüz yakalanmadı — Başlat/Dev ile sunucuyu çalıştır'); return; }
12474
+ const r = await beast.sandboxOpenUrl(sbRunUrl).catch(() => null);
12475
+ if (!r || !r.ok) toast('Açılamadı: ' + ((r && r.error) || 'bilinmeyen hata'));
12410
12476
  });
12411
12477
  }
@@ -4409,6 +4409,18 @@ body.sandbox-mode #sandboxRow { display: flex; flex-direction: row; }
4409
4409
  background: var(--panel);
4410
4410
  }
4411
4411
  #sbRunTitle { font-size: 11px; font-weight: 800; letter-spacing: 1px; color: var(--accent); flex: none; }
4412
+ #sbRunKind {
4413
+ flex: none;
4414
+ font-size: 10px;
4415
+ font-weight: 700;
4416
+ letter-spacing: 0.5px;
4417
+ text-transform: uppercase;
4418
+ color: var(--muted);
4419
+ border: 1px solid var(--border);
4420
+ border-radius: 4px;
4421
+ padding: 1px 6px;
4422
+ }
4423
+ #sbRunKind:empty { display: none; }
4412
4424
  #sbRunCwd {
4413
4425
  color: var(--muted);
4414
4426
  font-family: Consolas, monospace;