Skip to content
Merged
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
17 changes: 9 additions & 8 deletions catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"schemaVersion": 1,
"providerId": "official",
"name": "PI-Desktop Official Plugins",
"updatedAt": "2026-09-14T04:38:39Z",
"updatedAt": "2026-09-17T17:21:16Z",
"homepage": "https://github.com/vastsa/pi-desktop-plugins",
"plugins": [
{
Expand Down Expand Up @@ -1027,16 +1027,17 @@
"safetyNotes": "Reads only local metadata, always read-only, always on this device. Usage comes from the tool metadata files PI-Desktop, Claude Code, Codex and OpenCode already write; appearance (theme, language, active plugin theme CSS) and provider display names come from PI-Desktop's own local records. It never reads message text, tool arguments, project paths or credentials, never keeps a full session id (only an 8-character prefix), never writes anywhere except its own plugin settings, and makes no network request.",
"versions": [
{
"version": "0.4.8",
"publishedAt": "2026-09-12T17:13:25Z",
"changelog": "Release 0.4.8: Fold PI-Desktop completed-turn totals from the host database into the dashboard, so subagent spend that never lands on parent message.usage still appears without double-counting transcript rows. Settings no longer has a Usage page; this panel is the heatmap. Release 0.4.8\uff1a\u628a\u5bbf\u4e3b\u5df2\u5b8c\u6210\u56de\u5408\u7684 token \u5408\u8ba1\u6298\u5165\u4eea\u8868\u76d8\uff0c\u8f6c\u5f55\u672c message.usage \u672a\u8ba1\u5165\u7684\u5b50\u667a\u80fd\u4f53\u7528\u91cf\u4f1a\u8865\u4e0a\u4e14\u4e0d\u4e0e JSONL \u91cd\u590d\u3002\u8bbe\u7f6e\u9875\u4e0d\u518d\u6709\u7528\u91cf\u5165\u53e3\uff0c\u70ed\u529b\u56fe\u4ee5\u672c\u9762\u677f\u4e3a\u51c6\u3002 Release 0.4.7: Fix the extra empty right-side rail on Windows by keeping the stable scrollbar gutter on the dashboard scroller instead of reserving it again on the root viewport. Release 0.4.7\uff1a\u4fee\u590d Windows \u53f3\u4fa7\u591a\u51fa\u7a7a\u767d\u4fa7\u680f\u7684\u95ee\u9898\uff1a\u4ec5\u5728\u4eea\u8868\u76d8\u6eda\u52a8\u5bb9\u5668\u4fdd\u7559\u7a33\u5b9a\u6eda\u52a8\u6761\u69fd\u4f4d\uff0c\u4e0d\u518d\u5728\u6839\u89c6\u53e3\u91cd\u590d\u9884\u7559\u3002 Release 0.4.6: Fix the titlebar's appearance and refresh buttons overlapping the host window-control capsule. The panel paints through the 46px band under v3 chrome, but the capsule still floats over the top-right corner, so both buttons sat under it and stopped being clickable. The titlebar now reserves the capsule's 104px corner (kept at narrow widths too, where the gutter override previously reset it), the appearance menu stays anchored to the buttons that open it, and a long title truncates instead of running under the reserve. Release 0.4.6\uff1a\u4fee\u590d\u6807\u9898\u680f\u7684\u5916\u89c2\u4e0e\u5237\u65b0\u6309\u94ae\u4e0e\u5bbf\u4e3b\u7a97\u53e3\u63a7\u5236\u80f6\u56ca\u91cd\u53e0\u7684\u95ee\u9898\u3002v3 chrome \u4e0b\u9762\u677f\u53ef\u7ed8\u5236\u5230 46px \u9876\u680f\uff0c\u4f46\u80f6\u56ca\u4ecd\u6d6e\u4e8e\u53f3\u4e0a\u89d2\uff0c\u5bfc\u81f4\u4e24\u4e2a\u6309\u94ae\u88ab\u906e\u6321\u4e14\u65e0\u6cd5\u70b9\u51fb\u3002\u6807\u9898\u680f\u73b0\u4e3a\u80f6\u56ca\u4fdd\u7559 104px \u53f3\u4fa7\u7a7a\u95f4\uff08\u7a84\u5bbd\u5ea6\u4e0b\u7684 gutter \u8986\u76d6\u6b64\u524d\u4f1a\u91cd\u7f6e\u8be5\u4fdd\u7559\u503c\uff0c\u73b0\u5df2\u4fdd\u7559\uff09\uff0c\u5916\u89c2\u83dc\u5355\u7ee7\u7eed\u5bf9\u9f50\u89e6\u53d1\u6309\u94ae\uff0c\u8fc7\u957f\u6807\u9898\u4ee5\u7701\u7565\u53f7\u622a\u65ad\u3002 Release 0.4.5: Migrates the panel to v3 paint-through chrome so top-band controls remain clickable while blank space remains draggable. Release 0.4.4: Fix the window-control capsule being drawn with default black-on-white colors on a panel's very first open \u2014 with no cached appearance, data-theme stayed unset until the host answered, so the host's one-shot color snapshot at DOMContentLoaded missed; boot now falls back to the OS palette synchronously while content stays cloaked. Also stop startAppearanceWatch from resetting the appearance fingerprint, which made every open-command run rewrite hostAppearance. Release 0.4.4\uff1a\u4fee\u590d\u9996\u6b21\u6253\u5f00\u9762\u677f\u65f6\u7a97\u53e3\u63a7\u5236\u80f6\u56ca\u914d\u8272\u9519\u8bef\u7684\u95ee\u9898\u2014\u2014\u65e0\u7f13\u5b58\u5916\u89c2\u65f6 data-theme \u672a\u8bbe\u7f6e\uff0c\u5bbf\u4e3b\u5728 DOMContentLoaded \u7684\u53d6\u8272\u5feb\u7167\u62ff\u5230\u6d4f\u89c8\u5668\u9ed8\u8ba4\u9ed1/\u767d\uff0c\u73b0\u540c\u6b65\u56de\u9000 OS \u914d\u8272\uff08\u5185\u5bb9\u4ecd\u6309 data-booting \u9690\u85cf\u81f3\u771f\u5b9e\u5916\u89c2\u5230\u8fbe\uff09\uff1b\u4fee\u590d startAppearanceWatch \u91cd\u7f6e\u5916\u89c2\u6307\u7eb9\u5bfc\u81f4\u6bcf\u6b21\u6253\u5f00\u547d\u4ee4\u90fd\u91cd\u590d\u5199\u5165 hostAppearance \u7684\u95ee\u9898\u3002 Release 0.4.3: Align the panel with PI-Desktop's cross-platform 46px host drag band and minimal three-button window-control capsule. Fixes two visible faults: elements toggled with the hidden attribute were being kept on screen by their own CSS display, so the loading skeleton stayed over real data, an empty chip strip and the custom date inputs were always drawn, and the last scan sentence lingered; and the from/to fields now always show the window the selected range covers, so picking 7D/90D/ALL keeps them in step and switching to Custom inherits what you were looking at. Includes 0.4.1's scan progress with a real file count, stale-scan recovery, visibility-based polling and debounced source watching, on top of 0.4.0's theme/language following, filters and streak layer.",
"version": "0.5.0",
"publishedAt": "2026-09-17T17:21:16Z",
"changelog": "Release 0.5.0: The PI-Desktop data source now goes through the official pi.usage.listTurns contract (requires a PI-Desktop build with that API; declare the usage.read permission) instead of reading the host's pi.sqlite directly, which also fixes the counting gap where soft-deleted sessions were still included. Hosts without the contract automatically fall back to the previous database path. Release 0.5.0\uff1aPI-Desktop \u6570\u636e\u6e90\u8fc1\u79fb\u5230\u6b63\u5f0f\u7684 pi.usage.listTurns \u5951\u7ea6\uff08\u9700\u5bbf\u4e3b\u63d0\u4f9b\u8be5 API\uff1b\u58f0\u660e usage.read \u6743\u9650\uff09\uff0c\u4e0d\u518d\u76f4\u8bfb\u5bbf\u4e3b pi.sqlite\uff0c\u540c\u65f6\u4fee\u590d\u8f6f\u5220\u9664\u4f1a\u8bdd\u4ecd\u88ab\u8ba1\u5165\u7684\u53e3\u5f84\u7f3a\u53e3\uff1b\u65e0\u8be5\u5951\u7ea6\u7684\u65e7\u5bbf\u4e3b\u81ea\u52a8\u56de\u9000\u539f\u6570\u636e\u5e93\u8def\u5f84\u3002 Release 0.4.8: Fold PI-Desktop completed-turn totals from the host database into the dashboard, so subagent spend that never lands on parent message.usage still appears without double-counting transcript rows. Settings no longer has a Usage page; this panel is the heatmap. Release 0.4.8\uff1a\u628a\u5bbf\u4e3b\u5df2\u5b8c\u6210\u56de\u5408\u7684 token \u5408\u8ba1\u6298\u5165\u4eea\u8868\u76d8\uff0c\u8f6c\u5f55\u672c message.usage \u672a\u8ba1\u5165\u7684\u5b50\u667a\u80fd\u4f53\u7528\u91cf\u4f1a\u8865\u4e0a\u4e14\u4e0d\u4e0e JSONL \u91cd\u590d\u3002\u8bbe\u7f6e\u9875\u4e0d\u518d\u6709\u7528\u91cf\u5165\u53e3\uff0c\u70ed\u529b\u56fe\u4ee5\u672c\u9762\u677f\u4e3a\u51c6\u3002 Release 0.4.7: Fix the extra empty right-side rail on Windows by keeping the stable scrollbar gutter on the dashboard scroller instead of reserving it again on the root viewport. Release 0.4.7\uff1a\u4fee\u590d Windows \u53f3\u4fa7\u591a\u51fa\u7a7a\u767d\u4fa7\u680f\u7684\u95ee\u9898\uff1a\u4ec5\u5728\u4eea\u8868\u76d8\u6eda\u52a8\u5bb9\u5668\u4fdd\u7559\u7a33\u5b9a\u6eda\u52a8\u6761\u69fd\u4f4d\uff0c\u4e0d\u518d\u5728\u6839\u89c6\u53e3\u91cd\u590d\u9884\u7559\u3002 Release 0.4.6: Fix the titlebar's appearance and refresh buttons overlapping the host window-control capsule. The panel paints through the 46px band under v3 chrome, but the capsule still floats over the top-right corner, so both buttons sat under it and stopped being clickable. The titlebar now reserves the capsule's 104px corner (kept at narrow widths too, where the gutter override previously reset it), the appearance menu stays anchored to the buttons that open it, and a long title truncates instead of running under the reserve. Release 0.4.6\uff1a\u4fee\u590d\u6807\u9898\u680f\u7684\u5916\u89c2\u4e0e\u5237\u65b0\u6309\u94ae\u4e0e\u5bbf\u4e3b\u7a97\u53e3\u63a7\u5236\u80f6\u56ca\u91cd\u53e0\u7684\u95ee\u9898\u3002v3 chrome \u4e0b\u9762\u677f\u53ef\u7ed8\u5236\u5230 46px \u9876\u680f\uff0c\u4f46\u80f6\u56ca\u4ecd\u6d6e\u4e8e\u53f3\u4e0a\u89d2\uff0c\u5bfc\u81f4\u4e24\u4e2a\u6309\u94ae\u88ab\u906e\u6321\u4e14\u65e0\u6cd5\u70b9\u51fb\u3002\u6807\u9898\u680f\u73b0\u4e3a\u80f6\u56ca\u4fdd\u7559 104px \u53f3\u4fa7\u7a7a\u95f4\uff08\u7a84\u5bbd\u5ea6\u4e0b\u7684 gutter \u8986\u76d6\u6b64\u524d\u4f1a\u91cd\u7f6e\u8be5\u4fdd\u7559\u503c\uff0c\u73b0\u5df2\u4fdd\u7559\uff09\uff0c\u5916\u89c2\u83dc\u5355\u7ee7\u7eed\u5bf9\u9f50\u89e6\u53d1\u6309\u94ae\uff0c\u8fc7\u957f\u6807\u9898\u4ee5\u7701\u7565\u53f7\u622a\u65ad\u3002 Release 0.4.5: Migrates the panel to v3 paint-through chrome so top-band controls remain clickable while blank space remains draggable. Release 0.4.4: Fix the window-control capsule being drawn with default black-on-white colors on a panel's very first open \u2014 with no cached appearance, data-theme stayed unset until the host answered, so the host's one-shot color snapshot at DOMContentLoaded missed; boot now falls back to the OS palette synchronously while content stays cloaked. Also stop startAppearanceWatch from resetting the appearance fingerprint, which made every open-command run rewrite hostAppearance. Release 0.4.4\uff1a\u4fee\u590d\u9996\u6b21\u6253\u5f00\u9762\u677f\u65f6\u7a97\u53e3\u63a7\u5236\u80f6\u56ca\u914d\u8272\u9519\u8bef\u7684\u95ee\u9898\u2014\u2014\u65e0\u7f13\u5b58\u5916\u89c2\u65f6 data-theme \u672a\u8bbe\u7f6e\uff0c\u5bbf\u4e3b\u5728 DOMContentLoaded \u7684\u53d6\u8272\u5feb\u7167\u62ff\u5230\u6d4f\u89c8\u5668\u9ed8\u8ba4\u9ed1/\u767d\uff0c\u73b0\u540c\u6b65\u56de\u9000 OS \u914d\u8272\uff08\u5185\u5bb9\u4ecd\u6309 data-booting \u9690\u85cf\u81f3\u771f\u5b9e\u5916\u89c2\u5230\u8fbe\uff09\uff1b\u4fee\u590d startAppearanceWatch \u91cd\u7f6e\u5916\u89c2\u6307\u7eb9\u5bfc\u81f4\u6bcf\u6b21\u6253\u5f00\u547d\u4ee4\u90fd\u91cd\u590d\u5199\u5165 hostAppearance \u7684\u95ee\u9898\u3002 Release 0.4.3: Align the panel with PI-Desktop's cross-platform 46px host drag band and minimal three-button window-control capsule. Fixes two visible faults: elements toggled with the hidden attribute were being kept on screen by their own CSS display, so the loading skeleton stayed over real data, an empty chip strip and the custom date inputs were always drawn, and the last scan sentence lingered; and the from/to fields now always show the window the selected range covers, so picking 7D/90D/ALL keeps them in step and switching to Custom inherits what you were looking at. Includes 0.4.1's scan progress with a real file count, stale-scan recovery, visibility-based polling and debounced source watching, on top of 0.4.0's theme/language following, filters and streak layer.",
"minPiDesktop": ">=0.2.9",
"shasum": "220506deb22c680074fb65856ad0b769435246a7a850184e8226f602bc9eb427",
"url": "packages/pi.token-insights-0.4.8.piplug",
"sizeBytes": 171505,
"shasum": "f252d06f15a7c8f1a5acb1ac78edf2a276fcc385cfec1a888bcf5a2bfb0b0db0",
"url": "packages/pi.token-insights-0.5.0.piplug",
"sizeBytes": 177205,
"permissions": [
"ui.panel",
"agent.tool.register"
"agent.tool.register",
"usage.read"
],
"fs": {}
}
Expand Down
Binary file added packages/pi.token-insights-0.5.0.piplug
Binary file not shown.
135 changes: 131 additions & 4 deletions plugins/pi.token-insights/host-read.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@
* them here and hands the result to the panel through plugin settings.
*
* Everything here is strictly read-only and stays on this device:
* pi.usage.listTurns (official host contract) → completed-turn usage
* ~/.pi-desktop/pi.sqlite kv(ns='app', key='app') → { theme, language }
* providers(id, name) → display names
* turns (completed usage) → subagent-inclusive remainders
* turns (completed usage) → subagent-inclusive
* remainders, only on hosts without the API
* ~/.pi-desktop/plugins/registry.json + the theme plugin's manifest + CSS
*
* No message text, tool arguments, session ids or project paths are touched, no
Expand Down Expand Up @@ -288,12 +290,135 @@ function readHostAppearance(hostRoot) {
return value;
}

/**
* Completed-turn usage from the host. Prefers the official read-only
* `pi.usage.listTurns` contract (which already excludes soft-deleted sessions
* and carries no message text); hosts without it fall back to the direct
* database read below. Either way only token counts and ids leave this module.
*/
async function readCompletedTurnUsage(hostRoot, pi) {
const listTurns = typeof pi !== "undefined" ? pi?.usage?.listTurns : undefined;
if (typeof listTurns === "function") {
try {
return await readCompletedTurnUsageViaApi(pi);
} catch (error) {
// An older or momentarily unhappy host must never cost the panel its
// PI-Desktop numbers, so the database read stays as a safety net.
warnApiFallbackOnce(error);
}
}
return readCompletedTurnUsageFromDb(hostRoot);
}

/** The largest window the contract accepts, so walks never exceed one window. */
const API_WINDOW_MS = 365 * 24 * 60 * 60 * 1000;
/** A window with no completed turns means everything older is done; 8 windows ≈ 8 years. */
const API_MAX_WINDOWS = 8;
/** The contract's page ceiling; the fewest round trips per window. */
const API_PAGE_LIMIT = 500;

let apiFallbackWarned = false;

function warnApiFallbackOnce(error) {
if (apiFallbackWarned) return;
apiFallbackWarned = true;
try {
console.warn(
`[pi.token-insights] pi.usage.listTurns failed (${String(error?.message || error)}); reading the host database instead.`,
);
} catch {
/* a broken console must not break the scan */
}
}

/**
* Walks completed turns backward through the official `pi.usage.listTurns`
* contract: at most 365 days per window, every page of each window via the
* keyset cursor, and the walk stops at the first window with no rows at all.
* A `turnId` set dedupes across windows so a turn straddling a boundary can
* never be counted twice. Returns the same `{ events, diagnostics }` shape as
* the database reader, with `filesScanned` counting pages fetched.
*/
async function readCompletedTurnUsageViaApi(pi) {
const result = {
events: [],
diagnostics: { sourceId: "pi-desktop", filesScanned: 0, filesSkipped: 0, malformedLines: 0, usageMessages: 0 },
};
const seenTurnIds = new Set();
let toMs = Date.now();
for (let windowIndex = 0; windowIndex < API_MAX_WINDOWS; windowIndex += 1) {
// Inclusive endpoints, so the span stays within the contract's 365-day cap.
const fromMs = toMs - API_WINDOW_MS + 1;
let cursor = null;
let rowsInWindow = 0;
do {
const input = { fromMs, toMs, limit: API_PAGE_LIMIT };
// `cursor` is optional in the contract; a strict host may reject null.
if (cursor) input.cursor = cursor;
const page = await pi.usage.listTurns(input);
result.diagnostics.filesScanned += 1;
for (const row of page?.turns || []) {
rowsInWindow += 1;
const turnId = String(row?.turnId || "");
if (turnId) {
if (seenTurnIds.has(turnId)) continue;
seenTurnIds.add(turnId);
}
const event = turnEventFromApiRow(row);
if (!event) continue;
result.events.push(event);
result.diagnostics.usageMessages += 1;
}
cursor = page?.nextCursor || null;
} while (cursor);
if (rowsInWindow === 0) break;
toMs = fromMs - 1;
}
return result;
}

/**
* One contract row → the event shape the aggregator already consumes.
* The contract carries no `total`, so it is the sum of the five components;
* rows with no tokens at all are skipped, exactly like the database reader.
*/
function turnEventFromApiRow(row) {
const n = (value) => {
const parsedNumber = Number(value);
return Number.isFinite(parsedNumber) ? Math.max(0, parsedNumber) : 0;
};
const input = n(row?.inputTokens);
const output = n(row?.outputTokens);
const cacheRead = n(row?.cacheReadTokens);
const cacheWrite = n(row?.cacheWriteTokens);
const reasoning = n(row?.reasoningTokens);
if (!input && !output && !cacheRead && !cacheWrite && !reasoning) return null;
const timestamp = Number(row?.endedAt);
const sessionId = String(row?.sessionId || "").trim();
if (!sessionId || !Number.isFinite(timestamp)) return null;
return {
sourceId: "pi-desktop",
sessionId: `pi-desktop:${sessionId}`,
timestamp,
modelId: String(row.modelId || "Unknown model"),
providerId: String(row.providerId || "Unknown provider"),
tokens: {
input,
output,
cacheRead,
cacheWrite,
reasoning,
total: input + output + cacheRead + cacheWrite + reasoning,
},
};
}

/**
* Completed-turn usage from the host database. Only token columns and ids;
* no message text. Used to fold subagent spend that never landed on
* transcript `message.usage` into the PI-Desktop scan.
* no message text. Fallback for hosts without `pi.usage.listTurns` — the API
* path is preferred because it also excludes soft-deleted sessions.
*/
function readCompletedTurnUsage(hostRoot) {
function readCompletedTurnUsageFromDb(hostRoot) {
const result = {
events: [],
diagnostics: { sourceId: "pi-desktop", filesScanned: 0, filesSkipped: 0, malformedLines: 0, usageMessages: 0 },
Expand Down Expand Up @@ -362,6 +487,8 @@ function tokensFromTurnRow(row) {
module.exports = {
hostRootFromDataPath,
readCompletedTurnUsage,
readCompletedTurnUsageFromDb,
readCompletedTurnUsageViaApi,
readHostAppearance,
readProviderLabels,
__test: {
Expand Down
4 changes: 3 additions & 1 deletion plugins/pi.token-insights/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,9 @@ async function scanEvents(progress) {
scanCodexDirectory(roots.codex, progress),
scanOpenCodeDirectory(roots.openCode, progress),
]);
const turns = readCompletedTurnUsage(host);
// The official pi.usage.listTurns contract when the host offers it, with the
// direct database read kept inside as the fallback for older hosts.
const turns = await readCompletedTurnUsage(host, pi);
const piDesktop = mergePiDesktopTurnRemainder(piJsonl, turns.events);
if (turns.diagnostics?.filesScanned) {
piDesktop.diagnostics.filesScanned += turns.diagnostics.filesScanned;
Expand Down
Loading
Loading