opencode-visual-cache 1.6.2 → 1.7.0-beta.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.
Files changed (62) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +247 -247
  3. package/README_EN.md +247 -247
  4. package/dist/_version.d.ts +1 -1
  5. package/dist/_version.js +1 -1
  6. package/dist/core/color.d.ts +37 -0
  7. package/dist/core/color.js +108 -0
  8. package/dist/core/currency.d.ts +17 -0
  9. package/dist/core/currency.js +43 -0
  10. package/dist/core/estimate.d.ts +1 -0
  11. package/dist/core/estimate.js +40 -0
  12. package/dist/core/format.d.ts +15 -0
  13. package/dist/core/format.js +90 -0
  14. package/dist/core/index.d.ts +5 -0
  15. package/dist/core/index.js +5 -0
  16. package/dist/core/types.d.ts +17 -0
  17. package/dist/core/types.js +1 -0
  18. package/dist/index.js +12 -818
  19. package/dist/panel/TokenCachePanel.d.ts +10 -0
  20. package/dist/panel/TokenCachePanel.js +549 -0
  21. package/dist/panel/panel-api.d.ts +119 -0
  22. package/dist/panel/panel-api.js +1 -0
  23. package/dist/tui.js +223 -209
  24. package/dist/v2/commands.d.ts +10 -0
  25. package/dist/v2/commands.js +490 -0
  26. package/dist/v2/data.d.ts +27 -0
  27. package/dist/v2/data.js +103 -0
  28. package/dist/v2/index.d.ts +4 -0
  29. package/dist/v2/index.js +163 -0
  30. package/dist/v2/sidebar.d.ts +6 -0
  31. package/dist/v2/sidebar.js +36 -0
  32. package/dist/v2/status.d.ts +14 -0
  33. package/dist/v2/status.js +83 -0
  34. package/dist/v2/theme.d.ts +8 -0
  35. package/dist/v2/theme.js +16 -0
  36. package/dist/v2/types.d.ts +219 -0
  37. package/dist/v2/types.js +6 -0
  38. package/dist/v2/v2-panel-api.d.ts +8 -0
  39. package/dist/v2/v2-panel-api.js +176 -0
  40. package/dist/v2.js +2941 -0
  41. package/package.json +70 -67
  42. package/src/_version.ts +1 -1
  43. package/src/balance-providers.ts +153 -153
  44. package/src/core/color.ts +108 -0
  45. package/src/core/currency.ts +46 -0
  46. package/src/core/estimate.ts +36 -0
  47. package/src/core/format.ts +81 -0
  48. package/src/core/index.ts +5 -0
  49. package/src/core/types.ts +17 -0
  50. package/src/i18n.ts +380 -380
  51. package/src/index.tsx +1035 -2120
  52. package/src/panel/TokenCachePanel.tsx +776 -0
  53. package/src/panel/panel-api.ts +104 -0
  54. package/src/server.ts +10 -10
  55. package/src/v2/commands.ts +471 -0
  56. package/src/v2/data.ts +112 -0
  57. package/src/v2/index.tsx +184 -0
  58. package/src/v2/sidebar.tsx +69 -0
  59. package/src/v2/status.tsx +96 -0
  60. package/src/v2/theme.ts +19 -0
  61. package/src/v2/types.ts +159 -0
  62. package/src/v2/v2-panel-api.ts +177 -0
