beast-agent 2.38.0 → 2.38.2

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.38.0",
4
+ "version": "2.38.2",
5
5
  "description": "Ultra-fast local agent shell for Windows.",
6
6
  "author": "algokodcom (AlgoKod)",
7
7
  "license": "MIT",
@@ -1919,18 +1919,25 @@ class Engine {
1919
1919
  macro:
1920
1920
  'ROL: MAKRO AJANI 🌍 — büyük resim uzmanısın, İşlem AÇMAZSIN (mt5_trade/mt5_pending/mt5_close KULLANMA). Her turda web_search ile güncel makro manşetleri + ekonomik takvim riskleri (faiz, CPI, jeopolitik); DXY/altın/petrol bağıntılarını odak sembollere çevir; sembol başına yön eğilimi + TEMKİN/BEKLE notu ver.',
1921
1921
  })[String((session && session.financeRole) || '')] || '';
1922
- /* ROL → SKILL eşleştirmesi (ayarlar modalı): TAMAMEN İSTEĞE BAĞLI — boşsa
1923
- satır eklenmez ve ajan skill'i göreve göre kendi seçer (SKILLS kataloğu).
1924
- Doluysa rol için öncelikli skill'ler hatırlatılır. */
1922
+ /* ROL → SKILL eşleştirmesi (ayarlar modalı): rol başına TEK ve ZORUNLU
1923
+ skill. Seçili SKILL.md'nin TAM GÖVDESİ prompta gömülür — ajan skill
1924
+ aracını çağırmayı atlasa bile prosedür KESİN uygulanır. Eşleştirme
1925
+ boşsa satır eklenmez; ajan skill'i göreve göre kendi seçer (SKILLS). */
1925
1926
  const roleSkills = Array.isArray(session && session.financeRoleSkills)
1926
- ? session.financeRoleSkills.map((s) => String(s || '').trim()).filter(Boolean).slice(0, 8)
1927
+ ? session.financeRoleSkills.map((s) => String(s || '').trim()).filter(Boolean).slice(0, 1)
1927
1928
  : [];
1928
- const roleBlockFull = roleBlock
1929
- ? roleBlock +
1930
- (roleSkills.length
1931
- ? ` Bu rol için isteğe bağlı öncelikli skill'ler: ${roleSkills.map((n) => `skill("${n}")`).join(', ')} — görev gerektiriyorsa skill aracıyla oku ve prosedürüne göre uygula.`
1932
- : '')
1933
- : '';
1929
+ let roleSkillBlock = '';
1930
+ if (roleBlock && roleSkills.length) {
1931
+ const roleSkillName = roleSkills[0];
1932
+ const sb = this._skillBody({ name: roleSkillName });
1933
+ roleSkillBlock =
1934
+ ` Bu rolün ZORUNLU skill'i: skill("${roleSkillName}")` +
1935
+ (sb && sb.ok && sb.content
1936
+ ? ' — aşağıda TAM prosedürü verilmiştir; HER TURDA birebir uygula (ayrıca okumana gerek yok):\n\n' +
1937
+ `===== SKILL: ${roleSkillName} =====\n${sb.content}\n===== /SKILL =====`
1938
+ : ' — ilk turda bu skill\'i mutlaka oku ve prosedürüne birebir uy.');
1939
+ }
1940
+ const roleBlockFull = roleBlock ? roleBlock + roleSkillBlock : '';
1934
1941
  /* FİNANS EKİBİ: tüm finance ajanları tek DM grubundadır — sohbet grup
1935
1942
  thread'inde toplanır (ayrı ayrı 1:1 thread'lere dağılmaz) */
1936
1943
  const finJob = this._bgJobs && this._bgJobs.get(String(session && session.id));
