beast-agent 2.12.0 → 2.13.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.12.0",
4
+ "version": "2.13.0",
5
5
  "description": "Ultra-fast local agent shell for Windows.",
6
6
  "author": "algokodcom (AlgoKod)",
7
7
  "license": "MIT",
@@ -3529,6 +3529,16 @@ class Engine {
3529
3529
  else if (/\b(npm (run )?(dev|start)|npx serve|live-server)\b/i.test(cmdStr)) port = 3000;
3530
3530
  if (port) bcArtifacts.serverUrl = 'http://localhost:' + port;
3531
3531
  }
3532
+ /* Expo dev server: exp://<LAN-IP>:<port> çıktıdan yakalanır —
3533
+ telefon/Expo Go için QR renderer'a canlı iletilir */
3534
+ const xm = /exp:\/\/[^\s'"<>]+/i.exec(hay);
3535
+ if (xm) {
3536
+ const eurl = xm[0].replace(/[)\].,;:'"]+$/, '');
3537
+ if (bcArtifacts.expUrl !== eurl) {
3538
+ bcArtifacts.expUrl = eurl;
3539
+ emit({ type: 'bc-expurl', url: eurl });
3540
+ }
3541
+ }
3532
3542
  }
3533
3543
  } catch {}
3534
3544
  }
@@ -0,0 +1,279 @@
1
+ 'use strict';
2
+
3
+ /* ---------- BEAST × HEADROOM (OPSİYONEL token sıkıştırma) ----------
4
+ Ayarlardan (Kurulum sekmesi) açılıp kapanır; kapalıyken hiçbir şey yapmaz.
5
+ Açıkken: LLM istekleri yerel headroom proxy'sinden geçer — araç çıktıları,
6
+ loglar ve JSON yükleri modele gitmeden sıkıştırılır (testte %39-71 tasarruf).
7
+ Kurulum: uv tool install "headroom-ai[proxy]" (uv yoksa hata bildirir).
8
+ Her provider için AYRI proxy portu (base URL farkları için); llm.js
9
+ providerId → port haritasıyla yönlendirir, kapatınca düz bağlanır. */
10
+
11
+ const { spawn } = require('child_process');
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const os = require('os');
15
+ const { emitInstallProgress } = require('./progressbus');
16
+
17
+ const PORT_BASE = 18787;
18
+ const MAX_PROXIES = 8;
19
+
20
+ const state = {
21
+ enabled: false,
22
+ installing: false,
23
+ installFailed: false,
24
+ installError: '',
25
+ starting: false,
26
+ proxies: new Map(), // providerId -> { proc, port, baseUrl, ready }
27
+ providerGetter: null, // () => [{ id, name, baseUrl }]
28
+ statusSink: null, // (probe) => void — main renderer'a akıtır
29
+ };
30
+
31
+ function _emit() {
32
+ try { if (state.statusSink) state.statusSink(probe()); } catch {}
33
+ }
34
+
35
+ function headroomExe() {
36
+ const local = path.join(os.homedir(), '.local', 'bin', 'headroom.exe');
37
+ if (fs.existsSync(local)) return local;
38
+ try {
39
+ require('child_process').execSync('where headroom', { timeout: 5000, stdio: 'pipe' });
40
+ return 'headroom';
41
+ } catch {}
42
+ return '';
43
+ }
44
+
45
+ function uvExe() {
46
+ const local = path.join(os.homedir(), '.local', 'bin', 'uv.exe');
47
+ if (fs.existsSync(local)) return local;
48
+ try {
49
+ require('child_process').execSync('where uv', { timeout: 5000, stdio: 'pipe' });
50
+ return 'uv';
51
+ } catch {}
52
+ return '';
53
+ }
54
+
55
+ function probe() {
56
+ return {
57
+ installed: !!headroomExe(),
58
+ installing: state.installing,
59
+ failed: state.installFailed,
60
+ error: state.installError,
61
+ enabled: state.enabled,
62
+ starting: state.starting,
63
+ proxies: [...state.proxies.values()].map((p) => ({ port: p.port, baseUrl: p.baseUrl, ready: p.ready })),
64
+ };
65
+ }
66
+
67
+ /* ---------- KURULUM (arka plan) ----------
68
+ uv tool install headroom-ai[proxy] — kendi izole Python 3.13 ortamını kurar;
69
+ sistemde Python gerekmez. uv yoksa pip fallback, o da yoksa elle yönlendirir. */
70
+ function autoInstall() {
71
+ if (state.installing || headroomExe()) return;
72
+ const uv = uvExe();
73
+ if (!uv) {
74
+ state.installFailed = true;
75
+ state.installError = 'uv bulunamadı — kur: winget install astral-sh.uv';
76
+ _emit();
77
+ return;
78
+ }
79
+ state.installing = true;
80
+ state.installFailed = false;
81
+ state.installError = '';
82
+ _emit();
83
+ emitInstallProgress('headroom', { pct: 4 });
84
+ const proc = spawn(uv, ['tool', 'install', '--python', '3.13', 'headroom-ai[proxy]'], { windowsHide: true });
85
+ let buf = '';
86
+ let stage = 4;
87
+ const bump = () => {
88
+ stage = Math.min(92, stage + 7);
89
+ emitInstallProgress('headroom', { pct: stage });
90
+ };
91
+ const onData = (c) => {
92
+ const s = String(c);
93
+ if (/Download|Resolve|Installed|Prepared/i.test(s)) bump();
94
+ };
95
+ proc.stdout.on('data', onData);
96
+ proc.stderr.on('data', onData);
97
+ proc.on('close', (code) => {
98
+ state.installing = false;
99
+ if (code === 0 && headroomExe()) {
100
+ emitInstallProgress('headroom', { pct: 100 });
101
+ } else {
102
+ state.installFailed = true;
103
+ state.installError = 'kurulum başarısız — elle: uv tool install "headroom-ai[proxy]"';
104
+ }
105
+ _emit();
106
+ /* kurulum bitti ve açık kaldıysa proxy'leri hemen başlat */
107
+ if (state.enabled && headroomExe()) start().catch(() => {});
108
+ });
109
+ proc.on('error', () => {
110
+ state.installing = false;
111
+ state.installFailed = true;
112
+ state.installError = 'kurulum başlatılamadı (uv)';
113
+ _emit();
114
+ });
115
+ }
116
+
117
+ /* ---------- PROXY SÜREÇLERİ ----------
118
+ Provider başına bir proxy; cache modunu BOZMAMAK için mode=token,
119
+ Beast'in tanımadığı headroom_retrieve aracı enjekte EDİLMESİN diye
120
+ no-ccr (geri-getirilemez sıkıştırma, en yüksek tasarruf) ve rate limit
121
+ kapalı (Beast'in kendi istek patlamalarına takılmasın). Beacon kapalı. */
122
+ function nextPort() {
123
+ const used = new Set([...state.proxies.values()].map((p) => p.port));
124
+ for (let i = 0; i < MAX_PROXIES; i++) {
125
+ const port = PORT_BASE + i;
126
+ if (!used.has(port)) return port;
127
+ }
128
+ return 0;
129
+ }
130
+
131
+ function spawnFor(provider) {
132
+ return new Promise((resolve) => {
133
+ const exe = headroomExe();
134
+ if (!exe) return resolve({ ok: false, error: 'headroom kurulu değil' });
135
+ const port = nextPort();
136
+ if (!port) return resolve({ ok: false, error: 'port kalmadı' });
137
+ let proc;
138
+ try {
139
+ proc = spawn(exe, [
140
+ 'proxy', '--port', String(port), '--openai-api-url', provider.baseUrl,
141
+ '--mode', 'token', '--no-ccr', '--no-rate-limit',
142
+ ], {
143
+ windowsHide: true,
144
+ env: { ...process.env, HEADROOM_BEACON: 'off', DO_NOT_TRACK: '1' },
145
+ });
146
+ } catch (e) {
147
+ return resolve({ ok: false, error: String((e && e.message) || e) });
148
+ }
149
+ const rec = { proc, port, baseUrl: provider.baseUrl, ready: false };
150
+ state.proxies.set(provider.id, rec);
151
+ _emit();
152
+ proc.on('exit', () => {
153
+ if (state.proxies.get(provider.id) === rec) state.proxies.delete(provider.id);
154
+ _emit();
155
+ });
156
+ const t0 = Date.now();
157
+ const poll = setInterval(async () => {
158
+ let ready = false;
159
+ try {
160
+ const r = await fetch('http://127.0.0.1:' + port + '/health', { signal: AbortSignal.timeout(2000) });
161
+ ready = !!r.ok;
162
+ } catch {}
163
+ if (ready) {
164
+ clearInterval(poll);
165
+ rec.ready = true;
166
+ _emit();
167
+ return resolve({ ok: true, port });
168
+ }
169
+ if (Date.now() - t0 > 90000) {
170
+ clearInterval(poll);
171
+ try { rec.proc.kill(); } catch {}
172
+ state.proxies.delete(provider.id);
173
+ _emit();
174
+ resolve({ ok: false, error: 'proxy zaman aşımı (90sn)' });
175
+ }
176
+ }, 1200);
177
+ });
178
+ }
179
+
180
+ async function start() {
181
+ const exe = headroomExe();
182
+ if (!exe) {
183
+ autoInstall();
184
+ return { ok: false, error: 'kuruluyor — birkaç dakika sonra hazır olur' };
185
+ }
186
+ state.starting = true;
187
+ _emit();
188
+ try {
189
+ const providers = (state.providerGetter ? state.providerGetter() : []).slice(0, MAX_PROXIES);
190
+ const routes = {};
191
+ /* önce eskileri temizle: listeden çıkan provider'ın proxy'sini kapat */
192
+ const keep = new Set(providers.map((p) => p.id));
193
+ for (const [id, rec] of [...state.proxies.entries()]) {
194
+ if (keep.has(id)) continue;
195
+ try { rec.proc.kill(); } catch {}
196
+ try { spawn('taskkill', ['/pid', String(rec.proc.pid), '/T', '/F'], { windowsHide: true }); } catch {}
197
+ state.proxies.delete(id);
198
+ }
199
+ for (const p of providers) {
200
+ if (!p.baseUrl || state.proxies.has(p.id)) continue;
201
+ const r = await spawnFor(p);
202
+ if (r.ok) routes[p.id] = r.port;
203
+ }
204
+ try { require('./llm').setHeadroomRoutes(routes); } catch {}
205
+ state.enabled = true;
206
+ return { ok: true, routes };
207
+ } finally {
208
+ state.starting = false;
209
+ _emit();
210
+ }
211
+ }
212
+
213
+ async function setEnabled(on) {
214
+ if (on) {
215
+ state.enabled = true;
216
+ if (!headroomExe()) {
217
+ autoInstall();
218
+ return { ok: false, installing: true, error: 'headroom arka planda kuruluyor — birkaç dakika sonra hazır' };
219
+ }
220
+ return start();
221
+ }
222
+ state.enabled = false;
223
+ stopAll();
224
+ try { require('./llm').setHeadroomRoutes(null); } catch {}
225
+ return { ok: true };
226
+ }
227
+
228
+ /* provider listesi değişti (eklendi/silindi) → proxy setini senkronla */
229
+ function syncProviders() {
230
+ if (!state.enabled || !headroomExe()) return;
231
+ start().catch(() => {});
232
+ }
233
+
234
+ function stopAll() {
235
+ for (const rec of state.proxies.values()) {
236
+ try { rec.proc.kill(); } catch {}
237
+ try { spawn('taskkill', ['/pid', String(rec.proc.pid), '/T', '/F'], { windowsHide: true }); } catch {}
238
+ }
239
+ state.proxies.clear();
240
+ _emit();
241
+ }
242
+
243
+ function setProviderGetter(fn) { state.providerGetter = typeof fn === 'function' ? fn : null; }
244
+ function setStatusSink(fn) { state.statusSink = typeof fn === 'function' ? fn : null; }
245
+
246
+ /* proxy /stats özeti (sağlık duvarı yok — hata.yakalanır) */
247
+ async function stats() {
248
+ const rec = [...state.proxies.values()].find((p) => p.ready);
249
+ if (!rec) return { ok: false, running: false };
250
+ try {
251
+ const r = await fetch('http://127.0.0.1:' + rec.port + '/stats', { signal: AbortSignal.timeout(4000) });
252
+ const j = await r.json();
253
+ const s = (j && (j.tokens || j.summary)) || {};
254
+ return {
255
+ ok: true,
256
+ running: true,
257
+ port: rec.port,
258
+ input: Number(s.input) || 0,
259
+ output: Number(s.output) || 0,
260
+ saved: Number(s.saved) || 0,
261
+ proxyCompressionSaved: Number(s.proxy_compression_saved) || 0,
262
+ savingsPercent: Number(s.savings_percent) || 0,
263
+ allLayersSaved: Number(s.all_layers_saved) || 0,
264
+ };
265
+ } catch {
266
+ return { ok: false, running: true, port: rec.port };
267
+ }
268
+ }
269
+
270
+ module.exports = {
271
+ probe,
272
+ autoInstall,
273
+ setEnabled,
274
+ syncProviders,
275
+ stopAll,
276
+ setProviderGetter,
277
+ setStatusSink,
278
+ stats,
279
+ };
package/src/agent/llm.js CHANGED
@@ -347,6 +347,18 @@ function sessionHeaderId(body) {
347
347
  return _sessionFallback;
348
348
  }
