beast-agent 2.20.0 → 2.22.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.20.0",
4
+ "version": "2.22.0",
5
5
  "description": "Ultra-fast local agent shell for Windows.",
6
6
  "author": "algokodcom (AlgoKod)",
7
7
  "license": "MIT",
@@ -40,6 +40,22 @@ function invalidate() {
40
40
  cache = null;
41
41
  }
42
42
 
43
+ /* klasör damgası: ajan/kullanıcı klasöre elle müdahale edince (write_file
44
+ vb.) cache BAYATLAR — damga değişince otomatik yeniden taranır */
45
+ let lastStamp = 0;
46
+ function folderStamp() {
47
+ try { return fs.statSync(dir()).mtimeMs; } catch { return 0; }
48
+ }
49
+
50
+ function ensureFresh() {
51
+ const s = folderStamp();
52
+ if (dirty || !cache || s !== lastStamp) {
53
+ cache = scan();
54
+ dirty = false;
55
+ lastStamp = s;
56
+ }
57
+ }
58
+
43
59
  function readTool(d) {
44
60
  try {
45
61
  const json = JSON.parse(fs.readFileSync(path.join(d, 'tool.json'), 'utf8'));
@@ -81,10 +97,7 @@ function list() {
81
97
 
82
98
  /* engine _chatTurn'e OpenAI function formatında şemalar */
83
99
  function definitions() {
84
- if (dirty || !cache) {
85
- cache = scan();
86
- dirty = false;
87
- }
100
+ ensureFresh();
88
101
  return cache.map((t) => ({
89
102
  type: 'function',
90
103
  function: {
@@ -97,10 +110,7 @@ function definitions() {
97
110
  }
98
111
 
99
112
  function names() {
100
- if (dirty || !cache) {
101
- cache = scan();
102
- dirty = false;
103
- }
113
+ ensureFresh();
104
114
  return cache.map((t) => 'tool__' + t.id);
105
115
  }
106
116
 
@@ -2190,7 +2190,52 @@ class Engine {
2190
2190
  flat[k] = rest;
2191
2191
  }
2192
2192
  }
2193
- return [{ role: 'system', content: system }, ...head, ...flat];
2193
+ /* emniyet: assistant(tool_calls) ↔ tool sonucu ÇİFTLERİ hizala —
2194
+ yetim tool mesajı / cevapsız tool_call sağlayıcıda
2195
+ "Messages with role 'tool' must be a response to a preceding message
2196
+ with 'tool_calls'" (HTTP 400) verir (tur ortası enjeksiyon/kırpma
2197
+ çifti böldüğünde). */
2198
+ return Engine._alignToolPairs([{ role: 'system', content: system }, ...head, ...flat]);
2199
+ }
2200
+
2201
+ /* assistant(tool_calls) + hemen ardından gelen tool sonuçlarını hizalar:
2202
+ - cevabı GELMEYEN tool_call, assistant'tan DÜŞER (sonuç yok)
2203
+ - karşılıksız tool sonucu (yetim) DÜŞER
2204
+ - hiç sonucu olmayan assistant, düz metin asistana döner
2205
+ - tool_calls'sız mesajların ardından gelen tool'lar da yetimdir → düşer */
2206
+ static _alignToolPairs(msgs) {
2207
+ const out = [];
2208
+ for (let i = 0; i < msgs.length; i++) {
2209
+ const m = msgs[i];
2210
+ if (m && m.role === 'assistant' && Array.isArray(m.tool_calls) && m.tool_calls.length) {
2211
+ const results = [];
2212
+ let j = i + 1;
2213
+ while (j < msgs.length && msgs[j] && msgs[j].role === 'tool') {
2214
+ results.push(msgs[j]);
2215
+ j++;
2216
+ }
2217
+ const answered = new Set(results.map((r) => String(r.tool_call_id || '')));
2218
+ const kept = m.tool_calls.filter((tc) => answered.has(String(tc.id || '')));
2219
+ if (kept.length === m.tool_calls.length) {
2220
+ out.push(m);
2221
+ } else if (kept.length) {
2222
+ out.push({ ...m, tool_calls: kept });
2223
+ } else {
2224
+ const plain = { ...m };
2225
+ delete plain.tool_calls;
2226
+ out.push(plain);
2227
+ }
2228
+ const keepIds = new Set(kept.map((tc) => String(tc.id || '')));
2229
+ for (const r of results) {
2230
+ if (keepIds.has(String(r.tool_call_id || ''))) out.push(r);
2231
+ }
2232
+ i = j - 1;
2233
+ continue;
2234
+ }
2235
+ if (m && m.role === 'tool') continue; // yetim araç sonucu
2236
+ out.push(m);
2237
+ }
2238
+ return out;
2194
2239
  }
2195
2240
 
2196
2241
  _lastUserText(session) {
@@ -5317,23 +5362,25 @@ function emitSafe(engine, sessionId, ev) {
5317
5362
 
5318
5363
  /* AJAN DM aracı: paralel/finance ajanlarının birbirine kısa mesaj atması.
5319
5364
  Salt metin — hedef ajanın oturumuna [AJAN DM] bloğu olarak düşer; AJAN DM
5320
- panelinde iki yönlü görünür. */
5365
+ panelinde iki yönlü görünür. topic verirsen aynı konudaki mesajlar aynı
5366
+ oturumda toplanır (cevap zinciri tek thread'de devam eder). */
5321
5367
  const AGENT_DM_DEF = {
5322
5368
  type: 'function',
5323
5369
  function: {
5324
5370
  name: 'agent_dm',
5325
5371
  description:
5326
- 'Send a short DM to another RUNNING agent (parallel agents / finance agents) to coordinate: share findings, ask status, warn about risk, hand off work. `to` = target session id OR a keyword from the agent title (e.g. "GOLD", "Trader"). The message is delivered to the other agent and shown in the Agent DM panel.',
5372
+ 'Send a short DM to another running agent (parallel agents / finance agents) to coordinate: share findings, ask status, warn about risk, hand off work. `to` = target session id OR a keyword from the agent title (e.g. "GOLD", "Trader"). `topic` = short subject label — replies to the same topic stay in the SAME conversation thread, so ALWAYS reuse the topic you were DMed with when replying.',
5327
5373
  parameters: {
5328
5374
  type: 'object',
5329
5375
  properties: {
5330
5376
  to: { type: 'string', description: 'Target agent session id or a title keyword' },
5377
+ topic: { type: 'string', description: 'Short subject label for the conversation thread, e.g. "GOLD pozisyon riski"' },
5331
5378
  message: { type: 'string', description: 'Short message (1-3 sentences)' },
5332
5379
  },
5333
5380
  required: ['to', 'message'],
5334
5381
  },
5335
5382
  },
5336
- };
5383
+ };
5337
5384
 
5338
5385
  const TOOLS = [
5339
5386
  ...tools.definitions,
@@ -6150,12 +6197,30 @@ Engine.prototype._agentDmSend = function (fromSid, args) {
6150
6197
  if (String(target) === String(fromSid)) return { ok: false, error: 'kendine DM atılamaz' };
6151
6198
  const fromJob = jobs.get(String(fromSid));
6152
6199
  const toJob = jobs.get(target);
6200
+ /* FİNANS SINIRI: Beast Finance oturumu agent_dm kullansa bile finance
6201
+ dünyasının DIŞINA ÇIKMAZ — yalnız finance ajanlarına DM atabilir
6202
+ (trader ↔ GOLD işçisi ↔ finance işçileri). Finance dışı hedef reddedilir. */
6203
+ const fromSess = this.cache.get(String(fromSid)) || null;
6204
+ if (fromSess && fromSess.finance) {
6205
+ const targetSess = this.cache.get(String(target)) || null;
6206
+ if (!targetSess || !targetSess.finance) {
6207
+ return {
6208
+ ok: false,
6209
+ error:
6210
+ 'Beast Finance ajanı yalnız finance dünyasındaki ajanlara DM atabilir — hedef finance dışı: ' +
6211
+ String((toJob && toJob.title) || to),
6212
+ };
6213
+ }
6214
+ }
6215
+ /* konu etiketi: aynı konudaki DM'ler panelde AYNI oturumda toplanır */
6216
+ const topic = String((args && args.topic) || '').replace(/\s+/g, ' ').trim().slice(0, 60) || '(genel)';
6153
6217
  const dm = {
6154
6218
  at: new Date().toISOString(),
6155
6219
  from: String(fromSid),
6156
6220
  fromTitle: (fromJob && fromJob.title) || 'Ajan',
6157
6221
  to: String(target),
6158
6222
  toTitle: (toJob && toJob.title) || 'Ajan',
6223
+ topic,
6159
6224
  text,
6160
6225
  };
6161
6226
  this._agentDms = this._agentDms || [];
@@ -6165,15 +6230,16 @@ Engine.prototype._agentDmSend = function (fromSid, args) {
6165
6230
  fs.writeFileSync(this._agentDmsFile, JSON.stringify({ dms: this._agentDms }, null, 2));
6166
6231
  } catch {}
6167
6232
  emitSafe(this, target, { type: 'agent-dm', ...dm });
6168
- /* teslim: hedef meşgulse pending kuyruğunda bekler, done olunca düşer */
6233
+ /* teslim: hedef meşgulse pending kuyruğunda bekler, done olunca düşer.
6234
+ Cevap zinciri AYNI thread'den sürsün diye topic geri bildirilir. */
6169
6235
  (this._pendingReports = this._pendingReports || []).push({
6170
6236
  parentId: target,
6171
6237
  text:
6172
- `[AJAN DM — ${dm.fromTitle}]\n${text}\n` +
6173
- `(Cevabını agent_dm aracıyla ver — to: "${dm.fromTitle}")`,
6238
+ `[AJAN DM — ${dm.fromTitle} · konu: "${topic}"]\n${text}\n` +
6239
+ `(Cevabını agent_dm aracıyla ver — to: "${dm.fromTitle}", topic: "${topic}")`,
6174
6240
  });
6175
6241
  this.flushPendingReports(target);
6176
- return { ok: true, to: target, toTitle: dm.toTitle };
6242
+ return { ok: true, to: target, toTitle: dm.toTitle, topic };
6177
6243
  } catch (e) {
6178
6244
  return { ok: false, error: String((e && e.message) || e) };
6179
6245
  }
@@ -129,7 +129,8 @@ Get-Content "$env:APPDATA\\beast\\sessions\\bg-jobs.json" | ConvertFrom-Json | %
129
129
  | \`agent/agentdefs.js\` | Özel ajan tanımları: %APPDATA%\beast\agents\\*.md (frontmatter: model/tools/steps/mode + prompt gövdesi); /agent ile bağlanır, run_background(agent:) ile paralel ajana verilir |
130
130
  | \`agent/searxng.js\` | SearXNG entegrasyonu: gömülü Python'a kurulum + 127.0.0.1:8888 başlatıcı + JSON arama motoru |
131
131
  | \`agent/bots.js\` | Bot CRUD, 5 haneli bot kodu, izolasyon; ad-ilk-harf doğrulaması |
132
- | \`agent/skills.js\` | Builtin tohumlar (help, email, python-web-search, pdf, gold-trading + superpowers metodoloji paketi: brainstorming/writing-plans/executing-plans/TDD/paralel-ajan/alt-ajan-geliştirme/sistematik-debugging/doğrulama/skill-yazma) + SKILL.md taraması + taslaklar |
132
+ | \`agent/skills.js\` | Builtin tohumlar (help, email, python-web-search, pdf, gold-trading, tool-yazma + superpowers metodoloji paketi: brainstorming/writing-plans/executing-plans/TDD/paralel-ajan/alt-ajan-geliştirme/sistematik-debugging/doğrulama/skill-yazma) + SKILL.md taraması + taslaklar |
133
+ | \`agent/customtools.js\` | Kişisel toollar: %APPDATA%\\beast\\tools\\<ad>\\tool.json + run.js (stdin JSON → stdout JSON) — tool__<ad> olarak TÜM oturumlara açılır; sol panel TOOLLAR kutusunda yazılır/test edilir. Yeni tool yazarken \`tool-yazma\` skill'ini oku. |
133
134
  | \`agent/memory.js\`, \`agent/kb.js\` | Kalıcı hafıza + bilgi bankası |
134
135
  | \`cron.js\` (src kökü), \`agent/watchers.js\`, \`agent/bus.js\` | Cron, izleyici, olay merkezi — /stop bunlara DOKUNMAZ |
135
136
  | \`agent/usage.js\` | Token/maliyet sayaçları |
@@ -456,6 +457,87 @@ const { createWorker } = require('tesseract.js');
456
457
  | read_file boş metin döndü | taranmış PDF → 4. adım (görselleştir) + 5. adım (OCR) |
457
458
  | tesseract dil verisi hatası | ilk indirme internet ister; tekrar dene |`,
458
459
  },
460
+ {
461
+ folder: 'tool-yazma',
462
+ force: true,
463
+ body: `---
464
+ name: tool-yazma
465
+ description: Kişisel TOOL yazma/düzenleme rehberi — %APPDATA%\\beast\\tools\\<ad>\\ altına tool.json + run.js yazılacak veya mevcut bir tool editlenecekse ÖNCE bu skill'i oku. Tool'un sözleşmesi: stdin JSON argüman → stdout JSON sonuç; ajan tool__<ad> olarak çağırır (Beast Finance dahil tüm oturumlarda çalışır).
466
+ version: 1.0.0
467
+ ---
468
+
469
+ # Kişisel TOOL Yazma
470
+
471
+ ## TOOL NEDİR / NE ZAMAN YAZILIR
472
+
473
+ Kişisel tool = modelin alet çantasına GİREN gerçek bir araçtır: %APPDATA%\\beast\\tools\\<slug>\\ klasöründe **tool.json** (tanım) + **run.js** (kod) bulunur. Tüm oturumlarda — Beast Finance chat/trader/işçiler dahil — \`tool__<slug>\` adıyla çağrılır.
474
+
475
+ Yaz ZAMAN: mevcut araçlar işi görmüyorsa (örn. broker sembol adlandırması mt5_trade'i düşürüyorsa), özel bir API'ye bağlanman, tekrarlı bir hesap/işlem rutini gerekiyorsa. Basit işler için YAZMA — önce yerleşik araçları (mt5_*, web_search, python_run, run_command) dene.
476
+
477
+ ## SÖZLEŞME (birebir uygula)
478
+
479
+ - **stdin**: JSON argümanlar (tek satırda, güvenli parse)
480
+ - **stdout**: SADECE JSON sonuç — son satır JSON'a çevrilmeye çalışılır; günlük/metin yazarsan JSON bozulur, \`console.error\` kullan
481
+ - **çalışma klasörü**: tool'un kendi klasörü (yanına dosya yazabilirsin)
482
+ - **zaman aşımı**: 90 sn — uzun işleri kendin parçalama, \`run_background\`'a devret
483
+ - **sonuç biçimi**: \`{ "ok": true, ... }\` ya da \`{ "ok": false, "error": "sebep" }\` — ok alanı ZORUNLU
484
+
485
+ ## YENİ TOOL OLUŞTURMA (2 dosya, write_file ile)
486
+
487
+ \`%APPDATA%\\beast\\tools\\<slug>\\tool.json\`:
488
+ \`\`\`json
489
+ {
490
+ "name": "gold_emir",
491
+ "description": "GOLD için özel emir açar — side (buy/sell), lot, sl, tp alır; sonucu ticket ile döndürür.",
492
+ "parameters": {
493
+ "type": "object",
494
+ "properties": {
495
+ "side": { "type": "string", "description": "buy | sell" },
496
+ "lot": { "type": "number", "description": "0.01-1.0" },
497
+ "sl": { "type": "number" },
498
+ "tp": { "type": "number" }
499
+ },
500
+ "required": ["side", "lot"]
501
+ },
502
+ "updatedAt": "2026-09-09T00:00:00.000Z"
503
+ }
504
+ \`\`\`
505
+
506
+ \`%APPDATA%\\beast\\tools\\<slug>\\run.js\`:
507
+ \`\`\`javascript
508
+ 'use strict';
509
+ // stdin: JSON args · stdout: JSON sonuç (SADECE JSON yaz!)
510
+ const fs = require('fs');
511
+ let args = {};
512
+ try { args = JSON.parse(fs.readFileSync(0, 'utf8') || '{}'); } catch {}
513
+ (function main() {
514
+ const { side, lot, sl, tp } = args;
515
+ if (!side || !lot) {
516
+ console.log(JSON.stringify({ ok: false, error: 'side ve lot zorunlu' }));
517
+ return;
518
+ }
519
+ // --- kendi işin burada: HTTP çağrısı, MT5 köprüsü, dosya, hesap... ---
520
+ // Örn: https modülüyle özel endpoint'e istek at, sonucu ok alanıyla sar.
521
+ console.log(JSON.stringify({ ok: true, side, lot, sl: sl || 0, tp: tp || 0, ticket: 0 }));
522
+ })();
523
+ \`\`\`
524
+
525
+ Slug kuralları: a-z, 0-9, alt çizgi, tire — 2-24 karakter (örn: gold_emir, haber_ozet). Hazır taslak: %APPDATA%\\beast\\tools\\ornek_echo klasörünü referans al; yeni tool'u o biçimle yaz.
526
+
527
+ ## DÜZENLEME / DOĞRULAMA
528
+
529
+ 1. Mevcut toolu değiştirme: \`edit_file\` ile tool.json / run.js düzenle — ASLA tüm klasörü silip baştan yazma.
530
+ 2. Kayıttan sonra tool ANINDA aktifleşir (yeniden başlatma gerekmez) — bir sonraki turda \`tool__<slug>\` çağrılabilir.
531
+ 3. Doğrula: \`run_command\` ile \`echo \\"{\\\\\\"mesaj\\\\\\":\\\\\\"test\\\\\\"}\\" | node \\"%APPDATA%\\\\beast\\\\tools\\\\<slug>\\\\run.js\\"\` — çıktı JSON olmalı ve \`ok\` alanı içermeli.
532
+ 4. Hata çıktısı \`ok:false + error\` olsun — model hatayı okuyup düzeltir; çıplak stack yasak.
533
+
534
+ ## GÜVENLİK + STİL
535
+
536
+ - Şifre/API anahtarını ASLA stdout'a, log'a, skill'e yazma.
537
+ - Ağ çağrılarında timeout kullan (\`https\` modülü + \`req.setTimeout\` veya fetch); sonsuz bekleyen script yazma.
538
+ - Tek sorumluluk: bir tool = bir iş. Çok işli dev tool yazma — parçala.
539
+ - Kullanıcıya tool'u nasıl yöneteceğini söyle: sol panelde TOOLLAR kutusunda test/düzenle/sil + 📂 klasör.`,
540
+ },
459
541
  ];
460
542
 
461
543
  function writeSeed(seed) {
package/src/main.js CHANGED
@@ -5063,22 +5063,19 @@ ipcMain.handle('agent:send', (_e, { sessionId, text }) => {
5063
5063
  }
5064
5064
  }
5065
5065
  } catch {}
5066
- /* BEAST FINANCE: finance modu açıkken bu sohbet finance listesine girer ve
5067
- mt5_* araçlarını görür (botlar hariç); mod kapalıysa etiket kaldırılır */
5066
+ /* BEAST FINANCE: finance modunda yazılan sohbetler KALICI finance etiketi
5067
+ taşır — beast finance'ta kayıtlı kalır; normal modda yazmak etiketi
5068
+ KALDIRMAZ (oturum finance dışına çıkamaz) */
5068
5069
  try {
5069
5070
  if (sessionId) {
5070
5071
  const fsid = String(sessionId);
5071
5072
  let fsess = engine.cache.get(fsid);
5072
5073
  if (!fsess) { try { fsess = engine._load(fsid); } catch {} }
5073
- if (fsess && !fsess.botId && !fsess.bgJob) {
5074
- if (financeState.mode) {
5075
- if (!fsess.finance) engine.markFinance(fsid, false);
5076
- finApplyTraderFields(fsess);
5077
- fsess.financeTrader = false; /* chat copilot'ı — trader değil */
5078
- engine.cache.set(fsid, fsess);
5079
- } else if (fsess.finance) {
5080
- engine.unmarkFinance(fsid); /* normal modda yazınca normal sohbete döner */
5081
- }
5074
+ if (fsess && !fsess.botId && !fsess.bgJob && financeState.mode && !fsess.finance) {
5075
+ engine.markFinance(fsid, false);
5076
+ finApplyTraderFields(fsess);
5077
+ fsess.financeTrader = false; /* chat copilot'ı — trader değil */
5078
+ engine.cache.set(fsid, fsess);
5082
5079
  }
5083
5080
  }
5084
5081
  } catch {}
@@ -8369,6 +8366,22 @@ try {
8369
8366
  });
8370
8367
  } catch {}
8371
8368
 
8369
+ /* TOOLS klasörü izleyici: ajan write_file ile tool yazdığında (ya da elle)
8370
+ konsol OTOMATİK güncellenir — restart gerekmez */
8371
+ try {
8372
+ fs.mkdirSync(customtools.dir(), { recursive: true });
8373
+ let toolWatchTimer = null;
8374
+ fs.watch(customtools.dir(), { recursive: true }, () => {
8375
+ clearTimeout(toolWatchTimer);
8376
+ toolWatchTimer = setTimeout(() => {
8377
+ customtools.invalidate();
8378
+ try {
8379
+ if (win && !win.isDestroyed()) win.webContents.send('agent:event', { type: 'tools-changed' });
8380
+ } catch {}
8381
+ }, 400);
8382
+ });
8383
+ } catch {}
8384
+
8372
8385
  function financeEnsureBridge() {
8373
8386
  const f = finCfg();
8374
8387
  const candidates = [String(f.pythonPath || '').trim(), 'python', 'py -3'].filter(Boolean);
@@ -8744,10 +8757,9 @@ ipcMain.handle('finance:mode', async (_e, payload) => {
8744
8757
  (eski davranış WhatsApp oturumunu da finance listesine karıştırıyordu) */
8745
8758
  needNew = true;
8746
8759
  }
8747
- } else {
8748
- /* mod kapandı: aktif finance oturumu normal sohbete döner */
8749
- if (s.finance) engine.unmarkFinance(sid);
8750
8760
  }
8761
+ /* mod kapandığında finance oturumları etiketlerini KORUR —
8762
+ beast finance'ta kayıtlı kalırlar (kaybolmazlar) */
8751
8763
  engine.cache.set(sid, s);
8752
8764
  }
8753
8765
  } catch {}
@@ -373,6 +373,22 @@
373
373
  </aside>
374
374
  </div>
375
375
 
376
+ <div id="dmOverlay" hidden>
377
+ <div id="dmDialog">
378
+ <div id="dmDialogHead">
379
+ <span id="dmDialogTitle">AJAN DM — OTURUMLAR</span>
380
+ <span class="bot-bar-btns">
381
+ <button id="dmBackBtn" title="Oturum listesine dön" hidden>&#x25C0;&#xFE0E; Oturumlar</button>
382
+ <button id="dmColumnBtn" title="Yan sütunda canlı akış olarak da göster">&#x25A4;&#xFE0E;</button>
383
+ <button id="dmModalClear" title="Tüm DM geçmişini sil"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/></svg></button>
384
+ <button id="dmModalClose" title="Kapat">&#x2715;&#xFE0E;</button>
385
+ </span>
386
+ </div>
387
+ <div id="dmDialogList"></div>
388
+ <div class="dm-hint">Thread = ajan çifti + konu · gelen/giden hepsi kronolojik</div>
389
+ </div>
390
+ </div>
391
+
376
392
  <div id="toolOverlay" hidden>
377
393
  <div id="toolDialog">
378
394
  <div id="toolDialogHead">
@@ -45,6 +45,12 @@ const els = {
45
45
  dmBtn: $('#dmBtn'),
46
46
  dmRailList: $('#dmRailList'),
47
47
  dmClear: $('#dmClear'),
48
+ dmOverlay: $('#dmOverlay'),
49
+ dmDialogList: $('#dmDialogList'),
50
+ dmModalClose: $('#dmModalClose'),
51
+ dmModalClear: $('#dmModalClear'),
52
+ dmColumnBtn: $('#dmColumnBtn'),
53
+ dmBackBtn: $('#dmBackBtn'),
48
54
  watchBtn: $('#watchBtn'),
49
55
  watchPaneList: $('#watchPaneList'),
50
56
  watchPaneOpen: $('#watchPaneOpen'),
@@ -5019,25 +5025,41 @@ async function loadRailChat(id) {
5019
5025
  }
5020
5026
  }
5021
5027
 
5022
- /* ---------------- AJAN DM sütunu (ajanlar arası konuşma) ----------------
5023
- rail ile aynı mimari: sağda ikinci sütun, dmBtn ile açılır/kapanır.
5024
- DM'ler ajanların agent_dm araç çağrılarından gelir; çiftlere gruplanır. */
5028
+ /* ---------------- AJAN DM (ajanlar arası konuşma) ----------------
5029
+ Paralel Ajanlar gibi: ✉ sağdaki SÜTUNU açar/kapatır (oturum listesi,
5030
+ canlı akış). Sütunda bir oturuma tıklayınca SOHBET yalnızca MODAL'da
5031
+ açılır — gelen/giden hepsi kronolojik. Thread = ajan çifti + KONU. */
5025
5032
 
5026
5033
  const dmState = {
5027
- dms: [], // { at, from, fromTitle, to, toTitle, text }
5028
- open: new Set(), // açık thread key'leri
5034
+ dms: [], // { at, from, fromTitle, to, toTitle, topic, text }
5035
+ modalThread: null, // modalde açık oturum key'i (null = oturum listesi)
5029
5036
  };
5030
5037
 
5031
- function dmPairOf(dm) {
5038
+ function dmThreadOf(dm) {
5032
5039
  const a = String(dm.from || '');
5033
5040
  const b = String(dm.to || '');
5034
- return a < b ? a + '|' + b : b + '|' + a;
5041
+ const pair = a < b ? a + '|' + b : b + '|' + a;
5042
+ return pair + '|' + String(dm.topic || '(genel)').toLowerCase();
5035
5043
  }
5036
5044
 
5037
- function dmPairTitle(dm) {
5045
+ function dmThreadTitle(dm) {
5038
5046
  const a = String(dm.fromTitle || 'Ajan');
5039
5047
  const b = String(dm.toTitle || 'Ajan');
5040
- return a < b ? a + ' ↔ ' + b : b + ' ↔ ' + a;
5048
+ const pair = a < b ? a + ' ↔ ' + b : b + ' ↔ ' + a;
5049
+ const topic = String(dm.topic || '(genel)');
5050
+ return topic === '(genel)' ? pair : pair + ' · ' + topic;
5051
+ }
5052
+
5053
+ function dmThreads() {
5054
+ const threads = new Map();
5055
+ for (const dm of dmState.dms) {
5056
+ const key = dmThreadOf(dm);
5057
+ if (!threads.has(key)) threads.set(key, []);
5058
+ threads.get(key).push(dm);
5059
+ }
5060
+ return [...threads.entries()].sort(
5061
+ (x, y) => new Date(y[1][y[1].length - 1].at) - new Date(x[1][x[1].length - 1].at)
5062
+ );
5041
5063
  }
5042
5064
 
5043
5065
  function dmTime(iso) {
@@ -5046,51 +5068,34 @@ function dmTime(iso) {
5046
5068
  return d.toLocaleTimeString('tr-TR', { hour: '2-digit', minute: '2-digit' });
5047
5069
  }
5048
5070
 
5071
+ function dmMsgHtml(m) {
5072
+ return (
5073
+ `<div class="dmt-msg"><span class="dm-who">${escapeHtml(String(m.fromTitle || 'Ajan'))} → ${escapeHtml(String(m.toTitle || 'Ajan'))}</span>` +
5074
+ `${escapeHtml(String(m.text || ''))}<span class="dm-at">${dmTime(m.at)}</span></div>`
5075
+ );
5076
+ }
5077
+
5078
+ /* --- sağ sütun: OTURUM LİSTESİ (rail gibi) --- */
5079
+
5049
5080
  function renderDmRail() {
5050
5081
  const list = els.dmRailList;
5051
5082
  if (!list) return;
5052
5083
  if (!dmState.dms.length) {
5053
- list.innerHTML = '<div class="dm-empty">Ajanlar arasında DM yok.<br>Ajanlar agent_dm aracıyla birbirine mesaj attıkça burada görünür.</div>';
5084
+ list.innerHTML = '<div class="dm-empty">Ajanlar arasında DM yok.<br>Ajanlar agent_dm aracıyla birbirine mesaj attıkça oturumlar burada birikir.</div>';
5054
5085
  return;
5055
5086
  }
5056
- /* çiftlere grupla (kronolojik) — en yeni thread üstte */
5057
- const threads = new Map();
5058
- for (const dm of dmState.dms) {
5059
- const key = dmPairOf(dm);
5060
- if (!threads.has(key)) threads.set(key, []);
5061
- threads.get(key).push(dm);
5062
- }
5063
- const rows = [...threads.entries()].sort(
5064
- (x, y) => new Date(y[1][y[1].length - 1].at) - new Date(x[1][x[1].length - 1].at)
5065
- );
5066
5087
  list.innerHTML = '';
5067
- for (const [key, msgs] of rows) {
5068
- const open = dmState.open.has(key);
5088
+ for (const [key, msgs] of dmThreads()) {
5069
5089
  const last = msgs[msgs.length - 1];
5070
5090
  const el = document.createElement('div');
5071
- el.className = 'dmt' + (open ? ' open' : '');
5091
+ el.className = 'dmt';
5072
5092
  el.innerHTML =
5073
5093
  '<div class="dmt-head">' +
5074
- `<span class="dmt-title">${escapeHtml(dmPairTitle(last))}</span>` +
5094
+ `<span class="dmt-title">${escapeHtml(dmThreadTitle(last))}</span>` +
5075
5095
  `<span class="dmt-count">${msgs.length}</span>` +
5076
5096
  '</div>' +
5077
- `<div class="dmt-last">${escapeHtml(String(last.text || '').replace(/\s+/g, ' ').slice(0, 90))} · ${dmTime(last.at)}</div>` +
5078
- '<div class="dmt-msgs">' +
5079
- msgs
5080
- .map(
5081
- (m) =>
5082
- `<div class="dmt-msg"><span class="dm-who">${escapeHtml(String(m.fromTitle || 'Ajan'))} → ${escapeHtml(String(m.toTitle || 'Ajan'))}</span>` +
5083
- `${escapeHtml(String(m.text || ''))}<span class="dm-at">${dmTime(m.at)}</span></div>`
5084
- )
5085
- .join('') +
5086
- '</div>';
5087
- el.addEventListener('click', () => {
5088
- if (dmState.open.has(key)) dmState.open.delete(key);
5089
- else dmState.open.add(key);
5090
- renderDmRail();
5091
- const again = els.dmRailList.querySelector('.dmt.open .dmt-msgs');
5092
- if (again) again.scrollTop = again.scrollHeight;
5093
- });
5097
+ `<div class="dmt-last">${escapeHtml(String(last.text || '').replace(/\s+/g, ' ').slice(0, 90))} · ${dmTime(last.at)}</div>`;
5098
+ el.addEventListener('click', () => openDmModal(key)); // sohbet yalnız modalde
5094
5099
  list.appendChild(el);
5095
5100
  }
5096
5101
  }
@@ -5101,6 +5106,7 @@ async function refreshDmRail() {
5101
5106
  if (Array.isArray(dms)) dmState.dms = dms;
5102
5107
  } catch {}
5103
5108
  renderDmRail();
5109
+ if (els.dmOverlay && !els.dmOverlay.hidden) renderDmModal();
5104
5110
  }
5105
5111
 
5106
5112
  function toggleDmRail(hide) {
@@ -5111,18 +5117,97 @@ function toggleDmRail(hide) {
5111
5117
  if (!hide) refreshDmRail();
5112
5118
  }
5113
5119
 
5114
- if (els.dmBtn) {
5115
- els.dmBtn.addEventListener('click', () => {
5116
- toggleDmRail(!$('#dmRail').classList.contains('dm-hidden'));
5120
+ /* --- modal: SOHBET (yalnız burada tam döküm) --- */
5121
+
5122
+ function renderDmModal() {
5123
+ const list = els.dmDialogList;
5124
+ if (!list) return;
5125
+ if (!dmState.dms.length) {
5126
+ list.innerHTML = '<div class="dm-empty">Henüz ajan DM\u2019i yok.<br>Ajanlar agent_dm aracıyla birbirine mesaj attıkça oturumlar birikir.</div>';
5127
+ if (els.dmDialogTitle) els.dmDialogTitle.textContent = 'AJAN DM — OTURUMLAR';
5128
+ if (els.dmBackBtn) els.dmBackBtn.hidden = true;
5129
+ return;
5130
+ }
5131
+ if (dmState.modalThread) {
5132
+ /* TEK OTURUM: tam sohbet dökümü */
5133
+ const msgs = (dmThreads().find(([k]) => k === dmState.modalThread) || [null, []])[1];
5134
+ if (!msgs.length) {
5135
+ dmState.modalThread = null;
5136
+ return renderDmModal();
5137
+ }
5138
+ if (els.dmDialogTitle) els.dmDialogTitle.textContent = dmThreadTitle(msgs[msgs.length - 1]);
5139
+ if (els.dmBackBtn) els.dmBackBtn.hidden = false;
5140
+ list.innerHTML = '<div class="dm-thread-msgs">' + msgs.map(dmMsgHtml).join('') + '</div>';
5141
+ const box = list.querySelector('.dm-thread-msgs');
5142
+ if (box) box.scrollTop = box.scrollHeight;
5143
+ return;
5144
+ }
5145
+ /* OTURUM LİSTESİ */
5146
+ if (els.dmDialogTitle) els.dmDialogTitle.textContent = 'AJAN DM — OTURUMLAR';
5147
+ if (els.dmBackBtn) els.dmBackBtn.hidden = true;
5148
+ list.innerHTML = '';
5149
+ for (const [key, msgs] of dmThreads()) {
5150
+ const last = msgs[msgs.length - 1];
5151
+ const el = document.createElement('div');
5152
+ el.className = 'dmt';
5153
+ el.innerHTML =
5154
+ '<div class="dmt-head">' +
5155
+ `<span class="dmt-title">${escapeHtml(dmThreadTitle(last))}</span>` +
5156
+ `<span class="dmt-count">${msgs.length}</span>` +
5157
+ '</div>' +
5158
+ `<div class="dmt-last">${escapeHtml(String(last.text || '').replace(/\s+/g, ' ').slice(0, 120))} · ${dmTime(last.at)}</div>`;
5159
+ el.addEventListener('click', () => {
5160
+ dmState.modalThread = key;
5161
+ renderDmModal();
5162
+ });
5163
+ list.appendChild(el);
5164
+ }
5165
+ }
5166
+
5167
+ function openDmModal(threadKey) {
5168
+ if (!els.dmOverlay) return;
5169
+ dmState.modalThread = threadKey || null;
5170
+ els.dmOverlay.hidden = false;
5171
+ refreshDmRail().then(() => renderDmModal());
5172
+ }
5173
+
5174
+ function closeDmModal() {
5175
+ if (els.dmOverlay) els.dmOverlay.hidden = true;
5176
+ if (els.dmBtn) els.dmBtn.classList.remove('on');
5177
+ }
5178
+
5179
+ async function dmClearAll() {
5180
+ if (!(await uiConfirm('TÜM ajan DM geçmişi silinsin mi?', 'Sil'))) return;
5181
+ try { await beast.agentDmsClear(); } catch {}
5182
+ dmState.dms = [];
5183
+ dmState.modalThread = null;
5184
+ renderDmRail();
5185
+ renderDmModal();
5186
+ toast('Ajan DM geçmişi silindi');
5187
+ }
5188
+
5189
+ if (els.dmBtn) els.dmBtn.addEventListener('click', () => toggleDmRail($('#dmRail').classList.contains('dm-hidden')));
5190
+ if (els.dmModalClose) els.dmModalClose.addEventListener('click', closeDmModal);
5191
+ if (els.dmModalClear) els.dmModalClear.addEventListener('click', dmClearAll);
5192
+ if (els.dmBackBtn) {
5193
+ els.dmBackBtn.addEventListener('click', () => {
5194
+ dmState.modalThread = null; // oturum listesine dön
5195
+ renderDmModal();
5196
+ });
5197
+ }
5198
+ if (els.dmOverlay) {
5199
+ els.dmOverlay.addEventListener('click', (e) => {
5200
+ if (e.target === els.dmOverlay) closeDmModal();
5201
+ });
5202
+ els.dmOverlay.addEventListener('keydown', (e) => {
5203
+ if (e.key === 'Escape') { e.stopPropagation(); closeDmModal(); }
5117
5204
  });
5118
5205
  }
5119
- if (els.dmClear) {
5120
- els.dmClear.addEventListener('click', async () => {
5121
- if (!(await uiConfirm('TÜM ajan DM geçmişi silinsin mi?', 'Sil'))) return;
5122
- try { await beast.agentDmsClear(); } catch {}
5123
- dmState.dms = [];
5124
- renderDmRail();
5125
- toast('Ajan DM geçmişi silindi');
5206
+ if (els.dmClear) els.dmClear.addEventListener('click', dmClearAll);
5207
+ if (els.dmColumnBtn) {
5208
+ els.dmColumnBtn.addEventListener('click', () => {
5209
+ closeDmModal();
5210
+ toggleDmRail(false); // sütunda canlı akış
5126
5211
  });
5127
5212
  }
5128
5213
 
@@ -5456,7 +5541,7 @@ function onEvent(ev) {
5456
5541
  scheduleAgentsRender();
5457
5542
  return;
5458
5543
  }
5459
- /* AJAN DM: ajanlar arası mesaj — panel açıksa canlı düşer */
5544
+ /* AJAN DM: ajanlar arası mesaj — açık görünümde canlı düşer (modal/sütun) */
5460
5545
  if (ev.type === 'agent-dm') {
5461
5546
  dmState.dms.push({
5462
5547
  at: ev.at,
@@ -5464,9 +5549,11 @@ function onEvent(ev) {
5464
5549
  fromTitle: ev.fromTitle,
5465
5550
  to: ev.to,
5466
5551
  toTitle: ev.toTitle,
5552
+ topic: ev.topic,
5467
5553
  text: ev.text,
5468
5554
  });
5469
5555
  if (dmState.dms.length > 400) dmState.dms.splice(0, dmState.dms.length - 400);
5556
+ if (els.dmOverlay && !els.dmOverlay.hidden) renderDmModal();
5470
5557
  if ($('#dmRail') && !$('#dmRail').classList.contains('dm-hidden')) renderDmRail();
5471
5558
  return;
5472
5559
  }
@@ -5484,6 +5571,11 @@ function onEvent(ev) {
5484
5571
  scheduleToolRender();
5485
5572
  return;
5486
5573
  }
5574
+ /* TOOLS klasörü değişti (ajan yeni tool yazdı / elle edit) — konsolu tazele */
5575
+ if (ev.type === 'tools-changed') {
5576
+ scheduleToolRender();
5577
+ return;
5578
+ }
5487
5579
  /* OFFLINE MESAJ KUYRUĞU: bağlantı + kuyruk olayları (sessionId filtresinden önce) */
5488
5580
  if (ev.type === 'net' || ev.type === 'netQueue') { onNetEvent(ev); return; }
5489
5581
  /* Sandbox klonlama progress'i (sessionId'siz global olay) */
@@ -110,15 +110,16 @@ body {
110
110
  }
111
111
  #newChat:hover { background: rgba(19, 19, 21, 0.14); }
112
112
 
113
- #sessList { flex: 1; overflow-y: auto; padding: 4px 8px; }
113
+ #sessList { flex: 1 1 0; min-height: 0; overflow-y: auto; padding: 4px 8px; }
114
114
 
115
115
  /* ---------- İZLEYİCİLER paneli (sol kenar çubuğu — sohbet geçmişinin alt yarısı) ----------
116
- Yalnız agent (chat) modunda görünür; IDE/Studio/Sandbox'ta sessList ile birlikte gizli. */
116
+ Yalnız agent (chat) modunda görünür; IDE/Studio/Sandbox'ta sessList ile birlikte gizli.
117
+ Sohbet geçmişi + izleyiciler + toollar EŞİT yer paylaşır (flex 1 1 0). */
117
118
  #watchPane {
118
119
  display: none;
119
120
  flex-direction: column;
120
- height: 36%;
121
- min-height: 120px;
121
+ flex: 1 1 0;
122
+ min-height: 110px;
122
123
  border-top: 1px solid var(--border);
123
124
  background: var(--panel);
124
125
  }
@@ -2187,6 +2188,74 @@ body.rail-hidden #dmRailHead { padding-right: 148px; }
2187
2188
  margin-top: 2px;
2188
2189
  }
2189
2190
 
2191
+ /* ---------- AJAN DM modalı (✉) ---------- */
2192
+ #dmOverlay {
2193
+ position: fixed;
2194
+ inset: 0;
2195
+ background: rgba(0, 0, 0, 0.45);
2196
+ z-index: 300;
2197
+ display: flex;
2198
+ align-items: center;
2199
+ justify-content: center;
2200
+ }
2201
+ #dmOverlay[hidden] { display: none; }
2202
+ #dmDialog {
2203
+ width: min(680px, 92vw);
2204
+ max-height: 86vh;
2205
+ display: flex;
2206
+ flex-direction: column;
2207
+ background: var(--panel);
2208
+ border: 1px solid var(--border);
2209
+ border-radius: 12px;
2210
+ padding: 14px 16px;
2211
+ gap: 10px;
2212
+ }
2213
+ #dmDialogHead {
2214
+ display: flex;
2215
+ align-items: center;
2216
+ justify-content: space-between;
2217
+ font-size: 13px;
2218
+ font-weight: 800;
2219
+ letter-spacing: 1px;
2220
+ color: var(--text);
2221
+ }
2222
+ #dmDialogHead button {
2223
+ border: none;
2224
+ background: none;
2225
+ color: var(--muted);
2226
+ cursor: pointer;
2227
+ font-size: 13px;
2228
+ padding: 2px 6px;
2229
+ border-radius: 6px;
2230
+ display: inline-flex;
2231
+ align-items: center;
2232
+ }
2233
+ #dmDialogHead button:hover { color: var(--accent); background: var(--panel2); }
2234
+ #dmDialogHead .bot-bar-btns { display: flex; gap: 4px; }
2235
+ #dmDialogList {
2236
+ flex: 1 1 auto;
2237
+ min-height: 200px;
2238
+ overflow-y: auto;
2239
+ display: flex;
2240
+ flex-direction: column;
2241
+ align-items: stretch;
2242
+ }
2243
+ /* tek oturum sohbeti: kaydırılabilir blok */
2244
+ .dm-thread-msgs {
2245
+ display: flex;
2246
+ flex-direction: column;
2247
+ gap: 5px;
2248
+ max-height: 56vh;
2249
+ overflow-y: auto;
2250
+ }
2251
+ .dm-hint {
2252
+ flex: none;
2253
+ font-size: 10px;
2254
+ color: var(--muted);
2255
+ text-align: center;
2256
+ letter-spacing: 0.4px;
2257
+ }
2258
+
2190
2259
  /* PİYASA satırındaki ▶ ajan butonu */
2191
2260
  .fin-sym { position: relative; }
2192
2261
  .fin-sym .fs-agent {
@@ -2275,10 +2344,11 @@ body.rail-hidden #dmRailHead { padding-right: 148px; }
2275
2344
  color: var(--muted);
2276
2345
  }
2277
2346
 
2278
- /* ---------- sol panel: TOOLS konsolu (kişisel araçlar) ---------- */
2347
+ /* ---------- sol panel: TOOLS konsolu (kişisel araçlar) ----------
2348
+ Sohbet geçmişi + izleyiciler + toollar EŞİT yer paylaşır (flex 1 1 0). */
2279
2349
  #toolBox {
2280
- flex: none;
2281
- max-height: 32%;
2350
+ flex: 1 1 0;
2351
+ min-height: 90px;
2282
2352
  display: none;
2283
2353
  flex-direction: column;
2284
2354
  border-top: 1px solid var(--border);
@@ -2296,8 +2366,8 @@ body:not(.ide-mode):not(.studio-mode):not(.sandbox-mode) #toolBox { display: fle
2296
2366
  color: var(--muted);
2297
2367
  }
2298
2368
  #toolList {
2369
+ flex: 1 1 auto;
2299
2370
  min-height: 40px;
2300
- max-height: 200px;
2301
2371
  overflow-y: auto;
2302
2372
  display: flex;
2303
2373
  flex-direction: column;