blyuecode 0.3.0 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/ui/Chat.js CHANGED
@@ -8,6 +8,7 @@ import { Agent } from '../core/agent.js';
8
8
  import { permissions } from '../core/permission.js';
9
9
  import { InputBar } from './InputBar.js';
10
10
  import { ModelPicker } from './ModelPicker.js';
11
+ import { McpPanel } from './McpPanel.js';
11
12
  import { Settings } from './Settings.js';
12
13
  import { PermissionPrompt } from './PermissionPrompt.js';
13
14
  import { Spinner } from './Spinner.js';
@@ -127,6 +128,11 @@ export const Chat = ({ resumedCount = 0 }) => {
127
128
  setOverlay('settings');
128
129
  return;
129
130
  }
131
+ if (trimmed === '/mcp') {
132
+ setInput('');
133
+ setOverlay('mcp');
134
+ return;
135
+ }
130
136
  if (trimmed === '/debug') {
131
137
  setInput('');
132
138
  const lines = readDebug();
@@ -172,6 +178,7 @@ export const Chat = ({ resumedCount = 0 }) => {
172
178
  mkItem('tool', 'Commands:'),
173
179
  mkItem('tool', ' /model switch model (Ctrl+L)'),
174
180
  mkItem('tool', ' /settings open settings (Ctrl+O)'),
181
+ mkItem('tool', ' /mcp show MCP servers and tools'),
175
182
  mkItem('tool', ' /compact summarize history to free context'),
176
183
  mkItem('tool', ' /debug show last request to provider'),
177
184
  mkItem('tool', ' /clear clear session and permissions'),
@@ -246,6 +253,9 @@ export const Chat = ({ resumedCount = 0 }) => {
246
253
  if (overlay === 'model') {
247
254
  return (_jsx(Box, { flexDirection: "column", children: _jsx(ModelPicker, { current: currentModel, onSelect: handleModelSelect, onCancel: () => setOverlay('none') }) }));
248
255
  }
256
+ if (overlay === 'mcp') {
257
+ return (_jsx(Box, { flexDirection: "column", children: _jsx(McpPanel, { onClose: () => setOverlay('none') }) }));
258
+ }
249
259
  if (overlay === 'settings') {
250
260
  return (_jsx(Box, { flexDirection: "column", children: _jsx(Settings, { onClose: () => setOverlay('none'), onModelChange: (m) => {
251
261
  setCurrentModel(m);
@@ -0,0 +1,50 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useState, useEffect } from 'react';
3
+ import { Box, Text, useInput, useStdout } from 'ink';
4
+ import { listMcpServers, initMcpRegistry, closeAllMcp, } from '../core/mcpRegistry.js';
5
+ import { mcpConfigPath } from '../core/mcp.js';
6
+ import { theme } from './theme.js';
7
+ export const McpPanel = ({ onClose }) => {
8
+ const { stdout } = useStdout();
9
+ const width = Math.max(40, stdout?.columns ?? 60);
10
+ const inner = width - 4;
11
+ const [servers, setServers] = useState(() => listMcpServers());
12
+ const [reloading, setReloading] = useState(false);
13
+ const [msg, setMsg] = useState(null);
14
+ // Refresh list on mount in case registry changed since last render.
15
+ useEffect(() => {
16
+ setServers(listMcpServers());
17
+ }, []);
18
+ const reload = async () => {
19
+ setReloading(true);
20
+ setMsg(null);
21
+ try {
22
+ await closeAllMcp();
23
+ await initMcpRegistry();
24
+ setServers(listMcpServers());
25
+ setMsg('✓ reloaded');
26
+ }
27
+ catch (err) {
28
+ setMsg('✗ ' + err.message);
29
+ }
30
+ setReloading(false);
31
+ };
32
+ useInput((input, key) => {
33
+ if (key.escape)
34
+ return onClose();
35
+ if (input === 'r' || input === 'R') {
36
+ if (!reloading)
37
+ void reload();
38
+ }
39
+ });
40
+ const label = 'mcp servers';
41
+ const topPad = Math.max(1, inner - label.length - 4);
42
+ const totalTools = servers.reduce((n, s) => n + s.toolCount, 0);
43
+ const healthy = servers.filter((s) => !s.error).length;
44
+ return (_jsxs(Box, { flexDirection: "column", children: [_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(topPad) + '╮' })] }), _jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ ' }), _jsxs(Text, { color: theme.muted, children: ["config: ", mcpConfigPath()] })] }), _jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ ' }), _jsxs(Text, { color: theme.muted, children: [servers.length, " server", servers.length === 1 ? '' : 's', " \u00B7", ' '] }), _jsxs(Text, { color: healthy === servers.length ? theme.success : theme.warning, children: [healthy, " connected"] }), _jsxs(Text, { color: theme.muted, children: [" \u00B7 ", totalTools, " tools"] })] }), _jsx(Text, { color: theme.muted, children: '│ ' + '─'.repeat(Math.max(1, inner - 2)) }), servers.length === 0 ? (_jsxs(_Fragment, { children: [_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ ' }), _jsx(Text, { color: theme.warning, children: "no MCP servers configured" })] }), _jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ ' }), _jsxs(Text, { color: theme.muted, children: ["add one to ", mcpConfigPath(), " then press [r]"] })] })] })) : (servers.map((s) => {
45
+ const status = s.error ? '✗' : '●';
46
+ const statusColor = s.error ? theme.error : theme.success;
47
+ const autoTag = s.autoAllow ? ' [auto]' : '';
48
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ ' }), _jsx(Text, { color: statusColor, children: status + ' ' }), _jsx(Text, { color: theme.text, bold: true, children: s.name }), _jsxs(Text, { color: theme.muted, children: [autoTag, " \u00B7 ", s.toolCount, " tool", s.toolCount === 1 ? '' : 's'] })] }), s.error ? (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ ' }), _jsx(Text, { color: theme.error, children: s.error.slice(0, inner - 6) })] })) : (s.toolNames.slice(0, 4).map((t) => (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ · ' }), _jsx(Text, { color: theme.muted, children: t })] }, t)))), !s.error && s.toolNames.length > 4 && (_jsx(Text, { children: _jsx(Text, { color: theme.muted, children: '│ · +' + (s.toolNames.length - 4) + ' more' }) }))] }, s.name));
49
+ })), _jsx(Text, { color: theme.muted, children: '╰' + '─'.repeat(inner) + '╯' }), _jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: " [" }), _jsx(Text, { color: theme.accent, children: "r" }), _jsx(Text, { color: theme.muted, children: "] reload \u00B7 [" }), _jsx(Text, { color: theme.accent, children: "esc" }), _jsx(Text, { color: theme.muted, children: "] close" }), reloading && _jsx(Text, { color: theme.warning, children: " \u00B7 reloading\u2026" }), msg && _jsxs(Text, { color: theme.success, children: [" \u00B7 ", msg] })] })] }));
50
+ };
@@ -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, useMemo } from 'react';
3
3
  import { Box, Text, useInput, useStdout } from 'ink';