349
349
 
350
+ /* Headroom proxy yönlendirme: providerId -> yerel proxy portu haritası.
351
+ main (agent/headroom.js) ayarlardan açıldığında kurar; null = düz bağlan. */
352
+ let _headroomRoutes = null;
353
+ function setHeadroomRoutes(map) {
354
+ _headroomRoutes = map && Object.keys(map).length ? map : null;
355
+ }
356
+ function headroomUrlFor(sel) {
357
+ if (!_headroomRoutes || !sel) return '';
358
+ const port = Number(_headroomRoutes[String(sel.providerId || '')] || 0);
359
+ return port ? 'http://127.0.0.1:' + port + '/v1/chat/completions' : '';
360
+ }
361
+
350
362
  async function openChat(sel, body, { stream, signal, drops } = {}) {
351
363
  const d = drops || {};
352
364
  const payload = {
@@ -368,7 +380,10 @@ async function openChat(sel, body, { stream, signal, drops } = {}) {
368
380
  if (body.cacheKey && wantsCacheKey(sel) && !d.noCacheKey) {
369
381
  payload.prompt_cache_key = String(body.cacheKey);
370
382
  }
371
- return fetch(sel.url, {
383
+ /* Headroom açıkken istek yerel sıkıştırma proxy'sinden geçer (aynı gövde+başlıklar);
384
+ yoksa sağlayıcıya düz bağlan */
385
+ const target = headroomUrlFor(sel) || sel.url;
386
+ return fetch(target, {
372
387
  method: 'POST',
373
388
  signal,
374
389
  headers: {
@@ -624,6 +639,7 @@ module.exports = {
624
639
  capsFor,
625
640
  resetCaps,
626
641
  learnCaps,
642
+ setHeadroomRoutes,
627
643
  PARAM_LABELS,
628
644
  isNetworkError,
629
645
  wantsCacheKey,
package/src/main.js CHANGED
@@ -27,6 +27,8 @@ const usageMod = require('./agent/usage');
27
27
  const bus = require('./agent/bus');
28
28
  const computeruse = require('./agent/computeruse');
29
29
  const log = require('./agent/logger');
30
+ const headroom = require('./agent/headroom');
31
+ const QRCode = require('qrcode'); /* Expo Go QR (bc-expurl) — whatsapp ile aynı paket */
30
32
 
31
33
  /* #3 otomatik updater: sessiz — indirir, kapanışta kurar, kullanıcıya soru sormaz.
32
34
  Paketlenmemiş (npm start) modda devre dışı; Update sekmesi ve /update komutu kontrol eder. */
@@ -3109,6 +3111,10 @@ function reloadBackend() {
3109
3111
  if (ev.type === 'bc-preview' && /^https?:\/\/(?:localhost|127\.0\.0\.1)(?::\d{2,5})?/i.test(String(ev.url || ''))) {
3110
3112
  bcLastServerUrl = String(ev.url);
3111
3113
  }
3114
+ /* Expo Go QR: ajanın dev sunucusu exp:// adresi yazdıysa QR üretip panele bas */
3115
+ if (ev.type === 'bc-expurl') {
3116
+ bcExpQrMake(String(ev.url || '')).catch(() => {});
3117
+ }
3112
3118
  /* WA canlı iş takibi: oturum bir WhatsApp sohbetine bağlıysa araç
3113
3119
  hareketlerini (terminal, web, dosya…) kısa satırla bildir.
3114
3120
  Spam olmasın: sohbet başına en az 7 sn'de bir tek satır. */
@@ -3288,6 +3294,30 @@ function reloadBackend() {
3288
3294
  /* PANEL RUN köprüsü: ajanın panel_run aracı → ÇALIŞTIR panelindeki yönetilen
3289
3295
  süreç koşucusu (sandbox:run IPC ile aynı makine). */
3290
3296
  engine.sbRunHook = sbRunStartManaged;
3297
+
3298
+ /* HEADROOM (opsiyonel token sıkıştırma proxy'si): engine zincirinden provider
3299
+ listesi alınır; durum değişince renderer'a 'headroom-status' düşer */
3300
+ headroom.setProviderGetter(() => {
3301
+ const seen = new Map();
3302
+ for (const c of (engine && engine.cfg && engine.cfg.chain) || []) {
3303
+ if (!c || !c.providerId || !c.url || seen.has(c.providerId)) continue;
3304
+ seen.set(c.providerId, {
3305
+ id: String(c.providerId),
3306
+ name: String(c.providerName || c.providerId),
3307
+ baseUrl: String(c.url).replace(/\/chat\/completions$/, ''),
3308
+ });
3309
+ }
3310
+ return [...seen.values()];
3311
+ });
3312
+ headroom.setStatusSink((st) => {
3313
+ try {
3314
+ if (win && !win.isDestroyed()) win.webContents.send('agent:event', { type: 'headroom-status', status: st });
3315
+ } catch {}
3316
+ });
3317
+ /* ayarlardan açık kaldıysa: uygulama açılışında arka planda başlat */
3318
+ if (settings.headroom && settings.headroom.enabled) {
3319
+ setTimeout(() => { headroom.setEnabled(true).catch(() => {}); }, 4000);
3320
+ }
3291
3321
  return engine.publicState();
3292
3322
  }
3293
3323
 
@@ -3516,6 +3546,7 @@ app.whenReady().then(() => {
3516
3546
  flushBrowserStorage(); // x.com/google oturumları (cookies) diske yazılsın
3517
3547
  try { toolsMod.disposeShellSessions(); } catch {} // kalıcı shell oturumlarını kapat
3518
3548
  try { require('./agent/mcp').stopAll(); } catch {} // MCP server süreçlerini kapat
3549
+ try { headroom.stopAll(); } catch {} // headroom proxy süreçlerini kapat
3519
3550
  });
3520
3551
 
3521
3552
  if (process.argv.includes('--smoke')) {
@@ -6248,12 +6279,45 @@ ipcMain.handle('install:status', async () => {
6248
6279
  /* 8) Edge TTS (bulut) */
6249
6280
  rows.push({ id: 'edge', name: 'Edge TTS — seslendirme', state: 'cloud', detail: 'bulut — kurulum gerekmez' });
6250
6281
 
6282
+ /* 9) Headroom CLI — arka plan paket kurulumu burada görünür;
6283
+ aç/kapat Token Sıkıştırma sekmesinde */
6284
+ {
6285
+ const st = headroom.probe();
6286
+ let state = 'optional';
6287
+ if (st.installed) state = 'ok';
6288
+ else if (st.installing) state = 'loading';
6289
+ if (st.failed) state = 'failed';
6290
+ rows.push({
6291
+ id: 'headroom',
6292
+ name: 'Headroom CLI — token sıkıştırma motoru',
6293
+ state,
6294
+ detail: st.installing
6295
+ ? 'arka planda kuruluyor — uv tool install headroom-ai[proxy]'
6296
+ : st.failed
6297
+ ? (st.error || 'kurulum başarısız')
6298
+ : st.installed
6299
+ ? 'kurulu — aç/kapat: Token Sıkıştırma sekmesi'
6300
+ : 'kurulu değil — Token Sıkıştırma sekmesinden açınca otomatik kurulur',
6301
+ ...(st.installing ? pctFields('headroom') : {}),
6302
+ });
6303
+ }
6304
+
6251
6305
  return rows;
6252
6306
  });
6253
6307
 
6254
6308
  /* cua-driver kurulumunu şimdi başlat (Kurulum sekmesi otomatiği + elle tetikleme) */
6255
6309
  ipcMain.handle('cua:install', () => require('./agent/computeruse').autoInstall());
6256
6310
 
6311
+ /* HEADROOM (opsiyonel token sıkıştırma): aç/kapa + durum */
6312
+ ipcMain.handle('headroom:toggle', async (_e, on) => {
6313
+ settings.headroom = { ...(settings.headroom || {}), enabled: !!on };
6314
+ saveSettings();
6315
+ const r = await headroom.setEnabled(!!on).catch((e) => ({ ok: false, error: String((e && e.message) || e) }));
6316
+ return r;
6317
+ });
6318
+ ipcMain.handle('headroom:status', () => headroom.probe());
6319
+ ipcMain.handle('headroom:stats', () => headroom.stats());
6320
+
6257
6321
  /* embedding modelini şimdi indir (mem0 arama yolu ısıtılır) */
6258
6322
  ipcMain.handle('embed:prefetch', () => {
6259
6323
  try {
@@ -8690,6 +8754,23 @@ const bcStatic = { server: null, root: '', base: '' };
8690
8754
  /* MOBİL PROJE önizleme: npm run web (expo start --web) süreci + canlı adres */
8691
8755
  const bcMobile = { proc: null, url: '', root: '' };
8692
8756
 
8757
+ /* ---------- EXPO GO QR (yalnız Beast Code) ----------
8758
+ Ajan expo start çıktısında exp://<LAN-IP>:<port> yazınca QR'e çevrilir ve
8759
+ renderer'a { type:'bc-exp-qr', qr, url } düşer — telefonda Expo Go okutulur. */
8760
+ let bcLastExpUrl = '';
8761
+ let bcLastExpQr = '';
8762
+
8763
+ function bcExpQrMake(url) {
8764
+ if (!url || url === bcLastExpUrl) return Promise.resolve();
8765
+ bcLastExpUrl = url;
8766
+ return QRCode.toDataURL(url, { width: 240, margin: 1, errorCorrectionLevel: 'M' }).then((qr) => {
8767
+ bcLastExpQr = qr;
8768
+ if (win && !win.isDestroyed()) win.webContents.send('agent:event', { type: 'bc-exp-qr', qr, url });
8769
+ });
8770
+ }
8771
+
8772
+ ipcMain.handle('ide:expqr', () => ({ ok: true, qr: bcLastExpQr, url: bcLastExpUrl }));
8773
+
8693
8774
  /* workspace bir MOBİL UYGULAMA projesi mi? (expo / react-native / app.json) */
8694
8775
  function ideMobileProject(root) {
8695
8776
  try {
@@ -8714,6 +8795,49 @@ function ideMobileProject(root) {
8714
8795
  }
8715
8796
  }
8716
8797
 
8798
+ /* Beast Code PROJE TİPİ algılama: preview butonu ajana hangi talimatı vereceğini
8799
+ bununla seçer. kind: expo | react-native | web | static | unknown */
8800
+ function ideProjectInfo(root) {
8801
+ const out = { kind: 'unknown', name: '', cmd: '' };
8802
+ try {
8803
+ const pkgPath = path.join(root, 'package.json');
8804
+ let pkg = null;
8805
+ try { if (fs.existsSync(pkgPath)) pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); } catch {}
8806
+ if (!pkg) {
8807
+ out.kind = fs.existsSync(path.join(root, 'index.html')) ? 'static' : 'unknown';
8808
+ return out;
8809
+ }
8810
+ out.name = String(pkg.name || '');
8811
+ const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
8812
+ const scripts = pkg.scripts || {};
8813
+ const hasAppJson = fs.existsSync(path.join(root, 'app.json')) || fs.existsSync(path.join(root, 'app.config.js'));
8814
+ if (deps.expo || (hasAppJson && (deps['react-native'] || deps['react-dom']))) {
8815
+ out.kind = 'expo';
8816
+ out.cmd = scripts.web ? 'npm run web' : deps.expo ? 'npx expo start --web' : String(scripts.start || '');
8817
+ } else if (deps['react-native']) {
8818
+ out.kind = 'react-native';
8819
+ out.cmd = String(scripts.start || scripts.ios || '') || 'npx react-native start';
8820
+ } else if (deps.next) {
8821
+ out.kind = 'web';
8822
+ out.cmd = String(scripts.dev || scripts.start || '') || 'npx next dev';
8823
+ } else if (deps.vite || deps['react-scripts'] || deps['@angular/core'] || deps['vue']) {
8824
+ out.kind = 'web';
8825
+ out.cmd = String(scripts.dev || scripts.start || scripts.serve || '');
8826
+ } else if (scripts.dev) {
8827
+ out.kind = 'web';
8828
+ out.cmd = 'npm run dev';
8829
+ } else if (scripts.start) {
8830
+ out.kind = 'web';
8831
+ out.cmd = 'npm start';
8832
+ } else {
8833
+ out.kind = fs.existsSync(path.join(root, 'index.html')) ? 'static' : 'unknown';
8834
+ }
8835
+ } catch {}
8836
+ return out;
8837
+ }
8838
+
8839
+ ipcMain.handle('ide:projinfo', () => ({ ok: true, ...ideProjectInfo(ideRoot()) }));
8840
+
8717
8841
  function killMobilePreview() {
8718
8842
  const p = bcMobile.proc;
8719
8843
  if (p) {
@@ -8964,6 +9088,7 @@ ipcMain.handle('custom:set', (_e, list) => {
8964
9088
  settings.customProviders = Array.isArray(list) ? list : [];
8965
9089
  saveSettings();
8966
9090
  engine.setCustomProviders(settings.customProviders);
9091
+ headroom.syncProviders(); /* headroom açıksa proxy setini yeni listeye uydur */
8967
9092
  return engine.publicState();
8968
9093
  });
8969
9094
 
package/src/preload.js CHANGED
@@ -222,6 +222,11 @@ contextBridge.exposeInMainWorld('beast', {
222
222
  idePreview: () => ipcRenderer.invoke('ide:preview'),
223
223
  idePreviewFile: (rel) => ipcRenderer.invoke('ide:previewFile', rel),
224
224
  idePreviewUrl: (url) => ipcRenderer.invoke('ide:previewUrl', url),
225
+ ideProjInfo: () => ipcRenderer.invoke('ide:projinfo'),
226
+ ideExpQr: () => ipcRenderer.invoke('ide:expqr'),
227
+ headroomToggle: (on) => ipcRenderer.invoke('headroom:toggle', !!on),
228
+ headroomStatus: () => ipcRenderer.invoke('headroom:status'),
229
+ headroomStats: () => ipcRenderer.invoke('headroom:stats'),
225
230
  agentsClearAll: () => ipcRenderer.invoke('agents:clearAll'),
226
231
  ideDelete: (rel) => ipcRenderer.invoke('ide:delete', rel),
227
232
  onWaEvent: (cb) => ipcRenderer.on('wa:event', (_e, ev) => cb(ev)),
@@ -64,6 +64,7 @@
64
64
  set_title: 'AYARLAR',
65
65
  tab_lang: 'Dil',
66
66
  lang_sub: 'Arayüz dili — uygulama dili ve sesli komut dili buradan değişir',
67
+ tab_headroom: 'Token Sıkıştırma',
67
68
  tab_provider: 'Provider',
68
69
  tab_fallout: 'Fallout',
69
70
  tab_memory: 'Memory',
@@ -163,7 +164,7 @@
163
164
  em_mem_clear: 'Hafızayı Temizle',
164
165
  em_mem_cleared: 'Empati hafızası temizlendi',
165
166
  tab_cron: 'Cron',
166
- tab_usage: 'Maliyet · Limit',
167
+ tab_usage: 'Maliyet',
167
168
  tab_logs: 'Log',
168
169
  tab_dash: 'Dashboard',
169
170
  tab_limits: 'Limit',
@@ -696,6 +697,7 @@
696
697
  set_title: 'SETTINGS',
697
698
  tab_lang: 'Language',
698
699
  lang_sub: 'Interface language — app UI and voice command language',
700
+ tab_headroom: 'Token Compression',
699
701
  tab_provider: 'Provider',
700
702
  tab_fallout: 'Fallout',
701
703
  tab_memory: 'Memory',
@@ -795,7 +797,7 @@
795
797
  em_mem_clear: 'Clear Memory',
796
798
  em_mem_cleared: 'Empathy memory cleared',
797
799
  tab_cron: 'Cron',
798
- tab_usage: 'Cost · Limit',
800
+ tab_usage: 'Cost',
799
801
  tab_logs: 'Logs',
800
802
  tab_dash: 'Dashboard',
801
803
  tab_limits: 'Limit',
@@ -164,11 +164,21 @@
164
164
  <button id="codeShow" title="Editörü göster" data-i18n-title="tipCodeShow" hidden>&#x25A3;&#xFE0E;</button>
165
165
  <button id="bcNew" title="Yeni oturum" data-i18n-title="bc_new">*</button>
166
166
  <button id="bcClear" title="Çıktıyı temizle" data-i18n-title="bc_clear">Temizle</button>
167
+ <button id="bcQrBtn" title="Expo Go QR" hidden>&#x25A3;&#xFE0E;</button>
167
168
  </div>
168
169
  <div id="bcOut"></div>
169
170
  <div id="bcTodoWrap" hidden></div>
170
171
  <div id="bcStatus" hidden></div>
171
172
  <div id="bcChips" hidden></div>
173
+ <div id="bcQrCard" hidden>
174
+ <div class="bq-head">
175
+ <span>EXPO GO QR</span>
176
+ <button id="bcQrClose" title="Kapat">&#x00D7;</button>
177
+ </div>
178
+ <img id="bcQrImg" alt="Expo QR" draggable="false" />
179
+ <div id="bcQrUrl" title=""></div>
180
+ <div class="bq-hint">Telefonda <b>Expo Go</b> uygulamasıyla okut</div>
181
+ </div>
172
182
  <div id="bcInputRow">
173
183
  <button id="bcAttach" title="Dosya / resim ekle" data-i18n-title="tipAttach">+</button>
174
184
  <button id="bcMic" title="Sesli komut — konuş, bırakınca yazıya çevirsin" data-i18n-title="tipMic"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="2" width="6" height="12" rx="3"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><path d="M12 19v3"/></svg></button>
@@ -297,6 +307,7 @@
297
307
  <button class="tab" data-tab="empati" data-i18n="tab_empati">Empati Loop</button>
298
308
  <button class="tab" data-tab="cron" data-i18n="tab_cron">Cron</button>
299
309
  <button class="tab" data-tab="usage" data-i18n="tab_usage">Maliyet · Limit</button>
310
+ <button class="tab" data-tab="headroom" data-i18n="tab_headroom">Token Sıkıştırma</button>
300
311
  <button class="tab" data-tab="logs" data-i18n="tab_logs">Log</button>
301
312
  <button class="tab" data-tab="dash" data-i18n="tab_dash">Dashboard</button>
302
313
  <button class="tab" data-tab="sec" data-i18n="tab_security">Güvenlik</button>
@@ -322,6 +333,7 @@
322
333
  <div id="tab-events" class="pane" hidden></div>
323
334
  <div id="tab-empati" class="pane" hidden></div>
324
335
  <div id="tab-usage" class="pane" hidden></div>
336
+ <div id="tab-headroom" class="pane" hidden></div>
325
337
  <div id="tab-logs" class="pane" hidden></div>
326
338
  <div id="tab-dash" class="pane" hidden></div>
327
339
  <div id="tab-sec" class="pane" hidden></div>
@@ -83,6 +83,10 @@ const els = {
83
83
  bcAttach: $('#bcAttach'),
84
84
  bcMic: $('#bcMic'),
85
85
  bcChips: $('#bcChips'),
86
+ bcQrBtn: $('#bcQrBtn'),
87
+ bcQrCard: $('#bcQrCard'),
88
+ bcQrImg: $('#bcQrImg'),
89
+ bcQrUrl: $('#bcQrUrl'),
86
90
  studioBtn: $('#studioBtn'),
87
91
  sbBtn: $('#sbBtn'),
88
92
  sandboxRow: $('#sandboxRow'),
@@ -1089,6 +1093,7 @@ function addFiles(files, mode) {
1089
1093
  /* ---------------- settings ---------------- */
1090
1094
 
1091
1095
  let setTab = 'lang'; /* ayarlar artık DİL sekmesiyle açılır */
1096
+ let hrPaneTimer = null; /* headroom-status → Kurulum sekmesi debounce tazeleme */
1092
1097
 
1093
1098
  /* DİL sekmesi: arayüz dili seçimi (eski sağ-alt dil ikonunun yerine) */
1094
1099
  function renderLangPane() {
@@ -1154,6 +1159,7 @@ async function renderActiveSettingsTab() {
1154
1159
  case 'empati': await renderEmpatiPane(); break;
1155
1160
  case 'cron': await openCron(); break;
1156
1161
  case 'usage': await renderUsagePane(); break;
1162
+ case 'headroom': await renderHeadroomPane(); break;
1157
1163
  case 'agents': await refreshAgentsPane(); break;
1158
1164
  case 'logs': await renderLogPane(); break;
1159
1165
  case 'dash': await renderDashboardPane(); break;
@@ -1237,7 +1243,7 @@ function switchTab(name) {
1237
1243
  document.querySelectorAll('#setTabs .tab').forEach((b) =>
1238
1244
  b.classList.toggle('active', b.dataset.tab === name)
1239
1245
  );
1240
- for (const p of ['lang', 'provider', 'fallout', 'skills', 'agents', 'tts', 'install', 'email', 'integrations', 'websearch', 'mcp', 'events', 'empati', 'cron', 'usage', 'logs', 'dash', 'sec', 'update']) {
1246
+ for (const p of ['lang', 'provider', 'fallout', 'skills', 'agents', 'tts', 'install', 'email', 'integrations', 'websearch', 'mcp', 'events', 'empati', 'cron', 'usage', 'headroom', 'logs', 'dash', 'sec', 'update']) {
1241
1247
  const el = $('#tab-' + p);
1242
1248
  if (el) el.hidden = p !== name; // guard: eksik pane tüm sekmeleri kilitlemesin
1243
1249
  }
@@ -1245,6 +1251,7 @@ function switchTab(name) {
1245
1251
  if (name === 'cron') openCron();
1246
1252
  if (name === 'usage') renderUsagePane();
1247
1253
  if (name === 'install') renderInstallPane();
1254
+ if (name === 'headroom') renderHeadroomPane();
1248
1255
  if (name === 'events') renderEventsPane();
1249
1256
  if (name === 'empati') renderEmpatiPane();
1250
1257
  if (name === 'logs') renderLogPane();
@@ -1272,7 +1279,6 @@ async function renderUsagePane() {
1272
1279
  } catch {
1273
1280
  rep = { today: { total: {}, models: [] }, month: { total: {}, models: [] } };
1274
1281
  }
1275
- const wwi = await beast.whereWasIGet().catch(() => ({ enabled: false }));
1276
1282
  const t = rep.today.total || {};
1277
1283
  const mo = rep.month.total || {};
1278
1284
  const card = (label, v) =>
@@ -1307,10 +1313,7 @@ async function renderUsagePane() {
1307
1313
  '<div class="form-grid" style="grid-template-columns:auto auto;gap:8px;margin-top:8px">' +
1308
1314
  '<button id="bkNow" class="btn ghost">' + _t('us_backup_now') + '</button>' +
1309
1315
  '<button id="bkRestore" class="btn ghost">' + _t('us_backup_restore') + '</button>' +
1310
- '</div>' +
1311
- '<div class="divider"></div><h2>' + _t('us_where_h2') + '</h2>' +
1312
- '<div class="sub">' + _t('us_where_sub') + '</div>' +
1313
- `<label class="lock-row" style="margin-top:6px"><input type="checkbox" id="wwiOn" ${wwi.enabled ? 'checked' : ''}/><span>${_t('us_where_on')}</span></label>`;
1316
+ '</div>';
1314
1317
  $('#usReset').addEventListener('click', async () => {
1315
1318
  await beast.resetUsage();
1316
1319
  renderUsagePane();
@@ -1330,15 +1333,6 @@ async function renderUsagePane() {
1330
1333
  toast(_t('us_backup_err') + (r.error || '?'));
1331
1334
  }
1332
1335
  });
1333
- $('#wwiOn').addEventListener('change', async (e) => {
1334
- await beast.whereWasISet({ enabled: e.target.checked });
1335
- toast(e.target.checked ? _t('us_where_on_toast') : _t('us_where_off_toast'));
1336
- });
1337
- /* LİMİT ayarları aynı sekmenin alt bölümünde (Maliyet · Limit birleşik) */
1338
- const limBox = document.createElement('div');
1339
- limBox.id = 'limBox';
1340
- pane.appendChild(limBox);
1341
- await renderLimitsPane();
1342
1336
  }
1343
1337
 
1344
1338
  async function refreshFalloutPane() {
@@ -2215,6 +2209,93 @@ function ttsFlushTail() {
2215
2209
  /* ---------------- KURULUM SEKMESİ: bileşenler OTOMATİK kurulur ----------------
2216
2210
  Eksik/kısmen inmiş bileşen sekme açılınca kendiliğinden indirilir; kullanıcı
2217
2211
  hiçbir düğmeye basmaz. Durum canlı güncellenir (4 sn'de bir). */
2212
+ /* ---------- TOKEN SIKIŞTIRMA (Headroom) sekmesi ----------
2213
+ Opsiyonel yerel sıkıştırma proxy'si: açıkken LLM girdisi %30-70 küçülür.
2214
+ Aç/Kapat buradan; CLI kurulumu Kurulum sekmesinde görülür. */
2215
+ let hrStatsTimer = null;
2216
+
2217
+ async function renderHeadroomPane() {
2218
+ const pane = $('#tab-headroom');
2219
+ if (!pane) return;
2220
+ const st = await beast.headroomStatus().catch(() => null);
2221
+
2222
+ const fmtN = (n) => new Intl.NumberFormat('tr-TR').format(Math.round(Number(n) || 0));
2223
+ let stHtml = '';
2224
+ if (st) {
2225
+ const readyN = (st.proxies || []).filter((p) => p.ready).length;
2226
+ let stateTxt;
2227
+ if (st.installing) stateTxt = '⏳ arka planda kuruluyor — uv tool install headroom-ai[proxy] (birkaç dakika)';
2228
+ else if (st.enabled && readyN) stateTxt = '🟢 açık — proxy: ' + (st.proxies || []).filter((p) => p.ready).map((p) => '127.0.0.1:' + p.port).join(', ');
2229
+ else if (st.enabled) stateTxt = '⏳ proxy başlatılıyor… (ilk açılış ~30 sn)';
2230
+ else if (st.failed) stateTxt = '⛔ ' + (st.error || 'kurulum başarısız');
2231
+ else if (st.installed) stateTxt = '⚪ kapalı — kurulu, aşağıdaki düğmeyle etkinleştir';
2232
+ else stateTxt = '⚪ kapalı — açınca uv ile otomatik kurulur (birkaç dakika)';
2233
+ stHtml = '<div class="sub" style="margin-top:10px;font-size:12px;text-align:center">' + escapeHtml(stateTxt) + '</div>';
2234
+ }
2235
+
2236
+ const stats = await beast.headroomStats().catch(() => null);
2237
+ let statsHtml = '';
2238
+ if (stats && stats.ok && (stats.input || stats.saved)) {
2239
+ statsHtml =
2240
+ '<div class="divider"></div>' +
2241
+ '<h2 style="text-align:center">Tasarruf (proxy açıkken)</h2>' +
2242
+ '<div style="display:flex;gap:24px;flex-wrap:wrap;margin-top:8px;justify-content:center">' +
2243
+ '<div><div style="font-size:20px;font-weight:800;color:var(--ok)">%' + Math.round(stats.savingsPercent || 0) + '</div><div class="sub">sıkıştırma oranı</div></div>' +
2244
+ '<div><div style="font-size:20px;font-weight:800">' + new Intl.NumberFormat('tr-TR').format(stats.proxyCompressionSaved || 0) + '</div><div class="sub">kaydedilen token</div></div>' +
2245
+ '<div><div style="font-size:20px;font-weight:800">' + new Intl.NumberFormat('tr-TR').format(stats.input || 0) + '</div><div class="sub">toplam girdi token</div></div>' +
2246
+ '</div>';
2247
+ }
2248
+
2249
+ /* AÇ/KAPAT düğmesi: ortalanmış + SVG ikonlu (play/pause — uygulama ikon stili) */
2250
+ const svgPlay =
2251
+ '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polygon points="6 3 20 12 6 21 6 3"/></svg>';
2252
+ const svgPause =
2253
+ '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="6" y="4" width="4" height="16" rx="1"/><rect x="14" y="4" width="4" height="16" rx="1"/></svg>';
2254
+ pane.innerHTML =
2255
+ '<h2 style="text-align:center">' + _t('tab_headroom') + '</h2>' +
2256
+ '<div class="sub" style="text-align:center;max-width:620px;margin:0 auto">Headroom yerel proxy\u2019si — araç çıktıları, loglar ve JSON yükleri modele gitmeden sıkıştırılır; orijinali yerelde saklanır. Token faturasını %30-70 düşürür. Yalnız OpenAI-uyumlu sağlayıcı trafiğini etkiler; kapalıyken hiçbir şey değişmez.</div>' +
2257
+ '<div style="display:flex;flex-direction:column;align-items:center;gap:10px;margin-top:18px">' +
2258
+ '<button id="hrToggle" class="btn" style="width:auto;padding:11px 16px;display:inline-flex;align-items:center;gap:8px;justify-content:center" title=""></button>' +
2259
+ '<span id="hrToggleMsg" class="sub" style="margin:0;text-align:center;max-width:560px"></span>' +
2260
+ '</div>' +
2261
+ stHtml + statsHtml;
2262
+
2263
+ const btn = $('#hrToggle');
2264
+ if (btn) {
2265
+ const on = !!(st && st.enabled);
2266
+ /* yalnız SVG ikon — anlamı durum satırı + tooltip taşır */
2267
+ btn.innerHTML = on ? svgPause : svgPlay;
2268
+ btn.title = on ? 'Token sıkıştırmayı kapat' : 'Token sıkıştırmayı aç';
2269
+ btn.setAttribute('aria-label', btn.title);
2270
+ btn.addEventListener('click', async () => {
2271
+ const msg = $('#hrToggleMsg');
2272
+ btn.disabled = true;
2273
+ btn.innerHTML = '<span>…</span>';
2274
+ const r = await beast.headroomToggle(!on).catch(() => null);
2275
+ if (msg) {
2276
+ if (r && r.installing) {
2277
+ msg.textContent = 'Headroom arka planda kuruluyor — birkaç dakika sonra hazır olur, bu sekme açık kalabilir.';
2278
+ } else if (r && r.error) {
2279
+ msg.textContent = r.error;
2280
+ } else {
2281
+ msg.textContent = !on ? 'Açıldı — proxy hazır olunca trafiği yönlendirir' : 'Kapatıldı — trafik sağlayıcıya düz gidiyor';
2282
+ }
2283
+ }
2284
+ setTimeout(() => renderHeadroomPane(), 2500);
2285
+ });
2286
+ }
2287
+
2288
+ /* açıkken istatistikler canlı aksın */
2289
+ if (hrStatsTimer) clearInterval(hrStatsTimer);
2290
+ hrStatsTimer = setInterval(async () => {
2291
+ if (pane.hidden) { clearInterval(hrStatsTimer); return; }
2292
+ const s2 = await beast.headroomStatus().catch(() => null);
2293
+ const cur = $('#hrToggle');
2294
+ if (s2 && cur && cur.disabled) return; /* toggle işlemi sürüyor */
2295
+ await renderHeadroomPane();
2296
+ }, 6000);
2297
+ }
2298
+
2218
2299
  async function renderInstallPane() {
2219
2300
  const pane = $('#tab-install');
2220
2301
  if (!pane) return;
@@ -2282,10 +2363,24 @@ async function renderInstallPane() {
2282
2363
  '<div class="sub" style="font-size:11px">' + escapeHtml(r.detail || '') + (r.mb ? ' · ' + r.mb + ' MB' : '') + '</div>' +
2283
2364
  bar(r) +
2284
2365
  '</div>' +
2366
+ (r.canToggle
2367
+ ? '<button class="btn hr-tgl" data-on="' + (r.toggleOn ? '1' : '0') + '" style="width:auto;padding:2px 12px;flex:none">' + (r.toggleOn ? 'KAPAT' : 'AÇ') + '</button>'
2368
+ : '') +
2285
2369
  badge(r.state) +
2286
2370
  pctLabel(r) +
2287
2371
  '</div>'
2288
2372
  )).join('');
2373
+ /* Headroom aç/kapa: toggle kurulum + proxy başlatmayı arka planda tetikler */
2374
+ el.querySelectorAll('.hr-tgl').forEach((b) => {
2375
+ b.addEventListener('click', async () => {
2376
+ const on = b.dataset.on !== '1';
2377
+ b.disabled = true;
2378
+ b.textContent = '…';
2379
+ const r = await beast.headroomToggle(on).catch(() => null);
2380
+ toast(r && r.error ? r.error : (r && r.installing ? 'Headroom kuruluyor — birkaç dakika sürer' : (on ? 'Headroom açıldı' : 'Headroom kapatıldı')));
2381
+ refresh();
2382
+ });
2383
+ });
2289
2384
  };
2290
2385
 
2291
2386
  const refresh = async () => {
@@ -2852,67 +2947,6 @@ async function renderDashboardPane() {
2852
2947
  pane.appendChild(list);
2853
2948
  }
2854
2949
 
2855
- /* ---------------- Limit: provider bazlı girdi limiti ---------------- */
2856
-
2857
- async function renderLimitsPane() {
2858
- /* Maliyet sekmesine GÖMÜLÜ çalışır (ayrı sekme kaldırıldı) */
2859
- const pane = $('#tab-limits') || $('#limBox');
2860
- if (!pane) return;
2861
- const cur = await beast.getLimits().catch(() => ({ enabled: false, compress: true, default: 0, perProvider: {} }));
2862
- const provs = [...new Set((state.models || []).map((m) => m.providerName))].sort((a, b) => a.localeCompare(b));
2863
-
2864
- const provRows = provs.length
2865
- ? provs
2866
- .map((p) => {
2867
- const v = cur.perProvider[p] || 0;
2868
- return `<div class="usage-row" style="align-items:center">
2869
- <span style="flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${escapeHtml(p)}</span>
2870
- <input class="inp lim-prov" data-prov="${escapeHtml(p)}" type="number" min="0" step="1000"
2871
- value="${v || ''}" placeholder="0 (${_t('lim_unlimited')})" style="width:130px;flex:none;margin:0" />
2872
- </div>`;
2873
- })
2874
- .join('')
2875
- : '<p class="sub">' + _t('p_no_model') + '</p>';
2876
-
2877
- pane.innerHTML =
2878
- '<h2>' + _t('lim_h2') + '</h2>' +
2879
- '<div class="sub">' + _t('lim_sub') + '</div>' +
2880
- `<div class="fo-toggles" style="margin-top:10px">
2881
- <label class="lock-row"><input type="checkbox" id="limOn" ${cur.enabled ? 'checked' : ''}/><span>${_t('lim_enabled')}</span></label>
2882
- </div>
2883
- <div id="limDetail" style="${cur.enabled ? '' : 'opacity:.45'}">
2884
- <label class="lock-row"><input type="checkbox" id="limCompress" ${cur.compress ? 'checked' : ''}/><span>${_t('lim_compress')}</span></label>
2885
- <div class="form-grid" style="grid-template-columns:1fr auto;align-items:center;margin-top:8px">
2886
- <label class="mem-label" style="margin:0">${_t('lim_default')}</label>
2887
- <input id="limDefault" class="inp" type="number" min="0" step="1000" value="${cur.default || ''}" placeholder="0" style="width:130px;flex:none;margin:0" />
2888
- </div>
2889
- <h2 style="margin-top:16px">${_t('lim_prov_h2')}</h2>
2890
- <div class="sub">${_t('lim_prov_sub')}</div>
2891
- <div style="margin-top:6px">${provRows}</div>
2892
- <button id="limSave" class="btn ghost" style="margin-top:12px">${_t('lim_save')}</button>
2893
- </div>`;
2894
-
2895
- const det = pane.querySelector('#limDetail');
2896
- pane.querySelector('#limOn').addEventListener('change', (e) => {
2897
- det.style.opacity = e.target.checked ? '' : '.45';
2898
- });
2899
-
2900
- pane.querySelector('#limSave').addEventListener('click', async () => {
2901
- const perProvider = {};
2902
- for (const inp of pane.querySelectorAll('.lim-prov')) {
2903
- const n = Math.round(Number(inp.value) || 0);
2904
- if (n > 0) perProvider[inp.dataset.prov] = n;
2905
- }
2906
- await beast.setLimits({
2907
- enabled: pane.querySelector('#limOn').checked,
2908
- compress: pane.querySelector('#limCompress').checked,
2909
- default: Math.round(Number(pane.querySelector('#limDefault').value) || 0),
2910
- perProvider,
2911
- });
2912
- toast(_t('lim_saved_toast'));
2913
- });
2914
- }
2915
-
2916
2950
  /* ---------------- Güvenlik: ajan onay davranışı ---------------- */
2917
2951
 
2918
2952
  async function renderSecurityPane() {
@@ -4982,6 +5016,19 @@ function onEvent(ev) {
4982
5016
  return;
4983
5017
  }
4984
5018
  if (ev.type === 'install-progress') { updateInstallPct(ev); return; }
5019
+ /* Headroom durum değişimi → açık olan sekmeyi tazele (Token Sıkıştırma / Kurulum) */
5020
+ if (ev.type === 'headroom-status') {
5021
+ try {
5022
+ const iPane = document.getElementById('tab-install');
5023
+ const hPane = document.getElementById('tab-headroom');
5024
+ const open = (!iPane || iPane.hidden) && (!hPane || hPane.hidden) ? '' : (!hPane || hPane.hidden ? 'install' : 'headroom');
5025
+ if (open) {
5026
+ clearTimeout(hrPaneTimer);
5027
+ hrPaneTimer = setTimeout(() => { (open === 'headroom' ? renderHeadroomPane() : renderInstallPane()).catch(() => {}); }, 1500);
5028
+ }
5029
+ } catch {}
5030
+ return;
5031
+ }
4985
5032
  /* EMPATİ LOOP: proaktif bildirim (masaüstü) + sekme canlı yenileme.
4986
5033
  Toast'ta yalnız haber başlığı — link/markdown kalabalığı chat mesajında. */
4987
5034
  if (ev.type === 'proactive') {
@@ -7139,6 +7186,10 @@ async function setIdeMode(on) {
7139
7186
  bcBanner();
7140
7187
  renderBcHistory(); /* soldaki geçmiş listesi: IDE'ye girişte tazelenir */
7141
7188
  codeGutterRender(); /* dosya açık olmasa bile rakamlar görünür */
7189
+ /* Expo QR daha önce üretildiyse kart geri gelsin */
7190
+ beast.ideExpQr().then((r) => {
7191
+ if (r && r.ok && r.qr) bcQrShow(r.qr, r.url);
7192
+ }).catch(() => {});
7142
7193
  } else {
7143
7194
  /* terminal bölmeye alınmadan önce kapatılır, sonra ALT DOCK'a geri taşınır */
7144
7195
  if (termOpen) termSetOpen(false);
@@ -8008,11 +8059,32 @@ async function undoTask(id) {
8008
8059
  if (els.fileTasks) els.fileTasks.addEventListener('click', () => toggleTasksPanel());
8009
8060
 
8010
8061
  $('#filePreview').addEventListener('click', async () => {
8011
- const r = await beast.idePreview().catch(() => null);
8012
- if (r && r.ok) {
8013
- if (ideModeOn() === false) setIdeMode(true);
8014
- } else {
8062
+ if (!ideModeOn()) await setIdeMode(true);
8063
+ /* proje tipi algıla: statik site ajan gerektirmez, dev-server projelerinde
8064
+ HER PROJE FARKLI KOMUTLA çalışır — komutu ajan package.json'a bakıp seçer */
8065
+ const info = await beast.ideProjInfo().catch(() => null);
8066
+ const kind = (info && info.kind) || 'unknown';
8067
+ if (kind === 'static') {
8068
+ const r = await beast.idePreview().catch(() => null);
8069
+ if (r && r.ok) return;
8015
8070
  toast((r && r.error) || 'preview açılamadı');
8071
+ return;
8072
+ }
8073
+ const ask =
8074
+ kind === 'expo'
8075
+ ? 'Preview aç — bu klasör bir Expo/React Native projesi (app.json var). Web önizlemeyi arka planda başlat: package.json scripts.web varsa `npm run web`, yoksa `npx expo start --web`. Sunucu kalkınca çalışan http://localhost:PORT adresini raporda yaz.'
8076
+ : kind === 'react-native'
8077
+ ? 'Preview aç — bu klasör bir React Native projesi. Metro sunucusunu arka planda başlat: package.json scripts.start varsa `npm start`, yoksa `npx react-native start`. Sunucu kalkınca çalışan http://localhost:PORT adresini raporda yaz.'
8078
+ : kind === 'web'
8079
+ ? 'Preview aç — bu proje dev-server ister. Doğru komutu package.json scripts\'ından seç (dev/start/serve — `npm run dev` vb.) ve arka planda başlat. Sunucu kalkınca çalışan http://localhost:PORT adresini raporda yaz.'
8080
+ : 'Preview aç — bu projenin nasıl çalıştığını package.json / konfigürasyon dosyalarından anla, doğru dev komutunu arka planda başlat ve çalışan http://localhost:PORT adresini raporda yaz.';
8081
+ if (els.bcInput) {
8082
+ els.bcInput.value = ask;
8083
+ bcInputResize();
8084
+ bcRunCurrent();
8085
+ toast('Ajan preview görevini aldı');
8086
+ } else {
8087
+ toast('Beast Code paneli yok');
8016
8088
  }
8017
8089
  });
8018
8090
  if (els.codeSave) els.codeSave.addEventListener('click', codeSave);
@@ -8777,6 +8849,10 @@ function bcIngest(ev) {
8777
8849
  /* iş bitince üretilen site/app dahili tarayıcıda CANLI açılır */
8778
8850
  bcAutoPreview(ev.url);
8779
8851
  break;
8852
+ case 'bc-exp-qr':
8853
+ /* Expo dev server exp:// adresi → telefonda Expo Go için QR kart */
8854
+ bcQrShow(ev.qr, ev.url);
8855
+ break;
8780
8856
  case 'bc-mode':
8781
8857
  /* OpenCode disiplini çalışma modu: başlıkta rozet + panelde bilgi satırı */
8782
8858
  if (els.bcTitle) {
@@ -8904,6 +8980,33 @@ if (els.bcClear) els.bcClear.addEventListener('click', () => {
8904
8980
  bcHideTodos();
8905
8981
  bcFlushStream();
8906
8982
  });
8983
+
8984
+ /* ---------- Expo Go QR kartı (yalnız Beast Code) ---------- */
8985
+ function bcQrShow(qr, url) {
8986
+ if (!els.bcQrCard || !ideModeOn() || !qr) return;
8987
+ if (els.bcQrImg) els.bcQrImg.src = qr;
8988
+ if (els.bcQrUrl) {
8989
+ els.bcQrUrl.textContent = url || '';
8990
+ els.bcQrUrl.title = url || '';
8991
+ }
8992
+ if (els.bcQrBtn) els.bcQrBtn.hidden = false;
8993
+ els.bcQrCard.hidden = false;
8994
+ }
8995
+ if (els.bcQrBtn) els.bcQrBtn.addEventListener('click', () => {
8996
+ if (!els.bcQrCard) return;
8997
+ if (els.bcQrCard.hidden) {
8998
+ beast.ideExpQr().then((r) => {
8999
+ if (r && r.ok && r.qr) bcQrShow(r.qr, r.url);
9000
+ else toast('QR henüz yok — ajan expo start çalıştırınca çıkar');
9001
+ }).catch(() => {});
9002
+ } else {
9003
+ els.bcQrCard.hidden = true;
9004
+ }
9005
+ });
9006
+ if (els.bcQrCard) {
9007
+ const qclose = els.bcQrCard.querySelector('#bcQrClose');
9008
+ if (qclose) qclose.addEventListener('click', () => { els.bcQrCard.hidden = true; });
9009
+ }
8907
9010
  if (els.bcNew) els.bcNew.addEventListener('click', async () => {
8908
9011
  if (bcRunning) { toast('Mesaj sürüyor — önce ■ ile durdur'); return; }
8909
9012
  const r = await beast.beastcodeNew().catch(() => null);
@@ -2315,8 +2315,61 @@ body.ide-mode #composerWrap { display: none; }
2315
2315
  flex-direction: column;
2316
2316
  overflow: hidden;
2317
2317
  background: var(--bg);
2318
+ position: relative;
2318
2319
  }
2319
2320
  body.ide-mode #bcPanel { display: flex; }
2321
+
2322
+ /* ---------- Expo Go QR kartı (Beast Code) ---------- */
2323
+ #bcQrCard {
2324
+ position: absolute;
2325
+ right: 14px;
2326
+ bottom: 74px;
2327
+ z-index: 40;
2328
+ width: 232px;
2329
+ padding: 10px;
2330
+ background: var(--panel);
2331
+ border: 1px solid var(--border);
2332
+ border-radius: 12px;
2333
+ box-shadow: 0 10px 34px rgba(0, 0, 0, 0.45);
2334
+ }
2335
+ .bq-head {
2336
+ display: flex;
2337
+ align-items: center;
2338
+ justify-content: space-between;
2339
+ font-size: 10px;
2340
+ letter-spacing: 2px;
2341
+ font-weight: 800;
2342
+ color: var(--muted);
2343
+ margin-bottom: 6px;
2344
+ }
2345
+ #bcQrClose {
2346
+ background: none;
2347
+ border: none;
2348
+ color: var(--muted);
2349
+ font-size: 14px;
2350
+ cursor: pointer;
2351
+ line-height: 1;
2352
+ padding: 0 2px;
2353
+ }
2354
+ #bcQrClose:hover { color: var(--text); }
2355
+ #bcQrImg {
2356
+ width: 100%;
2357
+ display: block;
2358
+ border-radius: 8px;
2359
+ background: #fff;
2360
+ padding: 6px;
2361
+ image-rendering: pixelated;
2362
+ }
2363
+ #bcQrUrl {
2364
+ font-family: Consolas, monospace;
2365
+ font-size: 10px;
2366
+ color: var(--accent);
2367
+ margin-top: 6px;
2368
+ white-space: nowrap;
2369
+ overflow: hidden;
2370
+ text-overflow: ellipsis;
2371
+ }
2372
+ .bq-hint { font-size: 10.5px; color: var(--muted); margin-top: 4px; text-align: center; }
2320
2373
  #bcHead {
2321
2374
  flex: none;
2322
2375
  display: flex;