blyuecode 0.1.15 → 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/ui/Banner.js CHANGED
@@ -5,7 +5,7 @@ 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 { hueAt } from './animColor.js';
8
+ import { pastelAt } from './pastel.js';
9
9
  function getVersion() {
10
10
  try {
11
11
  const here = dirname(fileURLToPath(import.meta.url));
@@ -63,8 +63,6 @@ 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
66
  const MakerMarkInline = ({ color }) => (_jsx(Text, { color: color, bold: true, children: "\u25C6 L U M E N" }));
69
67
  export const Banner = () => {
70
68
  const { stdout } = useStdout();
@@ -77,14 +75,15 @@ export const Banner = () => {
77
75
  const pad = ' '.repeat(leftPad);
78
76
  const [animate] = useState(() => shouldAnimate());
79
77
  const [t, setT] = useState(0);
78
+ // Slow pastel drift — 24s full cycle, matches the rest of the TUI.
80
79
  useEffect(() => {
81
80
  if (!animate)
82
81
  return;
83
- const interval = setInterval(() => setT((v) => (v + 1 / 320) % 1), 120);
82
+ const interval = setInterval(() => setT((v) => (v + 1 / 200) % 1), 120);
84
83
  return () => clearInterval(interval);
85
84
  }, [animate]);
86
- const baseHue = animate ? t : 0.1;
87
- const brand = hueAt(baseHue);
85
+ const baseHue = animate ? t : 0.08;
86
+ const brand = pastelAt(baseHue);
88
87
  const version = getVersion();
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
+ 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: " " })] }));
90
89
  };
package/dist/ui/Chat.js CHANGED
@@ -12,13 +12,15 @@ import { Settings } from './Settings.js';
12
12
  import { PermissionPrompt } from './PermissionPrompt.js';
13
13
  import { Spinner } from './Spinner.js';
14
14
  import { toolToState } from './MascotPrompt.js';
15
+ import { useSlowColor } from './useSlowColor.js';
16
+ import { pastelAt } from './pastel.js';
15
17
  import { bumpActivity } from './activity.js';
16
18
  import { theme } from './theme.js';
17
19
  let nextId = 0;
18
20
  const mkItem = (kind, text) => ({ kind, text, id: nextId++ });
19
21
  const HistoryLine = ({ item }) => {
20
22
  if (item.kind === 'user') {
21
- return (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.accent, bold: true, children: '❯ ' }), _jsx(Text, { color: theme.text, bold: true, children: item.text })] }));
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 })] }));
22
24
  }
23
25
  if (item.kind === 'error') {
24
26
  return (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.error, children: '❯ ' }), _jsx(Text, { color: theme.error, children: item.text })] }));
@@ -93,6 +95,7 @@ export const Chat = ({ resumedCount = 0 }) => {
93
95
  setMascotState(state);
94
96
  setTimeout(() => setMascotState(undefined), ms);
95
97
  };
98
+ const slowColor = useSlowColor();
96
99
  useInput((inputChar, key) => {
97
100
  bumpActivity();
98
101
  if (permReq)
@@ -245,5 +248,5 @@ export const Chat = ({ resumedCount = 0 }) => {
245
248
  const shortModel = currentModel.length > maxModelLen
246
249
  ? '…' + currentModel.slice(-(maxModelLen - 1))
247
250
  : currentModel || 'unset';
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 })] })] })] }));
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 })] })] })] }));
249
252
  };
@@ -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
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,13 +20,6 @@ export const InputBar = ({ value, onChange, onSubmit, loading, mascotState, awai
18
20
  return () => clearInterval(int);
19
21
  }, [loading]);
20
22
  const hasText = value.length > 0;
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
23
  const state = mascotState ??
29
24
  (awaitingPermission ? 'permission' :
30
25
  loading && elapsedSec > 8 ? 'impatient' :
@@ -33,5 +28,5 @@ export const InputBar = ({ value, onChange, onSubmit, loading, mascotState, awai
33
28
  const label = awaitingPermission ? 'waiting' : loading ? 'thinking' : 'blyue';
34
29
  const status = loading ? ' ' + DOTS[dotI] + ' ' : '';
35
30
  const botLen = Math.max(0, width - 2 - status.length);
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" })] })] }));
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" })] })] }));
37
32
  };
@@ -3,6 +3,7 @@ import { useState, useEffect } from 'react';
3
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';
6
7
  import { Zzz } from './SleepZs.js';
7
8
  const FACES = {
8
9
  idle: ['◕‿◕', '◕‿◕', '◕_◕', '◕‿◕', '◕‿◕', '◕‿◕', '◕_◕', '◕‿◕'],
@@ -30,8 +31,6 @@ const SPEEDS = {
30
31
  };
31
32
  const DROWSY_FACE = '–_–';
32
33
  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
34
  const TOP_ROWS = 2;
36
35
  export function toolToState(tool) {
37
36
  if (tool === 'read_file')
@@ -49,6 +48,20 @@ export function toolToState(tool) {
49
48
  return 'thinking';
50
49
  }
51
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
+ }
52
65
  export const MascotPrompt = ({ state, keepAwake = false, }) => {
53
66
  const frames = FACES[state];
54
67
  const [i, setI] = useState(0);
@@ -57,6 +70,7 @@ export const MascotPrompt = ({ state, keepAwake = false, }) => {
57
70
  ...thresholds,
58
71
  disabled: keepAwake || idleDisabled(),
59
72
  });
73
+ const slowColor = useSlowColor();
60
74
  useEffect(() => {
61
75
  setI(0);
62
76
  if (frames.length <= 1)
@@ -70,12 +84,6 @@ export const MascotPrompt = ({ state, keepAwake = false, }) => {
70
84
  if (idle === 'drowsy') {
71
85
  return (_jsxs(Box, { flexDirection: "column", height: TOP_ROWS + 1, children: [_jsx(BlankRows, {}), _jsx(Text, { color: theme.muted, bold: true, children: ' ' + DROWSY_FACE })] }));
72
86
  }
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;
87
+ const color = isSemantic(state) ? semanticColor(state) : slowColor;
80
88
  return (_jsxs(Box, { flexDirection: "column", height: TOP_ROWS + 1, children: [_jsx(BlankRows, {}), _jsx(Text, { color: color, bold: true, children: ' ' + frames[i] })] }));
81
89
  };
@@ -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 { hueAt } from './animColor.js';
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
- useEffect(() => {
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: loading ? hue : theme.accent, bold: true, children: label }), _jsx(Text, { color: theme.muted, children: ' ' + '─'.repeat(midLen) + '╮' })] }));
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,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,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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blyuecode",
3
- "version": "0.1.15",
3
+ "version": "0.1.16",
4
4
  "description": "BLYUE Code — a coding agent by Lumen",
5
5
  "type": "module",
6
6
  "license": "MIT",