opencode-token-tracker 1.7.0 → 1.8.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 +61 -7
- package/README.zh-CN.md +59 -7
- package/dist/bin/opencode-tokens.js +593 -75
- package/dist/index.js +94 -41
- package/dist/lib/shared.d.ts +110 -3
- package/dist/lib/shared.js +288 -54
- package/package.json +5 -2
package/dist/index.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import { DEFAULT_CONFIG, formatCost, formatTokens, getStartOfDay, getStartOfWeek,
|
|
2
|
-
import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync, openSync, readSync, closeSync } from "fs";
|
|
1
|
+
import { DEFAULT_CONFIG, aggregateRootSession, buildMessageToast, buildSessionRecord, calculateCost, evaluateBudgetStatus, formatCost, formatTokens, getStartOfDay, getStartOfMonth, getStartOfWeek, hasBillableTokenUsage, validateConfig, } from "./lib/shared.js";
|
|
2
|
+
import { appendFileSync, createReadStream, existsSync, mkdirSync, readFileSync, statSync, openSync, readSync, closeSync } from "fs";
|
|
3
3
|
import { open } from "fs/promises";
|
|
4
4
|
import { join } from "path";
|
|
5
5
|
import { homedir } from "os";
|
|
6
|
+
import { createInterface } from "node:readline";
|
|
6
7
|
const CONFIG_DIR = join(homedir(), ".config", "opencode");
|
|
7
8
|
const CONFIG_FILE = join(CONFIG_DIR, "token-tracker.json");
|
|
8
9
|
const LOG_DIR = join(CONFIG_DIR, "logs", "token-tracker");
|
|
9
10
|
const LOG_FILE = join(LOG_DIR, "tokens.jsonl");
|
|
11
|
+
const SESSIONS_LOG_FILE = join(LOG_DIR, "sessions.jsonl");
|
|
10
12
|
// ============================================================================
|
|
11
13
|
// Configuration
|
|
12
14
|
// ============================================================================
|
|
@@ -51,6 +53,8 @@ function ensureLatestConfig() {
|
|
|
51
53
|
}
|
|
52
54
|
}
|
|
53
55
|
const sessionStats = new Map();
|
|
56
|
+
// 从侧车日志和实时会话事件学习父子关系,展示时归并至顶层任务。
|
|
57
|
+
const parentOf = new Map();
|
|
54
58
|
function getOrCreateSessionStats(sessionId) {
|
|
55
59
|
if (!sessionStats.has(sessionId)) {
|
|
56
60
|
sessionStats.set(sessionId, {
|
|
@@ -66,6 +70,31 @@ function getOrCreateSessionStats(sessionId) {
|
|
|
66
70
|
}
|
|
67
71
|
return sessionStats.get(sessionId);
|
|
68
72
|
}
|
|
73
|
+
function rememberSessionParent(info) {
|
|
74
|
+
// 仅标题更新不应抹去已有关系,与 CLI 的元数据合并口径一致。
|
|
75
|
+
if (!info.id || !info.parentID)
|
|
76
|
+
return;
|
|
77
|
+
parentOf.set(info.id, info.parentID);
|
|
78
|
+
}
|
|
79
|
+
async function restoreSessionParents() {
|
|
80
|
+
try {
|
|
81
|
+
const lines = createInterface({ input: createReadStream(SESSIONS_LOG_FILE), crlfDelay: Infinity });
|
|
82
|
+
for await (const line of lines) {
|
|
83
|
+
try {
|
|
84
|
+
const record = JSON.parse(line);
|
|
85
|
+
if (record?.type !== "session" || typeof record.sessionId !== "string" || typeof record.parentID !== "string")
|
|
86
|
+
continue;
|
|
87
|
+
rememberSessionParent({ id: record.sessionId, parentID: record.parentID });
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
// 跳过损坏行,后续有效记录仍可恢复关系。
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
// 无历史元数据或读取失败时,继续通过实时会话事件学习关系。
|
|
96
|
+
}
|
|
97
|
+
}
|
|
69
98
|
// ============================================================================
|
|
70
99
|
// Deduplication
|
|
71
100
|
// ============================================================================
|
|
@@ -89,11 +118,32 @@ function ensureLogDir() {
|
|
|
89
118
|
mkdirSync(LOG_DIR, { recursive: true });
|
|
90
119
|
}
|
|
91
120
|
}
|
|
92
|
-
function logJson(data) {
|
|
121
|
+
function logJson(data, recordedAt) {
|
|
93
122
|
ensureLogDir();
|
|
94
|
-
const entry = JSON.stringify({ ...data, _ts:
|
|
123
|
+
const entry = JSON.stringify({ ...data, _ts: recordedAt }) + "\n";
|
|
95
124
|
appendFileSync(LOG_FILE, entry);
|
|
96
125
|
}
|
|
126
|
+
// Append-only sidecar of session metadata (id/title/parentID/directory) used
|
|
127
|
+
// by the CLI to label `--by session` rows and roll child sessions up to their
|
|
128
|
+
// parent. Per-process dedup keeps writes to actual title/parent changes only.
|
|
129
|
+
const seenSessions = new Map();
|
|
130
|
+
function logSessionMeta(info) {
|
|
131
|
+
const record = buildSessionRecord(info);
|
|
132
|
+
if (!record)
|
|
133
|
+
return;
|
|
134
|
+
const signature = `${record.title ?? ""}|${record.parentID ?? ""}|${record.directory ?? ""}`;
|
|
135
|
+
if (seenSessions.get(record.sessionId) === signature)
|
|
136
|
+
return;
|
|
137
|
+
seenSessions.set(record.sessionId, signature);
|
|
138
|
+
// Bound memory the same way the message dedup set does.
|
|
139
|
+
if (seenSessions.size > 10000) {
|
|
140
|
+
const stale = Array.from(seenSessions.keys()).slice(0, 5000);
|
|
141
|
+
for (const key of stale)
|
|
142
|
+
seenSessions.delete(key);
|
|
143
|
+
}
|
|
144
|
+
ensureLogDir();
|
|
145
|
+
appendFileSync(SESSIONS_LOG_FILE, JSON.stringify({ type: "session", ...record, _ts: Date.now() }) + "\n");
|
|
146
|
+
}
|
|
97
147
|
const budgetTracker = {
|
|
98
148
|
dailySpent: 0,
|
|
99
149
|
weeklySpent: 0,
|
|
@@ -272,17 +322,14 @@ async function initBudgetTracker() {
|
|
|
272
322
|
}
|
|
273
323
|
budgetTracker.initialized = true;
|
|
274
324
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
*/
|
|
278
|
-
function accumulateBudget(cost) {
|
|
325
|
+
// 在写入当前消息之前切换周期,避免重载结果已经包含当前消耗。
|
|
326
|
+
function refreshBudgetPeriods(recordedAt) {
|
|
279
327
|
if (!budgetTracker.initialized)
|
|
280
328
|
return;
|
|
281
|
-
const now = new Date();
|
|
329
|
+
const now = new Date(recordedAt);
|
|
282
330
|
const currentDayStart = getStartOfDay(now);
|
|
283
331
|
const currentWeekStart = getStartOfWeek(now);
|
|
284
332
|
const currentMonthStart = getStartOfMonth(now);
|
|
285
|
-
// Period rollover detection — reset and reload from file for accuracy
|
|
286
333
|
if (currentDayStart !== budgetTracker.dayStart) {
|
|
287
334
|
budgetTracker.dayStart = currentDayStart;
|
|
288
335
|
budgetTracker.dailySpent = loadCostsSince(currentDayStart);
|
|
@@ -295,6 +342,11 @@ function accumulateBudget(cost) {
|
|
|
295
342
|
budgetTracker.monthStart = currentMonthStart;
|
|
296
343
|
budgetTracker.monthlySpent = loadCostsSince(currentMonthStart);
|
|
297
344
|
}
|
|
345
|
+
}
|
|
346
|
+
// 只在日志成功写入后累计;周期判断和日志使用同一个 recordedAt。
|
|
347
|
+
function accumulateBudget(cost) {
|
|
348
|
+
if (!budgetTracker.initialized)
|
|
349
|
+
return;
|
|
298
350
|
budgetTracker.dailySpent += cost;
|
|
299
351
|
budgetTracker.weeklySpent += cost;
|
|
300
352
|
budgetTracker.monthlySpent += cost;
|
|
@@ -307,11 +359,6 @@ function checkBudgetStatus() {
|
|
|
307
359
|
};
|
|
308
360
|
return evaluateBudgetStatus(config.budget, snapshot, budgetTracker.initialized);
|
|
309
361
|
}
|
|
310
|
-
function formatBudgetMessage(status) {
|
|
311
|
-
const pct = Math.round(status.percentage * 100);
|
|
312
|
-
const periodLabel = status.period.charAt(0).toUpperCase() + status.period.slice(1);
|
|
313
|
-
return `${periodLabel}: ${formatCost(status.spent)}/${formatCost(status.limit)} (${pct}%)`;
|
|
314
|
-
}
|
|
315
362
|
export const TokenTrackerPlugin = async ({ directory, client }) => {
|
|
316
363
|
try {
|
|
317
364
|
// Load config on plugin init (with validation)
|
|
@@ -322,6 +369,7 @@ export const TokenTrackerPlugin = async ({ directory, client }) => {
|
|
|
322
369
|
}
|
|
323
370
|
// Initialize in-memory budget tracker (reads JSONL once)
|
|
324
371
|
await initBudgetTracker();
|
|
372
|
+
await restoreSessionParents();
|
|
325
373
|
// 不再写 type:"init" 标记:OpenCode 会在多个子进程(LSP、工具 runner 等)独立加载
|
|
326
374
|
// plugin,每次启动会向 JSONL 写多份重复的 init 行,污染日志且无计费价值。
|
|
327
375
|
// 跨进程去重的根因修复留待后续版本。
|
|
@@ -366,8 +414,7 @@ export const TokenTrackerPlugin = async ({ directory, client }) => {
|
|
|
366
414
|
const reasoning = info.tokens.reasoning ?? 0;
|
|
367
415
|
const cacheRead = info.tokens.cache?.read ?? 0;
|
|
368
416
|
const cacheWrite = info.tokens.cache?.write ?? 0;
|
|
369
|
-
|
|
370
|
-
if (!hasTokens)
|
|
417
|
+
if (!hasBillableTokenUsage({ input, output, cacheRead, cacheWrite }))
|
|
371
418
|
return;
|
|
372
419
|
const dedupeKey = `${messageId}-${input}-${output}-${cacheRead}-${cacheWrite}`;
|
|
373
420
|
if (isDuplicate(dedupeKey))
|
|
@@ -384,6 +431,8 @@ export const TokenTrackerPlugin = async ({ directory, client }) => {
|
|
|
384
431
|
stats.totalCacheWrite += cacheWrite;
|
|
385
432
|
stats.totalCost += cost;
|
|
386
433
|
stats.messageCount += 1;
|
|
434
|
+
const recordedAt = Date.now();
|
|
435
|
+
refreshBudgetPeriods(recordedAt);
|
|
387
436
|
// Log to file
|
|
388
437
|
logJson({
|
|
389
438
|
type: "tokens",
|
|
@@ -399,35 +448,28 @@ export const TokenTrackerPlugin = async ({ directory, client }) => {
|
|
|
399
448
|
cacheRead,
|
|
400
449
|
cacheWrite,
|
|
401
450
|
cost,
|
|
402
|
-
});
|
|
451
|
+
}, recordedAt);
|
|
403
452
|
// Accumulate cost into in-memory budget tracker
|
|
404
453
|
accumulateBudget(cost);
|
|
405
454
|
// Show toast for this message
|
|
406
455
|
if (config.toast.enabled) {
|
|
407
456
|
const totalTokens = input + output;
|
|
457
|
+
// Roll sub-agent sessions up to their top-level session so
|
|
458
|
+
// `Session:` reflects the whole task, not just this agent's slice.
|
|
459
|
+
const rootStats = aggregateRootSession(sessionStats, parentOf, sessionId);
|
|
408
460
|
// Check budget status
|
|
409
461
|
const budgetStatus = checkBudgetStatus();
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
message = formatBudgetMessage(budgetStatus);
|
|
418
|
-
variant = "error";
|
|
419
|
-
}
|
|
420
|
-
else if (budgetStatus.warning) {
|
|
421
|
-
message = `${formatCost(cost)} | ${formatBudgetMessage(budgetStatus)}`;
|
|
422
|
-
variant = "warning";
|
|
423
|
-
}
|
|
424
|
-
}
|
|
462
|
+
const toast = buildMessageToast({
|
|
463
|
+
messageTokens: totalTokens,
|
|
464
|
+
messageCost: cost,
|
|
465
|
+
sessionTokens: rootStats.totalInput + rootStats.totalOutput,
|
|
466
|
+
sessionCost: rootStats.totalCost,
|
|
467
|
+
budget: budgetStatus,
|
|
468
|
+
});
|
|
425
469
|
try {
|
|
426
470
|
await client.tui.showToast({
|
|
427
471
|
body: {
|
|
428
|
-
|
|
429
|
-
message,
|
|
430
|
-
variant,
|
|
472
|
+
...toast,
|
|
431
473
|
duration: budgetStatus?.exceeded ? 5000 : config.toast.duration,
|
|
432
474
|
},
|
|
433
475
|
});
|
|
@@ -444,16 +486,17 @@ export const TokenTrackerPlugin = async ({ directory, client }) => {
|
|
|
444
486
|
const sessionId = props?.sessionID;
|
|
445
487
|
if (!sessionId)
|
|
446
488
|
return;
|
|
447
|
-
|
|
448
|
-
|
|
489
|
+
// 主会话可能尚未产生消息,是否展示摘要应由整组消耗决定。
|
|
490
|
+
const rootStats = aggregateRootSession(sessionStats, parentOf, sessionId);
|
|
491
|
+
if (rootStats.messageCount === 0)
|
|
449
492
|
return;
|
|
450
|
-
const duration = Math.round((Date.now() -
|
|
451
|
-
const totalTokens =
|
|
493
|
+
const duration = Math.round((Date.now() - rootStats.startTime) / 1000 / 60);
|
|
494
|
+
const totalTokens = rootStats.totalInput + rootStats.totalOutput;
|
|
452
495
|
try {
|
|
453
496
|
await client.tui.showToast({
|
|
454
497
|
body: {
|
|
455
498
|
title: `Session: ${formatTokens(totalTokens)} tokens`,
|
|
456
|
-
message: `${formatCost(
|
|
499
|
+
message: `${formatCost(rootStats.totalCost)} | ${rootStats.messageCount} msgs | ${duration}min`,
|
|
457
500
|
variant: "info",
|
|
458
501
|
duration: 5000,
|
|
459
502
|
},
|
|
@@ -461,6 +504,16 @@ export const TokenTrackerPlugin = async ({ directory, client }) => {
|
|
|
461
504
|
}
|
|
462
505
|
catch { }
|
|
463
506
|
}
|
|
507
|
+
// Capture session metadata (title/parentID) for the CLI session view
|
|
508
|
+
// and the in-memory parent map used to roll sub-agent usage up to the
|
|
509
|
+
// top-level session in toasts.
|
|
510
|
+
if (event.type === "session.created" || event.type === "session.updated") {
|
|
511
|
+
const props = event.properties;
|
|
512
|
+
if (props?.info) {
|
|
513
|
+
rememberSessionParent(props.info);
|
|
514
|
+
logSessionMeta(props.info);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
464
517
|
}
|
|
465
518
|
catch { }
|
|
466
519
|
},
|
package/dist/lib/shared.d.ts
CHANGED
|
@@ -4,15 +4,32 @@ export interface ModelPricing {
|
|
|
4
4
|
cacheRead?: number;
|
|
5
5
|
cacheWrite?: number;
|
|
6
6
|
}
|
|
7
|
+
export interface BillableTokenUsage {
|
|
8
|
+
input?: number;
|
|
9
|
+
output?: number;
|
|
10
|
+
cacheRead?: number;
|
|
11
|
+
cacheWrite?: number;
|
|
12
|
+
}
|
|
7
13
|
export interface ProviderModelPricingMap {
|
|
8
14
|
[provider: string]: ModelPricing;
|
|
9
15
|
}
|
|
10
16
|
export declare const BUILTIN_PRICING_META: {
|
|
11
|
-
readonly pricingLastUpdated: "2026-
|
|
12
|
-
readonly metadataLastUpdated: "2026-05
|
|
17
|
+
readonly pricingLastUpdated: "2026-10-05";
|
|
18
|
+
readonly metadataLastUpdated: "2026-10-05";
|
|
19
|
+
readonly baselineLastUpdated: "2026-05-29";
|
|
13
20
|
readonly source: "Provider official pricing pages";
|
|
14
|
-
readonly notes: "Manually maintained
|
|
21
|
+
readonly notes: "Partial refresh: DeepSeek Flash/Pro and Kimi K2.7 Code only. Other entries retain the baseline audit date. Manually maintained; recent audits do not guarantee unchanged prices.";
|
|
15
22
|
};
|
|
23
|
+
export interface BuiltinPricingAudit {
|
|
24
|
+
reviewedAt: string;
|
|
25
|
+
expiresAt?: string;
|
|
26
|
+
basis?: "peak";
|
|
27
|
+
exactOrPrefixed?: boolean;
|
|
28
|
+
sourceUrl?: string;
|
|
29
|
+
}
|
|
30
|
+
export type PricingFreshness = "recent" | "stale" | "expired" | "unknown";
|
|
31
|
+
export declare const PRICING_STALE_AFTER_DAYS = 90;
|
|
32
|
+
export declare const BUILTIN_PRICING_AUDITS: Record<string, BuiltinPricingAudit>;
|
|
16
33
|
export declare const BUILTIN_PRICING: Record<string, ModelPricing>;
|
|
17
34
|
export declare function formatCost(cost: number): string;
|
|
18
35
|
/**
|
|
@@ -21,7 +38,9 @@ export declare function formatCost(cost: number): string;
|
|
|
21
38
|
* @param millionDecimals - decimal places for M-level values (default 1)
|
|
22
39
|
*/
|
|
23
40
|
export declare function formatTokens(tokens: number, millionDecimals?: number): string;
|
|
41
|
+
export declare function hasBillableTokenUsage(usage: BillableTokenUsage): boolean;
|
|
24
42
|
export declare function getStartOfDay(date?: Date): number;
|
|
43
|
+
export declare function formatLocalDateKey(date: Date): string;
|
|
25
44
|
export declare function getStartOfWeek(date?: Date): number;
|
|
26
45
|
export declare function getStartOfMonth(date?: Date): number;
|
|
27
46
|
export interface ToastConfig {
|
|
@@ -60,14 +79,102 @@ export interface BudgetStatus {
|
|
|
60
79
|
exceeded: boolean;
|
|
61
80
|
warning: boolean;
|
|
62
81
|
}
|
|
82
|
+
export interface MessageToastInput {
|
|
83
|
+
messageTokens: number;
|
|
84
|
+
messageCost: number;
|
|
85
|
+
sessionTokens: number;
|
|
86
|
+
sessionCost: number;
|
|
87
|
+
budget: BudgetStatus | null;
|
|
88
|
+
}
|
|
89
|
+
export interface MessageToastBody {
|
|
90
|
+
title: string;
|
|
91
|
+
message: string;
|
|
92
|
+
variant: "info" | "warning" | "error";
|
|
93
|
+
}
|
|
94
|
+
export declare function formatBudgetMessage(status: BudgetStatus): string;
|
|
95
|
+
export declare function buildMessageToast(input: MessageToastInput): MessageToastBody;
|
|
63
96
|
export interface BudgetSpentSnapshot {
|
|
64
97
|
dailySpent: number;
|
|
65
98
|
weeklySpent: number;
|
|
66
99
|
monthlySpent: number;
|
|
67
100
|
}
|
|
68
101
|
export declare function evaluateBudgetStatus(budget: BudgetConfig, spent: BudgetSpentSnapshot, initialized: boolean): BudgetStatus | null;
|
|
102
|
+
export declare function getBuiltinPricingAudit(model: string): BuiltinPricingAudit | undefined;
|
|
103
|
+
export declare function getPricingFreshness(audit: BuiltinPricingAudit, now?: number): PricingFreshness;
|
|
104
|
+
export declare function getEffectivePricingAudit(config: TrackerConfig, model: string, provider: string): BuiltinPricingAudit | undefined;
|
|
69
105
|
export declare function resolvePricingStatus(config: TrackerConfig, model: string, provider: string): "provider cfg" | "model cfg" | "built-in" | "default";
|
|
70
106
|
export declare function round2(val: number): number;
|
|
71
107
|
export type ProviderFamily = "anthropic" | "openai" | "deepseek" | "google" | "other";
|
|
72
108
|
export declare function getProviderFamily(model: string, provider: string): ProviderFamily;
|
|
73
109
|
export declare function calculateCost(model: string, provider: string, input: number, output: number, cacheRead?: number, cacheWrite?: number, config?: TrackerConfig): number;
|
|
110
|
+
export interface SessionMeta {
|
|
111
|
+
sessionId: string;
|
|
112
|
+
title?: string;
|
|
113
|
+
parentID?: string;
|
|
114
|
+
directory?: string;
|
|
115
|
+
_ts: number;
|
|
116
|
+
}
|
|
117
|
+
/** Shape of a raw line in sessions.jsonl (all fields optional/untrusted). */
|
|
118
|
+
export interface SessionMetaRecord {
|
|
119
|
+
sessionId?: string;
|
|
120
|
+
title?: string;
|
|
121
|
+
parentID?: string;
|
|
122
|
+
directory?: string;
|
|
123
|
+
_ts?: number;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Resolve a session id to its root by following parentID links.
|
|
127
|
+
* Stops when no parent is known or a cycle is detected, so an unknown
|
|
128
|
+
* parent still yields a stable root id to group descendants under.
|
|
129
|
+
*/
|
|
130
|
+
export declare function resolveRootSession(sessionId: string, parentOf: Map<string, string | undefined>): string;
|
|
131
|
+
/** Per-session usage counters, summed across a root session's whole subtree. */
|
|
132
|
+
export interface SessionAggregate {
|
|
133
|
+
totalInput: number;
|
|
134
|
+
totalOutput: number;
|
|
135
|
+
totalReasoning: number;
|
|
136
|
+
totalCacheRead: number;
|
|
137
|
+
totalCacheWrite: number;
|
|
138
|
+
totalCost: number;
|
|
139
|
+
messageCount: number;
|
|
140
|
+
startTime: number;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Sum per-session stats across every session that resolves to the same root as
|
|
144
|
+
* `currentSessionId`, rolling a sub-agent session's usage up into its top-level
|
|
145
|
+
* (parent) session for toast / idle display.
|
|
146
|
+
*
|
|
147
|
+
* Aggregation happens at read time: a `parentID` learned only after a session's
|
|
148
|
+
* first message still merges on the next call (eventual consistency), so stored
|
|
149
|
+
* per-session stats never need re-bucketing. `startTime` is the earliest among
|
|
150
|
+
* the grouped sessions so duration reflects the whole task.
|
|
151
|
+
*/
|
|
152
|
+
export declare function aggregateRootSession(sessionStats: ReadonlyMap<string, SessionAggregate>, parentOf: Map<string, string | undefined>, currentSessionId: string): SessionAggregate;
|
|
153
|
+
/**
|
|
154
|
+
* Merge raw sessions.jsonl records into one SessionMeta per session id.
|
|
155
|
+
* Later non-empty fields win; empty/whitespace titles never clobber a real
|
|
156
|
+
* one (session.created may arrive before a title is generated).
|
|
157
|
+
*/
|
|
158
|
+
export declare function mergeSessionMeta(records: SessionMetaRecord[]): Map<string, SessionMeta>;
|
|
159
|
+
/** Truncate a label to maxWidth, appending an ellipsis when it overflows. */
|
|
160
|
+
export declare function truncateLabel(str: string, maxWidth: number): string;
|
|
161
|
+
/** Distinctive short code for a session id (its tail), for fallback display. */
|
|
162
|
+
export declare function shortSessionCode(sessionId?: string): string;
|
|
163
|
+
/** Pick a human label for a (root) session: title when known, else short code. */
|
|
164
|
+
export declare function sessionDisplayLabel(rootId: string, meta: SessionMeta | undefined, maxWidth: number): string;
|
|
165
|
+
/** Subset of an OpenCode Session event payload the tracker cares about. */
|
|
166
|
+
export interface SessionInfoInput {
|
|
167
|
+
id?: string;
|
|
168
|
+
title?: string;
|
|
169
|
+
parentID?: string;
|
|
170
|
+
directory?: string;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Build a session metadata record to persist, or null when there is nothing
|
|
174
|
+
* worth recording yet (no title and no parent link). Title is trimmed; a
|
|
175
|
+
* parentID is recorded even before a title exists so child sessions can be
|
|
176
|
+
* rolled up into their parent.
|
|
177
|
+
*/
|
|
178
|
+
export declare function buildSessionRecord(info: SessionInfoInput): (SessionMetaRecord & {
|
|
179
|
+
sessionId: string;
|
|
180
|
+
}) | null;
|