beast-agent 0.28.0 → 0.30.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 +1 -1
- package/src/agent/bots.js +8 -0
- package/src/agent/engine.js +99 -17
- package/src/agent/llm.js +153 -71
- package/src/agent/obscura.js +49 -18
- package/src/agent/skills.js +85 -27
- package/src/main.js +110 -22
- package/src/preload.js +3 -0
- package/src/renderer/i18n.js +30 -2
- package/src/renderer/index.html +1 -0
- package/src/renderer/renderer.js +173 -16
- package/src/renderer/style.css +42 -1
- package/tests/bg-jobs.test.js +45 -0
- package/tests/bots-name.test.js +42 -0
- package/tests/llm.test.js +70 -1
package/src/renderer/renderer.js
CHANGED
|
@@ -40,6 +40,7 @@ const els = {
|
|
|
40
40
|
todoPanel: $('#todoPanel'),
|
|
41
41
|
browserBtn: $('#browserBtn'),
|
|
42
42
|
eyeBtn: $('#eyeBtn'),
|
|
43
|
+
netDot: $('#netDot'),
|
|
43
44
|
railBtn: $('#railBtn'),
|
|
44
45
|
watchBtn: $('#watchBtn'),
|
|
45
46
|
cronBtn: $('#cronBtn'),
|
|
@@ -199,6 +200,14 @@ let netOnline = true;
|
|
|
199
200
|
let netQueueCount = 0;
|
|
200
201
|
const netPending = []; // { key, el } — bekleyen mesaj balonları
|
|
201
202
|
|
|
203
|
+
/* wifi göstergesi: bağlı = yeşil, kopuk = kırmızı */
|
|
204
|
+
function paintNetDot() {
|
|
205
|
+
if (!els.netDot) return;
|
|
206
|
+
els.netDot.classList.toggle('net-on', netOnline);
|
|
207
|
+
els.netDot.classList.toggle('net-off', !netOnline);
|
|
208
|
+
els.netDot.title = _t(netOnline ? 'tip_net_online' : 'tip_net_offline');
|
|
209
|
+
}
|
|
210
|
+
|
|
202
211
|
function setNetBadge(el, text) {
|
|
203
212
|
if (!el || !el.isConnected) return;
|
|
204
213
|
let badge = el.querySelector('.q-badge');
|
|
@@ -223,6 +232,7 @@ function onNetEvent(ev) {
|
|
|
223
232
|
if (ev.type === 'net') {
|
|
224
233
|
const was = netOnline;
|
|
225
234
|
netOnline = ev.online !== false;
|
|
235
|
+
paintNetDot();
|
|
226
236
|
if (was !== netOnline) {
|
|
227
237
|
if (netOnline) {
|
|
228
238
|
toast(_t('net_online'));
|
|
@@ -270,8 +280,14 @@ function renderTodos(items) {
|
|
|
270
280
|
})
|
|
271
281
|
.join('');
|
|
272
282
|
els.todoPanel.innerHTML =
|
|
273
|
-
`<div class="td-head"><span>GÖREVLER</span
|
|
283
|
+
`<div class="td-head"><span>GÖREVLER</span>` +
|
|
284
|
+
`<span class="td-right"><span class="td-count">${done}/${list.length}</span>` +
|
|
285
|
+
`<button class="td-x" title="${_t('td_hide')}">×</button></span></div>` + rows;
|
|
274
286
|
els.todoPanel.hidden = false;
|
|
287
|
+
/* X yalnız BU listeyi gizler — kalıcı kapanma yok: bir sonraki görev
|
|
288
|
+
güncellemesi (yeni görev eklendi / görev yapıldı) paneli yeniden gösterir */
|
|
289
|
+
const x = els.todoPanel.querySelector('.td-x');
|
|
290
|
+
if (x) x.addEventListener('click', () => { els.todoPanel.hidden = true; });
|
|
275
291
|
}
|
|
276
292
|
function setBusy(b) {
|
|
277
293
|
busy = b;
|
|
@@ -472,7 +488,41 @@ function finishToolCard(callId, ok, result) {
|
|
|
472
488
|
|
|
473
489
|
/* ---------------- sessions sidebar ---------------- */
|
|
474
490
|
|
|
491
|
+
/* sol panel sohbet listesi: kullanıcı ELLE sıralayabilir (sürükle-bırak).
|
|
492
|
+
sessionOrder: session id'lerinin tercih sırası — settings.json'da kalıcı.
|
|
493
|
+
Listede olmayan oturumlar engine sırasıyla (son mesaja göre) arkada gelir. */
|
|
494
|
+
let sessionOrder = [];
|
|
495
|
+
let sessionOrderLoaded = null;
|
|
496
|
+
function loadSessionOrder() {
|
|
497
|
+
if (!sessionOrderLoaded) {
|
|
498
|
+
sessionOrderLoaded = beast
|
|
499
|
+
.sessionsOrderGet()
|
|
500
|
+
.then((r) => { sessionOrder = (r && Array.isArray(r.order) ? r.order : []).map(String); })
|
|
501
|
+
.catch(() => {});
|
|
502
|
+
}
|
|
503
|
+
return sessionOrderLoaded;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function reorderSessions(dragId, targetId) {
|
|
507
|
+
const idx = (id) => sessionOrder.indexOf(id);
|
|
508
|
+
const dp = idx(dragId);
|
|
509
|
+
const tp = idx(targetId);
|
|
510
|
+
sessionOrder = sessionOrder.filter((x) => x !== dragId);
|
|
511
|
+
const ti = sessionOrder.indexOf(targetId);
|
|
512
|
+
if (ti === -1) {
|
|
513
|
+
/* hedef hiç elle sıralanmamış: listeden görünür konumunu korumak mümkün değil — sona ekle */
|
|
514
|
+
sessionOrder.push(targetId, dragId);
|
|
515
|
+
} else if (dp !== -1 && dp < tp) {
|
|
516
|
+
sessionOrder.splice(ti + 1, 0, dragId); /* aşağı sürüklendi → hedefin ALTINA */
|
|
517
|
+
} else {
|
|
518
|
+
sessionOrder.splice(ti, 0, dragId); /* yukarı sürüklendi → hedefin ÜSTÜNE */
|
|
519
|
+
}
|
|
520
|
+
beast.sessionsOrderSet(sessionOrder).catch(() => {});
|
|
521
|
+
refreshSessions();
|
|
522
|
+
}
|
|
523
|
+
|
|
475
524
|
async function renderSessions(list) {
|
|
525
|
+
await loadSessionOrder();
|
|
476
526
|
let waSet = new Set();
|
|
477
527
|
let tgSet = new Set();
|
|
478
528
|
try {
|
|
@@ -482,13 +532,19 @@ async function renderSessions(list) {
|
|
|
482
532
|
tgSet = new Set(await beast.tgListSessions());
|
|
483
533
|
} catch {}
|
|
484
534
|
els.sessList.innerHTML = '';
|
|
535
|
+
/* elle sıra: sessionOrder'daki id'ler önde (o sırayla), diğerleri engine sırasıyla arkada */
|
|
536
|
+
const rank = new Map(sessionOrder.map((id, i) => [id, i]));
|
|
537
|
+
const withRank = list.map((s, i) => ({ s, r: rank.has(s.id) ? rank.get(s.id) : 1e6 + i }));
|
|
538
|
+
withRank.sort((a, b) => a.r - b.r);
|
|
485
539
|
/* aktif botun oturumları — botlar arası geçişte liste de o bota göre değişir.
|
|
486
540
|
KATI KURAL: her oturum yalnız BAĞLI OLDUĞU botun listesinde görünür;
|
|
487
541
|
bot kaydı olmayan (eski) oturumlar Beast (varsayılan sahip) altında görünür. */
|
|
488
|
-
for (const s of
|
|
542
|
+
for (const { s } of withRank) {
|
|
489
543
|
if ((s.botId || 'beast') !== activeBotId) continue;
|
|
490
544
|
const row = document.createElement('div');
|
|
491
545
|
row.className = 'sess' + (s.id === activeId ? ' active' : '');
|
|
546
|
+
row.title = _t('sess_drag');
|
|
547
|
+
row.draggable = true;
|
|
492
548
|
row.innerHTML =
|
|
493
549
|
(waSet.has(s.id) ? '<span class="sess-wa" title="WhatsApp">W</span>' : '') +
|
|
494
550
|
(tgSet.has(s.id) ? '<span class="sess-tg" title="Telegram">T</span>' : '') +
|
|
@@ -506,12 +562,40 @@ async function renderSessions(list) {
|
|
|
506
562
|
const created = await beast.createSession();
|
|
507
563
|
await openSession(created.id);
|
|
508
564
|
}
|
|
565
|
+
sessionOrder = sessionOrder.filter((x) => x !== s.id);
|
|
509
566
|
refreshSessions();
|
|
510
567
|
});
|
|
568
|
+
/* sürükle-bırak: yukarı/aşağı yer değiştirme */
|
|
569
|
+
row.addEventListener('dragstart', (e) => {
|
|
570
|
+
dragSid = s.id;
|
|
571
|
+
row.classList.add('dragging');
|
|
572
|
+
try { e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', s.id); } catch {}
|
|
573
|
+
});
|
|
574
|
+
row.addEventListener('dragend', () => {
|
|
575
|
+
dragSid = null;
|
|
576
|
+
row.classList.remove('dragging');
|
|
577
|
+
els.sessList.querySelectorAll('.drag-over').forEach((x) => x.classList.remove('drag-over'));
|
|
578
|
+
});
|
|
579
|
+
row.addEventListener('dragover', (e) => {
|
|
580
|
+
if (!dragSid || dragSid === s.id) return;
|
|
581
|
+
e.preventDefault();
|
|
582
|
+
try { e.dataTransfer.dropEffect = 'move'; } catch {}
|
|
583
|
+
row.classList.add('drag-over');
|
|
584
|
+
});
|
|
585
|
+
row.addEventListener('dragleave', () => row.classList.remove('drag-over'));
|
|
586
|
+
row.addEventListener('drop', (e) => {
|
|
587
|
+
e.preventDefault();
|
|
588
|
+
row.classList.remove('drag-over');
|
|
589
|
+
if (!dragSid || dragSid === s.id) return;
|
|
590
|
+
reorderSessions(dragSid, s.id);
|
|
591
|
+
dragSid = null;
|
|
592
|
+
});
|
|
511
593
|
els.sessList.appendChild(row);
|
|
512
594
|
}
|
|
513
595
|
}
|
|
514
596
|
|
|
597
|
+
let dragSid = null;
|
|
598
|
+
|
|
515
599
|
async function refreshSessions() {
|
|
516
600
|
await renderSessions(await beast.listSessions());
|
|
517
601
|
renderBotCards(); // bot kartlarındaki numara/sayı etiketleri de tazelensin
|
|
@@ -749,6 +833,50 @@ async function refreshFalloutPane() {
|
|
|
749
833
|
renderFalloutPane();
|
|
750
834
|
}
|
|
751
835
|
|
|
836
|
+
/* Obscura kurulum ilerlemesi: main process'te sürer — ayarlardan çıkılsa da
|
|
837
|
+
kesilmez. Panel açıksa çubuk canlı güncellenir; bitişte tek kez toast atılır. */
|
|
838
|
+
let _obscuraWasRunning = false;
|
|
839
|
+
function paintObscuraState(st) {
|
|
840
|
+
const ocSt = $('#ocStatus');
|
|
841
|
+
const ocProg = $('#ocProg');
|
|
842
|
+
const ocBtn = $('#ocInstall');
|
|
843
|
+
if (!ocSt || !ocProg || !ocBtn || !st || !st.phase) return;
|
|
844
|
+
const pct = Math.max(0, Math.min(100, Number(st.pct) || 0));
|
|
845
|
+
const fill = ocProg.querySelector('.oc-prog-fill');
|
|
846
|
+
const txt = ocProg.querySelector('.oc-prog-text');
|
|
847
|
+
if (st.running) {
|
|
848
|
+
ocBtn.disabled = true;
|
|
849
|
+
ocBtn.textContent = _t('oc_install_running');
|
|
850
|
+
ocProg.hidden = false;
|
|
851
|
+
if (fill) fill.style.width = pct + '%';
|
|
852
|
+
const phaseKey = ({ 'hazırlanıyor': 'prep', 'indiriliyor': 'download', 'kuruluyor': 'extract', 'doğrulanıyor': 'verify' })[st.phase];
|
|
853
|
+
if (txt) txt.textContent = (phaseKey ? _t('oc_phase_' + phaseKey) : st.phase) + ' — ' + pct + '%';
|
|
854
|
+
ocSt.textContent = _t('oc_bg_note');
|
|
855
|
+
} else if (st.phase === 'tamamlandı' && pct >= 100) {
|
|
856
|
+
ocBtn.disabled = false;
|
|
857
|
+
ocBtn.textContent = _t('oc_install');
|
|
858
|
+
ocProg.hidden = false;
|
|
859
|
+
if (fill) fill.style.width = '100%';
|
|
860
|
+
if (txt) txt.textContent = _t('oc_phase_done') + ' — 100%';
|
|
861
|
+
ocSt.textContent = _t('oc_status_ok');
|
|
862
|
+
} else if (st.phase === 'hata') {
|
|
863
|
+
ocBtn.disabled = false;
|
|
864
|
+
ocBtn.textContent = _t('oc_install');
|
|
865
|
+
ocProg.hidden = true;
|
|
866
|
+
ocSt.textContent = _t('oc_install_fail') + (st.error || '?');
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
function onObscuraProgress(ev) {
|
|
871
|
+
if (ev && ev.running) _obscuraWasRunning = true;
|
|
872
|
+
paintObscuraState(ev);
|
|
873
|
+
if (ev && !ev.running && _obscuraWasRunning) {
|
|
874
|
+
_obscuraWasRunning = false;
|
|
875
|
+
if (ev.phase === 'tamamlandı') toast(_t('oc_installed_toast'));
|
|
876
|
+
else if (ev.phase === 'hata') toast(_t('oc_install_fail') + (ev.error || '?'));
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
752
880
|
/* Web Arama sekmesi: TinyFish anahtarı + Obscura (kurulum/aktiflik) + arama sırası */
|
|
753
881
|
async function renderWebSearchPane() {
|
|
754
882
|
const pane = $('#tab-websearch');
|
|
@@ -764,7 +892,10 @@ async function renderWebSearchPane() {
|
|
|
764
892
|
'<div id="ocStatus" class="sub" style="text-align:left;margin-top:8px"></div>' +
|
|
765
893
|
'<label class="mem-label" style="display:flex;align-items:center;gap:8px;cursor:pointer">' +
|
|
766
894
|
'<input id="ocEnabled" type="checkbox" style="width:auto" /> <span>' + _t('oc_enabled') + '</span></label>' +
|
|
767
|
-
'<div
|
|
895
|
+
'<div id="ocProg" class="oc-prog" hidden>' +
|
|
896
|
+
'<div class="oc-prog-bar"><div class="oc-prog-fill" style="width:0%"></div></div>' +
|
|
897
|
+
'<div class="oc-prog-text sub"></div></div>' +
|
|
898
|
+
'<div style="display:flex;justify-content:center;margin-top:12px">' +
|
|
768
899
|
'<button id="ocInstall" class="btn">' + _t('oc_install') + '</button></div>' +
|
|
769
900
|
/* --- Arama sırası --- */
|
|
770
901
|
'<div class="divider"></div>' +
|
|
@@ -786,22 +917,21 @@ async function renderWebSearchPane() {
|
|
|
786
917
|
const ocSt = $('#ocStatus');
|
|
787
918
|
const ocChk = $('#ocEnabled');
|
|
788
919
|
const ocBtn = $('#ocInstall');
|
|
789
|
-
const oc = await beast.obscuraGet().catch(() => ({ installed: false, enabled: true, dir: '' }));
|
|
920
|
+
const oc = await beast.obscuraGet().catch(() => ({ installed: false, enabled: true, dir: '', install: null }));
|
|
790
921
|
ocChk.checked = oc.enabled !== false;
|
|
791
|
-
|
|
922
|
+
/* kurulum arka planda sürüyorsa durum panelde GERİ YÜKLENİR —
|
|
923
|
+
ayarlardan çıkıp dönsen bile ilerleme kaybolmaz */
|
|
924
|
+
const ocState = oc.install || (await beast.obscuraInstallState().catch(() => null));
|
|
925
|
+
paintObscuraState(ocState);
|
|
792
926
|
ocChk.addEventListener('change', async () => {
|
|
793
927
|
await beast.obscuraSetEnabled(ocChk.checked).catch(() => {});
|
|
794
928
|
toast(ocChk.checked ? _t('oc_on_toast') : _t('oc_off_toast'));
|
|
795
929
|
});
|
|
796
930
|
ocBtn.addEventListener('click', async () => {
|
|
797
|
-
ocBtn.disabled
|
|
798
|
-
ocBtn.textContent = _t('oc_installing');
|
|
799
|
-
ocSt.textContent = _t('oc_installing');
|
|
931
|
+
if (ocBtn.disabled) return;
|
|
800
932
|
const r = await beast.obscuraInstall().catch(() => ({ ok: false, error: 'hata' }));
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
ocSt.textContent = r && r.ok ? _t('oc_status_ok') : _t('oc_install_fail') + (r && r.error ? r.error : '?');
|
|
804
|
-
toast(r && r.ok ? _t('oc_installed_toast') : _t('oc_install_fail') + (r && r.error ? r.error : '?'));
|
|
933
|
+
if (r && (r.ok || r.started || r.busy)) paintObscuraState(await beast.obscuraInstallState().catch(() => null));
|
|
934
|
+
else toast(_t('oc_install_fail') + ((r && r.error) || '?'));
|
|
805
935
|
});
|
|
806
936
|
|
|
807
937
|
/* --- Arama sırası --- */
|
|
@@ -3208,8 +3338,11 @@ function renderBotSettings(pane, b) {
|
|
|
3208
3338
|
})
|
|
3209
3339
|
);
|
|
3210
3340
|
$('#bSave').addEventListener('click', async () => {
|
|
3341
|
+
const newName = $('#bName').value.trim();
|
|
3342
|
+
/* İLK HARF ZORUNLU: ad değiştirilirken de harf karakteriyle başlamalı */
|
|
3343
|
+
if (newName && !/^\p{L}/u.test(newName)) { toast(_t('bot_name_letter')); $('#bName').focus(); return; }
|
|
3211
3344
|
const patch = {
|
|
3212
|
-
name:
|
|
3345
|
+
name: newName,
|
|
3213
3346
|
icon: (f.querySelector('#bIconPick button.on') || {}).dataset?.ic || b.icon,
|
|
3214
3347
|
prompt: $('#bPrompt').value,
|
|
3215
3348
|
seeBots: [...f.querySelectorAll('#bSee input:checked')].map((c) => c.dataset.see),
|
|
@@ -3235,8 +3368,12 @@ function renderBotSettings(pane, b) {
|
|
|
3235
3368
|
if (delBtn) delBtn.addEventListener('click', async () => {
|
|
3236
3369
|
if (!confirm(_ti('bot_confirm_del', b.name))) return;
|
|
3237
3370
|
const r = await beast.botsRemove(b.id);
|
|
3238
|
-
if (r.ok) {
|
|
3239
|
-
|
|
3371
|
+
if (r.ok) {
|
|
3372
|
+
/* bot silme sonrası main otomatik restart atar — yeniden çizmeye kalkışma */
|
|
3373
|
+
toast(r.restarting ? _t('bot_deleted_restart') : _t('bot_deleted'));
|
|
3374
|
+
botPageId = null;
|
|
3375
|
+
if (!r.restarting) { await refreshBots(); renderBotPage(); }
|
|
3376
|
+
} else toast(r.error || _t('bot_save_fail'));
|
|
3240
3377
|
});
|
|
3241
3378
|
/* (numara ekleme alanı kaldırıldı — Entegrasyonlar üzerinden yapılır) */
|
|
3242
3379
|
}
|
|
@@ -3358,6 +3495,8 @@ function renderBotAdd(pane) {
|
|
|
3358
3495
|
$('#bAddCreate').addEventListener('click', async () => {
|
|
3359
3496
|
const name = $('#bAddName').value.trim();
|
|
3360
3497
|
if (!name) { toast(_t('bot_name_req')); $('#bAddName').focus(); return; }
|
|
3498
|
+
/* İLK HARF ZORUNLU: bot adı harf karakteriyle başlamalı */
|
|
3499
|
+
if (!/^\p{L}/u.test(name)) { toast(_t('bot_name_letter')); $('#bAddName').focus(); return; }
|
|
3361
3500
|
const icon = (f.querySelector('#bAddIconPick button.on') || {}).dataset?.ic || '🤖';
|
|
3362
3501
|
const r = await beast.botsAdd({ name, icon, prompt: $('#bAddPrompt').value });
|
|
3363
3502
|
if (r.ok) {
|
|
@@ -3715,6 +3854,8 @@ function onEvent(ev) {
|
|
|
3715
3854
|
}
|
|
3716
3855
|
/* OFFLINE MESAJ KUYRUĞU: bağlantı + kuyruk olayları (sessionId filtresinden önce) */
|
|
3717
3856
|
if (ev.type === 'net' || ev.type === 'netQueue') { onNetEvent(ev); return; }
|
|
3857
|
+
/* Obscura kurulum ilerlemesi — ayarlardan çıkıp dönülsen de main'de sürer */
|
|
3858
|
+
if (ev.type === 'obscura-progress') { onObscuraProgress(ev); return; }
|
|
3718
3859
|
/* Beast Code oturumu (IDE modu ortasındaki panel): olayları panele akıt,
|
|
3719
3860
|
ana sohbeti kirletme */
|
|
3720
3861
|
if (bcSessionId && ev.sessionId === bcSessionId) {
|
|
@@ -4301,6 +4442,7 @@ function autosize() {
|
|
|
4301
4442
|
|
|
4302
4443
|
const SLASH_COMMANDS = [
|
|
4303
4444
|
{ cmd: '/help', desc: 'tüm komutları listele' },
|
|
4445
|
+
{ cmd: '/version', desc: 'Beast Agent sürümünü göster' },
|
|
4304
4446
|
{ cmd: '/new', desc: 'yeni oturum aç (kod verilir)' },
|
|
4305
4447
|
{ cmd: '/open ', desc: 'koddaki oturuma geç' },
|
|
4306
4448
|
{ cmd: '/sessions', desc: 'bu sohbetin oturumları' },
|
|
@@ -6166,6 +6308,15 @@ function bcIngest(ev) {
|
|
|
6166
6308
|
case 'tool-end':
|
|
6167
6309
|
bcToolBoxEnd(ev.callId, ev.ok, ev.result);
|
|
6168
6310
|
break;
|
|
6311
|
+
case 'bc-mode':
|
|
6312
|
+
/* OpenCode disiplini çalışma modu: başlıkta rozet + panelde bilgi satırı */
|
|
6313
|
+
if (els.bcTitle) {
|
|
6314
|
+
els.bcTitle.textContent =
|
|
6315
|
+
ev.mode === 'plan' ? 'BEAST CODE · PLAN' :
|
|
6316
|
+
ev.mode === 'build' ? 'BEAST CODE · BUILD' : 'BEAST CODE';
|
|
6317
|
+
}
|
|
6318
|
+
bcLine('t-dim', '[' + (ev.body || ev.mode || 'mod değişti') + ']');
|
|
6319
|
+
break;
|
|
6169
6320
|
case 'done':
|
|
6170
6321
|
bcFlushStream();
|
|
6171
6322
|
bcCloseToolGroup();
|
|
@@ -6203,7 +6354,12 @@ function bcRunCurrent() {
|
|
|
6203
6354
|
/* sürüyor/serbest kararını main'deki gerçek engine.isBusy verir —
|
|
6204
6355
|
panel bayat kilitli kaldıysa ilk mesajla kendini düzeltir */
|
|
6205
6356
|
beast.beastcodeSend(msg).then((r) => {
|
|
6206
|
-
if (r && r.ok) {
|
|
6357
|
+
if (r && r.ok && r.mode) {
|
|
6358
|
+
/* /plan · /build · /auto: mod değişimi — tur AÇILMAZ, kilit anında açılır
|
|
6359
|
+
(bilgi satırı bc-mode olayıyla düşer, başlık rozeti güncellenir) */
|
|
6360
|
+
bcSessionId = r.sessionId;
|
|
6361
|
+
bcSetBusy(false);
|
|
6362
|
+
} else if (r && r.ok) {
|
|
6207
6363
|
bcSessionId = r.sessionId;
|
|
6208
6364
|
} else if (r && r.busy) {
|
|
6209
6365
|
/* iş gerçekten sürüyor — kilit açılmaz, bilgi satırı düşer */
|
|
@@ -6238,6 +6394,7 @@ if (els.bcNew) els.bcNew.addEventListener('click', async () => {
|
|
|
6238
6394
|
bcCloseToolGroup();
|
|
6239
6395
|
bcHideTodos();
|
|
6240
6396
|
bcFlushStream();
|
|
6397
|
+
if (els.bcTitle) els.bcTitle.textContent = 'BEAST CODE';
|
|
6241
6398
|
bcLine('t-sys', 'yeni oturum — sonraki mesaj taze başlar');
|
|
6242
6399
|
}
|
|
6243
6400
|
});
|
package/src/renderer/style.css
CHANGED
|
@@ -119,6 +119,9 @@ body {
|
|
|
119
119
|
}
|
|
120
120
|
.sess:hover { background: var(--panel2); color: var(--text); }
|
|
121
121
|
.sess.active { background: var(--panel2); color: var(--text); box-shadow: inset 2px 0 0 var(--accent); }
|
|
122
|
+
/* sürükle-bırak sıralama görselleri */
|
|
123
|
+
.sess.dragging { opacity: 0.35; }
|
|
124
|
+
.sess.drag-over { box-shadow: inset 0 2px 0 var(--accent); }
|
|
122
125
|
.sess-wa {
|
|
123
126
|
width: 15px;
|
|
124
127
|
height: 15px;
|
|
@@ -291,6 +294,13 @@ body.browser-open #topbar { width: auto; }
|
|
|
291
294
|
#eyeBtn:not(.on) { opacity: 0.55; }
|
|
292
295
|
#eyeBtn.on { background: var(--accent-dim); color: var(--accent); }
|
|
293
296
|
|
|
297
|
+
/* internet göstergesi (topbar sağ uç): bağlı = YEŞİL wifi, kopuk = KIRMIZI */
|
|
298
|
+
#netDot { cursor: default; }
|
|
299
|
+
#netDot.net-on { color: #22c55e; }
|
|
300
|
+
#netDot.net-off { color: #ef4444; }
|
|
301
|
+
#netDot.net-off .net-slash { display: block; }
|
|
302
|
+
#netDot.net-on .net-slash { display: none; }
|
|
303
|
+
|
|
294
304
|
/* #19 sağ panel (paralel ajan konsolu) aç/kapa */
|
|
295
305
|
#railBtn.on { background: var(--accent-dim); color: var(--accent); }
|
|
296
306
|
body.rail-hidden #rail { display: none; }
|
|
@@ -770,6 +780,23 @@ body.term-open #settingsOverlay { right: var(--tw, 520px); }
|
|
|
770
780
|
word-break: break-word;
|
|
771
781
|
}
|
|
772
782
|
|
|
783
|
+
/* Obscura kurulum ilerleme çubuğu (Ayarlar → Web Arama) */
|
|
784
|
+
.oc-prog { margin-top: 10px; }
|
|
785
|
+
.oc-prog-bar {
|
|
786
|
+
height: 9px;
|
|
787
|
+
background: var(--accent-dim);
|
|
788
|
+
border: 1px solid var(--border);
|
|
789
|
+
border-radius: 6px;
|
|
790
|
+
overflow: hidden;
|
|
791
|
+
}
|
|
792
|
+
.oc-prog-fill {
|
|
793
|
+
height: 100%;
|
|
794
|
+
width: 0%;
|
|
795
|
+
background: var(--accent);
|
|
796
|
+
transition: width 0.25s ease;
|
|
797
|
+
}
|
|
798
|
+
.oc-prog-text { margin-top: 5px; font-size: 12px; color: var(--muted); }
|
|
799
|
+
|
|
773
800
|
/* markdown */
|
|
774
801
|
.md p { margin: 0 0 8px; }
|
|
775
802
|
.md p:last-child { margin-bottom: 0; }
|
|
@@ -910,6 +937,18 @@ body.term-open #settingsOverlay { right: var(--tw, 520px); }
|
|
|
910
937
|
margin-bottom: 5px;
|
|
911
938
|
}
|
|
912
939
|
#todoPanel .td-count { letter-spacing: 0; font-weight: 700; color: var(--text); }
|
|
940
|
+
#todoPanel .td-right { display: flex; align-items: center; gap: 8px; }
|
|
941
|
+
/* sağ köşe kapatma (×): yalnız bu listeyi gizler — yeni güncellemede yine görünür */
|
|
942
|
+
#todoPanel .td-x {
|
|
943
|
+
border: none;
|
|
944
|
+
background: none;
|
|
945
|
+
color: var(--muted);
|
|
946
|
+
font-size: 14px;
|
|
947
|
+
line-height: 1;
|
|
948
|
+
padding: 0 3px;
|
|
949
|
+
cursor: pointer;
|
|
950
|
+
}
|
|
951
|
+
#todoPanel .td-x:hover { color: var(--err); }
|
|
913
952
|
#todoPanel .td-row {
|
|
914
953
|
display: flex;
|
|
915
954
|
align-items: baseline;
|
|
@@ -1784,14 +1823,16 @@ body.browser-open #settingsDialog { width: min(70vw, calc(100vw - var(--bw, 480p
|
|
|
1784
1823
|
flex-shrink: 0;
|
|
1785
1824
|
}
|
|
1786
1825
|
|
|
1787
|
-
/* tek tıkla model (OpenCode Zen) hero butonu */
|
|
1826
|
+
/* tek tıkla model (OpenCode Zen) hero butonu — buton ortada */
|
|
1788
1827
|
.zen-hero {
|
|
1789
1828
|
padding: 10px 12px;
|
|
1790
1829
|
border: 1px dashed var(--border);
|
|
1791
1830
|
border-radius: 10px;
|
|
1792
1831
|
margin-bottom: 12px;
|
|
1793
1832
|
background: var(--panel);
|
|
1833
|
+
text-align: center;
|
|
1794
1834
|
}
|
|
1835
|
+
.zen-hero .sub { text-align: left; }
|
|
1795
1836
|
.zen-hero .btn { min-width: 220px; }
|
|
1796
1837
|
.zen-hero .btn .zen-load {
|
|
1797
1838
|
display: inline-block;
|
package/tests/bg-jobs.test.js
CHANGED
|
@@ -286,6 +286,51 @@ test('#5 eşzamanlılık limiti: fazlası kuyrukta bekler, slot boşalınca FIFO
|
|
|
286
286
|
|
|
287
287
|
/* ---------- İPTAL SEBEBİ DİSİPLİNİ ---------- */
|
|
288
288
|
|
|
289
|
+
test('üst oturum hata verirse rapor TEK SEFER düşer — sonsuz _run zinciri yok', async () => {
|
|
290
|
+
/* model yok → bg işi error ile biter → rapor üst oturuma flush edilir;
|
|
291
|
+
üst oturum da model-yok ile SYNC çökerse flush zinciri SONSUZA dönmemeli
|
|
292
|
+
(gerçek hata: flush → send → _run sync-throw → finally → flush … stack overflow) */
|
|
293
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'beast-flush-'));
|
|
294
|
+
let runCount = 0;
|
|
295
|
+
const eng = new Engine({}, { sessionsDir: dir, emit: () => {} });
|
|
296
|
+
const origRun = Engine.prototype._run;
|
|
297
|
+
eng._run = function (s) { runCount++; return origRun.call(this, s); };
|
|
298
|
+
eng.runBackground('pflush', 'iş', 'Rapor İş');
|
|
299
|
+
await sleep(700);
|
|
300
|
+
assert.ok(runCount <= 4, 'sonsuz _run zinciri yok (runCount=' + runCount + ')');
|
|
301
|
+
/* rapor tam olarak 1 kez üst oturuma gönderilmiş olmalı */
|
|
302
|
+
eng.deleteSession('pflush');
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
test('takılı iş ZORLA kapatılır: grup tamamlanır, birleşik rapor düşer', async () => {
|
|
306
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'beast-fc-'));
|
|
307
|
+
const eng = new Engine({}, { sessionsDir: dir, emit: () => {} });
|
|
308
|
+
const sent = [];
|
|
309
|
+
eng.send = (sid, payload) => { sent.push({ sid, text: String((payload && payload.text) || '') }); return true; };
|
|
310
|
+
const r = eng.runBackgroundMany(
|
|
311
|
+
'pfc',
|
|
312
|
+
[
|
|
313
|
+
{ title: 'A1', task: 'görev a' },
|
|
314
|
+
{ title: 'B1', task: 'görev b' },
|
|
315
|
+
],
|
|
316
|
+
'Zorla Grup'
|
|
317
|
+
);
|
|
318
|
+
assert.ok(r.ok);
|
|
319
|
+
/* A1 done; B1 takılı → force-close */
|
|
320
|
+
await eng.reportBackgroundDone(r.ids[0]);
|
|
321
|
+
const stuck = eng._bgJobs.get(r.ids[1]);
|
|
322
|
+
stuck.fixes = 99; /* öz-kurtarma hakları tükenmiş */
|
|
323
|
+
eng._bgForceClose(stuck, 'test: takıldı');
|
|
324
|
+
await sleep(30);
|
|
325
|
+
const j = eng._bgJobs.get(r.ids[1]);
|
|
326
|
+
assert.strictEqual(j.status, 'aborted', 'zorla kapatılan iş aborted');
|
|
327
|
+
assert.ok(String(j.error || '').includes('test: takıldı'), 'sebep kayda geçti');
|
|
328
|
+
const rep = sent.find((x) => x.sid === 'pfc' && x.text.includes('[ARKA PLAN GRUP BİTTİ'));
|
|
329
|
+
assert.ok(rep, 'birleşik rapor düştü — grup kilitlenmedi');
|
|
330
|
+
assert.ok(/B1/.test(rep.text) && /aborted/.test(rep.text), 'raporda takılan üye SEBEPİYLE görünüyor');
|
|
331
|
+
eng.deleteSession('pfc');
|
|
332
|
+
});
|
|
333
|
+
|
|
289
334
|
test('iptal SEBEBİ zorunlu: interrupt sebebi iş kaydına ve üst sohbete yazar', async () => {
|
|
290
335
|
const { eng } = tmpEngine();
|
|
291
336
|
const sent = [];
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/* Bot adı disiplini: İLK KARAKTER HARF ZORUNLU (ekleme + güncelleme) */
|
|
4
|
+
|
|
5
|
+
const test = require('node:test');
|
|
6
|
+
const assert = require('node:assert');
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const os = require('os');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
|
|
11
|
+
/* BEAST_DATA modül yüklenmeden önce ayarlanmalı (REG cache'i tek dosyada) */
|
|
12
|
+
process.env.BEAST_DATA = fs.mkdtempSync(path.join(os.tmpdir(), 'beast-bots-name-'));
|
|
13
|
+
const bots = require('../src/agent/bots');
|
|
14
|
+
|
|
15
|
+
test('bot ekleme: ad harf ile başlamalı', () => {
|
|
16
|
+
for (const bad of ['1Muhasebe', '9bot', '-abc', '_x', ' 3BoT']) {
|
|
17
|
+
const r = bots.add({ name: bad, prompt: '' });
|
|
18
|
+
assert.strictEqual(r.ok, false, bad + ' reddedilmeli');
|
|
19
|
+
assert.ok(/harf ile başlamalı/.test(r.error), r.error);
|
|
20
|
+
}
|
|
21
|
+
const okNum = bots.add({ name: 'Muhasebe', prompt: '' });
|
|
22
|
+
assert.ok(okNum.ok, 'harf ile başlayan ad kabul: ' + (okNum.error || ''));
|
|
23
|
+
const okTr = bots.add({ name: 'Çağrı', prompt: '' });
|
|
24
|
+
assert.ok(okTr.ok, 'Türkçe harf (Ç) kabul: ' + (okTr.error || ''));
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('bot güncelleme: geçersiz ad diğer alanlara dokunmadan reddedilir', () => {
|
|
28
|
+
const created = bots.add({ name: 'Depo', prompt: 'eski' });
|
|
29
|
+
assert.ok(created.ok);
|
|
30
|
+
const id = created.bot.id;
|
|
31
|
+
|
|
32
|
+
const bad = bots.update(id, { name: '3Depo', prompt: 'yeni' });
|
|
33
|
+
assert.strictEqual(bad.ok, false, 'geçersiz ad reddedildi');
|
|
34
|
+
const after = bots.get(id);
|
|
35
|
+
assert.strictEqual(after.name, 'Depo', 'eski ad korundu');
|
|
36
|
+
assert.strictEqual(after.prompt, 'eski', 'prompt değişmedi');
|
|
37
|
+
|
|
38
|
+
const good = bots.update(id, { name: 'Anbar2', prompt: 'yeni' });
|
|
39
|
+
assert.ok(good.ok, 'harf ile başlayan güncelleme kabul: ' + (good.error || ''));
|
|
40
|
+
assert.strictEqual(bots.get(id).prompt, 'yeni');
|
|
41
|
+
assert.strictEqual(bots.get(id).name, 'Anbar2');
|
|
42
|
+
});
|
package/tests/llm.test.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
require('./setup');
|
|
4
4
|
const test = require('node:test');
|
|
5
5
|
const assert = require('node:assert');
|
|
6
|
-
const { withRetries, friendlyError, sumUsage, chatStreamAuto } = require('../src/agent/llm');
|
|
6
|
+
const { withRetries, friendlyError, sumUsage, chatStreamAuto, setNetProbe, isNetworkError } = require('../src/agent/llm');
|
|
7
7
|
|
|
8
8
|
test('withRetries: 503 sonrası başarılı isteği tekrarlar', async () => {
|
|
9
9
|
let calls = 0;
|
|
@@ -134,3 +134,72 @@ test('chatStreamAuto: normal (stop) yanıtta hiç devam turu açılmaz', async (
|
|
|
134
134
|
srv.close();
|
|
135
135
|
}
|
|
136
136
|
});
|
|
137
|
+
|
|
138
|
+
/* ---------- ağ kopması dayanıklılığı (#retry) ---------- */
|
|
139
|
+
|
|
140
|
+
test('isNetworkError sınıflandırması', () => {
|
|
141
|
+
const net = new Error('fetch failed');
|
|
142
|
+
assert.equal(isNetworkError(net), true, 'status yok → ağ hatası');
|
|
143
|
+
const http = new Error('HTTP 503');
|
|
144
|
+
http.status = 503;
|
|
145
|
+
assert.equal(isNetworkError(http), false, 'status var → HTTP hatası');
|
|
146
|
+
const abort = new Error('iptal');
|
|
147
|
+
abort.name = 'AbortError';
|
|
148
|
+
assert.equal(isNetworkError(abort), false, 'abort ağ hatası sayılmaz');
|
|
149
|
+
assert.equal(isNetworkError(null), false);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('withRetries: internet kapalıyken probe bekler, dönünce başarır', async () => {
|
|
153
|
+
let online = false;
|
|
154
|
+
setNetProbe(() => online);
|
|
155
|
+
setTimeout(() => { online = true; }, 30);
|
|
156
|
+
let calls = 0;
|
|
157
|
+
const t0 = Date.now();
|
|
158
|
+
const r = await withRetries(async () => {
|
|
159
|
+
calls++;
|
|
160
|
+
if (calls === 1) throw new Error('fetch failed');
|
|
161
|
+
return 'ok';
|
|
162
|
+
});
|
|
163
|
+
assert.equal(r, 'ok');
|
|
164
|
+
assert.equal(calls, 2);
|
|
165
|
+
assert.ok(Date.now() - t0 > 2000, 'probe çevrimdışı dediği için bekledi');
|
|
166
|
+
setNetProbe(null);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('akış ortasında bağlantı koparsa: kısmi metinle devam turu açılır (hata balonu yok)', async () => {
|
|
170
|
+
const http = require('node:http');
|
|
171
|
+
const calls = [];
|
|
172
|
+
const srv = http.createServer((req, res) => {
|
|
173
|
+
let body = '';
|
|
174
|
+
req.on('data', (c) => (body += c));
|
|
175
|
+
req.on('end', () => {
|
|
176
|
+
calls.push(JSON.parse(body).messages);
|
|
177
|
+
if (calls.length === 1) {
|
|
178
|
+
/* birkaç delta yaz, sonra soketi ORTADAN kopar */
|
|
179
|
+
res.writeHead(200, { 'Content-Type': 'text/event-stream' });
|
|
180
|
+
res.write('data: ' + JSON.stringify({ choices: [{ delta: { content: 'Merhaba du' } }] }) + '\n\n');
|
|
181
|
+
setTimeout(() => res.destroy(), 30);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
res.writeHead(200, { 'Content-Type': 'text/event-stream' });
|
|
185
|
+
res.write('data: ' + JSON.stringify({ choices: [{ delta: { content: 'nya devam' } }] }) + '\n\n');
|
|
186
|
+
res.write('data: ' + JSON.stringify({ choices: [{ delta: {}, finish_reason: 'stop' }] }) + '\n\n');
|
|
187
|
+
res.write('data: [DONE]\n\n');
|
|
188
|
+
res.end();
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
await new Promise((r) => srv.listen(0, '127.0.0.1', r));
|
|
192
|
+
try {
|
|
193
|
+
const port = srv.address().port;
|
|
194
|
+
const sel = { url: `http://127.0.0.1:${port}/v1/chat/completions`, key: 'k', model: 'm' };
|
|
195
|
+
const r = await chatStreamAuto(sel, { messages: [{ role: 'user', content: 'selam' }] });
|
|
196
|
+
assert.equal(r.content, 'Merhaba dunya devam', 'kısmi + devam birleşti');
|
|
197
|
+
assert.equal(r.finishReason, 'stop');
|
|
198
|
+
assert.equal(calls.length, 2, 'devam turu açıldı');
|
|
199
|
+
const m2 = calls[1];
|
|
200
|
+
assert.equal(m2[m2.length - 2].role, 'assistant');
|
|
201
|
+
assert.match(m2[m2.length - 1].content, /DEVAM/);
|
|
202
|
+
} finally {
|
|
203
|
+
srv.close();
|
|
204
|
+
}
|
|
205
|
+
});
|