blyuecode 0.3.7 → 0.3.8

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.
@@ -124,7 +124,6 @@ export class Agent {
124
124
  const before = this.messages.length;
125
125
  const sys = this.messages[0];
126
126
  const rest = this.messages.slice(1);
127
- // Build a compact transcript
128
127
  const transcript = rest
129
128
  .map((m) => {
130
129
  if (m.role === 'tool') {
package/dist/core/diff.js CHANGED
@@ -1,10 +1,8 @@
1
- // Simple LCS-based line diff. Good enough for previews.
2
1
  export function lineDiff(oldText, newText) {
3
2
  const a = oldText.length ? oldText.split('\n') : [];
4
3
  const b = newText.length ? newText.split('\n') : [];
5
4
  const m = a.length;
6
5
  const n = b.length;
7
- // LCS table
8
6
  const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
9
7
  for (let i = m - 1; i >= 0; i--) {
10
8
  for (let j = n - 1; j >= 0; j--) {
@@ -38,7 +36,6 @@ export function lineDiff(oldText, newText) {
38
36
  out.push({ type: 'add', text: b[j++] });
39
37
  return out;
40
38
  }
41
- // Compact display: only show changed regions with context
42
39
  export function compactDiff(diff, context = 2) {
43
40
  const keep = new Set();
44
41
  diff.forEach((d, idx) => {
@@ -3,8 +3,7 @@ import path from 'path';
3
3
  import os from 'os';
4
4
  const URL = 'https://openrouter.ai/api/v1/models';
5
5
  const CACHE = path.join(os.homedir(), '.blyue', 'models-cache.json');
6
- const TTL_MS = 5 * 60 * 1000; // 5 minutes
7
- // Fallback if API is down or first-run offline
6
+ const TTL_MS = 5 * 60 * 1000;
8
7
  const FALLBACK = [
9
8
  { id: 'meta-llama/llama-3.3-70b-instruct:free', name: 'Llama 3.3 70B', context: 131072 },
10
9
  { id: 'google/gemini-2.0-flash-exp:free', name: 'Gemini 2.0 Flash', context: 1048576 },
@@ -12,7 +11,6 @@ const FALLBACK = [
12
11
  { id: 'qwen/qwen-2.5-coder-32b-instruct:free', name: 'Qwen 2.5 Coder 32B', context: 32768 },
13
12
  ];
14
13
  export async function fetchFreeModels(apiKey) {
15
- // Try cache first
16
14
  try {
17
15
  const raw = fs.readFileSync(CACHE, 'utf8');
18
16
  const cached = JSON.parse(raw);
@@ -44,7 +42,6 @@ export async function fetchFreeModels(apiKey) {
44
42
  context: m.context_length || 0,
45
43
  }))
46
44
  .sort((a, b) => b.context - a.context);
47
- // Save cache
48
45
  fs.mkdirSync(path.dirname(CACHE), { recursive: true });
49
46
  fs.writeFileSync(CACHE, JSON.stringify({ ts: Date.now(), models }));
50
47
  return models.length > 0 ? models : FALLBACK;
package/dist/core/mcp.js CHANGED
@@ -1,19 +1,5 @@
1
- // Minimal MCP client — stdio + HTTP/SSE transports, JSON-RPC 2.0.
2
- // Spec: https://spec.modelcontextprotocol.io/
3
- //
4
- // No external dependencies. Uses Node 20's global fetch for SSE.
5
- //
6
- // Usage:
7
- // const client = new McpClient(name, config);
8
- // await client.connect();
9
- // const tools = await client.listTools();
10
- // const result = await client.callTool('read_file', { path: '...' });
11
- // await client.close();
12
1
  import { spawn } from 'child_process';
13
2
  import { EventEmitter } from 'events';
14
- // ──────────────────────────────────────────────────────────────
15
- // Stdio transport — newline-delimited JSON-RPC over child stdin/stdout
16
- // ──────────────────────────────────────────────────────────────
17
3
  class StdioTransport extends EventEmitter {
18
4
  proc;
19
5
  buf = '';
@@ -39,16 +25,10 @@ class StdioTransport extends EventEmitter {
39
25
  this.emit('message', msg);
40
26
  }
41
27
  catch {
42
- // Not JSON — ignore (some servers log to stdout, which is rude
43
- // but happens; stderr is the correct channel for logs).
44
28
  }
45
29
  }
46
30
  });
47
31
  this.proc.stderr?.on('data', (chunk) => {
48
- // Servers log readiness banners and diagnostics to stderr — that's
49
- // the MCP spec's designated log channel. Never treat stderr as an
50
- // error: it would kill the in-flight initialize request. Keep the
51
- // last few KB around for debugging, but do nothing else with it.
52
32
  this.lastStderr = (this.lastStderr + chunk.toString('utf8')).slice(-4096);
53
33
  });
54
34
  this.proc.on('error', (err) => this.emit('error', err));
@@ -64,26 +44,15 @@ class StdioTransport extends EventEmitter {
64
44
  try {
65
45
  this.proc.stdin?.end();
66
46
  this.proc.kill('SIGTERM');
67
- // SIGKILL fallback if it doesn't die within 2s.
68
47
  setTimeout(() => {
69
48
  if (this.proc.exitCode === null)
70
49
  this.proc.kill('SIGKILL');
71
50
  }, 2000).unref?.();
72
51
  }
73
52
  catch {
74
- // already dead
75
53
  }
76
54
  }
77
55
  }
78
- // ──────────────────────────────────────────────────────────────
79
- // SSE transport — POST for sends, GET text/event-stream for receives
80
- //
81
- // MCP's HTTP+SSE transport (2024-11-05 spec):
82
- // 1. GET url -> text/event-stream, server sends an `endpoint` event
83
- // with the POST URL to use for requests.
84
- // 2. POST JSON-RPC requests to that endpoint; responses come back on
85
- // the SSE stream as `message` events.
86
- // ──────────────────────────────────────────────────────────────
87
56
  class SseTransport extends EventEmitter {
88
57
  cfg;
89
58
  postUrl = null;
@@ -141,8 +110,6 @@ class SseTransport extends EventEmitter {
141
110
  if (!data)
142
111
  return;
143
112
  if (event === 'endpoint') {
144
- // Server tells us where to POST. Relative URLs are resolved against
145
- // the SSE endpoint's origin.
146
113
  try {
147
114
  this.postUrl = new URL(data, this.cfg.url).toString();
148
115
  }
@@ -157,7 +124,6 @@ class SseTransport extends EventEmitter {
157
124
  this.emit('message', msg);
158
125
  }
159
126
  catch {
160
- // ignore malformed
161
127
  }
162
128
  }
163
129
  }
@@ -179,9 +145,6 @@ class SseTransport extends EventEmitter {
179
145
  this.abort?.abort();
180
146
  }
181
147
  }
182
- // ──────────────────────────────────────────────────────────────
183
- // Client
184
- // ──────────────────────────────────────────────────────────────
185
148
  export class McpClient {
186
149
  name;
187
150
  cfg;
@@ -210,7 +173,6 @@ export class McpClient {
210
173
  : new SseTransport(this.cfg);
211
174
  this.transport.on('message', (msg) => this.handleMessage(msg));
212
175
  this.transport.on('error', (err) => {
213
- // Route transport errors to any in-flight request so they don't hang.
214
176
  for (const [, p] of this.pending) {
215
177
  clearTimeout(p.timer);
216
178
  p.reject(err);
@@ -220,14 +182,12 @@ export class McpClient {
220
182
  this.transport.on('close', () => {
221
183
  this.connected = false;
222
184
  });
223
- // Handshake
224
185
  const result = (await this.request('initialize', {
225
186
  protocolVersion: '2024-11-05',
226
187
  capabilities: { tools: {} },
227
188
  clientInfo: { name: 'blyue', version: '0.2.0' },
228
189
  }));
229
190
  this.serverInfo = result?.serverInfo ?? null;
230
- // Spec requires this notification after initialize.
231
191
  this.notify('notifications/initialized', {});
232
192
  this.connected = true;
233
193
  }
@@ -256,7 +216,6 @@ export class McpClient {
256
216
  this.connected = false;
257
217
  this.cachedTools = null;
258
218
  }
259
- // ────────────────────────────────────────────
260
219
  handleMessage(msg) {
261
220
  if ('id' in msg && msg.id !== undefined) {
262
221
  const p = this.pending.get(msg.id);
@@ -272,7 +231,6 @@ export class McpClient {
272
231
  }
273
232
  return;
274
233
  }
275
- // Notifications from server (e.g. tools/list_changed) — currently ignored.
276
234
  }
277
235
  request(method, params) {
278
236
  if (!this.transport)
@@ -301,9 +259,6 @@ export class McpClient {
301
259
  this.transport.send({ jsonrpc: '2.0', method, params });
302
260
  }
303
261
  }
304
- // ──────────────────────────────────────────────────────────────
305
- // Config load / save
306
- // ──────────────────────────────────────────────────────────────
307
262
  import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
308
263
  import { homedir } from 'os';
309
264
  import { join, dirname } from 'path';
@@ -1,24 +1,9 @@
1
- // MCP registry — holds all connected MCP clients as a module-level
2
- // singleton so the tools layer can merge their tool defs into the LLM's
3
- // tool list without threading state through the agent.
4
- //
5
- // Lifecycle:
6
- // 1. index.tsx calls initMcpRegistry() once after config load
7
- // 2. tools.ts calls getMcpToolDefsSync() every agent-loop iteration
8
- // 3. agent calls executeTool() → falls through to executeMcpTool()
9
- // for names prefixed mcp__<server>__<tool>
10
1
  import { McpClient, loadMcpConfig, } from './mcp.js';
11
2
  import { permissions } from './permission.js';
12
3
  const servers = new Map();
13
4
  let cachedDefs = [];
14
5
  let initPromise = null;
15
6
  let initialized = false;
16
- /**
17
- * Called once on app startup, after config is loaded.
18
- * Never throws — a bad MCP server is logged and skipped so the TUI
19
- * still boots. (Failing closed here would make a typo in mcp.json
20
- * brick the whole app.)
21
- */
22
7
  export async function initMcpRegistry() {
23
8
  if (initPromise)
24
9
  return initPromise;
@@ -79,7 +64,6 @@ function rebuildDefs() {
79
64
  }
80
65
  cachedDefs = defs;
81
66
  }
82
- /** Synchronous — safe to call every iteration of the agent loop. */
83
67
  export function getMcpToolDefsSync() {
84
68
  return cachedDefs;
85
69
  }
@@ -95,10 +79,6 @@ export function parseMcpToolName(fullName) {
95
79
  return null;
96
80
  return { server: rest.slice(0, idx), tool: rest.slice(idx + 2) };
97
81
  }
98
- /**
99
- * Executes an MCP tool call. Routes through the permission broker
100
- * unless the server is marked autoAllow:true in mcp.json.
101
- */
102
82
  export async function executeMcpTool(fullName, args) {
103
83
  const parsed = parseMcpToolName(fullName);
104
84
  if (!parsed)
@@ -1,6 +1,3 @@
1
- // Tracks which models the user has picked, so the picker can surface a
2
- // RECENT section. Stored at ~/.blyue/model-usage.json. Bounded — one
3
- // entry per unique model ever picked.
4
1
  import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
5
2
  import { homedir } from 'os';
6
3
  import { join, dirname } from 'path';
@@ -29,7 +26,6 @@ function save(m) {
29
26
  writeFileSync(p, JSON.stringify(m, null, 2), 'utf8');
30
27
  cache = m;
31
28
  }
32
- /** Call when the user picks a model (Ctrl+L or /model). */
33
29
  export function recordModelUse(id) {
34
30
  if (!id)
35
31
  return;
@@ -38,7 +34,6 @@ export function recordModelUse(id) {
38
34
  m[id] = { count: prev.count + 1, lastUsed: Date.now() };
39
35
  save(m);
40
36
  }
41
- /** Top N most-recently-used model IDs, newest first. */
42
37
  export function getRecentModels(limit = 5) {
43
38
  const m = load();
44
39
  return Object.entries(m)
@@ -1,13 +1,9 @@
1
- // Universal model fetcher — works with any OpenAI-compatible /v1/models
2
- // endpoint: OpenAI, OpenRouter, Groq, xAI (Grok), Ollama, LM Studio,
3
- // llama.cpp, vLLM, together, deepseek, etc.
4
1
  const CACHE_TTL_MS = 5 * 60 * 1000;
5
2
  let cache = null;
6
3
  function currentBaseUrl() {
7
4
  const raw = process.env.OPENAI_BASE_URL ||
8
5
  process.env.OPENAI_API_BASE ||
9
6
  'https://api.openai.com/v1';
10
- // Strip trailing slash for consistent concat.
11
7
  return raw.replace(/\/+$/, '');
12
8
  }
13
9
  function currentApiKey() {
@@ -29,7 +25,6 @@ function providerLabel(baseUrl) {
29
25
  return 'Together';
30
26
  if (u.includes('localhost') || u.includes('127.0.0.1'))
31
27
  return 'local';
32
- // Fall back to the hostname.
33
28
  try {
34
29
  return new URL(baseUrl).host;
35
30
  }
@@ -40,10 +35,6 @@ function providerLabel(baseUrl) {
40
35
  export function currentProviderLabel() {
41
36
  return providerLabel(currentBaseUrl());
42
37
  }
43
- /**
44
- * Non-chat models we should hide from the picker. Conservative — better
45
- * to show a couple of extras than to hide something the user wants.
46
- */
47
38
  function isChatModel(id) {
48
39
  const l = id.toLowerCase();
49
40
  const block = [
@@ -74,11 +65,6 @@ function parseNumber(v) {
74
65
  const n = typeof v === 'string' ? parseFloat(v) : v;
75
66
  return Number.isFinite(n) ? n : undefined;
76
67
  }
77
- /**
78
- * Fetches the model list from the current provider. Cached for 5 minutes
79
- * per base URL, so reopening /model is instant but "new models" surface
80
- * within one cache window.
81
- */
82
68
  export async function fetchModels(opts) {
83
69
  const baseUrl = currentBaseUrl();
84
70
  const url = baseUrl + '/models';
@@ -129,8 +115,6 @@ export async function fetchModels(opts) {
129
115
  })
130
116
  .filter((m) => m !== null)
131
117
  .filter((m) => isChatModel(m.id));
132
- // Sort: free first, then alphabetical. (OpenRouter returns them in
133
- // arbitrary order; this makes the useful ones surface at the top.)
134
118
  models.sort((a, b) => {
135
119
  if (!!a.isFree !== !!b.isFree)
136
120
  return a.isFree ? -1 : 1;
@@ -142,9 +126,6 @@ export async function fetchModels(opts) {
142
126
  export function clearModelCache() {
143
127
  cache = null;
144
128
  }
145
- /**
146
- * Returns the current model ID — from config/env.
147
- */
148
129
  export function currentModelId() {
149
130
  return process.env.MODEL || '';
150
131
  }
@@ -1,12 +1,10 @@
1
1
  class PermissionBroker {
2
2
  pending = null;
3
- // Session-scoped "always allow" set (cleared on restart)
4
3
  alwaysAllow = new Set();
5
4
  listeners = new Set();
6
5
  nextId = 0;
7
6
  subscribe(fn) {
8
7
  this.listeners.add(fn);
9
- // Immediately notify with current state
10
8
  fn(this.pending?.req ?? null);
11
9
  return () => this.listeners.delete(fn);
12
10
  }
@@ -51,7 +51,6 @@ export class Provider {
51
51
  }
52
52
  throw new Error(raw);
53
53
  }
54
- // Non-streaming call (used by /compact)
55
54
  async chat(messages, tools = []) {
56
55
  const { messages: safeMessages } = this.sanitize(messages);
57
56
  try {
@@ -75,7 +74,6 @@ export class Provider {
75
74
  this.handleError(e);
76
75
  }
77
76
  }
78
- // Streaming call — yields deltas then a final summary
79
77
  async *chatStream(messages, tools) {
80
78
  const { messages: safeMessages, redactedCount } = this.sanitize(messages);
81
79
  try {
@@ -135,7 +133,6 @@ export class Provider {
135
133
  this.handleError(e);
136
134
  }
137
135
  }
138
- // Summarize helper for /compact
139
136
  async summarize(messages) {
140
137
  const res = await this.chat(messages, []);
141
138
  return res.content || '';
@@ -1,31 +1,20 @@
1
1
  const PATTERNS = [
2
- // OpenAI / OpenRouter
3
2
  [/sk-or-v1-[a-zA-Z0-9]{32,}/g, 'sk-or-v1-***REDACTED***'],
4
3
  [/sk-[a-zA-Z0-9]{20,}/g, 'sk-***REDACTED***'],
5
- // Anthropic
6
4
  [/sk-ant-[a-zA-Z0-9\-_]{20,}/g, 'sk-ant-***REDACTED***'],
7
- // GitHub
8
5
  [/ghp_[a-zA-Z0-9]{36,}/g, 'ghp_***REDACTED***'],
9
6
  [/gho_[a-zA-Z0-9]{36,}/g, 'gho_***REDACTED***'],
10
7
  [/ghs_[a-zA-Z0-9]{36,}/g, 'ghs_***REDACTED***'],
11
8
  [/github_pat_[a-zA-Z0-9_]{60,}/g, 'github_pat_***REDACTED***'],
12
- // AWS
13
9
  [/AKIA[0-9A-Z]{16}/g, 'AKIA***REDACTED***'],
14
10
  [/aws_secret_access_key\s*=\s*[A-Za-z0-9/+=]{40}/gi, 'aws_secret_access_key=***REDACTED***'],
15
- // Google
16
11
  [/AIza[0-9A-Za-z_\-]{35}/g, 'AIza***REDACTED***'],
17
- // Slack
18
12
  [/xox[baprs]-[a-zA-Z0-9-]{10,}/g, 'xox***REDACTED***'],
19
- // Stripe
20
13
  [/sk_live_[a-zA-Z0-9]{24,}/g, 'sk_live_***REDACTED***'],
21
14
  [/pk_live_[a-zA-Z0-9]{24,}/g, 'pk_live_***REDACTED***'],
22
- // JWT
23
15
  [/eyJ[a-zA-Z0-9_\-]{10,}\.eyJ[a-zA-Z0-9_\-]{10,}\.[a-zA-Z0-9_\-]{10,}/g, '***REDACTED JWT***'],
24
- // Bearer tokens
25
16
  [/Bearer\s+[a-zA-Z0-9_\-\.]{20,}/g, 'Bearer ***REDACTED***'],
26
- // PEM keys
27
17
  [/-----BEGIN [A-Z ]+-----[\s\S]*?-----END [A-Z ]+-----/g, '-----***REDACTED KEY***-----'],
28
- // Generic env-style secrets (password=, token=, secret=)
29
18
  [/(password|passwd|pwd|secret|token|api_key|apikey)\s*[:=]\s*["']?([A-Za-z0-9_\-\.]{16,})["']?/gi,
30
19
  '$1=***REDACTED***'],
31
20
  ];
@@ -21,25 +21,17 @@ export function loadSession(cwd = process.cwd()) {
21
21
  return null;
22
22
  }
23
23
  }
24
- const MAX_SESSION_BYTES = Number(process.env.BLYUE_MAX_SESSION_BYTES) || 5 * 1024 * 1024; // 5 MB per session
25
- const MAX_DIR_BYTES = Number(process.env.BLYUE_MAX_SESSIONS_DIR_BYTES) || 50 * 1024 * 1024; // 50 MB total
26
- /**
27
- * Trim the in-memory messages array so a session file never grows past
28
- * MAX_SESSION_BYTES. Drops oldest non-system messages first, keeping a
29
- * marker so the user (and the model) knows history was clipped.
30
- */
24
+ const MAX_SESSION_BYTES = Number(process.env.BLYUE_MAX_SESSION_BYTES) || 5 * 1024 * 1024;
25
+ const MAX_DIR_BYTES = Number(process.env.BLYUE_MAX_SESSIONS_DIR_BYTES) || 50 * 1024 * 1024;
31
26
  function trimToBudget(messages) {
32
27
  const size = (arr) => Buffer.byteLength(JSON.stringify(arr, null, 2), 'utf8');
33
28
  if (size(messages) <= MAX_SESSION_BYTES)
34
29
  return messages;
35
- // Keep the most recent N messages, dropping from the front. Stop as
36
- // soon as we're under budget; always keep at least the last 6.
37
30
  let arr = messages.slice();
38
31
  while (arr.length > 6 && size(arr) > MAX_SESSION_BYTES) {
39
32
  arr.shift();
40
33
  }
41
34
  if (size(arr) > MAX_SESSION_BYTES) {
42
- // Even the tail is too big — hard-truncate the largest text fields.
43
35
  arr = arr.map((m) => {
44
36
  const c = m.content;
45
37
  if (typeof c === 'string' && c.length > 8000) {
@@ -53,10 +45,6 @@ function trimToBudget(messages) {
53
45
  ...arr,
54
46
  ];
55
47
  }
56
- /**
57
- * Evict oldest session files (never the active one) until the sessions
58
- * directory is under MAX_DIR_BYTES.
59
- */
60
48
  function pruneDirectory(activeFile) {
61
49
  if (!fs.existsSync(SESSIONS_DIR))
62
50
  return;
@@ -74,7 +62,7 @@ function pruneDirectory(activeFile) {
74
62
  }
75
63
  if (total <= MAX_DIR_BYTES)
76
64
  return;
77
- stats.sort((a, b) => a.mtime - b.mtime); // oldest first
65
+ stats.sort((a, b) => a.mtime - b.mtime);
78
66
  for (const { file, size } of stats) {
79
67
  if (total <= MAX_DIR_BYTES)
80
68
  break;
package/dist/ui/Banner.js CHANGED
@@ -75,7 +75,6 @@ export const Banner = () => {
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.
79
78
  useEffect(() => {
80
79
  if (!animate)
81
80
  return;
package/dist/ui/Chat.js CHANGED
@@ -70,7 +70,6 @@ export const Chat = ({ resumedCount = 0 }) => {
70
70
  const [permReq, setPermReq] = useState(null);
71
71
  const [currentModel, setCurrentModel] = useState(process.env.MODEL || '');
72
72
  const [agent, setAgent] = useState(() => new Agent());
73
- // Mascot state
74
73
  const [mascotState, setMascotState] = useState(undefined);
75
74
  const [elapsedSec, setElapsedSec] = useState(0);
76
75
  const startRef = useRef(null);
@@ -78,7 +77,6 @@ export const Chat = ({ resumedCount = 0 }) => {
78
77
  const unsub = permissions.subscribe((req) => setPermReq(req));
79
78
  return unsub;
80
79
  }, []);
81
- // Track elapsed time during loading
82
80
  useEffect(() => {
83
81
  if (!loading) {
84
82
  startRef.current = null;
@@ -93,7 +91,6 @@ export const Chat = ({ resumedCount = 0 }) => {
93
91
  }, 500);
94
92
  return () => clearInterval(t);
95
93
  }, [loading]);
96
- // Flash a temporary mascot state
97
94
  const flash = (state, ms = 1200) => {
98
95
  setMascotState(state);
99
96
  setTimeout(() => setMascotState(undefined), ms);
@@ -118,7 +115,6 @@ export const Chat = ({ resumedCount = 0 }) => {
118
115
  if (!text.trim() || loading)
119
116
  return;
120
117
  const trimmed = text.trim();
121
- // ---- slash commands ----
122
118
  if (trimmed === '/model' || trimmed === '/models') {
123
119
  setInput('');
124
120
  setOverlay('model');
@@ -187,12 +183,10 @@ export const Chat = ({ resumedCount = 0 }) => {
187
183
  ]);
188
184
  return;
189
185
  }
190
- // ---- normal run ----
191
186
  setInput('');
192
187
  setHistory((h) => [...h, mkItem('user', trimmed)]);
193
188
  setLoading(true);
194
189
  setStreaming('');
195
- // Jump animation on submit
196
190
  flash('jump', 400);
197
191
  let textBuf = '';
198
192
  let toolBuf = '';
@@ -201,7 +195,6 @@ export const Chat = ({ resumedCount = 0 }) => {
201
195
  if (ev.type === 'text') {
202
196
  textBuf += ev.delta;
203
197
  setStreaming(textBuf);
204
- // Clear any tool mascot while text streams
205
198
  setMascotState(undefined);
206
199
  }
207
200
  else if (ev.type === 'tool_start') {
@@ -211,7 +204,6 @@ export const Chat = ({ resumedCount = 0 }) => {
211
204
  }
212
205
  toolBuf = '\n[Calling ' + ev.name + '...]\n';
213
206
  setStreaming(toolBuf);
214
- // React to the specific tool
215
207
  setMascotState(toolToState(ev.name));
216
208
  }
217
209
  else if (ev.type === 'compact') {
@@ -224,7 +216,6 @@ export const Chat = ({ resumedCount = 0 }) => {
224
216
  flush('tool', toolBuf + '[Result]: ' + ev.preview + '\n');
225
217
  toolBuf = '';
226
218
  setStreaming('');
227
- // Brief pulse before next step
228
219
  flash('success', 500);
229
220
  }
230
221
  }
@@ -2,10 +2,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from 'ink';
3
3
  import { Mascot } from './Mascot.js';
4
4
  export const MakerMark = ({ color, narrow }) => {
5
- // Phone / narrow: single line
6
5
  if (narrow) {
7
6
  return (_jsxs(Text, { children: [_jsx(Text, { color: color, bold: true, children: "\u2584\u2588\u2584 " }), _jsx(Text, { color: color, bold: true, children: "L U M E N" })] }));
8
7
  }
9
- // Wide: mascot on left, wordmark vertically centered
10
8
  return (_jsxs(Box, { flexDirection: "row", children: [_jsx(Mascot, { color: color }), _jsxs(Box, { flexDirection: "column", marginLeft: 2, justifyContent: "center", children: [_jsx(Text, { children: " " }), _jsx(Text, { children: " " }), _jsx(Text, { color: color, bold: true, children: "L U M E N" }), _jsx(Text, { children: " " }), _jsx(Text, { children: " " })] })] }));
11
9
  };
package/dist/ui/Mascot.js CHANGED
@@ -2,11 +2,6 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState, useEffect } from 'react';
3
3
  import { Box, Text } from 'ink';
4
4
  import os from 'os';
5
- // ─────────────────────────────────────────────
6
- // LUMEN mascot — gradient body, solid eyes
7
- // Light at top fades to dark at bottom
8
- // (shading = light emitting downward)
9
- // ─────────────────────────────────────────────
10
5
  const BODY_TOP = ' ▄▄▄▄▄▄▄ ';
11
6
  const BODY_LIGHT = '░░░░░░░░░';
12
7
  const BODY_FACE_A = '░ ● ● ░';
@@ -27,7 +22,6 @@ function shouldAnimate() {
27
22
  catch { }
28
23
  return true;
29
24
  }
30
- // Full mascot — 9 wide × 6 tall
31
25
  export const Mascot = ({ color }) => {
32
26
  const [pulse, setPulse] = useState(0);
33
27
  const [animate] = useState(() => shouldAnimate());
@@ -37,11 +31,8 @@ export const Mascot = ({ color }) => {
37
31
  const t = setInterval(() => setPulse((p) => (p + 1) % 3), 800);
38
32
  return () => clearInterval(t);
39
33
  }, [animate]);
40
- // Subtle pulse: alternate shade intensity on the middle row
41
34
  const mid = pulse === 0 ? BODY_MED : pulse === 1 ? '▒▒▒▒▒▒▒▒▒' : '░░▒░░▒░░';
42
35
  return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: color, bold: true, children: BODY_TOP }), _jsx(Text, { color: color, bold: true, children: BODY_LIGHT }), _jsx(Text, { color: color, bold: true, children: BODY_FACE_A }), _jsx(Text, { color: color, bold: true, children: BODY_FACE_B }), _jsx(Text, { color: color, bold: true, children: mid }), _jsx(Text, { color: color, bold: true, children: BODY_DARK }), _jsx(Text, { color: color, bold: true, children: BODY_BOT })] }));
43
36
  };
44
- // Compact — one line, for input bar & prompt
45
37
  export const MascotInline = ({ color }) => (_jsx(Text, { color: color, bold: true, children: "\u2584 " }));
46
- // Tiny — just the face, for very narrow terminals
47
38
  export const MascotMini = ({ color }) => (_jsx(Text, { color: color, bold: true, children: "\u2584\u2588\u2584 " }));
@@ -48,8 +48,6 @@ export function toolToState(tool) {
48
48
  return 'thinking';
49
49
  }
50
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
51
  function isSemantic(state) {
54
52
  return (state === 'error' || state === 'denied' ||
55
53
  state === 'success' || state === 'granted' ||
@@ -60,7 +58,7 @@ function semanticColor(state) {
60
58
  return theme.error;
61
59
  if (state === 'success' || state === 'granted')
62
60
  return theme.success;
63
- return theme.warning; // permission / impatient
61
+ return theme.warning;
64
62
  }
65
63
  export const MascotPrompt = ({ state, keepAwake = false, }) => {
66
64
  const frames = FACES[state];
@@ -11,7 +11,6 @@ export const McpPanel = ({ onClose }) => {
11
11
  const [servers, setServers] = useState(() => listMcpServers());
12
12
  const [reloading, setReloading] = useState(false);
13
13
  const [msg, setMsg] = useState(null);
14
- // Refresh list on mount in case registry changed since last render.
15
14
  useEffect(() => {
16
15
  setServers(listMcpServers());
17
16
  }, []);
@@ -57,7 +57,7 @@ export const ModelPicker = ({ current, onSelect, onCancel }) => {
57
57
  const paid = [];
58
58
  for (const m of matched) {
59
59
  if (seen.has(m.id))
60
- continue; // already in recent
60
+ continue;
61
61
  if (m.isFree)
62
62
  free.push(m);
63
63
  else
@@ -86,7 +86,6 @@ export const ModelPicker = ({ current, onSelect, onCancel }) => {
86
86
  pushSection('PAID', paid);
87
87
  }
88
88
  else {
89
- // 'all' — show recent first, then free, then paid
90
89
  pushSection('RECENT', recent);
91
90
  pushSection('FREE', free);
92
91
  pushSection('PAID', paid);
@@ -26,7 +26,7 @@ function shouldAnimate() {
26
26
  export const RocketBar = ({ label, loading, width }) => {
27
27
  const [animate] = useState(() => shouldAnimate());
28
28
  const [pos, setPos] = useState(0);
29
- const hue = useSlowColor(); // shared pastel clock — matches banner + face
29
+ const hue = useSlowColor();
30
30
  const fixed = 5 + label.length;
31
31
  const midLen = Math.max(0, width - fixed);
32
32
  useEffect(() => {
@@ -17,7 +17,6 @@ export const Settings = ({ onClose, onModelChange }) => {
17
17
  const [testResult, setTestResult] = useState(null);
18
18
  const [ollamaModels, setOllamaModels] = useState([]);
19
19
  const [ollamaChecked, setOllamaChecked] = useState(false);
20
- // MCP draft state
21
20
  const [mcpCfg, setMcpCfg] = useState(() => loadMcpConfig());
22
21
  const [mcpCursor, setMcpCursor] = useState(0);
23
22
  const [mcpDraftName, setMcpDraftName] = useState('');
@@ -75,7 +74,6 @@ export const Settings = ({ onClose, onModelChange }) => {
75
74
  },
76
75
  { label: 'Close', hint: 'esc', action: onClose },
77
76
  ];
78
- // ---- Main menu ----
79
77
  useInput((_, key) => {
80
78
  if (key.escape)
81
79
  return onClose();
@@ -86,7 +84,6 @@ export const Settings = ({ onClose, onModelChange }) => {
86
84
  if (key.return)
87
85
  mainItems[cursor].action();
88
86
  }, { isActive: screen === 'main' });
89
- // ---- n8n submenu ----
90
87
  const n8nItems = [
91
88
  { label: 'URL', hint: cfg.n8nUrl || 'not set', action: () => { setDraft(cfg.n8nUrl || ''); setScreen('n8n-url'); } },
92
89
  { label: 'API key', hint: cfg.n8nApiKey ? '•••' + cfg.n8nApiKey.slice(-4) : 'not set', action: () => { setDraft(cfg.n8nApiKey || ''); setScreen('n8n-key'); } },
@@ -122,7 +119,6 @@ export const Settings = ({ onClose, onModelChange }) => {
122
119
  n8nItems[n8nCursor].action();
123
120
  }, { isActive: screen === 'n8n' });
124
121
  useInput(() => setScreen('n8n'), { isActive: screen === 'n8n-test' });
125
- // ---- MCP submenu ----
126
122
  const serverNames = Object.keys(mcpCfg.servers || {});
127
123
  const mcpItems = [
128
124
  { label: '+ Add server', hint: 'new MCP server', action: () => {
@@ -170,7 +166,6 @@ export const Settings = ({ onClose, onModelChange }) => {
170
166
  if (key.return)
171
167
  mcpItems[mcpCursor].action();
172
168
  }, { isActive: screen === 'mcp' });
173
- // ---- MCP confirm delete ----
174
169
  useInput((input, key) => {
175
170
  if (input === 'y' || input === 'Y') {
176
171
  if (mcpConfirmDel) {
@@ -188,7 +183,6 @@ export const Settings = ({ onClose, onModelChange }) => {
188
183
  setScreen('mcp');
189
184
  }
190
185
  }, { isActive: screen === 'mcp-confirm-del' });
191
- // ---- MCP add wizard: autoAllow toggle ----
192
186
  useInput((input, key) => {
193
187
  if (key.escape)
194
188
  return setScreen('mcp');
@@ -223,12 +217,10 @@ export const Settings = ({ onClose, onModelChange }) => {
223
217
  void reloadMcp(`✓ added ${mcpDraftName.trim()}`);
224
218
  setScreen('mcp');
225
219
  };
226
- // ---- Frame helpers ----
227
220
  const inner = width - 4;
228
221
  const top = (label) => (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '╭─ ' }), _jsx(Text, { color: theme.accent, bold: true, children: label }), _jsx(Text, { color: theme.muted, children: ' ' + '─'.repeat(Math.max(1, inner - label.length - 3)) + '╮' })] }));
229
222
  const bot = (_jsx(Text, { color: theme.muted, children: '╰' + '─'.repeat(inner) + '╯' }));
230
223
  const row = (content) => (_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: theme.muted, children: '│ ' }), content] }));
231
- // ---- Main screen ----
232
224
  if (screen === 'main') {
233
225
  const baseUrlIsLocal = (cfg.baseUrl || '').includes('localhost') ||
234
226
  (cfg.baseUrl || '').includes('127.0.0.1');
@@ -237,7 +229,6 @@ export const Settings = ({ onClose, onModelChange }) => {
237
229
  return (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ ' }), _jsx(Text, { color: active ? theme.accent : theme.muted, bold: active, children: active ? '❯ ' : ' ' }), _jsx(Text, { color: active ? theme.text : theme.muted, bold: active, children: it.label.padEnd(20) }), _jsx(Text, { color: theme.muted, children: it.hint })] }, it.label));
238
230
  }), bot, _jsx(Text, { color: theme.muted, children: "\u2191\u2193 move \u00B7 \u21B5 select \u00B7 esc close" })] }));
239
231
  }
240
- // ---- API key / Base URL ----
241
232
  if (screen === 'apikey' || screen === 'baseurl') {
242
233
  const isKey = screen === 'apikey';
243
234
  const title = isKey ? 'api key' : 'base url';
@@ -248,7 +239,6 @@ export const Settings = ({ onClose, onModelChange }) => {
248
239
  };
249
240
  return (_jsxs(Box, { flexDirection: "column", children: [top(title), row(_jsx(Text, { color: theme.muted, children: isKey ? 'Paste your new API key' : 'e.g. http://localhost:11434/v1' })), row(_jsxs(Box, { children: [_jsx(Text, { color: theme.accent, bold: true, children: '❯ ' }), _jsx(KeyInput, { value: value, onChange: setValue, placeholder: isKey ? 'sk-...' : 'https://...', masked: isKey, onSubmit: () => setScreen('main') })] })), bot, _jsx(Text, { color: theme.muted, children: "type \u00B7 backspace \u00B7 \u21B5 save \u00B7 esc back" })] }));
250
241
  }
251
- // ---- Model ----
252
242
  if (screen === 'model') {
253
243
  const isLocal = (cfg.baseUrl || '').includes('localhost');
254
244
  return (_jsxs(Box, { flexDirection: "column", children: [top('model'), row(_jsx(Text, { color: theme.muted, children: isLocal && ollamaModels.length > 0
@@ -258,14 +248,12 @@ export const Settings = ({ onClose, onModelChange }) => {
258
248
  setScreen('main');
259
249
  } })] })), bot, _jsx(Text, { color: theme.muted, children: "type \u00B7 \u21B5 save \u00B7 esc back" })] }));
260
250
  }
261
- // ---- n8n submenu ----
262
251
  if (screen === 'n8n') {
263
252
  return (_jsxs(Box, { flexDirection: "column", children: [top('n8n integration'), n8nItems.map((it, i) => {
264
253
  const active = i === n8nCursor;
265
254
  return (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ ' }), _jsx(Text, { color: active ? theme.accent : theme.muted, bold: active, children: active ? '❯ ' : ' ' }), _jsx(Text, { color: active ? theme.text : theme.muted, bold: active, children: it.label.padEnd(20) }), _jsx(Text, { color: theme.muted, children: it.hint })] }, it.label));
266
255
  }), bot, _jsx(Text, { color: theme.muted, children: "\u2191\u2193 move \u00B7 \u21B5 select \u00B7 esc back" })] }));
267
256
  }
268
- // ---- n8n url/key ----
269
257
  if (screen === 'n8n-url' || screen === 'n8n-key') {
270
258
  const isKey = screen === 'n8n-key';
271
259
  const title = isKey ? 'n8n api key' : 'n8n url';
@@ -276,34 +264,27 @@ export const Settings = ({ onClose, onModelChange }) => {
276
264
  };
277
265
  return (_jsxs(Box, { flexDirection: "column", children: [top(title), row(_jsx(Text, { color: theme.muted, children: isKey ? 'Settings → n8n API → create key' : 'e.g. http://localhost:5678' })), row(_jsxs(Box, { children: [_jsx(Text, { color: theme.accent, bold: true, children: '❯ ' }), _jsx(KeyInput, { value: value, onChange: setValue, placeholder: isKey ? 'n8n_api_...' : 'http://localhost:5678', masked: isKey, onSubmit: () => setScreen('n8n') })] })), bot, _jsx(Text, { color: theme.muted, children: "type \u00B7 backspace \u00B7 \u21B5 save \u00B7 esc back" })] }));
278
266
  }
279
- // ---- n8n test ----
280
267
  if (screen === 'n8n-test') {
281
268
  return (_jsxs(Box, { flexDirection: "column", children: [top('n8n test'), row(_jsx(Text, { color: testResult?.startsWith('✓') ? theme.success : theme.muted, children: testResult || 'testing…' })), bot, _jsx(Text, { color: theme.muted, children: "press any key to go back" })] }));
282
269
  }
283
- // ---- MCP submenu ----
284
270
  if (screen === 'mcp') {
285
271
  return (_jsxs(Box, { flexDirection: "column", children: [top('mcp servers'), row(_jsx(Text, { color: theme.muted, children: "config: ~/.blyue/mcp.json" })), row(_jsxs(Text, { color: theme.muted, children: [serverNames.length, " server", serverNames.length === 1 ? '' : 's', mcpStatus ? ' · ' : '', mcpStatus && (_jsx(Text, { color: mcpStatus.startsWith('✗') ? theme.error : theme.success, children: mcpStatus }))] })), _jsx(Text, { color: theme.muted, children: '│ ' + '─'.repeat(Math.max(1, inner - 2)) }), mcpItems.map((it, i) => {
286
272
  const active = i === mcpCursor;
287
273
  return (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ ' }), _jsx(Text, { color: active ? theme.accent : theme.muted, bold: active, children: active ? '❯ ' : ' ' }), _jsx(Text, { color: active ? theme.text : theme.muted, bold: active, children: it.label.padEnd(22) }), _jsx(Text, { color: theme.muted, children: it.hint })] }, it.label));
288
274
  }), bot, _jsx(Text, { color: theme.muted, children: "\u2191\u2193 move \u00B7 \u21B5 select \u00B7 [d] delete \u00B7 esc back" })] }));
289
275
  }
290
- // ---- MCP confirm delete ----
291
276
  if (screen === 'mcp-confirm-del') {
292
277
  return (_jsxs(Box, { flexDirection: "column", children: [top('delete server'), row(_jsxs(Text, { color: theme.warning, children: ["Delete MCP server \"", mcpConfirmDel, "\"?"] })), row(_jsx(Text, { color: theme.muted, children: "This removes it from mcp.json." })), bot, _jsxs(Text, { children: [_jsx(Text, { color: theme.success, bold: true, children: " [y]" }), _jsx(Text, { color: theme.muted, children: " yes " }), _jsx(Text, { color: theme.error, bold: true, children: "[n]" }), _jsx(Text, { color: theme.muted, children: " no" })] })] }));
293
278
  }
294
- // ---- MCP add wizard: name ----
295
279
  if (screen === 'mcp-add-name') {
296
280
  return (_jsxs(Box, { flexDirection: "column", children: [top('add mcp · step 1/4'), row(_jsx(Text, { color: theme.muted, children: "Name (unique, e.g. \"filesystem\")" })), row(_jsxs(Box, { children: [_jsx(Text, { color: theme.accent, bold: true, children: '❯ ' }), _jsx(KeyInput, { value: mcpDraftName, onChange: setMcpDraftName, placeholder: "filesystem", onSubmit: () => setScreen('mcp-add-command') })] })), bot, _jsx(Text, { color: theme.muted, children: "type \u00B7 \u21B5 next \u00B7 esc cancel" })] }));
297
281
  }
298
- // ---- MCP add wizard: command ----
299
282
  if (screen === 'mcp-add-command') {
300
283
  return (_jsxs(Box, { flexDirection: "column", children: [top('add mcp · step 2/4'), row(_jsx(Text, { color: theme.muted, children: "Command (binary to run)" })), row(_jsxs(Box, { children: [_jsx(Text, { color: theme.accent, bold: true, children: '❯ ' }), _jsx(KeyInput, { value: mcpDraftCommand, onChange: setMcpDraftCommand, placeholder: "npx", onSubmit: () => setScreen('mcp-add-args') })] })), bot, _jsx(Text, { color: theme.muted, children: "type \u00B7 \u21B5 next \u00B7 esc cancel" })] }));
301
284
  }
302
- // ---- MCP add wizard: args ----
303
285
  if (screen === 'mcp-add-args') {
304
286
  return (_jsxs(Box, { flexDirection: "column", children: [top('add mcp · step 3/4'), row(_jsx(Text, { color: theme.muted, children: "Args (space-separated, can be empty)" })), row(_jsxs(Box, { children: [_jsx(Text, { color: theme.accent, bold: true, children: '❯ ' }), _jsx(KeyInput, { value: mcpDraftArgs, onChange: setMcpDraftArgs, placeholder: "-y @modelcontextprotocol/server-filesystem /home/user", onSubmit: () => setScreen('mcp-add-autollw') })] })), bot, _jsx(Text, { color: theme.muted, children: "type \u00B7 \u21B5 next \u00B7 esc cancel" })] }));
305
287
  }
306
- // ---- MCP add wizard: autoAllow ----
307
288
  if (screen === 'mcp-add-autollw') {
308
289
  return (_jsxs(Box, { flexDirection: "column", children: [top('add mcp · step 4/4'), row(_jsx(Text, { color: theme.muted, children: "Auto-allow calls to this server (skip permission prompts)?" })), row(_jsx(Text, { color: theme.warning, children: "Only enable for servers you fully trust." })), bot, _jsxs(Text, { children: [_jsx(Text, { color: theme.success, bold: true, children: " [y]" }), _jsx(Text, { color: theme.muted, children: " yes " }), _jsx(Text, { color: theme.error, bold: true, children: "[n]" }), _jsx(Text, { color: theme.muted, children: " no (recommended)" })] })] }));
309
290
  }
package/dist/ui/Setup.js CHANGED
@@ -91,7 +91,6 @@ export const Setup = ({ onDone }) => {
91
91
  setStep('provider');
92
92
  }
93
93
  };
94
- // Header: single line hint
95
94
  const Hint = ({ children }) => (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: theme.muted, children: children }) }));
96
95
  if (step === 'provider') {
97
96
  return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Hint, { children: "Select a provider \u2191\u2193 Enter" }), PROVIDERS.map((p, i) => {
@@ -2,8 +2,6 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState, useEffect } from 'react';
3
3
  import { Box, Text } from 'ink';
4
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
5
  const FRAMES = [
8
6
  [' ', ' '],
9
7
  [' ', ' z '],
@@ -6,7 +6,6 @@ const FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '
6
6
  export const Spinner = ({ label = 'Thinking' }) => {
7
7
  const [i, setI] = useState(0);
8
8
  useEffect(() => {
9
- // 200ms instead of 90ms → 5 fps not 11 fps
10
9
  const t = setInterval(() => setI((v) => (v + 1) % FRAMES.length), 200);
11
10
  return () => clearInterval(t);
12
11
  }, []);
@@ -1,9 +1,5 @@
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
1
  let counter = 0;
5
2
  const listeners = new Set();
6
- /** Call this from any real user input handler (keystroke, submit). */
7
3
  export function bumpActivity() {
8
4
  counter = (counter + 1) % Number.MAX_SAFE_INTEGER;
9
5
  for (const l of listeners)
@@ -1,4 +1,3 @@
1
- // HSL → hex, no dependencies
2
1
  function hslToHex(h, s, l) {
3
2
  h = ((h % 360) + 360) % 360;
4
3
  s /= 100;
@@ -11,9 +10,7 @@ function hslToHex(h, s, l) {
11
10
  };
12
11
  return `#${f(0)}${f(8)}${f(4)}`;
13
12
  }
14
- /** Returns a hex color at position t (0..1) along a soft palette loop. */
15
13
  export function hueAt(t, lightness = 65, saturation = 70) {
16
- // Weighted loop: cyan → blue → purple → pink → cyan
17
14
  const hue = 180 + Math.sin(t * Math.PI * 2) * 120;
18
15
  return hslToHex(hue, saturation, lightness);
19
16
  }
package/dist/ui/pastel.js CHANGED
@@ -1,6 +1,3 @@
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
1
  function hslToHex(h, s, l) {
5
2
  s /= 100;
6
3
  l /= 100;
@@ -13,18 +10,12 @@ function hslToHex(h, s, l) {
13
10
  const toHex = (x) => x.toString(16).padStart(2, '0');
14
11
  return '#' + toHex(f(0)) + toHex(f(8)) + toHex(f(4));
15
12
  }
16
- /** Full-hue pastel cycle. t in [0, 1). */
17
13
  export function pastelAt(t) {
18
14
  const h = (((t % 1) + 1) % 1) * 360;
19
15
  return hslToHex(h, 48, 76);
20
16
  }
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
17
  export function pastelWarmAt(t) {
27
18
  const t2 = ((t % 1) + 1) % 1;
28
- const h = (320 + t2 * 110) % 360; // 320° → 70°, wraps through warm
19
+ const h = (320 + t2 * 110) % 360;
29
20
  return hslToHex(h, 52, 76);
30
21
  }
@@ -1,6 +1,4 @@
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
1
+ const PERIOD_MS = 24_000;
4
2
  const TICK_MS = 120;
5
3
  let t = 0;
6
4
  const listeners = new Set();
@@ -13,7 +11,6 @@ function start() {
13
11
  for (const l of listeners)
14
12
  l();
15
13
  }, TICK_MS);
16
- // Don't keep the Node process alive just to animate colors.
17
14
  const anyInt = interval;
18
15
  anyInt.unref?.();
19
16
  }
@@ -27,11 +27,6 @@ export function useIdle({ drowsyAfterMs = 120_000, sleepAfterMs = 300_000, disab
27
27
  }, [active, drowsyAfterMs, sleepAfterMs, disabled]);
28
28
  return stage;
29
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
30
  export function defaultIdleThresholds() {
36
31
  if (process.env.BLYUE_FAST_IDLE === '1') {
37
32
  return { drowsyAfterMs: 3_000, sleepAfterMs: 6_000 };
@@ -1,10 +1,6 @@
1
1
  import { useSyncExternalStore } from 'react';
2
2
  import { subscribeSlowTick, getSlowTick } from './slowTick.js';
3
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
4
  export function useSlowColor(warm = false) {
9
5
  const t = useSyncExternalStore(subscribeSlowTick, getSlowTick, getSlowTick);
10
6
  return warm ? pastelWarmAt(t) : pastelAt(t);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blyuecode",
3
- "version": "0.3.7",
3
+ "version": "0.3.8",
4
4
  "description": "BLYUE Code — a coding agent by Lumen",
5
5
  "type": "module",
6
6
  "license": "MIT",