package/src/main.js CHANGED
@@ -5144,11 +5144,17 @@ ipcMain.handle('agent:send', (_e, { sessionId, text }) => {
5144
5144
  const fsid = String(sessionId);
5145
5145
  let fsess = engine.cache.get(fsid);
5146
5146
  if (!fsess) { try { fsess = engine._load(fsid); } catch {} }
5147
- if (fsess && !fsess.botId && !fsess.bgJob && financeState.mode && !fsess.finance) {
5148
- engine.markFinance(fsid, false);
5149
- finApplyTraderFields(fsess);
5150
- fsess.financeTrader = false; /* chat copilot'ı — trader değil */
5151
- engine.cache.set(fsid, fsess);
5147
+ if (fsess && !fsess.botId && !fsess.bgJob && financeState.mode) {
5148
+ if (!fsess.finance) {
5149
+ engine.markFinance(fsid, false);
5150
+ finApplyTraderFields(fsess);
5151
+ fsess.financeTrader = false; /* chat copilot'ı — trader değil */
5152
+ engine.cache.set(fsid, fsess);
5153
+ } else if (!financeState.agents.has(fsid)) {
5154
+ /* finance SOHBET botu: ajan talimatı/ayarlar HER MESAJDA tazelenir —
5155
+ asıl bot paralel ajanlardan geri kalmaz */
5156
+ finApplyChatFields(fsess);
5157
+ }
5152
5158
  }
5153
5159
  }
5154
5160
  } catch {}
@@ -8477,10 +8483,9 @@ function finCfg() {
8477
8483
  f.analysisAuto = true;
8478
8484
  if (!Number.isFinite(Number(f.analysisCount))) f.analysisCount = 2;
8479
8485
  f.analysisCount = Math.max(0, Math.min(FIN_ROLES_AUTO.length, Math.round(Number(f.analysisCount) || 0)));
8480
- /* ROL → SKILL eşleştirmesi: VARSAYILAN BOŞ — ajan göreve göre skill'i kendi
8481
- seçer (SKILLS kataloğu promptta). İstenirse ⚙ modalından rol başına
8482
- sabitlenir; skill adları kurulu katalogdan gelir. Eski sürümün otomatik
8483
- doldurduğu varsayılanlar bir kez temizlenir. */
8486
+ /* ROL → SKILL eşleştirmesi: rol başına TEK skill — modalda zorunlu tek
8487
+ seçim yapılır. Eski çoklu seçimlerden yalnız İLKİ korunur (göç). Skill
8488
+ adları kurulu katalogdan gelir. */
8484
8489
  if (f.roleSkillsDefaultsCleared !== true) {
8485
8490
  f.roleSkills = {};
8486
8491
  f.roleSkillsDefaultsCleared = true;
@@ -8488,7 +8493,7 @@ function finCfg() {
8488
8493
  if (!f.roleSkills || typeof f.roleSkills !== 'object') f.roleSkills = {};
8489
8494
  for (const d of FIN_ROLES) {
8490
8495
  const cur = Array.isArray(f.roleSkills[d.id]) ? f.roleSkills[d.id] : [];
8491
- f.roleSkills[d.id] = cur.map((s) => String(s || '').trim()).filter(Boolean).slice(0, 8);
8496
+ f.roleSkills[d.id] = cur.map((s) => String(s || '').trim()).filter(Boolean).slice(0, 1);
8492
8497
  }
8493
8498
  if (!Number(f.intervalSec)) f.intervalSec = 120;
8494
8499
  if (!Number(f.maxLot)) f.maxLot = 0.1;
@@ -9354,6 +9359,19 @@ function finApplyTraderFields(s, symbolsOverride, role) {
9354
9359
  engine.cache.set(String(s.id), s);
9355
9360
  }
9356
9361
 
9362
+ /* Finance SOHBET oturumu (chat copilot) alanlarını tazele — trader bayrağı
9363
+ KORUNUR. Strateji/sembol/limit/rol-skill değişiklikleri "asıl bot"a da
9364
+ ulaşsın; paralel ajanlar tur başında zaten tazeleniyor. */
9365
+ function finApplyChatFields(s) {
9366
+ if (!s) return;
9367
+ const wasTrader = !!s.financeTrader;
9368
+ finApplyTraderFields(s);
9369
+ s.financeTrader = wasTrader;
9370
+ if (!wasTrader) s.financeAuto = false; /* sohbet copilot'ı — otonom trader değil */
9371
+ engine.cache.set(String(s.id), s);
9372
+ return s;
9373
+ }
9374
+
9357
9375
  /* Yeni finance ajanı aç (ana trader YA DA sembol işçisi YA DA analiz ekibi rolü) */
9358
9376
  function finAgentCreate(symbols, isMain, role) {
9359
9377
  const f = finCfg();
@@ -9713,12 +9731,22 @@ ipcMain.handle('finance:settings', async (_e, patch) => {
9713
9731
  if (p.analysisTeam !== undefined) f.analysisTeam = finRolesValid(p.analysisTeam);
9714
9732
  if (p.analysisAuto !== undefined) f.analysisAuto = !!p.analysisAuto;
9715
9733
  if (p.analysisCount !== undefined) f.analysisCount = Math.max(0, Math.min(FIN_ROLES_AUTO.length, Math.round(Number(p.analysisCount) || 0)));
9716
- /* ROL → SKILL eşleştirmesi güncellemesi (modal) */
9734
+ /* ROL → SKILL eşleştirmesi güncellemesi (modal): rol başına TEK skill */
9717
9735
  if (p.roleSkills !== undefined && p.roleSkills && typeof p.roleSkills === 'object') {
9718
9736
  for (const d of FIN_ROLES) {
9719
9737
  if (p.roleSkills[d.id] === undefined) continue;
9720
9738
  f.roleSkills[d.id] = (Array.isArray(p.roleSkills[d.id]) ? p.roleSkills[d.id] : [])
9721
- .map((s) => String(s || '').trim()).filter(Boolean).slice(0, 8);
9739
+ .map((s) => String(s || '').trim()).filter(Boolean).slice(0, 1);
9740
+ }
9741
+ /* koşan rol ajanlarını da tazele — seçim sonraki turdan itibaren sistem
9742
+ promptuna girer (ajanı durdurmaya gerek yok) */
9743
+ if (financeState.agents && engine) {
9744
+ for (const [sid, a] of financeState.agents) {
9745
+ try {
9746
+ const ss = engine.cache.get(sid);
9747
+ if (ss && a && a.role) finApplyTraderFields(ss, a.symbols, a.role);
9748
+ } catch {}
9749
+ }
9722
9750
  }
9723
9751
  }
9724
9752
  if (p.pythonPath !== undefined) f.pythonPath = String(p.pythonPath || '').trim();
@@ -9737,6 +9765,18 @@ ipcMain.handle('finance:settings', async (_e, patch) => {
9737
9765
  if (ts) finApplyTraderFields(ts);
9738
9766
  } catch {}
9739
9767
  }
9768
+ /* finance SOHBET oturumları: strateji/sembol/limit/rol-skill değişince ANINDA
9769
+ tazelenir — "asıl bot" (Beast Finance sohbeti) de aynı ajan talimatını görür */
9770
+ if (engine) {
9771
+ try {
9772
+ for (const [ssid, ss] of engine.cache) {
9773
+ if (!ss || !ss.finance || ss.botId || ss.bgJob) continue;
9774
+ if (financeState.agents.has(String(ssid))) continue; /* ajanlar tur başında tazelenir */
9775
+ if (ssid === String(financeState.traderSid || '')) continue;
9776
+ finApplyChatFields(ss);
9777
+ }
9778
+ } catch {}
9779
+ }
9740
9780
  return { ok: true, cfg: f };
9741
9781
  });
9742
9782
 
@@ -153,7 +153,7 @@
153
153
  <button id="finSettings" title="Görünüm ayarları — bölümleri göster/gizle">&#9881;</button>
154
154
  </div>
155
155
  <div id="finBody">
156
- <div class="fin-card">
156
+ <div class="fin-card" data-fin-card="account">
157
157
  <div class="fin-card-title">HESAP</div>
158
158
  <div class="fin-grid">
159
159
  <div class="fin-kv"><span>Bakiye</span><b id="finBalance">—</b></div>
@@ -162,8 +162,8 @@
162
162
  <div class="fin-kv"><span>Serbest Marj</span><b id="finFree">—</b></div>
163
163
  </div>
164
164
  </div>
165
- <div class="fin-card fin-card-trader">
166
- <div class="fin-card-title">TRADE AJANI <span id="finTraderDot" class="fin-dot off" title="Trader durumu"></span><span class="file-spacer"></span><button id="finModelRefreshBtn" title="Modelleri yeniden çek — eksik/güncel modelleri tazeler">&#x27F3;</button><button id="finSkillsBtn" title="Rol → skill eşleştirmesi — hangi rol hangi skill'i okur">&#9881;</button></div>
165
+ <div class="fin-card fin-card-trader" data-fin-card="trader">
166
+ <div class="fin-card-title">TRADE AJANI <span id="finTraderDot" class="fin-dot off" title="Trader durumu"></span><span class="file-spacer"></span><button id="finModelRefreshBtn" title="Modelleri yeniden çek — eksik/güncel modelleri tazeler">&#x27F3;</button><button id="finSkillsBtn" title="Rol → skill eşleştirmesi — her role ZORUNLU tek skill (ajan her turda onu okur)">&#9881;</button></div>
167
167
  <div class="fin-field">
168
168
  <span>Model</span>
169
169
  <select id="finTraderModel" title="Trader ajanının modeli — farklı API/model seçilebilir"></select>
@@ -195,7 +195,7 @@
195
195
  </div>
196
196
  <div id="finTraderStatus">Kapalı</div>
197
197
  </div>
198
- <div class="fin-card" id="finRiskCard" hidden>
198
+ <div class="fin-card" id="finRiskCard" data-fin-card="risk" hidden>
199
199
  <div class="fin-card-title">RİSK OTOMASYONU <span id="finWatchDot" class="fin-dot off" title="Koruma döngüsü"></span><span id="finWatchInfo" class="fin-hint"></span></div>
200
200
  <div class="fin-field fin-check">
201
201
  <label title="5 saniyelik döngü: +R'da SL'yi başabaşa çeker, trailing uygular, kısmi TP alır — ajan turunu beklemez">
@@ -226,7 +226,7 @@
226
226
  </div>
227
227
  <div id="finAlertList" class="fin-alerts"><div class="fin-empty">Aktif fiyat alarmı yok</div></div>
228
228
  </div>
229
- <div class="fin-card" id="finNotifyCard" hidden>
229
+ <div class="fin-card" id="finNotifyCard" data-fin-card="notify" hidden>
230
230
  <div class="fin-card-title">BİLDİRİM <span class="fin-hint">— kanal seçimi ve olay filtresi</span></div>
231
231
  <div class="fin-field">
232
232
  <span>Bildirim kanalı</span>
@@ -244,7 +244,7 @@
244
244
  </label>
245
245
  </div>
246
246
  </div>
247
- <div class="fin-card" id="finPerfCard" hidden>
247
+ <div class="fin-card" id="finPerfCard" data-fin-card="perf" hidden>
248
248
  <div class="fin-card-title">PERFORMANS <span id="finStatsInfo" class="fin-hint"></span><span class="file-spacer"></span><button id="finReportBtn" title="Haftalık rapor üret — istatistik + işlem günlüğü + ajan değerlendirmesi">&#128196; Rapor</button></div>
249
249
  <div id="finEquityChart" class="fin-equity"></div>
250
250
  <div class="fin-grid">
@@ -256,13 +256,13 @@
256
256
  <div class="fin-kv"><span>İşlem (90g)</span><b id="finStatTrades">—</b></div>
257
257
  </div>
258
258
  </div>
259
- <div class="fin-card">
259
+ <div class="fin-card" data-fin-card="positions">
260
260
  <div class="fin-card-title">POZİSYONLAR <span id="finPosCount"></span></div>
261
261
  <div id="finPosList"><div class="fin-empty">Açık pozisyon yok</div></div>
262
262
  <div class="fin-sect-title">BEKLEYEN EMİRLER <span id="finOrdCount"></span></div>
263
263
  <div id="finOrdList"><div class="fin-empty">Bekleyen emir yok</div></div>
264
264
  </div>
265
- <div class="fin-card">
265
+ <div class="fin-card" data-fin-card="market">
266
266
  <div class="fin-card-title">
267
267
  PİYASA
268
268
  <span class="file-spacer"></span>
@@ -271,7 +271,7 @@
271
271
  </div>
272
272
  <div id="finSymList"><div class="fin-empty">—</div></div>
273
273
  </div>
274
- <div class="fin-card fin-grow" id="finFlowCard" hidden>
274
+ <div class="fin-card fin-grow" id="finFlowCard" data-fin-card="flow" hidden>
275
275
  <div class="fin-card-title">AKIŞ</div>
276
276
  <div id="finLog"></div>
277
277
  </div>
@@ -761,7 +761,7 @@
761
761
  </div>
762
762
  <div class="mini-body">
763
763
  <div id="finSkillsList" class="mini-list"></div>
764
- <div class="fin-sym-hint">İsteğe bağlı: rol başına skill sabitle. Boş bırakırsan ajan göreve göre skill'i kendi seçer — liste kurulu katalogdan gelir, yeni skill eklenince otomatik görünür.</div>
764
+ <div class="fin-sym-hint">Her rol için TEK skill ZORUNLUDUR — aynı role birden fazla skill seçilemez. Seçili skill kaldırılamaz; değiştirmek için başka bir skill'e tıkla. Seçim, koşan ajanlar dahil sonraki turdan itibaren rol ajanının sistem promptuna zorunlu skill olarak girer.</div>
765
765
  </div>
766
766
  </div>
767
767
  </div>
@@ -162,6 +162,7 @@ const els = {
162
162
  stSplit: $('#stSplit'),
163
163
  finBtn: $('#finBtn'),
164
164
  finPanel: $('#finPanel'),
165
+ finBody: $('#finBody'),
165
166
  finDot: $('#finDot'),
166
167
  finTerm: $('#finTerm'),
167
168
  finReconnect: $('#finReconnect'),
@@ -4949,7 +4950,9 @@ async function refreshAgentsPane() {
4949
4950
  kullanıcı çalışırken elle kapatırsa ezilmez; dahili tarayıcı ve IDE modu
4950
4951
  açıkken karışılmaz — orada railBtn ile elle açılır)
4951
4952
  - Tüm işler bitince otomatik KAPANIR (yalnız açılışı biz yaptıysak;
4952
- kullanıcı elle açtıysa açık kalır) */
4953
+ kullanıcı elle açtıysa açık kalır). BEAST FINANCE istisnası: trade ajanı
4954
+ durdurulunca konsol KAPANMAZ — duran ajanların raporları/geçmişi görünür
4955
+ kalsın; kullanıcı isterse railBtn ile kendisi kapatır. */
4953
4956
  function maybeAutoOpenRail() {
4954
4957
  const hasRunning = agentState.jobs.some((j) => j.status === 'running');
4955
4958
  const hidden = document.body.classList.contains('rail-hidden');
@@ -4959,7 +4962,8 @@ function maybeAutoOpenRail() {
4959
4962
  railPrefBeforeBrowser = true;
4960
4963
  }
4961
4964
  if (!hasRunning && agentState.autoOpened && !hidden &&
4962
- !railManualOpen && !document.body.classList.contains('browser-open')) {
4965
+ !railManualOpen && !document.body.classList.contains('browser-open') &&
4966
+ !financeModeOn()) {
4963
4967
  toggleRail(true);
4964
4968
  railManualOpen = false;
4965
4969
  }
@@ -9769,6 +9773,100 @@ if (els.finShowPerf) els.finShowPerf.addEventListener('change', () => finViewTog
9769
9773
  if (els.finShowFlow) els.finShowFlow.addEventListener('change', () => finViewToggle('flow', els.finShowFlow.checked));
9770
9774
  finViewApply();
9771
9775
 
9776
+ /* ---------- BÖLÜM SIRALAMA (sürükle-bırak) ----------
9777
+ Kartların sol üstündeki tutamak (⠿) ile bölümlerin yerini değiştir;
9778
+ sıra localStorage'a yazılır — panel kapatılıp açılsa da korunur. */
9779
+ const finOrderKey = 'beast.financeOrder';
9780
+ let finDragCard = null;
9781
+
9782
+ function finOrderLoad() {
9783
+ try {
9784
+ const v = JSON.parse(localStorage.getItem(finOrderKey) || 'null');
9785
+ return Array.isArray(v) ? v.map(String).filter(Boolean) : [];
9786
+ } catch { return []; }
9787
+ }
9788
+
9789
+ function finOrderApply() {
9790
+ const body = els.finBody;
9791
+ if (!body) return;
9792
+ const cards = [...body.querySelectorAll(':scope > .fin-card')];
9793
+ const byKey = new Map();
9794
+ for (const c of cards) if (c.dataset.finCard) byKey.set(c.dataset.finCard, c);
9795
+ for (const key of finOrderLoad()) {
9796
+ const c = byKey.get(key);
9797
+ if (c) { body.appendChild(c); byKey.delete(key); }
9798
+ }
9799
+ for (const c of byKey.values()) body.appendChild(c); /* yeni kartlar sona */
9800
+ }
9801
+
9802
+ function finOrderSave() {
9803
+ const body = els.finBody;
9804
+ if (!body) return;
9805
+ const keys = [...body.querySelectorAll(':scope > .fin-card')]
9806
+ .map((c) => c.dataset.finCard)
9807
+ .filter(Boolean);
9808
+ try { localStorage.setItem(finOrderKey, JSON.stringify(keys)); } catch {}
9809
+ }
9810
+
9811
+ function finCardsInit() {
9812
+ const body = els.finBody;
9813
+ if (!body) return;
9814
+ finOrderApply();
9815
+ const cards = [...body.querySelectorAll(':scope > .fin-card')];
9816
+ for (const card of cards) {
9817
+ const title = card.querySelector('.fin-card-title');
9818
+ if (!title) continue;
9819
+ const grip = document.createElement('span');
9820
+ grip.className = 'fin-grip';
9821
+ grip.title = 'Sürükle-bırak: bölümün yerini değiştir';
9822
+ grip.textContent = '⠿';
9823
+ title.prepend(grip);
9824
+ grip.addEventListener('mousedown', () => { card.draggable = true; });
9825
+ card.addEventListener('dragstart', (e) => {
9826
+ finDragCard = card;
9827
+ card.classList.add('fin-dragging');
9828
+ try {
9829
+ e.dataTransfer.effectAllowed = 'move';
9830
+ e.dataTransfer.setData('text/plain', card.dataset.finCard || '');
9831
+ } catch {}
9832
+ });
9833
+ card.addEventListener('dragend', () => {
9834
+ finDragCard = null;
9835
+ card.draggable = false;
9836
+ card.classList.remove('fin-dragging');
9837
+ body.querySelectorAll('.fin-drop-top, .fin-drop-bottom')
9838
+ .forEach((x) => x.classList.remove('fin-drop-top', 'fin-drop-bottom'));
9839
+ finOrderSave();
9840
+ });
9841
+ card.addEventListener('dragover', (e) => {
9842
+ if (!finDragCard || finDragCard === card) return;
9843
+ e.preventDefault();
9844
+ try { e.dataTransfer.dropEffect = 'move'; } catch {}
9845
+ const r = card.getBoundingClientRect();
9846
+ const after = e.clientY > r.top + r.height / 2;
9847
+ body.querySelectorAll('.fin-drop-top, .fin-drop-bottom')
9848
+ .forEach((x) => x.classList.remove('fin-drop-top', 'fin-drop-bottom'));
9849
+ card.classList.add(after ? 'fin-drop-bottom' : 'fin-drop-top');
9850
+ });
9851
+ card.addEventListener('dragleave', () => card.classList.remove('fin-drop-top', 'fin-drop-bottom'));
9852
+ card.addEventListener('drop', (e) => {
9853
+ e.preventDefault();
9854
+ if (!finDragCard || finDragCard === card) return;
9855
+ const r = card.getBoundingClientRect();
9856
+ const after = e.clientY > r.top + r.height / 2;
9857
+ if (after) card.after(finDragCard);
9858
+ else card.before(finDragCard);
9859
+ card.classList.remove('fin-drop-top', 'fin-drop-bottom');
9860
+ finOrderSave();
9861
+ });
9862
+ }
9863
+ document.addEventListener('mouseup', () => {
9864
+ if (finDragCard) return;
9865
+ body.querySelectorAll('.fin-card[draggable="true"]').forEach((c) => { c.draggable = false; });
9866
+ });
9867
+ }
9868
+ finCardsInit();
9869
+
9772
9870
  if (els.finInterval) els.finInterval.addEventListener('change', () => {
9773
9871
  const v = Math.max(30, Math.min(3600, Math.round(Number(els.finInterval.value) || 120)));
9774
9872
  els.finInterval.value = v;
@@ -10076,31 +10174,40 @@ function finRenderSkills() {
10076
10174
  head.innerHTML =
10077
10175
  '<b>' + escapeHtml(String(r.label || r.id)) + '</b>' +
10078
10176
  '<span>' + escapeHtml(String(r.desc || '').slice(0, 110)) + '</span>';
10177
+ const cur = Array.isArray(map[r.id]) ? map[r.id].map((x) => String(x || '').trim()).filter(Boolean) : [];
10178
+ const active = cur.length ? cur[0] : '';
10179
+ if (!active) {
10180
+ const warn = document.createElement('span');
10181
+ warn.className = 'fin-skill-req';
10182
+ warn.textContent = 'ZORUNLU — skill seçilmedi';
10183
+ head.appendChild(warn);
10184
+ }
10079
10185
  row.appendChild(head);
10080
10186
  const chips = document.createElement('div');
10081
10187
  chips.className = 'fin-skill-chips';
10082
10188
  for (const s of finSkillNames) {
10083
- const cur = Array.isArray(map[r.id]) ? map[r.id] : [];
10189
+ const on = active === s.name;
10084
10190
  const chip = document.createElement('label');
10085
- chip.className = 'fin-skill-chip' + (cur.includes(s.name) ? ' on' : '');
10191
+ chip.className = 'fin-skill-chip' + (on ? ' on' : '');
10086
10192
  chip.title = s.description || s.name;
10087
10193
  const cb = document.createElement('input');
10088
- cb.type = 'checkbox';
10089
- cb.checked = cur.includes(s.name);
10194
+ cb.type = 'radio';
10195
+ cb.name = 'fin-skill-role-' + r.id;
10196
+ cb.checked = on;
10197
+ /* TEK + ZORUNLU: yeni skill seçilir; mevcut seçim kaldırılamaz */
10090
10198
  cb.addEventListener('change', () => {
10091
- const liveBase = finCfgCache && finCfgCache.roleSkills && Array.isArray(finCfgCache.roleSkills[r.id])
10092
- ? finCfgCache.roleSkills[r.id]
10093
- : [];
10094
- const set = new Set(liveBase);
10095
- if (cb.checked) set.add(s.name); else set.delete(s.name);
10096
- const next = [...set];
10199
+ if (!cb.checked) return;
10200
+ const next = [s.name];
10097
10201
  /* yerel önbelleğe anında yaz — 3 sn'lik snapshot eski değeri geri getirmesin */
10098
10202
  finCfgCache = finCfgCache || {};
10099
10203
  finCfgCache.roleSkills = finCfgCache.roleSkills || {};
10100
10204
  finCfgCache.roleSkills[r.id] = next;
10101
- chip.classList.toggle('on', cb.checked);
10205
+ chips.querySelectorAll('.fin-skill-chip').forEach((el) => el.classList.remove('on'));
10206
+ chip.classList.add('on');
10207
+ const w = head.querySelector('.fin-skill-req');
10208
+ if (w) w.remove();
10102
10209
  finSaveCfg({ roleSkills: { [r.id]: next } });
10103
- toast((r.label || r.id) + ' → ' + (next.length ? next.join(', ') : 'skill kapalı'));
10210
+ toast((r.label || r.id) + ' → ' + s.name + ' (zorunlu tek skill)');
10104
10211
  });
10105
10212
  chip.appendChild(cb);
10106
10213
  chip.appendChild(document.createTextNode(s.name));
@@ -4816,6 +4816,22 @@ body.finance-mode #composerWrap { margin-right: var(--finW, 400px); }
4816
4816
  align-items: center;
4817
4817
  gap: 6px;
4818
4818
  }
4819
+ .fin-grip {
4820
+ flex: none;
4821
+ cursor: grab;
4822
+ color: var(--muted);
4823
+ font-size: 12px;
4824
+ line-height: 1;
4825
+ letter-spacing: -1px;
4826
+ opacity: 0.7;
4827
+ user-select: none;
4828
+ -webkit-user-select: none;
4829
+ }
4830
+ .fin-grip:hover { color: var(--accent); opacity: 1; }
4831
+ .fin-grip:active { cursor: grabbing; }
4832
+ .fin-card.fin-dragging { opacity: 0.45; border-style: dashed; }
4833
+ .fin-card.fin-drop-top { box-shadow: 0 -3px 0 0 var(--accent); }
4834
+ .fin-card.fin-drop-bottom { box-shadow: 0 3px 0 0 var(--accent); }
4819
4835
 
4820
4836
  .fin-grid {
4821
4837
  display: grid;
@@ -5209,6 +5225,14 @@ body.finance-mode #composerWrap { margin-right: var(--finW, 400px); }
5209
5225
  .fin-skill-role-head { display: flex; flex-direction: column; gap: 1px; margin-bottom: 7px; }
5210
5226
  .fin-skill-role-head b { font-size: 11.5px; color: var(--text); }
5211
5227
  .fin-skill-role-head span { font-size: 10px; color: var(--muted); }
5228
+ .fin-skill-req {
5229
+ align-self: flex-start;
5230
+ margin-top: 2px;
5231
+ font-size: 9.5px;
5232
+ font-weight: 800;
5233
+ letter-spacing: 0.5px;
5234
+ color: #ef4444;
5235
+ }
5212
5236
  .fin-skill-chips { display: flex; flex-wrap: wrap; gap: 4px; }
5213
5237
  .fin-skill-chip {
5214
5238
  display: inline-flex;