blyuecode 0.7.2 → 0.8.3
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/dist/core/deps.js +104 -0
- package/dist/core/image.js +69 -0
- package/dist/core/media.js +184 -0
- package/dist/core/player.js +208 -0
- package/dist/core/videoPlayer.js +44 -0
- package/dist/core/ytdlp.js +211 -0
- package/dist/ui/Chat.js +512 -1
- package/dist/ui/InputBar.js +8 -2
- package/dist/ui/MascotPrompt.js +27 -6
- package/dist/ui/splash.js +2 -2
- package/package.json +1 -1
package/dist/ui/Chat.js
CHANGED
|
@@ -26,8 +26,62 @@ import { AuditPanel } from './AuditPanel.js';
|
|
|
26
26
|
import { appendMemory } from '../core/memory.js';
|
|
27
27
|
import { audit } from '../core/audit.js';
|
|
28
28
|
import { listSkills, findSkill, matchSkills, ensureSkillsDir, } from '../core/skills.js';
|
|
29
|
+
import { generateImage } from '../core/image.js';
|
|
30
|
+
import { checkMediaDeps, installMediaDeps } from '../core/deps.js';
|
|
31
|
+
import { searchYouTube, downloadAudio, downloadVideo, parseQuality, estimateSizeMb, } from '../core/media.js';
|
|
32
|
+
import { player } from '../core/player.js';
|
|
33
|
+
import { playVideo } from '../core/videoPlayer.js';
|
|
34
|
+
import { updateYtdlp, ytdlpVersion, ytdlpLogPath, } from '../core/ytdlp.js';
|
|
29
35
|
let nextId = 0;
|
|
30
36
|
const mkItem = (kind, text) => ({ kind, text, id: nextId++ });
|
|
37
|
+
const fmtTime = (s) => {
|
|
38
|
+
const h = Math.floor(s / 3600);
|
|
39
|
+
const m = Math.floor((s % 3600) / 60);
|
|
40
|
+
const sec = Math.floor(s % 60);
|
|
41
|
+
return h > 0 ? h + ':' + String(m).padStart(2, '0') + ':' + String(sec).padStart(2, '0') : m + ':' + String(sec).padStart(2, '0');
|
|
42
|
+
};
|
|
43
|
+
const parseYtdlpProgress = (line) => {
|
|
44
|
+
const m = line.match(/\[download\]\s+([\d.]+)%\s+of\s+~?\s*([\d.]+\s*\w+)(?:\s+at\s+([\d.]+\s*\w+\/s))?(?:\s+ETA\s+([\d:]+))?/);
|
|
45
|
+
if (!m)
|
|
46
|
+
return null;
|
|
47
|
+
const [, pct, size, speed, eta] = m;
|
|
48
|
+
const pctNum = Math.min(100, Math.max(0, parseFloat(pct)));
|
|
49
|
+
const bars = 20;
|
|
50
|
+
const filled = Math.round((pctNum / 100) * bars);
|
|
51
|
+
const bar = '━'.repeat(filled) + '─'.repeat(bars - filled);
|
|
52
|
+
const parts = [pct + '%', size.trim()];
|
|
53
|
+
if (speed)
|
|
54
|
+
parts.push(speed.trim());
|
|
55
|
+
if (eta)
|
|
56
|
+
parts.push('ETA ' + eta);
|
|
57
|
+
return bar + ' ' + parts.join(' · ');
|
|
58
|
+
};
|
|
59
|
+
const parsePkgProgress = (line) => {
|
|
60
|
+
let m = line.match(/(\d+)%\s*\[/);
|
|
61
|
+
if (m) {
|
|
62
|
+
const pct = Math.min(100, Math.max(0, parseInt(m[1], 10)));
|
|
63
|
+
const bars = 20;
|
|
64
|
+
const filled = Math.round((pct / 100) * bars);
|
|
65
|
+
return '━'.repeat(filled) + '─'.repeat(bars - filled) + ' ' + pct + '%';
|
|
66
|
+
}
|
|
67
|
+
m = line.match(/^Get:\d+\s+\S+\s+\[([\d.,]+\s*\w+)\]/);
|
|
68
|
+
if (m)
|
|
69
|
+
return '· fetching ' + m[1];
|
|
70
|
+
m = line.match(/^Setting up ([^ ]+)/);
|
|
71
|
+
if (m)
|
|
72
|
+
return '· unpacking ' + m[1];
|
|
73
|
+
m = line.match(/^Unpacking ([^ ]+)/);
|
|
74
|
+
if (m)
|
|
75
|
+
return '· unpacking ' + m[1];
|
|
76
|
+
m = line.match(/^(Selecting|Preparing) ([^ ]+)/);
|
|
77
|
+
if (m)
|
|
78
|
+
return '· ' + m[2];
|
|
79
|
+
if (line.startsWith('Reading package') || line.startsWith('Building dependency'))
|
|
80
|
+
return '· ' + line.slice(0, 60);
|
|
81
|
+
if (line.includes('Setting up') || line.includes('unpack'))
|
|
82
|
+
return '· ' + line.slice(0, 60);
|
|
83
|
+
return null;
|
|
84
|
+
};
|
|
31
85
|
const HistoryLine = ({ item }) => {
|
|
32
86
|
if (item.kind === 'user') {
|
|
33
87
|
return (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: pastelAt(0.08), bold: true, children: '❯ ' }), _jsx(Text, { color: theme.text, bold: true, children: item.text })] }));
|
|
@@ -75,6 +129,13 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
75
129
|
const [streaming, setStreaming] = useState('');
|
|
76
130
|
const [overlay, setOverlay] = useState('none');
|
|
77
131
|
const [pendingSkill, setPendingSkill] = useState(null);
|
|
132
|
+
const [pendingMedia, setPendingMedia] = useState(null);
|
|
133
|
+
const [pendingInstall, setPendingInstall] = useState(null);
|
|
134
|
+
const [mediaStatus, setMediaStatus] = useState(null);
|
|
135
|
+
const [playerActive, setPlayerActive] = useState(false);
|
|
136
|
+
const [playProgress, setPlayProgress] = useState(null);
|
|
137
|
+
const [pendingVideoCleanup, setPendingVideoCleanup] = useState(null);
|
|
138
|
+
const [pendingVideoQuality, setPendingVideoQuality] = useState(null);
|
|
78
139
|
const [permReq, setPermReq] = useState(null);
|
|
79
140
|
const [currentModel, setCurrentModel] = useState(process.env.MODEL || '');
|
|
80
141
|
const [agent, setAgent] = useState(() => new Agent());
|
|
@@ -106,6 +167,73 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
106
167
|
const slowColor = useSlowColor();
|
|
107
168
|
useInput((inputChar, key) => {
|
|
108
169
|
bumpActivity();
|
|
170
|
+
if (pendingVideoQuality) {
|
|
171
|
+
const pt = pendingVideoQuality.track;
|
|
172
|
+
const pick = (q) => {
|
|
173
|
+
setPendingVideoQuality(null);
|
|
174
|
+
setHistory((h) => [...h, mkItem('tool', 'quality: ' + q + ' · download and play? [y] yes · [n] cancel')]);
|
|
175
|
+
setPendingMedia({ kind: 'video', track: pt, quality: q });
|
|
176
|
+
};
|
|
177
|
+
if (inputChar === '1') {
|
|
178
|
+
pick('360p');
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (inputChar === '2') {
|
|
182
|
+
pick('480p');
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (inputChar === '3') {
|
|
186
|
+
pick('720p');
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (inputChar === '4') {
|
|
190
|
+
pick('1080p');
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (inputChar === '5') {
|
|
194
|
+
pick('best');
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (inputChar === 'n' || inputChar === 'N' || key.escape) {
|
|
198
|
+
setPendingVideoQuality(null);
|
|
199
|
+
setInput('');
|
|
200
|
+
setHistory((h) => [...h, mkItem('tool', 'cancelled')]);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (pendingMedia) {
|
|
206
|
+
if (inputChar === 'y' || inputChar === 'Y') {
|
|
207
|
+
const pm = pendingMedia;
|
|
208
|
+
setPendingMedia(null);
|
|
209
|
+
setInput('');
|
|
210
|
+
void runPendingMedia(pm);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (inputChar === 'n' || inputChar === 'N' || key.escape) {
|
|
214
|
+
setPendingMedia(null);
|
|
215
|
+
setInput('');
|
|
216
|
+
setHistory((h) => [...h, mkItem('tool', 'cancelled')]);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (pendingInstall) {
|
|
222
|
+
if (inputChar === 'y' || inputChar === 'Y') {
|
|
223
|
+
const cmd = pendingInstall;
|
|
224
|
+
setPendingInstall(null);
|
|
225
|
+
setInput('');
|
|
226
|
+
void runPendingInstall(cmd);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (inputChar === 'n' || inputChar === 'N' || key.escape) {
|
|
230
|
+
setPendingInstall(null);
|
|
231
|
+
setInput('');
|
|
232
|
+
setHistory((h) => [...h, mkItem('tool', 'cancelled')]);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
109
237
|
if (permReq)
|
|
110
238
|
return;
|
|
111
239
|
if (key.ctrl && inputChar === 'l')
|
|
@@ -126,6 +254,100 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
126
254
|
return;
|
|
127
255
|
setHistory((h) => [...h, mkItem(kind, text)]);
|
|
128
256
|
};
|
|
257
|
+
const runPendingMedia = async (pm) => {
|
|
258
|
+
if (pm.kind === 'audio') {
|
|
259
|
+
setMediaStatus('downloading: ' + pm.track.title + '…');
|
|
260
|
+
try {
|
|
261
|
+
const d = await downloadAudio(pm.track, (p) => {
|
|
262
|
+
const line = p.line;
|
|
263
|
+
if (line) {
|
|
264
|
+
const pretty = parseYtdlpProgress(line);
|
|
265
|
+
if (pretty)
|
|
266
|
+
setMediaStatus('♪ ' + pretty);
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
setMediaStatus('loading into player…');
|
|
270
|
+
await player.play(d);
|
|
271
|
+
setMediaStatus(null);
|
|
272
|
+
setHistory((h) => [...h, mkItem('tool', '▶ ' + d.title + ' (deletes after — /loop /keep)')]);
|
|
273
|
+
}
|
|
274
|
+
catch (e) {
|
|
275
|
+
setMediaStatus(null);
|
|
276
|
+
const msg = e?.message || String(e);
|
|
277
|
+
if (/403|forbidden|stale/i.test(msg)) {
|
|
278
|
+
setHistory((h) => [
|
|
279
|
+
...h,
|
|
280
|
+
mkItem('error', msg),
|
|
281
|
+
mkItem('tool', 'run /update-ytdlp to fix (stale yt-dlp)'),
|
|
282
|
+
]);
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
setHistory((h) => [...h, mkItem('error', msg)]);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
setMediaStatus('downloading ' + pm.quality + ': ' + pm.track.title + '…');
|
|
291
|
+
try {
|
|
292
|
+
const v = await downloadVideo(pm.track, pm.quality, (p) => {
|
|
293
|
+
const line = p.line;
|
|
294
|
+
if (line) {
|
|
295
|
+
const pretty = parseYtdlpProgress(line);
|
|
296
|
+
if (pretty)
|
|
297
|
+
setMediaStatus('♪ ' + pretty);
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
setMediaStatus('opening player…');
|
|
301
|
+
const { cleanup, notice } = await playVideo(v);
|
|
302
|
+
setMediaStatus(null);
|
|
303
|
+
setPendingVideoCleanup(() => cleanup);
|
|
304
|
+
if (notice) {
|
|
305
|
+
setHistory((h) => [...h, mkItem('tool', '▶ ' + v.title), mkItem('tool', notice)]);
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
setHistory((h) => [...h, mkItem('tool', 'video ended · file cleaned up')]);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
catch (e) {
|
|
312
|
+
setMediaStatus(null);
|
|
313
|
+
setHistory((h) => [...h, mkItem('error', e?.message || String(e))]);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
const runPendingInstall = async (cmd) => {
|
|
318
|
+
setHistory((h) => [...h, mkItem('tool', 'running: ' + cmd)]);
|
|
319
|
+
setLoading(true);
|
|
320
|
+
const r = await installMediaDeps();
|
|
321
|
+
setLoading(false);
|
|
322
|
+
if (r.ok)
|
|
323
|
+
setHistory((h) => [...h, mkItem('tool', '✓ media tools installed')]);
|
|
324
|
+
else
|
|
325
|
+
setHistory((h) => [...h, mkItem('error', 'install failed: ' + r.output.slice(-400))]);
|
|
326
|
+
};
|
|
327
|
+
useEffect(() => {
|
|
328
|
+
const sync = () => {
|
|
329
|
+
try {
|
|
330
|
+
const st = player.getState();
|
|
331
|
+
setPlayerActive(st.playing);
|
|
332
|
+
if (st.playing && st.duration > 0 && st.position >= 0) {
|
|
333
|
+
setPlayProgress({ pos: st.position, dur: st.duration });
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
setPlayProgress(null);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
catch { }
|
|
340
|
+
};
|
|
341
|
+
player.on('update', sync);
|
|
342
|
+
player.on('queue-empty', sync);
|
|
343
|
+
player.on('exit', sync);
|
|
344
|
+
sync();
|
|
345
|
+
return () => {
|
|
346
|
+
player.removeListener('update', sync);
|
|
347
|
+
player.removeListener('queue-empty', sync);
|
|
348
|
+
player.removeListener('exit', sync);
|
|
349
|
+
};
|
|
350
|
+
}, []);
|
|
129
351
|
const handleSubmit = async (text) => {
|
|
130
352
|
bumpActivity();
|
|
131
353
|
if (!text.trim() || loading)
|
|
@@ -219,6 +441,284 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
219
441
|
]);
|
|
220
442
|
return;
|
|
221
443
|
}
|
|
444
|
+
if (trimmed === '/update-ytdlp') {
|
|
445
|
+
setInput('');
|
|
446
|
+
const before = await ytdlpVersion();
|
|
447
|
+
setHistory((h) => [...h, mkItem('tool', 'yt-dlp version: ' + before + ' · updating…')]);
|
|
448
|
+
setLoading(true);
|
|
449
|
+
const r = await updateYtdlp();
|
|
450
|
+
setLoading(false);
|
|
451
|
+
const after = await ytdlpVersion();
|
|
452
|
+
if (r.ok) {
|
|
453
|
+
setHistory((h) => [...h, mkItem('tool', '✓ yt-dlp ' + before + ' → ' + after)]);
|
|
454
|
+
}
|
|
455
|
+
else {
|
|
456
|
+
setHistory((h) => [...h, mkItem('error', 'update failed: ' + r.output.slice(-300))]);
|
|
457
|
+
}
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
if (trimmed === '/ytdlp-log') {
|
|
461
|
+
setInput('');
|
|
462
|
+
const { readYtdlpLog } = await import('../core/ytdlp.js');
|
|
463
|
+
const lines = readYtdlpLog(40);
|
|
464
|
+
if (lines.length === 0) {
|
|
465
|
+
setHistory((h) => [...h, mkItem('tool', 'no yt-dlp errors logged')]);
|
|
466
|
+
}
|
|
467
|
+
else {
|
|
468
|
+
setHistory((h) => [
|
|
469
|
+
...h,
|
|
470
|
+
mkItem('tool', 'yt-dlp log: ' + ytdlpLogPath()),
|
|
471
|
+
...lines.map((l) => mkItem('tool', ' ' + l.slice(0, 180))),
|
|
472
|
+
]);
|
|
473
|
+
}
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
if (trimmed === '/install-media') {
|
|
477
|
+
setInput('');
|
|
478
|
+
const deps = await checkMediaDeps();
|
|
479
|
+
if (deps.allPresent) {
|
|
480
|
+
setHistory((h) => [...h, mkItem('tool', 'media tools already installed')]);
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
const plat = deps.platform;
|
|
484
|
+
if (!plat.installCmd) {
|
|
485
|
+
setHistory((h) => [...h, mkItem('error', 'unknown platform — install yt-dlp ffmpeg mpv manually')]);
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
setHistory((h) => [
|
|
489
|
+
...h,
|
|
490
|
+
mkItem('tool', 'will run: ' + plat.installCmd),
|
|
491
|
+
mkItem('tool', '~' + plat.approximateSizeMb + ' MB via ' + plat.updateHint),
|
|
492
|
+
mkItem('tool', 'starting install…'),
|
|
493
|
+
]);
|
|
494
|
+
setLoading(true);
|
|
495
|
+
let lastLine = '';
|
|
496
|
+
const r = await installMediaDeps((line) => {
|
|
497
|
+
const pretty = parsePkgProgress(line);
|
|
498
|
+
if (pretty && pretty !== lastLine) {
|
|
499
|
+
lastLine = pretty;
|
|
500
|
+
setHistory((h) => [...h, mkItem('tool', ' ' + pretty)]);
|
|
501
|
+
}
|
|
502
|
+
else if (!pretty && line.length < 100) {
|
|
503
|
+
setHistory((h) => [...h, mkItem('tool', ' ' + line.slice(0, 100))]);
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
setLoading(false);
|
|
507
|
+
if (r.ok) {
|
|
508
|
+
setHistory((h) => [...h, mkItem('tool', '✓ media tools installed')]);
|
|
509
|
+
}
|
|
510
|
+
else {
|
|
511
|
+
setHistory((h) => [...h, mkItem('error', 'install failed: ' + r.output.slice(-400))]);
|
|
512
|
+
}
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
if (trimmed.startsWith('/image ')) {
|
|
516
|
+
setInput('');
|
|
517
|
+
const prompt = trimmed.slice(7).trim();
|
|
518
|
+
if (!prompt) {
|
|
519
|
+
setHistory((h) => [...h, mkItem('error', 'usage: /image <prompt>')]);
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
setLoading(true);
|
|
523
|
+
setHistory((h) => [...h, mkItem('tool', 'generating image: ' + prompt + '…')]);
|
|
524
|
+
try {
|
|
525
|
+
const r = await generateImage(prompt);
|
|
526
|
+
setHistory((h) => [...h, mkItem('tool', 'saved: ' + r.path), mkItem('tool', 'source: ' + r.url)]);
|
|
527
|
+
}
|
|
528
|
+
catch (e) {
|
|
529
|
+
setHistory((h) => [...h, mkItem('error', e?.message || String(e))]);
|
|
530
|
+
}
|
|
531
|
+
setLoading(false);
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
if (trimmed.startsWith('/play ')) {
|
|
535
|
+
setInput('');
|
|
536
|
+
const q = trimmed.slice(6).trim();
|
|
537
|
+
if (!q) {
|
|
538
|
+
setHistory((h) => [...h, mkItem('error', 'usage: /play <query>')]);
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
setMediaStatus('searching: ' + q + '…');
|
|
542
|
+
try {
|
|
543
|
+
const deps = await checkMediaDeps();
|
|
544
|
+
if (!deps.allPresent) {
|
|
545
|
+
setMediaStatus(null);
|
|
546
|
+
setHistory((h) => [...h,
|
|
547
|
+
mkItem('tool', 'missing media tools: ' + deps.missing.join(', ')),
|
|
548
|
+
mkItem('tool', 'run /install-media to install them'),
|
|
549
|
+
]);
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
const track = await searchYouTube(q);
|
|
553
|
+
setHistory((h) => [
|
|
554
|
+
...h,
|
|
555
|
+
mkItem('tool', 'found: ' + track.title + ' · ' + fmtTime(track.duration) + (track.uploader ? ' · ' + track.uploader : '')),
|
|
556
|
+
mkItem('tool', 'download and play? [y] yes · [n] cancel'),
|
|
557
|
+
]);
|
|
558
|
+
setMediaStatus(null);
|
|
559
|
+
setPendingMedia({ kind: 'audio', track });
|
|
560
|
+
}
|
|
561
|
+
catch (e) {
|
|
562
|
+
setMediaStatus(null);
|
|
563
|
+
const msg = e?.message || String(e);
|
|
564
|
+
if (/403|forbidden|stale/i.test(msg)) {
|
|
565
|
+
setHistory((h) => [
|
|
566
|
+
...h,
|
|
567
|
+
mkItem('error', msg),
|
|
568
|
+
mkItem('tool', 'try /update-ytdlp to fix'),
|
|
569
|
+
]);
|
|
570
|
+
}
|
|
571
|
+
else {
|
|
572
|
+
setHistory((h) => [...h, mkItem('error', msg)]);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
if (trimmed.startsWith('/video ')) {
|
|
578
|
+
setInput('');
|
|
579
|
+
const rest = trimmed.slice(7).trim();
|
|
580
|
+
if (!rest) {
|
|
581
|
+
setHistory((h) => [...h, mkItem('error', 'usage: /video <query> [quality]')]);
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
const parts = rest.split(/\s+/);
|
|
585
|
+
const last = parts[parts.length - 1];
|
|
586
|
+
const qualityPattern = /^(360p?|480p?|720p?|1080p?|best)$/i;
|
|
587
|
+
const qualityPattern2 = /^\d+p?$/i.test(last);
|
|
588
|
+
const looksLikeQuality = qualityPattern.test(last) || qualityPattern2;
|
|
589
|
+
const hasExplicit = qualityPattern.test(last) && parts.length > 1;
|
|
590
|
+
const quality = hasExplicit ? parseQuality(last) : '720p';
|
|
591
|
+
const query = hasExplicit ? parts.slice(0, -1).join(' ') : rest;
|
|
592
|
+
setMediaStatus('searching: ' + query + '…');
|
|
593
|
+
try {
|
|
594
|
+
const deps = await checkMediaDeps();
|
|
595
|
+
if (!deps.allPresent) {
|
|
596
|
+
setMediaStatus(null);
|
|
597
|
+
setHistory((h) => [...h,
|
|
598
|
+
mkItem('tool', 'missing media tools: ' + deps.missing.join(', ')),
|
|
599
|
+
mkItem('tool', 'run /install-media to install them (approval required)'),
|
|
600
|
+
]);
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
const t = await searchYouTube(query);
|
|
604
|
+
const dur = t.duration;
|
|
605
|
+
const meta = fmtTime(dur) + (t.uploader ? ' · ' + t.uploader : '');
|
|
606
|
+
setMediaStatus(null);
|
|
607
|
+
if (hasExplicit) {
|
|
608
|
+
setHistory((h) => [
|
|
609
|
+
...h,
|
|
610
|
+
mkItem('tool', 'found: ' + t.title + ' · ' + meta),
|
|
611
|
+
mkItem('tool', 'download ' + quality + ' and play? [y] yes · [n] cancel'),
|
|
612
|
+
]);
|
|
613
|
+
setPendingMedia({ kind: 'video', track: t, quality });
|
|
614
|
+
}
|
|
615
|
+
else {
|
|
616
|
+
setHistory((h) => [
|
|
617
|
+
...h,
|
|
618
|
+
mkItem('tool', 'found: ' + t.title + ' · ' + meta),
|
|
619
|
+
mkItem('tool', 'choose quality:'),
|
|
620
|
+
mkItem('tool', ' [1] 360p ~' + estimateSizeMb(dur, '360p') + ' MB'),
|
|
621
|
+
mkItem('tool', ' [2] 480p ~' + estimateSizeMb(dur, '480p') + ' MB'),
|
|
622
|
+
mkItem('tool', ' [3] 720p ~' + estimateSizeMb(dur, '720p') + ' MB'),
|
|
623
|
+
mkItem('tool', ' [4] 1080p ~' + estimateSizeMb(dur, '1080p') + ' MB'),
|
|
624
|
+
mkItem('tool', ' [5] best ~' + estimateSizeMb(dur, 'best') + ' MB'),
|
|
625
|
+
mkItem('tool', 'press 1-5 · [n] cancel'),
|
|
626
|
+
]);
|
|
627
|
+
setPendingVideoQuality({ track: t });
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
catch (e) {
|
|
631
|
+
setMediaStatus(null);
|
|
632
|
+
setHistory((h) => [...h, mkItem('error', e?.message || String(e))]);
|
|
633
|
+
}
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
if (trimmed === '/cleanup') {
|
|
637
|
+
setInput('');
|
|
638
|
+
if (pendingVideoCleanup) {
|
|
639
|
+
pendingVideoCleanup();
|
|
640
|
+
setPendingVideoCleanup(null);
|
|
641
|
+
setHistory((h) => [...h, mkItem('tool', 'video file cleaned up')]);
|
|
642
|
+
}
|
|
643
|
+
else {
|
|
644
|
+
setHistory((h) => [...h, mkItem('tool', 'nothing to clean up')]);
|
|
645
|
+
}
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
if (trimmed === '/pause') {
|
|
649
|
+
setInput('');
|
|
650
|
+
await player.togglePause();
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
if (trimmed === '/next') {
|
|
654
|
+
setInput('');
|
|
655
|
+
await player.next();
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
if (trimmed === '/stop') {
|
|
659
|
+
setInput('');
|
|
660
|
+
await player.stop();
|
|
661
|
+
setHistory((h) => [...h, mkItem('tool', 'stopped')]);
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
if (trimmed === '/loop') {
|
|
665
|
+
setInput('');
|
|
666
|
+
const on = !player.isLoop();
|
|
667
|
+
player.setLoop(on);
|
|
668
|
+
setHistory((h) => [...h, mkItem('tool', on ? 'loop on' : 'loop off')]);
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
if (trimmed === '/keep') {
|
|
672
|
+
setInput('');
|
|
673
|
+
const st = player.getState();
|
|
674
|
+
if (st.current) {
|
|
675
|
+
st.current.keep = true;
|
|
676
|
+
setHistory((h) => [...h, mkItem('tool', 'will keep: ' + st.current.title)]);
|
|
677
|
+
}
|
|
678
|
+
else
|
|
679
|
+
setHistory((h) => [...h, mkItem('tool', 'nothing playing')]);
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
if (trimmed === '/queue') {
|
|
683
|
+
setInput('');
|
|
684
|
+
const st = player.getState();
|
|
685
|
+
if (!st.current && st.queue.length === 0) {
|
|
686
|
+
setHistory((h) => [...h, mkItem('tool', 'nothing playing')]);
|
|
687
|
+
}
|
|
688
|
+
else {
|
|
689
|
+
const lines = [];
|
|
690
|
+
if (st.current)
|
|
691
|
+
lines.push('▶ ' + st.current.title);
|
|
692
|
+
for (const t of st.queue)
|
|
693
|
+
lines.push(' ⏸ ' + t.title);
|
|
694
|
+
setHistory((h) => [...h, ...lines.map((l) => mkItem('tool', l))]);
|
|
695
|
+
}
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
if (trimmed.startsWith('/remember ')) {
|
|
699
|
+
setInput('');
|
|
700
|
+
const text = trimmed.slice(10).trim();
|
|
701
|
+
if (!text) {
|
|
702
|
+
setHistory((h) => [...h, mkItem('error', 'usage: /remember <text>')]);
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
let section = 'Notes';
|
|
706
|
+
let note = text;
|
|
707
|
+
const pipe = text.indexOf('|');
|
|
708
|
+
if (pipe > 0) {
|
|
709
|
+
note = text.slice(0, pipe).trim();
|
|
710
|
+
section = text.slice(pipe + 1).trim() || 'Notes';
|
|
711
|
+
}
|
|
712
|
+
try {
|
|
713
|
+
appendMemory(note, section);
|
|
714
|
+
audit('memory_write', { section, note });
|
|
715
|
+
setHistory((h) => [...h, mkItem('tool', 'remembered (' + section + '): ' + note)]);
|
|
716
|
+
}
|
|
717
|
+
catch (e) {
|
|
718
|
+
setHistory((h) => [...h, mkItem('error', e?.message || String(e))]);
|
|
719
|
+
}
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
222
722
|
if (trimmed === '/debug') {
|
|
223
723
|
setInput('');
|
|
224
724
|
const lines = readDebug();
|
|
@@ -273,6 +773,17 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
273
773
|
mkItem('tool', ' /skill <n> force-activate a skill for next msg'),
|
|
274
774
|
mkItem('tool', ' /compact summarize history to free context'),
|
|
275
775
|
mkItem('tool', ' /debug show last request to provider'),
|
|
776
|
+
mkItem('tool', ' /image <p> generate an image'),
|
|
777
|
+
mkItem('tool', ' /play <q> search + play audio (auto-delete)'),
|
|
778
|
+
mkItem('tool', ' /video <q> [quality] download + play video'),
|
|
779
|
+
mkItem('tool', ' /pause /next /stop /queue playback control'),
|
|
780
|
+
mkItem('tool', ' /loop /keep playback behavior'),
|
|
781
|
+
mkItem('tool', ' /autoplay keep music going when queue ends'),
|
|
782
|
+
mkItem('tool', ' /cleanup delete the video file (Termux, after player closes)'),
|
|
783
|
+
mkItem('tool', ' /install-media install yt-dlp + ffmpeg + mpv'),
|
|
784
|
+
mkItem('tool', ' /update-ytdlp update yt-dlp (fixes HTTP 403)'),
|
|
785
|
+
mkItem('tool', ' /ytdlp-log show last yt-dlp errors'),
|
|
786
|
+
mkItem('tool', ' /remember <text> [| section] append to memory'),
|
|
276
787
|
mkItem('tool', ' /clear clear session and permissions'),
|
|
277
788
|
mkItem('tool', ' /help show this list'),
|
|
278
789
|
]);
|
|
@@ -391,5 +902,5 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
391
902
|
const shortModel = currentModel.length > maxModelLen
|
|
392
903
|
? '…' + currentModel.slice(-(maxModelLen - 1))
|
|
393
904
|
: currentModel || 'unset';
|
|
394
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: history, children: (item) => _jsx(HistoryLine, { item: item }, item.id) }), streaming ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.text, children: streaming }) })) : loading ? (_jsx(Box, { marginTop: 1, children: _jsx(Spinner, {}) })) : null, _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(InputBar, { value: input, onChange: setInput, onSubmit: handleSubmit, loading: loading, mascotState: mascotState, elapsedSec: elapsedSec }), _jsxs(Text, { children: [_jsx(Text, { color: slowColor, children: " /model" }), _jsx(Text, { color: theme.muted, children: " \u00B7 " }), _jsx(Text, { color: slowColor, children: "/settings" }), _jsx(Text, { color: theme.muted, children: " \u00B7 " }), _jsx(Text, { color: slowColor, children: "/compact" }), _jsx(Text, { color: theme.muted, children: " \u00B7 " }), _jsx(Text, { color: theme.text, children: shortModel })] })] })] }));
|
|
905
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: history, children: (item) => _jsx(HistoryLine, { item: item }, item.id) }), mediaStatus ? (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.accent, children: ["\u266A ", mediaStatus] }) })) : streaming ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.text, children: streaming }) })) : loading ? (_jsx(Box, { marginTop: 1, children: _jsx(Spinner, {}) })) : null, _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(InputBar, { value: input, onChange: setInput, onSubmit: handleSubmit, loading: loading, mascotState: playerActive ? 'playing' : mascotState, progress: playProgress, elapsedSec: elapsedSec }), _jsxs(Text, { children: [_jsx(Text, { color: slowColor, children: " /model" }), _jsx(Text, { color: theme.muted, children: " \u00B7 " }), _jsx(Text, { color: slowColor, children: "/settings" }), _jsx(Text, { color: theme.muted, children: " \u00B7 " }), _jsx(Text, { color: slowColor, children: "/compact" }), _jsx(Text, { color: theme.muted, children: " \u00B7 " }), _jsx(Text, { color: theme.text, children: shortModel })] })] })] }));
|
|
395
906
|
};
|
package/dist/ui/InputBar.js
CHANGED
|
@@ -8,7 +8,7 @@ import { MascotPrompt } from './MascotPrompt.js';
|
|
|
8
8
|
import { theme } from './theme.js';
|
|
9
9
|
import { useSlowColor } from './useSlowColor.js';
|
|
10
10
|
const DOTS = ['●○○○', '○●○○', '○○●○', '○○○●'];
|
|
11
|
-
export const InputBar = ({ value, onChange, onSubmit, loading, mascotState, awaitingPermission, elapsedSec = 0, }) => {
|
|
11
|
+
export const InputBar = ({ value, onChange, onSubmit, loading, mascotState, awaitingPermission, elapsedSec = 0, progress, }) => {
|
|
12
12
|
const { stdout } = useStdout();
|
|
13
13
|
const width = Math.max(24, stdout?.columns ?? 50);
|
|
14
14
|
const pastel = useSlowColor();
|
|
@@ -28,5 +28,11 @@ export const InputBar = ({ value, onChange, onSubmit, loading, mascotState, awai
|
|
|
28
28
|
const label = awaitingPermission ? 'waiting' : loading ? 'thinking' : 'blyue';
|
|
29
29
|
const status = loading ? ' ' + DOTS[dotI] + ' ' : '';
|
|
30
30
|
const botLen = Math.max(0, width - 2 - status.length);
|
|
31
|
-
|
|
31
|
+
const showProgress = !!(progress && progress.dur > 0 && progress.pos >= 0);
|
|
32
|
+
const fillRatio = showProgress
|
|
33
|
+
? Math.min(1, Math.max(0, progress.pos / progress.dur))
|
|
34
|
+
: 0;
|
|
35
|
+
const fillLen = showProgress ? Math.floor(botLen * fillRatio) : 0;
|
|
36
|
+
const emptyLen = showProgress ? Math.max(0, botLen - fillLen) : botLen;
|
|
37
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(MascotPrompt, { state: state }), _jsx(RocketBar, { label: label, loading: loading, width: width }), _jsxs(Box, { children: [_jsx(Text, { color: pastel, bold: true, children: '❯ ' }), hasText ? (_jsx(TextInput, { value: value, onChange: onChange, onSubmit: onSubmit })) : (_jsxs(_Fragment, { children: [_jsx(Placeholder, {}), _jsx(TextInput, { value: value, onChange: onChange, onSubmit: onSubmit })] }))] }), _jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '╰' }), showProgress ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: pastel, bold: true, children: '━'.repeat(fillLen) }), _jsx(Text, { color: theme.muted, children: '─'.repeat(emptyLen) })] })) : (_jsx(Text, { color: theme.muted, children: '─'.repeat(botLen) })), loading && _jsx(Text, { color: pastel, children: status }), _jsx(Text, { color: theme.muted, children: "\u256F" })] })] }));
|
|
32
38
|
};
|
package/dist/ui/MascotPrompt.js
CHANGED
|
@@ -22,13 +22,29 @@ const FACES = {
|
|
|
22
22
|
granted: ['◉‿◉', '◉▽◉', '◉‿◉'],
|
|
23
23
|
denied: ['◔︵◔', '◔_◔', '◔︵◔'],
|
|
24
24
|
impatient: ['◕~◕', '⊙_⊙', '◕~◕', '⊙_⊙'],
|
|
25
|
+
playing: [],
|
|
25
26
|
};
|
|
26
27
|
const SPEEDS = {
|
|
27
28
|
idle: 500, typing: 700, thinking: 120,
|
|
28
29
|
success: 300, error: 400, jump: 70,
|
|
29
30
|
read: 220, write: 260, patch: 260, list: 260, command: 220,
|
|
30
|
-
automate: 200, permission: 450, granted: 180, denied: 350,
|
|
31
|
+
automate: 200, permission: 450, granted: 180, denied: 350,
|
|
32
|
+
impatient: 300, playing: 240,
|
|
31
33
|
};
|
|
34
|
+
const MUSIC_FRAMES = [
|
|
35
|
+
{ top: ' ', mid: ' ', face: '◉◡◉' },
|
|
36
|
+
{ top: ' ♪ ', mid: ' ', face: '◉◡◉' },
|
|
37
|
+
{ top: ' ', mid: ' ♪ ', face: '◉‿◉' },
|
|
38
|
+
{ top: ' ', mid: '♫ ', face: '◕‿◕' },
|
|
39
|
+
{ top: ' ♫', mid: ' ', face: '◕ω◕' },
|
|
40
|
+
{ top: ' ', mid: ' ♪', face: '◉‿◉' },
|
|
41
|
+
{ top: '♪ ', mid: ' ♪ ', face: '◉◡◉' },
|
|
42
|
+
{ top: ' ', mid: ' ', face: '◉◡◉' },
|
|
43
|
+
{ top: ' ♫ ', mid: ' ', face: '◕‿◕' },
|
|
44
|
+
{ top: ' ', mid: '♪ ', face: '◉◠◉' },
|
|
45
|
+
{ top: ' ♪', mid: ' ', face: '◉◡◉' },
|
|
46
|
+
{ top: ' ', mid: ' ♫', face: '◕ᴗ◕' },
|
|
47
|
+
];
|
|
32
48
|
const DROWSY_FACE = '–_–';
|
|
33
49
|
const SLEEPING_FACE = '-‿-';
|
|
34
50
|
const TOP_ROWS = 2;
|
|
@@ -61,7 +77,6 @@ function semanticColor(state) {
|
|
|
61
77
|
return theme.warning;
|
|
62
78
|
}
|
|
63
79
|
export const MascotPrompt = ({ state, keepAwake = false, }) => {
|
|
64
|
-
const frames = FACES[state];
|
|
65
80
|
const [i, setI] = useState(0);
|
|
66
81
|
const thresholds = defaultIdleThresholds();
|
|
67
82
|
const idle = useIdle({
|
|
@@ -69,19 +84,25 @@ export const MascotPrompt = ({ state, keepAwake = false, }) => {
|
|
|
69
84
|
disabled: keepAwake || idleDisabled(),
|
|
70
85
|
});
|
|
71
86
|
const slowColor = useSlowColor();
|
|
87
|
+
const frameCount = state === 'playing' ? MUSIC_FRAMES.length : FACES[state].length;
|
|
72
88
|
useEffect(() => {
|
|
73
89
|
setI(0);
|
|
74
|
-
if (
|
|
90
|
+
if (frameCount <= 1)
|
|
75
91
|
return;
|
|
76
|
-
const t = setInterval(() => setI((v) => (v + 1) %
|
|
92
|
+
const t = setInterval(() => setI((v) => (v + 1) % frameCount), SPEEDS[state]);
|
|
77
93
|
return () => clearInterval(t);
|
|
78
|
-
}, [state,
|
|
94
|
+
}, [state, frameCount]);
|
|
79
95
|
if (idle === 'sleeping') {
|
|
80
96
|
return (_jsxs(Box, { flexDirection: "column", height: TOP_ROWS + 1, children: [_jsx(Zzz, {}), _jsx(Text, { color: theme.muted, bold: true, children: ' ' + SLEEPING_FACE })] }));
|
|
81
97
|
}
|
|
82
98
|
if (idle === 'drowsy') {
|
|
83
99
|
return (_jsxs(Box, { flexDirection: "column", height: TOP_ROWS + 1, children: [_jsx(BlankRows, {}), _jsx(Text, { color: theme.muted, bold: true, children: ' ' + DROWSY_FACE })] }));
|
|
84
100
|
}
|
|
101
|
+
if (state === 'playing') {
|
|
102
|
+
const fr = MUSIC_FRAMES[i % MUSIC_FRAMES.length];
|
|
103
|
+
return (_jsxs(Box, { flexDirection: "column", height: TOP_ROWS + 1, children: [_jsx(Text, { color: slowColor, children: fr.top }), _jsx(Text, { color: slowColor, children: fr.mid }), _jsx(Text, { color: slowColor, bold: true, children: ' ' + fr.face })] }));
|
|
104
|
+
}
|
|
105
|
+
const frames = FACES[state];
|
|
85
106
|
const color = isSemantic(state) ? semanticColor(state) : slowColor;
|
|
86
|
-
return (_jsxs(Box, { flexDirection: "column", height: TOP_ROWS + 1, children: [_jsx(BlankRows, {}), _jsx(Text, { color: color, bold: true, children: ' ' + frames[i] })] }));
|
|
107
|
+
return (_jsxs(Box, { flexDirection: "column", height: TOP_ROWS + 1, children: [_jsx(BlankRows, {}), _jsx(Text, { color: color, bold: true, children: ' ' + frames[i % frames.length] })] }));
|
|
87
108
|
};
|
package/dist/ui/splash.js
CHANGED
|
@@ -106,7 +106,7 @@ function sleep(ms) {
|
|
|
106
106
|
}
|
|
107
107
|
export async function playSplash(version) {
|
|
108
108
|
const cols = process.stdout.columns || 80;
|
|
109
|
-
process.stdout.write(HIDE_CURSOR);
|
|
109
|
+
process.stdout.write('\x1b[?1049h' + HIDE_CURSOR);
|
|
110
110
|
try {
|
|
111
111
|
for (let frame = 0; frame <= T_HOLD_END; frame++) {
|
|
112
112
|
process.stdout.write(HOME_CLEAR + buildFrame(frame, cols, version));
|
|
@@ -116,6 +116,6 @@ export async function playSplash(version) {
|
|
|
116
116
|
process.stdout.write(HOME_CLEAR);
|
|
117
117
|
}
|
|
118
118
|
finally {
|
|
119
|
-
process.stdout.write(SHOW_CURSOR);
|
|
119
|
+
process.stdout.write(SHOW_CURSOR + '\x1b[?1049l');
|
|
120
120
|
}
|
|
121
121
|
}
|