diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 5279f478..68e3d61d 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -30,7 +30,7 @@ Keep the section headings; fill what applies, delete what doesn't.
- [ ] `npm run compile` is clean
- [ ] User-facing strings go through `I18n` with **all eight UI locales** filled
-- [ ] Behaviour/documentation changes update **all seven README editions** together
+- [ ] Behaviour/documentation changes update **all nine README files** together (main page and eight locale editions)
- [ ] `CHANGELOG.md` updated (user-visible changes)
- [ ] `README.md` updated if behaviour/settings changed
- [ ] New settings default to existing behaviour (opt-in)
diff --git a/.github/scripts/automationPolicy.test.mjs b/.github/scripts/automationPolicy.test.mjs
index 176e123e..2031643d 100644
--- a/.github/scripts/automationPolicy.test.mjs
+++ b/.github/scripts/automationPolicy.test.mjs
@@ -127,13 +127,35 @@ test('release delivery retries safely and does not let one registry block the ot
assert.doesNotMatch(workflow, /if gh release download/);
assert.equal((workflow.match(/steps\.restore_package\.outputs\.restored != 'true'/g) ?? []).length, 4);
assert.match(workflow, /tag_name: \$\{\{ env\.RELEASE_TAG \}\}/);
- assert.match(workflow, /@vscode\/vsce@3\.9\.2 publish[\s\S]*?--skip-duplicate/);
+ assert.match(workflow, /@vscode\/vsce@4\.0\.0 publish[\s\S]*?--skip-duplicate/);
assert.match(workflow, /ovsx@1\.2\.0 publish[\s\S]*?--skip-duplicate/);
assert.equal((workflow.match(/continue-on-error: true/g) ?? []).length, 3);
assert.equal((workflow.match(/for attempt in 1 2 3/g) ?? []).length, 2);
assert.equal((workflow.match(/timeout-minutes: 12/g) ?? []).length, 2);
});
+test('Marketplace recovery reconciles the newest stable release without rebuilding it', () => {
+ const publish = read('.github/workflows/publish.yml');
+ const recovery = read('.github/workflows/marketplace-recovery.yml');
+
+ assert.match(publish, /concurrency:\s*\n\s+group: extension-registry-delivery/);
+ assert.match(recovery, /schedule:[\s\S]*?- cron: '37 \* \* \* \*'/);
+ assert.match(recovery, /workflow_dispatch:/);
+ assert.match(recovery, /permissions:\s*\n\s+contents: read/);
+ assert.match(recovery, /concurrency:\s*\n\s+group: extension-registry-delivery/);
+ assert.match(recovery, /releases\/latest/);
+ assert.match(recovery, /gh release download[\s\S]*?claude-code-usage\.vsix/);
+ assert.match(recovery, /verify-vsix\.mjs[\s\S]*?steps\.release\.outputs\.version/);
+ assert.doesNotMatch(recovery, /npm ci|npm run compile|vsce@4\.0\.0 package/);
+ assert.match(recovery, /vsce@4\.0\.0 show GrowthJack\.claude-code-usage --json/);
+ assert.match(recovery, /Microsoft\.VisualStudio\.Services\.VsixSha256/);
+ assert.match(recovery, /sha256sum claude-code-usage\.vsix/);
+ assert.match(recovery, /steps\.probe\.outputs\.needed == 'true'/);
+ assert.match(recovery, /vsce@4\.0\.0 publish[\s\S]*?--skip-duplicate/);
+ assert.match(recovery, /continue-on-error: true/);
+ assert.match(recovery, /recovery will retry next hour/i);
+});
+
test('release draft gets a post-merge reconciliation pass', () => {
const workflow = read('.github/workflows/release-drafter.yml');
assert.match(
diff --git a/.github/scripts/first-pass-lib.mjs b/.github/scripts/first-pass-lib.mjs
index b121bce1..0d7f4c8c 100644
--- a/.github/scripts/first-pass-lib.mjs
+++ b/.github/scripts/first-pass-lib.mjs
@@ -387,11 +387,17 @@ export function hasTrustedFirstPass(comments, kind) {
);
}
-export async function resolveFirstPassCandidates({ cheap, pro }) {
+export async function resolveFirstPassCandidates({ cheap, pro, onFailure = () => {} }) {
let cheapCandidate;
try {
cheapCandidate = await cheap();
- } catch {
+ if (!String(cheapCandidate?.reply || '').trim()) {
+ onFailure({ tier: 'cheap', reason: 'empty-reply' });
+ } else if (cheapCandidate.answerable === false) {
+ onFailure({ tier: 'cheap', reason: 'needs-source' });
+ }
+ } catch (error) {
+ onFailure({ tier: 'cheap', reason: safeFailureReason(error) });
cheapCandidate = null;
}
@@ -399,7 +405,11 @@ export async function resolveFirstPassCandidates({ cheap, pro }) {
if (!cheapCandidate || needsProFirstPass(cheapCandidate)) {
try {
proCandidate = await pro(cheapCandidate);
- } catch {
+ if (!String(proCandidate?.reply || '').trim()) {
+ onFailure({ tier: 'pro', reason: 'empty-reply' });
+ }
+ } catch (error) {
+ onFailure({ tier: 'pro', reason: safeFailureReason(error) });
proCandidate = null;
}
}
@@ -411,11 +421,18 @@ export async function resolveFirstPassCandidates({ cheap, pro }) {
}
}
+function safeFailureReason(error) {
+ const status = error?.status;
+ return Number.isInteger(status) && status >= 400 && status <= 599
+ ? `http-${status}`
+ : 'request-error';
+}
+
export function chooseFinalReply(cheap, pro) {
if (pro && typeof pro.reply === 'string' && pro.reply.trim() !== '') {
return { reply: pro.reply.trim(), generator: pro.generator };
}
- if (cheap && typeof cheap.reply === 'string' && cheap.reply.trim() !== '') {
+ if (cheap?.answerable !== false && typeof cheap?.reply === 'string' && cheap.reply.trim() !== '') {
return { reply: cheap.reply.trim(), generator: cheap.generator };
}
throw new Error('No non-empty model reply');
diff --git a/.github/scripts/first-pass.mjs b/.github/scripts/first-pass.mjs
index 9fddcf0d..ebf20237 100644
--- a/.github/scripts/first-pass.mjs
+++ b/.github/scripts/first-pass.mjs
@@ -27,7 +27,7 @@ const REPO_ROOT = resolve(process.cwd());
const env = process.env;
const base = (env.ANTHROPIC_BASE_URL || 'https://api.anthropic.com').replace(/\/$/, '');
const githubApi = (env.GITHUB_API_URL || 'https://api.github.com').replace(/\/$/, '');
-const model = env.CCU_BOT_MODEL || 'deepseek-v4-flash';
+const model = env.CCU_BOT_MODEL || 'deepseek-flash';
const modelPro = env.CCU_BOT_MODEL_PRO || 'deepseek-v4-pro';
const fail = (message) => {
@@ -119,14 +119,20 @@ const buildUser = (extraFiles) =>
`\n\n--- PROJECT DOCS ---\n${docs}` +
(extraFiles ? `\n\n--- REPO SOURCE FILES (read-only) ---\n${extraFiles}` : '');
-async function askModel(useModel, system, userText, think = false) {
+async function askModel(useModel, generator, system, userText, think = false) {
const body = {
model: useModel,
- max_tokens: 1400,
+ max_tokens: think ? 8192 : 1800,
system,
messages: [{ role: 'user', content: userText }],
};
- if (think) body.thinking = { type: 'enabled', budget_tokens: 6000 };
+ // DeepSeek defaults to reasoning even on the cheap tier. Reserve that tier's
+ // output budget for the actual reply; the pro tier may reason first.
+ if (generator.id === 'deepseek') {
+ body.thinking = { type: think ? 'enabled' : 'disabled' };
+ } else if (think) {
+ body.thinking = { type: 'enabled', budget_tokens: 6000 };
+ }
let response = await fetch(`${base}/v1/messages`, {
method: 'POST',
@@ -151,11 +157,11 @@ async function askModel(useModel, system, userText, think = false) {
body: JSON.stringify(body),
});
} else {
- throw new Error(`Model API error ${response.status}: ${errorText.slice(0, 500)}`);
+ throw Object.assign(new Error('Model API request failed'), { status: response.status });
}
}
if (!response.ok) {
- throw new Error(`Model API error ${response.status}: ${(await response.text()).slice(0, 500)}`);
+ throw Object.assign(new Error('Model API request failed'), { status: response.status });
}
const data = await response.json();
return (data.content || [])
@@ -186,20 +192,22 @@ try {
fail(`Comment lookup failed: ${error.message}`);
}
+const modelFailures = [];
const selected = await resolveFirstPassCandidates({
cheap: async () => ({
- ...parseFirstPassResponse(await askModel(model, buildSystem(false), buildUser(''))),
+ ...parseFirstPassResponse(await askModel(model, cheapGenerator, buildSystem(false), buildUser(''))),
generator: cheapGenerator,
}),
pro: async (cheapCandidate) => {
const extra = repoReader.read(cheapCandidate?.want_files || []).text;
return {
...parseFirstPassResponse(
- await askModel(modelPro, buildSystem(true), buildUser(extra), true),
+ await askModel(modelPro, proGenerator, buildSystem(true), buildUser(extra), true),
),
generator: proGenerator,
};
},
+ onFailure: (event) => modelFailures.push(event),
});
let commentBody;
@@ -213,11 +221,17 @@ if (selected) {
fail(`First-pass formatting failed: ${error.message}`);
}
} else {
+ const summary = ['cheap', 'pro']
+ .map((tier) => `${tier}=${modelFailures.find((event) => event.tier === tier)?.reason || 'no-usable-reply'}`)
+ .join(' ');
+ if (isPr) {
+ fail(`::error::First-pass PR review unavailable: ${summary}. No comment was posted.`);
+ }
const authorWroteInChinese = /[\u3400-\u9fff]/u.test(
`${env.ITEM_TITLE || ''}\n${env.ITEM_BODY || ''}`,
);
commentBody = formatFirstPassFallback({ kind, isChinese: authorWroteInChinese });
- console.warn(`Both model tiers failed to return a usable first-pass ${kind}; posting fallback.`);
+ console.warn(`Both model tiers failed to return a usable first-pass ${kind} (${summary}); posting fallback.`);
}
try {
diff --git a/.github/scripts/first-pass.test.mjs b/.github/scripts/first-pass.test.mjs
index 21ac6bac..be108e5c 100644
--- a/.github/scripts/first-pass.test.mjs
+++ b/.github/scripts/first-pass.test.mjs
@@ -28,14 +28,18 @@ import {
const ANTHROPIC_MESSAGES = 'anthropic-messages';
const REPO_ROOT = resolve(import.meta.dirname, '..', '..');
-async function runFallbackScenario(eventKind) {
+async function runFallbackScenario(eventKind, modelResponse = { content: [] }) {
const postedComments = [];
let modelCalls = 0;
+ const modelRequests = [];
const server = createServer(async (request, response) => {
if (request.method === 'POST' && request.url === '/v1/messages') {
modelCalls += 1;
- response.writeHead(200, { 'content-type': 'application/json' });
- response.end(JSON.stringify({ content: [] }));
+ const chunks = [];
+ for await (const chunk of request) chunks.push(chunk);
+ modelRequests.push(JSON.parse(Buffer.concat(chunks).toString('utf8')));
+ response.writeHead(modelResponse.status || 200, { 'content-type': 'application/json' });
+ response.end(JSON.stringify(modelResponse.body || modelResponse));
return;
}
if (request.method === 'GET' && request.url?.endsWith('/comments?per_page=100')) {
@@ -90,25 +94,49 @@ async function runFallbackScenario(eventKind) {
child.stdout.on('data', (chunk) => { stdout += chunk; });
child.stderr.on('data', (chunk) => { stderr += chunk; });
const exitCode = await new Promise((accept) => child.on('close', accept));
- return { exitCode, modelCalls, postedComments, stdout, stderr };
+ return { exitCode, modelCalls, modelRequests, postedComments, stdout, stderr };
} finally {
rmSync(fixtureRoot, { recursive: true, force: true });
await new Promise((accept) => server.close(accept));
}
}
-test('shared runner posts a deterministic fallback for issue and PR model failures', async () => {
- for (const eventKind of ['issue', 'pr']) {
- const result = await runFallbackScenario(eventKind);
- assert.equal(result.exitCode, 0, result.stderr);
- assert.equal(result.modelCalls, 2);
- assert.equal(result.postedComments.length, 1);
- assert.match(
- result.postedComments[0],
- new RegExp(`^🤖 Automated first-pass ${eventKind === 'issue' ? 'reply' : 'review'}`),
- );
- assert.match(result.postedComments[0], /not a maintainer decision/);
- }
+test('issue runner retains its fallback, but failed PR review posts nothing and fails visibly', async () => {
+ const issue = await runFallbackScenario('issue');
+ assert.equal(issue.exitCode, 0, issue.stderr);
+ assert.equal(issue.modelCalls, 2);
+ assert.match(issue.postedComments[0], /^🤖 Automated first-pass reply/);
+
+ const pr = await runFallbackScenario('pr');
+ assert.notEqual(pr.exitCode, 0);
+ assert.equal(pr.modelCalls, 2);
+ assert.equal(pr.postedComments.length, 0);
+ assert.match(pr.stderr, /cheap=empty-reply.*pro=empty-reply/);
+ assert.ok(pr.modelRequests[1].max_tokens > 6000);
+ assert.equal(pr.modelRequests[1].thinking?.type, 'enabled');
+ assert.doesNotMatch(pr.stderr, /test-model-key|test-github-token/);
+});
+
+test('API failures report status and tier, never remote body or credentials', async () => {
+ const result = await runFallbackScenario('pr', {
+ status: 401,
+ body: { error: 'sensitive untrusted upstream response' },
+ });
+ assert.notEqual(result.exitCode, 0);
+ assert.equal(result.postedComments.length, 0);
+ assert.match(result.stderr, /cheap=http-401.*pro=http-401/);
+ assert.doesNotMatch(result.stderr, /sensitive untrusted|test-model-key|test-github-token/);
+});
+
+test('DeepSeek tiers have compatible output budgets and reserve the cheap tier for a reply', async () => {
+ const result = await runFallbackScenario('pr', {
+ content: [{ type: 'text', text: '{"answerable":true}Specific finding in src/a.ts.' }],
+ });
+ assert.equal(result.exitCode, 0, result.stderr);
+ assert.equal(result.modelCalls, 1);
+ assert.equal(result.modelRequests[0].thinking?.type, 'disabled');
+ assert.ok(result.modelRequests[0].max_tokens >= 1400);
+ assert.match(result.postedComments[0], /Specific finding in src\/a\.ts/);
});
test('empty cheap replies escalate even when the control says answerable', () => {
@@ -169,6 +197,20 @@ test('candidate resolution returns null instead of throwing when both tiers fail
assert.equal(selected, null);
});
+test('an unanswered cheap draft is not posted when the pro tier fails', async () => {
+ const failures = [];
+ const selected = await resolveFirstPassCandidates({
+ cheap: async () => ({ reply: 'Need source', answerable: false }),
+ pro: async () => { throw new Error('secret remote text'); },
+ onFailure: (event) => failures.push(event),
+ });
+ assert.equal(selected, null);
+ assert.deepEqual(failures, [
+ { tier: 'cheap', reason: 'needs-source' },
+ { tier: 'pro', reason: 'request-error' },
+ ]);
+});
+
test('current transport truthfully supports DeepSeek and Claude per tier', () => {
const cheap = resolveGeneratorAttribution(undefined, 'deepseek-v4-flash', ANTHROPIC_MESSAGES);
const pro = resolveGeneratorAttribution('claude', 'claude-sonnet-4-5', ANTHROPIC_MESSAGES);
diff --git a/.github/workflows/marketplace-recovery.yml b/.github/workflows/marketplace-recovery.yml
new file mode 100644
index 00000000..1dad3806
--- /dev/null
+++ b/.github/workflows/marketplace-recovery.yml
@@ -0,0 +1,145 @@
+# The VS Code Marketplace Gallery API can remain unavailable for several hours.
+# Keep the release workflow honest (it still fails when delivery fails), then
+# reconcile the newest stable GitHub Release in the background. The canonical
+# Release VSIX is reused and verified; no package is rebuilt here.
+
+name: Recover VS Code Marketplace delivery
+
+on:
+ schedule:
+ # Avoid the top of the hour, when scheduled Actions and public services are
+ # commonly busiest. A successful no-op probe is cheap and idempotent.
+ - cron: '37 * * * *'
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+concurrency:
+ group: extension-registry-delivery
+ cancel-in-progress: false
+
+jobs:
+ recover:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout current release policy
+ uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
+
+ - name: Setup Node.js
+ uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
+ with:
+ node-version: '22'
+
+ - name: Resolve newest stable GitHub Release
+ id: release
+ run: |
+ TAG="$(gh api "repos/$GITHUB_REPOSITORY/releases/latest" --jq '.tag_name')"
+ if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
+ echo "Unexpected latest stable release tag: $TAG" >&2
+ exit 1
+ fi
+ echo "tag=$TAG" >> "$GITHUB_OUTPUT"
+ echo "version=${TAG#v}" >> "$GITHUB_OUTPUT"
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Download canonical Release VSIX
+ run: |
+ gh release download "${{ steps.release.outputs.tag }}" \
+ --pattern claude-code-usage.vsix \
+ --clobber
+ test -s claude-code-usage.vsix
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Verify canonical Release VSIX
+ run: >-
+ node .github/scripts/verify-vsix.mjs
+ claude-code-usage.vsix
+ "${{ steps.release.outputs.version }}"
+
+ - name: Probe Marketplace delivery
+ id: probe
+ run: |
+ set +e
+ npx -y @vscode/vsce@4.0.0 show GrowthJack.claude-code-usage --json \
+ > marketplace-extension.json 2> marketplace-probe.err
+ probe_status=$?
+ set -e
+
+ if [[ "$probe_status" != "0" ]]; then
+ echo "available=false" >> "$GITHUB_OUTPUT"
+ echo "needed=false" >> "$GITHUB_OUTPUT"
+ echo "::warning::Marketplace probe is unavailable; recovery will retry next hour."
+ cat marketplace-probe.err >&2
+ exit 0
+ fi
+
+ echo "available=true" >> "$GITHUB_OUTPUT"
+ version='${{ steps.release.outputs.version }}'
+ if ! jq -e --arg version "$version" \
+ '[.versions[]? | select(.version == $version)] | length > 0' \
+ marketplace-extension.json >/dev/null; then
+ echo "needed=true" >> "$GITHUB_OUTPUT"
+ exit 0
+ fi
+
+ published_sha="$(jq -r --arg version "$version" '
+ [.versions[]? | select(.version == $version) | .properties[]? |
+ select(.key == "Microsoft.VisualStudio.Services.VsixSha256") | .value][0] // empty
+ ' marketplace-extension.json)"
+ local_sha="$(sha256sum claude-code-usage.vsix | cut -d' ' -f1)"
+ if [[ -z "$published_sha" ]]; then
+ echo "Marketplace version $version has no VSIX SHA-256 property" >&2
+ exit 1
+ fi
+ if [[ "${published_sha,,}" != "${local_sha,,}" ]]; then
+ echo "Marketplace VSIX SHA-256 mismatch for $version" >&2
+ echo "release: $local_sha" >&2
+ echo "marketplace: $published_sha" >&2
+ exit 1
+ fi
+
+ echo "needed=false" >> "$GITHUB_OUTPUT"
+ echo "Marketplace already contains the canonical $version VSIX."
+
+ - name: Recover missing Marketplace version
+ id: publish
+ if: steps.probe.outputs.available == 'true' && steps.probe.outputs.needed == 'true'
+ continue-on-error: true
+ timeout-minutes: 5
+ run: >-
+ npx -y @vscode/vsce@4.0.0 publish
+ --packagePath claude-code-usage.vsix
+ --skip-duplicate
+ --pat "$VSCE_PAT"
+ env:
+ VSCE_PAT: ${{ secrets.VSCE_PAT }}
+
+ - name: Record recovery result
+ if: always()
+ run: |
+ if [[ "$PROBE_OUTCOME" != "success" ]]; then
+ echo "### Marketplace recovery preflight failed" >> "$GITHUB_STEP_SUMMARY"
+ echo "The canonical asset or integrity check failed; no publish was attempted." >> "$GITHUB_STEP_SUMMARY"
+ elif [[ "$PROBE_AVAILABLE" != "true" ]]; then
+ echo "### Marketplace recovery deferred" >> "$GITHUB_STEP_SUMMARY"
+ echo "The Gallery API probe was unavailable. The next hourly run will retry." >> "$GITHUB_STEP_SUMMARY"
+ elif [[ "$PUBLISH_NEEDED" != "true" ]]; then
+ echo "### Marketplace delivery is current" >> "$GITHUB_STEP_SUMMARY"
+ echo "The latest stable GitHub Release is already published with the expected SHA-256." >> "$GITHUB_STEP_SUMMARY"
+ elif [[ "$PUBLISH_OUTCOME" == "success" ]]; then
+ echo "### Marketplace delivery recovered" >> "$GITHUB_STEP_SUMMARY"
+ echo "Published $RELEASE_TAG from its canonical GitHub Release VSIX." >> "$GITHUB_STEP_SUMMARY"
+ else
+ echo "### Marketplace recovery will retry" >> "$GITHUB_STEP_SUMMARY"
+ echo "The publish attempt failed. The next hourly run will reuse the same verified VSIX." >> "$GITHUB_STEP_SUMMARY"
+ echo "::warning::Marketplace publish failed; scheduled recovery will retry next hour."
+ fi
+ env:
+ PROBE_OUTCOME: ${{ steps.probe.outcome }}
+ PROBE_AVAILABLE: ${{ steps.probe.outputs.available }}
+ PUBLISH_NEEDED: ${{ steps.probe.outputs.needed }}
+ PUBLISH_OUTCOME: ${{ steps.publish.outcome }}
+ RELEASE_TAG: ${{ steps.release.outputs.tag }}
diff --git a/.github/workflows/pr-first-pass.yml b/.github/workflows/pr-first-pass.yml
index 8c2db602..917d0664 100644
--- a/.github/workflows/pr-first-pass.yml
+++ b/.github/workflows/pr-first-pass.yml
@@ -1,9 +1,9 @@
-# Auto first-pass review on every NEW pull request — a CONTROLLED single model
-# call (not an autonomous agent). Uses pull_request_target for the token but
+# Auto first-pass review on every NEW external pull request — a CONTROLLED
+# cheap/pro model sequence (not an autonomous agent). Uses pull_request_target for the token but
# NEVER runs the PR's code: it checks out the BASE repo, fetches the diff via the
# API, asks the model, and posts one comment. Public text remains untrusted; the
# fixed runner has no tools, reads only validated base-repo text, and model
-# output can still be wrong.
+# output can still be wrong. Failed analysis leaves no generic PR comment.
#
# INERT until AUTOMATION_ENABLED = 'true' + the ANTHROPIC_API_KEY secret.
# Optional repo vars: ANTHROPIC_BASE_URL, CCU_BOT_MODEL / CCU_BOT_MODEL_PRO and
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index d62a3384..3fb13bd3 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -45,6 +45,10 @@ on:
permissions:
contents: write # needed by softprops/action-gh-release to attach the .vsix asset
+concurrency:
+ group: extension-registry-delivery
+ cancel-in-progress: false
+
jobs:
publish:
runs-on: ubuntu-latest
@@ -115,7 +119,7 @@ jobs:
- name: Package .vsix
if: github.event_name == 'release' || steps.restore_package.outputs.restored != 'true'
- run: npx -y @vscode/vsce@3.9.2 package --out claude-code-usage.vsix
+ run: npx -y @vscode/vsce@4.0.0 package --out claude-code-usage.vsix
# Some historical release tags predate the package verifier. Load the
# current workflow's policy only after packaging so policy files can
@@ -155,7 +159,7 @@ jobs:
timeout-minutes: 12
run: |
for attempt in 1 2 3; do
- if npx -y @vscode/vsce@3.9.2 publish --packagePath claude-code-usage.vsix --skip-duplicate --pat "$VSCE_PAT"; then
+ if npx -y @vscode/vsce@4.0.0 publish --packagePath claude-code-usage.vsix --skip-duplicate --pat "$VSCE_PAT"; then
exit 0
fi
if [[ "$attempt" == "3" ]]; then
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 0cd68a11..55796928 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -70,13 +70,14 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
- node-version: '20'
+ # @vscode/vsce 4 requires Node >=22.
+ node-version: '22'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Compile
run: npm run compile
- name: Package smoke VSIX
- run: npx -y @vscode/vsce@3.9.2 package --out /tmp/claude-code-usage-ci.vsix
+ run: npx -y @vscode/vsce@4.0.0 package --out /tmp/claude-code-usage-ci.vsix
- name: Verify smoke VSIX
run: node .github/scripts/verify-vsix.mjs /tmp/claude-code-usage-ci.vsix
diff --git a/AGENTS.md b/AGENTS.md
index 3ed71d86..fead8850 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -150,9 +150,9 @@ A static assertion of the first render is not acceptance evidence.
- Every user-facing string goes through `I18n` with all eight UI locales:
`en`, `de-DE`, `zh-TW`, `zh-CN`, `ja`, `ko`, `pt-BR`, and `id`.
-- Update all seven README files together: `README.md`, `README-en.md`,
- `README-zh-CN.md`, `README-zh-TW.md`, `README-ja.md`, `README-ko.md`, and
- `README-id.md`.
+- Update all nine README files together: `README.md`, `README-en.md`,
+ `README-de-DE.md`, `README-zh-CN.md`, `README-zh-TW.md`, `README-ja.md`,
+ `README-ko.md`, `README-pt-BR.md`, and `README-id.md`.
- For every artifact submitted for maintainer review—including a spec,
implementation plan, design, release checklist, policy, or substantial
contributor-document change—provide a faithful Chinese sibling or review
@@ -204,7 +204,7 @@ An unreviewed automated Codex first pass uses exactly:
- Attribution must name the actual generator. The current first-pass runner
defaults to DeepSeek and must not be relabelled Codex.
- The wrapper, not model output, owns the trusted footer and emits exactly one.
-- Credit Claude Code and OpenAI Codex as development tools in all seven README
+- Credit Claude Code and OpenAI Codex as development tools in all nine README
files. Do not put tools in Release Drafter's human contributor list. A commit
mainly written by OpenAI Codex must include this exact trailer:
diff --git a/AGENTS.zh-CN.md b/AGENTS.zh-CN.md
index 86e2e667..38ae0bb0 100644
--- a/AGENTS.zh-CN.md
+++ b/AGENTS.zh-CN.md
@@ -126,8 +126,9 @@ webview 刷新为整页替换。任何新增的用户可交互状态(展开/
- 每个用户可见字符串都通过 `I18n` 覆盖八个 UI locale:`en`、`de-DE`、
`zh-TW`、`zh-CN`、`ja`、`ko`、`pt-BR`、`id`。
-- 七份 README 必须一起更新:`README.md`、`README-en.md`、`README-zh-CN.md`、
- `README-zh-TW.md`、`README-ja.md`、`README-ko.md`、`README-id.md`。
+- 九份 README 必须一起更新:`README.md`、`README-en.md`、`README-de-DE.md`、
+ `README-zh-CN.md`、`README-zh-TW.md`、`README-ja.md`、`README-ko.md`、
+ `README-pt-BR.md`、`README-id.md`。
- 每份交给维护者审阅的 artifact(包括 spec、implementation plan、design、
release checklist、policy 或大幅 contributor 文档变更),都提供忠实中文副本
或中文 review companion,并确保交付时中文链接排在英文链接之前。
@@ -168,7 +169,7 @@ webview 刷新为整页替换。任何新增的用户可交互状态(展开/
- 归因必须写实际 generator。当前 first-pass runner 默认使用 DeepSeek,不得改标 Codex。
- trusted footer 由 wrapper 而不是模型输出负责,并且只能出现一次。
-- 在七份 README 中把 Claude Code 与 OpenAI Codex 记为开发工具;不得把工具放入
+- 在九份 README 中把 Claude Code 与 OpenAI Codex 记为开发工具;不得把工具放入
Release Drafter 的人类 contributor 列表。主要撰写代码的 OpenAI Codex commit 必须
带以下精确 trailer:
diff --git a/ARCHITECTURE-zh-CN.md b/ARCHITECTURE-zh-CN.md
index 53af4e25..a42fd6a9 100644
--- a/ARCHITECTURE-zh-CN.md
+++ b/ARCHITECTURE-zh-CN.md
@@ -13,15 +13,19 @@ v2.3.0 保留完整 Claude 体验,并增加 provider-specific 的 Codex Beta
v2.3.1 候选在不改变 provider 计量口径的前提下,增加一套默认关闭、
local-first 的建议有效性闭环、唯一的显式 BYOK 请求边界、可持久的历史任务状态、
Codex 有界的本地每周重置观测历史,以及滚动 30 天的日期→小时投影。
+v2.4 第一阶段把既有分享界面收敛为一个预览优先的工作台。呈现方式选择器可切换
+综合活动热力图、旧版 Claude 分享卡与仅 Claude 的 token 热力图;全宽导出预览始终位于控制项之前。
- Claude:精确的本地 token bucket、模型成本估算和 Anthropic OAuth 5 小时/每周配额。
-- Codex Beta:本地 processed/fresh/cache/output/reasoning 指标、模型与 effort 拆分、
+- Codex:本地 processed/fresh/cache/output/reasoning 指标、模型与 effort 拆分、
thread 结构、索引 coverage、quality flag 与结构化优化建议。已知模型还会显示明确限定的
API 等效成本估算;它绝不是账单或订阅扣费,未知模型保持未定价。
- Compare:只并列可比指标,绝不把不同 provider 的成本、配额或 token 求和为误导性总量。
-完整账单/发票对账、驱动任一 coding agent 与后台 telemetry 不在范围内。
-Opt-in GitHub 认证和跨设备聚合同步延后到 v2.4.x,届时单独做隐私审阅。
+完整账单/发票对账、驱动任一 coding agent、后台 telemetry 与跨设备聚合同步不在范围内。
+既有 Claude 热力图发布仍是用户明确触发、仅公开仓库且需精确确认目标的 GitHub 动作;
+打开、切换、预览以及本地 SVG/Markdown 导出严格不联网,也不请求 GitHub 身份;
+只有独立的发布动作可以执行该网络操作。
## 模块地图(`src/`)
@@ -50,7 +54,7 @@ Opt-in GitHub 认证和跨设备聚合同步延后到 v2.4.x,届时单独做
| `codexView.ts` / `codexViewComponents.ts` | Codex 本地化文案与默认 provider contract;不负责 HTML renderer、client script 或 CSS。 |
| `settings.ts` | 权威 `SETTINGS` catalog 和 `SettingsStore`;普通值进入 configuration/globalState,BYOK 凭证只进入 SecretStorage,且绝不进入 Webview snapshot。不得散落直接读取。 |
| `statusBar.ts` / `codexStatus.ts` | Provider-specific 状态展示和通用 Claude 配额格式化。 |
-| `webview.ts` | Claude/Codex 唯一一套 provider-aware dashboard shell、共享 render function、共享 client 行为、provider tab 与 Compare 展示。 |
+| `webview.ts` | Claude/Codex 唯一一套 provider-aware dashboard shell、共享 render function、共享 client 行为、provider tab、Compare 展示与统一分享工作台。 |
| `i18n.ts` | 八个 UI locale 的全部用户可见文案。 |
| `types.ts` | 共享 extension 和 Claude contract。 |
@@ -80,6 +84,12 @@ Claude JSONL
Claude aggregate + Codex scope ──> 同一套 `webview.ts` dashboard render stack
Claude aggregate + Codex scope ──> 并列 Compare(不跨 provider 求和)
+已物化 Claude + Codex 每日聚合
+ ──> 统一分享选择器 ──> 综合活动预览 ──> 显式本地 SVG/Markdown 导出
+已物化 Claude 聚合
+ ──> 统一分享选择器 ──> 旧版分享卡或 Claude token 热力图预览
+ ──> 显式本地导出,或仅公开仓库且精确确认目标的 Claude 热力图 GitHub 发布
+
已物化 provider snapshot
──> 隐私重建型 advice adapter
──> 本地证据 + 确定性建议
@@ -107,6 +117,24 @@ Provider panel 的实时 patch 会保留页面 anchor,以及标签栏、图表
驻留内存的 privacy-safe 结构 key;不会逐帧写入 Webview state,也不会发送给
Extension Host。Compare 显示的更新时间绑定到稳定的已渲染数据 snapshot,因此
数据未变化的 refresh 保持 byte-identical,不会替换整份文档。
+用户连续滚动时,Webview 会将 provider panel 的更新合并到 120 ms 的静默间隔,
+并以 500 ms 为最长等待边界;只应用并确认最新修订。队列只驻留内存,既不重读
+源日志,也不逐帧持久化滚动位置。
+
+分享工作台只渲染一次:两个 provider 都有数据时位于「对比」,否则作为 Claude「所有」的
+回退入口。`enableShareCard` 是唯一可见的分享开关。公共命令 ID `exportShareCard`、
+`exportHeatmap` 与 `publishHeatmapToGitHub` 至少保留一个版本并打开对应呈现方式,不绕过预览。
+退役的 `showHeatmap` 仍保留在 catalog 中并可被清除,但隐藏且不再创建重复面板。所选呈现方式
+只是本地 UI 偏好,不改变 provider 统计口径。显式兼容命令会生成 provider 生命周期内单调递增的
+revision,并把它作为独立的 live intent;Webview 应用后 ACK 精确 revision,host 随即删除 live intent,
+但不回退 revision 历史。因此,已确认的命令不会在普通重绘、分享重置或同一 provider 的
+dispose/reopen 后重放。「重置分享偏好」复用既有的确认式本地数据动作:host 发送带 request-id 的
+client action,Webview 验证八个 allowlist localStorage key 均已删除;只有匹配 ACK 为 true 才算成功,
+否则 host 报错并保留恢复 tombstone。
+
+在窄 Webview 中,1200×680 Claude 分享卡在可键盘聚焦的局部横向滚动区内保留原始宽度,
+不缩小到无法阅读。Renderer 测试逐一审计所有普通字号 SVG 标签,保证至少 4.5:1;浏览器 geometry
+测试保证标签不越出各自 viewBox,且完整产物仍包含在外围 Axe 扫描中。
## Token 与 limit 语义
@@ -166,6 +194,12 @@ watcher/coalescing 计数,以及操作系统未提供 filename 的 quota watch
diagnostics 还会记录实际 refresh trigger、watcher/debounce 计数、历史 backfill 模式与
foreground/background worker profile;通用失败路径无法确认模式时明确记为 `unknown`,不作猜测。
+分享预览只消费已经物化的聚合。综合活动仅在明确的活动可视化中把 Claude processed 与
+Codex processed 相加;Codex cached input 与 reasoning 子集不会重复计入,也不声称成本、额度、
+能力或生产率等价。Claude 分享卡与 Claude 热力图仍只属于 Claude。本地预览/导出不联网;
+也不存在 GitHub authentication、profile、头像或名称查询。Claude 热力图发布是独立的显式动作,
+继续遵守本地数据契约中的公开仓库探测,以及精确 branch/path 与创建/覆盖确认边界。
+
### Schema 3 索引契约
内部 schema 3 继续使用既有的 `globalStorage` 文件名 `codex-index-v1.json`;文件名是兼容路径,
@@ -307,7 +341,7 @@ v2.3.0 不推断 20、100 或 200 美元的订阅档位。本地 Codex 日志没
- 根据变更风险执行 strict TypeScript、red-green TDD、完整 `node:test`、F5 smoke test
和安装 VSIX smoke test。
- 用户可见字符串覆盖 `en`、`de-DE`、`zh-TW`、`zh-CN`、`ja`、`ko`、`pt-BR`、`id`;
- 七份 README 同步。
+ 九份 README(首页加八份语言版)同步。
- dormant 的 preparation/experiment 模块与仅供评审的 v2.3.1 文档必须保持不可从生产 command graph 到达,并排除出 VSIX。
- 不手工修改 `package.json` 版本。发布已审阅的 Release Drafter draft 后才创建 tag,
publish workflow 再从 tag 写入包版本。
diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
index eb876015..22db89e9 100644
--- a/ARCHITECTURE.md
+++ b/ARCHITECTURE.md
@@ -17,10 +17,14 @@ The v2.3.1 candidate adds a default-off, local-first advice-effectiveness loop,
a single explicit BYOK request boundary, durable historical-work state, a
bounded local history of Codex weekly reset observations, and a rolling 30-day
Codex date/hour projection without changing provider accounting.
+v2.4 phase 1 consolidates the existing sharing surfaces into one preview-first
+workspace. A presentation selector switches among the combined activity
+heatmap, the legacy Claude Share Card, and the Claude-only token heatmap; the
+full-width export preview always precedes its controls.
- Claude: exact local token buckets, model pricing estimates, and Anthropic
OAuth 5-hour/weekly quota.
-- Codex Beta: local processed/fresh/cache/output/reasoning metrics, model and
+- Codex: local processed/fresh/cache/output/reasoning metrics, model and
effort breakdowns, thread structure, index coverage, quality flags, and
structural optimization guidance. Known models also receive a clearly
qualified API-equivalent cost estimate; it is never a bill or subscription
@@ -28,9 +32,12 @@ Codex date/hour projection without changing provider accounting.
- Compare: side-by-side compatible metrics only. It never sums provider cost,
quota, or tokens into a misleading combined total.
-Full invoice reconciliation, driving either coding agent, and background
-telemetry are out of scope. Opt-in GitHub authentication and cross-device
-aggregate sync are deferred to v2.4.x after a separate privacy review.
+Full invoice reconciliation, driving either coding agent, background telemetry,
+and cross-device aggregate sync are out of scope. Existing Claude heatmap
+publication remains an explicit, public-repository-only GitHub action with an
+exact-target confirmation. Opening, switching, previewing, and local SVG/
+Markdown export are strictly network-free and never request GitHub identity;
+only the separate publish action can perform that network operation.
## Module map (`src/`)
@@ -59,7 +66,7 @@ aggregate sync are deferred to v2.4.x after a separate privacy review.
| `codexView.ts` / `codexViewComponents.ts` | Codex localized-copy and default-provider contracts; no HTML renderer, client script, or CSS ownership. |
| `settings.ts` | Canonical `SETTINGS` catalog and `SettingsStore`; ordinary values use configuration/globalState, while BYOK credentials use SecretStorage and never enter Webview snapshots. Do not scatter direct reads. |
| `statusBar.ts` / `codexStatus.ts` | Provider-specific status presentation and generic Claude quota formatting. |
-| `webview.ts` | Single provider-aware Claude/Codex dashboard shell, shared render functions, shared client behavior, provider tabs, and Compare presentation. |
+| `webview.ts` | Single provider-aware Claude/Codex dashboard shell, shared render functions, shared client behavior, provider tabs, Compare presentation, and the unified sharing workspace. |
| `i18n.ts` | All user-facing copy for all eight UI locales. |
| `types.ts` | Shared extension and Claude contracts. |
@@ -90,6 +97,12 @@ allowlisted Codex JSONL
Claude aggregates + Codex scopes ──> one `webview.ts` dashboard render stack
Claude aggregates + Codex scopes ──> side-by-side Compare (no cross-provider totals)
+materialized Claude + Codex daily aggregates
+ ──> unified sharing selector ──> combined activity preview ──> explicit local SVG/Markdown export
+materialized Claude aggregates
+ ──> unified sharing selector ──> legacy Share Card or Claude token heatmap preview
+ ──> explicit local export, or public-only exact-target GitHub publication for the Claude heatmap
+
materialized provider snapshots
──> privacy-rebuilding advice adapters
──> local evidence + deterministic recommendations
@@ -129,6 +142,34 @@ privacy-safe structural keys held only for the in-flight patch; they are not
written per frame to Webview state or sent to the Extension Host. Compare's
displayed update time is tied to its stable rendered data snapshot, so an
unchanged refresh remains byte-identical and does not replace the document.
+While the user is actively scrolling, the Webview coalesces provider-panel
+patches until a 120 ms quiet interval, with a 500 ms upper bound. The newest
+revision alone is applied and acknowledged; the live data queue stays in
+memory and never reads source logs or persists scroll state per frame.
+
+The sharing workspace is rendered once: in Compare when both providers have
+data, otherwise as a Claude All-time fallback. `enableShareCard` is the only
+visible sharing on/off control. The public command IDs `exportShareCard`,
+`exportHeatmap`, and `publishHeatmapToGitHub` remain registered for compatibility
+and open the matching presentation; they do not bypass preview. The retired
+`showHeatmap` setting remains catalogued and clearable for one release but is
+hidden and no longer creates a duplicate panel. The selected presentation is a
+local UI preference and does not alter provider accounting. An explicit
+compatibility command issues a provider-lifetime monotonically increasing
+revision as a separate live intent. The Webview applies it and acknowledges the
+exact revision; the host then removes the live intent without rewinding the
+revision history, so an acknowledged command cannot replay on a normal render,
+sharing reset, or dispose/reopen of the same provider. Reset Sharing
+Preferences uses the existing confirmed local-data action: the host sends a
+request-id client action, the Webview verifies deletion of the eight allowlisted
+localStorage keys, and the host reports failure (with its recovery tombstone)
+unless the matching ACK is true.
+
+At narrow Webview widths, the 1200×680 Claude Share Card retains its intrinsic
+width inside a keyboard-focusable local horizontal scroller instead of being
+scaled into illegibility. Renderer tests audit every normal-size SVG label at
+4.5:1 or better, while browser geometry tests keep labels inside each viewBox;
+the complete artifact remains inside the surrounding Axe scan.
## Token and limit semantics
@@ -205,6 +246,18 @@ the actual refresh trigger, watcher/debounce counts, historical-backfill mode,
and foreground/background worker profile; generic failure paths use `unknown`
rather than infer a mode that was not observed.
+Sharing previews are produced from already materialized aggregates. Combined
+activity adds Claude processed volume to Codex processed volume only for the
+explicit activity visualization; Codex cached input and reasoning subsets are
+not added twice, and no cost, quota, capability, or productivity equivalence is
+claimed. The Claude Share Card and Claude heatmap remain Claude-only. Local
+preview/export performs no network request and has no GitHub authentication,
+profile, avatar, or name lookup. Claude heatmap publication is a distinct
+explicit action and keeps the public-repository probe plus exact branch/path and
+create-or-overwrite confirmation described in the local data contract. A
+successful remote write is authoritative; its exact result is reported even if
+the single versioned destination-preference object cannot be persisted.
+
### Schema 3 index contract
Schema 3 deliberately keeps the established `globalStorage` filename
@@ -416,7 +469,8 @@ use an explicit opt-in account mapping rather than attaching prices by guess.
- Strict TypeScript, red-green TDD, full `node:test`, F5 smoke test, and installed
VSIX smoke test are required in proportion to the change.
- User-visible strings cover `en`, `de-DE`, `zh-TW`, `zh-CN`, `ja`, `ko`,
- `pt-BR`, and `id`; all seven README editions move together.
+ `pt-BR`, and `id`; all nine README files (the main page plus eight locale
+ editions) move together.
- Dormant preparation/experiment modules and review-only v2.3.1 documents stay
unreachable from the production command graph and are excluded from VSIX.
- `package.json` is not manually version-bumped. Publishing the reviewed Release
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8fe29963..7fc8a507 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,9 +4,132 @@ All notable changes to this fork compared to upstream
[`ClaudeCodeUsage/ClaudeCodeUsage`](https://github.com/ClaudeCodeUsage/ClaudeCodeUsage) (last
upstream release: 1.0.8). Format follows [Keep a Changelog](https://keepachangelog.com).
-## [2.3.3] — Unreleased
+## [Unreleased]
+
+### Added
+- **Documentation in every supported UI language** — German and Brazilian
+ Portuguese now have concise READMEs alongside the existing editions. The
+ Marketplace README links all eight language variants; English and Chinese
+ retain the fuller references. The extension description and tags identify
+ its Claude/Codex token and quota use cases without presenting estimates as
+ billing data.
+- **`statusBarQuotaFormat`** (default empty) — name the quota windows in the
+ status bar yourself when you want a different set, order or separators than
+ the built-in `5h 6% · wk 1%`: `{5h.pct}`, `{wk.pct}` (or `{7d.pct}`) and
+ `{model:Fable.pct}`, each also taking `.reset` and `.label`. `.reset` follows
+ `resetCountdownFormat` unless it names its own style, so one bar can mix them:
+ `{5h.reset:units}`, `:decimal`, `:clock` or `:at` (wall clock, `Thu 16:59`).
+ Empty keeps the built-in layout, and a template supersedes
+ `quotaFiveHourOnly` and `showScopedWeekly` since it names its windows itself.
+ A segment whose window your account does not report is dropped along with its
+ separator. The dashboard keeps this optional feature compact: a Built-in,
+ 5-hour-only, Weekly-only, or Custom dropdown reveals the template field only
+ when Custom is selected.
+
+### Changed
+- **Codex graduates from Beta** — after the shared interaction, accessibility,
+ performance, localization, package, and installed-extension gates passed,
+ current UI and documentation now present Codex as a first-class provider.
+ Historical v2.3 release notes retain their original Beta wording.
+- **One preview-first sharing workspace** — the full-width export preview is
+ now the visual focus, with controls below it and one presentation selector
+ for the combined Claude + Codex activity heatmap, the legacy Claude Share
+ Card, and the Claude-only token heatmap. Provider accounting and labels remain
+ distinct: the combined view is activity volume, while the two legacy views
+ continue to use Claude aggregates only.
+- **Command and setting compatibility** — `exportShareCard`, `exportHeatmap`,
+ and `publishHeatmapToGitHub` remain registered and open their matching preview
+ instead of bypassing the workspace. `enableShareCard` remains the one visible
+ on/off control and still defaults to `true`; retired `showHeatmap` state stays
+ catalogued and clearable for one release without creating a duplicate panel.
+- **Strict local-artifact boundary** — previews and local SVG/Markdown exports
+ use only materialized aggregates and cannot request GitHub authentication,
+ profile, avatar, or name data. Claude heatmap publication is the only sharing
+ network path; it remains a separate explicit public-repository action with
+ exact repository/branch/path and create-or-overwrite confirmation.
+- **Codex status bar** — newly defaulted metric is today's processed tokens;
+ explicit uncached/output choices remain available. The weekly indicator
+ continues to show remaining capacity, not used tokens.
+- **Smooth live scrolling** — during an active scroll burst, the newest
+ provider-panel refresh waits for a short quiet interval before replacing
+ its DOM. A 500 ms ceiling prevents continuous scrolling from starving live
+ updates; the existing scroll/focus preservation and single-flight index
+ boundaries remain in place.
+- **Bounded sharing refreshes** — only the selected presentation is rendered;
+ hidden Share Card and heatmap artifacts are not rebuilt on every refresh.
+ Materialized daily aggregates are reused while their input and pricing
+ identity is unchanged.
### Fixed
+- **Claude all-time drill-down cost on large histories** — month → day uses a
+ materialized configured-timezone daily aggregate built by the incremental
+ index, rather than rescanning retained records when a month is opened.
+ Eligible recent days can continue to the existing hour detail without any
+ source-log read.
+- **Raw Share Card scope persisted in browser storage** — project paths and
+ session identifiers now remain memory-only; only non-identifying display
+ controls survive a Webview reload.
+- **Large Claude transcript buffers retained in memory** — dedup now keeps
+ fixed-size UTF-16-accurate digests, bounded prompt/task excerpts are detached,
+ and usage-bearing assistant rows retain only the fields needed for usage
+ calculations after content analysis. An opt-in 2.4 GiB synthetic corpus with
+ content analysis enabled showed sampled peak RSS falling from about 3.04 GB
+ to 360 MB for long user prompts; a separate long assistant-body fixture also
+ stays below 350 MB. Unchanged files are not reread. This does not establish
+ Windows or real-history smoothness; #99 remains open.
+- **PR first-pass review that only posted boilerplate (#108–#112)** — a failed
+ model request or empty answer now fails the PR workflow with a safe tier/status
+ diagnostic and posts no comment; issue triage retains its explicit fallback.
+ DeepSeek's cheap tier disables default thinking to preserve reply tokens, the
+ escalation tier has an output cap above its thinking budget, and the default
+ flash identifier tracks the current supported model. A cheap reply marked
+ as needing source is never posted when escalation fails. Existing bot
+ comments are not edited retroactively.
+- **Full rebuild whenever a new transcript appeared (#99)** — a file that had
+ just been created was treated as an unsafe mutation, so every new session
+ rebuilt every contribution in the corpus. A new file is now read in full on
+ its own while the established files stay untouched; ordering state is
+ recomputed from metadata, and a new file carrying an already-owned UUID still
+ falls back to the full rebuild. Measured on 15 real transcripts with two
+ appends and one new file: 16 body reads and 3042 ms became 3 body reads and
+ 567 ms.
+- **Full re-read when several sessions append at once (#99)** — the content
+ analysis fast path required exactly one changed file, so a machine running
+ more than one agent never took it: every refresh rebuilt all contributions
+ from every file. Any number of pure tail appends now stays incremental.
+ Appends are parsed in full-scan order and new-UUID ownership is attributed to
+ the owning file, so results match the full loader. Measured on 15 real
+ transcripts (34 MB) with three files appended: 15 body reads and 8.7 s became
+ 3 body reads and 0.6 s.
+- **High CPU during indexing (#99)** — day, month and hour bucketing no longer
+ constructs a fresh `Intl.DateTimeFormat` for every ingested record. The
+ resolved zone and both formatters are memoised per zone, so a large local
+ history is indexed without pinning a core. On a 1.4 GB history the key
+ derivation went from ~1.1k to ~168k records/second (158x) with identical
+ keys; invalid and empty zones still fall back exactly as before.
+- **Dashboard date labels rebuilt a formatter for every row (#99)** — the same
+ per-call `Intl.DateTimeFormat` construction, in the table and chart labels:
+ `toLocaleDateString` built a fresh formatter for every date it rendered. The
+ formatter is now memoised per locale and options, with output identical to
+ `toLocaleDateString` for every UI locale; one label went from ~31 µs to ~1 µs.
+- **One-shot sharing command intent** — command-opened previews now override a
+ previously saved Claude tab and presentation exactly once. A provider-local
+ monotonic revision history is separate from the live intent; the Webview ACK
+ consumes that intent, so reset, normal rerender, and dispose/reopen cannot
+ replay an acknowledged command or reuse its revision.
+- **Verified sharing reset** — the in-workspace button now uses the same
+ confirmed host request → request-id client action → ACK protocol as Local Data
+ controls. Browser-storage deletion failure is reported, and successful reset
+ defaults survive a full reload.
+- **Readable and accessible SVG previews** — the 1200×680 Share Card keeps its
+ intrinsic width inside a local horizontal scroller at 360px. Deterministic
+ inner-SVG geometry and contrast tests cover every normal label at 4.5:1 or
+ better without excluding the artifact from the surrounding Axe scan.
+
+## [2.3.3] — 2026-09-16
+
+### Fixed
+
- **Startup with legacy workspace advice keys (#105)** — a workspace-scoped
plaintext BYOK key or unavailable SecretStorage no longer prevents the usage
status bar, commands, and dashboard from activating. The old key remains
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index f7aca9ba..3dc5c4e5 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -7,8 +7,8 @@ it grows.
## Project scope
Before proposing a feature, it helps to know the positioning: this extension is
-provider-aware and lightweight, supporting Claude Code and **Codex Beta**.
-Codex Beta is enabled by default and can be turned off in provider settings.
+provider-aware and lightweight, supporting Claude Code and **Codex**.
+Codex is enabled by default and can be turned off in provider settings.
The emphasis is truthful local usage and token attribution rather than billing
reconciliation. Features that sharpen attribution, privacy, or the advice
experience are the best fit.
@@ -127,6 +127,18 @@ The runner checks out only the base repository, never executes contributor code,
reads at most six allowlisted text files within byte budgets, and can only post
one comment. Public input remains untrusted and model output may be wrong. A
missing or empty PR diff stops the workflow without posting a review.
+When both model tiers fail or return no usable PR analysis, the workflow fails
+with a status-only diagnostic and **does not post a generic review**. Inspect
+the Actions log before retrying; API response bodies, credentials, PR text,
+and model replies are not included in this diagnostic. Issues retain a clearly
+labelled, provider-neutral triage fallback. Historical fallback comments are
+not automatically replaced. The bot only runs on `opened`, so a later retry
+requires an explicitly arranged workflow run, not a fresh push to the PR.
+
+中文说明:外部 PR 的自动初审若两层模型都失败,会在 Actions 中显示安全的
+状态分类并停止,不再发布泛泛的模板评论;日志不会包含密钥、接口原文或 PR
+内容。Issue 的分流兜底保持不变。旧评论不会自动改写,给 PR 推新提交也不会
+触发本工作流再次运行,是否重试由维护者决定。
Codex automatic attribution is not enabled in v2.2.1. It requires a separately
implemented and trusted OpenAI/Codex transport; a model name or repository
diff --git a/LOCAL-DATA.md b/LOCAL-DATA.md
index 65785be4..55dedf7c 100644
--- a/LOCAL-DATA.md
+++ b/LOCAL-DATA.md
@@ -15,16 +15,18 @@ v2.3.1 normative field-level contract is in
| Claude usage records | Claude Code's local `projects/**/*.jsonl` | Runtime usage records and an in-memory per-file index; bounded prompt samples only when content analysis is enabled | Released on window reload/exit. Data controls never touch source logs; the separately opt-in Session Actions feature may move one exactly confirmed log to the OS trash (recoverable) | None unless the user separately previews and sends an advice request |
| Codex usage records | `$CODEX_HOME/sessions/**/*.jsonl` and `archived_sessions/**/*.jsonl` | A versioned incremental index of pseudonymous file keys, offsets, numeric usage, dates, model/effort, and sanitized structural aggregates | Until rebuild, explicit clear, schema replacement, or host removal | None |
| Codex titles | Exactly `$CODEX_HOME/session_index.jsonl` | `id → thread_name` is streamed for runtime display; titles are not written to the index | Runtime only | None |
-| Quota observations | Claude's official quota response or structured Codex rate-limit events | Provider, machine-local anonymous account epoch, observed/reset time, period, used/remaining fraction, anonymous window identity, source, confidence, and quality flags | At most 180 days and 512 observations per provider/account/period after boundary-preserving compaction; clear separately or with all derived data | Claude quota lookup contacts Anthropic when enabled; Codex quota evidence is local |
-| Settings and UI preferences | User choices | Typed extension settings, selected tab/filters, bounded background-work state, heatmap title/range/privacy preview, and the optional display-currency preset | Until reset, clear, or uninstall; ordinary VS Code settings may participate in Settings Sync | No extension-initiated transfer; no exchange-rate lookup |
-| Share destination | User-entered GitHub target | Optional `owner/repository/path`; no GitHub credential | Until sharing preferences are reset | Only after an explicit publish action and exact destination confirmation |
+| Quota observations | Claude's official quota response or structured Codex rate-limit events | Provider, machine-local anonymous account epoch, observed/reset time, period, used/remaining fraction, anonymous window identity, source, confidence, and quality flags | At most 180 days and 512 observations per provider/account/period after boundary-preserving compaction; clear separately or with all derived data | Claude quota lookup contacts Anthropic when enabled; an expired Claude token is refreshed with Anthropic and written back to its credential source. Codex quota evidence is local |
+| Settings and UI preferences | User choices | Typed extension settings, selected tab/filters, bounded background-work state, sharing presentation, Share Card draft controls, heatmap title/range/privacy preview, and the optional display-currency preset | Until reset, clear, or uninstall; ordinary VS Code settings may participate in Settings Sync | No extension-initiated transfer; no exchange-rate lookup |
+| Share destination | User-entered GitHub target | One versioned `ccu.heatmapDestination.v1` object containing optional `owner/repository/path`; no GitHub credential | Until sharing preferences are reset | Only after an explicit publish action and exact destination confirmation. A successful write is reported even if saving this convenience preference fails |
| Advice evidence | Derived local aggregates and explicit feedback | Coarse observations, recommendations, ratings, snoozes, comparable-task metrics, coverage, and versions | Bounded local ledger; clear independently | Nothing by default; only the exact previewed request is sent after a separate action |
| Advice API key | User-provided secret | Secret value in VS Code SecretStorage only | Until the key or all derived data is cleared | Used only as the authorization credential for the explicitly configured endpoint |
## What is never cached
- OAuth access or refresh tokens, cookies, authorization headers, or GitHub
- credentials.
+ credentials in extension-owned derived storage. Claude's provider-owned
+ credential file or macOS Keychain item may be updated when its OAuth token
+ needs refreshing for a quota lookup.
- API keys outside VS Code SecretStorage.
- Full account identifiers, email addresses, display names, or subscription
names. Account continuity uses a machine-local HMAC fingerprint or an
@@ -32,6 +34,13 @@ v2.3.1 normative field-level contract is in
- Raw prompt/response bodies, tool arguments, commands, complete CLI output,
absolute source-log paths, repository remotes, or persistent thread titles.
+When Claude quota tracking is enabled, the extension uses the selected Claude
+profile's existing access token to query `api.anthropic.com/api/oauth/usage`.
+If it is expired, the existing refresh token is sent to
+`console.anthropic.com/v1/oauth/token`; the replacement access token is written
+back to that profile's credential file or macOS Keychain item. This is separate
+from optional BYOK AI advice, which never uses Claude subscription credentials.
+
Different anonymous account fingerprints are not merged. If local Codex logs
cannot establish one coherent account/window, the dashboard shows only used
API-equivalent value and explains why it cannot claim an account-level unused
@@ -45,10 +54,24 @@ Converted estimates carry `≈`; provider-native money such as actual usage
credits remains in its reported currency. The snapshot derives currency-per-USD
values from the [ECB euro reference rates](https://www.ecb.europa.eu/stats/policy_and_exchange_rates/euro_reference_exchange_rates/html/index.en.html).
-## Combined heatmap and exports
-
-The Compare heatmap reuses daily provider aggregates; it does not scan logs
-again or create another statistics cache.
+## Sharing workspace and exports
+
+The preview-first sharing workspace reuses materialized provider aggregates; it
+does not scan logs again or create another statistics cache. Its remembered
+presentation is one local enum (`ccu.sharing.template`): combined activity
+heatmap, Claude Share Card, or Claude token heatmap. `enableShareCard` is the
+only visible on/off setting. The retired `showHeatmap` value remains catalogued
+for bounded clearing and one-release compatibility, but it no longer creates a
+second panel. Explicit legacy-command coordination is runtime-only: the host
+keeps a monotonically increasing provider-lifetime revision separately from the
+single live intent, and removes that intent after the Webview acknowledges its
+exact revision. No command revision is stored in browser storage.
+
+Share Card project and session selections can contain raw local paths or
+session identifiers, so those values remain memory-only and disappear when the
+Webview closes. Browser storage keeps only non-identifying presentation
+controls such as range, theme, visible sections, number format, and the
+selected presentation.
```text
Claude processed = input + cache creation + cache read + output
@@ -64,13 +87,18 @@ equivalence.
Local SVG and Markdown exports contain only a bounded title, selected date
range, daily Claude/Codex/combined aggregates, labels, and caveats. They do not
contain accounts, projects, thread titles, paths, prompts, or log content.
+Claude Share Card and Claude token heatmap presentations consume Claude
+aggregates only; they do not imply that Codex exposes equivalent fields.
+Preview and local export are strictly network-free and do not request GitHub
+authentication, profile, avatar, or name data.
-Direct GitHub publication is optional and never runs in the background. v2.3.1
-requests `public_repo`, verifies the repository is public and resolves its
+Direct GitHub publication is a separate optional action and never runs in the
+background. It requests `public_repo`, verifies the repository is public and resolves its
default branch, then asks the user to confirm the exact
`owner/repository/branch/path` and whether the file will be created or replaced.
Private repositories use local SVG plus Markdown instead of silently requesting
-broader access.
+broader access. After GitHub accepts the write, success is authoritative and is
+reported before any warning that the atomic destination preference could not be saved.
## Clear and migration behavior
@@ -95,7 +123,10 @@ operation, leaves existing data intact, and is reported for manual removal.
Queued activation writes are drained before deletion and exact postconditions
are verified afterward. If the Webview cannot acknowledge its allowlisted
browser-state reset, a value-free pending action marker is retried on the next
-panel open instead of reporting a false success.
+panel open instead of reporting a false success. The reset button inside the
+sharing workspace uses this same host request → request-id client reset → ACK
+path; it reports browser-storage deletion failures and applies defaults only
+after the allowlisted deletion succeeds.
This clearing boundary is separate from the opt-in Session Actions feature.
That feature can move one user-selected Claude session log to the OS trash only
diff --git a/LOCAL-DATA.zh-CN.md b/LOCAL-DATA.zh-CN.md
index 16fc4b0b..99fb015c 100644
--- a/LOCAL-DATA.zh-CN.md
+++ b/LOCAL-DATA.zh-CN.md
@@ -14,21 +14,29 @@ Claude Code Usage 采用本地优先设计。插件只读取供应商拥有的
| Claude 用量记录 | Claude Code 本地 `projects/**/*.jsonl` | 运行时用量记录和内存中的逐文件索引;仅在启用内容分析时保留有界提示样本 | 窗口重载/退出即释放。本地数据控制绝不触碰源日志;另行启用的“会话操作”可把一个精确确认的日志移入系统回收站(可恢复) | 默认无;只有用户另行预览并发送建议请求时才可能包含获准内容 |
| Codex 用量记录 | `$CODEX_HOME/sessions/**/*.jsonl` 与 `archived_sessions/**/*.jsonl` | 版本化增量索引:匿名文件键、偏移量、数值用量、日期、模型/effort 和已净化结构聚合 | 保留到重建、明确清除、schema 替换或宿主移除 | 无 |
| Codex 标题 | 仅 `$CODEX_HOME/session_index.jsonl` | 流式读取 `id → thread_name` 供运行时显示;标题不写入索引 | 仅运行时 | 无 |
-| 额度观测 | Claude 官方额度响应或 Codex 结构化 rate-limit 事件 | 供应商、本机匿名账号 epoch、观测/重置时间、周期、已用/剩余比例、匿名窗口标识、来源、可信度和质量标志 | 保留不超过 180 天;边界保护压缩后每个供应商/账号/周期最多 512 条;可单独或随全部派生数据清除 | 启用时 Claude 额度查询会访问 Anthropic;Codex 额度证据只来自本地 |
-| 设置与 UI 偏好 | 用户选择 | 类型化设置、标签页/筛选、有限后台任务状态、热力图标题/范围/隐私预览,以及可选显示币种预设 | 保留到重置、清除或卸载;普通 VS Code 设置可能参与 Settings Sync | 插件不主动传输,也不查询汇率 |
-| 分享目标 | 用户输入的 GitHub 目标 | 可选 `owner/repository/path`;不保存 GitHub 凭据 | 保留到重置分享偏好 | 仅在用户明确发布并确认精确目标后发生 |
+| 额度观测 | Claude 官方额度响应或 Codex 结构化 rate-limit 事件 | 供应商、本机匿名账号 epoch、观测/重置时间、周期、已用/剩余比例、匿名窗口标识、来源、可信度和质量标志 | 保留不超过 180 天;边界保护压缩后每个供应商/账号/周期最多 512 条;可单独或随全部派生数据清除 | 启用时 Claude 额度查询会访问 Anthropic;过期 token 会经 Anthropic 刷新并写回原凭证来源。Codex 额度证据只来自本地 |
+| 设置与 UI 偏好 | 用户选择 | 类型化设置、标签页/筛选、有限后台任务状态、分享呈现方式、分享卡未应用的控件草稿、热力图标题/范围/隐私预览,以及可选显示币种预设 | 保留到重置、清除或卸载;普通 VS Code 设置可能参与 Settings Sync | 插件不主动传输,也不查询汇率 |
+| 分享目标 | 用户输入的 GitHub 目标 | 单个版本化 `ccu.heatmapDestination.v1` 对象,包含可选 `owner/repository/path`;不保存 GitHub 凭据 | 保留到重置分享偏好 | 仅在用户明确发布并确认精确目标后发生;即使保存此便利偏好失败,成功写入仍会如实报告 |
| 建议证据 | 本地派生聚合与明确反馈 | 粗粒度观察、建议、评分、暂停、可比任务指标、覆盖率和版本 | 有界本地台账;可独立清除 | 默认不发送;只有单独确认后才发送精确预览过的请求 |
| 建议 API Key | 用户自备密钥 | 仅存于 VS Code SecretStorage | 保留到清除密钥或全部派生数据 | 只作为用户明确配置端点的授权凭据 |
## 绝不缓存什么
-- OAuth access/refresh token、cookie、authorization header 或 GitHub 凭据。
+- 插件自有派生存储中不会缓存 OAuth access/refresh token、cookie、authorization
+ header 或 GitHub 凭据。为查询 Claude 额度而刷新 OAuth token 时,Claude
+ 自有的凭证文件或 macOS 钥匙串项目可能会被更新。
- VS Code SecretStorage 之外的 API key。
- 完整账号标识、邮箱、显示名或订阅名称。账号连续性只用本机 HMAC
fingerprint 或隔离的未归属 epoch。
- 原始提示/响应正文、工具参数、命令、完整 CLI 输出、源日志绝对路径、
仓库 remote 或持久化线程标题。
+启用 Claude 额度跟踪后,插件用所选 Claude 资料的现有 access token 查询
+`api.anthropic.com/api/oauth/usage`。如该 token 过期,会把现有 refresh token
+发送到 `console.anthropic.com/v1/oauth/token`,并将新 access token 写回该资料的
+凭证文件或 macOS 钥匙串项目。这与可选的自备密钥 AI 建议分开;后者不会使用
+Claude 订阅凭据。
+
不同匿名账号 fingerprint 不会合并。如果本地 Codex 日志无法证明属于同一账号
和同一窗口,仪表盘只显示已用 API 等效价值,并说明为何不能声称账号级未用额度。
@@ -39,9 +47,19 @@ Claude Code Usage 采用本地优先设计。插件只读取供应商拥有的
[欧洲央行欧元参考汇率](https://www.ecb.europa.eu/stats/policy_and_exchange_rates/euro_reference_exchange_rates/html/index.en.html)
换算得到。
-## 综合热力图与导出
+## 分享工作台与导出
+
+预览优先的分享工作台直接复用已物化的供应商聚合,不会再次扫描日志,也不会新增第二套
+统计缓存。记住的呈现方式只是本地枚举 `ccu.sharing.template`:综合活动热力图、Claude
+分享卡或 Claude token 热力图。`enableShareCard` 是唯一可见的开关。退役的 `showHeatmap`
+仍保留在 catalog 中,以便执行有界清除并维持一个版本的兼容性,但不再生成第二个面板。
+显式旧命令的协调仅存在于运行时:host 把 provider 生命周期内单调递增的 revision 与唯一
+live intent 分开保存,并在 Webview ACK 精确 revision 后删除 live intent。浏览器存储不保存
+命令 revision。
-“对比”热力图直接复用供应商日聚合,不会再次扫描日志,也不会新增第二套统计缓存。
+分享卡的项目或会话范围可能包含原始本地路径或会话标识,因此这些值只保留在内存中,
+Webview 关闭即消失。浏览器存储只保留不具识别性的呈现控件,例如范围、主题、显示区块、
+数字格式和所选呈现方式。
```text
Claude processed = input + cache creation + cache read + output
@@ -55,11 +73,14 @@ Codex 的 cached input 已包含在 input total 中,reasoning 已包含在 out
本地 SVG 与 Markdown 导出只含有界标题、所选日期范围、每日 Claude/Codex/综合聚合、
标签和免责声明;不含账号、项目、线程标题、路径、提示或日志内容。
+Claude 分享卡和 Claude token 热力图只消费 Claude 聚合,不暗示 Codex 具有等价字段。
+预览与本地导出严格不联网,也不请求 GitHub authentication、profile、头像或名称数据。
-直接发布到 GitHub 是可选动作,且绝不在后台运行。v2.3.1 仅请求 `public_repo`,
+直接发布到 GitHub 是独立的可选动作,且绝不在后台运行。它仅请求 `public_repo`,
先验证仓库公开性和默认分支,再要求用户确认精确的
`owner/repository/branch/path` 以及创建或覆盖动作。私有仓库应改用本地 SVG +
-Markdown,不会静默索取更宽权限。
+Markdown,不会静默索取更宽权限。GitHub 接受写入后,插件会先如实报告成功;若原子化
+目的地偏好保存失败,再另行显示警告。
## 清除与迁移
@@ -78,7 +99,9 @@ UI 或分享偏好、移除 BYOK 密钥。“清除全部扩展派生数据”
若发现 VS Code 已无法安全更新的未注册旧设置,整个操作会在零删除状态下停止,并提示
手动移除。激活期排队写入会先排空,删除后再验证精确后置条件。若 Webview 未能确认其
白名单浏览器状态已清除,插件会保存一个不含数据值的待重试动作,在下次打开面板时重放,
-而不会误报成功。
+而不会误报成功。分享工作台内部的重置按钮也复用同一条 host request → 带 request-id 的
+client reset → ACK 路径;浏览器存储删除失败会明确报错,且只有 allowlist 删除成功后才应用
+默认值。
这个清除边界与另行选择启用的“会话操作”相互独立。后者只有在用户针对单个 Claude
会话日志完成模态确认后,才会把它移入系统回收站(可恢复);它不会参与“清除全部”。
diff --git a/README-de-DE.md b/README-de-DE.md
new file mode 100644
index 00000000..89ef1cad
--- /dev/null
+++ b/README-de-DE.md
@@ -0,0 +1,61 @@
+# Claude Code Usage
+
+🌐 **Sprache**: [Hauptseite](README.md) · [English](README-en.md) · **Deutsch** · [繁體中文](README-zh-TW.md) · [简体中文](README-zh-CN.md) · [日本語](README-ja.md) · [한국어](README-ko.md) · [Português (Brasil)](README-pt-BR.md) · [Bahasa Indonesia](README-id.md)
+
+**Lokale Nutzungsübersicht für Claude Code und Codex in der VS-Code-Statusleiste.**
+Die Erweiterung liest lokale Nutzungsprotokolle und zeigt Anbieter-Daten mit
+jeweils passender Bedeutung an. Sie ist **kein Abrechnungswerkzeug**.
+
+## Auf einen Blick
+
+- **Claude Code:** geschätzte Token-Kosten und, bei gültiger Anmeldung, die
+ offiziellen 5-Stunden- und Wochenquoten des aktiven Claude-Profils.
+- **Codex:** heute verarbeitete Token und die zuletzt beobachtete
+ verbleibende Wochenquote. Die Quote ist kein garantierter Live-Kontostand.
+- **Dashboard:** Tages-, 30-Tage- und Gesamtansicht sowie Sitzungen, Projekte
+ und aufbereitete lokale Empfehlungen. Diagramme lassen sich nach Monat,
+ Tag und Stunde aufklappen, ohne beim Klick die Protokolle erneut zu lesen.
+- **Teilen:** Ein gemeinsamer Arbeitsbereich mit Vorschau vor dem Export.
+ Lokaler SVG-/Markdown-Export bleibt lokal; eine Veröffentlichung auf GitHub
+ erfordert eine gesonderte Bestätigung. Die Funktion ist standardmäßig an
+ und lässt sich in den Einstellungen ausschalten.
+
+
+
+Das Bild nutzt synthetische Daten, nicht das Konto einer Person. Weitere
+Abbildungen und Details stehen in der [ausführlichen englischen Hauptseite](README.md).
+
+## Daten richtig lesen
+
+Bei Codex gilt **verarbeitet = Eingabe + Ausgabe** und **nicht gecachte Nutzung
+= nicht gecachte Eingabe + Ausgabe**. Gecachte Eingabe ist bereits Teil der
+Eingabe; Reasoning ist bereits Teil der Ausgabe. Diese Werte dürfen nicht
+nochmals addiert werden. Angezeigte API-Äquivalentkosten sind Schätzungen
+anhand bekannter Modellpreise, **keine Rechnung oder Abo-Belastung**.
+Claude- und Codex-Kosten oder Quoten werden nicht zusammengerechnet.
+Fehlende oder noch nicht indizierte Protokolle können die Anzeige verkleinern;
+die Codex-Quote bleibt die letzte lokale Beobachtung.
+
+## Installation und Datenschutz
+
+In VS Code unter **Erweiterungen** nach `Claude Code Usage` suchen oder
+`ext install GrowthJack.claude-code-usage` ausführen. Das Dashboard öffnet
+sich über die Statusleiste oder den Befehl **Show Usage Details**.
+Sprache, Datenverzeichnis und Anzeigeoptionen können in den Einstellungen
+angepasst werden. Claude-Protokolle werden lokal gelesen; der Codex-Index
+speichert abgeleitete, pseudonyme Nutzungsdaten statt Gesprächsinhalten.
+KI-Beratung ist optional und sendet erst nach Vorschau und ausdrücklichem
+Senden mit einem eigenen API-Schlüssel. Einzelheiten und Löschwege:
+[Lokale Daten und Datenschutz](LOCAL-DATA.md).
+
+Die vollständige Funktionsbeschreibung steht auf [Englisch](README.md)
+und [vereinfachtem Chinesisch](README-zh-CN.md). Fehler und Vorschläge sind
+unter [Issues](https://github.com/ClaudeCodeUsage/ClaudeCodeUsage/issues) willkommen.
+
+## Credits und Lizenz
+
+MIT-Lizenz. Die Pflege verwendet [Claude Code](https://claude.com/claude-code)
+und [OpenAI Codex](https://developers.openai.com/codex/) als Werkzeuge;
+menschliche Beiträge stehen im [Changelog](CHANGELOG.md).
+
+[MIT-Lizenz](LICENSE)
diff --git a/README-en.md b/README-en.md
index b8bf7eb0..70b2177d 100644
--- a/README-en.md
+++ b/README-en.md
@@ -1,14 +1,14 @@
# Claude Code Usage
-🌐 **Language**: [🏠 Main](README.md) | **English** | [繁體中文](README-zh-TW.md) | [简体中文](README-zh-CN.md) | [日本語](README-ja.md) | [한국어](README-ko.md) | [Bahasa Indonesia](README-id.md)
+🌐 **Language**: [🏠 Main](README.md) | **English** | [Deutsch](README-de-DE.md) | [繁體中文](README-zh-TW.md) | [简体中文](README-zh-CN.md) | [日本語](README-ja.md) | [한국어](README-ko.md) | [Português (Brasil)](README-pt-BR.md) | [Bahasa Indonesia](README-id.md)
---
-**The local Claude Code and Codex usage coach in your status bar.** Not a billing tool. Claude retains its cost and quota views; Codex Beta adds token and behaviour insights with Codex-native semantics.
+**The local Claude Code and Codex usage coach in your status bar.** Not a billing tool. Claude retains its cost and quota views; Codex adds token and behaviour insights with Codex-native semantics.
-> **What it is:** a VS Code status-bar monitor that reads your local Claude Code conversation logs and shows **token-derived** usage and cost estimates — plus an optional AI advisor that suggests how to improve your prompts and reduce waste.
+> **What it is:** a VS Code status-bar monitor that reads local Claude Code and Codex usage logs, shows provider-appropriate token and quota views, and offers optional advice for reducing avoidable overhead.
>
-> **What it is _not_:** a billing tool. All amounts are estimates based on public per-million-token rates. Refer to your Anthropic account for actual charges.
+> **What it is _not_:** a billing tool. Claude costs and Codex API-equivalent values are estimates, not subscription charges or invoices. Refer to the relevant provider account for billing truth.
> Screenshots include English and Simplified Chinese. See the [main README](README.md) for the full feature reference.
@@ -48,6 +48,7 @@ These five v2.3 captures use the production renderer, synthetic fixtures, and VS
## Features
- **Status bar** — today's cost, current-session cost, and real 5-hour / weekly quota (`5h:N% wk:N%`) read from Claude Code's own OAuth session. Zero configuration.
+- **Quota format** — choose Built-in (default), 5-hour only, or Weekly only in ⚙ Settings; Custom reveals the optional template field.
- **Dashboard tabs** — Today / Last 30 Days / All Time, plus **Sessions / Projects / Content / Branches**, all sortable.
- **Stacked cost-composition charts** with a Y-axis and reference lines — see at a glance how much of each day / month went to input, output, cache-write and cache-read.
- **Content tab** — estimates which content consumes your tokens (your prompts vs. tool results vs. assistant output / thinking).
@@ -56,6 +57,34 @@ These five v2.3 captures use the production renderer, synthetic fixtures, and VS
- **Multi-vendor pricing** — Opus 4.x / Sonnet 4.x / Haiku 4.5 verified against Anthropic's public pricing; reference rates for OpenAI / Gemini / DeepSeek / Kimi / GLM / Qwen with family-aware fallback. `Refresh Token Pricing` pulls live LiteLLM data.
- **Personalisation** — language, timezone, decimal places, compact numbers, project grouping, dashboard auto-refresh toggle.
+## What's new in v2.4
+
+- **Codex status and scrolling** — today's processed tokens are the default
+ compact metric, while weekly quota shows remaining capacity. Live panel
+ updates briefly defer during scrolling, with a bounded delay.
+- **One preview-first sharing workspace** — a full-width export preview now
+ comes first, with controls below it and one presentation selector for the
+ **Combined activity heatmap**, **Claude Share Card**, and **Claude token
+ heatmap**. The combined presentation requires real data from both providers;
+ both legacy presentations remain Claude-only.
+- **Compatibility without duplicate panels** — `exportShareCard`,
+ `exportHeatmap`, and `publishHeatmapToGitHub` remain available and open the
+ matching presentation. `enableShareCard` is still the single visible sharing
+ switch and defaults to on; retired `showHeatmap` state remains only for one
+ release of compatibility and bounded clearing.
+- **Strict local-artifact and provider boundaries** — selecting, previewing,
+ and local SVG/Markdown export use materialized aggregates only and make no
+ network request, GitHub sign-in, or profile/avatar/name lookup. Only the
+ separate Claude-heatmap **Publish to GitHub** action can connect; it remains
+ public-repository-only and confirms the exact target and create/overwrite
+ action before writing. Combined activity does not claim billing,
+ productivity, capability, or provider equivalence.
+
+
+
+Production renderer, synthetic usage and VS Code theme variables; not an
+installed-VSIX or real-account screenshot.
+
## What's new in v2.3
- **Refined throughout the v2.3 line** — GPT-6 Astra and Fable 5.1 model
@@ -88,14 +117,17 @@ Also on the [Open VSX Registry](https://open-vsx.org/extension/GrowthJack/claude
## Configuration
-Open Settings (`Ctrl+,`) and search for **`Claude Code Usage`**. All settings are optional. The most useful:
+Open the dashboard's ⚙ Settings tab for most options, including the BYOK API
+key, which is stored in SecretStorage and is not synced. VS Code Settings
+(`Ctrl+,`) retains only `language`, `dataDirectory`, and
+`codex.dataDirectory`. The most useful options are:
- `language` — UI language (`auto` / `en` / `de-DE` / `zh-TW` / `zh-CN` / `ja` / `ko` / `pt-BR` / `id`).
- `timezone` — IANA timezone for date display (e.g. `Asia/Hong_Kong`).
- `usageLimitTracking` — show the real 5h / weekly quota indicator.
- `showCost` / `showContext` — toggle the cost item and the context-window fill indicator (like `/context`) in the status bar.
- Each of these status-bar items is opt-out — set `usageLimitTracking`, `showCost`, or `showContext` to `false` to hide just that one.
-- `advice.apiKey` — bring-your-own key shared by AI Advice and the Usage Optimizer (Anthropic or OpenAI-compatible endpoint).
+- `advice.apiKey` — enter your own key in the dashboard for AI Advice and the Usage Optimizer (Anthropic or OpenAI-compatible endpoint).
- `pauseDashboardRefresh` — pause dashboard auto-refresh (also toggleable in the dashboard header).
See the [full settings table in the main README](README.md#configuration).
diff --git a/README-id.md b/README-id.md
index 1bcb16fa..136f9de7 100644
--- a/README-id.md
+++ b/README-id.md
@@ -1,14 +1,14 @@
# Claude Code Usage
-🌐 **Bahasa**: [🏠 Main](README.md) | [English](README-en.md) | [繁體中文](README-zh-TW.md) | [简体中文](README-zh-CN.md) | [日本語](README-ja.md) | [한국어](README-ko.md) | **Bahasa Indonesia**
+🌐 **Bahasa**: [🏠 Main](README.md) | [English](README-en.md) | [Deutsch](README-de-DE.md) | [繁體中文](README-zh-TW.md) | [简体中文](README-zh-CN.md) | [日本語](README-ja.md) | [한국어](README-ko.md) | [Português (Brasil)](README-pt-BR.md) | **Bahasa Indonesia**
---
-**Pelatih penggunaan lokal Claude Code dan Codex di status bar.** Bukan alat billing. Tampilan biaya / kuota Claude tetap ada; Codex Beta menganalisis token dan perilaku sesuai semantik Codex.
+**Pelatih penggunaan lokal Claude Code dan Codex di status bar.** Bukan alat billing. Tampilan biaya / kuota Claude tetap ada; Codex menganalisis token dan perilaku sesuai semantik Codex.
-> **Apa ini:** monitor status bar VS Code yang membaca log percakapan Claude Code lokal Anda dan menampilkan estimasi penggunaan serta biaya **berbasis token** — plus penasihat AI opsional yang menyarankan cara memperbaiki prompt Anda dan mengurangi pemborosan.
+> **Apa ini:** monitor status bar VS Code yang membaca log penggunaan lokal Claude Code dan Codex, lalu menampilkan token, kuota, dan estimasi sesuai makna data masing-masing penyedia. Saran opsional membantu mengurangi pemakaian yang tidak perlu.
-> **ini _Bukanlah_:** alat billing. Semua angka adalah estimasi berdasarkan tarif publik per-juta-token. Rujuk ke akun Anthropic Anda untuk biaya yang sebenarnya.
+> **Apa yang bukan:** alat penagihan. Biaya Claude dan biaya setara API Codex hanyalah estimasi, bukan tagihan langganan. Periksa akun penyedia terkait untuk tagihan sebenarnya. Kuota Codex adalah pengamatan lokal terakhir, bukan saldo langsung.
> Screenshot mencakup UI bahasa Inggris dan Tionghoa Sederhana. Lihat [README utama](README.md) untuk referensi lengkap.
@@ -48,6 +48,7 @@ Lima gambar v2.3 ini menggunakan renderer produksi, data sintetis, dan variabel
## Fitur
- **Status bar** — biaya hari ini, biaya sesi saat ini, dan kuota 5-jam / mingguan yang sebenarnya (`5h:N% wk:N%`) dibaca dari sesi OAuth Claude Code sendiri. Tanpa konfigurasi.
+- **Format kuota** — pilih Bawaan (default), hanya 5 jam, atau hanya Mingguan di ⚙ Pengaturan; pilihan Kustom baru menampilkan kolom templat.
- **Tab dashboard** — Hari Ini / 30 Hari Terakhir / Sepanjang Waktu, plus **Sesi / Proyek / Konten / Branch**, semuanya bisa diurutkan.
- **Grafik komposisi biaya bertumpuk** dengan sumbu Y dan garis referensi — lihat sekilas berapa banyak dari tiap hari / bulan yang terpakai untuk masukan, keluaran, cache-write, dan cache-read.
- **Tab Konten** — memperkirakan konten mana yang menghabiskan token Anda (prompt Anda vs. hasil tool vs. output / pemikiran asisten).
@@ -56,6 +57,30 @@ Lima gambar v2.3 ini menggunakan renderer produksi, data sintetis, dan variabel
- **Harga multi-vendor** — Opus 4.x / Sonnet 4.x / Haiku 4.5 diverifikasi terhadap harga publik Anthropic; tarif referensi untuk OpenAI / Gemini / DeepSeek / Kimi / GLM / Qwen dengan fallback berbasis family model. `Refresh Token Pricing` menarik data LiteLLM langsung.
- **Personalisasi** — bahasa, zona waktu, angka desimal, angka ringkas, pengelompokan proyek, toggle penyegaran otomatis dashboard.
+## Yang baru di v2.4
+
+- **Status Codex dan pengguliran** — metrik ringkas bawaan menampilkan token
+ yang diproses hari ini, sedangkan jatah mingguan menampilkan sisanya.
+ Pembaruan panel ditunda sebentar saat menggulir, dengan batas waktu tunggu.
+- **Satu ruang kerja berbagi yang mengutamakan pratinjau** — pratinjau ekspor
+ selebar penuh menjadi fokus, dengan kontrol di bawahnya. Satu pemilih tampilan
+ berpindah antara **heatmap aktivitas gabungan**, **Claude Share Card**, dan
+ **heatmap token Claude**. Tampilan gabungan hanya tersedia saat kedua penyedia
+ memiliki data nyata; dua tampilan lama tetap khusus Claude.
+- **Entri kompatibel tanpa panel ganda** — `exportShareCard`, `exportHeatmap`,
+ dan `publishHeatmapToGitHub` tetap tersedia dan membuka pratinjau yang sesuai.
+ `enableShareCard` tetap menjadi satu-satunya sakelar berbagi yang terlihat dan
+ aktif secara default; `showHeatmap` yang dipensiunkan disimpan hanya untuk satu
+ rilis kompatibilitas status dan penghapusan terbatas.
+- **Batas artefak lokal dan penyedia yang ketat** — memilih tampilan, melihat
+ pratinjau, serta mengekspor SVG/Markdown lokal hanya memakai agregat yang
+ sudah tersedia dan tidak melakukan permintaan jaringan, login GitHub, atau
+ pengambilan profil/avatar/nama. Hanya tindakan terpisah **Publikasikan ke
+ GitHub** untuk heatmap Claude yang dapat terhubung; tindakan ini tetap khusus
+ repositori publik dan mengonfirmasi target serta aksi buat/timpa yang tepat
+ sebelum menulis. Aktivitas gabungan bukan klaim tagihan, produktivitas,
+ kemampuan, atau kesetaraan penyedia.
+
## Yang baru di v2.3
- **Penyempurnaan di seluruh seri v2.3** — metadata model GPT-6 Astra dan
diff --git a/README-ja.md b/README-ja.md
index 17f90839..8a3a9984 100644
--- a/README-ja.md
+++ b/README-ja.md
@@ -1,14 +1,14 @@
# Claude Code 使用量モニター
-🌐 **言語**: [🏠 Main](README.md) | [English](README-en.md) | [繁體中文](README-zh-TW.md) | [简体中文](README-zh-CN.md) | **日本語** | [한국어](README-ko.md) | [Bahasa Indonesia](README-id.md)
+🌐 **言語**: [🏠 Main](README.md) | [English](README-en.md) | [Deutsch](README-de-DE.md) | [繁體中文](README-zh-TW.md) | [简体中文](README-zh-CN.md) | **日本語** | [한국어](README-ko.md) | [Português (Brasil)](README-pt-BR.md) | [Bahasa Indonesia](README-id.md)
---
-**Claude Code と Codex のローカル使用量を改善するステータスバーコーチ。** 請求ツールではありません。Claude のコスト / クォータ表示を維持し、Codex Beta は Codex 固有のトークンと動作の意味で分析します。
+**Claude Code と Codex のローカル使用量を改善するステータスバーコーチ。** 請求ツールではありません。Claude のコスト / クォータ表示を維持し、Codex は Codex 固有のトークンと動作の意味で分析します。
-> **これは何か**:ローカルの Claude Code 会話ログを読み取り、**トークンから算出した**使用量とコストの見積もりを表示する VS Code ステータスバーモニター。さらに、プロンプトの改善と無駄削減を提案する任意の AI アドバイザー付き。
+> **これは何か**:ローカルの Claude Code と Codex の使用ログを読み取り、それぞれの意味に沿ってトークン、利用枠、推定値を表示する VS Code ステータスバーモニター。無駄な消費を減らす任意のアドバイスも提供します。
>
-> **これは何ではないか**:請求ツールではありません。表示される金額はすべて公開のトークン単価に基づく見積もりです。実際の請求は Anthropic アカウントをご確認ください。
+> **これは何ではないか**:請求ツールではありません。Claude のコストと Codex の API 相当コストは推定値であり、サブスクリプションの請求額ではありません。実際の請求は各プロバイダーのアカウントで確認してください。Codex の利用枠はローカルで最後に観測した値で、リアルタイム残高ではありません。
> スクリーンショットには英語と簡体字中国語を使用しています。詳細は[メイン README](README.md) を参照してください。
@@ -48,6 +48,7 @@
## 主な機能
- **ステータスバー** — 本日のコスト、現在のセッションのコスト、そして Claude Code 自身の OAuth セッションから読み取る実際の 5 時間 / 週間クォータ(`5h:N% wk:N%`)。設定不要。
+- **クォータ表示形式** — ⚙ 設定で標準(既定)・5 時間のみ・週間のみを選択できます。「カスタム」を選んだ場合だけテンプレート欄が表示されます。
- **ダッシュボードタブ** — 今日 / 直近 30 日 / 全期間 に加え、**Sessions / Projects / Content / Branches**。すべて並べ替え可能。
- **積み上げコスト構成チャート**(Y 軸と参照線付き)— 各日 / 各月のコストが入力・出力・キャッシュ書き込み・キャッシュ読み取りにどれだけ使われたか一目で分かります。
- **Content タブ** — どの内容がトークンを消費しているか(あなたのプロンプト vs ツール結果 vs アシスタント出力 / 思考)を推定。
@@ -56,6 +57,23 @@
- **マルチベンダー価格** — Opus 4.x / Sonnet 4.x / Haiku 4.5 は Anthropic 公式価格で検証済み。OpenAI / Gemini / DeepSeek / Kimi / GLM / Qwen の参考価格(ファミリー認識フォールバック付き)。`Refresh Token Pricing` で LiteLLM の最新価格を取得。
- **パーソナライズ** — 言語、タイムゾーン、小数点桁数、コンパクト数値、プロジェクトのグループ化、ダッシュボード自動更新の切り替え。
+## v2.4 の新機能
+
+- **Codex のステータスとスクロール** — 短い表示の既定値は今日の処理済みトークン、
+ 週間枠は残量です。スクロール中のパネル更新は短時間だけ延期し、待ち時間には上限を設けています。
+- **単一のプレビュー優先共有ワークスペース** — 全幅の書き出しプレビューを中心に置き、
+ コントロールをその下へ統一しました。1 つの表示形式セレクターで**統合アクティビティヒートマップ**、
+ **Claude Share Card**、**Claude トークンヒートマップ**を切り替えます。統合表示は両プロバイダーに
+ 実データがある場合だけ使え、従来の 2 表示は引き続き Claude 専用です。
+- **重複パネルなしの互換入口** — `exportShareCard`、`exportHeatmap`、
+ `publishHeatmapToGitHub` は維持され、対応するプレビューを開きます。`enableShareCard` は既定オンの
+ 唯一の表示切替です。廃止済みの `showHeatmap` は 1 リリース分の状態互換性と限定的な消去のためだけに残ります。
+- **厳格なローカル成果物/プロバイダー境界** — 表示の選択、プレビュー、ローカル
+ SVG/Markdown 書き出しは既に集計済みのデータだけを使い、ネットワーク要求、GitHub
+ サインイン、プロフィール/アバター/名前の取得を行いません。接続できるのは Claude
+ ヒートマップの独立した **GitHub に公開** 操作だけです。公開リポジトリ限定で、書き込み前に
+ 正確な対象と作成/上書き操作を確認します。統合活動は請求、成果、生産性、能力、プロバイダー間の等価性を示しません。
+
## v2.3 の新機能
- **v2.3 系列全体の改善** — GPT-6 Astra と Fable 5.1 のモデル情報、任意の
diff --git a/README-ko.md b/README-ko.md
index d9b5ec2e..dca36d59 100644
--- a/README-ko.md
+++ b/README-ko.md
@@ -1,14 +1,14 @@
# Claude Code 사용량 모니터
-🌐 **언어**: [🏠 Main](README.md) | [English](README-en.md) | [繁體中文](README-zh-TW.md) | [简体中文](README-zh-CN.md) | [日本語](README-ja.md) | **한국어** | [Bahasa Indonesia](README-id.md)
+🌐 **언어**: [🏠 Main](README.md) | [English](README-en.md) | [Deutsch](README-de-DE.md) | [繁體中文](README-zh-TW.md) | [简体中文](README-zh-CN.md) | [日本語](README-ja.md) | **한국어** | [Português (Brasil)](README-pt-BR.md) | [Bahasa Indonesia](README-id.md)
---
-**Claude Code와 Codex의 로컬 사용량을 개선하는 상태 표시줄 코치.** 청구 도구가 아닙니다. Claude 비용 / 쿼터 보기를 유지하면서 Codex Beta는 Codex 고유의 토큰과 동작 의미로 분석합니다.
+**Claude Code와 Codex의 로컬 사용량을 개선하는 상태 표시줄 코치.** 청구 도구가 아닙니다. Claude 비용 / 쿼터 보기를 유지하면서 Codex는 Codex 고유의 토큰과 동작 의미로 분석합니다.
-> **무엇인가**: 로컬 Claude Code 대화 로그를 읽어 **토큰 기반**의 사용량과 비용 추정치를 보여주는 VS Code 상태 표시줄 모니터. 프롬프트 개선과 낭비 절감을 제안하는 선택적 AI 어드바이저 포함.
+> **무엇인가**: 로컬 Claude Code와 Codex 사용 로그를 읽고 각 공급자의 의미에 맞게 토큰, 한도, 추정치를 보여주는 VS Code 상태 표시줄 모니터입니다. 불필요한 사용을 줄이는 선택적 조언도 제공합니다.
>
-> **무엇이 아닌가**: 청구 도구가 아닙니다. 표시되는 모든 금액은 공개된 100만 토큰당 단가 기준 추정치입니다. 실제 청구는 Anthropic 계정을 확인하세요.
+> **무엇이 아닌가**: 청구 도구가 아닙니다. Claude 비용과 Codex의 API 환산 비용은 추정치이며 구독 청구액이 아닙니다. 실제 청구는 해당 공급자 계정에서 확인하세요. Codex 한도는 로컬에서 마지막으로 관측한 값이며 실시간 잔액이 아닙니다.
> 스크린샷에는 영어와 중국어 간체 UI가 포함됩니다. 전체 기능 설명은 [메인 README](README.md)를 참고하세요.
@@ -48,6 +48,7 @@
## 기능
- **상태 표시줄** — 오늘 비용, 현재 세션 비용, 그리고 Claude Code 자체 OAuth 세션에서 읽는 실제 5시간 / 주간 쿼터(`5h:N% wk:N%`). 설정 불필요.
+- **쿼터 표시 형식** — ⚙ 설정에서 기본(기본값), 5시간만, 주간만 선택할 수 있습니다. 사용자 지정을 선택할 때만 템플릿 입력칸이 표시됩니다.
- **대시보드 탭** — 오늘 / 최근 30일 / 전체 기간, 그리고 **Sessions / Projects / Content / Branches**. 모두 정렬 가능.
- **누적 비용 구성 차트**(Y축과 기준선 포함) — 각 일 / 월의 비용 중 입력·출력·캐시 쓰기·캐시 읽기가 차지하는 비율을 한눈에.
- **Content 탭** — 어떤 콘텐츠가 토큰을 소비하는지 추정(내 프롬프트 vs 도구 결과 vs 어시스턴트 출력 / 사고).
@@ -56,6 +57,23 @@
- **멀티 벤더 가격** — Opus 4.x / Sonnet 4.x / Haiku 4.5는 Anthropic 공식 가격으로 검증. OpenAI / Gemini / DeepSeek / Kimi / GLM / Qwen 참고 가격(패밀리 인식 폴백 포함). `Refresh Token Pricing`으로 LiteLLM 최신 가격 가져오기.
- **개인화** — 언어, 시간대, 소수 자릿수, 간략 숫자, 프로젝트 그룹화, 대시보드 자동 새로고침 토글.
+## v2.4 새로운 기능
+
+- **Codex 상태와 스크롤** — 간결한 기본 지표는 오늘 처리한 토큰이며 주간 한도는
+ 남은 비율을 표시합니다. 스크롤 중 패널 갱신은 잠시 미루되 대기 시간에 상한을 둡니다.
+- **하나의 미리보기 우선 공유 작업 공간** — 전체 너비 내보내기 미리보기를 중심에 두고
+ 컨트롤은 아래에 배치했습니다. 하나의 표시 형식 선택기로 **통합 활동 히트맵**,
+ **Claude 공유 카드**, **Claude 토큰 히트맵**을 전환합니다. 통합 표시는 두 공급자 모두 실제 데이터가
+ 있을 때만 사용할 수 있으며, 기존 두 표시는 계속 Claude 전용입니다.
+- **중복 패널 없는 호환 진입점** — `exportShareCard`, `exportHeatmap`,
+ `publishHeatmapToGitHub`는 유지되며 해당 미리보기를 엽니다. `enableShareCard`는 기본 켜짐인
+ 유일한 표시 스위치입니다. 폐기된 `showHeatmap`은 한 릴리스 동안 상태 호환성과 제한된 삭제를 위해서만 유지됩니다.
+- **엄격한 로컬 산출물 및 공급자 경계** — 표시 선택, 미리보기, 로컬 SVG/Markdown
+ 내보내기는 이미 집계된 데이터만 사용하며 네트워크 요청, GitHub 로그인 또는 프로필/아바타/이름
+ 조회를 수행하지 않습니다. 연결할 수 있는 것은 Claude 히트맵의 별도 **GitHub에 게시** 작업뿐입니다.
+ 이 작업은 공개 저장소로 제한되며 쓰기 전에 정확한 대상과 생성/덮어쓰기 작업을 확인합니다.
+ 통합 활동은 청구, 생산성, 능력 또는 공급자 간 동등성을 뜻하지 않습니다.
+
## v2.3 새로운 기능
- **v2.3 계열 전반의 개선** — GPT-6 Astra와 Fable 5.1 모델 메타데이터,
diff --git a/README-pt-BR.md b/README-pt-BR.md
new file mode 100644
index 00000000..daea74a0
--- /dev/null
+++ b/README-pt-BR.md
@@ -0,0 +1,58 @@
+# Claude Code Usage
+
+🌐 **Idioma**: [Página principal](README.md) · [English](README-en.md) · [Deutsch](README-de-DE.md) · [繁體中文](README-zh-TW.md) · [简体中文](README-zh-CN.md) · [日本語](README-ja.md) · [한국어](README-ko.md) · **Português (Brasil)** · [Bahasa Indonesia](README-id.md)
+
+**Uso local do Claude Code e do Codex na barra de status do VS Code.** A
+extensão lê registros locais e mostra cada provedor conforme o significado
+dos seus dados. **Não é uma ferramenta de cobrança.**
+
+## O essencial
+
+- **Claude Code:** custos estimados a partir de tokens e, quando a conta está
+ autenticada, limites oficiais de 5 horas e semanais do perfil ativo.
+- **Codex:** tokens processados hoje e saldo semanal restante da última
+ observação local. Esse saldo não é uma consulta em tempo real à conta.
+- **Painel:** hoje, últimos 30 dias, histórico, sessões, projetos e sugestões
+ locais. Os gráficos podem detalhar mês, dia e hora sem reler os registros
+ a cada clique.
+- **Compartilhamento:** uma área única com prévia antes da exportação. SVG e
+ Markdown locais não enviam dados; publicar no GitHub exige confirmação
+ separada. A opção vem ativada e pode ser desativada nas configurações.
+
+
+
+A imagem usa dados sintéticos, não informações de uma conta real. Veja mais
+imagens e detalhes na [página principal completa em inglês](README.md).
+
+## Como interpretar os números
+
+No Codex, **processado = entrada + saída** e **uso sem cache = entrada sem
+cache + saída**. A entrada em cache já faz parte da entrada; os tokens de
+raciocínio já fazem parte da saída. Não some essas parcelas outra vez. O
+**custo equivalente de API** é apenas uma estimativa com preços conhecidos,
+não uma fatura nem cobrança da assinatura. Custos e limites de Claude e Codex
+não são somados entre si. Registros ausentes ou ainda não indexados podem
+reduzir os totais mostrados; o limite do Codex é a última observação local.
+
+## Instalação e privacidade
+
+Procure `Claude Code Usage` em **Extensões** no VS Code ou execute
+`ext install GrowthJack.claude-code-usage`. Abra o painel pela barra de status
+ou pelo comando **Show Usage Details**. Idioma, diretório de dados e opções
+de exibição ficam nas configurações. Os registros do Claude são lidos
+localmente; o índice do Codex guarda agregados pseudônimos, não o conteúdo
+das conversas. A orientação por IA é opcional: só envia uma solicitação após
+prévia e envio explícito, com sua própria chave de API. Detalhes e formas de
+apagar dados derivados: [Dados locais e privacidade](LOCAL-DATA.md).
+
+Para a referência completa, consulte [inglês](README.md) ou
+[chinês simplificado](README-zh-CN.md). Relate problemas e sugestões em
+[Issues](https://github.com/ClaudeCodeUsage/ClaudeCodeUsage/issues).
+
+## Créditos e licença
+
+Licença MIT. A manutenção usa [Claude Code](https://claude.com/claude-code)
+e [OpenAI Codex](https://developers.openai.com/codex/) como ferramentas;
+contribuições humanas constam no [changelog](CHANGELOG.md).
+
+[Licença MIT](LICENSE)
diff --git a/README-zh-CN.md b/README-zh-CN.md
index 475e520d..9d6cfd42 100644
--- a/README-zh-CN.md
+++ b/README-zh-CN.md
@@ -1,14 +1,14 @@
# Claude Code 使用量监控
-🌐 **语言**: [🏠 Main](README.md) | [English](README-en.md) | [繁體中文](README-zh-TW.md) | **简体中文** | [日本語](README-ja.md) | [한국어](README-ko.md) | [Bahasa Indonesia](README-id.md)
+🌐 **语言**: [🏠 Main](README.md) | [English](README-en.md) | [Deutsch](README-de-DE.md) | [繁體中文](README-zh-TW.md) | **简体中文** | [日本語](README-ja.md) | [한국어](README-ko.md) | [Português (Brasil)](README-pt-BR.md) | [Bahasa Indonesia](README-id.md)
---
-**状态栏中的 Claude Code 与 Codex 本地用量教练。** 它不是账单工具。Claude 保留成本与配额视图;Codex Beta 按自己的 token 与行为语义提供分析,无需硬套 Claude 的结构。
+**状态栏中的 Claude Code 与 Codex 本地用量教练。** 它不是账单工具。Claude 保留成本与配额视图;Codex 按自己的 token 与行为语义提供分析,无需硬套 Claude 的结构。
-> **它是什么**:一个 VS Code 状态栏小工具,读取本地 Claude Code 对话日志,用 token 数量乘以公开单价来估算使用量和成本;并可提供 AI 建议,帮你优化提示、减少浪费。
+> **它是什么**:一个 VS Code 状态栏小工具,读取本地 Claude Code 与 Codex 用量日志,分别展示各自的 token、额度与估算值;还可提供可选建议,帮助减少不必要的消耗。
>
-> **它不是什么**:账单工具。所有金额均为估算值(基于公开的每百万 token 单价),实际费用请以 Anthropic 官方账单为准。
+> **它不是什么**:账单工具。Claude 成本和 Codex API 等效成本都是估算值,不是订阅扣费或发票;实际账单请以对应供应商账号为准。Codex 额度仅是本地最后观测,不是实时余额。
> 截图包含中英文界面;各图标明主题与范围。
@@ -50,6 +50,7 @@
以上五张 v2.3 截图使用生产仪表板渲染器、合成夹具及 VS Code Light+/Dark+ 主题变量生成,不是个人用量或账单证据;原生 VSIX 安装单独验收。
- Codex 主状态项显示**今日已用 token**,周额度项显示**余量**:已用 36% 时为 `wk 64%`;悬浮卡保留已用进度条、重置时间和分行说明。额度来自本地最后观测,不是实时余额。
+- Claude 配额格式在 ⚙ 设置中用下拉栏选择:默认布局、仅 5 小时、仅每周或自定义;只有选“自定义”才显示模板输入框。
- 周期详情默认折叠;分享设置位于图下方。分享工作台默认开启、可关闭;色阶可选分位数、对数或线性。
- CLI 调用只有留下带 usage 的持久会话日志才能计入。未保存会话的历史调用不能回填;今日和最近 30 天都采用配置时区。
- Projects 为两个供应商新增仅 Token 的 30/90 天“项目 × 日期”热力图与每日堆叠趋势,并保留精确提示、覆盖状态、有界行数和“其他项目”长尾。
@@ -76,6 +77,25 @@ AI 个性化仍为可选项。自备 `advice.apiKey` 后,默认请求只含聚
---
+## 2.4 新功能
+
+- **Codex 状态栏与滚动**:默认简短指标显示今日已处理 Token,独立的每周额度显示
+ 剩余比例。滚动期间面板刷新短暂延后,且有最长等待边界。
+- **统一、预览优先的分享工作台**:全宽导出预览位于视觉中心,控制项统一放在下方;
+ 一个呈现方式选择器即可切换**综合活动热力图**、**Claude 分享卡**与 **Claude token
+ 热力图**。综合呈现只在两个供应商都有真实数据时出现,后两种旧版呈现仍只使用 Claude 数据。
+- **兼容入口,不再重复面板**:`exportShareCard`、`exportHeatmap` 与
+ `publishHeatmapToGitHub` 继续保留,并打开对应预览。`enableShareCard` 仍是唯一可见的
+ 分享开关且默认开启;退役的 `showHeatmap` 仅为一个版本的状态兼容与有界清除而保留。
+- **严格的本地产物与供应商边界**:切换、预览和本地 SVG/Markdown 导出只使用已物化聚合,
+ 不发起网络请求,也不登录 GitHub 或获取 profile/头像/名称。只有 Claude 热力图中独立的
+ **发布到 GitHub** 动作可以联网;它仍仅支持公开仓库,并在写入前确认精确目标及创建/覆盖动作。
+ 综合活动不代表账单、生产率、能力或跨供应商等价。
+
+
+
+此图来自生产渲染器、合成用量与 VS Code 主题变量;不是已安装 VSIX 或真实账号的截图。
+
## 2.3 新功能
- **2.3 系列持续完善**——新增 GPT-6 Astra、Fable 5.1 模型元数据与可选
@@ -96,7 +116,7 @@ AI 个性化仍为可选项。自备 `advice.apiKey` 后,默认请求只含聚
## 2.2 新功能
-- **用量分享卡**(可选,`enableShareCard`):一张可配置的单页 SVG 用量卡——自选时间范围 × 范围(总体 / 工程 / 会话)× 展示哪些指标,以及主题(**Claude 经典橙** / **奶油** / **极光暗色** / **自动**),可选带上 GitHub 头像与名称。自包含、可复现;提示词、路径、ID 一律不出本机。中文语言下使用 万/亿 单位。
+- **用量分享卡**(可选,`enableShareCard`):一张可配置的单页 SVG 用量卡——自选时间范围 × 范围(总体 / 工程 / 会话)× 展示哪些指标,以及主题(**Claude 经典橙** / **奶油** / **极光暗色** / **自动**)。预览和本地导出不会获取 GitHub 身份数据。自包含、可复现;提示词、路径、ID 一律不出本机。中文语言下使用 万/亿 单位。
- **只读会话查看器**(会话标签,**默认开启**):每行的「查看」按钮以只读方式重开某个历史会话的提示词与 Markdown 渲染的回答,帮你回忆内容,而**不必**像「恢复」那样把它重新塞回模型上下文。思考与工具调用收进折叠区,默认展示最近若干轮。
- **Token 热力图**(可选,`showHeatmap`):All 标签顶部的 GitHub 风格年度 token 热力图,并可**导出 / 发布到你的 GitHub 主页**(自包含 SVG,一键复制 Markdown 嵌入代码)。
- **实验性洞察**(可选,`showInsights`,Content 标签):基于本地日志的启发式估算(均标注为估算)——**缓存损耗账单**(模型切换 / 空闲导致重写缓存的花费)、**各模型缓存保温时长**、**大单轮**、**你的活跃时段**、以及**技能 ROI**(每美元产出的输出 token)。
@@ -117,7 +137,7 @@ AI 个性化仍为可选项。自备 `advice.apiKey` 后,默认请求只含聚
- **用量追踪面板**:对标官方 `/usage` 的"用量构成"视图,但支持全部模型 / 供应商和五档范围(日 / 周 / 月 / 会话 / 项目):>150k 上下文占比、8 小时以上会话占比、子代理密集占比、工作流占比,以及 Skills/子代理/插件/模型四类细分。今日标签页有精简卡片。
- **思考占比**:每会话的估算思考 token 占比(Sessions 列 + 今日卡片),过高时提示改用 `/effort`。
- **工作流配额护栏**:当 5 小时窗口剩余不足以完成一次运行时,仪表板显示可关闭的警告横幅(`claudeCodeUsage.workflowQuotaWarnPercent`)。
-- **设置搬进仪表板**:新增 ⚙ 设置标签页,就地管理所有选项;VS Code 原生设置只保留三个适合同步的(`language`、`dataDirectory`、`advice.apiKey`)。右上角按钮精简为 ✨ AI 建议 和 ⚙ 设置(都跳到对应标签);自动刷新开关挪进设置(暂停时右上角才出现手动 ↻)。如果你把成本、配额、上下文三项**全部隐藏**,状态栏会保留一个小图标作为回到仪表板的入口。
+- **设置搬进仪表板**:新增 ⚙ 设置标签页,就地管理大多数选项;VS Code 原生设置保留语言及 Claude/Codex 数据目录。自备 AI 密钥在仪表板填写、保存在 SecretStorage,不随 Settings Sync 同步。右上角按钮精简为 ✨ AI 建议 和 ⚙ 设置(都跳到对应标签);自动刷新开关挪进设置(暂停时右上角才出现手动 ↻)。如果你把成本、配额、上下文三项**全部隐藏**,状态栏会保留一个小图标作为回到仪表板的入口。
- **状态栏指标**(`statusBarMetric`):默认显示今日成本,也可切换为今日**总 token** 消耗(紧凑 k/M)。
- **模型专属每周上限**(`showScopedWeekly`,可选开启):显示 Anthropic
实际返回的受限模型名称,例如 `fable 17%`;由 PR #38
@@ -167,16 +187,18 @@ ext install GrowthJack.claude-code-usage
**绝大多数设置现在都在仪表板里。** 打开仪表板(运行 **Show Usage Details**,或点右上角 ⚙),用 **⚙ 设置**标签页 —— 分为「常规 / 状态栏 / 数据与刷新 / AI 建议与优化器」。改动即时生效。
-为了让 VS Code 原生设置 UI 保持清爽,只保留三个设置在那里(以便随 Settings Sync 同步)。打开设置(`Ctrl+,`)搜索 **`Claude Code Usage`**:
+为了让 VS Code 原生设置 UI 保持清爽,只保留以下三个普通设置;自备 AI 密钥请在仪表板的 ⚙ 设置中填写,它只存入 SecretStorage,不随 Settings Sync 同步。打开 VS Code 设置(`Ctrl+,`)搜索 **`Claude Code Usage`**:
| 设置 | 默认 | 作用 |
|---|---|---|
| `language` | `"auto"` | 界面语言:`auto` / `en` / `de-DE` / `zh-TW` / `zh-CN` / `ja` / `ko` / `pt-BR` / `id`。 |
| `dataDirectory` | `""` | 自定义 Claude 数据目录;留空 = 自动检测。 |
-| `advice.apiKey` | `""` | AI 建议 + 用量优化器的自备 API key;留空时不能发送请求。 |
+| `codex.dataDirectory` | `""` | 自定义 Codex 数据目录;留空 = `CODEX_HOME` 或 `~/.codex`。 |
其余全部 —— 刷新间隔、状态栏各项、数字 / 日期格式、项目分组、内容分析,以及所有 AI 建议 / 优化器选项 —— 都在仪表板的 ⚙ 设置标签页里。升级不丢配置:首次启动会做一次性迁移,把你 `settings.json` 里已有的值搬过来。
+如需自定义 Claude 配额状态栏,在 ⚙ 设置中选择“自定义”。模板支持 `{5h.pct}`、`{wk.pct}`(或 `{7d.pct}`)和 `{model:Fable.pct}`;每个窗口也支持 `.label`、`.reset`,重置样式可用 `:decimal`、`:units`、`:clock`、`:at`,例如 `{5h.pct} | {wk.reset:at}`。未报告的窗口及其分隔符会省略;选“默认”则沿用原有额度选项。
+
---
## 成本是如何计算的
@@ -211,7 +233,7 @@ ext install GrowthJack.claude-code-usage
| 建议状态/密钥 | 有界聚合证据;密钥仅在 SecretStorage | 另行点击发送后才传输精确预览过的请求 |
- 所有 token / 成本 / session 分析都在**本地**进行,只读取你的 `~/.claude/projects/**/*.jsonl` 文件。
-- 配额指示器用 Claude Code 现有的 OAuth token 调用 **`api.anthropic.com/api/oauth/usage`**,不发送任何额外凭证。
+- 配额指示器用 Claude Code 现有的 OAuth token 调用 **`api.anthropic.com/api/oauth/usage`**。如 token 已过期,插件会将现有 refresh token 发送至 **`console.anthropic.com/v1/oauth/token`**,并把刷新后的凭证写回选中的 Claude 凭证文件或 macOS 钥匙串。详见[本地数据与隐私](LOCAL-DATA.zh-CN.md)。
- **AI 建议**和**用量优化器**是仅有的会调用模型的功能,而且只在你查看完整请求后再次主动点击「发送」时调用。AI 建议默认只发送聚合证据;提示样本与用户上下文需要独立同意,并完整出现在预览中。优化器只包含你粘贴的文字。两者仅使用 `advice.apiUrl` 与自备 `advice.apiKey`;密钥只进入请求头,插件不使用订阅凭据,也没有后台请求。
### 已知限制
diff --git a/README-zh-TW.md b/README-zh-TW.md
index 22d52cad..be3c02a4 100644
--- a/README-zh-TW.md
+++ b/README-zh-TW.md
@@ -1,14 +1,14 @@
# Claude Code 使用量監控
-🌐 **語言**: [🏠 Main](README.md) | [English](README-en.md) | **繁體中文** | [简体中文](README-zh-CN.md) | [日本語](README-ja.md) | [한국어](README-ko.md) | [Bahasa Indonesia](README-id.md)
+🌐 **語言**: [🏠 Main](README.md) | [English](README-en.md) | [Deutsch](README-de-DE.md) | **繁體中文** | [简体中文](README-zh-CN.md) | [日本語](README-ja.md) | [한국어](README-ko.md) | [Português (Brasil)](README-pt-BR.md) | [Bahasa Indonesia](README-id.md)
---
-**看清 Claude Code 與 Codex 的本地用量,讓 AI 幫你用得更好。** 不是帳單工具。Claude 保留成本與配額檢視;Codex Beta 依自己的 token 與行為語意提供分析。
+**看清 Claude Code 與 Codex 的本地用量,讓 AI 幫你用得更好。** 不是帳單工具。Claude 保留成本與配額檢視;Codex 依自己的 token 與行為語意提供分析。
-> **它是什麼**:一個 VS Code 狀態列小工具,讀取本地 Claude Code 對話日誌,按 token × 公開單價估算用量與成本;並提供可選的 AI 建議功能,幫你優化提示詞、減少不必要的 token 消耗。
+> **它是什麼**:一個 VS Code 狀態列小工具,讀取本地 Claude Code 與 Codex 用量日誌,依各自語意顯示 token、配額與估算值;並提供可選的建議,協助減少不必要的消耗。
>
-> **它不是什麼**:帳單工具。顯示金額均為估算值,實際費用請以官方帳單為準。
+> **它不是什麼**:帳單工具。Claude 成本與 Codex API 等效成本均為估算值,不是訂閱扣款或發票;實際帳單請以相應供應商帳戶為準。Codex 配額僅為本地最後觀測,並非即時餘額。
> 截圖包含英文與簡體中文介面。完整功能說明請見[主 README](README.md)。
@@ -48,6 +48,7 @@
## 功能特色
- **狀態列** — 今日成本、當前 session 成本,以及真實的 5 小時 / 每週配額(`5h:N% wk:N%`),透過 Claude Code 自身的 OAuth 工作階段讀取,無需設定。
+- **配額格式** — ⚙ 設定可選內建(預設)、僅 5 小時或僅每週;選「自訂」時才會顯示範本輸入欄。
- **儀表板分頁** — 今日 / 最近 30 天 / 全部時間,外加 **Sessions / Projects / Content / Branches**,皆可排序。
- **堆疊式成本構成圖**,含 Y 軸與參考線 —— 一眼看出每日 / 每月的成本中,輸入、輸出、快取寫入、快取讀取各佔多少。
- **Content 分頁** — 估算哪些內容消耗你的 token(你的提示 vs 工具結果 vs 助理輸出 / 思考)。
@@ -56,6 +57,21 @@
- **多廠商定價** — Opus 4.x / Sonnet 4.x / Haiku 4.5 對照 Anthropic 官方定價;OpenAI / Gemini / DeepSeek / Kimi / GLM / Qwen 參考價,含家族感知回退。`Refresh Token Pricing` 可拉取 LiteLLM 即時價格。
- **個人化** — 語言、時區、小數位數、精簡數字、專案分組、儀表板自動刷新開關。
+## v2.4 新功能
+
+- **Codex 狀態列與捲動**:預設精簡指標顯示今日已處理 Token,獨立的每週額度顯示
+ 剩餘比例。捲動期間面板更新會短暫延後,並設有最長等待邊界。
+- **統一、預覽優先的分享工作台**:全寬匯出預覽成為視覺焦點,控制項統一置於下方;
+ 一個呈現方式選擇器即可切換**綜合活動熱力圖**、**Claude 分享卡**與 **Claude token
+ 熱力圖**。綜合呈現只在兩個供應商都有真實資料時出現,後兩種舊版呈現仍只使用 Claude 資料。
+- **相容入口,不再重複面板**:`exportShareCard`、`exportHeatmap` 與
+ `publishHeatmapToGitHub` 繼續保留並開啟對應預覽。`enableShareCard` 仍是唯一可見的
+ 分享開關且預設開啟;退役的 `showHeatmap` 僅為一個版本的狀態相容與有界清除而保留。
+- **嚴格的本機產物與供應商邊界**:切換、預覽和本機 SVG/Markdown 匯出只使用已物化彙總,
+ 不發出網路請求,也不登入 GitHub 或取得 profile/頭像/名稱。只有 Claude 熱力圖中獨立的
+ **發佈到 GitHub** 動作可以連線;它仍僅支援公開儲存庫,並在寫入前確認精確目標及建立/覆寫動作。
+ 綜合活動不代表帳單、生產力、能力或跨供應商等價。
+
## v2.3 新功能
- **v2.3 系列持續完善**——新增 GPT-6 Astra、Fable 5.1 模型中繼資料與
diff --git a/README.md b/README.md
index faf10925..80edacef 100644
--- a/README.md
+++ b/README.md
@@ -1,14 +1,13 @@
# Claude Code Usage
[](https://marketplace.visualstudio.com/items?itemName=growthjack.claude-code-usage)
-[](https://marketplace.cursorapi.com/items/?itemName=GrowthJack.claude-code-usage)
+[](https://open-vsx.org/extension/GrowthJack/claude-code-usage)
[](https://opensource.org/licenses/MIT)
**The local Claude Code and Codex usage coach in your status bar.** Not a
-billing tool. Claude keeps its cost and quota views; the v2.3 Codex Beta adds
-provider-specific token and behaviour insights through the same dashboard tabs,
-render functions, and visual system without pretending both providers expose
-the same data.
+billing tool. Claude keeps its cost and quota views; Codex adds provider-specific
+token and behaviour insights through the same dashboard tabs, render functions,
+and visual system without pretending both providers expose the same data.
> **What this is:** A VS Code status-bar monitor that reads your local
> Claude Code and Codex logs and shows provider-appropriate usage views — plus
@@ -23,7 +22,7 @@ the same data.
> **看清 Claude Code 与 Codex 的本地用量,让 AI 帮你用得更好。**
>
> **简介**:一个 VS Code 状态栏小工具。Claude 保留成本与配额视图;
-> v2.3 的 Codex Beta 则按 Codex 自身的数据语义展示 token、effort、任务结构
+> Codex 则按自身的数据语义展示 token、effort、任务结构
> 和本地优化建议,同时复用 Claude 仪表盘的标签页、渲染函数和视觉体系,
> 而不是另做一套页面或强行套用 Claude 的统计口径。
>
@@ -33,10 +32,12 @@ the same data.
🌐 **Multi-language documentation**:
[English](README-en.md) ·
+[Deutsch](README-de-DE.md) ·
[繁體中文](README-zh-TW.md) ·
[简体中文](README-zh-CN.md) ·
[日本語](README-ja.md) ·
[한국어](README-ko.md) ·
+[Português (Brasil)](README-pt-BR.md) ·
[Bahasa Indonesia](README-id.md)
---
@@ -100,6 +101,8 @@ Hover the quota indicator for a breakdown:
*Real `/usage` data: utilisation percent, plus time left and the wall-clock reset for every window.*
*Every weekly cap your plan meters gets its own row, per-model ones included (Anthropic supplies the name, so the row follows whichever model is capped), plus usage credits when you have them enabled.*
+⚙ Settings offers a compact quota-format dropdown: Built-in (default), 5-hour only, Weekly only, or Custom. Only Custom reveals the template field.
+
### Dashboard

@@ -160,6 +163,38 @@ same full-request preview and separate explicit Send action as AI advice.
---
+## What's new in 2.4
+
+- **Codex status and scrolling** — the default compact Token item shows today's
+ processed amount; the separate weekly indicator shows remaining capacity.
+ Live panel updates wait briefly for scrolling to pause, with a bounded delay.
+- **One preview-first sharing workspace** — a full-width export preview now
+ leads the surface, with controls below it and one presentation selector for
+ the **Combined activity heatmap**, **Claude Share Card**, and **Claude token
+ heatmap**. The combined presentation appears only with real data from both
+ providers; both legacy presentations remain Claude-only.
+- **Compatibility without duplicate panels** — `exportShareCard`,
+ `exportHeatmap`, and `publishHeatmapToGitHub` remain available and open the
+ matching presentation instead of bypassing preview. `enableShareCard` remains
+ the single visible sharing switch and still defaults to on; the retired
+ `showHeatmap` value is retained only for one-release state compatibility and
+ bounded clearing.
+- **Strict local-artifact boundary and provider truth** — switching, previewing,
+ and local SVG/Markdown export use materialized aggregates only and perform no
+ network request, GitHub sign-in, or profile/avatar/name lookup. Only the
+ separate **Publish to GitHub** action for the Claude heatmap can connect; it
+ remains public-repository-only and confirms the exact target and create/
+ overwrite action before writing. Combined activity is not billing,
+ productivity, capability, or cross-provider equivalence.
+
+
+
+This is a production-renderer capture with synthetic usage and VS Code theme
+variables, not evidence from an installed VSIX or a real account. The
+[Claude Share Card](images/v2.4.0/claude-sharing-zh-CN-dark.png) and
+[360 px dark-theme view](images/v2.4.0/narrow-sharing-de-DE-dark.png) use the same
+fixture boundary.
+
## What's new in 2.3
- **Refined throughout the v2.3 line** — GPT-6 Astra and Fable 5.1 model
@@ -279,7 +314,8 @@ same full-request preview and separate explicit Send action as AI advice.
- **Usage share card** (opt-in, `enableShareCard`) — a themed, configurable
one-page SVG of your usage: pick a range × scope (overall / project / session)
× which metrics to show, and a theme (**Claude Classic** / **Cream** /
- **Aurora Dark** / **Auto**), with an optional GitHub avatar + name.
+ **Aurora Dark** / **Auto**). Preview and local export never fetch GitHub
+ identity data.
Self-contained and deterministic; no prompts, paths or ids ever leave your
machine. Chinese locales use 万/亿 units.
- **Read-only conversation viewer** (Sessions tab, **on by default**) — a "view"
@@ -341,9 +377,10 @@ same full-request preview and separate explicit Send action as AI advice.
- **Workflow quota guard** — a dismissible banner before you start a run
the remaining 5-hour window can't finish
(`claudeCodeUsage.workflowQuotaWarnPercent`).
-- **Settings in the dashboard** — a new ⚙ Settings tab manages every option
- in place; VS Code's own Settings keeps only the three that benefit from
- syncing (`language`, `dataDirectory`, `advice.apiKey`). Header buttons
+- **Settings in the dashboard** — a new ⚙ Settings tab manages most options
+ in place; VS Code's own Settings keeps the language and provider data
+ directories. The advice API key is entered in the dashboard and stored in
+ SecretStorage, not Settings Sync. Header buttons
trimmed to ✨ AI advice and ⚙ Settings (both jump to their tab); the
auto-refresh toggle moved into Settings (a manual ↻ appears when paused).
If you hide the cost, quota *and* context items, the status bar keeps a small
@@ -417,7 +454,7 @@ ext install GrowthJack.claude-code-usage
### Cursor / Windsurf / Antigravity (Open VSX)
Same extension is published at the Open VSX Registry:
-[GrowthJack.claude-code-usage](https://marketplace.cursorapi.com/items/?itemName=GrowthJack.claude-code-usage).
+[GrowthJack.claude-code-usage](https://open-vsx.org/extension/GrowthJack/claude-code-usage).
### From a `.vsix` file
@@ -433,8 +470,10 @@ downloaded `.vsix`.
tab — grouped into General, Status bar, Data & refresh, and AI advice &
Optimizer. Changes apply immediately.
-To keep VS Code's own Settings UI uncluttered, only four settings stay there
-(so they still travel with Settings Sync). Open Settings (`Ctrl+,`) and search
+To keep VS Code's own Settings UI uncluttered, only three ordinary settings
+stay there. The BYOK advice key is entered in the dashboard's ⚙ Settings tab
+and stored in SecretStorage; it does not travel with Settings Sync. Open VS Code
+Settings (`Ctrl+,`) and search
for **`Claude Code Usage`**:
| Setting | Default | What it does |
@@ -442,13 +481,19 @@ for **`Claude Code Usage`**:
| `language` | `"auto"` | UI language: `auto` / `en` / `de-DE` / `zh-TW` / `zh-CN` / `ja` / `ko` / `pt-BR` / `id`. |
| `dataDirectory` | `""` | Custom Claude data dir; empty = auto-detect. |
| `codex.dataDirectory` | `""` | Custom Codex home; empty = `CODEX_HOME` or `~/.codex`. |
-| `advice.apiKey` | `""` | Bring-your-own key for AI advice + the Usage Optimizer; empty means no request can be sent. |
Everything else — refresh interval, status-bar items, number/date formatting,
project grouping, content analysis, and all the AI advice / Optimizer options —
is in the dashboard's ⚙ Settings tab. Upgrading keeps your existing values: a
one-time migration copies them out of `settings.json` on first launch.
+For a custom quota status-bar layout, choose **Custom** in ⚙ Settings. The
+template accepts `{5h.pct}`, `{wk.pct}` (or `{7d.pct}`), and
+`{model:Fable.pct}`; each window also supports `.label` and `.reset`.
+Reset styles include `:decimal`, `:units`, `:clock`, and `:at`, for example
+`{5h.pct} | {wk.reset:at}`. Missing windows and their separators are omitted.
+The built-in choice leaves the existing quota options unchanged.
+
---
## How costs are calculated
@@ -514,7 +559,10 @@ remote boundaries are in [Local data and privacy](LOCAL-DATA.md) ([简体中文]
effort, role, time, and quality metadata. It never stores raw IDs, full paths
or repository URLs, thread titles, or conversation bodies.
- The quota indicator calls **`api.anthropic.com/api/oauth/usage`** using
- Claude Code's existing OAuth token. No additional credentials are sent.
+ Claude Code's existing OAuth token. If that token has expired, the extension
+ sends the existing refresh token to **`console.anthropic.com/v1/oauth/token`**
+ and writes the refreshed credentials back to the selected Claude credential
+ file or macOS Keychain item. See [Local data and privacy](LOCAL-DATA.md).
- **AI advice** and the **Usage Optimizer** are the only features that call a
model — and only after *you* preview and explicitly send a prepared request.
Advice defaults to allowlisted aggregates; prompt samples and optional user
diff --git a/docs/superpowers/specs/2026-09-02-v2.3.1-local-data-contract.md b/docs/superpowers/specs/2026-09-02-v2.3.1-local-data-contract.md
index 684f0e98..c74b295a 100644
--- a/docs/superpowers/specs/2026-09-02-v2.3.1-local-data-contract.md
+++ b/docs/superpowers/specs/2026-09-02-v2.3.1-local-data-contract.md
@@ -27,12 +27,12 @@ This document is the source of truth for data read, retained, derived, displayed
| P3 | Latest Claude quota response **migration input** | `ccu.usageLimits.` in globalState | Legacy response plus timestamp | v2.3.1 reads once, normalizes into P2, then removes it. No raw response is a long-term source of truth | No additional request |
| P4 | Legacy Claude weekly history **migration input** | `ccu.weeklyQuotaHistory.v1.` in globalState | Legacy normalized observations | v2.3.1 imports, deduplicates into P2, then removes it | None |
| P5 | Legacy Codex quota history **migration input** | `quotaHistory` inside P1 | provider, observed/reset time, used percent | v2.3.1 normalizes into schema 2 without inventing an account. Rebuilding P1 must not erase already migrated P2 history | None |
-| P6 | Extension preferences and migration flags | Exact catalogued `ccu.setting.` names, `ccu.lastSeenVersion`, `ccu.codex.backgroundWork.v1`, migration flags, and `ccu.localData.pendingClientReset.v1` in globalState; language and data directories in VS Code configuration | Typed preferences, bounded worker status, schema/migration flags, or one value-free pending client action enum | Preferences remain until reset/clear/uninstall; the pending action remains only until a verified Webview acknowledgement or Clear All | Settings Sync may sync VS Code configuration according to the user's VS Code account; extension globalState is not intentionally uploaded by this extension |
+| P6 | Extension preferences and migration flags | Exact catalogued `ccu.setting.` names, `ccu.lastSeenVersion`, `ccu.codex.backgroundWork.v1`, migration flags, and `ccu.localData.pendingClientReset.v1` in globalState; language and data directories in VS Code configuration | Typed preferences, bounded worker status, schema/migration flags, or one value-free pending client action with a monotonic revision | Preferences remain until reset/clear/uninstall; the pending action remains only until its exact revision receives a verified Webview acknowledgement or Clear All | Settings Sync may sync VS Code configuration according to the user's VS Code account; extension globalState is not intentionally uploaded by this extension |
| P7 | Machine-local pseudonymization material | `ccu.codex.machineSalt` plus versioned local fingerprint salt | Random secret salt and algorithm version only | Until all derived data is cleared. Clearing it invalidates old fingerprints rather than relinking them | Never |
| P8 | BYOK advice credential | `claudeCodeUsage.secret.advice.apiKey` in VS Code SecretStorage | Secret value only | Until the user clears the key or extension data. VS Code documents SecretStorage as encrypted and not synchronized across machines; the concrete protection mechanism remains host-dependent | Sent only as an authorization credential to the user-configured endpoint after a separate Send action; never included in request body or preview |
| P9 | Advice-effectiveness ledger | `ccu.adviceEffectiveness.localState`, schema 3; legacy feedback key may exist | Consent mode, feedback, suppression records, comparable-pair metrics and results; no prompt body/path/account | Bounded by implementation (feedback/suppression 500, pairs/results 200). Clear Advice Data removes current and known legacy keys | Advice payload is sent only after preview and explicit Send |
| P10 | Webview preferences | Webview state and localStorage keys `ccu.activeTab`, `ccu.sessionFilter`, `ccu.sessionRange`, `ccu.sessionModel` | Tab, sort/filter/range, expanded state, scroll and chart selection | Until Reset UI State, Webview storage clear, or uninstall | Never |
-| P11 | Heatmap/share preferences | `showHeatmap`, share-card settings, Webview localStorage `ccu.combinedHeatmap.title`, `ccu.combinedHeatmap.range`, `ccu.combinedHeatmap.intensityMode`, `ccu.combinedHeatmap.privacyPreview`, `ccu.combinedHeatmap.palette`, `ccu.combinedHeatmap.customAccent`, plus globalState `ccu.heatmapRepo` and `ccu.heatmapPath` | Bounded title (80 characters), range/intensity/palette enums, privacy-preview boolean, validated six-digit hex accent, and optional GitHub `owner/repo/path` destination strings | Until Reset Sharing Preferences, Webview storage clear, or all derived data clear | Local SVG/Markdown export has none. Direct publication is public-repository-only in v2.3.1, requires an exact destination confirmation every time, and never caches GitHub credentials |
+| P11 | Sharing-workspace preferences | Active `enableShareCard`, retired compatibility `showHeatmap`, Webview localStorage `ccu.sharing.template`, `ccu.combinedHeatmap.title`, `ccu.combinedHeatmap.range`, `ccu.combinedHeatmap.intensityMode`, `ccu.combinedHeatmap.privacyPreview`, `ccu.combinedHeatmap.palette`, `ccu.combinedHeatmap.customAccent`, plus one versioned globalState object `ccu.heatmapDestination.v1`; legacy `ccu.heatmapRepo` and `ccu.heatmapPath` remain read/clear-only migration inputs | Presentation/range/intensity/palette enums, bounded title (80 characters), privacy-preview boolean, validated six-digit hex accent, and one optional GitHub `owner/repo/path` destination object. Explicit command revisions are provider-lifetime runtime coordination, not browser data | Preferences remain until Reset Sharing Preferences, Webview storage clear, or all derived data clear | Local preview and SVG/Markdown export have none and do not request GitHub identity. Direct publication is public-repository-only, requires an exact destination/action confirmation every time, and never caches GitHub credentials. A successful PUT is reported even if preference persistence fails |
| M1 | Runtime-only Claude/Codex views | Extension-host memory | Records, prepared aggregates, charts, current session mappings, bounded content sample | Released on reload/exit | None unless included in a separately previewed request/export |
| M2 | Runtime credentials and network responses | Extension-host memory or provider-owned credential store | OAuth access/refresh token, transient headers, response/error object | Never retained by this extension. Provider refresh may update the provider-owned credential store | Anthropic OAuth/quota endpoint or user-configured advice endpoint only |
@@ -128,7 +128,7 @@ Provider daily inputs are already deduplicated aggregates. If an identical provi
- Deterministic local SVG and Markdown snippets may contain only selected date range, title, normalized daily aggregates, provider labels, totals, and explicit caveats.
- Export/share payloads must pass privacy-canary tests proving absence of account IDs, fingerprints, project/thread names, repository names, local paths, prompt/log text, and credentials.
-- Optional GitHub owner/repo/path preferences are destinations, not credentials. GitHub authentication remains transient in the VS Code authentication provider. v2.3.1 requests only `public_repo`, verifies public visibility and the default branch, previews the exact owner/repo/branch/path and create-or-overwrite action, and persists destination strings only after a successful confirmed write. Private repositories use local SVG + Markdown instead of silently escalating to broad `repo` access.
+- Optional GitHub owner/repo/path preferences are destinations, not credentials. Local sharing preview/export performs no authentication or identity/profile lookup. Only the separate publish action uses transient GitHub authentication from the VS Code authentication provider, requests `public_repo`, verifies public visibility and the default branch, previews the exact owner/repo/branch/path and create-or-overwrite action, and persists destination strings only after a successful confirmed write. Private repositories use local SVG + Markdown instead of silently escalating to broad `repo` access.
- v2.3.1 release validation uses mocked GitHub publication only. No Goal step may write to a real repository.
- AI advice requires exact request preview and a separate Send action. Quota tracking sends no local logs. Pricing refresh sends no user usage.
diff --git a/docs/superpowers/specs/2026-09-02-v2.3.1-local-data-contract.zh-CN.md b/docs/superpowers/specs/2026-09-02-v2.3.1-local-data-contract.zh-CN.md
index 88330826..f46a81b5 100644
--- a/docs/superpowers/specs/2026-09-02-v2.3.1-local-data-contract.zh-CN.md
+++ b/docs/superpowers/specs/2026-09-02-v2.3.1-local-data-contract.zh-CN.md
@@ -27,12 +27,12 @@
| P3 | 最新 Claude 额度 response **迁移输入** | globalState `ccu.usageLimits.` | 旧版 response 与时间戳 | v2.3.1 读取一次并规范化到 P2,成功后删除;完整 response 不再作为长期事实 | 不新增请求 |
| P4 | Claude 周额度历史 **迁移输入** | globalState `ccu.weeklyQuotaHistory.v1.` | 旧版规范化观测 | 导入 P2、去重后删除 | 无 |
| P5 | Codex 旧额度历史 **迁移输入** | P1 内 `quotaHistory` | provider、观测/重置时间、已用百分比 | 迁移为 schema 2,不虚构账号;重建 P1 不得抹掉已迁移 P2 历史 | 无 |
-| P6 | 插件设置与迁移标记 | globalState 中精确列入清单的 `ccu.setting.`、`ccu.lastSeenVersion`、`ccu.codex.backgroundWork.v1`、迁移 flag 与 `ccu.localData.pendingClientReset.v1`;语言和数据目录在 VS Code configuration | 类型化偏好、有界后台任务状态、schema/迁移标记,或一个不含数据值的待执行客户端动作枚举 | 偏好保留到重置/清除/卸载;待执行动作只保留到 Webview 验证确认或清除全部 | VS Code configuration 可能按用户的 VS Code 账号设置参与 Settings Sync;插件自身不主动上传 globalState |
+| P6 | 插件设置与迁移标记 | globalState 中精确列入清单的 `ccu.setting.`、`ccu.lastSeenVersion`、`ccu.codex.backgroundWork.v1`、迁移 flag 与 `ccu.localData.pendingClientReset.v1`;语言和数据目录在 VS Code configuration | 类型化偏好、有界后台任务状态、schema/迁移标记,或一个带单调 revision 且不含数据值的待执行客户端动作 | 偏好保留到重置/清除/卸载;待执行动作只保留到其精确 revision 获得 Webview 验证确认或清除全部 | VS Code configuration 可能按用户的 VS Code 账号设置参与 Settings Sync;插件自身不主动上传 globalState |
| P7 | 本机匿名化材料 | `ccu.codex.machineSalt` 与带版本的 fingerprint salt | 随机盐和算法版本 | 保留到清除全部派生数据;清除后旧 fingerprint 失效,不跨盐重连 | 永不外发 |
| P8 | AI 建议 BYOK 凭证 | VS Code SecretStorage 的 `claudeCodeUsage.secret.advice.apiKey` | secret 值本身 | 用户清除 key 或清除插件数据前保留;VS Code 官方说明 SecretStorage 会加密且不跨设备同步,具体保护机制仍取决于宿主实现 | 只有用户预览后再次点击 Send,才作为授权凭证发送给用户配置的 endpoint;不进入请求正文或预览 |
| P9 | 建议效果账本 | `ccu.adviceEffectiveness.localState` schema 3;可能仍有旧 feedback key | consent 模式、反馈、抑制记录、可比较任务指标和结果;无 prompt/path/account | 实现上限:反馈/抑制约 500,对与结果约 200;“清除建议数据”同时删除当前键和已知旧键 | 请求只有在预览后显式 Send 才外发 |
| P10 | Webview 偏好 | Webview state 与 localStorage:`ccu.activeTab`、`ccu.sessionFilter`、`ccu.sessionRange`、`ccu.sessionModel` | tab、排序/筛选/范围、展开状态、滚动和图表选择 | 直到重置 UI、清除 Webview 存储或卸载 | 无 |
-| P11 | 热力图/分享偏好 | `showHeatmap`、share-card 设置、Webview localStorage `ccu.combinedHeatmap.title`、`ccu.combinedHeatmap.range`、`ccu.combinedHeatmap.intensityMode`、`ccu.combinedHeatmap.privacyPreview`、`ccu.combinedHeatmap.palette`、`ccu.combinedHeatmap.customAccent`,以及 globalState `ccu.heatmapRepo`、`ccu.heatmapPath` | 有界标题(80 字符)、范围/色阶/配色枚举、隐私预览布尔值、经校验的六位十六进制主色,以及可选 GitHub `owner/repo/path` 目的地字符串 | 直到重置分享偏好、清除 Webview 存储或清除全部派生数据 | 本地 SVG/Markdown 导出不联网。v2.3.1 直发只支持公开仓库,每次均须精确确认目标,且绝不缓存 GitHub 凭证 |
+| P11 | 分享工作台偏好 | 当前 `enableShareCard`、退役兼容 `showHeatmap`、Webview localStorage `ccu.sharing.template`、`ccu.combinedHeatmap.title`、`ccu.combinedHeatmap.range`、`ccu.combinedHeatmap.intensityMode`、`ccu.combinedHeatmap.privacyPreview`、`ccu.combinedHeatmap.palette`、`ccu.combinedHeatmap.customAccent`,以及单个版本化 globalState 对象 `ccu.heatmapDestination.v1`;旧 `ccu.heatmapRepo`、`ccu.heatmapPath` 只作为只读/可清除迁移输入保留 | 呈现方式/范围/色阶/配色枚举、有界标题(80 字符)、隐私预览布尔值、经校验的六位十六进制主色,以及单个可选 GitHub `owner/repo/path` 目的地对象。显式命令 revision 只属于 provider 生命周期内的运行时协调,不是浏览器数据 | 偏好保留到重置分享偏好、清除 Webview 存储或清除全部派生数据 | 本地预览与 SVG/Markdown 导出不联网,也不请求 GitHub 身份。直发只支持公开仓库,每次均须精确确认目标与动作,且绝不缓存 GitHub 凭证。即使偏好持久化失败,成功的 PUT 仍会如实报告 |
| M1 | Claude/Codex 运行期视图 | Extension Host 内存 | records、prepared aggregate、图表、当前会话映射和有界内容样本 | reload/退出即释放 | 除非进入单独预览并确认的请求/导出,否则无 |
| M2 | 运行期凭证和网络 response | Extension Host 内存或 provider 自有凭证存储 | OAuth access/refresh token、临时 header、response/error object | 插件绝不长期保留;provider 刷新流程可能更新其自有凭证存储 | 仅 Anthropic OAuth/额度端点或用户配置的建议端点 |
@@ -124,7 +124,7 @@ Provider 日输入本身已经去重。如果分享边界仍收到同一 provide
- 确定性本地 SVG 与 Markdown 片段只能包含所选日期范围、标题、规范化日聚合、provider 标签、总计和明确限制说明。
- 导出/分享必须通过隐私 canary 测试,确保不存在账号 ID/fingerprint、项目/线程名、repo 名、本地路径、prompt/log 文本或凭证。
-- GitHub owner/repo/path 是目的地偏好,不是凭证;GitHub 登录只临时使用 VS Code authentication provider。v2.3.1 只请求 `public_repo`,验证仓库为公开、核实默认分支,写入前预览精确 owner/repo/branch/path 以及创建或覆盖动作,并仅在确认写入成功后保存目的地字符串。私有仓库使用本地 SVG + Markdown,不静默升级到宽泛 `repo` 权限。
+- GitHub owner/repo/path 是目的地偏好,不是凭证;本地分享预览/导出不执行 authentication 或身份/profile 查询。只有独立的发布动作会临时使用 VS Code authentication provider 中的 GitHub 登录,请求 `public_repo`,验证仓库为公开、核实默认分支,写入前预览精确 owner/repo/branch/path 以及创建或覆盖动作,并仅在确认写入成功后保存目的地字符串。私有仓库使用本地 SVG + Markdown,不静默升级到宽泛 `repo` 权限。
- v2.3.1 的发布验证只允许 mock GitHub publish;Goal 全程不得写入真实仓库。
- AI 建议必须先给出精确请求预览,再单独点击 Send;额度跟踪不发送本地日志;价格刷新不发送用户用量。
diff --git a/images/v2.4.0/claude-sharing-zh-CN-dark.png b/images/v2.4.0/claude-sharing-zh-CN-dark.png
new file mode 100644
index 00000000..4d16facd
Binary files /dev/null and b/images/v2.4.0/claude-sharing-zh-CN-dark.png differ
diff --git a/images/v2.4.0/codex-month-zh-CN-dark.png b/images/v2.4.0/codex-month-zh-CN-dark.png
new file mode 100644
index 00000000..62db2dcb
Binary files /dev/null and b/images/v2.4.0/codex-month-zh-CN-dark.png differ
diff --git a/images/v2.4.0/compare-sharing-en-light.png b/images/v2.4.0/compare-sharing-en-light.png
new file mode 100644
index 00000000..6a40e0d3
Binary files /dev/null and b/images/v2.4.0/compare-sharing-en-light.png differ
diff --git a/images/v2.4.0/narrow-sharing-de-DE-dark.png b/images/v2.4.0/narrow-sharing-de-DE-dark.png
new file mode 100644
index 00000000..78338db4
Binary files /dev/null and b/images/v2.4.0/narrow-sharing-de-DE-dark.png differ
diff --git a/package.json b/package.json
index 64cbcca6..20caedea 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "claude-code-usage",
"displayName": "Claude Code Usage",
- "description": "Local Claude Code and Codex usage insights in the VS Code status bar",
+ "description": "Local Claude Code and Codex token usage and quota insights in the VS Code status bar",
"version": "2.1.1",
"publisher": "GrowthJack",
"author": "GrowthJack",
@@ -26,7 +26,10 @@
"ai",
"anthropic",
"token",
- "status-bar"
+ "status-bar",
+ "codex-usage",
+ "token-usage",
+ "quota"
],
"engines": {
"vscode": "^1.74.0"
diff --git a/src/claudeIncrementalIndex.ts b/src/claudeIncrementalIndex.ts
index e4917755..86d8d62c 100644
--- a/src/claudeIncrementalIndex.ts
+++ b/src/claudeIncrementalIndex.ts
@@ -13,6 +13,7 @@ import {
AnalysisStructuralEvent,
analyzeLine,
ClaudeDataLoader,
+ compactUsageRecord,
finalizeAnalysis,
mergeAnalysisAcc,
newAnalysisAcc,
@@ -26,7 +27,7 @@ import {
rollingDayKeys,
} from './dateKeys';
import { I18n } from './i18n';
-import { isRetryDuplicatePrompt } from './promptDedup';
+import { detachedPromptPrefix, isRetryDuplicatePrompt } from './promptDedup';
import {
buildProjectUsageMatrixSnapshot,
ProjectUsageMatrixSnapshot,
@@ -272,6 +273,9 @@ export interface ClaudeUsageAggregateSnapshot {
allTime: UsageData;
dailyForLast30Days: { date: string; data: UsageData }[];
dailyForMonth: { date: string; data: UsageData }[];
+ /** All indexed local-calendar days. Kept host-side so month drill-downs can
+ * filter materialized aggregates instead of rescanning raw usage records. */
+ dailyForAllTime: { date: string; data: UsageData }[];
monthlyForAllTime: { date: string; data: UsageData }[];
hourlyForToday: { hour: string; data: UsageData }[];
/** Sparse, already-materialized hours for active days in the rolling
@@ -302,7 +306,7 @@ export interface ClaudeUsageDashboardSnapshot extends ClaudeUsageAggregateSnapsh
interface FilePlan {
kind: 'append' | 'rebuild';
- analysisReason?: 'source' | 'cutoff' | 'ownership' | 'full';
+ analysisReason?: 'source' | 'cutoff' | 'window' | 'ownership' | 'full';
entry: UsageFileFingerprint;
fileId: string;
orderTimestampMs?: number;
@@ -1400,7 +1404,7 @@ async function parsePlan(
const role = message?.role ?? parsed.type;
if (agentInfo && base.agentTask === undefined && role === 'user') {
const task = textFromUserContent(message?.content, ' ').replace(/\s+/g, ' ').trim();
- if (task) base.agentTask = task.slice(0, 200);
+ if (task) base.agentTask = detachedPromptPrefix(task, 200);
}
if (!isSubagentFile && role === 'user' && !parsed.isMeta && !parsed.isSidechain &&
@@ -1412,7 +1416,7 @@ async function parsePlan(
timestamp: parsed.timestamp,
message: { usage: { input_tokens: 0, output_tokens: 0 } },
_isUserPrompt: true,
- _promptText: text.trim().slice(0, 4000),
+ _promptText: detachedPromptPrefix(text.trim(), 4000),
_sessionId: sessionInfo.sessionId,
_projectDirEncoded: sessionInfo.projectPath,
};
@@ -1440,7 +1444,7 @@ async function parsePlan(
}
if (!validateUsageRecord(parsed)) return;
- const record = parsed as unknown as ClaudeUsageRecord;
+ const record = compactUsageRecord(parsed as unknown as ClaudeUsageRecord);
record._sessionId = sessionInfo.sessionId;
record._projectDirEncoded = sessionInfo.projectPath;
const cwd = parsed.cwd;
@@ -2276,6 +2280,9 @@ export function claudeUsageAggregateSnapshot(
.filter(([day]) => day.startsWith(configuredMonth))
.map(([date, data]) => ({ date, data: cloneUsageData(data) }))
.sort((left, right) => right.date.localeCompare(left.date));
+ const dailyForAllTime = [...index.aggregates.byLocalDay.entries()]
+ .map(([date, data]) => ({ date, data: cloneUsageData(data) }))
+ .sort((left, right) => right.date.localeCompare(left.date));
const monthlyForAllTime = [...index.aggregates.byMonth.entries()]
.map(([month, data]) => ({ date: `${month}-01`, data: cloneUsageData(data) }))
.sort((left, right) => right.date.localeCompare(left.date));
@@ -2288,6 +2295,7 @@ export function claudeUsageAggregateSnapshot(
allTime: cloneUsageData(index.aggregates.allTime),
dailyForLast30Days,
dailyForMonth,
+ dailyForAllTime,
monthlyForAllTime,
hourlyForToday,
hourlyForLast30DaysByDay,
@@ -2495,12 +2503,23 @@ export async function updateClaudeUsageIndex(
continue;
}
if (needsAnalysisBody) {
+ // A file that only grew still has to be re-read in full when the window
+ // moved past one of its events: the stored aggregate counts that event,
+ // and a tail read cannot subtract it. The body is still an append, so
+ // the file keeps the UUIDs it owned and gains only new ones — the same
+ // shape as a plain append, and 'window' records that. Calling it
+ // 'source', as a mid-file edit, is what made this refresh re-read the
+ // whole corpus on ordinary window drift.
+ const grewOnly = !bodyUnchanged &&
+ entry.size > sameIdentity.fingerprint.size &&
+ await appendPrefixStillMatches(sameIdentity, entry) &&
+ await appendBoundaryStillMatches(sameIdentity, entry);
analysisRebases.delete(fileId);
analysisPayloadRebases.delete(fileId);
plans.push({
kind: 'rebuild',
- analysisReason: bodyUnchanged && !timeZoneChanged && Boolean(sameIdentity.analysis)
- ? 'cutoff'
+ analysisReason: !timeZoneChanged && Boolean(sameIdentity.analysis)
+ ? (bodyUnchanged ? 'cutoff' : (grewOnly ? 'window' : 'source'))
: 'source',
entry,
fileId,
@@ -2518,9 +2537,14 @@ export async function updateClaudeUsageIndex(
const plannedKind: FilePlan['kind'] = append && !rebaseChangedPayload
? 'append'
: 'rebuild';
+ // An append whose file also lost an event to the window cannot be served
+ // from the stored aggregate — that aggregate still counts the expired
+ // event — so it has to be re-read in full. The body still only grew:
+ // 'window' records that, keeping it apart from a genuine mid-file edit.
+ const windowForcedRebuild = append && rebaseChangedPayload;
plans.push({
kind: plannedKind,
- analysisReason: 'source',
+ analysisReason: windowForcedRebuild ? 'window' : 'source',
entry,
fileId,
prior: append && rebasedAnalysis && !rebaseChangedPayload ? rebasedAnalysis : sameIdentity,
@@ -2588,14 +2612,43 @@ export async function updateClaudeUsageIndex(
let forcedFullAnalysisRebuild = false;
if (analyzeContent) {
const sourcePlans = plans.filter((plan) => plan.analysisReason === 'source');
- const soleSourcePlan = sourcePlans.length === 1 && plans.length === 1
- ? sourcePlans[0]
- : undefined;
+ // Several sessions appending between two refreshes is the ordinary case on a
+ // machine running more than one agent, not an edge case: with the fast path
+ // limited to a single changed file, such a machine never took it and every
+ // refresh re-read the whole history. Any number of pure tail appends is
+ // safe here; the per-file UUID ownership check below still guards order.
+ // A file that simply appeared is as safe as a tail append: its own body is
+ // read in full, and the established contributions are untouched. Every new
+ // session starts a new transcript, so treating this as an unsafe mutation
+ // cost a full rebuild many times a day.
+ const isTailAppend = (plan: FilePlan): boolean =>
+ plan.kind === 'append' && Boolean(plan.prior) && (plan.prior?.firstTimestampMs ?? 0) > 0;
+ const isNewFile = (plan: FilePlan): boolean =>
+ plan.kind === 'rebuild' && !plan.prior && !plan.replacedFileId;
+ // The window keeps drifting, so on a long history almost every refresh also
+ // carries a file whose oldest event has just fallen out of it. Such a file
+ // is re-read only to recompute its aggregate under the new cutoff — its
+ // body on disk is unchanged ('cutoff' is assigned only when bodyUnchanged),
+ // so it owns exactly the UUIDs it owned before and cannot preempt anyone.
+ // A verified append can cross that cutoff in the same file; it and other
+ // appends remain bounded to their changed bodies. UUID ownership preemption
+ // is checked after parsing, while dropped UUIDs are handed to the ownership
+ // restoration pass below.
+ //
+ // Requiring "appends and nothing else" therefore rejected the fast path on
+ // ordinary drift: measured on a 650-file history, 136 of 140 refreshes
+ // re-read every body — 1.4 GB, ~60 s — to serve one appended file.
+ // 'cutoff' — тело не менялось; 'window' — файл дописан и потерял событие
+ // за окном. Второй случай может нести новые UUID, поэтому его пропускает
+ // проверка владения после разбора, ниже.
+ const isWindowRebuild = (plan: FilePlan): boolean =>
+ plan.analysisReason === 'cutoff' || plan.analysisReason === 'window';
+ const appendOnlyPlans = plans.length > 0 &&
+ plans.every((plan) => isTailAppend(plan) || isNewFile(plan) || isWindowRebuild(plan));
const safeTailAppend = Boolean(
- soleSourcePlan?.kind === 'append' && soleSourcePlan.prior &&
+ appendOnlyPlans &&
previousAnalysisRuntime && !timeZoneChanged &&
- previous.windowDays === windowDays && analysisPayloadRebases.size === 0 &&
- soleSourcePlan.prior.firstTimestampMs > 0,
+ previous.windowDays === windowDays,
);
const priorAnalysisCutoffMs = previousAnalysisRuntime?.cutoffMs ??
(previous.analyzeContent ? previous.analysisCutoffMs : undefined);
@@ -2660,7 +2713,9 @@ export async function updateClaudeUsageIndex(
analyzeContent && previousAnalysisRuntime && previous.contentAnalysis &&
!forcedFullAnalysisRebuild && calibrationCutoffIsStable &&
analysisPayloadRebases.size === 0 && deletions.length === 0 && moves.length === 0 &&
- plans.every((plan) => plan.kind === 'append' && Boolean(plan.prior)),
+ plans.every((plan) =>
+ (plan.kind === 'append' && Boolean(plan.prior)) ||
+ (plan.kind === 'rebuild' && !plan.prior && !plan.replacedFileId)),
);
await options.beforeBodyReads?.();
@@ -2705,11 +2760,12 @@ export async function updateClaudeUsageIndex(
for (const uuid of file.analysis?.seenUuids ?? []) analysisSeenUuids.add(uuid);
}
}
- const rebuildPlans = plans.filter((plan) => plan.kind === 'rebuild');
- if (rebuildPlans.length > 0) {
+ // The first file to carry a UUID owns it, so parsing has to follow the
+ // same order the full loader uses. With one plan the order is moot, but
+ // several changed files must be parsed in full-scan order — otherwise
+ // an expiring-window rebuild can claim a UUID before an earlier append.
+ if (plans.length > 1) {
plans.sort((left, right) => {
- if (left.kind !== right.kind) return left.kind === 'rebuild' ? -1 : 1;
- if (left.kind === 'append') return 0;
return (left.orderTimestampMs ?? 0) - (right.orderTimestampMs ?? 0) ||
left.entry.discoveryIndex - right.entry.discoveryIndex;
});
@@ -2738,21 +2794,47 @@ export async function updateClaudeUsageIndex(
// collision changes the full loader's first owner, so retry the analysis as
// one globally ordered rebuild. The first tail read remains bounded and is
// the evidence used to choose the safe path.
- if (fastAppendAnalysis && previousAnalysisRuntime && parsedPlans.length === 1 &&
- parsedPlans[0].kind === 'append') {
- const appended = parsedPlans[0];
- const filePosition = previousAnalysisRuntime.filePositionById.get(appended.fileId) ?? -1;
- let laterOwnerCollision = filePosition < 0;
- for (const uuid of appended.analysisTouchedUuids) {
+ //
+ // A 'window' plan is an append too — the file only grew, it is re-read in
+ // full solely because its stored aggregate still counts an event that has
+ // since left the window. Its appended lines can carry UUIDs owned by a
+ // later file exactly like a plain append, so it is probed here as well;
+ // that probe is what allows it to skip the full rebuild in the first place.
+ const windowRebasedAppends = parsedPlans.some(
+ (plan) => plan.analysisReason === 'window',
+ );
+ if ((fastAppendAnalysis || windowRebasedAppends) &&
+ previousAnalysisRuntime && parsedPlans.length > 0) {
+ let laterOwnerCollision = false;
+ for (const appended of parsedPlans) {
if (laterOwnerCollision) break;
- const ownerFileId = previousAnalysisRuntime.firstUuidFileByUuid.get(uuid);
- if (!ownerFileId || ownerFileId === appended.fileId) continue;
- const ownerPosition = previousAnalysisRuntime.filePositionById.get(ownerFileId);
- if (ownerPosition === undefined) {
+ if (appended.kind === 'rebuild' && !appended.prior) {
+ // A brand-new file owns only UUIDs nobody else has: anything else can
+ // move ownership, which the full loader resolves by scan order.
+ for (const uuid of appended.analysisTouchedUuids) {
+ if (previousAnalysisRuntime.firstUuidFileByUuid.has(uuid)) {
+ laterOwnerCollision = true;
+ break;
+ }
+ }
+ continue;
+ }
+ const filePosition = previousAnalysisRuntime.filePositionById.get(appended.fileId) ?? -1;
+ if (filePosition < 0) {
laterOwnerCollision = true;
break;
}
- laterOwnerCollision = ownerPosition > filePosition;
+ for (const uuid of appended.analysisTouchedUuids) {
+ if (laterOwnerCollision) break;
+ const ownerFileId = previousAnalysisRuntime.firstUuidFileByUuid.get(uuid);
+ if (!ownerFileId || ownerFileId === appended.fileId) continue;
+ const ownerPosition = previousAnalysisRuntime.filePositionById.get(ownerFileId);
+ if (ownerPosition === undefined) {
+ laterOwnerCollision = true;
+ break;
+ }
+ laterOwnerCollision = ownerPosition > filePosition;
+ }
}
if (laterOwnerCollision) {
forcedFullAnalysisRebuild = true;
@@ -2890,18 +2972,43 @@ export async function updateClaudeUsageIndex(
// calibration identities, and the already-bounded prompt/skill samples.
const merged = cloneAnalysisAcc(previousAnalysisRuntime.merged);
const appended = new Map();
+ const addedFileIds: string[] = [];
for (const plan of parsedPlans) {
- if (!plan.prior?.analysis || !plan.contribution.analysis) continue;
- addAppendAnalysisDelta(merged, plan.prior.analysis, plan.contribution.analysis);
+ if (!plan.contribution.analysis) continue;
+ if (plan.prior?.analysis) {
+ addAppendAnalysisDelta(merged, plan.prior.analysis, plan.contribution.analysis);
+ } else {
+ // A file that just appeared has no prior contribution: its whole
+ // analysis is the delta.
+ addAppendAnalysisDelta(
+ merged,
+ newAnalysisAcc(analysisCutoffMs),
+ plan.contribution.analysis,
+ );
+ addedFileIds.push(plan.fileId);
+ }
appended.set(plan.fileId, plan.contribution.analysis);
}
- const promptTail = refreshedPromptTail(previousAnalysisRuntime, appended);
+ // Ordering state is reused as is for pure appends — recomputing it would
+ // touch every historical file. New files have to take their place in the
+ // order, and that is metadata-only work: no body is re-read.
+ const orderedState: AnalysisRuntimeState = addedFileIds.length === 0
+ ? previousAnalysisRuntime
+ : (() => {
+ const orderedFileIds = analysisFilesInOrder(next).map((file) => file.fileId);
+ return {
+ ...previousAnalysisRuntime,
+ orderedFileIds,
+ filePositionById: new Map(orderedFileIds.map((fileId, at) => [fileId, at])),
+ };
+ })();
+ const promptTail = refreshedPromptTail(orderedState, appended);
const touchedToolIds = new Set();
for (const plan of parsedPlans) {
for (const toolId of plan.analysisTouchedToolIds) touchedToolIds.add(toolId);
}
const structural = refreshedStructuralRuntime(
- previousAnalysisRuntime,
+ orderedState,
merged,
appended,
touchedToolIds,
@@ -2919,7 +3026,22 @@ export async function updateClaudeUsageIndex(
? new Map(previousAnalysisRuntime.firstUuidFileByUuid)
: previousAnalysisRuntime.firstUuidFileByUuid;
if (firstUuidFileByUuid instanceof Map) {
- for (const uuid of analysisUuidAdditions) {
+ // With more than one appended file, ownership belongs to the file that
+ // comes first in the established order — not to whichever plan happened
+ // to be parsed first.
+ const appendedByOrder = [...parsedPlans].sort((left, right) =>
+ (orderedState.filePositionById.get(left.fileId) ?? 0) -
+ (orderedState.filePositionById.get(right.fileId) ?? 0));
+ const unassigned = new Set(analysisUuidAdditions);
+ for (const plan of appendedByOrder) {
+ if (unassigned.size === 0) break;
+ for (const uuid of plan.analysisTouchedUuids) {
+ if (!unassigned.delete(uuid)) continue;
+ firstUuidFileByUuid.set(uuid, plan.fileId);
+ }
+ }
+ // A UUID no appended file claims still needs an owner, as before.
+ for (const uuid of unassigned) {
firstUuidFileByUuid.set(uuid, parsedPlans[0]?.fileId ?? '');
}
}
@@ -2935,8 +3057,8 @@ export async function updateClaudeUsageIndex(
const runtime: AnalysisRuntimeState = {
asOfDay: analysisAsOfDay,
cutoffMs: analysisCutoffMs,
- orderedFileIds: previousAnalysisRuntime.orderedFileIds,
- filePositionById: previousAnalysisRuntime.filePositionById,
+ orderedFileIds: orderedState.orderedFileIds,
+ filePositionById: orderedState.filePositionById,
merged,
promptTail,
skillHead,
diff --git a/src/codexViewComponents.ts b/src/codexViewComponents.ts
index d1d56238..39b0f8d5 100644
--- a/src/codexViewComponents.ts
+++ b/src/codexViewComponents.ts
@@ -31,7 +31,6 @@ export type CodexQualityFlag = (typeof CODEX_QUALITY_FLAGS)[number];
export interface CodexViewCopy {
title: string;
- beta: string;
refresh: string;
overview: string;
explore: string;
@@ -160,7 +159,6 @@ export interface CodexViewCopy {
export const CODEX_COPY_EN: CodexViewCopy = {
title: 'Codex usage',
- beta: 'Beta',
refresh: 'Refresh',
overview: 'Overview',
explore: 'Explore',
diff --git a/src/combinedHeatmapSvg.ts b/src/combinedHeatmapSvg.ts
index 6ccaf4f2..913ce9ea 100644
--- a/src/combinedHeatmapSvg.ts
+++ b/src/combinedHeatmapSvg.ts
@@ -5,6 +5,7 @@ import {
selectCombinedHeatmapWindow,
} from './combinedHeatmap';
import { renderHeatmapSvg } from './heatmapSvg';
+import { HEATMAP_ARTIFACT_TRANSLATIONS, artifactLocale } from './i18n';
export type CombinedHeatmapPalette =
| 'academicViolet'
@@ -112,6 +113,7 @@ export interface CombinedHeatmapSvgOptions {
palette?: CombinedHeatmapPalette | string;
customAccent?: string;
intensityMode?: CombinedHeatmapIntensityMode | string;
+ locale?: string;
labels?: {
combined: string;
processedTokens: string;
@@ -119,7 +121,13 @@ export interface CombinedHeatmapSvgOptions {
};
}
-function compactNumber(value: number): string {
+function compactNumber(value: number, locale = 'en'): string {
+ if (locale !== 'en') {
+ return new Intl.NumberFormat(locale, {
+ notation: 'compact',
+ maximumFractionDigits: 1,
+ }).format(value);
+ }
const absolute = Math.abs(value);
const format = (scaled: number): string => scaled.toFixed(1).replace(/\.0$/, '');
if (absolute >= 1_000_000_000) return `${format(value / 1_000_000_000)}B`;
@@ -137,21 +145,23 @@ export function renderCombinedHeatmapSvg(
daily: Readonly>,
options: CombinedHeatmapSvgOptions,
): string {
+ const locale = artifactLocale(options.locale);
+ const artifactCopy = HEATMAP_ARTIFACT_TRANSLATIONS[locale];
const window = selectCombinedHeatmapWindow(
daily,
options.range ?? 'year',
options.endDateISO,
);
- const title = options.title ?? 'Claude + Codex local activity';
+ const title = options.title ?? artifactCopy.combinedTitle;
const labels = options.labels ?? {
- combined: 'Combined',
- processedTokens: 'processed tokens',
- footerNote: 'Local activity volume · not productivity, billing, or provider equivalence',
+ combined: artifactCopy.combined,
+ processedTokens: artifactCopy.processedTokens,
+ footerNote: artifactCopy.combinedFooter,
};
const subtitle = [
- `Claude ${compactNumber(window.totals.claudeProcessed)}`,
- `Codex ${compactNumber(window.totals.codexProcessed)}`,
- `${labels.combined} ${compactNumber(window.totals.combinedProcessed)} ${labels.processedTokens}`,
+ `Claude ${compactNumber(window.totals.claudeProcessed, locale)}`,
+ `Codex ${compactNumber(window.totals.codexProcessed, locale)}`,
+ `${labels.combined} ${compactNumber(window.totals.combinedProcessed, locale)} ${labels.processedTokens}`,
].join(' · ');
const palette = normalizeCombinedHeatmapPalette(options.palette);
const scale = palette === 'custom'
@@ -164,7 +174,8 @@ export function renderCombinedHeatmapSvg(
title,
subtitle,
footerNote: labels.footerNote,
- watermark: options.watermark ?? 'Made with Claude Code Usage',
+ watermark: options.watermark ?? artifactCopy.madeWith,
+ locale,
scale,
intensityMode: normalizeCombinedHeatmapIntensityMode(options.intensityMode),
background: '#fcfaff',
@@ -180,9 +191,9 @@ export function renderCombinedHeatmapSvg(
codexProcessed: 0,
combinedProcessed: 0,
};
- return `${dateISO} · Claude: ${compactNumber(usage.claudeProcessed)} · ` +
- `Codex: ${compactNumber(usage.codexProcessed)} · ` +
- `${labels.combined}: ${compactNumber(usage.combinedProcessed)} ${labels.processedTokens}`;
+ return `${dateISO} · Claude: ${compactNumber(usage.claudeProcessed, locale)} · ` +
+ `Codex: ${compactNumber(usage.codexProcessed, locale)} · ` +
+ `${labels.combined}: ${compactNumber(usage.combinedProcessed, locale)} ${labels.processedTokens}`;
},
});
}
diff --git a/src/dashboardRefreshClient.ts b/src/dashboardRefreshClient.ts
index 76f8118e..8574081e 100644
--- a/src/dashboardRefreshClient.ts
+++ b/src/dashboardRefreshClient.ts
@@ -506,5 +506,41 @@ function ccuApplyDashboardDataPatch(message) {
return false;
}
}
+
+// A full provider-panel swap during wheel/trackpad motion forces layout on the
+// scrolling frame. Keep the newest patch in memory until the burst quiets;
+// the cap ensures a long continuous gesture cannot starve live data forever.
+var __ccuLastScrollEventAt = -Infinity;
+var __ccuScrollPatchPending = null;
+var __ccuScrollPatchQuietTimer = 0;
+var __ccuScrollPatchMaxTimer = 0;
+function ccuFlushScrollPatch() {
+ if (__ccuScrollPatchQuietTimer) { clearTimeout(__ccuScrollPatchQuietTimer); }
+ if (__ccuScrollPatchMaxTimer) { clearTimeout(__ccuScrollPatchMaxTimer); }
+ __ccuScrollPatchQuietTimer = 0;
+ __ccuScrollPatchMaxTimer = 0;
+ var latest = __ccuScrollPatchPending;
+ __ccuScrollPatchPending = null;
+ if (latest) { ccuApplyDashboardDataPatch(latest); }
+}
+function ccuScheduleScrollPatchQuiet() {
+ if (__ccuScrollPatchQuietTimer) { clearTimeout(__ccuScrollPatchQuietTimer); }
+ __ccuScrollPatchQuietTimer = setTimeout(ccuFlushScrollPatch, 120);
+}
+window.addEventListener('scroll', function() {
+ __ccuLastScrollEventAt = performance.now();
+ if (__ccuScrollPatchPending) { ccuScheduleScrollPatchQuiet(); }
+}, { passive: true });
+function ccuQueueDashboardDataPatch(message) {
+ if (!__ccuScrollPatchPending && performance.now() - __ccuLastScrollEventAt >= 120) {
+ ccuApplyDashboardDataPatch(message);
+ return;
+ }
+ __ccuScrollPatchPending = message;
+ ccuScheduleScrollPatchQuiet();
+ if (!__ccuScrollPatchMaxTimer) {
+ __ccuScrollPatchMaxTimer = setTimeout(ccuFlushScrollPatch, 500);
+ }
+}
`;
}
diff --git a/src/dataLoader.ts b/src/dataLoader.ts
index 04ae4c20..0a675d03 100644
--- a/src/dataLoader.ts
+++ b/src/dataLoader.ts
@@ -5,7 +5,7 @@ import * as path from 'path';
// Removed tinyglobby dependency - using native fs instead
// Removed zod dependency - using native validation instead
import { calculateCostBreakdown, getModelRatesPerMillion } from './pricing';
-import { isRetryDuplicatePrompt } from './promptDedup';
+import { detachedPromptPrefix, isRetryDuplicatePrompt } from './promptDedup';
import {
dayKeyInZone,
formatHourLabel,
@@ -82,6 +82,41 @@ export function validateUsageRecord(data: any): data is ClaudeUsageRecord {
return true;
}
+/** Keep only usage evidence after content analysis has inspected the raw line.
+ * A usage-bearing assistant row may contain a very large response/tool body;
+ * retaining the parsed row would pin that body in every dashboard snapshot. */
+export function compactUsageRecord(data: ClaudeUsageRecord): ClaudeUsageRecord {
+ const sourceUsage = data.message.usage;
+ const usage: ClaudeUsageRecord['message']['usage'] = {
+ input_tokens: sourceUsage.input_tokens,
+ output_tokens: sourceUsage.output_tokens,
+ ...(sourceUsage.cache_creation_input_tokens !== undefined
+ ? { cache_creation_input_tokens: sourceUsage.cache_creation_input_tokens } : {}),
+ ...(sourceUsage.cache_read_input_tokens !== undefined
+ ? { cache_read_input_tokens: sourceUsage.cache_read_input_tokens } : {}),
+ ...(sourceUsage.cache_creation !== undefined ? {
+ cache_creation: {
+ ...(sourceUsage.cache_creation?.ephemeral_1h_input_tokens !== undefined
+ ? { ephemeral_1h_input_tokens: sourceUsage.cache_creation.ephemeral_1h_input_tokens } : {}),
+ ...(sourceUsage.cache_creation?.ephemeral_5m_input_tokens !== undefined
+ ? { ephemeral_5m_input_tokens: sourceUsage.cache_creation.ephemeral_5m_input_tokens } : {}),
+ },
+ } : {}),
+ };
+ return {
+ timestamp: data.timestamp,
+ ...(data.version !== undefined ? { version: data.version } : {}),
+ message: {
+ usage,
+ ...(data.message.model !== undefined ? { model: data.message.model } : {}),
+ ...(data.message.id !== undefined ? { id: data.message.id } : {}),
+ },
+ ...(data.costUSD !== undefined ? { costUSD: data.costUSD } : {}),
+ ...(data.requestId !== undefined ? { requestId: data.requestId } : {}),
+ ...(data.isApiErrorMessage !== undefined ? { isApiErrorMessage: data.isApiErrorMessage } : {}),
+ };
+}
+
function validationDropReason(data: any): string {
if (!data || typeof data !== 'object') return 'not-an-object';
if (typeof data.timestamp !== 'string') return 'timestamp-missing-or-non-string';
@@ -216,7 +251,7 @@ function collectPrompt(
// Web Component examples and must remain eligible for explicit opt-in.
acc.prompts.push({
cwd,
- text: trimmed.slice(0, 2500),
+ text: detachedPromptPrefix(trimmed, 2500),
observedAtEpochMs: Number.isFinite(observedAtEpochMs) ? observedAtEpochMs : 0,
});
if (acc.prompts.length > 600) {
@@ -234,7 +269,7 @@ function collectCommandUse(acc: AnalysisAcc, text: string, sessionId: string, ti
if (!m) {
return;
}
- const name = m[1].trim();
+ const name = detachedPromptPrefix(m[1].trim(), 80);
if (TRIVIAL_COMMANDS.has(name.toLowerCase())) {
return;
}
@@ -842,7 +877,7 @@ export class ClaudeDataLoader {
}
}
- // Per-session (per-file) map of prompt text → last-counted epoch ms,
+ // Per-session (per-file) map of prompt digest → last-counted epoch ms,
// to drop API-error retry re-logs of the same prompt from the Messages
// count (see promptDedup.ts).
const recentPrompts = new Map();
@@ -891,7 +926,7 @@ export class ClaudeDataLoader {
: '';
const trimmed = text.replace(/\s+/g, ' ').trim();
if (trimmed.length > 0) {
- agentInfo.task = trimmed.slice(0, 200);
+ agentInfo.task = detachedPromptPrefix(trimmed, 200);
}
}
}
@@ -930,7 +965,7 @@ export class ClaudeDataLoader {
// Kept generous so the "costliest messages" panel can show
// the whole triggering prompt (scrollable); giant pastes are
// capped so a few huge prompts can't bloat memory.
- _promptText: text.trim().slice(0, 4000),
+ _promptText: detachedPromptPrefix(text.trim(), 4000),
_sessionId: sessionInfo.sessionId,
_projectDirEncoded: sessionInfo.projectPath,
};
@@ -961,7 +996,7 @@ export class ClaudeDataLoader {
// Tag the record with the session/project it came from.
// Prefer the real working directory (`cwd`) recorded in the log line
// over the lossy, dash-encoded folder name when it is available.
- const record = data as ClaudeUsageRecord;
+ const record = compactUsageRecord(data as ClaudeUsageRecord);
record._sessionId = sessionInfo.sessionId;
record._projectDirEncoded = sessionInfo.projectPath;
const cwd = (parsed as { cwd?: unknown }).cwd;
diff --git a/src/dateKeys.ts b/src/dateKeys.ts
index f4d01a0f..6eae1669 100644
--- a/src/dateKeys.ts
+++ b/src/dateKeys.ts
@@ -7,21 +7,64 @@
// inside the new month's view. Deriving both the day and the month key from the
// same zone here removes that split.
+// Constructing an Intl.DateTimeFormat is expensive, and these helpers run once
+// per ingested record: a single index pass over a large local history calls them
+// millions of times. Both the zone resolution and the formatters are therefore
+// memoised per zone. Every distinct key is a configured zone, so the maps stay
+// at a handful of entries for the life of the process.
+const resolvedZones = new Map();
+const dayFormatters = new Map();
+const hourFormatters = new Map();
+
/** Returns a usable IANA zone, falling back to the system zone when necessary. */
export function resolveTimeZone(timeZone: string): string {
+ const cached = resolvedZones.get(timeZone);
+ if (cached !== undefined) {
+ return cached;
+ }
+ let resolved: string;
try {
- return new Intl.DateTimeFormat('en-CA', timeZone ? { timeZone } : undefined)
+ resolved = new Intl.DateTimeFormat('en-CA', timeZone ? { timeZone } : undefined)
.resolvedOptions()
.timeZone || 'UTC';
} catch {
- return new Intl.DateTimeFormat('en-CA').resolvedOptions().timeZone || 'UTC';
+ resolved = new Intl.DateTimeFormat('en-CA').resolvedOptions().timeZone || 'UTC';
}
+ resolvedZones.set(timeZone, resolved);
+ return resolved;
+}
+
+function dayFormatter(zone: string): Intl.DateTimeFormat {
+ const cached = dayFormatters.get(zone);
+ if (cached) {
+ return cached;
+ }
+ const formatter = new Intl.DateTimeFormat('en-CA', {
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit',
+ timeZone: zone,
+ });
+ dayFormatters.set(zone, formatter);
+ return formatter;
+}
+
+function hourFormatter(zone: string): Intl.DateTimeFormat {
+ const cached = hourFormatters.get(zone);
+ if (cached) {
+ return cached;
+ }
+ const formatter = new Intl.DateTimeFormat('en-GB', {
+ hour: '2-digit',
+ hourCycle: 'h23',
+ timeZone: zone,
+ });
+ hourFormatters.set(zone, formatter);
+ return formatter;
}
function partsInZone(date: Date, timeZone: string): { y: string; m: string; d: string } {
- const base: Intl.DateTimeFormatOptions = { year: 'numeric', month: '2-digit', day: '2-digit' };
- const fmt = new Intl.DateTimeFormat('en-CA', { ...base, timeZone: resolveTimeZone(timeZone) });
- const parts = fmt.formatToParts(date);
+ const parts = dayFormatter(resolveTimeZone(timeZone)).formatToParts(date);
const get = (t: string): string => parts.find((p) => p.type === t)?.value ?? '';
return { y: get('year'), m: get('month'), d: get('day') };
}
@@ -46,13 +89,8 @@ export function hourKeyInZone(date: Date, timeZone: string): string {
if (isNaN(date.getTime())) {
return '';
}
- const formatter = new Intl.DateTimeFormat('en-GB', {
- hour: '2-digit',
- hourCycle: 'h23',
- timeZone: resolveTimeZone(timeZone),
- });
- return formatter.formatToParts(date).find((part) => part.type === 'hour')
- ?.value ?? '';
+ return hourFormatter(resolveTimeZone(timeZone)).formatToParts(date)
+ .find((part) => part.type === 'hour')?.value ?? '';
}
/** Provider-neutral display label for a validated "00" through "23" hour key. */
diff --git a/src/extension.ts b/src/extension.ts
index 0d526949..fffa33a2 100644
--- a/src/extension.ts
+++ b/src/extension.ts
@@ -4,8 +4,6 @@ import * as path from 'path';
import * as os from 'os';
import { createHash, randomBytes } from 'crypto';
import { renderHeatmapSvg } from './heatmapSvg';
-import { DEFAULT_SECTIONS, ShareRange, buildShareCardData, shareCardFilename } from './shareCard';
-import { renderShareCardSvg } from './shareCardSvg';
import * as vscode from 'vscode';
import { ClaudeDataLoader } from './dataLoader';
import {
@@ -19,9 +17,12 @@ import { UsageWebviewProvider } from './webview';
import { I18n } from './i18n';
import { dayKeyInZone, resolveTimeZone } from './dateKeys';
import {
+ GITHUB_HEATMAP_DESTINATION_KEY,
GITHUB_PUBLIC_REPO_SCOPE,
+ completeSuccessfulGitHubPublish,
createGitHubPublishPlan,
githubPublishConfirmationDetail,
+ parseGitHubHeatmapDestination,
probePublicGitHubPublishTarget,
publishPublicGitHubFile,
} from './githubHeatmapPublish';
@@ -144,6 +145,7 @@ import {
LocalDataAction,
LocalDataActionResult,
LocalDataClientAction,
+ LocalDataClientResetTombstone,
LocalDataClientSummary,
LocalDataInventory,
LocalDataInventoryRow,
@@ -516,6 +518,7 @@ export class ClaudeCodeUsageExtension {
private initializationWriteFailure: unknown = null;
private localDataActionWrite: Promise = Promise.resolve();
private pendingClientResetReplay: Promise = Promise.resolve();
+ private pendingClientResetRevision = 0;
private clearingAllLocalData = false;
private localDataClearedRequiresReload = false;
private readonly localDataQuotaScopes = new Map<
@@ -694,8 +697,8 @@ export class ClaudeCodeUsageExtension {
this.buildLocalDataInventory(client);
this.webviewProvider.onRunLocalDataAction = (action, quotaScopeToken) =>
this.runLocalDataActionInteractive(action, quotaScopeToken);
- this.webviewProvider.onResetSharingPreferences = () =>
- this.resetSharingPreferencesHost();
+ this.webviewProvider.onExportClaudeHeatmap = () => this.exportHeatmap();
+ this.webviewProvider.onPublishClaudeHeatmap = () => this.publishHeatmapToGitHub();
this.webviewProvider.onLocalDataClientReady = () => {
void this.replayPendingClientReset();
};
@@ -820,13 +823,13 @@ export class ClaudeCodeUsageExtension {
this.outputChannel.show();
}),
vscode.commands.registerCommand('claudeCodeUsage.exportHeatmap', () => {
- this.exportHeatmap();
+ this.webviewProvider.showSharingWorkspace('claudeHeatmap');
}),
vscode.commands.registerCommand('claudeCodeUsage.publishHeatmapToGitHub', () => {
- this.publishHeatmapToGitHub();
+ this.webviewProvider.showSharingWorkspace('claudeHeatmap');
}),
vscode.commands.registerCommand('claudeCodeUsage.exportShareCard', () => {
- this.exportShareCard();
+ this.webviewProvider.showSharingWorkspace('claudeShareCard');
}),
vscode.commands.registerCommand('claudeCodeUsage.previewWhatsNew', () => {
this.previewWhatsNew();
@@ -1088,6 +1091,7 @@ export class ClaudeCodeUsageExtension {
key === QUOTA_FINGERPRINT_SALT_KEY || key === 'ccu.codex.machineSalt',
);
const sharingKeys = stateKeys.filter((key) =>
+ key === GITHUB_HEATMAP_DESTINATION_KEY ||
key === 'ccu.heatmapRepo' ||
key === 'ccu.heatmapPath' ||
sharingSettingKeys.has(key),
@@ -1326,35 +1330,77 @@ export class ClaudeCodeUsageExtension {
private async resetSharingPreferencesHost(): Promise {
await this.settings.resetSharingOwnedData();
+ await this.context.globalState.update(GITHUB_HEATMAP_DESTINATION_KEY, undefined);
await this.context.globalState.update('ccu.heatmapRepo', undefined);
await this.context.globalState.update('ccu.heatmapPath', undefined);
this.webviewProvider.clearSharingRuntimeState();
}
- private pendingClientReset(): LocalDataClientAction | undefined {
+ private pendingClientReset(): LocalDataClientResetTombstone | undefined {
const value = this.context.globalState.get(
LOCAL_DATA_PENDING_CLIENT_RESET_KEY,
);
- return value === 'reset-ui-state' ||
+ if (value === 'reset-ui-state' ||
value === 'reset-sharing-preferences' ||
- value === 'clear-all-client-state'
- ? value
- : undefined;
+ value === 'clear-all-client-state') {
+ return { schemaVersion: 1, revision: 0, action: value };
+ }
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined;
+ const candidate = value as Partial;
+ if (
+ candidate.schemaVersion !== 1 ||
+ !Number.isSafeInteger(candidate.revision) ||
+ Number(candidate.revision) < 1 ||
+ (candidate.action !== 'reset-ui-state' &&
+ candidate.action !== 'reset-sharing-preferences' &&
+ candidate.action !== 'clear-all-client-state')
+ ) {
+ return undefined;
+ }
+ this.pendingClientResetRevision = Math.max(
+ this.pendingClientResetRevision ?? 0,
+ Number(candidate.revision),
+ );
+ return candidate as LocalDataClientResetTombstone;
+ }
+
+ private nextPendingClientReset(action: LocalDataClientAction): LocalDataClientResetTombstone {
+ const current = this.pendingClientReset();
+ const revision = Math.max(
+ this.pendingClientResetRevision ?? 0,
+ current?.revision ?? 0,
+ ) + 1;
+ this.pendingClientResetRevision = revision;
+ return { schemaVersion: 1, revision, action };
+ }
+
+ private async clearPendingClientReset(
+ acknowledged: LocalDataClientResetTombstone,
+ ): Promise {
+ const current = this.pendingClientReset();
+ if (
+ !current ||
+ current.revision !== acknowledged.revision ||
+ current.action !== acknowledged.action
+ ) {
+ return;
+ }
+ await this.context.globalState.update(
+ LOCAL_DATA_PENDING_CLIENT_RESET_KEY,
+ undefined,
+ );
}
private replayPendingClientReset(): Promise {
const run = this.pendingClientResetReplay
.catch(() => undefined)
- .then(async () => {
+ .then(() => this.serializeLocalDataAction(async () => {
const pending = this.pendingClientReset();
if (!pending) return;
- if (await this.webviewProvider.requestClientLocalDataAction(pending)) {
- await this.context.globalState.update(
- LOCAL_DATA_PENDING_CLIENT_RESET_KEY,
- undefined,
- );
+ if (await this.webviewProvider.requestClientLocalDataAction(pending.action)) {
+ await this.clearPendingClientReset(pending);
}
- });
+ }));
this.pendingClientResetReplay = run;
return run;
}
@@ -1489,11 +1535,12 @@ export class ClaudeCodeUsageExtension {
}
}
if (result.clientAction) {
+ const tombstone = this.nextPendingClientReset(result.clientAction);
let tombstoneStored = false;
try {
await this.context.globalState.update(
LOCAL_DATA_PENDING_CLIENT_RESET_KEY,
- result.clientAction,
+ tombstone,
);
tombstoneStored = true;
} catch {
@@ -1505,10 +1552,7 @@ export class ClaudeCodeUsageExtension {
);
if (clientResetOk && tombstoneStored) {
try {
- await this.context.globalState.update(
- LOCAL_DATA_PENDING_CLIENT_RESET_KEY,
- undefined,
- );
+ await this.clearPendingClientReset(tombstone);
} catch {
// A stale tombstone only replays the same exact idempotent reset.
}
@@ -1719,6 +1763,7 @@ export class ClaudeCodeUsageExtension {
try {
const result = await fetchLatestPricing();
this.invalidateClaudeUsagePricingCache();
+ this.webviewProvider.invalidateShareCardPreview();
vscode.window.showInformationMessage(`${I18n.t.popup.pricingUpdated} (${result.updated})`);
// Force a full recompute so the new prices take effect.
void this.refreshData(true, 'pricing');
@@ -1741,6 +1786,7 @@ export class ClaudeCodeUsageExtension {
const daily = ClaudeDataLoader.getDailyUsageMap(records, timeZone);
const svg = renderHeatmapSvg(daily, {
endDateISO: dayKeyInZone(new Date(), timeZone),
+ locale: I18n.getLocale(),
});
const uri = await vscode.window.showSaveDialog({
defaultUri: vscode.Uri.file(path.join(os.homedir(), 'claude-code-heatmap.svg')),
@@ -1845,10 +1891,15 @@ export class ClaudeCodeUsageExtension {
}
const token = session.accessToken;
const login = session.account.label.split(/\s/)[0];
+ const storedDestination = parseGitHubHeatmapDestination(
+ this.context.globalState.get(GITHUB_HEATMAP_DESTINATION_KEY),
+ );
const repo = await vscode.window.showInputBox({
prompt: 'Public target repository (owner/name). Private repositories use local SVG export.',
- value: this.context.globalState.get('ccu.heatmapRepo') || `${login}/${login}`,
+ value: storedDestination?.repository ||
+ this.context.globalState.get('ccu.heatmapRepo') ||
+ `${login}/${login}`,
validateInput: (value) => {
try {
createGitHubPublishPlan(value, 'heatmap.svg');
@@ -1864,7 +1915,9 @@ export class ClaudeCodeUsageExtension {
const filePath =
(await vscode.window.showInputBox({
prompt: 'File path in the repo',
- value: this.context.globalState.get('ccu.heatmapPath') || 'claude-code-heatmap.svg',
+ value: storedDestination?.filePath ||
+ this.context.globalState.get('ccu.heatmapPath') ||
+ 'claude-code-heatmap.svg',
})) || '';
if (!filePath) {
return;
@@ -1879,7 +1932,7 @@ export class ClaudeCodeUsageExtension {
const timeZone = I18n.getTimezone();
const svg = renderHeatmapSvg(
ClaudeDataLoader.getDailyUsageMap(records, timeZone),
- { endDateISO: dayKeyInZone(new Date(), timeZone) },
+ { endDateISO: dayKeyInZone(new Date(), timeZone), locale: I18n.getLocale() },
);
const contentB64 = Buffer.from(svg, 'utf8').toString('base64');
const request = (
@@ -1921,70 +1974,26 @@ export class ClaudeCodeUsageExtension {
return;
}
- // Destination strings are convenience preferences, not credentials. Save
- // them only after the exact, confirmed write succeeds.
- await this.context.globalState.update('ccu.heatmapRepo', `${preview.owner}/${preview.repository}`);
- await this.context.globalState.update('ccu.heatmapPath', preview.filePath);
-
const view = 'View on GitHub';
- const pick = await vscode.window.showInformationMessage(
- `Heatmap published to ${preview.owner}/${preview.repository}.`,
- view
- );
- if (pick === view) {
- void vscode.env.openExternal(vscode.Uri.parse(preview.browserUrl));
- }
- }
-
- /** Export a one-page usage share card as a self-contained SVG. Only aggregate,
- * non-identifying metrics are drawn (privacy is enforced by ShareCardData's
- * shape). The user picks the range; the card opens for review after saving. */
- private async exportShareCard(): Promise {
- const records = this.cache.records;
- if (!records || records.length === 0) {
- vscode.window.showWarningMessage(I18n.t.popup.noDataMessage);
- return;
- }
- const ranges: (vscode.QuickPickItem & { range: ShareRange })[] = [
- { label: 'This month', range: 'month' },
- { label: 'Last 7 days', range: 'week' },
- { label: 'Today', range: 'today' },
- ];
- const picked = await vscode.window.showQuickPick(ranges, {
- placeHolder: 'Share card range',
- });
- if (!picked) {
- return;
- }
- const input = ClaudeDataLoader.buildShareInput(records, picked.range);
- const data = buildShareCardData(input, DEFAULT_SECTIONS);
- const kind = vscode.window.activeColorTheme?.kind;
- const isDark = kind === vscode.ColorThemeKind.Dark || kind === vscode.ColorThemeKind.HighContrast;
- const svg = renderShareCardSvg(data, {
- theme: 'claudeClassic',
- isDark,
- lang: I18n.getLocale(),
- formatCurrency: (amountUsd) => I18n.formatCurrency(amountUsd),
- });
- const defaultName = shareCardFilename(picked.range).replace(/\.png$/, '.svg');
- const uri = await vscode.window.showSaveDialog({
- defaultUri: vscode.Uri.file(path.join(os.homedir(), defaultName)),
- filters: { 'SVG image': ['svg'] },
- saveLabel: 'Export share card',
+ const completion = await completeSuccessfulGitHubPublish(preview, {
+ persistDestination: async (key, destination) => {
+ await this.context.globalState.update(key, destination);
+ },
+ showSuccess: async () => {
+ const pick = await vscode.window.showInformationMessage(
+ `Heatmap published to ${preview.owner}/${preview.repository} on ${preview.branch} at ${preview.filePath}.`,
+ view,
+ );
+ return pick === view;
+ },
+ showPersistenceWarning: async () => {
+ await vscode.window.showWarningMessage(
+ I18n.sharingWorkspace.publishPreferenceSaveWarning,
+ );
+ },
});
- if (!uri) {
- return;
- }
- try {
- await vscode.workspace.fs.writeFile(uri, Buffer.from(svg, 'utf8'));
- } catch (e) {
- vscode.window.showErrorMessage(`Share card export failed: ${(e as Error).message}`);
- return;
- }
- const open = 'Open';
- const pick = await vscode.window.showInformationMessage('Usage share card exported.', open);
- if (pick === open) {
- await vscode.commands.executeCommand('vscode.open', uri);
+ if (completion.openBrowser) {
+ void vscode.env.openExternal(vscode.Uri.parse(preview.browserUrl));
}
}
@@ -2092,7 +2101,7 @@ export class ClaudeCodeUsageExtension {
this.activePricingBackend = config.pricingBackend;
setPricingBackend(config.pricingBackend);
this.applyFormattingConfiguration(config);
- this.statusBar.setVisibility(config.showCost, config.showContext, config.usageLimitTracking, config.statusBarMetric, config.showScopedWeekly, config.quotaFiveHourOnly, config.showResetInStatusBar, config.resetCountdownFormat);
+ this.statusBar.setVisibility(config.showCost, config.showContext, config.usageLimitTracking, config.statusBarMetric, config.showScopedWeekly, config.quotaFiveHourOnly, config.showResetInStatusBar, config.resetCountdownFormat, config.statusBarQuotaFormat);
// Listen for configuration changes
vscode.workspace.onDidChangeConfiguration(e => {
@@ -2115,7 +2124,7 @@ export class ClaudeCodeUsageExtension {
}
private applyFormattingConfiguration(config: ExtensionConfig): void {
- I18n.setLanguage(config.language as any);
+ I18n.setLanguage(config.language as any, vscode.env.language);
I18n.setDecimalPlaces(config.decimalPlaces);
I18n.setCurrencyDisplay(config.displayCurrency);
I18n.setTokenDecimalPlaces(config.tokenDecimalPlaces);
@@ -2156,6 +2165,7 @@ export class ClaudeCodeUsageExtension {
showResetInStatusBar: s.get('showResetInStatusBar'),
quotaFiveHourOnly: s.get('quotaFiveHourOnly'),
resetCountdownFormat: s.get<'decimal' | 'units' | 'clock'>('resetCountdownFormat'),
+ statusBarQuotaFormat: s.get('statusBarQuotaFormat'),
usageLimitTracking: s.get('usageLimitTracking'),
adviceApiKey: s.get('advice.apiKey'),
adviceApiUrl: s.get('advice.apiUrl'),
@@ -3178,7 +3188,7 @@ export class ClaudeCodeUsageExtension {
// next tick.
'showCost', 'showContext', 'statusBarMetric',
'showScopedWeekly', 'quotaFiveHourOnly', 'showResetInStatusBar', 'resetCountdownFormat',
- 'statusBarProvider', 'codex.statusMetric',
+ 'statusBarQuotaFormat', 'statusBarProvider', 'codex.statusMetric',
]);
// Presentation-only dashboard toggles must not restart watchers, recreate
@@ -3210,7 +3220,7 @@ export class ClaudeCodeUsageExtension {
}
if (key && ClaudeCodeUsageExtension.STATUS_BAR_ONLY_SETTINGS.has(key)) {
const config = this.getConfiguration();
- this.statusBar.setVisibility(config.showCost, config.showContext, config.usageLimitTracking, config.statusBarMetric, config.showScopedWeekly, config.quotaFiveHourOnly, config.showResetInStatusBar, config.resetCountdownFormat);
+ this.statusBar.setVisibility(config.showCost, config.showContext, config.usageLimitTracking, config.statusBarMetric, config.showScopedWeekly, config.quotaFiveHourOnly, config.showResetInStatusBar, config.resetCountdownFormat, config.statusBarQuotaFormat);
this.statusBar.updateQuota(this.cache.usageLimits ?? null);
this.syncProviderUi();
return;
@@ -3276,9 +3286,10 @@ export class ClaudeCodeUsageExtension {
setPricingBackend(config.pricingBackend);
if (pricingBackendChanged) {
this.invalidateClaudeUsagePricingCache();
+ this.webviewProvider.invalidateShareCardPreview();
}
this.applyFormattingConfiguration(config);
- this.statusBar.setVisibility(config.showCost, config.showContext, config.usageLimitTracking, config.statusBarMetric, config.showScopedWeekly, config.quotaFiveHourOnly, config.showResetInStatusBar, config.resetCountdownFormat);
+ this.statusBar.setVisibility(config.showCost, config.showContext, config.usageLimitTracking, config.statusBarMetric, config.showScopedWeekly, config.quotaFiveHourOnly, config.showResetInStatusBar, config.resetCountdownFormat, config.statusBarQuotaFormat);
// Restart auto-refresh with new interval
this.startAutoRefresh();
@@ -4421,6 +4432,7 @@ export class ClaudeCodeUsageExtension {
materialized.costliestMessages,
materialized.hourlyForLast30DaysByDay,
materialized.projectUsageMatrix,
+ materialized.dailyForAllTime,
);
}
this.cache.lastUpdate = new Date(snapshotNow.getTime());
@@ -4532,7 +4544,7 @@ export class ClaudeCodeUsageExtension {
this.statusBar.updateUsageData(todayData, workspaceTodayData, undefined, undefined, calendarMonthData);
this.statusBar.updateContext(materialized.context);
if (updateWebview) {
- this.webviewProvider.updateData(sessionData, todayData, rolling30Data, allTimeData, dailyDataForRolling30, dailyDataForAllTime, hourlyDataForToday, undefined, dataDirectory, records, sessionBreakdown, projectBreakdown, contentAnalysis, branchBreakdown, workflowBreakdown, costliestMessages, hourlyDataForRolling30DaysByDay, materialized.projectUsageMatrix);
+ this.webviewProvider.updateData(sessionData, todayData, rolling30Data, allTimeData, dailyDataForRolling30, dailyDataForAllTime, hourlyDataForToday, undefined, dataDirectory, records, sessionBreakdown, projectBreakdown, contentAnalysis, branchBreakdown, workflowBreakdown, costliestMessages, hourlyDataForRolling30DaysByDay, materialized.projectUsageMatrix, materialized.dailyForAllTime);
}
}
@@ -4659,7 +4671,7 @@ export async function activate(context: vscode.ExtensionContext): Promise
console.log('Claude Code Usage extension is now active');
const settings = new SettingsStore(context);
- I18n.setLanguage(settings.get('language') as any);
+ I18n.setLanguage(settings.get('language') as any, vscode.env.language);
const secretMigrationFailure = await settings.initializeSecretsForActivation();
if (secretMigrationFailure) {
const needsManualWorkspaceMigration =
diff --git a/src/githubHeatmapPublish.ts b/src/githubHeatmapPublish.ts
index 5de322fb..7a2aec9a 100644
--- a/src/githubHeatmapPublish.ts
+++ b/src/githubHeatmapPublish.ts
@@ -1,4 +1,11 @@
export const GITHUB_PUBLIC_REPO_SCOPE = 'public_repo';
+export const GITHUB_HEATMAP_DESTINATION_KEY = 'ccu.heatmapDestination.v1';
+
+export interface GitHubHeatmapDestination {
+ schemaVersion: 1;
+ repository: string;
+ filePath: string;
+}
export interface GitHubApiResponse {
status: number;
@@ -37,6 +44,60 @@ export class GitHubPublishHttpError extends Error {
}
}
+export function parseGitHubHeatmapDestination(
+ value: unknown,
+): GitHubHeatmapDestination | undefined {
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined;
+ const candidate = value as Record;
+ if (
+ Object.keys(candidate).sort().join(',') !== 'filePath,repository,schemaVersion' ||
+ candidate.schemaVersion !== 1 ||
+ typeof candidate.repository !== 'string' ||
+ typeof candidate.filePath !== 'string'
+ ) {
+ return undefined;
+ }
+ try {
+ const plan = createGitHubPublishPlan(candidate.repository, candidate.filePath);
+ return {
+ schemaVersion: 1,
+ repository: `${plan.owner}/${plan.repository}`,
+ filePath: plan.filePath,
+ };
+ } catch {
+ return undefined;
+ }
+}
+
+export async function completeSuccessfulGitHubPublish(
+ preview: GitHubPublishPreview,
+ callbacks: {
+ persistDestination: (
+ key: typeof GITHUB_HEATMAP_DESTINATION_KEY,
+ destination: GitHubHeatmapDestination,
+ ) => Promise;
+ showSuccess: () => Promise;
+ showPersistenceWarning: () => Promise;
+ },
+): Promise<{ openBrowser: boolean; preferenceSaved: boolean }> {
+ const destination: GitHubHeatmapDestination = {
+ schemaVersion: 1,
+ repository: `${preview.owner}/${preview.repository}`,
+ filePath: preview.filePath,
+ };
+ let preferenceSaved = true;
+ try {
+ await callbacks.persistDestination(GITHUB_HEATMAP_DESTINATION_KEY, destination);
+ } catch {
+ preferenceSaved = false;
+ }
+ const openBrowser = await callbacks.showSuccess();
+ if (!preferenceSaved) {
+ await callbacks.showPersistenceWarning();
+ }
+ return { openBrowser, preferenceSaved };
+}
+
function encodedSegments(value: string): string {
return value.split('/').map(encodeURIComponent).join('/');
}
diff --git a/src/heatmapSvg.ts b/src/heatmapSvg.ts
index 6f9d1ed7..c05d2579 100644
--- a/src/heatmapSvg.ts
+++ b/src/heatmapSvg.ts
@@ -8,6 +8,14 @@
// README renders on a white/light canvas.
import { DayUsage, HeatMetric, intensityBucket } from './heatmap';
+import {
+ approximateSvgTextWidth,
+ escapeSvgAttribute,
+ escapeSvgText,
+ fitSvgText,
+} from './svgEscape';
+import { HEATMAP_ARTIFACT_TRANSLATIONS, artifactLocale } from './i18n';
+import { SupportedLanguage } from './types';
/** Claude-orange 5-step intensity ramp (bucket 0..4). 0 = empty cell. */
export const CLAUDE_ORANGE_SCALE = ['#ebedf0', '#fadcc9', '#f0aa82', '#e07d4f', '#c85a2b'];
@@ -30,6 +38,7 @@ export interface HeatmapSvgOptions {
accentColor?: string;
minWidth?: number; // optional card width floor for short date ranges
ariaLabel?: string;
+ locale?: string;
tooltip?: (dateISO: string, usage: DayUsage, value: number, metric: HeatMetric) => string;
}
@@ -114,7 +123,13 @@ function daysBetween(aISO: string, bISO: string): number {
}
/** Compact number: 5.3B / 1.2M / 345K / 42. */
-function compactNum(n: number): string {
+function compactNum(n: number, locale: SupportedLanguage = 'en'): string {
+ if (locale !== 'en') {
+ return new Intl.NumberFormat(locale, {
+ notation: 'compact',
+ maximumFractionDigits: 1,
+ }).format(n);
+ }
const abs = Math.abs(n);
const trim = (x: number): string => x.toFixed(1).replace(/\.0$/, '');
if (abs >= 1e9) return trim(n / 1e9) + 'B';
@@ -130,19 +145,25 @@ function ordinal(d: number): string {
return d + suffix;
}
-const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
const MONTHS_FULL = [
'January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December',
];
-/** "June 18th" for a YYYY-MM-DD key. */
-function longDate(dateISO: string): string {
+/** Locale-aware visible date for a YYYY-MM-DD key. */
+function longDate(dateISO: string, locale: SupportedLanguage): string {
+ if (locale !== 'en') {
+ return new Intl.DateTimeFormat(locale, {
+ month: 'long',
+ day: 'numeric',
+ timeZone: 'UTC',
+ }).format(new Date(`${dateISO}T00:00:00.000Z`));
+ }
const month = MONTHS_FULL[Number(dateISO.slice(5, 7)) - 1];
return `${month} ${ordinal(Number(dateISO.slice(8, 10)))}`;
}
-const esc = (s: string): string => s.replace(/&/g, '&').replace(//g, '>');
+const esc = escapeSvgText;
export interface HeatGridCell {
dateISO: string;
@@ -198,6 +219,8 @@ export function buildContributionGrid(
/** Render the trailing-year token heatmap as a self-contained SVG string. */
export function renderHeatmapSvg(daily: Record, opts: HeatmapSvgOptions = {}): string {
+ const locale = artifactLocale(opts.locale);
+ const copy = HEATMAP_ARTIFACT_TRANSLATIONS[locale];
const metric = opts.metric ?? 'tokens';
const weeks = Math.max(1, Math.min(53, opts.weeks ?? 53));
const today = opts.endDateISO ?? new Date().toISOString().slice(0, 10);
@@ -208,7 +231,7 @@ export function renderHeatmapSvg(daily: Record, opts: HeatmapS
color,
CLAUDE_ORANGE_SCALE[Math.min(index, CLAUDE_ORANGE_SCALE.length - 1)],
));
- const watermark = opts.watermark ?? 'Made with Claude Code Usage';
+ const watermark = opts.watermark ?? copy.madeWith;
// Trailing window: full weeks ending on the Saturday of today's week, cells
// only up to today (no future) — GitHub's default contribution view.
@@ -227,11 +250,11 @@ export function renderHeatmapSvg(daily: Record, opts: HeatmapS
const summary =
opts.title ??
(metric === 'cost'
- ? `$${compactNum(grid.total)} in Claude Code · ${year}`
+ ? `$${compactNum(grid.total, locale)} ${copy.inClaudeCode} · ${year}`
: metric === 'sessions'
- ? `${compactNum(grid.total)} sessions in Claude Code · ${year}`
- : `${compactNum(grid.total)} tokens in Claude Code · ${year}`);
- const noun = metric === 'sessions' ? 'sessions' : metric === 'cost' ? '' : 'tokens';
+ ? `${compactNum(grid.total, locale)} ${copy.sessions} ${copy.inClaudeCode} · ${year}`
+ : `${compactNum(grid.total, locale)} ${copy.tokens} ${copy.inClaudeCode} · ${year}`);
+ const noun = metric === 'sessions' ? copy.sessions : metric === 'cost' ? '' : copy.tokens;
const cell = 12;
const gap = 3;
@@ -245,6 +268,7 @@ export function renderHeatmapSvg(daily: Record, opts: HeatmapS
const footerH = opts.footerNote ? 48 : 30;
const width = Math.max(padL + gridW + 10, Math.max(0, opts.minWidth ?? 0));
const height = padT + gridH + footerH;
+ const renderedSummary = fitSvgText(summary, width - padL - 10, 15);
const parts: string[] = [];
const background = safeSvgColor(opts.background, '#ffffff');
@@ -255,13 +279,13 @@ export function renderHeatmapSvg(daily: Record, opts: HeatmapS
? safeSvgColor(opts.accentColor, '')
: undefined;
parts.push(
- `