@tianmucreations/jeeves 0.2.0 → 0.3.0
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/README.md +79 -18
- package/bin/jeeves +8 -1
- package/dist/agent/auto-ids.js +66 -0
- package/dist/agent/auto.js +178 -0
- package/dist/agent/context.js +55 -13
- package/dist/agent/errors.js +83 -22
- package/dist/agent/expert-chat.js +33 -0
- package/dist/agent/housekeeping.js +55 -0
- package/dist/agent/loop.js +168 -12
- package/dist/agent/permissions.js +167 -0
- package/dist/agent/research-gate.js +267 -0
- package/dist/agent/review.js +135 -0
- package/dist/agent/spending.js +73 -0
- package/dist/agent/systemPrompt.js +112 -0
- package/dist/app.js +25 -10
- package/dist/checkpoints/index.js +103 -0
- package/dist/checkpoints/store.js +239 -0
- package/dist/commands/address.js +5 -0
- package/dist/commands/clear.js +2 -0
- package/dist/commands/help.js +7 -4
- package/dist/commands/keys.js +1 -1
- package/dist/commands/verbose.js +1 -1
- package/dist/components/AddressPrompt.js +31 -0
- package/dist/components/Footer.js +74 -102
- package/dist/components/Input.js +76 -25
- package/dist/components/KeysManager.js +65 -20
- package/dist/components/ModelPicker.js +348 -75
- package/dist/components/ProjectPicker.js +4 -1
- package/dist/components/Transcript.js +29 -14
- package/dist/components/input-layout.js +34 -0
- package/dist/components/transcript-layout.js +13 -17
- package/dist/index.js +25 -7
- package/dist/ink/AlternateScreen.js +33 -16
- package/dist/ink/cursor.js +18 -0
- package/dist/ink/mouse.js +48 -0
- package/dist/keys/store.js +2 -1
- package/dist/models/registry.js +18 -2
- package/dist/platform/config.js +63 -7
- package/dist/providers/catalogue.js +293 -0
- package/dist/providers/direct-services.js +65 -0
- package/dist/providers/direct.js +145 -0
- package/dist/providers/index.js +123 -13
- package/dist/providers/models-snapshot.js +1037 -0
- package/dist/providers/ollama.js +21 -4
- package/dist/providers/openrouter.js +39 -4
- package/dist/providers/step-control.js +28 -0
- package/dist/providers/zai.js +31 -11
- package/dist/state/session.js +90 -36
- package/dist/state/today-spend.js +26 -0
- package/dist/tools/index.js +118 -10
- package/dist/tools/runBash.js +58 -11
- package/dist/tools/web/htmlToText.js +32 -0
- package/dist/tools/web/openrouterChat.js +31 -0
- package/dist/tools/web/research.js +191 -0
- package/package.json +34 -8
- package/dist/components/AlternateScreen.js +0 -74
|
@@ -5,19 +5,24 @@ import Spinner from 'ink-spinner';
|
|
|
5
5
|
import Fuse from 'fuse.js';
|
|
6
6
|
import { session, useSession } from '../state/session.js';
|
|
7
7
|
import { isToolCapable } from '../models/filter.js';
|
|
8
|
-
import { compactContext, compactPrice, isFastModel, resolveCurated, cleanModelName, } from '../models/registry.js';
|
|
9
|
-
import { setFavorites, setRecents, setDefaultModel, setDefaultProvider } from '../platform/config.js';
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
8
|
+
import { compactContext, compactPrice, isFastModel, resolveCurated, isFreeModel, cleanModelName, } from '../models/registry.js';
|
|
9
|
+
import { setFavorites, setRecents, setDefaultModel, setDefaultProvider, setDailyLimit, hasSavedDailyLimit, getDefaultModel } from '../platform/config.js';
|
|
10
|
+
import { hasCredentialsFor, storeZaiKey, PROVIDER_ROWS, storeOpenRouterKey, refreshCredit, storeDirectKey, storeCustomService, serviceKey, customServiceName, } from '../providers/index.js';
|
|
11
|
+
import { directService, isDirectService, CUSTOM_SERVICE_ID } from '../providers/direct-services.js';
|
|
12
|
+
import { loadDirectModels, checkCustomService } from '../providers/catalogue.js';
|
|
13
|
+
import { autoRowFor, noAutoNote } from '../agent/auto.js';
|
|
14
|
+
import { getCustomService } from '../platform/config.js';
|
|
15
|
+
import { keyLooksValid } from '../commands/keys.js';
|
|
12
16
|
import { listLocalOllamaModels, isOllamaOnline } from '../providers/ollama.js';
|
|
13
17
|
import { ZAI_MODELS } from '../providers/zai.js';
|
|
14
|
-
import {
|
|
15
|
-
const TABS = ['favorites', 'recent', 'all', 'tools'];
|
|
18
|
+
import { isMouseSequence } from '../ink/mouse.js';
|
|
19
|
+
const TABS = ['favorites', 'recent', 'all', 'tools', 'free'];
|
|
16
20
|
const TAB_LABELS = {
|
|
17
21
|
favorites: 'Favorites',
|
|
18
22
|
recent: 'Recent',
|
|
19
23
|
all: 'All',
|
|
20
24
|
tools: 'Tool-capable',
|
|
25
|
+
free: 'Free',
|
|
21
26
|
};
|
|
22
27
|
// The calm first step uses the shared provider list; keys live in the OS keychain (Phase 7).
|
|
23
28
|
const PROVIDER_ORDER = ['openrouter', 'zai', 'anthropic', 'openai', 'google', 'x-ai', 'groq', 'mistral', 'ollama'];
|
|
@@ -28,11 +33,14 @@ const PROVIDER_LABELS = {
|
|
|
28
33
|
openai: 'OpenAI',
|
|
29
34
|
google: 'Google',
|
|
30
35
|
'x-ai': 'xAI',
|
|
36
|
+
xai: 'xAI (Grok)',
|
|
31
37
|
groq: 'Groq',
|
|
32
38
|
mistral: 'Mistral',
|
|
33
39
|
ollama: 'Ollama',
|
|
34
40
|
};
|
|
35
41
|
function providerLabel(provider) {
|
|
42
|
+
if (provider === CUSTOM_SERVICE_ID)
|
|
43
|
+
return customServiceName();
|
|
36
44
|
return PROVIDER_LABELS[provider] ?? provider;
|
|
37
45
|
}
|
|
38
46
|
function groupModels(models) {
|
|
@@ -71,6 +79,9 @@ function poolFor(tab, models, favorites, recents) {
|
|
|
71
79
|
return recents.map((id) => byId.get(id)).filter((m) => m !== undefined);
|
|
72
80
|
if (tab === 'tools')
|
|
73
81
|
return models.filter(isToolCapable);
|
|
82
|
+
// Free models that can do tasks - a free model that can only chat is no use here.
|
|
83
|
+
if (tab === 'free')
|
|
84
|
+
return models.filter((model) => isFreeModel(model) && isToolCapable(model));
|
|
74
85
|
return models;
|
|
75
86
|
}
|
|
76
87
|
function fuzzyMatch(pool, query) {
|
|
@@ -115,6 +126,18 @@ export function ModelPicker({ rows, columns }) {
|
|
|
115
126
|
const [cursor, setCursor] = useState(0);
|
|
116
127
|
const [phase, setPhase] = useState('browse');
|
|
117
128
|
const [pending, setPending] = useState(null);
|
|
129
|
+
const [keyValue, setKeyValue] = useState('');
|
|
130
|
+
// Which service the key prompt is for.
|
|
131
|
+
const [keyFor, setKeyFor] = useState('zai');
|
|
132
|
+
// A direct connection's or the compatible service's models (null while loading).
|
|
133
|
+
const [directModels, setDirectModels] = useState(null);
|
|
134
|
+
const [addressValue, setAddressValue] = useState('');
|
|
135
|
+
// True while a key is being checked with its company.
|
|
136
|
+
const [checking, setChecking] = useState(false);
|
|
137
|
+
const [limitValue, setLimitValue] = useState('');
|
|
138
|
+
const [limitNote, setLimitNote] = useState('');
|
|
139
|
+
const [limitReturn, setLimitReturn] = useState('providers');
|
|
140
|
+
const [keyNote, setKeyNote] = useState('');
|
|
118
141
|
useEffect(() => {
|
|
119
142
|
let cancelled = false;
|
|
120
143
|
void isOllamaOnline().then((online) => {
|
|
@@ -125,8 +148,16 @@ export function ModelPicker({ rows, columns }) {
|
|
|
125
148
|
cancelled = true;
|
|
126
149
|
};
|
|
127
150
|
}, []);
|
|
128
|
-
const catalog = providerChoice === 'ollama'
|
|
129
|
-
|
|
151
|
+
const catalog = providerChoice === 'ollama'
|
|
152
|
+
? (ollamaModels ?? [])
|
|
153
|
+
: providerChoice === 'zai'
|
|
154
|
+
? ZAI_MODELS
|
|
155
|
+
: providerChoice === 'openrouter'
|
|
156
|
+
? s.models
|
|
157
|
+
: (directModels ?? []);
|
|
158
|
+
// A service without Auto says so in one line above its list.
|
|
159
|
+
const autoNote = step === 'full' ? noAutoNote(providerChoice, providerLabel(providerChoice)) : null;
|
|
160
|
+
const listHeight = Math.max(1, rows - 5 - (autoNote ? 1 : 0));
|
|
130
161
|
const items = useMemo(() => {
|
|
131
162
|
if (step === 'curated') {
|
|
132
163
|
const picks = resolveCurated(catalog);
|
|
@@ -135,6 +166,9 @@ export function ModelPicker({ rows, columns }) {
|
|
|
135
166
|
flat.push({ kind: 'model', model: pick.model, blurb: pick.blurb });
|
|
136
167
|
}
|
|
137
168
|
flat.push({ kind: 'header', label: '──────────' }, { kind: 'show-all' });
|
|
169
|
+
const freeCount = catalog.filter((model) => isFreeModel(model) && isToolCapable(model)).length;
|
|
170
|
+
if (freeCount > 0)
|
|
171
|
+
flat.push({ kind: 'show-free', count: freeCount });
|
|
138
172
|
return flat;
|
|
139
173
|
}
|
|
140
174
|
if (step === 'full') {
|
|
@@ -142,7 +176,7 @@ export function ModelPicker({ rows, columns }) {
|
|
|
142
176
|
const matched = fuzzyMatch(pool, query);
|
|
143
177
|
const flat = [{ kind: 'back' }];
|
|
144
178
|
if (providerChoice === 'ollama') {
|
|
145
|
-
flat.push({ kind: 'header', label: 'Ollama (
|
|
179
|
+
flat.push({ kind: 'header', label: 'Ollama (on this computer)' });
|
|
146
180
|
for (const model of matched)
|
|
147
181
|
flat.push({ kind: 'model', model });
|
|
148
182
|
}
|
|
@@ -164,20 +198,30 @@ export function ModelPicker({ rows, columns }) {
|
|
|
164
198
|
const start = Math.max(0, Math.min(items.length - listHeight, resolved - half));
|
|
165
199
|
const visible = items.slice(start, start + listHeight);
|
|
166
200
|
const highlighted = resolved >= 0 && items[resolved]?.kind === 'model' ? items[resolved].model : null;
|
|
201
|
+
// Every row does something when chosen: each service asks for its key right here
|
|
202
|
+
// if it is missing (the compatible service for its address first).
|
|
167
203
|
function providerEnabled(rowId) {
|
|
168
|
-
if (rowId === 'openrouter')
|
|
169
|
-
return hasCredentialsFor('openrouter');
|
|
170
|
-
if (rowId === 'zai')
|
|
171
|
-
return hasCredentialsFor('zai');
|
|
172
204
|
if (rowId === 'ollama')
|
|
173
205
|
return ollamaOnline === true;
|
|
174
|
-
return
|
|
206
|
+
return true;
|
|
175
207
|
}
|
|
176
208
|
function providerHint(rowId) {
|
|
177
209
|
if (rowId === 'ollama') {
|
|
178
210
|
return ollamaOnline === null ? 'checking…' : 'not running - start the Ollama app';
|
|
179
211
|
}
|
|
180
|
-
return '
|
|
212
|
+
return '';
|
|
213
|
+
}
|
|
214
|
+
// The compatible service's row names it once it has been added.
|
|
215
|
+
function rowDescription(row) {
|
|
216
|
+
if (row.id === CUSTOM_SERVICE_ID && hasCredentialsFor(CUSTOM_SERVICE_ID))
|
|
217
|
+
return `${customServiceName()} - your compatible service`;
|
|
218
|
+
return row.description;
|
|
219
|
+
}
|
|
220
|
+
function askForKey(service) {
|
|
221
|
+
setKeyFor(service);
|
|
222
|
+
setKeyValue('');
|
|
223
|
+
setKeyNote('');
|
|
224
|
+
setStep('key');
|
|
181
225
|
}
|
|
182
226
|
function enterModelStep(forProvider) {
|
|
183
227
|
setProviderChoice(forProvider);
|
|
@@ -196,20 +240,61 @@ export function ModelPicker({ rows, columns }) {
|
|
|
196
240
|
}
|
|
197
241
|
setStep(big ? 'curated' : 'full');
|
|
198
242
|
}
|
|
243
|
+
// A direct connection or the compatible service: its models load from the company.
|
|
244
|
+
function enterDirectStep(forProvider) {
|
|
245
|
+
setProviderChoice(forProvider);
|
|
246
|
+
setQuery('');
|
|
247
|
+
setTab('all');
|
|
248
|
+
setCursor(1);
|
|
249
|
+
setDirectModels(null);
|
|
250
|
+
setStep('full');
|
|
251
|
+
const loading = forProvider === CUSTOM_SERVICE_ID
|
|
252
|
+
? checkCustomService(getCustomService()?.baseURL ?? '', serviceKey(CUSTOM_SERVICE_ID) ?? '').then((check) => (check.ok ? check.models : []))
|
|
253
|
+
: isDirectService(forProvider)
|
|
254
|
+
? loadDirectModels(forProvider, serviceKey(forProvider) ?? '').then((models) => {
|
|
255
|
+
// Auto heads the list where the company has it (OpenAI).
|
|
256
|
+
const auto = autoRowFor(forProvider, models);
|
|
257
|
+
return auto ? [auto, ...models] : models;
|
|
258
|
+
})
|
|
259
|
+
: Promise.resolve([]);
|
|
260
|
+
void loading.then(setDirectModels).catch(() => setDirectModels([]));
|
|
261
|
+
}
|
|
199
262
|
function chooseProvider() {
|
|
263
|
+
// The row under the services: the daily spending limit.
|
|
264
|
+
if (providerCursor === PROVIDER_ROWS.length) {
|
|
265
|
+
setLimitReturn('providers');
|
|
266
|
+
setLimitValue('');
|
|
267
|
+
setLimitNote('');
|
|
268
|
+
setStep('limit');
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
200
271
|
const row = PROVIDER_ROWS[providerCursor];
|
|
201
272
|
if (!row)
|
|
202
273
|
return;
|
|
203
274
|
if (row.id === 'openrouter') {
|
|
204
275
|
if (!hasCredentialsFor('openrouter'))
|
|
205
|
-
return;
|
|
276
|
+
return askForKey('openrouter');
|
|
206
277
|
enterModelStep('openrouter');
|
|
207
278
|
}
|
|
208
279
|
else if (row.id === 'zai') {
|
|
209
280
|
if (!hasCredentialsFor('zai'))
|
|
210
|
-
return;
|
|
281
|
+
return askForKey('zai');
|
|
211
282
|
enterModelStep('zai');
|
|
212
283
|
}
|
|
284
|
+
else if (isDirectService(row.id)) {
|
|
285
|
+
if (!hasCredentialsFor(row.id))
|
|
286
|
+
return askForKey(row.id);
|
|
287
|
+
enterDirectStep(row.id);
|
|
288
|
+
}
|
|
289
|
+
else if (row.id === CUSTOM_SERVICE_ID) {
|
|
290
|
+
if (!hasCredentialsFor(CUSTOM_SERVICE_ID)) {
|
|
291
|
+
setAddressValue('');
|
|
292
|
+
setKeyNote('');
|
|
293
|
+
setStep('address');
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
enterDirectStep(CUSTOM_SERVICE_ID);
|
|
297
|
+
}
|
|
213
298
|
else if (row.id === 'ollama') {
|
|
214
299
|
if (ollamaOnline !== true)
|
|
215
300
|
return;
|
|
@@ -220,6 +305,12 @@ export function ModelPicker({ rows, columns }) {
|
|
|
220
305
|
}
|
|
221
306
|
}
|
|
222
307
|
function goBack() {
|
|
308
|
+
if (step === 'key' || step === 'address') {
|
|
309
|
+
setKeyValue('');
|
|
310
|
+
setKeyNote('');
|
|
311
|
+
setStep(step === 'key' && keyFor === CUSTOM_SERVICE_ID ? 'address' : 'providers');
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
223
314
|
if (step === 'full' && providerChoice === 'openrouter') {
|
|
224
315
|
setQuery('');
|
|
225
316
|
setCursor(1);
|
|
@@ -261,15 +352,17 @@ export function ModelPicker({ rows, columns }) {
|
|
|
261
352
|
goBack();
|
|
262
353
|
return;
|
|
263
354
|
}
|
|
264
|
-
if (current.kind === 'show-all') {
|
|
355
|
+
if (current.kind === 'show-all' || current.kind === 'show-free') {
|
|
265
356
|
setStep('full');
|
|
266
357
|
setQuery('');
|
|
267
|
-
setTab('all');
|
|
358
|
+
setTab(current.kind === 'show-free' ? 'free' : 'all');
|
|
268
359
|
setCursor(1);
|
|
269
360
|
return;
|
|
270
361
|
}
|
|
271
362
|
const model = current.model;
|
|
272
|
-
|
|
363
|
+
// Choosing the model already in use just closes - unless nothing was ever chosen,
|
|
364
|
+
// so a first choice of the starting model (Auto) still saves it and sets the daily limit.
|
|
365
|
+
if (model.id === s.model && providerChoice === s.providerId && getDefaultModel() !== null) {
|
|
273
366
|
s.closePicker();
|
|
274
367
|
return;
|
|
275
368
|
}
|
|
@@ -278,56 +371,198 @@ export function ModelPicker({ rows, columns }) {
|
|
|
278
371
|
setPhase('tool-warning');
|
|
279
372
|
return;
|
|
280
373
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
374
|
+
// A switch mid-conversation simply carries the conversation on (context
|
|
375
|
+
// housekeeping keeps it lean; /clear starts afresh). No "Switched to" line:
|
|
376
|
+
// the info bar already names the model.
|
|
377
|
+
applyModel(model);
|
|
378
|
+
finishChoice(model);
|
|
379
|
+
}
|
|
380
|
+
// The first time a model that costs money is chosen, the daily limit is set, so
|
|
381
|
+
// spending is visible in the bar from the first message.
|
|
382
|
+
function finishChoice(model) {
|
|
383
|
+
const paid = providerChoice === 'openrouter' || isDirectService(providerChoice);
|
|
384
|
+
if (paid && !isFreeModel(model) && !hasSavedDailyLimit()) {
|
|
385
|
+
setLimitReturn('close');
|
|
386
|
+
setLimitValue('');
|
|
387
|
+
setLimitNote('');
|
|
388
|
+
setStep('limit');
|
|
284
389
|
return;
|
|
285
390
|
}
|
|
286
|
-
applyModel(model);
|
|
287
|
-
s.addNotice(`Switched to ${model.name}.`);
|
|
288
391
|
s.closePicker();
|
|
289
392
|
}
|
|
290
393
|
useInput((input, key) => {
|
|
394
|
+
if (isMouseSequence(input))
|
|
395
|
+
return;
|
|
291
396
|
if (phase === 'tool-warning') {
|
|
292
397
|
if (key.return) {
|
|
293
398
|
if (pending) {
|
|
294
399
|
applyModel(pending);
|
|
295
|
-
|
|
400
|
+
finishChoice(pending);
|
|
401
|
+
}
|
|
402
|
+
else {
|
|
403
|
+
s.closePicker();
|
|
296
404
|
}
|
|
297
|
-
s.closePicker();
|
|
298
405
|
}
|
|
299
406
|
else if (key.escape) {
|
|
300
407
|
setPhase('browse');
|
|
301
408
|
}
|
|
302
409
|
return;
|
|
303
410
|
}
|
|
304
|
-
if (
|
|
305
|
-
if (
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
411
|
+
if (step === 'address') {
|
|
412
|
+
if (key.escape) {
|
|
413
|
+
goBack();
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
if (key.return) {
|
|
417
|
+
if (addressValue.trim().length < 4) {
|
|
418
|
+
setKeyNote("Paste the service's web address - its documentation gives it - or Esc");
|
|
419
|
+
return;
|
|
309
420
|
}
|
|
310
|
-
|
|
421
|
+
askForKey(CUSTOM_SERVICE_ID);
|
|
422
|
+
return;
|
|
311
423
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
424
|
+
if (key.backspace || key.delete) {
|
|
425
|
+
setAddressValue((v) => v.slice(0, -1));
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
if (!input || key.ctrl || key.meta)
|
|
429
|
+
return;
|
|
430
|
+
setKeyNote('');
|
|
431
|
+
setAddressValue((v) => v + input);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
if (step === 'key') {
|
|
435
|
+
if (checking)
|
|
436
|
+
return;
|
|
437
|
+
if (key.escape) {
|
|
438
|
+
goBack();
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
if (key.return && keyFor === CUSTOM_SERVICE_ID) {
|
|
442
|
+
// A service on this computer may need no key, so an empty key is allowed here.
|
|
443
|
+
setChecking(true);
|
|
444
|
+
setKeyNote(`Checking ${addressValue.trim()}…`);
|
|
445
|
+
void storeCustomService(addressValue, keyValue.trim()).then((result) => {
|
|
446
|
+
setChecking(false);
|
|
447
|
+
setKeyValue('');
|
|
448
|
+
if (result === 'saved' || result === 'saved-unchecked') {
|
|
449
|
+
if (result === 'saved-unchecked') {
|
|
450
|
+
s.addNotice(`${customServiceName()} shows its models to anyone, so the key couldn't be checked yet. If it's wrong, you'll be told on your first message.`);
|
|
451
|
+
}
|
|
452
|
+
setKeyNote('');
|
|
453
|
+
enterDirectStep(CUSTOM_SERVICE_ID);
|
|
454
|
+
}
|
|
455
|
+
else if (result === 'rejected') {
|
|
456
|
+
setKeyNote("The service didn't accept that key - paste it again, or Esc");
|
|
457
|
+
}
|
|
458
|
+
else if (result === 'keychain') {
|
|
459
|
+
setKeyNote('The Mac keychain was not reachable - press Enter and try again.');
|
|
460
|
+
}
|
|
461
|
+
else {
|
|
462
|
+
setStep('address');
|
|
463
|
+
setKeyNote("Couldn't find a compatible service at that address - check it, or Esc");
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
if (key.return && isDirectService(keyFor)) {
|
|
469
|
+
const trimmed = keyValue.trim();
|
|
470
|
+
const label = directService(keyFor).label;
|
|
471
|
+
if (!keyLooksValid(trimmed, keyFor)) {
|
|
472
|
+
setKeyNote('That looks too short to be a key - paste it again, or Esc');
|
|
473
|
+
setKeyValue('');
|
|
474
|
+
return;
|
|
316
475
|
}
|
|
317
|
-
|
|
318
|
-
|
|
476
|
+
setChecking(true);
|
|
477
|
+
setKeyNote(`Checking the key with ${label}…`);
|
|
478
|
+
void storeDirectKey(keyFor, trimmed).then((result) => {
|
|
479
|
+
setChecking(false);
|
|
480
|
+
setKeyValue('');
|
|
481
|
+
if (result === 'rejected') {
|
|
482
|
+
setKeyNote(`${label} didn't accept that key - paste it again, or Esc`);
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
if (result === 'keychain') {
|
|
486
|
+
setKeyNote('The Mac keychain was not reachable - press Enter and try again.');
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
if (result === 'saved-unchecked') {
|
|
490
|
+
s.addNotice(`Your ${label} key is saved, but ${label} couldn't be reached to check it. If it's wrong, you'll be told on your first message.`);
|
|
491
|
+
}
|
|
492
|
+
setKeyNote('');
|
|
493
|
+
if (s.status === 'disconnected')
|
|
494
|
+
s.setStatus('idle');
|
|
495
|
+
enterDirectStep(keyFor);
|
|
496
|
+
});
|
|
497
|
+
return;
|
|
319
498
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
499
|
+
if (key.return) {
|
|
500
|
+
const trimmed = keyValue.trim();
|
|
501
|
+
if (!keyLooksValid(trimmed, keyFor)) {
|
|
502
|
+
setKeyNote(keyFor === 'openrouter'
|
|
503
|
+
? 'Not an OpenRouter key (they start with sk-or-) - paste it again, or Esc'
|
|
504
|
+
: 'That looks too short to be a key - paste it again, or Esc');
|
|
505
|
+
setKeyValue('');
|
|
506
|
+
return;
|
|
324
507
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
508
|
+
const store = keyFor === 'openrouter' ? storeOpenRouterKey : storeZaiKey;
|
|
509
|
+
void store(trimmed).then((saved) => {
|
|
510
|
+
if (!saved) {
|
|
511
|
+
setKeyNote('The Mac keychain was not reachable - press Enter and try again.');
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
if (keyFor === 'zai') {
|
|
515
|
+
enterModelStep('zai');
|
|
516
|
+
}
|
|
517
|
+
else {
|
|
518
|
+
void refreshCredit();
|
|
519
|
+
if (s.status === 'disconnected')
|
|
520
|
+
s.setStatus('idle');
|
|
521
|
+
enterModelStep('openrouter');
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
return;
|
|
328
525
|
}
|
|
329
|
-
|
|
330
|
-
|
|
526
|
+
if (key.backspace || key.delete) {
|
|
527
|
+
setKeyValue((v) => v.slice(0, -1));
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
if (!input || key.ctrl || key.meta)
|
|
531
|
+
return;
|
|
532
|
+
setKeyNote('');
|
|
533
|
+
setKeyValue((v) => v + input);
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
if (step === 'limit') {
|
|
537
|
+
const done = () => (limitReturn === 'close' ? s.closePicker() : setStep('providers'));
|
|
538
|
+
// From a model choice, Esc or an empty Enter keeps the suggested limit.
|
|
539
|
+
if (key.escape || (key.return && limitValue === '' && limitReturn === 'close')) {
|
|
540
|
+
if (limitReturn === 'close') {
|
|
541
|
+
setDailyLimit(s.dailyLimit);
|
|
542
|
+
}
|
|
543
|
+
done();
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
if (key.return) {
|
|
547
|
+
const amount = Number(limitValue.replace(/^\$/, ''));
|
|
548
|
+
if (!Number.isFinite(amount) || amount <= 0 || amount > 1000) {
|
|
549
|
+
setLimitNote('Type an amount in dollars, like 3 or 7.50');
|
|
550
|
+
setLimitValue('');
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
const rounded = Math.round(amount * 100) / 100;
|
|
554
|
+
setDailyLimit(rounded);
|
|
555
|
+
s.setDailyLimit(rounded);
|
|
556
|
+
done();
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
if (key.backspace || key.delete) {
|
|
560
|
+
setLimitValue((v) => v.slice(0, -1));
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
if (/^[0-9.$]$/.test(input)) {
|
|
564
|
+
setLimitNote('');
|
|
565
|
+
setLimitValue((v) => (v + input).slice(0, 8));
|
|
331
566
|
}
|
|
332
567
|
return;
|
|
333
568
|
}
|
|
@@ -341,7 +576,7 @@ export function ModelPicker({ rows, columns }) {
|
|
|
341
576
|
return;
|
|
342
577
|
}
|
|
343
578
|
if (key.downArrow) {
|
|
344
|
-
setProviderCursor((current) => Math.min(PROVIDER_ROWS.length
|
|
579
|
+
setProviderCursor((current) => Math.min(PROVIDER_ROWS.length, current + 1));
|
|
345
580
|
return;
|
|
346
581
|
}
|
|
347
582
|
if (key.return) {
|
|
@@ -390,11 +625,36 @@ export function ModelPicker({ rows, columns }) {
|
|
|
390
625
|
}
|
|
391
626
|
});
|
|
392
627
|
if (step === 'providers') {
|
|
393
|
-
return (_jsxs(Box, { flexDirection: "column", height: rows, children: [_jsx(Text, { dimColor: true, children: "Choose an AI
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
628
|
+
return (_jsxs(Box, { flexDirection: "column", height: rows, children: [_jsx(Text, { dimColor: true, children: "Choose an AI service" }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [PROVIDER_ROWS.map((row, index) => {
|
|
629
|
+
const enabled = providerEnabled(row.id);
|
|
630
|
+
const selected = index === providerCursor;
|
|
631
|
+
return (_jsxs(Text, { inverse: selected, dimColor: !enabled && !selected, children: [column(' ' + row.label, 16), enabled ? _jsx(Text, { dimColor: true, children: rowDescription(row) }) : _jsx(Text, { children: providerHint(row.id) })] }, row.id));
|
|
632
|
+
}), _jsx(Text, { children: " " }), _jsxs(Text, { inverse: providerCursor === PROVIDER_ROWS.length, children: [column(' Daily limit', 16), _jsxs(Text, { dimColor: true, children: ["$", s.dailyLimit.toFixed(2), " a day - Enter to change"] })] })] }), _jsx(Text, { dimColor: true, children: "\u2191\u2193 move \u00B7 Enter choose \u00B7 Esc close" })] }));
|
|
633
|
+
}
|
|
634
|
+
if (step === 'limit') {
|
|
635
|
+
return (_jsxs(Box, { flexDirection: "column", height: rows, children: [_jsx(Text, { dimColor: true, children: "Daily spending limit" }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, justifyContent: "center", children: [_jsxs(Text, { children: [_jsxs(Text, { children: ["Most to spend in a day, in dollars (now $", s.dailyLimit.toFixed(2), "): "] }), _jsx(Text, { children: limitValue }), _jsx(Text, { inverse: true, children: " " })] }), _jsx(Text, { dimColor: true, children: "When today's spending reaches it, Jeeves stops and asks before spending more." })] }), limitNote ? (_jsx(Text, { color: "yellow", children: limitNote })) : (_jsx(Text, { dimColor: true, children: limitReturn === 'close' ? `Enter keeps $${s.dailyLimit.toFixed(2)} · or type an amount, then Enter` : 'type an amount · Enter save · Esc back' }))] }));
|
|
636
|
+
}
|
|
637
|
+
if (step === 'address') {
|
|
638
|
+
return (_jsxs(Box, { flexDirection: "column", height: rows, children: [_jsx(Text, { dimColor: true, children: "Other service \u2014 any service that accepts the OpenAI request format" }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, justifyContent: "center", children: [_jsxs(Text, { children: [_jsx(Text, { children: "Paste the service's web address: " }), _jsx(Text, { children: addressValue }), _jsx(Text, { inverse: true, children: " " })] }), _jsx(Text, { dimColor: true, children: "Its documentation gives it, for example https://api.together.xyz/v1" })] }), keyNote ? _jsx(Text, { color: "yellow", children: keyNote }) : _jsx(Text, { dimColor: true, children: "paste the address \u00B7 Enter next \u00B7 Esc back" })] }));
|
|
639
|
+
}
|
|
640
|
+
if (step === 'key') {
|
|
641
|
+
const direct = directService(keyFor);
|
|
642
|
+
const service = keyFor === 'openrouter' ? 'OpenRouter' : keyFor === 'zai' ? 'Z.ai' : keyFor === CUSTOM_SERVICE_ID ? 'service' : (direct?.label ?? keyFor);
|
|
643
|
+
const title = keyFor === 'openrouter'
|
|
644
|
+
? 'OpenRouter — one key unlocks 400+ models'
|
|
645
|
+
: keyFor === 'zai'
|
|
646
|
+
? 'Z.ai — GLM Coding Plan'
|
|
647
|
+
: keyFor === CUSTOM_SERVICE_ID
|
|
648
|
+
? `Other service — ${addressValue.trim()}`
|
|
649
|
+
: `${service} — a direct connection with your own key`;
|
|
650
|
+
const where = keyFor === 'openrouter'
|
|
651
|
+
? 'Get one at openrouter.ai/settings/keys. '
|
|
652
|
+
: direct
|
|
653
|
+
? `Get one at ${direct.keyPage}. `
|
|
654
|
+
: keyFor === CUSTOM_SERVICE_ID
|
|
655
|
+
? 'No key needed for a service on this computer - just press Enter. '
|
|
656
|
+
: '';
|
|
657
|
+
return (_jsxs(Box, { flexDirection: "column", height: rows, children: [_jsx(Text, { dimColor: true, children: title }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, justifyContent: "center", children: [_jsxs(Text, { children: [_jsxs(Text, { children: ["Paste your ", service, " API key (it stays hidden): "] }), _jsx(Text, { dimColor: true, children: keyValue ? `${keyValue.length} characters ` : '' }), _jsx(Text, { inverse: true, children: " " })] }), _jsxs(Text, { dimColor: true, children: [where, "It is stored in your Mac keychain and never shown again."] })] }), keyNote ? (_jsx(Text, { color: "yellow", children: keyNote })) : (_jsx(Text, { dimColor: true, children: "paste the key \u00B7 Enter save \u00B7 Esc back" }))] }));
|
|
398
658
|
}
|
|
399
659
|
if (step === 'curated') {
|
|
400
660
|
const picks = resolveCurated(catalog);
|
|
@@ -411,35 +671,48 @@ export function ModelPicker({ rows, columns }) {
|
|
|
411
671
|
if (item.kind === 'show-all') {
|
|
412
672
|
return (_jsxs(Text, { inverse: selected, dimColor: !selected, children: ["Show all ", catalog.length, " models \u2192"] }, "showall"));
|
|
413
673
|
}
|
|
674
|
+
if (item.kind === 'show-free') {
|
|
675
|
+
return (_jsxs(Text, { inverse: selected, dimColor: !selected, children: ["Free models (", item.count, ") - no charge, a daily limit on requests \u2192"] }, "showfree"));
|
|
676
|
+
}
|
|
414
677
|
const tools = isToolCapable(item.model) ? '✓' : '✗';
|
|
415
678
|
return (_jsxs(Text, { inverse: selected, children: [' ', cleanModelName(item.model.name), ' ', _jsxs(Text, { dimColor: true, children: ["\u2014 ", compactContext(item.model.contextLength), " ctx \u2014 ", item.blurb ?? '', " \u2014 ", tools, " tools"] })] }, `m${item.model.id}`));
|
|
416
|
-
})) }), _jsx(Text, { dimColor:
|
|
679
|
+
})) }), _jsx(Text, { color: phase === 'tool-warning' ? 'yellow' : undefined, dimColor: phase !== 'tool-warning', children: hint })] }));
|
|
417
680
|
}
|
|
418
681
|
const emptyMessage = providerChoice === 'ollama'
|
|
419
682
|
? ollamaModels === null
|
|
420
|
-
? 'Loading
|
|
683
|
+
? 'Loading the models on this computer…'
|
|
421
684
|
: ollamaModels.length === 0
|
|
422
685
|
? 'Could not reach Ollama - is the app still running?'
|
|
423
686
|
: ''
|
|
424
|
-
:
|
|
425
|
-
?
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
?
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
687
|
+
: providerChoice !== 'openrouter' && providerChoice !== 'zai'
|
|
688
|
+
? directModels === null
|
|
689
|
+
? `Loading the ${providerLabel(providerChoice)} models…`
|
|
690
|
+
: directModels.length === 0
|
|
691
|
+
? `Couldn't load the ${providerLabel(providerChoice)} models - check the internet connection, then open /model again.`
|
|
692
|
+
: items.length <= 1
|
|
693
|
+
? query
|
|
694
|
+
? `No models match "${query}".`
|
|
695
|
+
: 'No models in this list.'
|
|
696
|
+
: ''
|
|
697
|
+
: s.models.length === 0 && s.modelsNote
|
|
698
|
+
? s.modelsNote
|
|
699
|
+
: s.models.length === 0
|
|
700
|
+
? 'Loading the model list…'
|
|
701
|
+
: items.length <= 1
|
|
702
|
+
? query
|
|
703
|
+
? `No models match "${query}".`
|
|
704
|
+
: tab === 'favorites'
|
|
705
|
+
? 'No favorites yet - highlight a model and press +'
|
|
706
|
+
: tab === 'recent'
|
|
707
|
+
? 'No recently used models yet.'
|
|
708
|
+
: 'No models.'
|
|
709
|
+
: '';
|
|
437
710
|
const hint = phase === 'tool-warning'
|
|
438
|
-
? `This model can
|
|
439
|
-
:
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
711
|
+
? `This model can only chat, not do tasks · Enter use anyway · Esc pick another`
|
|
712
|
+
: `Tab list · ↑↓ move · type to search · + favorite · Enter select · Esc back`;
|
|
713
|
+
return (_jsxs(Box, { flexDirection: "column", height: rows, children: [_jsxs(Text, { dimColor: true, children: [providerLabel(providerChoice), " \u2014 all models"] }), _jsx(Box, { children: TABS.map((name) => (_jsxs(React.Fragment, { children: [_jsxs(Text, { inverse: name === tab, dimColor: name !== tab, children: [' ', TAB_LABELS[name], " (", catalog.length && name === 'tools' ? catalog.filter(isToolCapable).length : name === 'all' ? catalog.length : poolFor(name, catalog, s.favorites, s.recents).length, ")", ' '] }), _jsx(Text, { children: " " })] }, name))) }), _jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "search: " }), _jsx(Text, { children: query }), _jsx(Text, { dimColor: true, children: query ? '' : 'type to filter' })] }), _jsx(Text, { dimColor: true, children: tab === 'free'
|
|
714
|
+
? 'free models: no charge, but OpenRouter limits requests per day · » fast'
|
|
715
|
+
: 'memory in tokens (word pieces) · price per million tokens · ✓ tools · » fast' }), autoNote ? _jsx(Text, { color: "yellow", children: autoNote }) : null, _jsx(Box, { flexDirection: "column", height: listHeight, children: emptyMessage ? (_jsxs(Text, { dimColor: true, children: [_jsx(Spinner, { type: "dots" }), " ", emptyMessage] })) : (visible.map((item, index) => {
|
|
443
716
|
const absoluteIndex = start + index;
|
|
444
717
|
if (item.kind === 'header') {
|
|
445
718
|
return (_jsx(Text, { dimColor: true, children: `── ${item.label} ──` }, `h${absoluteIndex}`));
|
|
@@ -448,7 +721,7 @@ export function ModelPicker({ rows, columns }) {
|
|
|
448
721
|
const selected = absoluteIndex === resolved;
|
|
449
722
|
return (_jsx(Text, { inverse: selected, dimColor: !selected, children: backLabel() }, "back"));
|
|
450
723
|
}
|
|
451
|
-
if (item.kind === 'show-all') {
|
|
724
|
+
if (item.kind === 'show-all' || item.kind === 'show-free') {
|
|
452
725
|
return null;
|
|
453
726
|
}
|
|
454
727
|
const selected = absoluteIndex === resolved;
|
|
@@ -9,6 +9,7 @@ import { session, useSession } from '../state/session.js';
|
|
|
9
9
|
import { setRecentProjects } from '../platform/config.js';
|
|
10
10
|
import { homeLocations, listSubfolders, displayPath, projectNameProblem } from '../platform/paths.js';
|
|
11
11
|
import { hasCredentials } from '../providers/index.js';
|
|
12
|
+
import { isMouseSequence } from '../ink/mouse.js';
|
|
12
13
|
// The cursor skips header lines; every other row is selectable.
|
|
13
14
|
function resolveIndex(items, cursor) {
|
|
14
15
|
const start = cursor < 0 ? 0 : cursor;
|
|
@@ -159,6 +160,8 @@ export function ProjectPicker({ rows, columns }) {
|
|
|
159
160
|
return text.length > width ? text.slice(0, width - 1) + '…' : text;
|
|
160
161
|
}
|
|
161
162
|
useInput((input, key) => {
|
|
163
|
+
if (isMouseSequence(input))
|
|
164
|
+
return;
|
|
162
165
|
if (mode === 'create-name') {
|
|
163
166
|
if (key.escape) {
|
|
164
167
|
setMode('list');
|
|
@@ -309,7 +312,7 @@ export function ProjectPicker({ rows, columns }) {
|
|
|
309
312
|
}
|
|
310
313
|
if (item.kind === 'recent') {
|
|
311
314
|
const name = path.basename(item.folder);
|
|
312
|
-
return (_jsxs(Text, { inverse: selected, children: [` ${name}`.padEnd(26), _jsx(Text, { dimColor: true, children: displayPath(item.folder) })] }, `r${item.folder}`));
|
|
315
|
+
return (_jsxs(Text, { inverse: selected, wrap: "truncate-middle", children: [` ${name}`.padEnd(26), _jsx(Text, { dimColor: true, children: displayPath(item.folder) })] }, `r${item.folder}`));
|
|
313
316
|
}
|
|
314
317
|
if (item.kind === 'browse') {
|
|
315
318
|
return (_jsx(Text, { inverse: selected, children: ' Browse for a folder →' }, `b${absoluteIndex}`));
|