opencode-subagent-magazine 1.7.1 → 1.7.3

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.
Files changed (53) hide show
  1. package/dist/_version.d.ts +1 -1
  2. package/dist/panel/entry-map.d.ts +29 -0
  3. package/dist/tui.js +166 -19
  4. package/dist/v2.js +145 -22
  5. package/package.json +17 -4
  6. package/dist/_version.js +0 -2
  7. package/dist/clipboard.js +0 -85
  8. package/dist/core/color.js +0 -68
  9. package/dist/core/format.js +0 -113
  10. package/dist/core/index.js +0 -6
  11. package/dist/core/kv.js +0 -64
  12. package/dist/core/state-machine.js +0 -27
  13. package/dist/core/types.js +0 -9
  14. package/dist/core/usage.js +0 -1
  15. package/dist/i18n.js +0 -353
  16. package/dist/index.js +0 -528
  17. package/dist/panel/SubAgentPanel.js +0 -1644
  18. package/dist/panel/entry-map.js +0 -99
  19. package/dist/panel/panel-api.js +0 -1
  20. package/dist/panel/session-routing.js +0 -4
  21. package/dist/panel/store.js +0 -5
  22. package/dist/server.js +0 -14
  23. package/dist/v2/commands.js +0 -434
  24. package/dist/v2/db.js +0 -341
  25. package/dist/v2/index.js +0 -85
  26. package/dist/v2/settings-menu.js +0 -242
  27. package/dist/v2/theme.js +0 -12
  28. package/dist/v2/types.js +0 -6
  29. package/dist/v2/v2-panel-api.js +0 -623
  30. package/src/_version.ts +0 -2
  31. package/src/clipboard.ts +0 -134
  32. package/src/core/color.ts +0 -70
  33. package/src/core/format.ts +0 -105
  34. package/src/core/index.ts +0 -6
  35. package/src/core/kv.ts +0 -78
  36. package/src/core/state-machine.ts +0 -46
  37. package/src/core/types.ts +0 -85
  38. package/src/core/usage.ts +0 -16
  39. package/src/i18n.ts +0 -365
  40. package/src/index.tsx +0 -584
  41. package/src/panel/SubAgentPanel.tsx +0 -1877
  42. package/src/panel/entry-map.ts +0 -116
  43. package/src/panel/panel-api.ts +0 -100
  44. package/src/panel/session-routing.ts +0 -10
  45. package/src/panel/store.ts +0 -8
  46. package/src/server.ts +0 -19
  47. package/src/v2/commands.ts +0 -422
  48. package/src/v2/db.ts +0 -346
  49. package/src/v2/index.tsx +0 -149
  50. package/src/v2/settings-menu.ts +0 -259
  51. package/src/v2/theme.ts +0 -14
  52. package/src/v2/types.ts +0 -167
  53. package/src/v2/v2-panel-api.ts +0 -520