4
4
  import { fetchModels, currentProviderLabel, } from '../core/models.js';
5
5
  import { theme } from './theme.js';
@@ -12,6 +12,7 @@ export const ModelPicker = ({ current, onSelect, onCancel }) => {
12
12
  const [error, setError] = useState(null);
13
13
  const [idx, setIdx] = useState(0);
14
14
  const [filter, setFilter] = useState('');
15
+ const [tier, setTier] = useState('all');
15
16
  useEffect(() => {
16
17
  let cancelled = false;
17
18
  setLoading(true);
@@ -35,48 +36,121 @@ export const ModelPicker = ({ current, onSelect, onCancel }) => {
35
36
  cancelled = true;
36
37
  };
37
38
  }, [current]);
38
- const filtered = filter
39
- ? models.filter((m) => m.id.toLowerCase().includes(filter.toLowerCase()))
40
- : models;
39
+ const { free, paid } = useMemo(() => {
40
+ const q = filter.toLowerCase();
41
+ const matched = q
42
+ ? models.filter((m) => m.id.toLowerCase().includes(q))
43
+ : models;
44
+ const free = [];
45
+ const paid = [];
46
+ for (const m of matched) {
47
+ if (m.isFree)
48
+ free.push(m);
49
+ else
50
+ paid.push(m);
51
+ }
52
+ return { free, paid };
53
+ }, [models, filter]);
54
+ const rows = useMemo(() => {
55
+ const out = [];
56
+ let abs = 0;
57
+ const pushSection = (label, list) => {
58
+ if (list.length === 0)
59
+ return;
60
+ out.push({ kind: 'header', label, count: list.length });
61
+ for (const m of list) {
62
+ out.push({ kind: 'model', model: m, absoluteIdx: abs++ });
63
+ }
64
+ };
65
+ if (tier === 'all' || tier === 'free')
66
+ pushSection('FREE', free);
67
+ if (tier === 'all' || tier === 'paid')
68
+ pushSection('PAID', paid);
69
+ return out;
70
+ }, [free, paid, tier]);
71
+ const selectableIdxToRow = [];
72
+ rows.forEach((r, i) => {
73
+ if (r.kind === 'model')
74
+ selectableIdxToRow.push(i);
75
+ });
76
+ const totalSelectable = selectableIdxToRow.length;
77
+ useEffect(() => {
78
+ if (totalSelectable === 0) {
79
+ if (idx !== 0)
80
+ setIdx(0);
81
+ return;
82
+ }
83
+ if (idx >= totalSelectable)
84
+ setIdx(totalSelectable - 1);
85
+ }, [totalSelectable, idx]);
41
86
  useInput((input, key) => {
42
87
  if (key.escape)
43
88
  return onCancel();
44
- if (key.upArrow && filtered.length)
45
- setIdx((i) => (i - 1 + filtered.length) % filtered.length);
46
- if (key.downArrow && filtered.length)
47
- setIdx((i) => (i + 1) % filtered.length);
48
- if (key.return && filtered[idx])
49
- onSelect(filtered[idx].id);
50
- if (key.backspace || key.delete)
89
+ if (key.tab) {
90
+ setTier((t) => (t === 'all' ? 'free' : t === 'free' ? 'paid' : 'all'));
91
+ setIdx(0);
92
+ return;
93
+ }
94
+ if (key.upArrow && totalSelectable)
95
+ setIdx((i) => (i - 1 + totalSelectable) % totalSelectable);
96
+ if (key.downArrow && totalSelectable)
97
+ setIdx((i) => (i + 1) % totalSelectable);
98
+ if (key.return) {
99
+ const rowI = selectableIdxToRow[idx];
100
+ const row = rowI !== undefined ? rows[rowI] : undefined;
101
+ if (row && row.kind === 'model')
102
+ onSelect(row.model.id);
103
+ return;
104
+ }
105
+ if (key.backspace || key.delete) {
51
106
  setFilter((f) => f.slice(0, -1));
107
+ setIdx(0);
108
+ return;
109
+ }
52
110
  if (input && !key.ctrl && !key.meta && !key.upArrow && !key.downArrow) {
53
111
  const clean = input.replace(/[\x00-\x1F\x7F]/g, '');
54
- if (clean)
112
+ if (clean) {
55
113
  setFilter((f) => f + clean);
114
+ setIdx(0);
115
+ }
56
116
  }
57
117
  });
58
118
  const inner = width - 4;
59
- const maxVisible = 12;
60
- const start = Math.max(0, Math.min(idx - 6, Math.max(0, filtered.length - maxVisible)));
61
- const visible = filtered.slice(start, start + maxVisible);
62
- const label = filter ? `filter: ${filter}` : `${provider} models`;
63
- const topPad = Math.max(1, inner - label.length - 4);
64
- return (_jsxs(Box, { flexDirection: "column", children: [_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(topPad) + '╮' })] }), loading ? (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ ' }), _jsxs(Text, { color: theme.muted, children: ["fetching ", provider, " catalog\u2026"] })] })) : error ? (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ ' }), _jsxs(Text, { color: theme.error, children: ["\u2717 ", error.slice(0, inner - 6)] })] })) : visible.length === 0 ? (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ ' }), _jsx(Text, { color: theme.error, children: "no matches" })] })) : (visible.map((m, i) => {
65
- const realIdx = start + i;
66
- const active = realIdx === idx;
119
+ const total = free.length + paid.length;
120
+ const headerLabel = filter
121
+ ? `filter: ${filter} · ${total} match`
122
+ : `${provider} · ${total} total · ${free.length} free`;
123
+ const topPad = Math.max(1, inner - headerLabel.length - 4);
124
+ const maxVisible = 14;
125
+ const cursorRowI = selectableIdxToRow[idx] ?? 0;
126
+ let windowStart = Math.max(0, cursorRowI - Math.floor(maxVisible / 2));
127
+ windowStart = Math.min(windowStart, Math.max(0, rows.length - maxVisible));
128
+ const windowEnd = windowStart + maxVisible;
129
+ const visibleRows = rows.slice(windowStart, windowEnd);
130
+ const cursorAbs = idx;
131
+ const tierBadge = tier === 'all' ? 'all' : tier === 'free' ? 'FREE only' : 'PAID only';
132
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '╭─ ' }), _jsx(Text, { color: theme.accent, bold: true, children: headerLabel }), _jsx(Text, { color: theme.muted, children: ' ' + '─'.repeat(topPad) + '╮' })] }), loading ? (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ ' }), _jsxs(Text, { color: theme.muted, children: ["fetching ", provider, " catalog\u2026"] })] })) : error ? (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ ' }), _jsxs(Text, { color: theme.error, children: ["\u2717 ", error.slice(0, inner - 6)] })] })) : rows.length === 0 ? (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ ' }), _jsxs(Text, { color: theme.error, children: ["no matches", tier !== 'all' ? ` in ${tierBadge}` : ''] })] })) : (visibleRows.map((row, i) => {
133
+ if (row.kind === 'header') {
134
+ const sectionPad = Math.max(1, inner - row.label.length - row.count.toString().length - 5);
135
+ return (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ ' }), _jsx(Text, { color: theme.accent, bold: true, children: row.label }), _jsx(Text, { color: theme.muted, children: ' ' + '─'.repeat(sectionPad) + ' ' + row.count })] }, `h-${row.label}`));
136
+ }
137
+ const m = row.model;
138
+ const active = row.absoluteIdx === cursorAbs;
67
139
  const isCurrent = m.id === current;
68
140
  const ctx = m.contextLength
69
141
  ? m.contextLength >= 1000
70
142
  ? `${Math.round(m.contextLength / 1000)}k`
71
143
  : `${m.contextLength}`
72
144
  : '';
73
- const freeTag = m.isFree ? 'FREE' : '';
74
- // Right side: ctx + free badge
75
- const right = [ctx, freeTag].filter(Boolean).join(' · ');
76
- const label0 = m.id.length > inner - 10
77
- ? m.id.slice(0, inner - 13) + '…'
78
- : m.id;
79
- const pad = Math.max(1, inner - label0.length - right.length - 4);
80
- 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: label0 }), _jsx(Text, { color: theme.muted, children: ' '.repeat(pad) }), _jsx(Text, { color: theme.muted, children: right }), isCurrent && _jsx(Text, { color: theme.accent, children: " \u2713" })] }, m.id));
81
- })), _jsx(Text, { children: _jsx(Text, { color: theme.muted, children: '╰' + '─'.repeat(inner) + '╯' }) }), _jsx(Text, { color: theme.muted, children: "\u2191\u2193 move \u00B7 type to filter \u00B7 \u21B5 select \u00B7 esc cancel" })] }));
145
+ const price = m.isFree
146
+ ? 'FREE'
147
+ : m.pricing
148
+ ? `$${(m.pricing.prompt * 1_000_000).toFixed(2)}/M`
149
+ : '';
150
+ const right = [ctx, price].filter(Boolean).join(' · ');
151
+ const maxName = Math.max(8, inner - right.length - 6);
152
+ const name = m.id.length > maxName ? m.id.slice(0, maxName - 1) + '…' : m.id;
153
+ const pad = Math.max(1, inner - name.length - right.length - 4);
154
+ 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: name }), _jsx(Text, { color: theme.muted, children: ' '.repeat(pad) }), _jsx(Text, { color: m.isFree ? theme.success : theme.muted, children: right }), isCurrent && _jsx(Text, { color: theme.accent, children: " \u2713" })] }, `m-${m.id}`));
155
+ })), _jsx(Text, { children: _jsx(Text, { color: theme.muted, children: '╰' + '─'.repeat(inner) + '╯' }) }), _jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: " \u2191\u2193 move \u00B7 " }), _jsx(Text, { color: theme.accent, children: "tab" }), _jsxs(Text, { color: theme.muted, children: [" tier (", tierBadge, ") \u00B7 "] }), _jsx(Text, { color: theme.muted, children: "type to filter \u00B7 " }), _jsx(Text, { color: theme.accent, children: "\u21B5" }), _jsx(Text, { color: theme.muted, children: " select \u00B7 " }), _jsx(Text, { color: theme.accent, children: "esc" }), _jsx(Text, { color: theme.muted, children: " cancel" })] })] }));
82
156
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blyuecode",
3
- "version": "0.3.0",
3
+ "version": "0.3.4",
4
4
  "description": "BLYUE Code — a coding agent by Lumen",
5
5
  "type": "module",
6
6
  "license": "MIT",