diff --git a/README.md b/README.md index a30f42f..e485be3 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ - **斜杠命令**:`/cache-session` `/cache-session-back` `/cache-rate` `/cache-section` `/cache-config` `/cache-lang` 动态配置面板 - **子代理缓存查看**:`/cache-session` 自动扫描并列出子代理,选择一个即可切换面板显示其缓存统计,支持 `/cache-session-back` 返回主会话 - **已加载技能**:检测 session 中 LLM 调用 `skill` tool 的记录,展示已加载技能名及估算 Token 占用 -- **底部状态栏**:输入框提示行单行显示 命中率(含趋势)· Tokens · 余额,关闭侧边栏也能随时看到缓存统计,可经 `/cache-section` 隐藏 +- **底部状态栏**:输入框提示行单行显示 命中率(含趋势)· Tokens · 余额,关闭侧边栏也能随时看到缓存统计。opencode 1.x 下**默认关闭**(开启需重启 TUI 生效,见 [4.3](#43-区块可见性)),opencode 2.x 下默认显示 --- @@ -135,7 +135,7 @@ npm install -g opencode-visual-cache@latest ### 4.1 斜杠命令 -插件支持通过斜杠命令或命令面板(`Ctrl + P`)动态调整配置,所有设置即时生效并持久化: +插件支持通过斜杠命令或命令面板(`Ctrl + P`)动态调整配置,设置即时生效并持久化(**底部状态栏**开关例外,见 [4.3 区块可见性](#43-区块可见性)): | 命令 | 功能 | 使用方式 | |------|------|---------| @@ -143,7 +143,7 @@ npm install -g opencode-visual-cache@latest | `/cache-session-back` | 返回主会话统计 | 从子代理缓存视图切回主会话 | | `/cache-currency` | 切换货币单位 | 从列表选择货币(USD / CNY / EUR / JPY / GBP / KRW),自动填入默认汇率 | | `/cache-rate` | 调整汇率乘数 | 输入自定义汇率(如 `7.2`),用于费用换算 | -| `/cache-section` | 开关区块与边框 | 独立控制 Token 明细 / 模型与定价 / 估算 Token 分布 / 已加载技能 / 余额 / 底部状态栏 / 面板边框的显隐 | +| `/cache-section` | 开关区块与边框 | 独立控制 Token 明细 / 模型与定价 / 估算 Token 分布 / 已加载技能 / 余额 / 底部状态栏 / 面板边框的显隐(底部状态栏在 opencode 1.x 下默认关闭,开启需重启 TUI 生效) | | `/cache-config` | 查看当前配置 | 弹出当前货币、汇率、区块可见性状态 | | `/cache-lang` | 切换显示语言 | 从列表选择中文或 English,界面即时切换,无需重启 | | `/cache-balance` | 余额查询设置 | 选择余额提供商(菜单标注 Key 来源:用户 key / OpenCode / 未配置)/ 开关自动切换 | @@ -182,9 +182,13 @@ npm install -g opencode-visual-cache@latest - **估算 Token 分布**:按角色拆分的 Token 估算 - **已加载技能**:session 中 LLM 实际调用过的 Skill 名及估算 Token 占用 - **余额**:当前提供商账户余额(多提供商 + 自动切换) -- **底部状态栏**:输入框提示行的 命中率 · Tokens · 余额 单行统计 +- **底部状态栏**:输入框提示行的 命中率 · Tokens · 余额 单行统计(opencode 1.x 下默认关闭) -通过 `/cache-section` 切换后即时生效,无需重启。此外,该命令还可以开关面板的**外边框**——关闭后内容会顶格显示,释放额外空间。 +通过 `/cache-section` 切换后即时生效、无需重启(**底部状态栏**例外,见下)。此外,该命令还可以开关面板的**外边框**——关闭后内容会顶格显示,释放额外空间。 + +> **底部状态栏在 opencode 1.x 下默认关闭、开启需重启 TUI**:opencode 1.x 的输入框只有重建宿主的 `session_prompt` 插槽才能挂状态栏,而该插槽为 replace 模式——**多个重建者会被同时渲染**。若同时使用其它同样重建 `session_prompt` 的插件(如 [`oh-my-openagent`](https://github.com/code-yeongyu/oh-my-openagent)),会出现**重复输入框**。因此本插件默认不占用该插槽,提示行显示宿主默认路径。 +> +> 用 `/cache-section` 开启**底部状态栏**并**重启 TUI** 即可显示统计(此时会占用该插槽,与同类插件互斥)。opencode 2.x 使用独立的 `prompt.footer.status` 插槽,不存在此冲突,**默认显示**。 > **关于 Token 分布数值**:分布面板中"推理"为 API 返回的**精确值**;"系统提示"/"用户"/"子代理指令"/"Tool 调用"/"Tool 结果"为**估算值**——API 仅返回 token 总量,无法拆分各内容类型,插件按内容类型收集文本后基于字符计数近似估算,数值仅供参考。OpenCode 运行时注入的系统提示内容(环境信息、Skill 目录、工具 Schema 定义等,详见 [`system.ts`](https://github.com/anomalyco/opencode/blob/dev/packages/opencode/src/session/system.ts)、[`tools.ts`](https://github.com/anomalyco/opencode/blob/dev/packages/opencode/src/session/tools.ts))不在此估算范围内。 diff --git a/README_EN.md b/README_EN.md index 95187f5..cdcca0e 100644 --- a/README_EN.md +++ b/README_EN.md @@ -54,7 +54,7 @@ Interested in sub-agent monitoring? Check out [opencode-subagent-magazine](https - **Slash Commands**: `/cache-session` `/cache-session-back` `/cache-rate` `/cache-section` `/cache-config` `/cache-lang` for live panel configuration - **Sub-Agent Cache View**: `/cache-session` auto-scans and lists sub-agents; select one to switch the panel stats. Use `/cache-session-back` to return to the main session - **Loaded Skills**: Detects `skill` tool calls in the session and displays loaded skill names with estimated token footprint -- **Bottom Status Bar**: single-line hit rate (with trend) · Tokens · Balance in the prompt hint row — visible even with the sidebar closed; hide it anytime via `/cache-section` +- **Bottom Status Bar**: single-line hit rate (with trend) · Tokens · Balance in the prompt hint row — visible even with the sidebar closed. **Off by default on opencode 1.x** (turning it on requires a TUI restart, see [4.3](#43-section-visibility)); shown by default on opencode 2.x --- @@ -135,7 +135,7 @@ Open any session — the cache stats panel appears in the sidebar. ### 4.1 Slash Commands -The plugin supports slash commands and command palette (`Ctrl + P`) for runtime configuration. All changes take effect immediately and are persisted: +The plugin supports slash commands and command palette (`Ctrl + P`) for runtime configuration. Changes take effect immediately and are persisted (the **Bottom Bar** toggle is the exception on opencode 1.x — see [4.3 Section Visibility](#43-section-visibility)): | Command | Function | How to use | |---------|----------|------------| @@ -143,7 +143,7 @@ The plugin supports slash commands and command palette (`Ctrl + P`) for runtime | `/cache-session-back` | Return to main session | Switch back to main session from sub-agent cache view | | `/cache-currency` | Switch currency | Pick from a list (USD / CNY / EUR / JPY / GBP / KRW); default exchange rate auto-filled | | `/cache-rate` | Adjust exchange rate | Enter a custom rate (e.g. `7.2` for CNY) | -| `/cache-section` | Toggle sections & border | Independently show/hide Detail, Model & Pricing, Token Distribution, Loaded Skills, Balance, Bottom Bar, or the panel border | +| `/cache-section` | Toggle sections & border | Independently show/hide Detail, Model & Pricing, Token Distribution, Loaded Skills, Balance, Bottom Bar, or the panel border (Bottom Bar is off by default on opencode 1.x; turning it on requires a TUI restart) | | `/cache-config` | View current config | Displays currency, rate, and section visibility | | `/cache-lang` | Switch display language | Pick Chinese or English from the dialog — takes effect immediately, no restart needed | | `/cache-balance` | Balance query settings | Pick a balance provider (menu shows key source: user key / OpenCode / not set) / toggle auto-switch | @@ -182,9 +182,13 @@ Three sub-sections can be toggled independently to save sidebar space: - **Estimated Token Dist.**: per-role token breakdown - **Loaded Skills**: skill names the LLM actually loaded via the `skill` tool, with estimated token counts - **Balance**: the selected provider's account balance (multi-provider with auto-switch) -- **Bottom Status Bar**: the single-line hit rate · Tokens · Balance stats in the prompt hint row +- **Bottom Status Bar**: the single-line hit rate · Tokens · Balance stats in the prompt hint row (**off by default on opencode 1.x**) -Toggled via `/cache-section` — takes effect instantly, no restart required. The same command also toggles the panel **border**; turning it off removes the outline and padding so content fills the full width. +Toggled via `/cache-section` — takes effect instantly with no restart (the **Bottom Status Bar** is the exception, see below). The same command also toggles the panel **border**; turning it off removes the outline and padding so content fills the full width. + +> **The Bottom Bar is off by default on opencode 1.x; turning it on requires a TUI restart**: on opencode 1.x the hint row can only carry the bar by rebuilding the host's `session_prompt` slot, and that slot uses replace mode — **every rebuilder's output is rendered side by side**. If another plugin rebuilds `session_prompt` as well (e.g. [`oh-my-openagent`](https://github.com/code-yeongyu/oh-my-openagent)), you get **duplicate input boxes**. This plugin therefore does not claim that slot by default, and the hint row shows the host's default path. +> +> Run `/cache-section` to turn the **Bottom Bar** on and **restart the TUI** to show the stats (this claims the slot and is mutually exclusive with such plugins). opencode 2.x uses a dedicated `prompt.footer.status` slot, so there is no such conflict and the bar is **shown by default**. > **About Token Dist. values**: "Reasoning" is an exact value from the API; the other rows (system / user / sub-agent instr / tool call / tool result) are **estimates** — the API only reports total token counts, not how they split across content types, so the plugin collects text per content type and approximates via character counting. Values are indicative only. OpenCode runtime-injected system prompt content (environment info, skill catalog, tool schema definitions — see [`system.ts`](https://github.com/anomalyco/opencode/blob/dev/packages/opencode/src/session/system.ts), [`tools.ts`](https://github.com/anomalyco/opencode/blob/dev/packages/opencode/src/session/tools.ts)) is not covered by these estimates. diff --git a/src/i18n.ts b/src/i18n.ts index 4b67ab7..7b1c8ab 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -76,6 +76,8 @@ const ZH_T = { borderHidden: "面板边框 已隐藏", sectionShown: "{s} 已显示", sectionHidden: "{s} 已隐藏", + bottomRestartOn: "底部状态栏已开启,重启 TUI 后生效", + bottomRestartOff: "底部状态栏已关闭,重启 TUI 后生效", langSwitched: "语言已切换为中文", autoSwitchOn: "自动切换余额提供商: 开", autoSwitchOff: "自动切换余额提供商: 关", @@ -174,6 +176,8 @@ const EN_T: Translation = { borderHidden: "Panel border hidden", sectionShown: "{s} section shown", sectionHidden: "{s} section hidden", + bottomRestartOn: "Bottom bar enabled; restart the TUI to apply", + bottomRestartOff: "Bottom bar disabled; restart the TUI to apply", langSwitched: "Switched to English", autoSwitchOn: "Auto-switch balance provider: ON", autoSwitchOff: "Auto-switch balance provider: OFF", @@ -269,6 +273,8 @@ const JA_T: Translation = { borderHidden: "パネル枠線 非表示", sectionShown: "{s} 表示", sectionHidden: "{s} 非表示", + bottomRestartOn: "下部ステータスバーを有効化しました。TUI 再起動後に反映されます", + bottomRestartOff: "下部ステータスバーを無効化しました。TUI 再起動後に反映されます", langSwitched: "日本語に切り替えました", autoSwitchOn: "残高プロバイダ自動切替: オン", autoSwitchOff: "残高プロバイダ自動切替: オフ", @@ -364,6 +370,8 @@ const KO_T: Translation = { borderHidden: "패널 테두리 숨김", sectionShown: "{s} 표시", sectionHidden: "{s} 숨김", + bottomRestartOn: "하단 상태바를 켰습니다. TUI 재시작 후 적용됩니다", + bottomRestartOff: "하단 상태바를 껐습니다. TUI 재시작 후 적용됩니다", langSwitched: "한국어로 전환했습니다", autoSwitchOn: "잔액 프로바이더 자동 전환: 켜짐", autoSwitchOff: "잔액 프로바이더 자동 전환: 꺼짐", diff --git a/src/index.tsx b/src/index.tsx index 66a5885..83709c3 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -406,11 +406,11 @@ function BottomStatusBar(props: { api: TuiPluginApi; signals: PanelSignals; sess return truncateVisual(directory(), avail) }) - // 恢复显隐偏好(默认显示);关闭时回退为仅显示路径,与宿主默认 hint 行一致 + // 恢复显隐偏好(默认关闭);关闭时回退为仅显示路径,与宿主默认 hint 行一致 onMount(() => { try { - const v = props.api.kv.get(`${KV_PREFIX}.section.bottom`, true) - props.signals.setSectionBottom(v !== false) + const v = props.api.kv.get(`${KV_PREFIX}.section.bottom`, false) + props.signals.setSectionBottom(v === true) } catch {} }) @@ -459,7 +459,54 @@ function createSidebarSlot(api: TuiPluginApi, signals: PanelSignals): TuiSlotPlu } } +/** 等待 KV 就绪(最多 1500ms);供启动时读取偏好使用,超时按未就绪处理。 */ +async function kvReady(api: TuiPluginApi): Promise { + if (api.kv.ready) return + const deadline = Date.now() + 1500 + while (!api.kv.ready && Date.now() < deadline) { + await new Promise((resolve) => setTimeout(resolve, 10)) + } +} + +/** + * 输入框 hint 行(session_prompt slot,replace 模式):用宿主同一 Prompt 组件重渲染, + * 仅替换 hint 行左侧,插入 命中率 · 余额 · Tokens。是否注册由调用方在启动时决定。 + */ +function createPromptSlot(api: TuiPluginApi, signals: PanelSignals): TuiSlotPlugin { + return { + order: 55, + slots: { + session_prompt( + _ctx: TuiSlotContext, + input: { + session_id: string + visible?: boolean + disabled?: boolean + on_submit?: () => void + ref?: (ref: TuiPromptRef | undefined) => void + }, + ): JSX.Element { + return ( + } + // 接管 session_prompt 后需透传宿主的 session_prompt_right 插槽, + // 否则 oc-tps 等依赖该插槽的插件无法显示;无注册时 Slot 为 null。 + right={} + /> + ) + }, + }, + } +} + const tui: TuiPlugin = async (api: TuiPluginApi) => { + const KV_PREFIX = "cache_panel" + // ── shared panel signals ────────────────────────────────────── const [currencySymbol, setCurrencySymbol] = createSignal("$") const [exchangeRate, setExchangeRate] = createSignal(1) @@ -514,38 +561,18 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => { // 输入框 hint 行(session_prompt slot,replace 模式): // 用宿主同一 Prompt 组件重渲染输入框,仅替换 hint 行左侧—— // 在路径与右侧 token/commands 提示之间插入 命中率 · 余额 · Tokens。 - api.slots.register({ - order: 55, - slots: { - session_prompt( - _ctx: TuiSlotContext, - input: { - session_id: string - visible?: boolean - disabled?: boolean - on_submit?: () => void - ref?: (ref: TuiPromptRef | undefined) => void - }, - ): JSX.Element { - return ( - } - // 接管 session_prompt 后需透传宿主的 session_prompt_right 插槽, - // 否则 oc-tps 等依赖该插槽的插件无法显示;无注册时 Slot 为 null。 - right={} - /> - ) - }, - }, - }) + // 关闭底部状态栏即让出该 slot:宿主对 session_prompt 使用 replace 模式,多个重建者的 + // 输出会被全部渲染(不互相覆盖),与同样重建 session_prompt 的插件并存会产生重复输入框。 + // slot 注册无法注销,故是否占用须在启动时确定——改动需重启 TUI 生效。 + await kvReady(api) + // 默认关闭(让出):仅当用户显式开启时才占用输入框插槽 + let promptRebuild = false + try { + promptRebuild = api.kv.get(`${KV_PREFIX}.section.bottom`, false) === true + } catch {} + if (promptRebuild) api.slots.register(createPromptSlot(api, signals)) // ── slash commands for runtime config ── - const KV_PREFIX = "cache_panel" // ── 语言偏好恢复:KV 就绪后优先用户设置(/cache-lang),覆盖自动识别 ── const restoreLang = () => { @@ -724,7 +751,7 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => { const distOn = Boolean(api.kv.get(`${KV_PREFIX}.section.dist`, true)) const skillsOn = Boolean(api.kv.get(`${KV_PREFIX}.section.skills`, true)) const balanceOn = Boolean(api.kv.get(`${KV_PREFIX}.section.balance`, true)) - const bottomOn = Boolean(api.kv.get(`${KV_PREFIX}.section.bottom`, true)) + const bottomOn = Boolean(api.kv.get(`${KV_PREFIX}.section.bottom`, false)) const borderOn = Boolean(api.kv.get(`${KV_PREFIX}.border`, true)) const labels: Record = { detail: t("secDetail"), @@ -755,8 +782,10 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => { signals.setBorderVisible(!cur) api.ui.toast({ message: !cur ? t("borderShown") : t("borderHidden") }) } else { + const isBottom = opt.value === "bottom" const key = `${KV_PREFIX}.section.${opt.value}` - const cur = Boolean(api.kv.get(key, true)) + // bottom 默认关闭,其余区块默认开启 + const cur = Boolean(api.kv.get(key, !isBottom)) api.kv.set(key, !cur) if (opt.value === "detail") signals.setSectionDetail(!cur) if (opt.value === "model") signals.setSectionModel(!cur) @@ -764,8 +793,13 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => { if (opt.value === "skills") signals.setSectionSkills(!cur) if (opt.value === "balance") signals.setSectionBalance(!cur) if (opt.value === "bottom") signals.setSectionBottom(!cur) - const name = labels[opt.value] ?? opt.value - api.ui.toast({ message: t(!cur ? "sectionShown" : "sectionHidden", { s: name }) }) + if (isBottom) { + // 让出/占用 session_prompt 在启动时确定,重启 TUI 后才完全生效 + api.ui.toast({ message: t(cur ? "bottomRestartOff" : "bottomRestartOn"), duration: 6000 }) + } else { + const name = labels[opt.value] ?? opt.value + api.ui.toast({ message: t(!cur ? "sectionShown" : "sectionHidden", { s: name }) }) + } } dialog?.clear() }} @@ -787,7 +821,7 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => { const dist = Boolean(api.kv.get(`${KV_PREFIX}.section.dist`, true)) const skills = Boolean(api.kv.get(`${KV_PREFIX}.section.skills`, true)) const balance = Boolean(api.kv.get(`${KV_PREFIX}.section.balance`, true)) - const bottom = Boolean(api.kv.get(`${KV_PREFIX}.section.bottom`, true)) + const bottom = Boolean(api.kv.get(`${KV_PREFIX}.section.bottom`, false)) const on = (v: boolean) => v ? "ON" : "OFF" api.ui.toast({ title: t("panelConfigTitle"),