@itookit/dsht 0.5.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -2
- package/README.zh.md +6 -2
- package/dist/cli/dsht.js +18 -1
- package/dist/contracts.d.ts +22 -0
- package/dist/controller/controller.d.ts +3 -1
- package/dist/controller/controller.js +5 -0
- package/dist/controller/loop-prompts-schema.d.ts +16 -2
- package/dist/controller/loop-prompts-schema.js +106 -27
- package/dist/controller/loop-prompts.d.ts +17 -2
- package/dist/controller/loop-prompts.generated.js +2 -1
- package/dist/controller/loop-prompts.js +35 -9
- package/dist/controller/loop-protocols.d.ts +3 -1
- package/dist/controller/loop-protocols.js +8 -3
- package/dist/controller/loop-source.d.ts +74 -0
- package/dist/controller/loop-source.js +224 -0
- package/dist/cost/controller.d.ts +2 -0
- package/dist/cost/controller.js +14 -2
- package/dist/cost/index.d.ts +1 -1
- package/dist/cost/index.js +1 -1
- package/dist/cost/ledger-files.d.ts +20 -0
- package/dist/cost/ledger-files.js +115 -15
- package/dist/cost/ledger.d.ts +31 -6
- package/dist/cost/ledger.js +74 -22
- package/dist/cost/pricing.d.ts +39 -0
- package/dist/cost/pricing.js +46 -0
- package/dist/cost/types.d.ts +9 -3
- package/dist/storage/files.d.ts +8 -0
- package/dist/storage/files.js +18 -1
- package/dist/storage/index.d.ts +1 -1
- package/dist/storage/index.js +1 -1
- package/dist/ui/app.js +3 -1
- package/dist/ui/dialogs/cost.d.ts +6 -0
- package/dist/ui/dialogs/cost.js +5 -1
- package/dist/ui/dialogs/loop.d.ts +5 -4
- package/dist/ui/dialogs/loop.js +14 -6
- package/loop.yaml +230 -0
- package/package.json +5 -4
package/dist/cost/ledger.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { chargeFor, costDay, DEFAULT_PRICES, pricesDigest, PRICING_ENGINE_VERSION } from "./pricing.js";
|
|
1
|
+
import { chargeFor, costDay, costMonthStart, costWeekStart, costWindowStart, DEFAULT_PRICES, pricesDigest, PRICING_ENGINE_VERSION } from "./pricing.js";
|
|
2
2
|
import { foldSamples } from "./records.js";
|
|
3
|
-
import { loadLedgers, saveLedger } from "./ledger-files.js";
|
|
3
|
+
import { loadLedgers, pruneLedgers, saveLedger } from "./ledger-files.js";
|
|
4
4
|
/** Reasons one slice keeps at most, so a broken table cannot grow the ledger without bound. */
|
|
5
5
|
const UNPRICED_LIMIT = 8;
|
|
6
|
-
/** Per-origin cache of folded session totals; every scan replaces a session at its cut. */
|
|
6
|
+
/** Per-origin cache of folded session totals and day buckets; every scan replaces a session at its cut. */
|
|
7
7
|
export class CostLedger {
|
|
8
8
|
prices;
|
|
9
9
|
directory;
|
|
@@ -34,10 +34,19 @@ export class CostLedger {
|
|
|
34
34
|
return 'partial';
|
|
35
35
|
return this.scannedAt !== undefined || this.sessions.size > 0 ? 'complete' : 'partial';
|
|
36
36
|
}
|
|
37
|
-
/** Load the newest cut per session; a stored total is read as it was decided.
|
|
38
|
-
|
|
37
|
+
/** Load the newest cut per session; a stored total is read as it was decided.
|
|
38
|
+
*
|
|
39
|
+
* Slices and dead files that have left the retention window are deleted in the same pass, so the
|
|
40
|
+
* directory cannot grow with every session this client has ever seen. A slice is a projection of the
|
|
41
|
+
* host log, so letting one go costs a rescan of that session, never data.
|
|
42
|
+
* @param now - Clock that names the window, so a caller or a test can pin the boundary.
|
|
43
|
+
*/
|
|
44
|
+
async load(now = Date.now()) {
|
|
39
45
|
this.totals.clear();
|
|
40
46
|
this.sessions = await loadLedgers(this.directory);
|
|
47
|
+
for (const sessionId of await pruneLedgers(this.directory, costWindowStart(now), this.sessions)) {
|
|
48
|
+
this.sessions.delete(sessionId);
|
|
49
|
+
}
|
|
41
50
|
}
|
|
42
51
|
/** Replace one session using all billing events through the opening snapshot cut.
|
|
43
52
|
*
|
|
@@ -54,16 +63,28 @@ export class CostLedger {
|
|
|
54
63
|
const current = this.sessions.get(sessionId);
|
|
55
64
|
if ((current?.cut ?? -2) > cut)
|
|
56
65
|
return;
|
|
57
|
-
const
|
|
66
|
+
const scanDay = costDay(now);
|
|
67
|
+
const floor = costWindowStart(now);
|
|
58
68
|
const total = { amount: 0, unknown: 0, records: 0 };
|
|
59
|
-
const
|
|
69
|
+
const days = new Map();
|
|
60
70
|
const unpriced = new Set();
|
|
71
|
+
/** The day bucket to add to, created on first use so a session reports only the days it touched. */
|
|
72
|
+
const dayBucket = (day) => {
|
|
73
|
+
const found = days.get(day);
|
|
74
|
+
if (found !== undefined)
|
|
75
|
+
return found;
|
|
76
|
+
const created = { day, amount: 0, unknown: 0, records: 0 };
|
|
77
|
+
days.set(day, created);
|
|
78
|
+
return created;
|
|
79
|
+
};
|
|
61
80
|
for (const sample of foldSamples(events)) {
|
|
62
81
|
const decision = chargeFor(this.prices, sample.provider, sample.model, sample.time, sample.usage);
|
|
63
|
-
// A request belongs to the day it settled on
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
const
|
|
82
|
+
// A request belongs to the day it settled on. One with no settlement time is attributed to the
|
|
83
|
+
// day of this scan, which is the only day it can reach without guessing a tariff band, and is
|
|
84
|
+
// where a reader looks for what makes today's total inexact.
|
|
85
|
+
const day = sample.time === undefined ? scanDay : costDay(sample.time);
|
|
86
|
+
// Only the window is stored, so a session a year old writes days, not a year of them.
|
|
87
|
+
const buckets = day < floor ? [total] : [total, dayBucket(day)];
|
|
67
88
|
for (const bucket of buckets) {
|
|
68
89
|
bucket.records++;
|
|
69
90
|
if (decision.amount === undefined)
|
|
@@ -74,8 +95,8 @@ export class CostLedger {
|
|
|
74
95
|
if (decision.amount === undefined && unpriced.size < UNPRICED_LIMIT)
|
|
75
96
|
unpriced.add(`${sample.provider}/${sample.model}: ${decision.reason}`);
|
|
76
97
|
}
|
|
77
|
-
const saved = { version:
|
|
78
|
-
total, day:
|
|
98
|
+
const saved = { version: 4, sessionId, cut, engine: PRICING_ENGINE_VERSION, catalog: this.catalog,
|
|
99
|
+
total, days: [...days.values()].sort((a, b) => a.day < b.day ? -1 : a.day > b.day ? 1 : 0), unpriced: [...unpriced] };
|
|
79
100
|
// Another process may have persisted a newer cut of this session since it was last read.
|
|
80
101
|
if (this.directory && !await saveLedger(this.directory, saved))
|
|
81
102
|
return;
|
|
@@ -117,25 +138,56 @@ export class CostLedger {
|
|
|
117
138
|
}
|
|
118
139
|
/** Every session's requests on one Beijing calendar day.
|
|
119
140
|
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
141
|
+
* The day is a real bucket rather than only the day a scan ran on, so asking for another day
|
|
142
|
+
* reports that day's spend; the retention window is what limits how far back the answer reaches.
|
|
122
143
|
* @param now - Clock that names the day to report.
|
|
123
144
|
* @returns The day's total across cached sessions.
|
|
124
145
|
*/
|
|
125
146
|
today(now = Date.now()) {
|
|
126
147
|
const day = costDay(now);
|
|
127
|
-
|
|
148
|
+
return this.period(day, day);
|
|
149
|
+
}
|
|
150
|
+
/** Every session's requests in the natural week containing a day, through that day.
|
|
151
|
+
* @param now - Clock that names the day to report.
|
|
152
|
+
* @returns The week-to-date total across cached sessions.
|
|
153
|
+
*/
|
|
154
|
+
week(now = Date.now()) {
|
|
155
|
+
const day = costDay(now);
|
|
156
|
+
return this.period(costWeekStart(day), day);
|
|
157
|
+
}
|
|
158
|
+
/** Every session's requests in the natural month containing a day, through that day.
|
|
159
|
+
* @param now - Clock that names the day to report.
|
|
160
|
+
* @returns The month-to-date total across cached sessions.
|
|
161
|
+
*/
|
|
162
|
+
month(now = Date.now()) {
|
|
163
|
+
const day = costDay(now);
|
|
164
|
+
return this.period(costMonthStart(day), day);
|
|
165
|
+
}
|
|
166
|
+
/** Sum stored day buckets over a Beijing day range, inclusive at both ends.
|
|
167
|
+
*
|
|
168
|
+
* Days outside the retention window are absent rather than zero, so a range wider than the window
|
|
169
|
+
* reports what is still kept — which is why the fold and this sum share one window constant.
|
|
170
|
+
* @param from - First day to include, YYYY-MM-DD.
|
|
171
|
+
* @param to - Last day to include, YYYY-MM-DD.
|
|
172
|
+
* @returns The range's total across cached sessions.
|
|
173
|
+
*/
|
|
174
|
+
period(from, to) {
|
|
175
|
+
const key = `p:${from}..${to}`;
|
|
176
|
+
const cached = this.totals.get(key);
|
|
128
177
|
if (cached)
|
|
129
178
|
return cached;
|
|
130
179
|
const result = { amount: 0, unknown: 0, records: 0 };
|
|
131
180
|
for (const session of this.sessions.values()) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
181
|
+
for (const day of session.days) {
|
|
182
|
+
// ISO day strings compare chronologically, so a lexical range is the day range.
|
|
183
|
+
if (day.day < from || day.day > to)
|
|
184
|
+
continue;
|
|
185
|
+
result.amount += day.amount;
|
|
186
|
+
result.unknown += day.unknown;
|
|
187
|
+
result.records += day.records;
|
|
188
|
+
}
|
|
137
189
|
}
|
|
138
|
-
this.totals.set(
|
|
190
|
+
this.totals.set(key, result);
|
|
139
191
|
return result;
|
|
140
192
|
}
|
|
141
193
|
}
|
package/dist/cost/pricing.d.ts
CHANGED
|
@@ -29,6 +29,45 @@ export declare function pricesFrom(value: unknown): PriceVersion[];
|
|
|
29
29
|
* @returns Beijing calendar date, YYYY-MM-DD.
|
|
30
30
|
*/
|
|
31
31
|
export declare function costDay(time: number): string;
|
|
32
|
+
/** Beijing days of cost the ledger keeps, folds and reports.
|
|
33
|
+
*
|
|
34
|
+
* One number decides three things that have to agree: how much per-session history a slice stores,
|
|
35
|
+
* which slices a retention pass may delete, and how far back the day/week/month totals reach. Sixty
|
|
36
|
+
* days covers a natural month with room to compare it against the previous one.
|
|
37
|
+
*/
|
|
38
|
+
export declare const COST_WINDOW_DAYS = 60;
|
|
39
|
+
/** First millisecond of a Beijing calendar day.
|
|
40
|
+
*
|
|
41
|
+
* China has had no daylight saving since 1991, so a Beijing day is exactly 24 hours and an explicit
|
|
42
|
+
* offset is exact rather than an approximation.
|
|
43
|
+
* @param day - Beijing calendar date, YYYY-MM-DD.
|
|
44
|
+
* @returns Epoch milliseconds.
|
|
45
|
+
*/
|
|
46
|
+
export declare function costDayStart(day: string): number;
|
|
47
|
+
/** The Beijing day a whole number of days before another.
|
|
48
|
+
*
|
|
49
|
+
* The arithmetic is on the calendar date, not on an instant, so it cannot drift across a month, a
|
|
50
|
+
* year or a leap day.
|
|
51
|
+
* @param day - Beijing calendar date, YYYY-MM-DD.
|
|
52
|
+
* @param days - Days to subtract, zero or more.
|
|
53
|
+
* @returns Beijing calendar date, YYYY-MM-DD.
|
|
54
|
+
*/
|
|
55
|
+
export declare function costDaysBefore(day: string, days: number): string;
|
|
56
|
+
/** The Monday that begins the natural week containing a Beijing day.
|
|
57
|
+
* @param day - Beijing calendar date, YYYY-MM-DD.
|
|
58
|
+
* @returns Beijing calendar date of that week's Monday.
|
|
59
|
+
*/
|
|
60
|
+
export declare function costWeekStart(day: string): string;
|
|
61
|
+
/** The first day of the natural month containing a Beijing day.
|
|
62
|
+
* @param day - Beijing calendar date, YYYY-MM-DD.
|
|
63
|
+
* @returns Beijing calendar date, YYYY-MM-01.
|
|
64
|
+
*/
|
|
65
|
+
export declare function costMonthStart(day: string): string;
|
|
66
|
+
/** Oldest Beijing day a ledger still keeps, so every retention decision uses one boundary.
|
|
67
|
+
* @param now - Clock that names the current day.
|
|
68
|
+
* @returns Beijing calendar date, YYYY-MM-DD.
|
|
69
|
+
*/
|
|
70
|
+
export declare function costWindowStart(now: number): string;
|
|
32
71
|
/** Canonical form of a model name before matching.
|
|
33
72
|
*
|
|
34
73
|
* The host reports names that differ from the published table by width, case, surrounding space, or
|
package/dist/cost/pricing.js
CHANGED
|
@@ -117,6 +117,52 @@ export function pricesFrom(value) {
|
|
|
117
117
|
* @returns Beijing calendar date, YYYY-MM-DD.
|
|
118
118
|
*/
|
|
119
119
|
export function costDay(time) { return new Date(time + 8 * 3600_000).toISOString().slice(0, 10); }
|
|
120
|
+
/** Beijing days of cost the ledger keeps, folds and reports.
|
|
121
|
+
*
|
|
122
|
+
* One number decides three things that have to agree: how much per-session history a slice stores,
|
|
123
|
+
* which slices a retention pass may delete, and how far back the day/week/month totals reach. Sixty
|
|
124
|
+
* days covers a natural month with room to compare it against the previous one.
|
|
125
|
+
*/
|
|
126
|
+
export const COST_WINDOW_DAYS = 60;
|
|
127
|
+
/** First millisecond of a Beijing calendar day.
|
|
128
|
+
*
|
|
129
|
+
* China has had no daylight saving since 1991, so a Beijing day is exactly 24 hours and an explicit
|
|
130
|
+
* offset is exact rather than an approximation.
|
|
131
|
+
* @param day - Beijing calendar date, YYYY-MM-DD.
|
|
132
|
+
* @returns Epoch milliseconds.
|
|
133
|
+
*/
|
|
134
|
+
export function costDayStart(day) { return Date.parse(`${day}T00:00:00+08:00`); }
|
|
135
|
+
/** The Beijing day a whole number of days before another.
|
|
136
|
+
*
|
|
137
|
+
* The arithmetic is on the calendar date, not on an instant, so it cannot drift across a month, a
|
|
138
|
+
* year or a leap day.
|
|
139
|
+
* @param day - Beijing calendar date, YYYY-MM-DD.
|
|
140
|
+
* @param days - Days to subtract, zero or more.
|
|
141
|
+
* @returns Beijing calendar date, YYYY-MM-DD.
|
|
142
|
+
*/
|
|
143
|
+
export function costDaysBefore(day, days) {
|
|
144
|
+
const [year, month, date] = day.split('-').map(Number);
|
|
145
|
+
return new Date(Date.UTC(year, month - 1, date - days)).toISOString().slice(0, 10);
|
|
146
|
+
}
|
|
147
|
+
/** The Monday that begins the natural week containing a Beijing day.
|
|
148
|
+
* @param day - Beijing calendar date, YYYY-MM-DD.
|
|
149
|
+
* @returns Beijing calendar date of that week's Monday.
|
|
150
|
+
*/
|
|
151
|
+
export function costWeekStart(day) {
|
|
152
|
+
const [year, month, date] = day.split('-').map(Number);
|
|
153
|
+
const weekday = new Date(Date.UTC(year, month - 1, date)).getUTCDay();
|
|
154
|
+
return costDaysBefore(day, (weekday + 6) % 7);
|
|
155
|
+
}
|
|
156
|
+
/** The first day of the natural month containing a Beijing day.
|
|
157
|
+
* @param day - Beijing calendar date, YYYY-MM-DD.
|
|
158
|
+
* @returns Beijing calendar date, YYYY-MM-01.
|
|
159
|
+
*/
|
|
160
|
+
export function costMonthStart(day) { return `${day.slice(0, 8)}01`; }
|
|
161
|
+
/** Oldest Beijing day a ledger still keeps, so every retention decision uses one boundary.
|
|
162
|
+
* @param now - Clock that names the current day.
|
|
163
|
+
* @returns Beijing calendar date, YYYY-MM-DD.
|
|
164
|
+
*/
|
|
165
|
+
export function costWindowStart(now) { return costDaysBefore(costDay(now), COST_WINDOW_DAYS - 1); }
|
|
120
166
|
/** Canonical form of a model name before matching.
|
|
121
167
|
*
|
|
122
168
|
* The host reports names that differ from the published table by width, case, surrounding space, or
|
package/dist/cost/types.d.ts
CHANGED
|
@@ -52,7 +52,7 @@ export interface CostTotal {
|
|
|
52
52
|
unknown: number;
|
|
53
53
|
records: number;
|
|
54
54
|
}
|
|
55
|
-
/** One Beijing calendar day's requests, kept
|
|
55
|
+
/** One Beijing calendar day's requests, kept for every day inside the retention window. */
|
|
56
56
|
export interface DayTotal extends CostTotal {
|
|
57
57
|
day: string;
|
|
58
58
|
}
|
|
@@ -64,14 +64,20 @@ export interface DayTotal extends CostTotal {
|
|
|
64
64
|
* rules and the table behind these totals, so a process holding an older one cannot overwrite them.
|
|
65
65
|
*/
|
|
66
66
|
export interface SavedCost {
|
|
67
|
-
version:
|
|
67
|
+
version: 4;
|
|
68
68
|
sessionId: string;
|
|
69
69
|
/** Durable sequence the fold reached; a scan that opened an older cut may not replace this slice. */
|
|
70
70
|
cut: number;
|
|
71
71
|
engine: number;
|
|
72
72
|
catalog: string;
|
|
73
73
|
total: CostTotal;
|
|
74
|
-
day
|
|
74
|
+
/** One entry per Beijing day this session spent inside the retention window, oldest first.
|
|
75
|
+
*
|
|
76
|
+
* Days older than the window are dropped as the slice is written, so the file cannot grow with a
|
|
77
|
+
* session's history. A day a later scan no longer reports is simply gone from the next slice: the
|
|
78
|
+
* host log, not this file, is what a day's total is re-derived from.
|
|
79
|
+
*/
|
|
80
|
+
days: DayTotal[];
|
|
75
81
|
/** Distinct reasons an amount is missing, bounded, so a panel can say what makes a total inexact. */
|
|
76
82
|
unpriced: string[];
|
|
77
83
|
}
|
package/dist/storage/files.d.ts
CHANGED
|
@@ -3,6 +3,14 @@
|
|
|
3
3
|
* @returns File contents, or undefined when the file does not exist.
|
|
4
4
|
*/
|
|
5
5
|
export declare function readText(path: string): Promise<string | undefined>;
|
|
6
|
+
/** Last modification time of a file, without reading it.
|
|
7
|
+
*
|
|
8
|
+
* Used where only a file's age matters, so a file whose contents this build cannot parse is still
|
|
9
|
+
* judgeable — a generation of ledger written by an older build has to age out like any other.
|
|
10
|
+
* @param path - Absolute file path.
|
|
11
|
+
* @returns Epoch milliseconds, or undefined when the file does not exist.
|
|
12
|
+
*/
|
|
13
|
+
export declare function modifiedAt(path: string): Promise<number | undefined>;
|
|
6
14
|
/** Read a user-private file, rejecting a symlink, a foreign owner, or group/other access.
|
|
7
15
|
* @param path - Absolute file path.
|
|
8
16
|
* @param label - Name used in the validation error, such as `Cookie file`.
|
package/dist/storage/files.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Every filesystem read and write the client performs; no other module opens a file. */
|
|
2
2
|
import { constants } from 'node:fs';
|
|
3
|
-
import { open, readFile, rename, unlink, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { open, readFile, rename, stat, unlink, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { randomUUID } from 'node:crypto';
|
|
5
5
|
import { dirname, join } from 'node:path';
|
|
6
6
|
/** Read a UTF-8 file; an absent file is an ordinary result, not a failure.
|
|
@@ -17,6 +17,23 @@ export async function readText(path) {
|
|
|
17
17
|
throw error;
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
|
+
/** Last modification time of a file, without reading it.
|
|
21
|
+
*
|
|
22
|
+
* Used where only a file's age matters, so a file whose contents this build cannot parse is still
|
|
23
|
+
* judgeable — a generation of ledger written by an older build has to age out like any other.
|
|
24
|
+
* @param path - Absolute file path.
|
|
25
|
+
* @returns Epoch milliseconds, or undefined when the file does not exist.
|
|
26
|
+
*/
|
|
27
|
+
export async function modifiedAt(path) {
|
|
28
|
+
try {
|
|
29
|
+
return (await stat(path)).mtimeMs;
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
if (isMissing(error))
|
|
33
|
+
return undefined;
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
20
37
|
/** Read a user-private file, rejecting a symlink, a foreign owner, or group/other access.
|
|
21
38
|
* @param path - Absolute file path.
|
|
22
39
|
* @param label - Name used in the validation error, such as `Cookie file`.
|
package/dist/storage/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
/** Storage domain: every filesystem operation the client performs. */
|
|
2
|
-
export { appendPrivateFile, createPrivateFile, readPrivateFile, readText, removeFile, renameFile, writeExclusiveStream, writePrivateFile } from './files.ts';
|
|
2
|
+
export { appendPrivateFile, createPrivateFile, modifiedAt, readPrivateFile, readText, removeFile, renameFile, writeExclusiveStream, writePrivateFile } from './files.ts';
|
|
3
3
|
export { ensureDirectory, ensurePrivateDirectory, listEntries } from './directories.ts';
|
|
4
4
|
export { heapSnapshotName, writeHeapSnapshot } from './heap-snapshot.ts';
|
package/dist/storage/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
/** Storage domain: every filesystem operation the client performs. */
|
|
2
|
-
export { appendPrivateFile, createPrivateFile, readPrivateFile, readText, removeFile, renameFile, writeExclusiveStream, writePrivateFile } from "./files.js";
|
|
2
|
+
export { appendPrivateFile, createPrivateFile, modifiedAt, readPrivateFile, readText, removeFile, renameFile, writeExclusiveStream, writePrivateFile } from "./files.js";
|
|
3
3
|
export { ensureDirectory, ensurePrivateDirectory, listEntries } from "./directories.js";
|
|
4
4
|
export { heapSnapshotName, writeHeapSnapshot } from "./heap-snapshot.js";
|
package/dist/ui/app.js
CHANGED
|
@@ -1029,6 +1029,8 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = m
|
|
|
1029
1029
|
return {
|
|
1030
1030
|
...(session === undefined ? {} : { session: line(session) }),
|
|
1031
1031
|
today: line(ledger.today()),
|
|
1032
|
+
week: line(ledger.week()),
|
|
1033
|
+
month: line(ledger.month()),
|
|
1032
1034
|
scanning: ledger.scanning, coverage: ledger.coverage,
|
|
1033
1035
|
...(ledger.scannedAt === undefined ? {} : { scannedAt: ledger.scannedAt }),
|
|
1034
1036
|
customPrices: ledger.customPrices,
|
|
@@ -1057,5 +1059,5 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = m
|
|
|
1057
1059
|
? '↑ ↓ move · Space / 1–9 toggle · Enter confirm · Esc dismisses' : '↑ ↓ / 1–9 select · Enter confirm · Esc dismisses' })] }), _jsx(Text, { dimColor: true, children: question ? options.length > 0
|
|
1058
1060
|
? 'Choose "Other answer" to type · the draft is kept while this is open'
|
|
1059
1061
|
: 'Esc dismisses the question · the draft is kept while this is open'
|
|
1060
|
-
: 'Esc keeps this pending · the draft is kept while this is open' })] })] }) }), !queueOpen && !pending && state.screen === 'chat' && queued.length > 0 && _jsx(QueuedPreview, { queued: queued, width: width }), _jsx(TextInput, { value: input, onChange: setInput, onCursorChange: setCursor, onSubmit: () => { void submit(input); }, reservedKeys: approvalKeysActive ? ['1', '2', '3'] : questionKeysActive ? ['1', '2', '3', '4', '5', '6', '7', '8', '9', ...(question?.multiSelect === true ? [' '] : [])] : !removal && !models && !searchResults && (state.screen === 'workspaces' || state.screen === 'sessions') ? ['d'] : surfaceReservedKeys.length ? surfaceReservedKeys : undefined, width: draftWidth, maxRows: composerRows, promptColor: answerPending ? theme.colors.muted : theme.accent, focus: !copyMode && !answerPending && !loopForm, placeholder: state.screen === 'path' ? 'Absolute directory path on host' : 'Message, @host-file, or /help' }), referenceOpen && _jsx(ReferenceMenu, { matches: matches, index: referenceIndex }), loopMenuOpen && _jsx(LoopMenu, { records: loopCandidates, index: loopMenuCursor })] }), commandSuggestions && _jsx(Text, { dimColor: true, children: commandSuggestions.join(' ') }), commandHint && _jsxs(Text, { dimColor: true, children: [_jsxs(Text, { color: theme.accent, children: [commandHint.command, commandHint.usage === undefined ? '' : ` ${commandHint.usage}`] }), " \u00B7 ", commandHint.description] }), help && _jsx(HelpPanel, { page: currentHelpPage, pages: helpPages, pageSize: helpPageSize }), costExpanded && _jsx(CostPanel, { source: costSource }), _jsx(Frozen, { frozen: statusFrozen, identity: `${width}:${state.sessionId}:${statusExpanded}:${statusScroll}:${pauseReason ?? ''}`, children: _jsx(StatusBar, { source: statusSource, width: width, expanded: statusExpanded, scroll: statusScroll, pageSize: statusViewRows, onScroll: setStatusScroll, onOverflow: setStatusOverflow, onRows: setStatusBarRows, pauseReason: pauseReason, revision: state.version }) })] })] }) }) }) });
|
|
1062
|
+
: 'Esc keeps this pending · the draft is kept while this is open' })] })] }) }), !queueOpen && !pending && state.screen === 'chat' && queued.length > 0 && _jsx(QueuedPreview, { queued: queued, width: width }), _jsx(TextInput, { value: input, onChange: setInput, onCursorChange: setCursor, onSubmit: () => { void submit(input); }, reservedKeys: approvalKeysActive ? ['1', '2', '3'] : questionKeysActive ? ['1', '2', '3', '4', '5', '6', '7', '8', '9', ...(question?.multiSelect === true ? [' '] : [])] : !removal && !models && !searchResults && (state.screen === 'workspaces' || state.screen === 'sessions') ? ['d'] : surfaceReservedKeys.length ? surfaceReservedKeys : undefined, width: draftWidth, maxRows: composerRows, promptColor: answerPending ? theme.colors.muted : theme.accent, focus: !copyMode && !answerPending && !loopForm, placeholder: state.screen === 'path' ? 'Absolute directory path on host' : 'Message, @host-file, or /help' }), referenceOpen && _jsx(ReferenceMenu, { matches: matches, index: referenceIndex }), loopMenuOpen && _jsx(LoopMenu, { records: loopCandidates, index: loopMenuCursor, source: controller.queries.loopSource })] }), commandSuggestions && _jsx(Text, { dimColor: true, children: commandSuggestions.join(' ') }), commandHint && _jsxs(Text, { dimColor: true, children: [_jsxs(Text, { color: theme.accent, children: [commandHint.command, commandHint.usage === undefined ? '' : ` ${commandHint.usage}`] }), " \u00B7 ", commandHint.description] }), help && _jsx(HelpPanel, { page: currentHelpPage, pages: helpPages, pageSize: helpPageSize }), costExpanded && _jsx(CostPanel, { source: costSource }), _jsx(Frozen, { frozen: statusFrozen, identity: `${width}:${state.sessionId}:${statusExpanded}:${statusScroll}:${pauseReason ?? ''}`, children: _jsx(StatusBar, { source: statusSource, width: width, expanded: statusExpanded, scroll: statusScroll, pageSize: statusViewRows, onScroll: setStatusScroll, onOverflow: setStatusOverflow, onRows: setStatusBarRows, pauseReason: pauseReason, revision: state.version }) })] })] }) }) }) });
|
|
1061
1063
|
}
|
|
@@ -9,6 +9,8 @@ export interface CostLine {
|
|
|
9
9
|
export interface CostSource {
|
|
10
10
|
session?: CostLine;
|
|
11
11
|
today: CostLine;
|
|
12
|
+
week: CostLine;
|
|
13
|
+
month: CostLine;
|
|
12
14
|
scanning: boolean;
|
|
13
15
|
coverage: Coverage;
|
|
14
16
|
scannedAt?: number;
|
|
@@ -17,6 +19,10 @@ export interface CostSource {
|
|
|
17
19
|
missing: readonly string[];
|
|
18
20
|
}
|
|
19
21
|
/** Render cached totals while the independent HTTP cost scan refreshes.
|
|
22
|
+
*
|
|
23
|
+
* The three periods are natural Beijing periods counted from their own start through today, not
|
|
24
|
+
* sliding windows, so "this week" and "this month" match what an invoice would name. They only reach
|
|
25
|
+
* back as far as the retained ledger does, which is why the panel says how old its cached totals are.
|
|
20
26
|
* @param source - Plain billing summary, absent when this run has no ledger.
|
|
21
27
|
* @returns Billing panel, including unpriced models and refresh errors.
|
|
22
28
|
*/
|
package/dist/ui/dialogs/cost.js
CHANGED
|
@@ -4,6 +4,10 @@ import { useTheme } from "../theme/index.js";
|
|
|
4
4
|
import { Box, Text } from 'ink';
|
|
5
5
|
import { safeText } from "../../text.js";
|
|
6
6
|
/** Render cached totals while the independent HTTP cost scan refreshes.
|
|
7
|
+
*
|
|
8
|
+
* The three periods are natural Beijing periods counted from their own start through today, not
|
|
9
|
+
* sliding windows, so "this week" and "this month" match what an invoice would name. They only reach
|
|
10
|
+
* back as far as the retained ledger does, which is why the panel says how old its cached totals are.
|
|
7
11
|
* @param source - Plain billing summary, absent when this run has no ledger.
|
|
8
12
|
* @returns Billing panel, including unpriced models and refresh errors.
|
|
9
13
|
*/
|
|
@@ -12,7 +16,7 @@ export function CostPanel({ source }) {
|
|
|
12
16
|
const costs = source;
|
|
13
17
|
if (!costs)
|
|
14
18
|
return _jsx(Text, { children: "Cost tracking is unavailable" });
|
|
15
|
-
const rows = [['Session', costs.session], ['Today', costs.today]];
|
|
19
|
+
const rows = [['Session', costs.session], ['Today', costs.today], ['This week', costs.week], ['This month', costs.month]];
|
|
16
20
|
return _jsxs(Box, { flexDirection: "column", borderStyle: "single", paddingX: 1, children: [_jsx(Text, { bold: true, children: "Cost \u00B7 CNY estimate \u00B7 Asia/Shanghai \u00B7 /cost closes" }), rows.map(([label, total]) => _jsxs(Text, { children: [label, ": ", total ? `${total.text} · ${total.unknown} unpriced / ${total.records} requests` : '?'] }, label)), _jsx(Text, { dimColor: true, children: costs.scanning ? 'Refreshing all visible sessions…'
|
|
17
21
|
: costs.coverage === 'partial' ? 'Partial totals · awaiting a complete scan'
|
|
18
22
|
: costs.scannedAt ? `Last refresh: ${new Date(costs.scannedAt).toISOString()}`
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { LoopLimits, LoopRecord } from '../../contracts.ts';
|
|
1
|
+
import type { LoopLimits, LoopRecord, LoopSourceInfo } from '../../contracts.ts';
|
|
2
2
|
/** The numeric fields the form edits, in the order a run reads them. */
|
|
3
3
|
export type LoopField = 'from' | 'to' | 'score' | 'tries';
|
|
4
4
|
/** What the form hands to the runner: the four numbers and the record's variables as confirmed. */
|
|
@@ -15,12 +15,13 @@ export declare function loopRecordDefaults(record: LoopRecord): LoopLimits;
|
|
|
15
15
|
*
|
|
16
16
|
* It offers the same names and defaults the runner reads, so choosing from it cannot start a run
|
|
17
17
|
* other than the one the row describes.
|
|
18
|
-
* @param props - Matching records and the
|
|
19
|
-
* @returns The key line and up to six record rows.
|
|
18
|
+
* @param props - Matching records, the highlighted row, and where the records came from.
|
|
19
|
+
* @returns The key line, the source line, any warning, and up to six record rows.
|
|
20
20
|
*/
|
|
21
|
-
export declare function LoopMenu({ records, index }: {
|
|
21
|
+
export declare function LoopMenu({ records, index, source }: {
|
|
22
22
|
records: readonly LoopRecord[];
|
|
23
23
|
index: number;
|
|
24
|
+
source?: LoopSourceInfo;
|
|
24
25
|
}): import("react").JSX.Element;
|
|
25
26
|
/** The loop parameter form: one record's variables and defaults, editable in place, then a Start.
|
|
26
27
|
*
|
package/dist/ui/dialogs/loop.js
CHANGED
|
@@ -47,20 +47,28 @@ function labelWidth(rows) {
|
|
|
47
47
|
*
|
|
48
48
|
* It offers the same names and defaults the runner reads, so choosing from it cannot start a run
|
|
49
49
|
* other than the one the row describes.
|
|
50
|
-
* @param props - Matching records and the
|
|
51
|
-
* @returns The key line and up to six record rows.
|
|
50
|
+
* @param props - Matching records, the highlighted row, and where the records came from.
|
|
51
|
+
* @returns The key line, the source line, any warning, and up to six record rows.
|
|
52
52
|
*/
|
|
53
|
-
export function LoopMenu({ records, index }) {
|
|
53
|
+
export function LoopMenu({ records, index, source }) {
|
|
54
54
|
const theme = useTheme();
|
|
55
55
|
const start = Math.max(0, index - 5);
|
|
56
|
-
|
|
56
|
+
// Records are configuration now: whenever a user file supplied or replaced one, the list says so,
|
|
57
|
+
// because an operator who overrode a shipped record cannot tell the two apart from the name alone.
|
|
58
|
+
const origin = source?.file === undefined ? undefined : [
|
|
59
|
+
`Records from ${source.file}`,
|
|
60
|
+
...(source.overridden.length === 0 ? [] : [`replaced: ${source.overridden.join(', ')}`]),
|
|
61
|
+
...(source.added.length === 0 ? [] : [`added: ${source.added.join(', ')}`]),
|
|
62
|
+
].join(' · ');
|
|
63
|
+
return _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "Loop records \u00B7 \u2191 \u2193 select \u00B7 Enter confirm defaults \u00B7 Tab finish the name \u00B7 Esc close" }), origin !== undefined && _jsx(Text, { dimColor: true, wrap: "truncate-end", children: safeText(origin) }), (source?.warnings ?? []).map(warning => _jsx(Text, { color: theme.colors.error, wrap: "truncate-end", children: safeText(warning) }, warning)), records.slice(start, start + 6).map((record, offset) => {
|
|
57
64
|
const current = start + offset === index;
|
|
58
65
|
// The record's own inputs come before the artifact file: they are what the form will edit, and
|
|
59
66
|
// the first thing a narrow row must not lose.
|
|
60
67
|
const vars = Object.entries(record.vars).map(([name, value]) => `${name} ${value}`).join(' · ');
|
|
61
68
|
const target = vars === '' ? '' : ` · ${vars}`;
|
|
62
69
|
const artifact = record.artifact === undefined ? '' : ` · ${record.artifact}`;
|
|
63
|
-
|
|
70
|
+
const mine = record.fromFile === true ? ' · yours' : '';
|
|
71
|
+
return _jsxs(Text, { color: current ? theme.accent : undefined, wrap: "truncate-end", children: [current ? '❯ ' : ' ', record.name, " \u00B7 ", record.title, " \u00B7 ", record.steps, " rounds \u00B7 pass ", record.defaultScore, " \u00B7 \u2264", record.defaultTries, " tries", target, artifact, mine] }, record.name);
|
|
64
72
|
})] });
|
|
65
73
|
}
|
|
66
74
|
/** The loop parameter form: one record's variables and defaults, editable in place, then a Start.
|
|
@@ -207,7 +215,7 @@ export function LoopDialog({ record, enabled, onStart, onBack, onClose }) {
|
|
|
207
215
|
}
|
|
208
216
|
onStart({ limits: values, vars });
|
|
209
217
|
}, { isActive: !copyMode });
|
|
210
|
-
return _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsxs(Text, { bold: true, children: ["Run loop record \u00B7 ", safeText(record.name), " \u00B7 Esc close"] }), _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [record.steps, " rounds", record.artifact === undefined ? '' : ` · ${safeText(record.artifact)}`, " \u00B7 \u2191 \u2193 to a row, then type"] }), rows.map((item, index) => {
|
|
218
|
+
return _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsxs(Text, { bold: true, children: ["Run loop record \u00B7 ", safeText(record.name), record.fromFile === true ? ' · your file' : '', " \u00B7 Esc close"] }), _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [record.steps, " rounds", record.artifact === undefined ? '' : ` · ${safeText(record.artifact)}`, " \u00B7 \u2191 \u2193 to a row, then type"] }), rows.map((item, index) => {
|
|
211
219
|
const selected = index === row;
|
|
212
220
|
const cursor = selected ? '❯ ' : ' ';
|
|
213
221
|
if (item.kind === 'start' || item.kind === 'back')
|