@itookit/dsht 0.3.0 → 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/README.i18n.yaml +2 -2
- package/README.md +30 -15
- package/README.zh.md +30 -15
- package/dist/catalog/controller.d.ts +32 -0
- package/dist/catalog/controller.js +88 -0
- package/dist/catalog/index.d.ts +2 -0
- package/dist/catalog/index.js +2 -0
- package/dist/{cli.js → cli/index.js} +40 -24
- package/dist/controller/connection.d.ts +80 -0
- package/dist/controller/connection.js +190 -0
- package/dist/controller/controller.d.ts +260 -0
- package/dist/controller/controller.js +362 -0
- package/dist/controller/index.d.ts +5 -0
- package/dist/controller/index.js +3 -0
- package/dist/controller/memory-log.d.ts +35 -0
- package/dist/controller/memory-log.js +95 -0
- package/dist/cost/controller.d.ts +34 -0
- package/dist/cost/controller.js +107 -0
- package/dist/cost/index.d.ts +9 -0
- package/dist/cost/index.js +7 -0
- package/dist/cost/ledger-files.d.ts +20 -0
- package/dist/cost/ledger-files.js +128 -0
- package/dist/cost/ledger.d.ts +65 -0
- package/dist/cost/ledger.js +136 -0
- package/dist/cost/pricing.d.ts +48 -0
- package/dist/cost/pricing.js +141 -0
- package/dist/cost/records.d.ts +17 -0
- package/dist/cost/records.js +66 -0
- package/dist/cost/scanner.d.ts +22 -0
- package/dist/cost/scanner.js +95 -0
- package/dist/cost/types.d.ts +69 -0
- package/dist/cost/types.js +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/session/connection-view.d.ts +18 -0
- package/dist/session/connection-view.js +1 -0
- package/dist/{controller.d.ts → session/controller.d.ts} +102 -136
- package/dist/session/controller.js +616 -0
- package/dist/session/export-html.d.ts +9 -0
- package/dist/session/export-html.js +39 -0
- package/dist/{export.d.ts → session/export.d.ts} +1 -1
- package/dist/{export.js → session/export.js} +6 -20
- package/dist/{history.d.ts → session/history.d.ts} +17 -0
- package/dist/{history.js → session/history.js} +163 -2
- package/dist/session/index.d.ts +17 -0
- package/dist/session/index.js +10 -0
- package/dist/session/markdown.d.ts +39 -0
- package/dist/session/markdown.js +255 -0
- package/dist/session/math.d.ts +11 -0
- package/dist/session/math.js +82 -0
- package/dist/{navigation.d.ts → session/navigation.d.ts} +1 -1
- package/dist/{navigation.js → session/navigation.js} +1 -1
- package/dist/{references.js → session/references.js} +1 -1
- package/dist/{telemetry.d.ts → session/telemetry.d.ts} +1 -1
- package/dist/{telemetry.js → session/telemetry.js} +1 -1
- package/dist/{transcript.d.ts → session/transcript.d.ts} +23 -1
- package/dist/{transcript.js → session/transcript.js} +36 -18
- package/dist/session/types.d.ts +18 -0
- package/dist/session/types.js +2 -0
- package/dist/state.d.ts +41 -0
- package/dist/state.js +9 -0
- package/dist/storage/directories.d.ts +14 -0
- package/dist/storage/directories.js +24 -0
- package/dist/storage/files.d.ts +43 -0
- package/dist/storage/files.js +135 -0
- package/dist/storage/index.d.ts +3 -0
- package/dist/storage/index.js +3 -0
- package/dist/transport/auth.js +65 -0
- package/dist/transport/host.d.ts +13 -0
- package/dist/transport/host.js +1 -0
- package/dist/ui/app.d.ts +11 -0
- package/dist/ui/app.js +726 -0
- package/dist/ui/chat/header.d.ts +11 -0
- package/dist/ui/chat/header.js +14 -0
- package/dist/{history-view.d.ts → ui/chat/history-view.d.ts} +1 -1
- package/dist/{history-view.js → ui/chat/history-view.js} +3 -3
- package/dist/ui/chat/status.d.ts +79 -0
- package/dist/ui/chat/status.js +311 -0
- package/dist/ui/chat/viewport.d.ts +17 -0
- package/dist/ui/chat/viewport.js +14 -0
- package/dist/ui/commands/parse.d.ts +96 -0
- package/dist/ui/commands/parse.js +121 -0
- package/dist/ui/commands/registry.d.ts +33 -0
- package/dist/ui/commands/registry.js +72 -0
- package/dist/ui/copy-mode.d.ts +4 -0
- package/dist/ui/copy-mode.js +6 -0
- package/dist/{cost-view.d.ts → ui/dialogs/cost.d.ts} +1 -1
- package/dist/{cost-view.js → ui/dialogs/cost.js} +3 -3
- package/dist/ui/dialogs/index.d.ts +120 -0
- package/dist/ui/dialogs/index.js +113 -0
- package/dist/ui/dialogs/picker.d.ts +18 -0
- package/dist/ui/dialogs/picker.js +38 -0
- package/dist/ui/frozen.d.ts +8 -0
- package/dist/ui/frozen.js +7 -0
- package/dist/ui/input/references.d.ts +12 -0
- package/dist/ui/input/references.js +15 -0
- package/dist/ui/mount.d.ts +6 -0
- package/dist/ui/mount.js +11 -0
- package/dist/{theme.d.ts → ui/theme/index.d.ts} +1 -1
- package/package.json +19 -13
- package/dist/app.d.ts +0 -21
- package/dist/app.js +0 -805
- package/dist/auth.js +0 -108
- package/dist/controller.js +0 -961
- package/dist/cost.d.ts +0 -119
- package/dist/cost.js +0 -313
- package/dist/status.d.ts +0 -28
- package/dist/status.js +0 -157
- /package/dist/{cli.d.ts → cli/index.d.ts} +0 -0
- /package/dist/{memory.d.ts → session/memory.d.ts} +0 -0
- /package/dist/{memory.js → session/memory.js} +0 -0
- /package/dist/{references.d.ts → session/references.d.ts} +0 -0
- /package/dist/{auth.d.ts → transport/auth.d.ts} +0 -0
- /package/dist/{client.d.ts → transport/client.d.ts} +0 -0
- /package/dist/{client.js → transport/client.js} +0 -0
- /package/dist/{endpoint.d.ts → transport/endpoint.d.ts} +0 -0
- /package/dist/{endpoint.js → transport/endpoint.js} +0 -0
- /package/dist/{wire.d.ts → transport/wire.d.ts} +0 -0
- /package/dist/{wire.js → transport/wire.js} +0 -0
- /package/dist/{input-history.d.ts → ui/input/history.d.ts} +0 -0
- /package/dist/{input-history.js → ui/input/history.js} +0 -0
- /package/dist/{input.d.ts → ui/input/input.d.ts} +0 -0
- /package/dist/{input.js → ui/input/input.js} +0 -0
- /package/dist/{mouse.d.ts → ui/input/mouse.d.ts} +0 -0
- /package/dist/{mouse.js → ui/input/mouse.js} +0 -0
- /package/dist/{theme.js → ui/theme/index.js} +0 -0
package/dist/cost.d.ts
DELETED
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
import { type ObjectValue } from './wire.ts';
|
|
2
|
-
interface Rates {
|
|
3
|
-
input: number;
|
|
4
|
-
cacheRead: number;
|
|
5
|
-
cacheWrite: number;
|
|
6
|
-
output: number;
|
|
7
|
-
}
|
|
8
|
-
/** An explicit validity interval and weekday peak windows in the named time zone. */
|
|
9
|
-
export interface PriceVersion {
|
|
10
|
-
id: string;
|
|
11
|
-
provider: string;
|
|
12
|
-
model: string;
|
|
13
|
-
from: string;
|
|
14
|
-
until?: string;
|
|
15
|
-
currency: 'CNY';
|
|
16
|
-
source: string;
|
|
17
|
-
timezone: string;
|
|
18
|
-
peak: Rates;
|
|
19
|
-
offPeak: Rates;
|
|
20
|
-
weekdays: number[];
|
|
21
|
-
windows: [number, number][];
|
|
22
|
-
}
|
|
23
|
-
export declare const DEFAULT_PRICES: PriceVersion[];
|
|
24
|
-
/** Validate user-maintained price versions, rejecting ambiguous overlapping intervals.
|
|
25
|
-
* @param value - Parsed prices.json array.
|
|
26
|
-
* @returns Price versions with validated rates and schedules.
|
|
27
|
-
*/
|
|
28
|
-
export declare function pricesFrom(value: unknown): PriceVersion[];
|
|
29
|
-
/** Return the calendar date used by both daily and three-calendar-day summaries.
|
|
30
|
-
* @param time - Epoch milliseconds.
|
|
31
|
-
* @returns Beijing calendar date, YYYY-MM-DD.
|
|
32
|
-
*/
|
|
33
|
-
export declare function costDay(time: number): string;
|
|
34
|
-
/** Summary retains the known subtotal, the records it could not price, and the coarse estimates.
|
|
35
|
-
* `unknown` counts records with no amount at all; `estimated` counts records that only have a
|
|
36
|
-
* floor amount, including dated requests whose timestamp cannot place them inside the range.
|
|
37
|
-
*/
|
|
38
|
-
export interface CostTotal {
|
|
39
|
-
amount: number;
|
|
40
|
-
unknown: number;
|
|
41
|
-
estimated: number;
|
|
42
|
-
records: number;
|
|
43
|
-
}
|
|
44
|
-
/** How much of the visible history the cached ledger currently covers. */
|
|
45
|
-
export type Coverage = 'complete' | 'scanning' | 'partial';
|
|
46
|
-
/** Select a price by event time, applying half-open local peak windows.
|
|
47
|
-
* @param prices - Validated versions.
|
|
48
|
-
* @param provider - Provider identity from the recorded request.
|
|
49
|
-
* @param model - Recorded model name; official DeepSeek aliases fall back to Pro when containing pro, otherwise Flash.
|
|
50
|
-
* @param time - Recorded settlement timestamp used as a billing-time estimate.
|
|
51
|
-
* @returns Matching price version and per-million-token rates, if known.
|
|
52
|
-
*/
|
|
53
|
-
export declare function priceAt(prices: PriceVersion[], provider: string, model: string, time: number): {
|
|
54
|
-
price: PriceVersion;
|
|
55
|
-
rates: Rates;
|
|
56
|
-
} | undefined;
|
|
57
|
-
/** Select a rate without a settlement time, so an unattributable request still enters the total.
|
|
58
|
-
* The cheapest candidate off-peak rate is a floor: it never overstates, and the charge stays
|
|
59
|
-
* marked as estimated.
|
|
60
|
-
* @param prices - Validated versions.
|
|
61
|
-
* @param provider - Provider identity from the recorded request.
|
|
62
|
-
* @param model - Recorded model name.
|
|
63
|
-
* @returns The candidate version with the lowest off-peak input rate and its rates, if any.
|
|
64
|
-
*/
|
|
65
|
-
export declare function lowestPrice(prices: PriceVersion[], provider: string, model: string): {
|
|
66
|
-
price: PriceVersion;
|
|
67
|
-
rates: Rates;
|
|
68
|
-
} | undefined;
|
|
69
|
-
/** Keep only billing-relevant fields; prompts, tool bodies, cookies and keys never enter the ledger.
|
|
70
|
-
* @param records - One HTTP history page's records.
|
|
71
|
-
* @returns Minimal durable events for a deterministic usage fold.
|
|
72
|
-
*/
|
|
73
|
-
export declare function costRecords(records: unknown): ObjectValue[];
|
|
74
|
-
/** Per-origin cache of priced request settlements; each scan replaces a session at a fixed cut. */
|
|
75
|
-
export declare class CostLedger {
|
|
76
|
-
readonly prices: PriceVersion[];
|
|
77
|
-
readonly directory?: string | undefined;
|
|
78
|
-
private sessions;
|
|
79
|
-
private totals;
|
|
80
|
-
scannedAt?: number;
|
|
81
|
-
scanning: boolean;
|
|
82
|
-
error: string;
|
|
83
|
-
constructor(prices?: PriceVersion[], directory?: string | undefined);
|
|
84
|
-
/** Cached charges count as complete; only a failed scan or an empty ledger is partial.
|
|
85
|
-
* @returns Coverage of the current totals, so callers can mark them without re-deriving the rule.
|
|
86
|
-
*/
|
|
87
|
-
get coverage(): Coverage;
|
|
88
|
-
/** Load immutable cut files, keeping the newest complete scan for each session. */
|
|
89
|
-
load(): Promise<void>;
|
|
90
|
-
/** Replace one session using all billing events through the opening snapshot cut.
|
|
91
|
-
* @param sessionId - Host session identity.
|
|
92
|
-
* @param cut - Opening cursor, preventing a stale scan from overwriting a newer scan.
|
|
93
|
-
* @param events - Minimal events returned by costRecords, across all history pages.
|
|
94
|
-
*/
|
|
95
|
-
replace(sessionId: string, cut: number, events: ObjectValue[]): Promise<void>;
|
|
96
|
-
/** Whether this session has a complete cached scan.
|
|
97
|
-
* @param sessionId - Selected session identity.
|
|
98
|
-
* @returns True when a complete scan is available.
|
|
99
|
-
*/
|
|
100
|
-
hasSession(sessionId?: string): boolean;
|
|
101
|
-
/** Describe unpriced model/usage combinations without exposing conversation content.
|
|
102
|
-
* @returns Unique reasons across cached sessions.
|
|
103
|
-
*/
|
|
104
|
-
missing(): string[];
|
|
105
|
-
/** Summarize cached requests across one session or Beijing calendar days.
|
|
106
|
-
* @param sessionId - Optional session restriction.
|
|
107
|
-
* @param days - Today or today plus the preceding two calendar days.
|
|
108
|
-
* @param now - Clock used for date attribution.
|
|
109
|
-
* @returns Known subtotal, unpriceable count, and estimated count; an estimated record always
|
|
110
|
-
* names an amount, but a dated range only adds the records it can place inside that range.
|
|
111
|
-
*/
|
|
112
|
-
total(sessionId?: string, days?: 1 | 3, now?: number): CostTotal;
|
|
113
|
-
}
|
|
114
|
-
/** Compact estimates retain an asterisk whenever a subtotal is not exact.
|
|
115
|
-
* @param total - Summary from the ledger.
|
|
116
|
-
* @returns Yuan amount and incompleteness marker.
|
|
117
|
-
*/
|
|
118
|
-
export declare function costText(total: CostTotal): string;
|
|
119
|
-
export {};
|
package/dist/cost.js
DELETED
|
@@ -1,313 +0,0 @@
|
|
|
1
|
-
/** Versioned CNY estimates from durable request usage; provider invoices remain authoritative. */
|
|
2
|
-
import { createHash, randomUUID } from 'node:crypto';
|
|
3
|
-
import { mkdir, readFile, readdir, rename, writeFile, unlink } from 'node:fs/promises';
|
|
4
|
-
import { join } from 'node:path';
|
|
5
|
-
import { object, array } from "./wire.js";
|
|
6
|
-
const clocks = new Map();
|
|
7
|
-
/** Published rates verified on 2026-09-10; preceding dates require historical configuration.
|
|
8
|
-
* Flash and Pro are priced independently, and a separate cache write uses the cache-miss input rate.
|
|
9
|
-
*/
|
|
10
|
-
const OFFICIAL_PRICING = 'https://api-docs.deepseek.com/zh-cn/quick_start/pricing/';
|
|
11
|
-
const PEAK_SCHEDULE = { weekdays: [1, 2, 3, 4, 5], windows: [[540, 720], [840, 1080]] };
|
|
12
|
-
const FLASH_RATES = { peak: { input: 2, cacheRead: 0.04, cacheWrite: 2, output: 8 },
|
|
13
|
-
offPeak: { input: 1, cacheRead: 0.02, cacheWrite: 1, output: 4 } };
|
|
14
|
-
const PRO_RATES = { peak: { input: 9, cacheRead: 0.3, cacheWrite: 9, output: 27 },
|
|
15
|
-
offPeak: { input: 4.5, cacheRead: 0.15, cacheWrite: 4.5, output: 13.5 } };
|
|
16
|
-
export const DEFAULT_PRICES = [
|
|
17
|
-
{ id: 'deepseek-2026-09-10-flash', provider: 'deepseek-official', model: 'deepseek-flash',
|
|
18
|
-
from: '2026-09-10T00:00:00+08:00', currency: 'CNY', source: OFFICIAL_PRICING, timezone: 'Asia/Shanghai',
|
|
19
|
-
...PEAK_SCHEDULE, ...FLASH_RATES },
|
|
20
|
-
{ id: 'deepseek-2026-09-10-pro', provider: 'deepseek-official', model: 'deepseek-v4-pro',
|
|
21
|
-
from: '2026-09-10T00:00:00+08:00', until: '2026-09-14T12:00:00+08:00', currency: 'CNY',
|
|
22
|
-
source: OFFICIAL_PRICING, timezone: 'Asia/Shanghai', ...PEAK_SCHEDULE, ...PRO_RATES },
|
|
23
|
-
// The provider bills `deepseek-v4-pro` requests at Flash rates once V4 Pro is retired.
|
|
24
|
-
{ id: 'deepseek-2026-09-14-pro-served-by-flash', provider: 'deepseek-official', model: 'deepseek-v4-pro',
|
|
25
|
-
from: '2026-09-14T12:00:00+08:00', currency: 'CNY', source: OFFICIAL_PRICING, timezone: 'Asia/Shanghai',
|
|
26
|
-
...PEAK_SCHEDULE, ...FLASH_RATES },
|
|
27
|
-
];
|
|
28
|
-
/** Validate user-maintained price versions, rejecting ambiguous overlapping intervals.
|
|
29
|
-
* @param value - Parsed prices.json array.
|
|
30
|
-
* @returns Price versions with validated rates and schedules.
|
|
31
|
-
*/
|
|
32
|
-
export function pricesFrom(value) {
|
|
33
|
-
if (!Array.isArray(value))
|
|
34
|
-
throw new Error('prices.json must contain an array');
|
|
35
|
-
const ids = new Set();
|
|
36
|
-
for (const raw of value) {
|
|
37
|
-
const p = object(raw);
|
|
38
|
-
for (const key of ['id', 'provider', 'model', 'source', 'timezone', 'from'])
|
|
39
|
-
if (typeof p[key] !== 'string' || !p[key])
|
|
40
|
-
throw new Error(`Invalid price ${key}`);
|
|
41
|
-
if (ids.has(String(p.id)))
|
|
42
|
-
throw new Error('Duplicate price id');
|
|
43
|
-
ids.add(String(p.id));
|
|
44
|
-
const from = Date.parse(String(p.from));
|
|
45
|
-
const until = p.until === undefined ? Infinity : Date.parse(String(p.until));
|
|
46
|
-
if (!Number.isFinite(from) || !(until > from) || p.currency !== 'CNY')
|
|
47
|
-
throw new Error('Invalid price interval or currency');
|
|
48
|
-
new Intl.DateTimeFormat('en', { timeZone: String(p.timezone) }).format();
|
|
49
|
-
for (const key of ['peak', 'offPeak'])
|
|
50
|
-
for (const bucket of ['input', 'cacheRead', 'cacheWrite', 'output']) {
|
|
51
|
-
const rate = object(p[key])[bucket];
|
|
52
|
-
if (typeof rate !== 'number' || !Number.isFinite(rate) || rate < 0)
|
|
53
|
-
throw new Error('Invalid token rate');
|
|
54
|
-
}
|
|
55
|
-
if (!Array.isArray(p.weekdays) || p.weekdays.some(d => typeof d !== 'number' || !Number.isInteger(d) || d < 0 || d > 6)
|
|
56
|
-
|| !Array.isArray(p.windows) || p.windows.some(w => !Array.isArray(w) || w.length !== 2 || w.some(n => typeof n !== 'number' || !Number.isInteger(n)) || Number(w[0]) < 0 || Number(w[1]) > 1440 || Number(w[0]) >= Number(w[1])))
|
|
57
|
-
throw new Error('Invalid peak schedule');
|
|
58
|
-
}
|
|
59
|
-
const prices = value;
|
|
60
|
-
for (const [index, p] of prices.entries())
|
|
61
|
-
for (const q of prices.slice(index + 1)) {
|
|
62
|
-
if (p.provider === q.provider && p.model === q.model && Date.parse(p.from) < (q.until ? Date.parse(q.until) : Infinity)
|
|
63
|
-
&& Date.parse(q.from) < (p.until ? Date.parse(p.until) : Infinity))
|
|
64
|
-
throw new Error('Overlapping price intervals');
|
|
65
|
-
}
|
|
66
|
-
return prices;
|
|
67
|
-
}
|
|
68
|
-
/** Return the calendar date used by both daily and three-calendar-day summaries.
|
|
69
|
-
* @param time - Epoch milliseconds.
|
|
70
|
-
* @returns Beijing calendar date, YYYY-MM-DD.
|
|
71
|
-
*/
|
|
72
|
-
export function costDay(time) { return new Date(time + 8 * 3600_000).toISOString().slice(0, 10); }
|
|
73
|
-
/** Price family used when a recorded model name has no exact entry. */
|
|
74
|
-
function priceFamily(model) { return model.toLowerCase().includes('pro') ? 'deepseek-v4-pro' : 'deepseek-flash'; }
|
|
75
|
-
/** Candidate versions for one request: its exact model first, then the official model family. */
|
|
76
|
-
function candidates(prices, provider, model) {
|
|
77
|
-
const exact = prices.filter(p => p.provider === provider && p.model === model);
|
|
78
|
-
if (exact.length)
|
|
79
|
-
return exact;
|
|
80
|
-
return provider === 'deepseek-official' ? prices.filter(p => p.model === priceFamily(model)) : [];
|
|
81
|
-
}
|
|
82
|
-
/** Select a price by event time, applying half-open local peak windows.
|
|
83
|
-
* @param prices - Validated versions.
|
|
84
|
-
* @param provider - Provider identity from the recorded request.
|
|
85
|
-
* @param model - Recorded model name; official DeepSeek aliases fall back to Pro when containing pro, otherwise Flash.
|
|
86
|
-
* @param time - Recorded settlement timestamp used as a billing-time estimate.
|
|
87
|
-
* @returns Matching price version and per-million-token rates, if known.
|
|
88
|
-
*/
|
|
89
|
-
export function priceAt(prices, provider, model, time) {
|
|
90
|
-
const price = candidates(prices, provider, model).find(p => Date.parse(p.from) <= time && (p.until === undefined || time < Date.parse(p.until)));
|
|
91
|
-
if (!price)
|
|
92
|
-
return;
|
|
93
|
-
let clock = clocks.get(price.timezone);
|
|
94
|
-
if (!clock) {
|
|
95
|
-
clock = new Intl.DateTimeFormat('en-US', { timeZone: price.timezone, weekday: 'short', hour: '2-digit', minute: '2-digit', hourCycle: 'h23' });
|
|
96
|
-
clocks.set(price.timezone, clock);
|
|
97
|
-
}
|
|
98
|
-
const parts = clock.formatToParts(time);
|
|
99
|
-
const part = (name) => parts.find(p => p.type === name).value;
|
|
100
|
-
const day = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].indexOf(part('weekday'));
|
|
101
|
-
const minute = Number(part('hour')) * 60 + Number(part('minute'));
|
|
102
|
-
return { price, rates: price.weekdays.includes(day) && price.windows.some(([a, b]) => minute >= a && minute < b) ? price.peak : price.offPeak };
|
|
103
|
-
}
|
|
104
|
-
/** Select a rate without a settlement time, so an unattributable request still enters the total.
|
|
105
|
-
* The cheapest candidate off-peak rate is a floor: it never overstates, and the charge stays
|
|
106
|
-
* marked as estimated.
|
|
107
|
-
* @param prices - Validated versions.
|
|
108
|
-
* @param provider - Provider identity from the recorded request.
|
|
109
|
-
* @param model - Recorded model name.
|
|
110
|
-
* @returns The candidate version with the lowest off-peak input rate and its rates, if any.
|
|
111
|
-
*/
|
|
112
|
-
export function lowestPrice(prices, provider, model) {
|
|
113
|
-
let best;
|
|
114
|
-
for (const price of candidates(prices, provider, model)) {
|
|
115
|
-
if (best === undefined || price.offPeak.input < best.rates.input)
|
|
116
|
-
best = { price, rates: price.offPeak };
|
|
117
|
-
}
|
|
118
|
-
return best;
|
|
119
|
-
}
|
|
120
|
-
/** Keep only billing-relevant fields; prompts, tool bodies, cookies and keys never enter the ledger.
|
|
121
|
-
* @param records - One HTTP history page's records.
|
|
122
|
-
* @returns Minimal durable events for a deterministic usage fold.
|
|
123
|
-
*/
|
|
124
|
-
export function costRecords(records) {
|
|
125
|
-
return array(records).map(raw => object(object(raw).event)).filter(e => ['request/context', 'assistant/message', 'assistant/attempt', 'llm/retry-started', 'session/end-seed'].includes(String(e.type))).map(e => {
|
|
126
|
-
const d = object(e.data);
|
|
127
|
-
const m = object(d.message ?? {});
|
|
128
|
-
const stream = array(d.stream ?? []).map(r => object(object(r).chunk ?? {})).filter(c => c.type === 'usage');
|
|
129
|
-
return { seq: e.seq ?? null, time: e.time ?? null, type: e.type, data: {
|
|
130
|
-
inherited: d.inherited ?? false, turn: d.turn ?? null, step: d.step ?? null, provider: d.provider ?? null, model: d.model ?? null,
|
|
131
|
-
source: m.source ?? null, usage: d.usage ?? stream.at(-1)?.usage ?? null,
|
|
132
|
-
} };
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
/** Per-origin cache of priced request settlements; each scan replaces a session at a fixed cut. */
|
|
136
|
-
export class CostLedger {
|
|
137
|
-
prices;
|
|
138
|
-
directory;
|
|
139
|
-
sessions = new Map();
|
|
140
|
-
totals = new Map();
|
|
141
|
-
scannedAt;
|
|
142
|
-
scanning = false;
|
|
143
|
-
error = '';
|
|
144
|
-
constructor(prices = DEFAULT_PRICES, directory) {
|
|
145
|
-
this.prices = prices;
|
|
146
|
-
this.directory = directory;
|
|
147
|
-
}
|
|
148
|
-
/** Cached charges count as complete; only a failed scan or an empty ledger is partial.
|
|
149
|
-
* @returns Coverage of the current totals, so callers can mark them without re-deriving the rule.
|
|
150
|
-
*/
|
|
151
|
-
get coverage() {
|
|
152
|
-
if (this.scanning)
|
|
153
|
-
return 'scanning';
|
|
154
|
-
if (this.error)
|
|
155
|
-
return 'partial';
|
|
156
|
-
return this.scannedAt !== undefined || this.sessions.size > 0 ? 'complete' : 'partial';
|
|
157
|
-
}
|
|
158
|
-
/** Load immutable cut files, keeping the newest complete scan for each session. */
|
|
159
|
-
async load() {
|
|
160
|
-
if (!this.directory)
|
|
161
|
-
return;
|
|
162
|
-
this.totals.clear();
|
|
163
|
-
await mkdir(this.directory, { recursive: true, mode: 0o700 });
|
|
164
|
-
for (const name of await readdir(this.directory)) {
|
|
165
|
-
if (!name.endsWith('.json'))
|
|
166
|
-
continue;
|
|
167
|
-
let raw;
|
|
168
|
-
try {
|
|
169
|
-
raw = await readFile(join(this.directory, name), 'utf8');
|
|
170
|
-
}
|
|
171
|
-
catch (error) {
|
|
172
|
-
if (error instanceof Error && 'code' in error && error.code === 'ENOENT')
|
|
173
|
-
continue;
|
|
174
|
-
throw error;
|
|
175
|
-
}
|
|
176
|
-
const v = JSON.parse(raw);
|
|
177
|
-
if (v.version !== 1 || typeof v.sessionId !== 'string' || !Number.isSafeInteger(v.cut) || !Array.isArray(v.charges)
|
|
178
|
-
|| v.charges.some(c => !c || typeof c.key !== 'string' || typeof c.provider !== 'string' || typeof c.model !== 'string'
|
|
179
|
-
|| (c.amount !== undefined && (typeof c.amount !== 'number' || !Number.isFinite(c.amount) || c.amount < 0))
|
|
180
|
-
|| (c.estimated !== undefined && c.estimated !== true)
|
|
181
|
-
|| (c.time !== undefined && (typeof c.time !== 'number' || !Number.isFinite(c.time) || c.time < 0 || c.time > 8.64e15))
|
|
182
|
-
|| (c.usage !== undefined && Object.values(c.usage).some(n => typeof n !== 'number' || !Number.isSafeInteger(n) || n < 0))))
|
|
183
|
-
throw new Error('Invalid cost ledger');
|
|
184
|
-
for (const c of v.charges)
|
|
185
|
-
if (c.price)
|
|
186
|
-
pricesFrom([c.price]);
|
|
187
|
-
if ((this.sessions.get(v.sessionId)?.cut ?? -2) <= v.cut)
|
|
188
|
-
this.sessions.set(v.sessionId, v);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
/** Replace one session using all billing events through the opening snapshot cut.
|
|
192
|
-
* @param sessionId - Host session identity.
|
|
193
|
-
* @param cut - Opening cursor, preventing a stale scan from overwriting a newer scan.
|
|
194
|
-
* @param events - Minimal events returned by costRecords, across all history pages.
|
|
195
|
-
*/
|
|
196
|
-
async replace(sessionId, cut, events) {
|
|
197
|
-
if ((this.sessions.get(sessionId)?.cut ?? -2) > cut)
|
|
198
|
-
return;
|
|
199
|
-
const charges = [];
|
|
200
|
-
const inheritedCut = Math.max(-1, ...events.filter(e => e.type === 'session/end-seed' && object(e.data).inherited === true).map(e => Number(e.seq)));
|
|
201
|
-
let route = {};
|
|
202
|
-
let last;
|
|
203
|
-
for (const e of [...new Map(events.map(e => [Number(e.seq), e])).values()].sort((a, b) => Number(a.seq) - Number(b.seq))) {
|
|
204
|
-
const d = object(e.data);
|
|
205
|
-
if (e.type === 'request/context') {
|
|
206
|
-
route = d;
|
|
207
|
-
continue;
|
|
208
|
-
}
|
|
209
|
-
if (Number(e.seq) <= inheritedCut || e.type === 'session/end-seed')
|
|
210
|
-
continue;
|
|
211
|
-
if (e.type === 'llm/retry-started') {
|
|
212
|
-
if (last?.turn === d.turn && last?.step === d.step)
|
|
213
|
-
last = undefined;
|
|
214
|
-
continue;
|
|
215
|
-
}
|
|
216
|
-
const source = object(d.source ?? {});
|
|
217
|
-
const provider = String(source.provider ?? route.provider ?? '');
|
|
218
|
-
const model = String(source.model ?? route.model ?? '');
|
|
219
|
-
const time = typeof e.time === 'number' && Number.isFinite(e.time) && e.time >= 0 && e.time <= 8.64e15 ? e.time : undefined;
|
|
220
|
-
const u = object(d.usage ?? {});
|
|
221
|
-
const buckets = [u.inputTokens, u.outputTokens, u.cacheReadTokens ?? 0, u.cacheWriteTokens ?? 0];
|
|
222
|
-
const valid = buckets.every(n => typeof n === 'number' && Number.isSafeInteger(n) && n >= 0)
|
|
223
|
-
&& (u.totalTokens === undefined || typeof u.totalTokens === 'number' && Number.isSafeInteger(u.totalTokens) && u.totalTokens === buckets.reduce((sum, n) => sum + Number(n), 0));
|
|
224
|
-
const usage = valid ? { input: Number(buckets[0]), output: Number(buckets[1]), cacheRead: Number(buckets[2]), cacheWrite: Number(buckets[3]) } : undefined;
|
|
225
|
-
const index = last && d.turn !== null && d.step !== null && last.turn === d.turn && last.step === d.step ? last.index : charges.length;
|
|
226
|
-
const key = charges[index]?.key ?? String(e.seq);
|
|
227
|
-
if (!usage && charges[index]?.usage)
|
|
228
|
-
continue;
|
|
229
|
-
// Every scan reprices from the current configuration, so correcting prices.json updates
|
|
230
|
-
// stored totals instead of leaving the version an earlier scan happened to apply.
|
|
231
|
-
const selected = time === undefined ? lowestPrice(this.prices, provider, model) : priceAt(this.prices, provider, model, time);
|
|
232
|
-
const estimate = usage && selected ? (usage.input * selected.rates.input + usage.output * selected.rates.output + usage.cacheRead * selected.rates.cacheRead + usage.cacheWrite * selected.rates.cacheWrite) / 1e6 : undefined;
|
|
233
|
-
const amount = estimate !== undefined && Number.isFinite(estimate) ? estimate : undefined;
|
|
234
|
-
charges[index] = { key, time, provider, model, usage, price: selected?.price, amount,
|
|
235
|
-
...(time === undefined && amount !== undefined ? { estimated: true } : {}),
|
|
236
|
-
reason: !usage ? 'missing usage' : time === undefined ? 'missing timestamp, floor rate' : !selected ? 'no price version' : amount === undefined ? 'invalid estimate' : undefined };
|
|
237
|
-
last = { turn: d.turn, step: d.step, index };
|
|
238
|
-
}
|
|
239
|
-
const saved = { version: 1, sessionId, cut, charges };
|
|
240
|
-
if (this.directory) {
|
|
241
|
-
const prefix = createHash('sha256').update(sessionId).digest('hex') + '-';
|
|
242
|
-
const filename = `${prefix}${cut}.json`;
|
|
243
|
-
const temporary = join(this.directory, `${randomUUID()}.tmp`);
|
|
244
|
-
try {
|
|
245
|
-
await writeFile(temporary, JSON.stringify(saved) + '\n', { mode: 0o600, flag: 'wx' });
|
|
246
|
-
await rename(temporary, join(this.directory, filename));
|
|
247
|
-
}
|
|
248
|
-
finally {
|
|
249
|
-
await unlink(temporary).catch(error => { if (error.code !== 'ENOENT')
|
|
250
|
-
throw error; });
|
|
251
|
-
}
|
|
252
|
-
for (const name of await readdir(this.directory))
|
|
253
|
-
if (name.startsWith(prefix) && name.endsWith('.json') && Number(name.slice(prefix.length, -5)) < cut) {
|
|
254
|
-
await unlink(join(this.directory, name)).catch(error => { if (error.code !== 'ENOENT')
|
|
255
|
-
throw error; });
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
this.sessions.set(sessionId, saved);
|
|
259
|
-
this.totals.clear();
|
|
260
|
-
}
|
|
261
|
-
/** Whether this session has a complete cached scan.
|
|
262
|
-
* @param sessionId - Selected session identity.
|
|
263
|
-
* @returns True when a complete scan is available.
|
|
264
|
-
*/
|
|
265
|
-
hasSession(sessionId) { return sessionId !== undefined && this.sessions.has(sessionId); }
|
|
266
|
-
/** Describe unpriced model/usage combinations without exposing conversation content.
|
|
267
|
-
* @returns Unique reasons across cached sessions.
|
|
268
|
-
*/
|
|
269
|
-
missing() {
|
|
270
|
-
return [...new Set([...this.sessions.values()].flatMap(s => s.charges.filter(c => c.amount === undefined).map(c => `${c.provider}/${c.model}: ${c.reason}`)))];
|
|
271
|
-
}
|
|
272
|
-
/** Summarize cached requests across one session or Beijing calendar days.
|
|
273
|
-
* @param sessionId - Optional session restriction.
|
|
274
|
-
* @param days - Today or today plus the preceding two calendar days.
|
|
275
|
-
* @param now - Clock used for date attribution.
|
|
276
|
-
* @returns Known subtotal, unpriceable count, and estimated count; an estimated record always
|
|
277
|
-
* names an amount, but a dated range only adds the records it can place inside that range.
|
|
278
|
-
*/
|
|
279
|
-
total(sessionId, days, now = Date.now()) {
|
|
280
|
-
const cacheKey = JSON.stringify([sessionId, days, days ? costDay(now) : '']);
|
|
281
|
-
const cached = this.totals.get(cacheKey);
|
|
282
|
-
if (cached)
|
|
283
|
-
return cached;
|
|
284
|
-
const result = { amount: 0, unknown: 0, estimated: 0, records: 0 };
|
|
285
|
-
const end = costDay(now);
|
|
286
|
-
const start = costDay(now - ((days ?? 1) - 1) * 86400_000);
|
|
287
|
-
for (const session of this.sessions.values()) {
|
|
288
|
-
if (sessionId !== undefined && session.sessionId !== sessionId)
|
|
289
|
-
continue;
|
|
290
|
-
for (const charge of session.charges) {
|
|
291
|
-
if (days && charge.time !== undefined && (costDay(charge.time) < start || costDay(charge.time) > end))
|
|
292
|
-
continue;
|
|
293
|
-
result.records++;
|
|
294
|
-
if (charge.amount === undefined) {
|
|
295
|
-
result.unknown++;
|
|
296
|
-
continue;
|
|
297
|
-
}
|
|
298
|
-
const dated = days === undefined || charge.time !== undefined;
|
|
299
|
-
if (charge.estimated === true || !dated)
|
|
300
|
-
result.estimated++;
|
|
301
|
-
if (dated)
|
|
302
|
-
result.amount += charge.amount;
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
this.totals.set(cacheKey, result);
|
|
306
|
-
return result;
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
/** Compact estimates retain an asterisk whenever a subtotal is not exact.
|
|
310
|
-
* @param total - Summary from the ledger.
|
|
311
|
-
* @returns Yuan amount and incompleteness marker.
|
|
312
|
-
*/
|
|
313
|
-
export function costText(total) { return `~¥${total.amount.toFixed(4)}${total.unknown || total.estimated ? '*' : ''}`; }
|
package/dist/status.d.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import type { Controller } from './controller.ts';
|
|
2
|
-
import { type ObjectValue } from './wire.ts';
|
|
3
|
-
/** Format elapsed wall time, clamping clock skew instead of displaying negative durations.
|
|
4
|
-
* @param milliseconds - Elapsed duration.
|
|
5
|
-
* @returns Minute/second display, with hours when needed.
|
|
6
|
-
*/
|
|
7
|
-
export declare function elapsedTime(milliseconds: number): string;
|
|
8
|
-
/** Produce compact metadata lines without inferring missing provider measurements.
|
|
9
|
-
* @param values - Current host projection values.
|
|
10
|
-
* @param defaultModel - Host catalog default used before a session selects a route.
|
|
11
|
-
* @param running - Whether the current route or next route is primary.
|
|
12
|
-
* @returns Model, approximate context occupancy, and cumulative token buckets.
|
|
13
|
-
*/
|
|
14
|
-
export declare function metricLines(values: ObjectValue, defaultModel: ObjectValue | undefined, running: boolean): string[];
|
|
15
|
-
/** Fit the status summary to one terminal row; details remain available through /status.
|
|
16
|
-
* @param fields - Activity, model, cost, context, and cumulative usage groups in display order.
|
|
17
|
-
* @param width - Available terminal columns.
|
|
18
|
-
* @returns A terminal-safe single line, shortened by display width.
|
|
19
|
-
*/
|
|
20
|
-
export declare function compactStatus(fields: string[], width: number): string;
|
|
21
|
-
/** Render a live clock and selected-session metadata; the timer belongs to this mounted bar. */
|
|
22
|
-
export declare const StatusBar: import("react").NamedExoticComponent<{
|
|
23
|
-
controller: Controller;
|
|
24
|
-
expanded?: boolean;
|
|
25
|
-
width?: number;
|
|
26
|
-
revision?: number;
|
|
27
|
-
paused?: boolean;
|
|
28
|
-
}>;
|
package/dist/status.js
DELETED
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
/** Terminal status from host projections; cumulative usage and estimated context stay distinct. */
|
|
3
|
-
import { useTheme } from "./theme.js";
|
|
4
|
-
import { memo, useEffect, useState } from 'react';
|
|
5
|
-
import { Box, Text, useStdout } from 'ink';
|
|
6
|
-
import wrapAnsi from 'wrap-ansi';
|
|
7
|
-
import { costText } from "./cost.js";
|
|
8
|
-
import { toolLine } from "./transcript.js";
|
|
9
|
-
import { safeText } from "./wire.js";
|
|
10
|
-
/** Format elapsed wall time, clamping clock skew instead of displaying negative durations.
|
|
11
|
-
* @param milliseconds - Elapsed duration.
|
|
12
|
-
* @returns Minute/second display, with hours when needed.
|
|
13
|
-
*/
|
|
14
|
-
export function elapsedTime(milliseconds) {
|
|
15
|
-
const seconds = Math.max(0, Math.floor(milliseconds / 1000));
|
|
16
|
-
const minutes = Math.floor(seconds / 60);
|
|
17
|
-
if (minutes === 0)
|
|
18
|
-
return `${seconds}s`;
|
|
19
|
-
return `${minutes >= 60 ? `${Math.floor(minutes / 60)}h ` : ''}${minutes % 60}m ${seconds % 60}s`;
|
|
20
|
-
}
|
|
21
|
-
/** Produce compact metadata lines without inferring missing provider measurements.
|
|
22
|
-
* @param values - Current host projection values.
|
|
23
|
-
* @param defaultModel - Host catalog default used before a session selects a route.
|
|
24
|
-
* @param running - Whether the current route or next route is primary.
|
|
25
|
-
* @returns Model, approximate context occupancy, and cumulative token buckets.
|
|
26
|
-
*/
|
|
27
|
-
export function metricLines(values, defaultModel, running) {
|
|
28
|
-
const selection = record(values.modelSelection);
|
|
29
|
-
const current = running ? selection.lastUsed ?? selection.next ?? defaultModel : selection.next ?? selection.lastUsed ?? defaultModel;
|
|
30
|
-
const model = modelName(current);
|
|
31
|
-
const next = modelName(selection.next);
|
|
32
|
-
const pressure = record(values.contextPressure);
|
|
33
|
-
const used = numeric(pressure.projectedTokens) ?? numeric(pressure.pressureTokens);
|
|
34
|
-
const capacity = numeric(pressure.contextWindow);
|
|
35
|
-
const context = used !== undefined && capacity !== undefined && capacity > 0
|
|
36
|
-
? `~${Math.min(100, Math.round(used / capacity * 100))}% (${count(used)} / ${count(capacity)})`
|
|
37
|
-
: 'unknown';
|
|
38
|
-
const usage = record(values.tokenUsage);
|
|
39
|
-
const buckets = [usage.uncachedInputTokens, usage.outputTokens, usage.cacheReadTokens, usage.cacheWriteTokens].map(numeric);
|
|
40
|
-
const total = buckets.every(value => value !== undefined) ? buckets.reduce((a, b) => a + b, 0) : undefined;
|
|
41
|
-
return [
|
|
42
|
-
`Model: ${model}${running && next !== 'unknown' && next !== model ? ` · Next: ${next}` : ''}`,
|
|
43
|
-
`Context: ${context} · Tokens: ${count(total)} total`,
|
|
44
|
-
`In (uncached): ${count(buckets[0])} · Out: ${count(buckets[1])} · Cache read/write: ${count(buckets[2])}/${count(buckets[3])}`,
|
|
45
|
-
];
|
|
46
|
-
}
|
|
47
|
-
/** Fit the status summary to one terminal row; details remain available through /status.
|
|
48
|
-
* @param fields - Activity, model, cost, context, and cumulative usage groups in display order.
|
|
49
|
-
* @param width - Available terminal columns.
|
|
50
|
-
* @returns A terminal-safe single line, shortened by display width.
|
|
51
|
-
*/
|
|
52
|
-
export function compactStatus(fields, width) {
|
|
53
|
-
return compactStatusFields(fields, width).filter(Boolean).join(' ');
|
|
54
|
-
}
|
|
55
|
-
/** Keep group identities while fitting plain text; ANSI styling is applied only after layout. */
|
|
56
|
-
function compactStatusFields(fields, width) {
|
|
57
|
-
if (width <= 0)
|
|
58
|
-
return [];
|
|
59
|
-
const clean = fields.map(value => safeText(value).replace(/[\r\n\t]/g, ' '));
|
|
60
|
-
const join = () => clean.filter(Boolean).join(' ');
|
|
61
|
-
const fits = () => !wrapAnsi(join(), width, { hard: true, wordWrap: false, trim: false }).includes('\n');
|
|
62
|
-
if (fits())
|
|
63
|
-
return clean;
|
|
64
|
-
// Keep the stable activity column while it fits; reclaim it on narrow terminals.
|
|
65
|
-
clean[0] = clean[0]?.trimEnd() ?? '';
|
|
66
|
-
if (fits())
|
|
67
|
-
return clean;
|
|
68
|
-
clean[3] = (clean[3] ?? '').replace(/[█░]+ /u, '');
|
|
69
|
-
if (fits())
|
|
70
|
-
return clean;
|
|
71
|
-
clean[1] = toolLine(clean[1] ?? '', Math.max(8, Math.min(24, Math.floor(width / 4))));
|
|
72
|
-
if (fits())
|
|
73
|
-
return clean;
|
|
74
|
-
for (const index of [4, 2, 1, 3]) {
|
|
75
|
-
clean[index] = '';
|
|
76
|
-
if (fits())
|
|
77
|
-
return clean;
|
|
78
|
-
}
|
|
79
|
-
return [toolLine(join(), width)];
|
|
80
|
-
}
|
|
81
|
-
/** Compact token counts; one formatter is reused because construction dominates the format cost. */
|
|
82
|
-
const compactNumber = new Intl.NumberFormat('en', { notation: 'compact', maximumFractionDigits: 1 });
|
|
83
|
-
/** Render a live clock and selected-session metadata; the timer belongs to this mounted bar. */
|
|
84
|
-
export const StatusBar = memo(function StatusBar({ controller, expanded = false, width, paused = false }) {
|
|
85
|
-
const theme = useTheme();
|
|
86
|
-
const { stdout } = useStdout();
|
|
87
|
-
const [now, setNow] = useState(Date.now);
|
|
88
|
-
const running = controller.running;
|
|
89
|
-
const since = controller.workingSince;
|
|
90
|
-
useEffect(() => {
|
|
91
|
-
setNow(Date.now());
|
|
92
|
-
if (!running || paused)
|
|
93
|
-
return;
|
|
94
|
-
const timer = setInterval(() => setNow(Date.now()), 1000);
|
|
95
|
-
return () => clearInterval(timer);
|
|
96
|
-
}, [running, since, paused]);
|
|
97
|
-
const state = controller.state;
|
|
98
|
-
const workspace = state.workspaces.find(item => item.workspaceId === state.workspaceId);
|
|
99
|
-
const view = controller.telemetry.view(state.sessionId);
|
|
100
|
-
const costs = controller.costs;
|
|
101
|
-
const sessionCost = costs?.hasSession(state.sessionId) ? costText(costs.total(state.sessionId)) : '?';
|
|
102
|
-
const todayCost = costs ? costText(costs.total(undefined, 1, Date.now())) : '?';
|
|
103
|
-
// `*` belongs to costText alone; incomplete coverage is a separate degradation, reported by `!`.
|
|
104
|
-
const coverage = costs?.coverage ?? 'complete';
|
|
105
|
-
const label = workspace ? `${workspace.title} · ${workspace.path}` : 'none selected';
|
|
106
|
-
if (!expanded) {
|
|
107
|
-
const selection = record(view.values.modelSelection);
|
|
108
|
-
const route = record(running ? selection.lastUsed ?? selection.next ?? state.defaultModel : selection.next ?? selection.lastUsed ?? state.defaultModel);
|
|
109
|
-
const model = typeof route.model === 'string' ? `${route.model.replace(/^deepseek-/, '')}${typeof route.reasoningEffort === 'string' ? ` · ${route.reasoningEffort}` : ''}` : 'model ?';
|
|
110
|
-
const pressure = record(view.values.contextPressure);
|
|
111
|
-
const used = numeric(pressure.projectedTokens) ?? numeric(pressure.pressureTokens);
|
|
112
|
-
const capacity = numeric(pressure.contextWindow);
|
|
113
|
-
const percent = used !== undefined && capacity !== undefined && capacity > 0 ? Math.min(100, Math.round(used / capacity * 100)) : undefined;
|
|
114
|
-
const filled = percent === undefined ? 0 : Math.round(percent / 10);
|
|
115
|
-
const context = percent === undefined ? 'ctx ?' : `${'█'.repeat(filled)}${'░'.repeat(10 - filled)} ~${percent}%`;
|
|
116
|
-
const usage = record(view.values.tokenUsage);
|
|
117
|
-
const buckets = [usage.uncachedInputTokens, usage.outputTokens, usage.cacheReadTokens, usage.cacheWriteTokens].map(numeric);
|
|
118
|
-
const total = buckets.every(value => value !== undefined) ? buckets.reduce((a, b) => a + b, 0) : undefined;
|
|
119
|
-
const compactCount = (value) => value === undefined ? '?' : compactNumber.format(value);
|
|
120
|
-
const activity = running ? `◐ Working · ${since === undefined ? '?' : elapsedTime(now - since)}${state.transcript.activeTurnStartedAt === undefined ? '~' : ''} · Ctrl+C Stop` : '● Ready';
|
|
121
|
-
const warning = state.controlError || state.modelError || coverage === 'partial' ? '! ' : '';
|
|
122
|
-
const fields = [
|
|
123
|
-
`${warning}${!state.online ? 'Offline · ' : ''}${activity}`.padEnd(31),
|
|
124
|
-
model,
|
|
125
|
-
costs ? `${costs.hasSession(state.sessionId) ? compactCost(costs.total(state.sessionId)) : '?'}/${compactCost(costs.total(undefined, 1, Date.now()))}` : '?/?',
|
|
126
|
-
context,
|
|
127
|
-
`${count(numeric(record(view.values.sessionStats).turns))} turns · ${compactCount(total)} tok`,
|
|
128
|
-
];
|
|
129
|
-
const fitted = compactStatusFields(fields, width ?? Math.max(1, (stdout.columns ?? 80) - 2));
|
|
130
|
-
const colors = [
|
|
131
|
-
!state.online ? theme.status.offline : warning ? theme.status.warning : running ? theme.status.working : theme.status.ready,
|
|
132
|
-
theme.status.model, costs ? theme.status.cost : theme.status.usage,
|
|
133
|
-
percent === undefined ? theme.status.usage : percent >= 95 ? theme.status.critical : percent >= 80 ? theme.status.warning : theme.status.context,
|
|
134
|
-
theme.status.usage,
|
|
135
|
-
];
|
|
136
|
-
return _jsx(Text, { wrap: "truncate-end", children: fitted.map((text, index) => text ? _jsxs(Text, { children: [fitted.slice(0, index).some(Boolean) && _jsx(Text, { color: theme.colors.muted, children: ' ' }), _jsx(Text, { color: colors[index], bold: index === 0, children: text })] }, index) : null) });
|
|
137
|
-
}
|
|
138
|
-
return _jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.border, paddingX: 1, children: [_jsxs(Text, { color: running ? theme.colors.context : theme.colors.muted, children: [running
|
|
139
|
-
? `◐ Working · ${since === undefined ? 'unknown duration' : elapsedTime(now - since)}${state.transcript.activeTurnStartedAt === undefined ? ' (observed)' : ''} · Ctrl+C Stop`
|
|
140
|
-
: '● Ready · Ctrl+C exit', !state.online ? ' · disconnected, last known status' : ''] }), _jsxs(Text, { wrap: "truncate-end", children: ["Host: ", safeText(controller.base), " \u00B7 ", safeText(state.status)] }), state.sessionId && _jsxs(Text, { children: ["Session ID: ", safeText(state.sessionId)] }), controller.sessionMode && _jsxs(Text, { children: ["Mode: ", safeText(controller.sessionMode)] }), _jsxs(Text, { wrap: "truncate-end", children: ["Workspace: ", safeText(label)] }), metricLines(view.values, state.defaultModel, running).map((line, index) => _jsx(Text, { dimColor: true, children: safeText(line) }, index)), costs && _jsxs(Text, { dimColor: true, children: ["Cost (CNY estimate): Session ", sessionCost, " \u00B7 Today ", todayCost] }), costs && coverage === 'partial' && _jsxs(Text, { color: theme.colors.context, children: ["Cost coverage incomplete: ", costs.error ? safeText(costs.error) : 'no complete scan yet'] }), _jsxs(Text, { dimColor: true, children: ["Turns: ", count(numeric(record(view.values.sessionStats).turns))] }), _jsxs(Text, { dimColor: true, children: ["Queued: ", count(view.queued), " \u00B7 Active jobs: ", count(view.jobs)] }), state.controlError && _jsx(Text, { color: theme.colors.context, children: safeText(state.controlError) }), state.presetError && _jsxs(Text, { color: theme.colors.context, children: ["Preset names unavailable: ", safeText(state.presetError)] }), state.modelError && _jsxs(Text, { color: theme.colors.context, children: ["Model catalog unavailable: ", safeText(state.modelError)] })] });
|
|
141
|
-
});
|
|
142
|
-
function record(value) {
|
|
143
|
-
return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
144
|
-
}
|
|
145
|
-
function numeric(value) {
|
|
146
|
-
return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
147
|
-
}
|
|
148
|
-
function count(value) { return value === undefined ? '?' : value.toLocaleString('en-US'); }
|
|
149
|
-
function modelName(value) {
|
|
150
|
-
const model = record(value);
|
|
151
|
-
if (typeof model.model !== 'string' || typeof model.provider !== 'string')
|
|
152
|
-
return 'unknown';
|
|
153
|
-
return safeText(`${model.provider}/${model.model}${typeof model.reasoningEffort === 'string' ? ` (${model.reasoningEffort})` : ''}`);
|
|
154
|
-
}
|
|
155
|
-
function compactCost(total) {
|
|
156
|
-
return `~¥${total.amount.toFixed(2)}${total.unknown || total.estimated ? '*' : ''}`;
|
|
157
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|