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 +1 -1
- package/src/agent/engine.js +10 -0
- package/src/agent/headroom.js +279 -0
- package/src/agent/llm.js +17 -1
- package/src/main.js +125 -0
- package/src/preload.js +5 -0
- package/src/renderer/i18n.js +4 -2
- package/src/renderer/index.html +12 -0
- package/src/renderer/renderer.js +183 -80
- package/src/renderer/style.css +53 -0
package/package.json
CHANGED
package/src/agent/engine.js
CHANGED
|
@@ -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
|
-
|
|
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)),
|
package/src/renderer/i18n.js
CHANGED
|
@@ -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
|
|
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
|
|
800
|
+
tab_usage: 'Cost',
|
|
799
801
|
tab_logs: 'Logs',
|
|
800
802
|
tab_dash: 'Dashboard',
|
|
801
803
|
tab_limits: 'Limit',
|
package/src/renderer/index.html
CHANGED
|
@@ -164,11 +164,21 @@
|
|
|
164
164
|
<button id="codeShow" title="Editörü göster" data-i18n-title="tipCodeShow" hidden>▣︎</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>▣︎</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">×</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>
|
package/src/renderer/renderer.js
CHANGED
|
@@ -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
|
-
|
|
8012
|
-
|
|
8013
|
-
|
|
8014
|
-
|
|
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);
|
package/src/renderer/style.css
CHANGED
|
@@ -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;
|