@@ -0,0 +1,163 @@
1
+ import { jsx as _jsx } from "@opentui/solid/jsx-runtime";
2
+ /** @jsxImportSource @opentui/solid */
3
+ import { createSignal, createEffect, onMount, onCleanup, untrack } from "solid-js";
4
+ import { createPanelApi } from "./v2-panel-api";
5
+ import { TokenCachePanel } from "../panel/TokenCachePanel";
6
+ import { StatusView } from "./status";
7
+ import { mapTheme } from "./theme";
8
+ import { makeCommands, findOpencodeKeyV2 } from "./commands";
9
+ import { getBalanceProvider } from "../balance-providers";
10
+ import { LANG_META, detectLang } from "../i18n";
11
+ const KV_PREFIX = "cache_panel";
12
+ const BALANCE_POLL_MS = 5 * 60 * 1000; // 5 minutes(对齐 V1)
13
+ const DEBUG_LANG = typeof process !== "undefined" ? process.env?.CACHE_TUI_LANG : undefined;
14
+ const INIT_LANG = DEBUG_LANG !== undefined && LANG_META.some((m) => m.code === DEBUG_LANG)
15
+ ? DEBUG_LANG
16
+ : detectLang();
17
+ /** V2 侧创建面板信号(实验:默认值;偏好持久化经 PanelApi.kv → storage.store)。
18
+ * 返回 PanelSignals + setBalanceState:余额轮询由 PluginRoot 驱动(V1 同构)。 */
19
+ function createPanelSignals() {
20
+ const [currencySymbol, setCurrencySymbol] = createSignal("$");
21
+ const [exchangeRate, setExchangeRate] = createSignal(1);
22
+ const [langCode, setLangCode] = createSignal(INIT_LANG);
23
+ const [sectionDetail, setSectionDetail] = createSignal(true);
24
+ const [sectionModel, setSectionModel] = createSignal(true);
25
+ const [sectionDist, setSectionDist] = createSignal(true);
26
+ const [sectionSkills, setSectionSkills] = createSignal(true);
27
+ const [sectionBalance, setSectionBalance] = createSignal(true);
28
+ const [sectionBottom, setSectionBottom] = createSignal(true);
29
+ const [balanceRefresh, setBalanceRefresh] = createSignal(0);
30
+ const [balanceProviderId, setBalanceProviderId] = createSignal("");
31
+ const [autoBalance, setAutoBalance] = createSignal(true);
32
+ const [balanceUnsupported, setBalanceUnsupported] = createSignal(false);
33
+ const [balanceState, setBalanceState] = createSignal({ status: "idle", data: null, lastFetch: 0 });
34
+ const [balanceCurrency, setBalanceCurrency] = createSignal("");
35
+ const [borderVisible, setBorderVisible] = createSignal(true);
36
+ const [overrideSessionId, setOverrideSessionId] = createSignal(undefined);
37
+ const [sidebarVisible, setSidebarVisible] = createSignal(true);
38
+ return {
39
+ currencySymbol, setCurrencySymbol,
40
+ exchangeRate, setExchangeRate,
41
+ langCode: langCode, setLangCode: setLangCode,
42
+ sectionDetail, setSectionDetail,
43
+ sectionModel, setSectionModel,
44
+ sectionDist, setSectionDist,
45
+ sectionSkills, setSectionSkills,
46
+ sectionBalance, setSectionBalance,
47
+ sectionBottom, setSectionBottom,
48
+ balanceRefresh, setBalanceRefresh,
49
+ balanceProviderId, setBalanceProviderId,
50
+ autoBalance, setAutoBalance,
51
+ balanceUnsupported, setBalanceUnsupported,
52
+ balanceState,
53
+ setBalanceState,
54
+ balanceCurrency, setBalanceCurrency,
55
+ borderVisible, setBorderVisible,
56
+ overrideSessionId, setOverrideSessionId,
57
+ sidebarVisible, setSidebarVisible,
58
+ };
59
+ }
60
+ /** 面板根组件:在组件渲染上下文注册命令 layer(keymap.layer 必须由组件调用),
61
+ * 再渲染共享 TokenCachePanel;同时驱动余额轮询(对齐 V1 tui() 的 pollBalance)。 */
62
+ function PluginRoot(props) {
63
+ // 请求序号:防止定时轮询与手动刷新并发时,慢的旧请求覆盖新结果(对齐 V1)
64
+ let balanceSeq = 0;
65
+ // 对齐官方内置插件示例(feature-plugins/system/plugins.tsx):mode 用 global
66
+ props.context.keymap.layer(() => ({
67
+ mode: "global",
68
+ commands: makeCommands(props.context, props.api, props.signals),
69
+ }));
70
+ // 语言偏好恢复(对齐 V1 tui() restoreLang:优先用户 /cache-lang 设置,覆盖自动检测)
71
+ onMount(() => {
72
+ try {
73
+ const saved = props.api.kv.get(`${KV_PREFIX}.lang`);
74
+ if (saved && LANG_META.some((m) => m.code === saved)) {
75
+ props.signals.setLangCode(saved);
76
+ }
77
+ }
78
+ catch { }
79
+ });
80
+ // ── 余额轮询(对齐 V1 tui() pollBalance):手动 key 优先,缺失时自动复用 OpenCode 已认证 key ──
81
+ const pollBalance = async () => {
82
+ const provider = getBalanceProvider(props.signals.balanceProviderId());
83
+ const key = props.api.kv.get(`${KV_PREFIX}.balance.${provider.id}.key`, "")
84
+ || findOpencodeKeyV2(props.context, provider);
85
+ const set = props.signals.setBalanceState;
86
+ if (props.signals.balanceUnsupported()) {
87
+ set({ status: "idle", data: null, lastFetch: 0, error: undefined, key: undefined });
88
+ return;
89
+ }
90
+ if (!key) {
91
+ set({ status: "idle", data: null, lastFetch: 0, error: undefined, key: undefined });
92
+ return;
93
+ }
94
+ const now = Date.now();
95
+ const prev = props.signals.balanceState();
96
+ // key 已更换(重新输入)→ 强制重新查询,绕过缓存
97
+ if (prev.status === "ok" && prev.key === key && now - prev.lastFetch < BALANCE_POLL_MS)
98
+ return;
99
+ const seq = ++balanceSeq;
100
+ set({ ...prev, status: "loading", error: undefined, key });
101
+ const controller = new AbortController();
102
+ let timedOut = false;
103
+ const timer = setTimeout(() => { timedOut = true; controller.abort(); }, 10_000);
104
+ try {
105
+ const data = await provider.fetchBalance(key, controller.signal);
106
+ clearTimeout(timer);
107
+ if (seq !== balanceSeq)
108
+ return; // 已被更新的请求取代,丢弃过期结果
109
+ set({ status: "ok", data, lastFetch: Date.now(), error: undefined, key });
110
+ }
111
+ catch (err) {
112
+ clearTimeout(timer);
113
+ if (seq !== balanceSeq)
114
+ return;
115
+ const code = timedOut ? "TIMEOUT" : (err instanceof Error ? err.message : "");
116
+ set({ status: "error", data: null, lastFetch: 0, error: code, key });
117
+ }
118
+ };
119
+ // Re-fetch when the API key is (re)configured via /cache-balance-key。
120
+ // 注意:pollBalance 内部读写 balanceState 信号,若不做 untrack 包裹,
121
+ // effect 会追踪 balanceState 的变化并与 setBalanceState 形成无限循环。
122
+ createEffect(() => {
123
+ void props.signals.balanceRefresh();
124
+ untrack(() => { void pollBalance(); });
125
+ });
126
+ // 定时轮询(5 分钟);随插件生命周期清理
127
+ const balanceTimer = setInterval(pollBalance, BALANCE_POLL_MS);
128
+ onCleanup(() => clearInterval(balanceTimer));
129
+ // auto-clear override:用户导航到不同主会话时清除子代理视图(对齐 V1 createSidebarSlot)
130
+ let lastSlotSid = props.sessionID;
131
+ createEffect(() => {
132
+ const sid = props.sessionID;
133
+ if (sid !== lastSlotSid) {
134
+ lastSlotSid = sid;
135
+ if (props.signals.overrideSessionId()) {
136
+ props.signals.setOverrideSessionId(undefined);
137
+ void props.api.kv.set(`${KV_PREFIX}.session`, "");
138
+ }
139
+ }
140
+ });
141
+ return (_jsx(TokenCachePanel, { theme: mapTheme(props.context.theme), api: props.api, sessionId: props.sessionID, signals: props.signals }));
142
+ }
143
+ const mod = {
144
+ id: "opencode-visual-cache",
145
+ setup(context) {
146
+ const api = createPanelApi(context);
147
+ const signals = createPanelSignals();
148
+ // 侧边栏完整面板(与 V1 同一组件;命令 layer 在组件内注册)。
149
+ // prepend:排在宿主官方信息(问候/Context/用量)之前,紧跟会话标题。
150
+ context.ui.slot({
151
+ prepend: "sidebar.content",
152
+ render: (props) => (_jsx(PluginRoot, { context: context, api: api, signals: signals, sessionID: String(props.sessionID ?? "") })),
153
+ });
154
+ // 底部状态栏(完整口径与 V1 一致;余额读共享 signals.balanceState)
155
+ context.ui.slot({
156
+ append: "prompt.footer.status",
157
+ render: (props) => (_jsx(StatusView, { context: context, signals: signals, sessionID: String(props.sessionID ?? "") })),
158
+ });
159
+ // 偏好持久化(实验:storage.store 用法验证)
160
+ context.storage.store("opencode-visual-cache.panel", { initial: { collapsed: false } });
161
+ },
162
+ };
163
+ export default mod;
@@ -0,0 +1,6 @@
1
+ /** @jsxImportSource @opentui/solid */
2
+ import type { Context } from "./types";
3
+ export declare function SidebarView(props: {
4
+ context: Context;
5
+ sessionID: string;
6
+ }): import("solid-js").JSX.Element;
@@ -0,0 +1,36 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "@opentui/solid/jsx-runtime";
2
+ /** @jsxImportSource @opentui/solid */
3
+ import { createMemo, For, Show } from "solid-js";
4
+ import { collectSessionStats, hitRate } from "./data";
5
+ import { fmt, fmtCost, progressBar, visualPadEnd } from "../core";
6
+ const LABEL_GAP = 1;
7
+ const BAR_BRACKETS = 2;
8
+ const BAR_GAP = 1;
9
+ const PCT_FIXED_WIDTH = 5;
10
+ const MIN_PANEL_WIDTH = 20;
11
+ const DEFAULT_PANEL_WIDTH = 26;
12
+ export function SidebarView(props) {
13
+ const stats = createMemo(() => collectSessionStats(props.context, props.sessionID));
14
+ const rate = createMemo(() => hitRate(stats()));
15
+ const theme = props.context.theme;
16
+ const rateColor = () => {
17
+ const r = rate();
18
+ if (r >= 85)
19
+ return theme.text.feedback.success.default;
20
+ if (r >= 70)
21
+ return theme.text.feedback.warning.default;
22
+ return theme.text.feedback.error.default;
23
+ };
24
+ const barWidth = () => Math.max(MIN_PANEL_WIDTH, Math.min(DEFAULT_PANEL_WIDTH, props.context.app.version ? DEFAULT_PANEL_WIDTH : DEFAULT_PANEL_WIDTH)) -
25
+ LABEL_GAP - BAR_BRACKETS - BAR_GAP - PCT_FIXED_WIDTH - 1;
26
+ const rows = () => {
27
+ const s = stats();
28
+ return [
29
+ { label: "Hit", value: fmt(s.cacheRead) + " tok", color: rateColor() },
30
+ { label: "Miss", value: fmt(s.input + s.cacheWrite) + " tok", color: theme.text.default },
31
+ { label: "Write", value: fmt(s.cacheWrite) + " tok", color: theme.text.subdued },
32
+ { label: "Out", value: fmt(s.output) + " tok", color: theme.text.subdued },
33
+ ];
34
+ };
35
+ return (_jsx(Show, { when: stats().hasData, children: _jsxs("box", { children: [_jsxs("box", { flexDirection: "row", gap: LABEL_GAP, children: [_jsx("text", { fg: theme.text.default, children: "Cache" }), _jsxs("text", { fg: rateColor(), children: ["[", progressBar(rate(), barWidth()), "]"] }), _jsx("text", { fg: rateColor(), children: rate().toFixed(1) + "%" })] }), _jsx(For, { each: rows(), children: (row) => (_jsxs("box", { flexDirection: "row", gap: LABEL_GAP, children: [_jsx("text", { fg: theme.text.default, children: visualPadEnd(row.label, 5) }), _jsx("text", { fg: row.color, children: row.value })] })) }), _jsx(Show, { when: stats().cost > 0, children: _jsxs("box", { flexDirection: "row", gap: LABEL_GAP, children: [_jsx("text", { fg: theme.text.default, children: "Cost" }), _jsx("text", { fg: theme.text.default, children: fmtCost(stats().cost) })] }) })] }) }));
36
+ }
@@ -0,0 +1,14 @@
1
+ /** @jsxImportSource @opentui/solid */
2
+ import type { Context } from "./types";
3
+ import type { PanelSignals } from "../panel/panel-api";
4
+ /**
5
+ * 底部状态栏(prompt.footer.status)——对齐 V1 BottomStatusBar 统计段口径:
6
+ * 单条命中率(最后一条有 token 的 assistant 消息)+ 趋势 + Tokens 总量 + 余额。
7
+ * 余额读共享 signals.balanceState(PluginRoot 驱动轮询);provider 不支持余额时隐藏余额段(对齐 V1)。
8
+ * 颜色经 mapTheme(V1 形状)——与侧边栏命中率颜色同源,保证两处一致。
9
+ */
10
+ export declare function StatusView(props: {
11
+ context: Context;
12
+ signals: PanelSignals;
13
+ sessionID: string;
14
+ }): import("solid-js").JSX.Element;
@@ -0,0 +1,83 @@
1
+ import { jsx as _jsx } from "@opentui/solid/jsx-runtime";
2
+ /** @jsxImportSource @opentui/solid */
3
+ import { createMemo, For, Show } from "solid-js";
4
+ import { collectLastHitRate } from "./data";
5
+ import { desaturateTo, fmtCompact, formatBalanceText, MAX_SAT, FALLBACK, visualWidth } from "../core";
6
+ import { createT } from "../i18n";
7
+ import { mapTheme } from "./theme";
8
+ /**
9
+ * 底部状态栏(prompt.footer.status)——对齐 V1 BottomStatusBar 统计段口径:
10
+ * 单条命中率(最后一条有 token 的 assistant 消息)+ 趋势 + Tokens 总量 + 余额。
11
+ * 余额读共享 signals.balanceState(PluginRoot 驱动轮询);provider 不支持余额时隐藏余额段(对齐 V1)。
12
+ * 颜色经 mapTheme(V1 形状)——与侧边栏命中率颜色同源,保证两处一致。
13
+ */
14
+ export function StatusView(props) {
15
+ const t = createT(() => props.signals.langCode());
16
+ // 与侧边栏 TokenCachePanel 同一颜色来源(mapTheme → desaturateTo)
17
+ const pal = createMemo(() => {
18
+ const th = mapTheme(props.context.theme);
19
+ const sat = (k, fb) => desaturateTo(th[k], MAX_SAT, fb);
20
+ return {
21
+ text: sat("text", FALLBACK.text),
22
+ muted: sat("textMuted", FALLBACK.muted),
23
+ success: sat("success", FALLBACK.success),
24
+ warning: sat("warning", FALLBACK.warning),
25
+ error: sat("error", FALLBACK.error),
26
+ };
27
+ });
28
+ const stats = createMemo(() => collectLastHitRate(props.context, props.sessionID));
29
+ const hitColor = createMemo(() => {
30
+ const r = stats().hitRate;
31
+ if (r >= 85)
32
+ return pal().success;
33
+ if (r >= 70)
34
+ return pal().warning;
35
+ return pal().error;
36
+ });
37
+ // 命中率趋势:最后一条与上一条的差值;|Δ| < 0.05 视为无变化(null = 不显示)
38
+ const trend = createMemo(() => {
39
+ const s = stats();
40
+ if (s.prevHitRate < 0 || s.hitRate < 0)
41
+ return null;
42
+ const d = s.hitRate - s.prevHitRate;
43
+ return Math.abs(d) < 0.05 ? null : d;
44
+ });
45
+ // 余额文本(对齐 V1):ok → 数值;loading → …;error → ⚠;idle → -
46
+ const balanceText = createMemo(() => {
47
+ const s = props.signals.balanceState();
48
+ if (s.status === "ok" && s.data)
49
+ return formatBalanceText(s.data, props.signals.balanceCurrency(), props.signals.exchangeRate());
50
+ if (s.status === "loading")
51
+ return "\u2026";
52
+ if (s.status === "error")
53
+ return "\u26a0";
54
+ return "-";
55
+ });
56
+ const segs = createMemo(() => {
57
+ const s = stats();
58
+ const hr = s.hitRate >= 0 ? (Math.floor(s.hitRate * 10) / 10).toFixed(1) + "%" : "--";
59
+ const out = [
60
+ { text: t("barHit") + " ", color: pal().muted },
61
+ { text: hr, color: hitColor() },
62
+ ];
63
+ const tr = trend();
64
+ if (tr !== null) {
65
+ out.push({ text: " " + (tr > 0 ? "\u2191" : "\u2193") + Math.abs(tr).toFixed(1) + "%", color: tr > 0 ? pal().success : pal().error });
66
+ }
67
+ out.push({ text: " \u00b7 " + t("barTok") + " ", color: pal().muted });
68
+ out.push({ text: s ? fmtCompact(s.input + s.read + s.write) : "--", color: pal().text });
69
+ if (!props.signals.balanceUnsupported()) {
70
+ out.push({ text: " \u00b7 " + t("barBal") + " ", color: pal().muted });
71
+ out.push({ text: balanceText(), color: pal().text });
72
+ }
73
+ out.push({ text: " \u00b7 ", color: pal().muted });
74
+ return out;
75
+ });
76
+ const segsW = createMemo(() => {
77
+ let w = 0;
78
+ for (const sg of segs())
79
+ w += visualWidth(sg.text);
80
+ return w;
81
+ });
82
+ return (_jsx(Show, { when: segsW() > 0, children: _jsx("text", { children: _jsx(For, { each: segs(), children: (sg) => _jsx("span", { style: { fg: sg.color }, children: sg.text }) }) }) }));
83
+ }
@@ -0,0 +1,8 @@
1
+ import type { TuiThemeCurrent } from "@opencode-ai/plugin/tui";
2
+ import type { Context } from "./types";
3
+ /** V2 theme → V1 形状映射(组件按 primary/text/textMuted/… 字段消费)。
4
+ * 侧边栏与底部栏共用——保证命中率颜色等两处一致。
5
+ * - V1 primary → V2 interactive(v1-migrate.ts 官方映射:interactive = hues.byToken.primary)
6
+ * - step 取 300:更亮(对齐 V2 暗色强调惯例的亮端),500/400 在暗背景下偏深
7
+ */
8
+ export declare function mapTheme(theme: Context["theme"]): TuiThemeCurrent;
@@ -0,0 +1,16 @@
1
+ /** V2 theme → V1 形状映射(组件按 primary/text/textMuted/… 字段消费)。
2
+ * 侧边栏与底部栏共用——保证命中率颜色等两处一致。
3
+ * - V1 primary → V2 interactive(v1-migrate.ts 官方映射:interactive = hues.byToken.primary)
4
+ * - step 取 300:更亮(对齐 V2 暗色强调惯例的亮端),500/400 在暗背景下偏深
5
+ */
6
+ export function mapTheme(theme) {
7
+ return {
8
+ primary: theme.hue.interactive[300],
9
+ text: theme.text.default,
10
+ textMuted: theme.text.subdued,
11
+ success: theme.text.feedback.success.default,
12
+ warning: theme.text.feedback.warning.default,
13
+ error: theme.text.feedback.error.default,
14
+ border: theme.text.subdued,
15
+ };
16
+ }
@@ -0,0 +1,219 @@
1
+ /**
2
+ * V2 (opencode2) TUI plugin API — 最小本地类型(实验版)。
3
+ * 运行时由 opencode2 提供,此处仅用于本地类型检查;
4
+ * 结构对应 v2 分支 packages/plugin/src/tui/context.ts。
5
+ */
6
+ export interface App {
7
+ readonly version: string;
8
+ readonly channel: string;
9
+ }
10
+ export interface Theme {
11
+ readonly hue: {
12
+ /** V1 primary 的 V2 等价(v1-migrate.ts:interactive = hues.byToken.primary)。
13
+ * 取 300:更亮(暗背景下 400/500 偏深)。 */
14
+ readonly interactive: {
15
+ readonly 300: string;
16
+ };
17
+ /** 主题强调色别名。 */
18
+ readonly accent: {
19
+ readonly 500: string;
20
+ };
21
+ };
22
+ readonly text: {
23
+ readonly default: string;
24
+ readonly subdued: string;
25
+ readonly feedback: {
26
+ readonly success: {
27
+ readonly default: string;
28
+ };
29
+ readonly error: {
30
+ readonly default: string;
31
+ };
32
+ readonly warning: {
33
+ readonly default: string;
34
+ };
35
+ };
36
+ };
37
+ }
38
+ /** token 用量:结构与 V1 的 message.tokens 同构,字段以运行时实际数据为准。 */
39
+ export interface TokenUsage {
40
+ input?: number;
41
+ output?: number;
42
+ reasoning?: number;
43
+ cache?: {
44
+ read?: number;
45
+ write?: number;
46
+ };
47
+ }
48
+ export interface MessageInfo {
49
+ readonly id: string;
50
+ readonly type: string;
51
+ readonly agent?: string;
52
+ readonly model?: string;
53
+ readonly time: {
54
+ readonly created: number;
55
+ readonly completed?: number;
56
+ };
57
+ readonly cost?: unknown;
58
+ readonly tokens?: TokenUsage;
59
+ readonly content?: unknown[];
60
+ }
61
+ export interface SessionInfo {
62
+ readonly id: string;
63
+ readonly title?: string;
64
+ readonly time?: {
65
+ readonly created: number;
66
+ readonly updated: number;
67
+ };
68
+ readonly model?: string;
69
+ readonly agent?: string;
70
+ readonly parentID?: string;
71
+ }
72
+ export interface Data {
73
+ readonly session: {
74
+ get(sessionID: string): SessionInfo | undefined;
75
+ list(): SessionInfo[];
76
+ cost(sessionID: string): number;
77
+ status(sessionID: string): string;
78
+ readonly message: {
79
+ list(sessionID: string): MessageInfo[];
80
+ sync(sessionID: string): Promise<void>;
81
+ };
82
+ };
83
+ readonly location: {
84
+ readonly provider: {
85
+ list(location?: unknown): unknown[];
86
+ };
87
+ readonly model: {
88
+ list(location?: unknown): unknown[];
89
+ };
90
+ readonly mcp: {
91
+ readonly server: {
92
+ list(location?: unknown): unknown[];
93
+ };
94
+ };
95
+ };
96
+ readonly on: (type: string, handler: (event: unknown) => void) => () => void;
97
+ readonly listen: (handler: (event: unknown) => void) => () => void;
98
+ }
99
+ export interface Storage {
100
+ store<Value extends object>(key: string, options: {
101
+ readonly initial: Value;
102
+ }): readonly [Value, (mutation: (draft: Value) => void) => Promise<void>];
103
+ memory<Value extends object>(key: string, options: {
104
+ readonly initial: Value;
105
+ }): readonly [Value, (mutation: (draft: Value) => void) => void];
106
+ }
107
+ export type SlotClaim = {
108
+ readonly render: (input: Record<string, any>) => unknown;
109
+ } & ({
110
+ readonly append: string;
111
+ readonly prepend?: never;
112
+ readonly before?: never;
113
+ readonly after?: never;
114
+ readonly replace?: never;
115
+ } | {
116
+ readonly prepend: string;
117
+ readonly append?: never;
118
+ readonly before?: never;
119
+ readonly after?: never;
120
+ readonly replace?: never;
121
+ } | {
122
+ readonly before: string;
123
+ readonly append?: never;
124
+ readonly prepend?: never;
125
+ readonly after?: never;
126
+ readonly replace?: never;
127
+ } | {
128
+ readonly after: string;
129
+ readonly append?: never;
130
+ readonly prepend?: never;
131
+ readonly before?: never;
132
+ readonly replace?: never;
133
+ } | {
134
+ readonly replace: string;
135
+ readonly append?: never;
136
+ readonly prepend?: never;
137
+ readonly before?: never;
138
+ readonly after?: never;
139
+ });
140
+ export interface KeymapCommand {
141
+ readonly id?: string;
142
+ readonly title?: string;
143
+ readonly description?: string;
144
+ readonly group?: string;
145
+ readonly palette?: true;
146
+ readonly slash?: {
147
+ readonly name: string;
148
+ readonly aliases?: string[];
149
+ readonly arguments?: true;
150
+ };
151
+ /** 旧字段(opencode-dcp 兼容写法:palette 可见性 + slash 补全依赖它们) */
152
+ readonly namespace?: string;
153
+ readonly name?: string;
154
+ readonly desc?: string;
155
+ readonly category?: string;
156
+ readonly slashName?: string;
157
+ readonly slashAliases?: string[];
158
+ readonly run: (input?: string) => void | false | Promise<void>;
159
+ }
160
+ export interface Context {
161
+ readonly app: App;
162
+ readonly options: Record<string, any>;
163
+ readonly location: {
164
+ readonly directory?: string;
165
+ } | undefined;
166
+ readonly renderer: {
167
+ readonly terminalWidth: number;
168
+ };
169
+ readonly theme: Theme;
170
+ readonly data: Data;
171
+ readonly storage: Storage;
172
+ readonly ui: {
173
+ slot(claim: SlotClaim): () => void;
174
+ readonly toast: {
175
+ show(options: {
176
+ readonly message: string;
177
+ readonly title?: string;
178
+ readonly variant?: string;
179
+ }): void;
180
+ };
181
+ readonly dialog: {
182
+ prompt(options: {
183
+ readonly title: string;
184
+ readonly message?: string;
185
+ readonly placeholder?: string;
186
+ }): Promise<string | undefined>;
187
+ select<Value>(options: {
188
+ readonly title: string;
189
+ readonly placeholder?: string;
190
+ readonly options: readonly {
191
+ readonly title: string;
192
+ readonly value: Value;
193
+ readonly description?: string;
194
+ readonly category?: string;
195
+ readonly disabled?: boolean;
196
+ }[];
197
+ readonly current?: Value;
198
+ }): Promise<Value | undefined>;
199
+ };
200
+ /** 当前路由:命令需要当前会话 ID(V2 Route = { type: "session", sessionID })。 */
201
+ readonly router: {
202
+ current(): {
203
+ readonly type?: string;
204
+ readonly sessionID?: string;
205
+ readonly params?: Record<string, unknown>;
206
+ };
207
+ };
208
+ };
209
+ readonly keymap: {
210
+ layer(input: () => {
211
+ readonly commands?: readonly KeymapCommand[];
212
+ }): void;
213
+ shortcuts(id: string): readonly string[];
214
+ };
215
+ }
216
+ export type PluginModule = {
217
+ readonly id: string;
218
+ readonly setup: (context: Context) => void | (() => void | Promise<void>) | Promise<void | (() => void | Promise<void>)>;
219
+ };
@@ -0,0 +1,6 @@
1
+ /**
2
+ * V2 (opencode2) TUI plugin API — 最小本地类型(实验版)。
3
+ * 运行时由 opencode2 提供,此处仅用于本地类型检查;
4
+ * 结构对应 v2 分支 packages/plugin/src/tui/context.ts。
5
+ */
6
+ export {};
@@ -0,0 +1,8 @@
1
+ import type { Context } from "./types";
2
+ import type { PanelApi } from "../panel/panel-api";
3
+ /**
4
+ * V2 (opencode2) context → PanelApi 适配实现。
5
+ * 核心能力(session/messages/tokens/storage/event/renderer/part)完整映射;
6
+ * 无 V2 对应的能力(config)以空值兜底。
7
+ */
8
+ export declare function createPanelApi(context: Context): PanelApi;