@zenera/cli 1.1.25 → 1.1.27
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 +13 -1
- package/dist/catalog.d.ts +20 -0
- package/dist/catalog.js +60 -0
- package/dist/commands/index.js +2 -0
- package/dist/commands/inspect.d.ts +5 -1
- package/dist/commands/inspect.js +54 -14
- package/dist/commands/memory.js +28 -3
- package/dist/commands/meta.js +24 -5
- package/dist/commands/models.js +2 -1
- package/dist/commands/sandbox.js +73 -3
- package/dist/commands/view.d.ts +8 -0
- package/dist/commands/view.js +114 -0
- package/dist/engine.d.ts +3 -1
- package/dist/engine.js +6 -0
- package/dist/eventlog.d.ts +5 -0
- package/dist/eventlog.js +25 -1
- package/dist/liveness.js +20 -2
- package/dist/main.js +0 -0
- package/dist/meta/editor.d.ts +1 -1
- package/dist/meta/editor.js +4 -21
- package/dist/meta/finetune/analyze-brief.d.ts +26 -0
- package/dist/meta/finetune/analyze-brief.js +131 -0
- package/dist/meta/finetune/apply-brief.d.ts +23 -0
- package/dist/meta/finetune/apply-brief.js +247 -0
- package/dist/meta/finetune/brief.d.ts +45 -0
- package/dist/meta/finetune/brief.js +158 -0
- package/dist/meta/finetune/command.d.ts +1 -1
- package/dist/meta/finetune/command.js +224 -46
- package/dist/meta/finetune/cost.d.ts +46 -0
- package/dist/meta/finetune/cost.js +105 -0
- package/dist/meta/finetune/dataset/types.d.ts +3 -1
- package/dist/meta/finetune/feedback.d.ts +14 -2
- package/dist/meta/finetune/feedback.js +22 -3
- package/dist/meta/finetune/guard.d.ts +29 -0
- package/dist/meta/finetune/guard.js +104 -0
- package/dist/meta/finetune/improvements.d.ts +50 -3
- package/dist/meta/finetune/improvements.js +290 -33
- package/dist/meta/finetune/live.d.ts +17 -7
- package/dist/meta/finetune/live.js +56 -10
- package/dist/meta/finetune/loop.js +59 -10
- package/dist/meta/finetune/recheck.d.ts +58 -0
- package/dist/meta/finetune/recheck.js +311 -0
- package/dist/meta/finetune/report.d.ts +9 -2
- package/dist/meta/finetune/report.js +551 -438
- package/dist/meta/finetune/sampler.js +8 -2
- package/dist/meta/finetune/train.d.ts +6 -2
- package/dist/meta/finetune/train.js +55 -21
- package/dist/meta/finetune/tui.d.ts +3 -3
- package/dist/meta/finetune/tui.js +37 -12
- package/dist/meta/finetune/tuning.d.ts +27 -1
- package/dist/meta/finetune/tuning.js +21 -2
- package/dist/meta/finetune/usage.d.ts +4 -0
- package/dist/meta/finetune/usage.js +35 -0
- package/dist/meta/finetune/view.d.ts +9 -0
- package/dist/meta/finetune/view.js +27 -0
- package/dist/meta/host.d.ts +2 -1
- package/dist/meta/host.js +2 -1
- package/dist/meta.d.ts +291 -0
- package/dist/meta.js +981 -0
- package/dist/resources.d.ts +215 -0
- package/dist/resources.js +856 -0
- package/dist/scaffold.d.ts +3 -4
- package/dist/scaffold.js +21 -9
- package/dist/trace.d.ts +5 -0
- package/dist/trace.js +54 -5
- package/dist/tui/app.js +19 -5
- package/dist/tui/links.d.ts +27 -0
- package/dist/tui/links.js +142 -0
- package/dist/tui/models.d.ts +3 -1
- package/dist/tui/models.js +65 -7
- package/dist/tui/sandbox.d.ts +10 -0
- package/dist/tui/sandbox.js +131 -0
- package/package.json +3 -2
- package/templates/editor/.github/copilot-instructions.md +29 -43
- package/templates/editor/.github/prompts/analyze.prompt.md +3 -1
- package/templates/editor/.github/prompts/finetune-apply.prompt.md +221 -42
- package/templates/editor/.github/prompts/project-review.prompt.md +1 -1
- package/templates/editor/.github/skills/zen-analyze-run/SKILL.md +162 -57
- package/templates/editor/.github/skills/zen-cli/references/finetune.md +74 -14
- package/templates/editor/.github/skills/zen-cli/references/inspect.md +16 -2
- package/templates/editor/.github/skills/zen-cli/references/projects.md +4 -6
- package/templates/editor/.github/skills/zen-cli/references/sandbox.md +25 -13
- package/templates/editor/.github/skills/zen-finetune/SKILL.md +74 -33
- package/templates/editor/.github/skills/zen-inspect/SKILL.md +14 -1
- package/templates/editor/.github/skills/zen-inspect-ask/SKILL.md +4 -3
- package/templates/editor/.github/skills/zen-instructions/SKILL.md +4 -12
- package/templates/editor/.github/skills/zen-memory/SKILL.md +64 -39
- package/templates/editor/.github/skills/zen-review/SKILL.md +6 -31
- package/templates/editor/.github/skills/zen-review/scripts/check-paths.sh +1 -2
- package/templates/editor/.github/skills/zen-review/scripts/review.sh +3 -12
- package/templates/editor/.github/skills/zen-sandbox/SKILL.md +2 -1
- package/templates/editor/.github/skills/zen-spec-sync/SKILL.md +2 -4
- package/templates/project/agents/fork-instructions.md +78 -28
- package/templates/project/agents/memory-instructions.md +222 -493
- package/templates/project/agents/tools-instructions.md +7 -0
- package/templates/editor/.github/skills/zen-memory/scripts/check-instructions.sh +0 -116
package/dist/scaffold.d.ts
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
export declare const TEMPLATES: string;
|
|
2
|
-
/** How to use the memory graph: a house rules file, landing under `agents/`. */
|
|
3
|
-
export declare const MEMORY_RULES: string;
|
|
4
2
|
/**
|
|
5
3
|
* The project files that are ours rather than the project's.
|
|
6
4
|
*
|
|
@@ -32,13 +30,14 @@ export interface CopyOptions {
|
|
|
32
30
|
/**
|
|
33
31
|
* Copies one template directory into `dir` at `rel`, depth first, sorted so the
|
|
34
32
|
* list it returns is the same on every machine. Only a `.tmpl` is read as text;
|
|
35
|
-
* everything else is copied byte for byte.
|
|
33
|
+
* everything else is copied byte for byte. A file already holding those bytes
|
|
34
|
+
* is left alone and not listed.
|
|
36
35
|
*/
|
|
37
36
|
export declare function copyTree(from: string, dir: string, rel: string, opts: CopyOptions): string[];
|
|
38
37
|
/**
|
|
39
38
|
* Replaces every file in a project that nobody is meant to be maintaining:
|
|
40
39
|
* `SHARED_RULES` in the project tree, and the editor tree whole. Returns the
|
|
41
|
-
* relative paths
|
|
40
|
+
* relative paths that actually changed, project files first.
|
|
42
41
|
*
|
|
43
42
|
* This is the other half of `keep: true`. A scaffold never overwrites, which is
|
|
44
43
|
* right for the files a project goes on to make its own and wrong for the ones
|
package/dist/scaffold.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { chmodSync,
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, } from 'node:fs';
|
|
2
2
|
import { dirname, join, sep } from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { editorFiles } from "./meta/editor.js";
|
|
@@ -19,7 +19,6 @@ import { editorFiles } from "./meta/editor.js";
|
|
|
19
19
|
//
|
|
20
20
|
// The trees are copied whole and nothing enumerates them, so adding a file to
|
|
21
21
|
// a new project is adding a file to `templates/project/` and nothing else.
|
|
22
|
-
// `MEMORY_RULES` is the single exception, and it is named below.
|
|
23
22
|
//
|
|
24
23
|
// What is there is deliberately close to empty: a template full of
|
|
25
24
|
// commented-out options is a template nobody reads and everybody deletes. The
|
|
@@ -37,7 +36,7 @@ const TEMPLATE = '.tmpl';
|
|
|
37
36
|
/** How to use file tools: a house rules file, landing under `agents/`. */
|
|
38
37
|
const FILE_RULES = join('agents', 'files-instructions.md');
|
|
39
38
|
/** How to use the memory graph: a house rules file, landing under `agents/`. */
|
|
40
|
-
|
|
39
|
+
const MEMORY_RULES = join('agents', 'memory-instructions.md');
|
|
41
40
|
/** How tools are called: the same, and equally not the project's to maintain. */
|
|
42
41
|
const TOOL_RULES = join('agents', 'tools-instructions.md');
|
|
43
42
|
/** How forking behaves: the same again, reaching only agents that declare `fork:`. */
|
|
@@ -143,10 +142,20 @@ function target(name) {
|
|
|
143
142
|
}
|
|
144
143
|
return DOTFILES[name] ?? name;
|
|
145
144
|
}
|
|
145
|
+
/** Writes `body` to `path` unless those are already its bytes; true if it wrote. */
|
|
146
|
+
function writeIfChanged(path, body) {
|
|
147
|
+
const bytes = typeof body === 'string' ? Buffer.from(body) : body;
|
|
148
|
+
if (existsSync(path) && readFileSync(path).equals(bytes)) {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
writeFileSync(path, bytes);
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
146
154
|
/**
|
|
147
155
|
* Copies one template directory into `dir` at `rel`, depth first, sorted so the
|
|
148
156
|
* list it returns is the same on every machine. Only a `.tmpl` is read as text;
|
|
149
|
-
* everything else is copied byte for byte.
|
|
157
|
+
* everything else is copied byte for byte. A file already holding those bytes
|
|
158
|
+
* is left alone and not listed.
|
|
150
159
|
*/
|
|
151
160
|
export function copyTree(from, dir, rel, opts) {
|
|
152
161
|
const written = [];
|
|
@@ -166,19 +175,21 @@ export function copyTree(from, dir, rel, opts) {
|
|
|
166
175
|
const body = entry.name.endsWith(TEMPLATE)
|
|
167
176
|
? render(readFileSync(source, 'utf8'), opts.vars ?? {})
|
|
168
177
|
: readFileSync(source);
|
|
169
|
-
|
|
178
|
+
const changed = writeIfChanged(join(dir, child), body);
|
|
170
179
|
// The bytes are copied, not the file, so a script arrives unrunnable.
|
|
171
180
|
if (child.endsWith('.sh') || child.endsWith('.mjs')) {
|
|
172
181
|
chmodSync(join(dir, child), 0o755);
|
|
173
182
|
}
|
|
174
|
-
|
|
183
|
+
if (changed) {
|
|
184
|
+
written.push(child);
|
|
185
|
+
}
|
|
175
186
|
}
|
|
176
187
|
return written;
|
|
177
188
|
}
|
|
178
189
|
/**
|
|
179
190
|
* Replaces every file in a project that nobody is meant to be maintaining:
|
|
180
191
|
* `SHARED_RULES` in the project tree, and the editor tree whole. Returns the
|
|
181
|
-
* relative paths
|
|
192
|
+
* relative paths that actually changed, project files first.
|
|
182
193
|
*
|
|
183
194
|
* This is the other half of `keep: true`. A scaffold never overwrites, which is
|
|
184
195
|
* right for the files a project goes on to make its own and wrong for the ones
|
|
@@ -190,8 +201,9 @@ export function refreshShared(dir) {
|
|
|
190
201
|
for (const rel of SHARED_RULES) {
|
|
191
202
|
const path = join(dir, rel);
|
|
192
203
|
mkdirSync(dirname(path), { recursive: true });
|
|
193
|
-
|
|
194
|
-
|
|
204
|
+
if (writeIfChanged(path, readFileSync(join(TEMPLATES, 'project', rel)))) {
|
|
205
|
+
written.push(rel);
|
|
206
|
+
}
|
|
195
207
|
}
|
|
196
208
|
written.push(...editorFiles(dir));
|
|
197
209
|
return written;
|
package/dist/trace.d.ts
CHANGED
|
@@ -81,4 +81,9 @@ export interface NodeDetail {
|
|
|
81
81
|
*/
|
|
82
82
|
export declare function parseNodeIds(words: readonly string[], known: Map<string, unknown>): string[];
|
|
83
83
|
export declare function nodeDetail(trace: Trace, ids: readonly string[], payloads: PayloadResolver): Promise<NodeDetail[]>;
|
|
84
|
+
/**
|
|
85
|
+
* The two questions a recorded request is usually opened for, answered without
|
|
86
|
+
* printing it: which tools the call was offered, and what conversation it saw.
|
|
87
|
+
*/
|
|
88
|
+
export declare function requestParts(json: string): NodePart[];
|
|
84
89
|
//# sourceMappingURL=trace.d.ts.map
|
package/dist/trace.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { coveredIds, totalUsage, } from '@zenera/neo';
|
|
1
|
+
import { contentToText, coveredIds, totalUsage, } from '@zenera/neo';
|
|
2
2
|
import { format } from "./narrate.js";
|
|
3
3
|
import { describeCall, summarise } from "./tui/wrap.js";
|
|
4
4
|
// ---------------------------------------------------------------------------
|
|
@@ -719,12 +719,61 @@ export async function nodeDetail(trace, ids, payloads) {
|
|
|
719
719
|
ts: e.node.ts,
|
|
720
720
|
label: labelOf(e.node, {}),
|
|
721
721
|
facts: factsOf(e.node),
|
|
722
|
-
parts: (named.get(e) ?? []).
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
722
|
+
parts: (named.get(e) ?? []).flatMap((part) => {
|
|
723
|
+
const text = part.literal ?? blobs.get(part.payload?.sha256 ?? '') ?? '(unresolved)';
|
|
724
|
+
return part.name === 'request'
|
|
725
|
+
? [{ name: part.name, text }, ...requestParts(text)]
|
|
726
|
+
: [{ name: part.name, text }];
|
|
727
|
+
}),
|
|
726
728
|
}));
|
|
727
729
|
}
|
|
730
|
+
/** Longest a message or a tool result runs in `messages` before it is clipped. */
|
|
731
|
+
const MESSAGE_CLIP = 400;
|
|
732
|
+
const clip = (s, n = MESSAGE_CLIP) => {
|
|
733
|
+
const flat = s.replace(/\s+/g, ' ').trim();
|
|
734
|
+
return flat.length <= n ? flat : `${flat.slice(0, n)} … (+${flat.length - n} chars)`;
|
|
735
|
+
};
|
|
736
|
+
/**
|
|
737
|
+
* The two questions a recorded request is usually opened for, answered without
|
|
738
|
+
* printing it: which tools the call was offered, and what conversation it saw.
|
|
739
|
+
*/
|
|
740
|
+
export function requestParts(json) {
|
|
741
|
+
let req;
|
|
742
|
+
try {
|
|
743
|
+
req = JSON.parse(json);
|
|
744
|
+
}
|
|
745
|
+
catch {
|
|
746
|
+
return [];
|
|
747
|
+
}
|
|
748
|
+
const out = [];
|
|
749
|
+
if (Array.isArray(req.messages)) {
|
|
750
|
+
const lines = req.messages.map((m) => {
|
|
751
|
+
switch (m.role) {
|
|
752
|
+
case 'system':
|
|
753
|
+
return `[system] ${clip(m.content, 200)}`;
|
|
754
|
+
case 'user':
|
|
755
|
+
return `[user] ${clip(contentToText(m.content))}`;
|
|
756
|
+
case 'assistant':
|
|
757
|
+
return [
|
|
758
|
+
`[assistant${m.agent ? ` @${m.agent}` : ''}] ${clip(m.content) || '(no text)'}`,
|
|
759
|
+
...(m.toolCalls ?? []).map((c) => ` -> ${c.name} ${clip(c.args, 300)} #${c.id}`),
|
|
760
|
+
].join('\n');
|
|
761
|
+
case 'tool':
|
|
762
|
+
return `[tool ${m.name} #${m.callId}${m.isError ? ' ERROR' : ''}] ${clip(m.content)}`;
|
|
763
|
+
default:
|
|
764
|
+
return `[${m.role ?? '?'}]`;
|
|
765
|
+
}
|
|
766
|
+
});
|
|
767
|
+
out.push({ name: 'messages', text: lines.join('\n') });
|
|
768
|
+
}
|
|
769
|
+
if (Array.isArray(req.tools)) {
|
|
770
|
+
const names = req.tools
|
|
771
|
+
.map((t) => t?.name)
|
|
772
|
+
.filter((n) => typeof n === 'string');
|
|
773
|
+
out.push({ name: 'tools', text: names.join('\n') });
|
|
774
|
+
}
|
|
775
|
+
return out;
|
|
776
|
+
}
|
|
728
777
|
const GONE = '(the blob is not in this session’s store; only the preview survives)';
|
|
729
778
|
async function resolve(payloads, p) {
|
|
730
779
|
try {
|
package/dist/tui/app.js
CHANGED
|
@@ -9,6 +9,7 @@ import { History } from "../history.js";
|
|
|
9
9
|
import { format } from "../narrate.js";
|
|
10
10
|
import { display } from "../session.js";
|
|
11
11
|
import { CliError } from "../term.js";
|
|
12
|
+
import { hostUrls, hyperlink, hyperlinks, linkBlock, mountsOf } from "./links.js";
|
|
12
13
|
import { resolveTheme, THEMES } from "./theme.js";
|
|
13
14
|
import { ACTIVITY_ROWS, answerWidth, blocksOf, BOX_CHROME, boxWidth, BRANCH_ROWS, branchRows, budgetOf, CHROME_ROWS, clip, describeCall, gistOf, gridOf, summarise, THINKING_ROWS, TIME_COL, unmarked, windowOf, } from "./wrap.js";
|
|
14
15
|
const BANNER = { key: 'banner' };
|
|
@@ -30,6 +31,9 @@ const FRAME_MS = 100;
|
|
|
30
31
|
// A context rather than props only because every part of the view wants it.
|
|
31
32
|
const ThemeContext = React.createContext(THEMES.dark);
|
|
32
33
|
const useTheme = () => useContext(ThemeContext);
|
|
34
|
+
const LinksContext = React.createContext(undefined);
|
|
35
|
+
/** Without hyperlinks the only way to reach the file is to print where it is. */
|
|
36
|
+
const shownText = (text, links) => links && !links.osc8 ? hostUrls(text, links.mounts) : text;
|
|
33
37
|
function Row({ line }) {
|
|
34
38
|
const theme = useTheme();
|
|
35
39
|
const style = theme.line[line.kind];
|
|
@@ -87,7 +91,10 @@ function Call({ line }) {
|
|
|
87
91
|
*/
|
|
88
92
|
function Spans({ spans }) {
|
|
89
93
|
const theme = useTheme();
|
|
90
|
-
|
|
94
|
+
const links = useContext(LinksContext);
|
|
95
|
+
return (_jsx(_Fragment, { children: spans.map((s, i) => (_jsx(Text, { bold: s.bold === true, italic: s.italic === true, underline: s.href !== undefined && s.href !== '', ...(s.code ? { color: theme.code.color } : {}), children: links?.osc8 && s.href?.startsWith('file:')
|
|
96
|
+
? hyperlink(s.href, s.text)
|
|
97
|
+
: s.text }, i))) }));
|
|
91
98
|
}
|
|
92
99
|
/**
|
|
93
100
|
* One block of an answer.
|
|
@@ -128,11 +135,13 @@ function BlockView({ block, gap, width, }) {
|
|
|
128
135
|
* answer and boxing each of them turns the transcript into a stack of frames;
|
|
129
136
|
* the box means *this is the answer*, and it only means that if it is rare.
|
|
130
137
|
*/
|
|
131
|
-
function Answer({ text, boxed }) {
|
|
138
|
+
function Answer({ text: said, boxed }) {
|
|
132
139
|
const theme = useTheme();
|
|
140
|
+
const links = useContext(LinksContext);
|
|
133
141
|
const { stdout } = useStdout();
|
|
142
|
+
const text = shownText(said, links);
|
|
134
143
|
const width = boxWidth(text, stdout?.columns ?? 80);
|
|
135
|
-
const blocks = blocksOf(text);
|
|
144
|
+
const blocks = blocksOf(text).map((b) => (links?.osc8 ? linkBlock(b, links.mounts) : b));
|
|
136
145
|
const inner = width - (boxed ? 4 : GUTTER);
|
|
137
146
|
const body = blocks.map((block, i) => (
|
|
138
147
|
// Items of one list are one thing; a blank row between them is two
|
|
@@ -145,6 +154,10 @@ function Answer({ text, boxed }) {
|
|
|
145
154
|
function App({ engine, options, theme }) {
|
|
146
155
|
const { exit } = useApp();
|
|
147
156
|
const { stdout } = useStdout();
|
|
157
|
+
const [links] = useState(() => ({
|
|
158
|
+
mounts: mountsOf(engine.files),
|
|
159
|
+
osc8: hyperlinks(),
|
|
160
|
+
}));
|
|
148
161
|
const [lines, setLines] = useState([]);
|
|
149
162
|
const [live, setLive] = useState('');
|
|
150
163
|
const [thinking, setThinking] = useState('');
|
|
@@ -651,7 +664,7 @@ function App({ engine, options, theme }) {
|
|
|
651
664
|
recall(1);
|
|
652
665
|
}
|
|
653
666
|
});
|
|
654
|
-
return (_jsx(ThemeContext.Provider, { value: theme, children: _jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: [BANNER, ...lines], children: (item) => isBanner(item) ? (_jsx(Header, { engine: engine, readOnly: options.readOnly, started: options.started }, item.key)) : (_jsx(Row, { line: item }, item.key)) }), _jsxs(Box, { flexDirection: "column", justifyContent: "flex-end", height: busy ? grown.current : undefined, overflow: "hidden", children: [_jsx(Branches, { boxes: fitted.boxes, spin: spin, frame: frame, columns: columns }), streaming && budget.thinking ? (_jsx(Reasoning, { text: thinking, columns: columns, frame: frame, live: musing })) : null, _jsx(Activity, { rows: fitted.trunk, hidden: fitted.hidden, frame: frame }), live ? _jsx(Streaming, { text: live, columns: columns, rows: textRows }) : null] }), _jsx(Footer, { agent: agent, busy: busy, spin: spin, frame: frame, running: mine, forked: branches.current.size, step: step, model: model, stats: stats, inflight: inflight, reasoning: musing && streaming }), busy ? null : (_jsxs(Box, { children: [_jsx(Text, { color: theme.accent, children: "\u203A " }), _jsx(TextInput, { defaultValue: draft, placeholder: "Ask something\u2026 (\u2191 for history, /help for commands)", onChange: remember, onSubmit: submit }, generation)] }))] }) }));
|
|
667
|
+
return (_jsx(ThemeContext.Provider, { value: theme, children: _jsx(LinksContext.Provider, { value: links, children: _jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: [BANNER, ...lines], children: (item) => isBanner(item) ? (_jsx(Header, { engine: engine, readOnly: options.readOnly, started: options.started }, item.key)) : (_jsx(Row, { line: item }, item.key)) }), _jsxs(Box, { flexDirection: "column", justifyContent: "flex-end", height: busy ? grown.current : undefined, overflow: "hidden", children: [_jsx(Branches, { boxes: fitted.boxes, spin: spin, frame: frame, columns: columns }), streaming && budget.thinking ? (_jsx(Reasoning, { text: thinking, columns: columns, frame: frame, live: musing })) : null, _jsx(Activity, { rows: fitted.trunk, hidden: fitted.hidden, frame: frame }), live ? _jsx(Streaming, { text: live, columns: columns, rows: textRows }) : null] }), _jsx(Footer, { agent: agent, busy: busy, spin: spin, frame: frame, running: mine, forked: branches.current.size, step: step, model: model, stats: stats, inflight: inflight, reasoning: musing && streaming }), busy ? null : (_jsxs(Box, { children: [_jsx(Text, { color: theme.accent, children: "\u203A " }), _jsx(TextInput, { defaultValue: draft, placeholder: "Ask something\u2026 (\u2191 for history, /help for commands)", onChange: remember, onSubmit: submit }, generation)] }))] }) }) }));
|
|
655
668
|
}
|
|
656
669
|
function Header({ engine, readOnly, started, }) {
|
|
657
670
|
const theme = useTheme();
|
|
@@ -952,7 +965,8 @@ function Reasoning({ text, columns, frame, live, }) {
|
|
|
952
965
|
const gist = gistOf(text, columns - GUTTER - 2);
|
|
953
966
|
return (_jsx(Box, { height: 1, paddingLeft: GUTTER, overflow: "hidden", children: _jsx(Text, { wrap: "truncate-end", children: live ? (_jsx(Shimmer, { text: gist, frame: frame, color: theme.thinking.color })) : (_jsx(Text, { color: theme.thinking.color, children: gist })) }) }));
|
|
954
967
|
}
|
|
955
|
-
function Streaming({ text, columns, rows }) {
|
|
968
|
+
function Streaming({ text: said, columns, rows }) {
|
|
969
|
+
const text = shownText(said, useContext(LinksContext));
|
|
956
970
|
// The same width the finished answer will take, so landing it reflows
|
|
957
971
|
// nothing: what is on screen is what stays there.
|
|
958
972
|
//
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Workspace } from '@zenera/neo';
|
|
2
|
+
import type { Block, Span } from './wrap.ts';
|
|
3
|
+
export interface Mounts {
|
|
4
|
+
/** every name a tree is reached by, longest first */
|
|
5
|
+
names: readonly string[];
|
|
6
|
+
/** the host path an agent path names, or nothing when it is in no tree */
|
|
7
|
+
resolve(path: string): string | undefined;
|
|
8
|
+
}
|
|
9
|
+
/** Exactly the trees the file tools reach, under the same containment rule. */
|
|
10
|
+
export declare function mountsOf(files: Workspace): Mounts;
|
|
11
|
+
/** The host `file:` url a mounted path names, or nothing. */
|
|
12
|
+
export declare function mountUrl(token: string, mounts: Mounts): string | undefined;
|
|
13
|
+
/**
|
|
14
|
+
* The same spans with the same text, mounted paths carrying a host `href`.
|
|
15
|
+
*
|
|
16
|
+
* Only `href` changes, so every width measured from `span.text` still holds.
|
|
17
|
+
*/
|
|
18
|
+
export declare function linkSpans(spans: readonly Span[], mounts: Mounts): Span[];
|
|
19
|
+
/** A block whose prose and cells link their mounted paths; a fence is left alone. */
|
|
20
|
+
export declare function linkBlock(block: Block, mounts: Mounts): Block;
|
|
21
|
+
/** For a terminal with no hyperlinks: every `file:///<mount>/…` url, rewritten to the host. */
|
|
22
|
+
export declare function hostUrls(text: string, mounts: Mounts): string;
|
|
23
|
+
/** `text` as an OSC 8 hyperlink to `href`. */
|
|
24
|
+
export declare function hyperlink(href: string, text: string): string;
|
|
25
|
+
/** Whether this terminal turns OSC 8 into a link; an unknown one is assumed not to. */
|
|
26
|
+
export declare function hyperlinks(env?: NodeJS.ProcessEnv): boolean;
|
|
27
|
+
//# sourceMappingURL=links.d.ts.map
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { pathToFileURL } from 'node:url';
|
|
2
|
+
/** Exactly the trees the file tools reach, under the same containment rule. */
|
|
3
|
+
export function mountsOf(files) {
|
|
4
|
+
const names = [...(files.mount ? [files.mount] : []), ...files.mounts.map((m) => m.at)];
|
|
5
|
+
return {
|
|
6
|
+
names: names.sort((a, b) => b.length - a.length),
|
|
7
|
+
resolve: (path) => {
|
|
8
|
+
try {
|
|
9
|
+
return files.within(path);
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
const FILE = 'file://';
|
|
18
|
+
/** Prose punctuation that ends a sentence, not a path. */
|
|
19
|
+
const TRAILING = /[.,;:!?'"]+$/;
|
|
20
|
+
/** The host `file:` url a mounted path names, or nothing. */
|
|
21
|
+
export function mountUrl(token, mounts) {
|
|
22
|
+
let path = token;
|
|
23
|
+
let fragment = '';
|
|
24
|
+
if (path.startsWith(FILE)) {
|
|
25
|
+
path = path.slice(FILE.length);
|
|
26
|
+
const cut = path.search(/[?#]/);
|
|
27
|
+
if (cut >= 0) {
|
|
28
|
+
fragment = path.slice(cut);
|
|
29
|
+
path = path.slice(0, cut);
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
path = decodeURIComponent(path);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// `within` also takes relative paths and `/`; only a mount name counts here.
|
|
39
|
+
if (!mounts.names.some((n) => path === n || path.startsWith(`${n}/`))) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
const host = mounts.resolve(path);
|
|
43
|
+
return host ? pathToFileURL(host).href + fragment : undefined;
|
|
44
|
+
}
|
|
45
|
+
const escaped = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, (c) => `\\${c}`);
|
|
46
|
+
/** Every spelling of a mounted path in running text. */
|
|
47
|
+
function pattern(mounts) {
|
|
48
|
+
const names = mounts.names.map(escaped).join('|');
|
|
49
|
+
return new RegExp(`(?<![\\w/.:-])(?:${FILE})?(?:${names})(?![\\w-]|\\.\\w)(?:/[^\\s<>"'\`()[\\]{}|\\\\^]*)?`, 'g');
|
|
50
|
+
}
|
|
51
|
+
/** Each mounted path in `text`, without the punctuation that follows it. */
|
|
52
|
+
function* matches(text, mounts) {
|
|
53
|
+
if (mounts.names.length === 0) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
for (const m of text.matchAll(pattern(mounts))) {
|
|
57
|
+
const token = m[0].replace(TRAILING, '');
|
|
58
|
+
if (token) {
|
|
59
|
+
yield { at: m.index, token };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The same spans with the same text, mounted paths carrying a host `href`.
|
|
65
|
+
*
|
|
66
|
+
* Only `href` changes, so every width measured from `span.text` still holds.
|
|
67
|
+
*/
|
|
68
|
+
export function linkSpans(spans, mounts) {
|
|
69
|
+
const out = [];
|
|
70
|
+
for (const span of spans) {
|
|
71
|
+
if (span.href) {
|
|
72
|
+
const url = mountUrl(span.href, mounts);
|
|
73
|
+
out.push(url ? { ...span, href: url } : span);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
let from = 0;
|
|
77
|
+
for (const { at, token } of matches(span.text, mounts)) {
|
|
78
|
+
const url = mountUrl(token, mounts);
|
|
79
|
+
if (!url) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (at > from) {
|
|
83
|
+
out.push({ ...span, text: span.text.slice(from, at) });
|
|
84
|
+
}
|
|
85
|
+
out.push({ ...span, text: token, href: url });
|
|
86
|
+
from = at + token.length;
|
|
87
|
+
}
|
|
88
|
+
if (from === 0) {
|
|
89
|
+
out.push(span);
|
|
90
|
+
}
|
|
91
|
+
else if (from < span.text.length) {
|
|
92
|
+
out.push({ ...span, text: span.text.slice(from) });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
/** A block whose prose and cells link their mounted paths; a fence is left alone. */
|
|
98
|
+
export function linkBlock(block, mounts) {
|
|
99
|
+
return {
|
|
100
|
+
...block,
|
|
101
|
+
...(block.spans ? { spans: linkSpans(block.spans, mounts) } : {}),
|
|
102
|
+
...(block.cells
|
|
103
|
+
? { cells: block.cells.map((row) => row.map((cell) => linkSpans(cell, mounts))) }
|
|
104
|
+
: {}),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/** For a terminal with no hyperlinks: every `file:///<mount>/…` url, rewritten to the host. */
|
|
108
|
+
export function hostUrls(text, mounts) {
|
|
109
|
+
let out = '';
|
|
110
|
+
let from = 0;
|
|
111
|
+
for (const { at, token } of matches(text, mounts)) {
|
|
112
|
+
const url = token.startsWith(FILE) ? mountUrl(token, mounts) : undefined;
|
|
113
|
+
if (!url) {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
out += text.slice(from, at) + url;
|
|
117
|
+
from = at + token.length;
|
|
118
|
+
}
|
|
119
|
+
return out + text.slice(from);
|
|
120
|
+
}
|
|
121
|
+
/** `text` as an OSC 8 hyperlink to `href`. */
|
|
122
|
+
export function hyperlink(href, text) {
|
|
123
|
+
return `\u001b]8;;${href}\u001b\\${text}\u001b]8;;\u001b\\`;
|
|
124
|
+
}
|
|
125
|
+
/** Whether this terminal turns OSC 8 into a link; an unknown one is assumed not to. */
|
|
126
|
+
export function hyperlinks(env = process.env) {
|
|
127
|
+
const forced = env.FORCE_HYPERLINK;
|
|
128
|
+
if (forced !== undefined && forced !== '') {
|
|
129
|
+
return forced !== '0' && forced !== 'false';
|
|
130
|
+
}
|
|
131
|
+
if (env.WT_SESSION || env.KITTY_WINDOW_ID || env.KONSOLE_VERSION) {
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
if (['iTerm.app', 'vscode', 'WezTerm', 'ghostty'].includes(env.TERM_PROGRAM ?? '')) {
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
if (env.TERM === 'xterm-kitty' || env.TERM === 'xterm-ghostty') {
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
return Number(env.VTE_VERSION ?? 0) >= 5000;
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=links.js.map
|
package/dist/tui/models.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Catalog, type CatalogEntry } from '../catalog.ts';
|
|
1
|
+
import { type Balance, type Catalog, type CatalogEntry } from '../catalog.ts';
|
|
2
2
|
import type { Provider } from '../keys.ts';
|
|
3
3
|
export interface ProviderRow {
|
|
4
4
|
provider: Provider;
|
|
@@ -23,6 +23,8 @@ export interface BrowseOptions {
|
|
|
23
23
|
initial?: Provider;
|
|
24
24
|
theme?: string;
|
|
25
25
|
load(provider: Provider, refresh: boolean): Promise<Catalog>;
|
|
26
|
+
/** what the account has left to spend, for providers that say */
|
|
27
|
+
balance?(provider: Provider): Promise<Balance | undefined>;
|
|
26
28
|
test(entry: CatalogEntry): Promise<Verdict>;
|
|
27
29
|
}
|
|
28
30
|
/**
|
package/dist/tui/models.js
CHANGED
|
@@ -17,7 +17,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
17
17
|
// ---------------------------------------------------------------------------
|
|
18
18
|
import { Box, render, Text, useApp, useInput, useStdout } from 'ink';
|
|
19
19
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
20
|
-
import { byGroup, isPartner, matches, } from "../catalog.js";
|
|
20
|
+
import { byGroup, isPartner, matches, perMillion, } from "../catalog.js";
|
|
21
21
|
import { ago } from "../term.js";
|
|
22
22
|
import { resolveTheme } from "./theme.js";
|
|
23
23
|
import { clip, scrollTop, wrap } from "./wrap.js";
|
|
@@ -25,6 +25,25 @@ const ROLES = ['chat', 'embedding', 'image', 'audio'];
|
|
|
25
25
|
const message = (err) => (err instanceof Error ? err.message : String(err));
|
|
26
26
|
const thousands = (n) => n === undefined ? '' : n >= 1000 ? `${Math.round(n / 1000)}k` : String(n);
|
|
27
27
|
const ms = (n) => (n < 1000 ? `${n}ms` : `${(n / 1000).toFixed(1)}s`);
|
|
28
|
+
const money = (n) => {
|
|
29
|
+
const abs = Math.abs(n);
|
|
30
|
+
return `${n < 0 ? '-' : ''}$${abs !== 0 && abs < 0.01 ? abs.toFixed(4) : abs.toFixed(2)}`;
|
|
31
|
+
};
|
|
32
|
+
/** The balance as one line; `short` for the providers screen. */
|
|
33
|
+
function account(b, short) {
|
|
34
|
+
const parts = [];
|
|
35
|
+
if (b.credits !== undefined) {
|
|
36
|
+
parts.push(`balance ${money(b.credits)}`);
|
|
37
|
+
}
|
|
38
|
+
if (b.cap && !short) {
|
|
39
|
+
const reset = b.cap.reset ? ` ${b.cap.reset}` : '';
|
|
40
|
+
parts.push(`key cap ${money(b.cap.remaining)} left of ${money(b.cap.limit)}${reset}`);
|
|
41
|
+
}
|
|
42
|
+
if (b.free && !short) {
|
|
43
|
+
parts.push(`free ${b.free.remaining}/${b.free.limit} requests left today`);
|
|
44
|
+
}
|
|
45
|
+
return parts.join(' · ');
|
|
46
|
+
}
|
|
28
47
|
function freshness(origin, fetchedAt) {
|
|
29
48
|
switch (origin) {
|
|
30
49
|
case 'curated':
|
|
@@ -87,6 +106,7 @@ function Browser({ options, theme, onPick }) {
|
|
|
87
106
|
const [role, setRole] = useState(0);
|
|
88
107
|
const [selected, setSelected] = useState(0);
|
|
89
108
|
const [verdicts, setVerdicts] = useState(new Map());
|
|
109
|
+
const [balances, setBalances] = useState(new Map());
|
|
90
110
|
const [closing, setClosing] = useState(false);
|
|
91
111
|
const top = useRef(0);
|
|
92
112
|
const provider = options.providers[at].provider;
|
|
@@ -102,10 +122,18 @@ function Browser({ options, theme, onPick }) {
|
|
|
102
122
|
setLoading((now) => (now === p ? undefined : now));
|
|
103
123
|
});
|
|
104
124
|
};
|
|
125
|
+
const fetchBalance = (p) => {
|
|
126
|
+
options.balance?.(p).then((b) => b && setBalances((prev) => new Map(prev).set(p, b)), (err) => setBalances((prev) => new Map(prev).set(p, message(err))));
|
|
127
|
+
};
|
|
105
128
|
useEffect(() => {
|
|
106
129
|
if (start >= 0) {
|
|
107
130
|
load(provider, false);
|
|
108
131
|
}
|
|
132
|
+
for (const p of options.providers) {
|
|
133
|
+
if (p.credential) {
|
|
134
|
+
fetchBalance(p.provider);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
109
137
|
// Once, for `zen models browse <provider>`; every later open is a key.
|
|
110
138
|
}, []);
|
|
111
139
|
useEffect(() => {
|
|
@@ -153,7 +181,14 @@ function Browser({ options, theme, onPick }) {
|
|
|
153
181
|
// what is left. A row is kept in hand: a frame as tall as the viewport is
|
|
154
182
|
// the one Ink cannot erase (see tui/app.tsx).
|
|
155
183
|
const problem = catalog?.problem;
|
|
156
|
-
const
|
|
184
|
+
const balance = balances.get(provider);
|
|
185
|
+
const funds = balance === undefined
|
|
186
|
+
? []
|
|
187
|
+
: wrap(typeof balance === 'string'
|
|
188
|
+
? `balance could not be read: ${balance}`
|
|
189
|
+
: account(balance, false) || 'balance: nothing reported', Math.max(20, size.columns - 1));
|
|
190
|
+
const short = typeof balance === 'string' || (balance?.credits !== undefined && balance.credits <= 0);
|
|
191
|
+
const chrome = 7 + (problem ? 1 : 0) + funds.length;
|
|
157
192
|
const height = Math.max(3, size.rows - 1 - chrome);
|
|
158
193
|
top.current = scrollTop(index, top.current, height, rows.length);
|
|
159
194
|
useInput((input, key) => {
|
|
@@ -266,6 +301,7 @@ function Browser({ options, theme, onPick }) {
|
|
|
266
301
|
}
|
|
267
302
|
else if (input === 'r') {
|
|
268
303
|
load(provider, true);
|
|
304
|
+
fetchBalance(provider);
|
|
269
305
|
}
|
|
270
306
|
else if (key.escape && query) {
|
|
271
307
|
setQuery('');
|
|
@@ -293,7 +329,8 @@ function Browser({ options, theme, onPick }) {
|
|
|
293
329
|
const fresh = cat
|
|
294
330
|
? freshness(cat.origin, cat.fetchedAt)
|
|
295
331
|
: freshness(p.origin, p.fetchedAt);
|
|
296
|
-
|
|
332
|
+
const b = balances.get(p.provider);
|
|
333
|
+
return (_jsxs(Row, { children: [_jsxs(Text, { color: here ? theme.accent : undefined, bold: here, children: [here ? '› ' : ' ', p.provider.padEnd(nameW)] }), p.credential ? (_jsx(Text, { color: theme.code.color, children: p.credential.padEnd(15) })) : (_jsx(Text, { color: theme.line.error.color, children: 'no credential'.padEnd(15) })), _jsxs(Text, { color: theme.chrome.color, children: [`${count} model${count === 1 ? '' : 's'}`.padEnd(13), p.credential ? fresh : `zen key add ${p.provider}`] }), b && typeof b !== 'string' ? (_jsx(Text, { color: theme.code.color, children: ` ${account(b, true)}` })) : null] }, p.provider));
|
|
297
334
|
}), _jsx(Text, { children: " " }), _jsx(Keys, { theme: theme, text: "\u2191\u2193 move \u00B7 enter open \u00B7 q quit" })] }));
|
|
298
335
|
}
|
|
299
336
|
if (screen === 'detail' && current) {
|
|
@@ -302,9 +339,24 @@ function Browser({ options, theme, onPick }) {
|
|
|
302
339
|
const header = catalog
|
|
303
340
|
? `${provider} · ${catalog.entries.length} models · ${freshness(catalog.origin, catalog.fetchedAt)}`
|
|
304
341
|
: provider;
|
|
305
|
-
const
|
|
342
|
+
const withMaker = rows.some((e) => e.publisher);
|
|
343
|
+
const priced = rows.some((e) => e.pricing);
|
|
344
|
+
const ROLE_W = Math.max(4, ...rows.map((e) => e.roles.join('+').length)) + 2;
|
|
345
|
+
const CTX_W = 9;
|
|
346
|
+
const PRICE_W = 10;
|
|
347
|
+
const fixed = (withMaker ? 12 : 0) + ROLE_W + CTX_W + (priced ? PRICE_W * 2 : 0) + 'test'.length + 2;
|
|
348
|
+
const idW = Math.max(10, Math.min(46, Math.max(8, ...rows.map((e) => e.id.length)) + 2, width - fixed));
|
|
306
349
|
const shown = rows.slice(top.current, top.current + height);
|
|
307
350
|
const filter = typing ? `/${query}▌` : query ? `/${query}` : '';
|
|
351
|
+
const columns = [
|
|
352
|
+
` ${'model'.padEnd(idW)}`,
|
|
353
|
+
withMaker ? 'maker'.padEnd(12) : '',
|
|
354
|
+
'role'.padEnd(ROLE_W),
|
|
355
|
+
'context'.padStart(CTX_W - 2).padEnd(CTX_W),
|
|
356
|
+
priced ? 'in $/1M'.padStart(PRICE_W - 2).padEnd(PRICE_W) : '',
|
|
357
|
+
priced ? 'out $/1M'.padStart(PRICE_W - 2).padEnd(PRICE_W) : '',
|
|
358
|
+
'test',
|
|
359
|
+
].join('');
|
|
308
360
|
let body;
|
|
309
361
|
if (failure) {
|
|
310
362
|
body = _jsx(Row, { color: theme.line.error.color, children: failure });
|
|
@@ -319,11 +371,17 @@ function Browser({ options, theme, onPick }) {
|
|
|
319
371
|
body = (_jsx(_Fragment, { children: shown.map((e, i) => {
|
|
320
372
|
const here = top.current + i === index;
|
|
321
373
|
const v = said(verdicts.get(e.ref), theme);
|
|
322
|
-
return (_jsxs(Row, { children: [_jsxs(Text, { color: here ? theme.accent : undefined, bold: here, children: [here ? '› ' : ' ', clip(e.id, idW - 2).padEnd(idW)] }),
|
|
374
|
+
return (_jsxs(Row, { children: [_jsxs(Text, { color: here ? theme.accent : undefined, bold: here, children: [here ? '› ' : ' ', clip(e.id, idW - 2).padEnd(idW)] }), withMaker ? (_jsx(Text, { color: isPartner(e) ? theme.warn : theme.code.color, children: clip(e.publisher ?? '', 11).padEnd(12) })) : null, _jsxs(Text, { color: theme.chrome.color, children: [e.roles.join('+').padEnd(ROLE_W), thousands(e.contextLength)
|
|
375
|
+
.padStart(CTX_W - 2)
|
|
376
|
+
.padEnd(CTX_W)] }), priced ? (_jsxs(Text, { color: e.pricing?.free ? theme.code.color : undefined, children: [perMillion(e.pricing?.prompt)
|
|
377
|
+
.padStart(PRICE_W - 2)
|
|
378
|
+
.padEnd(PRICE_W), (e.pricing?.free ? '' : perMillion(e.pricing?.completion))
|
|
379
|
+
.padStart(PRICE_W - 2)
|
|
380
|
+
.padEnd(PRICE_W)] })) : null, v ? _jsx(Text, { color: v.color, children: v.text }) : null] }, e.ref));
|
|
323
381
|
}) }));
|
|
324
382
|
}
|
|
325
383
|
const preview = current ? [current.name, current.description].filter(Boolean).join(' — ') : '';
|
|
326
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Row, { children: [_jsx(Text, { bold: true, color: theme.accent, children: header }), _jsxs(Text, { color: theme.chrome.color, children: [' role: ', only ?? 'all', filter ? ' ' : ''] }), _jsx(Text, { color: theme.code.color, children: filter })] }), problem ? (_jsxs(Row, { color: theme.warn, children: ["could not be listed: ", problem.detail ?? 'no reason given'] })) : null, _jsx(Text, { children: " " }), _jsx(Box, { flexDirection: "column", height: height, children: body }), _jsx(Row, { color: theme.chrome.color, children: clip(preview, width) || ' ' }), _jsx(Row, { color: theme.chrome.color, children: rows.length === 0
|
|
384
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Row, { children: [_jsx(Text, { bold: true, color: theme.accent, children: header }), _jsxs(Text, { color: theme.chrome.color, children: [' role: ', only ?? 'all', filter ? ' ' : ''] }), _jsx(Text, { color: theme.code.color, children: filter })] }), problem ? (_jsxs(Row, { color: theme.warn, children: ["could not be listed: ", problem.detail ?? 'no reason given'] })) : null, funds.map((line, i) => (_jsx(Row, { color: short ? theme.warn : theme.code.color, children: line }, `funds${i}`))), _jsx(Text, { children: " " }), _jsx(Row, { bold: true, color: theme.chrome.color, children: columns }), _jsx(Box, { flexDirection: "column", height: height, children: body }), _jsx(Row, { color: theme.chrome.color, children: clip(preview, width) || ' ' }), _jsx(Row, { color: theme.chrome.color, children: rows.length === 0
|
|
327
385
|
? ' '
|
|
328
386
|
: `${index + 1} of ${rows.length}${rows.length < (catalog?.entries.length ?? 0) ? ` (${catalog.entries.length} in all)` : ''}` }), _jsx(Keys, { theme: theme, text: typing
|
|
329
387
|
? 'type to filter · enter keep · esc clear · ↑↓ move'
|
|
@@ -350,7 +408,7 @@ function Detail({ entry, catalog, verdict, theme, width, rows }) {
|
|
|
350
408
|
add('pricing', entry.pricing?.free
|
|
351
409
|
? 'free'
|
|
352
410
|
: entry.pricing?.prompt
|
|
353
|
-
?
|
|
411
|
+
? `${perMillion(entry.pricing.prompt)} per 1M input tokens, ${perMillion(entry.pricing.completion) || '?'} per 1M output tokens`
|
|
354
412
|
: undefined);
|
|
355
413
|
add('released', entry.created);
|
|
356
414
|
add('source', entry.source === 'live' && catalog
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type ResourceSampler } from '../resources.ts';
|
|
2
|
+
export interface WatchOptions {
|
|
3
|
+
sampler: ResourceSampler;
|
|
4
|
+
intervalMs: number;
|
|
5
|
+
explain: boolean;
|
|
6
|
+
theme?: string;
|
|
7
|
+
}
|
|
8
|
+
/** Runs the live screen until the person leaves it. */
|
|
9
|
+
export declare function watch(options: WatchOptions): Promise<void>;
|
|
10
|
+
//# sourceMappingURL=sandbox.d.ts.map
|