Greasy Fork is available in English.
在 OpenCode 工作区页面把「已用用量」改为显示「剩余用量」(left),进度条同步反转为剩余量。
// ==UserScript==
// @name OpenCode Workspace — Show Usage Left
// @name:zh-CN OpenCode 工作区 — 显示剩余用量
// @namespace https://opencode.ai/
// @version 0.0.1
// @description:en Shows remaining usage (left) instead of used percentage on OpenCode workspace pages.
// @description:zh-CN 在 OpenCode 工作区页面把「已用用量」改为显示「剩余用量」(left),进度条同步反转为剩余量。
// @description On OpenCode workspace pages, show remaining usage (left) instead of used percentage.
// @author OpenCode (DeepSeek-V4-Flash-0731)
// @match https://opencode.ai/workspace/*
// @grant none
// @run-at document-idle
// @license MIT
// ==/UserScript==
(function () {
"use strict";
const dataDesc = Object.getOwnPropertyDescriptor(CharacterData.prototype, "data");
const nodeValueDesc = Object.getOwnPropertyDescriptor(Node.prototype, "nodeValue");
function scanValueNodes(span) {
let numeric = null;
let pct = null;
const nodes = span.childNodes;
for (let i = 0; i < nodes.length; i++) {
const n = nodes[i];
if (n.nodeType !== Node.TEXT_NODE) continue;
if (n.data.trim().startsWith("%")) pct = n;
else if (/^\d/.test(n.data)) numeric = n;
if (numeric && pct) break;
}
return [numeric, pct];
}
function convertSpan(span) {
if (!span) return;
const [node, pctNode] = scanValueNodes(span);
if (!node) return;
if (pctNode && pctNode.data !== "% left" && dataDesc && dataDesc.set) {
dataDesc.set.call(pctNode, "% left");
}
const p = parseFloat(node.data);
if (Number.isNaN(p)) return;
let used = span.__usageLeft;
if (used === undefined) {
used = p;
span.__usageLeft = used;
} else if (Math.abs(p + used - 100) > 0.01) {
used = p;
span.__usageLeft = used;
}
const left = Math.max(0, Math.round((100 - used) * 10) / 10);
let out = String(left);
if (!pctNode) out = out + (node.data.includes("%") ? "% left" : " left");
if (node.data !== out && dataDesc && dataDesc.set) {
dataDesc.set.call(node, out);
}
const item = span.closest('[data-slot="usage-item"]');
if (item) {
const bar = item.querySelector('[data-slot="progress-bar"]');
if (bar) {
const w = left + "%";
if (bar.style.width !== w) bar.style.width = w;
}
}
}
function scan() {
document.querySelectorAll('[data-slot="usage-value"]').forEach(convertSpan);
}
if (dataDesc && dataDesc.set) {
Object.defineProperty(CharacterData.prototype, "data", {
get: dataDesc.get,
set: function (value) {
dataDesc.set.call(this, value);
if (
this.nodeType === Node.TEXT_NODE &&
this.parentElement &&
this.parentElement.getAttribute("data-slot") === "usage-value"
) {
convertSpan(this.parentElement);
}
},
configurable: true,
});
}
if (nodeValueDesc && nodeValueDesc.set) {
Object.defineProperty(Node.prototype, "nodeValue", {
get: nodeValueDesc.get,
set: function (value) {
nodeValueDesc.set.call(this, value);
if (
this.nodeType === Node.TEXT_NODE &&
this.parentElement &&
this.parentElement.getAttribute("data-slot") === "usage-value"
) {
convertSpan(this.parentElement);
}
},
configurable: true,
});
}
const observer = new MutationObserver(scan);
observer.observe(document.documentElement, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ["style"],
});
scan();
setInterval(scan, 2000);
})();