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 +10 -0
- package/dist/ui/McpPanel.js +50 -0
- package/dist/ui/ModelPicker.js +103 -29
- package/package.json +1 -1
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
|
+
};
|
package/dist/ui/ModelPicker.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, 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
|
|
39
|
-
|
|
40
|
-
|
|
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.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const topPad = Math.max(1, inner -
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
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
|
};
|