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/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
  };
@@ -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
- 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: '╰' + '─'.repeat(botLen) }), loading && _jsx(Text, { color: pastel, children: status }), _jsx(Text, { color: theme.muted, children: "\u256F" })] })] }));
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
  };
@@ -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, impatient: 300,
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 (frames.length <= 1)
90
+ if (frameCount <= 1)
75
91
  return;
76
- const t = setInterval(() => setI((v) => (v + 1) % frames.length), SPEEDS[state]);
92
+ const t = setInterval(() => setI((v) => (v + 1) % frameCount), SPEEDS[state]);
77
93
  return () => clearInterval(t);
78
- }, [state, frames.length]);
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
  }