blyuecode 0.1.14 → 0.1.15
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 +4 -4
- package/dist/ui/Chat.js +45 -11
- package/dist/ui/InputBar.js +15 -6
- package/dist/ui/MascotPrompt.js +59 -16
- package/dist/ui/SleepZs.js +26 -0
- package/dist/ui/activity.js +20 -0
- package/dist/ui/useIdle.js +43 -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,4 +1,4 @@
|
|
|
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';
|
|
@@ -63,6 +63,9 @@ const CODE_SMALL = [
|
|
|
63
63
|
];
|
|
64
64
|
const BLYUE_WIDTH = 44;
|
|
65
65
|
const BLYUE_SMALL_WIDTH = 27;
|
|
66
|
+
// Single <Text> — was previously a fragment with two <Text> children,
|
|
67
|
+
// which React/Ink flagged as "list without keys" under the parent Text.
|
|
68
|
+
const MakerMarkInline = ({ color }) => (_jsx(Text, { color: color, bold: true, children: "\u25C6 L U M E N" }));
|
|
66
69
|
export const Banner = () => {
|
|
67
70
|
const { stdout } = useStdout();
|
|
68
71
|
const width = stdout?.columns ?? 80;
|
|
@@ -70,7 +73,6 @@ export const Banner = () => {
|
|
|
70
73
|
const blyue = wide ? BLYUE_BIG : BLYUE_SMALL;
|
|
71
74
|
const code = wide ? CODE_BIG : CODE_SMALL;
|
|
72
75
|
const blockWidth = wide ? BLYUE_WIDTH : BLYUE_SMALL_WIDTH;
|
|
73
|
-
// Left padding to center whole block in terminal
|
|
74
76
|
const leftPad = Math.max(0, Math.floor((width - blockWidth) / 2));
|
|
75
77
|
const pad = ' '.repeat(leftPad);
|
|
76
78
|
const [animate] = useState(() => shouldAnimate());
|
|
@@ -86,5 +88,3 @@ export const Banner = () => {
|
|
|
86
88
|
const version = getVersion();
|
|
87
89
|
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [pad, _jsx(MakerMarkInline, { color: brand })] }), _jsx(Text, { children: " " }), blyue.map((line, i) => (_jsx(Text, { color: hueAt((baseHue + i * 0.06) % 1), bold: true, children: pad + line }, 'b' + i))), code.map((line, i) => (_jsx(Text, { color: hueAt((baseHue + i * 0.06 + 0.35) % 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
90
|
};
|
|
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,6 +11,8 @@ 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 { bumpActivity } from './activity.js';
|
|
14
16
|
import { theme } from './theme.js';
|
|
15
17
|
let nextId = 0;
|
|
16
18
|
const mkItem = (kind, text) => ({ kind, text, id: nextId++ });
|
|
@@ -63,12 +65,36 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
63
65
|
const [permReq, setPermReq] = useState(null);
|
|
64
66
|
const [currentModel, setCurrentModel] = useState(process.env.MODEL || '');
|
|
65
67
|
const [agent, setAgent] = useState(() => new Agent());
|
|
66
|
-
|
|
68
|
+
// Mascot state
|
|
69
|
+
const [mascotState, setMascotState] = useState(undefined);
|
|
70
|
+
const [elapsedSec, setElapsedSec] = useState(0);
|
|
71
|
+
const startRef = useRef(null);
|
|
67
72
|
useEffect(() => {
|
|
68
73
|
const unsub = permissions.subscribe((req) => setPermReq(req));
|
|
69
74
|
return unsub;
|
|
70
75
|
}, []);
|
|
76
|
+
// Track elapsed time during loading
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
if (!loading) {
|
|
79
|
+
startRef.current = null;
|
|
80
|
+
setElapsedSec(0);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
startRef.current = Date.now();
|
|
84
|
+
const t = setInterval(() => {
|
|
85
|
+
if (startRef.current) {
|
|
86
|
+
setElapsedSec(Math.floor((Date.now() - startRef.current) / 1000));
|
|
87
|
+
}
|
|
88
|
+
}, 500);
|
|
89
|
+
return () => clearInterval(t);
|
|
90
|
+
}, [loading]);
|
|
91
|
+
// Flash a temporary mascot state
|
|
92
|
+
const flash = (state, ms = 1200) => {
|
|
93
|
+
setMascotState(state);
|
|
94
|
+
setTimeout(() => setMascotState(undefined), ms);
|
|
95
|
+
};
|
|
71
96
|
useInput((inputChar, key) => {
|
|
97
|
+
bumpActivity();
|
|
72
98
|
if (permReq)
|
|
73
99
|
return;
|
|
74
100
|
if (key.ctrl && inputChar === 'p')
|
|
@@ -82,6 +108,7 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
82
108
|
setHistory((h) => [...h, mkItem(kind, text)]);
|
|
83
109
|
};
|
|
84
110
|
const handleSubmit = async (text) => {
|
|
111
|
+
bumpActivity();
|
|
85
112
|
if (!text.trim() || loading)
|
|
86
113
|
return;
|
|
87
114
|
const trimmed = text.trim();
|
|
@@ -110,15 +137,18 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
110
137
|
setInput('');
|
|
111
138
|
setLoading(true);
|
|
112
139
|
setStreaming('');
|
|
140
|
+
flash('thinking', 1000);
|
|
113
141
|
try {
|
|
114
142
|
const r = await agent.compact();
|
|
115
143
|
setHistory((h) => [
|
|
116
144
|
...h,
|
|
117
145
|
mkItem('tool', `Compacted: ${r.before} → ${r.after} messages (summary ${r.summaryLen} chars)`),
|
|
118
146
|
]);
|
|
147
|
+
flash('success', 1500);
|
|
119
148
|
}
|
|
120
149
|
catch (e) {
|
|
121
150
|
setHistory((h) => [...h, mkItem('error', e.message)]);
|
|
151
|
+
flash('error', 2000);
|
|
122
152
|
}
|
|
123
153
|
setLoading(false);
|
|
124
154
|
return;
|
|
@@ -128,6 +158,7 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
128
158
|
agent.clear();
|
|
129
159
|
permissions.resetSession();
|
|
130
160
|
setHistory([mkItem('tool', '(conversation cleared)')]);
|
|
161
|
+
flash('success', 1200);
|
|
131
162
|
return;
|
|
132
163
|
}
|
|
133
164
|
if (trimmed === '/help') {
|
|
@@ -150,8 +181,7 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
150
181
|
setLoading(true);
|
|
151
182
|
setStreaming('');
|
|
152
183
|
// Jump animation on submit
|
|
153
|
-
|
|
154
|
-
setTimeout(() => setReaction(null), 400);
|
|
184
|
+
flash('jump', 400);
|
|
155
185
|
let textBuf = '';
|
|
156
186
|
let toolBuf = '';
|
|
157
187
|
try {
|
|
@@ -159,6 +189,8 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
159
189
|
if (ev.type === 'text') {
|
|
160
190
|
textBuf += ev.delta;
|
|
161
191
|
setStreaming(textBuf);
|
|
192
|
+
// Clear any tool mascot while text streams
|
|
193
|
+
setMascotState(undefined);
|
|
162
194
|
}
|
|
163
195
|
else if (ev.type === 'tool_start') {
|
|
164
196
|
if (textBuf) {
|
|
@@ -167,26 +199,28 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
167
199
|
}
|
|
168
200
|
toolBuf = '\n[Calling ' + ev.name + '...]\n';
|
|
169
201
|
setStreaming(toolBuf);
|
|
202
|
+
// React to the specific tool
|
|
203
|
+
setMascotState(toolToState(ev.name));
|
|
170
204
|
}
|
|
171
205
|
else if (ev.type === 'tool_end') {
|
|
172
206
|
flush('tool', toolBuf + '[Result]: ' + ev.preview + '\n');
|
|
173
207
|
toolBuf = '';
|
|
174
208
|
setStreaming('');
|
|
209
|
+
// Brief pulse before next step
|
|
210
|
+
flash('success', 500);
|
|
175
211
|
}
|
|
176
212
|
}
|
|
177
213
|
if (textBuf)
|
|
178
214
|
flush('ai', textBuf);
|
|
179
|
-
|
|
180
|
-
setReaction('success');
|
|
181
|
-
setTimeout(() => setReaction(null), 2000);
|
|
215
|
+
flash('success', 2000);
|
|
182
216
|
}
|
|
183
217
|
catch (e) {
|
|
184
218
|
setHistory((h) => [...h, mkItem('error', e?.message || String(e))]);
|
|
185
|
-
|
|
186
|
-
setTimeout(() => setReaction(null), 3000);
|
|
219
|
+
flash('error', 3000);
|
|
187
220
|
}
|
|
188
221
|
setStreaming('');
|
|
189
222
|
setLoading(false);
|
|
223
|
+
setMascotState(undefined);
|
|
190
224
|
};
|
|
191
225
|
const handleModelSelect = (id) => {
|
|
192
226
|
process.env.MODEL = id;
|
|
@@ -196,7 +230,7 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
196
230
|
setHistory((h) => [...h, mkItem('tool', `model → ${id}`)]);
|
|
197
231
|
};
|
|
198
232
|
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 }) })] }));
|
|
233
|
+
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
234
|
}
|
|
201
235
|
if (overlay === 'model') {
|
|
202
236
|
return (_jsx(Box, { flexDirection: "column", children: _jsx(ModelPicker, { current: currentModel, onSelect: handleModelSelect, onCancel: () => setOverlay('none') }) }));
|
|
@@ -211,5 +245,5 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
211
245
|
const shortModel = currentModel.length > maxModelLen
|
|
212
246
|
? '…' + currentModel.slice(-(maxModelLen - 1))
|
|
213
247
|
: 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,
|
|
248
|
+
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: theme.accent, children: " /model" }), _jsx(Text, { color: theme.muted, children: " \u00B7 " }), _jsx(Text, { color: theme.accent, children: "/settings" }), _jsx(Text, { color: theme.muted, children: " \u00B7 " }), _jsx(Text, { color: theme.accent, children: "/compact" }), _jsx(Text, { color: theme.muted, children: " \u00B7 " }), _jsx(Text, { color: theme.text, children: shortModel })] })] })] }));
|
|
215
249
|
};
|
package/dist/ui/InputBar.js
CHANGED
|
@@ -7,7 +7,7 @@ import { RocketBar } from './RocketBar.js';
|
|
|
7
7
|
import { MascotPrompt } from './MascotPrompt.js';
|
|
8
8
|
import { theme } from './theme.js';
|
|
9
9
|
const DOTS = ['●○○○', '○●○○', '○○●○', '○○○●'];
|
|
10
|
-
export const InputBar = ({ value, onChange, onSubmit, loading,
|
|
10
|
+
export const InputBar = ({ value, onChange, onSubmit, loading, mascotState, awaitingPermission, elapsedSec = 0, }) => {
|
|
11
11
|
const { stdout } = useStdout();
|
|
12
12
|
const width = Math.max(24, stdout?.columns ?? 50);
|
|
13
13
|
const [dotI, setDotI] = useState(0);
|
|
@@ -18,11 +18,20 @@ export const InputBar = ({ value, onChange, onSubmit, loading, reaction }) => {
|
|
|
18
18
|
return () => clearInterval(int);
|
|
19
19
|
}, [loading]);
|
|
20
20
|
const hasText = value.length > 0;
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
// Priority:
|
|
22
|
+
// explicit mascotState
|
|
23
|
+
// → permission waiting
|
|
24
|
+
// → impatient if running > 8s
|
|
25
|
+
// → thinking if loading
|
|
26
|
+
// → typing if has text
|
|
27
|
+
// → idle
|
|
28
|
+
const state = mascotState ??
|
|
29
|
+
(awaitingPermission ? 'permission' :
|
|
30
|
+
loading && elapsedSec > 8 ? 'impatient' :
|
|
31
|
+
loading ? 'thinking' :
|
|
32
|
+
hasText ? 'typing' : 'idle');
|
|
33
|
+
const label = awaitingPermission ? 'waiting' : loading ? 'thinking' : 'blyue';
|
|
25
34
|
const status = loading ? ' ' + DOTS[dotI] + ' ' : '';
|
|
26
35
|
const botLen = Math.max(0, width - 2 - status.length);
|
|
27
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(MascotPrompt, { state:
|
|
36
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(MascotPrompt, { state: state }), _jsx(RocketBar, { label: label, loading: loading, width: width }), _jsxs(Box, { children: [_jsx(Text, { color: theme.accent, 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: theme.accent, children: status }), _jsx(Text, { color: theme.muted, children: "\u256F" })] })] }));
|
|
28
37
|
};
|
package/dist/ui/MascotPrompt.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
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 { Zzz } from './SleepZs.js';
|
|
6
7
|
const FACES = {
|
|
7
8
|
idle: ['◕‿◕', '◕‿◕', '◕_◕', '◕‿◕', '◕‿◕', '◕‿◕', '◕_◕', '◕‿◕'],
|
|
8
9
|
typing: ['◉‿◉', '◉‿◉', '◉‿◉', '◉_◉'],
|
|
@@ -10,19 +11,52 @@ const FACES = {
|
|
|
10
11
|
success: ['^‿^', '^▽^', '^‿^'],
|
|
11
12
|
error: ['◔︵◔', '◔︵◔', '◔_◔'],
|
|
12
13
|
jump: ['◕‿◕', '◉▽◉', '^▽^', '◉▽◉', '◕‿◕'],
|
|
14
|
+
read: ['◉_◉', '◎_◎', '◉_◉'],
|
|
15
|
+
write: ['◕✎◕', '◕✐◕', '◕✎◕'],
|
|
16
|
+
patch: ['◕~◕', '◕≈◕', '◕~◕'],
|
|
17
|
+
list: ['◉◡◉', '◉◠◉', '◉◡◉'],
|
|
18
|
+
command: ['◕⌨◕', '◕⎚◕', '◕⌨◕'],
|
|
19
|
+
automate: ['◕⚙◕', '◕⚙︎◕', '◕⚙◕'],
|
|
20
|
+
permission: ['◔_◔', '◔◡◔', '◔_◔'],
|
|
21
|
+
granted: ['◉‿◉', '◉▽◉', '◉‿◉'],
|
|
22
|
+
denied: ['◔︵◔', '◔_◔', '◔︵◔'],
|
|
23
|
+
impatient: ['◕~◕', '⊙_⊙', '◕~◕', '⊙_⊙'],
|
|
13
24
|
};
|
|
14
|
-
// Frame speeds in ms
|
|
15
25
|
const SPEEDS = {
|
|
16
|
-
idle: 500,
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
error: 400,
|
|
21
|
-
jump: 70,
|
|
26
|
+
idle: 500, typing: 700, thinking: 120,
|
|
27
|
+
success: 300, error: 400, jump: 70,
|
|
28
|
+
read: 220, write: 260, patch: 260, list: 260, command: 220,
|
|
29
|
+
automate: 200, permission: 450, granted: 180, denied: 350, impatient: 300,
|
|
22
30
|
};
|
|
23
|
-
|
|
31
|
+
const DROWSY_FACE = '–_–';
|
|
32
|
+
const SLEEPING_FACE = '-‿-';
|
|
33
|
+
// Reserved vertical rows above the face. Every state renders exactly
|
|
34
|
+
// this many rows so Ink never has to reflow the layout below us.
|
|
35
|
+
const TOP_ROWS = 2;
|
|
36
|
+
export function toolToState(tool) {
|
|
37
|
+
if (tool === 'read_file')
|
|
38
|
+
return 'read';
|
|
39
|
+
if (tool === 'write_file')
|
|
40
|
+
return 'write';
|
|
41
|
+
if (tool === 'apply_patch')
|
|
42
|
+
return 'patch';
|
|
43
|
+
if (tool === 'list_files')
|
|
44
|
+
return 'list';
|
|
45
|
+
if (tool === 'run_command')
|
|
46
|
+
return 'command';
|
|
47
|
+
if (tool.startsWith('n8n_'))
|
|
48
|
+
return 'automate';
|
|
49
|
+
return 'thinking';
|
|
50
|
+
}
|
|
51
|
+
const BlankRows = () => (_jsx(_Fragment, { children: Array.from({ length: TOP_ROWS }, (_, i) => (_jsx(Text, { children: " " }, i))) }));
|
|
52
|
+
export const MascotPrompt = ({ state, keepAwake = false, }) => {
|
|
24
53
|
const frames = FACES[state];
|
|
25
54
|
const [i, setI] = useState(0);
|
|
55
|
+
const thresholds = defaultIdleThresholds();
|
|
56
|
+
const idle = useIdle({
|
|
57
|
+
...thresholds,
|
|
58
|
+
disabled: keepAwake || idleDisabled(),
|
|
59
|
+
});
|
|
26
60
|
useEffect(() => {
|
|
27
61
|
setI(0);
|
|
28
62
|
if (frames.length <= 1)
|
|
@@ -30,9 +64,18 @@ export const MascotPrompt = ({ state }) => {
|
|
|
30
64
|
const t = setInterval(() => setI((v) => (v + 1) % frames.length), SPEEDS[state]);
|
|
31
65
|
return () => clearInterval(t);
|
|
32
66
|
}, [state, frames.length]);
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
67
|
+
if (idle === 'sleeping') {
|
|
68
|
+
return (_jsxs(Box, { flexDirection: "column", height: TOP_ROWS + 1, children: [_jsx(Zzz, {}), _jsx(Text, { color: theme.muted, bold: true, children: ' ' + SLEEPING_FACE })] }));
|
|
69
|
+
}
|
|
70
|
+
if (idle === 'drowsy') {
|
|
71
|
+
return (_jsxs(Box, { flexDirection: "column", height: TOP_ROWS + 1, children: [_jsx(BlankRows, {}), _jsx(Text, { color: theme.muted, bold: true, children: ' ' + DROWSY_FACE })] }));
|
|
72
|
+
}
|
|
73
|
+
const color = state === 'error' || state === 'denied'
|
|
74
|
+
? theme.error
|
|
75
|
+
: state === 'success' || state === 'granted'
|
|
76
|
+
? theme.success
|
|
77
|
+
: state === 'permission' || state === 'impatient'
|
|
78
|
+
? theme.warning
|
|
79
|
+
: theme.accent;
|
|
80
|
+
return (_jsxs(Box, { flexDirection: "column", height: TOP_ROWS + 1, children: [_jsx(BlankRows, {}), _jsx(Text, { color: color, bold: true, children: ' ' + frames[i] })] }));
|
|
38
81
|
};
|
|
@@ -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,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
|
+
}
|