Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions external_plugins/zcode-usage-panel/.zcode-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"name": "zcode-usage-panel",
"version": "0.2.1",
"description": "Live usage panel for ZCode: per-tool/MCP/Skill call stats, token shares and TPS.",
"description_i18n": {
"en": "Live usage panel for ZCode: per-tool/MCP/Skill call stats, token shares and TPS.",
"zh-CN": "ZCode 用量面板:实时展示工具/MCP/Skill 调用明细、token 占比与 TPS。"
},
"author": {
"name": "taohouchao"
},
"license": "MIT",
"mcpServers": {
"usage": {
"type": "stdio",
"command": "node",
"args": [
"${ZCODE_PLUGIN_ROOT}/payload/dist/server.mjs"
],
"env": {
"NODE_NO_WARNINGS": "1"
}
}
},
"ui": {
"surfaces": [
{
"id": "usage",
"title": {
"en": "Usage Panel",
"zh-CN": "用量面板"
},
"server": "usage",
"resourceUri": "ui://zcode-usage-panel/index.html",
"availability": "session"
}
]
}
}
21 changes: 21 additions & 0 deletions external_plugins/zcode-usage-panel/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 taohouchao

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
55 changes: 55 additions & 0 deletions external_plugins/zcode-usage-panel/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# zcode-usage-panel

[中文](./README_CN.md)

A ZCode MCP App plugin that pins a live **Usage Panel** in the session sidebar, showing what the current session is spending in real time.

## Features

- **Top summary**: total tokens (input/output/cache split), model call count, tool call count, average/current TPS, currently running tools
- **TPS sparkline**: generation-phase TPS (`output / (duration − TTFT)`) over the last 40 model calls, with a mean reference line
- **Prompt composition** (v0.2.0): breakdown of the system prompt — AGENTS.md instructions (user-global + project), memory entries, skill manifests (user / plugin / built-in sources, with both "manifest-resident estimate" and "full-text characters" readings), MCP server declarations vs. servers actually called this session, and subagent definitions. When the session's model-io JSONL header has not yet been rotated away, real injection records (per-module character counts + tool list) are shown too
- **Collapsible detail sections** (expand state survives reopen):
- Tools: built-in / MCP (grouped by server) / Skill (by skill name), with call counts, count/time share, total/average duration, output volume (bytes), and error counts
- Models: token breakdown by model × agent × source (main conversation / subagent / compaction…), with shares and average TPS
- Recent turns
- **Subsession rollup** (v0.2.0): model and tool calls from subagents (parent_id) are merged into session stats; model details keep a "source" distinction
- **Session switching**: follows the latest interactive session by default (auto mode), or can be pinned to one of the recent 20 sessions
- **Live updates**: `resources/subscribe` push (~1s), falling back to 2s polling when subscription is unavailable

## Requirements

- ZCode Desktop with MCP App support (session sidebar panels)
- **Node.js ≥ 24** on `PATH` (the server uses the built-in `node:sqlite` module; no native dependencies)

## Install

Install **zcode-usage-panel** from the official ZCode plugin marketplace, then open it from the "+" menu in the right sidebar of a task **with an active session**.

## Permissions & side effects

