blyuecode 0.1.14 → 0.1.16
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/index.js +1 -1
- package/dist/ui/Banner.js +8 -9
- package/dist/ui/Chat.js +49 -12
- package/dist/ui/InputBar.js +10 -6
- package/dist/ui/MascotPrompt.js +67 -16
- package/dist/ui/RocketBar.js +3 -11
- package/dist/ui/SleepZs.js +26 -0
- package/dist/ui/activity.js +20 -0
- package/dist/ui/pastel.js +30 -0
- package/dist/ui/slowTick.js +38 -0
- package/dist/ui/useIdle.js +43 -0
- package/dist/ui/useSlowColor.js +11 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -23,7 +23,7 @@ const App = () => {
|
|
|
23
23
|
setReady(true);
|
|
24
24
|
}
|
|
25
25
|
}, [consented]);
|
|
26
|
-
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Static, { items: ['banner'], children: () => _jsx(Banner, {}) }), !consented ? (_jsx(Consent, { onAccept: () => setConsented(true) })) : ready ? (_jsx(Chat, { resumedCount: resumedCount })) : (_jsx(Setup, { onDone: () => setReady(true) }))] }));
|
|
26
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Static, { items: ['banner'], children: () => _jsx(Banner, {}, "banner") }), !consented ? (_jsx(Consent, { onAccept: () => setConsented(true) })) : ready ? (_jsx(Chat, { resumedCount: resumedCount })) : (_jsx(Setup, { onDone: () => setReady(true) }))] }));
|
|
27
27
|
};
|
|
28
28
|
render(_jsx(App, {}), {
|
|
29
29
|
patchConsole: false,
|
package/dist/ui/Banner.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useState, useEffect } from 'react';
|
|
3
3
|
import { Box, Text, useStdout } from 'ink';
|
|
4
4
|
import { readFileSync } from 'fs';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
6
|
import { dirname, join } from 'path';
|
|
7
7
|
import os from 'os';
|
|
8
|
-
import {
|
|
8
|
+
import { pastelAt } from './pastel.js';
|
|
9
9
|
function getVersion() {
|
|
10
10
|
try {
|
|
11
11
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
@@ -63,6 +63,7 @@ const CODE_SMALL = [
|
|
|
63
63
|
];
|
|
64
64
|
const BLYUE_WIDTH = 44;
|
|
65
65
|
const BLYUE_SMALL_WIDTH = 27;
|
|
66
|
+
const MakerMarkInline = ({ color }) => (_jsx(Text, { color: color, bold: true, children: "\u25C6 L U M E N" }));
|
|
66
67
|
export const Banner = () => {
|
|
67
68
|
const { stdout } = useStdout();
|
|
68
69
|
const width = stdout?.columns ?? 80;
|
|
@@ -70,21 +71,19 @@ export const Banner = () => {
|
|
|
70
71
|
const blyue = wide ? BLYUE_BIG : BLYUE_SMALL;
|
|
71
72
|
const code = wide ? CODE_BIG : CODE_SMALL;
|
|
72
73
|
const blockWidth = wide ? BLYUE_WIDTH : BLYUE_SMALL_WIDTH;
|
|
73
|
-
// Left padding to center whole block in terminal
|
|
74
74
|
const leftPad = Math.max(0, Math.floor((width - blockWidth) / 2));
|
|
75
75
|
const pad = ' '.repeat(leftPad);
|
|
76
76
|
const [animate] = useState(() => shouldAnimate());
|
|
77
77
|
const [t, setT] = useState(0);
|
|
78
|
+
// Slow pastel drift — 24s full cycle, matches the rest of the TUI.
|
|
78
79
|
useEffect(() => {
|
|
79
80
|
if (!animate)
|
|
80
81
|
return;
|
|
81
|
-
const interval = setInterval(() => setT((v) => (v + 1 /
|
|
82
|
+
const interval = setInterval(() => setT((v) => (v + 1 / 200) % 1), 120);
|
|
82
83
|
return () => clearInterval(interval);
|
|
83
84
|
}, [animate]);
|
|
84
|
-
const baseHue = animate ? t : 0.
|
|
85
|
-
const brand =
|
|
85
|
+
const baseHue = animate ? t : 0.08;
|
|
86
|
+
const brand = pastelAt(baseHue);
|
|
86
87
|
const version = getVersion();
|
|
87
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [pad, _jsx(MakerMarkInline, { color: brand })] }), _jsx(Text, { children: " " }), blyue.map((line, i) => (_jsx(Text, { color:
|
|
88
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [pad, _jsx(MakerMarkInline, { color: brand })] }), _jsx(Text, { children: " " }), blyue.map((line, i) => (_jsx(Text, { color: pastelAt((baseHue + i * 0.035) % 1), bold: true, children: pad + line }, 'b' + i))), code.map((line, i) => (_jsx(Text, { color: pastelAt((baseHue + i * 0.035 + 0.18) % 1), bold: true, children: pad + line }, 'c' + i))), _jsx(Text, { children: " " }), _jsxs(Text, { children: [pad, _jsx(Text, { color: "gray", children: 'a coding agent · v' + version })] }), _jsx(Text, { children: " " })] }));
|
|
88
89
|
};
|
|
89
|
-
// Inline version of MakerMark so it aligns inside the padded Text
|
|
90
|
-
const MakerMarkInline = ({ color }) => (_jsxs(_Fragment, { children: [_jsx(Text, { color: color, bold: true, children: "\u25C6 " }), _jsx(Text, { color: color, bold: true, children: "L U M E N" })] }));
|
package/dist/ui/Chat.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useState, useEffect } from 'react';
|
|
2
|
+
import { useState, useEffect, useRef } from 'react';
|
|
3
3
|
import { Box, Text, useInput, useStdout, Static } from 'ink';
|
|
4
4
|
import fs from 'fs';
|
|
5
5
|
import path from 'path';
|
|
@@ -11,12 +11,16 @@ import { ModelPicker } from './ModelPicker.js';
|
|
|
11
11
|
import { Settings } from './Settings.js';
|
|
12
12
|
import { PermissionPrompt } from './PermissionPrompt.js';
|
|
13
13
|
import { Spinner } from './Spinner.js';
|
|
14
|
+
import { toolToState } from './MascotPrompt.js';
|
|
15
|
+
import { useSlowColor } from './useSlowColor.js';
|
|
16
|
+
import { pastelAt } from './pastel.js';
|
|
17
|
+
import { bumpActivity } from './activity.js';
|
|
14
18
|
import { theme } from './theme.js';
|
|
15
19
|
let nextId = 0;
|
|
16
20
|
const mkItem = (kind, text) => ({ kind, text, id: nextId++ });
|
|
17
21
|
const HistoryLine = ({ item }) => {
|
|
18
22
|
if (item.kind === 'user') {
|
|
19
|
-
return (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color:
|
|
23
|
+
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 })] }));
|
|
20
24
|
}
|
|
21
25
|
if (item.kind === 'error') {
|
|
22
26
|
return (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.error, children: '❯ ' }), _jsx(Text, { color: theme.error, children: item.text })] }));
|
|
@@ -63,12 +67,37 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
63
67
|
const [permReq, setPermReq] = useState(null);
|
|
64
68
|
const [currentModel, setCurrentModel] = useState(process.env.MODEL || '');
|
|
65
69
|
const [agent, setAgent] = useState(() => new Agent());
|
|
66
|
-
|
|
70
|
+
// Mascot state
|
|
71
|
+
const [mascotState, setMascotState] = useState(undefined);
|
|
72
|
+
const [elapsedSec, setElapsedSec] = useState(0);
|
|
73
|
+
const startRef = useRef(null);
|
|
67
74
|
useEffect(() => {
|
|
68
75
|
const unsub = permissions.subscribe((req) => setPermReq(req));
|
|
69
76
|
return unsub;
|
|
70
77
|
}, []);
|
|
78
|
+
// Track elapsed time during loading
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
if (!loading) {
|
|
81
|
+
startRef.current = null;
|
|
82
|
+
setElapsedSec(0);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
startRef.current = Date.now();
|
|
86
|
+
const t = setInterval(() => {
|
|
87
|
+
if (startRef.current) {
|
|
88
|
+
setElapsedSec(Math.floor((Date.now() - startRef.current) / 1000));
|
|
89
|
+
}
|
|
90
|
+
}, 500);
|
|
91
|
+
return () => clearInterval(t);
|
|
92
|
+
}, [loading]);
|
|
93
|
+
// Flash a temporary mascot state
|
|
94
|
+
const flash = (state, ms = 1200) => {
|
|
95
|
+
setMascotState(state);
|
|
96
|
+
setTimeout(() => setMascotState(undefined), ms);
|
|
97
|
+
};
|
|
98
|
+
const slowColor = useSlowColor();
|
|
71
99
|
useInput((inputChar, key) => {
|
|
100
|
+
bumpActivity();
|
|
72
101
|
if (permReq)
|
|
73
102
|
return;
|
|
74
103
|
if (key.ctrl && inputChar === 'p')
|
|
@@ -82,6 +111,7 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
82
111
|
setHistory((h) => [...h, mkItem(kind, text)]);
|
|
83
112
|
};
|
|
84
113
|
const handleSubmit = async (text) => {
|
|
114
|
+
bumpActivity();
|
|
85
115
|
if (!text.trim() || loading)
|
|
86
116
|
return;
|
|
87
117
|
const trimmed = text.trim();
|
|
@@ -110,15 +140,18 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
110
140
|
setInput('');
|
|
111
141
|
setLoading(true);
|
|
112
142
|
setStreaming('');
|
|
143
|
+
flash('thinking', 1000);
|
|
113
144
|
try {
|
|
114
145
|
const r = await agent.compact();
|
|
115
146
|
setHistory((h) => [
|
|
116
147
|
...h,
|
|
117
148
|
mkItem('tool', `Compacted: ${r.before} → ${r.after} messages (summary ${r.summaryLen} chars)`),
|
|
118
149
|
]);
|
|
150
|
+
flash('success', 1500);
|
|
119
151
|
}
|
|
120
152
|
catch (e) {
|
|
121
153
|
setHistory((h) => [...h, mkItem('error', e.message)]);
|
|
154
|
+
flash('error', 2000);
|
|
122
155
|
}
|
|
123
156
|
setLoading(false);
|
|
124
157
|
return;
|
|
@@ -128,6 +161,7 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
128
161
|
agent.clear();
|
|
129
162
|
permissions.resetSession();
|
|
130
163
|
setHistory([mkItem('tool', '(conversation cleared)')]);
|
|
164
|
+
flash('success', 1200);
|
|
131
165
|
return;
|
|
132
166
|
}
|
|
133
167
|
if (trimmed === '/help') {
|
|
@@ -150,8 +184,7 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
150
184
|
setLoading(true);
|
|
151
185
|
setStreaming('');
|
|
152
186
|
// Jump animation on submit
|
|
153
|
-
|
|
154
|
-
setTimeout(() => setReaction(null), 400);
|
|
187
|
+
flash('jump', 400);
|
|
155
188
|
let textBuf = '';
|
|
156
189
|
let toolBuf = '';
|
|
157
190
|
try {
|
|
@@ -159,6 +192,8 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
159
192
|
if (ev.type === 'text') {
|
|
160
193
|
textBuf += ev.delta;
|
|
161
194
|
setStreaming(textBuf);
|
|
195
|
+
// Clear any tool mascot while text streams
|
|
196
|
+
setMascotState(undefined);
|
|
162
197
|
}
|
|
163
198
|
else if (ev.type === 'tool_start') {
|
|
164
199
|
if (textBuf) {
|
|
@@ -167,26 +202,28 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
167
202
|
}
|
|
168
203
|
toolBuf = '\n[Calling ' + ev.name + '...]\n';
|
|
169
204
|
setStreaming(toolBuf);
|
|
205
|
+
// React to the specific tool
|
|
206
|
+
setMascotState(toolToState(ev.name));
|
|
170
207
|
}
|
|
171
208
|
else if (ev.type === 'tool_end') {
|
|
172
209
|
flush('tool', toolBuf + '[Result]: ' + ev.preview + '\n');
|
|
173
210
|
toolBuf = '';
|
|
174
211
|
setStreaming('');
|
|
212
|
+
// Brief pulse before next step
|
|
213
|
+
flash('success', 500);
|
|
175
214
|
}
|
|
176
215
|
}
|
|
177
216
|
if (textBuf)
|
|
178
217
|
flush('ai', textBuf);
|
|
179
|
-
|
|
180
|
-
setReaction('success');
|
|
181
|
-
setTimeout(() => setReaction(null), 2000);
|
|
218
|
+
flash('success', 2000);
|
|
182
219
|
}
|
|
183
220
|
catch (e) {
|
|
184
221
|
setHistory((h) => [...h, mkItem('error', e?.message || String(e))]);
|
|
185
|
-
|
|
186
|
-
setTimeout(() => setReaction(null), 3000);
|
|
222
|
+
flash('error', 3000);
|
|
187
223
|
}
|
|
188
224
|
setStreaming('');
|
|
189
225
|
setLoading(false);
|
|
226
|
+
setMascotState(undefined);
|
|
190
227
|
};
|
|
191
228
|
const handleModelSelect = (id) => {
|
|
192
229
|
process.env.MODEL = id;
|
|
@@ -196,7 +233,7 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
196
233
|
setHistory((h) => [...h, mkItem('tool', `model → ${id}`)]);
|
|
197
234
|
};
|
|
198
235
|
if (permReq) {
|
|
199
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: history, children: (item) => _jsx(HistoryLine, { item: item }, item.id) }), _jsx(Box, { marginTop: 1, children: _jsx(PermissionPrompt, { req: permReq }) })] }));
|
|
236
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: history, children: (item) => _jsx(HistoryLine, { item: item }, item.id) }), _jsx(Box, { marginTop: 1, children: _jsx(PermissionPrompt, { req: permReq }) }), _jsx(Box, { marginTop: 1, children: _jsx(InputBar, { value: "", onChange: () => { }, onSubmit: () => { }, loading: false, mascotState: "permission", awaitingPermission: true }) })] }));
|
|
200
237
|
}
|
|
201
238
|
if (overlay === 'model') {
|
|
202
239
|
return (_jsx(Box, { flexDirection: "column", children: _jsx(ModelPicker, { current: currentModel, onSelect: handleModelSelect, onCancel: () => setOverlay('none') }) }));
|
|
@@ -211,5 +248,5 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
211
248
|
const shortModel = currentModel.length > maxModelLen
|
|
212
249
|
? '…' + currentModel.slice(-(maxModelLen - 1))
|
|
213
250
|
: currentModel || 'unset';
|
|
214
|
-
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,
|
|
251
|
+
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 })] })] })] }));
|
|
215
252
|
};
|
package/dist/ui/InputBar.js
CHANGED
|
@@ -6,10 +6,12 @@ import { Placeholder } from './Placeholder.js';
|
|
|
6
6
|
import { RocketBar } from './RocketBar.js';
|
|
7
7
|
import { MascotPrompt } from './MascotPrompt.js';
|
|
8
8
|
import { theme } from './theme.js';
|
|
9
|
+
import { useSlowColor } from './useSlowColor.js';
|
|
9
10
|
const DOTS = ['●○○○', '○●○○', '○○●○', '○○○●'];
|
|
10
|
-
export const InputBar = ({ value, onChange, onSubmit, loading,
|
|
11
|
+
export const InputBar = ({ value, onChange, onSubmit, loading, mascotState, awaitingPermission, elapsedSec = 0, }) => {
|
|
11
12
|
const { stdout } = useStdout();
|
|
12
13
|
const width = Math.max(24, stdout?.columns ?? 50);
|
|
14
|
+
const pastel = useSlowColor();
|
|
13
15
|
const [dotI, setDotI] = useState(0);
|
|
14
16
|
useEffect(() => {
|
|
15
17
|
if (!loading)
|
|
@@ -18,11 +20,13 @@ export const InputBar = ({ value, onChange, onSubmit, loading, reaction }) => {
|
|
|
18
20
|
return () => clearInterval(int);
|
|
19
21
|
}, [loading]);
|
|
20
22
|
const hasText = value.length > 0;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
const state = mascotState ??
|
|
24
|
+
(awaitingPermission ? 'permission' :
|
|
25
|
+
loading && elapsedSec > 8 ? 'impatient' :
|
|
26
|
+
loading ? 'thinking' :
|
|
27
|
+
hasText ? 'typing' : 'idle');
|
|
28
|
+
const label = awaitingPermission ? 'waiting' : loading ? 'thinking' : 'blyue';
|
|
25
29
|
const status = loading ? ' ' + DOTS[dotI] + ' ' : '';
|
|
26
30
|
const botLen = Math.max(0, width - 2 - status.length);
|
|
27
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(MascotPrompt, { state:
|
|
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" })] })] }));
|
|
28
32
|
};
|
package/dist/ui/MascotPrompt.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useState, useEffect } from 'react';
|
|
3
|
-
import { Text } from 'ink';
|
|
3
|
+
import { Box, Text } from 'ink';
|
|
4
4
|
import { theme } from './theme.js';
|
|
5
|
-
|
|
5
|
+
import { useIdle, defaultIdleThresholds, idleDisabled } from './useIdle.js';
|
|
6
|
+
import { useSlowColor } from './useSlowColor.js';
|
|
7
|
+
import { Zzz } from './SleepZs.js';
|
|
6
8
|
const FACES = {
|
|
7
9
|
idle: ['◕‿◕', '◕‿◕', '◕_◕', '◕‿◕', '◕‿◕', '◕‿◕', '◕_◕', '◕‿◕'],
|
|
8
10
|
typing: ['◉‿◉', '◉‿◉', '◉‿◉', '◉_◉'],
|
|
@@ -10,19 +12,65 @@ const FACES = {
|
|
|
10
12
|
success: ['^‿^', '^▽^', '^‿^'],
|
|
11
13
|
error: ['◔︵◔', '◔︵◔', '◔_◔'],
|
|
12
14
|
jump: ['◕‿◕', '◉▽◉', '^▽^', '◉▽◉', '◕‿◕'],
|
|
15
|
+
read: ['◉_◉', '◎_◎', '◉_◉'],
|
|
16
|
+
write: ['◕✎◕', '◕✐◕', '◕✎◕'],
|
|
17
|
+
patch: ['◕~◕', '◕≈◕', '◕~◕'],
|
|
18
|
+
list: ['◉◡◉', '◉◠◉', '◉◡◉'],
|
|
19
|
+
command: ['◕⌨◕', '◕⎚◕', '◕⌨◕'],
|
|
20
|
+
automate: ['◕⚙◕', '◕⚙︎◕', '◕⚙◕'],
|
|
21
|
+
permission: ['◔_◔', '◔◡◔', '◔_◔'],
|
|
22
|
+
granted: ['◉‿◉', '◉▽◉', '◉‿◉'],
|
|
23
|
+
denied: ['◔︵◔', '◔_◔', '◔︵◔'],
|
|
24
|
+
impatient: ['◕~◕', '⊙_⊙', '◕~◕', '⊙_⊙'],
|
|
13
25
|
};
|
|
14
|
-
// Frame speeds in ms
|
|
15
26
|
const SPEEDS = {
|
|
16
|
-
idle: 500,
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
error: 400,
|
|
21
|
-
jump: 70,
|
|
27
|
+
idle: 500, typing: 700, thinking: 120,
|
|
28
|
+
success: 300, error: 400, jump: 70,
|
|
29
|
+
read: 220, write: 260, patch: 260, list: 260, command: 220,
|
|
30
|
+
automate: 200, permission: 450, granted: 180, denied: 350, impatient: 300,
|
|
22
31
|
};
|
|
23
|
-
|
|
32
|
+
const DROWSY_FACE = '–_–';
|
|
33
|
+
const SLEEPING_FACE = '-‿-';
|
|
34
|
+
const TOP_ROWS = 2;
|
|
35
|
+
export function toolToState(tool) {
|
|
36
|
+
if (tool === 'read_file')
|
|
37
|
+
return 'read';
|
|
38
|
+
if (tool === 'write_file')
|
|
39
|
+
return 'write';
|
|
40
|
+
if (tool === 'apply_patch')
|
|
41
|
+
return 'patch';
|
|
42
|
+
if (tool === 'list_files')
|
|
43
|
+
return 'list';
|
|
44
|
+
if (tool === 'run_command')
|
|
45
|
+
return 'command';
|
|
46
|
+
if (tool.startsWith('n8n_'))
|
|
47
|
+
return 'automate';
|
|
48
|
+
return 'thinking';
|
|
49
|
+
}
|
|
50
|
+
const BlankRows = () => (_jsx(_Fragment, { children: Array.from({ length: TOP_ROWS }, (_, i) => (_jsx(Text, { children: " " }, i))) }));
|
|
51
|
+
// States whose color is semantic (meaning-bearing) — these stay their
|
|
52
|
+
// theme colors. Everything else drifts through the pastel palette.
|
|
53
|
+
function isSemantic(state) {
|
|
54
|
+
return (state === 'error' || state === 'denied' ||
|
|
55
|
+
state === 'success' || state === 'granted' ||
|
|
56
|
+
state === 'permission' || state === 'impatient');
|
|
57
|
+
}
|
|
58
|
+
function semanticColor(state) {
|
|
59
|
+
if (state === 'error' || state === 'denied')
|
|
60
|
+
return theme.error;
|
|
61
|
+
if (state === 'success' || state === 'granted')
|
|
62
|
+
return theme.success;
|
|
63
|
+
return theme.warning; // permission / impatient
|
|
64
|
+
}
|
|
65
|
+
export const MascotPrompt = ({ state, keepAwake = false, }) => {
|
|
24
66
|
const frames = FACES[state];
|
|
25
67
|
const [i, setI] = useState(0);
|
|
68
|
+
const thresholds = defaultIdleThresholds();
|
|
69
|
+
const idle = useIdle({
|
|
70
|
+
...thresholds,
|
|
71
|
+
disabled: keepAwake || idleDisabled(),
|
|
72
|
+
});
|
|
73
|
+
const slowColor = useSlowColor();
|
|
26
74
|
useEffect(() => {
|
|
27
75
|
setI(0);
|
|
28
76
|
if (frames.length <= 1)
|
|
@@ -30,9 +78,12 @@ export const MascotPrompt = ({ state }) => {
|
|
|
30
78
|
const t = setInterval(() => setI((v) => (v + 1) % frames.length), SPEEDS[state]);
|
|
31
79
|
return () => clearInterval(t);
|
|
32
80
|
}, [state, frames.length]);
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
81
|
+
if (idle === 'sleeping') {
|
|
82
|
+
return (_jsxs(Box, { flexDirection: "column", height: TOP_ROWS + 1, children: [_jsx(Zzz, {}), _jsx(Text, { color: theme.muted, bold: true, children: ' ' + SLEEPING_FACE })] }));
|
|
83
|
+
}
|
|
84
|
+
if (idle === 'drowsy') {
|
|
85
|
+
return (_jsxs(Box, { flexDirection: "column", height: TOP_ROWS + 1, children: [_jsx(BlankRows, {}), _jsx(Text, { color: theme.muted, bold: true, children: ' ' + DROWSY_FACE })] }));
|
|
86
|
+
}
|
|
87
|
+
const color = isSemantic(state) ? semanticColor(state) : slowColor;
|
|
88
|
+
return (_jsxs(Box, { flexDirection: "column", height: TOP_ROWS + 1, children: [_jsx(BlankRows, {}), _jsx(Text, { color: color, bold: true, children: ' ' + frames[i] })] }));
|
|
38
89
|
};
|
package/dist/ui/RocketBar.js
CHANGED
|
@@ -3,7 +3,7 @@ import { useState, useEffect } from 'react';
|
|
|
3
3
|
import { Text } from 'ink';
|
|
4
4
|
import os from 'os';
|
|
5
5
|
import { theme } from './theme.js';
|
|
6
|
-
import {
|
|
6
|
+
import { useSlowColor } from './useSlowColor.js';
|
|
7
7
|
function shouldAnimate() {
|
|
8
8
|
if (process.env.BLYUE_ANIMATE === '1')
|
|
9
9
|
return true;
|
|
@@ -25,20 +25,13 @@ function shouldAnimate() {
|
|
|
25
25
|
}
|
|
26
26
|
export const RocketBar = ({ label, loading, width }) => {
|
|
27
27
|
const [animate] = useState(() => shouldAnimate());
|
|
28
|
-
const [t, setT] = useState(0);
|
|
29
28
|
const [pos, setPos] = useState(0);
|
|
30
|
-
|
|
31
|
-
if (!animate)
|
|
32
|
-
return;
|
|
33
|
-
const int = setInterval(() => setT((v) => (v + 0.00625) % 1), 120);
|
|
34
|
-
return () => clearInterval(int);
|
|
35
|
-
}, [animate]);
|
|
29
|
+
const hue = useSlowColor(); // shared pastel clock — matches banner + face
|
|
36
30
|
const fixed = 5 + label.length;
|
|
37
31
|
const midLen = Math.max(0, width - fixed);
|
|
38
32
|
useEffect(() => {
|
|
39
33
|
if (!loading || midLen < 6)
|
|
40
34
|
return;
|
|
41
|
-
// Static mode: no rocket motion, just fixed at 1/3 position
|
|
42
35
|
if (!animate) {
|
|
43
36
|
setPos(Math.floor(midLen / 3));
|
|
44
37
|
return;
|
|
@@ -47,9 +40,8 @@ export const RocketBar = ({ label, loading, width }) => {
|
|
|
47
40
|
const int = setInterval(() => setPos((p) => (p + 1) % track), 90);
|
|
48
41
|
return () => clearInterval(int);
|
|
49
42
|
}, [loading, midLen, animate]);
|
|
50
|
-
const hue = animate ? hueAt(t) : hueAt(0.1);
|
|
51
43
|
if (!loading || midLen < 6) {
|
|
52
|
-
return (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '╭─ ' }), _jsx(Text, { color:
|
|
44
|
+
return (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '╭─ ' }), _jsx(Text, { color: hue, bold: true, children: label }), _jsx(Text, { color: theme.muted, children: ' ' + '─'.repeat(midLen) + '╮' })] }));
|
|
53
45
|
}
|
|
54
46
|
const p = Math.min(pos, Math.max(0, midLen - 3));
|
|
55
47
|
const before = '─'.repeat(p);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect } from 'react';
|
|
3
|
+
import { Box, Text } from 'ink';
|
|
4
|
+
import { theme } from './theme.js';
|
|
5
|
+
// Two rows, always exactly two rows — the mascot container reserves this
|
|
6
|
+
// space whether a z is visible or not, so Ink never reflows.
|
|
7
|
+
const FRAMES = [
|
|
8
|
+
[' ', ' '],
|
|
9
|
+
[' ', ' z '],
|
|
10
|
+
[' ', ' z '],
|
|
11
|
+
[' z ', ' z '],
|
|
12
|
+
[' z ', ' z '],
|
|
13
|
+
[' z ', ' '],
|
|
14
|
+
[' z', ' '],
|
|
15
|
+
[' ', ' '],
|
|
16
|
+
];
|
|
17
|
+
const SPEED_MS = 800;
|
|
18
|
+
export const Zzz = () => {
|
|
19
|
+
const [i, setI] = useState(0);
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
const t = setInterval(() => setI((v) => (v + 1) % FRAMES.length), SPEED_MS);
|
|
22
|
+
return () => clearInterval(t);
|
|
23
|
+
}, []);
|
|
24
|
+
const [far, near] = FRAMES[i];
|
|
25
|
+
return (_jsxs(Box, { flexDirection: "column", height: 2, children: [_jsx(Text, { color: theme.muted, children: far }), _jsx(Text, { color: theme.muted, children: near })] }));
|
|
26
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Tiny global activity bus — avoids prop-drilling an `activeSignal` number
|
|
2
|
+
// through every component. Chat.tsx calls bumpActivity() from useInput;
|
|
3
|
+
// MascotPrompt subscribes via useIdle().
|
|
4
|
+
let counter = 0;
|
|
5
|
+
const listeners = new Set();
|
|
6
|
+
/** Call this from any real user input handler (keystroke, submit). */
|
|
7
|
+
export function bumpActivity() {
|
|
8
|
+
counter = (counter + 1) % Number.MAX_SAFE_INTEGER;
|
|
9
|
+
for (const l of listeners)
|
|
10
|
+
l();
|
|
11
|
+
}
|
|
12
|
+
export function getActivity() {
|
|
13
|
+
return counter;
|
|
14
|
+
}
|
|
15
|
+
export function subscribeActivity(fn) {
|
|
16
|
+
listeners.add(fn);
|
|
17
|
+
return () => {
|
|
18
|
+
listeners.delete(fn);
|
|
19
|
+
};
|
|
20
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Pastel/faded palette — Claude-Code-inspired soft muting.
|
|
2
|
+
// All colors sit at ~76% lightness / ~48% saturation, so they read as
|
|
3
|
+
// "faded" on both dark and light terminals.
|
|
4
|
+
function hslToHex(h, s, l) {
|
|
5
|
+
s /= 100;
|
|
6
|
+
l /= 100;
|
|
7
|
+
const k = (n) => (n + h / 30) % 12;
|
|
8
|
+
const a = s * Math.min(l, 1 - l);
|
|
9
|
+
const f = (n) => {
|
|
10
|
+
const c = l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)));
|
|
11
|
+
return Math.round(255 * c);
|
|
12
|
+
};
|
|
13
|
+
const toHex = (x) => x.toString(16).padStart(2, '0');
|
|
14
|
+
return '#' + toHex(f(0)) + toHex(f(8)) + toHex(f(4));
|
|
15
|
+
}
|
|
16
|
+
/** Full-hue pastel cycle. t in [0, 1). */
|
|
17
|
+
export function pastelAt(t) {
|
|
18
|
+
const h = (((t % 1) + 1) % 1) * 360;
|
|
19
|
+
return hslToHex(h, 48, 76);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Warm-only variant — rose → peach → gold → sage → back. If the full
|
|
23
|
+
* wheel feels too rainbow-y for your taste, swap pastelAt -> pastelWarmAt
|
|
24
|
+
* in the four call sites below.
|
|
25
|
+
*/
|
|
26
|
+
export function pastelWarmAt(t) {
|
|
27
|
+
const t2 = ((t % 1) + 1) % 1;
|
|
28
|
+
const h = (320 + t2 * 110) % 360; // 320° → 70°, wraps through warm
|
|
29
|
+
return hslToHex(h, 52, 76);
|
|
30
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// A single module-level tick shared by every component that wants to
|
|
2
|
+
// fade in sync. One interval, N subscribers, no per-component drift.
|
|
3
|
+
const PERIOD_MS = 24_000; // full pastel cycle
|
|
4
|
+
const TICK_MS = 120;
|
|
5
|
+
let t = 0;
|
|
6
|
+
const listeners = new Set();
|
|
7
|
+
let interval = null;
|
|
8
|
+
function start() {
|
|
9
|
+
if (interval)
|
|
10
|
+
return;
|
|
11
|
+
interval = setInterval(() => {
|
|
12
|
+
t = (t + TICK_MS / PERIOD_MS) % 1;
|
|
13
|
+
for (const l of listeners)
|
|
14
|
+
l();
|
|
15
|
+
}, TICK_MS);
|
|
16
|
+
// Don't keep the Node process alive just to animate colors.
|
|
17
|
+
const anyInt = interval;
|
|
18
|
+
anyInt.unref?.();
|
|
19
|
+
}
|
|
20
|
+
function stop() {
|
|
21
|
+
if (interval) {
|
|
22
|
+
clearInterval(interval);
|
|
23
|
+
interval = null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export function subscribeSlowTick(fn) {
|
|
27
|
+
listeners.add(fn);
|
|
28
|
+
if (listeners.size === 1)
|
|
29
|
+
start();
|
|
30
|
+
return () => {
|
|
31
|
+
listeners.delete(fn);
|
|
32
|
+
if (listeners.size === 0)
|
|
33
|
+
stop();
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export function getSlowTick() {
|
|
37
|
+
return t;
|
|
38
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { useEffect, useRef, useState, useSyncExternalStore } from 'react';
|
|
2
|
+
import { subscribeActivity, getActivity } from './activity.js';
|
|
3
|
+
export function useActivity() {
|
|
4
|
+
return useSyncExternalStore(subscribeActivity, getActivity, getActivity);
|
|
5
|
+
}
|
|
6
|
+
export function useIdle({ drowsyAfterMs = 120_000, sleepAfterMs = 300_000, disabled = false, } = {}) {
|
|
7
|
+
const active = useActivity();
|
|
8
|
+
const [stage, setStage] = useState('awake');
|
|
9
|
+
const timersRef = useRef([]);
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
for (const t of timersRef.current)
|
|
12
|
+
clearTimeout(t);
|
|
13
|
+
timersRef.current = [];
|
|
14
|
+
if (disabled) {
|
|
15
|
+
setStage('awake');
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
setStage('awake');
|
|
19
|
+
const drowsyT = setTimeout(() => setStage('drowsy'), drowsyAfterMs);
|
|
20
|
+
const sleepT = setTimeout(() => setStage('sleeping'), sleepAfterMs);
|
|
21
|
+
timersRef.current = [drowsyT, sleepT];
|
|
22
|
+
return () => {
|
|
23
|
+
for (const t of timersRef.current)
|
|
24
|
+
clearTimeout(t);
|
|
25
|
+
timersRef.current = [];
|
|
26
|
+
};
|
|
27
|
+
}, [active, drowsyAfterMs, sleepAfterMs, disabled]);
|
|
28
|
+
return stage;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Idle thresholds.
|
|
32
|
+
* BLYUE_FAST_IDLE=1 -> 3s / 6s (dev testing only)
|
|
33
|
+
* default -> 2 min drowsy / 5 min sleep
|
|
34
|
+
*/
|
|
35
|
+
export function defaultIdleThresholds() {
|
|
36
|
+
if (process.env.BLYUE_FAST_IDLE === '1') {
|
|
37
|
+
return { drowsyAfterMs: 3_000, sleepAfterMs: 6_000 };
|
|
38
|
+
}
|
|
39
|
+
return { drowsyAfterMs: 120_000, sleepAfterMs: 300_000 };
|
|
40
|
+
}
|
|
41
|
+
export function idleDisabled() {
|
|
42
|
+
return process.env.BLYUE_NO_SLEEP === '1';
|
|
43
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { useSyncExternalStore } from 'react';
|
|
2
|
+
import { subscribeSlowTick, getSlowTick } from './slowTick.js';
|
|
3
|
+
import { pastelAt, pastelWarmAt } from './pastel.js';
|
|
4
|
+
/**
|
|
5
|
+
* Returns a pastel hex color that drifts slowly and stays in sync with
|
|
6
|
+
* every other component that uses this hook.
|
|
7
|
+
*/
|
|
8
|
+
export function useSlowColor(warm = false) {
|
|
9
|
+
const t = useSyncExternalStore(subscribeSlowTick, getSlowTick, getSlowTick);
|
|
10
|
+
return warm ? pastelWarmAt(t) : pastelAt(t);
|
|
11
|
+
}
|