@yanzc/claude-pro 0.5.5 → 0.5.6
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 +1 -1
- package/package.json +1 -1
- package/src/api.js +23 -7
- package/src/commands.js +1 -0
- package/src/pricing.js +59 -0
- package/src/print.js +9 -0
- package/src/restart.js +20 -3
- package/src/session.js +3 -0
- package/src/ui/repl.js +18 -5
package/README.md
CHANGED
|
@@ -161,7 +161,7 @@ claude-pro config set ANTHROPIC_MODEL deepseek-pro --project
|
|
|
161
161
|
## 终端标题、页脚与窗口缩放
|
|
162
162
|
|
|
163
163
|
- **终端标题**:标签/窗口标题显示 `✳ <会话标题> | <目录> · <模型> · <effort>`(会话标题是**当前话题**:每次发消息后,后台让小模型(haiku)判断是否换了话题,换了就生成一个简短标题,和官方客户端一样;"继续"之类的消息不会改标题。话题记在会话文件里,`/resume`、重启后仍在,`/resume` 列表也显示它。旧会话没有话题时,恢复时按最近一条提问生成。还没生成时显示第一条提问。最多 40 字,保证模型和 effort 不被窄标签截掉。`CLAUDE_PRO_DISABLE_TOPIC_TITLE=1` 关闭,省掉每条消息一次的小请求)。忙碌时前缀在 `⠂`/`⠐` 之间闪动,限流暂停时变 `⏸`,退出时恢复进入前的标题。`/model`、`/effort`、`/clear`、`/resume` 后自动更新。**VS Code**(1.140 起)只对它认得的 AI 命令行(claude/codex/copilot/gemini)把程序设置的标题显示在标签上(`terminal.integrated.tabs.allowAgentCliTitle`,默认开),其余一律显示进程名 `node`;它在 Windows 上靠 `node.exe` 命令行里的 `\claude-code\` 路径段识别 Claude Code,所以监督进程用 `bin\claude-code\..\claude-pro.js` 启动会话(目录不必存在,Node 自己化简 `..`)。不经过监督进程的启动方式(`CLAUDE_PRO_NO_SUPERVISOR=1`)或 Linux/macOS 上,要在 VS Code 设置里把 `terminal.integrated.tabs.title` 改成 `${sequence}` 才能看到标题。
|
|
164
|
-
- **页脚**:输入框下方右侧常驻当前模型和 effort(如 `sonnet-5-5 · high`);没发送 effort(auto)或该模型不支持时只显示模型;窄窗口放不下时先让位给"上下文快满"
|
|
164
|
+
- **页脚**:输入框下方右侧常驻当前模型和 effort(如 `sonnet-5-5 · high`);没发送 effort(auto)或该模型不支持时只显示模型;窄窗口放不下时先让位给"上下文快满"等提示,再不行就不显示。模型前面是**当前对话累计花费**(如 `$1.23 · opus-5-5 · effort high`):按官方价(和网关扣费同一张价目表,含 5 分钟/1 小时缓存写入、长上下文价)把每个请求算进去,子 agent、压缩、话题标题、WebFetch 都算,按 Esc 中断的请求按已读入的输入和估算的已输出部分算;记在会话文件里,重启、`/resume` 后接着累计,`/clear` 从零开始。`/cost` 第一行也显示它,`-p --output-format json` 的结果带本次运行的 `total_cost_usd`。不认识价格的模型(第三方端点的自定义模型)不计;网关上游分组倍率不是 1 的账户以网关流水为准。
|
|
165
165
|
- **缩放窗口**:拖动期间不重绘,停下约 80ms 后从左上角整屏绝对重绘一次,输入框不会再留下残影。注意:很短的会话里这次重绘会覆盖终端上方 shell 的内容。
|
|
166
166
|
|
|
167
167
|
## 自动重启:改了自己的代码马上生效
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yanzc/claude-pro",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.6",
|
|
4
4
|
"description": "claude-pro: a self-implemented coding agent for Anthropic-compatible APIs or local Claude Code subscription login. Full access with no permission prompts; file edits are checkpointed and can be rewound.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
package/src/api.js
CHANGED
|
@@ -10,6 +10,7 @@ import { readGatewayLogin, fetchGatewayModels, cachedGatewayModel, gatewayEffort
|
|
|
10
10
|
import { createCodexFetch, codexModels, codexEffort, nearestEffort } from './codex/client.js';
|
|
11
11
|
import { SIGNATURE_PREFIX } from './codex/translate-request.js';
|
|
12
12
|
import { fitImagesIn } from './utils/images.js';
|
|
13
|
+
import { costOf } from './pricing.js';
|
|
13
14
|
|
|
14
15
|
let cached = null;
|
|
15
16
|
|
|
@@ -342,6 +343,14 @@ export function isAbort(err) {
|
|
|
342
343
|
return err?.name === 'AbortError' || err instanceof Anthropic.APIUserAbortError || /^(?:request was )?aborted\.?$/i.test(err?.message || '');
|
|
343
344
|
}
|
|
344
345
|
|
|
346
|
+
// USD for a request at the list prices (see pricing.js), by the model that answered, else the one asked for;
|
|
347
|
+
// null when neither has a price. A cost goes to config.onCost.
|
|
348
|
+
function charge(config, requested, served, usage) {
|
|
349
|
+
const cost = costOf(served, usage) ?? costOf(requested, usage);
|
|
350
|
+
if (cost > 0) config.onCost?.(cost);
|
|
351
|
+
return cost;
|
|
352
|
+
}
|
|
353
|
+
|
|
345
354
|
// Stream a message. `onEvent` receives:
|
|
346
355
|
// {type:'block_start', index, block}
|
|
347
356
|
// {type:'text', index, text} text delta
|
|
@@ -354,7 +363,8 @@ export function isAbort(err) {
|
|
|
354
363
|
// {type:'rate_limit_resume', waitedMs} the repeated request was accepted: the pause is over
|
|
355
364
|
// {type:'reset'} partial output from a failed attempt must be discarded
|
|
356
365
|
// `waitForRateLimit: false` reports a rate limit as an error instead of waiting for it (side queries
|
|
357
|
-
// where nobody wants to sit through a pause). Returns {message, usage, stopReason}.
|
|
366
|
+
// where nobody wants to sit through a pause). Returns {message, usage, stopReason, model, cost, cache}.
|
|
367
|
+
// What each request costs (an interrupted one too) goes to config.onCost, the conversation's running total.
|
|
358
368
|
export async function streamMessage(config, request, { signal, onEvent = () => {}, waitForRateLimit = true } = {}) {
|
|
359
369
|
if (signal?.aborted) throw Object.assign(new Error('aborted'), { name: 'AbortError', partial: [] });
|
|
360
370
|
// A token that has expired (or is about to) is renewed first, as the official client does.
|
|
@@ -382,6 +392,10 @@ export async function streamMessage(config, request, { signal, onEvent = () => {
|
|
|
382
392
|
for (;;) {
|
|
383
393
|
let emitted = false;
|
|
384
394
|
const startedAt = Date.now(); // the cache TTL counts from the start of the request, not from the end of the reply
|
|
395
|
+
const blocks = [];
|
|
396
|
+
const jsonBufs = [];
|
|
397
|
+
let usage = {};
|
|
398
|
+
let model = params.model;
|
|
385
399
|
try {
|
|
386
400
|
const stream = await client.messages.create(params, options);
|
|
387
401
|
if (paused) {
|
|
@@ -389,11 +403,7 @@ export async function streamMessage(config, request, { signal, onEvent = () => {
|
|
|
389
403
|
paused = null;
|
|
390
404
|
onEvent({ type: 'rate_limit_resume', waitedMs: overloaded ? retryState.overloadedWaitMs : retryState.waitedMs, ...(overloaded ? { overloaded } : {}) });
|
|
391
405
|
}
|
|
392
|
-
const blocks = [];
|
|
393
|
-
const jsonBufs = [];
|
|
394
|
-
let usage = {};
|
|
395
406
|
let stopReason = null;
|
|
396
|
-
let model = params.model;
|
|
397
407
|
let stopped = false;
|
|
398
408
|
for await (const ev of stream) {
|
|
399
409
|
switch (ev.type) {
|
|
@@ -457,9 +467,15 @@ export async function streamMessage(config, request, { signal, onEvent = () => {
|
|
|
457
467
|
const key = cacheKey(config, params.model, login);
|
|
458
468
|
cacheRefused.set(key, new Set([...(cacheRefused.get(key) || []), ...refused]));
|
|
459
469
|
}
|
|
460
|
-
|
|
470
|
+
const cost = charge(config, params.model, model, usage);
|
|
471
|
+
return { message: { role: 'assistant', content }, usage, stopReason, model, cost, cache: { ...cachePlanOf(params), fingerprint: fingerprint(params), at: startedAt } };
|
|
461
472
|
} catch (raw) {
|
|
462
|
-
if (signal?.aborted || isAbort(raw))
|
|
473
|
+
if (signal?.aborted || isAbort(raw)) {
|
|
474
|
+
// An interrupted reply is billed for what it had read and written: the output so far is estimated.
|
|
475
|
+
const written = blocks.reduce((n, b) => n + (b?.text?.length || 0) + (b?.thinking?.length || 0), 0) + jsonBufs.join('').length;
|
|
476
|
+
charge(config, params.model, model, { ...usage, output_tokens: Math.max(usage.output_tokens || 0, Math.ceil(written / 4)) });
|
|
477
|
+
throw Object.assign(new Error('aborted'), { name: 'AbortError', partial: raw?.partial || [] });
|
|
478
|
+
}
|
|
463
479
|
const err = describeError(raw, config);
|
|
464
480
|
// The token was refused. A running native Claude Code session may have renewed the shared token
|
|
465
481
|
// while this request was in flight: read it again. If it is the same one, it may have been revoked
|
package/src/commands.js
CHANGED
|
@@ -463,6 +463,7 @@ export function buildCommands(app) {
|
|
|
463
463
|
const b = a.subUsage;
|
|
464
464
|
const cacheLines = a.cache.describe();
|
|
465
465
|
out([
|
|
466
|
+
`Total cost: ${formatUsd(app.cost)} ${c.dim('for this conversation, at list prices (sub-agents, compaction and side requests included)')}`,
|
|
466
467
|
`Total input: ${formatTokens(u.input + u.cacheRead + u.cacheWrite)} tokens ${c.dim(`(${formatTokens(u.cacheRead)} cache read, ${formatTokens(u.cacheWrite)} cache write${u.cacheWrite1h ? `, ${formatTokens(u.cacheWrite1h)} of it for 1 hour` : ''})`)}`,
|
|
467
468
|
`Total output: ${formatTokens(u.output)} tokens`,
|
|
468
469
|
`API requests: ${u.requests}${u.compactions ? c.dim(` + ${u.compactions} to compact the conversation`) : ''}`,
|
package/src/pricing.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// What a request costs, in USD, for the running total of a conversation (the footer, /cost). The prices are the
|
|
2
|
+
// official per-million-token ones the gateway bills at (its pricing.mjs); a gateway upstream group's multiplier
|
|
3
|
+
// (1 unless the account is set otherwise) is not known here. A Claude model that is not in the table is priced
|
|
4
|
+
// as the current model of its family, as the gateway serves it. longContext: a prompt (input + cache reads +
|
|
5
|
+
// cache writes) above `threshold` tokens is billed at those prices.
|
|
6
|
+
|
|
7
|
+
const PRICES = [
|
|
8
|
+
{ id: 'claude-fable-5-1', family: 'fable', input: 10, output: 50, cacheWrite5m: 12.5, cacheWrite1h: 20, cacheRead: 0.25 },
|
|
9
|
+
{ id: 'claude-opus-5-5', family: 'opus', input: 4, output: 20, cacheWrite5m: 5, cacheWrite1h: 8, cacheRead: 0.2 },
|
|
10
|
+
{ id: 'claude-sonnet-5-5', family: 'sonnet', input: 2, output: 10, cacheWrite5m: 2.5, cacheWrite1h: 4, cacheRead: 0.1 },
|
|
11
|
+
{
|
|
12
|
+
id: 'claude-haiku-5-5', family: 'haiku', input: 0.1, output: 0.5, cacheWrite5m: 0.125, cacheWrite1h: 0.2, cacheRead: 0.01,
|
|
13
|
+
longContext: { threshold: 100_000, input: 0.5, output: 2.5, cacheWrite5m: 0.625, cacheWrite1h: 1, cacheRead: 0.05 },
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
id: 'gpt-6-astra', input: 10, output: 50, cacheRead: 1, cacheWrite5m: 12.5, cacheWrite1h: 12.5,
|
|
17
|
+
longContext: { threshold: 272_000, input: 20, output: 75, cacheWrite5m: 25, cacheWrite1h: 25, cacheRead: 2 },
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
id: 'gpt-6-1-sol', input: 2, output: 10, cacheRead: 0.1, cacheWrite5m: 2.5, cacheWrite1h: 2.5,
|
|
21
|
+
longContext: { threshold: 272_000, input: 4, output: 15, cacheWrite5m: 5, cacheWrite1h: 5, cacheRead: 0.2 },
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: 'gpt-6-luna', input: 0.1, output: 0.5, cacheRead: 0.01, cacheWrite5m: 0.125, cacheWrite1h: 0.125,
|
|
25
|
+
longContext: { threshold: 272_000, input: 0.2, output: 0.75, cacheWrite5m: 0.25, cacheWrite1h: 0.25, cacheRead: 0.02 },
|
|
26
|
+
},
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
// claude-opus-5-5, anthropic/claude-opus-5-5[1m], claude-haiku-5-5-20261001, gpt-6.1-sol:high -> the table's id form
|
|
30
|
+
function cleanModel(model) {
|
|
31
|
+
const value = String(model || '').trim().toLowerCase();
|
|
32
|
+
return (value.includes('/') ? value.slice(value.lastIndexOf('/') + 1) : value)
|
|
33
|
+
.replace(/\[1m\]$/, '')
|
|
34
|
+
.replace(/[:@](none|minimal|low|medium|high|xhigh|max|ultra)$/, '')
|
|
35
|
+
.replace(/(\d)\.(\d)/g, '$1-$2')
|
|
36
|
+
.replace(/-\d{8}$/, '');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// The prices of a model, or null when it has none here (a model of another endpoint).
|
|
40
|
+
export function priceOf(model) {
|
|
41
|
+
const id = cleanModel(model);
|
|
42
|
+
const exact = PRICES.find((p) => p.id === id);
|
|
43
|
+
if (exact) return exact;
|
|
44
|
+
const family = id.startsWith('claude-') && id.match(/\b(fable|opus|sonnet|haiku)\b/)?.[1];
|
|
45
|
+
return (family && PRICES.find((p) => p.family === family)) || null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// USD for one request's usage as the API reports it; null for a model with no price.
|
|
49
|
+
export function costOf(model, usage = {}) {
|
|
50
|
+
const price = priceOf(model);
|
|
51
|
+
if (!price) return null;
|
|
52
|
+
const n = (v) => Math.max(0, Number(v) || 0);
|
|
53
|
+
const input = n(usage.input_tokens);
|
|
54
|
+
const read = n(usage.cache_read_input_tokens);
|
|
55
|
+
const oneHour = n(usage.cache_creation?.ephemeral_1h_input_tokens);
|
|
56
|
+
const writes = Math.max(n(usage.cache_creation_input_tokens), n(usage.cache_creation?.ephemeral_5m_input_tokens) + oneHour);
|
|
57
|
+
const p = price.longContext && input + read + writes > price.longContext.threshold ? price.longContext : price;
|
|
58
|
+
return (input * p.input + n(usage.output_tokens) * p.output + read * p.cacheRead + (writes - oneHour) * p.cacheWrite5m + oneHour * p.cacheWrite1h) / 1e6;
|
|
59
|
+
}
|
package/src/print.js
CHANGED
|
@@ -53,8 +53,16 @@ export async function runPrint({ prompt, flags, cwd, stdout = process.stdout, st
|
|
|
53
53
|
};
|
|
54
54
|
const runtime = await createRuntime({ cwd, flags, io });
|
|
55
55
|
let store = new SessionStore(cwd);
|
|
56
|
+
let cost = 0; // what this run's requests cost (total_cost_usd)
|
|
57
|
+
let conversationCost = 0; // and the conversation's, kept with the session
|
|
58
|
+
runtime.config.onCost = (usd) => {
|
|
59
|
+
cost += usd;
|
|
60
|
+
conversationCost += usd;
|
|
61
|
+
store.setCost(conversationCost);
|
|
62
|
+
};
|
|
56
63
|
if (flags.resumeFile) {
|
|
57
64
|
const loaded = SessionStore.load(flags.resumeFile);
|
|
65
|
+
conversationCost = loaded.cost;
|
|
58
66
|
runtime.agent.messages = loaded.messages;
|
|
59
67
|
runtime.agent.restoreFileState(loaded.messages, loaded.times);
|
|
60
68
|
runtime.agent.lastActivityAt = loaded.lastAt;
|
|
@@ -97,6 +105,7 @@ export async function runPrint({ prompt, flags, cwd, stdout = process.stdout, st
|
|
|
97
105
|
session_id: store.id,
|
|
98
106
|
duration_ms: Date.now() - started,
|
|
99
107
|
num_turns: a.usage.requests,
|
|
108
|
+
total_cost_usd: Number(cost.toFixed(6)),
|
|
100
109
|
usage: { input_tokens: used.input, output_tokens: used.output, cache_read_input_tokens: used.cacheRead, cache_creation_input_tokens: used.cacheWrite },
|
|
101
110
|
});
|
|
102
111
|
} else {
|
package/src/restart.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// process to the next, the check that the new code starts before it is trusted, and the watch on the
|
|
3
3
|
// source files.
|
|
4
4
|
|
|
5
|
+
import crypto from 'node:crypto';
|
|
5
6
|
import fs from 'node:fs';
|
|
6
7
|
import path from 'node:path';
|
|
7
8
|
import { spawn } from 'node:child_process';
|
|
@@ -83,18 +84,34 @@ export function runSelfCheck({ root = INSTALL_DIR, signal, timeoutMs = 60_000 }
|
|
|
83
84
|
|
|
84
85
|
// Watch the program's own source (src/ and bin/) and call `onChange(file)` once the files have stopped
|
|
85
86
|
// changing for a moment. Returns {close()}. A platform that cannot watch just never calls back.
|
|
87
|
+
// Only a change of content counts: Windows also reports a file whose last access time was updated, which
|
|
88
|
+
// reading it does (at most once an hour: another window starting, npm packing it, a search).
|
|
86
89
|
export function watchSource(root, onChange) {
|
|
87
90
|
const watchers = [];
|
|
88
91
|
let timer = null;
|
|
89
|
-
const
|
|
90
|
-
|
|
92
|
+
const SOURCE = /\.(m?js|json)$/i;
|
|
93
|
+
const digest = (file) => { try { return crypto.createHash('sha1').update(fs.readFileSync(file)).digest('base64'); } catch { return null; } };
|
|
94
|
+
const known = new Map(); // file -> digest of its content when last seen (null: not there)
|
|
95
|
+
for (const dir of ['src', 'bin']) {
|
|
96
|
+
let names = [];
|
|
97
|
+
try { names = fs.readdirSync(path.join(root, dir), { recursive: true }); } catch { /* not there */ }
|
|
98
|
+
for (const name of names) if (SOURCE.test(name)) known.set(path.join(root, dir, name), digest(path.join(root, dir, name)));
|
|
99
|
+
}
|
|
100
|
+
const hit = (dir) => (_event, name) => {
|
|
101
|
+
if (name && !SOURCE.test(String(name))) return;
|
|
102
|
+
if (name) {
|
|
103
|
+
const file = path.join(root, dir, String(name));
|
|
104
|
+
const now = digest(file);
|
|
105
|
+
if (now === (known.get(file) ?? null)) return;
|
|
106
|
+
known.set(file, now);
|
|
107
|
+
}
|
|
91
108
|
clearTimeout(timer);
|
|
92
109
|
timer = setTimeout(() => { try { onChange(String(name || '')); } catch { /* the callback's problem */ } }, 400);
|
|
93
110
|
timer.unref?.();
|
|
94
111
|
};
|
|
95
112
|
for (const dir of ['src', 'bin']) {
|
|
96
113
|
try {
|
|
97
|
-
const watcher = fs.watch(path.join(root, dir), { recursive: true, persistent: false }, hit);
|
|
114
|
+
const watcher = fs.watch(path.join(root, dir), { recursive: true, persistent: false }, hit(dir));
|
|
98
115
|
watcher.on('error', () => { /* a directory that vanished, a watch limit: not worth a crash */ });
|
|
99
116
|
watchers.push(watcher);
|
|
100
117
|
} catch { /* not supported here */ }
|
package/src/session.js
CHANGED
|
@@ -77,6 +77,8 @@ export class SessionStore {
|
|
|
77
77
|
// The name the user gave the conversation with /rename (null: none again); it wins over the topic.
|
|
78
78
|
setName(name) { this.append({ type: 'meta', name, at: Date.now() }); }
|
|
79
79
|
reset(messages) { this.append({ type: 'reset', messages, at: Date.now() }); }
|
|
80
|
+
// What the conversation's requests have cost so far (USD, see pricing.js), written after each one; the newest wins.
|
|
81
|
+
setCost(usd) { this.append({ type: 'meta', cost: usd, at: Date.now() }); }
|
|
80
82
|
// The system prompt this conversation runs behind, kept so that a resumed one continues behind the same prompt
|
|
81
83
|
// and so with the prompt cache it already has (see runtime.resumeSystemPrompt). Written when it first appears
|
|
82
84
|
// and whenever it changes (after a compaction); the newest one wins when the session is loaded.
|
|
@@ -114,6 +116,7 @@ export class SessionStore {
|
|
|
114
116
|
topic: typeof meta.topic === 'string' && meta.topic.trim() ? meta.topic : null,
|
|
115
117
|
name: typeof meta.name === 'string' && meta.name.trim() ? meta.name : null,
|
|
116
118
|
systemPrompt: typeof meta.systemPrompt === 'string' && meta.systemPrompt.trim() ? meta.systemPrompt : null,
|
|
119
|
+
cost: Number(meta.cost) > 0 ? Number(meta.cost) : 0,
|
|
117
120
|
};
|
|
118
121
|
}
|
|
119
122
|
|
package/src/ui/repl.js
CHANGED
|
@@ -18,7 +18,7 @@ import { createRuntime } from '../runtime.js';
|
|
|
18
18
|
import { SessionStore, firstUserText } from '../session.js';
|
|
19
19
|
import { defaultEffort, effortSupported, effortToSend, quickQuery } from '../api.js';
|
|
20
20
|
import { CONFIG_DIR, VERSION, APP_NAME, INSTALL_DIR } from '../config.js';
|
|
21
|
-
import { readGatewayLogin } from '../gateway.js';
|
|
21
|
+
import { readGatewayLogin, formatUsd } from '../gateway.js';
|
|
22
22
|
import { TOPIC_TITLE_SYSTEM, topicTitlePrompt } from '../prompts.js';
|
|
23
23
|
import { buildCommands } from '../commands.js';
|
|
24
24
|
import { canRestart, writeRestartState, runSelfCheck, watchSource, restartNote, RESTART_EXIT_CODE, MAX_RESTARTS, RESTART_WINDOW_MS } from '../restart.js';
|
|
@@ -31,7 +31,7 @@ import { readClipboardImage, imageFromFile, watchClipboard, writeClipboard, read
|
|
|
31
31
|
import { fitImagesIn } from '../utils/images.js';
|
|
32
32
|
import { downloadsDir, filesToSend, formatBytes } from '../utils/downloads.js';
|
|
33
33
|
import { copyUnique } from '../transfer.js';
|
|
34
|
-
import { checkForUpdate, installUpdate, installedVersion, isNewer, CHECK_EVERY_MS } from '../updater.js';
|
|
34
|
+
import { checkForUpdate, installUpdate, installedVersion, installedByNpm, isNewer, CHECK_EVERY_MS } from '../updater.js';
|
|
35
35
|
import { PeerNode, peersEnabled, findPeer, resolvePeer, machineName, cleanName, peerMessagePrompt } from '../peers.js';
|
|
36
36
|
import { RemoteControl, transcriptItems, onlineEnabled, REMOTE_WIDTH } from '../remote.js';
|
|
37
37
|
import { qrLines } from '../qr.js';
|
|
@@ -196,6 +196,7 @@ export class App {
|
|
|
196
196
|
this.remotePeersTimer = null;
|
|
197
197
|
this.transfers = new Map(); // files on their way to or from other sessions, for the footer
|
|
198
198
|
this.transferSeq = 0;
|
|
199
|
+
this.cost = 0; // what this conversation's requests have cost so far (USD at list prices): the footer and /cost
|
|
199
200
|
this.fileIndex = new FileIndex(cwd, () => this.runtime?.config);
|
|
200
201
|
this.store = new SessionStore(cwd);
|
|
201
202
|
// What the terminal prints goes to the remote page too, except what it is told about as events.
|
|
@@ -245,6 +246,12 @@ export class App {
|
|
|
245
246
|
this.runtime = await createRuntime({ cwd: this.cwd, flags: this.flags, io: this });
|
|
246
247
|
// The conversation's id, sent to the gateway with each request (see streamMessage); it follows /clear and /resume.
|
|
247
248
|
Object.defineProperty(this.runtime.config, 'sessionId', { get: () => this.store.id, configurable: true });
|
|
249
|
+
// Every request's cost (the conversation's, its sub-agents', side queries'), kept with the session.
|
|
250
|
+
this.runtime.config.onCost = (usd) => {
|
|
251
|
+
this.cost += usd;
|
|
252
|
+
this.store.setCost(this.cost);
|
|
253
|
+
this.screen.schedule();
|
|
254
|
+
};
|
|
248
255
|
this.commands = buildCommands(this);
|
|
249
256
|
if (pinOff(this.config.env.CLAUDE_PRO_PIN_INPUT)) { this.screen.pin = false; this.screen.pinned = null; }
|
|
250
257
|
// What the session showed before a restart: scrolled back to in fullscreen, repainted after a resize otherwise.
|
|
@@ -992,9 +999,10 @@ export class App {
|
|
|
992
999
|
if (!canRestart() || /^(0|false|no|off)$/i.test(this.config.env.CLAUDE_PRO_AUTO_RESTART || '')) return;
|
|
993
1000
|
this.sourceWatch = watchSource(INSTALL_DIR, (file) => {
|
|
994
1001
|
this.sourceChanged = file || true;
|
|
995
|
-
// another window (or npm) installed a new version: say so, and load it once this session is idle
|
|
1002
|
+
// another window (or npm) installed a new version: say so, and load it once this session is idle. In a
|
|
1003
|
+
// source checkout a newer version in package.json is a release being made, not an install.
|
|
996
1004
|
const disk = installedVersion();
|
|
997
|
-
if (disk !== VERSION && isNewer(disk, VERSION) && this.updateRestart !== disk) {
|
|
1005
|
+
if (installedByNpm() && disk !== VERSION && isNewer(disk, VERSION) && this.updateRestart !== disk) {
|
|
998
1006
|
this.updateRestart = disk;
|
|
999
1007
|
this.pendingUpdate = null;
|
|
1000
1008
|
this.notice(`${APP_NAME} ${disk} was installed (by another window or npm); this one restarts to load it ${this.working() ? 'once its work is done' : 'now'}.`);
|
|
@@ -1674,6 +1682,7 @@ export class App {
|
|
|
1674
1682
|
this.remote?.register().catch(() => {});
|
|
1675
1683
|
}
|
|
1676
1684
|
this.firstPrompt = loaded.title;
|
|
1685
|
+
this.cost = loaded.cost;
|
|
1677
1686
|
this.agent.messages = loaded.messages;
|
|
1678
1687
|
this.agent.restoreFileState(loaded.messages, loaded.times);
|
|
1679
1688
|
this.agent.lastActivityAt = loaded.lastAt; // a long pause before this turn clears old tool results, as it would have
|
|
@@ -1711,6 +1720,7 @@ export class App {
|
|
|
1711
1720
|
this.topic = null;
|
|
1712
1721
|
this.customTitle = null;
|
|
1713
1722
|
this.firstPrompt = null;
|
|
1723
|
+
this.cost = 0;
|
|
1714
1724
|
this.remote?.resync();
|
|
1715
1725
|
}
|
|
1716
1726
|
|
|
@@ -2174,8 +2184,11 @@ export class App {
|
|
|
2174
2184
|
// An effort that is not set is not sent: the API then uses the model's default, which is shown instead.
|
|
2175
2185
|
const level = effort || defaultEffort(cfg.mainModel.id, cfg);
|
|
2176
2186
|
const info = c.dim(level ? `${model} · effort ${level}` : model);
|
|
2187
|
+
// What the conversation has cost so far goes before the model, and is the first to go when room is short.
|
|
2188
|
+
const spent = this.cost > 0 ? c.dim(formatUsd(this.cost)) : '';
|
|
2189
|
+
const join = (...parts) => parts.filter(Boolean).join(c.dim(' · '));
|
|
2177
2190
|
const room = w - stringWidth(left) - 1;
|
|
2178
|
-
const right = [notice
|
|
2191
|
+
const right = [join(notice, spent, info), join(notice, info), notice, info].find((r) => r && stringWidth(r) < room) || '';
|
|
2179
2192
|
const gap = room - stringWidth(right);
|
|
2180
2193
|
return right ? left + ' '.repeat(gap) + right : left;
|
|
2181
2194
|
}
|