@leo000001/opencode-quota-sidebar 1.0.2 → 1.2.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 +20 -4
- package/dist/cost.js +5 -2
- package/dist/descendants.d.ts +22 -0
- package/dist/descendants.js +78 -0
- package/dist/events.d.ts +8 -0
- package/dist/events.js +31 -0
- package/dist/format.js +127 -23
- package/dist/index.js +190 -631
- package/dist/persistence.d.ts +13 -0
- package/dist/persistence.js +63 -0
- package/dist/providers/core/openai.js +2 -1
- package/dist/providers/third_party/rightcode.js +12 -11
- package/dist/quota.js +18 -8
- package/dist/quota_service.d.ts +23 -0
- package/dist/quota_service.js +188 -0
- package/dist/storage.d.ts +2 -0
- package/dist/storage.js +62 -24
- package/dist/storage_chunks.js +74 -1
- package/dist/storage_parse.js +8 -0
- package/dist/storage_paths.d.ts +1 -0
- package/dist/storage_paths.js +12 -4
- package/dist/title.d.ts +5 -0
- package/dist/title.js +26 -2
- package/dist/title_apply.d.ts +33 -0
- package/dist/title_apply.js +189 -0
- package/dist/title_refresh.d.ts +9 -0
- package/dist/title_refresh.js +46 -0
- package/dist/tools.d.ts +56 -0
- package/dist/tools.js +63 -0
- package/dist/types.d.ts +12 -0
- package/dist/usage.js +148 -47
- package/dist/usage_service.d.ts +31 -0
- package/dist/usage_service.js +417 -0
- package/package.json +1 -1
- package/quota-sidebar.config.example.json +5 -1
package/README.md
CHANGED
|
@@ -17,6 +17,8 @@ Add the package name to `plugin` in your `opencode.json`. OpenCode uses Bun to i
|
|
|
17
17
|
}
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
+
Note for OpenCode `>=1.2.15`: TUI settings (`theme`/`keybinds`/`tui`) moved to `tui.json`, but plugin loading still stays in `opencode.json` (`plugin: []`).
|
|
21
|
+
|
|
20
22
|
## Development (build from source)
|
|
21
23
|
|
|
22
24
|
```bash
|
|
@@ -55,6 +57,7 @@ Want to add support for another provider (Google Antigravity, Zhipu AI, Firmware
|
|
|
55
57
|
- line 5: `$X.XX as API cost` (equivalent API billing for subscription-auth providers)
|
|
56
58
|
- quota lines: quota text like `OpenAI 5h 80% Rst 16:20`, with multi-window continuation lines indented (e.g. ` Weekly 70% Rst 03-01`)
|
|
57
59
|
- RightCode daily quota shows `$remaining/$dailyTotal` + expiry (e.g. `RC Daily $105/$60 Exp 02-27`, without trailing percent) and also shows balance on the next indented line when available
|
|
60
|
+
- Session-scoped usage/quota can include descendant subagent sessions (enabled by default via `sidebar.includeChildren=true`). Traversal is bounded by `childrenMaxDepth` (default 6), `childrenMaxSessions` (default 128), and `childrenConcurrency` (default 5); truncation is logged when `OPENCODE_QUOTA_DEBUG=1`. Day/week/month ranges never merge children — only session scope does.
|
|
58
61
|
- Toast message includes three sections: `Token Usage`, `Cost as API` (per provider), and `Quota`
|
|
59
62
|
- Quota snapshots are de-duplicated before rendering to avoid repeated provider lines
|
|
60
63
|
- Custom tools:
|
|
@@ -81,6 +84,7 @@ The plugin stores lightweight global state and date-partitioned session chunks.
|
|
|
81
84
|
- Session chunks: `<opencode-data>/quota-sidebar-sessions/YYYY/MM/DD.json`
|
|
82
85
|
- per-session title state (`baseTitle`, `lastAppliedTitle`)
|
|
83
86
|
- `createdAt`
|
|
87
|
+
- `parentID` (when the session is a subagent child session)
|
|
84
88
|
- cached usage summary used by `quota_summary`
|
|
85
89
|
- incremental aggregation cursor
|
|
86
90
|
|
|
@@ -103,6 +107,7 @@ memory on startup. Chunk files remain on disk for historical range scans.
|
|
|
103
107
|
|
|
104
108
|
- Node.js: >= 18 (for `fetch` + `AbortController`)
|
|
105
109
|
- OpenCode: plugin SDK `@opencode-ai/plugin` ^1.2.10
|
|
110
|
+
- OpenCode config split: if you are on `>=1.2.15`, keep this plugin in `opencode.json` and keep TUI-only keys in `tui.json`.
|
|
106
111
|
|
|
107
112
|
## Optional commands
|
|
108
113
|
|
|
@@ -141,7 +146,11 @@ Create `quota-sidebar.config.json` under your project root:
|
|
|
141
146
|
"enabled": true,
|
|
142
147
|
"width": 36,
|
|
143
148
|
"showCost": true,
|
|
144
|
-
"showQuota": true
|
|
149
|
+
"showQuota": true,
|
|
150
|
+
"includeChildren": true,
|
|
151
|
+
"childrenMaxDepth": 6,
|
|
152
|
+
"childrenMaxSessions": 128,
|
|
153
|
+
"childrenConcurrency": 5
|
|
145
154
|
},
|
|
146
155
|
"quota": {
|
|
147
156
|
"refreshMs": 300000,
|
|
@@ -166,11 +175,18 @@ Create `quota-sidebar.config.json` under your project root:
|
|
|
166
175
|
Notes:
|
|
167
176
|
|
|
168
177
|
- `sidebar.showCost` controls API-cost visibility in sidebar title, `quota_summary` markdown report, and toast message.
|
|
178
|
+
- `sidebar.width` is measured in terminal cells. CJK/emoji truncation is best-effort to avoid sidebar overflow.
|
|
179
|
+
- `sidebar.includeChildren` controls whether session-scoped usage/quota includes descendant subagent sessions (default: `true`).
|
|
180
|
+
- `sidebar.childrenMaxDepth` limits how many levels of nested subagents are traversed (default: `6`, clamped 1–32).
|
|
181
|
+
- `sidebar.childrenMaxSessions` caps the total number of descendant sessions aggregated (default: `128`, clamped 0–2000).
|
|
182
|
+
- `sidebar.childrenConcurrency` controls parallel fetches for descendant session messages (default: `5`, clamped 1–10).
|
|
169
183
|
- `output` now includes reasoning tokens. Reasoning is no longer rendered as a separate line.
|
|
170
184
|
- API cost excludes reasoning tokens from output billing (uses `tokens.output` only for output-price multiplication).
|
|
171
185
|
- `quota.providers` is the extensible per-adapter switch map.
|
|
172
186
|
- If API Cost is `$0.00`, it usually means the model/provider has no pricing mapping in OpenCode at the moment, so equivalent API cost cannot be estimated.
|
|
173
187
|
|
|
188
|
+
`quota_summary` also supports an optional `includeChildren` flag (only effective for `period=session`) to override the config per call. For `day`/`week`/`month` periods, children are never merged — each session is counted independently.
|
|
189
|
+
|
|
174
190
|
## Debug logging
|
|
175
191
|
|
|
176
192
|
Set `OPENCODE_QUOTA_DEBUG=1` to enable debug logging to stderr. This logs:
|
|
@@ -196,9 +212,9 @@ Set `OPENCODE_QUOTA_DEBUG=1` to enable debug logging to stderr. This logs:
|
|
|
196
212
|
- OpenAI OAuth token refresh is disabled by default; set
|
|
197
213
|
`quota.refreshAccessToken=true` if you want the plugin to refresh access
|
|
198
214
|
tokens when expired.
|
|
199
|
-
- State file writes refuse to
|
|
200
|
-
- The `OPENCODE_QUOTA_DATA_HOME` env var
|
|
201
|
-
testing; do not set this in production.
|
|
215
|
+
- State/chunk file writes refuse to write through symlinked targets (best-effort defense-in-depth).
|
|
216
|
+
- The `OPENCODE_QUOTA_DATA_HOME` env var overrides the OpenCode data directory
|
|
217
|
+
path (for testing); do not set this in production.
|
|
202
218
|
|
|
203
219
|
## Contributing
|
|
204
220
|
|
package/dist/cost.js
CHANGED
|
@@ -65,9 +65,12 @@ export function guessModelCostDivisor(rates) {
|
|
|
65
65
|
: MODEL_COST_DIVISOR_PER_TOKEN;
|
|
66
66
|
}
|
|
67
67
|
export function calcEquivalentApiCostForMessage(message, rates) {
|
|
68
|
+
// For providers that expose reasoning tokens separately, they are still
|
|
69
|
+
// billed as output/completion tokens (same unit price). Our UI also merges
|
|
70
|
+
// reasoning into the single Output statistic, so API cost should match that.
|
|
71
|
+
const billedOutput = message.tokens.output + message.tokens.reasoning;
|
|
68
72
|
const rawCost = message.tokens.input * rates.input +
|
|
69
|
-
|
|
70
|
-
message.tokens.output * rates.output +
|
|
73
|
+
billedOutput * rates.output +
|
|
71
74
|
message.tokens.cache.read * rates.cacheRead +
|
|
72
75
|
message.tokens.cache.write * rates.cacheWrite;
|
|
73
76
|
const divisor = guessModelCostDivisor(rates);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Session } from '@opencode-ai/sdk';
|
|
2
|
+
export type DescendantsOptions = {
|
|
3
|
+
maxDepth: number;
|
|
4
|
+
maxSessions: number;
|
|
5
|
+
concurrency: number;
|
|
6
|
+
};
|
|
7
|
+
export type DescendantsDeps = {
|
|
8
|
+
listChildren: (sessionID: string) => Promise<Session[]>;
|
|
9
|
+
getParentID: (sessionID: string) => string | undefined;
|
|
10
|
+
onDiscover: (session: {
|
|
11
|
+
id: string;
|
|
12
|
+
title: string;
|
|
13
|
+
createdAt: number;
|
|
14
|
+
parentID: string | undefined;
|
|
15
|
+
}) => void;
|
|
16
|
+
debug?: (message: string) => void;
|
|
17
|
+
now?: () => number;
|
|
18
|
+
};
|
|
19
|
+
export declare function createDescendantsResolver(deps: DescendantsDeps): {
|
|
20
|
+
invalidateForAncestors: (sessionID: string | undefined) => void;
|
|
21
|
+
listDescendantSessionIDs: (sessionID: string, opts: DescendantsOptions) => Promise<string[]>;
|
|
22
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { mapConcurrent } from './helpers.js';
|
|
2
|
+
export function createDescendantsResolver(deps) {
|
|
3
|
+
const cache = new Map();
|
|
4
|
+
const ttlMs = 5_000;
|
|
5
|
+
const now = deps.now || Date.now;
|
|
6
|
+
const debug = deps.debug || (() => { });
|
|
7
|
+
const invalidateForAncestors = (sessionID) => {
|
|
8
|
+
if (!sessionID)
|
|
9
|
+
return;
|
|
10
|
+
const visited = new Set();
|
|
11
|
+
let current = sessionID;
|
|
12
|
+
for (let i = 0; i < 512 && current; i++) {
|
|
13
|
+
if (visited.has(current))
|
|
14
|
+
return;
|
|
15
|
+
visited.add(current);
|
|
16
|
+
cache.delete(current);
|
|
17
|
+
current = deps.getParentID(current);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
const listDescendantSessionIDs = async (sessionID, opts) => {
|
|
21
|
+
if (opts.maxSessions <= 0) {
|
|
22
|
+
cache.set(sessionID, { sessionIDs: [], expiresAt: now() + ttlMs });
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
const cached = cache.get(sessionID);
|
|
26
|
+
if (cached && cached.expiresAt > now()) {
|
|
27
|
+
return cached.sessionIDs;
|
|
28
|
+
}
|
|
29
|
+
const visited = new Set([sessionID]);
|
|
30
|
+
const descendants = [];
|
|
31
|
+
let frontier = [sessionID];
|
|
32
|
+
let depth = 0;
|
|
33
|
+
while (frontier.length > 0 &&
|
|
34
|
+
depth < opts.maxDepth &&
|
|
35
|
+
descendants.length < opts.maxSessions) {
|
|
36
|
+
const levels = await mapConcurrent(frontier, opts.concurrency, async (id) => {
|
|
37
|
+
const children = await deps.listChildren(id).catch((error) => {
|
|
38
|
+
debug(`listChildren failed for ${id}: ${String(error)}`);
|
|
39
|
+
return [];
|
|
40
|
+
});
|
|
41
|
+
return children;
|
|
42
|
+
});
|
|
43
|
+
const nextFrontier = [];
|
|
44
|
+
for (const children of levels) {
|
|
45
|
+
for (const child of children) {
|
|
46
|
+
if (visited.has(child.id))
|
|
47
|
+
continue;
|
|
48
|
+
visited.add(child.id);
|
|
49
|
+
descendants.push(child.id);
|
|
50
|
+
deps.onDiscover({
|
|
51
|
+
id: child.id,
|
|
52
|
+
title: child.title,
|
|
53
|
+
createdAt: child.time.created,
|
|
54
|
+
parentID: child.parentID,
|
|
55
|
+
});
|
|
56
|
+
nextFrontier.push(child.id);
|
|
57
|
+
if (descendants.length >= opts.maxSessions)
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
if (descendants.length >= opts.maxSessions)
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
frontier = nextFrontier;
|
|
64
|
+
depth += 1;
|
|
65
|
+
}
|
|
66
|
+
cache.set(sessionID, { sessionIDs: descendants, expiresAt: now() + ttlMs });
|
|
67
|
+
const truncatedByDepth = depth >= opts.maxDepth && frontier.length > 0;
|
|
68
|
+
const truncatedByCount = descendants.length >= opts.maxSessions && frontier.length > 0;
|
|
69
|
+
if (truncatedByDepth || truncatedByCount) {
|
|
70
|
+
debug(`descendants truncated for ${sessionID}: depth=${depth}/${opts.maxDepth}, sessions=${descendants.length}/${opts.maxSessions}`);
|
|
71
|
+
}
|
|
72
|
+
return descendants;
|
|
73
|
+
};
|
|
74
|
+
return {
|
|
75
|
+
invalidateForAncestors,
|
|
76
|
+
listDescendantSessionIDs,
|
|
77
|
+
};
|
|
78
|
+
}
|
package/dist/events.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { AssistantMessage, Event, Session } from '@opencode-ai/sdk';
|
|
2
|
+
export declare function createEventDispatcher(handlers: {
|
|
3
|
+
onSessionCreated: (session: Session) => Promise<void>;
|
|
4
|
+
onSessionUpdated: (session: Session) => Promise<void>;
|
|
5
|
+
onSessionDeleted: (session: Session) => Promise<void>;
|
|
6
|
+
onMessageRemoved: (sessionID: string) => Promise<void>;
|
|
7
|
+
onAssistantMessageCompleted: (message: AssistantMessage) => Promise<void>;
|
|
8
|
+
}): (event: Event) => Promise<void>;
|
package/dist/events.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
function isAssistantMessage(message) {
|
|
2
|
+
return message.role === 'assistant';
|
|
3
|
+
}
|
|
4
|
+
export function createEventDispatcher(handlers) {
|
|
5
|
+
return async (event) => {
|
|
6
|
+
if (event.type === 'session.created') {
|
|
7
|
+
await handlers.onSessionCreated(event.properties.info);
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
if (event.type === 'session.updated') {
|
|
11
|
+
await handlers.onSessionUpdated(event.properties.info);
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
if (event.type === 'session.deleted') {
|
|
15
|
+
await handlers.onSessionDeleted(event.properties.info);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (event.type === 'message.removed') {
|
|
19
|
+
await handlers.onMessageRemoved(event.properties.sessionID);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (event.type !== 'message.updated')
|
|
23
|
+
return;
|
|
24
|
+
if (!isAssistantMessage(event.properties.info))
|
|
25
|
+
return;
|
|
26
|
+
const completed = event.properties.info.time.completed;
|
|
27
|
+
if (typeof completed !== 'number' || !Number.isFinite(completed))
|
|
28
|
+
return;
|
|
29
|
+
await handlers.onAssistantMessageCompleted(event.properties.info);
|
|
30
|
+
};
|
|
31
|
+
}
|
package/dist/format.js
CHANGED
|
@@ -19,17 +19,113 @@ function shortNumber(value, decimals = 1) {
|
|
|
19
19
|
function sidebarNumber(value) {
|
|
20
20
|
return shortNumber(value, 1);
|
|
21
21
|
}
|
|
22
|
+
function sanitizeLine(value) {
|
|
23
|
+
// Sidebars/titles must be plain text: no ANSI and no embedded newlines.
|
|
24
|
+
return (stripAnsi(value)
|
|
25
|
+
.replace(/\r?\n/g, ' ')
|
|
26
|
+
// Remove control characters that can corrupt TUI rendering.
|
|
27
|
+
.replace(/[\x00-\x1F\x7F-\x9F]/g, ' '));
|
|
28
|
+
}
|
|
29
|
+
function isCombiningCodePoint(codePoint) {
|
|
30
|
+
return ((codePoint >= 0x0300 && codePoint <= 0x036f) ||
|
|
31
|
+
(codePoint >= 0x1ab0 && codePoint <= 0x1aff) ||
|
|
32
|
+
(codePoint >= 0x1dc0 && codePoint <= 0x1dff) ||
|
|
33
|
+
(codePoint >= 0x20d0 && codePoint <= 0x20ff) ||
|
|
34
|
+
(codePoint >= 0xfe20 && codePoint <= 0xfe2f));
|
|
35
|
+
}
|
|
36
|
+
function isVariationSelector(codePoint) {
|
|
37
|
+
return ((codePoint >= 0xfe00 && codePoint <= 0xfe0f) ||
|
|
38
|
+
(codePoint >= 0xe0100 && codePoint <= 0xe01ef));
|
|
39
|
+
}
|
|
40
|
+
function isWideCodePoint(codePoint) {
|
|
41
|
+
// Based on commonly used fullwidth ranges (similar to string-width).
|
|
42
|
+
// This intentionally errs toward width=2 to avoid sidebar overflow.
|
|
43
|
+
if (codePoint >= 0x1100) {
|
|
44
|
+
if (codePoint <= 0x115f ||
|
|
45
|
+
codePoint === 0x2329 ||
|
|
46
|
+
codePoint === 0x232a ||
|
|
47
|
+
(codePoint >= 0x2e80 && codePoint <= 0xa4cf && codePoint !== 0x303f) ||
|
|
48
|
+
(codePoint >= 0xac00 && codePoint <= 0xd7a3) ||
|
|
49
|
+
(codePoint >= 0xf900 && codePoint <= 0xfaff) ||
|
|
50
|
+
(codePoint >= 0xfe10 && codePoint <= 0xfe19) ||
|
|
51
|
+
(codePoint >= 0xfe30 && codePoint <= 0xfe6f) ||
|
|
52
|
+
(codePoint >= 0xff00 && codePoint <= 0xff60) ||
|
|
53
|
+
(codePoint >= 0xffe0 && codePoint <= 0xffe6) ||
|
|
54
|
+
(codePoint >= 0x20000 && codePoint <= 0x3fffd)) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// Emoji/symbol ranges (best-effort).
|
|
59
|
+
if ((codePoint >= 0x1f300 && codePoint <= 0x1f5ff) ||
|
|
60
|
+
(codePoint >= 0x1f600 && codePoint <= 0x1f64f) ||
|
|
61
|
+
(codePoint >= 0x1f680 && codePoint <= 0x1f6ff) ||
|
|
62
|
+
(codePoint >= 0x1f900 && codePoint <= 0x1f9ff) ||
|
|
63
|
+
(codePoint >= 0x1fa70 && codePoint <= 0x1faff) ||
|
|
64
|
+
(codePoint >= 0x2600 && codePoint <= 0x26ff) ||
|
|
65
|
+
(codePoint >= 0x2700 && codePoint <= 0x27bf)) {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
function cellWidthOfCodePoint(codePoint) {
|
|
71
|
+
if (codePoint === 0)
|
|
72
|
+
return 0;
|
|
73
|
+
// ZWJ sequences should not add width (best-effort).
|
|
74
|
+
if (codePoint === 0x200d)
|
|
75
|
+
return 0;
|
|
76
|
+
if (codePoint < 32 || (codePoint >= 0x7f && codePoint < 0xa0))
|
|
77
|
+
return 0;
|
|
78
|
+
if (isCombiningCodePoint(codePoint))
|
|
79
|
+
return 0;
|
|
80
|
+
if (isVariationSelector(codePoint))
|
|
81
|
+
return 0;
|
|
82
|
+
return isWideCodePoint(codePoint) ? 2 : 1;
|
|
83
|
+
}
|
|
84
|
+
function stringCellWidth(value) {
|
|
85
|
+
let width = 0;
|
|
86
|
+
for (const char of value) {
|
|
87
|
+
width += cellWidthOfCodePoint(char.codePointAt(0) || 0);
|
|
88
|
+
}
|
|
89
|
+
return width;
|
|
90
|
+
}
|
|
91
|
+
function padEndCells(value, targetWidth) {
|
|
92
|
+
const current = stringCellWidth(value);
|
|
93
|
+
if (current >= targetWidth)
|
|
94
|
+
return value;
|
|
95
|
+
return `${value}${' '.repeat(targetWidth - current)}`;
|
|
96
|
+
}
|
|
97
|
+
function truncateToCellWidth(value, width) {
|
|
98
|
+
if (width <= 0)
|
|
99
|
+
return '';
|
|
100
|
+
let used = 0;
|
|
101
|
+
let out = '';
|
|
102
|
+
for (const char of value) {
|
|
103
|
+
const w = cellWidthOfCodePoint(char.codePointAt(0) || 0);
|
|
104
|
+
if (used + w > width)
|
|
105
|
+
break;
|
|
106
|
+
used += w;
|
|
107
|
+
out += char;
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
22
111
|
/**
|
|
23
|
-
* Truncate `value` to at most `width`
|
|
112
|
+
* Truncate `value` to at most `width` terminal cells.
|
|
24
113
|
* Keep plain text only (no ANSI) to avoid renderer corruption.
|
|
25
114
|
*/
|
|
26
115
|
function fitLine(value, width) {
|
|
27
116
|
if (width <= 0)
|
|
28
117
|
return '';
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
118
|
+
const safe = sanitizeLine(value);
|
|
119
|
+
if (stringCellWidth(safe) <= width)
|
|
120
|
+
return safe;
|
|
121
|
+
if (width <= 1)
|
|
122
|
+
return truncateToCellWidth(safe, width);
|
|
123
|
+
const head = truncateToCellWidth(safe, width - 1);
|
|
124
|
+
// If we couldn't fit any characters with a suffix reserved, fall back to a
|
|
125
|
+
// best-effort truncation without the suffix.
|
|
126
|
+
if (!head)
|
|
127
|
+
return truncateToCellWidth(safe, width);
|
|
128
|
+
return `${head}~`;
|
|
33
129
|
}
|
|
34
130
|
function formatApiCostValue(value) {
|
|
35
131
|
const safe = Number.isFinite(value) && value > 0 ? value : 0;
|
|
@@ -41,12 +137,16 @@ function formatApiCostLine(value) {
|
|
|
41
137
|
function alignPairs(pairs, indent = ' ') {
|
|
42
138
|
if (pairs.length === 0)
|
|
43
139
|
return [];
|
|
44
|
-
const
|
|
45
|
-
|
|
140
|
+
const safePairs = pairs.map((pair) => ({
|
|
141
|
+
label: sanitizeLine(pair.label || ''),
|
|
142
|
+
value: sanitizeLine(pair.value || ''),
|
|
143
|
+
}));
|
|
144
|
+
const labelWidth = Math.max(...safePairs.map((pair) => stringCellWidth(pair.label)), 0);
|
|
145
|
+
return safePairs.map((pair) => {
|
|
46
146
|
if (!pair.label) {
|
|
47
147
|
return `${indent}${' '.repeat(labelWidth)} ${pair.value}`;
|
|
48
148
|
}
|
|
49
|
-
return `${indent}${pair.label
|
|
149
|
+
return `${indent}${padEndCells(pair.label, labelWidth)} ${pair.value}`;
|
|
50
150
|
});
|
|
51
151
|
}
|
|
52
152
|
/**
|
|
@@ -83,8 +183,8 @@ export function renderSidebarTitle(baseTitle, usage, quotas, config) {
|
|
|
83
183
|
if (config.sidebar.showQuota) {
|
|
84
184
|
const visibleQuotas = collapseQuotaSnapshots(quotas).filter((q) => ['ok', 'error', 'unsupported', 'unavailable'].includes(q.status));
|
|
85
185
|
const labelWidth = visibleQuotas.reduce((max, item) => {
|
|
86
|
-
const label = quotaDisplayLabel(item);
|
|
87
|
-
return Math.max(max, label
|
|
186
|
+
const label = sanitizeLine(quotaDisplayLabel(item));
|
|
187
|
+
return Math.max(max, stringCellWidth(label));
|
|
88
188
|
}, 0);
|
|
89
189
|
const quotaItems = visibleQuotas
|
|
90
190
|
.flatMap((item) => compactQuotaWide(item, labelWidth))
|
|
@@ -105,9 +205,9 @@ export function renderSidebarTitle(baseTitle, usage, quotas, config) {
|
|
|
105
205
|
* Copilot: "Copilot Monthly 70% Rst 03-01"
|
|
106
206
|
*/
|
|
107
207
|
function compactQuotaWide(quota, labelWidth = 0) {
|
|
108
|
-
const label = quotaDisplayLabel(quota);
|
|
109
|
-
const
|
|
110
|
-
const withLabel = (content) => `${
|
|
208
|
+
const label = sanitizeLine(quotaDisplayLabel(quota));
|
|
209
|
+
const labelPadded = padEndCells(label, labelWidth);
|
|
210
|
+
const withLabel = (content) => `${labelPadded} ${content}`;
|
|
111
211
|
if (quota.status === 'error')
|
|
112
212
|
return [withLabel('Remaining ?')];
|
|
113
213
|
if (quota.status === 'unsupported')
|
|
@@ -126,12 +226,12 @@ function compactQuotaWide(quota, labelWidth = 0) {
|
|
|
126
226
|
: `${Math.round(win.remainingPercent)}%`;
|
|
127
227
|
const parts = win.label
|
|
128
228
|
? showPercent
|
|
129
|
-
? [win.label, pct]
|
|
130
|
-
: [win.label]
|
|
229
|
+
? [sanitizeLine(win.label), pct]
|
|
230
|
+
: [sanitizeLine(win.label)]
|
|
131
231
|
: [pct];
|
|
132
232
|
const reset = compactReset(win.resetAt);
|
|
133
233
|
if (reset) {
|
|
134
|
-
parts.push(`${win.resetLabel || 'Rst'} ${reset}`);
|
|
234
|
+
parts.push(`${sanitizeLine(win.resetLabel || 'Rst')} ${reset}`);
|
|
135
235
|
}
|
|
136
236
|
return parts.join(' ');
|
|
137
237
|
};
|
|
@@ -203,6 +303,7 @@ function periodLabel(period) {
|
|
|
203
303
|
}
|
|
204
304
|
export function renderMarkdownReport(period, usage, quotas, options) {
|
|
205
305
|
const showCost = options?.showCost !== false;
|
|
306
|
+
const mdCell = (value) => sanitizeLine(value).replace(/\|/g, '\\|');
|
|
206
307
|
const rightCodeSubscriptionProviderIDs = new Set(collapseQuotaSnapshots(quotas)
|
|
207
308
|
.filter((quota) => quota.adapterID === 'rightcode')
|
|
208
309
|
.filter((quota) => quota.status === 'ok')
|
|
@@ -252,34 +353,37 @@ export function renderMarkdownReport(period, usage, quotas, options) {
|
|
|
252
353
|
};
|
|
253
354
|
const providerRows = Object.values(usage.providers)
|
|
254
355
|
.sort((a, b) => b.total - a.total)
|
|
255
|
-
.map((provider) =>
|
|
256
|
-
|
|
257
|
-
|
|
356
|
+
.map((provider) => {
|
|
357
|
+
const providerID = mdCell(provider.providerID);
|
|
358
|
+
return showCost
|
|
359
|
+
? `| ${providerID} | ${shortNumber(provider.input)} | ${shortNumber(provider.output)} | ${shortNumber(provider.cacheRead + provider.cacheWrite)} | ${shortNumber(provider.total)} | ${measuredCostCell(provider.providerID, provider.cost)} | ${apiCostCell(provider.providerID, provider.apiCost)} |`
|
|
360
|
+
: `| ${providerID} | ${shortNumber(provider.input)} | ${shortNumber(provider.output)} | ${shortNumber(provider.cacheRead + provider.cacheWrite)} | ${shortNumber(provider.total)} |`;
|
|
361
|
+
});
|
|
258
362
|
const quotaLines = collapseQuotaSnapshots(quotas).flatMap((quota) => {
|
|
259
363
|
// Multi-window detail
|
|
260
364
|
if (quota.windows && quota.windows.length > 0 && quota.status === 'ok') {
|
|
261
365
|
return quota.windows.map((win) => {
|
|
262
366
|
if (win.showPercent === false) {
|
|
263
367
|
const winLabel = win.label ? ` (${win.label})` : '';
|
|
264
|
-
return `- ${quota.label}${winLabel}: ${quota.status} | reset ${dateLine(win.resetAt)}
|
|
368
|
+
return mdCell(`- ${quota.label}${winLabel}: ${quota.status} | reset ${dateLine(win.resetAt)}`);
|
|
265
369
|
}
|
|
266
370
|
const remaining = win.remainingPercent === undefined
|
|
267
371
|
? '-'
|
|
268
372
|
: `${win.remainingPercent.toFixed(1)}%`;
|
|
269
373
|
const winLabel = win.label ? ` (${win.label})` : '';
|
|
270
|
-
return `- ${quota.label}${winLabel}: ${quota.status} | remaining ${remaining} | reset ${dateLine(win.resetAt)}
|
|
374
|
+
return mdCell(`- ${quota.label}${winLabel}: ${quota.status} | remaining ${remaining} | reset ${dateLine(win.resetAt)}`);
|
|
271
375
|
});
|
|
272
376
|
}
|
|
273
377
|
if (quota.status === 'ok' && quota.balance) {
|
|
274
378
|
return [
|
|
275
|
-
`- ${quota.label}: ${quota.status} | balance ${quota.balance.currency}${quota.balance.amount.toFixed(2)}
|
|
379
|
+
mdCell(`- ${quota.label}: ${quota.status} | balance ${quota.balance.currency}${quota.balance.amount.toFixed(2)}`),
|
|
276
380
|
];
|
|
277
381
|
}
|
|
278
382
|
const remaining = quota.remainingPercent === undefined
|
|
279
383
|
? '-'
|
|
280
384
|
: `${quota.remainingPercent.toFixed(1)}%`;
|
|
281
385
|
return [
|
|
282
|
-
`- ${quota.label}: ${quota.status} | remaining ${remaining} | reset ${dateLine(quota.resetAt)}${quota.note ? ` | ${quota.note}` : ''}
|
|
386
|
+
mdCell(`- ${quota.label}: ${quota.status} | remaining ${remaining} | reset ${dateLine(quota.resetAt)}${quota.note ? ` | ${quota.note}` : ''}`),
|
|
283
387
|
];
|
|
284
388
|
});
|
|
285
389
|
return [
|