opencode-tokenwatch 0.1.0 → 0.2.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.
@@ -0,0 +1,362 @@
1
+ import { createSignal, createMemo, createEffect, For, Show, onMount, onCleanup } from "solid-js";
2
+ import { RGBA } from "@opentui/core";
3
+ import { formatTokens, formatCost, formatDuration } from "./formatter.js";
4
+ import { t, setLanguage } from "./i18n.js";
5
+ const DEFAULT_CONFIG = {
6
+ sidebar: { showPerformance: true, showPricing: true, showTokenDistribution: true, showTrend: true },
7
+ language: "auto",
8
+ };
9
+ function progressBarWidth(percent, width) {
10
+ if (percent >= 100)
11
+ return width;
12
+ return Math.floor((percent / 100) * width);
13
+ }
14
+ function progressFilled(percent, width) {
15
+ return "█".repeat(Math.max(0, progressBarWidth(percent, width)));
16
+ }
17
+ function progressRemaining(percent, width) {
18
+ return "░".repeat(Math.max(0, width - progressBarWidth(percent, width)));
19
+ }
20
+ function hitRateColor(rate) {
21
+ if (rate >= 85)
22
+ return RGBA.fromInts(76, 175, 80, 255);
23
+ if (rate >= 70)
24
+ return RGBA.fromInts(255, 193, 7, 255);
25
+ return RGBA.fromInts(244, 67, 54, 255);
26
+ }
27
+ function estimateTokens(text) {
28
+ if (!text || text.length === 0)
29
+ return 0;
30
+ let ascii = 0, cjk = 0;
31
+ for (const c of text) {
32
+ const code = c.codePointAt(0) ?? 0;
33
+ if ((code >= 0x4E00 && code <= 0x9FFF) || (code >= 0x3040 && code <= 0x30FF) ||
34
+ (code >= 0xAC00 && code <= 0xD7A3) || (code >= 0x1100 && code <= 0x11FF) ||
35
+ (code >= 0x2E80 && code <= 0x2EFF))
36
+ cjk++;
37
+ else
38
+ ascii++;
39
+ }
40
+ const trimmed = text.trimStart();
41
+ const jsonLike = (trimmed.startsWith("{") || trimmed.startsWith("[")) && /"[^"]+"\s*:/.test(text);
42
+ const codeLike = !jsonLike && /```|^import |^export |^function |^const |^let |^var |^class |^interface |^type |^def |^fn |^pub |^use |^mod |^package /m.test(text);
43
+ const asciiPerToken = jsonLike ? 2 : codeLike ? 2.5 : 4;
44
+ return Math.max(1, Math.ceil(ascii / asciiPerToken + cjk / 1.5));
45
+ }
46
+ function loadCollapseState(api) {
47
+ try {
48
+ return api.kv?.get?.("tokenwatch-collapse") ?? { global: false, models: {}, subBlocks: {} };
49
+ }
50
+ catch {
51
+ return { global: false, models: {}, subBlocks: {} };
52
+ }
53
+ }
54
+ function saveCollapseState(api, state) {
55
+ try {
56
+ api.kv?.set?.("tokenwatch-collapse", state);
57
+ }
58
+ catch { /* non-critical */ }
59
+ }
60
+ export function loadConfig(api) {
61
+ const base = { sidebar: { ...DEFAULT_CONFIG.sidebar }, language: DEFAULT_CONFIG.language };
62
+ try {
63
+ const pluginCfg = api.config?.pluginConfig?.["opencode-tokenwatch"];
64
+ if (pluginCfg?.sidebar)
65
+ Object.assign(base.sidebar, pluginCfg.sidebar);
66
+ if (pluginCfg?.language)
67
+ base.language = pluginCfg.language;
68
+ const overrides = api.kv?.get?.("tokenwatch-config");
69
+ if (overrides?.sidebar)
70
+ Object.assign(base.sidebar, overrides.sidebar);
71
+ if (overrides?.language)
72
+ base.language = overrides.language;
73
+ }
74
+ catch { /* defaults */ }
75
+ return base;
76
+ }
77
+ export function TokenWatchPanel(props) {
78
+ const { api, theme, perfTracker } = props;
79
+ const getMessages = () => props.messages;
80
+ const [config, setConfig] = createSignal(loadConfig(api));
81
+ let knownCfgVer = api.kv?.get?.("tokenwatch-config-version");
82
+ createEffect(() => {
83
+ const timer = setInterval(() => {
84
+ const v = api.kv?.get?.("tokenwatch-config-version");
85
+ if (v != null && v !== knownCfgVer) {
86
+ knownCfgVer = v;
87
+ setConfig(loadConfig(api));
88
+ }
89
+ }, 500);
90
+ onCleanup(() => clearInterval(timer));
91
+ });
92
+ const [collapse, setCollapse] = createSignal(loadCollapseState(api));
93
+ const [panelWidth, setPanelWidth] = createSignal(40);
94
+ createEffect(() => setLanguage(config().language));
95
+ const primaryColor = () => theme.current.primary;
96
+ const mutedColor = () => theme.current.textMuted;
97
+ const textColor = () => theme.current.text;
98
+ const modelStats = createMemo(() => {
99
+ const map = new Map();
100
+ for (const msg of props.allTokenMessages) {
101
+ const key = `${msg.providerID}/${msg.modelID}`;
102
+ let e = map.get(key);
103
+ if (!e) {
104
+ e = { providerID: msg.providerID, modelID: msg.modelID, totalInput: 0, totalOutput: 0, cacheRead: 0, cacheWrite: 0, totalCost: 0, requestCount: 0 };
105
+ map.set(key, e);
106
+ }
107
+ e.totalInput += msg.inputTokens;
108
+ e.totalOutput += msg.outputTokens;
109
+ e.cacheRead += msg.cacheRead;
110
+ e.cacheWrite += msg.cacheWrite;
111
+ e.totalCost += msg.cost;
112
+ e.requestCount++;
113
+ }
114
+ return Array.from(map.entries()).sort((a, b) => (b[1].totalInput + b[1].totalOutput) - (a[1].totalInput + a[1].totalOutput));
115
+ });
116
+ const sessionTotals = createMemo(() => {
117
+ let i = 0, o = 0, cr = 0, cw = 0, r = 0, c = 0;
118
+ for (const [, s] of modelStats()) {
119
+ i += s.totalInput;
120
+ o += s.totalOutput;
121
+ cr += s.cacheRead;
122
+ cw += s.cacheWrite;
123
+ r += s.requestCount;
124
+ c += s.totalCost;
125
+ }
126
+ return { totalInput: i, totalOutput: o, totalCacheRead: cr, totalCacheWrite: cw, totalRequests: r, totalCost: c, totalTokens: i + o + cr + cw };
127
+ });
128
+ const modelHitRate = createMemo(() => {
129
+ return modelStats().map(([key, stat]) => {
130
+ const denom = stat.totalInput + stat.cacheRead;
131
+ if (denom === 0)
132
+ return { key, rate: 0, msgs: [] };
133
+ const msgs = [];
134
+ for (const msg of props.allTokenMessages) {
135
+ const pk = `${msg.providerID}/${msg.modelID}`;
136
+ if (pk !== key)
137
+ continue;
138
+ msgs.push(msg);
139
+ }
140
+ return { key, rate: (stat.cacheRead / denom) * 100, msgs };
141
+ });
142
+ });
143
+ const modelTrend = createMemo(() => {
144
+ return modelHitRate().map(({ key, msgs }) => {
145
+ if (msgs.length < 6)
146
+ return { key, trend: null };
147
+ const sumSlice = (start, end) => {
148
+ let sumCache = 0, sumTotal = 0;
149
+ for (let i = start; i < end && i < msgs.length; i++) {
150
+ const input = msgs[i].inputTokens;
151
+ const cache = msgs[i].cacheRead;
152
+ sumCache += cache;
153
+ sumTotal += input + cache;
154
+ }
155
+ return { sumCache, sumTotal };
156
+ };
157
+ const n = msgs.length;
158
+ const recent = sumSlice(n - 3, n);
159
+ const prev = sumSlice(n - 6, n - 3);
160
+ const rateRecent = recent.sumTotal > 0 ? (recent.sumCache / recent.sumTotal) * 100 : 0;
161
+ const ratePrev = prev.sumTotal > 0 ? (prev.sumCache / prev.sumTotal) * 100 : 0;
162
+ return { key, trend: rateRecent - ratePrev };
163
+ });
164
+ });
165
+ const [partVersion, setPartVersion] = createSignal(0);
166
+ const tokenDistribution = createMemo(() => {
167
+ void partVersion();
168
+ const dist = {};
169
+ try {
170
+ const cfg = api.state.config;
171
+ const agents = cfg?.agent;
172
+ if (agents) {
173
+ for (const ac of Object.values(agents)) {
174
+ const a = ac;
175
+ if (typeof a?.prompt === "string" && a.prompt) {
176
+ dist.system = estimateTokens(a.prompt);
177
+ break;
178
+ }
179
+ }
180
+ }
181
+ }
182
+ catch { }
183
+ for (const msg of getMessages()) {
184
+ const role = msg.role;
185
+ if (role === "user") {
186
+ if (msg.system)
187
+ dist.system = (dist.system ?? 0) + estimateTokens(msg.system);
188
+ let parts = [];
189
+ try {
190
+ parts = api.state.part(msg.id);
191
+ }
192
+ catch {
193
+ continue;
194
+ }
195
+ for (const p of parts) {
196
+ if (p.type === "text" && !p.synthetic && !p.ignored) {
197
+ dist.user = (dist.user ?? 0) + estimateTokens(p.text ?? "");
198
+ }
199
+ else if (p.type === "file" && p.source?.text?.value) {
200
+ dist.user = (dist.user ?? 0) + estimateTokens(p.source.text.value);
201
+ }
202
+ }
203
+ }
204
+ else if (role === "assistant") {
205
+ let parts = [];
206
+ try {
207
+ parts = api.state.part(msg.id);
208
+ }
209
+ catch {
210
+ continue;
211
+ }
212
+ for (const p of parts) {
213
+ if (p.type === "tool") {
214
+ let rawInput = "";
215
+ try {
216
+ rawInput = p.state?.raw ?? JSON.stringify(p.state?.input);
217
+ }
218
+ catch {
219
+ try {
220
+ rawInput = JSON.stringify(p.state);
221
+ }
222
+ catch { }
223
+ }
224
+ if (rawInput)
225
+ dist.toolCall = (dist.toolCall ?? 0) + estimateTokens(rawInput);
226
+ if (p.state?.status === "completed" && p.state?.output) {
227
+ dist.toolResult = (dist.toolResult ?? 0) + estimateTokens(p.state.output);
228
+ }
229
+ else if (p.state?.status === "error" && p.state?.error) {
230
+ dist.toolResult = (dist.toolResult ?? 0) + estimateTokens(p.state.error);
231
+ }
232
+ }
233
+ else if (p.type === "reasoning") {
234
+ dist.agent = (dist.agent ?? 0) + estimateTokens(p.text ?? "");
235
+ }
236
+ else if (p.type === "subtask") {
237
+ dist.agent = (dist.agent ?? 0) + estimateTokens(p.prompt || p.description || "");
238
+ }
239
+ }
240
+ const tokens = msg.tokens;
241
+ if (tokens?.output)
242
+ dist.output = (dist.output ?? 0) + tokens.output;
243
+ }
244
+ }
245
+ return dist;
246
+ });
247
+ const toggle = {
248
+ global: () => setCollapse(p => { const n = { ...p, global: !p.global }; saveCollapseState(api, n); return n; }),
249
+ model: (k) => setCollapse(p => { const n = { ...p, models: { ...p.models, [k]: !p.models[k] } }; saveCollapseState(api, n); return n; }),
250
+ sub: (k) => setCollapse(p => { const n = { ...p, subBlocks: { ...p.subBlocks, [k]: !p.subBlocks[k] } }; saveCollapseState(api, n); return n; }),
251
+ };
252
+ onMount(() => {
253
+ const unsubPart = api.event?.on?.("message.part.updated", () => setPartVersion(v => v + 1));
254
+ const unsubMsg = api.event?.on?.("message.updated", () => setPartVersion(v => v + 1));
255
+ onCleanup(() => { try {
256
+ unsubPart?.();
257
+ unsubMsg?.();
258
+ }
259
+ catch { } });
260
+ });
261
+ return (<box flexDirection="column" width={panelWidth()}>
262
+ <box onMouseDown={toggle.global}>
263
+ <text fg={primaryColor()}>
264
+ {collapse().global ? "▶" : "▼"} {t("panelTitle")}
265
+ {collapse().global ? ` ${t("cacheRead")}:${formatTokens(sessionTotals().totalCacheRead)} ${t("requests")}:${sessionTotals().totalRequests}` : ""}
266
+ </text>
267
+ </box>
268
+
269
+ <Show when={!collapse().global}>
270
+ <text fg={mutedColor()}>
271
+ {t("total")}:{formatTokens(sessionTotals().totalTokens)} {t("requests")}:{sessionTotals().totalRequests}
272
+ </text>
273
+ <text fg={mutedColor()}>
274
+ {t("input")}:{formatTokens(sessionTotals().totalInput)} {t("output")}:{formatTokens(sessionTotals().totalOutput)} {t("cacheRead")}:{formatTokens(sessionTotals().totalCacheRead)}
275
+ </text>
276
+ <Show when={sessionTotals().totalCost > 0}>
277
+ <text fg={mutedColor()}>
278
+ {t("cost")}:{formatCost(sessionTotals().totalCost)}
279
+ </text>
280
+ </Show>
281
+
282
+ <For each={modelStats()}>
283
+ {([key, stat]) => {
284
+ const modelCollapsed = () => collapse().models[key] !== true;
285
+ const totalInput = stat.totalInput + stat.cacheRead;
286
+ const hitRate = totalInput > 0 ? (stat.cacheRead / totalInput) * 100 : 0;
287
+ const trendData = modelTrend().find(h => h.key === key);
288
+ const trendStr = trendData?.trend !== null && trendData?.trend !== undefined && trendData.trend !== 0
289
+ ? (trendData.trend >= 0 ? `${t("trendUp")}${trendData.trend.toFixed(1)}%` : `${t("trendDown")}${Math.abs(trendData.trend).toFixed(1)}%`)
290
+ : "";
291
+ const title = `${stat.providerID}/${stat.modelID}`;
292
+ const shortTitle = title.length > 32 ? title.slice(0, 30) + "…" : title;
293
+ return (<box flexDirection="column">
294
+ <box onMouseDown={() => toggle.model(key)}>
295
+ <text fg={primaryColor()}>{modelCollapsed() ? "▼" : "▶"} {shortTitle}</text>
296
+ </box>
297
+ <text fg={mutedColor()}>
298
+ {t("total")}:{formatTokens(stat.totalInput + stat.totalOutput + stat.cacheRead + stat.cacheWrite)} {t("requests")}:{stat.requestCount}
299
+ </text>
300
+ <text fg={mutedColor()}>
301
+ {t("input")}:{formatTokens(stat.totalInput)} {t("output")}:{formatTokens(stat.totalOutput)}
302
+ </text>
303
+ <text fg={mutedColor()}>
304
+ {t("cache")}:{formatTokens(stat.cacheRead + stat.cacheWrite)}(
305
+ <span style={{ fg: hitRateColor(hitRate) }}>
306
+ {progressFilled(hitRate, Math.max(3, Math.floor(panelWidth() / 3)))}
307
+ {progressRemaining(hitRate, Math.max(3, Math.floor(panelWidth() / 3)))}
308
+ {" "}{hitRate.toFixed(0)}%
309
+ </span>
310
+ {trendStr ? ` ${trendStr}` : ""})
311
+ </text>
312
+ <Show when={stat.totalCost > 0}>
313
+ <text fg={mutedColor()}>
314
+ {t("cost")}:{formatCost(stat.totalCost)}
315
+ </text>
316
+ </Show>
317
+
318
+ <Show when={modelCollapsed()}>
319
+ <Show when={config().sidebar.showPerformance}>
320
+ <box flexDirection="column">
321
+ <box onMouseDown={() => toggle.sub(`perf-${key}`)}>
322
+ <text fg={textColor()}>{!collapse().subBlocks[`perf-${key}`] ? "▼" : "▶"} ── {t("performance")} ──</text>
323
+ </box>
324
+ <Show when={!collapse().subBlocks[`perf-${key}`]}>
325
+ <text fg={mutedColor()}>
326
+ {t("ttft")}:{formatDuration(perfTracker.getSessionStats().models[key]?.avgTTFT ?? null)} {t("tps")}:{perfTracker.getSessionStats().models[key]?.avgTPS?.toFixed(1) ?? "—"} {t("latency")}:{formatDuration(perfTracker.getSessionStats().models[key]?.avgLatency ?? null)}
327
+ </text>
328
+ </Show>
329
+ </box>
330
+ </Show>
331
+
332
+ <Show when={config().sidebar.showPricing && stat.totalCost > 0}>
333
+ <box flexDirection="column">
334
+ <box onMouseDown={() => toggle.sub(`pricing-${key}`)}>
335
+ <text fg={textColor()}>{!collapse().subBlocks[`pricing-${key}`] ? "▼" : "▶"} ── {t("pricing")} ──</text>
336
+ </box>
337
+ <Show when={!collapse().subBlocks[`pricing-${key}`]}>
338
+ <text fg={mutedColor()}> {t("cost")}:{formatCost(stat.totalCost)}</text>
339
+ <text fg={mutedColor()}> {t("modelLabel")}:{stat.providerID}/{stat.modelID}</text>
340
+ </Show>
341
+ </box>
342
+ </Show>
343
+ </Show>
344
+ </box>);
345
+ }}
346
+ </For>
347
+
348
+ <Show when={config().sidebar.showTokenDistribution}>
349
+ <box flexDirection="column">
350
+ <box onMouseDown={() => toggle.sub("token-dist")}>
351
+ <text fg={primaryColor()}>{!collapse().subBlocks["token-dist"] ? "▼" : "▶"} ── {t("tokenDistribution")} ──</text>
352
+ </box>
353
+ <Show when={!collapse().subBlocks["token-dist"]}>
354
+ <For each={Object.entries(tokenDistribution())}>
355
+ {([role, tokens]) => (<text fg={mutedColor()}> {t(role)}:{formatTokens(tokens)}</text>)}
356
+ </For>
357
+ </Show>
358
+ </box>
359
+ </Show>
360
+ </Show>
361
+ </box>);
362
+ }
package/dist/tui.d.ts CHANGED
@@ -1,3 +1,17 @@
1
1
  import type { TuiPluginModule } from "@opencode-ai/plugin/tui";
2
- declare const plugin: TuiPluginModule;
2
+ export interface TokenMessage {
3
+ id: string;
4
+ sessionID: string;
5
+ providerID: string;
6
+ modelID: string;
7
+ inputTokens: number;
8
+ outputTokens: number;
9
+ reasoningTokens: number;
10
+ cacheRead: number;
11
+ cacheWrite: number;
12
+ cost: number;
13
+ }
14
+ declare const plugin: TuiPluginModule & {
15
+ id: string;
16
+ };
3
17
  export default plugin;
package/dist/tui.jsx ADDED
@@ -0,0 +1,120 @@
1
+ import { createSignal } from "solid-js";
2
+ import { registerCommands } from "./commands.jsx";
3
+ import { createPerfTracker } from "./perf-tracker.js";
4
+ import { TokenWatchPanel } from "./sidebar.jsx";
5
+ function kvKey(sessionID) {
6
+ return "tokenwatch-msgs-" + sessionID;
7
+ }
8
+ const tui = async (api) => {
9
+ const perfTracker = createPerfTracker();
10
+ const [sidebarRevision, setSidebarRevision] = createSignal(0);
11
+ const [allTokenMessages, setAllTokenMessages] = createSignal([]);
12
+ let currentSlotSessionID = "";
13
+ const cleanups = [];
14
+ registerCommands(api);
15
+ function persistToKv(sessionID, msgs) {
16
+ try {
17
+ api.kv?.set?.(kvKey(sessionID), msgs);
18
+ }
19
+ catch { /* non-critical */ }
20
+ }
21
+ const unsubMsgUpdated = api.event.on("message.updated", (event) => {
22
+ const info = event.properties?.info;
23
+ perfTracker.handleMessageUpdated(event);
24
+ if (info?.role === "assistant" && info?.tokens?.total > 0) {
25
+ setAllTokenMessages(prev => {
26
+ const msg = {
27
+ id: info.id,
28
+ sessionID: info.sessionID ?? "",
29
+ providerID: info.providerID ?? "unknown",
30
+ modelID: info.modelID ?? "unknown",
31
+ inputTokens: info.tokens?.input ?? 0,
32
+ outputTokens: info.tokens?.output ?? 0,
33
+ reasoningTokens: info.tokens?.reasoning ?? 0,
34
+ cacheRead: info.tokens?.cache?.read ?? 0,
35
+ cacheWrite: info.tokens?.cache?.write ?? 0,
36
+ cost: info.cost ?? 0,
37
+ };
38
+ const idx = prev.findIndex(m => m.id === msg.id);
39
+ let next;
40
+ if (idx >= 0) {
41
+ next = [...prev];
42
+ next[idx] = msg;
43
+ }
44
+ else {
45
+ next = [...prev, msg];
46
+ }
47
+ persistToKv(currentSlotSessionID, next);
48
+ return next;
49
+ });
50
+ }
51
+ setSidebarRevision((v) => v + 1);
52
+ });
53
+ cleanups.push(unsubMsgUpdated);
54
+ const unsubPartUpdated = api.event.on("message.part.updated", (event) => {
55
+ perfTracker.handlePartUpdated({
56
+ message_id: event.properties?.part?.messageID,
57
+ type: event.properties?.part?.type,
58
+ text: event.properties?.part?.type === "text" ? event.properties?.part?.text : undefined,
59
+ time: { start: event.properties?.part?.time?.start },
60
+ });
61
+ });
62
+ cleanups.push(unsubPartUpdated);
63
+ const unsubRemoved = api.event.on("message.removed", () => {
64
+ setSidebarRevision((v) => v + 1);
65
+ });
66
+ cleanups.push(unsubRemoved);
67
+ api.lifecycle?.onDispose?.(() => {
68
+ for (const cleanup of cleanups)
69
+ cleanup();
70
+ });
71
+ api.slots.register({
72
+ order: 50,
73
+ slots: {
74
+ sidebar_content: (_ctx, { session_id }) => {
75
+ sidebarRevision();
76
+ if (session_id && session_id !== currentSlotSessionID) {
77
+ currentSlotSessionID = session_id;
78
+ perfTracker.reset();
79
+ let loaded = [];
80
+ try {
81
+ const saved = api.kv?.get?.(kvKey(session_id));
82
+ if (saved && saved.length > 0)
83
+ loaded = saved;
84
+ }
85
+ catch { }
86
+ if (loaded.length === 0) {
87
+ const existing = api.state.session.messages(session_id);
88
+ for (const msg of existing) {
89
+ if (msg.role !== "assistant")
90
+ continue;
91
+ const tokens = msg.tokens;
92
+ if (!tokens)
93
+ continue;
94
+ loaded.push({
95
+ id: msg.id,
96
+ sessionID: session_id,
97
+ providerID: msg.providerID ?? "unknown",
98
+ modelID: msg.modelID ?? "unknown",
99
+ inputTokens: tokens?.input ?? 0,
100
+ outputTokens: tokens?.output ?? 0,
101
+ reasoningTokens: tokens?.reasoning ?? 0,
102
+ cacheRead: tokens?.cache?.read ?? 0,
103
+ cacheWrite: tokens?.cache?.write ?? 0,
104
+ cost: msg.cost ?? 0,
105
+ });
106
+ }
107
+ }
108
+ setAllTokenMessages(loaded);
109
+ }
110
+ const messages = api.state.session.messages(session_id);
111
+ return <TokenWatchPanel api={api} theme={api.theme} perfTracker={perfTracker} messages={messages} allTokenMessages={allTokenMessages()}/>;
112
+ },
113
+ },
114
+ });
115
+ };
116
+ const plugin = {
117
+ id: "opencode-tokenwatch",
118
+ tui,
119
+ };
120
+ export default plugin;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "opencode-tokenwatch",
3
- "version": "0.1.0",
4
- "description": "Token usage analytics plugin for OpenCode with live sidebar stats and /usage reports",
3
+ "version": "0.2.0",
4
+ "description": "Real-time token usage, cache analytics & performance dashboard plugin for OpenCode CLI",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
@@ -12,7 +12,7 @@
12
12
  },
13
13
  "./tui": {
14
14
  "types": "./dist/tui.d.ts",
15
- "import": "./dist/tui.js"
15
+ "import": "./dist/tui.jsx"
16
16
  },
17
17
  "./package.json": "./package.json"
18
18
  },