- **Read-only access to `~/.zcode/cli/db/db.sqlite`** (ZCode's local usage database), opened with `mode=ro`. The plugin never writes to it or to any other file.
- **No network access**, no command execution, no hooks, no third-party services.
- The panel page and server are self-contained bundles; runtime has zero npm dependencies. Build-time dependencies (esbuild, TypeScript, MCP SDK) are not shipped in the install payload.
- No third-party code, assets, or services are bundled beyond open-source build tooling (see `package.json`; all MIT/Apache-2.0 licensed).

## Known limitations

- The panel only appears in the sidebar "+" menu of tasks with an active session (host `availability: "session"` behavior).
- `db.sqlite` is a ZCode-internal schema (verified against 0.16.x); a ZCode upgrade may break queries — the panel then shows a degradation error instead of a blank page.
- The host caps HTML resources at 4 MiB (current payload is ~610 KB).

## Development

```bash
npm install
npm run typecheck # tsc --noEmit
npm run build # bundles to payload/dist/ (server.mjs + index.html)
npm run smoke # full MCP JSON-RPC round trip against the real server bundle
```

`SERVER_PATH` / `NODE_BIN` environment variables can point `npm run smoke` at an alternative bundle or runtime (used to verify the published payload with the system Node).

## License

[MIT](./LICENSE)
55 changes: 55 additions & 0 deletions external_plugins/zcode-usage-panel/README_CN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# zcode-usage-panel

[English](./README.md)

ZCode MCP App 插件:在会话右侧边栏常驻「用量面板」,实时展示当前会话的用量明细。

## 功能

- **顶部汇总**:总 tokens(输入/输出/缓存拆分)、模型调用次数、工具调用次数、平均/当前 TPS、正在运行的工具
- **TPS 曲线**:最近 40 次模型调用的生成段 TPS(`output / (duration − TTFT)`)sparkline,含均值虚线
- **提示词构成**(v0.2.0):扫描系统提示词的组成——AGENTS.md 指令(用户全局+项目级)、记忆项目、技能清单(用户级/插件/App 内置三源;每组与每条 SKILL.md 同时给出「清单常驻估算」与「全文字符」两口径)、MCP server 声明与本会话实际调用过的 MCP、子代理定义;若会话的 model-io JSONL 头部尚未被滚动丢弃,还会展示真实注入记录(各模块字符量 + 工具清单)
- **下拉明细**(可折叠区块,展开状态跨重开记忆):
- 工具明细:内置 / MCP(按 server 分组)/ Skill(具体 skill 名),列含次数、次数/耗时占比、总/平均耗时、输出量(工具输出字节数)、错误数
- 模型明细:按 model × agent × 来源(主对话/子代理/压缩…)的 token 细分、占比、平均 TPS
- 最近轮次
- **子会话并入**(v0.2.0):子代理(parent_id 挂靠)的模型调用与工具调用全部计入会话统计,模型明细以"来源"区分
- **会话切换**:默认跟随最新交互会话(自动模式),可固定最近 20 个会话之一
- **动态更新**:`resources/subscribe` 订阅推送(约 1s),无订阅能力回退 2s 轮询

## 环境要求

- 支持 MCP App(会话侧栏面板)的 ZCode Desktop
- `PATH` 上有 **Node.js ≥ 24**(server 使用内置 `node:sqlite` 模块,零原生依赖)

## 安装

从 ZCode 官方插件市场安装 **zcode-usage-panel**,在**有活跃会话**的任务里通过右侧边栏"+"菜单打开。

## 权限与副作用

- **只读访问 `~/.zcode/cli/db/db.sqlite`**(ZCode 本地用量库,`mode=ro` 打开),绝不写入该文件或任何其他文件。
- **无网络访问**、无命令执行、无 Hook、无第三方服务。
- 面板页面与 server 均为自包含 bundle,运行时零 npm 依赖;构建期依赖(esbuild、TypeScript、MCP SDK)不进入安装包。
- 除开源构建工具链外(见 `package.json`,均为 MIT/Apache-2.0 许可),不捆绑第三方代码、素材或服务。

## 已知边界

- 面板需活跃会话才出现在边栏"+"菜单(宿主 `availability: "session"` 行为)。
- db.sqlite 是 ZCode 内部 schema(0.16.x 验证),版本升级后查询可能失效——面板会显示降级错误而非白屏。
- HTML resource 宿主上限 4 MiB(当前产物约 610KB)。

## 开发

```bash
npm install
npm run typecheck # tsc --noEmit
npm run build # 产物输出到 payload/dist/(server.mjs + index.html)
npm run smoke # 用真实 server bundle 跑完整 MCP JSON-RPC 链路
```

`npm run smoke` 支持 `SERVER_PATH` / `NODE_BIN` 环境变量指定其他产物与运行时(发布验证用系统 Node 跑 payload 下的 bundle 时使用)。

## 许可证

[MIT](./LICENSE)
145 changes: 145 additions & 0 deletions external_plugins/zcode-usage-panel/build.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
// 构建:server bundle (ESM/node) + 面板页面(IIFE 内联进自包含 HTML)
// 产物输出到 payload/dist/(安装载荷,随仓库分发)。
import { build } from "esbuild";
import { mkdirSync, writeFileSync, statSync } from "node:fs";
import { resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";

const root = resolve(dirname(fileURLToPath(import.meta.url)));
// OUT_DIR 供发布构建覆盖输出位置(如官方市场仓库的 payload/dist),默认本地 dev 市场。
const outDir = process.env.OUT_DIR ? resolve(process.env.OUT_DIR) : resolve(root, "payload/dist");
mkdirSync(outDir, { recursive: true });

// ---------- server ----------
await build({
entryPoints: [resolve(root, "src/server/index.ts")],
bundle: true,
platform: "node",
format: "esm",
target: "node22",
outfile: resolve(outDir, "server.mjs"),
legalComments: "inline",
logLevel: "info",
});

// ---------- page → 单文件 HTML ----------
const page = await build({
entryPoints: [resolve(root, "src/page/main.ts")],
bundle: true,
platform: "browser",
format: "iife",
target: "es2022",
minify: true,
write: false,
legalComments: "inline",
logLevel: "info",
});
const js = page.outputFiles[0].text;

const html = `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>用量面板</title>
<style>
:root{--bg:#f7f7f8;--card:#ffffff;--line:#e4e4e7;--text:#18181b;--dim:#71717a;
--accent:#2563eb;--bar:#60a5fa;--err:#dc2626;--ok:#16a34a;--warn:#d97706;
--thead:#f4f4f5;--row-hover:#f0f4ff}
html[data-theme=dark]{--bg:#17181a;--card:#1e2023;--line:#2c2e32;--text:#e6e6e9;--dim:#9d9da6;
--accent:#60a5fa;--bar:#3b82f6;--err:#f87171;--ok:#4ade80;--warn:#fbbf24;
--thead:#242629;--row-hover:#22262e}
*{box-sizing:border-box}
html,body{margin:0;padding:0;background:var(--bg);color:var(--text);
font:12px/1.5 -apple-system,BlinkMacSystemFont,"PingFang SC","Segoe UI",sans-serif}
body{padding:10px 10px 14px}
.head{display:flex;flex-direction:column;gap:6px;margin-bottom:8px}
.head-row{display:flex;align-items:center;gap:6px}
.brand{font-weight:700;font-size:13px}
.status-text{color:var(--dim);font-size:11px}
.dot{width:8px;height:8px;border-radius:50%;display:inline-block;margin-left:auto}
.dot-live{background:var(--ok)}
.dot-poll{background:var(--warn)}
.dot-error{background:var(--err)}
.dot-off{background:var(--dim)}
.session-select{width:100%;padding:4px 6px;border:1px solid var(--line);border-radius:6px;
background:var(--card);color:var(--text);font-size:12px}
.session-meta{display:flex;flex-wrap:wrap;gap:8px;color:var(--dim);font-size:10.5px}
.banner{background:color-mix(in srgb,var(--err) 12%,transparent);border:1px solid var(--err);
color:var(--text);border-radius:6px;padding:6px 8px;margin-bottom:8px;font-size:11px}
.cards{display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-bottom:10px}
.card{background:var(--card);border:1px solid var(--line);border-radius:8px;padding:6px 8px}
.card-title{color:var(--dim);font-size:10.5px}
.card-value{font-size:16px;font-weight:700;color:var(--accent);font-variant-numeric:tabular-nums}
.card-sub{color:var(--dim);font-size:10px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.tps-block{background:var(--card);border:1px solid var(--line);border-radius:8px;
padding:8px;margin-bottom:10px}
.tps-head{display:flex;justify-content:space-between;align-items:baseline;margin-bottom:4px}
.tps-label{color:var(--dim);font-size:11px}
.tps-now{font-weight:700;color:var(--accent);font-variant-numeric:tabular-nums}
.spark{width:100%;height:44px;display:block}
.spark-line{fill:none;stroke:var(--bar);stroke-width:1.5}
.spark-avg{stroke:var(--dim);stroke-width:0.75;stroke-dasharray:3 3;opacity:.7}
.tps-foot{color:var(--dim);font-size:10.5px;margin-top:4px}
.sec{background:var(--card);border:1px solid var(--line);border-radius:8px;
margin-bottom:8px;overflow:hidden}
.sec-summary{display:flex;align-items:center;gap:6px;padding:7px 10px;cursor:pointer;
font-weight:600;user-select:none;list-style:none}
.sec-summary::-webkit-details-marker{display:none}
.sec[open]>.sec-summary .arrow{transform:rotate(90deg)}
.arrow{display:inline-block;transition:transform .12s;color:var(--dim);font-size:10px}
.sec-hint{margin-left:auto;color:var(--dim);font-weight:400;font-size:10.5px}
.sec-body{padding:0 10px 10px}
.grp{margin-bottom:8px}
.grp:last-child{margin-bottom:0}
.grp-title{padding:4px 0;font-size:11px}
.kv-list{display:flex;flex-direction:column;gap:2px}
.kv{display:flex;justify-content:space-between;gap:8px;align-items:baseline}
.kv-v{font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}
.kv-k{color:var(--dim);font-size:10.5px;white-space:nowrap;flex-shrink:0}
.mono{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:10.5px}
.sub-sec{margin-bottom:6px;border:1px solid var(--line);border-radius:6px}
.sub-summary{display:flex;align-items:center;gap:5px;padding:4px 8px;cursor:pointer;font-size:11px;user-select:none;list-style:none}
.sub-summary::-webkit-details-marker{display:none}
.sub-sec[open]>.sub-summary .arrow{transform:rotate(90deg)}
.sub-sec .tbl{margin:0 8px 8px}
.tbl{width:100%;border-collapse:collapse;font-variant-numeric:tabular-nums}
.tbl th{text-align:left;color:var(--dim);font-weight:500;font-size:10.5px;
padding:3px 6px;background:var(--thead);position:sticky;top:0}
.tbl td{padding:3px 6px;border-top:1px solid var(--line);vertical-align:middle}
.tbl tr:hover td{background:var(--row-hover)}
.td-name{max-width:150px}
.td-name .sub{font-size:10px}
.td-num{text-align:right;white-space:nowrap}
.dim{color:var(--dim)}
.sub{margin-top:1px}
.kind{display:inline-block;font-size:9px;line-height:1;padding:2px 4px;border-radius:4px;
margin-right:5px;color:#fff;background:var(--dim);vertical-align:1px}
.kind-mcp{background:#7c3aed}
.kind-skill{background:#0d9488}
.kind-builtin{background:#52525b}
.running-pill{margin-left:5px;font-size:9px;color:var(--ok);border:1px solid var(--ok);
border-radius:4px;padding:0 3px}
.row-err td:first-child{color:var(--err)}
.bar{height:3px;border-radius:2px;background:color-mix(in srgb,var(--dim) 25%,transparent);
margin-top:2px;min-width:44px}
.bar-in{height:100%;border-radius:2px;background:var(--bar)}
.empty{color:var(--dim);padding:8px 2px}
.foot{color:var(--dim);font-size:10px;margin-top:4px;line-height:1.6}
.boot-error{color:var(--err);padding:12px;font-size:12px}
</style>
</head>
<body>
<script>
${js}
</script>
</body>
</html>
`;

const htmlPath = resolve(outDir, "index.html");
writeFileSync(htmlPath, html);
const size = statSync(htmlPath).size;
const limit = 4 * 1024 * 1024 - 512 * 1024; // 宿主 4MiB 限制,留余量
if (size > limit) throw new Error(`index.html ${size}B 超出宿主资源限制`);
console.log(`✓ index.html ${(size / 1024).toFixed(1)}KB / server.mjs 已输出到 ${outDir}`);
Loading