@gavin-hu/my-pi-agent 0.18.0 → 0.20.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 +66 -52
- package/README.zh-CN.md +56 -46
- package/docs/extensions/README.md +34 -0
- package/docs/extensions/board.md +507 -0
- package/{extensions/bytefly/README.md → docs/extensions/bytefly.md} +774 -574
- package/{extensions/check/README.md → docs/extensions/check.md} +83 -68
- package/{extensions/context/README.md → docs/extensions/context.md} +40 -23
- package/docs/extensions/file-browse.md +561 -0
- package/{extensions/file-open/README.md → docs/extensions/file-open.md} +31 -23
- package/{extensions/file-read/README.md → docs/extensions/file-read.md} +47 -55
- package/{extensions/goal/README.md → docs/extensions/goal.md} +115 -131
- package/{extensions/job/README.md → docs/extensions/job.md} +62 -52
- package/{extensions/memory/README.md → docs/extensions/memory.md} +71 -60
- package/{extensions/plan/README.md → docs/extensions/plan.md} +440 -322
- package/{extensions/question/README.md → docs/extensions/question.md} +54 -31
- package/{extensions/rewind/README.md → docs/extensions/rewind.md} +115 -122
- package/docs/extensions/session-bus.md +298 -0
- package/{extensions/subagent/README.md → docs/extensions/subagent.md} +165 -118
- package/docs/extensions/team.md +254 -0
- package/{extensions/todo/README.md → docs/extensions/todo.md} +85 -88
- package/{extensions/watch/README.md → docs/extensions/watch.md} +54 -36
- package/{extensions/web-access/README.md → docs/extensions/web-access.md} +93 -50
- package/{extensions/wechat/README.md → docs/extensions/wechat.md} +260 -163
- package/{extensions/worktree/README.md → docs/extensions/worktree.md} +137 -79
- package/extensions/board/commands.ts +105 -0
- package/extensions/board/config.ts +36 -0
- package/extensions/board/format.ts +170 -0
- package/extensions/board/index.ts +40 -0
- package/extensions/board/input.ts +54 -0
- package/extensions/board/join.ts +42 -0
- package/extensions/board/page.ts +388 -0
- package/extensions/board/result.ts +108 -0
- package/extensions/board/rows.ts +48 -0
- package/extensions/board/schema.ts +90 -0
- package/extensions/board/seat.ts +28 -0
- package/extensions/board/snapshot.ts +42 -0
- package/extensions/board/tools.ts +444 -0
- package/extensions/board/transport/router.ts +91 -0
- package/extensions/board/transport/server.ts +138 -0
- package/extensions/board/types.ts +121 -0
- package/extensions/board/view/client.ts +51 -0
- package/extensions/board/view/dialog.ts +43 -0
- package/extensions/board/view/keys.ts +125 -0
- package/extensions/board/view/live.ts +231 -0
- package/extensions/board/view/names.ts +91 -0
- package/extensions/board/view/page.ts +118 -0
- package/extensions/board/view/render.ts +519 -0
- package/extensions/board/view/styles.ts +228 -0
- package/extensions/board/view/team.ts +327 -0
- package/extensions/bytefly/footer/budget.ts +6 -5
- package/extensions/bytefly/footer/collect/entries.ts +12 -4
- package/extensions/bytefly/footer/collect/snapshot.ts +8 -17
- package/extensions/bytefly/footer/config.ts +19 -8
- package/extensions/bytefly/footer/price/price.ts +30 -8
- package/extensions/bytefly/footer/register.ts +5 -5
- package/extensions/bytefly/footer/render/format.ts +18 -0
- package/extensions/bytefly/footer/render/lines.ts +23 -7
- package/extensions/bytefly/footer/render/segments.ts +117 -31
- package/extensions/bytefly/footer/types.ts +29 -5
- package/extensions/bytefly/footer/unpriced.ts +42 -17
- package/extensions/bytefly/panel/commands.ts +67 -7
- package/extensions/bytefly/panel/filter.ts +68 -0
- package/extensions/bytefly/panel/format.ts +16 -15
- package/extensions/bytefly/panel/join.ts +56 -13
- package/extensions/bytefly/panel/prices.ts +43 -0
- package/extensions/bytefly/panel/settings-runtime.ts +0 -6
- package/extensions/bytefly/panel/settings.ts +75 -43
- package/extensions/bytefly/panel/tui/entries.ts +26 -8
- package/extensions/bytefly/panel/tui/frame.ts +130 -93
- package/extensions/bytefly/panel/tui/keys.ts +352 -0
- package/extensions/bytefly/panel/tui/list-view.ts +25 -46
- package/extensions/bytefly/panel/tui/panes.ts +76 -0
- package/extensions/bytefly/panel/tui/section-view.ts +126 -0
- package/extensions/bytefly/panel/tui/settings-section.ts +54 -125
- package/extensions/bytefly/panel/tui/settings-view.ts +30 -131
- package/extensions/bytefly/panel/tui/side-pane.ts +224 -0
- package/extensions/bytefly/panel/tui.ts +306 -259
- package/extensions/bytefly/picker/discovery.ts +0 -2
- package/extensions/bytefly/picker/types.ts +0 -2
- package/extensions/bytefly/update/check.ts +39 -0
- package/extensions/bytefly/update/format.ts +15 -0
- package/extensions/bytefly/update/register.ts +73 -34
- package/extensions/bytefly/update/state.ts +6 -3
- package/extensions/check/commands.ts +2 -1
- package/extensions/context/tui.ts +1 -1
- package/extensions/file-browse/{config.ts → config/config.ts} +1 -1
- package/extensions/file-browse/files/classify.ts +136 -0
- package/extensions/file-browse/files/files.ts +12 -0
- package/extensions/file-browse/files/index.ts +90 -0
- package/extensions/file-browse/files/list.ts +261 -0
- package/extensions/file-browse/{paths.ts → files/paths.ts} +1 -1
- package/extensions/file-browse/files/tree.ts +101 -0
- package/extensions/file-browse/{git.ts → git/git.ts} +10 -3
- package/extensions/file-browse/index.ts +4 -4
- package/extensions/file-browse/{live.ts → live/live.ts} +20 -69
- package/extensions/file-browse/{router.ts → transport/router.ts} +21 -17
- package/extensions/file-browse/transport/server.ts +110 -0
- package/extensions/file-browse/{chrome.ts → view/chrome.ts} +4 -4
- package/extensions/file-browse/{client.ts → view/client.ts} +1 -1
- package/extensions/file-browse/{diff-html.ts → view/diff-html.ts} +3 -3
- package/extensions/file-browse/view/emit.ts +51 -0
- package/extensions/file-browse/{html.ts → view/html.ts} +19 -18
- package/extensions/file-browse/{icons.ts → view/icons.ts} +1 -1
- package/extensions/file-browse/{markdown.ts → view/markdown.ts} +18 -20
- package/extensions/file-browse/{raw-html.ts → view/raw-html.ts} +19 -14
- package/extensions/file-browse/{styles.ts → view/styles.ts} +29 -5
- package/extensions/file-browse/{tree.ts → view/tree.ts} +1 -1
- package/extensions/file-browse/{urls.ts → view/urls.ts} +1 -1
- package/extensions/file-read/schema.ts +2 -1
- package/extensions/job/runtime.ts +4 -4
- package/extensions/memory/store.ts +12 -5
- package/extensions/plan/blocks.ts +23 -2
- package/extensions/plan/commands.ts +21 -16
- package/extensions/plan/feedback.ts +185 -52
- package/extensions/plan/format.ts +10 -3
- package/extensions/plan/index.ts +23 -5
- package/extensions/plan/plan-files.ts +88 -0
- package/extensions/plan/plan-index.ts +43 -9
- package/extensions/plan/plan-text.ts +33 -23
- package/extensions/plan/plans.ts +27 -34
- package/extensions/plan/policy.ts +30 -17
- package/extensions/plan/review.ts +3 -27
- package/extensions/plan/runtime.ts +52 -11
- package/extensions/plan/sections.ts +112 -0
- package/extensions/plan/steps.ts +3 -22
- package/extensions/plan/todos.ts +51 -3
- package/extensions/plan/tools/exit.ts +20 -95
- package/extensions/plan/tools/todo-seed.ts +4 -2
- package/extensions/plan/tools/write.ts +12 -1
- package/extensions/plan/tui.ts +5 -34
- package/extensions/plan/types.ts +15 -8
- package/extensions/plan/web-blocks.ts +89 -0
- package/extensions/plan/web-chrome.ts +3 -5
- package/extensions/plan/web-client.ts +179 -330
- package/extensions/plan/web-collab.ts +3 -3
- package/extensions/plan/web-comments.ts +18 -8
- package/extensions/plan/web-composer.ts +80 -0
- package/extensions/plan/web-draft.ts +40 -0
- package/extensions/plan/web-keys.ts +134 -0
- package/extensions/plan/web-outline.ts +192 -0
- package/extensions/plan/web-page.ts +4 -8
- package/extensions/plan/web-plan.ts +150 -0
- package/extensions/plan/web-router.ts +3 -58
- package/extensions/plan/web-settle.ts +194 -0
- package/extensions/plan/web-share-session.ts +24 -30
- package/extensions/plan/web-streams.ts +14 -14
- package/extensions/plan/web-tokens.ts +2 -2
- package/extensions/plan/web.ts +66 -120
- package/extensions/rewind/{commands.ts → commands/commands.ts} +5 -5
- package/extensions/rewind/{review.ts → commands/review.ts} +15 -14
- package/extensions/rewind/{rewind.ts → commands/rewind.ts} +6 -6
- package/extensions/rewind/{config.ts → config/config.ts} +1 -1
- package/extensions/rewind/index.ts +3 -3
- package/extensions/rewind/{policy.ts → state/policy.ts} +4 -4
- package/extensions/rewind/{runtime.ts → state/runtime.ts} +11 -11
- package/extensions/rewind/{snapshot-run.ts → state/snapshot-run.ts} +1 -1
- package/extensions/rewind/{timeline.ts → state/timeline.ts} +2 -2
- package/extensions/rewind/{format.ts → text/format.ts} +3 -3
- package/extensions/rewind/{git.ts → transport/git.ts} +2 -2
- package/extensions/rewind/{restore.ts → transport/restore.ts} +2 -2
- package/extensions/rewind/{snapshot.ts → transport/snapshot.ts} +1 -1
- package/extensions/rewind/{store.ts → transport/store.ts} +1 -1
- package/extensions/rewind/{checklist.ts → view/checklist.ts} +6 -6
- package/extensions/rewind/{hunk-picker.ts → view/hunk-picker.ts} +8 -8
- package/extensions/rewind/{path-picker.ts → view/path-picker.ts} +7 -7
- package/extensions/rewind/{review-tui.ts → view/review-tui.ts} +11 -11
- package/extensions/rewind/{rewind-tui.ts → view/rewind-tui.ts} +17 -11
- package/extensions/rewind/{status.ts → view/status.ts} +4 -4
- package/extensions/session-bus/commands.ts +17 -69
- package/extensions/session-bus/format.ts +22 -45
- package/extensions/session-bus/index.ts +13 -7
- package/extensions/session-bus/join-request.ts +40 -0
- package/extensions/session-bus/mail-watch.ts +134 -0
- package/extensions/session-bus/notice.ts +32 -4
- package/{lib/claims → extensions/session-bus}/overlap.ts +11 -18
- package/extensions/session-bus/protocol.ts +19 -49
- package/extensions/session-bus/resolve.ts +67 -0
- package/extensions/session-bus/result.ts +16 -51
- package/extensions/session-bus/rows.ts +11 -5
- package/extensions/session-bus/runtime.ts +220 -267
- package/extensions/session-bus/store.ts +55 -7
- package/extensions/session-bus/tools.ts +31 -66
- package/extensions/session-bus/tui.ts +5 -5
- package/extensions/session-bus/types.ts +23 -14
- package/extensions/subagent/agents.ts +9 -8
- package/extensions/subagent/commands.ts +2 -1
- package/extensions/subagent/invocation.ts +4 -2
- package/extensions/team/commands.ts +145 -0
- package/extensions/team/dispatch.ts +133 -0
- package/extensions/team/format.ts +119 -0
- package/extensions/team/index.ts +34 -0
- package/extensions/team/schema.ts +29 -0
- package/extensions/team/tools.ts +267 -0
- package/extensions/team/types.ts +65 -0
- package/extensions/todo/index.ts +4 -1
- package/extensions/todo/nudge.ts +8 -2
- package/extensions/web-access/fetch/page.ts +4 -1
- package/extensions/web-access/fetch/structured.ts +447 -0
- package/extensions/web-access/fetch/tool.ts +2 -1
- package/extensions/web-access/search/tool.ts +2 -1
- package/extensions/wechat/bridge.ts +106 -251
- package/extensions/wechat/{commands.ts → commands/commands.ts} +26 -18
- package/extensions/wechat/{doctor.ts → commands/doctor.ts} +5 -5
- package/extensions/wechat/{history.ts → commands/history.ts} +1 -1
- package/extensions/wechat/{login.ts → commands/login.ts} +3 -3
- package/extensions/wechat/{slash.ts → commands/slash.ts} +38 -2
- package/extensions/wechat/dialog/channels.ts +136 -0
- package/extensions/wechat/{interaction.ts → dialog/interaction.ts} +37 -37
- package/extensions/wechat/{prompt.ts → dialog/prompt.ts} +2 -2
- package/extensions/wechat/{remote-ui.ts → dialog/remote-ui.ts} +1 -1
- package/extensions/wechat/index.ts +10 -10
- package/extensions/wechat/intake.ts +245 -0
- package/extensions/wechat/{admission.ts → peer/admission.ts} +2 -2
- package/extensions/wechat/{peer-session.ts → peer/peer-session.ts} +2 -2
- package/extensions/wechat/{peer-worker.ts → peer/peer-worker.ts} +100 -22
- package/extensions/wechat/{presence.ts → peer/presence.ts} +8 -7
- package/extensions/wechat/{reply-pipeline.ts → peer/reply-pipeline.ts} +1 -1
- package/extensions/wechat/{credentials.ts → state/credentials.ts} +2 -2
- package/extensions/wechat/{state.ts → state/state.ts} +14 -2
- package/extensions/wechat/{format.ts → text/format.ts} +3 -3
- package/extensions/wechat/{notices.ts → text/notices.ts} +8 -0
- package/extensions/wechat/tools.ts +29 -12
- package/extensions/wechat/{client.ts → transport/client.ts} +26 -2
- package/extensions/wechat/{deliver.ts → transport/deliver.ts} +72 -28
- package/extensions/wechat/{inbound.ts → transport/inbound.ts} +5 -5
- package/extensions/wechat/{media.ts → transport/media.ts} +11 -5
- package/extensions/wechat/transport/outbound.ts +142 -0
- package/extensions/wechat/{poll.ts → transport/poll.ts} +3 -3
- package/extensions/wechat/types.ts +10 -1
- package/extensions/wechat/{login-screen.ts → view/login-screen.ts} +4 -4
- package/extensions/wechat/{status-screen.ts → view/status-screen.ts} +31 -26
- package/extensions/worktree/commands.ts +24 -17
- package/extensions/worktree/exit.ts +12 -6
- package/extensions/worktree/git.ts +22 -14
- package/extensions/worktree/guard.ts +18 -290
- package/extensions/worktree/handoff.ts +10 -2
- package/extensions/worktree/index.ts +2 -1
- package/extensions/worktree/lifecycle.ts +2 -0
- package/extensions/worktree/registry.ts +20 -11
- package/extensions/worktree/root-tools.ts +13 -3
- package/extensions/worktree/runtime.ts +8 -2
- package/extensions/worktree/screen.ts +15 -8
- package/extensions/worktree/shell.ts +303 -0
- package/extensions/worktree/state.ts +1 -1
- package/extensions/worktree/status.ts +42 -9
- package/extensions/worktree/tools.ts +19 -13
- package/lib/README.md +11 -8
- package/lib/board/card.ts +479 -0
- package/lib/board/links.ts +141 -0
- package/lib/board/page-pointer.ts +310 -0
- package/lib/board/round-text.ts +81 -0
- package/lib/board/round.ts +94 -0
- package/lib/board/rules.ts +437 -0
- package/lib/board/session.ts +105 -0
- package/lib/board/store.ts +497 -0
- package/lib/board/view.ts +234 -0
- package/lib/errors.ts +12 -4
- package/lib/format.ts +35 -0
- package/lib/http/guard.ts +112 -0
- package/lib/http/index.ts +16 -5
- package/lib/http/serve.ts +237 -0
- package/lib/http/server.ts +12 -10
- package/lib/http/sink.ts +135 -0
- package/lib/http/token.ts +38 -0
- package/lib/plan-entry.ts +11 -0
- package/lib/policy.ts +25 -2
- package/lib/requirements.ts +6 -7
- package/lib/session-label.ts +64 -0
- package/lib/session-roster.ts +201 -0
- package/lib/session-tool.ts +184 -0
- package/lib/spawn.ts +37 -3
- package/lib/stray-fields.ts +45 -0
- package/lib/tool-names.ts +38 -2
- package/lib/tui/glyphs.ts +3 -2
- package/lib/web/README.md +57 -4
- package/lib/web/client-runtime.ts +123 -0
- package/lib/web/components.ts +5 -0
- package/lib/web/index.ts +4 -2
- package/lib/web/markup.ts +88 -1
- package/lib/web/names.ts +42 -0
- package/lib/web/runtime.ts +7 -1
- package/lib/web/tokens.ts +25 -2
- package/package.json +26 -14
- package/skills/next/SKILL.md +4 -3
- package/skills/project-docs/SKILL.md +133 -0
- package/skills/project-docs/references/audit.md +131 -0
- package/skills/project-docs/references/changelog.md +105 -0
- package/skills/project-docs/references/discovery.md +127 -0
- package/skills/project-docs/references/readme.md +80 -0
- package/skills/project-docs/references/release.md +84 -0
- package/skills/project-docs/references/roadmap.md +93 -0
- package/skills/project-docs/scripts/state.ts +316 -0
- package/skills/refactor/SKILL.md +100 -125
- package/skills/refactor/references/handoff.md +35 -0
- package/skills/refactor/references/signals.md +54 -0
- package/skills/refactor/scripts/inventory.ts +587 -0
- package/extensions/bytefly/panel/tui/extensions-view.ts +0 -40
- package/extensions/bytefly/panel/tui/skills-view.ts +0 -50
- package/extensions/file-browse/README.md +0 -506
- package/extensions/file-browse/files.ts +0 -564
- package/extensions/file-browse/server.ts +0 -141
- package/extensions/job/process.ts +0 -35
- package/extensions/plan/split.ts +0 -87
- package/extensions/session-bus/README.md +0 -247
- package/extensions/session-bus/team.ts +0 -102
- package/extensions/wechat/outbound.ts +0 -100
- package/lib/plans/sections.ts +0 -172
- package/lib/plans/setting.ts +0 -52
- package/lib/plans/split.ts +0 -215
- package/lib/plans/store.ts +0 -19
- package/lib/plans/team.ts +0 -151
- package/skills/changelog/SKILL.md +0 -148
- package/skills/parallel-dev/SKILL.md +0 -150
- /package/extensions/file-browse/{rev.ts → live/rev.ts} +0 -0
- /package/extensions/file-browse/{diff.ts → text/diff.ts} +0 -0
- /package/extensions/file-browse/{fuzzy.ts → text/fuzzy.ts} +0 -0
- /package/extensions/file-browse/{live-client.ts → view/live-client.ts} +0 -0
- /package/extensions/file-browse/{palette.ts → view/palette.ts} +0 -0
- /package/extensions/rewind/{patch.ts → text/patch.ts} +0 -0
- /package/extensions/rewind/{paths.ts → text/paths.ts} +0 -0
- /package/extensions/rewind/{queue.ts → transport/queue.ts} +0 -0
- /package/extensions/rewind/{temp-index.ts → transport/temp-index.ts} +0 -0
- /package/extensions/wechat/{lock.ts → state/lock.ts} +0 -0
- /package/extensions/wechat/{channel.ts → text/channel.ts} +0 -0
|
@@ -18,7 +18,7 @@ import type { DelegationLedger } from "../delegation.ts";
|
|
|
18
18
|
import { createPricer } from "../price/price.ts";
|
|
19
19
|
import type { ByteflyConfig } from "../price/types.ts";
|
|
20
20
|
import { zeroUsage } from "../../../../lib/usage.ts";
|
|
21
|
-
import type { ContextInfo, FooterData, PricedUsage, StatusSnapshot
|
|
21
|
+
import type { ContextInfo, FooterData, PricedUsage, StatusSnapshot } from "../types.ts";
|
|
22
22
|
import { usageRecords } from "./entries.ts";
|
|
23
23
|
|
|
24
24
|
/**
|
|
@@ -42,22 +42,10 @@ export interface SnapshotReader {
|
|
|
42
42
|
peek(): PricedUsage | undefined;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
/** The five counters, without the unpriced count that only the notice reads. */
|
|
46
|
-
function countsOf(priced: PricedUsage): UsageTotals {
|
|
47
|
-
return {
|
|
48
|
-
input: priced.input,
|
|
49
|
-
output: priced.output,
|
|
50
|
-
cacheRead: priced.cacheRead,
|
|
51
|
-
cacheWrite: priced.cacheWrite,
|
|
52
|
-
cost: priced.cost,
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
|
|
56
45
|
export function createSnapshotReader(config: ByteflyConfig, delegation: DelegationLedger): SnapshotReader {
|
|
57
46
|
const price = createPricer(config);
|
|
58
47
|
let key: string | null = null;
|
|
59
|
-
let usage: PricedUsage = { ...zeroUsage(), unpriced: 0 };
|
|
60
|
-
let usageCounts: UsageTotals = zeroUsage();
|
|
48
|
+
let usage: PricedUsage = { ...zeroUsage(), unpriced: 0, unpricedModels: [] };
|
|
61
49
|
let delegationTotals = { runs: 0, usage: zeroUsage() };
|
|
62
50
|
let cacheHitRate: number | null = null;
|
|
63
51
|
let context: ContextInfo = { tokens: null, contextWindow: 0, percent: null };
|
|
@@ -73,11 +61,13 @@ export function createSnapshotReader(config: ByteflyConfig, delegation: Delegati
|
|
|
73
61
|
const active = current ? { provider: current.provider, model: current.id } : undefined;
|
|
74
62
|
const scanned = usageRecords(session.getEntries(), active);
|
|
75
63
|
usage = price(scanned.records);
|
|
76
|
-
usageCounts = countsOf(usage);
|
|
77
64
|
cacheHitRate = scanned.cacheHitRate;
|
|
78
65
|
// The child runs are priced by the same pricer, so a currency or rate
|
|
79
66
|
// change moves both numbers together — and neither is folded into the other.
|
|
80
|
-
|
|
67
|
+
// A child run's unpriced records are its own: the ledger keeps one amount per run,
|
|
68
|
+
// and nothing here reports them separately.
|
|
69
|
+
const { unpriced: _childUnpriced, unpricedModels: _childModels, ...childUsage } = price(delegation.records());
|
|
70
|
+
delegationTotals = { runs: delegation.runs(), usage: childUsage };
|
|
81
71
|
|
|
82
72
|
const usageInfo = ctx.getContextUsage();
|
|
83
73
|
context = {
|
|
@@ -105,8 +95,9 @@ export function createSnapshotReader(config: ByteflyConfig, delegation: Delegati
|
|
|
105
95
|
model,
|
|
106
96
|
thinkingLevel: ctx.thinkingLevel ?? null,
|
|
107
97
|
context,
|
|
108
|
-
usage
|
|
98
|
+
usage,
|
|
109
99
|
currency: config.currency,
|
|
100
|
+
budget: config.budget,
|
|
110
101
|
cacheHitRate,
|
|
111
102
|
providerCount: footerData.getAvailableProviderCount(),
|
|
112
103
|
delegation: delegationTotals,
|
|
@@ -9,32 +9,43 @@
|
|
|
9
9
|
|
|
10
10
|
import { GLYPHS, SEPARATORS, STATUS_KEYS } from "../../../lib/tui/glyphs.ts";
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Status keys that are alerts rather than modes, and so are never dropped.
|
|
14
|
+
* A mode can be read again from the session; a failure cannot be re-derived once
|
|
15
|
+
* its chip is off the bar. Annotated rather than inferred so the list can be read
|
|
16
|
+
* against an arbitrary key from `getExtensionStatuses()`.
|
|
17
|
+
*/
|
|
18
|
+
const ALERT_STATUS_KEYS: readonly string[] = [STATUS_KEYS.jobsFailure];
|
|
19
|
+
|
|
12
20
|
export const CONFIG = {
|
|
13
21
|
/** Context percentage thresholds. */
|
|
14
22
|
thresholds: { warn: 70, danger: 90 },
|
|
15
23
|
/** The block counts the context gauge steps down through. */
|
|
16
24
|
gauge: { widths: [10, 5, 3, 0] as const },
|
|
17
|
-
/**
|
|
18
|
-
|
|
19
|
-
* the footer's own solid `│`, not the shared `╎`: that glyph is the transcript
|
|
20
|
-
* rail (and `chrome.rail`'s default), and a status line is not a rail.
|
|
21
|
-
*/
|
|
22
|
-
separators: { group: " │ ", item: SEPARATORS.item },
|
|
25
|
+
/** Separator printed before every segment but the first in its zone. */
|
|
26
|
+
separators: { item: SEPARATORS.item },
|
|
23
27
|
icons: {
|
|
24
28
|
branch: GLYPHS.branch,
|
|
25
29
|
detached: GLYPHS.detached,
|
|
26
30
|
worktree: GLYPHS.worktree,
|
|
31
|
+
board: GLYPHS.board,
|
|
27
32
|
browse: GLYPHS.browse,
|
|
28
33
|
delegation: GLYPHS.delegation,
|
|
29
34
|
},
|
|
30
35
|
/** Status key the worktree extension uses; routed to line 1's right zone. */
|
|
31
36
|
worktreeStatusKey: STATUS_KEYS.worktree,
|
|
37
|
+
/** Status key the board extension publishes its page port under. */
|
|
38
|
+
boardStatusKey: STATUS_KEYS.board,
|
|
32
39
|
/** Status key the file-browse extension uses; routed to line 1's right zone. */
|
|
33
40
|
browseStatusKey: STATUS_KEYS.browse,
|
|
34
41
|
/** Status key whose ` · detail` suffix (the plan file name) is dropped. */
|
|
35
42
|
planStatusKey: STATUS_KEYS.planMode,
|
|
36
|
-
/**
|
|
37
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Status keys that are alerts rather than modes, and so are never dropped.
|
|
45
|
+
* A mode can be read again from the session; a failure cannot be re-derived
|
|
46
|
+
* once its chip is off the bar.
|
|
47
|
+
*/
|
|
48
|
+
alertStatusKeys: ALERT_STATUS_KEYS,
|
|
38
49
|
labels: { noModel: "no model", detached: "detached" },
|
|
39
50
|
/** Minimum gap kept between the left and right zones. */
|
|
40
51
|
minGap: 2,
|
|
@@ -5,13 +5,14 @@
|
|
|
5
5
|
* matching rates and the timezone formatter. Both currencies price a record
|
|
6
6
|
* from `rates` and `peak` when a key matches. They differ only for a model no
|
|
7
7
|
* key matches: `USD` adds Pi's recorded dollar amount (its catalog is already
|
|
8
|
-
* per model), while `CNY` adds nothing
|
|
9
|
-
*
|
|
8
|
+
* per model), while `CNY` adds nothing. A record neither of them prices is
|
|
9
|
+
* unpriced, and it is counted by *model* rather than by record: the count says
|
|
10
|
+
* the total is short, and the key says how to fix it.
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
13
|
import { zeroUsage } from "../../../../lib/usage.ts";
|
|
13
14
|
import type { ByteflyConfig, ModelRates } from "./types.ts";
|
|
14
|
-
import type { PricedUsage, UsageRecord } from "../types.ts";
|
|
15
|
+
import type { PricedUsage, UnpricedModel, UsageRecord } from "../types.ts";
|
|
15
16
|
import { createRateLookup } from "./rates.ts";
|
|
16
17
|
import { isPeak, peakFormatter } from "./schedule.ts";
|
|
17
18
|
|
|
@@ -44,7 +45,10 @@ export function createPricer(config: ByteflyConfig): (records: readonly UsageRec
|
|
|
44
45
|
const format = config.peak.windows.length > 0 ? peakFormatter(config.peak.timezone) : null;
|
|
45
46
|
|
|
46
47
|
return (records) => {
|
|
47
|
-
const totals: PricedUsage = { ...zeroUsage(), unpriced: 0 };
|
|
48
|
+
const totals: PricedUsage = { ...zeroUsage(), unpriced: 0, unpricedModels: [] };
|
|
49
|
+
// One tally per model key, in first-seen order, so a report reads in the order the
|
|
50
|
+
// session met the models rather than in hash order.
|
|
51
|
+
const unpriced = new Map<string, number>();
|
|
48
52
|
for (const record of records) {
|
|
49
53
|
totals.input += record.input;
|
|
50
54
|
totals.output += record.output;
|
|
@@ -55,15 +59,33 @@ export function createPricer(config: ByteflyConfig): (records: readonly UsageRec
|
|
|
55
59
|
totals.cost += ratedCost(config, format, record, rates);
|
|
56
60
|
continue;
|
|
57
61
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
+
if (dollars) {
|
|
63
|
+
totals.cost += record.recordedCost;
|
|
64
|
+
// Pi priced it, unless its own amount is zero — which is what it records for a
|
|
65
|
+
// model its catalog does not know. Then nothing has priced this record.
|
|
66
|
+
if (record.recordedCost !== 0) continue;
|
|
67
|
+
}
|
|
68
|
+
totals.unpriced += 1;
|
|
69
|
+
const key = rateKey(record);
|
|
70
|
+
unpriced.set(key, (unpriced.get(key) ?? 0) + 1);
|
|
62
71
|
}
|
|
72
|
+
totals.unpricedModels = [...unpriced.entries()]
|
|
73
|
+
.map(([key, count]): UnpricedModel => ({ key, records: count }))
|
|
74
|
+
.sort((a, b) => b.records - a.records || a.key.localeCompare(b.key));
|
|
63
75
|
return totals;
|
|
64
76
|
};
|
|
65
77
|
}
|
|
66
78
|
|
|
79
|
+
/**
|
|
80
|
+
* The `price.rates` key that would price this record.
|
|
81
|
+
*
|
|
82
|
+
* `*` stands in for a side the record does not name, because that is what the lookup
|
|
83
|
+
* matches; a model id that contains a slash is kept whole, the way OpenRouter's ids are.
|
|
84
|
+
*/
|
|
85
|
+
function rateKey(record: UsageRecord): string {
|
|
86
|
+
return `${record.provider ?? "*"}/${record.model ?? "*"}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
67
89
|
/** One record's cost at its matched rates, after the peak schedule. */
|
|
68
90
|
function ratedCost(
|
|
69
91
|
config: ByteflyConfig,
|
|
@@ -72,12 +72,12 @@ export function registerFooter(pi: ExtensionAPI, deps: FooterDeps = {}): void {
|
|
|
72
72
|
if (!budget || ctx.mode !== "tui") return;
|
|
73
73
|
const usage = budget.read.peek();
|
|
74
74
|
if (!usage) return;
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
if (!warnedUnpriced && usage.
|
|
75
|
+
// A model with no rate contributes nothing: CNY has no fallback, and USD's fallback
|
|
76
|
+
// is Pi's own amount, which is zero for a model its catalog cannot price either.
|
|
77
|
+
if (!warnedUnpriced && usage.unpricedModels.length > 0) {
|
|
78
78
|
warnedUnpriced = true;
|
|
79
|
-
pi.sendMessage(unpricedNotice(usage.
|
|
80
|
-
deps.notify?.(unpricedBody(usage.
|
|
79
|
+
pi.sendMessage(unpricedNotice(usage.unpricedModels));
|
|
80
|
+
deps.notify?.(unpricedBody(usage.unpricedModels));
|
|
81
81
|
}
|
|
82
82
|
if (warned || budget.limit <= 0) return;
|
|
83
83
|
const state = budgetState(budget.limit, usage.cost);
|
|
@@ -39,6 +39,24 @@ export function formatCost(cost: number, currency: Currency = DEFAULT_CURRENCY):
|
|
|
39
39
|
return `${symbol}${cost.toFixed(decimals)}`;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
/**
|
|
43
|
+
* A budget ceiling, in the configured currency, with trailing zeros dropped.
|
|
44
|
+
*
|
|
45
|
+
* A ceiling is not a price: it is the round number the user typed, and the bar
|
|
46
|
+
* pairs it with the spend (`$0.31/$5`), where the currency's display precision
|
|
47
|
+
* would spend two of the bar's columns on `$5.000`. The budget notice names the
|
|
48
|
+
* same figure through here, so the bar and the notice cannot disagree.
|
|
49
|
+
*/
|
|
50
|
+
export function formatLimit(limit: number, currency: Currency = DEFAULT_CURRENCY): string {
|
|
51
|
+
const { decimals } = currency;
|
|
52
|
+
const symbol = CURRENCY_SYMBOLS[currency.code];
|
|
53
|
+
if (!Number.isFinite(limit) || limit <= 0) return `${symbol}${(0).toFixed(decimals)}`;
|
|
54
|
+
const minimum = 10 ** -decimals;
|
|
55
|
+
if (limit < minimum) return `${symbol}<${minimum.toFixed(decimals)}`;
|
|
56
|
+
const text = limit.toFixed(decimals);
|
|
57
|
+
return `${symbol}${text.includes(".") ? text.replace(/\.?0+$/, "") : text}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
42
60
|
/** Rounded percentage, or `?` when the value is unknown. */
|
|
43
61
|
export function formatPercent(percent: number | null): string {
|
|
44
62
|
return percent === null ? "?" : `${Math.round(percent)}%`;
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Turn a `StatusSnapshot` into the two footer lines.
|
|
3
3
|
*
|
|
4
|
-
* Line 1 —
|
|
5
|
-
*
|
|
6
|
-
* usage
|
|
4
|
+
* Line 1 — state: left `pwd` and the session name, right the alert, the mode
|
|
5
|
+
* chips, then `⎇ branch · ⑂ worktree · ⊞ 5043 · ◉ 4909`. Line 2 — meters: left
|
|
6
|
+
* the context gauge and usage, right the model and thinking level.
|
|
7
|
+
*
|
|
8
|
+
* The split is one job per row. The state chips change while a session runs, so
|
|
9
|
+
* they ride the line with slack; the meters are the dense row, and a fact that
|
|
10
|
+
* moves onto the top row is one the bottom row stops dropping to fit.
|
|
7
11
|
*/
|
|
8
12
|
|
|
9
13
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
10
14
|
import {
|
|
15
|
+
alertSegment,
|
|
11
16
|
branchSegment,
|
|
12
17
|
cacheSegment,
|
|
13
18
|
contextSegment,
|
|
@@ -15,8 +20,8 @@ import {
|
|
|
15
20
|
delegationSegment,
|
|
16
21
|
modelSegment,
|
|
17
22
|
modesSegment,
|
|
23
|
+
portsSegment,
|
|
18
24
|
pwdSegment,
|
|
19
|
-
browseSegment,
|
|
20
25
|
sessionSegment,
|
|
21
26
|
thinkingSegment,
|
|
22
27
|
tokensSegment,
|
|
@@ -29,11 +34,23 @@ function defined(segments: Array<Segment | null>): Segment[] {
|
|
|
29
34
|
return segments.filter((segment): segment is Segment => segment !== null);
|
|
30
35
|
}
|
|
31
36
|
|
|
32
|
-
/** Build the
|
|
37
|
+
/** Build the state and meter lines for a snapshot. */
|
|
33
38
|
export function buildLines(snapshot: StatusSnapshot, theme: Theme, home: string | undefined): LineSpec[] {
|
|
34
39
|
const line1: LineSpec = {
|
|
35
40
|
left: defined([pwdSegment(snapshot, theme, home), sessionSegment(snapshot, theme)]),
|
|
36
|
-
right:
|
|
41
|
+
// The state chips lead the right zone: it is right-aligned, so a chip that
|
|
42
|
+
// appears at its start leaves the location chips where they were, the same
|
|
43
|
+
// stability the gauge gets by sitting at column 0.
|
|
44
|
+
right: defined([
|
|
45
|
+
alertSegment(snapshot, theme),
|
|
46
|
+
modesSegment(snapshot, theme),
|
|
47
|
+
branchSegment(snapshot, theme),
|
|
48
|
+
worktreeSegment(snapshot, theme),
|
|
49
|
+
// Last, so the two ports form one run behind the location chips. A page starting
|
|
50
|
+
// or stopping then changes the end of the line, and `⎇ branch · ⑂ worktree` stays
|
|
51
|
+
// the adjacent pair a reader scans for where the work lands.
|
|
52
|
+
portsSegment(snapshot, theme),
|
|
53
|
+
]),
|
|
37
54
|
};
|
|
38
55
|
|
|
39
56
|
const line2Left = defined([
|
|
@@ -43,7 +60,6 @@ export function buildLines(snapshot: StatusSnapshot, theme: Theme, home: string
|
|
|
43
60
|
delegationSegment(snapshot, theme),
|
|
44
61
|
tokensSegment(snapshot, theme),
|
|
45
62
|
cacheSegment(snapshot, theme),
|
|
46
|
-
modesSegment(snapshot, theme),
|
|
47
63
|
]);
|
|
48
64
|
|
|
49
65
|
const line2Right = defined([modelSegment(snapshot, theme), thinkingSegment(snapshot, theme)]);
|
|
@@ -13,8 +13,17 @@ import { formatTokens, sanitize, sanitizeLine } from "../../../../lib/format.ts"
|
|
|
13
13
|
import { gaugeBar } from "../../../../lib/tui/gauge.ts";
|
|
14
14
|
import { chip } from "../../../../lib/tui/rows.ts";
|
|
15
15
|
import { paint } from "../../../../lib/tui/tokens.ts";
|
|
16
|
+
import { budgetState } from "../budget.ts";
|
|
16
17
|
import { CONFIG } from "../config.ts";
|
|
17
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
contextRole,
|
|
20
|
+
formatCost,
|
|
21
|
+
formatCwd,
|
|
22
|
+
formatLimit,
|
|
23
|
+
formatPercent,
|
|
24
|
+
thinkingColor,
|
|
25
|
+
truncateLabel,
|
|
26
|
+
} from "./format.ts";
|
|
18
27
|
import { compactStatus, dropStatusDetail } from "./status.ts";
|
|
19
28
|
import type { Segment, StatusSnapshot } from "../types.ts";
|
|
20
29
|
|
|
@@ -91,38 +100,85 @@ export function worktreeSegment(snapshot: StatusSnapshot, theme: Theme): Segment
|
|
|
91
100
|
return { id: "worktree", weight: 3, droppable: true, separator, forms };
|
|
92
101
|
}
|
|
93
102
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
103
|
+
/**
|
|
104
|
+
* The pages this session is serving, as one cluster.
|
|
105
|
+
*
|
|
106
|
+
* The board and file-browse chips say one thing — a page is up on this port — so they
|
|
107
|
+
* share a segment, a weight, and a floor. Read separately they were a mode chip and a
|
|
108
|
+
* port chip: `▤ 5043` rode the mode weight and outlived `◉ 4909` by four columns, which
|
|
109
|
+
* made two facts of the same kind look like two kinds of fact. Shared, they also drop
|
|
110
|
+
* together, and the surviving form is the pair of glyphs, so a bar too narrow for a port
|
|
111
|
+
* still says a page is up.
|
|
112
|
+
*/
|
|
113
|
+
export function portsSegment(snapshot: StatusSnapshot, theme: Theme): Segment | null {
|
|
114
|
+
const open = [
|
|
115
|
+
{ key: CONFIG.boardStatusKey, icon: CONFIG.icons.board },
|
|
116
|
+
{ key: CONFIG.browseStatusKey, icon: CONFIG.icons.browse },
|
|
117
|
+
]
|
|
118
|
+
.filter((entry) => snapshot.statuses.has(entry.key))
|
|
119
|
+
.map((entry) => ({
|
|
120
|
+
icon: paint(theme, "ok", entry.icon),
|
|
121
|
+
port: chipLabel(snapshot.statuses.get(entry.key) ?? ""),
|
|
122
|
+
}));
|
|
123
|
+
if (open.length === 0) return null;
|
|
124
|
+
const joined = (separator: string): string =>
|
|
125
|
+
open
|
|
126
|
+
.map((entry) => (entry.port ? `${entry.icon} ${paint(theme, "accent", entry.port)}` : entry.icon))
|
|
127
|
+
.join(separator);
|
|
128
|
+
// Widest first: the item separator, then a single space, then the glyphs alone. A
|
|
129
|
+
// one-page session has two forms; two pages sharing a padded bar have three.
|
|
130
|
+
const forms = [
|
|
131
|
+
...new Set([joined(faint(theme, CONFIG.separators.item)), joined(" "), open.map((entry) => entry.icon).join(" ")]),
|
|
132
|
+
];
|
|
104
133
|
return {
|
|
105
|
-
id: "
|
|
134
|
+
id: "ports",
|
|
106
135
|
weight: 3,
|
|
107
136
|
droppable: true,
|
|
108
|
-
separator,
|
|
109
|
-
forms
|
|
110
|
-
`${paint(theme, "ok", icon)} ${paint(theme, "accent", label)}`,
|
|
111
|
-
`${paint(theme, "ok", icon)}${paint(theme, "accent", label)}`,
|
|
112
|
-
paint(theme, "ok", icon),
|
|
113
|
-
],
|
|
137
|
+
separator: faint(theme, CONFIG.separators.item),
|
|
138
|
+
forms,
|
|
114
139
|
};
|
|
115
140
|
}
|
|
116
141
|
|
|
117
|
-
/**
|
|
118
|
-
function
|
|
119
|
-
return
|
|
142
|
+
/** Whether a status key names an alert instead of a mode. */
|
|
143
|
+
function isAlert(key: string): boolean {
|
|
144
|
+
return CONFIG.alertStatusKeys.includes(key);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The alerts: a failed job, and anything else whose chip means "this broke".
|
|
149
|
+
*
|
|
150
|
+
* Not droppable, because every other fact on the bar can be read again from
|
|
151
|
+
* somewhere — the branch from git, the mode from the session — and a failure
|
|
152
|
+
* that scrolled off the bar is a failure nobody is looking at. It gives up its
|
|
153
|
+
* text last, keeping the count (`✗1`) instead of collapsing to a bare glyph.
|
|
154
|
+
*/
|
|
155
|
+
export function alertSegment(snapshot: StatusSnapshot, theme: Theme): Segment | null {
|
|
156
|
+
const alerts = [...snapshot.statuses.entries()]
|
|
157
|
+
.filter(([key]) => isAlert(key))
|
|
158
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
159
|
+
.map(([, value]) => sanitize(value))
|
|
160
|
+
.filter(Boolean);
|
|
161
|
+
if (alerts.length === 0) return null;
|
|
162
|
+
const separator = faint(theme, CONFIG.separators.item);
|
|
163
|
+
return {
|
|
164
|
+
id: "alert",
|
|
165
|
+
weight: 1,
|
|
166
|
+
droppable: false,
|
|
167
|
+
separator,
|
|
168
|
+
forms: [alerts.join(separator), alerts.map((status) => compactStatus(status)).join(" ")],
|
|
169
|
+
};
|
|
120
170
|
}
|
|
121
171
|
|
|
122
172
|
export function modesSegment(snapshot: StatusSnapshot, theme: Theme): Segment | null {
|
|
123
173
|
const others = [...snapshot.statuses.entries()]
|
|
124
|
-
.filter(
|
|
125
|
-
|
|
174
|
+
.filter(
|
|
175
|
+
([key]) =>
|
|
176
|
+
key !== CONFIG.worktreeStatusKey &&
|
|
177
|
+
key !== CONFIG.boardStatusKey &&
|
|
178
|
+
key !== CONFIG.browseStatusKey &&
|
|
179
|
+
!isAlert(key),
|
|
180
|
+
)
|
|
181
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
126
182
|
.map(([key, value]) => {
|
|
127
183
|
const status = sanitize(value);
|
|
128
184
|
// The plan chip trails the plan file name; the slot shows the mode only.
|
|
@@ -137,11 +193,11 @@ export function modesSegment(snapshot: StatusSnapshot, theme: Theme): Segment |
|
|
|
137
193
|
const icons = others.map((status) => compactStatus(status)).join(" ");
|
|
138
194
|
return {
|
|
139
195
|
id: "statuses",
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
weight:
|
|
196
|
+
// Weight 2, ahead of the branch: a mode is stated again by the session, while
|
|
197
|
+
// `⎇` is the only thing on the bar that says where the work will land.
|
|
198
|
+
weight: 2,
|
|
143
199
|
droppable: true,
|
|
144
|
-
separator: faint(theme, CONFIG.separators.
|
|
200
|
+
separator: faint(theme, CONFIG.separators.item),
|
|
145
201
|
forms: [full, icons],
|
|
146
202
|
};
|
|
147
203
|
}
|
|
@@ -170,15 +226,42 @@ export function windowSegment(snapshot: StatusSnapshot, theme: Theme): Segment |
|
|
|
170
226
|
};
|
|
171
227
|
}
|
|
172
228
|
|
|
229
|
+
/**
|
|
230
|
+
* What the session cost, and how many records the total leaves out.
|
|
231
|
+
*
|
|
232
|
+
* A session whose every record went unpriced has a total of zero, and a segment that
|
|
233
|
+
* vanished at zero was the loudest version of the silence: nothing on the bar said the
|
|
234
|
+
* work was unpriced rather than free. So the count is its own fact, and the price comes
|
|
235
|
+
* first where there is one — the narrower forms drop the price before the warning.
|
|
236
|
+
*
|
|
237
|
+
* A configured budget turns the price into a position: `$0.31/$5`, drawn in `warn` once
|
|
238
|
+
* the spend reaches the ceiling. That is the condition the budget notice fires on, read
|
|
239
|
+
* from the same `budgetState`, so the bar cannot call a session over budget that the
|
|
240
|
+
* notice has not, and the ceiling is visible from the first frame rather than only in
|
|
241
|
+
* the one notice that names it.
|
|
242
|
+
*/
|
|
173
243
|
export function costSegment(snapshot: StatusSnapshot, theme: Theme): Segment | null {
|
|
174
|
-
const { cost } = snapshot.usage;
|
|
175
|
-
|
|
244
|
+
const { cost, unpriced } = snapshot.usage;
|
|
245
|
+
const budget = snapshot.budget;
|
|
246
|
+
if (cost === 0 && unpriced === 0 && budget <= 0) return null;
|
|
247
|
+
const price = formatCost(cost, snapshot.currency);
|
|
248
|
+
const spend = budget > 0 ? `${price}/${formatLimit(budget, snapshot.currency)}` : price;
|
|
249
|
+
const spendText = paint(theme, budgetState(budget, cost).exceeded ? "warn" : "meta", spend);
|
|
250
|
+
const leftOut = unpriced === 1 ? "1 unpriced" : `${unpriced} unpriced`;
|
|
251
|
+
// A zero total with no ceiling says nothing a reader can use, so the price form
|
|
252
|
+
// appears only once there is a spend or a budget to hold it against.
|
|
253
|
+
const priced = cost === 0 && budget <= 0 ? [] : [spendText];
|
|
254
|
+
const forms = [
|
|
255
|
+
`${spendText}${faint(theme, CONFIG.separators.item)}${paint(theme, "warn", leftOut)}`,
|
|
256
|
+
paint(theme, "warn", leftOut),
|
|
257
|
+
...priced,
|
|
258
|
+
];
|
|
176
259
|
return {
|
|
177
260
|
id: "cost",
|
|
178
261
|
weight: 2,
|
|
179
262
|
droppable: true,
|
|
180
263
|
separator: faint(theme, CONFIG.separators.item),
|
|
181
|
-
forms:
|
|
264
|
+
forms: unpriced === 0 ? [spendText] : forms,
|
|
182
265
|
};
|
|
183
266
|
}
|
|
184
267
|
|
|
@@ -256,7 +339,10 @@ export function modelSegment(snapshot: StatusSnapshot, theme: Theme): Segment {
|
|
|
256
339
|
forms: [paint(theme, "meta", CONFIG.labels.noModel)],
|
|
257
340
|
};
|
|
258
341
|
}
|
|
259
|
-
|
|
342
|
+
// Only several providers make the name ambiguous, and an id that already carries
|
|
343
|
+
// its provider (`deepseek/deepseek-v4-pro`) would only say it twice.
|
|
344
|
+
const qualify = snapshot.providerCount > 1 && !model.id.toLowerCase().startsWith(model.provider.toLowerCase());
|
|
345
|
+
const provider = qualify ? `(${model.provider}) ` : "";
|
|
260
346
|
return {
|
|
261
347
|
id: "model",
|
|
262
348
|
weight: 1,
|
|
@@ -37,14 +37,30 @@ export interface UsageTotals {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
|
-
*
|
|
40
|
+
* One model this session could not price, as the key that would price it.
|
|
41
41
|
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
|
|
42
|
+
* The key is the `price.rates` vocabulary — `provider/model`, with `*` standing in
|
|
43
|
+
* for a side the record does not name — so it is copy-pasteable into the config.
|
|
44
|
+
*/
|
|
45
|
+
export interface UnpricedModel {
|
|
46
|
+
key: string;
|
|
47
|
+
/** How many records in this session carried it. */
|
|
48
|
+
records: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The totals plus the records the mode had no rate for.
|
|
53
|
+
*
|
|
54
|
+
* CNY has no fallback, so those records contribute no cost at all. USD falls back
|
|
55
|
+
* to Pi's recorded amount — except when that amount is `0`, which is what Pi
|
|
56
|
+
* records for a model its own catalog cannot price, and then nothing has priced
|
|
57
|
+
* the record either. Both cases want the same fix, so they are one list: the
|
|
58
|
+
* models, and the key that would price each. `unpriced` is summed from it, so the
|
|
59
|
+
* count and the names cannot disagree.
|
|
45
60
|
*/
|
|
46
61
|
export interface PricedUsage extends UsageTotals {
|
|
47
62
|
unpriced: number;
|
|
63
|
+
unpricedModels: UnpricedModel[];
|
|
48
64
|
}
|
|
49
65
|
|
|
50
66
|
/** Context-window usage for the active model. */
|
|
@@ -75,9 +91,17 @@ export interface StatusSnapshot {
|
|
|
75
91
|
model: ModelInfo | null;
|
|
76
92
|
thinkingLevel: ThinkingLevel | null;
|
|
77
93
|
context: ContextInfo;
|
|
78
|
-
|
|
94
|
+
/** The session's totals, with the records nothing could price riding along. */
|
|
95
|
+
usage: PricedUsage;
|
|
79
96
|
/** How `usage.cost` is rendered. */
|
|
80
97
|
currency: Currency;
|
|
98
|
+
/**
|
|
99
|
+
* The session's configured spend ceiling in `currency`; `0` means no ceiling.
|
|
100
|
+
*
|
|
101
|
+
* The bar pairs it with the cost (`$0.31/$5`) so the allowance is a fact on the
|
|
102
|
+
* screen, not something the one budget notice alone carries.
|
|
103
|
+
*/
|
|
104
|
+
budget: number;
|
|
81
105
|
/** Latest cache hit rate as a percentage, or `null` when unknown. */
|
|
82
106
|
cacheHitRate: number | null;
|
|
83
107
|
providerCount: number;
|
|
@@ -1,36 +1,61 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The one notice a
|
|
2
|
+
* The one notice a session posts when records had no rate.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* A record nothing priced contributes no cost at all — CNY has no fallback, and USD
|
|
5
|
+
* falls back to Pi's recorded amount, which is `0` for a model its catalog does not
|
|
6
|
+
* know — so the footer's total is smaller than the work done, with nothing on screen
|
|
7
|
+
* saying why. This is the one sentence that says it, once per session, and it names the
|
|
8
|
+
* models by the key that would price them rather than only counting records: a count
|
|
9
|
+
* tells a reader the total is short, and a key tells them what to do about it.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* Two fixes are honest and the notice does not pick for the reader: switch to `USD`
|
|
12
|
+
* (Pi's catalog is already per model, tiers included) or state yuan rates under
|
|
13
|
+
* `price.rates`. No figure is quoted, because there is none to quote.
|
|
13
14
|
*/
|
|
14
15
|
|
|
15
16
|
import { noticeMessage, type NoticeMessage } from "../../../lib/notice.ts";
|
|
17
|
+
import type { UnpricedModel } from "./types.ts";
|
|
18
|
+
|
|
19
|
+
/** How many keys the notice and its OS body name before they count the rest. */
|
|
20
|
+
export const NAMED_MODELS = 3;
|
|
21
|
+
|
|
22
|
+
/** The keys to print, most records first, and how many were left out. */
|
|
23
|
+
function named(models: readonly UnpricedModel[]): string {
|
|
24
|
+
const shown = models.slice(0, NAMED_MODELS).map((model) => model.key);
|
|
25
|
+
const rest = models.length - shown.length;
|
|
26
|
+
return rest > 0 ? `${shown.join(", ")} +${rest} more` : shown.join(", ");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* What a count of records reads as: `1 record`, `2 records`.
|
|
31
|
+
*
|
|
32
|
+
* Exported because the notice, its OS body, and `/bytefly prices` all say it, and three
|
|
33
|
+
* wordings for one fact is how a report starts disagreeing with the message beside it.
|
|
34
|
+
*/
|
|
35
|
+
export function recordsText(count: number): string {
|
|
36
|
+
return count === 1 ? "1 record" : `${count} records`;
|
|
37
|
+
}
|
|
16
38
|
|
|
17
39
|
/** The notice naming what the total leaves out, and how to price it. */
|
|
18
|
-
export function unpricedNotice(
|
|
40
|
+
export function unpricedNotice(models: readonly UnpricedModel[]): NoticeMessage {
|
|
41
|
+
const count = models.reduce((total, model) => total + model.records, 0);
|
|
19
42
|
const subject = count === 1 ? "1 record has" : `${count} records have`;
|
|
20
43
|
return noticeMessage({
|
|
21
44
|
content:
|
|
22
|
-
`${subject} no
|
|
23
|
-
"Switch `price.currency.code` to USD to use Pi's recorded cost, or add
|
|
45
|
+
`${subject} no rate, so ${count === 1 ? "it is" : "they are"} not in this total: ${named(models)}. ` +
|
|
46
|
+
"Switch `price.currency.code` to USD to use Pi's recorded cost, or add `price.rates` entries for those keys.",
|
|
24
47
|
details: {
|
|
25
48
|
source: "bytefly",
|
|
26
|
-
title: "Records with no
|
|
49
|
+
title: "Records with no rate",
|
|
27
50
|
severity: "info",
|
|
28
|
-
items:
|
|
51
|
+
items: models.slice(0, NAMED_MODELS).map((model) => ({ label: model.key, detail: recordsText(model.records) })),
|
|
29
52
|
},
|
|
30
53
|
});
|
|
31
54
|
}
|
|
32
55
|
|
|
33
|
-
/** The one-line body an OS notification carries. */
|
|
34
|
-
export function unpricedBody(
|
|
35
|
-
|
|
56
|
+
/** The one-line body an OS notification carries: the first key is the actionable part. */
|
|
57
|
+
export function unpricedBody(models: readonly UnpricedModel[]): string {
|
|
58
|
+
const count = models.reduce((total, model) => total + model.records, 0);
|
|
59
|
+
const first = models[0]?.key ?? "";
|
|
60
|
+
return `${recordsText(count)} with no rate · ${first} · use USD or add price.rates`;
|
|
36
61
|
}
|