package/src/clipboard.ts DELETED
@@ -1,134 +0,0 @@
1
- import { spawn } from "node:child_process"
2
- import { platform, release } from "node:os"
3
-
4
- export type ClipboardMethod =
5
- | "pbcopy"
6
- | "osascript"
7
- | "wl-copy"
8
- | "xclip"
9
- | "xsel"
10
- | "powershell"
11
- | "osc52"
12
- | "none"
13
-
14
- export interface CopyTextResult {
15
- copied: boolean
16
- method: ClipboardMethod
17
- error?: string
18
- }
19
-
20
- function runWithInput(command: string, args: string[], input: string): Promise<void> {
21
- return new Promise((resolve, reject) => {
22
- const child = spawn(command, args, {
23
- stdio: ["pipe", "ignore", "ignore"],
24
- windowsHide: true,
25
- })
26
-
27
- child.once("error", reject)
28
- child.once("close", (code) => {
29
- if (code === 0) resolve()
30
- else reject(new Error(`${command} exited with code ${code}`))
31
- })
32
-
33
- child.stdin?.end(input)
34
- })
35
- }
36
-
37
- function writeOsc52(text: string): boolean {
38
- if (!process.stdout.isTTY) return false
39
-
40
- const payload = Buffer.from(text, "utf8").toString("base64")
41
- const sequence = `\x1b]52;c;${payload}\x07`
42
- const wrapped = process.env.TMUX || process.env.STY
43
- ? `\x1bPtmux;\x1b${sequence}\x1b\\`
44
- : sequence
45
-
46
- process.stdout.write(wrapped)
47
- return true
48
- }
49
-
50
- async function tryCommand(
51
- method: ClipboardMethod,
52
- command: string,
53
- args: string[],
54
- text: string,
55
- ): Promise<CopyTextResult | undefined> {
56
- try {
57
- await runWithInput(command, args, text)
58
- return { copied: true, method }
59
- } catch {
60
- return undefined
61
- }
62
- }
63
-
64
- export async function copyText(text: string): Promise<CopyTextResult> {
65
- if (!text) return { copied: false, method: "none", error: "empty_text" }
66
-
67
- const os = platform()
68
- const isWsl = release().toLowerCase().includes("microsoft")
69
-
70
- const attempts: Array<() => Promise<CopyTextResult | undefined>> = []
71
-
72
- if (os === "darwin") {
73
- attempts.push(() => tryCommand("pbcopy", "pbcopy", [], text))
74
- attempts.push(() =>
75
- tryCommand(
76
- "osascript",
77
- "osascript",
78
- ["-e", "set the clipboard to (read (POSIX file \"/dev/stdin\") as text)"],
79
- text,
80
- ),
81
- )
82
- }
83
-
84
- if (os === "linux" && isWsl) {
85
- attempts.push(() =>
86
- tryCommand(
87
- "powershell",
88
- "powershell.exe",
89
- [
90
- "-NonInteractive",
91
- "-NoProfile",
92
- "-Command",
93
- "[Console]::InputEncoding=[Text.Encoding]::UTF8; Set-Clipboard ([Console]::In.ReadToEnd())",
94
- ],
95
- text,
96
- ),
97
- )
98
- } else if (os === "linux") {
99
- if (process.env.WAYLAND_DISPLAY) {
100
- attempts.push(() => tryCommand("wl-copy", "wl-copy", [], text))
101
- }
102
- attempts.push(() => tryCommand("xclip", "xclip", ["-selection", "clipboard"], text))
103
- attempts.push(() => tryCommand("xsel", "xsel", ["--clipboard", "--input"], text))
104
- }
105
-
106
- if (os === "win32") {
107
- attempts.push(() =>
108
- tryCommand(
109
- "powershell",
110
- "powershell.exe",
111
- [
112
- "-NonInteractive",
113
- "-NoProfile",
114
- "-Command",
115
- "[Console]::InputEncoding=[Text.Encoding]::UTF8; Set-Clipboard ([Console]::In.ReadToEnd())",
116
- ],
117
- text,
118
- ),
119
- )
120
- }
121
-
122
- for (const attempt of attempts) {
123
- const result = await attempt()
124
- if (result) return result
125
- }
126
-
127
- try {
128
- if (writeOsc52(text)) return { copied: true, method: "osc52" }
129
- } catch {
130
- // Continue to the explicit failure result.
131
- }
132
-
133
- return { copied: false, method: "none", error: "clipboard_unavailable" }
134
- }
package/src/core/color.ts DELETED
@@ -1,70 +0,0 @@
1
- /** Morandi palette helpers — desaturate any theme color toward a muted fallback. */
2
-
3
- export function rgb(raw: unknown): { r: number; g: number; b: number } | null {
4
- if (typeof raw === "string" && raw.startsWith("#")) {
5
- const h = raw.slice(1)
6
- return {
7
- r: parseInt(h.slice(0, 2), 16),
8
- g: parseInt(h.slice(2, 4), 16),
9
- b: parseInt(h.slice(4, 6), 16),
10
- }
11
- }
12
- if (raw && typeof raw === "object") {
13
- const o = raw as Record<string, unknown>
14
- if (typeof o.r === "number" && typeof o.g === "number" && typeof o.b === "number") {
15
- const scale = o.r > 1 || o.g > 1 || o.b > 1 ? 1 : 255
16
- return { r: Math.round(o.r * scale), g: Math.round(o.g * scale), b: Math.round(o.b * scale) }
17
- }
18
- }
19
- return null
20
- }
21
-
22
- export function saturation(r: number, g: number, b: number): number {
23
- const max = Math.max(r, g, b) / 255
24
- const min = Math.min(r, g, b) / 255
25
- const delta = max - min
26
- if (delta === 0) return 0
27
- const L = (max + min) / 2
28
- return L <= 0.5 ? delta / (max + min) : delta / (2 - max - min)
29
- }
30
-
31
- function desaturateTo(raw: unknown, maxSat: number, fallback: string): string {
32
- const c = rgb(raw)
33
- if (!c) return fallback
34
- const sat = saturation(c.r, c.g, c.b)
35
- if (sat <= maxSat) {
36
- return "#" + [c.r, c.g, c.b].map((v) => v.toString(16).padStart(2, "0")).join("")
37
- }
38
- const luma = c.r * 0.299 + c.g * 0.587 + c.b * 0.114
39
- let lo = 0, hi = 1
40
- for (let i = 0; i < 12; i++) {
41
- const mid = (lo + hi) / 2
42
- const nr = Math.round(c.r + (luma - c.r) * mid)
43
- const ng = Math.round(c.g + (luma - c.g) * mid)
44
- const nb = Math.round(c.b + (luma - c.b) * mid)
45
- if (saturation(nr, ng, nb) > maxSat) lo = mid
46
- else hi = mid
47
- }
48
- const nr = Math.round(c.r + (luma - c.r) * hi)
49
- const ng = Math.round(c.g + (luma - c.g) * hi)
50
- const nb = Math.round(c.b + (luma - c.b) * hi)
51
- return "#" + [nr, ng, nb].map((v) => Math.max(0, Math.min(255, v)).toString(16).padStart(2, "0")).join("")
52
- }
53
-
54
- function dimColor(hex: string, factor = 0.5): string {
55
- const c = rgb(hex)
56
- if (!c) return hex
57
- const r = Math.round(c.r * factor)
58
- const g = Math.round(c.g * factor)
59
- const b = Math.round(c.b * factor)
60
- return "#" + [r, g, b].map((v) => Math.max(0, Math.min(255, v)).toString(16).padStart(2, "0")).join("")
61
- }
62
-
63
- export const FALLBACK = {
64
- primary: "#8B9DAF", text: "#C5C5BB", muted: "#7A7A72",
65
- success: "#9CAF8B", warning: "#C5B88D", error: "#B08A8A", border: "#6B6B63",
66
- } as const
67
-
68
- export const MAX_SAT = 0.28
69
-
70
- export { desaturateTo, dimColor }
@@ -1,105 +0,0 @@
1
- import type { TimeFormat } from "./types"
2
-
3
- /** Visual width of a single character (CJK/wide chars count as 2). */
4
- function charColumns(c: string): number {
5
- const code = c.codePointAt(0) ?? 0
6
- if (code < 0x20) return 0
7
- if (code < 0x7f) return 1
8
- if (code < 0xa0) return 0
9
- if (
10
- (code >= 0x1100 && code <= 0x115f) ||
11
- (code >= 0x2e80 && code <= 0xa4cf) ||
12
- (code >= 0xac00 && code <= 0xd7a3) ||
13
- (code >= 0xf900 && code <= 0xfaff) ||
14
- (code >= 0xfe10 && code <= 0xfe6f) ||
15
- (code >= 0xff01 && code <= 0xff60) ||
16
- (code >= 0xffe0 && code <= 0xffe6) ||
17
- (code >= 0x1f300 && code <= 0x1f64f) ||
18
- (code >= 0x20000 && code <= 0x3fffd)
19
- )
20
- return 2
21
- return 1
22
- }
23
-
24
- export function visualWidth(s: string): number {
25
- let w = 0
26
- for (const c of s) w += charColumns(c)
27
- return w
28
- }
29
-
30
- export function truncate(text: string, maxCols: number): string {
31
- if (visualWidth(text) <= maxCols) return text
32
- let cols = 0
33
- let i = 0
34
- for (const c of text) {
35
- const w = charColumns(c)
36
- if (cols + w > maxCols - 1) break
37
- cols += w
38
- i += c.length
39
- }
40
- return text.slice(0, i) + "\u2026"
41
- }
42
-
43
- export function fmtDurationShort(ms: number, running: boolean): string {
44
- if (running && ms < 2000) return ""
45
- if (ms < 1000) return (ms / 1000).toFixed(2) + "s"
46
- if (ms < 60000) return (ms / 1000).toFixed(2) + "s"
47
- const m = Math.floor(ms / 60000)
48
- const s = Math.round((ms % 60000) / 1000)
49
- return `${m}m${s}s`
50
- }
51
-
52
- /** All supported elapsed-time display modes, in picker order. */
53
- export const TIME_FORMATS: readonly TimeFormat[] = ["short", "decimal", "clock", "compact", "seconds"]
54
-
55
- /** Neutral samples shown in the time-format picker (language independent). */
56
- export const TIME_FORMAT_SAMPLES: Record<TimeFormat, string> = {
57
- decimal: "45.3s · 9.5m · 1.9h",
58
- short: "45.32s · 112m35s",
59
- clock: "0:45 · 1:52:35",
60
- compact: "45s · 1h52m",
61
- seconds: "45s · 6755s",
62
- }
63
-
64
- /** Formats elapsed milliseconds using the configured display mode. */
65
- export function fmtDuration(ms: number, running: boolean, mode: TimeFormat = "short"): string {
66
- if (running && ms < 2000) return ""
67
- const t = Number.isFinite(ms) && ms > 0 ? ms : 0
68
- switch (mode) {
69
- case "short":
70
- return fmtDurationShort(t, false)
71
- case "compact": {
72
- if (t < 60000) return `${Math.floor(t / 1000)}s`
73
- if (t < 3600000) return `${Math.floor(t / 60000)}m${Math.floor((t % 60000) / 1000)}s`
74
- return `${Math.floor(t / 3600000)}h${Math.floor((t % 3600000) / 60000)}m`
75
- }
76
- case "clock": {
77
- const s = Math.floor((t % 60000) / 1000)
78
- if (t < 3600000) return `${Math.floor(t / 60000)}:${String(s).padStart(2, "0")}`
79
- const m = Math.floor((t % 3600000) / 60000)
80
- return `${Math.floor(t / 3600000)}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`
81
- }
82
- case "seconds":
83
- return `${Math.floor(t / 1000)}s`
84
- case "decimal":
85
- default: {
86
- // Promote units when rounding would show "60.0s" / "60.0m".
87
- if (t < 59950) return `${(t / 1000).toFixed(1)}s`
88
- if (t < 3597000) return `${(t / 60000).toFixed(1)}m`
89
- return `${(t / 3600000).toFixed(1)}h`
90
- }
91
- }
92
- }
93
-
94
- export function fmtTokens(n: number): string {
95
- if (n < 1000) return `${n}`
96
- if (n < 1000000) return `${(n / 1000).toFixed(1)}k`
97
- return `${(n / 1000000).toFixed(1)}M`
98
- }
99
-
100
- export function safeErrorMsg(err: unknown): string {
101
- if (!err) return ""
102
- if (typeof err === "string") return err
103
- if (typeof err === "object") return String((err as any).message || (err as any).code || "")
104
- return ""
105
- }
package/src/core/index.ts DELETED
@@ -1,6 +0,0 @@
1
- export * from "./types"
2
- export * from "./format"
3
- export * from "./color"
4
- export * from "./kv"
5
- export * from "./usage"
6
- export * from "./state-machine"
package/src/core/kv.ts DELETED
@@ -1,78 +0,0 @@
1
- import type { SessionRecord } from "./types"
2
-
3
- export const KV_PREFIX = "subagent_magazine"
4
- export const SESSION_DATA_KEY = `${KV_PREFIX}.session_data`
5
-
6
- /** Minimal KV surface used by the magazine's persistence layer. */
7
- export interface KVApi {
8
- get(key: string, fallback?: unknown): unknown
9
- set(key: string, value: unknown): void
10
- /**
11
- * Optional atomic read-modify-write. When the host exposes it (V2), the
12
- * updater runs under the host storage lock against the latest on-disk value,
13
- * which is what makes concurrent TUI instances safe: no instance can
14
- * overwrite another one's fresh writes with a stale in-memory snapshot.
15
- */
16
- update?(key: string, updater: (current: unknown) => unknown): void | Promise<void>
17
- }
18
-
19
- /** Setting keys shared by V1/V2 (values are raw strings in KV). */
20
- export const SETTING_KEYS = {
21
- lang: `${KV_PREFIX}.lang`,
22
- maxEntries: `${KV_PREFIX}.max_entries`,
23
- order: `${KV_PREFIX}.order`,
24
- scrollMode: `${KV_PREFIX}.scroll_mode`,
25
- open: `${KV_PREFIX}.open`,
26
- ttlDays: `${KV_PREFIX}.ttl_days`,
27
- border: `${KV_PREFIX}.border`,
28
- showEntryCost: `${KV_PREFIX}.show_entry_cost`,
29
- showEntryTime: `${KV_PREFIX}.show_entry_time`,
30
- showEntryTokens: `${KV_PREFIX}.show_entry_tokens`,
31
- timeFormat: `${KV_PREFIX}.time_format`,
32
- showOrigin: `${KV_PREFIX}.show_origin`,
33
- dbSync: `${KV_PREFIX}.db_sync`,
34
- } as const
35
-
36
- export function loadSessionData(kv: KVApi): Record<string, SessionRecord> {
37
- try {
38
- const raw = kv.get(SESSION_DATA_KEY, "{}")
39
- return JSON.parse(String(raw))
40
- } catch { return {} }
41
- }
42
-
43
- /**
44
- * Atomic read-merge-write of the session store.
45
- *
46
- * `mutator` receives the latest persisted map (read under the host lock when
47
- * `kv.update` is available) and may mutate it freely; the result is written
48
- * back. Every writer must go through here: writing a whole in-memory snapshot
49
- * directly would silently drop entries created by the other TUIs.
50
- */
51
- export function updateSessionData(
52
- kv: KVApi,
53
- mutator: (data: Record<string, SessionRecord>) => void,
54
- ): void | Promise<void> {
55
- try {
56
- if (typeof kv.update === "function") {
57
- return kv.update(SESSION_DATA_KEY, (raw) => {
58
- const data = parseSessionData(raw)
59
- mutator(data)
60
- return JSON.stringify(data)
61
- })
62
- }
63
- // Fallback (V1 hosts without atomic update): read-modify-write. The window
64
- // is small and the mutation is merged against the value we just read.
65
- const data = loadSessionData(kv)
66
- mutator(data)
67
- kv.set(SESSION_DATA_KEY, JSON.stringify(data))
68
- } catch {}
69
- }
70
-
71
- function parseSessionData(raw: unknown): Record<string, SessionRecord> {
72
- try { return JSON.parse(String(raw ?? "{}")) } catch { return {} }
73
- }
74
-
75
- export function readTTLDays(kv: KVApi): number {
76
- const ttlDaysRaw = parseInt(String(kv.get(SETTING_KEYS.ttlDays, "3")), 10)
77
- return Number.isNaN(ttlDaysRaw) ? 3 : ttlDaysRaw
78
- }
@@ -1,46 +0,0 @@
1
- import type { SubEntry, SubStatus } from "./types"
2
- import type { TodoStats } from "./usage"
3
-
4
- /** Final status decision when a child session settles (V1 `session.idle` semantics). */
5
- export function settleOnIdle(entry: SubEntry): SubStatus {
6
- if (entry.status === "cancel_requested" && entry.abortAccepted) return "cancelled"
7
- return "done"
8
- }
9
-
10
- /** Agent-name normalization used by the fallback matching chain. */
11
- export function normalizeAgent(s: string): string {
12
- return s.toLowerCase().replace(/[^a-z0-9-]/g, "")
13
- }
14
-
15
- /** Snapshot of usage values captured when a child session settles. */
16
- export interface SettleSnapshot {
17
- tokens?: number
18
- cost?: number
19
- model?: string
20
- todo?: TodoStats
21
- }
22
-
23
- /** Apply a settled status + usage backfill onto an entry (no-op preserving already-settled). */
24
- export function settleEntry(
25
- entry: SubEntry,
26
- targetStatus: SubStatus,
27
- nowTs: number,
28
- snap: SettleSnapshot,
29
- errorMsg?: string,
30
- ): SubEntry {
31
- const alreadySettled = entry.status !== "running" && entry.status !== "cancel_requested"
32
- const finalStatus = targetStatus === "error" ? "error" : settleOnIdle(entry)
33
- return {
34
- ...entry,
35
- ...(alreadySettled ? {} : { status: finalStatus, endedAt: nowTs }),
36
- tokens: entry.tokens ?? snap.tokens,
37
- cost: entry.cost ?? snap.cost,
38
- model: entry.model ?? snap.model,
39
- todoTotal: entry.todoTotal ?? snap.todo?.total,
40
- todoDone: entry.todoDone ?? snap.todo?.done,
41
- error: errorMsg || entry.error,
42
- }
43
- }
44
-
45
- /** Time-based scan heuristic: how old must a running entry be before assuming completion. */
46
- export const STALE_RUNNING_MS = 30 * 60 * 1000
package/src/core/types.ts DELETED
@@ -1,85 +0,0 @@
1
- import type { LangCode } from "../i18n"
2
-
3
- export type SubStatus = "running" | "done" | "error" | "cancel_requested" | "cancelled"
4
-
5
- export interface SubEntry {
6
- id: string
7
- title: string
8
- agent: string
9
- prompt: string
10
- error?: string
11
- tokens?: number
12
- cost?: number
13
- status: SubStatus
14
- sessionId?: string
15
- startedAt: number
16
- endedAt?: number
17
- model?: string
18
- todoTotal?: number
19
- todoDone?: number
20
- cancelRequestedAt?: number
21
- abortAccepted?: boolean
22
- cancelReason?: "manual"
23
- /** 来源:工具调用条目 / 衍生会话条目(spawn)。 */
24
- origin?: "tool" | "sub"
25
- /** 工具输入带 background/run_in_background(后台衍生会话)。 */
26
- background?: boolean
27
- }
28
-
29
- export type Lang = LangCode
30
- export type SortOrder = "desc" | "asc"
31
- export type ScrollMode = "wheel" | "click"
32
- /** How elapsed time is rendered in the sidebar (see TIME_FORMAT_SAMPLES). */
33
- export type TimeFormat = "decimal" | "short" | "clock" | "compact" | "seconds"
34
-
35
- /** OpenCode built-in tool names that spawn sub-agents or delegate tasks. */
36
- export const SUBAGENT_TOOLS = new Set(["task", "subagent", "delegate", "call_omo_agent"])
37
-
38
- /** 工具输入是否声明后台执行(V1/V2 两种字段名)。 */
39
- export function isBackgroundInput(input: unknown): boolean {
40
- if (!input || typeof input !== "object") return false
41
- const i = input as Record<string, unknown>
42
- return i.background === true || i.run_in_background === true
43
- }
44
-
45
- export interface ChildRecord {
46
- scroll: number
47
- expanded: string
48
- entries: SubEntry[]
49
- clearedIds?: string[]
50
- }
51
-
52
- export interface SessionRecord {
53
- ts: number
54
- entries: SubEntry[]
55
- scroll: number
56
- expanded: string
57
- children: Record<string, ChildRecord>
58
- clearedIds?: string[]
59
- }
60
-
61
- export interface SharedSignals {
62
- lang: () => Lang
63
- setLang: (l: Lang) => void
64
- maxEntries: () => number
65
- setMaxEntries: (n: number) => void
66
- sortOrder: () => SortOrder
67
- setSortOrder: (o: SortOrder) => void
68
- scrollMode: () => ScrollMode
69
- setScrollMode: (m: ScrollMode) => void
70
- borderVisible: () => boolean
71
- setBorderVisible: (v: boolean) => void
72
- showEntryCost: () => boolean
73
- setShowEntryCost: (v: boolean) => void
74
- showEntryTime: () => boolean
75
- setShowEntryTime: (v: boolean) => void
76
- showEntryTokens: () => boolean
77
- setShowEntryTokens: (v: boolean) => void
78
- timeFormat: () => TimeFormat
79
- setTimeFormat: (f: TimeFormat) => void
80
- showOrigin: () => boolean
81
- setShowOrigin: (v: boolean) => void
82
- dbSync: () => boolean
83
- setDbSync: (v: boolean) => void
84
- sessionId: string
85
- }
package/src/core/usage.ts DELETED
@@ -1,16 +0,0 @@
1
- export interface TodoStats {
2
- total: number
3
- done: number
4
- }
5
-
6
- /**
7
- * Data-source abstraction for per-session usage reads.
8
- * V1 and V2 provide their own implementations backed by their respective
9
- * client APIs; the state machine consumes only this surface.
10
- */
11
- export interface UsageReader {
12
- readSessionTokens(sid: string): number | undefined
13
- readSessionCost(sid: string): number | undefined
14
- readSessionModel(sid: string): string | undefined
15
- readSessionTodo(sid: string): TodoStats | undefined
16
- }