blyuecode 0.3.0 → 0.3.1

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.
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState, useEffect } from 'react';
2
+ import { useState, useEffect, useMemo } from 'react';
3
3
  import { Box, Text, useInput, useStdout } from 'ink';
4
4
  import { fetchModels, currentProviderLabel, } from '../core/models.js';
5
5
  import { theme } from './theme.js';
@@ -12,6 +12,7 @@ export const ModelPicker = ({ current, onSelect, onCancel }) => {
12
12
  const [error, setError] = useState(null);
13
13
  const [idx, setIdx] = useState(0);
14
14
  const [filter, setFilter] = useState('');
15
+ const [tier, setTier] = useState('all');
15
16
  useEffect(() => {
16
17
  let cancelled = false;
17
18
  setLoading(true);
@@ -35,48 +36,121 @@ export const ModelPicker = ({ current, onSelect, onCancel }) => {
35
36
  cancelled = true;
36
37
  };
37
38
  }, [current]);
38
- const filtered = filter
39
- ? models.filter((m) => m.id.toLowerCase().includes(filter.toLowerCase()))
40
- : models;
39
+ const { free, paid } = useMemo(() => {
40
+ const q = filter.toLowerCase();
41
+ const matched = q
42
+ ? models.filter((m) => m.id.toLowerCase().includes(q))
43
+ : models;
44
+ const free = [];
45
+ const paid = [];
46
+ for (const m of matched) {
47
+ if (m.isFree)
48
+ free.push(m);
49
+ else
50
+ paid.push(m);
51
+ }
52
+ return { free, paid };
53
+ }, [models, filter]);
54
+ const rows = useMemo(() => {
55
+ const out = [];
56
+ let abs = 0;
57
+ const pushSection = (label, list) => {
58
+ if (list.length === 0)
59
+ return;
60
+ out.push({ kind: 'header', label, count: list.length });
61
+ for (const m of list) {
62
+ out.push({ kind: 'model', model: m, absoluteIdx: abs++ });
63
+ }
64
+ };
65
+ if (tier === 'all' || tier === 'free')
66
+ pushSection('FREE', free);
67
+ if (tier === 'all' || tier === 'paid')
68
+ pushSection('PAID', paid);
69
+ return out;
70
+ }, [free, paid, tier]);
71
+ const selectableIdxToRow = [];
72
+ rows.forEach((r, i) => {
73
+ if (r.kind === 'model')
74
+ selectableIdxToRow.push(i);
75
+ });
76
+ const totalSelectable = selectableIdxToRow.length;
77
+ useEffect(() => {
78
+ if (totalSelectable === 0) {
79
+ if (idx !== 0)
80
+ setIdx(0);
81
+ return;
82
+ }
83
+ if (idx >= totalSelectable)
84
+ setIdx(totalSelectable - 1);
85
+ }, [totalSelectable, idx]);
41
86
  useInput((input, key) => {
42
87
  if (key.escape)
43
88
  return onCancel();
44
- if (key.upArrow && filtered.length)
45
- setIdx((i) => (i - 1 + filtered.length) % filtered.length);
46
- if (key.downArrow && filtered.length)
47
- setIdx((i) => (i + 1) % filtered.length);
48
- if (key.return && filtered[idx])
49
- onSelect(filtered[idx].id);
50
- if (key.backspace || key.delete)
89
+ if (key.tab) {
90
+ setTier((t) => (t === 'all' ? 'free' : t === 'free' ? 'paid' : 'all'));
91
+ setIdx(0);
92
+ return;
93
+ }
94
+ if (key.upArrow && totalSelectable)
95
+ setIdx((i) => (i - 1 + totalSelectable) % totalSelectable);
96
+ if (key.downArrow && totalSelectable)
97
+ setIdx((i) => (i + 1) % totalSelectable);
98
+ if (key.return) {
99
+ const rowI = selectableIdxToRow[idx];
100
+ const row = rowI !== undefined ? rows[rowI] : undefined;
101
+ if (row && row.kind === 'model')
102
+ onSelect(row.model.id);
103
+ return;
104
+ }
105
+ if (key.backspace || key.delete) {
51
106
  setFilter((f) => f.slice(0, -1));
107
+ setIdx(0);
108
+ return;
109
+ }
52
110
  if (input && !key.ctrl && !key.meta && !key.upArrow && !key.downArrow) {
53
111
  const clean = input.replace(/[\x00-\x1F\x7F]/g, '');
54
- if (clean)
112
+ if (clean) {
55
113
  setFilter((f) => f + clean);
114
+ setIdx(0);
115
+ }
56
116
  }
57
117
  });
58
118
  const inner = width - 4;
59
- const maxVisible = 12;
60
- const start = Math.max(0, Math.min(idx - 6, Math.max(0, filtered.length - maxVisible)));
61
- const visible = filtered.slice(start, start + maxVisible);
62
- const label = filter ? `filter: ${filter}` : `${provider} models`;
63
- const topPad = Math.max(1, inner - label.length - 4);
64
- return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '╭─ ' }), _jsx(Text, { color: theme.accent, bold: true, children: label }), _jsx(Text, { color: theme.muted, children: ' ' + '─'.repeat(topPad) + '╮' })] }), loading ? (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ ' }), _jsxs(Text, { color: theme.muted, children: ["fetching ", provider, " catalog\u2026"] })] })) : error ? (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ ' }), _jsxs(Text, { color: theme.error, children: ["\u2717 ", error.slice(0, inner - 6)] })] })) : visible.length === 0 ? (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ ' }), _jsx(Text, { color: theme.error, children: "no matches" })] })) : (visible.map((m, i) => {
65
- const realIdx = start + i;
66
- const active = realIdx === idx;
119
+ const total = free.length + paid.length;
120
+ const headerLabel = filter
121
+ ? `filter: ${filter} · ${total} match`
122
+ : `${provider} · ${total} total · ${free.length} free`;
123
+ const topPad = Math.max(1, inner - headerLabel.length - 4);
124
+ const maxVisible = 14;
125
+ const cursorRowI = selectableIdxToRow[idx] ?? 0;
126
+ let windowStart = Math.max(0, cursorRowI - Math.floor(maxVisible / 2));
127
+ windowStart = Math.min(windowStart, Math.max(0, rows.length - maxVisible));
128
+ const windowEnd = windowStart + maxVisible;
129
+ const visibleRows = rows.slice(windowStart, windowEnd);
130
+ const cursorAbs = idx;
131
+ const tierBadge = tier === 'all' ? 'all' : tier === 'free' ? 'FREE only' : 'PAID only';
132
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '╭─ ' }), _jsx(Text, { color: theme.accent, bold: true, children: headerLabel }), _jsx(Text, { color: theme.muted, children: ' ' + '─'.repeat(topPad) + '╮' })] }), loading ? (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ ' }), _jsxs(Text, { color: theme.muted, children: ["fetching ", provider, " catalog\u2026"] })] })) : error ? (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ ' }), _jsxs(Text, { color: theme.error, children: ["\u2717 ", error.slice(0, inner - 6)] })] })) : rows.length === 0 ? (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ ' }), _jsxs(Text, { color: theme.error, children: ["no matches", tier !== 'all' ? ` in ${tierBadge}` : ''] })] })) : (visibleRows.map((row, i) => {
133
+ if (row.kind === 'header') {
134
+ const sectionPad = Math.max(1, inner - row.label.length - row.count.toString().length - 5);
135
+ return (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ ' }), _jsx(Text, { color: theme.accent, bold: true, children: row.label }), _jsx(Text, { color: theme.muted, children: ' ' + '─'.repeat(sectionPad) + ' ' + row.count })] }, `h-${row.label}`));
136
+ }
137
+ const m = row.model;
138
+ const active = row.absoluteIdx === cursorAbs;
67
139
  const isCurrent = m.id === current;
68
140
  const ctx = m.contextLength
69
141
  ? m.contextLength >= 1000
70
142
  ? `${Math.round(m.contextLength / 1000)}k`
71
143
  : `${m.contextLength}`
72
144
  : '';
73
- const freeTag = m.isFree ? 'FREE' : '';
74
- // Right side: ctx + free badge
75
- const right = [ctx, freeTag].filter(Boolean).join(' · ');
76
- const label0 = m.id.length > inner - 10
77
- ? m.id.slice(0, inner - 13) + '…'
78
- : m.id;
79
- const pad = Math.max(1, inner - label0.length - right.length - 4);
80
- return (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ ' }), _jsx(Text, { color: active ? theme.accent : theme.muted, bold: active, children: active ? '❯ ' : ' ' }), _jsx(Text, { color: active ? theme.text : theme.muted, bold: active, children: label0 }), _jsx(Text, { color: theme.muted, children: ' '.repeat(pad) }), _jsx(Text, { color: theme.muted, children: right }), isCurrent && _jsx(Text, { color: theme.accent, children: " \u2713" })] }, m.id));
81
- })), _jsx(Text, { children: _jsx(Text, { color: theme.muted, children: '╰' + '─'.repeat(inner) + '╯' }) }), _jsx(Text, { color: theme.muted, children: "\u2191\u2193 move \u00B7 type to filter \u00B7 \u21B5 select \u00B7 esc cancel" })] }));
145
+ const price = m.isFree
146
+ ? 'FREE'
147
+ : m.pricing
148
+ ? `$${(m.pricing.prompt * 1_000_000).toFixed(2)}/M`
149
+ : '';
150
+ const right = [ctx, price].filter(Boolean).join(' · ');
151
+ const maxName = Math.max(8, inner - right.length - 6);
152
+ const name = m.id.length > maxName ? m.id.slice(0, maxName - 1) + '…' : m.id;
153
+ const pad = Math.max(1, inner - name.length - right.length - 4);
154
+ return (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ ' }), _jsx(Text, { color: active ? theme.accent : theme.muted, bold: active, children: active ? '❯ ' : ' ' }), _jsx(Text, { color: active ? theme.text : theme.muted, bold: active, children: name }), _jsx(Text, { color: theme.muted, children: ' '.repeat(pad) }), _jsx(Text, { color: m.isFree ? theme.success : theme.muted, children: right }), isCurrent && _jsx(Text, { color: theme.accent, children: " \u2713" })] }, `m-${m.id}`));
155
+ })), _jsx(Text, { children: _jsx(Text, { color: theme.muted, children: '╰' + '─'.repeat(inner) + '╯' }) }), _jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: " \u2191\u2193 move \u00B7 " }), _jsx(Text, { color: theme.accent, children: "tab" }), _jsxs(Text, { color: theme.muted, children: [" tier (", tierBadge, ") \u00B7 "] }), _jsx(Text, { color: theme.muted, children: "type to filter \u00B7 " }), _jsx(Text, { color: theme.accent, children: "\u21B5" }), _jsx(Text, { color: theme.muted, children: " select \u00B7 " }), _jsx(Text, { color: theme.accent, children: "esc" }), _jsx(Text, { color: theme.muted, children: " cancel" })] })] }));
82
156
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blyuecode",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "BLYUE Code — a coding agent by Lumen",
5
5
  "type": "module",
6
6
  "license": "MIT",