@leo000001/opencode-quota-sidebar 4.0.5 → 4.0.11
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 +504 -446
- package/README.zh-CN.md +516 -458
- package/dist/cli.d.ts +38 -0
- package/dist/cli.js +153 -42
- package/dist/cli_render.d.ts +4 -4
- package/dist/cli_render.js +74 -74
- package/dist/cost.d.ts +21 -4
- package/dist/cost.js +493 -264
- package/dist/format.d.ts +5 -5
- package/dist/format.js +288 -287
- package/dist/history_usage.d.ts +15 -9
- package/dist/history_usage.js +28 -22
- package/dist/index.d.ts +3 -3
- package/dist/index.js +35 -34
- package/dist/models_dev_pricing.d.ts +6 -0
- package/dist/models_dev_pricing.js +226 -0
- package/dist/opencode_pricing.d.ts +14 -0
- package/dist/opencode_pricing.js +273 -0
- package/dist/storage.d.ts +3 -3
- package/dist/storage.js +27 -28
- package/dist/storage_parse.d.ts +1 -1
- package/dist/storage_parse.js +51 -45
- package/dist/storage_paths.d.ts +1 -0
- package/dist/storage_paths.js +26 -11
- package/dist/title_apply.d.ts +5 -22
- package/dist/title_apply.js +19 -61
- package/dist/tui.d.ts +1 -1
- package/dist/tui.tsx +481 -471
- package/dist/tui_helpers.d.ts +5 -3
- package/dist/tui_helpers.js +62 -34
- package/dist/types.d.ts +8 -10
- package/dist/usage.d.ts +9 -6
- package/dist/usage.js +27 -21
- package/dist/usage_service.d.ts +8 -7
- package/dist/usage_service.js +261 -150
- package/package.json +1 -1
package/dist/storage_paths.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import os from
|
|
2
|
-
import path from
|
|
3
|
-
import { isDateKey } from
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { isDateKey } from "./storage_dates.js";
|
|
4
4
|
/**
|
|
5
5
|
* Resolve the OpenCode data directory.
|
|
6
6
|
*
|
|
@@ -17,8 +17,8 @@ export function resolveOpencodeDataDir() {
|
|
|
17
17
|
return path.resolve(override);
|
|
18
18
|
const xdg = process.env.XDG_DATA_HOME?.trim();
|
|
19
19
|
if (xdg)
|
|
20
|
-
return path.join(path.resolve(xdg),
|
|
21
|
-
return path.join(os.homedir(),
|
|
20
|
+
return path.join(path.resolve(xdg), "opencode");
|
|
21
|
+
return path.join(os.homedir(), ".local", "share", "opencode");
|
|
22
22
|
}
|
|
23
23
|
/**
|
|
24
24
|
* Resolve OpenCode config directory.
|
|
@@ -31,23 +31,38 @@ export function resolveOpencodeConfigDir() {
|
|
|
31
31
|
return path.resolve(override);
|
|
32
32
|
const xdg = process.env.XDG_CONFIG_HOME?.trim();
|
|
33
33
|
if (xdg)
|
|
34
|
-
return path.join(path.resolve(xdg),
|
|
35
|
-
return path.join(os.homedir(),
|
|
34
|
+
return path.join(path.resolve(xdg), "opencode");
|
|
35
|
+
return path.join(os.homedir(), ".config", "opencode");
|
|
36
|
+
}
|
|
37
|
+
export function opencodeConfigPaths(worktree, directory) {
|
|
38
|
+
const configDir = resolveOpencodeConfigDir();
|
|
39
|
+
return [
|
|
40
|
+
path.join(configDir, "opencode.jsonc"),
|
|
41
|
+
path.join(configDir, "opencode.json"),
|
|
42
|
+
path.join(worktree, "opencode.jsonc"),
|
|
43
|
+
path.join(worktree, "opencode.json"),
|
|
44
|
+
path.join(directory, "opencode.jsonc"),
|
|
45
|
+
path.join(directory, "opencode.json"),
|
|
46
|
+
path.join(worktree, ".opencode", "opencode.jsonc"),
|
|
47
|
+
path.join(worktree, ".opencode", "opencode.json"),
|
|
48
|
+
path.join(directory, ".opencode", "opencode.jsonc"),
|
|
49
|
+
path.join(directory, ".opencode", "opencode.json"),
|
|
50
|
+
];
|
|
36
51
|
}
|
|
37
52
|
export function stateFilePath(dataDir) {
|
|
38
|
-
return path.join(dataDir,
|
|
53
|
+
return path.join(dataDir, "quota-sidebar.state.json");
|
|
39
54
|
}
|
|
40
55
|
export function authFilePath(dataDir) {
|
|
41
|
-
return path.join(dataDir,
|
|
56
|
+
return path.join(dataDir, "auth.json");
|
|
42
57
|
}
|
|
43
58
|
export function chunkRootPathFromStateFile(statePath) {
|
|
44
|
-
return path.join(path.dirname(statePath),
|
|
59
|
+
return path.join(path.dirname(statePath), "quota-sidebar-sessions");
|
|
45
60
|
}
|
|
46
61
|
export function chunkFilePath(rootPath, dateKey) {
|
|
47
62
|
// Defense-in-depth: ensure we never build paths from untrusted inputs.
|
|
48
63
|
if (!isDateKey(dateKey)) {
|
|
49
64
|
throw new Error(`invalid dateKey: ${dateKey}`);
|
|
50
65
|
}
|
|
51
|
-
const [year, month, day] = dateKey.split(
|
|
66
|
+
const [year, month, day] = dateKey.split("-");
|
|
52
67
|
return path.join(rootPath, year, month, `${day}.json`);
|
|
53
68
|
}
|
package/dist/title_apply.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import type { PluginInput } from
|
|
2
|
-
import type { QuotaSidebarConfig, QuotaSidebarState, QuotaSnapshot, SessionState } from
|
|
3
|
-
import type { UsageSummary } from
|
|
4
|
-
import { type TitleView } from
|
|
1
|
+
import type { PluginInput } from "@opencode-ai/plugin";
|
|
2
|
+
import type { QuotaSidebarConfig, QuotaSidebarState, QuotaSnapshot, SessionState } from "./types.js";
|
|
3
|
+
import type { UsageSummary } from "./usage.js";
|
|
4
|
+
import { type TitleView } from "./format.js";
|
|
5
5
|
export declare function createTitleApplicator(deps: {
|
|
6
6
|
state: QuotaSidebarState;
|
|
7
7
|
config: QuotaSidebarConfig;
|
|
8
|
-
client: PluginInput[
|
|
8
|
+
client: PluginInput["client"];
|
|
9
9
|
directory: string;
|
|
10
10
|
ensureSessionState: (sessionID: string, title: string, createdAt: number, parentID?: string | null) => SessionState;
|
|
11
11
|
markDirty: (dateKey: string | undefined) => void;
|
|
@@ -30,22 +30,5 @@ export declare function createTitleApplicator(deps: {
|
|
|
30
30
|
restoreSessionTitle: (sessionID: string, options?: {
|
|
31
31
|
abortIfEnabled?: boolean;
|
|
32
32
|
}) => Promise<boolean>;
|
|
33
|
-
restoreAllVisibleTitles: (options?: {
|
|
34
|
-
abortIfEnabled?: boolean;
|
|
35
|
-
}) => Promise<{
|
|
36
|
-
attempted: number;
|
|
37
|
-
restored: number;
|
|
38
|
-
listFailed: boolean;
|
|
39
|
-
}>;
|
|
40
|
-
refreshAllTouchedTitles: () => Promise<{
|
|
41
|
-
attempted: number;
|
|
42
|
-
refreshed: number;
|
|
43
|
-
listFailed: boolean;
|
|
44
|
-
}>;
|
|
45
|
-
refreshAllVisibleTitles: () => Promise<{
|
|
46
|
-
attempted: number;
|
|
47
|
-
refreshed: number;
|
|
48
|
-
listFailed: boolean;
|
|
49
|
-
}>;
|
|
50
33
|
forgetSession: (sessionID: string) => void;
|
|
51
34
|
};
|
package/dist/title_apply.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { canonicalizeTitle, canonicalizeTitleForCompare, looksDecorated, normalizeBaseTitle, } from
|
|
2
|
-
import { toCachedSessionUsage } from
|
|
3
|
-
import { swallow, debug
|
|
4
|
-
import { resolveTitleView, selectDesktopCompactProviderIDs, } from
|
|
5
|
-
import { collapseQuotaSnapshots } from
|
|
1
|
+
import { canonicalizeTitle, canonicalizeTitleForCompare, looksDecorated, normalizeBaseTitle, } from "./title.js";
|
|
2
|
+
import { toCachedSessionUsage } from "./usage.js";
|
|
3
|
+
import { swallow, debug } from "./helpers.js";
|
|
4
|
+
import { resolveTitleView, selectDesktopCompactProviderIDs, } from "./format.js";
|
|
5
|
+
import { collapseQuotaSnapshots } from "./quota_render.js";
|
|
6
6
|
export function createTitleApplicator(deps) {
|
|
7
7
|
const pendingAppliedTitle = new Map();
|
|
8
8
|
const recentRestore = new Map();
|
|
@@ -39,13 +39,13 @@ export function createTitleApplicator(deps) {
|
|
|
39
39
|
query: { directory: deps.directory },
|
|
40
40
|
throwOnError: true,
|
|
41
41
|
})
|
|
42
|
-
.catch(swallow(
|
|
42
|
+
.catch(swallow("applyTitle:getSession"));
|
|
43
43
|
if (!session)
|
|
44
44
|
return false;
|
|
45
45
|
if (!session.data ||
|
|
46
|
-
typeof session.data.title !==
|
|
46
|
+
typeof session.data.title !== "string" ||
|
|
47
47
|
!session.data.time ||
|
|
48
|
-
typeof session.data.time.created !==
|
|
48
|
+
typeof session.data.time.created !== "number") {
|
|
49
49
|
debug(`applyTitle skipped malformed session payload for ${sessionID}`);
|
|
50
50
|
return false;
|
|
51
51
|
}
|
|
@@ -53,7 +53,7 @@ export function createTitleApplicator(deps) {
|
|
|
53
53
|
// Detect whether the current title is our own decorated form.
|
|
54
54
|
const currentTitle = session.data.title;
|
|
55
55
|
if (canonicalizeTitle(currentTitle) !==
|
|
56
|
-
canonicalizeTitle(sessionState.lastAppliedTitle ||
|
|
56
|
+
canonicalizeTitle(sessionState.lastAppliedTitle || "")) {
|
|
57
57
|
if (looksDecorated(currentTitle)) {
|
|
58
58
|
if (/\r?\n/.test(currentTitle)) {
|
|
59
59
|
const normalizedBase = normalizeBaseTitle(currentTitle);
|
|
@@ -65,7 +65,8 @@ export function createTitleApplicator(deps) {
|
|
|
65
65
|
// Ignore decorated echoes as base-title source.
|
|
66
66
|
// If we previously applied a decorated title, treat this as an
|
|
67
67
|
// equivalent echo (OpenCode may normalize whitespace) and keep
|
|
68
|
-
// lastAppliedTitle in sync so
|
|
68
|
+
// Keep lastAppliedTitle in sync with server echoes so restore logic can
|
|
69
|
+
// still tell whether the current title is ours.
|
|
69
70
|
if (sessionState.lastAppliedTitle &&
|
|
70
71
|
looksDecorated(sessionState.lastAppliedTitle)) {
|
|
71
72
|
if (sessionState.lastAppliedTitle !== currentTitle) {
|
|
@@ -97,7 +98,7 @@ export function createTitleApplicator(deps) {
|
|
|
97
98
|
const view = deps.getTitleView?.(sessionID) ??
|
|
98
99
|
resolveTitleView({ config: deps.config });
|
|
99
100
|
const panelQuotaProviders = Array.from(new Set(Object.keys(usage.providers)));
|
|
100
|
-
const quotaProviders = Array.from(new Set(view ===
|
|
101
|
+
const quotaProviders = Array.from(new Set(view === "compact"
|
|
101
102
|
? selectDesktopCompactProviderIDs(usage, deps.config)
|
|
102
103
|
: panelQuotaProviders));
|
|
103
104
|
const quotas = deps.config.sidebar.showQuota && quotaProviders.length > 0
|
|
@@ -156,7 +157,7 @@ export function createTitleApplicator(deps) {
|
|
|
156
157
|
body: { title: nextTitle },
|
|
157
158
|
throwOnError: true,
|
|
158
159
|
})
|
|
159
|
-
.catch(swallow(
|
|
160
|
+
.catch(swallow("applyTitle:update"));
|
|
160
161
|
if (!updated) {
|
|
161
162
|
pendingAppliedTitle.delete(sessionID);
|
|
162
163
|
sessionState.lastAppliedTitle = previousApplied;
|
|
@@ -191,7 +192,7 @@ export function createTitleApplicator(deps) {
|
|
|
191
192
|
// of our own update. Extract the base title from line 1 instead of
|
|
192
193
|
// treating the whole decorated string as the new base title.
|
|
193
194
|
if (canonicalizeTitleForCompare(args.incomingTitle) ===
|
|
194
|
-
canonicalizeTitleForCompare(args.sessionState.lastAppliedTitle ||
|
|
195
|
+
canonicalizeTitleForCompare(args.sessionState.lastAppliedTitle || "")) {
|
|
195
196
|
return;
|
|
196
197
|
}
|
|
197
198
|
if (looksDecorated(args.incomingTitle) &&
|
|
@@ -235,18 +236,18 @@ export function createTitleApplicator(deps) {
|
|
|
235
236
|
query: { directory: deps.directory },
|
|
236
237
|
throwOnError: true,
|
|
237
238
|
})
|
|
238
|
-
.catch(swallow(
|
|
239
|
+
.catch(swallow("restoreSessionTitle:get"));
|
|
239
240
|
if (!session)
|
|
240
241
|
return false;
|
|
241
242
|
if (!session.data ||
|
|
242
|
-
typeof session.data.title !==
|
|
243
|
+
typeof session.data.title !== "string" ||
|
|
243
244
|
!session.data.time ||
|
|
244
|
-
typeof session.data.time.created !==
|
|
245
|
+
typeof session.data.time.created !== "number") {
|
|
245
246
|
debug(`restoreSessionTitle skipped malformed session payload for ${sessionID}`);
|
|
246
247
|
return false;
|
|
247
248
|
}
|
|
248
249
|
const sessionState = deps.ensureSessionState(sessionID, session.data.title, session.data.time.created, session.data.parentID ?? null);
|
|
249
|
-
const baseTitle = canonicalizeTitle(sessionState.baseTitle) ||
|
|
250
|
+
const baseTitle = canonicalizeTitle(sessionState.baseTitle) || "Session";
|
|
250
251
|
if (session.data.title === baseTitle) {
|
|
251
252
|
if (sessionState.lastAppliedTitle !== undefined) {
|
|
252
253
|
sessionState.lastAppliedTitle = undefined;
|
|
@@ -264,7 +265,7 @@ export function createTitleApplicator(deps) {
|
|
|
264
265
|
body: { title: baseTitle },
|
|
265
266
|
throwOnError: true,
|
|
266
267
|
})
|
|
267
|
-
.catch(swallow(
|
|
268
|
+
.catch(swallow("restoreSessionTitle:update"));
|
|
268
269
|
if (!updated)
|
|
269
270
|
return false;
|
|
270
271
|
pendingAppliedTitle.delete(sessionID);
|
|
@@ -278,53 +279,10 @@ export function createTitleApplicator(deps) {
|
|
|
278
279
|
deps.scheduleSave();
|
|
279
280
|
return true;
|
|
280
281
|
};
|
|
281
|
-
const restoreAllVisibleTitles = async (options) => {
|
|
282
|
-
const touched = Object.entries(deps.state.sessions)
|
|
283
|
-
.filter(([, sessionState]) => Boolean(sessionState.lastAppliedTitle))
|
|
284
|
-
.map(([sessionID]) => sessionID);
|
|
285
|
-
const results = await mapConcurrent(touched, deps.restoreConcurrency, async (sessionID) => restoreSessionTitle(sessionID, options));
|
|
286
|
-
return {
|
|
287
|
-
attempted: touched.length,
|
|
288
|
-
restored: results.filter(Boolean).length,
|
|
289
|
-
listFailed: false,
|
|
290
|
-
};
|
|
291
|
-
};
|
|
292
|
-
const refreshAllTouchedTitles = async () => {
|
|
293
|
-
const touched = Object.entries(deps.state.sessions)
|
|
294
|
-
.filter(([, sessionState]) => Boolean(sessionState.lastAppliedTitle))
|
|
295
|
-
.map(([sessionID]) => sessionID);
|
|
296
|
-
const results = await mapConcurrent(touched, deps.restoreConcurrency, async (sessionID) => applyTitle(sessionID));
|
|
297
|
-
return {
|
|
298
|
-
attempted: touched.length,
|
|
299
|
-
refreshed: results.filter(Boolean).length,
|
|
300
|
-
listFailed: false,
|
|
301
|
-
};
|
|
302
|
-
};
|
|
303
|
-
const refreshAllVisibleTitles = async () => {
|
|
304
|
-
const list = await deps.client.session
|
|
305
|
-
.list({
|
|
306
|
-
query: { directory: deps.directory },
|
|
307
|
-
throwOnError: true,
|
|
308
|
-
})
|
|
309
|
-
.catch(swallow('refreshAllVisibleTitles:list'));
|
|
310
|
-
if (!list?.data || !Array.isArray(list.data)) {
|
|
311
|
-
return { attempted: 0, refreshed: 0, listFailed: true };
|
|
312
|
-
}
|
|
313
|
-
const sessions = list.data.filter((session) => Boolean(session && typeof session.id === 'string'));
|
|
314
|
-
const results = await mapConcurrent(sessions, deps.restoreConcurrency, async (session) => applyTitle(session.id));
|
|
315
|
-
return {
|
|
316
|
-
attempted: sessions.length,
|
|
317
|
-
refreshed: results.filter(Boolean).length,
|
|
318
|
-
listFailed: false,
|
|
319
|
-
};
|
|
320
|
-
};
|
|
321
282
|
return {
|
|
322
283
|
applyTitle,
|
|
323
284
|
handleSessionUpdatedTitle,
|
|
324
285
|
restoreSessionTitle,
|
|
325
|
-
restoreAllVisibleTitles,
|
|
326
|
-
refreshAllTouchedTitles,
|
|
327
|
-
refreshAllVisibleTitles,
|
|
328
286
|
forgetSession,
|
|
329
287
|
};
|
|
330
288
|
}
|
package/dist/tui.d.ts
CHANGED