@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.
@@ -1,6 +1,6 @@
1
- import os from 'node:os';
2
- import path from 'node:path';
3
- import { isDateKey } from './storage_dates.js';
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), 'opencode');
21
- return path.join(os.homedir(), '.local', 'share', 'opencode');
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), 'opencode');
35
- return path.join(os.homedir(), '.config', 'opencode');
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, 'quota-sidebar.state.json');
53
+ return path.join(dataDir, "quota-sidebar.state.json");
39
54
  }
40
55
  export function authFilePath(dataDir) {
41
- return path.join(dataDir, 'auth.json');
56
+ return path.join(dataDir, "auth.json");
42
57
  }
43
58
  export function chunkRootPathFromStateFile(statePath) {
44
- return path.join(path.dirname(statePath), 'quota-sidebar-sessions');
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
  }
@@ -1,11 +1,11 @@
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';
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['client'];
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
  };
@@ -1,8 +1,8 @@
1
- import { canonicalizeTitle, canonicalizeTitleForCompare, looksDecorated, normalizeBaseTitle, } from './title.js';
2
- import { toCachedSessionUsage } from './usage.js';
3
- import { swallow, debug, mapConcurrent } from './helpers.js';
4
- import { resolveTitleView, selectDesktopCompactProviderIDs, } from './format.js';
5
- import { collapseQuotaSnapshots } from './quota_render.js';
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('applyTitle:getSession'));
42
+ .catch(swallow("applyTitle:getSession"));
43
43
  if (!session)
44
44
  return false;
45
45
  if (!session.data ||
46
- typeof session.data.title !== 'string' ||
46
+ typeof session.data.title !== "string" ||
47
47
  !session.data.time ||
48
- typeof session.data.time.created !== 'number') {
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 restoreAllVisibleTitles still works.
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 === 'compact'
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('applyTitle:update'));
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('restoreSessionTitle:get'));
239
+ .catch(swallow("restoreSessionTitle:get"));
239
240
  if (!session)
240
241
  return false;
241
242
  if (!session.data ||
242
- typeof session.data.title !== 'string' ||
243
+ typeof session.data.title !== "string" ||
243
244
  !session.data.time ||
244
- typeof session.data.time.created !== 'number') {
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) || 'Session';
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('restoreSessionTitle:update'));
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
@@ -1,5 +1,5 @@
1
1
  /** @jsxImportSource @opentui/solid */
2
- import type { TuiPluginModule } from '@opencode-ai/plugin/tui';
2
+ import type { TuiPluginModule } from "@opencode-ai/plugin/tui";
3
3
  declare const plugin: TuiPluginModule & {
4
4
  id: string;
5
5
  };