beast-agent 2.8.2 → 2.9.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.8.2",
4
+ "version": "2.9.0",
5
5
  "description": "Ultra-fast local agent shell for Windows.",
6
6
  "author": "algokodcom (AlgoKod)",
7
7
  "license": "MIT",
@@ -334,6 +334,9 @@ class Engine {
334
334
  this._bgGroups = new Map(); // groupId -> {parentId,total,results,dead}
335
335
  /* silinen oturum işaretleri: rapor/zincir buraya gönderilmez (hayalet oturum yok) */
336
336
  this._deletedSessions = new Set();
337
+ /* OPENCODE STEER: koşan tur sırasında gelen mesajlar — güvenli floş
338
+ noktasına kadar burada bekler (sid -> msg[]) */
339
+ this._steerBuf = new Map();
337
340
  /* #17 kalıcı ajan geçmişi: restart sonrası işler + sohbetler kaybolmasın */
338
341
  this._bgJobsFile = path.join(this.sessionsDir, 'bg-jobs.json');
339
342
  this._loadBgJobsPersisted();
@@ -1405,6 +1408,7 @@ class Engine {
1405
1408
  if (cached && cached.code) this._codeIndex.delete(cached.code);
1406
1409
  this.cache.delete(String(id));
1407
1410
  this.todos.delete(String(id));
1411
+ this._steerBuf && this._steerBuf.delete(String(id));
1408
1412
  this.sessionPerm.delete(String(id));
1409
1413
  this.sessionTools.delete(String(id));
1410
1414
  this._errCount && this._errCount.delete(String(id));
@@ -2009,7 +2013,7 @@ class Engine {
2009
2013
  const small = this.modelFor('subagent') || sel || this.sel;
2010
2014
  const res = await chatOnce(
2011
2015
  small,
2012
- { messages: [{ role: 'user', content: prompt }], temperature: 0.2 },
2016
+ { messages: [{ role: 'user', content: prompt }], temperature: 0.2, cacheKey: String(session.id || '') },
2013
2017
  { signal }
2014
2018
  );
2015
2019
  const text = String(res.content || '').replace(/```(?:markdown)?/gi, '').trim();
@@ -2172,7 +2176,7 @@ class Engine {
2172
2176
  `# SOHBET PARÇASI\n${transcript}`;
2173
2177
  const res = await chatOnce(
2174
2178
  this.sel,
2175
- { messages: [{ role: 'user', content: prompt }], temperature: 0.2 },
2179
+ { messages: [{ role: 'user', content: prompt }], temperature: 0.2, cacheKey: String(session.id || '') },
2176
2180
  { signal }
2177
2181
  );
2178
2182
  const txt = String(res.content || '').trim();
@@ -2231,7 +2235,7 @@ class Engine {
2231
2235
  if (this._stopped && !userAction) return false;
2232
2236
  if (userAction) this._stopped = false;
2233
2237
  const s = this._load(String(sessionId));
2234
- if (!s || this.ctrls.has(s.id)) return false;
2238
+ if (!s) return false;
2235
2239
 
2236
2240
  let msg;
2237
2241
  if (typeof payload === 'string') {
@@ -2262,6 +2266,11 @@ class Engine {
2262
2266
  ...imgs.map((im) => ({ type: 'image', name: String(im.name || 'resim') })),
2263
2267
  ...files,
2264
2268
  ];
2269
+ /* MEDYA KAYDI: WA/Chat UI'dan gelen fotoğraflar ve belgeler OTURUMUN
2270
+ media klasörüne yazılır (orijinal dosya güvende). msg.media jsonl'e
2271
+ düşer; prune/compaction görselleri metne indirse bile kayıp yok. */
2272
+ const media = this._saveMedia(s.id, imgs, files);
2273
+ if (media) msg.media = media;
2265
2274
  }
2266
2275
  }
2267
2276
 
@@ -2274,6 +2283,35 @@ class Engine {
2274
2283
  /* Önceki tur yanıtlanmadan kaldıysa (hata/durdurma sonrası artık user mesajı)
2275
2284
  katı sağlayıcılar art arda user mesajını reddeder: "messages illegal" (400).
2276
2285
  Yeni metni o mesajla BİRLEŞTİR — dosyadaki son satır da güncellenir. */
2286
+ /* OPENCODE STEER PORTU (runtime.queue.ts + prompt.ts runLoop): oturum
2287
+ KOŞARKEN gelen mesaj hemen geçmişe yazılmaz — _steerBuf'ta bekler,
2288
+ koşan tur GÜVENLİ floş noktasında (tur başı / çıkış kontrolü) ekler.
2289
+ Böylece araç sonuçlarının arasına sıkışmaz, isteğin yarısındaki mesaj
2290
+ mutasyona uğramaz. UI'ya yine de ANINDA düşer. */
2291
+ if (this.ctrls.has(s.id)) {
2292
+ let buf = this._steerBuf.get(s.id);
2293
+ if (!buf) {
2294
+ buf = [];
2295
+ this._steerBuf.set(s.id, buf);
2296
+ }
2297
+ const lastBuf = buf[buf.length - 1];
2298
+ if (
2299
+ lastBuf && lastBuf.role === 'user' &&
2300
+ !Array.isArray(lastBuf.content) && !Array.isArray(msg.content) &&
2301
+ typeof msg.content === 'string'
2302
+ ) {
2303
+ /* ardışık steer'ler tek mesajda birleşir (art arda user reddi olmasın) */
2304
+ lastBuf.content = lastBuf.content + '\n' + msg.content;
2305
+ if (msg.attachments || lastBuf.attachments) {
2306
+ lastBuf.attachments = [...(lastBuf.attachments || []), ...(msg.attachments || [])];
2307
+ }
2308
+ } else {
2309
+ buf.push(msg);
2310
+ }
2311
+ this.emit({ type: 'message', sessionId: s.id, message: msg });
2312
+ this.emit({ type: 'sessions' });
2313
+ return true;
2314
+ }
2277
2315
  const lastMsg = s.messages[s.messages.length - 1];
2278
2316
  if (
2279
2317
  lastMsg && lastMsg.role === 'user' &&
@@ -2298,6 +2336,57 @@ class Engine {
2298
2336
  return true;
2299
2337
  }
2300
2338
 
2339
+ /* Steer tamponunu güvenli noktada geçmişe akıtır. Dönüş: bir şey akıtıldı mı. */
2340
+ _flushSteer(sid, session, emit) {
2341
+ const buf = this._steerBuf.get(sid);
2342
+ if (!buf || !buf.length) return false;
2343
+ this._steerBuf.delete(sid);
2344
+ for (const m of buf) {
2345
+ session.messages.push(m);
2346
+ try {
2347
+ this._append(session, m);
2348
+ } catch {}
2349
+ }
2350
+ if (emit) emit({ type: 'sessions' });
2351
+ return true;
2352
+ }
2353
+
2354
+ /* MEDYA KAYDI: mesaj eklerindeki fotoğraflar (dataUrl) ve metin belgeleri
2355
+ oturumun media klasörüne yazılır: <sessionsDir>/media/<sid>/zaman-metin.
2356
+ Dönen liste msg.media olarak jsonl'e yazılır — orijinal dosya hiç
2357
+ kaybolmaz (eski görsellerin metne indirilmesinden bağımsız). */
2358
+ _saveMedia(sid, imgs, files) {
2359
+ const out = [];
2360
+ try {
2361
+ const dir = path.join(this.sessionsDir, 'media', String(sid || ''));
2362
+ fs.mkdirSync(dir, { recursive: true });
2363
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
2364
+ const safe = (n, fb) => String(n || fb).replace(/[\\/:*?"<>|]/g, '_').slice(0, 80);
2365
+ const stripExt = (n) => String(n || '').replace(/\.[a-z0-9]{1,5}$/i, '');
2366
+ let i = 0;
2367
+ for (const im of imgs) {
2368
+ const m = /^data:([^;,]+)?(;base64)?,(.*)$/.exec(String(im.dataUrl || ''));
2369
+ if (!m) continue;
2370
+ try {
2371
+ const mime = String(m[1] || 'image/png');
2372
+ const ext = (mime.split('/')[1] || 'png').replace(/[^a-z0-9]/gi, '').slice(0, 5) || 'png';
2373
+ const buf = m[2] ? Buffer.from(m[3], 'base64') : Buffer.from(decodeURIComponent(m[3]), 'utf8');
2374
+ const file = stamp + '-' + ++i + '-' + safe(stripExt(im.name), 'resim') + '.' + ext;
2375
+ fs.writeFileSync(path.join(dir, file), buf);
2376
+ out.push({ file, name: String(im.name || 'resim'), kind: 'image' });
2377
+ } catch {}
2378
+ }
2379
+ for (const f of files) {
2380
+ try {
2381
+ const file = stamp + '-' + ++i + '-' + safe(stripExt(f.name), 'dosya') + '.txt';
2382
+ fs.writeFileSync(path.join(dir, file), String(f.content || ''), 'utf8');
2383
+ out.push({ file, name: String(f.name || 'dosya'), kind: 'file' });
2384
+ } catch {}
2385
+ }
2386
+ } catch {}
2387
+ return out.length ? out : null;
2388
+ }
2389
+
2301
2390
  /* SESSİZ BAĞLAM ENJEKSİYONU: metin oturum geçmişine düşer ama _run()
2302
2391
  TETİKLENMEZ — ajan "görür", cevap ÜRETMEZ. WhatsApp gruplarında
2303
2392
  @mention moduyla kullanılır: grubun tüm konuşması bağlam olarak
@@ -3249,6 +3338,10 @@ class Engine {
3249
3338
  e.name = 'AbortError';
3250
3339
  throw e;
3251
3340
  }
3341
+ /* STEER FLOŞU (tur başı): koşan tur sırasında gelen mesajlar burada
3342
+ geçmişe eklenir — araç sonuçlarının arasına sıkışmaz, sonraki
3343
+ _chatTurn isteğinde model görür */
3344
+ this._flushSteer(sid, session, emit);
3252
3345
  /* SÜRE SINIRI YOK. Ana oturum sınırsız tur koşar (opencode birebir);
3253
3346
  yalnız bg ajan son 3 tura gelirken "raporu yaz ve bitir" uyarısı alır. */
3254
3347
  if (session.bgJob && turn === maxTurns - 3 && !wrapNudged) {
@@ -3278,6 +3371,9 @@ class Engine {
3278
3371
  }
3279
3372
  this._bgTrim(session); // #21 bg geçmişini olabildiğince ince tut
3280
3373
  emit({ type: 'status', status: 'thinking' });
3374
+ /* opencode runLoop portu: istek kurulmadan ÖNCEKİ mesaj sayısı —
3375
+ koşan tur SIRASINDA gelen (steer) user mesajlarını saptar */
3376
+ const preLen = session.messages.length;
3281
3377
  const res = await this._chatTurn(session, ctrl.signal, (delta) =>
3282
3378
  emit({ type: 'token', delta })
3283
3379
  );
@@ -3298,6 +3394,16 @@ class Engine {
3298
3394
  emit({ type: 'message', message: assistant });
3299
3395
 
3300
3396
  if (!res.toolCalls || !res.toolCalls.length) {
3397
+ /* opencode runLoop portu (prompt.ts:1111-1115): koşan tur SIRASINDA
3398
+ yeni user mesajı geldiyse (steer) tur BİTMEZ — tampondaki mesaj
3399
+ geçmişe akıtılır, model sonraki istekte görür ve cevaplar;
3400
+ eski cevap akışı bozulmaz. */
3401
+ if (
3402
+ session.messages.slice(preLen).some((m) => m.role === 'user') ||
3403
+ this._flushSteer(sid, session, emit)
3404
+ ) {
3405
+ continue;
3406
+ }
3301
3407
  /* GÖREV LİSTESİ DİSİPLİNİ: ajan işi bitti sanıyor ama listede hâlâ
3302
3408
  bekleyen/aktif madde varsa BİR KEZ hatırlat ve tura devam et —
3303
3409
  liste ya tamamlanmalı ya kalan maddeler listeden düşürülmeli. */
@@ -3458,6 +3564,10 @@ class Engine {
3458
3564
  this._bgFinish(sid, 'error', 'bg görevi tur limitine ulaştı (maxTurns)');
3459
3565
  } catch (e) {
3460
3566
  const aborted = e && (e.name === 'AbortError' || ctrl.signal.aborted);
3567
+ /* STEER FLOŞU (hata/durdurma): tur sırasında gelen mesajlar yine de
3568
+ geçmişe yazılsın — konuşma bütünlüğü korunur, sonraki tur bağlamı
3569
+ görür (opencode interrupted-part disiplini) */
3570
+ this._flushSteer(sid, session, emit);
3461
3571
  if (aborted) {
3462
3572
  this._clearCrash(); // kullanıcı durdurdu — kurtarma yok
3463
3573
  /* öz-kurtarma/superyorizon kick'i bekliyorsa bu tur BİTİŞ değildir —
@@ -3571,7 +3681,7 @@ class Engine {
3571
3681
  const llm = async (sys, user) => {
3572
3682
  const res = await chatOnce(
3573
3683
  this.sel,
3574
- { messages: [{ role: 'user', content: sys + '\n\n' + user }], temperature: 0.1 },
3684
+ { messages: [{ role: 'user', content: sys + '\n\n' + user }], temperature: 0.1, cacheKey: String(session.id || '') },
3575
3685
  { signal }
3576
3686
  );
3577
3687
  return String(res.content || '');
@@ -3607,7 +3717,7 @@ class Engine {
3607
3717
 
3608
3718
  const res = await chatOnce(
3609
3719
  this.sel,
3610
- { messages: [{ role: 'user', content: prompt }], temperature: 0.1 },
3720
+ { messages: [{ role: 'user', content: prompt }], temperature: 0.1, cacheKey: String(session.id || '') },
3611
3721
  { signal }
3612
3722
  );
3613
3723
  let parsed = null;
@@ -3661,7 +3771,7 @@ const skills = require('./skills');
3661
3771
 
3662
3772
  const res = await chatOnce(
3663
3773
  this.sel,
3664
- { messages: [{ role: 'user', content: prompt }], temperature: 0.2 },
3774
+ { messages: [{ role: 'user', content: prompt }], temperature: 0.2, cacheKey: String(session.id || '') },
3665
3775
  {}
3666
3776
  );
3667
3777
  const draft = parseReflectionJson(res.content || '');
@@ -3745,7 +3855,7 @@ const skills = require('./skills');
3745
3855
  try {
3746
3856
  const res = await chatOnce(
3747
3857
  this.sel,
3748
- { messages: [{ role: 'user', content: prompt }], temperature: 0.2 },
3858
+ { messages: [{ role: 'user', content: prompt }], temperature: 0.2, cacheKey: 'nightref' },
3749
3859
  { signal: ctrl.signal }
3750
3860
  );
3751
3861
  return String(res.content || '');
package/src/main.js CHANGED
@@ -642,7 +642,7 @@ async function transcribeCloud(provider, buf, mimetype, lang) {
642
642
  if (lang === 'tr' || lang === 'en') form.append('language', lang);
643
643
  const res = await fetch(base + '/audio/transcriptions', {
644
644
  method: 'POST',
645
- headers: { Authorization: 'Bearer ' + key },
645
+ headers: { Authorization: 'Bearer ' + key, 'x-opencode-session': OPENCODE_SESSION },
646
646
  body: form,
647
647
  });
648
648
  if (!res.ok) throw new Error(provider + ' ' + res.status + ': ' + String(await res.text()).slice(0, 180));
@@ -954,7 +954,11 @@ async function synthesizeSpeech(text) {
954
954
  const url = String(cfg.baseUrl).replace(/\/+$/, '') + '/audio/speech';
955
955
  const res = await fetch(url, {
956
956
  method: 'POST',
957
- headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + cfg.key },
957
+ headers: {
958
+ 'Content-Type': 'application/json',
959
+ Authorization: 'Bearer ' + cfg.key,
960
+ 'x-opencode-session': OPENCODE_SESSION,
961
+ },
958
962
  body: JSON.stringify({
959
963
  model: cfg.model || 'tts-1',
960
964
  input: String(text).slice(0, 4000),
@@ -1726,6 +1730,23 @@ function sessionFileAlive(sid) {
1726
1730
 
1727
1731
  /* kanalın yaşayan tek oturumunu döndürür; geçersizse eski bağlamdan devralır,
1728
1732
  o da yoksa (ilk kullanım) yeni açar ve tüm jid'leri ona bağlar */
1733
+ /* WA'dan gelen belge orijinallerini oturumun media klasörüne yazar;
1734
+ döner: media/<sid>/<dosya> (relatif yol — ajan read_file ile açabilir) */
1735
+ function saveSessionMedia(sid, name, buf) {
1736
+ try {
1737
+ if (!buf || !buf.length) return null;
1738
+ const dir = path.join(engine.sessionsDir, 'media', String(sid || ''));
1739
+ fs.mkdirSync(dir, { recursive: true });
1740
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
1741
+ const safe = String(name || 'dosya').replace(/[\\/:*?"<>|]/g, '_').slice(0, 120);
1742
+ const file = stamp + '-' + safe;
1743
+ fs.writeFileSync(path.join(dir, file), buf);
1744
+ return 'media/' + String(sid || '') + '/' + file;
1745
+ } catch {
1746
+ return null;
1747
+ }
1748
+ }
1749
+
1729
1750
  function ensureWaSession(jid) {
1730
1751
  if (!waSingleSid || !sessionFileAlive(waSingleSid)) {
1731
1752
  let best = '';
@@ -2429,12 +2450,15 @@ async function processWaMessage(jid, payload, senderNum, requeues = 0) {
2429
2450
  if (inst && inst.error) {
2430
2451
  text += `\n[SKILL.md alındı ama kaydedilemedi]`;
2431
2452
  } else {
2453
+ /* BELGE KAYDI: orijinal dosya oturum media klasörüne yazılır —
2454
+ model yalnız metnini görür, orijinale ajan read_file ile ulaşabilir */
2455
+ const savedDoc = saveSessionMedia(sid, md.name, md.buf);
2432
2456
  const dt = documentToText(md);
2433
2457
  if (dt && dt.trim()) {
2434
2458
  attachments.push({ type: 'file', name: md.name, content: dt.slice(0, 20000) });
2435
- text += `\n[belge alındı: ${md.name}]`;
2459
+ text += `\n[belge alındı: ${md.name}${savedDoc ? ' · kaydedildi: ' + savedDoc : ''}]`;
2436
2460
  } else {
2437
- text += `\n[belge alındı ama okunamadı: ${md.name}]`;
2461
+ text += `\n[belge alındı ama okunamadı: ${md.name}${savedDoc ? ' · kaydedildi: ' + savedDoc : ''}]`;
2438
2462
  }
2439
2463
  }
2440
2464
  }
@@ -5149,6 +5173,14 @@ function queueDesktopMessage(sessionId, text) {
5149
5173
  chatQueueOfflineAdd(sid, { text: t, attachments: hasAtts ? text.attachments : undefined });
5150
5174
  return;
5151
5175
  }
5176
+ /* OPENCODE STEER: koşan tur varken mesaj ANINDA konuşmaya eklenir —
5177
+ ajan sonraki istekte görür, eski cevap akışı bozulmaz. Kuyrukta beklemesine
5178
+ gerek yok; boşta debounce penceresi yine birleştirir. */
5179
+ if (engine.isBusy(sid)) {
5180
+ waLog(`desktop: oturum meşgul — mesaj steer olarak konuşmaya eklendi sid=${sid}`);
5181
+ engine.send(sid, hasAtts ? { text: t, attachments: text.attachments } : t, { userAction: true });
5182
+ return;
5183
+ }
5152
5184
  let q = desktopQueue.get(sid);
5153
5185
  if (!q) {
5154
5186
  q = { timer: null, msgs: [] };
@@ -5156,16 +5188,9 @@ function queueDesktopMessage(sessionId, text) {
5156
5188
  }
5157
5189
  q.msgs.push({ text: t, attachments: hasAtts ? text.attachments : undefined });
5158
5190
 
5159
- const busy = engine.isBusy(sid);
5160
- if (!busy) {
5161
- /* agent boşta: kısa pencere — hızlı ikinci mesaj ilkine eklenir */
5162
- clearTimeout(q.timer);
5163
- q.timer = setTimeout(() => flushDesktop(sid).catch(() => {}), DESKTOP_DEBOUNCE_MS);
5164
- } else {
5165
- /* agent çalışıyor: pencere yok, iş bitince hepsi birlikte gider.
5166
- Bekleyen paket olduğunda 'done' olayı flushDesktop'u çağırır. */
5167
- waLog(`desktop: oturum meşgul, mesaj beklemede (${q.msgs.length}) sid=${sid}`);
5168
- }
5191
+ /* agent boşta: kısa pencere — hızlı ikinci mesaj ilkine eklenir */
5192
+ clearTimeout(q.timer);
5193
+ q.timer = setTimeout(() => flushDesktop(sid).catch(() => {}), DESKTOP_DEBOUNCE_MS);
5169
5194
  }
5170
5195
 
5171
5196
  async function flushDesktop(sessionId) {
@@ -6205,19 +6230,27 @@ function cronEmit() {
6205
6230
  const cronAnswerPending = new Map();
6206
6231
 
6207
6232
  /* OTOMATİK YENİ SOHBET YOK: cron/izleyici/fallout tetiklendiğinde oturum
6208
- seçimi — kayıtlı id geçerliyse O, değilse EN GÜNCEL oturum kullanılır.
6209
- Hiç oturum yoksa (ilk kurulum) yeni açılır. Böylece soldaki sohbet
6210
- geçmişine "+ Yeni Sohbet" olmadan hayalet sohbetler düşmez. */
6233
+ seçimi — kayıtlı id'nin DOSYASI hâlâ duruyorsa O, değilse EN GÜNCEL
6234
+ (meşgul olmayan) oturum kullanılır; hiç oturum yoksa (ilk kurulum) yeni
6235
+ açılır. Böylece soldaki sohbet geçmişine "+ Yeni Sohbet" olmadan hayalet
6236
+ sohbetler düşmez.
6237
+ DİKKAT: _load(id) silinmiş oturum için bile boş bir hayalet nesne
6238
+ üretip truthy döner — canlılık kontrolü MUTLAKA sessionFileAlive ile
6239
+ yapılır (ensureWa/Tg/DcSession ile aynı kural). Ayrıca meşgul oturuma
6240
+ send() mesajı DÜŞÜRÜR (false döner, cron cevabı kaybolur) — o yüzden
6241
+ meşgul oturumlar da atlanır. */
6211
6242
  function reuseOrLatestSession(preferredId) {
6212
6243
  const sid = String(preferredId || '');
6213
- if (sid) {
6244
+ if (sid && sessionFileAlive(sid)) {
6214
6245
  try {
6215
- if (engine._load(sid)) return sid;
6246
+ if (engine._load(sid) && !engine.isBusy(sid)) return sid;
6216
6247
  } catch {}
6217
6248
  }
6218
6249
  try {
6219
6250
  const list = engine.listSessions(); // updatedAt'e göre yeni→eski sıralı
6220
- if (list.length) return String(list[0].id);
6251
+ for (const v of list) {
6252
+ if (!engine.isBusy(v.id)) return String(v.id);
6253
+ }
6221
6254
  } catch {}
6222
6255
  return engine.createSession().id;
6223
6256
  }
@@ -5402,9 +5402,25 @@ function opText(w) {
5402
5402
  return `${sym} ${w.value ?? '?'}`;
5403
5403
  }
5404
5404
 
5405
- async function renderWatchersModal() {
5405
+ /* kısa saat: bugünse "14:32", başka günse "07.09 14:32" */
5406
+ function fmtWhen(iso) {
5407
+ if (!iso) return '';
5408
+ const d = new Date(iso);
5409
+ if (isNaN(d.getTime())) return '';
5410
+ const t = d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
5411
+ if (d.toDateString() === new Date().toDateString()) return t;
5412
+ return d.toLocaleDateString([], { day: '2-digit', month: '2-digit' }) + ' ' + t;
5413
+ }
5414
+
5415
+ let lastWatchKey = '';
5416
+ async function renderWatchersModal(force) {
5406
5417
  let rows = [];
5407
5418
  try { rows = (await beast.watchersList()) || []; } catch {}
5419
+ /* canlı yenilemede içerik değişmediyse DOM'a dokunma — buton tıklamaları
5420
+ yeniden kurulurken yutulmasın */
5421
+ const key = JSON.stringify(rows);
5422
+ if (!force && key === lastWatchKey) return;
5423
+ lastWatchKey = key;
5408
5424
  els.watchList.innerHTML = '';
5409
5425
  if (!rows.length) {
5410
5426
  els.watchList.innerHTML = '<div class="mini-empty">' + _t('w_empty') + '</div>';
@@ -5443,9 +5459,13 @@ async function renderWatchersModal() {
5443
5459
  }
5444
5460
  }
5445
5461
 
5446
- async function renderCronModal() {
5462
+ let lastCronKey = '';
5463
+ async function renderCronModal(force) {
5447
5464
  let jobs = [];
5448
5465
  try { jobs = (await beast.cronList()) || []; } catch {}
5466
+ const key = JSON.stringify(jobs);
5467
+ if (!force && key === lastCronKey) return;
5468
+ lastCronKey = key;
5449
5469
  els.cronModalList.innerHTML = '';
5450
5470
  if (!jobs.length) {
5451
5471
  els.cronModalList.innerHTML = '<div class="mini-empty">' + _t('cr_modal_empty') + '</div>';
@@ -5454,11 +5474,15 @@ async function renderCronModal() {
5454
5474
  for (const j of jobs) {
5455
5475
  const row = document.createElement('div');
5456
5476
  row.className = 'mini-row' + (j.enabled ? '' : ' off');
5477
+ /* ANLIK DURUM: sıradaki tetiklenme + son çalışma (varsa) */
5478
+ const live =
5479
+ (j.enabled && j.nextRunAt ? ` · sonraki <b>${escapeHtml(fmtWhen(j.nextRunAt))}</b>` : '') +
5480
+ (j.lastRunAt ? ` · son ${escapeHtml(fmtWhen(j.lastRunAt))}` : '');
5457
5481
  row.innerHTML =
5458
5482
  `<span class="mr-dot"></span>` +
5459
5483
  `<div class="mr-main">` +
5460
5484
  `<div class="mr-name">${escapeHtml(j.name || j.id)}</div>` +
5461
- `<div class="mr-meta"><b>${escapeHtml(j.schedule)}</b> · ${escapeHtml(String(j.prompt || '').slice(0, 70))}</div>` +
5485
+ `<div class="mr-meta"><b>${escapeHtml(j.schedule)}</b> · ${escapeHtml(String(j.prompt || '').slice(0, 70))}${live}</div>` +
5462
5486
  `</div>`;
5463
5487
  const btns = document.createElement('div');
5464
5488
  btns.style.cssText = 'display:flex;gap:6px;flex:none';
@@ -5488,14 +5512,41 @@ function toggleMini(overlay, hide) {
5488
5512
  overlay.hidden = !!hide;
5489
5513
  }
5490
5514
 
5515
+ /* CANLI YENİLEME: mini modal açık kaldığı sürece periyodik taze çekim yapar;
5516
+ render fonksiyonları içerik değişmemişse DOM'u yeniden kurmaz. Kapalıysa
5517
+ zamanlayıcı kendini durdurur. */
5518
+ let miniRefreshTimer = null;
5519
+ function startMiniRefresh() {
5520
+ stopMiniRefresh();
5521
+ miniRefreshTimer = setInterval(async () => {
5522
+ if (els.watchOverlay && !els.watchOverlay.hidden) await renderWatchersModal();
5523
+ else if (els.cronOverlay && !els.cronOverlay.hidden) await renderCronModal();
5524
+ else stopMiniRefresh();
5525
+ }, 4000);
5526
+ }
5527
+ function stopMiniRefresh() {
5528
+ if (miniRefreshTimer) { clearInterval(miniRefreshTimer); miniRefreshTimer = null; }
5529
+ }
5530
+
5531
+ function closeWatchModal() {
5532
+ toggleMini(els.watchOverlay, true);
5533
+ stopMiniRefresh();
5534
+ }
5535
+ function closeCronModal() {
5536
+ toggleMini(els.cronOverlay, true);
5537
+ stopMiniRefresh();
5538
+ }
5539
+
5491
5540
  async function openWatchModal() {
5492
5541
  toggleMini(els.watchOverlay, false);
5493
- await renderWatchersModal();
5542
+ await renderWatchersModal(true); /* her basışta TAZE çekim */
5543
+ startMiniRefresh();
5494
5544
  }
5495
5545
 
5496
5546
  async function openCronModal() {
5497
5547
  toggleMini(els.cronOverlay, false);
5498
- await renderCronModal();
5548
+ await renderCronModal(true); /* her basışta TAZE çekim */
5549
+ startMiniRefresh();
5499
5550
  }
5500
5551
 
5501
5552
  /* ---------------- state / controls ---------------- */
@@ -5671,8 +5722,9 @@ async function sendCurrent() {
5671
5722
  if (!busy) {
5672
5723
  setBusy(true);
5673
5724
  } else {
5674
- /* agent çalışıyor — mesaj kuyruğa alınır, iş bitince otomatik gönderilir */
5675
- setStatus('kuyruğa eklendi — iş bitince gider');
5725
+ /* agent çalışıyor — mesaj ANINDA konuşmaya eklenir (opencode steer):
5726
+ koşan tur sonraki istekte görür, eski cevap akışı bozulmaz */
5727
+ setStatus('mesaj ajanın akışına eklendi — sıradaki adımda değerlendirir');
5676
5728
  }
5677
5729
  beast.send(activeId, { text, attachments: atts });
5678
5730
  }
@@ -6203,13 +6255,13 @@ async function init() {
6203
6255
  /* cron modülü — olay bağlama */
6204
6256
  if (els.watchBtn) els.watchBtn.addEventListener('click', openWatchModal);
6205
6257
  if (els.cronBtn) els.cronBtn.addEventListener('click', openCronModal);
6206
- if (els.watchClose) els.watchClose.addEventListener('click', () => toggleMini(els.watchOverlay, true));
6207
- if (els.cronClose) els.cronClose.addEventListener('click', () => toggleMini(els.cronOverlay, true));
6258
+ if (els.watchClose) els.watchClose.addEventListener('click', closeWatchModal);
6259
+ if (els.cronClose) els.cronClose.addEventListener('click', closeCronModal);
6208
6260
  if (els.watchOverlay) els.watchOverlay.addEventListener('click', (e) => {
6209
- if (e.target === els.watchOverlay) toggleMini(els.watchOverlay, true);
6261
+ if (e.target === els.watchOverlay) closeWatchModal();
6210
6262
  });
6211
6263
  if (els.cronOverlay) els.cronOverlay.addEventListener('click', (e) => {
6212
- if (e.target === els.cronOverlay) toggleMini(els.cronOverlay, true);
6264
+ if (e.target === els.cronOverlay) closeCronModal();
6213
6265
  });
6214
6266
  els.cronPreset.addEventListener('change', () => {
6215
6267
  if (els.cronPreset.value !== '__custom') els.cronSchedule.value = els.cronPreset.value;
@@ -6294,8 +6346,8 @@ async function init() {
6294
6346
  document.addEventListener('keydown', (e) => {
6295
6347
  if (e.key === 'Escape') {
6296
6348
  if ($('#botOverlay') && !$('#botOverlay').hidden) { botOverlaySetOpen(false); return; }
6297
- if (els.watchOverlay && !els.watchOverlay.hidden) { toggleMini(els.watchOverlay, true); return; }
6298
- if (els.cronOverlay && !els.cronOverlay.hidden) { toggleMini(els.cronOverlay, true); return; }
6349
+ if (els.watchOverlay && !els.watchOverlay.hidden) { closeWatchModal(); return; }
6350
+ if (els.cronOverlay && !els.cronOverlay.hidden) { closeCronModal(); return; }
6299
6351
  if (!els.settingsOverlay.hidden) closeSettings();
6300
6352
  }
6301
6353
  // Ctrl+Shift+S: ekran görüntüsü al → vision'a gönderilmek üzere ekle