blyuecode 0.1.11 → 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/localCheck.js +14 -0
- 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/dist/ui/Settings.js +28 -22
- 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
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export async function detectOllama(url = 'http://localhost:11434') {
|
|
2
|
+
try {
|
|
3
|
+
const res = await fetch(`${url}/api/tags`, {
|
|
4
|
+
signal: AbortSignal.timeout(1500),
|
|
5
|
+
});
|
|
6
|
+
if (!res.ok)
|
|
7
|
+
return [];
|
|
8
|
+
const json = await res.json();
|
|
9
|
+
return (json.models || []).map((m) => m.name);
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return [];
|
|
13
|
+
}
|
|
14
|
+
}
|
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
|
+
};
|
package/dist/ui/Settings.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useState } from 'react';
|
|
2
|
+
import { useState, useEffect } from 'react';
|
|
3
3
|
import { Box, Text, useInput, useStdout } from 'ink';
|
|
4
4
|
import { KeyInput } from './KeyInput.js';
|
|
5
5
|
import { getConfig, saveConfig, applyConfig } from '../core/config.js';
|
|
6
|
+
import { detectOllama } from '../core/localCheck.js';
|
|
6
7
|
import { theme } from './theme.js';
|
|
7
8
|
export const Settings = ({ onClose, onModelChange }) => {
|
|
8
9
|
const { stdout } = useStdout();
|
|
@@ -12,6 +13,15 @@ export const Settings = ({ onClose, onModelChange }) => {
|
|
|
12
13
|
const [cfg, setCfg] = useState(() => getConfig());
|
|
13
14
|
const [draft, setDraft] = useState('');
|
|
14
15
|
const [testResult, setTestResult] = useState(null);
|
|
16
|
+
const [ollamaModels, setOllamaModels] = useState([]);
|
|
17
|
+
const [ollamaChecked, setOllamaChecked] = useState(false);
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
(async () => {
|
|
20
|
+
const models = await detectOllama();
|
|
21
|
+
setOllamaModels(models);
|
|
22
|
+
setOllamaChecked(true);
|
|
23
|
+
})();
|
|
24
|
+
}, []);
|
|
15
25
|
const persist = (next) => {
|
|
16
26
|
saveConfig(next);
|
|
17
27
|
applyConfig(next);
|
|
@@ -28,7 +38,6 @@ export const Settings = ({ onClose, onModelChange }) => {
|
|
|
28
38
|
: 'not configured',
|
|
29
39
|
action: () => setScreen('n8n'),
|
|
30
40
|
},
|
|
31
|
-
{ label: 'Reset all config', hint: 'delete ~/.blyue/.env', action: () => setScreen('reset') },
|
|
32
41
|
{ label: 'Close', hint: 'esc', action: onClose },
|
|
33
42
|
];
|
|
34
43
|
// ---- Main menu navigation ----
|
|
@@ -42,7 +51,7 @@ export const Settings = ({ onClose, onModelChange }) => {
|
|
|
42
51
|
if (key.return)
|
|
43
52
|
mainItems[cursor].action();
|
|
44
53
|
}, { isActive: screen === 'main' });
|
|
45
|
-
// ---- n8n submenu
|
|
54
|
+
// ---- n8n submenu ----
|
|
46
55
|
const n8nItems = [
|
|
47
56
|
{ label: 'URL', hint: cfg.n8nUrl || 'not set', action: () => { setDraft(cfg.n8nUrl || ''); setScreen('n8n-url'); } },
|
|
48
57
|
{ label: 'API key', hint: cfg.n8nApiKey ? '•••' + cfg.n8nApiKey.slice(-4) : 'not set', action: () => { setDraft(cfg.n8nApiKey || ''); setScreen('n8n-key'); } },
|
|
@@ -77,18 +86,23 @@ export const Settings = ({ onClose, onModelChange }) => {
|
|
|
77
86
|
if (key.return)
|
|
78
87
|
n8nItems[n8nCursor].action();
|
|
79
88
|
}, { isActive: screen === 'n8n' });
|
|
89
|
+
// ---- n8n test ----
|
|
90
|
+
useInput(() => setScreen('n8n'), { isActive: screen === 'n8n-test' });
|
|
80
91
|
// ---- Frame helpers ----
|
|
81
92
|
const inner = width - 4;
|
|
82
93
|
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)) + '╮' })] }));
|
|
83
94
|
const bot = (_jsx(Text, { color: theme.muted, children: '╰' + '─'.repeat(inner) + '╯' }));
|
|
84
95
|
const row = (content) => (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ ' }), content] }));
|
|
85
|
-
// ----
|
|
96
|
+
// ---- Main screen ----
|
|
86
97
|
if (screen === 'main') {
|
|
87
|
-
|
|
98
|
+
const baseUrlIsLocal = (cfg.baseUrl || '').includes('localhost') ||
|
|
99
|
+
(cfg.baseUrl || '').includes('127.0.0.1');
|
|
100
|
+
return (_jsxs(Box, { flexDirection: "column", children: [top('settings'), ollamaChecked && ollamaModels.length > 0 && (row(_jsxs(Text, { children: [_jsx(Text, { color: theme.success, children: "\u25CF " }), _jsx(Text, { color: theme.muted, children: "Ollama detected \u00B7 " }), _jsxs(Text, { color: theme.text, children: [ollamaModels.length, " model(s)"] })] }))), ollamaChecked && ollamaModels.length === 0 && baseUrlIsLocal && (row(_jsxs(Text, { children: [_jsx(Text, { color: theme.error, children: "\u25CF " }), _jsx(Text, { color: theme.muted, children: "Ollama not running at localhost:11434" })] }))), mainItems.map((it, i) => {
|
|
88
101
|
const active = i === cursor;
|
|
89
102
|
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));
|
|
90
103
|
}), bot, _jsx(Text, { color: theme.muted, children: "\u2191\u2193 move \u00B7 \u21B5 select \u00B7 esc close" })] }));
|
|
91
104
|
}
|
|
105
|
+
// ---- API key / Base URL editors ----
|
|
92
106
|
if (screen === 'apikey' || screen === 'baseurl') {
|
|
93
107
|
const isKey = screen === 'apikey';
|
|
94
108
|
const title = isKey ? 'api key' : 'base url';
|
|
@@ -97,21 +111,26 @@ export const Settings = ({ onClose, onModelChange }) => {
|
|
|
97
111
|
const next = { ...cfg, [isKey ? 'apiKey' : 'baseUrl']: v };
|
|
98
112
|
persist(next);
|
|
99
113
|
};
|
|
100
|
-
return (_jsxs(Box, { flexDirection: "column", children: [top(title), row(_jsx(Text, { color: theme.muted, children: isKey ? 'Paste your new API key' : 'e.g.
|
|
114
|
+
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" })] }));
|
|
101
115
|
}
|
|
116
|
+
// ---- Model editor ----
|
|
102
117
|
if (screen === 'model') {
|
|
103
|
-
|
|
104
|
-
return (_jsxs(Box, { flexDirection: "column", children: [top('model'), row(_jsx(Text, { color: theme.muted, children:
|
|
118
|
+
const isLocal = (cfg.baseUrl || '').includes('localhost');
|
|
119
|
+
return (_jsxs(Box, { flexDirection: "column", children: [top('model'), row(_jsx(Text, { color: theme.muted, children: isLocal && ollamaModels.length > 0
|
|
120
|
+
? 'Available: ' + ollamaModels.slice(0, 3).join(', ')
|
|
121
|
+
: 'Type model name' })), row(_jsxs(Box, { children: [_jsx(Text, { color: theme.accent, bold: true, children: '❯ ' }), _jsx(KeyInput, { value: cfg.model, onChange: (v) => persist({ ...cfg, model: v }), placeholder: "qwen2.5-coder:1.5b", onSubmit: () => {
|
|
105
122
|
onModelChange?.(cfg.model);
|
|
106
123
|
setScreen('main');
|
|
107
124
|
} })] })), bot, _jsx(Text, { color: theme.muted, children: "type \u00B7 \u21B5 save \u00B7 esc back" })] }));
|
|
108
125
|
}
|
|
126
|
+
// ---- n8n submenu ----
|
|
109
127
|
if (screen === 'n8n') {
|
|
110
128
|
return (_jsxs(Box, { flexDirection: "column", children: [top('n8n integration'), n8nItems.map((it, i) => {
|
|
111
129
|
const active = i === n8nCursor;
|
|
112
130
|
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));
|
|
113
131
|
}), bot, _jsx(Text, { color: theme.muted, children: "\u2191\u2193 move \u00B7 \u21B5 select \u00B7 esc back" })] }));
|
|
114
132
|
}
|
|
133
|
+
// ---- n8n URL / key editors ----
|
|
115
134
|
if (screen === 'n8n-url' || screen === 'n8n-key') {
|
|
116
135
|
const isKey = screen === 'n8n-key';
|
|
117
136
|
const title = isKey ? 'n8n api key' : 'n8n url';
|
|
@@ -122,22 +141,9 @@ export const Settings = ({ onClose, onModelChange }) => {
|
|
|
122
141
|
};
|
|
123
142
|
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" })] }));
|
|
124
143
|
}
|
|
144
|
+
// ---- n8n test result ----
|
|
125
145
|
if (screen === 'n8n-test') {
|
|
126
|
-
useInput(() => setScreen('n8n'), { isActive: screen === 'n8n-test' });
|
|
127
146
|
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" })] }));
|
|
128
147
|
}
|
|
129
|
-
if (screen === 'reset') {
|
|
130
|
-
return (_jsxs(Box, { flexDirection: "column", children: [top('reset config'), row(_jsx(Text, { color: theme.error, children: "Delete ~/.blyue/.env and reconfigure?" })), row(_jsx(Text, { color: theme.muted, children: "Press Y to confirm, N to cancel." })), bot] }));
|
|
131
|
-
}
|
|
132
|
-
return null;
|
|
133
|
-
};
|
|
134
|
-
// Handle Y/N for reset outside component main body
|
|
135
|
-
export const SettingsResetHandler = ({ onConfirm, onCancel, }) => {
|
|
136
|
-
useInput((input) => {
|
|
137
|
-
if (input === 'y' || input === 'Y')
|
|
138
|
-
onConfirm();
|
|
139
|
-
if (input === 'n' || input === 'N')
|
|
140
|
-
onCancel();
|
|
141
|
-
});
|
|
142
148
|
return null;
|
|
143
149
|
};
|