@leo000001/opencode-quota-sidebar 3.0.0 → 3.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -2
- package/CONTRIBUTING.md +4 -1
- package/README.md +210 -514
- package/README.zh-CN.md +337 -0
- package/SECURITY.md +2 -2
- package/assets/OpenCode-Quota-Sidebar.png +0 -0
- package/dist/cost.d.ts +3 -3
- package/dist/cost.js +258 -169
- package/dist/format.d.ts +4 -0
- package/dist/format.js +24 -10
- package/dist/providers/common.d.ts +6 -0
- package/dist/providers/common.js +32 -12
- package/dist/providers/core/anthropic.d.ts +1 -1
- package/dist/providers/core/anthropic.js +43 -39
- package/dist/providers/core/kimi_for_coding.d.ts +1 -1
- package/dist/providers/core/kimi_for_coding.js +44 -64
- package/dist/providers/core/minimax_cn_coding_plan.d.ts +2 -0
- package/dist/providers/core/minimax_cn_coding_plan.js +214 -0
- package/dist/providers/core/zhipu_coding_plan.d.ts +1 -1
- package/dist/providers/core/zhipu_coding_plan.js +41 -61
- package/dist/providers/index.d.ts +3 -3
- package/dist/providers/index.js +5 -5
- package/dist/providers/third_party/rightcode.d.ts +1 -1
- package/dist/providers/third_party/rightcode.js +41 -61
- package/dist/providers/third_party/xyai.d.ts +2 -0
- package/dist/providers/third_party/{xyai_vibe.js → xyai.js} +113 -79
- package/dist/quota.d.ts +2 -2
- package/dist/quota.js +24 -18
- package/dist/quota_render.d.ts +1 -1
- package/dist/quota_render.js +23 -17
- package/dist/storage_parse.js +1 -0
- package/dist/title.js +7 -7
- package/dist/title_apply.js +18 -1
- package/dist/tui.tsx +133 -36
- package/dist/tui_helpers.d.ts +16 -0
- package/dist/tui_helpers.js +146 -0
- package/dist/types.d.ts +2 -0
- package/package.json +9 -2
- package/quota-sidebar.config.example.json +45 -45
- package/dist/providers/third_party/buzz.d.ts +0 -2
- package/dist/providers/third_party/buzz.js +0 -156
- package/dist/providers/third_party/xyai_vibe.d.ts +0 -2
package/dist/quota.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import fs from
|
|
2
|
-
import { isRecord, swallow } from
|
|
3
|
-
import { createDefaultProviderRegistry } from
|
|
4
|
-
import { sanitizeBaseURL } from
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import { isRecord, swallow } from "./helpers.js";
|
|
3
|
+
import { createDefaultProviderRegistry } from "./providers/index.js";
|
|
4
|
+
import { sanitizeBaseURL } from "./providers/common.js";
|
|
5
5
|
function resolveContext(providerID, providerOptions) {
|
|
6
6
|
return { providerID, providerOptions };
|
|
7
7
|
}
|
|
8
8
|
function authCandidates(providerID, normalizedProviderID, adapterID) {
|
|
9
9
|
const candidates = new Set([providerID]);
|
|
10
|
-
if (adapterID ===
|
|
11
|
-
candidates.add(
|
|
10
|
+
if (adapterID === "github-copilot") {
|
|
11
|
+
candidates.add("github-copilot-enterprise");
|
|
12
12
|
}
|
|
13
13
|
candidates.add(normalizedProviderID);
|
|
14
14
|
candidates.add(adapterID);
|
|
@@ -34,11 +34,12 @@ export function quotaSort(left, right) {
|
|
|
34
34
|
export function listDefaultQuotaProviderIDs() {
|
|
35
35
|
// Keep default report behavior stable for built-in subscription providers.
|
|
36
36
|
return [
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
37
|
+
"openai",
|
|
38
|
+
"kimi-for-coding",
|
|
39
|
+
"zhipuai-coding-plan",
|
|
40
|
+
"minimax-cn-coding-plan",
|
|
41
|
+
"github-copilot",
|
|
42
|
+
"anthropic",
|
|
42
43
|
];
|
|
43
44
|
}
|
|
44
45
|
export function createQuotaRuntime() {
|
|
@@ -58,10 +59,15 @@ export function createQuotaRuntime() {
|
|
|
58
59
|
if (adapter?.id && normalizedProviderID !== providerID) {
|
|
59
60
|
keyBase = `${adapter.id}:${providerID}`;
|
|
60
61
|
}
|
|
61
|
-
//
|
|
62
|
-
// RC-
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
// Some third-party adapters intentionally preserve provider-specific labels
|
|
63
|
+
// (for example RC-openai or an XYAI alias) even when they share one adapter.
|
|
64
|
+
// Keep the original provider identity in cache keys so same-host aliases with
|
|
65
|
+
// different auth/config entries do not overwrite each other.
|
|
66
|
+
if ((adapter?.id === "rightcode" &&
|
|
67
|
+
normalizedProviderID.startsWith("rightcode-")) ||
|
|
68
|
+
(adapter?.id === "xyai" &&
|
|
69
|
+
providerID !== "xyai" &&
|
|
70
|
+
providerID !== "xyai-vibe")) {
|
|
65
71
|
keyBase = normalizedProviderID;
|
|
66
72
|
}
|
|
67
73
|
return baseURL ? `${keyBase}@${baseURL}` : keyBase;
|
|
@@ -110,16 +116,16 @@ export function quotaCacheKey(providerID, providerOptions) {
|
|
|
110
116
|
}
|
|
111
117
|
export async function loadAuthMap(authPath) {
|
|
112
118
|
const parsed = await fs
|
|
113
|
-
.readFile(authPath,
|
|
119
|
+
.readFile(authPath, "utf8")
|
|
114
120
|
.then((value) => JSON.parse(value))
|
|
115
|
-
.catch(swallow(
|
|
121
|
+
.catch(swallow("loadAuthMap"));
|
|
116
122
|
if (!isRecord(parsed))
|
|
117
123
|
return {};
|
|
118
124
|
return Object.entries(parsed).reduce((acc, [key, value]) => {
|
|
119
125
|
if (!isRecord(value))
|
|
120
126
|
return acc;
|
|
121
127
|
const type = value.type;
|
|
122
|
-
if (type !==
|
|
128
|
+
if (type !== "oauth" && type !== "api" && type !== "wellknown")
|
|
123
129
|
return acc;
|
|
124
130
|
acc[key] = value;
|
|
125
131
|
return acc;
|
package/dist/quota_render.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { QuotaSnapshot } from
|
|
1
|
+
import type { QuotaSnapshot } from "./types.js";
|
|
2
2
|
export declare function canonicalProviderID(providerID: string): string;
|
|
3
3
|
export declare function displayShortLabel(providerID: string): string;
|
|
4
4
|
export declare function quotaDisplayLabel(quota: QuotaSnapshot): string;
|
package/dist/quota_render.js
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
const PROVIDER_SHORT_LABELS = {
|
|
2
|
-
openai:
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
openai: "OpenAI",
|
|
3
|
+
"kimi-for-coding": "Kimi",
|
|
4
|
+
"zhipuai-coding-plan": "Zhipu",
|
|
5
|
+
"minimax-cn-coding-plan": "MiniMax",
|
|
6
|
+
"github-copilot": "Copilot",
|
|
7
|
+
anthropic: "Anthropic",
|
|
8
|
+
rightcode: "RC",
|
|
9
|
+
xyai: "XYAI",
|
|
8
10
|
};
|
|
9
11
|
export function canonicalProviderID(providerID) {
|
|
10
|
-
if (providerID.startsWith(
|
|
11
|
-
return
|
|
12
|
-
if (providerID ===
|
|
13
|
-
return
|
|
12
|
+
if (providerID.startsWith("github-copilot"))
|
|
13
|
+
return "github-copilot";
|
|
14
|
+
if (providerID === "zhipuai-coding-plan")
|
|
15
|
+
return "zhipuai-coding-plan";
|
|
16
|
+
if (providerID === "minimax-cn-coding-plan")
|
|
17
|
+
return "minimax-cn-coding-plan";
|
|
18
|
+
if (providerID === "xyai-vibe")
|
|
19
|
+
return "xyai";
|
|
14
20
|
return providerID;
|
|
15
21
|
}
|
|
16
22
|
export function displayShortLabel(providerID) {
|
|
@@ -18,8 +24,8 @@ export function displayShortLabel(providerID) {
|
|
|
18
24
|
const direct = PROVIDER_SHORT_LABELS[canonical];
|
|
19
25
|
if (direct)
|
|
20
26
|
return direct;
|
|
21
|
-
if (canonical.startsWith(
|
|
22
|
-
return `RC-${canonical.slice(
|
|
27
|
+
if (canonical.startsWith("rightcode-")) {
|
|
28
|
+
return `RC-${canonical.slice("rightcode-".length)}`;
|
|
23
29
|
}
|
|
24
30
|
return providerID;
|
|
25
31
|
}
|
|
@@ -34,13 +40,13 @@ export function quotaDisplayLabel(quota) {
|
|
|
34
40
|
return displayShortLabel(quota.providerID);
|
|
35
41
|
}
|
|
36
42
|
function quotaKey(quota) {
|
|
37
|
-
if (quota.adapterID ===
|
|
43
|
+
if (quota.adapterID === "rightcode")
|
|
38
44
|
return `rightcode:${quota.providerID}`;
|
|
39
45
|
return `${quota.adapterID || quota.providerID}:${quota.providerID}`;
|
|
40
46
|
}
|
|
41
47
|
function quotaScore(quota) {
|
|
42
48
|
let score = 0;
|
|
43
|
-
if (quota.status ===
|
|
49
|
+
if (quota.status === "ok")
|
|
44
50
|
score += 10;
|
|
45
51
|
if (quota.windows && quota.windows.length > 0) {
|
|
46
52
|
score += 5 + quota.windows.length;
|
|
@@ -53,13 +59,13 @@ function quotaScore(quota) {
|
|
|
53
59
|
}
|
|
54
60
|
export function collapseQuotaSnapshots(quotas) {
|
|
55
61
|
const grouped = new Map();
|
|
56
|
-
const hasRightCodeBase = quotas.some((quota) => quota.adapterID ===
|
|
62
|
+
const hasRightCodeBase = quotas.some((quota) => quota.adapterID === "rightcode" && quotaDisplayLabel(quota) === "RC");
|
|
57
63
|
for (const quota of quotas) {
|
|
58
64
|
// If both RC (balance) and RC-variant (subscription) exist,
|
|
59
65
|
// treat balance as owned by RC.
|
|
60
66
|
const normalizedQuota = hasRightCodeBase &&
|
|
61
|
-
quota.adapterID ===
|
|
62
|
-
quotaDisplayLabel(quota).startsWith(
|
|
67
|
+
quota.adapterID === "rightcode" &&
|
|
68
|
+
quotaDisplayLabel(quota).startsWith("RC-")
|
|
63
69
|
? { ...quota, balance: undefined }
|
|
64
70
|
: quota;
|
|
65
71
|
const key = quotaKey(normalizedQuota);
|
package/dist/storage_parse.js
CHANGED
package/dist/title.js
CHANGED
|
@@ -57,28 +57,28 @@ function isCoreDecoratedDetail(line) {
|
|
|
57
57
|
function isQuotaDecoratedDetail(line) {
|
|
58
58
|
if (!line)
|
|
59
59
|
return false;
|
|
60
|
-
if (/^(OAI|Cop|Ant|Kimi|XYAI|
|
|
60
|
+
if (/^(OAI|Cop|Ant|Kimi|XYAI|RC(?:-[^\s]+)?)(?:\s+(?:\?|unsupported|unavailable|error|(?:\d+h|D|W|M)\d{1,3}|D[\d.,]+\/[\d.,]+|B(?:[¥$-])?[\d.,]+))+$/i.test(line)) {
|
|
61
61
|
return true;
|
|
62
62
|
}
|
|
63
|
-
if (/^(OpenAI|Copilot|Anthropic|Kimi|XYAI|
|
|
63
|
+
if (/^(OpenAI|Copilot|Anthropic|Kimi|XYAI|RC(?:-[^\s]+)?)\s*$/.test(line)) {
|
|
64
64
|
return true;
|
|
65
65
|
}
|
|
66
66
|
if (/^(?:(?:Daily\s+\$[\d.,]+\/\$[\d.,]+|\$[\d.,]+\/\$[\d.,]+)(?:\s+(?:Rst|Exp\+?)\s+[-:\d]+)?|(?:\d+[hdw]|Weekly|Monthly)\s+\d{1,3}%(?:\s+Rst\s+[-:\d]+)?|Balance\s+\$[\d.,]+|Remaining\s+\?|(?:error|unsupported|unavailable))$/.test(line)) {
|
|
67
67
|
return true;
|
|
68
68
|
}
|
|
69
|
-
if (/^(?:D\$?[\d.,]+\/\$?[\d.,]+|B(?:[¥$-])?[\d.,]+|(?:\d+[hdw]|[DWM])\d{1,3}|S7d\d{1,3})(?:\s+(?:R|E\+?)\d[\d:.-]*)?$/.test(line)) {
|
|
69
|
+
if (/^(?:D\$?[\d.,]+\/\$?[\d.,]+|B(?:[¥$-])?[\d.,]+|(?:\d+[hdw]|[DWM])\d{1,3}|(?:S7d|O7d|OA7d|Co7d)\d{1,3})(?:\s+(?:R|E\+?)\d[\d:.-]*)?$/.test(line)) {
|
|
70
70
|
return true;
|
|
71
71
|
}
|
|
72
|
-
if (/^(OpenAI|Copilot|Anthropic|Kimi|XYAI|
|
|
72
|
+
if (/^(OpenAI|Copilot|Anthropic|Kimi|XYAI|RC(?:-[^\s]+)?)(?:\s+(?:(?:Daily\s+\$[\d.,]+\/\$[\d.,]+|\$[\d.,]+\/\$[\d.,]+)(?:\s+(?:Rst|Exp\+?)\s+[-:\d]+)?|(?:\d+[hdw]|Weekly|Monthly)\s+\d{1,3}%(?:\s+Rst\s+[-:\d]+)?|(?:error|unsupported|unavailable)))$/.test(line)) {
|
|
73
73
|
return true;
|
|
74
74
|
}
|
|
75
|
-
if (/^(OpenAI|Copilot|Anthropic|Kimi|XYAI|
|
|
75
|
+
if (/^(OpenAI|Copilot|Anthropic|Kimi|XYAI|RC(?:-[^\s]+)?)(?:\s+(?:D\$?[\d.,]+\/\$?[\d.,]+|B(?:[¥$-])?[\d.,]+|(?:\d+[hdw]|[DWM])\d{1,3}|(?:S7d|O7d|OA7d|Co7d)\d{1,3})(?:\s+(?:R|E\+?)\d[\d:.-]*)?)$/.test(line)) {
|
|
76
76
|
return true;
|
|
77
77
|
}
|
|
78
|
-
if (/^(?:(?:D\$?[\d.,]+\/\$?[\d.,]+|B(?:[¥$-])?[\d.,]+|(?:\d+[hdw]|[DWM])\d{1,3}|S7d\d{1,3}|(?:R|E\+?)\d[\d:.-]*))(?:\s+(?:(?:D\$?[\d.,]+\/\$?[\d.,]+|B(?:[¥$-])?[\d.,]+|(?:\d+[hdw]|[DWM])\d{1,3}|S7d\d{1,3}|(?:R|E\+?)\d[\d:.-]*)))*$/.test(line)) {
|
|
78
|
+
if (/^(?:(?:D\$?[\d.,]+\/\$?[\d.,]+|B(?:[¥$-])?[\d.,]+|(?:\d+[hdw]|[DWM])\d{1,3}|(?:S7d|O7d|OA7d|Co7d)\d{1,3}|(?:R|E\+?)\d[\d:.-]*))(?:\s+(?:(?:D\$?[\d.,]+\/\$?[\d.,]+|B(?:[¥$-])?[\d.,]+|(?:\d+[hdw]|[DWM])\d{1,3}|(?:S7d|O7d|OA7d|Co7d)\d{1,3}|(?:R|E\+?)\d[\d:.-]*)))*$/.test(line)) {
|
|
79
79
|
return true;
|
|
80
80
|
}
|
|
81
|
-
if (/^(OpenAI|Copilot|Anthropic|Kimi|XYAI|
|
|
81
|
+
if (/^(OpenAI|Copilot|Anthropic|Kimi|XYAI|RC(?:-[^\s]+)?)(?:\s+(?:(?:D\$?[\d.,]+\/\$?[\d.,]+|B(?:[¥$-])?[\d.,]+|(?:\d+[hdw]|[DWM])\d{1,3}|(?:S7d|O7d|OA7d|Co7d)\d{1,3}|(?:R|E\+?)\d[\d:.-]*)))*$/.test(line)) {
|
|
82
82
|
return true;
|
|
83
83
|
}
|
|
84
84
|
return false;
|
package/dist/title_apply.js
CHANGED
|
@@ -15,6 +15,16 @@ export function createTitleApplicator(deps) {
|
|
|
15
15
|
balance: quota.balance ? { ...quota.balance } : undefined,
|
|
16
16
|
windows: quota.windows?.map((win) => ({ ...win })),
|
|
17
17
|
}));
|
|
18
|
+
const sameProviderIDs = (left, right) => {
|
|
19
|
+
if (left.length !== right.length)
|
|
20
|
+
return false;
|
|
21
|
+
const rightSet = new Set(right);
|
|
22
|
+
for (const value of left) {
|
|
23
|
+
if (!rightSet.has(value))
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
return true;
|
|
27
|
+
};
|
|
18
28
|
const applyTitle = async (sessionID) => {
|
|
19
29
|
if (!deps.config.sidebar.enabled)
|
|
20
30
|
return false;
|
|
@@ -82,16 +92,23 @@ export function createTitleApplicator(deps) {
|
|
|
82
92
|
const usage = await deps.summarizeSessionUsageForDisplay(sessionID, deps.config.sidebar.includeChildren);
|
|
83
93
|
const view = deps.getTitleView?.(sessionID) ??
|
|
84
94
|
resolveTitleView({ config: deps.config });
|
|
95
|
+
const panelQuotaProviders = Array.from(new Set(Object.keys(usage.providers)));
|
|
85
96
|
const quotaProviders = Array.from(new Set(view === 'compact'
|
|
86
97
|
? selectDesktopCompactProviderIDs(usage, deps.config)
|
|
87
|
-
:
|
|
98
|
+
: panelQuotaProviders));
|
|
88
99
|
const quotas = deps.config.sidebar.showQuota && quotaProviders.length > 0
|
|
89
100
|
? await deps.getQuotaSnapshots(quotaProviders)
|
|
90
101
|
: [];
|
|
102
|
+
const panelQuotas = deps.config.sidebar.showQuota && panelQuotaProviders.length > 0
|
|
103
|
+
? sameProviderIDs(quotaProviders, panelQuotaProviders)
|
|
104
|
+
? quotas
|
|
105
|
+
: await deps.getQuotaSnapshots(panelQuotaProviders)
|
|
106
|
+
: [];
|
|
91
107
|
sessionState.sidebarPanel = {
|
|
92
108
|
version: 1,
|
|
93
109
|
updatedAt: Date.now(),
|
|
94
110
|
usage: toCachedSessionUsage(usage),
|
|
111
|
+
panelQuotas: cloneQuotas(collapseQuotaSnapshots(panelQuotas)),
|
|
95
112
|
quotas: cloneQuotas(collapseQuotaSnapshots(quotas)),
|
|
96
113
|
};
|
|
97
114
|
stateMutated = true;
|
package/dist/tui.tsx
CHANGED
|
@@ -6,11 +6,16 @@ import type {
|
|
|
6
6
|
} from '@opencode-ai/plugin/tui'
|
|
7
7
|
import { createMemo, createSignal, For, onCleanup, Show } from 'solid-js'
|
|
8
8
|
|
|
9
|
+
import { fitLine, renderSidebarUsageLines } from './format.js'
|
|
9
10
|
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
fallbackQuotaGroupsFromTitle,
|
|
12
|
+
quotaGroupsAreCollapsible,
|
|
13
|
+
quotaGroupsSummary,
|
|
14
|
+
quotaGroupsUseBullets,
|
|
15
|
+
renderSidebarQuotaGroups,
|
|
16
|
+
sidebarPanelQuotaSnapshots,
|
|
17
|
+
type SidebarQuotaGroup,
|
|
18
|
+
} from './tui_helpers.js'
|
|
14
19
|
import {
|
|
15
20
|
loadConfig,
|
|
16
21
|
loadState,
|
|
@@ -31,7 +36,7 @@ type SidebarPanelData = {
|
|
|
31
36
|
enabled: boolean
|
|
32
37
|
width: number
|
|
33
38
|
usageLines: string[]
|
|
34
|
-
|
|
39
|
+
quotaGroups: SidebarQuotaGroup[]
|
|
35
40
|
compactTitle?: string
|
|
36
41
|
}
|
|
37
42
|
|
|
@@ -96,7 +101,7 @@ async function loadSidebarPanel(
|
|
|
96
101
|
enabled,
|
|
97
102
|
width,
|
|
98
103
|
usageLines: [],
|
|
99
|
-
|
|
104
|
+
quotaGroups: [],
|
|
100
105
|
compactTitle: session?.lastAppliedTitle,
|
|
101
106
|
}
|
|
102
107
|
}
|
|
@@ -104,8 +109,8 @@ async function loadSidebarPanel(
|
|
|
104
109
|
const usageLines = usage
|
|
105
110
|
? renderSidebarUsageLines(usage, panelConfig(config))
|
|
106
111
|
: []
|
|
107
|
-
const
|
|
108
|
-
session?.sidebarPanel
|
|
112
|
+
const quotaGroups = renderSidebarQuotaGroups(
|
|
113
|
+
sidebarPanelQuotaSnapshots(session?.sidebarPanel),
|
|
109
114
|
panelConfig(config),
|
|
110
115
|
)
|
|
111
116
|
|
|
@@ -113,7 +118,7 @@ async function loadSidebarPanel(
|
|
|
113
118
|
enabled,
|
|
114
119
|
width,
|
|
115
120
|
usageLines,
|
|
116
|
-
|
|
121
|
+
quotaGroups,
|
|
117
122
|
compactTitle,
|
|
118
123
|
}
|
|
119
124
|
}
|
|
@@ -193,20 +198,83 @@ function useSidebarPanelData(api: TuiPluginApi, sessionID: () => string) {
|
|
|
193
198
|
return panel
|
|
194
199
|
}
|
|
195
200
|
|
|
196
|
-
function
|
|
197
|
-
|
|
201
|
+
function SectionHeading(props: {
|
|
202
|
+
api: TuiPluginApi
|
|
203
|
+
value: string
|
|
204
|
+
collapsible?: boolean
|
|
205
|
+
open?: boolean
|
|
206
|
+
summary?: string
|
|
207
|
+
onToggle?: () => void
|
|
208
|
+
}) {
|
|
209
|
+
const clickable = () => props.collapsible === true && props.onToggle
|
|
210
|
+
|
|
211
|
+
return (
|
|
212
|
+
<box
|
|
213
|
+
flexDirection="row"
|
|
214
|
+
gap={1}
|
|
215
|
+
onMouseDown={() => {
|
|
216
|
+
if (!clickable()) return
|
|
217
|
+
props.onToggle?.()
|
|
218
|
+
}}
|
|
219
|
+
>
|
|
220
|
+
<Show when={props.collapsible}>
|
|
221
|
+
<text fg={props.api.theme.current.text}>{props.open ? '▼' : '▶'}</text>
|
|
222
|
+
</Show>
|
|
223
|
+
<text fg={props.api.theme.current.text}>
|
|
224
|
+
<b>{props.value}</b>
|
|
225
|
+
<Show when={props.summary}>
|
|
226
|
+
<span style={{ fg: props.api.theme.current.textMuted }}>
|
|
227
|
+
{' '}
|
|
228
|
+
{props.summary}
|
|
229
|
+
</span>
|
|
230
|
+
</Show>
|
|
231
|
+
</text>
|
|
232
|
+
</box>
|
|
233
|
+
)
|
|
198
234
|
}
|
|
199
235
|
|
|
200
|
-
function
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
236
|
+
function quotaToneColor(api: TuiPluginApi, tone: SidebarQuotaGroup['tone']) {
|
|
237
|
+
const theme = api.theme.current
|
|
238
|
+
if (tone === 'success') return theme.success
|
|
239
|
+
if (tone === 'warning') return theme.warning
|
|
240
|
+
if (tone === 'error') return theme.error
|
|
241
|
+
return theme.textMuted
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function QuotaGroupBlock(props: {
|
|
245
|
+
api: TuiPluginApi
|
|
246
|
+
group: SidebarQuotaGroup
|
|
247
|
+
bullet: boolean
|
|
248
|
+
}) {
|
|
249
|
+
const content = (
|
|
250
|
+
<box gap={0}>
|
|
251
|
+
<text>
|
|
252
|
+
<span style={{ fg: props.api.theme.current.text }}>
|
|
253
|
+
{props.group.shortLabel}
|
|
254
|
+
</span>
|
|
255
|
+
<Show when={props.group.detail}>
|
|
256
|
+
<span style={{ fg: props.api.theme.current.textMuted }}>
|
|
257
|
+
{' '}
|
|
258
|
+
{props.group.detail}
|
|
259
|
+
</span>
|
|
260
|
+
</Show>
|
|
261
|
+
</text>
|
|
262
|
+
<For each={props.group.continuationLines}>
|
|
263
|
+
{(line) => <text fg={props.api.theme.current.textMuted}>{line}</text>}
|
|
264
|
+
</For>
|
|
265
|
+
</box>
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
return (
|
|
269
|
+
<Show when={props.bullet} fallback={content}>
|
|
270
|
+
<box flexDirection="row" gap={1}>
|
|
271
|
+
<text flexShrink={0} fg={quotaToneColor(props.api, props.group.tone)}>
|
|
272
|
+
•
|
|
273
|
+
</text>
|
|
274
|
+
{content}
|
|
275
|
+
</box>
|
|
276
|
+
</Show>
|
|
277
|
+
)
|
|
210
278
|
}
|
|
211
279
|
|
|
212
280
|
function fallbackUsageCostLineFromTitle(title: string, width: number) {
|
|
@@ -220,6 +288,7 @@ function fallbackUsageCostLineFromTitle(title: string, width: number) {
|
|
|
220
288
|
|
|
221
289
|
function SidebarContentView(props: { api: TuiPluginApi; sessionID: string }) {
|
|
222
290
|
const panel = useSidebarPanelData(props.api, () => props.sessionID)
|
|
291
|
+
const [quotaOpen, setQuotaOpen] = createSignal(true)
|
|
223
292
|
const width = createMemo(
|
|
224
293
|
() => panel()?.width || DEFAULT_WIDTH - SECTION_INDENT,
|
|
225
294
|
)
|
|
@@ -234,22 +303,32 @@ function SidebarContentView(props: { api: TuiPluginApi; sessionID: string }) {
|
|
|
234
303
|
const costLine = fallbackUsageCostLineFromTitle(compactTitle(), width())
|
|
235
304
|
return costLine ? [...liveLines, costLine] : liveLines
|
|
236
305
|
})
|
|
237
|
-
const
|
|
238
|
-
const
|
|
239
|
-
if (
|
|
240
|
-
return
|
|
306
|
+
const quotaGroups = createMemo(() => {
|
|
307
|
+
const liveGroups = panel()?.quotaGroups || []
|
|
308
|
+
if (liveGroups.length > 0) return liveGroups
|
|
309
|
+
return fallbackQuotaGroupsFromTitle(compactTitle(), width())
|
|
241
310
|
})
|
|
242
311
|
const hasUsage = createMemo(() => usageLines().length > 0)
|
|
243
|
-
const hasQuota = createMemo(() =>
|
|
312
|
+
const hasQuota = createMemo(() => quotaGroups().length > 0)
|
|
313
|
+
const quotaBullets = createMemo(() => quotaGroupsUseBullets(quotaGroups()))
|
|
314
|
+
const quotaCollapsible = createMemo(() =>
|
|
315
|
+
quotaGroupsAreCollapsible(quotaGroups()),
|
|
316
|
+
)
|
|
317
|
+
const quotaSummary = createMemo(() => {
|
|
318
|
+
if (!quotaCollapsible() || quotaOpen()) return undefined
|
|
319
|
+
return quotaGroupsSummary(quotaGroups())
|
|
320
|
+
})
|
|
244
321
|
|
|
245
322
|
return (
|
|
246
323
|
<box gap={0}>
|
|
247
324
|
<Show when={hasUsage()}>
|
|
248
325
|
<box gap={0}>
|
|
249
|
-
{
|
|
326
|
+
<SectionHeading api={props.api} value="Usage" />
|
|
250
327
|
<box gap={0}>
|
|
251
328
|
<For each={usageLines()}>
|
|
252
|
-
{(line) =>
|
|
329
|
+
{(line) => (
|
|
330
|
+
<text fg={props.api.theme.current.textMuted}>{line}</text>
|
|
331
|
+
)}
|
|
253
332
|
</For>
|
|
254
333
|
</box>
|
|
255
334
|
</box>
|
|
@@ -257,12 +336,27 @@ function SidebarContentView(props: { api: TuiPluginApi; sessionID: string }) {
|
|
|
257
336
|
|
|
258
337
|
<Show when={hasQuota()}>
|
|
259
338
|
<box paddingTop={hasUsage() ? 1 : 0} gap={0}>
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
339
|
+
<SectionHeading
|
|
340
|
+
api={props.api}
|
|
341
|
+
value="Quota"
|
|
342
|
+
collapsible={quotaCollapsible()}
|
|
343
|
+
open={quotaOpen()}
|
|
344
|
+
summary={quotaSummary()}
|
|
345
|
+
onToggle={() => setQuotaOpen((value) => !value)}
|
|
346
|
+
/>
|
|
347
|
+
<Show when={!quotaCollapsible() || quotaOpen()}>
|
|
348
|
+
<box gap={0}>
|
|
349
|
+
<For each={quotaGroups()}>
|
|
350
|
+
{(group) => (
|
|
351
|
+
<QuotaGroupBlock
|
|
352
|
+
api={props.api}
|
|
353
|
+
group={group}
|
|
354
|
+
bullet={quotaBullets()}
|
|
355
|
+
/>
|
|
356
|
+
)}
|
|
357
|
+
</For>
|
|
358
|
+
</box>
|
|
359
|
+
</Show>
|
|
266
360
|
</box>
|
|
267
361
|
</Show>
|
|
268
362
|
</box>
|
|
@@ -292,10 +386,13 @@ function SidebarTitleView(props: {
|
|
|
292
386
|
|
|
293
387
|
return (
|
|
294
388
|
<box gap={0} paddingRight={1}>
|
|
295
|
-
{sectionHeading(props.api, 'TITLE')}
|
|
296
389
|
<box gap={0}>
|
|
297
390
|
<For each={titleLines()}>
|
|
298
|
-
{(line) =>
|
|
391
|
+
{(line) => (
|
|
392
|
+
<text fg={props.api.theme.current.text}>
|
|
393
|
+
<b>{line}</b>
|
|
394
|
+
</text>
|
|
395
|
+
)}
|
|
299
396
|
</For>
|
|
300
397
|
<Show when={shareLine()}>
|
|
301
398
|
<text fg={props.api.theme.current.textMuted}>{shareLine()}</text>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { QuotaSidebarConfig, QuotaSnapshot, SidebarPanelState } from './types.js';
|
|
2
|
+
export type SidebarQuotaTone = 'success' | 'warning' | 'error' | 'muted';
|
|
3
|
+
export type SidebarQuotaGroup = {
|
|
4
|
+
providerID: string;
|
|
5
|
+
status: QuotaSnapshot['status'];
|
|
6
|
+
tone: SidebarQuotaTone;
|
|
7
|
+
shortLabel: string;
|
|
8
|
+
detail: string;
|
|
9
|
+
continuationLines: string[];
|
|
10
|
+
};
|
|
11
|
+
export declare function renderSidebarQuotaGroups(quotas: QuotaSnapshot[], config: QuotaSidebarConfig): SidebarQuotaGroup[];
|
|
12
|
+
export declare function sidebarPanelQuotaSnapshots(panel?: SidebarPanelState): QuotaSnapshot[];
|
|
13
|
+
export declare function fallbackQuotaGroupsFromTitle(title: string, width: number): SidebarQuotaGroup[];
|
|
14
|
+
export declare function quotaGroupsUseBullets(groups: SidebarQuotaGroup[]): boolean;
|
|
15
|
+
export declare function quotaGroupsAreCollapsible(groups: SidebarQuotaGroup[]): boolean;
|
|
16
|
+
export declare function quotaGroupsSummary(groups: SidebarQuotaGroup[]): string | undefined;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { fitLine, renderSidebarQuotaLineGroups } from './format.js';
|
|
2
|
+
import { collapseQuotaSnapshots } from './quota_render.js';
|
|
3
|
+
const VISIBLE_QUOTA_STATUSES = new Set([
|
|
4
|
+
'ok',
|
|
5
|
+
'error',
|
|
6
|
+
'unsupported',
|
|
7
|
+
'unavailable',
|
|
8
|
+
]);
|
|
9
|
+
function parseQuotaLineParts(lines) {
|
|
10
|
+
const firstLine = lines[0]?.trimStart() || '';
|
|
11
|
+
const match = /^(\S+)(?:\s+(.*))?$/.exec(firstLine);
|
|
12
|
+
const shortLabel = match?.[1] || firstLine || 'Quota';
|
|
13
|
+
const detail = match?.[2] || '';
|
|
14
|
+
const continuationLines = lines
|
|
15
|
+
.slice(1)
|
|
16
|
+
.map((line) => line.trimEnd())
|
|
17
|
+
.filter((line) => Boolean(line.trim()));
|
|
18
|
+
return {
|
|
19
|
+
shortLabel,
|
|
20
|
+
detail,
|
|
21
|
+
continuationLines,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function quotaPercents(quota) {
|
|
25
|
+
const values = [];
|
|
26
|
+
if (quota.remainingPercent !== undefined &&
|
|
27
|
+
Number.isFinite(quota.remainingPercent)) {
|
|
28
|
+
values.push(quota.remainingPercent);
|
|
29
|
+
}
|
|
30
|
+
for (const window of quota.windows || []) {
|
|
31
|
+
if (window.remainingPercent !== undefined &&
|
|
32
|
+
Number.isFinite(window.remainingPercent)) {
|
|
33
|
+
values.push(window.remainingPercent);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return values;
|
|
37
|
+
}
|
|
38
|
+
function quotaTone(quota) {
|
|
39
|
+
if (quota.status === 'error')
|
|
40
|
+
return 'error';
|
|
41
|
+
if (quota.status === 'unsupported' || quota.status === 'unavailable') {
|
|
42
|
+
return 'muted';
|
|
43
|
+
}
|
|
44
|
+
if (quota.status !== 'ok')
|
|
45
|
+
return 'muted';
|
|
46
|
+
const percents = quotaPercents(quota);
|
|
47
|
+
if (percents.length === 0) {
|
|
48
|
+
if (quota.balance && Number.isFinite(quota.balance.amount)) {
|
|
49
|
+
if (quota.balance.amount < 0)
|
|
50
|
+
return 'error';
|
|
51
|
+
return 'muted';
|
|
52
|
+
}
|
|
53
|
+
return 'muted';
|
|
54
|
+
}
|
|
55
|
+
const remaining = Math.min(...percents);
|
|
56
|
+
if (remaining <= 5)
|
|
57
|
+
return 'error';
|
|
58
|
+
if (remaining <= 20)
|
|
59
|
+
return 'warning';
|
|
60
|
+
return 'success';
|
|
61
|
+
}
|
|
62
|
+
function fallbackQuotaTone(detail) {
|
|
63
|
+
const safe = detail.trim();
|
|
64
|
+
if (!safe)
|
|
65
|
+
return 'muted';
|
|
66
|
+
if (/\b(?:unsupported|unavailable)\b/i.test(safe))
|
|
67
|
+
return 'muted';
|
|
68
|
+
if (/\berror\b/i.test(safe) || /^\?$/.test(safe))
|
|
69
|
+
return 'error';
|
|
70
|
+
if (/\bB-/.test(safe))
|
|
71
|
+
return 'error';
|
|
72
|
+
const percents = [
|
|
73
|
+
...safe.matchAll(/\b(?:\d+[hdw]|[DWM]|S7d|O7d|OA7d|Co7d)(\d{1,3})\b/gi),
|
|
74
|
+
]
|
|
75
|
+
.map((match) => Number(match[1]))
|
|
76
|
+
.filter((value) => Number.isFinite(value));
|
|
77
|
+
if (percents.length === 0)
|
|
78
|
+
return 'muted';
|
|
79
|
+
const remaining = Math.min(...percents);
|
|
80
|
+
if (remaining <= 5)
|
|
81
|
+
return 'error';
|
|
82
|
+
if (remaining <= 20)
|
|
83
|
+
return 'warning';
|
|
84
|
+
return 'success';
|
|
85
|
+
}
|
|
86
|
+
export function renderSidebarQuotaGroups(quotas, config) {
|
|
87
|
+
const visibleQuotaCount = collapseQuotaSnapshots(quotas).filter((quota) => VISIBLE_QUOTA_STATUSES.has(quota.status)).length;
|
|
88
|
+
const renderConfig = visibleQuotaCount > 1
|
|
89
|
+
? {
|
|
90
|
+
...config,
|
|
91
|
+
sidebar: {
|
|
92
|
+
...config.sidebar,
|
|
93
|
+
width: Math.max(8, config.sidebar.width - 2),
|
|
94
|
+
},
|
|
95
|
+
}
|
|
96
|
+
: config;
|
|
97
|
+
return renderSidebarQuotaLineGroups(quotas, renderConfig).map((group) => {
|
|
98
|
+
const parsed = parseQuotaLineParts(group.lines);
|
|
99
|
+
return {
|
|
100
|
+
providerID: group.quota.providerID,
|
|
101
|
+
status: group.quota.status,
|
|
102
|
+
tone: quotaTone(group.quota),
|
|
103
|
+
shortLabel: parsed.shortLabel,
|
|
104
|
+
detail: parsed.detail,
|
|
105
|
+
continuationLines: parsed.continuationLines,
|
|
106
|
+
};
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
export function sidebarPanelQuotaSnapshots(panel) {
|
|
110
|
+
return panel?.panelQuotas || panel?.quotas || [];
|
|
111
|
+
}
|
|
112
|
+
export function fallbackQuotaGroupsFromTitle(title, width) {
|
|
113
|
+
const parts = (title || '')
|
|
114
|
+
.split(' | ')
|
|
115
|
+
.map((part) => part.trim())
|
|
116
|
+
.filter(Boolean);
|
|
117
|
+
const quotaParts = parts
|
|
118
|
+
.slice(1)
|
|
119
|
+
.filter((part) => !/^Cd\d/.test(part) && !/^Est\b/.test(part));
|
|
120
|
+
if (quotaParts.length === 0)
|
|
121
|
+
return [];
|
|
122
|
+
const contentWidth = quotaParts.length > 1 ? Math.max(1, width - 2) : width;
|
|
123
|
+
return quotaParts.map((part, index) => {
|
|
124
|
+
const line = fitLine(part, contentWidth);
|
|
125
|
+
const parsed = parseQuotaLineParts([line]);
|
|
126
|
+
return {
|
|
127
|
+
providerID: `fallback:${index}`,
|
|
128
|
+
status: 'ok',
|
|
129
|
+
tone: fallbackQuotaTone(parsed.detail),
|
|
130
|
+
shortLabel: parsed.shortLabel,
|
|
131
|
+
detail: parsed.detail,
|
|
132
|
+
continuationLines: parsed.continuationLines,
|
|
133
|
+
};
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
export function quotaGroupsUseBullets(groups) {
|
|
137
|
+
return groups.length > 1;
|
|
138
|
+
}
|
|
139
|
+
export function quotaGroupsAreCollapsible(groups) {
|
|
140
|
+
return groups.length > 2;
|
|
141
|
+
}
|
|
142
|
+
export function quotaGroupsSummary(groups) {
|
|
143
|
+
if (groups.length === 0)
|
|
144
|
+
return undefined;
|
|
145
|
+
return `(${groups.length})`;
|
|
146
|
+
}
|