blyuecode 0.1.12 → 0.1.14
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/core/agent.js +69 -10
- package/dist/core/provider.js +81 -24
- package/dist/core/tools.js +71 -3
- package/dist/ui/Chat.js +62 -6
- package/dist/ui/InputBar.js +6 -2
- package/dist/ui/MakerMark.js +10 -2
- package/dist/ui/Mascot.js +47 -0
- package/dist/ui/MascotPrompt.js +38 -0
- package/package.json +1 -1
package/dist/core/agent.js
CHANGED
|
@@ -25,7 +25,7 @@ export class Agent {
|
|
|
25
25
|
}
|
|
26
26
|
persist() {
|
|
27
27
|
try {
|
|
28
|
-
saveSession(this.messages.slice(1));
|
|
28
|
+
saveSession(this.messages.slice(1));
|
|
29
29
|
}
|
|
30
30
|
catch { }
|
|
31
31
|
}
|
|
@@ -41,25 +41,34 @@ export class Agent {
|
|
|
41
41
|
this.persist();
|
|
42
42
|
while (true) {
|
|
43
43
|
const tools = getToolDefs();
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
let content = '';
|
|
45
|
+
let toolCalls = [];
|
|
46
|
+
for await (const ev of this.getProvider().chatStream(this.messages, tools)) {
|
|
47
|
+
if (ev.type === 'delta') {
|
|
48
|
+
content += ev.text;
|
|
49
|
+
yield { type: 'text', delta: ev.text };
|
|
50
|
+
}
|
|
51
|
+
else if (ev.type === 'final') {
|
|
52
|
+
if (ev.content)
|
|
53
|
+
content = ev.content;
|
|
54
|
+
toolCalls = ev.toolCalls;
|
|
55
|
+
}
|
|
47
56
|
}
|
|
48
|
-
if (
|
|
57
|
+
if (toolCalls.length === 0) {
|
|
49
58
|
this.persist();
|
|
50
59
|
break;
|
|
51
60
|
}
|
|
52
61
|
this.messages.push({
|
|
53
62
|
role: 'assistant',
|
|
54
|
-
content:
|
|
55
|
-
tool_calls:
|
|
63
|
+
content: content || null,
|
|
64
|
+
tool_calls: toolCalls.map((tc) => ({
|
|
56
65
|
id: tc.id,
|
|
57
66
|
type: 'function',
|
|
58
67
|
function: { name: tc.name, arguments: tc.arguments },
|
|
59
68
|
})),
|
|
60
69
|
});
|
|
61
|
-
for (const tc of
|
|
62
|
-
yield
|
|
70
|
+
for (const tc of toolCalls) {
|
|
71
|
+
yield { type: 'tool_start', name: tc.name };
|
|
63
72
|
let args = {};
|
|
64
73
|
try {
|
|
65
74
|
args = JSON.parse(tc.arguments || '{}');
|
|
@@ -72,9 +81,59 @@ export class Agent {
|
|
|
72
81
|
content: result,
|
|
73
82
|
});
|
|
74
83
|
const preview = result.length > 500 ? result.slice(0, 500) + '...' : result;
|
|
75
|
-
yield
|
|
84
|
+
yield { type: 'tool_end', name: tc.name, preview };
|
|
76
85
|
}
|
|
77
86
|
this.persist();
|
|
78
87
|
}
|
|
79
88
|
}
|
|
89
|
+
async compact() {
|
|
90
|
+
if (this.messages.length <= 4) {
|
|
91
|
+
throw new Error('Not enough history to compact (need > 3 messages).');
|
|
92
|
+
}
|
|
93
|
+
const before = this.messages.length;
|
|
94
|
+
const sys = this.messages[0];
|
|
95
|
+
const rest = this.messages.slice(1);
|
|
96
|
+
// Build a compact transcript
|
|
97
|
+
const transcript = rest
|
|
98
|
+
.map((m) => {
|
|
99
|
+
if (m.role === 'tool') {
|
|
100
|
+
return `[tool result] ${(m.content || '').slice(0, 600)}`;
|
|
101
|
+
}
|
|
102
|
+
if (m.role === 'assistant' && m.tool_calls) {
|
|
103
|
+
const calls = m.tool_calls.map((tc) => tc.function.name).join(', ');
|
|
104
|
+
return `[assistant → tools: ${calls}] ${m.content || ''}`;
|
|
105
|
+
}
|
|
106
|
+
return `[${m.role}] ${(m.content || '').slice(0, 1200)}`;
|
|
107
|
+
})
|
|
108
|
+
.join('\n\n');
|
|
109
|
+
const summaryPrompt = [
|
|
110
|
+
{
|
|
111
|
+
role: 'system',
|
|
112
|
+
content: 'Summarize the conversation history below for an AI coding agent. ' +
|
|
113
|
+
'Include: what the user asked for, what files were touched, what tools were used, ' +
|
|
114
|
+
'what was accomplished, and any pending tasks or important context. ' +
|
|
115
|
+
'Be specific about filenames and outcomes. Aim for 300-600 words. ' +
|
|
116
|
+
'Output only the summary, no preamble.',
|
|
117
|
+
},
|
|
118
|
+
{ role: 'user', content: transcript },
|
|
119
|
+
];
|
|
120
|
+
const summary = await this.getProvider().summarize(summaryPrompt);
|
|
121
|
+
this.messages = [
|
|
122
|
+
sys,
|
|
123
|
+
{
|
|
124
|
+
role: 'user',
|
|
125
|
+
content: `[Summary of prior conversation:\n${summary}\n]`,
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
role: 'assistant',
|
|
129
|
+
content: 'Understood. I have the context from our previous conversation.',
|
|
130
|
+
},
|
|
131
|
+
];
|
|
132
|
+
this.persist();
|
|
133
|
+
return {
|
|
134
|
+
before,
|
|
135
|
+
after: this.messages.length,
|
|
136
|
+
summaryLen: summary.length,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
80
139
|
}
|
package/dist/core/provider.js
CHANGED
|
@@ -37,16 +37,81 @@ export class Provider {
|
|
|
37
37
|
}
|
|
38
38
|
catch { }
|
|
39
39
|
}
|
|
40
|
-
|
|
41
|
-
const
|
|
40
|
+
handleError(e) {
|
|
41
|
+
const status = e?.status || e?.response?.status;
|
|
42
|
+
const raw = e?.error?.message || e?.message || String(e);
|
|
43
|
+
if (status === 401) {
|
|
44
|
+
throw new Error(`401 Unauthorized — bad API key.\n Fix: rm ~/.blyue/.env then run again.`);
|
|
45
|
+
}
|
|
46
|
+
if (status === 404) {
|
|
47
|
+
throw new Error(`404 Model unavailable: "${this.model}".\n Fix: rm ~/.blyue/.env then pick a different model.`);
|
|
48
|
+
}
|
|
49
|
+
if (status === 429) {
|
|
50
|
+
throw new Error(`429 Rate limit / quota. Try again or switch model.`);
|
|
51
|
+
}
|
|
52
|
+
throw new Error(raw);
|
|
53
|
+
}
|
|
54
|
+
// Non-streaming call (used by /compact)
|
|
55
|
+
async chat(messages, tools = []) {
|
|
56
|
+
const { messages: safeMessages } = this.sanitize(messages);
|
|
42
57
|
try {
|
|
43
58
|
const res = await this.client.chat.completions.create({
|
|
59
|
+
model: this.model,
|
|
60
|
+
messages: safeMessages,
|
|
61
|
+
tools,
|
|
62
|
+
tool_choice: tools.length ? 'auto' : undefined,
|
|
63
|
+
});
|
|
64
|
+
const msg = res.choices[0].message;
|
|
65
|
+
return {
|
|
66
|
+
content: msg.content,
|
|
67
|
+
toolCalls: (msg.tool_calls || []).map((tc) => ({
|
|
68
|
+
id: tc.id,
|
|
69
|
+
name: tc.function.name,
|
|
70
|
+
arguments: tc.function.arguments,
|
|
71
|
+
})),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
catch (e) {
|
|
75
|
+
this.handleError(e);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Streaming call — yields deltas then a final summary
|
|
79
|
+
async *chatStream(messages, tools) {
|
|
80
|
+
const { messages: safeMessages, redactedCount } = this.sanitize(messages);
|
|
81
|
+
try {
|
|
82
|
+
const stream = await this.client.chat.completions.create({
|
|
44
83
|
model: this.model,
|
|
45
84
|
messages: safeMessages,
|
|
46
85
|
tools,
|
|
47
86
|
tool_choice: 'auto',
|
|
87
|
+
stream: true,
|
|
48
88
|
});
|
|
49
|
-
|
|
89
|
+
let content = '';
|
|
90
|
+
const toolCallsMap = new Map();
|
|
91
|
+
for await (const chunk of stream) {
|
|
92
|
+
const delta = chunk.choices?.[0]?.delta;
|
|
93
|
+
if (!delta)
|
|
94
|
+
continue;
|
|
95
|
+
if (delta.content) {
|
|
96
|
+
content += delta.content;
|
|
97
|
+
yield { type: 'delta', text: delta.content };
|
|
98
|
+
}
|
|
99
|
+
if (delta.tool_calls) {
|
|
100
|
+
for (const tc of delta.tool_calls) {
|
|
101
|
+
const idx = tc.index ?? 0;
|
|
102
|
+
if (!toolCallsMap.has(idx)) {
|
|
103
|
+
toolCallsMap.set(idx, { id: '', name: '', args: '' });
|
|
104
|
+
}
|
|
105
|
+
const e = toolCallsMap.get(idx);
|
|
106
|
+
if (tc.id)
|
|
107
|
+
e.id = tc.id;
|
|
108
|
+
if (tc.function?.name)
|
|
109
|
+
e.name = tc.function.name;
|
|
110
|
+
if (tc.function?.arguments)
|
|
111
|
+
e.args += tc.function.arguments;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
50
115
|
const totalBytes = JSON.stringify(safeMessages).length;
|
|
51
116
|
this.writeDebug({
|
|
52
117
|
model: this.model,
|
|
@@ -57,30 +122,22 @@ export class Provider {
|
|
|
57
122
|
messageCount: safeMessages.length,
|
|
58
123
|
redactedCount,
|
|
59
124
|
timestamp: new Date().toISOString(),
|
|
125
|
+
streaming: true,
|
|
60
126
|
});
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
arguments: tc.function.arguments,
|
|
68
|
-
})),
|
|
69
|
-
};
|
|
127
|
+
const toolCalls = [...toolCallsMap.values()].map((tc) => ({
|
|
128
|
+
id: tc.id,
|
|
129
|
+
name: tc.name,
|
|
130
|
+
arguments: tc.args,
|
|
131
|
+
}));
|
|
132
|
+
yield { type: 'final', content: content || null, toolCalls };
|
|
70
133
|
}
|
|
71
134
|
catch (e) {
|
|
72
|
-
|
|
73
|
-
const raw = e?.error?.message || e?.message || String(e);
|
|
74
|
-
if (status === 401) {
|
|
75
|
-
throw new Error(`401 Unauthorized — bad API key.\n Fix: rm ~/.blyue/.env then run again.`);
|
|
76
|
-
}
|
|
77
|
-
if (status === 404) {
|
|
78
|
-
throw new Error(`404 Model unavailable: "${this.model}".\n Fix: rm ~/.blyue/.env then pick a different model.`);
|
|
79
|
-
}
|
|
80
|
-
if (status === 429) {
|
|
81
|
-
throw new Error(`429 Rate limit / quota. Try again or switch model.`);
|
|
82
|
-
}
|
|
83
|
-
throw new Error(raw);
|
|
135
|
+
this.handleError(e);
|
|
84
136
|
}
|
|
85
137
|
}
|
|
138
|
+
// Summarize helper for /compact
|
|
139
|
+
async summarize(messages) {
|
|
140
|
+
const res = await this.chat(messages, []);
|
|
141
|
+
return res.content || '';
|
|
142
|
+
}
|
|
86
143
|
}
|
package/dist/core/tools.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import fs from 'fs/promises';
|
|
2
|
-
import { existsSync, readFileSync } from 'fs';
|
|
3
|
-
import { exec } from 'child_process';
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, unlinkSync } from 'fs';
|
|
3
|
+
import { exec, execSync } from 'child_process';
|
|
4
4
|
import { promisify } from 'util';
|
|
5
|
+
import { tmpdir } from 'os';
|
|
6
|
+
import { join } from 'path';
|
|
5
7
|
import { hasN8n } from './config.js';
|
|
6
8
|
import { permissions } from './permission.js';
|
|
7
9
|
import { lineDiff, compactDiff } from './diff.js';
|
|
@@ -25,7 +27,7 @@ const BASE_TOOLS = [
|
|
|
25
27
|
type: 'function',
|
|
26
28
|
function: {
|
|
27
29
|
name: 'write_file',
|
|
28
|
-
description: 'Write content to a file (overwrites existing)',
|
|
30
|
+
description: 'Write content to a file (overwrites existing). Prefer apply_patch for edits to existing files.',
|
|
29
31
|
parameters: {
|
|
30
32
|
type: 'object',
|
|
31
33
|
properties: {
|
|
@@ -36,6 +38,21 @@ const BASE_TOOLS = [
|
|
|
36
38
|
},
|
|
37
39
|
},
|
|
38
40
|
},
|
|
41
|
+
{
|
|
42
|
+
type: 'function',
|
|
43
|
+
function: {
|
|
44
|
+
name: 'apply_patch',
|
|
45
|
+
description: 'Apply a unified diff patch to files. Use for targeted edits — cheaper and safer than rewriting whole files. ' +
|
|
46
|
+
'Format: standard unified diff with --- a/path, +++ b/path, and @@ hunks.',
|
|
47
|
+
parameters: {
|
|
48
|
+
type: 'object',
|
|
49
|
+
properties: {
|
|
50
|
+
patch: { type: 'string', description: 'Unified diff patch content' },
|
|
51
|
+
},
|
|
52
|
+
required: ['patch'],
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
},
|
|
39
56
|
{
|
|
40
57
|
type: 'function',
|
|
41
58
|
function: {
|
|
@@ -137,6 +154,57 @@ export async function executeTool(name, args) {
|
|
|
137
154
|
await fs.writeFile(path, content, 'utf8');
|
|
138
155
|
return 'Wrote ' + path;
|
|
139
156
|
}
|
|
157
|
+
case 'apply_patch': {
|
|
158
|
+
const patchText = args.patch;
|
|
159
|
+
if (!patchText || !patchText.trim())
|
|
160
|
+
return 'Error: empty patch';
|
|
161
|
+
const lines = patchText.split('\n');
|
|
162
|
+
const files = [
|
|
163
|
+
...new Set(lines
|
|
164
|
+
.filter((l) => l.startsWith('--- ') || l.startsWith('+++ '))
|
|
165
|
+
.map((l) => l.slice(4).trim())
|
|
166
|
+
.filter((f) => f !== '/dev/null')),
|
|
167
|
+
];
|
|
168
|
+
if (!permissions.isAlwaysAllowed('apply_patch')) {
|
|
169
|
+
const diffPreview = lines.slice(0, 50).map((l) => {
|
|
170
|
+
if (l.startsWith('+') && !l.startsWith('+++'))
|
|
171
|
+
return { type: 'add', text: l.slice(1) };
|
|
172
|
+
if (l.startsWith('-') && !l.startsWith('---'))
|
|
173
|
+
return { type: 'remove', text: l.slice(1) };
|
|
174
|
+
return { type: 'same', text: l };
|
|
175
|
+
});
|
|
176
|
+
const answer = await permissions.ask({
|
|
177
|
+
tool: 'apply_patch',
|
|
178
|
+
title: 'Apply patch',
|
|
179
|
+
body: [
|
|
180
|
+
`Files: ${files.join(', ') || '(unknown)'}`,
|
|
181
|
+
`${lines.length} lines`,
|
|
182
|
+
],
|
|
183
|
+
diff: diffPreview,
|
|
184
|
+
danger: 'medium',
|
|
185
|
+
});
|
|
186
|
+
if (answer === 'no')
|
|
187
|
+
return 'Denied by user: apply_patch';
|
|
188
|
+
}
|
|
189
|
+
const p = /^---\s+a\//m.test(patchText) ? '1' : '0';
|
|
190
|
+
const tmp = join(tmpdir(), `blyue-patch-${Date.now()}.diff`);
|
|
191
|
+
writeFileSync(tmp, patchText);
|
|
192
|
+
try {
|
|
193
|
+
const out = execSync(`patch -p${p} --no-backup-if-mismatch --forward < "${tmp}"`, { cwd: process.cwd(), encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
194
|
+
return 'Patch applied.\n' + (out || '');
|
|
195
|
+
}
|
|
196
|
+
catch (e) {
|
|
197
|
+
const stderr = e.stderr?.toString() || '';
|
|
198
|
+
const stdout = e.stdout?.toString() || '';
|
|
199
|
+
throw new Error(`Patch failed.\nstdout: ${stdout}\nstderr: ${stderr}`);
|
|
200
|
+
}
|
|
201
|
+
finally {
|
|
202
|
+
try {
|
|
203
|
+
unlinkSync(tmp);
|
|
204
|
+
}
|
|
205
|
+
catch { }
|
|
206
|
+
}
|
|
207
|
+
}
|
|
140
208
|
case 'list_files': {
|
|
141
209
|
const dir = args.path || '.';
|
|
142
210
|
const entries = await fs.readdir(dir, { withFileTypes: true });
|
package/dist/ui/Chat.js
CHANGED
|
@@ -10,6 +10,7 @@ import { InputBar } from './InputBar.js';
|
|
|
10
10
|
import { ModelPicker } from './ModelPicker.js';
|
|
11
11
|
import { Settings } from './Settings.js';
|
|
12
12
|
import { PermissionPrompt } from './PermissionPrompt.js';
|
|
13
|
+
import { Spinner } from './Spinner.js';
|
|
13
14
|
import { theme } from './theme.js';
|
|
14
15
|
let nextId = 0;
|
|
15
16
|
const mkItem = (kind, text) => ({ kind, text, id: nextId++ });
|
|
@@ -27,9 +28,8 @@ const HistoryLine = ({ item }) => {
|
|
|
27
28
|
};
|
|
28
29
|
const DEBUG_FILE = path.join(os.homedir(), '.blyue', 'last-request.json');
|
|
29
30
|
function readDebug() {
|
|
30
|
-
if (!fs.existsSync(DEBUG_FILE))
|
|
31
|
+
if (!fs.existsSync(DEBUG_FILE))
|
|
31
32
|
return ['No request sent yet in this session.'];
|
|
32
|
-
}
|
|
33
33
|
try {
|
|
34
34
|
const d = JSON.parse(fs.readFileSync(DEBUG_FILE, 'utf8'));
|
|
35
35
|
const kb = (d.approxBytes / 1024).toFixed(1);
|
|
@@ -58,10 +58,12 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
58
58
|
});
|
|
59
59
|
const [input, setInput] = useState('');
|
|
60
60
|
const [loading, setLoading] = useState(false);
|
|
61
|
+
const [streaming, setStreaming] = useState('');
|
|
61
62
|
const [overlay, setOverlay] = useState('none');
|
|
62
63
|
const [permReq, setPermReq] = useState(null);
|
|
63
64
|
const [currentModel, setCurrentModel] = useState(process.env.MODEL || '');
|
|
64
65
|
const [agent, setAgent] = useState(() => new Agent());
|
|
66
|
+
const [reaction, setReaction] = useState(null);
|
|
65
67
|
useEffect(() => {
|
|
66
68
|
const unsub = permissions.subscribe((req) => setPermReq(req));
|
|
67
69
|
return unsub;
|
|
@@ -74,10 +76,16 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
74
76
|
if (key.ctrl && inputChar === 's')
|
|
75
77
|
setOverlay('settings');
|
|
76
78
|
});
|
|
79
|
+
const flush = (kind, text) => {
|
|
80
|
+
if (!text)
|
|
81
|
+
return;
|
|
82
|
+
setHistory((h) => [...h, mkItem(kind, text)]);
|
|
83
|
+
};
|
|
77
84
|
const handleSubmit = async (text) => {
|
|
78
85
|
if (!text.trim() || loading)
|
|
79
86
|
return;
|
|
80
87
|
const trimmed = text.trim();
|
|
88
|
+
// ---- slash commands ----
|
|
81
89
|
if (trimmed === '/model' || trimmed === '/models') {
|
|
82
90
|
setInput('');
|
|
83
91
|
setOverlay('model');
|
|
@@ -98,6 +106,23 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
98
106
|
]);
|
|
99
107
|
return;
|
|
100
108
|
}
|
|
109
|
+
if (trimmed === '/compact') {
|
|
110
|
+
setInput('');
|
|
111
|
+
setLoading(true);
|
|
112
|
+
setStreaming('');
|
|
113
|
+
try {
|
|
114
|
+
const r = await agent.compact();
|
|
115
|
+
setHistory((h) => [
|
|
116
|
+
...h,
|
|
117
|
+
mkItem('tool', `Compacted: ${r.before} → ${r.after} messages (summary ${r.summaryLen} chars)`),
|
|
118
|
+
]);
|
|
119
|
+
}
|
|
120
|
+
catch (e) {
|
|
121
|
+
setHistory((h) => [...h, mkItem('error', e.message)]);
|
|
122
|
+
}
|
|
123
|
+
setLoading(false);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
101
126
|
if (trimmed === '/clear') {
|
|
102
127
|
setInput('');
|
|
103
128
|
agent.clear();
|
|
@@ -112,24 +137,55 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
112
137
|
mkItem('tool', 'Commands:'),
|
|
113
138
|
mkItem('tool', ' /model switch model (Ctrl+P)'),
|
|
114
139
|
mkItem('tool', ' /settings open settings (Ctrl+S)'),
|
|
140
|
+
mkItem('tool', ' /compact summarize history to free context'),
|
|
115
141
|
mkItem('tool', ' /debug show last request to provider'),
|
|
116
142
|
mkItem('tool', ' /clear clear session and permissions'),
|
|
117
143
|
mkItem('tool', ' /help show this list'),
|
|
118
144
|
]);
|
|
119
145
|
return;
|
|
120
146
|
}
|
|
147
|
+
// ---- normal run ----
|
|
121
148
|
setInput('');
|
|
122
149
|
setHistory((h) => [...h, mkItem('user', trimmed)]);
|
|
123
150
|
setLoading(true);
|
|
151
|
+
setStreaming('');
|
|
152
|
+
// Jump animation on submit
|
|
153
|
+
setReaction('jump');
|
|
154
|
+
setTimeout(() => setReaction(null), 400);
|
|
155
|
+
let textBuf = '';
|
|
156
|
+
let toolBuf = '';
|
|
124
157
|
try {
|
|
125
|
-
for await (const
|
|
126
|
-
|
|
127
|
-
|
|
158
|
+
for await (const ev of agent.run(trimmed)) {
|
|
159
|
+
if (ev.type === 'text') {
|
|
160
|
+
textBuf += ev.delta;
|
|
161
|
+
setStreaming(textBuf);
|
|
162
|
+
}
|
|
163
|
+
else if (ev.type === 'tool_start') {
|
|
164
|
+
if (textBuf) {
|
|
165
|
+
flush('ai', textBuf);
|
|
166
|
+
textBuf = '';
|
|
167
|
+
}
|
|
168
|
+
toolBuf = '\n[Calling ' + ev.name + '...]\n';
|
|
169
|
+
setStreaming(toolBuf);
|
|
170
|
+
}
|
|
171
|
+
else if (ev.type === 'tool_end') {
|
|
172
|
+
flush('tool', toolBuf + '[Result]: ' + ev.preview + '\n');
|
|
173
|
+
toolBuf = '';
|
|
174
|
+
setStreaming('');
|
|
175
|
+
}
|
|
128
176
|
}
|
|
177
|
+
if (textBuf)
|
|
178
|
+
flush('ai', textBuf);
|
|
179
|
+
// Success reaction
|
|
180
|
+
setReaction('success');
|
|
181
|
+
setTimeout(() => setReaction(null), 2000);
|
|
129
182
|
}
|
|
130
183
|
catch (e) {
|
|
131
184
|
setHistory((h) => [...h, mkItem('error', e?.message || String(e))]);
|
|
185
|
+
setReaction('error');
|
|
186
|
+
setTimeout(() => setReaction(null), 3000);
|
|
132
187
|
}
|
|
188
|
+
setStreaming('');
|
|
133
189
|
setLoading(false);
|
|
134
190
|
};
|
|
135
191
|
const handleModelSelect = (id) => {
|
|
@@ -155,5 +211,5 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
155
211
|
const shortModel = currentModel.length > maxModelLen
|
|
156
212
|
? '…' + currentModel.slice(-(maxModelLen - 1))
|
|
157
213
|
: currentModel || 'unset';
|
|
158
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: history, children: (item) => _jsx(HistoryLine, { item: item }, item.id) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(InputBar, { value: input, onChange: setInput, onSubmit: handleSubmit, loading: loading }), _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: "/
|
|
214
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: history, children: (item) => _jsx(HistoryLine, { item: item }, item.id) }), streaming ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.text, children: streaming }) })) : loading ? (_jsx(Box, { marginTop: 1, children: _jsx(Spinner, {}) })) : null, _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(InputBar, { value: input, onChange: setInput, onSubmit: handleSubmit, loading: loading, reaction: reaction }), _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 })] })] })] }));
|
|
159
215
|
};
|
package/dist/ui/InputBar.js
CHANGED
|
@@ -4,9 +4,10 @@ import { Box, Text, useStdout } from 'ink';
|
|
|
4
4
|
import TextInput from 'ink-text-input';
|
|
5
5
|
import { Placeholder } from './Placeholder.js';
|
|
6
6
|
import { RocketBar } from './RocketBar.js';
|
|
7
|
+
import { MascotPrompt } from './MascotPrompt.js';
|
|
7
8
|
import { theme } from './theme.js';
|
|
8
9
|
const DOTS = ['●○○○', '○●○○', '○○●○', '○○○●'];
|
|
9
|
-
export const InputBar = ({ value, onChange, onSubmit, loading }) => {
|
|
10
|
+
export const InputBar = ({ value, onChange, onSubmit, loading, reaction }) => {
|
|
10
11
|
const { stdout } = useStdout();
|
|
11
12
|
const width = Math.max(24, stdout?.columns ?? 50);
|
|
12
13
|
const [dotI, setDotI] = useState(0);
|
|
@@ -17,8 +18,11 @@ export const InputBar = ({ value, onChange, onSubmit, loading }) => {
|
|
|
17
18
|
return () => clearInterval(int);
|
|
18
19
|
}, [loading]);
|
|
19
20
|
const hasText = value.length > 0;
|
|
21
|
+
// Decide mascot state, priority order:
|
|
22
|
+
// reaction > loading > typing > idle
|
|
23
|
+
const mascotState = reaction ?? (loading ? 'thinking' : hasText ? 'typing' : 'idle');
|
|
20
24
|
const label = loading ? 'thinking' : 'blyue';
|
|
21
25
|
const status = loading ? ' ' + DOTS[dotI] + ' ' : '';
|
|
22
26
|
const botLen = Math.max(0, width - 2 - status.length);
|
|
23
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_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" })] })] }));
|
|
27
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(MascotPrompt, { state: mascotState }), _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" })] })] }));
|
|
24
28
|
};
|
package/dist/ui/MakerMark.js
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Text } from 'ink';
|
|
3
|
-
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { Mascot } from './Mascot.js';
|
|
4
|
+
export const MakerMark = ({ color, narrow }) => {
|
|
5
|
+
// Phone / narrow: single line
|
|
6
|
+
if (narrow) {
|
|
7
|
+
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
|
+
}
|
|
9
|
+
// Wide: mascot on left, wordmark vertically centered
|
|
10
|
+
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
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect } from 'react';
|
|
3
|
+
import { Box, Text } from 'ink';
|
|
4
|
+
import 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
|
+
const BODY_TOP = ' ▄▄▄▄▄▄▄ ';
|
|
11
|
+
const BODY_LIGHT = '░░░░░░░░░';
|
|
12
|
+
const BODY_FACE_A = '░ ● ● ░';
|
|
13
|
+
const BODY_FACE_B = '░ ⌣ ░';
|
|
14
|
+
const BODY_MED = '▒▒▒▒▒▒▒▒▒';
|
|
15
|
+
const BODY_DARK = '▓▓▓▓▓▓▓▓▓';
|
|
16
|
+
const BODY_BOT = ' ▀▀▀▀▀▀▀ ';
|
|
17
|
+
function shouldAnimate() {
|
|
18
|
+
if (process.env.BLYUE_ANIMATE === '1')
|
|
19
|
+
return true;
|
|
20
|
+
if (process.env.BLYUE_STATIC === '1')
|
|
21
|
+
return false;
|
|
22
|
+
try {
|
|
23
|
+
const gb = os.totalmem() / 1024 / 1024 / 1024;
|
|
24
|
+
if (gb < 8)
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
catch { }
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
// Full mascot — 9 wide × 6 tall
|
|
31
|
+
export const Mascot = ({ color }) => {
|
|
32
|
+
const [pulse, setPulse] = useState(0);
|
|
33
|
+
const [animate] = useState(() => shouldAnimate());
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (!animate)
|
|
36
|
+
return;
|
|
37
|
+
const t = setInterval(() => setPulse((p) => (p + 1) % 3), 800);
|
|
38
|
+
return () => clearInterval(t);
|
|
39
|
+
}, [animate]);
|
|
40
|
+
// Subtle pulse: alternate shade intensity on the middle row
|
|
41
|
+
const mid = pulse === 0 ? BODY_MED : pulse === 1 ? '▒▒▒▒▒▒▒▒▒' : '░░▒░░▒░░';
|
|
42
|
+
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
|
+
};
|
|
44
|
+
// Compact — one line, for input bar & prompt
|
|
45
|
+
export const MascotInline = ({ color }) => (_jsx(Text, { color: color, bold: true, children: "\u2584 " }));
|
|
46
|
+
// Tiny — just the face, for very narrow terminals
|
|
47
|
+
export const MascotMini = ({ color }) => (_jsx(Text, { color: color, bold: true, children: "\u2584\u2588\u2584 " }));
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect } from 'react';
|
|
3
|
+
import { Text } from 'ink';
|
|
4
|
+
import { theme } from './theme.js';
|
|
5
|
+
// Face frames per state
|
|
6
|
+
const FACES = {
|
|
7
|
+
idle: ['◕‿◕', '◕‿◕', '◕_◕', '◕‿◕', '◕‿◕', '◕‿◕', '◕_◕', '◕‿◕'],
|
|
8
|
+
typing: ['◉‿◉', '◉‿◉', '◉‿◉', '◉_◉'],
|
|
9
|
+
thinking: ['◐_◐', '◓_◓', '◑_◑', '◒_◒'],
|
|
10
|
+
success: ['^‿^', '^▽^', '^‿^'],
|
|
11
|
+
error: ['◔︵◔', '◔︵◔', '◔_◔'],
|
|
12
|
+
jump: ['◕‿◕', '◉▽◉', '^▽^', '◉▽◉', '◕‿◕'],
|
|
13
|
+
};
|
|
14
|
+
// Frame speeds in ms
|
|
15
|
+
const SPEEDS = {
|
|
16
|
+
idle: 500,
|
|
17
|
+
typing: 700,
|
|
18
|
+
thinking: 120,
|
|
19
|
+
success: 300,
|
|
20
|
+
error: 400,
|
|
21
|
+
jump: 70,
|
|
22
|
+
};
|
|
23
|
+
export const MascotPrompt = ({ state }) => {
|
|
24
|
+
const frames = FACES[state];
|
|
25
|
+
const [i, setI] = useState(0);
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
setI(0);
|
|
28
|
+
if (frames.length <= 1)
|
|
29
|
+
return;
|
|
30
|
+
const t = setInterval(() => setI((v) => (v + 1) % frames.length), SPEEDS[state]);
|
|
31
|
+
return () => clearInterval(t);
|
|
32
|
+
}, [state, frames.length]);
|
|
33
|
+
// Colors react to state too
|
|
34
|
+
const color = state === 'error' ? theme.error :
|
|
35
|
+
state === 'success' ? theme.success :
|
|
36
|
+
theme.accent;
|
|
37
|
+
return (_jsx(Text, { children: _jsx(Text, { color: color, bold: true, children: ' ' + frames[i] }) }));
|
|
38
|
+
};
|