blyuecode 0.3.4 → 0.3.5
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 +31 -0
- package/dist/core/modelUsage.js +51 -0
- package/dist/ui/Chat.js +8 -0
- package/dist/ui/ModelPicker.js +47 -9
- package/package.json +1 -1
package/dist/core/agent.js
CHANGED
|
@@ -36,9 +36,40 @@ export class Agent {
|
|
|
36
36
|
this.messages = [{ role: 'system', content: SYSTEM_PROMPT }];
|
|
37
37
|
clearSession();
|
|
38
38
|
}
|
|
39
|
+
estimateTokens() {
|
|
40
|
+
let chars = 0;
|
|
41
|
+
for (const m of this.messages) {
|
|
42
|
+
if (typeof m.content === 'string')
|
|
43
|
+
chars += m.content.length;
|
|
44
|
+
if (m.tool_calls)
|
|
45
|
+
chars += JSON.stringify(m.tool_calls).length;
|
|
46
|
+
}
|
|
47
|
+
return Math.ceil(chars / 4);
|
|
48
|
+
}
|
|
49
|
+
async maybeAutoCompact() {
|
|
50
|
+
if (process.env.BLYUE_NO_AUTO_COMPACT === '1')
|
|
51
|
+
return null;
|
|
52
|
+
const limit = Number(process.env.BLYUE_AUTO_COMPACT_TOKENS) || 80_000;
|
|
53
|
+
const est = this.estimateTokens();
|
|
54
|
+
if (est < limit)
|
|
55
|
+
return null;
|
|
56
|
+
if (this.messages.length <= 4)
|
|
57
|
+
return null;
|
|
58
|
+
try {
|
|
59
|
+
const r = await this.compact();
|
|
60
|
+
return { before: r.before, after: r.after };
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
39
66
|
async *run(userInput) {
|
|
40
67
|
this.messages.push({ role: 'user', content: userInput });
|
|
41
68
|
this.persist();
|
|
69
|
+
const compacted = await this.maybeAutoCompact();
|
|
70
|
+
if (compacted) {
|
|
71
|
+
yield { type: 'compact', before: compacted.before, after: compacted.after };
|
|
72
|
+
}
|
|
42
73
|
while (true) {
|
|
43
74
|
const tools = getToolDefs();
|
|
44
75
|
let content = '';
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Tracks which models the user has picked, so the picker can surface a
|
|
2
|
+
// RECENT section. Stored at ~/.blyue/model-usage.json. Bounded — one
|
|
3
|
+
// entry per unique model ever picked.
|
|
4
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
5
|
+
import { homedir } from 'os';
|
|
6
|
+
import { join, dirname } from 'path';
|
|
7
|
+
const filePath = () => join(homedir(), '.blyue', 'model-usage.json');
|
|
8
|
+
let cache = null;
|
|
9
|
+
function load() {
|
|
10
|
+
if (cache)
|
|
11
|
+
return cache;
|
|
12
|
+
const p = filePath();
|
|
13
|
+
if (!existsSync(p)) {
|
|
14
|
+
cache = {};
|
|
15
|
+
return cache;
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
cache = JSON.parse(readFileSync(p, 'utf8'));
|
|
19
|
+
return cache;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
cache = {};
|
|
23
|
+
return cache;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function save(m) {
|
|
27
|
+
const p = filePath();
|
|
28
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
29
|
+
writeFileSync(p, JSON.stringify(m, null, 2), 'utf8');
|
|
30
|
+
cache = m;
|
|
31
|
+
}
|
|
32
|
+
/** Call when the user picks a model (Ctrl+L or /model). */
|
|
33
|
+
export function recordModelUse(id) {
|
|
34
|
+
if (!id)
|
|
35
|
+
return;
|
|
36
|
+
const m = load();
|
|
37
|
+
const prev = m[id] ?? { count: 0, lastUsed: 0 };
|
|
38
|
+
m[id] = { count: prev.count + 1, lastUsed: Date.now() };
|
|
39
|
+
save(m);
|
|
40
|
+
}
|
|
41
|
+
/** Top N most-recently-used model IDs, newest first. */
|
|
42
|
+
export function getRecentModels(limit = 5) {
|
|
43
|
+
const m = load();
|
|
44
|
+
return Object.entries(m)
|
|
45
|
+
.sort(([, a], [, b]) => b.lastUsed - a.lastUsed)
|
|
46
|
+
.slice(0, limit)
|
|
47
|
+
.map(([id]) => id);
|
|
48
|
+
}
|
|
49
|
+
export function clearModelUsage() {
|
|
50
|
+
save({});
|
|
51
|
+
}
|
package/dist/ui/Chat.js
CHANGED
|
@@ -18,6 +18,7 @@ import { pastelAt } from './pastel.js';
|
|
|
18
18
|
import { bumpActivity } from './activity.js';
|
|
19
19
|
import { theme } from './theme.js';
|
|
20
20
|
import { getConfig, saveConfig, applyConfig } from '../core/config.js';
|
|
21
|
+
import { recordModelUse } from '../core/modelUsage.js';
|
|
21
22
|
let nextId = 0;
|
|
22
23
|
const mkItem = (kind, text) => ({ kind, text, id: nextId++ });
|
|
23
24
|
const HistoryLine = ({ item }) => {
|
|
@@ -213,6 +214,12 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
213
214
|
// React to the specific tool
|
|
214
215
|
setMascotState(toolToState(ev.name));
|
|
215
216
|
}
|
|
217
|
+
else if (ev.type === 'compact') {
|
|
218
|
+
setHistory((h) => [
|
|
219
|
+
...h,
|
|
220
|
+
mkItem('tool', `auto-compact: ${ev.before} → ${ev.after} messages`),
|
|
221
|
+
]);
|
|
222
|
+
}
|
|
216
223
|
else if (ev.type === 'tool_end') {
|
|
217
224
|
flush('tool', toolBuf + '[Result]: ' + ev.preview + '\n');
|
|
218
225
|
toolBuf = '';
|
|
@@ -234,6 +241,7 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
234
241
|
setMascotState(undefined);
|
|
235
242
|
};
|
|
236
243
|
const handleModelSelect = (id) => {
|
|
244
|
+
recordModelUse(id);
|
|
237
245
|
process.env.MODEL = id;
|
|
238
246
|
try {
|
|
239
247
|
const cfg = getConfig();
|
package/dist/ui/ModelPicker.js
CHANGED
|
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
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
|
+
import { getRecentModels } from '../core/modelUsage.js';
|
|
5
6
|
import { theme } from './theme.js';
|
|
6
7
|
export const ModelPicker = ({ current, onSelect, onCancel }) => {
|
|
7
8
|
const { stdout } = useStdout();
|
|
@@ -36,20 +37,33 @@ export const ModelPicker = ({ current, onSelect, onCancel }) => {
|
|
|
36
37
|
cancelled = true;
|
|
37
38
|
};
|
|
38
39
|
}, [current]);
|
|
39
|
-
const { free, paid } = useMemo(() => {
|
|
40
|
+
const { recent, free, paid } = useMemo(() => {
|
|
40
41
|
const q = filter.toLowerCase();
|
|
41
42
|
const matched = q
|
|
42
43
|
? models.filter((m) => m.id.toLowerCase().includes(q))
|
|
43
44
|
: models;
|
|
45
|
+
const byId = new Map(matched.map((m) => [m.id, m]));
|
|
46
|
+
const recentIds = getRecentModels(5);
|
|
47
|
+
const recent = [];
|
|
48
|
+
const seen = new Set();
|
|
49
|
+
for (const id of recentIds) {
|
|
50
|
+
const m = byId.get(id);
|
|
51
|
+
if (m && !seen.has(id)) {
|
|
52
|
+
recent.push(m);
|
|
53
|
+
seen.add(id);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
44
56
|
const free = [];
|
|
45
57
|
const paid = [];
|
|
46
58
|
for (const m of matched) {
|
|
59
|
+
if (seen.has(m.id))
|
|
60
|
+
continue; // already in recent
|
|
47
61
|
if (m.isFree)
|
|
48
62
|
free.push(m);
|
|
49
63
|
else
|
|
50
64
|
paid.push(m);
|
|
51
65
|
}
|
|
52
|
-
return { free, paid };
|
|
66
|
+
return { recent, free, paid };
|
|
53
67
|
}, [models, filter]);
|
|
54
68
|
const rows = useMemo(() => {
|
|
55
69
|
const out = [];
|
|
@@ -62,12 +76,23 @@ export const ModelPicker = ({ current, onSelect, onCancel }) => {
|
|
|
62
76
|
out.push({ kind: 'model', model: m, absoluteIdx: abs++ });
|
|
63
77
|
}
|
|
64
78
|
};
|
|
65
|
-
if (tier === '
|
|
79
|
+
if (tier === 'recent') {
|
|
80
|
+
pushSection('RECENT', recent);
|
|
81
|
+
}
|
|
82
|
+
else if (tier === 'free') {
|
|
83
|
+
pushSection('FREE', free);
|
|
84
|
+
}
|
|
85
|
+
else if (tier === 'paid') {
|
|
86
|
+
pushSection('PAID', paid);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
// 'all' — show recent first, then free, then paid
|
|
90
|
+
pushSection('RECENT', recent);
|
|
66
91
|
pushSection('FREE', free);
|
|
67
|
-
if (tier === 'all' || tier === 'paid')
|
|
68
92
|
pushSection('PAID', paid);
|
|
93
|
+
}
|
|
69
94
|
return out;
|
|
70
|
-
}, [free, paid, tier]);
|
|
95
|
+
}, [recent, free, paid, tier]);
|
|
71
96
|
const selectableIdxToRow = [];
|
|
72
97
|
rows.forEach((r, i) => {
|
|
73
98
|
if (r.kind === 'model')
|
|
@@ -87,7 +112,13 @@ export const ModelPicker = ({ current, onSelect, onCancel }) => {
|
|
|
87
112
|
if (key.escape)
|
|
88
113
|
return onCancel();
|
|
89
114
|
if (key.tab) {
|
|
90
|
-
setTier((t) =>
|
|
115
|
+
setTier((t) => t === 'all'
|
|
116
|
+
? 'free'
|
|
117
|
+
: t === 'free'
|
|
118
|
+
? 'paid'
|
|
119
|
+
: t === 'paid'
|
|
120
|
+
? 'recent'
|
|
121
|
+
: 'all');
|
|
91
122
|
setIdx(0);
|
|
92
123
|
return;
|
|
93
124
|
}
|
|
@@ -116,7 +147,7 @@ export const ModelPicker = ({ current, onSelect, onCancel }) => {
|
|
|
116
147
|
}
|
|
117
148
|
});
|
|
118
149
|
const inner = width - 4;
|
|
119
|
-
const total = free.length + paid.length;
|
|
150
|
+
const total = free.length + paid.length + recent.length;
|
|
120
151
|
const headerLabel = filter
|
|
121
152
|
? `filter: ${filter} · ${total} match`
|
|
122
153
|
: `${provider} · ${total} total · ${free.length} free`;
|
|
@@ -128,11 +159,18 @@ export const ModelPicker = ({ current, onSelect, onCancel }) => {
|
|
|
128
159
|
const windowEnd = windowStart + maxVisible;
|
|
129
160
|
const visibleRows = rows.slice(windowStart, windowEnd);
|
|
130
161
|
const cursorAbs = idx;
|
|
131
|
-
const tierBadge = tier === 'all'
|
|
162
|
+
const tierBadge = tier === 'all'
|
|
163
|
+
? 'all'
|
|
164
|
+
: tier === 'free'
|
|
165
|
+
? 'FREE only'
|
|
166
|
+
: tier === 'paid'
|
|
167
|
+
? 'PAID only'
|
|
168
|
+
: 'RECENT only';
|
|
132
169
|
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
170
|
if (row.kind === 'header') {
|
|
134
171
|
const sectionPad = Math.max(1, inner - row.label.length - row.count.toString().length - 5);
|
|
135
|
-
|
|
172
|
+
const headerColor = row.label === 'RECENT' ? theme.warning : theme.accent;
|
|
173
|
+
return (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ ' }), _jsx(Text, { color: headerColor, bold: true, children: row.label }), _jsx(Text, { color: theme.muted, children: ' ' + '─'.repeat(sectionPad) + ' ' + row.count })] }, `h-${row.label}`));
|
|
136
174
|
}
|
|
137
175
|
const m = row.model;
|
|
138
176
|
const active = row.absoluteIdx === cursorAbs;
|