blyuecode 0.7.1 → 0.8.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/dist/ui/Chat.js CHANGED
@@ -26,8 +26,20 @@ 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
+ };
31
43
  const HistoryLine = ({ item }) => {
32
44
  if (item.kind === 'user') {
33
45
  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 +87,13 @@ export const Chat = ({ resumedCount = 0 }) => {
75
87
  const [streaming, setStreaming] = useState('');
76
88
  const [overlay, setOverlay] = useState('none');
77
89
  const [pendingSkill, setPendingSkill] = useState(null);
90
+ const [pendingMedia, setPendingMedia] = useState(null);
91
+ const [pendingInstall, setPendingInstall] = useState(null);
92
+ const [mediaStatus, setMediaStatus] = useState(null);
93
+ const [playerActive, setPlayerActive] = useState(false);
94
+ const [playProgress, setPlayProgress] = useState(null);
95
+ const [pendingVideoCleanup, setPendingVideoCleanup] = useState(null);
96
+ const [pendingVideoQuality, setPendingVideoQuality] = useState(null);
78
97
  const [permReq, setPermReq] = useState(null);
79
98
  const [currentModel, setCurrentModel] = useState(process.env.MODEL || '');
80
99
  const [agent, setAgent] = useState(() => new Agent());
@@ -106,6 +125,73 @@ export const Chat = ({ resumedCount = 0 }) => {
106
125
  const slowColor = useSlowColor();
107
126
  useInput((inputChar, key) => {
108
127
  bumpActivity();
128
+ if (pendingVideoQuality) {
129
+ const pt = pendingVideoQuality.track;
130
+ const pick = (q) => {
131
+ setPendingVideoQuality(null);
132
+ setHistory((h) => [...h, mkItem('tool', 'quality: ' + q + ' · download and play? [y] yes · [n] cancel')]);
133
+ setPendingMedia({ kind: 'video', track: pt, quality: q });
134
+ };
135
+ if (inputChar === '1') {
136
+ pick('360p');
137
+ return;
138
+ }
139
+ if (inputChar === '2') {
140
+ pick('480p');
141
+ return;
142
+ }
143
+ if (inputChar === '3') {
144
+ pick('720p');
145
+ return;
146
+ }
147
+ if (inputChar === '4') {
148
+ pick('1080p');
149
+ return;
150
+ }
151
+ if (inputChar === '5') {
152
+ pick('best');
153
+ return;
154
+ }
155
+ if (inputChar === 'n' || inputChar === 'N' || key.escape) {
156
+ setPendingVideoQuality(null);
157
+ setInput('');
158
+ setHistory((h) => [...h, mkItem('tool', 'cancelled')]);
159
+ return;
160
+ }
161
+ return;
162
+ }
163
+ if (pendingMedia) {
164
+ if (inputChar === 'y' || inputChar === 'Y') {
165
+ const pm = pendingMedia;
166
+ setPendingMedia(null);
167
+ setInput('');
168
+ void runPendingMedia(pm);
169
+ return;
170
+ }
171
+ if (inputChar === 'n' || inputChar === 'N' || key.escape) {
172
+ setPendingMedia(null);
173
+ setInput('');
174
+ setHistory((h) => [...h, mkItem('tool', 'cancelled')]);
175
+ return;
176
+ }
177
+ return;
178
+ }
179
+ if (pendingInstall) {
180
+ if (inputChar === 'y' || inputChar === 'Y') {
181
+ const cmd = pendingInstall;
182
+ setPendingInstall(null);
183
+ setInput('');
184
+ void runPendingInstall(cmd);
185
+ return;
186
+ }
187
+ if (inputChar === 'n' || inputChar === 'N' || key.escape) {
188
+ setPendingInstall(null);
189
+ setInput('');
190
+ setHistory((h) => [...h, mkItem('tool', 'cancelled')]);
191
+ return;
192
+ }
193
+ return;
194
+ }
109
195
  if (permReq)
110
196
  return;
111
197
  if (key.ctrl && inputChar === 'l')
@@ -126,6 +212,86 @@ export const Chat = ({ resumedCount = 0 }) => {
126
212
  return;
127
213
  setHistory((h) => [...h, mkItem(kind, text)]);
128
214
  };
215
+ const runPendingMedia = async (pm) => {
216
+ if (pm.kind === 'audio') {
217
+ setMediaStatus('downloading: ' + pm.track.title + '…');
218
+ try {
219
+ const d = await downloadAudio(pm.track);
220
+ setMediaStatus('loading into player…');
221
+ await player.play(d);
222
+ setMediaStatus(null);
223
+ setHistory((h) => [...h, mkItem('tool', '▶ ' + d.title + ' (deletes after — /loop /keep)')]);
224
+ }
225
+ catch (e) {
226
+ setMediaStatus(null);
227
+ const msg = e?.message || String(e);
228
+ if (/403|forbidden|stale/i.test(msg)) {
229
+ setHistory((h) => [
230
+ ...h,
231
+ mkItem('error', msg),
232
+ mkItem('tool', 'run /update-ytdlp to fix (stale yt-dlp)'),
233
+ ]);
234
+ }
235
+ else {
236
+ setHistory((h) => [...h, mkItem('error', msg)]);
237
+ }
238
+ }
239
+ }
240
+ else {
241
+ setMediaStatus('downloading ' + pm.quality + ': ' + pm.track.title + '…');
242
+ try {
243
+ const v = await downloadVideo(pm.track, pm.quality);
244
+ setMediaStatus('opening player…');
245
+ const { cleanup, notice } = await playVideo(v);
246
+ setMediaStatus(null);
247
+ setPendingVideoCleanup(() => cleanup);
248
+ if (notice) {
249
+ setHistory((h) => [...h, mkItem('tool', '▶ ' + v.title), mkItem('tool', notice)]);
250
+ }
251
+ else {
252
+ setHistory((h) => [...h, mkItem('tool', 'video ended · file cleaned up')]);
253
+ }
254
+ }
255
+ catch (e) {
256
+ setMediaStatus(null);
257
+ setHistory((h) => [...h, mkItem('error', e?.message || String(e))]);
258
+ }
259
+ }
260
+ };
261
+ const runPendingInstall = async (cmd) => {
262
+ setHistory((h) => [...h, mkItem('tool', 'running: ' + cmd)]);
263
+ setLoading(true);
264
+ const r = await installMediaDeps();
265
+ setLoading(false);
266
+ if (r.ok)
267
+ setHistory((h) => [...h, mkItem('tool', '✓ media tools installed')]);
268
+ else
269
+ setHistory((h) => [...h, mkItem('error', 'install failed: ' + r.output.slice(-400))]);
270
+ };
271
+ useEffect(() => {
272
+ const sync = () => {
273
+ try {
274
+ const st = player.getState();
275
+ setPlayerActive(st.playing);
276
+ if (st.playing && st.duration > 0 && st.position >= 0) {
277
+ setPlayProgress({ pos: st.position, dur: st.duration });
278
+ }
279
+ else {
280
+ setPlayProgress(null);
281
+ }
282
+ }
283
+ catch { }
284
+ };
285
+ player.on('update', sync);
286
+ player.on('queue-empty', sync);
287
+ player.on('exit', sync);
288
+ sync();
289
+ return () => {
290
+ player.removeListener('update', sync);
291
+ player.removeListener('queue-empty', sync);
292
+ player.removeListener('exit', sync);
293
+ };
294
+ }, []);
129
295
  const handleSubmit = async (text) => {
130
296
  bumpActivity();
131
297
  if (!text.trim() || loading)
@@ -219,6 +385,266 @@ export const Chat = ({ resumedCount = 0 }) => {
219
385
  ]);
220
386
  return;
221
387
  }
388
+ if (trimmed === '/update-ytdlp') {
389
+ setInput('');
390
+ const before = await ytdlpVersion();
391
+ setHistory((h) => [...h, mkItem('tool', 'yt-dlp version: ' + before + ' · updating…')]);
392
+ setLoading(true);
393
+ const r = await updateYtdlp();
394
+ setLoading(false);
395
+ const after = await ytdlpVersion();
396
+ if (r.ok) {
397
+ setHistory((h) => [...h, mkItem('tool', '✓ yt-dlp ' + before + ' → ' + after)]);
398
+ }
399
+ else {
400
+ setHistory((h) => [...h, mkItem('error', 'update failed: ' + r.output.slice(-300))]);
401
+ }
402
+ return;
403
+ }
404
+ if (trimmed === '/ytdlp-log') {
405
+ setInput('');
406
+ const { readYtdlpLog } = await import('../core/ytdlp.js');
407
+ const lines = readYtdlpLog(40);
408
+ if (lines.length === 0) {
409
+ setHistory((h) => [...h, mkItem('tool', 'no yt-dlp errors logged')]);
410
+ }
411
+ else {
412
+ setHistory((h) => [
413
+ ...h,
414
+ mkItem('tool', 'yt-dlp log: ' + ytdlpLogPath()),
415
+ ...lines.map((l) => mkItem('tool', ' ' + l.slice(0, 180))),
416
+ ]);
417
+ }
418
+ return;
419
+ }
420
+ if (trimmed === '/install-media') {
421
+ setInput('');
422
+ const deps = await checkMediaDeps();
423
+ if (deps.allPresent) {
424
+ setHistory((h) => [...h, mkItem('tool', 'media tools already installed')]);
425
+ return;
426
+ }
427
+ const plat = deps.platform;
428
+ if (!plat.installCmd) {
429
+ setHistory((h) => [...h, mkItem('error', 'unknown platform — install yt-dlp ffmpeg mpv manually')]);
430
+ return;
431
+ }
432
+ setHistory((h) => [
433
+ ...h,
434
+ mkItem('tool', 'will run: ' + plat.installCmd),
435
+ mkItem('tool', '~' + plat.approximateSizeMb + ' MB via ' + plat.updateHint),
436
+ mkItem('tool', 'proceed? [y] yes · [n] cancel'),
437
+ ]);
438
+ setPendingInstall(plat.installCmd);
439
+ return;
440
+ }
441
+ if (trimmed.startsWith('/image ')) {
442
+ setInput('');
443
+ const prompt = trimmed.slice(7).trim();
444
+ if (!prompt) {
445
+ setHistory((h) => [...h, mkItem('error', 'usage: /image <prompt>')]);
446
+ return;
447
+ }
448
+ setLoading(true);
449
+ setHistory((h) => [...h, mkItem('tool', 'generating image: ' + prompt + '…')]);
450
+ try {
451
+ const r = await generateImage(prompt);
452
+ setHistory((h) => [...h, mkItem('tool', 'saved: ' + r.path), mkItem('tool', 'source: ' + r.url)]);
453
+ }
454
+ catch (e) {
455
+ setHistory((h) => [...h, mkItem('error', e?.message || String(e))]);
456
+ }
457
+ setLoading(false);
458
+ return;
459
+ }
460
+ if (trimmed.startsWith('/play ')) {
461
+ setInput('');
462
+ const q = trimmed.slice(6).trim();
463
+ if (!q) {
464
+ setHistory((h) => [...h, mkItem('error', 'usage: /play <query>')]);
465
+ return;
466
+ }
467
+ setMediaStatus('searching: ' + q + '…');
468
+ try {
469
+ const deps = await checkMediaDeps();
470
+ if (!deps.allPresent) {
471
+ setMediaStatus(null);
472
+ setHistory((h) => [...h,
473
+ mkItem('tool', 'missing media tools: ' + deps.missing.join(', ')),
474
+ mkItem('tool', 'run /install-media to install them'),
475
+ ]);
476
+ return;
477
+ }
478
+ const track = await searchYouTube(q);
479
+ setHistory((h) => [
480
+ ...h,
481
+ mkItem('tool', 'found: ' + track.title + ' · ' + fmtTime(track.duration) + (track.uploader ? ' · ' + track.uploader : '')),
482
+ mkItem('tool', 'download and play? [y] yes · [n] cancel'),
483
+ ]);
484
+ setMediaStatus(null);
485
+ setPendingMedia({ kind: 'audio', track });
486
+ }
487
+ catch (e) {
488
+ setMediaStatus(null);
489
+ const msg = e?.message || String(e);
490
+ if (/403|forbidden|stale/i.test(msg)) {
491
+ setHistory((h) => [
492
+ ...h,
493
+ mkItem('error', msg),
494
+ mkItem('tool', 'try /update-ytdlp to fix'),
495
+ ]);
496
+ }
497
+ else {
498
+ setHistory((h) => [...h, mkItem('error', msg)]);
499
+ }
500
+ }
501
+ return;
502
+ }
503
+ if (trimmed.startsWith('/video ')) {
504
+ setInput('');
505
+ const rest = trimmed.slice(7).trim();
506
+ if (!rest) {
507
+ setHistory((h) => [...h, mkItem('error', 'usage: /video <query> [quality]')]);
508
+ return;
509
+ }
510
+ const parts = rest.split(/\s+/);
511
+ const last = parts[parts.length - 1];
512
+ const qualityPattern = /^(360p?|480p?|720p?|1080p?|best)$/i;
513
+ const qualityPattern2 = /^\d+p?$/i.test(last);
514
+ const looksLikeQuality = qualityPattern.test(last) || qualityPattern2;
515
+ const hasExplicit = qualityPattern.test(last) && parts.length > 1;
516
+ const quality = hasExplicit ? parseQuality(last) : '720p';
517
+ const query = hasExplicit ? parts.slice(0, -1).join(' ') : rest;
518
+ setMediaStatus('searching: ' + query + '…');
519
+ try {
520
+ const deps = await checkMediaDeps();
521
+ if (!deps.allPresent) {
522
+ setMediaStatus(null);
523
+ setHistory((h) => [...h,
524
+ mkItem('tool', 'missing media tools: ' + deps.missing.join(', ')),
525
+ mkItem('tool', 'run /install-media to install them (approval required)'),
526
+ ]);
527
+ return;
528
+ }
529
+ const t = await searchYouTube(query);
530
+ const dur = t.duration;
531
+ const meta = fmtTime(dur) + (t.uploader ? ' · ' + t.uploader : '');
532
+ setMediaStatus(null);
533
+ if (hasExplicit) {
534
+ setHistory((h) => [
535
+ ...h,
536
+ mkItem('tool', 'found: ' + t.title + ' · ' + meta),
537
+ mkItem('tool', 'download ' + quality + ' and play? [y] yes · [n] cancel'),
538
+ ]);
539
+ setPendingMedia({ kind: 'video', track: t, quality });
540
+ }
541
+ else {
542
+ setHistory((h) => [
543
+ ...h,
544
+ mkItem('tool', 'found: ' + t.title + ' · ' + meta),
545
+ mkItem('tool', 'choose quality:'),
546
+ mkItem('tool', ' [1] 360p ~' + estimateSizeMb(dur, '360p') + ' MB'),
547
+ mkItem('tool', ' [2] 480p ~' + estimateSizeMb(dur, '480p') + ' MB'),
548
+ mkItem('tool', ' [3] 720p ~' + estimateSizeMb(dur, '720p') + ' MB'),
549
+ mkItem('tool', ' [4] 1080p ~' + estimateSizeMb(dur, '1080p') + ' MB'),
550
+ mkItem('tool', ' [5] best ~' + estimateSizeMb(dur, 'best') + ' MB'),
551
+ mkItem('tool', 'press 1-5 · [n] cancel'),
552
+ ]);
553
+ setPendingVideoQuality({ track: t });
554
+ }
555
+ }
556
+ catch (e) {
557
+ setMediaStatus(null);
558
+ setHistory((h) => [...h, mkItem('error', e?.message || String(e))]);
559
+ }
560
+ return;
561
+ }
562
+ if (trimmed === '/cleanup') {
563
+ setInput('');
564
+ if (pendingVideoCleanup) {
565
+ pendingVideoCleanup();
566
+ setPendingVideoCleanup(null);
567
+ setHistory((h) => [...h, mkItem('tool', 'video file cleaned up')]);
568
+ }
569
+ else {
570
+ setHistory((h) => [...h, mkItem('tool', 'nothing to clean up')]);
571
+ }
572
+ return;
573
+ }
574
+ if (trimmed === '/pause') {
575
+ setInput('');
576
+ await player.togglePause();
577
+ return;
578
+ }
579
+ if (trimmed === '/next') {
580
+ setInput('');
581
+ await player.next();
582
+ return;
583
+ }
584
+ if (trimmed === '/stop') {
585
+ setInput('');
586
+ await player.stop();
587
+ setHistory((h) => [...h, mkItem('tool', 'stopped')]);
588
+ return;
589
+ }
590
+ if (trimmed === '/loop') {
591
+ setInput('');
592
+ const on = !player.isLoop();
593
+ player.setLoop(on);
594
+ setHistory((h) => [...h, mkItem('tool', on ? 'loop on' : 'loop off')]);
595
+ return;
596
+ }
597
+ if (trimmed === '/keep') {
598
+ setInput('');
599
+ const st = player.getState();
600
+ if (st.current) {
601
+ st.current.keep = true;
602
+ setHistory((h) => [...h, mkItem('tool', 'will keep: ' + st.current.title)]);
603
+ }
604
+ else
605
+ setHistory((h) => [...h, mkItem('tool', 'nothing playing')]);
606
+ return;
607
+ }
608
+ if (trimmed === '/queue') {
609
+ setInput('');
610
+ const st = player.getState();
611
+ if (!st.current && st.queue.length === 0) {
612
+ setHistory((h) => [...h, mkItem('tool', 'nothing playing')]);
613
+ }
614
+ else {
615
+ const lines = [];
616
+ if (st.current)
617
+ lines.push('▶ ' + st.current.title);
618
+ for (const t of st.queue)
619
+ lines.push(' ⏸ ' + t.title);
620
+ setHistory((h) => [...h, ...lines.map((l) => mkItem('tool', l))]);
621
+ }
622
+ return;
623
+ }
624
+ if (trimmed.startsWith('/remember ')) {
625
+ setInput('');
626
+ const text = trimmed.slice(10).trim();
627
+ if (!text) {
628
+ setHistory((h) => [...h, mkItem('error', 'usage: /remember <text>')]);
629
+ return;
630
+ }
631
+ let section = 'Notes';
632
+ let note = text;
633
+ const pipe = text.indexOf('|');
634
+ if (pipe > 0) {
635
+ note = text.slice(0, pipe).trim();
636
+ section = text.slice(pipe + 1).trim() || 'Notes';
637
+ }
638
+ try {
639
+ appendMemory(note, section);
640
+ audit('memory_write', { section, note });
641
+ setHistory((h) => [...h, mkItem('tool', 'remembered (' + section + '): ' + note)]);
642
+ }
643
+ catch (e) {
644
+ setHistory((h) => [...h, mkItem('error', e?.message || String(e))]);
645
+ }
646
+ return;
647
+ }
222
648
  if (trimmed === '/debug') {
223
649
  setInput('');
224
650
  const lines = readDebug();
@@ -273,6 +699,17 @@ export const Chat = ({ resumedCount = 0 }) => {
273
699
  mkItem('tool', ' /skill <n> force-activate a skill for next msg'),
274
700
  mkItem('tool', ' /compact summarize history to free context'),
275
701
  mkItem('tool', ' /debug show last request to provider'),
702
+ mkItem('tool', ' /image <p> generate an image'),
703
+ mkItem('tool', ' /play <q> search + play audio (auto-delete)'),
704
+ mkItem('tool', ' /video <q> [quality] download + play video'),
705
+ mkItem('tool', ' /pause /next /stop /queue playback control'),
706
+ mkItem('tool', ' /loop /keep playback behavior'),
707
+ mkItem('tool', ' /autoplay keep music going when queue ends'),
708
+ mkItem('tool', ' /cleanup delete the video file (Termux, after player closes)'),
709
+ mkItem('tool', ' /install-media install yt-dlp + ffmpeg + mpv'),
710
+ mkItem('tool', ' /update-ytdlp update yt-dlp (fixes HTTP 403)'),
711
+ mkItem('tool', ' /ytdlp-log show last yt-dlp errors'),
712
+ mkItem('tool', ' /remember <text> [| section] append to memory'),
276
713
  mkItem('tool', ' /clear clear session and permissions'),
277
714
  mkItem('tool', ' /help show this list'),
278
715
  ]);
@@ -391,5 +828,5 @@ export const Chat = ({ resumedCount = 0 }) => {
391
828
  const shortModel = currentModel.length > maxModelLen
392
829
  ? '…' + currentModel.slice(-(maxModelLen - 1))
393
830
  : 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 })] })] })] }));
831
+ 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
832
  };
@@ -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
  };