@tianmucreations/jeeves 0.3.1 → 0.3.2
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/agent/errors.js +1 -1
- package/dist/agent/loop.js +34 -1
- package/dist/agent/systemPrompt.js +19 -5
- package/dist/app.js +23 -1
- package/dist/checkpoints/index.js +34 -4
- package/dist/commands/clear.js +2 -0
- package/dist/commands/help.js +16 -14
- package/dist/commands/keys.js +2 -1
- package/dist/components/AddressPrompt.js +21 -6
- package/dist/components/Footer.js +6 -1
- package/dist/components/Header.js +5 -1
- package/dist/components/HelpView.js +1 -1
- package/dist/components/Input.js +135 -13
- package/dist/components/KeysManager.js +33 -18
- package/dist/components/ModelPicker.js +27 -8
- package/dist/components/OpenRouterConnect.js +114 -0
- package/dist/components/ProjectPicker.js +55 -16
- package/dist/components/Transcript.js +39 -2
- package/dist/components/input-layout.js +129 -26
- package/dist/components/markdown.js +185 -0
- package/dist/components/transcript-layout.js +50 -2
- package/dist/index.js +2 -1
- package/dist/ink/mouse.js +39 -6
- package/dist/ink/quit.js +21 -0
- package/dist/ink/selection.js +128 -0
- package/dist/keys/store.js +72 -2
- package/dist/platform/address.js +16 -0
- package/dist/platform/chat-folder.js +24 -0
- package/dist/platform/config.js +3 -0
- package/dist/platform/wording.js +10 -0
- package/dist/providers/direct.js +8 -2
- package/dist/providers/index.js +21 -3
- package/dist/providers/ollama.js +8 -2
- package/dist/providers/openrouter-signin.js +102 -0
- package/dist/providers/openrouter.js +17 -2
- package/dist/providers/silence.js +39 -0
- package/dist/providers/zai.js +17 -13
- package/dist/state/session.js +41 -2
- package/dist/tools/index.js +22 -5
- package/dist/tools/runBash.js +17 -5
- package/dist/tools/web/research.js +119 -21
- package/dist/tools/web/zai-search.js +79 -0
- package/package.json +2 -1
package/dist/agent/errors.js
CHANGED
|
@@ -37,7 +37,7 @@ export function plainError(error, providerId) {
|
|
|
37
37
|
const service = serviceName(providerId);
|
|
38
38
|
const make = (message, kind) => ({ message, kind, detail: raw });
|
|
39
39
|
if (/^no .*\bkey\b/.test(text)) {
|
|
40
|
-
return make(
|
|
40
|
+
return make('Jeeves isn\'t connected to an AI service yet - type /keys to connect one. It takes about a minute.', 'auth');
|
|
41
41
|
}
|
|
42
42
|
if (status === 401 ||
|
|
43
43
|
status === 403 ||
|
package/dist/agent/loop.js
CHANGED
|
@@ -11,7 +11,8 @@ import { isToolCapable } from '../models/filter.js';
|
|
|
11
11
|
import { autoCatalogue } from './auto.js';
|
|
12
12
|
import { isAuto, workingModelId, workerModel, expertModel, topModel, AUTO_NOTE, shouldTakeOver, createAskExpertTool, newAutoTurnState, topModelPriceRatio, topModelQuestion, REVIEW_FINISHED_JOBS } from './auto.js';
|
|
13
13
|
import { jobNeedsReview, reviewJob, fixRequest, startReproducing, stopReproducing, UNCHECKED_NOTICE } from './review.js';
|
|
14
|
-
import { requestApproval } from './permissions.js';
|
|
14
|
+
import { requestApproval, hasPendingApproval, answerApproval } from './permissions.js';
|
|
15
|
+
import { killAllRunningCommands } from '../tools/runBash.js';
|
|
15
16
|
import { clearOldToolResults } from './housekeeping.js';
|
|
16
17
|
import { startJob, endJob, reportStepCost, withinLimits } from './spending.js';
|
|
17
18
|
import { getAddress } from '../platform/config.js';
|
|
@@ -26,6 +27,24 @@ Chat-Only Model
|
|
|
26
27
|
|
|
27
28
|
The model currently selected can only chat. For now you have no tools: you cannot read files, write files, list folders, or run commands, whatever the sections above say. If you are asked to do something that needs them, say plainly that the model in use can only chat, and suggest typing /model to choose one that can do tasks. Never pretend to have done it.`;
|
|
28
29
|
const DISCONNECTING = new Set(['auth', 'network', 'payment']);
|
|
30
|
+
// The job now running, so the person can stop it (Claude Code's Esc: the request is
|
|
31
|
+
// cancelled and running commands are closed).
|
|
32
|
+
let currentStop = null;
|
|
33
|
+
// Stops the job now running: the model request, any command it started, any
|
|
34
|
+
// question waiting for an answer, and messages waiting their turn. Returns false
|
|
35
|
+
// when nothing was running.
|
|
36
|
+
export function stopTurn() {
|
|
37
|
+
if (!currentStop || currentStop.signal.aborted)
|
|
38
|
+
return false;
|
|
39
|
+
currentStop.abort();
|
|
40
|
+
killAllRunningCommands();
|
|
41
|
+
while (hasPendingApproval())
|
|
42
|
+
answerApproval(false);
|
|
43
|
+
while (session.takeQueued() !== undefined) {
|
|
44
|
+
// Messages sent while busy are dropped too - "stop" means stop.
|
|
45
|
+
}
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
29
48
|
export async function runTurn(input) {
|
|
30
49
|
if (input.startsWith('/') && input.length > 1 && !input.startsWith('/ ')) {
|
|
31
50
|
if (input === '/help') {
|
|
@@ -37,6 +56,9 @@ export async function runTurn(input) {
|
|
|
37
56
|
else if (input === '/keys') {
|
|
38
57
|
session.openKeys();
|
|
39
58
|
}
|
|
59
|
+
else if (input === '/folder') {
|
|
60
|
+
session.openFolderPicker();
|
|
61
|
+
}
|
|
40
62
|
else if (input === '/verbose') {
|
|
41
63
|
session.addNotice(toggleVerbose());
|
|
42
64
|
}
|
|
@@ -68,6 +90,7 @@ export async function runTurn(input) {
|
|
|
68
90
|
}
|
|
69
91
|
else if (input === '/clear') {
|
|
70
92
|
clearConversation();
|
|
93
|
+
session.addNotice('Started a fresh conversation - the earlier one is cleared.');
|
|
71
94
|
}
|
|
72
95
|
else if (input === '/exit') {
|
|
73
96
|
session.requestExit();
|
|
@@ -102,6 +125,7 @@ export async function runTurn(input) {
|
|
|
102
125
|
const autoState = newAutoTurnState();
|
|
103
126
|
session.setActiveModel(auto ? workerModel() : null);
|
|
104
127
|
const stop = new AbortController();
|
|
128
|
+
currentStop = stop;
|
|
105
129
|
let countedSteps = 0;
|
|
106
130
|
session.beginTurn();
|
|
107
131
|
session.setStatus('working');
|
|
@@ -155,15 +179,18 @@ export async function runTurn(input) {
|
|
|
155
179
|
return { modelId: stepModel, messages: tidied.freedTokens > 0 ? tidied.messages : undefined };
|
|
156
180
|
},
|
|
157
181
|
onToken: (token) => {
|
|
182
|
+
session.setThinking(false);
|
|
158
183
|
if (assistantId === null)
|
|
159
184
|
assistantId = session.startAssistant();
|
|
160
185
|
session.appendToken(assistantId, token);
|
|
161
186
|
},
|
|
162
187
|
onReasoning: (delta) => {
|
|
188
|
+
session.setThinking(true);
|
|
163
189
|
if (session.verbose)
|
|
164
190
|
session.appendReasoning(delta);
|
|
165
191
|
},
|
|
166
192
|
onToolCall: () => {
|
|
193
|
+
session.setThinking(false);
|
|
167
194
|
// Hide pre-tool chatter so only the final answer stays visible (spec 2.3). The
|
|
168
195
|
// entry is removed, not just emptied, so the final answer appears below the
|
|
169
196
|
// actions it reports on rather than above them.
|
|
@@ -219,10 +246,16 @@ export async function runTurn(input) {
|
|
|
219
246
|
reportStepCost(cost);
|
|
220
247
|
session.setPlanResetAt(null);
|
|
221
248
|
session.setActiveModel(auto ? workerModel() : null);
|
|
249
|
+
session.setThinking(false);
|
|
250
|
+
if (currentStop === stop)
|
|
251
|
+
currentStop = null;
|
|
222
252
|
endJob();
|
|
223
253
|
session.setStatus('idle');
|
|
224
254
|
}
|
|
225
255
|
catch (error) {
|
|
256
|
+
session.setThinking(false);
|
|
257
|
+
if (currentStop === stop)
|
|
258
|
+
currentStop = null;
|
|
226
259
|
endJob();
|
|
227
260
|
session.setActiveModel(auto ? workerModel() : null);
|
|
228
261
|
if (stop.signal.aborted) {
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { getAddress } from '../platform/config.js';
|
|
2
|
+
import { partOfDay } from '../platform/address.js';
|
|
2
3
|
// The system prompt is the personality and the rulebook, copied verbatim from the
|
|
3
4
|
// product specification. {{ADDRESS}} is replaced with the user's saved form of
|
|
4
5
|
// address (config key "address", asked once on first launch, changeable via
|
|
5
6
|
// /address); "Sir" is the fallback if none is saved yet.
|
|
6
7
|
export const SYSTEM_PROMPT_TEMPLATE = `Identity
|
|
7
8
|
|
|
8
|
-
You are Jeeves, a gentleman's personal assistant built by Tianmu Creations. You speak with quiet formality, dry wit, and impeccable discretion, in the tradition of P.G. Wodehouse. You are competent, unflappable, and never flustered. You do not use modern slang. You do not use emoji. Your replies are concise and warm, never servile. When you complete a task, you say so plainly and stop. You address the user as {{ADDRESS}}.
|
|
9
|
+
You are Jeeves, a gentleman's personal assistant built by Tianmu Creations. You speak with quiet formality, dry wit, and impeccable discretion, in the tradition of P.G. Wodehouse. Your wit never costs clarity: say the plain fact first. You are competent, unflappable, and never flustered. You do not use modern slang. You do not use emoji. Your replies are concise and warm, never servile. When you complete a task, you say so plainly and stop. You address the user as {{ADDRESS}}.
|
|
9
10
|
|
|
10
11
|
The person using you may have no technical background at all: they describe what they want in ordinary words, and you do the work by reading files, writing files, listing folders, and running shell commands.
|
|
11
12
|
|
|
@@ -65,6 +66,7 @@ You have seven tools: readFile, listDir, writeFile, runBash, webSearch, readWebP
|
|
|
65
66
|
Researching the Web
|
|
66
67
|
|
|
67
68
|
For facts about the outside world — versions, prices, dates, rules, current events, how a product works — research before stating them:
|
|
69
|
+
Today's date is {{TODAY}}. What you learned in training stops well before it, so "this year", "the current season" and "the latest" mean the year of today's date: search for that year by name, and never present an earlier year's results as current.
|
|
68
70
|
1. webSearch to find where to look. Its snippets are not checked facts.
|
|
69
71
|
2. readWebPage on the most official source: the maker's own website, documentation, release list, or registry, in preference to news or blogs.
|
|
70
72
|
3. State the fact only once readWebPage has returned the exact quote, and name the source in a few words.
|
|
@@ -85,8 +87,10 @@ Never use placeholders or guess missing parameters in tool calls.
|
|
|
85
87
|
Complete tasks fully. Do not stop mid-task or leave work incomplete.
|
|
86
88
|
Tone and Style
|
|
87
89
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
+
Write for a person who has not seen your working. {{ADDRESS}} cannot see your thinking or most of what your tools returned — only your words. They do not know names, labels or shorthand you made up along the way (such as "Corner A", "the finalists", "the capacity corner"), so never use them: say what the thing actually is.
|
|
91
|
+
Use complete, plain sentences that can be read once and understood. Never put a metaphor or figure of speech in place of a fact.
|
|
92
|
+
In a longer job, a progress update says in a sentence or two what you found, what it means for {{ADDRESS}}, and what you are doing next.
|
|
93
|
+
Be brief, but clarity comes first: if {{ADDRESS}} would have to read it twice or ask what you meant, it was too short. A simple question gets a short, direct answer.
|
|
90
94
|
Lead with the answer, not the reasoning. Skip filler, preamble, and unnecessary transitions.
|
|
91
95
|
Never say "Let me...", "I'll now...", or "First, I will..." before acting. Just act, then report the result in a sentence or two.
|
|
92
96
|
Do not summarise your own actions. Do not explain your code unless asked.
|
|
@@ -97,14 +101,24 @@ Writing files and running non-read-only commands may ask the user for permission
|
|
|
97
101
|
If the user declines a permission, do not ask again for the same action. Acknowledge it briefly and continue with whatever can still be done.
|
|
98
102
|
Environment
|
|
99
103
|
|
|
104
|
+
Today's date is {{TODAY}}, and it is {{PART_OF_DAY}} (this computer's own date and clock). Greet by that - never guess the time of day.
|
|
100
105
|
The computer is macOS. The working directory is the user's chosen project folder; relative paths refer to it.
|
|
101
106
|
Shell commands run in the user's default shell. Prefer cross-platform-safe commands.
|
|
102
107
|
If a task would be destructive or hard to undo, say so plainly before doing it.
|
|
103
108
|
Professional Objectivity
|
|
104
109
|
|
|
105
110
|
Prioritise technical accuracy over validating the user's beliefs. If the user's approach has a problem, say so plainly and offer the better path.`;
|
|
106
|
-
|
|
107
|
-
|
|
111
|
+
// Today's date on this computer, as Claude Code gives it (constants/common.ts
|
|
112
|
+
// getLocalISODate): the local calendar date, so it is right wherever the person is.
|
|
113
|
+
// Without it, a question about "the season" was answered with last year's (19 Sept).
|
|
114
|
+
export function localISODate(now = new Date()) {
|
|
115
|
+
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
|
|
116
|
+
}
|
|
117
|
+
// The part of the day as well (owner's request, 19 Sept): the model otherwise
|
|
118
|
+
// guessed "Good evening". It changes three times a day, so the prompt stays
|
|
119
|
+
// cacheable in between.
|
|
120
|
+
export function buildSystemPrompt(address, today = localISODate(), dayPart = partOfDay()) {
|
|
121
|
+
return SYSTEM_PROMPT_TEMPLATE.replaceAll('{{ADDRESS}}', address).replaceAll('{{TODAY}}', today).replaceAll('{{PART_OF_DAY}}', dayPart);
|
|
108
122
|
}
|
|
109
123
|
// The address the user saved on first launch; "Sir" until one is saved.
|
|
110
124
|
export function getSystemPrompt() {
|
package/dist/app.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect } from 'react';
|
|
3
|
-
import { Box, Text, useStdout } from 'ink';
|
|
3
|
+
import { Box, Text, useInput, useStdout } from 'ink';
|
|
4
4
|
import { Header } from './components/Header.js';
|
|
5
5
|
import { Transcript } from './components/Transcript.js';
|
|
6
6
|
import { Input, inputRowsFor } from './components/Input.js';
|
|
@@ -16,6 +16,8 @@ import { KeysManager } from './components/KeysManager.js';
|
|
|
16
16
|
import { HelpView } from './components/HelpView.js';
|
|
17
17
|
import { ProjectPicker } from './components/ProjectPicker.js';
|
|
18
18
|
import { AddressPrompt } from './components/AddressPrompt.js';
|
|
19
|
+
import { ENABLE_MOUSE_TRACKING, DISABLE_MOUSE_TRACKING } from './ink/mouse.js';
|
|
20
|
+
import { pressCtrlCToQuit } from './ink/quit.js';
|
|
19
21
|
// The main window: a rounded box border around the top section only, with the
|
|
20
22
|
// info bar on its own row below the box. Top to bottom: plain top border, header
|
|
21
23
|
// row inside the box (Jeeves left, dot right), transcript (flexGrow), internal
|
|
@@ -66,6 +68,26 @@ export function App() {
|
|
|
66
68
|
}
|
|
67
69
|
});
|
|
68
70
|
}, []);
|
|
71
|
+
// The mouse is Jeeves's only on the conversation screen (wheel scrolling, drag to
|
|
72
|
+
// copy). On every other screen - keys, models, help, folders, the first question -
|
|
73
|
+
// it goes back to the terminal, so its own selecting and Cmd+C copy work: a web
|
|
74
|
+
// address such as where to get a key can be copied (owner, 19 Sept: "I still
|
|
75
|
+
// can't copy things").
|
|
76
|
+
const onConversation = !(s.wizardActive || s.keysOpen || s.pickerOpen || s.helpOpen || s.addressOpen || s.launchStage !== 'ready');
|
|
77
|
+
// On the setup screens Ctrl+C is only for quitting - still twice, never at once.
|
|
78
|
+
// (The conversation screen's typing box handles its own.)
|
|
79
|
+
useInput((input, key) => {
|
|
80
|
+
if (key.ctrl && input === 'c' && !onConversation)
|
|
81
|
+
pressCtrlCToQuit();
|
|
82
|
+
});
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
try {
|
|
85
|
+
process.stdout.write(onConversation ? ENABLE_MOUSE_TRACKING : DISABLE_MOUSE_TRACKING);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// A closed stream must never crash the app.
|
|
89
|
+
}
|
|
90
|
+
}, [onConversation]);
|
|
69
91
|
if (s.wizardActive) {
|
|
70
92
|
return _jsx(KeysManager, { mode: "wizard", rows: rows, columns: columns });
|
|
71
93
|
}
|
|
@@ -90,11 +90,41 @@ export function isOutsideProject(target, folder = process.cwd()) {
|
|
|
90
90
|
export function commandMayReachOutside(command, folder = process.cwd()) {
|
|
91
91
|
if (/(^|[\s;|&])sudo\b|\bbrew\s|\s(-g|--global)\b|\bapt(-get)?\s|\bchoco\s|\bwinget\s/.test(command))
|
|
92
92
|
return true;
|
|
93
|
-
if (
|
|
93
|
+
if (/\$HOME|%USERPROFILE%/.test(command))
|
|
94
94
|
return true;
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
95
|
+
// A quoted path is one path however many spaces it holds - a project folder
|
|
96
|
+
// called "Income Streams Research" was read only up to "Income", so every command
|
|
97
|
+
// naming the folder in full counted as outside it and could never be "always
|
|
98
|
+
// allowed" (19 Sept). Quoted text is judged whole: a path is checked as a path,
|
|
99
|
+
// anything else (bash -c "...") as a command of its own. A backslash-escaped
|
|
100
|
+
// space (Income\ Streams) joins a word the same way.
|
|
101
|
+
const quoted = [];
|
|
102
|
+
const rest = command
|
|
103
|
+
.replace(/"([^"]*)"|'([^']*)'/g, (_match, double, single) => {
|
|
104
|
+
quoted.push(double ?? single ?? '');
|
|
105
|
+
return ' ';
|
|
106
|
+
})
|
|
107
|
+
.replace(/\\ /g, '\u0000');
|
|
108
|
+
for (const text of quoted) {
|
|
109
|
+
if (/^~(\/|$)/.test(text))
|
|
110
|
+
return true;
|
|
111
|
+
if (/^(\/|[A-Za-z]:\\|\.\.(\/|\\|$))/.test(text)) {
|
|
112
|
+
if (text !== '/dev/null' && isOutsideProject(text, folder))
|
|
113
|
+
return true;
|
|
114
|
+
// A path that goes on into more command (bash -c "/x/run.sh && rm /y") is also
|
|
115
|
+
// read word by word - a false question is safe, a false "allowed" is not.
|
|
116
|
+
if (/[;&|<>]|\s[-/~.]/.test(text) && commandMayReachOutside(text, folder))
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
else if (text.trim() && commandMayReachOutside(text, folder)) {
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (/(^|[\s=])~(\/|\s|$)|(^|[\s=/])\.\.(\/|\\|\s|$)/.test(rest))
|
|
124
|
+
return true;
|
|
125
|
+
for (const match of rest.matchAll(/(?:^|[\s=])((?:\/|[A-Za-z]:\\)\S*)/g)) {
|
|
126
|
+
const candidate = match[1].replace(/\u0000/g, ' ');
|
|
127
|
+
if (candidate === '/dev/null')
|
|
98
128
|
continue;
|
|
99
129
|
if (isOutsideProject(candidate, folder))
|
|
100
130
|
return true;
|
package/dist/commands/clear.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { session } from '../state/session.js';
|
|
2
2
|
import { resetStickySession } from '../providers/openrouter.js';
|
|
3
3
|
import { resetResearchGate } from '../agent/research-gate.js';
|
|
4
|
+
import { resetBorrowedSearch } from '../tools/web/research.js';
|
|
4
5
|
// Starts fresh: wipes the screen and the conversation the model remembers.
|
|
5
6
|
export function clearConversation() {
|
|
6
7
|
session.clearTranscript();
|
|
@@ -8,4 +9,5 @@ export function clearConversation() {
|
|
|
8
9
|
session.setLastReasoning('');
|
|
9
10
|
resetStickySession();
|
|
10
11
|
resetResearchGate();
|
|
12
|
+
resetBorrowedSearch();
|
|
11
13
|
}
|
package/dist/commands/help.js
CHANGED
|
@@ -1,21 +1,23 @@
|
|
|
1
|
+
import { WORD_JUMP_KEYS } from '../platform/wording.js';
|
|
1
2
|
export const COMMANDS = [
|
|
2
3
|
{ command: '/help', description: 'show this list' },
|
|
3
|
-
{ command: '/model', description: '
|
|
4
|
-
{ command: '/keys', description: '
|
|
4
|
+
{ command: '/model', description: 'choose the AI service and model' },
|
|
5
|
+
{ command: '/keys', description: 'connect an AI service, or remove one' },
|
|
6
|
+
{ command: '/folder', description: 'work in a different folder, or just chat' },
|
|
7
|
+
{ command: '/undo', description: "put the folder back to how it was before Jeeves's last change" },
|
|
8
|
+
{ command: '/ask', description: 'ask before every change again (after "always allow")' },
|
|
9
|
+
{ command: '/clear', description: 'start a fresh conversation' },
|
|
5
10
|
{ command: '/address', description: 'change how Jeeves addresses you' },
|
|
6
|
-
{ command: '/verbose', description:
|
|
7
|
-
{ command: '/undo', description: 'put the project folder back to how it was before the last change' },
|
|
8
|
-
{ command: '/ask', description: 'ask before every change in this project folder again (after "always allow")' },
|
|
9
|
-
{ command: '/clear', description: 'start a fresh conversation and clear the screen' },
|
|
11
|
+
{ command: '/verbose', description: 'show technical details as well (for curious people)' },
|
|
10
12
|
{ command: '/exit', description: 'quit' },
|
|
11
13
|
];
|
|
12
14
|
export const KEY_BINDINGS = [
|
|
13
|
-
{ command: '
|
|
14
|
-
{ command: '
|
|
15
|
-
{ command: '
|
|
16
|
-
{ command: '
|
|
17
|
-
{ command: '
|
|
18
|
-
{ command: '
|
|
19
|
-
{ command: '
|
|
20
|
-
{ command: '
|
|
15
|
+
{ command: 'Enter', description: 'send your message, or choose in a list' },
|
|
16
|
+
{ command: 'Esc', description: 'stop what Jeeves is doing, or go back' },
|
|
17
|
+
{ command: 'Ctrl+C', description: 'clear what you are typing or stop Jeeves; press it twice to quit' },
|
|
18
|
+
{ command: 'y / a / n', description: 'answer a question: yes, always allow in this folder, or no' },
|
|
19
|
+
{ command: '↑ ↓', description: 'move in a list, or scroll the conversation; Page Up / Page Down jump a whole screen; End returns to the newest' },
|
|
20
|
+
{ command: '← →', description: `move the cursor in what you are typing; ${WORD_JUMP_KEYS} jump a word; Ctrl+A / Ctrl+E go to the start / end; or click where you want it` },
|
|
21
|
+
{ command: 'copying', description: 'drag over any text - it is copied when you let go' },
|
|
22
|
+
{ command: 'Ctrl+R', description: 'show the notes Jeeves made while thinking about the last answer' },
|
|
21
23
|
];
|
package/dist/commands/keys.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { KEY_STORE } from '../platform/wording.js';
|
|
1
2
|
// Plain-English key helpers; the interactive screens live in KeysManager.
|
|
2
3
|
export function keyLooksValid(key, provider) {
|
|
3
4
|
const trimmed = key.trim();
|
|
@@ -8,7 +9,7 @@ export function keyLooksValid(key, provider) {
|
|
|
8
9
|
}
|
|
9
10
|
export function describeKeySource(source) {
|
|
10
11
|
if (source === 'keychain')
|
|
11
|
-
return
|
|
12
|
+
return `key stored in ${KEY_STORE}`;
|
|
12
13
|
if (source === 'env')
|
|
13
14
|
return 'key in a local file - add it with /keys to store it safely';
|
|
14
15
|
return 'no key';
|
|
@@ -4,19 +4,33 @@ import { Box, Text, useInput } from 'ink';
|
|
|
4
4
|
import { session } from '../state/session.js';
|
|
5
5
|
import { getAddress, setAddress } from '../platform/config.js';
|
|
6
6
|
import { isMouseSequence } from '../ink/mouse.js';
|
|
7
|
+
import { cleanAddress, timeOfDayGreeting, ADDRESS_MAX } from '../platform/address.js';
|
|
8
|
+
import { splitTypedBurst } from './input-layout.js';
|
|
7
9
|
// The first-launch question, in Jeeves' own voice, asked once before the project
|
|
8
10
|
// picker whenever no address is saved; /address reopens the same screen later.
|
|
9
11
|
export function AddressPrompt({ rows }) {
|
|
10
12
|
const [value, setValue] = useState('');
|
|
13
|
+
const [note, setNote] = useState('');
|
|
11
14
|
const firstRun = session.launchStage === 'address';
|
|
12
15
|
useInput((input, key) => {
|
|
13
16
|
if (isMouseSequence(input))
|
|
14
17
|
return;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
// Typing and Enter can arrive together (as in the main typing box, 18 Sept):
|
|
19
|
+
// the text before the line break is typed text, the break is Enter.
|
|
20
|
+
const burst = key.return ? { typed: '', enter: true } : splitTypedBurst(input);
|
|
21
|
+
if (burst.enter) {
|
|
22
|
+
const typedValue = (value + burst.typed).slice(0, ADDRESS_MAX);
|
|
23
|
+
// Nothing typed keeps the current address; on the first run there is none to
|
|
24
|
+
// keep, so Jeeves asks rather than guessing "Sir".
|
|
25
|
+
const address = cleanAddress(typedValue) ?? getAddress();
|
|
26
|
+
if (!address) {
|
|
27
|
+
setValue(typedValue);
|
|
28
|
+
setNote("Type Sir, Ma'am, your name, or whatever you'd like to be called.");
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
setAddress(address);
|
|
18
32
|
session.addressDone();
|
|
19
|
-
session.addNotice(`Very good - I shall address you as ${address
|
|
33
|
+
session.addNotice(`Very good - I shall address you as ${address}.`);
|
|
20
34
|
return;
|
|
21
35
|
}
|
|
22
36
|
if (key.backspace || key.delete) {
|
|
@@ -25,7 +39,8 @@ export function AddressPrompt({ rows }) {
|
|
|
25
39
|
}
|
|
26
40
|
if (!input || key.ctrl || key.meta)
|
|
27
41
|
return;
|
|
28
|
-
|
|
42
|
+
setNote('');
|
|
43
|
+
setValue((v) => (v.length >= ADDRESS_MAX ? v : v + input));
|
|
29
44
|
});
|
|
30
|
-
return (_jsxs(Box, { flexDirection: "column", height: rows, children: [
|
|
45
|
+
return (_jsxs(Box, { flexDirection: "column", height: rows, children: [_jsxs(Text, { dimColor: true, children: [timeOfDayGreeting(), ". Before we begin \u2014 how shall I address you? Sir, Ma'am, or something else?"] }), _jsx(Box, { flexGrow: 1, justifyContent: "center", flexDirection: "column", minHeight: 1, children: _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "Call me: " }), _jsx(Text, { children: value }), _jsx(Text, { inverse: true, children: " " })] }) }), note ? _jsx(Text, { color: "yellow", children: note }) : null, firstRun ? _jsx(Text, { dimColor: true, children: "type Sir, Ma'am or a name \u00B7 Enter save" }) : _jsx(Text, { dimColor: true, children: "type a new title \u00B7 Enter save \u00B7 unchanged keeps the current one" })] }));
|
|
31
46
|
}
|
|
@@ -5,6 +5,7 @@ import { useSession } from '../state/session.js';
|
|
|
5
5
|
import { allowanceToday } from '../agent/spending.js';
|
|
6
6
|
import { isAuto, workerModel } from '../agent/auto.js';
|
|
7
7
|
import { isDirectService, CUSTOM_SERVICE_ID } from '../providers/direct-services.js';
|
|
8
|
+
import { hasCredentials } from '../providers/index.js';
|
|
8
9
|
export function shortModelName(model) {
|
|
9
10
|
const short = model.split('/').pop();
|
|
10
11
|
return short && short.length > 0 ? short : model;
|
|
@@ -14,6 +15,8 @@ const money = (value) => `$${value.toFixed(2)}`;
|
|
|
14
15
|
// and on the right what today has cost and what is left - or, for a flat-rate
|
|
15
16
|
// plan, whether it has allowance. Warnings appear only when they matter.
|
|
16
17
|
export function footerSegments(info) {
|
|
18
|
+
if (info.connected === false)
|
|
19
|
+
return [{ text: `not connected - ${info.connectHint ?? 'type /keys to connect'}`, color: 'yellow' }];
|
|
17
20
|
const busy = info.busyNote ?? (info.tidying ? 'tidying up…' : null);
|
|
18
21
|
const direct = isDirectService(info.providerId);
|
|
19
22
|
if (busy && info.providerId !== 'openrouter' && !direct)
|
|
@@ -72,11 +75,13 @@ export function Footer() {
|
|
|
72
75
|
providerId: s.providerId,
|
|
73
76
|
allowance: allowanceToday(),
|
|
74
77
|
tidying: s.tidying,
|
|
75
|
-
busyNote: s.busyNote,
|
|
78
|
+
busyNote: s.busyNote ?? (s.thinkingSince !== null ? 'thinking…' : null),
|
|
76
79
|
todaySpend: s.todaySpend,
|
|
77
80
|
creditRemaining: s.creditRemaining,
|
|
78
81
|
creditIsAccount: s.creditIsAccount,
|
|
79
82
|
planResetAt: s.planResetAt,
|
|
83
|
+
// Whether a service is set up at all - not the brief "disconnected" of an internet drop.
|
|
84
|
+
connected: hasCredentials(),
|
|
80
85
|
});
|
|
81
86
|
const rightWidth = segments.reduce((sum, segment) => sum + segment.text.length, 0) + 3 * (segments.length - 1);
|
|
82
87
|
// In Auto mode the bar names the model actually working: "auto · deepseek-v4-flash-0731".
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
3
|
import { TrafficLight } from './TrafficLight.js';
|
|
4
|
+
// The name in Tianmu Creations gold (tianmucreations.com's --gold), as in the desktop
|
|
5
|
+
// window. A terminal keeps its own font, so only the colour can match; Terminal.app
|
|
6
|
+
// shows the nearest of its 256 colours.
|
|
7
|
+
export const BRAND_GOLD = '#c9a96a';
|
|
4
8
|
export function Header() {
|
|
5
|
-
return (_jsxs(Box, { justifyContent: "space-between", children: [_jsx(Text, { color:
|
|
9
|
+
return (_jsxs(Box, { justifyContent: "space-between", children: [_jsx(Text, { color: BRAND_GOLD, bold: true, children: "Jeeves" }), _jsx(TrafficLight, {})] }));
|
|
6
10
|
}
|
|
@@ -10,5 +10,5 @@ export function HelpView({ rows }) {
|
|
|
10
10
|
s.closeHelp();
|
|
11
11
|
}
|
|
12
12
|
});
|
|
13
|
-
return (_jsxs(Box, { flexDirection: "column", height: rows, children: [_jsx(Text, { dimColor: true, children: "Help - what you can type" }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, justifyContent: "center", children: [_jsx(Text, { children: "Commands" }), COMMANDS.map((entry) => (_jsxs(
|
|
13
|
+
return (_jsxs(Box, { flexDirection: "column", height: rows, children: [_jsx(Text, { dimColor: true, children: "Help - what you can type" }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, justifyContent: "center", children: [_jsx(Text, { children: "Commands" }), COMMANDS.map((entry) => (_jsxs(Box, { children: [_jsx(Box, { width: 11, flexShrink: 0, children: _jsx(Text, { children: ' ' + entry.command }) }), _jsx(Text, { dimColor: true, wrap: "wrap", children: entry.description })] }, entry.command))), _jsx(Text, { children: " " }), _jsx(Text, { children: "Keys" }), KEY_BINDINGS.map((entry) => (_jsxs(Box, { children: [_jsx(Box, { width: 11, flexShrink: 0, children: _jsx(Text, { children: ' ' + entry.command }) }), _jsx(Text, { dimColor: true, wrap: "wrap", children: entry.description })] }, entry.command))), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "Type anything else in plain English and press Enter - that's all you need." })] }), _jsx(Text, { dimColor: true, children: "Esc close" })] }));
|
|
14
14
|
}
|