beast-agent 0.27.0 → 0.29.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 +96 -55
- package/src/agent/llm.js +153 -71
- package/src/agent/obscura.js +276 -0
- package/src/agent/skills.js +14 -10
- package/src/agent/tools.js +105 -51
- package/src/main.js +117 -30
- package/src/preload.js +6 -3
- package/src/renderer/i18n.js +72 -26
- package/src/renderer/index.html +1 -0
- package/src/renderer/renderer.js +177 -41
- package/src/renderer/style.css +38 -1
- package/tests/bg-jobs.test.js +71 -3
- package/tests/bots-name.test.js +42 -0
- package/tests/llm.test.js +70 -1
- package/tests/obscura.test.js +124 -0
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'));
|
|
@@ -383,6 +393,17 @@ function addErrorBubble(text) {
|
|
|
383
393
|
scrollDown(true);
|
|
384
394
|
}
|
|
385
395
|
|
|
396
|
+
/* ajan durduruldu/iptal edildi — SEBEP zorunlu, sohbete sönük not olarak düşer */
|
|
397
|
+
function addStopNote(reason) {
|
|
398
|
+
streamEl = null;
|
|
399
|
+
showEmpty(false);
|
|
400
|
+
const div = document.createElement('div');
|
|
401
|
+
div.className = 'msg msg-sys';
|
|
402
|
+
div.textContent = '\u23F9 Durduruldu — sebep: ' + String(reason || 'sebep belirtilmedi');
|
|
403
|
+
els.msgs.appendChild(div);
|
|
404
|
+
scrollDown(true);
|
|
405
|
+
}
|
|
406
|
+
|
|
386
407
|
/* ---------------- tool cards ---------------- */
|
|
387
408
|
/* Ard arda gelen araç kartları TEK kutuda toplanır (.tool-box): çalışırken
|
|
388
409
|
lacivert outline döner, gövde max 420px scroll'lu. Grup; tool_calls İÇEREN
|
|
@@ -738,12 +759,77 @@ async function refreshFalloutPane() {
|
|
|
738
759
|
renderFalloutPane();
|
|
739
760
|
}
|
|
740
761
|
|
|
741
|
-
/*
|
|
762
|
+
/* Obscura kurulum ilerlemesi: main process'te sürer — ayarlardan çıkılsa da
|
|
763
|
+
kesilmez. Panel açıksa çubuk canlı güncellenir; bitişte tek kez toast atılır. */
|
|
764
|
+
let _obscuraWasRunning = false;
|
|
765
|
+
function paintObscuraState(st) {
|
|
766
|
+
const ocSt = $('#ocStatus');
|
|
767
|
+
const ocProg = $('#ocProg');
|
|
768
|
+
const ocBtn = $('#ocInstall');
|
|
769
|
+
if (!ocSt || !ocProg || !ocBtn || !st || !st.phase) return;
|
|
770
|
+
const pct = Math.max(0, Math.min(100, Number(st.pct) || 0));
|
|
771
|
+
const fill = ocProg.querySelector('.oc-prog-fill');
|
|
772
|
+
const txt = ocProg.querySelector('.oc-prog-text');
|
|
773
|
+
if (st.running) {
|
|
774
|
+
ocBtn.disabled = true;
|
|
775
|
+
ocBtn.textContent = _t('oc_install_running');
|
|
776
|
+
ocProg.hidden = false;
|
|
777
|
+
if (fill) fill.style.width = pct + '%';
|
|
778
|
+
const phaseKey = ({ 'hazırlanıyor': 'prep', 'indiriliyor': 'download', 'kuruluyor': 'extract', 'doğrulanıyor': 'verify' })[st.phase];
|
|
779
|
+
if (txt) txt.textContent = (phaseKey ? _t('oc_phase_' + phaseKey) : st.phase) + ' — ' + pct + '%';
|
|
780
|
+
ocSt.textContent = _t('oc_bg_note');
|
|
781
|
+
} else if (st.phase === 'tamamlandı' && pct >= 100) {
|
|
782
|
+
ocBtn.disabled = false;
|
|
783
|
+
ocBtn.textContent = _t('oc_install');
|
|
784
|
+
ocProg.hidden = false;
|
|
785
|
+
if (fill) fill.style.width = '100%';
|
|
786
|
+
if (txt) txt.textContent = _t('oc_phase_done') + ' — 100%';
|
|
787
|
+
ocSt.textContent = _t('oc_status_ok');
|
|
788
|
+
} else if (st.phase === 'hata') {
|
|
789
|
+
ocBtn.disabled = false;
|
|
790
|
+
ocBtn.textContent = _t('oc_install');
|
|
791
|
+
ocProg.hidden = true;
|
|
792
|
+
ocSt.textContent = _t('oc_install_fail') + (st.error || '?');
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function onObscuraProgress(ev) {
|
|
797
|
+
if (ev && ev.running) _obscuraWasRunning = true;
|
|
798
|
+
paintObscuraState(ev);
|
|
799
|
+
if (ev && !ev.running && _obscuraWasRunning) {
|
|
800
|
+
_obscuraWasRunning = false;
|
|
801
|
+
if (ev.phase === 'tamamlandı') toast(_t('oc_installed_toast'));
|
|
802
|
+
else if (ev.phase === 'hata') toast(_t('oc_install_fail') + (ev.error || '?'));
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
/* Web Arama sekmesi: TinyFish anahtarı + Obscura (kurulum/aktiflik) + arama sırası */
|
|
742
807
|
async function renderWebSearchPane() {
|
|
743
808
|
const pane = $('#tab-websearch');
|
|
744
809
|
if (!pane) return;
|
|
745
810
|
const tf = await beast.tinyfishGet().catch(() => ({ set: false, masked: '' }));
|
|
746
811
|
pane.innerHTML =
|
|
812
|
+
'<h2>' + _t('ws_h2') + '</h2>' +
|
|
813
|
+
'<div class="sub">' + _t('ws_sub') + '</div>' +
|
|
814
|
+
/* --- Obscura --- */
|
|
815
|
+
'<div class="divider"></div>' +
|
|
816
|
+
'<h2>' + _t('oc_h2') + '</h2>' +
|
|
817
|
+
'<div class="sub">' + _t('oc_sub') + '</div>' +
|
|
818
|
+
'<div id="ocStatus" class="sub" style="text-align:left;margin-top:8px"></div>' +
|
|
819
|
+
'<label class="mem-label" style="display:flex;align-items:center;gap:8px;cursor:pointer">' +
|
|
820
|
+
'<input id="ocEnabled" type="checkbox" style="width:auto" /> <span>' + _t('oc_enabled') + '</span></label>' +
|
|
821
|
+
'<div id="ocProg" class="oc-prog" hidden>' +
|
|
822
|
+
'<div class="oc-prog-bar"><div class="oc-prog-fill" style="width:0%"></div></div>' +
|
|
823
|
+
'<div class="oc-prog-text sub"></div></div>' +
|
|
824
|
+
'<div style="display:flex;justify-content:center;margin-top:12px">' +
|
|
825
|
+
'<button id="ocInstall" class="btn">' + _t('oc_install') + '</button></div>' +
|
|
826
|
+
/* --- Arama sırası --- */
|
|
827
|
+
'<div class="divider"></div>' +
|
|
828
|
+
'<h2>' + _t('so_h2') + '</h2>' +
|
|
829
|
+
'<div class="sub">' + _t('so_sub') + '</div>' +
|
|
830
|
+
'<div id="soList" style="margin-top:10px"></div>' +
|
|
831
|
+
/* --- TinyFish --- */
|
|
832
|
+
'<div class="divider"></div>' +
|
|
747
833
|
'<h2>' + _t('tf_h2') + '</h2>' +
|
|
748
834
|
'<div class="sub">' + _t('tf_sub') + '</div>' +
|
|
749
835
|
'<div id="tfStatus" class="sub" style="text-align:left;margin-top:8px"></div>' +
|
|
@@ -751,26 +837,79 @@ async function renderWebSearchPane() {
|
|
|
751
837
|
'<input id="tfKeyInp" class="inp" type="password" placeholder="tf_..." autocomplete="new-password" spellcheck="false" />' +
|
|
752
838
|
'<div class="form-grid" style="grid-template-columns:auto auto;gap:8px;margin-top:8px">' +
|
|
753
839
|
'<button id="tfSave" class="btn">' + _t('ws_save') + '</button>' +
|
|
754
|
-
'<button id="tfClear" class="btn ghost">' + _t('ws_clear') + '</button></div>'
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
840
|
+
'<button id="tfClear" class="btn ghost">' + _t('ws_clear') + '</button></div>';
|
|
841
|
+
|
|
842
|
+
/* --- Obscura durumu --- */
|
|
843
|
+
const ocSt = $('#ocStatus');
|
|
844
|
+
const ocChk = $('#ocEnabled');
|
|
845
|
+
const ocBtn = $('#ocInstall');
|
|
846
|
+
const oc = await beast.obscuraGet().catch(() => ({ installed: false, enabled: true, dir: '', install: null }));
|
|
847
|
+
ocChk.checked = oc.enabled !== false;
|
|
848
|
+
/* kurulum arka planda sürüyorsa durum panelde GERİ YÜKLENİR —
|
|
849
|
+
ayarlardan çıkıp dönsen bile ilerleme kaybolmaz */
|
|
850
|
+
const ocState = oc.install || (await beast.obscuraInstallState().catch(() => null));
|
|
851
|
+
paintObscuraState(ocState);
|
|
852
|
+
ocChk.addEventListener('change', async () => {
|
|
853
|
+
await beast.obscuraSetEnabled(ocChk.checked).catch(() => {});
|
|
854
|
+
toast(ocChk.checked ? _t('oc_on_toast') : _t('oc_off_toast'));
|
|
855
|
+
});
|
|
856
|
+
ocBtn.addEventListener('click', async () => {
|
|
857
|
+
if (ocBtn.disabled) return;
|
|
858
|
+
const r = await beast.obscuraInstall().catch(() => ({ ok: false, error: 'hata' }));
|
|
859
|
+
if (r && (r.ok || r.started || r.busy)) paintObscuraState(await beast.obscuraInstallState().catch(() => null));
|
|
860
|
+
else toast(_t('oc_install_fail') + ((r && r.error) || '?'));
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
/* --- Arama sırası --- */
|
|
864
|
+
const soBox = $('#soList');
|
|
865
|
+
let rows = [];
|
|
866
|
+
const soRes = await beast.searchOrderGet().catch(() => ({ chain: [] }));
|
|
867
|
+
rows = (soRes && Array.isArray(soRes.chain) && soRes.chain.length) ? soRes.chain.map((x) => ({ id: x.id, on: x.on !== false })) : [];
|
|
868
|
+
const engName = (id) => _t('so_engine_' + id) || id;
|
|
869
|
+
function renderRows() {
|
|
870
|
+
soBox.innerHTML = '';
|
|
871
|
+
rows.forEach((row, i) => {
|
|
872
|
+
const el = document.createElement('div');
|
|
873
|
+
el.className = 'so-row' + (row.on ? '' : ' off');
|
|
874
|
+
el.style.cssText = 'display:flex;align-items:center;gap:8px;padding:6px 8px;border:1px solid var(--border);border-radius:8px;margin-bottom:6px';
|
|
875
|
+
el.innerHTML =
|
|
876
|
+
'<span class="so-idx" style="min-width:18px;color:var(--muted);font-size:12px">' + (i + 1) + '.</span>' +
|
|
877
|
+
'<input type="checkbox" class="so-chk" style="width:auto" ' + (row.on ? 'checked' : '') + ' />' +
|
|
878
|
+
'<span style="flex:1">' + escapeHtml(engName(row.id)) + '</span>' +
|
|
879
|
+
'<button class="so-up" title="Yukarı" style="width:auto;padding:2px 8px">↑</button>' +
|
|
880
|
+
'<button class="so-down" title="Aşağı" style="width:auto;padding:2px 8px">↓</button>';
|
|
881
|
+
el.querySelector('.so-chk').addEventListener('change', async (e) => {
|
|
882
|
+
row.on = e.target.checked;
|
|
883
|
+
el.classList.toggle('off', !row.on);
|
|
884
|
+
await saveOrder();
|
|
885
|
+
});
|
|
886
|
+
el.querySelector('.so-up').addEventListener('click', async () => {
|
|
887
|
+
if (i === 0) return;
|
|
888
|
+
[rows[i - 1], rows[i]] = [rows[i], rows[i - 1]];
|
|
889
|
+
renderRows();
|
|
890
|
+
await saveOrder();
|
|
891
|
+
});
|
|
892
|
+
el.querySelector('.so-down').addEventListener('click', async () => {
|
|
893
|
+
if (i >= rows.length - 1) return;
|
|
894
|
+
[rows[i + 1], rows[i]] = [rows[i], rows[i + 1]];
|
|
895
|
+
renderRows();
|
|
896
|
+
await saveOrder();
|
|
897
|
+
});
|
|
898
|
+
soBox.appendChild(el);
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
async function saveOrder() {
|
|
902
|
+
const r = await beast.searchOrderSet(rows).catch(() => null);
|
|
903
|
+
if (r && r.ok) toast(_t('so_saved_toast'));
|
|
904
|
+
else toast(_t('ws_fail_toast'));
|
|
905
|
+
}
|
|
906
|
+
if (rows.length) renderRows();
|
|
907
|
+
|
|
908
|
+
/* --- TinyFish --- */
|
|
769
909
|
const tfSt = $('#tfStatus');
|
|
770
910
|
const setTfSt = (r) => {
|
|
771
911
|
tfSt.textContent = r.set ? _t('tf_status_set') + r.masked : _t('tf_status_unset');
|
|
772
912
|
};
|
|
773
|
-
setExaSt(await beast.exaGet().catch(() => ({ set: false, masked: '' })));
|
|
774
913
|
setTfSt(tf);
|
|
775
914
|
$('#tfSave').addEventListener('click', async () => {
|
|
776
915
|
const v = $('#tfKeyInp').value.trim();
|
|
@@ -790,24 +929,6 @@ async function renderWebSearchPane() {
|
|
|
790
929
|
setTfSt({ set: false, masked: '' });
|
|
791
930
|
toast(_t('tf_cleared_toast'));
|
|
792
931
|
});
|
|
793
|
-
$('#exaSave').addEventListener('click', async () => {
|
|
794
|
-
const v = $('#exaKeyInp').value.trim();
|
|
795
|
-
if (!v) { toast(_t('ws_empty_toast')); return; }
|
|
796
|
-
const rr = await beast.exaSet(v).catch(() => null);
|
|
797
|
-
$('#exaKeyInp').value = '';
|
|
798
|
-
if (rr && rr.set) {
|
|
799
|
-
setExaSt(rr);
|
|
800
|
-
toast(_t('ws_saved_toast'));
|
|
801
|
-
} else {
|
|
802
|
-
toast(_t('ws_fail_toast'));
|
|
803
|
-
}
|
|
804
|
-
});
|
|
805
|
-
$('#exaClear').addEventListener('click', async () => {
|
|
806
|
-
await beast.exaClear().catch(() => {});
|
|
807
|
-
$('#exaKeyInp').value = '';
|
|
808
|
-
setExaSt({ set: false, masked: '' });
|
|
809
|
-
toast(_t('ws_cleared_toast'));
|
|
810
|
-
});
|
|
811
932
|
}
|
|
812
933
|
|
|
813
934
|
async function renderProviderPane() {
|
|
@@ -3143,8 +3264,11 @@ function renderBotSettings(pane, b) {
|
|
|
3143
3264
|
})
|
|
3144
3265
|
);
|
|
3145
3266
|
$('#bSave').addEventListener('click', async () => {
|
|
3267
|
+
const newName = $('#bName').value.trim();
|
|
3268
|
+
/* İLK HARF ZORUNLU: ad değiştirilirken de harf karakteriyle başlamalı */
|
|
3269
|
+
if (newName && !/^\p{L}/u.test(newName)) { toast(_t('bot_name_letter')); $('#bName').focus(); return; }
|
|
3146
3270
|
const patch = {
|
|
3147
|
-
name:
|
|
3271
|
+
name: newName,
|
|
3148
3272
|
icon: (f.querySelector('#bIconPick button.on') || {}).dataset?.ic || b.icon,
|
|
3149
3273
|
prompt: $('#bPrompt').value,
|
|
3150
3274
|
seeBots: [...f.querySelectorAll('#bSee input:checked')].map((c) => c.dataset.see),
|
|
@@ -3170,8 +3294,12 @@ function renderBotSettings(pane, b) {
|
|
|
3170
3294
|
if (delBtn) delBtn.addEventListener('click', async () => {
|
|
3171
3295
|
if (!confirm(_ti('bot_confirm_del', b.name))) return;
|
|
3172
3296
|
const r = await beast.botsRemove(b.id);
|
|
3173
|
-
if (r.ok) {
|
|
3174
|
-
|
|
3297
|
+
if (r.ok) {
|
|
3298
|
+
/* bot silme sonrası main otomatik restart atar — yeniden çizmeye kalkışma */
|
|
3299
|
+
toast(r.restarting ? _t('bot_deleted_restart') : _t('bot_deleted'));
|
|
3300
|
+
botPageId = null;
|
|
3301
|
+
if (!r.restarting) { await refreshBots(); renderBotPage(); }
|
|
3302
|
+
} else toast(r.error || _t('bot_save_fail'));
|
|
3175
3303
|
});
|
|
3176
3304
|
/* (numara ekleme alanı kaldırıldı — Entegrasyonlar üzerinden yapılır) */
|
|
3177
3305
|
}
|
|
@@ -3293,6 +3421,8 @@ function renderBotAdd(pane) {
|
|
|
3293
3421
|
$('#bAddCreate').addEventListener('click', async () => {
|
|
3294
3422
|
const name = $('#bAddName').value.trim();
|
|
3295
3423
|
if (!name) { toast(_t('bot_name_req')); $('#bAddName').focus(); return; }
|
|
3424
|
+
/* İLK HARF ZORUNLU: bot adı harf karakteriyle başlamalı */
|
|
3425
|
+
if (!/^\p{L}/u.test(name)) { toast(_t('bot_name_letter')); $('#bAddName').focus(); return; }
|
|
3296
3426
|
const icon = (f.querySelector('#bAddIconPick button.on') || {}).dataset?.ic || '🤖';
|
|
3297
3427
|
const r = await beast.botsAdd({ name, icon, prompt: $('#bAddPrompt').value });
|
|
3298
3428
|
if (r.ok) {
|
|
@@ -3447,7 +3577,7 @@ function renderAgentRail() {
|
|
|
3447
3577
|
`<span class="ag-dot"></span>` +
|
|
3448
3578
|
`<span class="sess-title">${escapeHtml(j.title)}</span>` +
|
|
3449
3579
|
(j.code ? `<span class="sess-code" title="Oturum kodu">${escapeHtml(j.code)}</span>` : ``) +
|
|
3450
|
-
`<span class="rj-time">${j.status === 'running' ? when + ' · ' + agentTimerHtml(j.startedAt) + ' · ' + _t('ag_working') : (agStText(j.status) === _t('ag_st_done') ? '\u2713' : (agStText(j.status) || j.status))}</span>` +
|
|
3580
|
+
`<span class="rj-time">${j.status === 'running' ? when + ' · ' + agentTimerHtml(j.startedAt) + ' · ' + _t('ag_working') : (agStText(j.status) === _t('ag_st_done') ? '\u2713' : (agStText(j.status) || j.status) + (j.status === 'aborted' && j.error ? ' — ' + escapeHtml(String(j.error).slice(0, 70)) : ''))}</span>` +
|
|
3451
3581
|
(j.status === 'running'
|
|
3452
3582
|
? `<button class="rj-cancel" title="${_t('ag_cancel')}">×</button>`
|
|
3453
3583
|
: `<button class="rj-cancel" title="${_t('ag_delete')}"><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="M3 6h18"/><path d="M8 6V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M10 11v6M14 11v6"/></svg></button>`) +
|
|
@@ -3577,7 +3707,7 @@ function renderAgentsPane() {
|
|
|
3577
3707
|
`<div class="ag-head">` +
|
|
3578
3708
|
`<span class="ag-dot"></span>` +
|
|
3579
3709
|
`<span class="ag-title">${escapeHtml(j.title)}</span>` +
|
|
3580
|
-
`<span class="ag-st">${agStText(j.status) || j.status}</span>` +
|
|
3710
|
+
`<span class="ag-st" title="${j.error ? escapeHtml(String(j.error)) : ''}">${agStText(j.status) || j.status}</span>` +
|
|
3581
3711
|
`<span class="ag-time">${when}${j.status === 'running' ? ' · ' + agentTimerHtml(j.startedAt) : j.endedAt ? ` · ${fmtAgo(j.endedAt)}` : ''}</span>` +
|
|
3582
3712
|
(j.status === 'running'
|
|
3583
3713
|
? `<button class="ag-btn cancel" title="${_t('ag_cancel')}">${_t('ag_cancel')}</button>`
|
|
@@ -3650,6 +3780,8 @@ function onEvent(ev) {
|
|
|
3650
3780
|
}
|
|
3651
3781
|
/* OFFLINE MESAJ KUYRUĞU: bağlantı + kuyruk olayları (sessionId filtresinden önce) */
|
|
3652
3782
|
if (ev.type === 'net' || ev.type === 'netQueue') { onNetEvent(ev); return; }
|
|
3783
|
+
/* Obscura kurulum ilerlemesi — ayarlardan çıkıp dönülsen de main'de sürer */
|
|
3784
|
+
if (ev.type === 'obscura-progress') { onObscuraProgress(ev); return; }
|
|
3653
3785
|
/* Beast Code oturumu (IDE modu ortasındaki panel): olayları panele akıt,
|
|
3654
3786
|
ana sohbeti kirletme */
|
|
3655
3787
|
if (bcSessionId && ev.sessionId === bcSessionId) {
|
|
@@ -3764,6 +3896,8 @@ function onEvent(ev) {
|
|
|
3764
3896
|
break;
|
|
3765
3897
|
case 'done':
|
|
3766
3898
|
closeChatToolGroup();
|
|
3899
|
+
/* iptal/durdurma sebebini SOHBETE yaz — sebepsiz durdurma yok */
|
|
3900
|
+
if (ev.aborted) addStopNote(ev.reason);
|
|
3767
3901
|
setBusy(false);
|
|
3768
3902
|
setStatus(''); /* son durum balonu ("düşünüyor…" vb.) yapışmasın */
|
|
3769
3903
|
/* cevabın SONU görünsün: markdown son render sonrası iki kez en alta kilitle */
|
|
@@ -4234,6 +4368,7 @@ function autosize() {
|
|
|
4234
4368
|
|
|
4235
4369
|
const SLASH_COMMANDS = [
|
|
4236
4370
|
{ cmd: '/help', desc: 'tüm komutları listele' },
|
|
4371
|
+
{ cmd: '/version', desc: 'Beast Agent sürümünü göster' },
|
|
4237
4372
|
{ cmd: '/new', desc: 'yeni oturum aç (kod verilir)' },
|
|
4238
4373
|
{ cmd: '/open ', desc: 'koddaki oturuma geç' },
|
|
4239
4374
|
{ cmd: '/sessions', desc: 'bu sohbetin oturumları' },
|
|
@@ -6103,7 +6238,8 @@ function bcIngest(ev) {
|
|
|
6103
6238
|
bcFlushStream();
|
|
6104
6239
|
bcCloseToolGroup();
|
|
6105
6240
|
bcSetBusy(false);
|
|
6106
|
-
|
|
6241
|
+
/* iptal/durdurma SEBEBİ panelde de görünür */
|
|
6242
|
+
bcLine(ev.aborted ? 't-err' : 't-dim', ev.aborted ? '[durduruldu — sebep: ' + (ev.reason || 'sebep belirtilmedi') + ']' : '(tamamlandı)');
|
|
6107
6243
|
bcRefreshPreview();
|
|
6108
6244
|
if (ideModeOn() && els.bcInput) els.bcInput.focus();
|
|
6109
6245
|
break;
|
package/src/renderer/style.css
CHANGED
|
@@ -291,6 +291,13 @@ body.browser-open #topbar { width: auto; }
|
|
|
291
291
|
#eyeBtn:not(.on) { opacity: 0.55; }
|
|
292
292
|
#eyeBtn.on { background: var(--accent-dim); color: var(--accent); }
|
|
293
293
|
|
|
294
|
+
/* internet göstergesi (topbar sağ uç): bağlı = YEŞİL wifi, kopuk = KIRMIZI */
|
|
295
|
+
#netDot { cursor: default; }
|
|
296
|
+
#netDot.net-on { color: #22c55e; }
|
|
297
|
+
#netDot.net-off { color: #ef4444; }
|
|
298
|
+
#netDot.net-off .net-slash { display: block; }
|
|
299
|
+
#netDot.net-on .net-slash { display: none; }
|
|
300
|
+
|
|
294
301
|
/* #19 sağ panel (paralel ajan konsolu) aç/kapa */
|
|
295
302
|
#railBtn.on { background: var(--accent-dim); color: var(--accent); }
|
|
296
303
|
body.rail-hidden #rail { display: none; }
|
|
@@ -759,6 +766,34 @@ body.term-open #settingsOverlay { right: var(--tw, 520px); }
|
|
|
759
766
|
padding: 10px 14px;
|
|
760
767
|
}
|
|
761
768
|
|
|
769
|
+
/* durdurma/iptal notu: sebep zorunlu — nötr, sönük ton */
|
|
770
|
+
.msg-sys {
|
|
771
|
+
border: 1px dashed var(--border);
|
|
772
|
+
background: var(--panel);
|
|
773
|
+
color: var(--muted);
|
|
774
|
+
border-radius: 10px;
|
|
775
|
+
padding: 8px 12px;
|
|
776
|
+
font-size: 12.5px;
|
|
777
|
+
word-break: break-word;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
/* Obscura kurulum ilerleme çubuğu (Ayarlar → Web Arama) */
|
|
781
|
+
.oc-prog { margin-top: 10px; }
|
|
782
|
+
.oc-prog-bar {
|
|
783
|
+
height: 9px;
|
|
784
|
+
background: var(--accent-dim);
|
|
785
|
+
border: 1px solid var(--border);
|
|
786
|
+
border-radius: 6px;
|
|
787
|
+
overflow: hidden;
|
|
788
|
+
}
|
|
789
|
+
.oc-prog-fill {
|
|
790
|
+
height: 100%;
|
|
791
|
+
width: 0%;
|
|
792
|
+
background: var(--accent);
|
|
793
|
+
transition: width 0.25s ease;
|
|
794
|
+
}
|
|
795
|
+
.oc-prog-text { margin-top: 5px; font-size: 12px; color: var(--muted); }
|
|
796
|
+
|
|
762
797
|
/* markdown */
|
|
763
798
|
.md p { margin: 0 0 8px; }
|
|
764
799
|
.md p:last-child { margin-bottom: 0; }
|
|
@@ -1773,14 +1808,16 @@ body.browser-open #settingsDialog { width: min(70vw, calc(100vw - var(--bw, 480p
|
|
|
1773
1808
|
flex-shrink: 0;
|
|
1774
1809
|
}
|
|
1775
1810
|
|
|
1776
|
-
/* tek tıkla model (OpenCode Zen) hero butonu */
|
|
1811
|
+
/* tek tıkla model (OpenCode Zen) hero butonu — buton ortada */
|
|
1777
1812
|
.zen-hero {
|
|
1778
1813
|
padding: 10px 12px;
|
|
1779
1814
|
border: 1px dashed var(--border);
|
|
1780
1815
|
border-radius: 10px;
|
|
1781
1816
|
margin-bottom: 12px;
|
|
1782
1817
|
background: var(--panel);
|
|
1818
|
+
text-align: center;
|
|
1783
1819
|
}
|
|
1820
|
+
.zen-hero .sub { text-align: left; }
|
|
1784
1821
|
.zen-hero .btn { min-width: 220px; }
|
|
1785
1822
|
.zen-hero .btn .zen-load {
|
|
1786
1823
|
display: inline-block;
|
package/tests/bg-jobs.test.js
CHANGED
|
@@ -271,18 +271,86 @@ test('#5 eşzamanlılık limiti: fazlası kuyrukta bekler, slot boşalınca FIFO
|
|
|
271
271
|
assert.deepEqual(st, ['queued', 'queued', 'running', 'running']);
|
|
272
272
|
assert.strictEqual(sent.length, 2);
|
|
273
273
|
|
|
274
|
-
/* slot açılır → sıradaki queued iş running olur ve gönderilir
|
|
274
|
+
/* slot açılır → sıradaki queued iş running olur ve gönderilir.
|
|
275
|
+
_bgFinish('aborted') artık üst sohbete [ARKA PLAN İPTAL + SEBEP] raporu da düşürür. */
|
|
275
276
|
eng._bgFinish(ids[0], 'aborted');
|
|
276
277
|
await sleep(30);
|
|
277
278
|
const queued = [...eng._bgJobs.values()].filter((j) => j.status === 'queued').length;
|
|
278
279
|
const running = [...eng._bgJobs.values()].filter((j) => j.status === 'running').length;
|
|
279
280
|
assert.strictEqual(queued, 1);
|
|
280
281
|
assert.strictEqual(running, 2);
|
|
281
|
-
|
|
282
|
+
/* 3 gönderim = kuyruktan başlayan iş + 1 rapor = üst sohbete düşen iptal sebebi */
|
|
283
|
+
assert.strictEqual(sent.length, 4);
|
|
284
|
+
assert.ok(eng._pendingReports.length === 0, 'iptal raporu üst sohbete düştü');
|
|
282
285
|
});
|
|
283
286
|
|
|
284
|
-
/* ----------
|
|
287
|
+
/* ---------- İPTAL SEBEBİ DİSİPLİNİ ---------- */
|
|
288
|
+
|
|
289
|
+
test('iptal SEBEBİ zorunlu: interrupt sebebi iş kaydına ve üst sohbete yazar', async () => {
|
|
285
290
|
const { eng } = tmpEngine();
|
|
291
|
+
const sent = [];
|
|
292
|
+
eng.send = (sid, payload) => { sent.push({ sid, text: String((payload && payload.text) || '') }); return true; };
|
|
293
|
+
const r = eng.runBackground('pr', 'koşan iş', 'Sebep İş');
|
|
294
|
+
const id = r.backgroundId;
|
|
295
|
+
const c = new AbortController();
|
|
296
|
+
eng.ctrls.set(id, c);
|
|
297
|
+
|
|
298
|
+
eng.interrupt(id, 'kullanıcı panelden iptal etti');
|
|
299
|
+
assert.ok(c.signal.aborted, 'ctrl kesildi');
|
|
300
|
+
assert.strictEqual(eng._abortReasons.get(id), 'kullanıcı panelden iptal etti', 'sebep haritada');
|
|
301
|
+
|
|
302
|
+
/* _run catch'inin yaptığı iş: haritadan sebebi okuyup _bgFinish'e verir */
|
|
303
|
+
const why = eng._abortReasons.get(id);
|
|
304
|
+
eng._abortReasons.delete(id);
|
|
305
|
+
eng._bgFinish(id, 'aborted', why);
|
|
306
|
+
|
|
307
|
+
const j = eng._bgJobs.get(id);
|
|
308
|
+
assert.strictEqual(j.status, 'aborted');
|
|
309
|
+
assert.strictEqual(j.error, 'kullanıcı panelden iptal etti', 'sebep iş kaydına yazıldı');
|
|
310
|
+
const rep = sent.find((x) => x.sid === 'pr' && x.text.includes('[ARKA PLAN İPTAL'));
|
|
311
|
+
assert.ok(rep, 'üst sohbete İPTAL raporu düştü');
|
|
312
|
+
assert.ok(/Sebep: kullanıcı panelden/.test(rep.text), 'rapor SEBEPİ içerir');
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test('kuyruktaki ajan interrupt ile SEBEPİYLE kesilir', async () => {
|
|
316
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'beast-qc-'));
|
|
317
|
+
const eng = new Engine({}, { sessionsDir: dir, emit: () => {} });
|
|
318
|
+
eng._bgLimit = 0; // yapıcı min 1 kelepçeler; sıfırla → her iş queued kalır
|
|
319
|
+
eng.send = () => true;
|
|
320
|
+
const r = eng.runBackground('pq2', 'kuyruk işi', 'Kuyruk İş');
|
|
321
|
+
await sleep(80); // admit → bgLimit 0 → queued
|
|
322
|
+
assert.strictEqual(eng._bgJobs.get(r.backgroundId).status, 'queued');
|
|
323
|
+
|
|
324
|
+
assert.strictEqual(eng.interrupt(r.backgroundId, 'sıra iptal edildi'), true);
|
|
325
|
+
const j = eng._bgJobs.get(r.backgroundId);
|
|
326
|
+
assert.strictEqual(j.status, 'aborted');
|
|
327
|
+
assert.strictEqual(j.error, 'sıra iptal edildi', 'kuyruktaki işin iptal sebebi kayda geçti');
|
|
328
|
+
eng.deleteSession(r.backgroundId);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
test('task_cancel reason ZORUNLU: sebep iş kaydına ve note mesajına düşer', async () => {
|
|
332
|
+
const { eng } = tmpEngine();
|
|
333
|
+
const r = eng.runBackground('pc', 'iş', 'İptal Edilen');
|
|
334
|
+
const id = r.backgroundId;
|
|
335
|
+
const c = new AbortController();
|
|
336
|
+
eng.ctrls.set(id, c);
|
|
337
|
+
|
|
338
|
+
const out = JSON.parse(await eng._execTool('task_cancel', { id, reason: 'yanlış kaynağa baktı' }, {}));
|
|
339
|
+
assert.ok(out.ok, 'cancel başarılı');
|
|
340
|
+
assert.ok(/yanlış kaynağa baktı/.test(out.note), 'note ceza sebep içerir');
|
|
341
|
+
assert.strictEqual(eng._abortReasons.get(id), 'yanlış kaynağa baktı');
|
|
342
|
+
assert.ok(c.signal.aborted);
|
|
343
|
+
|
|
344
|
+
/* reason verilmezse varsayılan sebep yine kayda düşer */
|
|
345
|
+
const id2 = eng.runBackground('pc', 'iş 2', 'İptal 2').backgroundId;
|
|
346
|
+
eng.ctrls.set(id2, new AbortController());
|
|
347
|
+
const out2 = JSON.parse(await eng._execTool('task_cancel', { id: id2 }, {}));
|
|
348
|
+
assert.ok(out2.ok);
|
|
349
|
+
assert.ok(eng._abortReasons.get(id2).includes('task_cancel'), 'varsayılan sebep CEO kaynağını söyler');
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
/* ---------- /stop anahtarı ---------- */
|
|
353
|
+
test('stopAll: koşan turları keser, paralel ajanı aborted işaretler', async () => { const { eng } = tmpEngine();
|
|
286
354
|
const r = eng.runBackground('pS', 'koşan iş', 'Koşu');
|
|
287
355
|
const id = r.backgroundId;
|
|
288
356
|
|
|
@@ -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
|
+
});
|