@gavin-hu/my-pi-agent 0.11.4 → 0.13.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 +38 -26
- package/README.zh-CN.md +33 -23
- package/extensions/bytefly/README.md +381 -225
- package/extensions/bytefly/chrome.ts +7 -2
- package/extensions/bytefly/doctor/register.ts +6 -0
- package/extensions/bytefly/doctor/runtime.ts +52 -6
- package/extensions/bytefly/doctor/tools.ts +1 -3
- package/extensions/bytefly/footer/collect/snapshot.ts +12 -2
- package/extensions/bytefly/footer/config.ts +7 -1
- package/extensions/bytefly/footer/delegation.ts +100 -0
- package/extensions/bytefly/footer/price/config.ts +54 -15
- package/extensions/bytefly/footer/price/price.ts +20 -7
- package/extensions/bytefly/footer/price/types.ts +13 -7
- package/extensions/bytefly/footer/register.ts +13 -2
- package/extensions/bytefly/footer/render/format.ts +3 -2
- package/extensions/bytefly/footer/render/lines.ts +2 -0
- package/extensions/bytefly/footer/render/segments.ts +30 -2
- package/extensions/bytefly/footer/types.ts +12 -0
- package/extensions/bytefly/header/component.ts +21 -6
- package/extensions/bytefly/header/config.ts +3 -0
- package/extensions/bytefly/header/register.ts +34 -3
- package/extensions/bytefly/index.ts +15 -4
- package/extensions/bytefly/notify/config.ts +2 -0
- package/extensions/bytefly/notify/format.ts +19 -1
- package/extensions/bytefly/notify/register.ts +37 -13
- package/extensions/bytefly/notify/types.ts +2 -0
- package/extensions/bytefly/panel/commands.ts +34 -13
- package/extensions/bytefly/panel/settings-runtime.ts +71 -28
- package/extensions/bytefly/panel/settings.ts +293 -48
- package/extensions/bytefly/panel/tui/entries.ts +58 -0
- package/extensions/bytefly/panel/tui/extensions-view.ts +86 -0
- package/extensions/bytefly/panel/tui/frame.ts +71 -0
- package/extensions/bytefly/panel/tui/nav.ts +75 -0
- package/extensions/bytefly/panel/tui/settings-view.ts +115 -0
- package/extensions/bytefly/panel/tui.ts +205 -319
- package/extensions/bytefly/picker/runtime.ts +61 -2
- package/extensions/bytefly/picker/toggle.ts +56 -0
- package/extensions/bytefly/update/cache.ts +73 -0
- package/extensions/bytefly/update/check.ts +59 -0
- package/extensions/bytefly/update/format.ts +87 -0
- package/extensions/bytefly/update/register.ts +158 -0
- package/extensions/bytefly/update/state.ts +40 -0
- package/extensions/bytefly/update/version.ts +83 -0
- package/extensions/document/README.md +24 -14
- package/extensions/document/config.ts +4 -0
- package/extensions/document/deadline.ts +61 -0
- package/extensions/document/tool.ts +14 -5
- package/extensions/file-browse/README.md +12 -8
- package/extensions/file-browse/client.ts +6 -7
- package/extensions/file-browse/files.ts +0 -4
- package/extensions/file-browse/git.ts +9 -10
- package/extensions/file-browse/html.ts +17 -7
- package/extensions/file-browse/router.ts +24 -15
- package/extensions/file-open/README.md +30 -15
- package/extensions/file-open/index.ts +4 -3
- package/extensions/file-open/open.ts +52 -21
- package/extensions/goal/README.md +45 -13
- package/extensions/goal/clarify-prompt.ts +5 -36
- package/extensions/goal/clarify.ts +55 -86
- package/extensions/goal/commands.ts +41 -35
- package/extensions/goal/format.ts +12 -4
- package/extensions/goal/schema.ts +6 -2
- package/extensions/goal/tools.ts +28 -8
- package/extensions/goal/tui.ts +1 -1
- package/extensions/goal/types.ts +6 -2
- package/extensions/job/README.md +16 -12
- package/extensions/job/runtime.ts +23 -6
- package/extensions/job/status.ts +4 -2
- package/extensions/memory/README.md +56 -14
- package/extensions/memory/commands.ts +20 -2
- package/extensions/memory/doctor.ts +137 -0
- package/extensions/memory/format.ts +24 -7
- package/extensions/memory/runtime.ts +49 -0
- package/extensions/memory/store.ts +31 -0
- package/extensions/memory/tui.ts +136 -12
- package/extensions/plan/README.md +83 -36
- package/extensions/plan/blocks.ts +1 -35
- package/extensions/plan/commands.ts +84 -19
- package/extensions/plan/index.ts +35 -11
- package/extensions/plan/plan-index.ts +234 -0
- package/extensions/plan/plan-text.ts +95 -0
- package/extensions/plan/plans.ts +91 -2
- package/extensions/plan/policy.ts +4 -3
- package/extensions/plan/review.ts +246 -0
- package/extensions/plan/runtime.ts +73 -24
- package/extensions/plan/steps.ts +60 -39
- package/extensions/plan/todos.ts +29 -60
- package/extensions/plan/tools.ts +188 -244
- package/extensions/plan/tui.ts +18 -31
- package/extensions/plan/types.ts +12 -0
- package/extensions/plan/web-client.ts +51 -18
- package/extensions/plan/web-page.ts +7 -2
- package/extensions/plan/web-router.ts +11 -3
- package/extensions/plan/web.ts +106 -41
- package/extensions/question/README.md +1 -1
- package/extensions/question/tools.ts +1 -1
- package/extensions/rewind/README.md +28 -8
- package/extensions/rewind/commands.ts +1 -1
- package/extensions/rewind/format.ts +4 -4
- package/extensions/rewind/git.ts +56 -18
- package/extensions/rewind/index.ts +3 -5
- package/extensions/rewind/path-picker.ts +198 -0
- package/extensions/rewind/paths.ts +128 -0
- package/extensions/rewind/restore.ts +69 -28
- package/extensions/rewind/rewind.ts +80 -3
- package/extensions/rewind/runtime.ts +13 -5
- package/extensions/rewind/snapshot-run.ts +5 -2
- package/extensions/rewind/types.ts +2 -0
- package/extensions/subagent/README.md +7 -1
- package/extensions/subagent/accounting.ts +34 -0
- package/extensions/subagent/index.ts +23 -3
- package/extensions/subagent/stream.ts +2 -0
- package/extensions/subagent/types.ts +2 -0
- package/extensions/todo/README.md +19 -11
- package/extensions/todo/index.ts +5 -3
- package/extensions/todo/nudge.ts +58 -9
- package/extensions/todo/runtime.ts +28 -17
- package/extensions/watch/README.md +8 -3
- package/extensions/watch/index.ts +19 -0
- package/extensions/web-access/README.md +46 -16
- package/extensions/web-access/charset.ts +58 -0
- package/extensions/web-access/fetch/find.ts +11 -7
- package/extensions/web-access/fetch/format.ts +64 -18
- package/extensions/web-access/fetch/page.ts +5 -1
- package/extensions/web-access/fetch/tool.ts +20 -21
- package/extensions/web-access/http.ts +49 -8
- package/extensions/web-access/search/facts.ts +38 -0
- package/extensions/web-access/search/tool.ts +14 -3
- package/extensions/web-access/text.ts +50 -11
- package/extensions/wechat/README.md +5 -4
- package/extensions/wechat/tools.ts +0 -2
- package/extensions/worktree/README.md +74 -15
- package/extensions/worktree/config.ts +11 -0
- package/extensions/worktree/exit.ts +35 -3
- package/extensions/worktree/gaps.ts +74 -0
- package/extensions/worktree/guard.ts +50 -4
- package/extensions/worktree/handoff.ts +55 -0
- package/extensions/worktree/index.ts +12 -2
- package/extensions/worktree/runtime.ts +20 -0
- package/extensions/worktree/status.ts +15 -2
- package/extensions/worktree/tools.ts +3 -2
- package/lib/README.md +12 -9
- package/lib/config.ts +43 -1
- package/lib/env.ts +12 -0
- package/lib/events.ts +86 -1
- package/lib/goal-state.ts +95 -0
- package/lib/requirements.ts +340 -46
- package/lib/todo-state.ts +62 -0
- package/lib/tui/README.md +91 -0
- package/lib/tui/glyphs.ts +2 -0
- package/lib/web/layout.ts +12 -2
- package/package.json +5 -3
- package/themes/bytefly-dark.json +1 -1
- package/assets/bytefly.schema.json +0 -785
- package/assets/chrome.png +0 -0
- package/assets/file-browse.png +0 -0
- package/assets/job.png +0 -0
- package/assets/plan-review.png +0 -0
- package/assets/question.png +0 -0
- package/assets/rewind.png +0 -0
- package/assets/todo.png +0 -0
- package/assets/tool-renderings.png +0 -0
- package/assets/tools/ask_user_question-cancelled.png +0 -0
- package/assets/tools/ask_user_question.png +0 -0
- package/assets/tools/bash.png +0 -0
- package/assets/tools/edit.png +0 -0
- package/assets/tools/enter_plan_mode.png +0 -0
- package/assets/tools/enter_worktree.png +0 -0
- package/assets/tools/exit_plan_mode.png +0 -0
- package/assets/tools/exit_worktree.png +0 -0
- package/assets/tools/find.png +0 -0
- package/assets/tools/goal.png +0 -0
- package/assets/tools/grep.png +0 -0
- package/assets/tools/job.png +0 -0
- package/assets/tools/list_worktrees.png +0 -0
- package/assets/tools/ls.png +0 -0
- package/assets/tools/memory-long-subject.png +0 -0
- package/assets/tools/memory.png +0 -0
- package/assets/tools/notice.png +0 -0
- package/assets/tools/open_path.png +0 -0
- package/assets/tools/prune_worktrees.png +0 -0
- package/assets/tools/read.png +0 -0
- package/assets/tools/read_doc-running.png +0 -0
- package/assets/tools/read_doc.png +0 -0
- package/assets/tools/run_doctor.png +0 -0
- package/assets/tools/send_wechat.png +0 -0
- package/assets/tools/set_plan_steps.png +0 -0
- package/assets/tools/subagent.png +0 -0
- package/assets/tools/todo.png +0 -0
- package/assets/tools/turn-rule.png +0 -0
- package/assets/tools/user-message.png +0 -0
- package/assets/tools/watch.png +0 -0
- package/assets/tools/web_fetch-rejected.png +0 -0
- package/assets/tools/web_fetch.png +0 -0
- package/assets/tools/web_search.png +0 -0
- package/assets/tools/write.png +0 -0
- package/assets/tools/write_plan.png +0 -0
|
@@ -50,8 +50,13 @@ export const DEFAULT_RAIL_GLYPH: string = GLYPHS.divider;
|
|
|
50
50
|
|
|
51
51
|
export const DEFAULT_CHROME_CONFIG: ChromeConfig = { rail: DEFAULT_RAIL_GLYPH };
|
|
52
52
|
|
|
53
|
-
/**
|
|
54
|
-
|
|
53
|
+
/**
|
|
54
|
+
* Whether `value` is a single, unstyled terminal column.
|
|
55
|
+
*
|
|
56
|
+
* The Settings row validates with this same function, so the editor cannot save
|
|
57
|
+
* a glyph the next read would replace.
|
|
58
|
+
*/
|
|
59
|
+
export function isRailGlyph(value: string): boolean {
|
|
55
60
|
// Control characters first: a bare ESC confuses a width measurement.
|
|
56
61
|
if (/[\u0000-\u001f\u007f]/.test(value)) return false;
|
|
57
62
|
return [...value].length === 1 && visibleWidth(value) === 1;
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
19
|
+
import { WEB_SEARCH_PROVIDER_CHANNEL } from "../../../lib/events.ts";
|
|
19
20
|
import { missingFindings } from "../../../lib/requirements.ts";
|
|
20
21
|
import { loadConfig } from "./config.ts";
|
|
21
22
|
import { warningNotice } from "./format.ts";
|
|
@@ -39,6 +40,11 @@ export function registerDoctor(pi: ExtensionAPI, deps: DoctorDeps = {}): DoctorR
|
|
|
39
40
|
|
|
40
41
|
registerTools(pi, runtime);
|
|
41
42
|
|
|
43
|
+
// `web-access` publishes what it resolved at session start; the doctor reports
|
|
44
|
+
// the provider, its key variable, and (on a refresh) its endpoint from that,
|
|
45
|
+
// without importing the extension.
|
|
46
|
+
pi.events.on(WEB_SEARCH_PROVIDER_CHANNEL, (facts) => runtime.noteSearchFacts(facts));
|
|
47
|
+
|
|
42
48
|
pi.on("session_start", async (_event, ctx) => {
|
|
43
49
|
const config = loadConfig(ctx.cwd);
|
|
44
50
|
runtime.configure({ cwd: ctx.cwd, config });
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { CONFIG_DIR_NAME, getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
12
|
+
import type { WebSearchProviderEvent } from "../../../lib/events.ts";
|
|
12
13
|
import { readJsonResult } from "../../../lib/config.ts";
|
|
13
14
|
import { isExtensionEnabled } from "../../../lib/env.ts";
|
|
14
15
|
import {
|
|
@@ -19,11 +20,25 @@ import {
|
|
|
19
20
|
type ExtensionReport,
|
|
20
21
|
type ProbeSeams,
|
|
21
22
|
} from "../../../lib/requirements.ts";
|
|
23
|
+
import { loadNotifyConfig } from "../notify/config.ts";
|
|
22
24
|
import { DEFAULT_CONFIG, type DoctorConfig } from "./config.ts";
|
|
23
25
|
|
|
24
26
|
/** Seams a test may replace; everything else is built from the host. */
|
|
25
27
|
export type DoctorSeamOverrides = Partial<ProbeSeams>;
|
|
26
28
|
|
|
29
|
+
/**
|
|
30
|
+
* The notify capability's configured delivery, read fresh from the document.
|
|
31
|
+
*
|
|
32
|
+
* Reading at probe time rather than from the session's snapshot means a
|
|
33
|
+
* corrected method shows up on the next `r` instead of only after a reload.
|
|
34
|
+
*/
|
|
35
|
+
export function notifySeam(cwd: string): () => { enabled: boolean; method: string } {
|
|
36
|
+
return () => {
|
|
37
|
+
const notify = loadNotifyConfig(cwd);
|
|
38
|
+
return { enabled: notify.enabled, method: notify.method };
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
27
42
|
export interface DoctorRuntime {
|
|
28
43
|
/** Point the runtime at a session: its cwd and effective config. */
|
|
29
44
|
configure(options: { cwd: string; config: DoctorConfig }): void;
|
|
@@ -33,10 +48,28 @@ export interface DoctorRuntime {
|
|
|
33
48
|
probeAll(options?: { refresh?: boolean }): Promise<ExtensionReport[]>;
|
|
34
49
|
/** The cached reports from the last probe, if any. */
|
|
35
50
|
peek(): ExtensionReport[] | undefined;
|
|
36
|
-
/**
|
|
51
|
+
/** Remember the search provider `web-access` published for this session. */
|
|
52
|
+
noteSearchFacts(facts: unknown): void;
|
|
53
|
+
/** Drop the cached reports and the published facts; the configured cwd is kept. */
|
|
37
54
|
reset(): void;
|
|
38
55
|
}
|
|
39
56
|
|
|
57
|
+
/** How long a reachability probe may take; a doctor refresh is not a fetch. */
|
|
58
|
+
const REACH_TIMEOUT_MS = 3_000;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Probe a URL's reachability. Any HTTP answer proves the host is there; a 5xx is
|
|
62
|
+
* reported because it usually means the server is up but broken.
|
|
63
|
+
*/
|
|
64
|
+
async function probeReach(url: string): Promise<{ ok: boolean; error?: string }> {
|
|
65
|
+
try {
|
|
66
|
+
const response = await fetch(url, { method: "GET", signal: AbortSignal.timeout(REACH_TIMEOUT_MS) });
|
|
67
|
+
return response.status < 500 ? { ok: true } : { ok: false, error: `HTTP ${response.status}` };
|
|
68
|
+
} catch (error) {
|
|
69
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
40
73
|
/** Build a runtime over the host's `exec`, with `deps.seams` for tests. */
|
|
41
74
|
export function createDoctorRuntime(
|
|
42
75
|
pi: Pick<ExtensionAPI, "exec">,
|
|
@@ -45,9 +78,11 @@ export function createDoctorRuntime(
|
|
|
45
78
|
let cwd = process.cwd();
|
|
46
79
|
let config: DoctorConfig = DEFAULT_CONFIG;
|
|
47
80
|
let cached: ExtensionReport[] | undefined;
|
|
81
|
+
/** What `web-access` published at session start; absent when it did not load. */
|
|
82
|
+
let searchProvider: WebSearchProviderEvent | undefined;
|
|
48
83
|
|
|
49
|
-
/** The full seam set for the current session. */
|
|
50
|
-
const seams = (): ProbeSeams => ({
|
|
84
|
+
/** The full seam set for the current session; `refresh` is the caller's. */
|
|
85
|
+
const seams = (refresh: boolean): ProbeSeams => ({
|
|
51
86
|
exec: async (command, args) => {
|
|
52
87
|
const result = await pi.exec(command, args, { timeout: config.timeoutMs });
|
|
53
88
|
return { code: result.code };
|
|
@@ -56,6 +91,11 @@ export function createDoctorRuntime(
|
|
|
56
91
|
dirs: { agentDir: getAgentDir(), cwd, configDirName: CONFIG_DIR_NAME },
|
|
57
92
|
platform: process.platform,
|
|
58
93
|
isEnabled: (extension) => isExtensionEnabled(extension) && !config.skip.includes(extension),
|
|
94
|
+
notify: notifySeam(cwd),
|
|
95
|
+
isTTY: process.stdout.isTTY === true,
|
|
96
|
+
searchProvider: () => searchProvider,
|
|
97
|
+
reach: probeReach,
|
|
98
|
+
refresh,
|
|
59
99
|
...deps.seams,
|
|
60
100
|
});
|
|
61
101
|
|
|
@@ -68,20 +108,26 @@ export function createDoctorRuntime(
|
|
|
68
108
|
|
|
69
109
|
async probeRequired() {
|
|
70
110
|
// Concurrent: a probe is a short-lived process, and session start must
|
|
71
|
-
// not wait on the sum of every timeout.
|
|
72
|
-
|
|
111
|
+
// not wait on the sum of every timeout. Never a refresh: this runs before
|
|
112
|
+
// anyone asked, so no probe may touch the network.
|
|
113
|
+
return Promise.all(hardRequirementExtensions().map((name) => probeExtension(name, seams(false))));
|
|
73
114
|
},
|
|
74
115
|
|
|
75
116
|
async probeAll(options = {}) {
|
|
76
117
|
if (cached && !options.refresh) return cached;
|
|
77
|
-
cached = await probeDeclared(seams(), Object.keys(REQUIREMENTS));
|
|
118
|
+
cached = await probeDeclared(seams(options.refresh === true), Object.keys(REQUIREMENTS));
|
|
78
119
|
return cached;
|
|
79
120
|
},
|
|
80
121
|
|
|
81
122
|
peek: () => cached,
|
|
82
123
|
|
|
124
|
+
noteSearchFacts(facts) {
|
|
125
|
+
searchProvider = facts && typeof facts === "object" ? (facts as WebSearchProviderEvent) : undefined;
|
|
126
|
+
},
|
|
127
|
+
|
|
83
128
|
reset() {
|
|
84
129
|
cached = undefined;
|
|
130
|
+
searchProvider = undefined;
|
|
85
131
|
},
|
|
86
132
|
};
|
|
87
133
|
}
|
|
@@ -57,9 +57,7 @@ export function registerTools(pi: ExtensionAPI, runtime: DoctorRuntime): void {
|
|
|
57
57
|
],
|
|
58
58
|
parameters: DoctorParams,
|
|
59
59
|
outputSchema: DoctorOutput,
|
|
60
|
-
|
|
61
|
-
defaultActive: true,
|
|
62
|
-
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
|
|
60
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
63
61
|
|
|
64
62
|
async execute(_toolCallId, params, _signal, onUpdate, _ctx) {
|
|
65
63
|
const args = params as DoctorArgs;
|
|
@@ -7,9 +7,14 @@
|
|
|
7
7
|
* hold thousands of entries and the footer renders on every frame. The config is
|
|
8
8
|
* captured when the reader is built, so re-reading the file means building a new
|
|
9
9
|
* reader (which is what installing the footer does).
|
|
10
|
+
*
|
|
11
|
+
* The child-run ledger's version joins the memo key: a finished subagent invalidates
|
|
12
|
+
* the same way an appended entry does, so the roll-up appears on the next frame
|
|
13
|
+
* without re-pricing every frame.
|
|
10
14
|
*/
|
|
11
15
|
|
|
12
16
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
17
|
+
import type { DelegationLedger } from "../delegation.ts";
|
|
13
18
|
import { createPricer } from "../price/price.ts";
|
|
14
19
|
import type { ByteflyConfig } from "../price/types.ts";
|
|
15
20
|
import type { ContextInfo, FooterData, StatusSnapshot } from "../types.ts";
|
|
@@ -23,22 +28,26 @@ import { emptyUsage, usageRecords } from "./entries.ts";
|
|
|
23
28
|
* are stale; this avoids copying the full entry list and rebuilding the session
|
|
24
29
|
* projection per frame. Pi's own footer memoizes its stats on the same idea.
|
|
25
30
|
*/
|
|
26
|
-
export function createSnapshotReader(config: ByteflyConfig) {
|
|
31
|
+
export function createSnapshotReader(config: ByteflyConfig, delegation: DelegationLedger) {
|
|
27
32
|
const price = createPricer(config);
|
|
28
33
|
let key: string | null = null;
|
|
29
34
|
let usage = emptyUsage();
|
|
35
|
+
let delegationTotals = { runs: 0, usage: emptyUsage() };
|
|
30
36
|
let cacheHitRate: number | null = null;
|
|
31
37
|
let context: ContextInfo = { tokens: null, contextWindow: 0, percent: null };
|
|
32
38
|
|
|
33
39
|
return (ctx: ExtensionContext, footerData: FooterData): StatusSnapshot => {
|
|
34
40
|
const session = ctx.sessionManager;
|
|
35
41
|
const current = ctx.model;
|
|
36
|
-
const nextKey = `${session.getSessionId()}:${session.getLeafId() ?? ""}:${current?.provider ?? ""}/${current?.id ?? ""}`;
|
|
42
|
+
const nextKey = `${session.getSessionId()}:${session.getLeafId() ?? ""}:${current?.provider ?? ""}/${current?.id ?? ""}:${delegation.version()}`;
|
|
37
43
|
if (nextKey !== key) {
|
|
38
44
|
const active = current ? { provider: current.provider, model: current.id } : undefined;
|
|
39
45
|
const scanned = usageRecords(session.getEntries(), active);
|
|
40
46
|
usage = price(scanned.records);
|
|
41
47
|
cacheHitRate = scanned.cacheHitRate;
|
|
48
|
+
// The child runs are priced by the same pricer, so a currency or rate
|
|
49
|
+
// change moves both numbers together — and neither is folded into the other.
|
|
50
|
+
delegationTotals = { runs: delegation.runs(), usage: price(delegation.records()) };
|
|
42
51
|
|
|
43
52
|
const usageInfo = ctx.getContextUsage();
|
|
44
53
|
context = {
|
|
@@ -65,6 +74,7 @@ export function createSnapshotReader(config: ByteflyConfig) {
|
|
|
65
74
|
currency: config.currency,
|
|
66
75
|
cacheHitRate,
|
|
67
76
|
providerCount: footerData.getAvailableProviderCount(),
|
|
77
|
+
delegation: delegationTotals,
|
|
68
78
|
};
|
|
69
79
|
};
|
|
70
80
|
}
|
|
@@ -16,7 +16,13 @@ export const CONFIG = {
|
|
|
16
16
|
gauge: { full: GLYPHS.gaugeFull, empty: GLYPHS.gaugeEmpty, widths: [10, 5, 3, 0] as const },
|
|
17
17
|
/** Separators printed before grouped and other segments. */
|
|
18
18
|
separators: { group: SEPARATORS.group, item: SEPARATORS.item },
|
|
19
|
-
icons: {
|
|
19
|
+
icons: {
|
|
20
|
+
branch: GLYPHS.branch,
|
|
21
|
+
detached: GLYPHS.detached,
|
|
22
|
+
worktree: GLYPHS.worktree,
|
|
23
|
+
browse: GLYPHS.browse,
|
|
24
|
+
delegation: GLYPHS.delegation,
|
|
25
|
+
},
|
|
20
26
|
/** Status key the worktree extension uses; routed to line 1's right zone. */
|
|
21
27
|
worktreeStatusKey: STATUS_KEYS.worktree,
|
|
22
28
|
/** Status key the file-browse extension uses; routed to line 1's right zone. */
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The child-run ledger: what this session's delegated runs spent.
|
|
3
|
+
*
|
|
4
|
+
* `subagent` publishes one event per finished child process on
|
|
5
|
+
* `SUBAGENT_FINISHED_CHANNEL`. The footer shows that spend as its own number, so
|
|
6
|
+
* the session's total keeps meaning what it always did.
|
|
7
|
+
*
|
|
8
|
+
* The ledger keeps *pricing inputs*, not a total: `currency.code` can change
|
|
9
|
+
* between sessions (a settings edit re-installs the footer and rebuilds the
|
|
10
|
+
* pricer), and a stored figure would then be in the wrong unit. It also keeps the
|
|
11
|
+
* peak tariff honest, because each record carries the time the run finished.
|
|
12
|
+
*
|
|
13
|
+
* Bounded on purpose: the last {@link MAX_DELEGATION_RECORDS} runs are retained —
|
|
14
|
+
* each is a full child `pi` process, so hundreds in one session is already
|
|
15
|
+
* extraordinary — while the run count keeps counting past the window. Every field
|
|
16
|
+
* of an event is re-validated, because the bus is not a trusted channel.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { SubagentFinishedEvent } from "../../../lib/events.ts";
|
|
20
|
+
import type { UsageRecord } from "./types.ts";
|
|
21
|
+
|
|
22
|
+
/** Runs whose pricing inputs are retained. */
|
|
23
|
+
export const MAX_DELEGATION_RECORDS = 500;
|
|
24
|
+
|
|
25
|
+
export interface DelegationLedger {
|
|
26
|
+
/** Note one finished child run; a malformed payload is ignored. */
|
|
27
|
+
note(event: unknown): void;
|
|
28
|
+
/** How many runs finished this session, including ones past the window. */
|
|
29
|
+
runs(): number;
|
|
30
|
+
/** Pricing inputs for the retained runs, oldest first. */
|
|
31
|
+
records(): UsageRecord[];
|
|
32
|
+
/** Changes on every noted run, so a reader can tell its totals are stale. */
|
|
33
|
+
version(): number;
|
|
34
|
+
/** Forget everything; called at session start. */
|
|
35
|
+
reset(): void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** A finite, non-negative number, or `null` when the field is not one. */
|
|
39
|
+
function count(value: unknown): number | null {
|
|
40
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** A non-empty string, or `null`. */
|
|
44
|
+
function text(value: unknown): string | null {
|
|
45
|
+
return typeof value === "string" && value.trim() !== "" ? value : null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The event as a usage record, or `null` when it is not one we can price. */
|
|
49
|
+
function toRecord(event: SubagentFinishedEvent): UsageRecord | null {
|
|
50
|
+
if (text(event.agent) === null) return null;
|
|
51
|
+
const input = count(event.input);
|
|
52
|
+
const output = count(event.output);
|
|
53
|
+
const cacheRead = count(event.cacheRead);
|
|
54
|
+
const cacheWrite = count(event.cacheWrite);
|
|
55
|
+
const recordedCost = count(event.recordedCost);
|
|
56
|
+
// A type-mismatched field is a malformed event, not a zero: refuse the whole
|
|
57
|
+
// record rather than counting a run whose spend is unknown.
|
|
58
|
+
if (input === null || output === null || cacheRead === null || cacheWrite === null || recordedCost === null) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
provider: text(event.provider),
|
|
63
|
+
model: text(event.model),
|
|
64
|
+
input,
|
|
65
|
+
output,
|
|
66
|
+
cacheRead,
|
|
67
|
+
cacheWrite,
|
|
68
|
+
recordedCost,
|
|
69
|
+
at: typeof event.at === "number" && Number.isFinite(event.at) ? event.at : null,
|
|
70
|
+
isAssistant: true,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function createDelegationLedger(): DelegationLedger {
|
|
75
|
+
let retained: UsageRecord[] = [];
|
|
76
|
+
let total = 0;
|
|
77
|
+
let version = 0;
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
note(event) {
|
|
81
|
+
if (!event || typeof event !== "object") return;
|
|
82
|
+
const record = toRecord(event as SubagentFinishedEvent);
|
|
83
|
+
if (!record) return;
|
|
84
|
+
total += 1;
|
|
85
|
+
version += 1;
|
|
86
|
+
retained.push(record);
|
|
87
|
+
// Keep the window at the cap without re-allocating the whole array per run
|
|
88
|
+
// until it is actually exceeded.
|
|
89
|
+
if (retained.length > MAX_DELEGATION_RECORDS) retained = retained.slice(-MAX_DELEGATION_RECORDS);
|
|
90
|
+
},
|
|
91
|
+
runs: () => total,
|
|
92
|
+
records: () => [...retained],
|
|
93
|
+
version: () => version,
|
|
94
|
+
reset() {
|
|
95
|
+
retained = [];
|
|
96
|
+
total = 0;
|
|
97
|
+
version = 0;
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
@@ -9,13 +9,14 @@
|
|
|
9
9
|
*
|
|
10
10
|
* The built-in table is DeepSeek's published yuan pricing, with the peak
|
|
11
11
|
* schedule that doubles the price during peak hours. It is the default value of
|
|
12
|
-
* `rates
|
|
13
|
-
*
|
|
12
|
+
* `rates`: `currency.code` chooses which table that is, and the peak schedule
|
|
13
|
+
* applies to both. Only an unmatched model differs — `CNY` contributes no cost
|
|
14
|
+
* for it, while `USD` adds Pi's recorded dollars.
|
|
14
15
|
*/
|
|
15
16
|
|
|
16
17
|
import { asRecord, clampInteger, clampNumber, cleanString, loadConfigSection } from "../../../../lib/config.ts";
|
|
17
18
|
import { parseWindow } from "./schedule.ts";
|
|
18
|
-
import type { ByteflyConfig, Currency, ModelRates, PeakSchedule, PeakWindow } from "./types.ts";
|
|
19
|
+
import type { ByteflyConfig, Currency, CurrencyCode, ModelRates, PeakSchedule, PeakWindow } from "./types.ts";
|
|
19
20
|
|
|
20
21
|
/** Monday to Friday, the days DeepSeek charges its peak rate. */
|
|
21
22
|
const WEEKDAYS: readonly number[] = [1, 2, 3, 4, 5];
|
|
@@ -26,8 +27,8 @@ const WEEKDAYS: readonly number[] = [1, 2, 3, 4, 5];
|
|
|
26
27
|
* Pi's catalog prices every call in dollars at one flat rate, which is wrong
|
|
27
28
|
* twice over for DeepSeek: the bill is in yuan, and peak hours cost double.
|
|
28
29
|
* These are the published peak figures; `DEFAULT_PEAK` supplies the tariff. A
|
|
29
|
-
* `bytefly.json` entry with the same key replaces one of these
|
|
30
|
-
*
|
|
30
|
+
* `bytefly.json` entry with the same key replaces one of these. A model with no
|
|
31
|
+
* matching entry contributes no cost; USD mode uses Pi's recorded cost instead.
|
|
31
32
|
*
|
|
32
33
|
* Source: <https://api-docs.deepseek.com/quick_start/pricing>.
|
|
33
34
|
*/
|
|
@@ -36,6 +37,24 @@ export const DEFAULT_RATES: Readonly<Record<string, ModelRates>> = {
|
|
|
36
37
|
"deepseek/deepseek-v4-pro": { input: 9, output: 27, cacheRead: 0.3, cacheWrite: 0, offPeakMultiplier: 0.5 },
|
|
37
38
|
};
|
|
38
39
|
|
|
40
|
+
/**
|
|
41
|
+
* The same two models in DeepSeek's published dollars, per million tokens at the
|
|
42
|
+
* peak rate; `offPeakMultiplier` halves them off-peak. The published table has no
|
|
43
|
+
* cache-write line, so `cacheWrite` is 0 here exactly as in the yuan table.
|
|
44
|
+
*
|
|
45
|
+
* Source: <https://api-docs.deepseek.com/quick_start/pricing> (USD table).
|
|
46
|
+
*/
|
|
47
|
+
export const DEFAULT_RATES_USD: Readonly<Record<string, ModelRates>> = {
|
|
48
|
+
"deepseek/deepseek-flash": { input: 0.3, output: 1.2, cacheRead: 0.006, cacheWrite: 0, offPeakMultiplier: 0.5 },
|
|
49
|
+
"deepseek/deepseek-v4-pro": { input: 1.32, output: 3.96, cacheRead: 0.044, cacheWrite: 0, offPeakMultiplier: 0.5 },
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/** The built-in table for each currency: `rates` are stated in the selected one. */
|
|
53
|
+
export const DEFAULT_RATES_BY_CURRENCY: Readonly<Record<CurrencyCode, Readonly<Record<string, ModelRates>>>> = {
|
|
54
|
+
CNY: DEFAULT_RATES,
|
|
55
|
+
USD: DEFAULT_RATES_USD,
|
|
56
|
+
};
|
|
57
|
+
|
|
39
58
|
/**
|
|
40
59
|
* DeepSeek's peak hours, in the zone the provider states them in: 01:00-04:00
|
|
41
60
|
* and 06:00-10:00 UTC on weekdays, which is 09:00-12:00 and 14:00-18:00 in
|
|
@@ -51,17 +70,24 @@ export const DEFAULT_PEAK: PeakSchedule = {
|
|
|
51
70
|
],
|
|
52
71
|
};
|
|
53
72
|
|
|
54
|
-
/**
|
|
55
|
-
export const
|
|
73
|
+
/** The symbol each algorithm prints. It is not configurable, so it can never disagree with the code. */
|
|
74
|
+
export const CURRENCY_SYMBOLS: Readonly<Record<CurrencyCode, string>> = { CNY: "¥", USD: "$" };
|
|
75
|
+
|
|
76
|
+
/** The default precision each algorithm supplies when a config sets only its code. */
|
|
77
|
+
export const CURRENCY_DEFAULTS: Readonly<Record<CurrencyCode, Currency>> = {
|
|
78
|
+
CNY: { code: "CNY", decimals: 2 },
|
|
79
|
+
USD: { code: "USD", decimals: 3 },
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/** Yuan, the currency `DEFAULT_RATES` is stated in. */
|
|
83
|
+
export const DEFAULT_CURRENCY: Currency = CURRENCY_DEFAULTS.CNY;
|
|
56
84
|
|
|
57
85
|
export const DEFAULT_BYTEFLY_CONFIG: ByteflyConfig = {
|
|
58
86
|
currency: DEFAULT_CURRENCY,
|
|
59
|
-
fallbackToRecordedCost: true,
|
|
60
87
|
rates: DEFAULT_RATES,
|
|
61
88
|
peak: DEFAULT_PEAK,
|
|
62
89
|
};
|
|
63
90
|
|
|
64
|
-
const MAX_SYMBOL_LENGTH = 4;
|
|
65
91
|
const MAX_DECIMALS = 4;
|
|
66
92
|
const MAX_RATE = 1_000_000;
|
|
67
93
|
const MAX_RATE_KEYS = 200;
|
|
@@ -77,12 +103,22 @@ function acceptsTimeZone(timezone: string): boolean {
|
|
|
77
103
|
}
|
|
78
104
|
}
|
|
79
105
|
|
|
106
|
+
/** A supported ISO code, or the base code for a missing/mistyped value. */
|
|
107
|
+
function currencyCode(raw: unknown, base: CurrencyCode): CurrencyCode {
|
|
108
|
+
const text = typeof raw === "string" ? raw.trim().toUpperCase() : "";
|
|
109
|
+
return text === "CNY" || text === "USD" ? text : base;
|
|
110
|
+
}
|
|
111
|
+
|
|
80
112
|
function currencyValue(raw: unknown, base: Currency): Currency {
|
|
81
113
|
const value = asRecord(raw);
|
|
82
114
|
if (!value) return base;
|
|
115
|
+
const code = currencyCode(value.code, base.code);
|
|
116
|
+
// A changed code supplies its own default precision unless this layer
|
|
117
|
+
// overrides it; an unchanged code keeps the precision customized above it.
|
|
118
|
+
const display = code !== base.code ? CURRENCY_DEFAULTS[code] : base;
|
|
83
119
|
return {
|
|
84
|
-
|
|
85
|
-
decimals: clampInteger(value.decimals,
|
|
120
|
+
code,
|
|
121
|
+
decimals: clampInteger(value.decimals, display.decimals, 0, MAX_DECIMALS),
|
|
86
122
|
};
|
|
87
123
|
}
|
|
88
124
|
|
|
@@ -137,11 +173,14 @@ function peakValue(raw: unknown, base: PeakSchedule): PeakSchedule {
|
|
|
137
173
|
/** Validate a raw config object over `base`. Total: never throws, never guesses. */
|
|
138
174
|
export function normalizeByteflyConfig(raw: Record<string, unknown> | undefined, base: ByteflyConfig): ByteflyConfig {
|
|
139
175
|
if (!raw) return base;
|
|
176
|
+
const currency = currencyValue(raw.currency, base.currency);
|
|
177
|
+
// `rates` are stated in the selected currency, so a layer that switches the
|
|
178
|
+
// code starts from that currency's table instead of reinterpreting the old
|
|
179
|
+
// numbers in the new unit. A layer that sets both merges onto the new table.
|
|
180
|
+
const rateBase = currency.code === base.currency.code ? base.rates : DEFAULT_RATES_BY_CURRENCY[currency.code];
|
|
140
181
|
return {
|
|
141
|
-
currency
|
|
142
|
-
|
|
143
|
-
typeof raw.fallbackToRecordedCost === "boolean" ? raw.fallbackToRecordedCost : base.fallbackToRecordedCost,
|
|
144
|
-
rates: ratesValue(raw.rates, base.rates),
|
|
182
|
+
currency,
|
|
183
|
+
rates: ratesValue(raw.rates, rateBase),
|
|
145
184
|
peak: peakValue(raw.peak, base.peak),
|
|
146
185
|
};
|
|
147
186
|
}
|
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
* Price usage records from configured rates.
|
|
3
3
|
*
|
|
4
4
|
* `createPricer` owns the two things that must not be rebuilt per record: the
|
|
5
|
-
* matching rates and the timezone formatter.
|
|
6
|
-
*
|
|
5
|
+
* matching rates and the timezone formatter. `currency.code` chooses the
|
|
6
|
+
* algorithm: CNY prices matched records from `rates` and `peak`, while USD
|
|
7
|
+
* totals Pi's recorded dollar cost and ignores both. A CNY-mode record with no
|
|
8
|
+
* matching rate contributes nothing: an unpriced model shows no price at all.
|
|
7
9
|
*/
|
|
8
10
|
|
|
9
11
|
import { emptyUsage } from "../collect/entries.ts";
|
|
@@ -32,6 +34,9 @@ function tokenCost(record: UsageRecord, rates: ModelRates): number {
|
|
|
32
34
|
* its price always describe the same set of records.
|
|
33
35
|
*/
|
|
34
36
|
export function createPricer(config: ByteflyConfig): (records: readonly UsageRecord[]) => UsageTotals {
|
|
37
|
+
// Both currencies price from `rates` and the tariff; they differ only in what
|
|
38
|
+
// an unmatched model falls back to, and Pi's recorded cost is already dollars.
|
|
39
|
+
const dollars = config.currency.code === "USD";
|
|
35
40
|
// Building a formatter is the expensive part, and only a schedule needs one.
|
|
36
41
|
const format = config.peak.windows.length > 0 ? peakFormatter(config.peak.timezone) : null;
|
|
37
42
|
|
|
@@ -42,16 +47,24 @@ export function createPricer(config: ByteflyConfig): (records: readonly UsageRec
|
|
|
42
47
|
totals.output += record.output;
|
|
43
48
|
totals.cacheRead += record.cacheRead;
|
|
44
49
|
totals.cacheWrite += record.cacheWrite;
|
|
45
|
-
|
|
50
|
+
const rates = findRates(config.rates, record.provider, record.model);
|
|
51
|
+
if (rates !== undefined) {
|
|
52
|
+
totals.cost += ratedCost(config, format, record, rates);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (dollars) totals.cost += record.recordedCost;
|
|
46
56
|
}
|
|
47
57
|
return totals;
|
|
48
58
|
};
|
|
49
59
|
}
|
|
50
60
|
|
|
51
|
-
/** One record's cost
|
|
52
|
-
function
|
|
53
|
-
|
|
54
|
-
|
|
61
|
+
/** One record's cost at its matched rates, after the peak schedule. */
|
|
62
|
+
function ratedCost(
|
|
63
|
+
config: ByteflyConfig,
|
|
64
|
+
format: Intl.DateTimeFormat | null,
|
|
65
|
+
record: UsageRecord,
|
|
66
|
+
rates: ModelRates,
|
|
67
|
+
): number {
|
|
55
68
|
const cost = tokenCost(record, rates);
|
|
56
69
|
// An undated record is never discounted: we cannot tell which tariff applied.
|
|
57
70
|
if (format === null || record.at === null) return cost;
|
|
@@ -6,15 +6,23 @@
|
|
|
6
6
|
* `../types.ts`.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
/**
|
|
9
|
+
/** The two pricing algorithms, named by the currency they total. */
|
|
10
|
+
export type CurrencyCode = "CNY" | "USD";
|
|
11
|
+
|
|
12
|
+
/** How a cost is calculated and rendered. */
|
|
10
13
|
export interface Currency {
|
|
11
|
-
/**
|
|
12
|
-
|
|
13
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Which table `rates` starts from, and what an unmatched model contributes:
|
|
16
|
+
* both currencies price a matched record from `rates` and the peak schedule,
|
|
17
|
+
* while `CNY` gives an unmatched model no cost and `USD` adds Pi's recorded
|
|
18
|
+
* dollars for it.
|
|
19
|
+
*/
|
|
20
|
+
code: CurrencyCode;
|
|
21
|
+
/** Digits after the decimal point. The symbol follows from `code`. */
|
|
14
22
|
decimals: number;
|
|
15
23
|
}
|
|
16
24
|
|
|
17
|
-
/** Per-million-token rates for one model,
|
|
25
|
+
/** Per-million-token rates for one model, in yuan; used by the CNY algorithm. */
|
|
18
26
|
export interface ModelRates {
|
|
19
27
|
input: number;
|
|
20
28
|
output: number;
|
|
@@ -45,8 +53,6 @@ export interface PeakSchedule {
|
|
|
45
53
|
/** Everything the price calculation is configured with. */
|
|
46
54
|
export interface ByteflyConfig {
|
|
47
55
|
currency: Currency;
|
|
48
|
-
/** Fall back to Pi's recorded cost for a record that no rate matches. */
|
|
49
|
-
fallbackToRecordedCost: boolean;
|
|
50
56
|
/** Rates keyed by `provider/model`, matched through wildcards. */
|
|
51
57
|
rates: Readonly<Record<string, ModelRates>>;
|
|
52
58
|
peak: PeakSchedule;
|
|
@@ -11,16 +11,23 @@
|
|
|
11
11
|
|
|
12
12
|
import { homedir } from "node:os";
|
|
13
13
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
14
|
+
import { SUBAGENT_FINISHED_CHANNEL } from "../../../lib/events.ts";
|
|
14
15
|
import { createSnapshotReader } from "./collect/snapshot.ts";
|
|
16
|
+
import { createDelegationLedger } from "./delegation.ts";
|
|
15
17
|
import { loadByteflyConfig } from "./price/config.ts";
|
|
16
18
|
import { createFooter } from "./render/footer.ts";
|
|
17
19
|
|
|
18
20
|
/** Register the footer install/restore lifecycle. */
|
|
19
21
|
export function registerFooter(pi: ExtensionAPI): void {
|
|
22
|
+
// One ledger per load: it is this session's child-run accounting, priced by
|
|
23
|
+
// whichever reader is installed (a config change installs a new one).
|
|
24
|
+
const delegation = createDelegationLedger();
|
|
25
|
+
pi.events.on(SUBAGENT_FINISHED_CHANNEL, (event) => delegation.note(event));
|
|
26
|
+
|
|
20
27
|
const install = (ctx: ExtensionContext): void => {
|
|
21
28
|
if (ctx.mode !== "tui") return;
|
|
22
29
|
const config = loadByteflyConfig(ctx.cwd);
|
|
23
|
-
const read = createSnapshotReader(config);
|
|
30
|
+
const read = createSnapshotReader(config, delegation);
|
|
24
31
|
|
|
25
32
|
ctx.ui.setFooter((tui, _theme, footerData) =>
|
|
26
33
|
createFooter(
|
|
@@ -33,7 +40,11 @@ export function registerFooter(pi: ExtensionAPI): void {
|
|
|
33
40
|
);
|
|
34
41
|
};
|
|
35
42
|
|
|
36
|
-
pi.on("session_start", (_event, ctx) =>
|
|
43
|
+
pi.on("session_start", (_event, ctx) => {
|
|
44
|
+
// A new session in this process starts from zero: the ledger is not branch state.
|
|
45
|
+
delegation.reset();
|
|
46
|
+
install(ctx);
|
|
47
|
+
});
|
|
37
48
|
pi.on("session_tree", (_event, ctx) => install(ctx));
|
|
38
49
|
pi.on("session_shutdown", (_event, ctx) => {
|
|
39
50
|
if (ctx.mode !== "tui") return;
|
|
@@ -11,7 +11,7 @@ import { shortenPath } from "../../../../lib/format.ts";
|
|
|
11
11
|
import { gaugeRole } from "../../../../lib/tui/gauge.ts";
|
|
12
12
|
import type { TuiRole } from "../../../../lib/tui/tokens.ts";
|
|
13
13
|
import { CONFIG } from "../config.ts";
|
|
14
|
-
import { DEFAULT_CURRENCY } from "../price/config.ts";
|
|
14
|
+
import { CURRENCY_SYMBOLS, DEFAULT_CURRENCY } from "../price/config.ts";
|
|
15
15
|
import type { Currency } from "../price/types.ts";
|
|
16
16
|
|
|
17
17
|
/** Theme tokens for the reasoning-effort indicator. */
|
|
@@ -31,7 +31,8 @@ export type ThinkingColor =
|
|
|
31
31
|
* `¥<0.01` — so a small cost is never reported as free.
|
|
32
32
|
*/
|
|
33
33
|
export function formatCost(cost: number, currency: Currency = DEFAULT_CURRENCY): string {
|
|
34
|
-
const {
|
|
34
|
+
const { decimals } = currency;
|
|
35
|
+
const symbol = CURRENCY_SYMBOLS[currency.code];
|
|
35
36
|
if (!Number.isFinite(cost) || cost <= 0) return `${symbol}${(0).toFixed(decimals)}`;
|
|
36
37
|
const minimum = 10 ** -decimals;
|
|
37
38
|
if (cost < minimum) return `${symbol}<${minimum.toFixed(decimals)}`;
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
cacheSegment,
|
|
13
13
|
contextSegment,
|
|
14
14
|
costSegment,
|
|
15
|
+
delegationSegment,
|
|
15
16
|
modelSegment,
|
|
16
17
|
modesSegment,
|
|
17
18
|
pwdSegment,
|
|
@@ -39,6 +40,7 @@ export function buildLines(snapshot: StatusSnapshot, theme: Theme, home: string
|
|
|
39
40
|
contextSegment(snapshot, theme),
|
|
40
41
|
windowSegment(snapshot, theme),
|
|
41
42
|
costSegment(snapshot, theme),
|
|
43
|
+
delegationSegment(snapshot, theme),
|
|
42
44
|
tokensSegment(snapshot, theme),
|
|
43
45
|
cacheSegment(snapshot, theme),
|
|
44
46
|
modesSegment(snapshot, theme),
|