From ebf30a805bc31ee5c177329893e1621d4d7dd7f3 Mon Sep 17 00:00:00 2001 From: "Jiaming WANG (Carl)" <74130028+Carl723000@users.noreply.github.com> Date: Tue, 22 Sep 2026 11:07:22 +0800 Subject: [PATCH 01/10] fix: auto-recover marketplace delivery (#107) --- .github/scripts/automationPolicy.test.mjs | 24 +++- .github/workflows/marketplace-recovery.yml | 145 +++++++++++++++++++++ .github/workflows/publish.yml | 8 +- .github/workflows/test.yml | 5 +- src/test/repositoryPolicy.test.ts | 8 +- 5 files changed, 181 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/marketplace-recovery.yml 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/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/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/src/test/repositoryPolicy.test.ts b/src/test/repositoryPolicy.test.ts index 3edc57cc..30e35cf1 100644 --- a/src/test/repositoryPolicy.test.ts +++ b/src/test/repositoryPolicy.test.ts @@ -82,7 +82,7 @@ function assertExactVscePin(commands: readonly WorkflowStepValue[]): void { ); assert.ok(tokens.length > 0, 'workflow must invoke VSCE'); for (const token of tokens) { - assert.equal(token, '@vscode/vsce@3.9.2', `unexpected VSCE invocation ${token}`); + assert.equal(token, '@vscode/vsce@4.0.0', `unexpected VSCE invocation ${token}`); } } @@ -1687,7 +1687,7 @@ test('CI has separate Node, browser, and package release gates', () => { assert.match(workflow, /PLAYWRIGHT_BROWSERS_PATH:\s*\/ms-playwright/); assert.match(workflow, /needs:\s*\[test, ui\]/); assert.ok(runValues.includes('npm run test:ui')); - const packageAt = runValues.indexOf('npx -y @vscode/vsce@3.9.2 package --out /tmp/claude-code-usage-ci.vsix'); + const packageAt = runValues.indexOf('npx -y @vscode/vsce@4.0.0 package --out /tmp/claude-code-usage-ci.vsix'); const verifyAt = runValues.indexOf('node .github/scripts/verify-vsix.mjs /tmp/claude-code-usage-ci.vsix'); assert.ok(packageAt >= 0 && verifyAt > packageAt, 'smoke VSIX verification must follow packaging'); assertExactVscePin(runs); @@ -1713,10 +1713,10 @@ test('publish pins compatible registry CLIs and isolates all three delivery sink const releaseNodeMatch = workflow.match(/node-version:\s*'(\d+)'/); assert.ok(releaseNodeMatch, 'publish workflow must pin a Node major'); const releaseNodeMajor = Number(releaseNodeMatch[1]); - const packageStep = findRun('npx -y @vscode/vsce@3.9.2 package --out claude-code-usage.vsix'); + const packageStep = findRun('npx -y @vscode/vsce@4.0.0 package --out claude-code-usage.vsix'); const verifyStep = findRun('node .release-policy/.github/scripts/verify-vsix.mjs claude-code-usage.vsix "${RELEASE_TAG#v}"'); const restoreStep = runs.find(({ value }) => value.includes('gh release download "$RELEASE_TAG"')); - const publishStep = runs.find(({ value }) => value.includes('@vscode/vsce@3.9.2 publish')); + const publishStep = runs.find(({ value }) => value.includes('@vscode/vsce@4.0.0 publish')); const openVsxStep = runs.find(({ value }) => value.includes('ovsx@1.2.0 publish')); const resultStep = runs.find(({ value }) => value.includes('VSCODE_OUTCOME')); const attachStep = uses.find(({ value }) => From 19d0bb05da78ef0e92b4fa0cc0d79512a1c232bf Mon Sep 17 00:00:00 2001 From: Alfiefe10 Date: Tue, 22 Sep 2026 05:09:28 +0200 Subject: [PATCH 02/10] test: pin the clock for the incremental content-analysis body reads (#110) The fixture timestamps are fixed at 2026-08-21 while the test asks for a 30-day window against the real clock, so on 2026-09-20 the corpus aged out of that window and the warm update read a second body. The assertion was about incremental body reads, not about the window, and it expired by date on every branch. Pinning Date.now the way the neighbouring content tests already do keeps it meaningful. --- src/test/claudeIncrementalIndex.test.ts | 94 +++++++++++++------------ 1 file changed, 50 insertions(+), 44 deletions(-) diff --git a/src/test/claudeIncrementalIndex.test.ts b/src/test/claudeIncrementalIndex.test.ts index 0daad04f..b5b81fa2 100644 --- a/src/test/claudeIncrementalIndex.test.ts +++ b/src/test/claudeIncrementalIndex.test.ts @@ -235,53 +235,59 @@ test('cold production index preserves the established full-loader record semanti }); test('content analysis is materialized from per-file contributions without a second body scan', async () => { - const { root, first } = await fixture(); - await appendFile(first, [ - JSON.stringify({ - type: 'assistant', - uuid: 'tool-use-line', - timestamp: '2026-08-21T08:02:00.000Z', - message: { - role: 'assistant', - content: [{ type: 'tool_use', id: 'tool-1', name: 'Read', input: { file_path: '/tmp/a' } }], - }, - }), - JSON.stringify({ - type: 'user', - uuid: 'tool-result-line', - timestamp: '2026-08-21T08:03:00.000Z', - message: { - role: 'user', - content: [{ type: 'tool_result', tool_use_id: 'tool-1', content: 'fixture result' }], - }, - }), - '', - ].join('\n'), 'utf8'); + const previousNow = Date.now; + Date.now = () => Date.parse('2026-09-10T12:00:00.000Z'); + try { + const { root, first } = await fixture(); + await appendFile(first, [ + JSON.stringify({ + type: 'assistant', + uuid: 'tool-use-line', + timestamp: '2026-08-21T08:02:00.000Z', + message: { + role: 'assistant', + content: [{ type: 'tool_use', id: 'tool-1', name: 'Read', input: { file_path: '/tmp/a' } }], + }, + }), + JSON.stringify({ + type: 'user', + uuid: 'tool-result-line', + timestamp: '2026-08-21T08:03:00.000Z', + message: { + role: 'user', + content: [{ type: 'tool_result', tool_use_id: 'tool-1', content: 'fixture result' }], + }, + }), + '', + ].join('\n'), 'utf8'); - const incremental = await updateClaudeUsageIndex(createClaudeUsageIndex(), root, { - analyzeContent: true, - windowDays: 30, - }); - const full = await ClaudeDataLoader.loadUsageRecords(root, { - analyzeContent: true, - windowDays: 30, - }); + const incremental = await updateClaudeUsageIndex(createClaudeUsageIndex(), root, { + analyzeContent: true, + windowDays: 30, + }); + const full = await ClaudeDataLoader.loadUsageRecords(root, { + analyzeContent: true, + windowDays: 30, + }); - assert.equal(incremental.diagnostics.bodyReads, 2); - assert.deepEqual(incremental.contentAnalysis, full.contentAnalysis); + assert.equal(incremental.diagnostics.bodyReads, 2); + assert.deepEqual(incremental.contentAnalysis, full.contentAnalysis); - const tail = `${promptLine('content tail', '2026-08-21T10:00:00.000Z')}\n`; - await appendFile(first, tail, 'utf8'); - const warm = await updateClaudeUsageIndex(incremental.index, root, { - analyzeContent: true, - windowDays: 30, - }); - const warmFull = await ClaudeDataLoader.loadUsageRecords(root, { - analyzeContent: true, - windowDays: 30, - }); - assert.equal(warm.diagnostics.bodyReads, 1); - assert.deepEqual(warm.contentAnalysis, warmFull.contentAnalysis); + const tail = `${promptLine('content tail', '2026-08-21T10:00:00.000Z')}\n`; + await appendFile(first, tail, 'utf8'); + const warm = await updateClaudeUsageIndex(incremental.index, root, { + analyzeContent: true, + windowDays: 30, + }); + const warmFull = await ClaudeDataLoader.loadUsageRecords(root, { + analyzeContent: true, + windowDays: 30, + }); + assert.equal(warm.diagnostics.bodyReads, 1); + assert.deepEqual(warm.contentAnalysis, warmFull.contentAnalysis); + } finally { + Date.now = previousNow; + } }); test('cross-file tool results retain global tool and Skill preamble attribution', async () => { From c8ba35147e8ad0232ec382f3e7d796d905ed2c51 Mon Sep 17 00:00:00 2001 From: Roman Syuzyov Date: Tue, 22 Sep 2026 08:16:08 +0500 Subject: [PATCH 03/10] perf: memoise Intl formatters in date bucketing (#99) (#109) Day, month and hour keys are derived once per ingested record, and each call built a new Intl.DateTimeFormat - twice, because resolveTimeZone constructed one of its own. Formatter construction dominates that path: a CPU profile of a pegged extension host attributed 57% of samples to resolveTimeZone and partsInZone alone. Memoise the resolved zone and both formatters per zone. Keys are unchanged, including the empty-zone and invalid-zone fallbacks; the cache holds one entry per configured zone. Measured on a 1.4 GB local history: 1,059 -> 167,982 records/second (158x), 0 key mismatches over the comparison set. Co-authored-by: Jiaming WANG (Carl) <74130028+Carl723000@users.noreply.github.com> --- CHANGELOG.md | 6 ++++ src/dateKeys.ts | 62 +++++++++++++++++++++++++++++++-------- src/test/dateKeys.test.ts | 41 ++++++++++++++++++++++++++ 3 files changed, 97 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fe29963..7f17b6f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ upstream release: 1.0.8). Format follows [Keep a Changelog](https://keepachangel ## [2.3.3] — Unreleased ### Fixed +- **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. - **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/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/test/dateKeys.test.ts b/src/test/dateKeys.test.ts index ee9372fa..d73b312e 100644 --- a/src/test/dateKeys.test.ts +++ b/src/test/dateKeys.test.ts @@ -108,3 +108,44 @@ test('timezone resolution returns a usable canonical zone', () => { assert.notEqual(resolveTimeZone(''), ''); assert.equal(resolveTimeZone('Not/AZone'), resolveTimeZone('')); }); + +test('repeated bucketing reuses one formatter per zone instead of building one per record', () => { + // Chatham is not used by the other cases here, so this zone starts cold. + const zone = 'Pacific/Chatham'; + const sample = new Date('2026-07-20T18:20:00.000Z'); + const realDateTimeFormat = Intl.DateTimeFormat; + const intl = Intl as unknown as { DateTimeFormat: typeof Intl.DateTimeFormat }; + let constructed = 0; + intl.DateTimeFormat = new Proxy(realDateTimeFormat, { + construct(target, args: unknown[]): object { + constructed += 1; + return Reflect.construct(target, args); + }, + }) as typeof Intl.DateTimeFormat; + + try { + dayKeyInZone(sample, zone); + hourKeyInZone(sample, zone); + const afterWarmUp = constructed; + assert.ok(afterWarmUp > 0, 'the first call in a zone still has to build its formatters'); + + for (let i = 0; i < 500; i++) { + dayKeyInZone(new Date(sample.getTime() + i * 3_600_000), zone); + monthKeyInZone(new Date(sample.getTime() + i * 3_600_000), zone); + hourKeyInZone(new Date(sample.getTime() + i * 3_600_000), zone); + } + + assert.equal(constructed, afterWarmUp); + } finally { + intl.DateTimeFormat = realDateTimeFormat; + } +}); + +test('memoised zones keep their own calendars apart', () => { + const d = new Date('2026-06-30T20:00:00Z'); + assert.equal(dayKeyInZone(d, 'Asia/Hong_Kong'), '2026-07-01'); + assert.equal(dayKeyInZone(d, 'UTC'), '2026-06-30'); + assert.equal(dayKeyInZone(d, 'Asia/Hong_Kong'), '2026-07-01'); + assert.equal(hourKeyInZone(d, 'UTC'), '20'); + assert.equal(hourKeyInZone(d, 'Asia/Hong_Kong'), '04'); +}); From 40271d225d78f2973aae42c179fda70571d24c29 Mon Sep 17 00:00:00 2001 From: Roman Syuzyov Date: Tue, 22 Sep 2026 08:26:00 +0500 Subject: [PATCH 04/10] perf: keep multi-file appends on the incremental path (#99) (#111) The content analysis fast path required exactly one changed file (sourcePlans.length === 1 && plans.length === 1). A machine running several agents appends to several transcripts between two refreshes, so that gate never held: every refresh fell back to replaceWithFullAnalysisPlans() and re-read every file. The diagnostics show the shape of it - changed=3 reused=641 alongside bodies=644 and bytes=1.4 GB. The application side already loops over plans, so the gate is what limited it. Allow any number of pure tail appends, and make two ordering guarantees the single-file case never needed: - parse appends in full-scan order, so the first file to carry a UUID owns it exactly as in the full loader (the comparator returned 0 for two appends, leaving discovery order to decide); - attribute each new UUID to the appended file that actually carries it, earliest first, instead of to parsedPlans[0]. The per-file UUID ownership check that forces a rebuild now runs for every appended file rather than only a lone one. Measured on 15 real transcripts (34 MB), three files appended: 15 body reads and 8680 ms became 3 body reads and 586 ms. Co-authored-by: Claude Opus 5 (1M context) Co-authored-by: Jiaming WANG (Carl) <74130028+Carl723000@users.noreply.github.com> Co-authored-by: OpenAI Codex <215057067+openai-codex[bot]@users.noreply.github.com> --- CHANGELOG.md | 8 +++ src/claudeIncrementalIndex.ts | 69 +++++++++++++++++-------- src/test/claudeIncrementalIndex.test.ts | 46 +++++++++++++++++ 3 files changed, 102 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f17b6f9..de3b0669 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ upstream release: 1.0.8). Format follows [Keep a Changelog](https://keepachangel ## [2.3.3] — Unreleased ### Fixed +- **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 diff --git a/src/claudeIncrementalIndex.ts b/src/claudeIncrementalIndex.ts index e4917755..386a676f 100644 --- a/src/claudeIncrementalIndex.ts +++ b/src/claudeIncrementalIndex.ts @@ -2588,14 +2588,18 @@ 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. + const appendOnlyPlans = plans.length > 0 && sourcePlans.length === plans.length && + plans.every((plan) => plan.kind === 'append' && plan.prior && + plan.prior.firstTimestampMs > 0); const safeTailAppend = Boolean( - soleSourcePlan?.kind === 'append' && soleSourcePlan.prior && + appendOnlyPlans && previousAnalysisRuntime && !timeZoneChanged && - previous.windowDays === windowDays && analysisPayloadRebases.size === 0 && - soleSourcePlan.prior.firstTimestampMs > 0, + previous.windowDays === windowDays && analysisPayloadRebases.size === 0, ); const priorAnalysisCutoffMs = previousAnalysisRuntime?.cutoffMs ?? (previous.analyzeContent ? previous.analysisCutoffMs : undefined); @@ -2705,11 +2709,13 @@ 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 appends must be parsed in full-scan order — otherwise + // ownership falls to whichever file happened to be discovered first. + 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 +2744,27 @@ 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) { + if (fastAppendAnalysis && previousAnalysisRuntime && parsedPlans.length > 0 && + parsedPlans.every((plan) => plan.kind === 'append')) { + 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) { + 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; @@ -2919,7 +2931,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) => + (previousAnalysisRuntime.filePositionById.get(left.fileId) ?? 0) - + (previousAnalysisRuntime.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 ?? ''); } } diff --git a/src/test/claudeIncrementalIndex.test.ts b/src/test/claudeIncrementalIndex.test.ts index b5b81fa2..ac3b9d7d 100644 --- a/src/test/claudeIncrementalIndex.test.ts +++ b/src/test/claudeIncrementalIndex.test.ts @@ -451,6 +451,52 @@ test('calibration follows missing request IDs, a cross-file winner, and cutoff e } }); +test('concurrent appends to several files stay incremental instead of forcing a full rebuild', async () => { + const previousNow = Date.now; + Date.now = () => Date.parse('2026-09-10T12:00:00.000Z'); + const root = await mkdtemp(path.join(os.tmpdir(), 'ccu-claude-multi-append-')); + roots.push(root); + const project = path.join(root, 'projects', '-fixture-multi-append'); + await mkdir(project, { recursive: true }); + const files: string[] = []; + try { + for (let index = 0; index < 64; index += 1) { + const file = path.join(project, `session-${String(index).padStart(3, '0')}.jsonl`); + files.push(file); + await writeFile(file, `${usageLine(`multi-append-${index}`, index + 1, 1, { + timestamp: new Date(Date.parse('2026-09-09T00:00:00.000Z') + index * 1_000).toISOString(), + })} +`, 'utf8'); + } + const cold = await updateClaudeUsageIndex(createClaudeUsageIndex(), root); + assert.equal(cold.diagnostics.bodyReads, 64); + + // Three sessions writing at once is the ordinary case for a machine running + // several agents, not an edge case. + const appended = [files[10], files[30], files[63]]; + let offset = 0; + let appendedBytes = 0; + for (const file of appended) { + offset += 1; + const tail = `${usageLine(`multi-append-tail-${offset}`, 9, 3, { + timestamp: new Date(Date.parse('2026-09-09T00:10:00.000Z') + offset * 1_000).toISOString(), + })} +`; + appendedBytes += Buffer.byteLength(tail); + await appendFile(file, tail, 'utf8'); + } + + const warm = await updateClaudeUsageIndex(cold.index, root); + const full = await ClaudeDataLoader.loadUsageRecords(root, { analyzeContent: true }); + + assert.equal(warm.diagnostics.bodyReads, appended.length); + assert.equal(warm.diagnostics.bytesRead, appendedBytes); + assert.deepEqual(warm.contentAnalysis, full.contentAnalysis); + } finally { + Date.now = previousNow; + } +}); + test('default content analysis does not revisit historical file contributions after a small append', async () => { const previousNow = Date.now; Date.now = () => Date.parse('2026-09-10T12:00:00.000Z'); From 71b85492dae3cf4f4330975bc68ab2203857ad42 Mon Sep 17 00:00:00 2001 From: Roman Syuzyov Date: Tue, 22 Sep 2026 08:36:01 +0500 Subject: [PATCH 05/10] perf: a new transcript no longer rebuilds the corpus (#99) (#112) * perf: keep multi-file appends on the incremental path (#99) The content analysis fast path required exactly one changed file (sourcePlans.length === 1 && plans.length === 1). A machine running several agents appends to several transcripts between two refreshes, so that gate never held: every refresh fell back to replaceWithFullAnalysisPlans() and re-read every file. The diagnostics show the shape of it - changed=3 reused=641 alongside bodies=644 and bytes=1.4 GB. The application side already loops over plans, so the gate is what limited it. Allow any number of pure tail appends, and make two ordering guarantees the single-file case never needed: - parse appends in full-scan order, so the first file to carry a UUID owns it exactly as in the full loader (the comparator returned 0 for two appends, leaving discovery order to decide); - attribute each new UUID to the appended file that actually carries it, earliest first, instead of to parsedPlans[0]. The per-file UUID ownership check that forces a rebuild now runs for every appended file rather than only a lone one. Measured on 15 real transcripts (34 MB), three files appended: 15 body reads and 8680 ms became 3 body reads and 586 ms. Co-Authored-By: Claude Opus 5 (1M context) * perf: a new transcript no longer rebuilds the corpus (#99) A file that had just appeared was classified as an unsafe source mutation, so plans were replaced with a full rebuild of every file. Every new session starts its own transcript, so on a machine running agents this happened many times a day - and it is why the multi-file append fast path rarely helped in practice. A new file is as safe as a tail append: its own body is read in full and the established contributions are untouched. Let such plans through the gate, add the new file's whole analysis as its delta, and recompute ordering state from metadata only when files were actually added, so pure appends still never touch historical files. Ownership is guarded: a new file carrying a UUID somebody already owns falls back to the full rebuild, because the full loader resolves that by scan order. Measured on 15 real transcripts, two appends plus one new file: 16 body reads and 3042 ms became 3 body reads and 567 ms. Co-Authored-By: Claude Opus 5 (1M context) --------- Co-authored-by: Claude Opus 5 (1M context) Co-authored-by: Carl <74130028+Carl723000@users.noreply.github.com> Co-authored-by: OpenAI Codex <215057067+openai-codex[bot]@users.noreply.github.com> --- CHANGELOG.md | 8 +++ src/claudeIncrementalIndex.ts | 70 ++++++++++++++++---- src/test/claudeIncrementalIndex.test.ts | 86 +++++++++++++++++++++++++ 3 files changed, 151 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index de3b0669..a51aa5e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ upstream release: 1.0.8). Format follows [Keep a Changelog](https://keepachangel ## [2.3.3] — Unreleased ### Fixed +- **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 diff --git a/src/claudeIncrementalIndex.ts b/src/claudeIncrementalIndex.ts index 386a676f..cead9ef8 100644 --- a/src/claudeIncrementalIndex.ts +++ b/src/claudeIncrementalIndex.ts @@ -2593,9 +2593,16 @@ export async function updateClaudeUsageIndex( // 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; const appendOnlyPlans = plans.length > 0 && sourcePlans.length === plans.length && - plans.every((plan) => plan.kind === 'append' && plan.prior && - plan.prior.firstTimestampMs > 0); + plans.every((plan) => isTailAppend(plan) || isNewFile(plan)); const safeTailAppend = Boolean( appendOnlyPlans && previousAnalysisRuntime && !timeZoneChanged && @@ -2664,7 +2671,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?.(); @@ -2744,11 +2753,21 @@ 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 > 0 && - parsedPlans.every((plan) => plan.kind === 'append')) { + if (fastAppendAnalysis && previousAnalysisRuntime && parsedPlans.length > 0) { let laterOwnerCollision = false; for (const appended of parsedPlans) { if (laterOwnerCollision) break; + if (appended.kind === 'rebuild') { + // 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; @@ -2902,18 +2921,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, @@ -2935,8 +2979,8 @@ export async function updateClaudeUsageIndex( // comes first in the established order — not to whichever plan happened // to be parsed first. const appendedByOrder = [...parsedPlans].sort((left, right) => - (previousAnalysisRuntime.filePositionById.get(left.fileId) ?? 0) - - (previousAnalysisRuntime.filePositionById.get(right.fileId) ?? 0)); + (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; @@ -2962,8 +3006,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/test/claudeIncrementalIndex.test.ts b/src/test/claudeIncrementalIndex.test.ts index ac3b9d7d..a4fae523 100644 --- a/src/test/claudeIncrementalIndex.test.ts +++ b/src/test/claudeIncrementalIndex.test.ts @@ -451,6 +451,92 @@ test('calibration follows missing request IDs, a cross-file winner, and cutoff e } }); +test('a newly started session file does not rebuild the established corpus', async (t) => { + const previousNow = Date.now; + Date.now = () => Date.parse('2026-09-10T12:00:00.000Z'); + const root = await mkdtemp(path.join(os.tmpdir(), 'ccu-claude-new-file-')); + roots.push(root); + const project = path.join(root, 'projects', '-fixture-new-file'); + await mkdir(project, { recursive: true }); + const files: string[] = []; + try { + for (let index = 0; index < 64; index += 1) { + const file = path.join(project, `session-${String(index).padStart(3, '0')}.jsonl`); + files.push(file); + await writeFile(file, `${analysisTextLine( + `new-file-seed-${index}`, + `seed ${index}`, + new Date(Date.parse('2026-09-09T00:00:00.000Z') + index * 1_000).toISOString(), + )} +`, 'utf8'); + } + const cold = await updateClaudeUsageIndex(createClaudeUsageIndex(), root); + assert.equal(cold.diagnostics.bodyReads, 64); + + // Every new session starts its own transcript while other sessions append. + await appendFile(files[7], `${analysisTextLine( + 'new-file-tail-a', 'tail a', '2026-09-09T00:10:00.000Z', + )} +`, 'utf8'); + await appendFile(files[40], `${analysisTextLine( + 'new-file-tail-b', 'tail b', '2026-09-09T00:11:00.000Z', + )} +`, 'utf8'); + await writeFile(path.join(project, 'session-new.jsonl'), `${analysisTextLine( + 'new-file-fresh', 'fresh session', '2026-09-09T00:12:00.000Z', + )} +`, 'utf8'); + + const warm = await updateClaudeUsageIndex(cold.index, root); + const full = await ClaudeDataLoader.loadUsageRecords(root, { analyzeContent: true }); + + // File identity is (dev, inode), and NTFS reuses file record numbers: a file + // created here can land on the identity of one the fixture already indexed, + // which reads as a move and legitimately forces the full rebuild. That is a + // property of the filesystem, not of this path, so skip rather than assert + // a number the run cannot deliver. The correctness check below still holds. + if (warm.diagnostics.changed.move > 0) { + t.skip('the filesystem reused a file identity, so this refresh is a move, not an addition'); + return; + } + + assert.equal(warm.diagnostics.bodyReads, 3); + assert.deepEqual(warm.contentAnalysis, full.contentAnalysis); + } finally { + Date.now = previousNow; + } +}); + +test('a new file carrying an already-owned UUID falls back to the full rebuild', async () => { + const previousNow = Date.now; + Date.now = () => Date.parse('2026-09-10T12:00:00.000Z'); + const root = await mkdtemp(path.join(os.tmpdir(), 'ccu-claude-new-file-owned-')); + roots.push(root); + const project = path.join(root, 'projects', '-fixture-new-file-owned'); + await mkdir(project, { recursive: true }); + try { + const established = path.join(project, 'session-established.jsonl'); + await writeFile(established, `${analysisTextLine( + 'shared-owned-uuid', 'established content', '2026-09-09T10:00:00.000Z', + )} +`, 'utf8'); + const cold = await updateClaudeUsageIndex(createClaudeUsageIndex(), root); + + // Earlier by timestamp, so the full loader hands ownership to the new file. + await writeFile(path.join(project, 'session-earlier.jsonl'), `${analysisTextLine( + 'shared-owned-uuid', 'a different body for the same uuid', '2026-09-09T09:00:00.000Z', + )} +`, 'utf8'); + + const warm = await updateClaudeUsageIndex(cold.index, root); + const full = await ClaudeDataLoader.loadUsageRecords(root, { analyzeContent: true }); + + assert.deepEqual(warm.contentAnalysis, full.contentAnalysis); + } finally { + Date.now = previousNow; + } +}); + test('concurrent appends to several files stay incremental instead of forcing a full rebuild', async () => { const previousNow = Date.now; Date.now = () => Date.parse('2026-09-10T12:00:00.000Z'); From dd096f5a0293e640b6bb0d86b3339b4c9b7c2978 Mon Sep 17 00:00:00 2001 From: Alfiefe10 Date: Tue, 22 Sep 2026 05:59:35 +0200 Subject: [PATCH 06/10] Add optional status-bar quota format template (#108) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: optional status-bar quota format template Adds statusBarQuotaFormat (default empty, which keeps the built-in layout). A template names the quota windows itself: {5h|wk|7d|model:NAME}.{pct|reset|label}, e.g. "{5h.pct} | {7d.pct} · {model:Fable.pct}". Separators come out as typed. A segment whose windows the account does not report is dropped together with its separator, so one template works across plans. The warning colour follows the windows the template shows. * test: cover the quota format status-bar wiring Drives setVisibility/updateQuota against the vscode stub so the path from the setting to the bar text and its warning colour is pinned, not only the pure formatter. * fix: keep typed spacing and unknown tokens in a quota format Literal text kept its own whitespace collapsed, and a segment naming only windows the account lacks took any typo in it down as well, which contradicted the rule that an unrecognised token stays visible. A missing window now leaves a marker that closes just its own gap. Also documents the setting in the two README editions that carry settings detail. * feat: let a reset token name its own countdown style resetCountdownFormat is one setting for the whole bar, so a template could not put a countdown for the 5h window beside a wall clock for the week. A reset token now takes an optional style - {5h.reset:units}, :decimal, :clock or :at - and falls back to the global format without one. A reset the API left blank closes its gap the way a missing window does. * ux: keep quota format presets compact across locales * docs: place quota format in current configuration guidance * docs: separate published v2.3.3 from unreleased changes --------- Co-authored-by: Carl <74130028+Carl723000@users.noreply.github.com> --- CHANGELOG.md | 21 ++- README-en.md | 1 + README-id.md | 1 + README-ja.md | 1 + README-ko.md | 1 + README-zh-CN.md | 3 + README-zh-TW.md | 1 + README.md | 9 ++ src/extension.ts | 9 +- src/i18n.ts | 52 +++++++ src/quotaFormat.ts | 224 ++++++++++++++++++++++++++- src/settings.ts | 11 ++ src/statusBar.ts | 9 +- src/test/quotaFormat.test.ts | 213 +++++++++++++++++++++++++ src/test/repositoryPolicy.test.ts | 16 +- src/test/statusBarThresholds.test.ts | 43 +++++ src/types.ts | 2 + src/webview.ts | 69 +++++++++ tests/ui/claude-settings.spec.mjs | 55 +++++++ 19 files changed, 728 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a51aa5e6..809252ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,21 @@ 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 +- **`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. ### Fixed - **Full rebuild whenever a new transcript appeared (#99)** — a file that had @@ -29,6 +43,11 @@ upstream release: 1.0.8). Format follows [Keep a Changelog](https://keepachangel 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. + +## [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/README-en.md b/README-en.md index b8bf7eb0..31b93911 100644 --- a/README-en.md +++ b/README-en.md @@ -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). diff --git a/README-id.md b/README-id.md index 1bcb16fa..4e516875 100644 --- a/README-id.md +++ b/README-id.md @@ -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). diff --git a/README-ja.md b/README-ja.md index 17f90839..e652d3ed 100644 --- a/README-ja.md +++ b/README-ja.md @@ -48,6 +48,7 @@ ## 主な機能 - **ステータスバー** — 本日のコスト、現在のセッションのコスト、そして Claude Code 自身の OAuth セッションから読み取る実際の 5 時間 / 週間クォータ(`5h:N% wk:N%`)。設定不要。 +- **クォータ表示形式** — ⚙ 設定で標準(既定)・5 時間のみ・週間のみを選択できます。「カスタム」を選んだ場合だけテンプレート欄が表示されます。 - **ダッシュボードタブ** — 今日 / 直近 30 日 / 全期間 に加え、**Sessions / Projects / Content / Branches**。すべて並べ替え可能。 - **積み上げコスト構成チャート**(Y 軸と参照線付き)— 各日 / 各月のコストが入力・出力・キャッシュ書き込み・キャッシュ読み取りにどれだけ使われたか一目で分かります。 - **Content タブ** — どの内容がトークンを消費しているか(あなたのプロンプト vs ツール結果 vs アシスタント出力 / 思考)を推定。 diff --git a/README-ko.md b/README-ko.md index d9b5ec2e..1ba840ef 100644 --- a/README-ko.md +++ b/README-ko.md @@ -48,6 +48,7 @@ ## 기능 - **상태 표시줄** — 오늘 비용, 현재 세션 비용, 그리고 Claude Code 자체 OAuth 세션에서 읽는 실제 5시간 / 주간 쿼터(`5h:N% wk:N%`). 설정 불필요. +- **쿼터 표시 형식** — ⚙ 설정에서 기본(기본값), 5시간만, 주간만 선택할 수 있습니다. 사용자 지정을 선택할 때만 템플릿 입력칸이 표시됩니다. - **대시보드 탭** — 오늘 / 최근 30일 / 전체 기간, 그리고 **Sessions / Projects / Content / Branches**. 모두 정렬 가능. - **누적 비용 구성 차트**(Y축과 기준선 포함) — 각 일 / 월의 비용 중 입력·출력·캐시 쓰기·캐시 읽기가 차지하는 비율을 한눈에. - **Content 탭** — 어떤 콘텐츠가 토큰을 소비하는지 추정(내 프롬프트 vs 도구 결과 vs 어시스턴트 출력 / 사고). diff --git a/README-zh-CN.md b/README-zh-CN.md index 475e520d..9d53b9b1 100644 --- a/README-zh-CN.md +++ b/README-zh-CN.md @@ -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 天“项目 × 日期”热力图与每日堆叠趋势,并保留精确提示、覆盖状态、有界行数和“其他项目”长尾。 @@ -177,6 +178,8 @@ ext install GrowthJack.claude-code-usage 其余全部 —— 刷新间隔、状态栏各项、数字 / 日期格式、项目分组、内容分析,以及所有 AI 建议 / 优化器选项 —— 都在仪表板的 ⚙ 设置标签页里。升级不丢配置:首次启动会做一次性迁移,把你 `settings.json` 里已有的值搬过来。 +如需自定义 Claude 配额状态栏,在 ⚙ 设置中选择“自定义”。模板支持 `{5h.pct}`、`{wk.pct}`(或 `{7d.pct}`)和 `{model:Fable.pct}`;每个窗口也支持 `.label`、`.reset`,重置样式可用 `:decimal`、`:units`、`:clock`、`:at`,例如 `{5h.pct} | {wk.reset:at}`。未报告的窗口及其分隔符会省略;选“默认”则沿用原有额度选项。 + --- ## 成本是如何计算的 diff --git a/README-zh-TW.md b/README-zh-TW.md index 22d52cad..66b641be 100644 --- a/README-zh-TW.md +++ b/README-zh-TW.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 助理輸出 / 思考)。 diff --git a/README.md b/README.md index faf10925..76a7d535 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,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 ![Dashboard — summary and charts](images/v2-dashboard-en.png) @@ -449,6 +451,13 @@ 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 diff --git a/src/extension.ts b/src/extension.ts index 0d526949..6b711802 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -2092,7 +2092,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 => { @@ -2156,6 +2156,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 +3179,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 +3211,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; @@ -3278,7 +3279,7 @@ export class ClaudeCodeUsageExtension { this.invalidateClaudeUsagePricingCache(); } 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(); diff --git a/src/i18n.ts b/src/i18n.ts index 31b7f968..38f231f6 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -138,6 +138,11 @@ export interface Translations { settingsGroupProviders: string; settingsGroupFeatures: string; settingsGroupStatusBar: string; + quotaFormatBuiltIn: string; + quotaFormatFiveHour: string; + quotaFormatWeekly: string; + quotaFormatCustom: string; + quotaFormatShortHelp: string; settingsGroupData: string; settingsGroupAdvice: string; adviceEffectiveness: AdviceEffectivenessTranslations; @@ -1215,6 +1220,11 @@ const translations: Record = { settingsGroupProviders: 'Providers', settingsGroupFeatures: 'Optional features', settingsGroupStatusBar: 'Status bar', + quotaFormatBuiltIn: 'Built-in', + quotaFormatFiveHour: '5-hour only', + quotaFormatWeekly: 'Weekly only', + quotaFormatCustom: 'Custom…', + quotaFormatShortHelp: 'Choose a compact layout. Built-in keeps the quota options above.', settingsGroupData: 'Data & refresh', settingsGroupAdvice: 'AI advice & Optimizer', adviceEffectiveness: { @@ -1516,6 +1526,11 @@ const translations: Record = { settingsGroupProviders: "Anbieter", settingsGroupFeatures: "Optionale Funktionen", settingsGroupStatusBar: "Statusleiste", + quotaFormatBuiltIn: 'Standard', + quotaFormatFiveHour: 'Nur 5 Stunden', + quotaFormatWeekly: 'Nur Woche', + quotaFormatCustom: 'Benutzerdefiniert…', + quotaFormatShortHelp: 'Kompaktes Layout wählen. „Standard“ übernimmt die obigen Kontingentoptionen.', settingsGroupData: "Daten & Aktualisierung", settingsGroupAdvice: "KI-Beratung & Optimizer", adviceEffectiveness: { @@ -1820,6 +1835,11 @@ const translations: Record = { settingsGroupProviders: '供應商', settingsGroupFeatures: '選用功能', settingsGroupStatusBar: '狀態列', + quotaFormatBuiltIn: '內建', + quotaFormatFiveHour: '僅 5 小時', + quotaFormatWeekly: '僅每週', + quotaFormatCustom: '自訂…', + quotaFormatShortHelp: '選擇精簡版面;「內建」沿用上方配額選項。', settingsGroupData: '資料與重新整理', settingsGroupAdvice: 'AI 建議與最佳化工具', adviceEffectiveness: { @@ -2117,6 +2137,11 @@ const translations: Record = { settingsGroupProviders: '供应商', settingsGroupFeatures: '可选功能', settingsGroupStatusBar: '状态栏', + quotaFormatBuiltIn: '默认', + quotaFormatFiveHour: '仅 5 小时', + quotaFormatWeekly: '仅每周', + quotaFormatCustom: '自定义…', + quotaFormatShortHelp: '选择简洁布局;“默认”沿用上方额度选项。', settingsGroupData: '数据与刷新', settingsGroupAdvice: 'AI 建议与优化器', adviceEffectiveness: { @@ -2414,6 +2439,11 @@ const translations: Record = { settingsGroupProviders: 'プロバイダー', settingsGroupFeatures: 'オプション機能', settingsGroupStatusBar: 'ステータスバー', + quotaFormatBuiltIn: '標準', + quotaFormatFiveHour: '5時間のみ', + quotaFormatWeekly: '週のみ', + quotaFormatCustom: 'カスタム…', + quotaFormatShortHelp: '簡潔な表示を選択します。「標準」は上の利用枠設定を使います。', settingsGroupData: 'データと更新', settingsGroupAdvice: 'AI アドバイス & オプティマイザー', adviceEffectiveness: { @@ -2716,6 +2746,11 @@ const translations: Record = { settingsGroupProviders: '공급자', settingsGroupFeatures: '선택 기능', settingsGroupStatusBar: '상태 표시줄', + quotaFormatBuiltIn: '기본', + quotaFormatFiveHour: '5시간만', + quotaFormatWeekly: '주간만', + quotaFormatCustom: '사용자 지정…', + quotaFormatShortHelp: '간결한 표시를 선택하세요. 기본은 위의 한도 옵션을 따릅니다.', settingsGroupData: '데이터 및 새로고침', settingsGroupAdvice: 'AI 조언 & 옵티마이저', adviceEffectiveness: { @@ -3018,6 +3053,11 @@ const translations: Record = { settingsGroupProviders: 'Provedores', settingsGroupFeatures: 'Recursos opcionais', settingsGroupStatusBar: 'Barra de status', + quotaFormatBuiltIn: 'Padrão', + quotaFormatFiveHour: 'Só 5 horas', + quotaFormatWeekly: 'Só semanal', + quotaFormatCustom: 'Personalizado…', + quotaFormatShortHelp: 'Escolha um layout compacto. Padrão mantém as opções de limite acima.', settingsGroupData: 'Dados e atualização', settingsGroupAdvice: 'Conselho de IA e Optimizer', adviceEffectiveness: { @@ -3319,6 +3359,11 @@ const translations: Record = { settingsGroupProviders: 'Penyedia', settingsGroupFeatures: 'Fitur opsional', settingsGroupStatusBar: 'Status bar', + quotaFormatBuiltIn: 'Bawaan', + quotaFormatFiveHour: 'Hanya 5 jam', + quotaFormatWeekly: 'Hanya mingguan', + quotaFormatCustom: 'Kustom…', + quotaFormatShortHelp: 'Pilih tampilan ringkas. Bawaan mengikuti opsi kuota di atas.', settingsGroupData: 'Data & penyegaran', settingsGroupAdvice: 'Saran AI & Optimizer', adviceEffectiveness: { @@ -3623,6 +3668,7 @@ const SETTINGS_I18N: Partial p.trim().toLowerCase()); + const known = name === '5h' || name === 'wk' || name === '7d' || /^model:./.test(name); + if (!known || rest.length > 0 || (field !== 'pct' && field !== 'reset' && field !== 'label')) { + return null; + } + if (style === undefined) { + return { name, field }; + } + const styled = style === 'decimal' || style === 'units' || style === 'clock' || style === 'at'; + return field === 'reset' && styled ? { name, field, style } : null; +} + +function parseTemplate(template: string): TemplatePart[] { + const parts: TemplatePart[] = []; + let last = 0; + for (const m of template.matchAll(TEMPLATE_TOKEN)) { + const start = m.index ?? 0; + if (start > last) { + parts.push({ kind: 'text', text: template.slice(last, start) }); + } + last = start + m[0].length; + if (m[1] === undefined) { + parts.push({ kind: 'text', text: m[0][0] }); // {{ or }} + continue; + } + const token = parseToken(m[1]); + parts.push(token ? { kind: 'token', ...token } : { kind: 'unknown', text: m[0] }); + } + if (last < template.length) { + parts.push({ kind: 'text', text: template.slice(last) }); + } + return parts; +} + +/** The window a parsed token refers to, or undefined when the account does not + * report it. */ +function tokenWindow(windows: QuotaWindow[], name: string): QuotaWindow | undefined { + if (name === '5h') { + return windows.find((w) => w.kind === 'session'); + } + if (name === 'wk' || name === '7d') { + return windows.find((w) => w.kind === 'weekly_all'); + } + const model = name.slice('model:'.length).trim(); + return windows.find( + (w) => w.kind === 'weekly_scoped' && (w.scopeLabel ?? '').trim().toLowerCase() === model + ); +} + +/** The windows a template actually names, in order, skipping any the account + * does not report. Unlike shownWindows this ignores the 5h-only and + * scoped-weekly toggles: those answer "which windows do you want", and an + * explicit template has already answered it. */ +function templateWindows(windows: QuotaWindow[], template: string): QuotaWindow[] { + const found: QuotaWindow[] = []; + for (const part of parseTemplate(template)) { + if (part.kind !== 'token') { + continue; + } + const w = tokenWindow(windows, part.name); + if (w && !found.includes(w)) { + found.push(w); + } + } + return found; +} + +function templateReset(resetsAt: string, now: number, style?: ResetStyle): string { + if (style !== 'at') { + return compactReset(resetsAt, now, style); + } + const t = Date.parse(resetsAt); + return isNaN(t) ? '' : wallClockReset(new Date(t), now); +} + +/** Renders a template against the live windows. '' when nothing survives, which + * is what hides the status-bar item. */ +function formatQuotaTemplate( + windows: QuotaWindow[], + template: string, + now: number, + resetFormat?: ResetCountdownFormat +): string { + let out = ''; + let sep = ''; // the separator in front of the segment being built + let segment = ''; + let named = 0; // window tokens in this segment... + let reported = 0; // ...and how many of them the account reports + let unknown = false; // ...and whether one of them was a typo + const endSegment = (nextSep: string): void => { + const text = segment + .replace(GAP_RUN, (_m, before: string, after: string) => (before || after ? ' ' : '')) + .trim(); + // Nothing named, or something named and present: print it. A segment that + // named only windows the account lacks goes, unless it also carries a typo, + // which has to stay visible. + if (text && (named === 0 || reported > 0 || unknown)) { + out = out ? out + sep + text : text; + } + sep = nextSep; + segment = ''; + named = 0; + reported = 0; + unknown = false; + }; + + for (const part of parseTemplate(template)) { + if (part.kind === 'text') { + // split() with a capture group alternates text and separator. + part.text.split(SEPARATOR).forEach((piece, i) => { + if (i % 2 === 1) { + endSegment(piece); + } else { + segment += piece; + } + }); + continue; + } + if (part.kind === 'unknown') { + segment += part.text; + unknown = true; + continue; + } + named++; + const w = tokenWindow(windows, part.name); + if (!w) { + segment += GAP; + continue; + } + reported++; + if (part.field === 'pct') { + segment += `${Math.round(w.utilization)}%`; + } else if (part.field === 'label') { + segment += segmentLabel(w); + } else { + // A reset the API left blank closes up the same way a missing window does. + segment += templateReset(w.resetsAt, now, part.style ?? resetFormat) || GAP; + } + } + endSegment(''); + return out; +} + /** * The inner status-bar quota text (no icon prefix). Examples: * default → "5h 6% · wk 1%" * showReset → "5h 6% ↻4.8h | wk 1% ↻1.6d" * fiveHourOnly → "5h 6%" * showScopedWeekly → "5h 6% · wk 1% (fable 12%)" + * A non-empty opts.template replaces all four and is handled above. * Returns '' when there's nothing to show. * * Per-model caps that reset alongside the all-models week are nested into its @@ -223,6 +437,10 @@ function shownWindows(windows: QuotaWindow[] | null, opts: QuotaStatusOptions): */ export function formatQuotaStatusText(windows: QuotaWindow[] | null, opts: QuotaStatusOptions): string { const now = opts.now ?? Date.now(); + const template = (opts.template ?? '').trim(); + if (template) { + return formatQuotaTemplate(windows ?? [], template, now, opts.resetFormat); + } const share = (w: QuotaWindow): string => `${segmentLabel(w)} ${Math.round(w.utilization)}%`; const parts = groupQuotaRows(shownWindows(windows, opts)).map((row) => { let s = share(row.window); @@ -250,7 +468,11 @@ export function formatQuotaStatusText(windows: QuotaWindow[] | null, opts: Quota * excluded on purpose: a red bar with no visible cause reads as a bug. The * tooltip lists every window unconditionally, so the figure stays reachable. */ export function worstShownUtilisation(windows: QuotaWindow[] | null, opts: QuotaStatusOptions): number { - return shownWindows(windows, opts).reduce((worst, w) => Math.max(worst, w.utilization), 0); + const template = (opts.template ?? '').trim(); + const shown = template + ? templateWindows(windows ?? [], template) + : shownWindows(windows, opts); + return shown.reduce((worst, w) => Math.max(worst, w.utilization), 0); } /** How full a bar is, in the three steps the UI paints. */ diff --git a/src/settings.ts b/src/settings.ts index aab0e727..a1fde7d9 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -565,6 +565,17 @@ export const SETTINGS: SettingDef[] = [ enumValues: ['decimal', 'units', 'clock'], enumLabels: ['Decimal (4.8h / 1.6d)', 'Units (4h 48m / 1d 14h)', 'Local time (18:20 / 2026-07-22)'], }, + { + // Explicit status-bar layout replacing the built-in one; grammar in quotaFormat.ts. + key: 'statusBarQuotaFormat', + type: 'string', + default: '', + storage: 'state', + group: 'statusBar', + label: 'Quota: status bar format', + help: 'Empty keeps the built-in layout (5h 6% · wk 1%). Otherwise: {5h.pct}, {wk.pct} (or {7d.pct}) and {model:Fable.pct}, each also taking .reset and .label — e.g. "{5h.pct} | {7d.pct} · {model:Fable.pct}". A window your plan does not report renders empty. .reset can name its own style: {5h.reset:units}, :decimal, :clock or :at (wall clock).', + maxLength: 120, + }, { key: 'workflowQuotaWarnPercent', type: 'number', diff --git a/src/statusBar.ts b/src/statusBar.ts index 2f2503fd..40c3604f 100644 --- a/src/statusBar.ts +++ b/src/statusBar.ts @@ -53,6 +53,7 @@ export class StatusBarManager { private quotaFiveHourOnly: boolean = false; // show only the 5h window private showResetInBar: boolean = false; // append reset countdown to the bar private resetCountdownFormat: ResetCountdownFormat = 'decimal'; // style of that countdown (#74) + private quotaFormat: string = ''; // statusBarQuotaFormat; '' keeps the built-in layout private provider: 'claude' | 'codex' = 'claude'; private lastClaudeUsage: { todayData: UsageData | null; @@ -139,7 +140,8 @@ export class StatusBarManager { showScopedWeekly: boolean = false, quotaFiveHourOnly: boolean = false, showResetInBar: boolean = false, - resetCountdownFormat: ResetCountdownFormat = 'decimal' + resetCountdownFormat: ResetCountdownFormat = 'decimal', + quotaFormat: string = '' ): void { this.showCost = showCost; this.showContext = showContext; @@ -149,6 +151,7 @@ export class StatusBarManager { this.quotaFiveHourOnly = quotaFiveHourOnly; this.showResetInBar = showResetInBar; this.resetCountdownFormat = resetCountdownFormat; + this.quotaFormat = quotaFormat; if (!showContext) { this.contextItem.hide(); } @@ -349,7 +352,9 @@ export class StatusBarManager { showReset: this.showResetInBar, fiveHourOnly: this.quotaFiveHourOnly, showScopedWeekly: this.showScopedWeekly, - resetFormat: this.resetCountdownFormat + resetFormat: this.resetCountdownFormat, + // When set this names the windows itself, so the toggles above no longer apply. + template: this.quotaFormat }; const text = formatQuotaStatusText(live, opts); if (!text) { diff --git a/src/test/quotaFormat.test.ts b/src/test/quotaFormat.test.ts index 58434bab..f7e21866 100644 --- a/src/test/quotaFormat.test.ts +++ b/src/test/quotaFormat.test.ts @@ -6,7 +6,10 @@ import * as assert from 'node:assert/strict'; import { CONTEXT_FILL_THRESHOLDS, + CUSTOM_QUOTA_STATUS_TEMPLATE, + FIVE_HOUR_QUOTA_STATUS_TEMPLATE, QUOTA_FILL_THRESHOLDS, + WEEKLY_QUOTA_STATUS_TEMPLATE, compactReset, fillLevel, formatMonthlyReset, @@ -347,3 +350,213 @@ test('out-of-range fills clamp to the nearest level', () => { assert.equal(fillLevel(-5, QUOTA_FILL_THRESHOLDS), 'normal'); assert.equal(fillLevel(140, QUOTA_FILL_THRESHOLDS), 'error'); }); + + +// --------------------------------------------------------------------------- +// Status-bar format template (statusBarQuotaFormat). +// +// The built-in layout stays the default: the template is empty out of the box +// and everything above keeps passing unchanged. When set, it names the windows +// explicitly, which is why it ignores the 5h-only and scoped-weekly toggles — +// those answer "which windows do you want", and the template already said. +// --------------------------------------------------------------------------- + +const base = { showReset: false, fiveHourOnly: false, showScopedWeekly: false, now: NOW }; + +test('an empty template keeps the built-in layout', () => { + assert.equal(formatQuotaStatusText(live, { ...base, template: '' }), '5h 6% · wk 1%'); + assert.equal(formatQuotaStatusText(live, { ...base, template: ' ' }), '5h 6% · wk 1%'); +}); + +test('dashboard presets render short, named quota windows', () => { + assert.equal(formatQuotaStatusText(live, { ...base, template: FIVE_HOUR_QUOTA_STATUS_TEMPLATE }), '5h 6%'); + assert.equal(formatQuotaStatusText(live, { ...base, template: WEEKLY_QUOTA_STATUS_TEMPLATE }), 'wk 1%'); + assert.equal(formatQuotaStatusText(live, { ...base, template: CUSTOM_QUOTA_STATUS_TEMPLATE }), '5h 6% · wk 1%'); +}); + +test('a template renders the windows it names', () => { + assert.equal(formatQuotaStatusText(live, { ...base, template: '{5h.pct} | {wk.pct}' }), '6% | 1%'); +}); + +test('model tokens match the API scope label, case-insensitively', () => { + assert.equal(formatQuotaStatusText(live, { ...base, template: '{model:Fable.pct}' }), '12%'); + assert.equal(formatQuotaStatusText(live, { ...base, template: '{model:fable.pct}' }), '12%'); + assert.equal(formatQuotaStatusText(live, { ...base, template: '{model:FABLE.pct}' }), '12%'); +}); + +test('a model name containing a dot still parses', () => { + // "Fable 5.1" is a label the API really sends, so the field has to be split + // off at the last dot rather than the first. + const dotted: QuotaWindow[] = [ + { kind: 'weekly_scoped', scopeLabel: 'Fable 5.1', utilization: 7, decimals: 0, resetsAt: at(38.4 * H), isActive: true }, + ]; + assert.equal(formatQuotaStatusText(dotted, { ...base, template: '{model:Fable 5.1.pct}' }), '7%'); + assert.equal(formatQuotaStatusText(dotted, { ...base, template: '{model:Fable 5.1.label}' }), 'fable 5.1'); +}); + +test('7d is an alias for the all-models week', () => { + const a = formatQuotaStatusText(live, { ...base, template: '{7d.pct}' }); + const b = formatQuotaStatusText(live, { ...base, template: '{wk.pct}' }); + assert.equal(a, '1%'); + assert.equal(a, b); +}); + +test('reset tokens follow the reset countdown format', () => { + assert.equal(formatQuotaStatusText(live, { ...base, template: '{5h.reset}' }), '4.8h'); + assert.equal( + formatQuotaStatusText(live, { ...base, template: '{5h.reset}', resetFormat: 'units' }), + '4h 48m' + ); + assert.equal(formatQuotaStatusText(live, { ...base, template: '{wk.reset}' }), '1.6d'); +}); + +test('a clock reset keeps its date intact', () => { + // '-' is a separator, but only in the template's own text: the hyphens of a + // rendered "2023-11-16" must not be taken for one. NOW is local noon, so the + // +38.4h weekly reset lands on the 16th in every timezone. + assert.equal( + formatQuotaStatusText(live, { ...base, template: '{wk.reset}', resetFormat: 'clock' }), + '2023-11-16' + ); + assert.equal( + formatQuotaStatusText(live, { ...base, template: '{5h.pct} | {wk.reset}', resetFormat: 'clock' }), + '6% | 2023-11-16' + ); +}); + +test('a reset token can name its own style, whatever the global one is', () => { + // The global format is one setting for the whole bar; a style on the token is + // what lets a countdown for the 5h window sit beside a wall clock for the week. + const t = '{5h.reset:units} | {wk.reset:decimal} | {5h.reset:clock} | {5h.reset}'; + assert.equal(formatQuotaStatusText(live, { ...base, template: t }), '4h 48m | 1.6d | 16:48 | 4.8h'); + assert.equal( + formatQuotaStatusText(live, { ...base, template: t, resetFormat: 'clock' }), + '4h 48m | 1.6d | 16:48 | 16:48' + ); +}); + +test('reset:at is the wall clock, with a weekday once it is not today', () => { + assert.equal(formatQuotaStatusText(live, { ...base, template: '{5h.reset:at}' }), '16:48'); + assert.match(formatQuotaStatusText(live, { ...base, template: '{wk.reset:at}' }), /^.+ 02:24$/); + assert.match( + formatQuotaStatusText(live, { ...base, template: '{model:Fable 5.1.reset:at}' }), + /^$/ // no such cap on this account: the dotted name still parses, and drops + ); +}); + +test('an unknown reset style, or a style on another field, is a visible typo', () => { + assert.equal(formatQuotaStatusText(live, { ...base, template: '{5h.reset:hours}' }), '{5h.reset:hours}'); + assert.equal(formatQuotaStatusText(live, { ...base, template: '{5h.pct:units}' }), '{5h.pct:units}'); +}); + +test('a reset the API left blank closes up like a missing window', () => { + const fresh: QuotaWindow[] = [{ kind: 'session', utilization: 0, decimals: 0, resetsAt: '', isActive: false }]; + assert.equal(formatQuotaStatusText(fresh, { ...base, template: '{5h.pct} {5h.reset} left' }), '0% left'); +}); + +test('label tokens give the same short names the built-in layout uses', () => { + assert.equal( + formatQuotaStatusText(live, { ...base, template: '{5h.label} {5h.pct} · {wk.label} {wk.pct}' }), + '5h 6% · wk 1%' + ); + assert.equal(formatQuotaStatusText(live, { ...base, template: '{model:Fable.label}' }), 'fable'); +}); + +test('a window the account does not report renders empty, and takes its dangling separator with it', () => { + // The case that matters: a plan with no per-model cap, or a signed-out weekly. + assert.equal(formatQuotaStatusText(live, { ...base, template: '{5h.pct} · {model:Opus.pct}' }), '6%'); + assert.equal(formatQuotaStatusText(live, { ...base, template: '{model:Opus.pct} · {5h.pct}' }), '6%'); + assert.equal( + formatQuotaStatusText(live, { ...base, template: '{5h.pct} | {model:Opus.pct} | {wk.pct}' }), + '6% | 1%' + ); +}); + +test('separators come out exactly as typed', () => { + assert.equal(formatQuotaStatusText(live, { ...base, template: '{5h.pct}/{wk.pct}' }), '6%/1%'); + assert.equal(formatQuotaStatusText(live, { ...base, template: '{5h.pct}, {wk.pct}' }), '6%, 1%'); + assert.equal(formatQuotaStatusText(live, { ...base, template: '{5h.pct} - {model:Opus.pct}' }), '6%'); +}); + +test('literal text goes with the window it describes', () => { + // Without this a plan with no Opus cap would be left showing a bare "opus". + assert.equal(formatQuotaStatusText(live, { ...base, template: '{5h.pct} · opus {model:Opus.pct}' }), '6%'); + assert.equal( + formatQuotaStatusText(live, { ...base, template: 'F {model:Fable.pct} · O {model:Opus.pct}' }), + 'F 12%' + ); + // Text with no token of its own is never dropped. + assert.equal(formatQuotaStatusText(live, { ...base, template: 'Claude | {5h.pct}' }), 'Claude | 6%'); +}); + +test('braces are escaped by doubling', () => { + assert.equal(formatQuotaStatusText(live, { ...base, template: '{{5h.pct}} {5h.pct}' }), '{5h.pct} 6%'); +}); + +test('an unrecognised token is left verbatim so a typo stays visible', () => { + assert.equal(formatQuotaStatusText(live, { ...base, template: '{5h.pcnt}' }), '{5h.pcnt}'); + assert.equal(formatQuotaStatusText(live, { ...base, template: '{nope.pct}' }), '{nope.pct}'); +}); + +test('literal spacing is kept as typed', () => { + assert.equal(formatQuotaStatusText(live, { ...base, template: 'A B {5h.pct}' }), 'A B 6%'); + assert.equal(formatQuotaStatusText(live, { ...base, template: '{5h.pct} {wk.pct}' }), '6% 1%'); +}); + +test('a vanished token closes its own gap without disturbing the rest', () => { + // The space either side of the missing cap collapses to one, so the line does + // not show where something used to be. + assert.equal( + formatQuotaStatusText(live, { ...base, template: '{5h.pct} {model:Opus.pct} {wk.pct}' }), + '6% 1%' + ); + assert.equal( + formatQuotaStatusText(live, { ...base, template: '{5h.pct}{model:Opus.pct}{wk.pct}' }), + '6%1%' + ); +}); + +test('an unrecognised token survives even beside a window the account lacks', () => { + // Dropping the segment would take the typo with it, and a silently shorter + // line is exactly what the verbatim rule exists to prevent. + assert.equal( + formatQuotaStatusText(live, { ...base, template: '{nope.pct} {model:Opus.pct}' }), + '{nope.pct}' + ); + assert.equal( + formatQuotaStatusText(live, { ...base, template: '{5h.pct} | {nope.pct} {model:Opus.pct}' }), + '6% | {nope.pct}' + ); +}); + +test('a template that renders to nothing returns empty so the item hides', () => { + assert.equal(formatQuotaStatusText(live, { ...base, template: '{model:Opus.pct}' }), ''); + assert.equal(formatQuotaStatusText(live, { ...base, template: '{model:Opus.pct} | ' }), ''); +}); + +test('the template selects windows itself, ignoring the 5h-only and scoped-weekly toggles', () => { + // fiveHourOnly would hide the week, and showScopedWeekly gates the model cap; + // naming them in the template is the more specific instruction. + assert.equal( + formatQuotaStatusText(live, { ...base, fiveHourOnly: true, template: '{wk.pct}' }), + '1%' + ); + assert.equal( + formatQuotaStatusText(live, { ...base, showScopedWeekly: false, template: '{model:Fable.pct}' }), + '12%' + ); +}); + +test('the warning colour follows the windows the template actually shows', () => { + // Same reasoning as the built-in layout: a red bar with no visible cause + // reads as a bug. A template showing only the quiet 5h window must not turn + // red because of a loud weekly cap it never prints. + const loud: QuotaWindow[] = [ + { kind: 'session', utilization: 6, decimals: 0, resetsAt: at(4.8 * H), isActive: false }, + { kind: 'weekly_all', utilization: 95, decimals: 0, resetsAt: at(38.4 * H), isActive: true }, + ]; + assert.equal(worstShownUtilisation(loud, { ...base, template: '{5h.pct}' }), 6); + assert.equal(worstShownUtilisation(loud, { ...base, template: '{5h.pct} | {wk.pct}' }), 95); + // No template: unchanged, both windows are shown. + assert.equal(worstShownUtilisation(loud, base), 95); +}); diff --git a/src/test/repositoryPolicy.test.ts b/src/test/repositoryPolicy.test.ts index 30e35cf1..17992ac0 100644 --- a/src/test/repositoryPolicy.test.ts +++ b/src/test/repositoryPolicy.test.ts @@ -1549,18 +1549,24 @@ test('changelog records the V2.2.2 energy patch after the released V2.2.1 baseli assert.doesNotMatch(changelog, /^## \[2\.2\.[01]\] — Unreleased$/m); }); -test('changelog preserves v2.3 release order and keeps named candidate fixes out of v2.3.2', () => { +test('changelog separates unreleased changes from published v2.3.3 and v2.3.2 fixes', () => { const changelog = repoFile('CHANGELOG.md'); - assert.match(changelog, /^## \[2\.3\.3\] — Unreleased$/m); + assert.match(changelog, /^## \[Unreleased\]$/m); + assert.match(changelog, /^## \[2\.3\.3\] — 2026-09-16$/m); assert.match(changelog, /^## \[2\.3\.2\] — 2026-09-12$/m); assert.match(changelog, /^## \[2\.3\.1\] — 2026-09-08$/m); assert.match(changelog, /^## \[2\.3\.0\] — 2026-08-28$/m); - const candidateStart = changelog.indexOf('## [2.3.3]'); + const unreleasedStart = changelog.indexOf('## [Unreleased]'); + const publishedStart = changelog.indexOf('## [2.3.3]'); const releasedStart = changelog.indexOf('## [2.3.2]'); const previousReleaseStart = changelog.indexOf('## [2.3.1]'); - const candidateSection = changelog.slice(candidateStart, releasedStart); + const unreleasedSection = changelog.slice(unreleasedStart, publishedStart); + const publishedSection = changelog.slice(publishedStart, releasedStart); const releasedSection = changelog.slice(releasedStart, previousReleaseStart); + assert.match(unreleasedSection, /statusBarQuotaFormat/); + assert.match(unreleasedSection, /High CPU during indexing \(#99\)/); + assert.doesNotMatch(publishedSection, /statusBarQuotaFormat/); for (const candidateFix of [ 'Smoother live Webview refreshes', 'Stable unchanged Compare refreshes', @@ -1571,7 +1577,7 @@ test('changelog preserves v2.3 release order and keeps named candidate fixes out 'Bounded credentials-watcher recovery', 'Exact bounded Claude content analysis', ]) { - assert.match(candidateSection, new RegExp(candidateFix)); + assert.match(publishedSection, new RegExp(candidateFix)); assert.doesNotMatch(releasedSection, new RegExp(candidateFix)); } }); diff --git a/src/test/statusBarThresholds.test.ts b/src/test/statusBarThresholds.test.ts index a6dd3341..a5c3bdc3 100644 --- a/src/test/statusBarThresholds.test.ts +++ b/src/test/statusBarThresholds.test.ts @@ -318,3 +318,46 @@ test('Codex quota tooltip escapes provider labels', () => { assert.match(tooltip.value, /<unsafe & label>/); assert.doesNotMatch(tooltip.value, / { + // quotaFormat.test.ts pins the rendering; this pins the half you would + // otherwise only see in the Extension Development Host — that the template + // argument setVisibility receives is what the bar ends up drawing. + const manager = bareStatusBar(); + manager.statusBarItem = statusItem(); + manager.quotaItem = statusItem(); + manager.contextItem = statusItem(); + manager.provider = 'claude'; + const resetsAt = new Date(Date.now() + 24 * 60 * 60 * 1_000).toISOString(); + const quota = { + five_hour: { utilization: 6, resets_at: resetsAt }, + seven_day: { utilization: 1, resets_at: resetsAt }, + limits: [ + { kind: 'session', group: 'session', percent: 6, resets_at: resetsAt, scope: null, is_active: true }, + { kind: 'weekly_all', group: 'weekly', percent: 1, resets_at: resetsAt, scope: null, is_active: true }, + { + kind: 'weekly_scoped', + group: 'weekly', + percent: 92, + resets_at: resetsAt, + scope: { model: { id: null, display_name: 'Fable' } }, + is_active: true, + }, + ], + }; + + // Empty template, scoped weeklies off: the built-in layout, and the loud + // per-model cap it does not draw must not colour the item either. + manager.setVisibility(true, true, true, 'cost', false, false, false, 'decimal', ''); + manager.updateQuota(quota); + assert.equal(manager.quotaItem.text, '$(dashboard) 5h 6% · wk 1%'); + assert.equal(manager.quotaItem.backgroundColor, undefined); + + // Same data, same toggles: naming that cap in a template both shows it and + // hands it the colour. + manager.setVisibility(true, true, true, 'cost', false, false, false, 'decimal', '{5h.pct} | {7d.pct} · {model:Fable.pct}'); + manager.updateQuota(quota); + assert.equal(manager.quotaItem.text, '$(dashboard) 6% | 1% · 92%'); + assert.equal(manager.quotaItem.backgroundColor?.id, 'statusBarItem.errorBackground'); + assert.equal(manager.quotaItem.visible, true); +}); diff --git a/src/types.ts b/src/types.ts index b9ce531a..c7a2e8a1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -337,6 +337,8 @@ export interface ExtensionConfig { quotaFiveHourOnly: boolean; // Format of the reset countdown appended to the quota item (issue #74). resetCountdownFormat: 'decimal' | 'units' | 'clock'; + // Explicit status-bar layout that replaces the built-in one. '' = built-in. + statusBarQuotaFormat: string; // Fetch real 5-hour / weekly limit utilisation via Claude Code's OAuth session. usageLimitTracking: boolean; // LLM "usage advice" feature (OpenAI-compatible endpoint, e.g. DeepSeek). diff --git a/src/webview.ts b/src/webview.ts index 288206dd..b233f430 100644 --- a/src/webview.ts +++ b/src/webview.ts @@ -34,6 +34,11 @@ import { parseConversation } from './conversationLog'; import { renderConversationViewer } from './conversationViewerHtml'; import { formatUsageDate, shortUsageDate } from './usageDateLabels'; import { normalizeQuotaWindows } from './quotaWindows'; +import { + CUSTOM_QUOTA_STATUS_TEMPLATE, + FIVE_HOUR_QUOTA_STATUS_TEMPLATE, + WEEKLY_QUOTA_STATUS_TEMPLATE, +} from './quotaFormat'; import { buildWeeklyValueTimeline, claudeWeeklyEquivalentUsage, @@ -3383,7 +3388,44 @@ export class UsageWebviewProvider { /** One row in the settings panel: label + help + the right input control. The * label / help follow the plugin language (I18n.settingText), falling back to * the catalog English for the English UI. */ + private renderQuotaFormatSettingRow(it: SettingView): string { + const esc = (value: string): string => this.escapeHtml(value); + const copy = I18n.t.popup; + const tr = I18n.settingText(it.key); + const label = tr.label ?? it.label; + const detailedHelp = tr.help ?? it.help ?? ''; + const id = 'set_statusBarQuotaFormat'; + const presetId = `${id}_preset`; + const raw = String(it.value); + const selected = raw === '' || raw === FIVE_HOUR_QUOTA_STATUS_TEMPLATE || + raw === WEEKLY_QUOTA_STATUS_TEMPLATE ? raw : 'custom'; + const option = (value: string, text: string): string => + ''; + const options = option('', copy.quotaFormatBuiltIn) + + option(FIVE_HOUR_QUOTA_STATUS_TEMPLATE, copy.quotaFormatFiveHour) + + option(WEEKLY_QUOTA_STATUS_TEMPLATE, copy.quotaFormatWeekly) + + option('custom', copy.quotaFormatCustom); + const custom = selected === 'custom'; + return '
' + esc(copy.quotaFormatShortHelp) + '
' + + '
' + + '
' + + '' + + '
' + esc(detailedHelp) + '
' + + '
'; + } + private renderSettingRow(it: SettingView): string { + if (it.key === 'statusBarQuotaFormat') { + return this.renderQuotaFormatSettingRow(it); + } const esc = (s: string): string => this.escapeHtml(s); const tr = I18n.settingText(it.key); const label = tr.label ?? it.label; @@ -8366,6 +8408,9 @@ export class UsageWebviewProvider { } .set-control input[type="number"] { min-width: 96px; width: 96px; } .set-control textarea { resize: vertical; min-width: 260px; } + .quota-format-control { flex-direction: column; align-items: stretch; gap: 6px; } + .quota-format-custom[hidden] { display: none; } + .quota-format-custom input { width: 100%; box-sizing: border-box; } .set-switch { position: relative; display: inline-block; width: 40px; height: 22px; } .set-switch input { opacity: 0; width: 0; height: 0; } .set-slider { @@ -11469,6 +11514,30 @@ function setSetting(key, value, type) { vscode.postMessage({ command: 'updateSetting', key: key, value: value }); } +function setQuotaFormatPreset(select) { + var input = document.getElementById('set_statusBarQuotaFormat'); + var custom = document.getElementById('set_statusBarQuotaFormat_custom'); + if (!input || !custom) { return; } + var isCustom = select.value === 'custom'; + custom.hidden = !isCustom; + if (isCustom) { + if (!input.value) { input.value = select.dataset.defaultTemplate || ''; } + setSetting('statusBarQuotaFormat', input.value, 'string'); + input.focus(); + } else { + setSetting('statusBarQuotaFormat', select.value, 'string'); + } +} + +function setQuotaFormatCustom(input) { + var preset = document.getElementById('set_statusBarQuotaFormat_preset'); + // Leaving the input can fire its change event after a preset was selected. + // Do not let a stale custom draft overwrite that deliberate choice. + if (preset && preset.value === 'custom') { + setSetting('statusBarQuotaFormat', input.value, 'string'); + } +} + function resetAllSettings(keys) { vscode.postMessage({ command: 'resetAllSettings', keys: Array.isArray(keys) ? keys : undefined }); } diff --git a/tests/ui/claude-settings.spec.mjs b/tests/ui/claude-settings.spec.mjs index 9c52c760..37cd572b 100644 --- a/tests/ui/claude-settings.spec.mjs +++ b/tests/ui/claude-settings.spec.mjs @@ -30,3 +30,58 @@ test('Claude pricing backend dropdown posts the selected Bedrock backend', async value: 'aws-bedrock-in-region', }); }); + +test('quota format offers compact presets and reveals template syntax only for custom layouts', async ({ page }) => { + await openClaude(page); + await page.locator('#tab-settings').click(); + + const preset = page.locator('#set_statusBarQuotaFormat_preset'); + const custom = page.locator('#set_statusBarQuotaFormat'); + await expect(preset).toBeVisible(); + await expect(preset).toHaveValue(''); + await expect(custom).toBeHidden(); + await expect(preset.locator('option')).toHaveCount(4); + + await preset.selectOption('{wk.label} {wk.pct}'); + await expect.poll(() => page.evaluate(() => window.__ccuPostedMessages.at(-1))).toEqual({ + command: 'updateSetting', + key: 'statusBarQuotaFormat', + value: '{wk.label} {wk.pct}', + }); + await expect(custom).toBeHidden(); + + await preset.selectOption('custom'); + await expect(custom).toBeVisible(); + await expect(custom).toHaveAttribute('maxlength', '120'); + await custom.fill('{5h.pct} | {wk.pct}'); + await custom.dispatchEvent('change'); + await expect.poll(() => page.evaluate(() => window.__ccuPostedMessages.at(-1))).toEqual({ + command: 'updateSetting', + key: 'statusBarQuotaFormat', + value: '{5h.pct} | {wk.pct}', + }); + + await preset.selectOption(''); + await expect(custom).toBeHidden(); + await expect.poll(() => page.evaluate(() => window.__ccuPostedMessages.at(-1))).toEqual({ + command: 'updateSetting', + key: 'statusBarQuotaFormat', + value: '', + }); +}); + +for (const locale of ['en', 'de-DE', 'zh-TW', 'zh-CN', 'ja', 'ko', 'pt-BR', 'id']) { + test(`${locale} quota format remains usable at narrow width`, async ({ page }) => { + await openClaude(page, { locale, width: 360, height: 800 }); + await page.locator('#tab-settings').click(); + const preset = page.locator('#set_statusBarQuotaFormat_preset'); + await expect(preset).toBeVisible(); + await preset.selectOption('custom'); + await expect(page.locator('#set_statusBarQuotaFormat')).toBeVisible(); + const bounds = await page.locator('.settings-panel').evaluate((element) => ({ + client: element.clientWidth, + scroll: element.scrollWidth, + })); + expect(bounds.scroll).toBeLessThanOrEqual(bounds.client + 1); + }); +} From 097c45fdbc89ebffa85709ae3035b3109e89954c Mon Sep 17 00:00:00 2001 From: Roman Syuzyov Date: Wed, 23 Sep 2026 07:48:37 +0500 Subject: [PATCH 07/10] perf: window drift no longer forces a full rebuild (#99) (#113) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On a long history the rolling window is always drifting past something: some file's oldest event has just left it, so the file arrives as a 'cutoff' rebuild, or its stored aggregate is rebased in place. The analysis fast path demanded "appends and nothing else", so either case rejected it and every refresh re-read the whole corpus. Measured on a 650-file, 1.4 GB history with several agents running: 136 of 140 refreshes re-read every body to serve one appended file — around 60 s of parse time per refresh, roughly once a minute, which is what pegged the extension host. Neither case invalidates the other files. A 'cutoff' rebuild is assigned only when the body on disk is unchanged, so the file owns exactly the UUIDs it owned before and cannot preempt a later owner; a rebase only narrows an aggregate that is already stored, and the UUIDs it drops are handed to the ownership pass, which re-plans precisely those files that could claim them. What does invalidate every stored contribution is a changed analysis context — no prior runtime, a different time zone, a different window length — and that still forces the rebuild, as do deletions, moves and a cutoff moving backward. The guards that matter keep their tests: a rebuilt earlier file preempting a later UUID owner, and an append that gives a zero-timestamp file its first timestamp, both still fall back to the full rebuild. --- src/claudeIncrementalIndex.ts | 18 +++++-- src/test/claudeIncrementalIndex.test.ts | 64 +++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 3 deletions(-) diff --git a/src/claudeIncrementalIndex.ts b/src/claudeIncrementalIndex.ts index cead9ef8..e4d712fe 100644 --- a/src/claudeIncrementalIndex.ts +++ b/src/claudeIncrementalIndex.ts @@ -2601,12 +2601,24 @@ export async function updateClaudeUsageIndex( plan.kind === 'append' && Boolean(plan.prior) && (plan.prior?.firstTimestampMs ?? 0) > 0; const isNewFile = (plan: FilePlan): boolean => plan.kind === 'rebuild' && !plan.prior && !plan.replacedFileId; - const appendOnlyPlans = plans.length > 0 && sourcePlans.length === plans.length && - plans.every((plan) => isTailAppend(plan) || isNewFile(plan)); + // 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 rebase is the same story without the read: it only narrows a stored + // aggregate, and the UUIDs it drops are handed to the ownership 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. + const isWindowRebuild = (plan: FilePlan): boolean => plan.analysisReason === 'cutoff'; + const appendOnlyPlans = plans.length > 0 && + plans.every((plan) => isTailAppend(plan) || isNewFile(plan) || isWindowRebuild(plan)); const safeTailAppend = Boolean( appendOnlyPlans && previousAnalysisRuntime && !timeZoneChanged && - previous.windowDays === windowDays && analysisPayloadRebases.size === 0, + previous.windowDays === windowDays, ); const priorAnalysisCutoffMs = previousAnalysisRuntime?.cutoffMs ?? (previous.analyzeContent ? previous.analysisCutoffMs : undefined); diff --git a/src/test/claudeIncrementalIndex.test.ts b/src/test/claudeIncrementalIndex.test.ts index a4fae523..d4d9e6fa 100644 --- a/src/test/claudeIncrementalIndex.test.ts +++ b/src/test/claudeIncrementalIndex.test.ts @@ -507,6 +507,70 @@ test('a newly started session file does not rebuild the established corpus', asy } }); +test('the window drifting past an old event keeps the refresh incremental', async (t) => { + const previousNow = Date.now; + let now = Date.parse('2026-09-10T12:00:00.000Z'); + Date.now = () => now; + const root = await mkdtemp(path.join(os.tmpdir(), 'ccu-claude-window-drift-')); + roots.push(root); + const project = path.join(root, 'projects', '-fixture-window-drift'); + await mkdir(project, { recursive: true }); + const files: string[] = []; + try { + // One file holds an event that is about to fall out of the window, the rest + // are an established corpus that nothing touches. + for (let index = 0; index < 32; index += 1) { + const file = path.join(project, `session-${String(index).padStart(3, '0')}.jsonl`); + files.push(file); + await writeFile(file, `${analysisTextLine( + `drift-seed-${index}`, + `seed ${index}`, + new Date(Date.parse('2026-09-10T00:00:00.000Z') + index * 1_000).toISOString(), + )} +`, 'utf8'); + } + // Only this file holds an event old enough to leave the window when the + // clock moves below; every other file stays entirely inside it. + await appendFile(files[3], `${analysisTextLine( + 'drift-expiring', 'about to expire', '2026-09-09T13:00:00.000Z', + )} +`, 'utf8'); + + const cold = await updateClaudeUsageIndex(createClaudeUsageIndex(), root, { windowDays: 1 }); + assert.equal(cold.diagnostics.bodyReads, 32); + + // The clock moves past the oldest event of files[3]: it now needs a body + // read to recompute its aggregate ('cutoff'), while another session simply + // appends. Before this path existed, that pair forced every body to be + // re-read — the ordinary case on a long history, where the window is always + // drifting past something. + now = Date.parse('2026-09-10T14:00:00.000Z'); + await appendFile(files[20], `${analysisTextLine( + 'drift-tail', 'tail', '2026-09-10T13:55:00.000Z', + )} +`, 'utf8'); + + const warm = await updateClaudeUsageIndex(cold.index, root, { windowDays: 1 }); + const full = await ClaudeDataLoader.loadUsageRecords(root, { + analyzeContent: true, + windowDays: 1, + }); + + if (warm.diagnostics.changed.move > 0) { + t.skip('the filesystem reused a file identity, so this refresh is a move, not an addition'); + return; + } + + assert.ok( + warm.diagnostics.bodyReads < 32, + `window drift re-read the whole corpus: ${warm.diagnostics.bodyReads} bodies`, + ); + assert.deepEqual(warm.contentAnalysis, full.contentAnalysis); + } finally { + Date.now = previousNow; + } +}); + test('a new file carrying an already-owned UUID falls back to the full rebuild', async () => { const previousNow = Date.now; Date.now = () => Date.parse('2026-09-10T12:00:00.000Z'); From 02f9ed8403822c444a7078078e82ed5a8b92299c Mon Sep 17 00:00:00 2001 From: jackieyangjq <134819426+jackieyangjq@users.noreply.github.com> Date: Wed, 23 Sep 2026 10:48:52 +0100 Subject: [PATCH 08/10] perf: memoise the dashboard date-label formatter (#99) (#115) formatUsageDate called toLocaleDateString once per table row and chart point, constructing a fresh Intl.DateTimeFormat each time. That is the same per-call cost #109 removed from dateKeys, and it showed up at 3-8% self time in extension-host profiles captured while the dashboard was rendering. Formatters are now cached per locale and options. When no date field is requested, the numeric year/month/day default that toLocaleDateString applies is added explicitly, so output stays identical. A new test compares the result against toLocaleDateString for all eight UI locales across several option shapes, including the cached second call. Co-authored-by: Claude Opus 5.5 --- CHANGELOG.md | 5 +++++ src/test/usageDateLabels.test.ts | 36 ++++++++++++++++++++++++++++++++ src/usageDateLabels.ts | 31 ++++++++++++++++++++++++++- 3 files changed, 71 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 809252ca..ff0103ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,11 @@ upstream release: 1.0.8). Format follows [Keep a Changelog](https://keepachangel 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. ## [2.3.3] — 2026-09-16 diff --git a/src/test/usageDateLabels.test.ts b/src/test/usageDateLabels.test.ts index 36f9b213..af1eadf0 100644 --- a/src/test/usageDateLabels.test.ts +++ b/src/test/usageDateLabels.test.ts @@ -25,3 +25,39 @@ test('date keys are rendered verbatim instead of shifting in behind-UTC zones', }; assert.equal(formatUsageDate('2026-06-01', 'en-US', options, true), 'June 2026'); }); + +test('memoised labels match toLocaleDateString for every UI locale and option shape', () => { + const locales = ['en', 'de-DE', 'zh-TW', 'zh-CN', 'ja', 'ko', 'pt-BR', 'id']; + const dailyOptions: Intl.DateTimeFormatOptions[] = [ + {}, + { timeZone: 'America/New_York' }, + { timeZone: 'Asia/Shanghai' }, + { year: 'numeric', month: 'numeric', day: 'numeric' }, + { month: 'short', day: 'numeric', weekday: 'short' }, + { dateStyle: 'medium' }, + { hour: '2-digit' }, + ]; + const keys = ['2026-01-01', '2026-02-28', '2026-07-01', '2026-12-31']; + for (const locale of locales) { + for (const options of dailyOptions) { + for (const key of keys) { + const [year, month, day] = key.split('-').map(Number); + const date = new Date(Date.UTC(year, month - 1, day, 12)); + // Twice, so the second call is served by the cached formatter. + for (let i = 0; i < 2; i++) { + assert.equal( + formatUsageDate(key, locale, options), + date.toLocaleDateString(locale, { ...options, timeZone: 'UTC' }), + `${locale} ${JSON.stringify(options)} ${key}` + ); + assert.equal( + formatUsageDate(key, locale, options, true), + new Date(Date.UTC(year, month - 1, 1, 12)) + .toLocaleDateString(locale, { year: 'numeric', month: 'long', timeZone: 'UTC' }), + `${locale} monthly ${key}` + ); + } + } + } + } +}); diff --git a/src/usageDateLabels.ts b/src/usageDateLabels.ts index 611f54ea..f0e484d7 100644 --- a/src/usageDateLabels.ts +++ b/src/usageDateLabels.ts @@ -20,5 +20,34 @@ export function formatUsageDate( const options: Intl.DateTimeFormatOptions = monthly ? { year: 'numeric', month: 'long' } : dailyOptions; - return date.toLocaleDateString(locale, { ...options, timeZone: 'UTC' }); + return usageDateFormatter(locale, { ...options, timeZone: 'UTC' }).format(date); +} + +// The dashboard formats one label per table row and chart point, and +// toLocaleDateString constructs a fresh Intl.DateTimeFormat on every call (the +// same cost #99 removed from dateKeys). Formatters are memoised per locale and +// options; the dashboard only ever asks for a handful of combinations. +const usageDateFormatters = new Map(); + +function usageDateFormatter( + locale: string, + options: Intl.DateTimeFormatOptions, +): Intl.DateTimeFormat { + const key = `${locale}|${JSON.stringify(options)}`; + const cached = usageDateFormatters.get(key); + if (cached) { + return cached; + } + // toLocaleDateString always renders a date: with no date field requested it + // defaults to numeric year, month and day. Intl.DateTimeFormat only does so + // when no time field is requested either, so apply the default explicitly. + const hasDateField = options.weekday !== undefined || options.year !== undefined || + options.month !== undefined || options.day !== undefined || + options.dateStyle !== undefined; + const formatter = new Intl.DateTimeFormat( + locale, + hasDateField ? options : { ...options, year: 'numeric', month: 'numeric', day: 'numeric' }, + ); + usageDateFormatters.set(key, formatter); + return formatter; } From 7df7a35c25b70f2a981e0e202ac955c752c73332 Mon Sep 17 00:00:00 2001 From: Roman Syuzyov Date: Wed, 23 Sep 2026 14:54:20 +0500 Subject: [PATCH 09/10] perf: an appended file leaving the window stays incremental (#99) (#114) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * perf: an appended file leaving the window stays incremental (#99) Follow-up to the previous window-drift fix, from the same measurement: full rebuilds became rare but did not stop, and the remaining ones each cost 50-120 s of parse time. The remaining cause is the file that is appended to in the very refresh where one of its own events leaves the window. Its stored aggregate still counts the expired event, so a tail read cannot serve it and the file has to be re-read in full. Until now that made it indistinguishable from a mid-file edit — it was planned as 'source', which rejects the analysis fast path and re-reads every body. On a machine with several agents the pairing is routine: sessions append constantly, and on a long history the window is always drifting past something. Such a file only grew. It keeps every UUID it owned and can gain only new ones, which is exactly the shape of a plain append, so it is planned as 'window' and allowed on the fast path — and, like an append, it is put through the ownership probe after parsing: if one of its lines carries a UUID owned by a later file, the full rebuild still happens. A genuine mid-file edit remains 'source', since the append prefix and boundary checks are what tell the two apart. The same reason covers the second shape of the pairing, where the cheap rebase succeeds but changes the payload: that append was already being downgraded to a rebuild, and it is a window rebuild too, not a source edit. Test: `a file that is appended to while losing an event to the window stays incremental` — it fails on the parent commit with "re-read the whole corpus: 8 bodies" and asserts the result equals what the full loader produces. Co-Authored-By: Claude Opus 5 (1M context) * fix: preserve UUID ownership across mixed window updates Parse changed Claude files in canonical order when an append and a window rebuild happen together. The new regression fails before this fix and matches the full loader afterward. Co-authored-by: OpenAI Codex <215057067+openai-codex[bot]@users.noreply.github.com> --------- Co-authored-by: Claude Opus 5 (1M context) Co-authored-by: Carl <74130028+Carl723000@users.noreply.github.com> Co-authored-by: OpenAI Codex <215057067+openai-codex[bot]@users.noreply.github.com> --- src/claudeIncrementalIndex.ts | 49 ++++++++++--- src/test/claudeIncrementalIndex.test.ts | 95 +++++++++++++++++++++++++ 2 files changed, 134 insertions(+), 10 deletions(-) diff --git a/src/claudeIncrementalIndex.ts b/src/claudeIncrementalIndex.ts index e4d712fe..6388947a 100644 --- a/src/claudeIncrementalIndex.ts +++ b/src/claudeIncrementalIndex.ts @@ -302,7 +302,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; @@ -2495,12 +2495,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 +2529,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, @@ -2612,7 +2628,11 @@ export async function updateClaudeUsageIndex( // 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. - const isWindowRebuild = (plan: FilePlan): boolean => plan.analysisReason === 'cutoff'; + // '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( @@ -2732,11 +2752,10 @@ export async function updateClaudeUsageIndex( } // 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 appends must be parsed in full-scan order — otherwise - // ownership falls to whichever file happened to be discovered first. + // 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; return (left.orderTimestampMs ?? 0) - (right.orderTimestampMs ?? 0) || left.entry.discoveryIndex - right.entry.discoveryIndex; }); @@ -2765,11 +2784,21 @@ 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 > 0) { + // + // 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; - if (appended.kind === 'rebuild') { + 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) { diff --git a/src/test/claudeIncrementalIndex.test.ts b/src/test/claudeIncrementalIndex.test.ts index d4d9e6fa..d98b974c 100644 --- a/src/test/claudeIncrementalIndex.test.ts +++ b/src/test/claudeIncrementalIndex.test.ts @@ -571,6 +571,101 @@ test('the window drifting past an old event keeps the refresh incremental', asyn } }); +test('a file that is appended to while losing an event to the window stays incremental', async (t) => { + const previousNow = Date.now; + let now = Date.parse('2026-09-10T12:00:00.000Z'); + Date.now = () => now; + const root = await mkdtemp(path.join(os.tmpdir(), 'ccu-claude-window-append-')); + roots.push(root); + const project = path.join(root, 'projects', '-fixture-window-append'); + await mkdir(project, { recursive: true }); + const files: string[] = []; + try { + for (let index = 0; index < 8; index += 1) { + const file = path.join(project, `session-${String(index).padStart(3, '0')}.jsonl`); + files.push(file); + await writeFile(file, `${analysisTextLine( + `window-append-seed-${index}`, + `seed ${index}`, + new Date(Date.parse('2026-09-10T00:00:00.000Z') + index * 1_000).toISOString(), + )} +`, 'utf8'); + } + // This file alone holds an event old enough to leave the window below. + await appendFile(files[2], `${analysisTextLine( + 'window-append-expiring', 'about to expire', '2026-09-09T13:00:00.000Z', + )} +`, 'utf8'); + + const cold = await updateClaudeUsageIndex(createClaudeUsageIndex(), root, { windowDays: 1 }); + assert.equal(cold.diagnostics.bodyReads, 8); + + // The same file is appended to in the refresh where its oldest event leaves + // the window. Its stored aggregate still counts the expired event, so the + // tail cannot simply be added to it and the file is re-read in full — but + // the body only grew, and re-reading one file is no reason to re-read 32. + now = Date.parse('2026-09-10T14:00:00.000Z'); + await appendFile(files[2], `${analysisTextLine( + 'window-append-tail', 'tail', '2026-09-10T13:55:00.000Z', + )} +`, 'utf8'); + + const warm = await updateClaudeUsageIndex(cold.index, root, { windowDays: 1 }); + const full = await ClaudeDataLoader.loadUsageRecords(root, { + analyzeContent: true, + windowDays: 1, + }); + + assert.ok( + warm.diagnostics.bodyReads < 8, + `an appended file losing an event re-read the whole corpus: ${warm.diagnostics.bodyReads} bodies, ` + + `changed=${JSON.stringify(warm.diagnostics.changed)}`, + ); + assert.deepEqual(warm.contentAnalysis, full.contentAnalysis); + } finally { + Date.now = previousNow; + } +}); + +test('mixed window rebuild and append give a new UUID to the earliest file', async () => { + const previousNow = Date.now; + let now = Date.parse('2026-09-10T12:00:00.000Z'); + Date.now = () => now; + const root = await mkdtemp(path.join(os.tmpdir(), 'ccu-claude-mixed-window-owner-')); + roots.push(root); + const project = path.join(root, 'projects', '-fixture-mixed-window-owner'); + await mkdir(project, { recursive: true }); + try { + const earlier = path.join(project, 'earlier.jsonl'); + const later = path.join(project, 'later.jsonl'); + await writeFile(earlier, `${analysisTextLine( + 'mixed-earlier-seed', 'early seed', '2026-09-10T00:00:00.000Z', + )}\n`, 'utf8'); + await writeFile(later, [ + analysisTextLine('mixed-later-seed', 'later seed', '2026-09-10T00:01:00.000Z'), + analysisTextLine('mixed-expiring', 'expires', '2026-09-09T13:00:00.000Z'), + ].join('\n') + '\n', 'utf8'); + const cold = await updateClaudeUsageIndex(createClaudeUsageIndex(), root, { windowDays: 1 }); + + now = Date.parse('2026-09-10T14:00:00.000Z'); + await appendFile(earlier, `${analysisTextLine( + 'mixed-shared-uuid', 'earlier owner', '2026-09-10T13:55:00.000Z', + )}\n`, 'utf8'); + await appendFile(later, `${analysisTextLine( + 'mixed-shared-uuid', 'later should not own', '2026-09-10T13:56:00.000Z', + )}\n`, 'utf8'); + const warm = await updateClaudeUsageIndex(cold.index, root, { windowDays: 1 }); + const full = await ClaudeDataLoader.loadUsageRecords(root, { + analyzeContent: true, + windowDays: 1, + }); + assert.equal(warm.diagnostics.bodyReads, 2); + assert.deepEqual(warm.contentAnalysis, full.contentAnalysis); + } finally { + Date.now = previousNow; + } +}); + test('a new file carrying an already-owned UUID falls back to the full rebuild', async () => { const previousNow = Date.now; Date.now = () => Date.parse('2026-09-10T12:00:00.000Z'); From 12b0da465e67d00a027f2edce9d001789b712781 Mon Sep 17 00:00:00 2001 From: "Jiaming WANG (Carl)" <74130028+Carl723000@users.noreply.github.com> Date: Wed, 23 Sep 2026 22:51:19 +0800 Subject: [PATCH 10/10] feat: align Claude and Codex experience for v2.4.0 (#116) Unify sharing previews, finish provider interaction and localization parity, preserve legacy commands, and keep large histories and live scrolling bounded. Co-authored-by: OpenAI Codex <215057067+openai-codex[bot]@users.noreply.github.com> --- .github/PULL_REQUEST_TEMPLATE.md | 2 +- .github/scripts/first-pass-lib.mjs | 25 +- .github/scripts/first-pass.mjs | 32 +- .github/scripts/first-pass.test.mjs | 74 +- .github/workflows/pr-first-pass.yml | 6 +- AGENTS.md | 8 +- AGENTS.zh-CN.md | 7 +- ARCHITECTURE-zh-CN.md | 44 +- ARCHITECTURE.md | 66 +- CHANGELOG.md | 77 ++ CONTRIBUTING.md | 16 +- LOCAL-DATA.md | 55 +- LOCAL-DATA.zh-CN.md | 41 +- README-de-DE.md | 61 + README-en.md | 43 +- README-id.md | 32 +- README-ja.md | 25 +- README-ko.md | 25 +- README-pt-BR.md | 58 + README-zh-CN.md | 37 +- README-zh-TW.md | 23 +- README.md | 69 +- .../2026-09-02-v2.3.1-local-data-contract.md | 6 +- ...-09-02-v2.3.1-local-data-contract.zh-CN.md | 6 +- images/v2.4.0/claude-sharing-zh-CN-dark.png | Bin 0 -> 309813 bytes images/v2.4.0/codex-month-zh-CN-dark.png | Bin 0 -> 77474 bytes images/v2.4.0/compare-sharing-en-light.png | Bin 0 -> 126405 bytes images/v2.4.0/narrow-sharing-de-DE-dark.png | Bin 0 -> 100892 bytes package.json | 7 +- src/claudeIncrementalIndex.ts | 22 +- src/codexViewComponents.ts | 2 - src/combinedHeatmapSvg.ts | 35 +- src/dashboardRefreshClient.ts | 36 + src/dataLoader.ts | 49 +- src/extension.ts | 195 +-- src/githubHeatmapPublish.ts | 61 + src/heatmapSvg.ts | 79 +- src/i18n.ts | 696 ++++++++++- src/localDataControls.ts | 17 +- src/promptDedup.ts | 23 +- src/settings.ts | 16 +- src/shareCardSvg.ts | 174 ++- src/sharingCommandIntent.ts | 42 + src/svgEscape.ts | 58 + src/test/claudeIncrementalIndex.test.ts | 127 +- src/test/codexStatus.test.ts | 2 +- src/test/combinedHeatmap.test.ts | 21 + src/test/combinedHeatmapI18n.test.ts | 84 +- src/test/dashboardOnlySettings.test.ts | 15 + src/test/extensionWindowActivity.test.ts | 16 +- src/test/githubHeatmapPublish.test.ts | 118 +- src/test/heatmapSvg.test.ts | 106 +- src/test/i18nAutoLanguage.test.ts | 24 + src/test/localDataControls.test.ts | 5 + src/test/localDataExtension.test.ts | 58 + src/test/monthDetailRequest.test.ts | 63 + src/test/promptDedup.test.ts | 29 +- src/test/providerSelection.test.ts | 8 +- src/test/repositoryPolicy.test.ts | 159 ++- src/test/settingsPrivacy.test.ts | 26 +- src/test/shareCardSvg.test.ts | 155 ++- src/test/sharePresentationCache.test.ts | 97 ++ src/test/shareWorkspaceRouting.test.ts | 117 ++ src/test/sharingCommandIntent.test.ts | 123 ++ src/webview.ts | 1046 ++++++++++++----- tests/perf/measure-large-claude-index.mjs | 195 +++ tests/perf/measure-large-codex-index.mjs | 203 ++++ tests/ui/chart-date-labels.spec.mjs | 2 +- tests/ui/codex-accessibility.spec.mjs | 2 +- tests/ui/codex-interactions.spec.mjs | 53 +- tests/ui/codex-visual.spec.mjs | 22 +- tests/ui/combined-heatmap.spec.mjs | 470 +++++++- tests/ui/currency-display.spec.mjs | 2 +- tests/ui/dashboard-live-refresh.spec.mjs | 93 +- tests/ui/local-data-controls.spec.mjs | 46 +- tests/ui/support/app.mjs | 8 +- tests/ui/support/capture-readme.mjs | 2 +- tests/ui/support/capture-v240.mjs | 82 ++ tests/ui/support/measure-scroll-refresh.mjs | 88 ++ tests/ui/support/render-harness.cjs | 49 +- tests/ui/support/server.mjs | 9 + 81 files changed, 5197 insertions(+), 878 deletions(-) create mode 100644 README-de-DE.md create mode 100644 README-pt-BR.md create mode 100644 images/v2.4.0/claude-sharing-zh-CN-dark.png create mode 100644 images/v2.4.0/codex-month-zh-CN-dark.png create mode 100644 images/v2.4.0/compare-sharing-en-light.png create mode 100644 images/v2.4.0/narrow-sharing-de-DE-dark.png create mode 100644 src/sharingCommandIntent.ts create mode 100644 src/svgEscape.ts create mode 100644 src/test/i18nAutoLanguage.test.ts create mode 100644 src/test/monthDetailRequest.test.ts create mode 100644 src/test/sharePresentationCache.test.ts create mode 100644 src/test/shareWorkspaceRouting.test.ts create mode 100644 src/test/sharingCommandIntent.test.ts create mode 100644 tests/perf/measure-large-claude-index.mjs create mode 100644 tests/perf/measure-large-codex-index.mjs create mode 100644 tests/ui/support/capture-v240.mjs create mode 100644 tests/ui/support/measure-scroll-refresh.mjs 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/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/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/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 ff0103ff..7fc8a507 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ upstream release: 1.0.8). Format follows [Keep a Changelog](https://keepachangel ## [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 @@ -20,7 +26,65 @@ upstream release: 1.0.8). Format follows [Keep a Changelog](https://keepachangel 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 @@ -48,6 +112,19 @@ upstream release: 1.0.8). Format follows [Keep a Changelog](https://keepachangel `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 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. + +![Codex-Übersicht, synthetische Daten](images/v2.3.1/codex-overview-zh-CN-dark.png) + +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 31b93911..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. @@ -57,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. + +![v2.4 combined sharing workspace, light theme](images/v2.4.0/compare-sharing-en-light.png) + +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 @@ -89,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 4e516875..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. @@ -57,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 e652d3ed..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) を参照してください。 @@ -57,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 1ba840ef..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)를 참고하세요. @@ -57,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. + +![Visão geral do Codex com dados sintéticos](images/v2.3.1/codex-overview-zh-CN-dark.png) + +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 9d53b9b1..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 额度仅是本地最后观测,不是实时余额。 > 截图包含中英文界面;各图标明主题与范围。 @@ -77,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** 动作可以联网;它仍仅支持公开仓库,并在写入前确认精确目标及创建/覆盖动作。 + 综合活动不代表账单、生产率、能力或跨供应商等价。 + +![v2.4 统一分享工作台,浅色主题](images/v2.4.0/compare-sharing-en-light.png) + +此图来自生产渲染器、合成用量与 VS Code 主题变量;不是已安装 VSIX 或真实账号的截图。 + ## 2.3 新功能 - **2.3 系列持续完善**——新增 GPT-6 Astra、Fable 5.1 模型元数据与可选 @@ -97,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)。 @@ -118,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 @@ -168,13 +187,13 @@ 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` 里已有的值搬过来。 @@ -214,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 66b641be..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)。 @@ -57,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 76a7d535..80edacef 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,13 @@ # Claude Code Usage [![VSCode Marketplace](https://img.shields.io/visual-studio-marketplace/v/growthjack.claude-code-usage?style=flat-square&logo=visual-studio-code&label=VS%20Code%20Marketplace)](https://marketplace.visualstudio.com/items?itemName=growthjack.claude-code-usage) -[![Open VSX Registry](https://img.shields.io/open-vsx/v/GrowthJack/claude-code-usage?style=flat-square&logo=eclipseide&label=Open%20VSX)](https://marketplace.cursorapi.com/items/?itemName=GrowthJack.claude-code-usage) +[![Open VSX Registry](https://img.shields.io/open-vsx/v/GrowthJack/claude-code-usage?style=flat-square&logo=eclipseide&label=Open%20VSX)](https://open-vsx.org/extension/GrowthJack/claude-code-usage) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square)](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) --- @@ -162,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. + +![v2.4 combined sharing workspace, light theme](images/v2.4.0/compare-sharing-en-light.png) + +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 @@ -281,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" @@ -343,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 @@ -419,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 @@ -435,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 | @@ -444,7 +481,6 @@ 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 — @@ -523,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 0000000000000000000000000000000000000000..4d16facdc4042202e46cf6fd23c6487219d0307a GIT binary patch literal 309813 zcmd3OWmHyew=U93sYnO{A_{_((k&{X2q^d>-637lB_JRT1|2FT(%s$CA>Ae2oo6nU zcc1-ZkFoa{`;2jZe17BAC)RVX`=0ZvxxAlAiQ!!!yMTg%g7;Ybkqin7`Z)aWJ2oo( zjSHDO6AH>jl*f;RU)V-1MPn5X*A948DD3+9lbetlvG%{#viEvB`T)P=UCi}69?7>lbRK!hn8(!4(jLxXNL^yj@(YiikK~ zNU6YNB;R-_U8STtPAZz$s`INzFs})w)%FSbwZgjjWks7X4%7OXz!*-GjaSI~e!tnE z^3jg!fTK?CY<-Sp>MC*en`j`CHvs|+Ezh8{}(S`mrk`u4@ zbV4zCeE4yc{4o1JK1aMrIYz+2+6$lJNsZiamndEPk?Psc44SPY?Fl(9#>=KAd+}$b z;G5e^?KTH<^}DlmeC0SFxj*^)>iB*7(B=f0WStb^G~-uoM+#?ec+1YBXqfQg@xPzS z`v&vV*G*v4?*}B~yn1&2@2%>=VE=ZuRtu>AaZy&n5`SM2 zL`9XRn706{13OJA-)Nxvp*W{6KQW^`YqQqL(S8^Dzn|=^oC~Wg7D%}~m=`PH(Cc@V zO`}-7EVuVT_VXuIH8npAOvln4b}31V&lA%23@3J-U6M0nr6#Ot+C#4ZHrYjTVb$a;~#$rc3c@M7)w=Pqmzt4cVOXgvplKqyMYRgB!E5yof>1|+ZtX93t(KvbE*?wWiGfVO%{*)vY%@CjXH2hwI!|n>{sA>7W z==a2st1rT4;|lm}R*n>69XEHx)G7pxcjAw^_y(MLT_#G`yX&pyJG2G@%GNslcx11y z5lC76NL@eM1(FXN0w{)Z_4&*vA5lx9VCG9ka*}E%M<|l@lyUJVIwXx2RJ?Bby7AzM zXENcGeAr>xiIcb2k=LrXmhZ$N`EJ5Wg(+>Io%yuO+5L~ejNc2IBka5tA(B~5543w;Reh59Osyhm46cCedR%f8CLJGy>Kf}cR(l9s^n!SNy+Q^ zF7>>D9K8aApJWf2em>9P`DPm6Y;u&Fu`@Eg$K<)9Dszil`qp=mTfJXoSnQ<5ve|9d6xMPP~a+M@o6*DbML?p3iF301YHuNXWzTn*e< zNXf8v7rSQ89by5cq$ZSEF?VjOTe{?E!^QDnK56e&w z-O;>0)!oCf(-UczgKnN*QI=~}UQv!4O=b4mj!Rh$Wzu8ElfGFBBX6(-*DBGdbz39s z#+*)1rl{JpsG==?B)el}9CgQ@^c>j@=(tqOeXVfX8PJP$yk{`o7)<4~b;%9uTIOjz zx#=~-&FK%);e0mCwL#Pm6QyGXO@<5MwGF9DQ1}Uy0y6Zr^?FkkzNwdwzB15#)*yLu z?Ns%6%bU%*!eDUe@mfM=9NqHkCfHOai?5AJS_+%u{8vVF?zih4w5oC2thhy}G+qC* zPC=x`gSp{%wRGKo1|647zxNGhQq~P`BM>?gUy`lG!oT+Fbs*))b9{2nmZSYT5-nP3 zRmW4aG#8cM!RHaq#ca_tKV)|yq$-&|^_rVey(;kJP zQZ{#F^+`Gfqs4iPGiB8yanOs3twEt(y`hr_o5DAQ_KA z&+VQ8`^B{UVGEqYPxT{cMpX-1-x7EY^q=hxuk|V6j~ovWtlmlP8Xq-W z`-S8rqiH*WcfQOAU!6~Hk4!ujvVp&ps;f~|!@$TID7KY2;j}Nw; zCOk;62vo1;50JhHo8Vh1tTA5ek(IXFXi934Y^(7i5ZjegvHE9D?eyUG-Vl60a}pcr zGy_w5Pse9#gFc;_PX!d~UAHov4{GbM1pB{>EZX7B4)1S_8GAsLo}j87a8Mny40ryZ z=X_fKtNpAov)`((m@$I`&QxO5C%=|igYV&c2*-D19G`FL4L@}h-RoGvvIzk4tw2z*Y_?I%Tp!lp3Z_4-!iwwbmPyLYIuUBmcQC|ctoXzj(gxXIKah60tQr#Iyzb@ zSsioQRe9${Y2PT{eGZLSsCZr}FqlRt#ONv4_jq0D_2|2L3s(zPkAHk?n23}vw+o#z z>0jwlvazB2tax&;O0(WcwZi-C4BmJPK z9hX~)UMipKL#olR%~-{)bLA&>Bx)^TcWdyZV)*x=>T?DZNQ5%KRJFr6v6|~Rr<(Ob zMm|}L%IS)oN?60;s>}EXX~Cl9{2|GeE8(Nj)`R-9E#a{69H;7PI_9H!xviW^52N6( z_b!|`t=GvPu^m9$_+Zr1{Ux5)z|J>=Z;HJ_u`1&4fswgo<|j_j7XD#A8md4feD+K= z4ofCW;C{OcCD~ogw@SZWR`6;u2HAbdUuuGS2XoQ&p|ATXsE2vd?L8byE?u+%&0iQoMzK&2{R#a4LsOg zJ#AJy*?@Zau`Wpv#eL>MuAqOXdv}SbE&AIw}G5oE~kmTx4ql0>Vs{je)mqbyHQ&~R5TU>$l19i0oYh3dgIY^Ehu5d0nb z^IdWOYE@W5l|=g8$08)kW`a++Cm!PSZ27TCxj?DNA2hi0sNSwJmC|V|BcphMLB1CX zZY0zN;($TNONk^@!eQi2{%z8a;_|d_6wCYzr-ihkQ=3qwN21Amq>D0}Br{B1hXYD0 z#9_*g*J0)+T6F_9;Y^nVOHORrdVOSLHkrTQq*b3v!FoM8g?a5I)6#WM#`3#+)%x9u zu=VD1bX$2BzR?8o%+Arp*luuD@DSA#)$i^9iWOT+&2fIo92)FiwwPXG$$QkRWcr07 z*R$tN6zluglBH~%s0HYt6GqP`a-b*v63Un#DcMzZSoV&&URX!0biLbGIYocs(@WKG z0S8XyUAPFtdnx(r$m#BXZmUb)A{~@(*w^cH!+h!)K@?$m!)zgWKH=X6xUbP!;c`0P zt=I2$`6oG(Qhw^jto%)>&Da-F(nI;i6S*nj!WV3O_>GNvO%ia)*uUu#JpRipgATMO zYrRK`tx}q_bR*psl1${Lr0Pm=ipgwzT$g*(4)@ll5)A%hNzLH+v02XI+PmaQB>+889;bt8>+M?v7{)%-;w<-cK3tWdt27~lI?$lQioCd$EvaLryNhS9p!X=m$CTDJ( zX#qvr8`DiBYL4qsJeEKm%_qI-OiI`5ZHCPo1a^nJ$87-0TP~&*0B*jVVqbX<>%}Q= z^tEvjY2NvG4;&Izm*ed~p_9E%F{%+@smD7*rm;>t^xPdo#swq6VwC6Cp{gj`&nKL5 zJc-<1HJ4+KdwOutjefN?w?y-isG_N&=$-r{s5gg2u#u2ywf2hD(0HhNbaA!f)C87U zMmFNd=j!paar2hD4a%=OMc(0(bK<6;_wBCL5Y%KBf7*rPFR;^3)9Yrpi2q~Aq$Ew5 zxj`^m$mN8^WnUr=!F{H0E|^2>tvM(TuEFPfT1v;~vi94hB$-sRg}leGy&YG zsL`dnqYlQ`BGaH0L-8s5OJ5hf!ztljc*-sh7ZH};$048|VBC<;RIf@^$N~O@3tQj= z>5J)`yTg{!>%9|D&GS*cpBNoZ!P>atFqX-7ly!U>E?A+`}tdhR8!~78>zk6 z3$f#GnHJB}a%})KX;?_jtiDkHS@3wPn@94?AnY@OyAC@>{mtFdLL*`SOs8r*YGtS` zcMDvK(6qwDB}F-g_7e6eYX(;9EdkX%ZR~5SaDXr^na>cTMRJ1ha#uog(XSZ87?;E2 z-Lcad78mb-?ni8;m#ho|#R^tLaB#4gh!4jAa7Ig+Sf8FEOFgaYhN#zOfn;f2&WF9c z9buS$UV=VG!D%{JENIE2?TNEV-cLC!D#y$sC0ig9gtSk&C*Bfvl;Kk@4;9!DW;Otz z9x|;^*&L|(dodemak`jR(N?`Yn=Nb;{B;f$9gpdvKOO{q0j@%5y%MGs;L0`(M=Gf1 zX~cb7p2G5J8?A1!6!d+WLrv1xm@Z^aHF#W%(5^%?3MYM5UUpbcu|Wsc8@l=q=p~1q zFlZ&bi>Wzn+*7!JWrmCIC0lV9UaC3ibVY(xVcM6jvJ}!sDxOe{Uft>pN2*EM!B$+> zZEJRIqGmZ3;3`{=eDD*e*>FAQbzilxJ=j=F&WnJeufmR~Z49Q-NNbJzhG=?~hyc@g zTwNsv0*Ft;bkhe|O`F2uDtD|^K%2m#zMXB0rqA%hq43%O&|VtI0sI&f`~1qPok9bJ z&9#;xq0@b7@+BZ8E~kfMn5L7*dsBfLEGdp`4CMhgnV!-w=^$l2U4sJ)k4!suGP2!@ zQpCV{t5Zx~EVU<7qvqdwebY;^P1r-2Xw@l6m(0MInCWv}WZ(hd{RrNajB;Kh>J5MF z;*~2%sS+6_&gm2-r+&OBc(1^mf2Usy;LCxj%c0UH{?D8?UK~uKCg}ov<=n%hL-YkG zz<^xoWl(H;$W2Q-`7wO{{xosoJeSrMCCZC@OPTI>zn$*7oa(8qSj>ESS^c>!oQ-b3 zPQ;g3Cqa6IJ3C{{`7rH8D}~MQB_;bMJod>EpQ!C^kTuh?>d9o@0GaBJ{;QISyTb`r zv>rAWTR)07JbGGe0oc6h;BYBNni2I*2~;oBoL0_R??>XrC!hy7Lp_f#;@<9&9T5tC zeGCjw(+UdK`-KAG_85Uy*|>i>fw=4d-Lw>FX*la}xJt}7H$DM(E2?tG=fzfK9%wX3 zNzp$DTALuCvW1 z^M>~Bx6H((Xlt^!y$9b@(zDlrABTC(NQJRhrBy&p291t4sY)i9UAr+zer++M!o%#(TQLU&phn7F7G>&0`dKIev zlVYnyPn{~vk_;c$l%?S!wq8_B?nU1O8GVq?h*+p1^U7bL`?rVP)fz3eJNw9+k;3kn zKli8~|Dz~7B;Fe-Vkoub!R5(Axv%!9i=&~OY#8g&rN1w3O1k;)ouT+&5|PiHp|rO_ z#s3|RI``vXRx9UF>Cr~B>Y~pIIl91~yqatSDFqI8mKR0K0Wty15tK9jAjx@^ zRlV}vC7O7YhHI8B?gWB`3*S?uBG~8C3P#^w&>t&5HYr)Q1{9-(9sroDWVM`J<%!%7 z)YhzCkx0I%)#c} za>3XMQd_~<0EN3VLruu91;F=wuhBsE40N{e!M#HBsrOnqgL#HFaNOgf`$bK`31LV{ z)jYk%XU&-ZV5wxxVQb-M^BmBic~5eaFizt^S1d|ytA!Uv*vZ#$b(25ZP|t#Ha67d5 z;s9xV+VR3m&fvUI7ei+N-wlEnje@jumB}#X3W8@1sx9e^bOlKfLb3LK|E74 zP+ubr0LD^8sAQ_UmRyfIj?>Y4mh}-BNnfq`1wo5x@vW+u-MCqR-1yR@iwtce@RKaJ zE~q{r`}zTkw*t8Oq;KY9ne*}&8NX&3urjggC;x=w!U;}5+$R~cBzbf0AqnQ9T18*A zqe&7{szV}nj3M19qRO!0*WHrXCRugF`BJN+WfT1q*+v{|Kxk7-H-1DK=ZDWNhr5?vK?=6^Z`RkIlS>WmF;s0E+~900~LTm;%;G*HYK2%23P?>xnJB(V@)V>M&p-giFP|23kkW;=lqzc<{7 zNpAE;`9o($`2mAbjo{&u`%{{(NB~Osmov>8p>v}hn8A6CG+^aSBp?pEztt*vM*y0w zSx&xA0qxQS3qSd273%?vqU4@j4TilHK>qkCo<>RgmY@OUt6rk0e-NzcA>uXmhQjSW zCqcBAx0~UE4PuF0{fCTwdrGNN0AK1h_t&ggeLCvzRaI8%OrW|hqiuZ_c3+A$Qzn(Z zIhiSw$g%7yRgHHe0G%ZrNBu>b24;WuU0Iuc4X+}XuQjjm?vRq7;|ib{hs|E2IL?5X zzqd*G?sS??$Q7gR;0f6rDJDAR29%RGefRDtITEVNGZ&7d9@6z*QczeF*HV8P)KK3a zv6X#QJFZTGkKFZ3;oMi>+M&;SF?O>cy1e_RCKg~=dlpCCQ0NMJi$gO)UsslhGgB}~M^S1=o4Sp9j>V5;6f$K}TS+l)&3I^NUmKD8Rxt@&=T z%avGPO<9dt#ZB;YgvXx-)p_8t=`(ZREo6)ot-t&{J^KekV{7#TTvApPG}I5cr#l%g z$2jiZ!8~)H-Jdns_CVVwi^C!(Aa^(SbQ1aT8YFJ_G$&wA>mnt?chU=Q1Sd*vYU;l)q<>`xI*7k}@2|ZS`P?^mVJ#vmSZSy1p~6ded^X=@R$DR^RsWRcB(g%WLU9MbK!0Oe zT(E|RCizZG9$U3~*ZX?gnvW4emL@l)j=znY6hXTfjP;w}tT;V3U6R;aX?Nbq0re8g z!7bTuR5M5OS5^620RtR@T4}yzcG(H|u_;HxF5G8Yz#nuX)6moMd2)KR<@AyDB9J3@ zk=mv#9(OUhhC=+C{HMRn;ec0wF4Pz>8AX2edDPRIx7VB8i8%rOK_}{)MuFN=5+P=! zVB(pH4OKkfQm!*)eX1czN<$=v)j4Y%-ZH?*jPsD~t$ z<7?AMJ9M0aT8enCV0K+GG+uq;At{1Sd`r-Sqwf9}4@4CWAPG4ivkxTiZN-RTM%mgg zkTAW|ClyiregJ)oSn2{(%ohi1HblRlx=^-11_0t^eNpX&AhU<4*L+0@w30b315Hvn zj95tf;QhTO_+gJheP!lC0eb{!ET0l8~of! zUSe*GtOaS+rpCk!we;n!ffGG0z_UbdVC!{(5r$rRL8>FTft=Hb^j>%%|Au&m zYMI$=bRM^(&j$Yz51aN+WQGs5;oTc|H%2e7+mm*@bkcFm`~EGf zYgPj{5qh{b_nkuFLJC6RX3`70m-x)o%wptq9MRR@>U^O?EJSwYj)E~~oL`>t%#2tq zxS|AxK8$xon;a?h^=QTwHC~PfsPjQLzK6@lFP|+zs_~)MGrVdJkBl8V5yQ9MCJWp4 z=7$z#=lz+2QF}vW!8drE1=ZDsv`JqyKYaNxE2W-`ChhB&b=+f6>(5bLTw|m~;JW{o zoP9T0v*C@oN8HJ(%c)_7@AA8A*DY38czr_o`9WJwc&@%%S^T-CgWRN$+J1(%1|h1C zT9OjmNpCNy%+aA+=Ys%SD_5UHyJ}#v?jaI5G8u{PTkq@)Qkc3W&FTE8pmE~>6Vn{) z<#;gKi1RbT6`T;`c6XAYJ<_z?yBV)vwO&GNtVV3M{Phd>&wPKP4-yz)VrSTmGb)sd z<)hpzF%xuxeL!J19lU~D16IK$W{i-_F~1HLGgb$3l5E=_K8V0>Lys|D>d%^-AqO#s z#5z|;!qBR=L|_A8=P5XgCJ<6V&?G1#PQZ~`KIo6rjp(5kjbEY4zdDL5+V*H%Kpl7K zMhl^#295?}Fv=rEf;Oi8%z+!9$#*<8Ua@2Qx^X<9^5a5lOsdLC13I#-_axQ`_>ZR{aJ>^jH=siI+k#)p3I5-D%u# z+#Y()*I7{6&r$bl?Q!`{1NQKq5Se^k`|eV|wh2z7i)QM$4@f9hIU=mzcUF|Oae~#4 z8iYbFgqY2;>J(#Y>5V{lok!kS~f=hY?Ol9Shxmfu0E zZz@zbE(FqvwI)*(ohU+_z7YiL@cE)GzSF41hE_ZZh#I%0!mx4EM&dQ55>{EA+{1#q z?_n$5N#}UC%Wb%X3-0E3TWJwzq`E8B$lb@-ysr7@ScNbf@L+LZN;NFwXraK_!tknd z!*fq@6)FM>HSr*L@G{F6A6#<&^Y24u>C0CRIiG%C0LMgcX;do8ck0pb@RC8cygJ++ zcd9wat7J=QTe{1*}K2Q4*oa3_zU z0zZt5G|E0-8tXuF_ukhvN;BHIQ#1m86hgP!5eb&Ex5L@Cq8PLt#@69FAw1wtj9`1V z;o}_Wk6bajS&x-o2*H^ks%CAkUuC>?ORrQOo zA9X(cMzLAo+~xc{zSX`B$OBxVADA98%v7!aJjvi09v6x3EmoM3mOpsmlg3EG^RSQH z*;{5`8V@w5s(Ze0_z_@M87UZ1@JU_?y_v}rRNuh-Hn1fp_^E{||Dpf6+R;}-1?`g$ zWEBeb$TBmkEwrr6?CSh~t%g90O0pyE0`lbVd?Uf{sZpv9w@cspO2z33CpoT>sktPz zwUztt?i5t~H5r$oEi)K1mt_aN@lhE7(6#e`PF#W>-E-d1Cs=2^cJw{(947be-7?0V zG=@w(R{nIYw^v2)t+ny3z8!ctOML6(kv-=uLomZz53|jbMrEA@CA`Ml)pzi(aj$_i zDwcR>dp={oGNy_NfUs>uhF6f&W46S_(R9Wm(Pb8x#hV*Jhk?2Ko@| z*8e)a9tRn_sX(1QBr51hU8WO)HR@xt3clPh_rbp&2h42s29Gx`C+_>>o2n5+O+ z{aIRLG*MjZHpeRt=bs4=`|&E9ER<+0C&>>ZYOtfqvrFLU(fMWCW{l$Uzd)V;{_%<%)b|X#TTQE=a`s5V#6PWV*QS++=-*e%Ro{zsh!5`e@=k!+l-tcN% zprFriqEqA@QQS84-gQvjRZYvbTMo5GOsU7P3urqvqy^cr$3%upKh|Lg;w3-VeickZ z!K#kpw+pcN)lw^@7%b1%?|@hFcCQ0X_j~|;oVI_Kc`2)|3fzEvrp4VRT8%g1h&+HT71*QavpfPKCjV(XfOv*)Y?H1lr`tFmsGM2d~wtzkmp? zOYuvCH3@!Drq-IP^9Yn#|0;kBo{D5I!eVv!&AM>C&O6YLszjLGGqdLo=iHn$Q#4%> zdQ{x5{S~DxEY@-3@#ZX1Ka;lF#A981@p1I=^`)i*fK0ftZ@Nti@#U{Evn87>Lv^e2*&KPT65bYAJ*O?mL4#u+Yg1yWQOSL%ac z-?kQHRt;a{o$S36yx<7>w*@A;f29p|itbB7W*q+-?Y6MHXgF>;7Hae7`&@PJz9wZa ze9bLCEfM_S5yvC-B8`?&(5|N4IvBG*f5Q!YyK@&CgfGG2CUxI1#)P7m*3dXP>NB9|uS9AMuWEKz8f#kC>`cjE z>|9)2@36$(elAAg;p(Qs9!Oul)g^KFkvkTFXpDl7u}8L-?aEzinyexluI&}gy{?HG zubS$#;)U;|N_%!w{#o2c9?;S5%27p<8jd7Lgg%k|C}J&P?tHKR$Ly#Tn=A3pr&9IO}XMBMcBCUt8p#KF*|#)8)$KDkJ#3qn6X zw7RTe0MMj6xHuj&4&cu{odIHDS+bn}kgFrLs6n0|4Oe!{!)sA#+*ITB^RYn`TXF53 z_cqJ*nA*Tp+z%gEw6q0M@L1fGsajLf^;agm5!1kCdLircufx>}p=+0luSD8MOoMsr zLH;(>^OpN34ux0iGHyudq4m``4eq6S;&8pqWIOqEZc{9d%1_}6g^3=B3_&k84e-TW z#5P{OJxgcgWn;0O#guk09XlB`QvDV3>Ty;zTE z6%1^6);Vw7-xwSA@q3$gC2`!uyR7;|do9w9KF^mkqyAjKsc!`(F^%U4?)gp8;S8xi zMD)W~N^ZUQ<0(YeWT;HeIeaf2B{TJiDnT-W8V^H2y%L4HtY0OWyMil_?|pq&fvH-q z{;I9fFIOSS1Lj_?_^ZJ#?s4zRKCxrvzF`l2+(2D$-sVyX6WjKkrk%UjGlM5}%8b%4 zYoTs-kK@(gxz&TwV#Qr4PkiO!IlbGex|iMk($r(mf5hlz-&g*2j*^uVs-DS4>$hZe z9SD<|nmiR5%OnU3uU|{KTy~YH>G8BgC*%8{tm%YwQn^&&NAb4lEzSw!au-)Uf603x zqy-n-C_i$aNZ`#;rO|4r{=|F?3%|BpXjSR(ln z8W#S)qPUQzXdoriKhN0Q6?KGuuLt~b)HuOZekNA`r_}QQ&rJNH@p2!!ZxUMmLvX&@DbQPYs9 zmV?gIQ(#5uxK;&i_s4NBxK8=t4JkXW*C{S8>HPTOMS0NikYKrRDge?WOW469SZ>c**6TsY4Okj40;+)cSNF;xLq(3} zy`>LUAEb{BOy)4mzRXzg?0=dGQA&qJqKkFR_E!d98%g!;)v-9crY4`ik;H4ap*^kW z@z=p>l#ybJom8?W<$e9_=MKK=O(8l+FaWfGs}z_ViN--pMxzUu+u$#hb9{#A!%iG> z!$72gybsrtIjDB&kmWx6k=pc0Ay<3AJ2VDikW0}JkHui?+w-_#gT@HvilYr0v4ExS zB!ZJ+P_=~)=4f^&L^I+oKK7t+-Rf$U=SndWMAQY9Am#gU#pbA zHL?MICPu|VvFv(G>=h+tNcwbMk5DNzn*ej92m+nszW@G)`FP!{J>c5rnU0-!kf^e{ ztbpJl=ez6k!nPBk^i}Ym_|^6* zFUY|?0KX2LDJ4U(hCr%7ZAQl>z%Kue;Dku^p#Ad8b-89!W8KA=YuIN3>4;>iJpJza zvLcdjnI`9g?-Jm{rGRyo?}DZL5So*{Q)EC)05W~Z!d0WeNkp^j}>x(Fa{6HWg4VnG$qwz=7DJn z5p(^{ugJFEzw7Gz9t`8UpG`0z1h(6sK^BIj5KN+#TrfqS&_ie2h(jm24GlW*6q{Bm z>h|yALnM<^VrDMtX7~#k2HK#(I=wVrfu@}CyGjIINDfERTr?xd+t*EYAwrb%`=`DT zZ#7vrhVdWdzbGM~@w?^ze*Fxa1Ke8qy;xMFnUbB8zJ9il|K(uJe_kA`yGQ`*%{jE| zNZbk>w>}<@c7TnRZ9HORs8$f;WI1coGGP${bp1XSfv0U*w>n*fqDsEbe6AU)L# zTgV(V!^c_!uj2dzxF_E|BoN5md#Nso$d!mA2Z|H~?I6+~9Rukl2t4&W!u(T*tQ1e3 z+)7WfEYumuPuLfDfCIRPFtt1d=1?TPe*!koETrnrYKl0gMCIp@CkbE^A?9&3o1PSG zqI}3kAejOfY#FrIV2z;eOH9#Za=dlS;!1@K|kzD)VHP*xJ` z^da0ocCu5ztWtb^-oPE{DH%nx5u1SA3o4G8TIlY@W_ClMVGvGOj1dx9evrJffwS(wSysfc_7vUP`tmD%}LK02~TUAwv2l#u1gX+76F^Li{>4KhroI(C{^yI=SApT|*kZ@tc9R192 z_UQf$+av*!aID`FQQybzVe3F(NS#(g61K`DSha|9mydb_Iwuk_n{YN~-iD~(2&9ay zFNqI*rE9cIl#&;@ybZF}pji#xbKz2QCz$;hnVt$Bbv`tv2~s2*FC&)Y|&9aL4us+MPWO%^+**TBP}EcpfpOeFJ}Q5E+bC<7W^nx;Yxva%7@_@F1-tQ7saLBk6$0Hi5X*mc%; z3lQva6++@z@iIum2a~%3b`fjt9`+}5%zW4(5fLf&eiVG1MX#rwm2zKYfl=+iZ*&Bn z^drO4y3mUoHCjJD(t{yKJFE`~1~z2{wcDaMcUO#g{nfoc*4p>aMR*H9-U2NegQ(-g z0^5@F0kBX&M#n5n(QrM+E%G12-b+@gQywH7m{yQbBV0Wvo}XBx(3hahEHvl3wS-Kz z@GdeER=^eMw#OK!2m=b}TOl#$^hBpea6?8UB`ri^j0f|L4RB2$b5)GnQGIyU-39t14^H=X}HnYcL*XNd3@PbF+Io*$4K4_(pXm#OOG8qoeg zAh=g4s+e!2Pb=rWt9tdOI9$2O;ry!eZV9D;J^#U6oIHl(+ocuo0za(mLUo|vw_SsT z1yZR%?=|?7NfEjN@k=x4tu=9ztk^aNf#JW)`p-;s%J;8f3xaT04tsbNkQ`?`RUn9c zpX3w)F_>5^0ur0a0)SdM8-8;dR592uxnO-)KcxgEf6}fQMgq{1#D0O@4STi##*7dx zED?jSq9YFAA$RbDkZq_X#@Rx2(XVA7`T$_ZVp9(i3Yl9h6-Ql1=Ti_#t>M7aWOZEQ z2AqnoMeVH^OxN$i0Fx48=Of@x&aTQ01Gy1b%+6i%l2hi5|J_-|)8tVxkxl4v9*!Lo zoOY)p@3#w(r#glbR%kV@LclY9ar(tNeVZ#(>^Ij>t_eCKNnR$^$rq(ILDeNU>Mnfn zW@l{sAR2Iu=k?q(M|PN66FQn8Xtch;tU9~%UV_Ml@?iQef9-cOVL50fD#Qp zea2>%iqaR@my-}oSM< zJpLZAfLr>{Lb+jMEI{U^W$XD?1x!X=Y(!#=OnE0rDl}WC8G*4-mYPlNK#6%Y$#p9H z44Qt8=bu?uBB6IMj%&j7^$>_-_`Rp#4~8%QjX)Dvup!Me{ZrNe z1CCxj7L@%R9g7!+k-GgDX}H(KCrJno4-y3m$_pcFz%byYK~w^Jt4JX3PX(g}kLeHZ zp#k7pT4h7Q}4J?|2npjTwh$5d4k*XaLM9xmJ(m(u2mpV2dJLL!Z+>jsVng^yb4Rc8?rJ|v4T#3H-CR$*KGr%XH|>@XUO6KgX?3d-~R%C zSp8>!^1nV9{QvRCJD2{3nlKJ?uNZJ7^of_w#~L-B(J(;*3^eu%%BOaS6GPTQ{tV__ ziA!LofwJQO)5p-Ix9@7zi!(vsg90LaTgaT~H69s;X^5830f9l_9W<^@q{(;2H9#gE z21K4KX~6y*c0OEQj)kd)#i4>#DBu-87*YzC$CZAo(*7Cq>j*ys#Bg?(;!!v_zri&M zlqOXxoM{0|nKhocS5a4CtOMwP3p7{(5b9>Tky`vD8DJVcnF7qHx+9;>HV7wH@a18*O$LLX>gNoO-(QY+=ayUk2hfu=LkYH zXNN_c4EQyio&z}4cn#QikytE!kP#-7;y$+y`HKe&o*cdMHG85rrAQd%QPz>sKUg&Y zBCe8UkYah7rv$DjKt%$Bucr!<8fD`aIf4jzgVFY-SRT2M-#{FY7!%$C65}Y8P1^<_ zy8{_^8Vi`4H35HV$S4Qz@+VeTlNsP+Co47;F!4sTMpRe`G3GfSW9}Y9@D^ZbDDRNU z_cKJzc^`+l5M?sTvJFVNG~!$U>@9XVJz)(&?B!H62z=g?iGxJ-olj2yc9l$cl8*wE zm8W1wKp^GpRTX<^yv9fHb38dlKn^%vgLxq^xkRya?Qz!{6?Nj@!L&sTL|nlk1oJ}n z-+cpdYND8nF@%#)<~bnG3>m4+&wI!%%{pjQh2g4f(W!2TuSyl&Y%Iv-J#!=x zeqnuxBsFkYN-t76q~-M%L!xdxYozCP={h_;VE6!dvMyAYJcrrL zBWUrYvY|DQa$%d2f{|*(dqEH*Yr>AKw&8?S@IF5zxzuS{il#4fE5$1XC*@gj_N%)? z2}7PY!K3sc%zqsck+6ep5O-GE8qZ*|06^GxE0p`0wMlk z{B{=Z$n<kxzOWj_STZD`sUE6)ey2YXbJKzrrK^5gD(8h@3`+<_d3qrHkNtE#cfK67q_Gao+}qp66oW(lGF+n0r*_ z9D?}n*m2#Tph7=Xdzs> z3)}p1co&?mWfWf%C1J6lpo8j>v2;>tDdL<&-({@ywoyvy+ zh#&Y$4YXl(VdD<@0a8nTe8D~SIbM-wl`nPP7VoAdH(ahxnQb zfQ$pM@UwO|JXWLPWIJQ-^1ZUA29C2lv3c^0tRtj!`7B>9gI>wixNYN;e+^d9?L=B-J zj0(K61m-*nR2d-U8=(&>9uyvrJ&~xjWISMr+Zx`+(Qsx3INe`^TYr~+07B|D1g6Wb zLIC$H`Wh4gX?dTh-)F^d{862VgoDp2^G$zvf&!A8g~8NMkJ(mWj$&tL1VZhBR6?U^ z`|wjvAn_;c0R<26d`bJB$b+BQq&-kq@djRQ(lO9zdFx&p7^2C4eN!K){x_!VK_Y04 z;7EZ{r}uX!2x$pJy7-&>%Jnhrp0BHtYo*{_z|a~zMd9BkX28$``#0Idr#!Hf?Z8>X9xl#|qacS>z7v@-$?Ljxi9z*|iV zCot(-QYhtDpaTBZf)KoN4~7ZZx=zf8@rE8Vy*YmP;Ii{Jh?Pwly^xabZ<`hqH?7Wg zM(9w9D&ropOk6UR_bz@UJb&hf!}-NXBWuXC$TJh$_bTY6<#s&*s+M^C8i3JSL6fPg zl4%)Hk-}8*DHPOFK;-X{w0J3O*lPV6wzFq)1bM!}O4Y&qnGLb}X`o*W%svX zx3nI>fDGZS$Ye@|))=6XpotGc-)2_r+JeZh2fBPT6GnD^fCjNiu&j|Y-=kxcTtj`SboZf^eN~mU-p{U2~9bUOcI?2n!rnLVzPM{f9NDmHs#Jrja?gDQ$l5b83ik#sWiP z(D2W@tYkG%LOHi8(}Y{K_9-racL5npMva@t*@BI*50p@k2qHZ+l;GU9c?M0*R8%1MKjbE1|jN(Qst{4 z&~{bus>|e^y(<*?FVWT8o@?Hrf_dpg!G36Q#)k6OJXQW2zWsC0wcLy0)>Yj{8X)pK z3?3+CCt8!NoF=nadi#wRsvXABkD(XeAH}uC=1GSQY76JSN!PrKfY2AyoRVM{A^i|A zppbti7Wl&O#kH`M~^4L=eE-;eL>0+V zn~`UkP-jwTe1PY9m~;Px%5wrt!>YSzZbz50NvCR+de;F)Q{$uz^a6Nh!(i~SgES5J z2;u(2+`yx0WZVRs{c__)anrA*(9p6vHN=98YuhqGC%NH`Tm=(!lu2X!ZrXdXuNGjc z@GZs&lyAr`$37n1z9bQxhGJ@zo!l7wKt16zBp*!DY({O zGPC6|lr;umbi&x|Awe7>!P>qsB7?Cw%0)Ig;xlr!ukl>My%_Gnlrk0Myf%Nq!(H@h zJn`6U)F*G2fCL34sNA-x7~1&N08JP_23u|sb}TYg_^{IK2G4#944gpi@}#gZbrVT9 z@}jW5uX|bUBIbGg3Fyl1iXkd!nU~w0951X$G z8E%DcT@_N}JpxkDPDx5?_Jwqu%H*&3qNLVW97xJNx^U^Q(TWoEQDhP#Bj>NiB>8FS zvx%UiCcs<)d0CxI5Q>;kU5QCHq|6boEDLi&A~=(1l^4~%5aye_^$v|O{OUon^t;K~ zi^CgIUO!%GTM`U+oF)HV|B2}DbI?fO4zn9KXT}|COB6260EqPKxhU0PE+qu&f<=*0 zwzgTk_)Fxz>Z0C!uh@u@2J%f~Tb0>YTA5^cAPSD9jYiGu zRx!mJ!v|BqK;5Dr%I(Z2#n!vw3muu+qFD0X$u8thc3u|=X|9DhMIFmHHwF}oHGy0sD{8|8gUrf<8NzGf<+J25V zWeK`__>1`U`ilKl)7nj2@#n?#t=ICQJK3@-W7u}qpo75l;+2CXJt^dClnEIZk^XOZ zPS`RhgZflUV_BNqLVrrSJ?A0xVIoNAi0ZF`NbjfiG1Z5!7SmFye{t0L%%rv|B z4FwIUM-Dg-Mm$Rq5|kaP9!IJL#?e zl9dK?z!%7iKxXc=!&dfn{pkp#DQgmp5hPLe6hm3HDk37FD!KUai|+9L@k-r>1KpLw zKiTAIEYVh6hK*S62~GvZbw>2gOpUZNEg%Zh=%$RZ8SPHJie(86138x=jI}h~#wP94 zR?UTn60BLR$cBz?n=MvKQdjH(xk7YfAS9G641RCJ^Yu)bJ-%?*)ThHV_cz3(hFAP*2>}O zA&t?RxL*LN)93Jt+g76rKqx9vs+=eH`b;7Cs)LVn@Z3yD<7O_GD3IjiC1m}$o0+z3 z8HR#6V5F=~+iF~J7aAfTsYBrpge5dH67{XykBGWP}4KA%U1Ah)-C zd|@exj>_{3!h)oa)pK0Ql)I18-cSH*cHl_OE+RpRW~Zv6CLm*PJiZ~iM^drpVH++J zX7}odn7MpUbCW*58Y(Nl%ZcMAouaS6?rokggHJ83nqs>p{u~OKCoqW(UWyOn9Gp^P z8Z)^@{Hmp@nlQa~`5;+0f?StLBM0!??mxMR&o*#R*~d=(DL$(Z44#$SN)tS|cX3n~ zv`zL8Xdfy)s?n~0x?7b)S1godMo!0?)SUlbJJid z)^0{Mp0D}#PzWB3w9~6;bjdR1yF|-pXDFnndksSGn-8`#T$qU5ZrrrTB7Rg85 zxhgNKY+otkN7sJtvF(TG`n=0~201^~pR^`5CTmi$ZR*wy2?VKHR|I@|fI8r>9#>23 zdiO8Wb2bAJm39@Qj)zyO1BtK#4`rSuGWQ8o4nHQ`e;Ut)&{#pIgF@*-j!G6C9U3O_ z?5c#y{HmX%vca3AkTwhGn;xijIQbrKhrNjHpe4Y`aNUla?OUdv*+@NegviZUf9*HI z9PKe2mMs3F9c5Fm!K4BDM>+T?wv+a~7cITdO#SSw|HIvTMm4#v?V@wqn05gH=}MJA zK9^xm5!bOekbAay@kbItXw zbIurN>_7X@{^!8t9q`oqD);pc9vgNaR(UhdD)lCe?GWG-kj7ypa1C+o^Xjg$7L*1YRiJprIO!ztaMf>;EA zx&e9O+i!EQ(1~K|#Pjj5wCIQ@$KQ86l8ZIf(jBfe*|yGO-r;O}X!Wqe>f;Aft7XB7 zsAUl^lw57}=86@==t0<9d8m55&$~xjQv3;M8m9_{G*+2be@(&vU9y;kXVZy3{^zPUJc>O<^s;)g%hMXu1j zpzAa50my7|X3gisEW8VO9CQ2A9_S0dRXCpS3iK$QuNe~@1&{I9zuNK#9n}iIn_24gsna{) z*x}4Pee}%oW04D7PL8Z-us3pa#vfhK?~vdkQuOX08?EO`S}eEs0tnm^$UWCg=Ctb; zD*W$DDgt`=Va$s7y*M6hzX2#3|CKr5`1N~-=i@UH?BUlHFnMzq&R-$}U1tv{M%uPy zZ~?X>*U#7;ZGvnHy)%A<U^uBLC=Nn5@`AgZ;w?58sR!L%%HpbIkH&K%~ldV5Yfh z5k987rvAy}g;pm^gzNDc*H}x+TIBVF=RPHNMS_j1Rv+JfYQ@~C)Dw7~A*2L-uL^hKQEZa*oJt?kSpsJ?%6t7GuWXZ`|(IKx}?)`ZrqUHy+(f?pCf1etls+YrGn>4CD;HbsNWWLt*fb zFe5rTiE?YzJP!wosb`??TMhmlbYH)IoSwFN?C!FB5z#H|_eHHU%+F%4fB!!J#XTif zA?RU#`svzohHjZvX?nrpd+5N+5bu5HXtPPvT(B}Do#pGuz32@PdQyX5gi4Q=@+pQ794(T{ptji!TW51o(Fl_59YZJP`Oqr-GjY>b?T{&ndO6soiT zE*{!l=KR1%BXc^t2muncO5)1%Cywj-0i;3+9DOg_OS(2`d*OBNA)vaiZQor15QF}` zWxM8K`2m3AR(5}}U}_$MERv8ZUETYCS0~mE@3hZA#>MtpfaBT9&;mrvi5z1|`H(0d zQwiz?ZX-MGn%{eW(|QU#B=x`kA_FYnxs!2TrJ-%~in5(c7S@$tqnB;-w>hEBUXB^p zNAoc6Ri22?U4P*(N6Q+w9}_qiGi_V>T0{LpEpxD1bwzO&h2drKk9$q$2BN1V0hKbFu6l#}3% zbTfR72e;oJl9GP~0V?w8gC|ADB{Dh?+`s!VsyeIT*yUj7RUncU_`h9y%$o8buSoQ3 zE9m)*3dOLxBI-Nq<*WSv%KntjK7Q(p^{4if)5nInAGu6tI2#R1W--&xIDUJlb%}G= zV9EaS>P?>(m*X-I1~&N54XRu^`8lHT(XkJXJk9S@4p1Q8Rwe{U7*9E}@py4#b{ zAWTn}d{jvR-%T?H%2ZuU4fLp8*M}RnlZ!wSW>~u4D!nwjJ($}o+9xmSW`FjkUODEn zx7|b=@cwW=b3PLZt7Y*U%>7+g@i71WCF}c_BUK#*n3qKGMdq)s$R>6De)PmwL2toN z;pcf_7qu7ja5|b7%dW{|0mofn-Z6W!T*6Q7cX2s~ULl3Ga0JVox!RnbXA>cp^Yggr z6W4`TRl!bCvwy9(jGQW`{-(8}Yu9Lh|L{p4=|g>aW$d%!_}`%q-P#}cc4Q`e*a5iw z(;ww!`~}=eW5dZPridb)f{Q0kB~2DtHD}#qO8NKc{i4x?SoWWA<3AjZUCaKz()Rq{ zqH1~$Bg}LkFfhmu?tsdv1a1Eyf+goQ@J9i2-ye?wo3l9fvG5;C(ah07OfCbs902JS z+e0*5G_Bhi07l2P%4Nw1(}1geVKV{zR_$qkKV!7)xG?C4JqBez>Qu-`mI0JY5a4a= z1C{}NwX}+o8$Zry_u?@3>;bf=GKhW{9%Tq4K(E*Vy=uo0j3`K;NAqGCky3%c|Md@d zZoT*p_#0x-12l6WCNNwtH<$WIF+TEGP$x5# zSI}!L3-ofp%lH771tTy4SX+rYn!(GvkwAV5+L3#Rb%K`{g405OSB7Y4)XWL(__386~7GPQro0(^* zL$#;199`+Jco}V^Wh1uSLMX<42Fzff;rJJz^X^1nY#9J{(E@;GsY;KC7a<_&dIk8w zmVp`#sA|T=$Vf$C`;T;Hox=#Y0;y2IrqKi#q-g6oAW%*Kkq8{3&wzb&+a8eRE~D!p z8T5{lO=~NV;GV_;&(%3j z>0?}Tg8}Np%eco}f#m`9q7mh2#QaEjX<-)&%>_L_Mn)o`Iv!B88 z0u%8t;K^d;kZ|BX!vZ2=X{d1O?C*@O(wvh)j?%ec2ygJr0SSMuOa>S>!dOLbdxGGM z0YC?e(^pSvDme}24c`U}XFCn_j%G&5F9Fi{*uPw(jCCwg(=Q!FzQM>h2Rt;`bD?%( zl%SjwL!-sB)CUp31-%9C5empQ(H^pP0`%Szr~>RaK+n@kz0^pSq=5%6=R^FA z{^0^D8dX%pPS*bXH)(Mv_{{Ug8!M%Kno(SGw(XbJ#RR}pZ+`vrXnJhE2F#gf;**2N0v{z&grEAq2Dbmu8uL&nIJskk}5b zrA|~aG&msR1eY-zFAwAlwa)Ma(%BA4(@Tle|SXQTg{-rc+IW%l|Ct=Fv;Su` zq5r;J{&!-S{x>A27r0()u*qwqw19C2ycOWn@qct*2R?R5v?5TLeEGnt#3lC%)iQgP z0vNoo1=@T;KI*x5@&E3y1U(l@C@006@2@c25uoi4*b_g~wWG3Y)KLiRcI%6CkEDXamGhum9)ViwOfZ_D^R%vB9x`2w@!FOZ_=`?|)w7peq0M*fi7h zsei?8x6S_NO%cE74EO1hOtzd~$C=6u3j05osOrtHOv_)=2WMh+{`m!PhA4`o@Ia9@ zdKTuCUjYJ}9yoOWmB4iN8%T?~pm{|8?Wb_-JjdH-Jqo!ZISP|lEUP8ux-HuJ`%~qs zG>w);o{Sv6lQm4~xf_u}YwFDp%17UGGE?DNq^EDh0Ge@Js)l~}d|5y)F}in86X#`a zZYG&)DAtWictO98VQuNVDohUYdS-4yE-lwFYF=pE@9NItEs0igW?vkm%Im!U7V|cj zW|MbbSk>?x>Wf6eM|IQY)|o$!5#vkWJ9>OO_0%caYBv2N@vW}Ylx3d;fgTacCv((! zhdna#)XvD)KN8({@PmhL;xo}<1HGYwhE2uP2jl%+y^tM5PsEmXV9dZlV4d&ilfG>B zKK{OWLsrN9|AS(O6-x*Rjzp z8Jz+SsiKpmSMK-s=I1)p3ybTLiD5C9I^O3>ob9MwPFc_AY9M?j`9(Tr-NDu@LJpVh z+XKZSy1J@qWoyFi*T?Z{a3%X@`*m~Ec?ze~R6hng>fKRnSUU8vcu21ZMmj&@e>%Y5 zfLBpi?6_b0kVmX1p(4wr4c0BJMUuR6hqO`uB&X4?eJ;?#<6P7m|C8fcb#kmjqe65< z`(VpW(NX$1N_}@f_8W9{;Y;!3<;jPtL6$luD(j}~PcpmZUP(@@kT~?dY4r3i-pNFi zY{0R)i!s_r%+RkY#1^fQax7@#gZv${tE;}c>)M_OUyQ4AQw^q+@M+YwSz;Ld^IFR9 zbVdNJM_XvbAx7?{th$=%+x4HQS|*BGa9(Ar)%kJ1{^<~QFH+^L@g7(X_PI~6ORb?? zzDY+W5|eL4r8W?m=@}@wDE3U77Ka>7sYzU?NVtajsb*oRS|K)G zHbFYL3J#zY^o<4}1y9t!U5Bu{!lB4z;h&aT!YCE<&A^m*=iJZP&gcvxKB)xb;&N-e zs;jlKrz7ANDVn=0xwjIJ=Hq?dLeK1c-5_1;vrrzjHg1jB6)f145ei7DJvVsWT*~Fj zs6yE6#*3b-zn~$gJ%e7@{pGh_DE6u>V)mecvsU|vz})u3MB(~BE^(0e@1^&BY-|xJ zJWq6!t(Jb*IK(ouA>>xZY<>!7BfEs-aSl{7$j?Bb3Bx-@*NfVsHlTOLEHOLGn#%qW z4t6QzxU~AyTbe2BXV8O>p`De(ZwO!icu3RS6%;8`t+k7=ZwciXQX=+WE{KzB4C5H; zeJ$Vox)IgBHZe$-NfTM!fPA5ZcO*iHywKUp-eiG&X1j`4l(aB5QPPr>X(Bj#e=XlR z=d)m`aGtZeSoTJTqrUf;Z}W5U0{s>RNWl$fB}J|biRy9|1XMW479(+xVDG~3z(_Qs0h0{(c=aW(}2eo^Q2y9nqPSCGW@y0-CmgFDUcd+PrMOkG5hp4%PO0o&Q?w) z3}TE`TMB8CBVc1~+$Lr-bD$?SmnYO8*L~32xEP~Y6Bij;fE7b7G~r+s(bH?NI}u(1 zyFJp1b1z{PIzhdvFOF~hXCt~~OXOGnfLE;yZ|^!s^gTO)oxl#>{PE#V!&u4Z z?93GLg40cz%)EpBlt_+lvSU71Ci0ELz#mYglY<5HICYY*M;*nLzc0PqcLZ}?skEsH zYg_G5+8MbFp~6@aMM9ah!qm}97$jzVL?>zx5#fb!{d|mBj7JX#6SVkj7~Yh?T9|J# z;FKe4&fSF#HgNMY>bjOgTGP!IboV^jtI}V-2efCj2lxa`iL7aRR zknMs0Q~A8ioV5T=znFFbOPcgvVdNU>HEd*qzIWZ zgDt)$67vp5E zQGF!;bc08!F|%G@d%YVYVXE2;+NVyHqP?gfT5HZnQxN4^jGo@PN2CV|tHD_@#M z4lf^o?a9g%aI>C|J|P#K+h49bsLgl?7=ElfjZF4YVX-S1Td@3^`9?Q;(H=FlvD-0^Xb5CuNjPEr=!EFWisjkL`0pG` zF^=C=pJxUUhs#gt^5Dx~j>+ynRZ~g*Vo-(QjjBqWYB~yiMKvICIO{+nB3Kj0%L9+G zYLOcAyJRu06-9V*4Qaw(-q7u3>~}M>&V-8_q5;Up!Sh;z8N%!eT_Zne09h+OOuO+u z%;9Db%<~I5wvOU;_(z)nCZ9%JXr)F;A)ouOp^!X7IasdH+#{F--b4sBj>S~xf~gpW z7k^9pdt08O#jWLFn&D;13XIr=#Jgxn1h_KDVaxr=Y9iNE3j}Rq%Y|A|ji(dHx2k<9 zF6wBx3qxVjEQxR$graNIB;y3(&QV!iXlfD`E#L|jJ-?pN#EL>PclE^a5BXwRqQ-vV z;m1)-s9#FITPl0#YbaM3t+cCQ=LtB$QRNU;qTyV6rEh<`FKKj*lqQw1z|;S5&?cK8 zA7o{GJCO#HruF-pI6pf_-WFu-qx+K7R{Qy%8!D#BC@~??&>l;?pRUBg0w)Jor`o`y zuFuR@eIJ+|&%E+3@dN6`xRtu-T>1%9_ER0o*iyeHe#s%MbPqe;% zd{cczrQcP`v{kB(d%T_<(Dod0UhAf~q%N8(I&n`=!>&s!>PE$A8zDTiZ|och2jX+LJSHNXGvCPUZ%xCJ7BY+|$bnZ|0=*jHxMC^+4KHo&J+2`k#bVy)lZQNHfXqK`Cr zlm-=SzJz89YY!z7;iB?32x5}r(F!mX1nN!R_nUsRLIVyG(_zQki}<3VC6H49Jci+G7Lt5K8_kuO~K6JiT|oS*haHnpL~wfpAv`yn#j zFNj)$uqVD4U1@v}m(Pb%Uy_&gJ-sLADKaGro@QabB%)!z*kb4|8@JRh3h&b{?8tjl zNn1Jg`_qt`KIxp2@w@#kLi2=R*!}QdmRFD=ru>gbD>*U|VqjMf1xz}8COT&4S@Y$D z4}>+c(%!J>Ig8fKx)%(dJY=8ra0y(DWj5lp5kus9@=EroW;M25<11zNYjQn0TH{e3 zj~0OZT$t2JerxL=UKnE-Z5$Pzi}!hyFN3{b8T$#P5HmR7;2@Hpq8SnR72|1A-Gjn$ zL6uODfwKc=0=y9)O%fKaDNmoT;pPyPlnWJ~zi(`mpW9dbzO?Mdb(dX^ zJ-f4%*JS0HkdHm|Yp{v3yebKz-LqfvzHFxm-P6PW3ilFgZAm2G1#jqCp5ZYTE*j7I z<-C*0JWy{|=y&2jkvL+oh^*aE%z!sJ+pXUEH>1O3S&=% zmtO1+@DEcgvKOkct^PQI@%yQchiOCUJ|jJ&QOa%7Lt(82lB61r)_ndsg9)YV&Y$TL z`K*;Z0|!6zM2Nm;$zHn8*RdY)5YyaGa;zx17?VbdE@R-U|c zQYP2accHkKn<^gN3Ip9$dcy5nSsNWZH+Y~dPfzq1tjzd*<|!T)KOK9WPN8SlZU$D! z^KFWbM;w{DG@~o%{SuxUoj5HuWQkilw1SzC(Y)fq8OUhd8N#}`SUN|c#R9~NSvyK= zo%o8{IS_skN-uBCUb8%lp~y(dz71L$>gw;={G7{ucIly+SmhJlkd*j2xon7Hn2Y`p zB8QvEja(AxtDlqzlU9}tJBsY_NhjW_9-;K>Bh(Q#qflYy9Or6ZL;?VECTz>X=p{0C z1wLS1`P`5^_|tUh2>mv|4d6g81;rOKC*?AqWtGRca07>QJLlN_@oT%{JJznXpX(}w$Bh#>sdX_vuG?#|r57{W z)4QRC2Tk~k0r!-3Maq>&d6{Bmq0ISlqZk_(v(~E-8>(JhSIdk~5>QLQaP5VKVfLQs zoh(PO^waT%<+j#R9bpY{F?6ujyD!)f_l;WWmz%HgF(`m|cAaN9<|W$d$i1Z!&mTMs zqMVXEJKKo8Gz4%$57kn}>7_|j54ok(@gCA=-o_e2O9pFIUy3EGO3t< zs)4)PP-A@T&FN;ONJKZR`>lF739jqXJoJNv$-_MT zp_y+k>s^Jd+(_$uhS*Y3lxXzba@c6EvgW0DY)Y}*{Mb|~MlI#HFA@}-E>_pVKACjr z=3j4zT=%neKSz#I_W$EXf`bl~_F8&P_J%irurIs>U~?Vtvo*W@i@u6CS(^4)h3(9; zDCM1ERu4+>-C)-66SH$mU*0MR6lBt^g`8f#ffXgrf#XD@El(-?KuUI)>ay#*Zje7X z;*Z!_-!}iPEL!$D2st?6L~Q4;z_2w?_*)`Cy*L_Eh3%zjIGUtm*)aLv)H1Hq;iUxv zshN$cE4ZS@0b45DP8TY~vdb zeP_ObYD-xaSMgVRIna^$t*3s8Y04DXm)M<3o}p>Z3c~#2%F5=GE}lm(*6Ti~K1yp_ ztS~z4NPEZho#L$8|2R!gd~UCI{o!-@t~JV}G)`;a`MH1;x>9xz(TU)*Cd2iIk z8-HYNY79eM@E%m?euUPd5lhQmt`%g^)rHU*y*)j+i&7jaMH1Cd`j%QF*1(uZx0;kGt4AL z=lAxfTMl56mrwrHq{Andto+l?Tys)^H8lmt;vuy!jDiNVwYCgaI`JlXFV&D_Rp!*A zk3tT1v0XuZlm3n}*%7-JQi7Inoy7t~aMQ*qC;a8GOJBgW_#B<(6aH4*I=EQjT6MPg zf!A{Ls2(R%7yl0ZD+jMw(4kb`=ZkhREZV-T+SZdf?;S|~Y!5PY7Z%+2<={T;IL zPtM~U+0S;ZQ+a?so)flRq8Byg=0U1g@|K9_iff%lh4?L=A=XS-k`B_qel3}~Xd*a7gHjK10BIia7TW2K7)mQtW z!W3g&eq)sw@{4hYVg8d^m*tKwZt3vwM&8SA6I&t01SwhxhxKl>9>^z@YL|@xJf?4j zZ$C3OfM6}zv>SxkrK*}`PZqqD73+8wQ5bmqY5_I@RP^&s36XZsmJZdq_`8P4%oyz) zBUu+v(HAn)h_@4qTp z=0s~`px5UgOh3@HTG!Q4f6@MEgb?J2qs|*>JXw`>{t*4) zqDs-A{nP_ZfTG7gMv}oXv5L_8k=-ft=7kD*m(Q3jI@m*(e>=);GG`mbv&H7SmE%v! zo?p@6k%DeI-qzP#e==QB?onVl_^!FrH;p7zT89pET+S8d`Mpk+_*WF-E0d7Ua(7*V zk-zC+bss2dTl&E}^!R(82a0RVA#G=MJycR()@@uQEFL0_Mdy>3;+?r%oT~C(Mn~l@ z>K#WZXwLg5!#w1WB%F=ijK^G-E#*xpsa?x_Ps{Trnbp)w#4%d~Fd}{t<))|b%cmc2 zSr9*2db=^z%3kkyqR&wnV>TaZLg}u|g3TJ46F@16vY(JeeF`$Mtwuo+Q8*sxfI|Xj zqHT}PKrva}X?DQ}jV}k(FW$;Phf<*17T4Q(cuyd@K(WACz5Ot;#RrRW%yt?>sRN?v z{ZLp#3B|f`;QZ}ec9H_V7 zm3i3a;Gr-4Ll*?l3ePICMrvl)$b5}kNzq*-)p=<$)m<*C`AM0|`gIe&xEoSK8hZ_XP^>N>DX_kn$Fe2!ZkI*$aSS&^ zgR~t;=!R|f@}%dAy} zZ5SZBnPot_S9$us>>B9s4J_*I?PrTvIQp2Y_j)Rk2hInaBa>@Pcv zm~Sdd)U%1lSd7%Jc6L4)*EdI~gkk1Pi%0!a-Ka@5n;mHfoOW-CXGT9(7#XninYr#t zRJraNL!80zYkXRNm`PjLHFusPu!HM-?|!Ttlbn1UG5x&mlsY~jIk#Fr>K`ru>phy- zZOWASj?mLx|1)|)@$GKq(HfuAqkeTrH|x7;eQ&?mD=jZC+!u2o(mvkF7l3(8=6JDDrVq8KEnm6!t_C)k`Xe{QBF=he`FnzHD8--3Ue>q|%znkTFO^pg141~Ww z=l`Tux3jC%X=MgVL*d1x8$HX>h5PX;j_F2|VO%$dOohhp2)Vzyd&+)a5FbR&9gKot zgF6ppEt&kZEzr#jx*!KL%hS#ku22OwMg~d%j3Dm{E-tiRelng2xex=ChlKSBd!d{} zx18KTia7Gk;t9Ce1;9~U20D?sI24n=PT>a3ZHK{tn+~IFVMq~jS&v^X0pSKG4TYqxUpn9I) zmX=__1g3l1ewcmSm(%yypi5H{9 zMJ*oW|G>uTadT`Pki>aR>E#7C`5eVA6yD#R&#a)^_RP7+=5jQz$UJFm$s~V`gvKgU zfTPP@B~o?GctnM~)-X;QICNl2`#n;L_6;pGy-*sSM^N7FpSfn`q0ejXB@nBW2J>vhOt~ZqKMM48>yLI?aVkP- zQA#x8z|L4XG%TJ^Z`yl%Y=5YTZZK`H)ui4FCv;C5N`s31SlUh% znrI2Ap1!zw-W zbyDg}GC!r>^*l#boG*WV?&(5&Skgz`Rb%beXNa{!qn;k$REUa815mNot+0wra@&o+ z4xfj>7GjRI{hWAsjcZV0RA$ZnEi-n9eM80#6Kh%NP95|Oc6PK|4)M@OTJs!%I@ojv zWY;W55FRo9u(GGryoHn@BkrU?y4B5XIQ|$40XU4 z&ir`(3&SfG(I9Lv2${SEkiznvWqi5g82J&PC`n})&8G+d>UD>&_74Cq!v+h6 z`YK^()o5P3jOwElmX-i_DRblrO*phB0-dUUP zc+18+^I4}&cr03R1ySwKTX*})=F+Wli`Bhe_b2`yW7;W}OHIP;rkp>Gijy^{vDe#o zWgjTJsJ!c&%RJFce2G&>iDc07E}&Z(}X#;x)RoDkQ2xnZMmRa`e3Jihs$pIVEFat8n4_Gu`F# zs`VXql{!=9tr3yA^k5Guq&3(zd=gm+H0}g=`%NUQJrn-PM*9*G(l+* z@9V@{v#w+);>TuxC?rr8-~DkH)u>W7q4~J*?Y7OcrX0l)W4iytYR|#jk73wjLRFsC zAHyxAx^ujJ!b>)0290A=s}Mccd8_1^CnUwzbxVf8TMO=yI{xTLvq@&~YR?F4VYZLo ztTAKZ82>7x9nX_ZJ{~4+{z6vh*=9ka5CeET62s(^0WLvmsJuf8Nh)CyvW+em5JC%V(Bx16 z*y1pU)k^(lUz7bc}s0^*w5_qO`-X8cvH0zy#_ZNN&K1H`L@9>&CyTzHn*syE`Bxz zC+ORjeI7H|35cogTu=;CwOXIcuSEEzLJzz!>)DU^PqnL2=KGlQGX+^wf*{lOKp((0 zE~dO!yft>1eO?=_huL8rip#3m6r|D0&uu-0kK1TxH&!m+$~7j`N%n9?at4U~Fi#lI zO6ZbuE;j}m3rswN#lt8;aWv;mr09Bf2Je<6HInR+F9cm3j7iK8eo7J8k(g?D8dfDo zuq;&%n3!H`ixDkY3$>Job~xzQq`fe+Ge<8kmC72L8kS7^2$s$?6WRZz7653+8l!MK z*X^TuIEAdfYgVhS{t1AFRVSne5DN@1Pg>Z^qpxb@HQ0Seoj-S)?%@VS4cvXDAFKK#LH{nKYSmuO{c6-vX(J*DiMu2WD(`iZ+ z0`0WroS~a?cF8bVQW;AgGD};EA0czr3~-M6+r{ZFxZhbfFo&L9X!OwkY#EYCDY3|M zt#=Qy#N0+?6JNU+CikT$r-i;G9eq^_DZs)305iVORgh!kD1XuQC2p@=wgwbZ_wrXx zxL%fgMUCUIWFdC;8|p8IW8WmUiV<}wojqj8iZkDC;2q6sGYVO@z@6H=fg%rrnYeg} zy_hTZZ{=X$L7^*0nudTkPx?dRaLUkKJz>n0l;e6fb-YIuSONfC-gCURW}M;ryUs49 zvaY+q?8!CJK|&C@crJa^igGo~VN8Lv-#Q7_sG$Jt(pfge3w0r*{sD!`N8q2vb7Z)h z^uhyOoV6~uv*Pd`tM;p<-?snyp154fDHSBYkM+iuYuqNZXJ&!r-q{BwY$7#xT^2XZ zw2hS~$DC!P3wwQSU9Be_zOGE{27LA_%BH*IOQ0kCBx zH{qv4YcJ9k?M^9k;%&^#~_J!_v(K)m&=#onCgh0s># ziq@L*7#(*QS^A<<e-ZOi@fPX>MGlxA>dtvHVSn4TYuH)G+|L@@NdMkI0uBpkIkN7 ztUGUR_dr5iBtl?y&roO&suiHsnIOnZh=KTyuH8wN@A`Rm^~>w;S2=*f?-#9D=N+4Y z0=i;a5P5IKz%QTcov_w?U~)duq4Z)n5YYwbO%7rgT<;^;FxQj*F&F{f09WcbNp?B?C^ zuJ}ee!fO%!z%>t#n;0pJ^(AQ{y2bTU^!A_S980bLX-X~hQyM-MHov6_$Y@-Z1YvGo zrYzKO1SpyZ+rIrT#Rm;UUAJJ%lfuKAsF3B<0^$Bvf;U90qOUmf&Y?*CB-_B_{j#Vk ze=kHrxxj0(6>-5c4gW3BMblpvUST~4<|g_sf%uO~{QT zM_oR-3~Y8sv8S=!jkb>$+l@jurVcXl*+nHEp?fKS@=;|P;4hQ~Lnz(@kO4F@1-zTHiix!t5o@|-io8`5eWKMcS*vwI<+K5`XdK59E00KD*AT^SY%_D&81FZ2nR z$<-vR2O?r&8dRh`uOqPC@tzH;w>L%$V1O<+(15i?8=^v+K-Eb47f`Us2A4kAKDPOb z|G!uPGkYD1&PW`1GrUIvnPDW zPrLG&TQ7p0R^|*k{DT4tkC;A5L*)to+Ox3B*GQTwRF63+qIXH9NRx+mhp7b3yZCOI zvS;`trw^XcUB;`Wl?vr4MZ=SVATar^K3B`UK#wwax1SB0YI)7L(5dJ~Blk8nl>><> z^}zOzk5`s`b|0zq1hOsH=~i$|#3;9&Ee%X~pW#dTwXUH-Tyz)UTk!}`8F}%sN^AW! zC&p$z3;r4PSdcbvAiP&IwhZT7H`rfBKmO>KRkpt|B>oF+IqPC$cY4%DTOHvzUO4qH zU{`<1HyXhap-d8GYyNx(@Dc7eb#jw9q}~(5%U`BFMS=ObD(NED8(QA41yHvz@s&M# zwh0MGv)JTQ<*oumR@)gq)0{~p3#1kz1JFOH5LL-AF!Dw?aFeJbR0<-1x2TC$`JC~9 zMp{O%lP2~53m9@ko6-8hn3J7pj+eH0GjAMv{K$?SA;p4&DfPmG=-MKEp*cFwd7S*3 zvCws<0Vq<4is#To%vY3yPelPtUgvu43jyHnOb;mOrD=8^|F&moOGxQRT=%N- zHSBFtXVJZl4qTZIu^2%xf%gw-t5>bv)Bzjvsl!Jb!u@c5@0}b%tuPH0m(;u9zv!?V zbEwHf>iz*wPERU zl)i1ybF)ao0zPBU%J)iWb1bH@h8Q~W#^^`(tp4)1U)fT-(eID^JppQsPhuYb zBQIRk+bxAdV*%kr)cevrM>m8e@dzeIg@F-t(9(|VVSl3pyxjkw1gg3aXN>PVAl*gLrmKzE z+A??6)O1wph5`$1zg17Un&APr)c2?7v&kGw_|Ah{f(Pi^F^QgvHoSAX=DaXYyI_*x zyR!LW=KP?FENa@%U;v}$(&6e6KhM{*mqM$+mK#Mym~W+<%BzY2&YK2YWV*heN#;)4 z_I@VNu=VX?kNo;1L6p}I{OmBhc8Yg0EU=d_WxsW+&J}zcze{HkkNY-M26%*EfH@YE3R`u*bpQWOFDM)YNk!Z!lr);{~#*L+~^*^h=0K3S!KQKdxPd8+q^f{iSZ1E=-S86+G1~~@2 zsHgmLq_}WFr>CKuIjDc6YdFEDni)1cl<1NV>8Yo89-Log=(A_3o!5me{|zle}^mlKR z49R&BVCs1%Py^=ypQ|7+#gOdHbG!tw*%K%%I2zcNZNBx@qWkL> z_$DM;i*oS6x-0Ipu%q_Eea?e`JG|gT(K41!!r>=A!M*;k*)QlD$8eZIDZQ?_8Ad)Z~9XUG{h7950jEz=MT(2ynv8Gr!|iaIrp zEAj4lk4qAF;t~y!VVs0I<%le@_yl?zhBE%Xl5eRHt1rS2IO~QOJ_MlQG z8!D&){*#mR@{lG%GgwiV22Yma%+Kw25evK9hylYLV@h0y&%2CmkC_2Dz&1&*Dh~py zdXPI5zW&f9u+@RsFm)su<;ZK6;f~f>NQ^7d%LG)a6Sef4lij@+>nK%kN5KQ&TaqxF z5@zPzG{W=2P=;;aDD2u|0A4wi=mFQhi&$sad&OE9v3)T={HgBlpplk%f)7*7vIBBq zY{epRyp|gMD1fs+GE$Q|aktFV-2dGODJ-9J)6!-w305M(7CI9@dn7hJUL+!-WFwoM zq$}Xf)zkYS8m(jq+X*lhO(oZO;c0({_SFg{a<^R)oeYD($U$r2eE~T6n?x~(Ins9c zF{>-}lMl~B8X2Rjj%XtF{695>uAb3}_na64r>g}}kSlMYwWz(vFfHNYe(D!gP4%_M zN5u9sA3QJbv)v?h6i)Px8LI*KMOqMn5be>zG?o|zUEm!(0cfF*hf?g}xjtx9e(!@o z=!KZ2alL(yk@0Po{xH1tD@=3Oc3o{&ghKvcwAC3Il-D`(xwP2a311RR&1qxd`9FV+ zR@$%;{22x9fX9L6*heS$H=PuelHKcfcig7-7OQx}z$TcKaD3`gdLuB(p0|b2G@pI~ zdUYsB`+bO4{z__Q{+;OP;KAwSV8II;*UUtw))!|^g3As@)}X>voDxIdY}M2h2VCx7 zl^Ay`aPZ=jQ{3I+t-Y_uO{l*L*TToBH|O7VXsRE8Xf#ixuRKVOrttT$btvojkzeNu zGsh&vbvu;Ys=2PW7_+c}6Sk>2)#R*hXqOv|$~#v;&w0yUS9>@X*+j4kRW!XWq>>t{ z*enZ7R?Qi=9-L-zvc%tHa$1^0Ai&{g09MQ=8=?cy92nL?U|@{Keb+a}0D~@P8haRM zT)>yhUI6xr=yRKGz$p=_P-v2bL0)#DoKx+HTyAO*RxTiig06+m808ybn$>D~R}1=< zWg*~)OryWjE^`8b;{V;aaI$lgi_NkZwq?Y4PFeHoRvHpm(NZ&jL25agi=*)N?!1RE zz3YFm^_Ed>ZEM(Q6?V}U3Z-cA;>F#qxI^&*Ee=77YoWNLSfD|ILxLCg(jvv(C0GdV z8tkrp&OPJ1-x!zQAtT9JbI!-!_3RM=elSqZd#TGH2CJs#ny3rtSz%ZTAP>T|Rob2W zb~s35Y4cHG=&$@vvBr1c+&IZ$GUejEGU9#8jus>|W}h#?A!CG@67+ zg2j)Y4Ao5(H~hn=zr?Bo`{`0)d<{)Z>d8*?E!e)dnC8G(U-D!Pm0X}=q_NtXA|nEj zoHgJyw$lkQiz>;?Km_5?oDNP;g^*FP_itZP>KfR9o;I@^VB5qgEdm=BrS6G)>PH$6 z0s}z%Cz|efmi)xezw79c2qO_dCYPTTIk zjzh}7c2d0azv+3z*9P%W$1i}YN^(TW5f`)Lotf<2HuZEBf3rJ)O^wa31}?U=YxHv! zvC{CVD|FY89$@x*gVG)uvM$zzj)ZZ5zK1c?DFIS^i(>ZPa&AC2##I4+xtJ4pZ9%Pf zbCsJQhIn%O@*)!5vv(x9U!`*Qqdw@q=-dwl#8-!GZ+UJqA;j$jJJ8(y#jU8xy5!_B zYcwuy@)H_&yULHcuDq zY69l58K*#^W^`l2n={H2K}LHV(>`sk@RxfUGNKZ*rVwH;OsVekdXco+w{T?b?U%Wz zVSc@>0AiA9&qkJ8*K2rQK$4tfOpgkFtW)!eR;GE$WzUDB>t#ThGDK%LMZ85(s*eG7-M`L*Hv`@Ol~g+T zH>tEc`|l-E{%?Q`tDu~&oEQMGws2s9Exfn>50A*I0AjE#Du5A~%Sb)lwU+qyzcD3| zeHhC|kDjlI?xh_Mr}M(7-O;HA+cZEifOoO_j5_k}lsvFA0p=h8&v4y~9taz%yhz1i{;w@0Vh&@AZ3(Ek|Tu09^V)z)|PQ zwPcN26v*(V$13LhPYXz$a$rPfDR$#Sqt`ji*5WSjqq47b!Wf z9)+O@`RETLs5U@io&romP)3ElmJYRXmY3#N-$4IndjaP zivmQDP@kfW`*5jrbK9Fi*lGrRr*qb2ghormioo9Gj^obuZBK8rMEfUtU>PM1@7A7h zSj|E1yqnY|#GFN8%DOSUkAQ4~fX3TVAQ)BN8W`=wyPM)=pO+z!S%S9;%zIt4PxUt% z_tcm}4qG!{>{TS>4~d?6%z}`!h4BzoQ#X&HW({IqW>YHQOo6jOy`Z8F+$u$qdjhq? zqu#zW(VK1$sji&ALag#Q)Ip3ygwuR#D!MVsn}hX$0}3T?#vw!FE2 z)?&RLQF8(k)%is>2D?Z~@Of(CbfO;fk)7O53-GJ5yi9GmOnm$QGso}={JYWB&c2K_ zt^@!H|2i1mc6PBq%r6e0cfe^rX~bnn6<4gvaaIH{ys;g*)kY(2;Dp;z*ilAj+iO`+ zPfAi$-(SakqyE!G{>&|X<+5`l)4u)Idw)XM@|#A5-jHcX!7{uN@H(*+TcofX$CI^- zQdn~VT+8jG;%BzA{kMYtZ6;*zukAAk88t&30OKzS#|?Ad_?{=CMtGmWmlXKv$a_JW|Qh`U@0YyP5e#@2y^2htk; z+d~78z=EA!mVkJzeKwGfN~z{lM-EK3dKYrpm#g~y+c^Gep4~6EjNWjRRuMbo%3CNl~S9e zZtiDAaUV1~6@bD1jqiAC?X=(PsQwr)9Qg0wVc)<&TC%X(c=CL{Q(98EyH|;Z!{|S| zwbRY%&4ER!g11Oe?N9dkpX;+#M0SOzX3FY4kJ}(F87$c&Xz7%iP*Wtc~$yTDRqrP-L$Hhxnd>vfg; zqe$|63?p8INI%W=Wsj=+xp7CPyB#IXI72k9T_(QAexYS-?v%GG7U$jn>xv4V;oijN zA7ZT0b=2$d8YmsjGMJuuKm7Nf3pkktie`nIrkR%EwPah}$BA2fNIJwtT)-R?}oJl=~qgjE9I(i+}iU&B`o!pjBaM>?iu0DREr#^Mqdxz6veWqQr40@A45`L*>5%J%BPSEALR#L&x9m z(?8|=v`nqW>mts}5}hg|E27>3>)FT!hkQ^!CF7K;MzuiNjV9R4a>;a$IL#dhu-x#- zot6A?FVEKh} z?XYaT-1Gyflf;`%bSxr?07g z9v|I!!*%M#YCSk*cEs{4!)uI8sk-d>V&WHeipxr>#Uq9WGF%}c`^GXB{KRFK+n?4Y5 zXS`fS1aUz{p`Vty$)ndMh8`>J=WU4BzN@HYLJUW3FeF~TH{_FZ)lQRrInA4tduchs zKoyanaq)4+OPR4AiB##r6!d{{-k_=N>q2cz+!)Z8>RKAb+r-~oi+n$8J?c|=O|3X@ zZqwG)NfX-FKW$JmV!6I&1f!Xx00ucNHNS?OEE$RUd94XQjJr(PxY8@@6p|QrU0@lz z@*e$lnzvDTueP6Xnh8ZY#BD!&*_9n4M64=_CGZT_a9R%Jbpm z6G?a4?iZ(1KrCHul7(W+p4|Yz?N>O z&_*b@dw)3-M8?bcz<4J?a9UU{nV{80sz$)t@MCwjHlJvUh*ZIgkv5$z;=FE?N405X zd6t?fG?TIQHEUv@V_uUNi$)lnec6!o#OHac)=p=tN5orJTBZ^S-?$_IhRKn%?)SH6brEwLOH%OkBv7-Fv{fnI0 zd*R}-6Tx3zMm)OWPAL1z;f!Btj3srTQatFbD+M*rk(xX)%<~&1oEvC^zi#! zq#`~CO;*1s`Sjo8ofl39M*f~(j~!!5gce?XmcHOMp&z<8?yhOD)XXunja=C%${8#~ zRjrFePkX72*(!gp6MjR$xQr3u`a5n14StoALj?5}sugl=WZZ`QHH&~b>17IRLz60Y2&#}|5`OaJp4})!~ixY}# z?~2GR&?BW-*Xpiftc}TJ(XygzDI9DHpa5Q zyy<&sG$kfQABdt|hbqFh$#j)E-|C;e|LROUjDdL*JYGq9L+4Q3Mnd4DiN|`bw=sP) zzyhP4deX(WO*%f0YVU#g;oIti%5@Mnu|ilRWuo?-+0lC<+{N9V54*A-u@Xmu@HHYw z_?m`VB-&~<7Re?k`TL$kj&zP5Il%gCC|BVHh;;bcnz~MfE0ov+OE;*XW9++G#`z-u4|fdF|FnPz zqtg;CsLT%?;^Gf7)jm?gTKU~#+WmFmG7E3Ij{1xyz{ceNH1W#2ycs%Kd_q3|_U^uO zH9_$um%0^27lo4uu6@3C=^f$BCN(|Wxu(l#!A9X|^@|nE1jtd~BI%OMj~JgrhhMu1 z*SI|cgYbH81+ZVoDJAG%wYCj@NS8gJ;bG_RQ*~JL4ZXyCb4pCo%B&m8xnlLGiGGUT zIq#bsU*{1+DrL&C?dRJ6ym|f2d;4+IvalUH8q{wA3(=SJe;AzNEQeL0KZm+93c8$2 zzKOdNW%jPN9`3*RjtJ(11Ct&Z4iNOi;mH@12hykFAs{xl8qLHy`P*1u@_#;SkYr?s zUt@(=Xse#f#tD;A+BsSYe?q6!GHA;_BG?}11MVLlBdFo{6R{tR&%9j$EJBU@<7~B> z?;)2uE^cJ#An}&+2P1ugC$9OljZA^~&n{O-`SQ9+iN3!la1~1mf3i^b-Edk(g~+A! z@dT3y7QYQf>kl{S{UBmTu?zH*KWpnfTVECXWf@5*!s!Fl3H4zlQu-yg08xBl*#$@1>KNq&-gFv|#-)*4d;WHaevU`F8u^+fyg_&*x5I2YfBS zGY{}^XF`TUrdDSTG0B2@6QO+^DfC@qcH-eG!?GOqkqs)5hkDB8=y`#P((XXrB%QO) zx6l~(ng<3}Kj|7kCX7M_Q`NaIGwOqs%_{io?0gs5X?*oHwt&cm+p5KIcZG^l0@MGW&2?DMk65Y`T+wzzEQ+K%zr zP9YGVtF!IKvzuP?75b(gIugNY~=YON6 z3m^x{J5pf!+flGB9;-i9mW>4z4WQnq^!pP=PhVcC$C2p*1@cqUDAW#_u8V(><6*1q z{%&EWpFTh6X&l^)+0vfB|Il9ems&pULjzLEMHga@VkNyK*7FJ<72ENLO{YeDPOHDK zPSR^_mHtq~ZudMmE6|RZF5S10y7s#yVZ{`D?>~y#BtD+o6qFi_fWHn!@IT69={;1g`MyL(;4n-|Jj> zZ?I->vKEFvb&eViaXOnT`e<(`NTLJPaUZ=Inz_@y8_=?o^1#*5%yah1#BKDa=X}1$ zY*G6+$EV^vAtVsw6g+(|cRhsU$XA9O9F%1W7C~fA%=-C;)OWmc;|VI#g{XMFJb+If z#{z?TZZGAVqOb1nR2)bjZFA$rVQlgr21Cj7^)=x6q)U&^Vx#_vGx*7`Ux+fO_ugjU z%MMg<_|%`l+DyP2d8;b`kquaG%jmo|^SPXycFYCEIgal8fU z+amPP%|jx0)i)5?7nBQP1pmRK3yE|>-p38V?MSMSNk7oWcWswk*P*mY?@`pe+4MJzfqMDm_$R1r|D=J6mFsrS~}zj{Cdg;QxadOTvBg*QTTy z`@pPij8mdrC!DiMA0-jcS~q-K2-*2q&RN+b#CCA)iYQ8i`JW3XR<)`Fz6k5TKUH@X zpJs<^kd*0fU#iFXR8$eLLQAY95f<)=B}OQF>tV+^k6tE@c_}Nzbd<>4&n012A(6bN ziudj6yI-|VoK>bK5*KoxzR5aqyT{&SF8k%PiJs69dnhHbQ_t@k4{|u#d6sRG=b^dB zul3oEItUK$4|>P;I=NNR0=0xa&o@|K();pYAx=FZE14eIttk z5-dCu9~7slNQs!_{qc?#b7o)r#Z=62o#X7!%Q6Qw%hSq(Hc8a($)%u#{|-NQ;NiL= zT4dy{M9|%%M)2A!zQ+EVGSDxm<>L~Vqx6V+`|4!lpt7fpzm`9C!Dsxe)I|{dmKI7# zA>luBNR#G_EMMGj-nB&qtw$A+a#*6q&!)^Ax=0{`?YJo;oqyn6O0?=$Ldi0+c!H9q zrBJ5j@NiY5SaXUdCOf`%^jGqBj88AH%vku;e&IUEW&htmQmx~m>)Vi2?S3*{IBHav zS_XuN*c0s{uc=53Ik(rY3@Dly`$=%qtjvbA8+sM0{77_P*GJe56Ca!v1V4UwtxIdU zr$Y%UL?ZirfH5-(cukG>Bio+rDULLnnyn9$rNVrLn5@XW6c2RnR4IwFO~+_ll)yn- z=M8sIjqRM6f0l}F5bmaMq4^2h#K;Qw8Wd`DK`}8QccmM3hzoVdJ*HN$d-tXMMzQ+m zPIk0#fRMaQ-k&a;xIpA+i2u=~0duD7KG3~9QSN`c_m0!KZ67fxIGcuq-DUFH0PTty z=u4wN+ijoYzpi?dUms-5+Q+Q^aIGo`I6_S`XByhlR8M+psSW1$Ci(mTp17OnA>fX< zw*3|e`m5+j+SfL3)63|I{LIWYTr0TP*AD_7p&reK_pWW;Hm2S?Sw927i? z)Y2txcX7G&f}9&?mv_n=PJ1H`Bo7A0jp`hGSMckXxUCn2Tq@)Zp%U|}Li|Gc%Wv+c z#6u*5gdVwH)!6Ya1o&^ST<-=@imIQP7gLsVGxMph%`x>Jj5b8^rhqxP)SkZ`5N}c? zO>|pztMnntWMJ+sr+o;nnm~1K)cBh?{LQta#O~}sk&MWlk&I?^x^T0x{Zdl$e)c6% zLwX21835Q*f3@Fc%>zOTCdUph;(fW5#Hgs-S6h`&l!Z1+Bw6#U3cBWtZ?kk9EW0x| zzLX%y45YY=zs+zLKdGTf32y9WIcsY-a4^H}@ZkD1kraH6Jf0)6yfiJVzuY$JCDXm% zHSNjfgo@`i1*B#x@$(giClu##>}2MJ49i$ArisDrddm56NxucWq?seWqa+P>pbTZ@ zU-5Y}@rM5mhww^9PqrHo6&SI!gZtCpv4at%mI9W38i*&v&95b#Cmgk2Flpg?RV$cm z?mC_6*>HN}-eBev_Hd`Pu{+*xzyIF5e4k++y?}A0A zsI2C9FbOt$JJJlOW1mVn=-)z{RJvH)U6f<*R-=KZLH7>-z6i$)*qRh1GjWSrvdMCz zlcev7qqrZsmJJWQ*=jf<*8k8J)F9ug7UB!3Xn%c<5<~`Ku2R>S)lJ-MzdDpv%~_{A zF4U^LS~r-uG7nr_>=84!3nPgQ@CWa$-L?1&7*b)5n2~`Ow`lW#MG#zKr?j>k3Y6oH zV(sJ{Zvo}0ADkpHrzZyru^*a1NK?-l&k-z&54HFelp?;TNSeT7mXgED6?%PBpRQVc zbEh@@>4Wx#pBRzk&qM)6kxcp-eYZdRY;G1WBJi#@lW2nOKCkBYmL6fBngh={^fa=E zOXPk^yl)-DcLQapuMo=s zMKF_{P`gcoPvYT!aCYJAhT~n59^B1G!nBXE9oQmc+Hi?nDut_rJ}ql<32_4}Le6cU zUk|F?PQ#Br%6mlnq9nMFU!LVs{~<6NmXE~`dIBiQ?2?Gs|0v3D$`5p>bS5GY<}T~~ zT9m&GvH5K-wf-P+WZne%sj?hH5zu1ulaO*Hf;|s{ZF{%LL1P&!`*%@7MurEwFDdb- z8rVXeRqu2s3w0Jl4D;n3MnTy&dBL~E+ppShdL_^@k7>BRuIY3I{PR3lthnE`ymOP|FEtIw#cN(_Muj(4*dGhz>X#tdHg$yc55@iMd$t zUHheHV2$#>nSJFS*f6<~jyUeoNc(FY{9M84=QCFN@uI<*ixGOg&Mn|@iSR$=E_tv- zcOQn~9d7?JJ2}lVWF57O3?LLgUzfe`+!`~SG@pHR-Ea~=-1_Yz;qJH}giYm<0UeLO z*|Ir5xQn0>#T?4Ui*CC}7!9J_Y%+pInPCAPCzbZqSLAHU(~#qx=$}4oOW0I}muU0A z!MjD@>3iyiox0ko52k($6Sbet>!uQF`zp%HT2HrkW*$H4GUT6WSIqLpoKuu9dgtNK z)LRc?E|RNTdo~MhxQ#x4h7=VRZq&GQek9Rp=5@ThXx?oQmsDV<@|fwk*}wY(mfUkc zT-o1W2tqqKl7nCojFlhbjkH}an%sSKhkTv$u5lJ5JMHYjPjwCz9L@{ud|I%^|NMx) zw<#r)ec9!NKgPUuN&-!9{W`d#2u@(ZyzGfwE!N`YoQTHf<%{21b#FBJ>OJ)4Z8vEf zW&!guR*~a_M$QTNBUCp5ffG+bp<_oBmz7XhE7s+1AXwP&lFtc}uCaOAxiuna=CKv^%)wwhFMSGIx(gSkP6z>{gP;F@Xz?5rY?CP!!N2Ar2#(j zLSBC?hGzck-Gij#Z%B@RCZ78P7PTAN70!lj`dw5BWB5SB!nf-#G)O$)$^umn-kwbh zKl_Vu76Oo=gyI;vOZJQf_xn(LPlUrY5yI*6`D#V`z3^!ovC!5`)#LQsi~Y%@``$~g z_Pas{nHqU{@{DY2T#vx>66>OQ$HprQ?7iW+Z}&bcBnBw+^9QF_sa9aE4?}ww3&0E5 zkC+0EQlzc$DGh$nGhOo(;d{&;v=dTy zsfHjThiNdPw8xy>0dA4hoeiDWuDgk-Po&nh=t?Uz+qwC->ADbc?bgrkMzVj!R^IqE;h>F%8)!@cN{9@RbWPw9yMYGR z?aZc%Pw z7ZfvHSYh04m<{8{kOR;p^H3dcKG*PEt1oib_JQdelAu z2PcM%ikg~UCd>aE_}(Nvwi^RM^Yx&M^=hs|x0#xguc9Dh>#ab?ZNI;xJ$cj0sEWzd zb;94X`(jtitw5_0yre|*_*+st#`;5pD2NLgBs{N3t3Yz5Tr_A{qqkOuRYno=)SY1u zbsxl6rzMus6`=ca7}0TPJY^H-_bffNsHgH4QaD87Z(fcma2WELE{}ed;@U}F>|oD?~Z+a zf`;WC-&62ozO8|cEu$Dq52mK3_N$S1yO4mRuvh1sqx6BYj%0suVu#0pb|9d(!ji}5 zc-@5a)qNRIiS$AyS0dng&&w>}wzjNp!7XC1KEjPr%szMYWGwB9HCbJdDTq!DIwnHB zLvU=gaodg(^|&d%;e^CYg&!mb_3tQpV7~t*?aL%p!DH3RZ`*D$CF2bHt>Y(<)Ct2; zOAsJpCS34n-lm$#w*^{>`cL;tj#Yn%3_`QFc?M6>#iPpR0b5~<%~j7xX5FVV+ix)G z^~h@}|J3h5k@W3NKj=ndqwV@c+cVOUgA^ZWQajIP{& zMc4-MnxE^W`9&_@2JleH&pg*X60?@$aaT_~W)5Gh09Mw+QnY(+xCmkK)W>`ewxiNi zHGWXbUc4HXlhDUTR93+0!^I`3*&9Re*5L3S+kKwfQ*Nk;({@PM$)sHJ-Vl$<#b<+! z=*JkCABF{O?)wz+thn2vnb&pfQ(NH>X}a@o=Hfr$L0mn0bnS<~Cwcd-k`?U!=6}6m z=2ObEV~B9GfB#D*+fD$VWNaCor9o`}oBs`gz=a(&3S^Tmb!#f*#ZLc}4TTN7`MM5_e@=B3YjEDaf_y{Nu7HQ?{m#V$GZLB8PC@7E49u zbQ#Atkn#q#nr@LKkazU~Zu|2)?b4_bZ_+*$9q!^_OHFBSOZQq2H6#A8==>m{oa>ma z{L^Yw4|S%H>sadrQqu3-(UU7nBA_XS_n(lGiazws*|hnrtOY^P_3Z^L=zB zmA^hSxmnUco8I6^j`hoy|w7g)U|9L_(hoH-Hpm^;~384iMJM?YeXH5`HNk1J#Fr8 zo;v379gVZpmRWj)){O?pYcJYX8~*F(^qu$Kv&3A_yv@>;wWQeys;_uo)n=PaQ>&R@ zOhQNsB8C`d(C`mItf+Joh_`1RGy%=xtvAXNOtvZSY8@^FVNRaAGgTTeiGS||dv<)v z%fWi2=gw4$kLHzu=e(6heMZ2A@_R)lzDK&_5R+o%hVfyxW)e)lnb4x`nvv;KXLI+(FrbgR0>LE!M_A_DfcG^dF5A-&8f^Y5(v*N_w5;yU zd~OH*K<#j~bb;wb{~dY_F)F=>`ff=~dfUS$4FV64mg*94eBu*%u5U8%!a^n*RC>kF zIeHi=Dd2!IkD+R7S~4^%!q=hs={RO^{7QMyN zQ-onGHCd*LvnCjqJF_=NM2dx(vGi`qe~dKD7-$K4PFod7L-`IWU`2tat2d*PQ$txx zh=XV+{SQTJ*`_#c)=`W0G(oFMOau~#Qr5Hir=7&{?PqoK2Q9aC=7G0ot16s}ZZU%w zi#D*1bSDYR&d6~M@suEkz1nF@jcD3?x<6ZvM7S$AbqGXXbJYIgUV+K9q0QdBb7R+_ z&$%$V?Hsei8FYAeZ7KwuW+$8t4}NcPmXw{XePX-41_h4%&iXBDNaR}cQ>%L9EpEKC zYJs}+_q@j757hnZq%{Rv5FCm(4#4i{Fwr^Mcn`r&G3KULhF8%{u47*N^S$CQ3ohX{ z^zPzm?J7)fxiJcCOs#0A`j=6cwr-FMp+8TX+|M5bRcZ*%e|nyt4eg0m&*G6ERKHks zzsU5ODfb$t9xv2DS`b#b;6D9BBjma83$zTy5;qX zTgO+ms&eGV-E5i7U+9LaJzcWKDXuTi>aPOiuDF^vuv);k7*2lsQ}f;7_B-mlW7RYv zC8ty${dunuvsp=nxc#{TT;$Y2&>h?XcLbs5pf?w1VBXeM7<%NHw~t|CN5WEn&8@(= z*B~aj^_VZ6U`hD#HCH|RMFr<#1+r|~T->UAU`6ugo#i^1tL2#d{)+p=Ch&CY6`&eG zp_vFPK99LCwG9Lgm7jsyHlQ6X_jCMP)iY0a$F_Xs5Jf($Up@Wi}! z3~54<8ZUVNIn(Um`}B9DdPq0|Up&)^;fpy1`K#LC&}Sv_)6__tG4 zFEbZ96f7ZevrJHmh4`Nq@Hj2P<-EPx+<(&BBqEOv)V8+oJ}eX#aD5?p*I6#<+5KgE zA_tHFZoCi`?YHW80rD%Gdy9nO{&%S9-Gp^kwz7sD_4$}v#b2op`E_r5hN2M~b<~tYF6FwbW%JFp)yBvWdjLucL_nKELGp@Jv zU(HuaHK%u$cYHLw(aV0Hry(&YMDI{8;W8eF?IdEGX9X7e@`s~tje6}FF|$ZdF+K&V zvQV}Rr&JHP-oJ`!Uzz(1CaRno1m(#788?XQ#qGxZiqymAkwa_gdBR$TD&Je;^y4)}`b zV5-i5Nb%P|9+^*9-m!n%OBz;08FRBN^Z&73`n&T-F;3ZCBa9tZp^4aj?7Wx^;V!l< zeP+u^WiJ-}R8F(^sw^*&xCjnw5ka*w&vx=5Ne2#C_j051m?b=J6Pc0Jd`|nfbj{L8 z*J)E#O4myZLh+kJpm9~qb605J`o?pna!q64p3@6xUMAxs%HGb0S?)+T0eVa?nXR<8E?oT2 zhMEt4B4iqgxH&xdKr||BxKaNxH94b7h#E*PP@3pVoZ{fD5;{8n^y9fygZha}U&W*1 zhM?>ju(I5b(WByRW0h6G`5ned)wR3v`L^~)eV6-6J>E{tFPhrjcX6%sM3_x0!42OUQwD|GFX{K{y zoU5%yKN;xJ1w-k@j|SY?Wnqk|mzzn$zZU5HrhZ$eUq~L4U7ui#ooNIEAEhVU-RxA@ zQ#Wt3vW1?QAltzz9A|@G8xrNI$Q}UqUk>1MNjH#gE@9Kuosp*B9-(b(DL3`g0d8x@ ztItF%CIs1=AQj!On`31Qx|srFOo!dkr9_j$+Nj30O1={a$0Y|J};50JO> z!;|R7Y3$ia8!<4v+^bcx(xeY4%JLqf6E6;=XyV;&At5T1=Vb!>oeSs(3US~c z=b6&+iT-dX(nve%(8;Wmn7LlOki7iZ8F_<$8_PSqx*l>eB)(pGlj)_M*04;&&28~p z?raetd*+_M+2_U!-$yehh(Z}x^<&oN=)ldd-Rb_03;~KTX(qGpEzEmFN+nJ zRKW;10a@^8>>j;OZ89a?r`T)pHfCJcdcqf*byhW882QGeX#F5)LS>kR?S3e|Tt|og{KdU)N`!Kf`p4!sZ_K0>wuct!AR+~ZsB#K5uW{xK0i+2gP^llyeK49A-{ce!k3S9 zFvn8N^x4cs8kE?H+rYrrBK`|emYRwRuZ#Wc=>pT=)U+&F_6Ry5xq;I+RpC!sLjEsZ z41b_+)|%VIWHD!|O=m@Maw=LHKh`y!I~-Sqm~HSq2Rj!sR=T!KSYh2G4@lapy$)sd zFEXN-f0Wd4@LK=wIe!mR-y^NPH`Ybaw9%p&F!|lwbE3jo$pCo=(skzIt7mq1^37M| z5zuQbbo>p2eMr;Xudf3HWbcC8?QMs}&o_bcU^cGSzf8G`RPF=W(iHG)9kwdPNnjW3 zo~g0|jtcKXX>5L=+V;xk%+zn?ch~@T|4oT1sC80*e>#HT*5!O zaHByE8NT@{XyP#o7`KIHw0<|EiZynNQYzv-i{9uWrSSXmvy)&nuv-K2i z17lXkJy4R?bLo;tbIppjx1j&RI{nrGgV1L|E8?K~x_qHxQv#^61Sjt|?r=K(2Aq}y zf52c8qi|;dH7tqrQ2BTZMGd0ufW}s*X><3BTf#uV9#{za>yPN6cAZi8+v{on&D!U_ z-+Ju~2!cUtdtMW#X$1l2GkcqV2rP)@4=-OI*S20mC_wGkOF1zAz98-Xab-5V8Ow1s z0zezAOBb^lrWQBzJ+3MNwfJoIz*u`&(%JS~d&(oID?vgnv=>O%l7LtOVV8DU6nEnE zxnWUO&GnFIz}a3UEby)XU}EXcV#V??r1l}?&fJpfa}M>UCvhiJqOhRPyF*$L)C+KT zW-ut9%v1&`pRAr$eLP2AFsTA#eba9F^8P*#h_T6kmLy1Gc}WTlXQ+v8JnlpWK;=Vv)>$Ya8!evZ{Z&tEB%4D(d^mJ9M&r7Zs>0 z^Q}BnxWIX605P?N^hweXIZ9BZX}-?#yDXv!I6YF_EQ=7s zr#a_f!?c`mEz^|MZa_JH!xYaBxXg??T9R!tJOrvse~E(H)s9xjOfMFA-PYe9C<09O z;kAwQhQ8BSCz&JKtr45XH!bQhq0ln$?yU!R*yljeHqh2BWk;0at`(UU!&Dy!cVqcd zs!0`!@K_}ql?&9psHc|4Xypt?rz1zBegStl25w)G? zdV4w%Pvrpg#Pv9tMU`vOi-lSTnqMH={nZwb3%H$J=1Z2@xAe5>4oF`qxM$9T6KMI{vqr8mhxEfkyd* z&$Jy<&NsMTsy^tb8`T=te`qodP##M;tR8xLuoD>F0EPweaJd1}9(^ZQrm%znxo z*>D0UWclqh(*I9O9))v*bn1|4=}oGM|7fPyVi{AFu~Q=of7(2Pw^C`jcG|RkOqp%b zqi1hl?7JF^h6pOTBUCWTCx*)m`)oy#`bXpve(yNoH20gdm6ZPMLzI+;2&Y4{0R3mm z_AW_7;`8`H%jM=DbXL&ahOzS^2pKRee9ivW@A%wU%wjQ(HPhZ&J5 z!@WM+W4oap^}(<=eo7SKv(s+fvaA&!mSr(fiP#zrvTfDla#XNp?whK2{rO4s6Ew04afH{4byH>cUNq*Z>~n2mqN+yiMCzJ=WDLDwW`O1?yhVu#4lC} z!?@a4m976JufUf6shP_2+s&K@hs!scX3xD#x57{$8l&X=6LHZOPp zS)N!%a$7orvak2}_S)QUa8grOCr^Bj^JZkkPRx zVrGg7+8+`P0Y%CR2+UMKAj+X-@DW8S&%;9+g=oRkwkuKz_z#Vj99tN$c!R`i2# zfXL0SgmB*yI~g^4VfhRu)bk!*yZ67GaDRBT)-XVlA~Wj3<)EFL_rZ(_P^Jln+H+$z zjBZ!9a=N>vrrs%>d4D2S+GAR}mP5L|M`tctv`b{<;GhC8~}%&vNBN(=e<|Do$W zqnhfwE#B7#57Hh*ic}k*ARy9fL{LPeNazTtNGF6MgqkQtnt+P*5|rM1=)Dtq???+J zK!DKF$lacE&p7A9y<>=9#Eb!Z@BdzFuHT$1`UhdB5>P`6N`P{|ps;@jNI+_hm$sJB zNvXB!j`9nrTL~9#qF}h1&xu^f^uaWVx#Pvs_pBe7iSJx9lai9a+6w-onHmdq9;;$F zYw+Hfw$eIqvdbRr-%UfDB=W}@Uwj$!w}^)}p=_b!y*8y$Tb*ZsR7 z^drUoQ=v&x0h%w+^vBY?fQ}@0&xo$Q8AHf@UewzQ+IJkx{tf#dEkJH!Dql4yoAH@d z@{-B3^hc+gSIu6usA~0PhYk1F63MH~sji1%fwYj0sm$FeSRM~*f!O9g*nr1ir&)A` zL8IS3bsKNHy>~Y?bw*q@k&ImZBwN#<$N5J4UqSJdCpQE`V}9&kwTE1AebE7ld>Fh^ zTXg$U^px&+^{5>u;71b!fK&?yt?{pnlm74Nr25-6E!{^6ET2GoR~wTqSJ_ z>uUHp^ys!87RGbo@Kr!DmzbO`UPCTJz?h}|ViI$G5>9OGsDV8fekrEdtm(Jic}n$< zp3#dIOmqkd20HL?;WHd%$gl}~aW=Y751(b|RDp>DE*1;QHqGa-LPr@+=wwGN*d+78 zIpkbdGLxZ=2G(!)8)1tUjzv<5i!fv(s}cS0^2&ipvt|O(aeL`n$i|X*Ezlu9aKlH+ zJm~~OAiG)d=E_dS=d96FZ#=9yh(C4apw$b2xYUgqQs?uX|9Vs6mgDazR3R+t>S#a) z7nonKsq+}`SbRzCa6vsNE>3pV>Qaxl)aTNrZ*uOhtpi4Z78tH^osQO*fW2M$M&0%Q zrN=jgCvVFNdNi-^b1=S6A>wazYS4&EWr_dwSdO{h?-9;RmdsPOYi?wkg^3L-B7?H8 zEwNfX5R7j93|x+2?>r9;&AaxpxC;x48nTgVSEN4t7YO-(^b1iRp%*Q!n2r^|3&o(5 zR^g>xDDs}^N=^AVIxJrGPVw;y<{o3tsL{<_#q~ zhzR%yo967NmybIn8j!gC2Rb&OMX7R$zFK&w)*Y3(s_MPc&d=hO{p)W1171@Lr;5}z z`uSLS|E{B7s-O>pqk?>hot}(n@6`dB5ct_`k{zHN%acpn8-hzsL_ME*Zhk{>fq`T1 zJvj!LRt&M|C~qBtBsxZxC0$oKBnIV4^|&T0V!m*RvHO73gt~!&0c^>;8+f60ayQiD zYNteG77})=n>xn5&bJ&KC-e1^0v8fqe(#vLnv1yPO1XIW?gz(`z?IBrR2#eKsh=@V zj?Mo(;47s$UU;0-#i4J%{yO@uX!O)1eo((*q|4;ryFX-k|C%6<=k=7{N30!in#HTH zJ5`p;+>h#ZbQ(LPCUh!&m`uBwX*;?l?iAHaEoi#a#H=wYq2JW~q)e}~BVE8N2GV+Z zI!X1=T|JS9&}VYuRj?NvPZd@dKjWs~!YuapY6gdZE^Xs4c^lc373sdDayar5qjz{BK!3;Y}NP_hW}Dpvd6&XaYIR5=<}YyyYA6PJ~4yX^+BGiuCj zd%AMI^4r0bY1syw9g6cUkQq}`rf5#DGq=%eAHw;ci&>DC`E~_ zJ?vdxEbZKVZ9ROZ^zdwy$^hZc>oBM?TijdwcU8FY(Cx}@mlgKrPqtq-VHHm*yIg@@ zGu%oyi%+WZmWY&JPEAN_6Zl%-RiU{4uPUs^Kbz$Zc}<_~c<4^}B}3^)v(rr?PzBJw zbAIZ}XkI0tcd9+L@P$fOu&HKM2ZogqXUL@^puryq_M8mN7VKrL${KHz$_h!lo z7rw9wKbgEpx4kn{Q9bX&b-eLXxR|42VX+Yy6fJm)BJf4kUS|a|JpMbb4%Zaoh+-R- zEf~Dmq2^v0rEcpBm-O^oe`p#o;y=gj)oaV`R_Z7djxwS_i?+B%LPG7d+N%>n6 z?Fzp!Kj6ItBI#^MiEM5FTzeHZeVfhP#;qjp*LP?KO8odipD*`~W+sQYaR5#jQYIK{ z$qvcmzlxgIar^d<6ckDCZy*sf_T4;ZLfPHC?){r18B0E-U1fh_kYniv7+{vCkxu!& zRHvmTinhYu*-(P`#Dtc#F@nMzhR%_cEZ78<#%c)hvroV?A`!`vsFe6E&h6>B%Y zTT2qen62rG7B?&d&&VDq5ZtHYZHyR&{UM6`{`BV=5BHSR|LS(1uR7KOuxL)+(_=ro z!4@x&wW4+nj{1y7=iRROqUWH;%XpAvGg(<#r^t_ALXJxSc@@7j^cXFeLS^1CZb`BN z9;hJhe`&vut#z!RicjM@>;Qz?aeMu(i|O?=6TAudsR)7*ZJT*-*p$e7JBRY^$pp?x zWl;Njw(-W(B1MKhJKfa)R46I&2%=_4fhYY3qivu;5BxIroT`#mlg1nH8JIHn|+V^bNRqd$A<867W$69)TQ;J#*>4aV>v_T zFcjLqu8r+co{2uoq$R$tu8!co@tD3R#N@qJx#No#tA`$P8h!Sj#Zn(eB%>~^{BdJ` zP(~9kcx-z;bOnyEVY9ehByx)>tfuHY5F@LTfBQlSAK>G9GSAsb*gI9=#utp1@ufR* z`T4V1As2XD{`1@Xo2TqwGheq|=*oDnDrG4^R+|Bit}>#YV25NDJ)nT@B};Yf*hde7 zY-0whWr-bNm-AqH48XHkX27YtL!fhx<8GXkRD`xNe!kig@eIWQQuuZH!ymO|ZDesS zJOonk9_=_oS-=7rjd*KJYpxL&%}PL&>AT{IL$cJB^Gh_LM6eI`*a$)C+|X$_5_-bA zv@#4^sy!W?8*JXmk2N$mh&1IuN_!ke3@H}%ZOu0N#*3OG$nSy-;4$0Vs!@ujm9b>! zcUu2NL(ArlB(!TBH1OL0_=1P)Zn%bt3I6|8iTA%~nuh{j+T#G0Kycut710s@P)go% z$T-_|*CKWqL3>&LZE&KXc#fzHknKS%;{CK}7U9u!UDDS6w^9&_rjki0R{onQ@!n?+ zY(%*H9~c2s&nrbs^OT$Cg=GkgNB9w6j(~d}$K)K)YR_Cl2LTq%|+qnsC z6h~7z?V-yBMyb8bIe7TvFy+YAG=(x}AoH)7S^z1qrDY243Bh z&wYnj5##eBJV7tGf{tk`{?BrIMw5h~s$+K1YCxz>ZmBCZY0$j9p_t z8=lKi2YF8hjDUZ7TQOEfCF4gcbn>2`Z3Z$sI6BlWe!z_H8lnju*fgR|A}Jx6=dChsD+!br$yu znNm3vV{y0Ljb;Se`ESlOyg>=1CrtY5&Aiwb)4$Nz zI$bEsn2ezv&v?0v>odVzHO1qNp`GvA=vAo)QK}5J#2@rII>~kV%~>T)1nK^g+M_!s z>YMhGhln#3_!ksqu>P@z_d-%32;c8+cNt!-qe~jyp#AN9W0nu(@w7X|{aVZmZb3f5#{5VoDbfNi^57Z=1dT zFZ$O%OxN5MUlaldUJd`vnxfV}9q8L}fr2`LWo{?N41!;>40)Y0WHkoRcJ>!}OYCpR zOj6Yf(m1gQ8Xx$Tj&POZv)6Qgay54qo?#;}Es-jWA{-LW^>tR|E=DPIelG@+S&ncq z7lN`zVM0C7`||;h0_?SS2V4#|CS&2BsI@jZ_jVN=rTklSid+tO(Y#OOvC!PZS81{bnHfy$ zOXmJX9)@P_yQIB_NG;uDDM#MLONabA$?}l4ngc~daWT^{F6qqEtQyh{g3L;SaF8(+ z1ZX~P+K>E}+E;!)u6`sl062>U;_1O*Lx~d>rFqBZGD|(%?LDmQY1LQ@Ql;HUz(5we zXaX}Kwkbc^&`5^^6h#4NFjk3J4377LadpSf;}A=iqSHW#*1|-IE{cRrZx%7#Fu?e) z!rwkInNaxoOygGB(qVHZ(4O&EX6;~G_RXZ#%vX3wE@6F!W1(fJeVaNj+D?Xnd0=rw^u!|Nnx zN|z4e{GA%)vA`%pT#1PeX*wNrChahf3bw%d&)7bfG@Uuk{_G8`o<2+z7~&LI#(p(X!8VGxH^FYXzsh{q!GWy|6s>GWjY?J6t=m|91@an7eNGB13Kh(I zNR`Xs9xADGwd#88c5a{Z1=kTWQ?U`c^Mt)0DFdIcDPE|9RIF+*W|SY5qaUNW>oTb1 zMrdRLg)0~W)t{f#C%{h!0M`#TibY$&s&<{0yOcC&SAq}zN``xR zcpL*W+nF%<;YdgYoyZ(z05yA(nZazU)%w_}d?rq93Pcu-1adGg5p?YS+UW1fWlnbb zYueo}%AS$bf1!HOqJhRN=3-LzpHH$kTG^XLKTFx>B>YrY4VKl&=p>^aa`qL59l2v* zbls`jW$fwR4>!H4-y;RkH;{T%vh$nm>!|K9B^t(};D?Qnyopikz# zZoBfpu)d>%J!OZk!_UB>ba5#Hn^z=|#-ZBzu!~CC3-mgZ3lT;Dr{G9`9KF%EA&SEX=_aGb$ z-*y`G><||$rQ0Uq8XB>Sq&yu$RG2~|d6EklPyYD^TY85wi{`~L&z{9FhmZ**D1xfQ zDWe`?SOm-j(M@v|r`MevPDv#oi5fMe@`CTt8+dRmvjWDeK!8WyQy4_{mY`p(5i^4xQjPC=zMBWn_p3{j;dMO{BPiQ|WmxmjdY`BU6uw?@I{kR4#}l+9*lY--`!c zy(fzsNC4(siP4#6*zH->UV}=!cOkGER&r))Ry#igwjJ_`JNGAyMJa?|lg^m)!$Ocg zN6Yr$oX}UBFR_b_>LvW~Yjf7_7==AVD84|n?hgsq)JWK)t|Lk`->rq>#G3!4l*)e` z@R;=9@3md>p-d!7*mQ7n8diUHESxDciJ)WY^4OYjx(|1N9`;xp|$ClcpQYOu`g|v2-K-FmlU* zw?nJ{7ys0SN*!;X>C|?yasNnCo%xs&++U|+q8IdEnm7$a<6aIOAfRiR>xc{#j+wnE zuA((i`wgJpkGd@6Q7r%b0s?A+bquDTm@F$?;f95ib8j#6(pbnK~em?hPfE}EwH(sp&@_iOn0m&AW0JMNXt1hL(L zFqvo#!!A9gVAH$en4?799nJuZH+!9K2e|V)9hB|H24)_B?DpB50*L2xP+s^B-5KGt zhoFV`pY2_RkA6qzzEdWMKvtuhXJgEuQbni1E$JRn-`&QICfagnBYD0eWgfP%tCe5S zOg@UyV8Ivkthsql<@&_qx{{qo+{5_SZI0djABEOL1c3tIpRNmG^8InU)#rfb)yOEb zvM{2qx@hZCUCi z3I;|!XJp`6lHd4&K10n`LZ-5OPX!FLo}57H_iBgS&|*6H#f4dHNe#mkn@PSpG|!td zGR_?{p-H8?g?q2qz4qVuVyB64ojR7)-;4XUyavE@D5Z!G#_4aR$Z`RS8oN%L z_7fu@(I*0>_?<=$90^7Fq_v@kc9t&YPbmN+kI$Dq&wFaXZHOg$*%ONsBW=ZeArPWK z+9ymm@aW)4;@JBz6;=s@;tN~vWqVt)Fx{7c-7X0+`*S6S^euiT2O*YlqIhzrI0ZP# zLU{ZiDui!u$MWyv+A4cvitElU{)6!LJ%js-j@Uj(gH$)3_^$_V0L{%5JJQ<$8OY>) zxJ*lcAtTt3mG%(-2qtY~<*fR>DjD>UVD8t#`qt-4 zKF9gy`e=t93FmSGn~DJ|y7}laI-qc3O@jqNTF(I(0BPt*icy8uob;}|=X-$Oe_nMx z{q6@e~LTdJg1~0AK%|#FEY->T#QiPIjDL zn?alL|A^*QNWmc!MZr8dRKVEflsd)lKvG{SkVx`%(%tLP(%R?ux^#yb7sFMq$cZsh zVrF4i@ADd*gm|&)jztSqMyae1eJ#y?(vr&+r4lP)e5(UMMkbY?C7m+(EmaA?wT>hT zb?cQacGMY%n}O&4pwVbp~@h}fYeb?U)fmzmQ5U)9t@Q%Tt1jw_Wsa2Dih#jK1` zJJGeUKF3t(d}$(eJ=iceoyct2JZgt6k@pv>PZalu*Nsf21m;N4hPE%p-D$4QuXqiS zw9d`;MaQ&73U(~HVqqPdGZi+JbM#O_NP%MM*y^bzT#v8XZp1CDi85T>(T|T{4)Mbi z1VfM?9q?>LA1svaQg+%;y8u(jUw7CM;J5_24e@j%y)EWWc+y+>e>|C*yXQ;O-iLl1 zaB0G@Bb`%mL_XJ$a0^YF_xX;<8A5`lYI#~rnevnL2&jOfW*Ma;$?)k?gDGRM66jIa z<)1)VWJWG(emM6j*6@9?=A>AKTT+l?i+-?km&43mG1HpOI$<$u-l>U6z=K{7<%9KY zsV@1RMlT^KJFC&tIjh}~RwpQ~Vr!K8kVQSN!t(qm{b#I+`NGHDJrFJt^1fNZS6MYya1bV=M%^Azds?h&Oyr%uYltt3VOOsI=AJ4TirZFC3OGR3kyK;uR-`G21O` zfv;R+jYhcmG{3Ly(TfP**|lCV#e-0To|F3B&aOw>L*2M~(u6J2ZmDdnGbh!hZHC?X zkrzIK~J^(V(a^OhF@)ykB1?9pd~%*fCOlujy{?)$CVk(~au53~ePAPB4LmPVA}oG3z$u zJ@L814@GGjDI*siecz~zhd<>ECV}9NoocltwS6x}IxhjzUUG?q$GNppQDwfD+G%fy zs&n&l-RY9I`=2DoLqVMu|3I?Yav66+XI!d0%))iuz~^Xtpl-GOWX`u&+5enVX?v@i z&xVjjudt$iMvi=H5R;#jG^-k|eA0dGE{EMxQ#(9_n>b4^=68P3eG9RIJ|0DjR{Ne) zUOkdx(BY}+;uK8r6u?<{F>Jf$q+mMIFP0cIXT{qL2`?c?j2@3{1``ez%~pn-m3%(Z zXT0m&(N|_$mNzmYtz|G*$ejR47rb>QM;U*X;aE})$MF;Q$x6N_vl}ge?jCZe3KdrS zu{sE_9adY8)Q~uqV;}>Wtkc4pNZ6dqx^-yR?|nvB7b2~etQ@o}JKqpbue)T%>^qs7 z0_m0L?2XV*sz2QS4pKrwPo5bVD%D z`=$usJ&}rB`B$7b+=rMFnvr%-sXOC7S=4eRRq5)|gWK7$P2jdwZ55L(bw)M){Ap(< z((AC{PkuRn!tmqru7GP$*#gjpnz!@zZ)-XN5tjUy8=GZHRO}4+eladR$gem_W#bC$ z<8?q#2aHHOt_PT=RN$c$h7+1 zO}2jIX`h5nzSuc&?1eOgFJ_sg8V=$vZIyuD&}6J8G=qbG)(FQQ91WMCe{ZoPXac0Z~89T)2c8 zO6`6qnH_#_qC23@28b65evyoKJ6dQdI|DyiPi_KF#YqA8`_(AETNYx53Rc8Q5S$dN zMVQtHFXT*JMSu@>jVwhHK+R7yVD$f^1$alW`E{!eiQ*seSy3+tGx<{+qeJ{2AzJ+= z)gLym^a0)npw(_5w&CY?8_=3V%8f&xLlg-;fjhYsVN1X*TsK+AwNEG1tmD*-b)MTT z7Z%2+fDb_j{^U+0ZvuITa40c(p%w5BW}@hOHhQ(mPO7~82Vk)j`UucW;Z2F8KQ)lQ+D$JaY&i8Gfzl)TF@e|hM=LEU$AuQt+*9#3&go;&znk%O{^pg&|e*hPWA(_3XY>=jZqPBg{ zw?c4gAV~&0orSKpgrDqTOBiPTgyV8Nl2HJz!^z@A?3jm9eTmjpiVE7)!IXmz$|-}t zAoZ<(URHdnvd1=BUbH;+U!1RXVmKKr zSTW#qx1r9V{1v2Cl-8zWrjdf&mF(`*rd+1J?J9yu7+t1D|N7aS4#<>cyl!?j0Hluz z1BLx_saw)2y|`GEY{Ffo_&bAo2EnODzkve3KT`;!w2VzHZE=-n!6XFGZlqx+JH0I3 zRSpvV7H;9JKaTe6{J6lX2oDwW< zYvDHG;8;Rx;gLZQdetjz22*4`capG>e1j~&WHXQ~b7m!9FhN5t2%ZARo)7HM_W+Xw{3Q@f| z`*t-F->nAkBQ_WqS+0`aEx`eyoXffV!5X66EEwZKP!v-p9gQsuf1ibI*0`Ks>}gJh zp{p%?YmIRHf%*9sjq#sc+y1tePwY1U@D~T>?$Xv0aiEV6fO$D{%%mfM=CoD&(gB|M%Bu?PGlQIDgroOyq&mMzk6rPnsK@uZHcKSeop zKpW_5-vX1)IarVS!%|{NQ`hXy$v#kk({lTCd=BC99(*7;@3t@@GY^+p8}h%i{py#p z{f|%VkEdPQ3`RaN}=7d8pAOhzEAt(}1+!wqxjP7ffQTdb+CD?O3T&v1Kl z!AJVV$iWW}0U&+558ER3qPe(1C@(9_C+@Nuh@B2G>% zc?@zS!$r=Q+p9;E#4Srs7Mt1mSZ@xb!M&lzr@@`3jgSXA3Lp3UR%X~6kNe#F5j9

lE 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 z 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..6a40e0d3427066297fafa6eb5847856d129ee7de GIT binary patch literal 126405 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%0Av^w{>jR-b8fhLOqS!KpQ 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 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, @@ -2622,8 +2630,10 @@ export async function updateClaudeUsageIndex( // 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 rebase is the same story without the read: it only narrows a stored - // aggregate, and the UUIDs it drops are handed to the ownership pass below. + // 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 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/extension.ts b/src/extension.ts index 6b711802..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)); } } @@ -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); @@ -3277,6 +3286,7 @@ 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, config.statusBarQuotaFormat); @@ -4422,6 +4432,7 @@ export class ClaudeCodeUsageExtension { materialized.costliestMessages, materialized.hourlyForLast30DaysByDay, materialized.projectUsageMatrix, + materialized.dailyForAllTime, ); } this.cache.lastUpdate = new Date(snapshotNow.getTime()); @@ -4533,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); } } @@ -4660,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( - `` + `` ); parts.push(``); if (accentColor) { parts.push(``); } - parts.push(`${esc(summary)}`); + parts.push(`${esc(renderedSummary)}`); if (opts.subtitle) { parts.push(`${esc(opts.subtitle)}`); } @@ -270,15 +294,15 @@ export function renderHeatmapSvg(daily: Record, opts: HeatmapS for (const c of grid.cells) { const x = padL + c.col * step; const y = padT + c.row * step; - const when = longDate(c.dateISO); + const when = longDate(c.dateISO, locale); const usage = daily[c.dateISO] ?? { tokens: 0, cost: 0, sessions: 0 }; const tip = opts.tooltip ? opts.tooltip(c.dateISO, usage, c.value, metric) : c.value <= 0 - ? `No ${noun || 'usage'} on ${when}` + ? `${copy.no} ${noun || copy.usage} ${copy.on} ${when}` : metric === 'cost' - ? `$${compactNum(c.value)} on ${when}` - : `${compactNum(c.value)} ${noun} on ${when}`; + ? `$${compactNum(c.value, locale)} ${copy.on} ${when}` + : `${compactNum(c.value, locale)} ${noun} ${copy.on} ${when}`; parts.push( `${esc(tip)}` ); @@ -294,12 +318,20 @@ export function renderHeatmapSvg(daily: Record, opts: HeatmapS const m = Number(first.dateISO.slice(5, 7)) - 1; if (m !== lastMonth) { lastMonth = m; - parts.push(`${MONTHS[m]}`); + const monthLabel = new Intl.DateTimeFormat(locale, { + month: 'short', + timeZone: 'UTC', + }).format(new Date(Date.UTC(2026, m, 1))); + parts.push(`${esc(monthLabel)}`); } } // Weekday labels (Mon / Wed / Fri). - for (const [row, label] of [[1, 'Mon'], [3, 'Wed'], [5, 'Fri']] as [number, string][]) { + for (const [row, weekday] of [[1, 1], [3, 3], [5, 5]] as [number, number][]) { + const label = new Intl.DateTimeFormat(locale, { + weekday: 'short', + timeZone: 'UTC', + }).format(new Date(Date.UTC(2026, 0, 4 + weekday))); parts.push(`${label}`); } @@ -318,13 +350,16 @@ export function renderHeatmapSvg(daily: Record, opts: HeatmapS // Anchor to the rendered card rather than the raw grid. Short 30/90-day // ranges use minWidth, so a grid-relative position can fall outside the // viewBox as the legend gains additional bands. - let lx = Math.max(padL, width - (scale.length * step + 66)); - parts.push(`Less`); - lx += 26; + const lessWidth = approximateSvgTextWidth(copy.less, 11); + const moreWidth = approximateSvgTextWidth(copy.more, 11); + const legendWidth = lessWidth + 8 + scale.length * step + 4 + moreWidth; + let lx = Math.max(padL, width - legendWidth - 10); + parts.push(`${esc(copy.less)}`); + lx += lessWidth + 8; for (let b = 0; b < scale.length; b++) { parts.push(``); } - parts.push(`More`); + parts.push(`${esc(copy.more)}`); parts.push(''); return parts.join('\n'); diff --git a/src/i18n.ts b/src/i18n.ts index 38f231f6..54383a8d 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -13,6 +13,60 @@ export interface ProviderTranslations { codex: CodexViewCopy; } +/** Complete copy contract for the preview-first sharing workspace. Keeping the + * contract here makes every presentation follow the same eight-locale guard. */ +export interface SharingWorkspaceTranslations { + eyebrow: string; + panelTitle: string; + description: string; + privacyBadge: string; + presentationLabel: string; + combinedPresentation: string; + combinedPresentationDescription: string; + claudeCardPresentation: string; + claudeCardPresentationDescription: string; + claudeHeatmapPresentation: string; + claudeHeatmapPresentationDescription: string; + combinedUnavailable: string; + rangeRollingGroup: string; + rangeSpecificMonthGroup: string; + last7: string; + thisMonth: string; + last12Months: string; + todayHourly: string; + lastMonthSuffix: string; + scopeLabel: string; + scopeAllGroup: string; + overall: string; + byProject: string; + bySession: string; + themeLabel: string; + claudeClassic: string; + claudeCream: string; + auroraDark: string; + autoTheme: string; + cardContentsLabel: string; + totalTokens: string; + estimatedCost: string; + cacheHitRate: string; + topModel: string; + sessionCount: string; + tokenMix: string; + dailyPulse: string; + badge: string; + messageCount: string; + projectName: string; + fullNumbers: string; + fullNumbersHelp: string; + previewPending: string; + generating: string; + cardBuildFailed: string; + publishGitHub: string; + publishPreferenceSaveWarning: string; + claudeHeatmapDisclaimer: string; + noClaudeData: string; +} + export interface WeeklyValueCopy { title: string; description: string; @@ -941,16 +995,16 @@ function providerTranslations( const PROVIDERS: Record = { en: providerTranslations( - { claude: 'Claude', codexBeta: 'Codex Beta', compare: 'Compare' }, + { claude: 'Claude', codexBeta: 'Codex', compare: 'Compare' }, {}, ), 'de-DE': providerTranslations( - { claude: 'Claude', codexBeta: 'Codex Beta', compare: 'Vergleichen' }, + { claude: 'Claude', codexBeta: 'Codex', compare: 'Vergleichen' }, { ...TASK8_CODEX_COPY['de-DE'], accountSnapshotLastObserved: 'Nutzung fasst Anmeldungen in diesem Codex home zusammen · Limits werden zuletzt beobachtet, nicht kombiniert', - title: 'Codex-Nutzung', beta: 'Beta', overview: 'Übersicht', usageTrend: 'Nutzungstrend', daily: 'Täglich', date: 'Datum', role: 'Rolle', scope: 'Bereich', threadLabel: 'Thread', rootRole: 'Hauptaufgabe', childRole: 'Subagent', approvalReviewerRole: 'Freigabe-Prüfer', unknownRole: 'Unbekannt', noDailyData: 'Noch keine tägliche Codex-Nutzung indexiert.', noThreadData: 'Noch keine Codex-Threads indexiert.', lastTask: 'Letzte Aufgabe', last7Days: 'Letzte 7 Tage', last30Days: 'Letzte 30 Tage', projects: 'Projekte', projectLabel: 'Projekt', + title: 'Codex-Nutzung', overview: 'Übersicht', usageTrend: 'Nutzungstrend', daily: 'Täglich', date: 'Datum', role: 'Rolle', scope: 'Bereich', threadLabel: 'Thread', rootRole: 'Hauptaufgabe', childRole: 'Subagent', approvalReviewerRole: 'Freigabe-Prüfer', unknownRole: 'Unbekannt', noDailyData: 'Noch keine tägliche Codex-Nutzung indexiert.', noThreadData: 'Noch keine Codex-Threads indexiert.', lastTask: 'Letzte Aufgabe', last7Days: 'Letzte 7 Tage', last30Days: 'Letzte 30 Tage', projects: 'Projekte', projectLabel: 'Projekt', unnamedSession: 'Unbenannte Sitzung', unidentifiedProject: 'Nicht identifiziertes Projekt', parentThread: 'Übergeordnet', parentTask: 'Übergeordnete Aufgabe', searchThreads: 'Sitzungen suchen', sessions: 'Sitzungen', modelsEffort: 'Modelle & Aufwand', clearFilters: 'Filter löschen', activeFilters: 'Aktive Filter', all: 'Alle', localDirectory: 'Lokaler Ordner', lastActive: 'Zuletzt aktiv', expand: 'Erweitern', viewAllSessions: 'Alle Sitzungen anzeigen', sortBy: 'Sortieren nach', usageLimits: 'Nutzungslimits', resets: 'Zurücksetzung', credits: 'Guthaben', unlimited: 'Unbegrenzt', allTime: 'Gesamter Zeitraum', behavior: 'Verhalten', settings: 'Einstellungen', monthly: 'Monatlich', tokenComposition: 'Token-Zusammensetzung', freshInput: 'Eingabe ohne Cache', reasoningSubset: 'In Ausgabe enthalten', threadRoleComposition: 'Thread-Rollenverteilung', childThreadsPerRootTask: 'Unter-Threads / Hauptaufgabe', childFreshShare: 'Anteil der Nutzung ohne Cache durch Unter-Threads', approvalFreshShare: 'Anteil der Nutzung ohne Cache durch Freigabeprüfung', highEffortFreshShare: 'Anteil der Nutzung ohne Cache bei hohem Aufwand', processedToFreshRatio: 'Verarbeitet / Nutzung ohne Cache', reasoningOutputShare: 'Reasoning-Anteil der Ausgabe', postPatchToolCallsPerPatchCall: 'Tool-Call-Proxy nach Patch / Patch-Aufruf', patchCalls: 'Patch-Aufrufe', compactions: 'Kontextkomprimierungen', processed: 'Verarbeitet', apiEquivalentCost: 'API-äquivalente Kosten', apiEquivalentCostHelp: 'Aus derzeit indexierten Token mit aktuellen offiziellen API-Preisen geschätzt; keine Rechnung oder Abonnementbelastung. Preisabdeckung: {coverage}.', fresh: 'Nutzung ohne Cache', input: 'Eingabe', cachedInput: 'Gecachte Eingabe', output: 'Ausgabe', reasoning: 'Reasoning', @@ -961,12 +1015,12 @@ const PROVIDERS: Record = { TASK5_CODEX_COPY['de-DE'], ), 'zh-TW': providerTranslations( - { claude: 'Claude', codexBeta: 'Codex Beta', compare: '比較' }, + { claude: 'Claude', codexBeta: 'Codex', compare: '比較' }, { ...TASK8_CODEX_COPY['zh-TW'], accountSnapshotLastObserved: '用量會合併此 Codex home 中的多個登入 · 額度只顯示最後觀測,不合併', - title: 'Codex 用量', beta: 'Beta', overview: '總覽', usageTrend: '用量趨勢', daily: '按日', date: '日期', role: '角色', scope: '範圍', threadLabel: '執行緒', rootRole: '根任務', childRole: 'Subagent', approvalReviewerRole: '權限審批', unknownRole: '未知', noDailyData: '尚未索引到 Codex 每日用量。', noThreadData: '尚未索引到 Codex 執行緒。', lastTask: '最近任務', last7Days: '最近 7 天', last30Days: '最近 30 天', projects: '專案', projectLabel: '專案', + title: 'Codex 用量', overview: '總覽', usageTrend: '用量趨勢', daily: '按日', date: '日期', role: '角色', scope: '範圍', threadLabel: '執行緒', rootRole: '根任務', childRole: 'Subagent', approvalReviewerRole: '權限審批', unknownRole: '未知', noDailyData: '尚未索引到 Codex 每日用量。', noThreadData: '尚未索引到 Codex 執行緒。', lastTask: '最近任務', last7Days: '最近 7 天', last30Days: '最近 30 天', projects: '專案', projectLabel: '專案', unnamedSession: '未命名工作階段', unidentifiedProject: '未識別專案', parentThread: '父執行緒', parentTask: '父任務', searchThreads: '搜尋工作階段', sessions: '工作階段', modelsEffort: '模型與推理強度', clearFilters: '清除篩選條件', activeFilters: '作用中的篩選條件', all: '全部', localDirectory: '本機資料夾', lastActive: '最後活動', expand: '展開', viewAllSessions: '檢視所有工作階段', sortBy: '排序依據', usageLimits: '用量限制', resets: '重設時間', credits: '點數', unlimited: '無上限', allTime: '全部時間', behavior: '行為', settings: '設定', monthly: '按月', tokenComposition: 'Token 構成', freshInput: '未快取輸入', reasoningSubset: '已包含在輸出中', threadRoleComposition: '執行緒角色構成', childThreadsPerRootTask: '每個根任務的子執行緒數', childFreshShare: '子執行緒未快取用量占比', approvalFreshShare: '審批未快取用量占比', highEffortFreshShare: '高推理強度未快取用量占比', processedToFreshRatio: '已處理 / 未快取用量', reasoningOutputShare: '推理占輸出比例', postPatchToolCallsPerPatchCall: '每次修補呼叫的修補後工具呼叫代理量', patchCalls: '修補呼叫次數', compactions: '上下文壓縮次數', processed: '已處理', apiEquivalentCost: 'API 等效成本', apiEquivalentCostHelp: '依目前已建立索引的 Token 與現行官方 API 單價估算;不是帳單或訂閱扣款。已定價模型涵蓋率:{coverage}。', fresh: '未快取用量', input: '輸入', cachedInput: '快取輸入', output: '輸出', reasoning: '推理', @@ -977,12 +1031,12 @@ const PROVIDERS: Record = { TASK5_CODEX_COPY['zh-TW'], ), 'zh-CN': providerTranslations( - { claude: 'Claude', codexBeta: 'Codex Beta', compare: '对比' }, + { claude: 'Claude', codexBeta: 'Codex', compare: '对比' }, { ...TASK8_CODEX_COPY['zh-CN'], accountSnapshotLastObserved: '用量会合并此 Codex home 中的多个登录 · 额度只显示最后观测,不合并', - title: 'Codex 用量', beta: 'Beta', overview: '概览', usageTrend: '用量趋势', daily: '按日', date: '日期', role: '角色', scope: '范围', threadLabel: '线程', rootRole: '根任务', childRole: 'Subagent', approvalReviewerRole: '权限审批', unknownRole: '未知', noDailyData: '尚未索引到 Codex 每日用量。', noThreadData: '尚未索引到 Codex 线程。', lastTask: '最近任务', last7Days: '最近 7 天', last30Days: '最近 30 天', projects: '项目', projectLabel: '项目', + title: 'Codex 用量', overview: '概览', usageTrend: '用量趋势', daily: '按日', date: '日期', role: '角色', scope: '范围', threadLabel: '线程', rootRole: '根任务', childRole: 'Subagent', approvalReviewerRole: '权限审批', unknownRole: '未知', noDailyData: '尚未索引到 Codex 每日用量。', noThreadData: '尚未索引到 Codex 线程。', lastTask: '最近任务', last7Days: '最近 7 天', last30Days: '最近 30 天', projects: '项目', projectLabel: '项目', unnamedSession: '未命名会话', unidentifiedProject: '未识别项目', parentThread: '父线程', parentTask: '父任务', searchThreads: '搜索会话', sessions: '会话', modelsEffort: '模型与推理强度', clearFilters: '清除筛选条件', activeFilters: '生效的筛选条件', all: '全部', localDirectory: '本地文件夹', lastActive: '最后活动', expand: '展开', viewAllSessions: '查看所有会话', sortBy: '排序依据', usageLimits: '用量限制', resets: '重置时间', credits: '点数', unlimited: '无限制', allTime: '全部时间', behavior: '行为', settings: '设置', monthly: '按月', tokenComposition: 'Token 构成', freshInput: '未缓存输入', reasoningSubset: '已包含在输出中', threadRoleComposition: '线程角色构成', childThreadsPerRootTask: '每个根任务的子线程数', childFreshShare: '子线程未缓存用量占比', approvalFreshShare: '审批未缓存用量占比', highEffortFreshShare: '高推理强度未缓存用量占比', processedToFreshRatio: '已处理 / 未缓存用量', reasoningOutputShare: '推理占输出比例', postPatchToolCallsPerPatchCall: '每次补丁调用的补丁后工具调用代理量', patchCalls: '补丁调用次数', compactions: '上下文压缩次数', processed: '已处理', apiEquivalentCost: 'API 等效成本', apiEquivalentCostHelp: '按当前已索引 Token 和现行官方 API 单价估算;不是账单或订阅扣费。已定价模型覆盖率:{coverage}。', fresh: '未缓存用量', input: '输入', cachedInput: '缓存输入', output: '输出', reasoning: '推理', @@ -993,12 +1047,12 @@ const PROVIDERS: Record = { TASK5_CODEX_COPY['zh-CN'], ), ja: providerTranslations( - { claude: 'Claude', codexBeta: 'Codex Beta', compare: '比較' }, + { claude: 'Claude', codexBeta: 'Codex', compare: '比較' }, { ...TASK8_CODEX_COPY.ja, accountSnapshotLastObserved: 'この Codex home の複数ログインの使用量を合算 · 上限は最終観測のみで、合算しません', - title: 'Codex 使用量', beta: 'ベータ', overview: '概要', usageTrend: '使用量の推移', daily: '日別', date: '日付', role: '役割', scope: '範囲', threadLabel: 'スレッド', rootRole: 'ルート', childRole: 'サブエージェント', approvalReviewerRole: '承認レビュアー', unknownRole: '不明', noDailyData: '日別の Codex 使用量はまだ索引化されていません。', noThreadData: 'Codex スレッドはまだ索引化されていません。', lastTask: '最近のタスク', last7Days: '過去 7 日', last30Days: '過去 30 日', projects: 'プロジェクト', projectLabel: 'プロジェクト', + title: 'Codex 使用量', overview: '概要', usageTrend: '使用量の推移', daily: '日別', date: '日付', role: '役割', scope: '範囲', threadLabel: 'スレッド', rootRole: 'ルート', childRole: 'サブエージェント', approvalReviewerRole: '承認レビュアー', unknownRole: '不明', noDailyData: '日別の Codex 使用量はまだ索引化されていません。', noThreadData: 'Codex スレッドはまだ索引化されていません。', lastTask: '最近のタスク', last7Days: '過去 7 日', last30Days: '過去 30 日', projects: 'プロジェクト', projectLabel: 'プロジェクト', unnamedSession: '名前のないセッション', unidentifiedProject: '未識別のプロジェクト', parentThread: '親スレッド', parentTask: '親タスク', searchThreads: 'セッションを検索', sessions: 'セッション', modelsEffort: 'モデルと推論強度', clearFilters: 'フィルターをクリア', activeFilters: '適用中のフィルター', all: 'すべて', localDirectory: 'ローカルフォルダー', lastActive: '最終アクティブ', expand: '展開', viewAllSessions: 'すべてのセッションを表示', sortBy: '並べ替え', usageLimits: '使用量上限', resets: 'リセット', credits: 'クレジット', unlimited: '無制限', allTime: '全期間', behavior: '行動', settings: '設定', monthly: '月別', tokenComposition: 'トークン構成', freshInput: '非キャッシュ入力', reasoningSubset: '出力に含まれます', threadRoleComposition: 'スレッド役割構成', childThreadsPerRootTask: 'ルートタスクあたりの子スレッド', childFreshShare: '子スレッドの非キャッシュ使用量比率', approvalFreshShare: '承認の非キャッシュ使用量比率', highEffortFreshShare: '高推論強度の非キャッシュ使用量比率', processedToFreshRatio: '処理済み / 非キャッシュ使用量', reasoningOutputShare: '出力に占める推論', postPatchToolCallsPerPatchCall: 'パッチ呼び出しあたりのパッチ後ツール呼び出しプロキシ', patchCalls: 'パッチ呼び出し', compactions: 'コンテキスト圧縮', processed: '処理済み', apiEquivalentCost: 'API 等価コスト', apiEquivalentCostHelp: '現在索引済みの Token を現行の公式 API 単価で見積もった値です。請求額やサブスクリプション料金ではありません。価格適用率: {coverage}。', fresh: '非キャッシュ使用量', input: '入力', cachedInput: 'キャッシュ入力', output: '出力', reasoning: '推論', @@ -1009,12 +1063,12 @@ const PROVIDERS: Record = { TASK5_CODEX_COPY.ja, ), ko: providerTranslations( - { claude: 'Claude', codexBeta: 'Codex Beta', compare: '비교' }, + { claude: 'Claude', codexBeta: 'Codex', compare: '비교' }, { ...TASK8_CODEX_COPY.ko, accountSnapshotLastObserved: '이 Codex home의 여러 로그인 사용량을 합산 · 한도는 마지막 관측만 표시하며 합산하지 않음', - title: 'Codex 사용량', beta: '베타', overview: '개요', usageTrend: '사용량 추이', daily: '일별', date: '날짜', role: '역할', scope: '범위', threadLabel: '스레드', rootRole: '루트', childRole: '하위 에이전트', approvalReviewerRole: '승인 검토자', unknownRole: '알 수 없음', noDailyData: '아직 일별 Codex 사용량이 인덱싱되지 않았습니다.', noThreadData: '아직 Codex 스레드가 인덱싱되지 않았습니다.', lastTask: '최근 작업', last7Days: '최근 7일', last30Days: '최근 30일', projects: '프로젝트', projectLabel: '프로젝트', + title: 'Codex 사용량', overview: '개요', usageTrend: '사용량 추이', daily: '일별', date: '날짜', role: '역할', scope: '범위', threadLabel: '스레드', rootRole: '루트', childRole: '하위 에이전트', approvalReviewerRole: '승인 검토자', unknownRole: '알 수 없음', noDailyData: '아직 일별 Codex 사용량이 인덱싱되지 않았습니다.', noThreadData: '아직 Codex 스레드가 인덱싱되지 않았습니다.', lastTask: '최근 작업', last7Days: '최근 7일', last30Days: '최근 30일', projects: '프로젝트', projectLabel: '프로젝트', unnamedSession: '이름 없는 세션', unidentifiedProject: '식별되지 않은 프로젝트', parentThread: '상위 스레드', parentTask: '상위 작업', searchThreads: '세션 검색', sessions: '세션', modelsEffort: '모델 및 추론 강도', clearFilters: '필터 지우기', activeFilters: '활성 필터', all: '전체', localDirectory: '로컬 폴더', lastActive: '마지막 활동', expand: '펼치기', viewAllSessions: '모든 세션 보기', sortBy: '정렬 기준', usageLimits: '사용량 한도', resets: '재설정', credits: '크레딧', unlimited: '무제한', allTime: '전체 기간', behavior: '행동', settings: '설정', monthly: '월별', tokenComposition: '토큰 구성', freshInput: '캐시되지 않은 입력', reasoningSubset: '출력에 포함됨', threadRoleComposition: '스레드 역할 구성', childThreadsPerRootTask: '루트 작업당 하위 스레드', childFreshShare: '하위 스레드 캐시되지 않은 사용량 비율', approvalFreshShare: '승인 캐시되지 않은 사용량 비율', highEffortFreshShare: '고강도 캐시되지 않은 사용량 비율', processedToFreshRatio: '처리됨 / 캐시되지 않은 사용량', reasoningOutputShare: '출력 중 추론 비율', postPatchToolCallsPerPatchCall: '패치 호출당 패치 후 도구 호출 프록시', patchCalls: '패치 호출', compactions: '컨텍스트 압축', processed: '처리됨', apiEquivalentCost: 'API 등가 비용', apiEquivalentCostHelp: '현재 인덱싱된 Token에 현행 공식 API 단가를 적용한 추정치입니다. 청구액이나 구독 결제액이 아닙니다. 가격 적용률: {coverage}.', fresh: '캐시되지 않은 사용량', input: '입력', cachedInput: '캐시 입력', output: '출력', reasoning: '추론', @@ -1025,12 +1079,12 @@ const PROVIDERS: Record = { TASK5_CODEX_COPY.ko, ), 'pt-BR': providerTranslations( - { claude: 'Claude', codexBeta: 'Codex Beta', compare: 'Comparar' }, + { claude: 'Claude', codexBeta: 'Codex', compare: 'Comparar' }, { ...TASK8_CODEX_COPY['pt-BR'], accountSnapshotLastObserved: 'O uso combina logins neste Codex home · limites são a última observação, não somados', - title: 'Uso do Codex', beta: 'Beta', overview: 'Visão geral', usageTrend: 'Tendência de uso', daily: 'Diário', date: 'Data', role: 'Função', scope: 'Escopo', threadLabel: 'Thread', rootRole: 'Raiz', childRole: 'Subagente', approvalReviewerRole: 'Revisor de aprovação', unknownRole: 'Desconhecido', noDailyData: 'Nenhum uso diário do Codex foi indexado.', noThreadData: 'Nenhuma thread do Codex foi indexada.', lastTask: 'Tarefa recente', last7Days: 'Últimos 7 dias', last30Days: 'Últimos 30 dias', projects: 'Projetos', projectLabel: 'Projeto', + title: 'Uso do Codex', overview: 'Visão geral', usageTrend: 'Tendência de uso', daily: 'Diário', date: 'Data', role: 'Função', scope: 'Escopo', threadLabel: 'Thread', rootRole: 'Raiz', childRole: 'Subagente', approvalReviewerRole: 'Revisor de aprovação', unknownRole: 'Desconhecido', noDailyData: 'Nenhum uso diário do Codex foi indexado.', noThreadData: 'Nenhuma thread do Codex foi indexada.', lastTask: 'Tarefa recente', last7Days: 'Últimos 7 dias', last30Days: 'Últimos 30 dias', projects: 'Projetos', projectLabel: 'Projeto', unnamedSession: 'Sessão sem nome', unidentifiedProject: 'Projeto não identificado', parentThread: 'Thread pai', parentTask: 'Tarefa pai', searchThreads: 'Pesquisar sessões', sessions: 'Sessões', modelsEffort: 'Modelos e esforço', clearFilters: 'Limpar filtros', activeFilters: 'Filtros ativos', all: 'Tudo', localDirectory: 'Pasta local', lastActive: 'Última atividade', expand: 'Expandir', viewAllSessions: 'Ver todas as sessões', sortBy: 'Ordenar por', usageLimits: 'Limites de uso', resets: 'Redefinição', credits: 'Créditos', unlimited: 'Ilimitado', allTime: 'Todo o período', behavior: 'Comportamento', settings: 'Configurações', monthly: 'Mensal', tokenComposition: 'Composição de tokens', freshInput: 'Entrada sem cache', reasoningSubset: 'Incluído na saída', threadRoleComposition: 'Composição por função da thread', childThreadsPerRootTask: 'Threads filhas / tarefa raiz', childFreshShare: 'Participação do uso sem cache das threads filhas', approvalFreshShare: 'Participação do uso sem cache de aprovação', highEffortFreshShare: 'Participação do uso sem cache de alto esforço', processedToFreshRatio: 'Processado / uso sem cache', reasoningOutputShare: 'Participação do raciocínio na saída', postPatchToolCallsPerPatchCall: 'Proxy de chamadas de ferramenta pós-patch / chamada de patch', patchCalls: 'Chamadas de patch', compactions: 'Compactações', processed: 'Processado', apiEquivalentCost: 'Custo equivalente de API', apiEquivalentCostHelp: 'Estimado a partir dos Tokens indexados no momento com os preços oficiais atuais da API; não é uma fatura nem uma cobrança de assinatura. Cobertura de preços: {coverage}.', fresh: 'Uso sem cache', input: 'Entrada', cachedInput: 'Entrada em cache', output: 'Saída', reasoning: 'Raciocínio', @@ -1041,12 +1095,12 @@ const PROVIDERS: Record = { TASK5_CODEX_COPY['pt-BR'], ), id: providerTranslations( - { claude: 'Claude', codexBeta: 'Codex Beta', compare: 'Bandingkan' }, + { claude: 'Claude', codexBeta: 'Codex', compare: 'Bandingkan' }, { ...TASK8_CODEX_COPY.id, accountSnapshotLastObserved: 'Penggunaan menggabungkan login di Codex home ini · batas adalah pengamatan terakhir, tidak dijumlahkan', - title: 'Penggunaan Codex', beta: 'Beta', overview: 'Ringkasan', usageTrend: 'Tren penggunaan', daily: 'Harian', date: 'Tanggal', role: 'Peran', scope: 'Cakupan', threadLabel: 'Thread', rootRole: 'Utama', childRole: 'Subagen', approvalReviewerRole: 'Peninjau persetujuan', unknownRole: 'Tidak diketahui', noDailyData: 'Belum ada penggunaan harian Codex yang diindeks.', noThreadData: 'Belum ada thread Codex yang diindeks.', lastTask: 'Tugas terbaru', last7Days: '7 hari terakhir', last30Days: '30 hari terakhir', projects: 'Proyek', projectLabel: 'Proyek', + title: 'Penggunaan Codex', overview: 'Ringkasan', usageTrend: 'Tren penggunaan', daily: 'Harian', date: 'Tanggal', role: 'Peran', scope: 'Cakupan', threadLabel: 'Thread', rootRole: 'Utama', childRole: 'Subagen', approvalReviewerRole: 'Peninjau persetujuan', unknownRole: 'Tidak diketahui', noDailyData: 'Belum ada penggunaan harian Codex yang diindeks.', noThreadData: 'Belum ada thread Codex yang diindeks.', lastTask: 'Tugas terbaru', last7Days: '7 hari terakhir', last30Days: '30 hari terakhir', projects: 'Proyek', projectLabel: 'Proyek', unnamedSession: 'Sesi tanpa nama', unidentifiedProject: 'Proyek tidak teridentifikasi', parentThread: 'Thread induk', parentTask: 'Tugas induk', searchThreads: 'Cari sesi', sessions: 'Sesi', modelsEffort: 'Model & upaya', clearFilters: 'Hapus filter', activeFilters: 'Filter aktif', all: 'Semua', localDirectory: 'Folder lokal', lastActive: 'Terakhir aktif', expand: 'Perluas', viewAllSessions: 'Lihat semua sesi', sortBy: 'Urutkan berdasarkan', usageLimits: 'Batas penggunaan', resets: 'Reset', credits: 'Kredit', unlimited: 'Tanpa batas', allTime: 'Sepanjang waktu', behavior: 'Perilaku', settings: 'Pengaturan', monthly: 'Bulanan', tokenComposition: 'Komposisi token', freshInput: 'Input tanpa cache', reasoningSubset: 'Termasuk dalam output', threadRoleComposition: 'Komposisi peran thread', childThreadsPerRootTask: 'Thread anak / tugas utama', childFreshShare: 'Porsi penggunaan tanpa cache thread anak', approvalFreshShare: 'Porsi penggunaan tanpa cache persetujuan', highEffortFreshShare: 'Porsi penggunaan tanpa cache effort tinggi', processedToFreshRatio: 'Diproses / penggunaan tanpa cache', reasoningOutputShare: 'Porsi penalaran dalam output', postPatchToolCallsPerPatchCall: 'Proksi panggilan alat pasca-patch / panggilan patch', patchCalls: 'Panggilan patch', compactions: 'Pemadatan konteks', processed: 'Diproses', apiEquivalentCost: 'Biaya ekuivalen API', apiEquivalentCostHelp: 'Perkiraan dari Token yang saat ini terindeks dengan harga API resmi terkini; bukan tagihan atau biaya langganan. Cakupan harga: {coverage}.', fresh: 'Penggunaan tanpa cache', input: 'Input', cachedInput: 'Input cache', output: 'Output', reasoning: 'Penalaran', @@ -3635,6 +3689,545 @@ const translations: Record = { // non-English UIs so the descriptions follow the plugin language too. Code / // command tokens (token, /v1/messages, opus:NN%, k/M, git/folder/flat …) are kept // verbatim. Generated with DeepSeek V4 Pro and validated for completeness. +type SharingWorkspaceDerivedField = + | 'thisMonth' + | 'totalTokens' + | 'cacheHitRate' + | 'topModel' + | 'sessionCount' + | 'tokenMix' + | 'messageCount' + | 'projectName'; + +function sharingWorkspaceCopy( + locale: SupportedLanguage, + copy: Omit, +): SharingWorkspaceTranslations { + const popup = translations[locale].popup; + return { + ...copy, + thisMonth: popup.thisMonth, + totalTokens: popup.totalTokens, + cacheHitRate: popup.cacheHitRate, + topModel: popup.model, + sessionCount: popup.sessions, + tokenMix: popup.tokenComposition, + messageCount: popup.messages, + projectName: popup.project, + }; +} + +/** Locale-complete copy for the unified sharing workspace. Presentation names + * remain provider-qualified so the selector never implies metric equivalence. */ +export const SHARING_WORKSPACE_TRANSLATIONS: Record = { + en: sharingWorkspaceCopy('en', { + eyebrow: 'Share', + panelTitle: 'Sharing workspace', + description: 'Choose a presentation, review the full export preview, then use the controls below it.', + privacyBadge: 'Local preview · explicit export', + presentationLabel: 'Presentation', + combinedPresentation: 'Combined activity heatmap', + combinedPresentationDescription: 'Claude and Codex processed-token activity by local calendar day.', + claudeCardPresentation: 'Claude Share Card', + claudeCardPresentationDescription: 'Claude-only usage totals, estimates, and optional aggregate scope details.', + claudeHeatmapPresentation: 'Claude token heatmap', + claudeHeatmapPresentationDescription: 'The legacy Claude-only GitHub-style yearly token heatmap.', + combinedUnavailable: 'Available when both Claude and Codex have indexed daily activity.', + rangeRollingGroup: 'Rolling ranges', + rangeSpecificMonthGroup: 'Specific month', + last7: 'Last 7 days', + last12Months: 'Last 12 months', + todayHourly: 'Today (hourly)', + lastMonthSuffix: 'last month', + scopeLabel: 'Scope', + scopeAllGroup: 'All Claude usage', + overall: 'Overall', + byProject: 'By project', + bySession: 'By session', + themeLabel: 'Theme', + claudeClassic: 'Claude Classic', + claudeCream: 'Claude Cream', + auroraDark: 'Aurora Dark', + autoTheme: 'Auto (follow VS Code)', + cardContentsLabel: 'Card contents', + estimatedCost: 'Estimated cost', + dailyPulse: 'Daily pulse', + badge: 'Badge', + fullNumbers: 'Full numbers', + fullNumbersHelp: 'Show exact token counts instead of compact values such as 1.2M.', + previewPending: 'The preview will appear here.', + generating: 'Generating preview…', + cardBuildFailed: 'Could not build the card:', + publishGitHub: 'Publish to GitHub…', + publishPreferenceSaveWarning: 'The file was published, but this destination could not be saved for next time.', + claudeHeatmapDisclaimer: 'Claude-only token activity. Publishing still requires the existing public-repository checks and exact confirmation.', + noClaudeData: 'No Claude usage is available for sharing yet.', + }), + 'de-DE': sharingWorkspaceCopy('de-DE', { + eyebrow: 'Freigeben', + panelTitle: 'Freigabe-Arbeitsbereich', + description: 'Wähle eine Darstellung, prüfe die vollständige Exportvorschau und nutze danach die darunterliegenden Steuerelemente.', + privacyBadge: 'Lokale Vorschau · ausdrücklicher Export', + presentationLabel: 'Darstellung', + combinedPresentation: 'Kombinierte Aktivitäts-Heatmap', + combinedPresentationDescription: 'Verarbeitete Token-Aktivität von Claude und Codex nach lokalem Kalendertag.', + claudeCardPresentation: 'Claude-Freigabekarte', + claudeCardPresentationDescription: 'Nur Claude: Nutzungssummen, Schätzungen und optionale aggregierte Bereichsdetails.', + claudeHeatmapPresentation: 'Claude-Token-Heatmap', + claudeHeatmapPresentationDescription: 'Die bisherige GitHub-artige Jahres-Heatmap nur für Claude.', + combinedUnavailable: 'Verfügbar, sobald Claude und Codex beide indexierte Tagesaktivität haben.', + rangeRollingGroup: 'Gleitende Zeiträume', + rangeSpecificMonthGroup: 'Bestimmter Monat', + last7: 'Letzte 7 Tage', + last12Months: 'Letzte 12 Monate', + todayHourly: 'Heute (stündlich)', + lastMonthSuffix: 'letzter Monat', + scopeLabel: 'Bereich', + scopeAllGroup: 'Gesamte Claude-Nutzung', + overall: 'Gesamt', + byProject: 'Nach Projekt', + bySession: 'Nach Sitzung', + themeLabel: 'Design', + claudeClassic: 'Claude Classic', + claudeCream: 'Claude Cream', + auroraDark: 'Aurora Dark', + autoTheme: 'Automatisch (VS Code folgen)', + cardContentsLabel: 'Karteninhalt', + estimatedCost: 'Geschätzte Kosten', + dailyPulse: 'Tagesrhythmus', + badge: 'Abzeichen', + fullNumbers: 'Vollständige Zahlen', + fullNumbersHelp: 'Zeigt exakte Tokenzahlen statt kompakter Werte wie 1,2 Mio.', + previewPending: 'Die Vorschau erscheint hier.', + generating: 'Vorschau wird erstellt…', + cardBuildFailed: 'Die Karte konnte nicht erstellt werden:', + publishGitHub: 'Auf GitHub veröffentlichen…', + publishPreferenceSaveWarning: 'Die Datei wurde veröffentlicht, aber dieses Ziel konnte nicht für das nächste Mal gespeichert werden.', + claudeHeatmapDisclaimer: 'Nur Claude-Tokenaktivität. Für die Veröffentlichung gelten weiterhin die Prüfungen für öffentliche Repositories und die exakte Bestätigung.', + noClaudeData: 'Noch keine Claude-Nutzung zum Teilen verfügbar.', + }), + 'zh-TW': sharingWorkspaceCopy('zh-TW', { + eyebrow: '分享', + panelTitle: '分享工作區', + description: '選擇呈現方式,先檢查完整匯出預覽,再使用下方控制項。', + privacyBadge: '本機預覽 · 明確匯出', + presentationLabel: '呈現方式', + combinedPresentation: '綜合活動熱力圖', + combinedPresentationDescription: '依本機自然日呈現 Claude 與 Codex 的已處理 Token 活動量。', + claudeCardPresentation: 'Claude 分享卡', + claudeCardPresentationDescription: '僅含 Claude 用量總計、估算與可選的彙總範圍資訊。', + claudeHeatmapPresentation: 'Claude Token 熱力圖', + claudeHeatmapPresentationDescription: '舊版僅 Claude 的 GitHub 風格年度 Token 熱力圖。', + combinedUnavailable: 'Claude 與 Codex 都有已索引的每日活動後即可使用。', + rangeRollingGroup: '滾動範圍', + rangeSpecificMonthGroup: '指定月份', + last7: '最近 7 天', + last12Months: '最近 12 個月', + todayHourly: '今天(每小時)', + lastMonthSuffix: '上個月', + scopeLabel: '範圍', + scopeAllGroup: '全部 Claude 用量', + overall: '整體', + byProject: '依專案', + bySession: '依工作階段', + themeLabel: '主題', + claudeClassic: 'Claude 經典', + claudeCream: 'Claude 奶油', + auroraDark: '極光深色', + autoTheme: '自動(跟隨 VS Code)', + cardContentsLabel: '卡片內容', + estimatedCost: '預估成本', + dailyPulse: '每日節奏', + badge: '徽章', + fullNumbers: '完整數字', + fullNumbersHelp: '顯示精確 Token 數,而不是 1.2M 等精簡值。', + previewPending: '預覽會顯示在這裡。', + generating: '正在產生預覽…', + cardBuildFailed: '無法建立卡片:', + publishGitHub: '發佈到 GitHub…', + publishPreferenceSaveWarning: '檔案已發佈,但無法儲存此目的地供下次使用。', + claudeHeatmapDisclaimer: '僅顯示 Claude Token 活動。發佈仍須通過既有的公開儲存庫檢查與精確確認。', + noClaudeData: '尚無可分享的 Claude 用量。', + }), + 'zh-CN': sharingWorkspaceCopy('zh-CN', { + eyebrow: '分享', + panelTitle: '分享工作台', + description: '选择呈现方式,先检查完整导出预览,再使用下方控件。', + privacyBadge: '本地预览 · 明确导出', + presentationLabel: '呈现方式', + combinedPresentation: '综合活动热力图', + combinedPresentationDescription: '按本地自然日呈现 Claude 与 Codex 的已处理 Token 活动量。', + claudeCardPresentation: 'Claude 分享卡', + claudeCardPresentationDescription: '仅含 Claude 用量总计、估算与可选的汇总范围信息。', + claudeHeatmapPresentation: 'Claude Token 热力图', + claudeHeatmapPresentationDescription: '旧版仅 Claude 的 GitHub 风格年度 Token 热力图。', + combinedUnavailable: 'Claude 与 Codex 都有已索引的每日活动后即可使用。', + rangeRollingGroup: '滚动范围', + rangeSpecificMonthGroup: '指定月份', + last7: '最近 7 天', + last12Months: '最近 12 个月', + todayHourly: '今天(每小时)', + lastMonthSuffix: '上个月', + scopeLabel: '范围', + scopeAllGroup: '全部 Claude 用量', + overall: '总体', + byProject: '按项目', + bySession: '按会话', + themeLabel: '主题', + claudeClassic: 'Claude 经典', + claudeCream: 'Claude 奶油', + auroraDark: '极光深色', + autoTheme: '自动(跟随 VS Code)', + cardContentsLabel: '卡片内容', + estimatedCost: '预估成本', + dailyPulse: '每日节奏', + badge: '徽章', + fullNumbers: '完整数字', + fullNumbersHelp: '显示精确 Token 数,而不是 1.2M 等紧凑值。', + previewPending: '预览会显示在这里。', + generating: '正在生成预览…', + cardBuildFailed: '无法生成卡片:', + publishGitHub: '发布到 GitHub…', + publishPreferenceSaveWarning: '文件已发布,但无法保存此目标供下次使用。', + claudeHeatmapDisclaimer: '仅显示 Claude Token 活动。发布仍须通过现有的公共仓库检查与精确确认。', + noClaudeData: '尚无可分享的 Claude 用量。', + }), + ja: sharingWorkspaceCopy('ja', { + eyebrow: '共有', + panelTitle: '共有ワークスペース', + description: '表示形式を選び、完全な書き出しプレビューを確認してから、下のコントロールを使用します。', + privacyBadge: 'ローカルプレビュー · 明示的な書き出し', + presentationLabel: '表示形式', + combinedPresentation: '統合アクティビティヒートマップ', + combinedPresentationDescription: 'ローカル暦日ごとの Claude と Codex の処理済みトークン活動。', + claudeCardPresentation: 'Claude 共有カード', + claudeCardPresentationDescription: 'Claude のみの使用量合計、推定値、任意の集計範囲情報。', + claudeHeatmapPresentation: 'Claude トークンヒートマップ', + claudeHeatmapPresentationDescription: '従来の Claude 専用 GitHub 風年間トークンヒートマップ。', + combinedUnavailable: 'Claude と Codex の両方にインデックス済みの日別活動があると利用できます。', + rangeRollingGroup: 'ローリング期間', + rangeSpecificMonthGroup: '指定月', + last7: '過去 7 日間', + last12Months: '過去 12 か月', + todayHourly: '今日(1時間ごと)', + lastMonthSuffix: '先月', + scopeLabel: '範囲', + scopeAllGroup: 'Claude の全使用量', + overall: '全体', + byProject: 'プロジェクト別', + bySession: 'セッション別', + themeLabel: 'テーマ', + claudeClassic: 'Claude クラシック', + claudeCream: 'Claude クリーム', + auroraDark: 'オーロラダーク', + autoTheme: '自動(VS Code に合わせる)', + cardContentsLabel: 'カード内容', + estimatedCost: '推定コスト', + dailyPulse: '日別パルス', + badge: 'バッジ', + fullNumbers: '完全な数値', + fullNumbersHelp: '1.2M のような省略値ではなく正確なトークン数を表示します。', + previewPending: 'プレビューはここに表示されます。', + generating: 'プレビューを生成中…', + cardBuildFailed: 'カードを作成できませんでした:', + publishGitHub: 'GitHub に公開…', + publishPreferenceSaveWarning: 'ファイルは公開されましたが、次回用にこの公開先を保存できませんでした。', + claudeHeatmapDisclaimer: 'Claude のトークン活動のみです。公開には従来どおり公開リポジトリの検査と正確な確認が必要です。', + noClaudeData: '共有できる Claude 使用量はまだありません。', + }), + ko: sharingWorkspaceCopy('ko', { + eyebrow: '공유', + panelTitle: '공유 작업 공간', + description: '표시 형식을 선택하고 전체 내보내기 미리보기를 확인한 뒤 아래 컨트롤을 사용하세요.', + privacyBadge: '로컬 미리보기 · 명시적 내보내기', + presentationLabel: '표시 형식', + combinedPresentation: '통합 활동 히트맵', + combinedPresentationDescription: '로컬 달력 날짜별 Claude와 Codex의 처리된 토큰 활동입니다.', + claudeCardPresentation: 'Claude 공유 카드', + claudeCardPresentationDescription: 'Claude 전용 사용량 합계, 추정치 및 선택적 집계 범위 정보입니다.', + claudeHeatmapPresentation: 'Claude 토큰 히트맵', + claudeHeatmapPresentationDescription: '기존 Claude 전용 GitHub 스타일 연간 토큰 히트맵입니다.', + combinedUnavailable: 'Claude와 Codex 모두 인덱싱된 일별 활동이 있을 때 사용할 수 있습니다.', + rangeRollingGroup: '이동 범위', + rangeSpecificMonthGroup: '특정 월', + last7: '최근 7일', + last12Months: '최근 12개월', + todayHourly: '오늘(시간별)', + lastMonthSuffix: '지난달', + scopeLabel: '범위', + scopeAllGroup: '전체 Claude 사용량', + overall: '전체', + byProject: '프로젝트별', + bySession: '세션별', + themeLabel: '테마', + claudeClassic: 'Claude 클래식', + claudeCream: 'Claude 크림', + auroraDark: '오로라 다크', + autoTheme: '자동(VS Code 따름)', + cardContentsLabel: '카드 내용', + estimatedCost: '예상 비용', + dailyPulse: '일별 흐름', + badge: '배지', + fullNumbers: '전체 숫자', + fullNumbersHelp: '1.2M 같은 축약값 대신 정확한 토큰 수를 표시합니다.', + previewPending: '미리보기가 여기에 표시됩니다.', + generating: '미리보기 생성 중…', + cardBuildFailed: '카드를 만들 수 없습니다:', + publishGitHub: 'GitHub에 게시…', + publishPreferenceSaveWarning: '파일은 게시되었지만 다음 사용을 위해 이 대상을 저장하지 못했습니다.', + claudeHeatmapDisclaimer: 'Claude 토큰 활동만 표시합니다. 게시하려면 기존 공개 저장소 검사와 정확한 확인이 계속 필요합니다.', + noClaudeData: '아직 공유할 Claude 사용량이 없습니다.', + }), + 'pt-BR': sharingWorkspaceCopy('pt-BR', { + eyebrow: 'Compartilhar', + panelTitle: 'Espaço de compartilhamento', + description: 'Escolha uma apresentação, revise a prévia completa da exportação e então use os controles abaixo dela.', + privacyBadge: 'Prévia local · exportação explícita', + presentationLabel: 'Apresentação', + combinedPresentation: 'Mapa de calor de atividade combinado', + combinedPresentationDescription: 'Atividade de tokens processados do Claude e do Codex por dia do calendário local.', + claudeCardPresentation: 'Cartão de compartilhamento do Claude', + claudeCardPresentationDescription: 'Totais, estimativas e detalhes opcionais de escopo agregado somente do Claude.', + claudeHeatmapPresentation: 'Mapa de calor de tokens do Claude', + claudeHeatmapPresentationDescription: 'O mapa de calor anual legado, estilo GitHub, somente do Claude.', + combinedUnavailable: 'Disponível quando Claude e Codex têm atividade diária indexada.', + rangeRollingGroup: 'Intervalos móveis', + rangeSpecificMonthGroup: 'Mês específico', + last7: 'Últimos 7 dias', + last12Months: 'Últimos 12 meses', + todayHourly: 'Hoje (por hora)', + lastMonthSuffix: 'mês passado', + scopeLabel: 'Escopo', + scopeAllGroup: 'Todo o uso do Claude', + overall: 'Geral', + byProject: 'Por projeto', + bySession: 'Por sessão', + themeLabel: 'Tema', + claudeClassic: 'Claude Clássico', + claudeCream: 'Claude Creme', + auroraDark: 'Aurora Escuro', + autoTheme: 'Automático (seguir o VS Code)', + cardContentsLabel: 'Conteúdo do cartão', + estimatedCost: 'Custo estimado', + dailyPulse: 'Pulso diário', + badge: 'Selo', + fullNumbers: 'Números completos', + fullNumbersHelp: 'Mostra contagens exatas de tokens em vez de valores compactos como 1,2 mi.', + previewPending: 'A prévia aparecerá aqui.', + generating: 'Gerando prévia…', + cardBuildFailed: 'Não foi possível criar o cartão:', + publishGitHub: 'Publicar no GitHub…', + publishPreferenceSaveWarning: 'O arquivo foi publicado, mas este destino não pôde ser salvo para a próxima vez.', + claudeHeatmapDisclaimer: 'Somente atividade de tokens do Claude. A publicação ainda exige as verificações de repositório público e a confirmação exata existentes.', + noClaudeData: 'Ainda não há uso do Claude disponível para compartilhar.', + }), + id: sharingWorkspaceCopy('id', { + eyebrow: 'Bagikan', + panelTitle: 'Ruang kerja berbagi', + description: 'Pilih presentasi, tinjau pratinjau ekspor lengkap, lalu gunakan kontrol di bawahnya.', + privacyBadge: 'Pratinjau lokal · ekspor eksplisit', + presentationLabel: 'Presentasi', + combinedPresentation: 'Heatmap aktivitas gabungan', + combinedPresentationDescription: 'Aktivitas token yang diproses Claude dan Codex per hari kalender lokal.', + claudeCardPresentation: 'Kartu Berbagi Claude', + claudeCardPresentationDescription: 'Total penggunaan, estimasi, dan detail cakupan agregat opsional khusus Claude.', + claudeHeatmapPresentation: 'Heatmap token Claude', + claudeHeatmapPresentationDescription: 'Heatmap token tahunan gaya GitHub lama yang hanya mencakup Claude.', + combinedUnavailable: 'Tersedia saat Claude dan Codex memiliki aktivitas harian terindeks.', + rangeRollingGroup: 'Rentang bergulir', + rangeSpecificMonthGroup: 'Bulan tertentu', + last7: '7 hari terakhir', + last12Months: '12 bulan terakhir', + todayHourly: 'Hari ini (per jam)', + lastMonthSuffix: 'bulan lalu', + scopeLabel: 'Cakupan', + scopeAllGroup: 'Semua penggunaan Claude', + overall: 'Keseluruhan', + byProject: 'Menurut proyek', + bySession: 'Menurut sesi', + themeLabel: 'Tema', + claudeClassic: 'Claude Klasik', + claudeCream: 'Claude Krem', + auroraDark: 'Aurora Gelap', + autoTheme: 'Otomatis (ikuti VS Code)', + cardContentsLabel: 'Isi kartu', + estimatedCost: 'Perkiraan biaya', + dailyPulse: 'Denyut harian', + badge: 'Lencana', + fullNumbers: 'Angka lengkap', + fullNumbersHelp: 'Tampilkan jumlah token persis, bukan nilai ringkas seperti 1,2 jt.', + previewPending: 'Pratinjau akan muncul di sini.', + generating: 'Membuat pratinjau…', + cardBuildFailed: 'Kartu tidak dapat dibuat:', + publishGitHub: 'Publikasikan ke GitHub…', + publishPreferenceSaveWarning: 'File telah dipublikasikan, tetapi tujuan ini tidak dapat disimpan untuk penggunaan berikutnya.', + claudeHeatmapDisclaimer: 'Hanya aktivitas token Claude. Publikasi tetap memerlukan pemeriksaan repositori publik dan konfirmasi target yang persis.', + noClaudeData: 'Belum ada penggunaan Claude yang dapat dibagikan.', + }), +}; + +export interface ShareCardArtifactTranslations { + subtitle: string; + totalTokens: string; + spent: string; + sessionsUnit: string; + estCost: string; + cacheHit: string; + topModel: string; + sessions: string; + messages: string; + workflows: string; + peakCtx: string; + tokenMix: string; + input: string; + output: string; + cacheWrite: string; + cacheRead: string; + daily: string; + hourly: string; + peak: string; + madeWith: string; + today: string; + last7: string; + last30: string; + thisMonth: string; + last12Months: string; +} + +export interface HeatmapArtifactTranslations { + tokens: string; + sessions: string; + usage: string; + inClaudeCode: string; + no: string; + on: string; + less: string; + more: string; + madeWith: string; + combinedTitle: string; + combined: string; + processedTokens: string; + combinedFooter: string; +} + +export const SHARE_CARD_ARTIFACT_TRANSLATIONS: Record = { + en: { + subtitle: 'AI coding usage snapshot', totalTokens: 'total tokens', spent: 'estimated spend', sessionsUnit: 'sessions', + estCost: 'est. cost', cacheHit: 'cache hit', topModel: 'top model', sessions: 'sessions', messages: 'messages', + workflows: 'workflows', peakCtx: 'peak ctx', tokenMix: 'Token mix', input: 'Input', output: 'Output', + cacheWrite: 'Cache write', cacheRead: 'Cache read', daily: 'Daily pulse', hourly: 'Hourly pulse', peak: 'peak', + madeWith: 'Made with Claude Code Usage', today: 'today', last7: 'the last 7 days', last30: 'the last 30 days', + thisMonth: 'this month', last12Months: 'the last 12 months', + }, + 'de-DE': { + subtitle: 'Momentaufnahme der KI-Programmier-Nutzung', totalTokens: 'Gesamttoken', spent: 'geschätzte Ausgaben', sessionsUnit: 'Sitzungen', + estCost: 'geschätzte Kosten', cacheHit: 'Cache-Treffer', topModel: 'Topmodell', sessions: 'Sitzungen', messages: 'Nachrichten', + workflows: 'Workflows', peakCtx: 'max. Kontext', tokenMix: 'Token-Mix', input: 'Eingabe', output: 'Ausgabe', + cacheWrite: 'Cache-Schreiben', cacheRead: 'Cache-Lesen', daily: 'Tagesverlauf', hourly: 'Stundenverlauf', peak: 'Spitze', + madeWith: 'Erstellt mit Claude Code Usage', today: 'heute', last7: 'die letzten 7 Tage', last30: 'die letzten 30 Tage', + thisMonth: 'dieser Monat', last12Months: 'die letzten 12 Monate', + }, + 'zh-TW': { + subtitle: 'AI 程式設計用量快照', totalTokens: '總 token', spent: '預計花費', sessionsUnit: '會話', + estCost: '預計成本', cacheHit: '快取命中', topModel: '主力模型', sessions: '會話數', messages: '訊息數', + workflows: '工作流', peakCtx: '峰值上下文', tokenMix: 'Token 組成', input: '輸入', output: '輸出', + cacheWrite: '快取寫入', cacheRead: '快取讀取', daily: '每日節奏', hourly: '每小時節奏', peak: '峰值', + madeWith: '由 Claude Code Usage 製作', today: '今天', last7: '最近 7 天', last30: '最近 30 天', + thisMonth: '本月', last12Months: '最近 12 個月', + }, + 'zh-CN': { + subtitle: 'AI 编程用量快照', totalTokens: '总 token', spent: '预计花费', sessionsUnit: '会话', + estCost: '预计成本', cacheHit: '缓存命中', topModel: '主力模型', sessions: '会话数', messages: '消息数', + workflows: '工作流', peakCtx: '峰值上下文', tokenMix: 'Token 组成', input: '输入', output: '输出', + cacheWrite: '缓存写入', cacheRead: '缓存读取', daily: '每日节奏', hourly: '每小时节奏', peak: '峰值', + madeWith: '由 Claude Code Usage 制作', today: '今天', last7: '最近 7 天', last30: '最近 30 天', + thisMonth: '本月', last12Months: '最近 12 个月', + }, + ja: { + subtitle: 'AI コーディング使用量のスナップショット', totalTokens: '総トークン', spent: '推定使用額', sessionsUnit: 'セッション', + estCost: '推定コスト', cacheHit: 'キャッシュヒット', topModel: '最多モデル', sessions: 'セッション', messages: 'メッセージ', + workflows: 'ワークフロー', peakCtx: '最大コンテキスト', tokenMix: 'トークン構成', input: '入力', output: '出力', + cacheWrite: 'キャッシュ書込', cacheRead: 'キャッシュ読込', daily: '日別推移', hourly: '時間別推移', peak: 'ピーク', + madeWith: 'Claude Code Usage で作成', today: '今日', last7: '過去 7 日間', last30: '過去 30 日間', + thisMonth: '今月', last12Months: '過去 12 か月', + }, + ko: { + subtitle: 'AI 코딩 사용량 스냅샷', totalTokens: '총 토큰', spent: '예상 지출', sessionsUnit: '세션', + estCost: '예상 비용', cacheHit: '캐시 적중', topModel: '최다 모델', sessions: '세션', messages: '메시지', + workflows: '워크플로', peakCtx: '최대 컨텍스트', tokenMix: '토큰 구성', input: '입력', output: '출력', + cacheWrite: '캐시 쓰기', cacheRead: '캐시 읽기', daily: '일별 흐름', hourly: '시간별 흐름', peak: '최고', + madeWith: 'Claude Code Usage로 제작', today: '오늘', last7: '최근 7일', last30: '최근 30일', + thisMonth: '이번 달', last12Months: '최근 12개월', + }, + 'pt-BR': { + subtitle: 'Resumo do uso de programação com IA', totalTokens: 'total de tokens', spent: 'gasto estimado', sessionsUnit: 'sessões', + estCost: 'custo estimado', cacheHit: 'acerto de cache', topModel: 'modelo principal', sessions: 'sessões', messages: 'mensagens', + workflows: 'fluxos', peakCtx: 'pico de contexto', tokenMix: 'Composição de tokens', input: 'Entrada', output: 'Saída', + cacheWrite: 'Gravação em cache', cacheRead: 'Leitura do cache', daily: 'Pulso diário', hourly: 'Pulso por hora', peak: 'pico', + madeWith: 'Criado com Claude Code Usage', today: 'hoje', last7: 'os últimos 7 dias', last30: 'os últimos 30 dias', + thisMonth: 'este mês', last12Months: 'os últimos 12 meses', + }, + id: { + subtitle: 'Ringkasan penggunaan coding AI', totalTokens: 'total token', spent: 'perkiraan pengeluaran', sessionsUnit: 'sesi', + estCost: 'perkiraan biaya', cacheHit: 'cache hit', topModel: 'model teratas', sessions: 'sesi', messages: 'pesan', + workflows: 'alur kerja', peakCtx: 'konteks puncak', tokenMix: 'Komposisi token', input: 'Input', output: 'Output', + cacheWrite: 'Tulis cache', cacheRead: 'Baca cache', daily: 'Denyut harian', hourly: 'Denyut per jam', peak: 'puncak', + madeWith: 'Dibuat dengan Claude Code Usage', today: 'hari ini', last7: '7 hari terakhir', last30: '30 hari terakhir', + thisMonth: 'bulan ini', last12Months: '12 bulan terakhir', + }, +}; + +export const HEATMAP_ARTIFACT_TRANSLATIONS: Record = { + en: { tokens: 'tokens', sessions: 'sessions', usage: 'usage', inClaudeCode: 'in Claude Code', no: 'No', on: 'on', less: 'Less', more: 'More', madeWith: 'Made with Claude Code Usage', combinedTitle: 'Claude + Codex local activity', combined: 'Combined', processedTokens: 'processed tokens', combinedFooter: 'Local activity volume · not productivity, billing, or provider equivalence' }, + 'de-DE': { tokens: 'Token', sessions: 'Sitzungen', usage: 'Nutzung', inClaudeCode: 'in Claude Code', no: 'Keine', on: 'am', less: 'Weniger', more: 'Mehr', madeWith: 'Erstellt mit Claude Code Usage', combinedTitle: 'Lokale Aktivität von Claude + Codex', combined: 'Kombiniert', processedTokens: 'verarbeitete Token', combinedFooter: 'Lokales Aktivitätsvolumen · keine Produktivität, Abrechnung oder Anbieter-Gleichwertigkeit' }, + 'zh-TW': { tokens: 'Token', sessions: '會話', usage: '用量', inClaudeCode: '於 Claude Code', no: '無', on: '日期', less: '較少', more: '較多', madeWith: '由 Claude Code Usage 製作', combinedTitle: 'Claude + Codex 本機活動', combined: '綜合', processedTokens: '已處理 Token', combinedFooter: '本機活動量 · 不代表生產力、帳單或供應商等價性' }, + 'zh-CN': { tokens: 'Token', sessions: '会话', usage: '用量', inClaudeCode: '于 Claude Code', no: '无', on: '日期', less: '较少', more: '较多', madeWith: '由 Claude Code Usage 制作', combinedTitle: 'Claude + Codex 本地活动', combined: '综合', processedTokens: '已处理 Token', combinedFooter: '本地活动量 · 不代表生产力、账单或供应商等价性' }, + ja: { tokens: 'トークン', sessions: 'セッション', usage: '使用量', inClaudeCode: '(Claude Code)', no: 'なし:', on: '日付', less: '少ない', more: '多い', madeWith: 'Claude Code Usage で作成', combinedTitle: 'Claude + Codex のローカルアクティビティ', combined: '合計', processedTokens: '処理済みトークン', combinedFooter: 'ローカル活動量 · 生産性、請求、プロバイダー同等性を示すものではありません' }, + ko: { tokens: '토큰', sessions: '세션', usage: '사용량', inClaudeCode: 'Claude Code에서', no: '없음:', on: '날짜', less: '적게', more: '많이', madeWith: 'Claude Code Usage로 제작', combinedTitle: 'Claude + Codex 로컬 활동', combined: '합계', processedTokens: '처리된 토큰', combinedFooter: '로컬 활동량 · 생산성, 청구 또는 제공자 동등성을 의미하지 않음' }, + 'pt-BR': { tokens: 'tokens', sessions: 'sessões', usage: 'uso', inClaudeCode: 'no Claude Code', no: 'Sem', on: 'em', less: 'Menos', more: 'Mais', madeWith: 'Criado com Claude Code Usage', combinedTitle: 'Atividade local do Claude + Codex', combined: 'Combinado', processedTokens: 'tokens processados', combinedFooter: 'Volume de atividade local · não representa produtividade, cobrança ou equivalência entre provedores' }, + id: { tokens: 'token', sessions: 'sesi', usage: 'penggunaan', inClaudeCode: 'di Claude Code', no: 'Tidak ada', on: 'pada', less: 'Lebih sedikit', more: 'Lebih banyak', madeWith: 'Dibuat dengan Claude Code Usage', combinedTitle: 'Aktivitas lokal Claude + Codex', combined: 'Gabungan', processedTokens: 'token diproses', combinedFooter: 'Volume aktivitas lokal · bukan produktivitas, tagihan, atau kesetaraan penyedia' }, +}; + +export const SHARE_BADGE_TRANSLATIONS: Record> = { + 'context-marathoner': { + en: { title: 'Context Marathon', line: "Half-marathon of context — the model's still catching its breath." }, + 'de-DE': { title: 'Kontextmarathon', line: 'Ein Halbmarathon an Kontext — das Modell holt noch Luft.' }, + 'zh-TW': { title: 'Context 馬拉松', line: '上下文跑了個半馬,模型還在喘。' }, 'zh-CN': { title: 'Context 马拉松', line: '上下文跑了个半马,模型还在喘。' }, + ja: { title: 'コンテキストマラソン', line: 'コンテキストはハーフマラソン級。モデルも息切れ中。' }, ko: { title: '컨텍스트 마라톤', line: '컨텍스트가 하프 마라톤급이라 모델도 숨을 고르는 중.' }, + 'pt-BR': { title: 'Maratona de contexto', line: 'Uma meia maratona de contexto — o modelo ainda recupera o fôlego.' }, id: { title: 'Maraton konteks', line: 'Konteks setara setengah maraton — model masih mengatur napas.' }, + }, + 'cache-saver': { + en: { title: 'Cache Alchemist', line: 'High cache hits — barely a token wasted.' }, + 'de-DE': { title: 'Cache-Alchemist', line: 'Hohe Cache-Treffer — kaum ein Token verschwendet.' }, + 'zh-TW': { title: '快取日子人', line: '快取命中高,token 沒白燒。' }, 'zh-CN': { title: '缓存日子人', line: '缓存命中高,token 没白烧。' }, + ja: { title: 'キャッシュ錬金術師', line: '高いキャッシュヒット率。無駄なトークンはほぼなし。' }, ko: { title: '캐시 연금술사', line: '높은 캐시 적중률로 낭비되는 토큰이 거의 없습니다.' }, + 'pt-BR': { title: 'Alquimista do cache', line: 'Muitos acertos de cache — quase nenhum token desperdiçado.' }, id: { title: 'Alkemis cache', line: 'Cache hit tinggi — nyaris tak ada token terbuang.' }, + }, + 'token-sprinter': { + en: { title: 'Token Sprinter', line: 'Full throttle. 🔥' }, 'de-DE': { title: 'Token-Sprinter', line: 'Vollgas. 🔥' }, + 'zh-TW': { title: '無限火力', line: '開炮!!!' }, 'zh-CN': { title: '无限火力', line: '开炮!!!' }, + ja: { title: 'トークンスプリンター', line: '全開です。🔥' }, ko: { title: '토큰 스프린터', line: '전속력. 🔥' }, + 'pt-BR': { title: 'Velocista de tokens', line: 'A todo vapor. 🔥' }, id: { title: 'Sprinter token', line: 'Kecepatan penuh. 🔥' }, + }, + 'workflow-pilot': { + en: { title: 'Workflow Pilot', line: "You're not coding — you're running a crew." }, 'de-DE': { title: 'Workflow-Pilot', line: 'Du programmierst nicht nur — du leitest ein Team.' }, + 'zh-TW': { title: 'Agent 包工頭', line: '你不是在寫程式,你是在使喚一支小隊。' }, 'zh-CN': { title: 'Agent 包工头', line: '你不是在写代码,你是在使唤一支小队。' }, + ja: { title: 'ワークフローパイロット', line: 'コーディングではなく、チームを動かしています。' }, ko: { title: '워크플로 파일럿', line: '코딩을 넘어 하나의 팀을 지휘하고 있습니다.' }, + 'pt-BR': { title: 'Piloto de fluxos', line: 'Você não está só programando — está liderando uma equipe.' }, id: { title: 'Pilot alur kerja', line: 'Anda bukan sekadar coding — Anda memimpin tim.' }, + }, + 'steady-builder': { + en: { title: 'Steady Builder', line: 'No rush, no burnout — steady progress.' }, 'de-DE': { title: 'Stetiger Builder', line: 'Keine Hektik, kein Burnout — stetiger Fortschritt.' }, + 'zh-TW': { title: '節奏大師', line: '不捲不燥,程式碼穩步推進。' }, 'zh-CN': { title: '节奏大师', line: '不卷不燥,代码稳步推进。' }, + ja: { title: '着実なビルダー', line: '焦らず燃え尽きず、着実に前進。' }, ko: { title: '꾸준한 빌더', line: '서두르지 않고 번아웃 없이 꾸준히 전진.' }, + 'pt-BR': { title: 'Construtor constante', line: 'Sem pressa nem esgotamento — progresso constante.' }, id: { title: 'Pembangun konsisten', line: 'Tanpa terburu-buru atau burnout — terus maju.' }, + }, +}; + +export function artifactLocale(value: string | undefined): SupportedLanguage { + return value === 'de-DE' || value === 'zh-TW' || value === 'zh-CN' || value === 'ja' || + value === 'ko' || value === 'pt-BR' || value === 'id' ? value : 'en'; +} + const SETTINGS_I18N: Partial>> = { 'de-DE': { 'language': { label: 'Anzeigesprache', help: 'UI-Sprache. "auto" folgt VS Code.' }, @@ -3644,19 +4237,19 @@ const SETTINGS_I18N: Partial= 0 && tsMs - prev <= windowMs; } diff --git a/src/settings.ts b/src/settings.ts index a1fde7d9..667cc93d 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -52,6 +52,9 @@ export interface SettingDef { maxLength?: number; secret?: boolean; // mask the input (apiKey) multiline?: boolean; // render a textarea + // Compatibility-only settings remain readable/resettable but are omitted + // from the dashboard so a superseding control can be the single UI switch. + visible?: boolean; // Dashboard visibility. Omitted settings are Claude-only; explicitly list // both providers for truly shared controls. providers?: SettingProvider[]; @@ -305,7 +308,7 @@ export const SETTINGS: SettingDef[] = [ default: true, storage: 'state', group: 'providers', - label: 'Enable Codex Beta', + label: 'Enable Codex', help: 'Read privacy-safe usage aggregates from local Codex session logs.', providers: ['claude', 'codex'], }, @@ -369,6 +372,7 @@ export const SETTINGS: SettingDef[] = [ group: 'features', label: 'Show token heatmap (All-time tab)', help: 'Show a GitHub-style yearly token heatmap on the All tab. Off by default — mainly a shareable view of what you can already see elsewhere. Use "Export Token Heatmap" for a GitHub-profile SVG.', + visible: false, }, { key: 'showEfficiency', @@ -386,7 +390,7 @@ export const SETTINGS: SettingDef[] = [ storage: 'state', group: 'features', label: 'Enable sharing workspace', - help: 'On by default. Show the Compare sharing workspace and provider share card. Turn it off to hide sharing UI; exporting still requires an explicit action.', + help: 'On by default. Show the single preview-first sharing workspace. Turn it off to hide sharing UI; the legacy export commands still open the matching preview explicitly.', providers: ['claude', 'codex'], }, { @@ -465,13 +469,13 @@ export const SETTINGS: SettingDef[] = [ { key: 'codex.statusMetric', type: 'enum', - default: 'fresh', + default: 'processed', storage: 'state', group: 'statusBar', label: 'Codex status metric', - help: "Today's uncached usage, processed tokens, or output tokens.", - enumValues: ['fresh', 'processed', 'output'], - enumLabels: ['Uncached', 'Processed', 'Output'], + help: "Today's processed tokens (default), uncached usage, or output tokens.", + enumValues: ['processed', 'fresh', 'output'], + enumLabels: ['Processed', 'Uncached', 'Output'], providers: ['codex'], }, { diff --git a/src/shareCardSvg.ts b/src/shareCardSvg.ts index 5928e410..aaf6b6c6 100644 --- a/src/shareCardSvg.ts +++ b/src/shareCardSvg.ts @@ -9,19 +9,26 @@ // • auroraDark — technical, high-contrast, social-share ready. // Landscape 1200×680 only for now (square/portrait/story = a later patch). -import { ShareCardData, rangeLabel } from './shareCard'; +import { ShareCardData, ShareRange } from './shareCard'; +import { escapeSvgAttribute, escapeSvgText } from './svgEscape'; +import { + SHARE_BADGE_TRANSLATIONS, + SHARE_CARD_ARTIFACT_TRANSLATIONS, + ShareCardArtifactTranslations, + artifactLocale, +} from './i18n'; +import { SupportedLanguage } from './types'; const FONT = '-apple-system,BlinkMacSystemFont,"Segoe UI","Inter","Microsoft YaHei","PingFang SC",sans-serif'; -const esc = (s: string): string => - String(s).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); +const esc = escapeSvgText; const truncate = (s: string, n: number): string => (s.length > n ? s.slice(0, n - 1) + '…' : s); // Compact token count. Chinese cards use 万 / 亿 (万 / 億 for Traditional) to fit // the language; others use K / M / B. `keepDecimal` forces one decimal (the hero // keeps it even for round values, e.g. "5.0亿"). -function compact(n: number, lang: 'en' | 'zh-CN' | 'zh-TW' = 'en', keepDecimal = false): string { +function compact(n: number, lang: CardLang = 'en', keepDecimal = false): string { const a = Math.abs(n); const t = (x: number): string => (keepDecimal ? x.toFixed(1) : x.toFixed(1).replace(/\.0$/, '')); if (lang === 'zh-CN' || lang === 'zh-TW') { @@ -31,6 +38,13 @@ function compact(n: number, lang: 'en' | 'zh-CN' | 'zh-TW' = 'en', keepDecimal = if (a >= 1e4) return t(n / 1e4) + wan; return String(Math.round(n)); } + if (lang !== 'en') { + return new Intl.NumberFormat(lang, { + notation: 'compact', + minimumFractionDigits: keepDecimal ? 1 : 0, + maximumFractionDigits: 1, + }).format(n); + } if (a >= 1e9) return t(n / 1e9) + 'B'; if (a >= 1e6) return t(n / 1e6) + 'M'; if (a >= 1e3) return t(n / 1e3) + 'K'; @@ -41,11 +55,15 @@ function money(n: number): string { return n >= 100 ? '$' + Math.round(n).toLocaleString('en-US') : '$' + n.toFixed(2); } -const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; -function shortDay(iso: string | undefined): string { +function shortDay(iso: string | undefined, locale: SupportedLanguage): string { if (!iso) return ''; const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(iso); - return m ? `${MONTHS[Number(m[2]) - 1] || m[2]} ${Number(m[3])}` : iso; + if (!m) return iso; + return new Intl.DateTimeFormat(locale, { + month: 'short', + day: 'numeric', + timeZone: 'UTC', + }).format(new Date(Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3])))); } // ---- Theme tokens ----------------------------------------------------------- @@ -91,10 +109,10 @@ export const SHARE_CARD_THEMES: Record = { panelFill: 'rgba(255,255,255,0.74)', panelBorder: '#F0D8C9', primaryText: '#2B2B2B', - secondaryText: '#6B6B6B', - mutedText: '#9A8478', + secondaryText: '#57443A', + mutedText: '#654E43', softLine: '#F0D8C9', - watermark: 'rgba(43,43,43,0.50)', + watermark: '#654E43', badgeFill: 'rgba(255,255,255,0.86)', badgeBorder: 'rgba(200,90,43,0.16)', }, @@ -111,10 +129,10 @@ export const SHARE_CARD_THEMES: Record = { panelFill: 'rgba(255,255,255,0.82)', panelBorder: 'rgba(200,90,43,0.14)', primaryText: '#6B341F', - secondaryText: '#9A6A55', - mutedText: '#B9907E', + secondaryText: '#5F3527', + mutedText: '#684033', softLine: '#EFD8CB', - watermark: 'rgba(107,52,31,0.58)', + watermark: '#684033', badgeFill: 'rgba(255,255,255,0.86)', badgeBorder: 'rgba(200,90,43,0.20)', }, @@ -131,10 +149,10 @@ export const SHARE_CARD_THEMES: Record = { panelFill: 'rgba(255,255,255,0.10)', panelBorder: 'rgba(255,255,255,0.20)', primaryText: '#FFF7F2', - secondaryText: '#C9D1D9', - mutedText: '#9CA3AF', + secondaryText: '#FFF7F2', + mutedText: '#F5EFEA', softLine: 'rgba(255,255,255,0.18)', - watermark: 'rgba(255,247,242,0.62)', + watermark: '#F5EFEA', badgeFill: 'rgba(255,255,255,0.12)', badgeBorder: 'rgba(255,255,255,0.24)', }, @@ -149,92 +167,41 @@ export function resolveShareCardTheme(theme: ShareCardTheme | undefined, isDark? return isDark ? 'auroraDark' : 'claudeClassic'; // 'auto' / undefined } -// The card renders in the UI language (Carl: no en/zh mixing). Fully localized -// for en / zh-CN / zh-TW (the maintainer's languages); other UI languages fall -// back to en so a card never mixes languages. Card-specific strings live here -// (self-contained + testable) rather than bloating the shared i18n table. -export type CardLang = 'en' | 'zh-CN' | 'zh-TW'; +export type CardLang = SupportedLanguage; export function cardLang(lang?: string): CardLang { - return lang === 'zh-CN' || lang === 'zh-TW' ? lang : 'en'; + return artifactLocale(lang); } -interface CardStrings { - subtitle: string; - totalTokens: string; - spent: string; - sessionsUnit: string; - estCost: string; - cacheHit: string; - topModel: string; - sessions: string; - messages: string; - workflows: string; - peakCtx: string; - tokenMix: string; - input: string; - output: string; - cacheWrite: string; - cacheRead: string; - daily: string; - hourly: string; - peak: string; - madeWith: string; +const CARD_STRINGS: Record = + SHARE_CARD_ARTIFACT_TRANSLATIONS; +export const BADGE_COPY = SHARE_BADGE_TRANSLATIONS; + +function localizedRangeLabel( + range: ShareRange, + supplied: string | undefined, + locale: CardLang, + copy: ShareCardArtifactTranslations, +): string { + if (range.startsWith('month:')) { + const match = /^(\d{4})-(\d{2})$/.exec(range.slice('month:'.length)); + if (match) { + return new Intl.DateTimeFormat(locale, { + month: 'long', + year: 'numeric', + timeZone: 'UTC', + }).format(new Date(Date.UTC(Number(match[1]), Number(match[2]) - 1, 1))); + } + } + switch (range) { + case 'today': return copy.today; + case 'week': return copy.last7; + case 'last30': return copy.last30; + case 'month': return copy.thisMonth; + case 'year': return copy.last12Months; + default: return supplied || String(range); + } } -const CARD_STRINGS: Record = { - en: { - subtitle: 'AI coding usage snapshot', totalTokens: 'total tokens', spent: 'estimated spend', sessionsUnit: 'sessions', - estCost: 'est. cost', cacheHit: 'cache hit', topModel: 'top model', sessions: 'sessions', messages: 'messages', - workflows: 'workflows', peakCtx: 'peak ctx', tokenMix: 'Token mix', input: 'Input', output: 'Output', - cacheWrite: 'Cache write', cacheRead: 'Cache read', daily: 'Daily pulse', hourly: 'Hourly pulse', peak: 'peak', - madeWith: 'Made with Claude Code Usage', - }, - 'zh-CN': { - subtitle: 'AI 编程用量快照', totalTokens: '总 token', spent: '预计花费', sessionsUnit: '会话', - estCost: '预计成本', cacheHit: '缓存命中', topModel: '主力模型', sessions: '会话数', messages: '消息数', - workflows: '工作流', peakCtx: '峰值上下文', tokenMix: 'Token 组成', input: '输入', output: '输出', - cacheWrite: '缓存写入', cacheRead: '缓存读取', daily: '每日节奏', hourly: '每小时节奏', peak: '峰值', - madeWith: '由 Claude Code Usage 制作', - }, - 'zh-TW': { - subtitle: 'AI 程式設計用量快照', totalTokens: '總 token', spent: '預計花費', sessionsUnit: '會話', - estCost: '預計成本', cacheHit: '快取命中', topModel: '主力模型', sessions: '會話數', messages: '訊息數', - workflows: '工作流', peakCtx: '峰值上下文', tokenMix: 'Token 組成', input: '輸入', output: '輸出', - cacheWrite: '快取寫入', cacheRead: '快取讀取', daily: '每日節奏', hourly: '每小時節奏', peak: '峰值', - madeWith: '由 Claude Code Usage 製作', - }, -}; - -// On-brand badge copy per language (title + one-line personality), keyed by -// selectShareBadge ids. Title AND line use the same language — no mixing. -export const BADGE_COPY: Record> = { - 'context-marathoner': { - en: { title: 'Context Marathon', line: "Half-marathon of context — the model's still catching its breath." }, - 'zh-CN': { title: 'Context 马拉松', line: '上下文跑了个半马,模型还在喘。' }, - 'zh-TW': { title: 'Context 馬拉松', line: '上下文跑了個半馬,模型還在喘。' }, - }, - 'cache-saver': { - en: { title: 'Cache Alchemist', line: 'High cache hits — barely a token wasted.' }, - 'zh-CN': { title: '缓存日子人', line: '缓存命中高,token 没白烧。' }, - 'zh-TW': { title: '快取日子人', line: '快取命中高,token 沒白燒。' }, - }, - 'token-sprinter': { - en: { title: 'Token Sprinter', line: 'Full throttle. 🔥' }, - 'zh-CN': { title: '无限火力', line: '开炮!!!' }, - 'zh-TW': { title: '無限火力', line: '開炮!!!' }, - }, - 'workflow-pilot': { - en: { title: 'Workflow Pilot', line: "You're not coding — you're running a crew." }, - 'zh-CN': { title: 'Agent 包工头', line: '你不是在写代码,你是在使唤一支小队。' }, - 'zh-TW': { title: 'Agent 包工頭', line: '你不是在寫程式,你是在使喚一支小隊。' }, - }, - 'steady-builder': { - en: { title: 'Steady Builder', line: 'No rush, no burnout — steady progress.' }, - 'zh-CN': { title: '节奏大师', line: '不卷不燥,代码稳步推进。' }, - 'zh-TW': { title: '節奏大師', line: '不捲不燥,程式碼穩步推進。' }, - }, -}; - const REPO = 'github.com/ClaudeCodeUsage'; // ---- Renderer --------------------------------------------------------------- @@ -263,7 +230,7 @@ export function renderShareCardSvg(data: ShareCardData, opts: ShareCardSvgOption const p: string[] = []; p.push( - `` + `` ); // Aurora background: vertical gradient + two soft radial blobs (deterministic). @@ -280,7 +247,8 @@ export function renderShareCardSvg(data: ShareCardData, opts: ShareCardSvgOption // --- Brand block (top-left) --- p.push(`Claude Code Usage`); p.push(`${esc(S.subtitle)}`); - const range = (data.rangeLabel || rangeLabel(data.range)) + (data.projectName ? ' · ' + truncate(data.projectName, 28) : ''); + const range = localizedRangeLabel(data.range, data.rangeLabel, L, S) + + (data.projectName ? ' · ' + truncate(data.projectName, 28) : ''); p.push(`${esc(range)}`); // --- Corner (top-right): avatar + badge card + optional name --- @@ -291,7 +259,7 @@ export function renderShareCardSvg(data: ShareCardData, opts: ShareCardSvgOption const ax = W - M - s; const ay = 46; p.push(``); - p.push(``); + p.push(``); p.push(``); avatarBottom = ay + s; } @@ -338,7 +306,7 @@ export function renderShareCardSvg(data: ShareCardData, opts: ShareCardSvgOption let heroValue = ''; let heroUnit = ''; if (data.totalTokens != null) { - heroValue = opts.fullNumbers ? data.totalTokens.toLocaleString('en-US') : compact(data.totalTokens, L, true); + heroValue = opts.fullNumbers ? data.totalTokens.toLocaleString(L) : compact(data.totalTokens, L, true); heroUnit = S.totalTokens; } else if (data.estimatedCost != null) { heroValue = formatMoney(data.estimatedCost); @@ -456,10 +424,10 @@ export function renderShareCardSvg(data: ShareCardData, opts: ShareCardSvgOption p.push(``); }); if (data.rhythmStart) { - p.push(`${esc(shortDay(data.rhythmStart))}`); + p.push(`${esc(shortDay(data.rhythmStart, L))}`); } if (data.rhythmEnd && nn > 1) { - p.push(`${esc(shortDay(data.rhythmEnd))}`); + p.push(`${esc(shortDay(data.rhythmEnd, L))}`); } }, }); diff --git a/src/sharingCommandIntent.ts b/src/sharingCommandIntent.ts new file mode 100644 index 00000000..16e9c8b3 --- /dev/null +++ b/src/sharingCommandIntent.ts @@ -0,0 +1,42 @@ +export type SharingTemplate = 'combinedHeatmap' | 'claudeShareCard' | 'claudeHeatmap'; + +export interface SharingCommandIntent { + revision: number; + template: SharingTemplate; +} + +/** + * Provider-lifetime command history with a separate, acknowledgement-gated + * live intent. The revision is never browser state: it only prevents a delayed + * acknowledgement from consuming a newer explicit host command. + */ +export class SharingCommandIntentLedger { + private revision = 0; + private liveIntent: SharingCommandIntent | undefined; + + issue(template: SharingTemplate): SharingCommandIntent { + const intent = { revision: ++this.revision, template }; + this.liveIntent = intent; + return { ...intent }; + } + + pending(): SharingCommandIntent | undefined { + return this.liveIntent ? { ...this.liveIntent } : undefined; + } + + acknowledge(revision: number): boolean { + if (!Number.isSafeInteger(revision) || this.liveIntent?.revision !== revision) { + return false; + } + this.liveIntent = undefined; + return true; + } + + clearPending(): void { + this.liveIntent = undefined; + } + + latestRevision(): number { + return this.revision; + } +} diff --git a/src/svgEscape.ts b/src/svgEscape.ts new file mode 100644 index 00000000..2f6f08e5 --- /dev/null +++ b/src/svgEscape.ts @@ -0,0 +1,58 @@ +export function escapeSvgText(value: unknown): string { + return String(value) + .replace(/&/g, '&') + .replace(//g, '>'); +} + +export function escapeSvgAttribute(value: unknown): string { + return escapeSvgText(value) + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +function approximateGlyphWidthEm(character: string): number { + if (/\s/u.test(character)) return 0.35; + const codePoint = character.codePointAt(0) ?? 0; + if ( + codePoint >= 0x1100 && + (codePoint <= 0x11ff || + (codePoint >= 0x2e80 && codePoint <= 0x9fff) || + (codePoint >= 0xac00 && codePoint <= 0xd7af) || + (codePoint >= 0xf900 && codePoint <= 0xfaff) || + codePoint >= 0x1f000) + ) { + return 1; + } + if (/[WM@#%&]/u.test(character)) return 0.98; + if (/[A-Z]/u.test(character)) return 0.74; + if (/[a-z0-9]/u.test(character)) return 0.62; + return 0.5; +} + +export function fitSvgText( + value: string, + maxWidthPx: number, + fontSizePx: number, +): string { + const characters = Array.from(value); + if (approximateSvgTextWidth(value, fontSizePx) <= maxWidthPx) return value; + + const available = Math.max(0, maxWidthPx - fontSizePx); + const kept: string[] = []; + let used = 0; + for (const character of characters) { + const advance = approximateGlyphWidthEm(character) * fontSizePx; + if (used + advance > available) break; + kept.push(character); + used += advance; + } + return `${kept.join('').trimEnd()}…`; +} + +export function approximateSvgTextWidth(value: string, fontSizePx: number): number { + return Array.from(value).reduce( + (total, character) => total + approximateGlyphWidthEm(character) * fontSizePx, + 0, + ); +} diff --git a/src/test/claudeIncrementalIndex.test.ts b/src/test/claudeIncrementalIndex.test.ts index d98b974c..6fd926f1 100644 --- a/src/test/claudeIncrementalIndex.test.ts +++ b/src/test/claudeIncrementalIndex.test.ts @@ -64,6 +64,37 @@ function usageLine( return JSON.stringify(value); } +test('usage records retain numeric evidence but not assistant response bodies', async () => { + const root = await mkdtemp(path.join(os.tmpdir(), 'ccu-claude-compact-usage-')); + roots.push(root); + const project = path.join(root, 'projects', '-fixture-compact'); + await mkdir(project, { recursive: true }); + const source = JSON.parse(usageLine('large-response', 12, 4)) as Record; + source.message.content = [{ type: 'text', text: 'synthetic-only-'.repeat(17_000) }]; + source.message.usage.cache_creation = { + ephemeral_1h_input_tokens: 2, + ephemeral_5m_input_tokens: 0, + }; + source.costUSD = 1.25; + await writeFile(path.join(project, 'session-compact.jsonl'), `${JSON.stringify(source)}\n`, 'utf8'); + + const incremental = await updateClaudeUsageIndex(createClaudeUsageIndex(), root, { + analyzeContent: true, + }); + const full = await ClaudeDataLoader.loadUsageRecords(root, { analyzeContent: true }); + for (const record of [incremental.records[0], full.records[0]]) { + assert.equal(record.message.usage.input_tokens, 12); + assert.equal(record.message.usage.output_tokens, 4); + assert.deepEqual(record.message.usage, source.message.usage); + assert.equal(record.message.model, 'claude-sonnet-4-5'); + assert.equal(record.message.id, 'message-large-response'); + assert.equal(record.requestId, 'request-large-response'); + assert.equal(record.costUSD, 1.25); + assert.equal('content' in record.message, false); + assert.equal(JSON.stringify(record).includes('synthetic-only-'), false); + } +}); + function promptLine(text: string, timestamp = '2026-08-21T08:00:00.000Z'): string { return JSON.stringify({ type: 'user', @@ -603,19 +634,17 @@ test('a file that is appended to while losing an event to the window stays incre // The same file is appended to in the refresh where its oldest event leaves // the window. Its stored aggregate still counts the expired event, so the // tail cannot simply be added to it and the file is re-read in full — but - // the body only grew, and re-reading one file is no reason to re-read 32. + // the body only grew, and re-reading one file is no reason to re-read all eight. now = Date.parse('2026-09-10T14:00:00.000Z'); await appendFile(files[2], `${analysisTextLine( 'window-append-tail', 'tail', '2026-09-10T13:55:00.000Z', )} `, 'utf8'); - const warm = await updateClaudeUsageIndex(cold.index, root, { windowDays: 1 }); const full = await ClaudeDataLoader.loadUsageRecords(root, { analyzeContent: true, windowDays: 1, }); - assert.ok( warm.diagnostics.bodyReads < 8, `an appended file losing an event re-read the whole corpus: ${warm.diagnostics.bodyReads} bodies, ` + @@ -666,6 +695,42 @@ test('mixed window rebuild and append give a new UUID to the earliest file', asy } }); +test('same-file expiry plus an earlier UUID preemption falls back to ordered analysis', async () => { + const previousNow = Date.now; + let now = Date.parse('2026-09-10T12:00:00.000Z'); + Date.now = () => now; + const root = await mkdtemp(path.join(os.tmpdir(), 'ccu-claude-same-file-uuid-')); + roots.push(root); + const project = path.join(root, 'projects', '-fixture-same-file-uuid'); + await mkdir(project, { recursive: true }); + try { + const earlier = path.join(project, 'earlier.jsonl'); + const later = path.join(project, 'later.jsonl'); + await writeFile(earlier, [ + analysisTextLine('earlier-seed', 'early seed', '2026-09-10T00:00:00.000Z'), + analysisTextLine('earlier-expiring', 'expires', '2026-09-09T13:00:00.000Z'), + ].join('\n') + '\n', 'utf8'); + await writeFile(later, `${analysisTextLine( + 'shared-window-uuid', 'later owner', '2026-09-10T00:01:00.000Z', + )}\n`, 'utf8'); + const cold = await updateClaudeUsageIndex(createClaudeUsageIndex(), root, { windowDays: 1 }); + + now = Date.parse('2026-09-10T14:00:00.000Z'); + await appendFile(earlier, `${analysisTextLine( + 'shared-window-uuid', 'earlier owner', '2026-09-10T13:55:00.000Z', + )}\n`, 'utf8'); + const warm = await updateClaudeUsageIndex(cold.index, root, { windowDays: 1 }); + const full = await ClaudeDataLoader.loadUsageRecords(root, { + analyzeContent: true, + windowDays: 1, + }); + assert.equal(warm.diagnostics.bodyReads, 3); + assert.deepEqual(warm.contentAnalysis, full.contentAnalysis); + } finally { + Date.now = previousNow; + } +}); + test('a new file carrying an already-owned UUID falls back to the full rebuild', async () => { const previousNow = Date.now; Date.now = () => Date.parse('2026-09-10T12:00:00.000Z'); @@ -742,6 +807,62 @@ test('concurrent appends to several files stay incremental instead of forcing a } }); +test('a 645-session corpus reads only seven live tails and one new session', async (t) => { + const previousNow = Date.now; + Date.now = () => Date.parse('2026-09-10T12:00:00.000Z'); + const root = await mkdtemp(path.join(os.tmpdir(), 'ccu-claude-many-sessions-')); + roots.push(root); + const project = path.join(root, 'projects', '-fixture-many-sessions'); + await mkdir(project, { recursive: true }); + const files: string[] = []; + try { + const start = Date.parse('2026-09-09T00:00:00.000Z'); + for (let index = 0; index < 645; index += 1) { + const file = path.join(project, `session-${String(index).padStart(3, '0')}.jsonl`); + files.push(file); + await writeFile(file, `${usageLine(`many-seed-${index}`, 10, 1, { + timestamp: new Date(start + index * 1_000).toISOString(), + })}\n`, 'utf8'); + } + const cold = await updateClaudeUsageIndex(createClaudeUsageIndex(), root); + assert.equal(cold.diagnostics.bodyReads, 645); + + let expectedBytesRead = 0; + for (const [offset, index] of [7, 91, 183, 275, 367, 459, 644].entries()) { + const tail = `${usageLine(`many-tail-${index}`, 20, 2, { + timestamp: new Date(start + (1_800 + offset) * 1_000).toISOString(), + })}\n`; + expectedBytesRead += Buffer.byteLength(tail); + await appendFile(files[index], tail, 'utf8'); + } + const newFile = `${usageLine('many-new', 30, 3, { + timestamp: '2026-09-09T00:31:00.000Z', + })}\n`; + // A new file is read once to establish canonical source order and once to + // parse its body. Both reads remain bounded to the new file, never history. + expectedBytesRead += 2 * Buffer.byteLength(newFile); + await writeFile(path.join(project, 'session-new.jsonl'), newFile, 'utf8'); + + const warm = await updateClaudeUsageIndex(cold.index, root); + const full = await ClaudeDataLoader.loadUsageRecords(root, { analyzeContent: true }); + assert.deepEqual(warm.contentAnalysis, full.contentAnalysis); + await assertMatchesFull(root, warm.records); + // Some filesystems reuse a removed file identity, legitimately turning a + // new session into a move/rebuild. Correctness still holds; only the + // bounded-read assertion becomes inapplicable. + if (warm.diagnostics.changed.move > 0) { + t.skip('the filesystem reused a file identity, so this refresh is a move'); + return; + } + assert.equal(warm.diagnostics.filesDiscovered, 646); + assert.equal(warm.diagnostics.bodyReads, 8); + assert.equal(warm.diagnostics.linesParsed, 8); + assert.equal(warm.diagnostics.bytesRead, expectedBytesRead); + } finally { + Date.now = previousNow; + } +}); + test('default content analysis does not revisit historical file contributions after a small append', async () => { const previousNow = Date.now; Date.now = () => Date.parse('2026-09-10T12:00:00.000Z'); diff --git a/src/test/codexStatus.test.ts b/src/test/codexStatus.test.ts index ecf63d06..de419a9e 100644 --- a/src/test/codexStatus.test.ts +++ b/src/test/codexStatus.test.ts @@ -59,7 +59,7 @@ function limit( }; } -test('Codex status defaults to fresh and shows remaining weekly quota', () => { +test('the explicit uncached Codex metric preserves its value and shows remaining weekly quota', () => { const formatted = formatCodexStatus(scope, 'fresh', limit(NOW + 60_000, [ { label: 'primary', diff --git a/src/test/combinedHeatmap.test.ts b/src/test/combinedHeatmap.test.ts index 66f74303..fed7699d 100644 --- a/src/test/combinedHeatmap.test.ts +++ b/src/test/combinedHeatmap.test.ts @@ -272,3 +272,24 @@ test('title, filename, and Markdown are bounded and deterministic', () => { '![Claude + Codex activity](claude-codex-activity-90d-2026-07-20.svg)', ); }); + +test('combined exported SVG copy is localized in all eight UI locales', () => { + const expected = { + en: ['Less', 'More', 'Made with Claude Code Usage'], + 'de-DE': ['Weniger', 'Mehr', 'Erstellt mit Claude Code Usage'], + 'zh-TW': ['較少', '較多', '由 Claude Code Usage 製作'], + 'zh-CN': ['较少', '较多', '由 Claude Code Usage 制作'], + ja: ['少ない', '多い', 'Claude Code Usage で作成'], + ko: ['적게', '많이', 'Claude Code Usage로 제작'], + 'pt-BR': ['Menos', 'Mais', 'Criado com Claude Code Usage'], + id: ['Lebih sedikit', 'Lebih banyak', 'Dibuat dengan Claude Code Usage'], + }; + for (const [locale, markers] of Object.entries(expected)) { + const svg = renderCombinedHeatmapSvg({}, { + range: '30d', + endDateISO: '2026-07-22', + locale, + }); + for (const marker of markers) assert.match(svg, new RegExp(marker)); + } +}); diff --git a/src/test/combinedHeatmapI18n.test.ts b/src/test/combinedHeatmapI18n.test.ts index 0fe3b757..6bf01a56 100644 --- a/src/test/combinedHeatmapI18n.test.ts +++ b/src/test/combinedHeatmapI18n.test.ts @@ -1,60 +1,86 @@ -import * as fs from 'fs'; -import * as path from 'path'; +import * as fs from 'node:fs'; +import * as path from 'node:path'; import { test } from 'node:test'; import * as assert from 'node:assert/strict'; +import { + I18n, + SHARING_WORKSPACE_TRANSLATIONS, + SharingWorkspaceTranslations, +} from '../i18n'; import { SupportedLanguage } from '../types'; -// webview.ts imports vscode, so this repository-policy test inspects the source -// instead of loading the module under plain node:test. This mirrors the settings -// catalogue coverage guard in quotaI18n.test.ts. -const SOURCE = fs.readFileSync(path.join(__dirname, '..', '..', 'src', 'webview.ts'), 'utf8'); const LOCALES: SupportedLanguage[] = ['en', 'de-DE', 'zh-TW', 'zh-CN', 'ja', 'ko', 'pt-BR', 'id']; const NON_ENGLISH = LOCALES.filter((locale) => locale !== 'en'); +const WEBVIEW_SOURCE = fs.readFileSync( + path.join(__dirname, '..', '..', 'src', 'webview.ts'), + 'utf8', +); -function copyFields(): string[] { - const start = SOURCE.indexOf('interface CombinedHeatmapUiCopy {'); - const end = SOURCE.indexOf('\n}', start); +function combinedCopyFields(): string[] { + const start = WEBVIEW_SOURCE.indexOf('interface CombinedHeatmapUiCopy {'); + const end = WEBVIEW_SOURCE.indexOf('\n}', start); assert.ok(start >= 0 && end > start, 'CombinedHeatmapUiCopy interface must exist'); - const fields = [...SOURCE.slice(start, end).matchAll(/^\s+(\w+): string;$/gm)] + const fields = [...WEBVIEW_SOURCE.slice(start, end).matchAll(/^\s+(\w+): string;$/gm)] .map((match) => match[1]); - assert.equal(fields.length, 38, 'the Share studio copy contract changed; update its locale coverage'); + assert.equal(fields.length, 38, 'the combined presentation copy contract changed; update locale coverage'); return fields; } -function copyFunction(): string { - const start = SOURCE.indexOf('function combinedHeatmapUiCopy('); - const end = SOURCE.indexOf('\nexport class UsageWebviewProvider', start); +function combinedCopyFunction(): string { + const start = WEBVIEW_SOURCE.indexOf('function combinedHeatmapUiCopy('); + const end = WEBVIEW_SOURCE.indexOf('\nexport class UsageWebviewProvider', start); assert.ok(start >= 0 && end > start, 'combinedHeatmapUiCopy must exist'); - return SOURCE.slice(start, end); + return WEBVIEW_SOURCE.slice(start, end); } -function localeBranch(locale: SupportedLanguage): string | undefined { - const source = copyFunction(); - const marker = `if (locale === '${locale}')`; - const markerStart = source.indexOf(marker); - if (markerStart < 0) { - return undefined; - } +function combinedLocaleBranch(locale: SupportedLanguage): string | undefined { + const source = combinedCopyFunction(); + const markerStart = source.indexOf(`if (locale === '${locale}')`); + if (markerStart < 0) return undefined; const objectStart = source.indexOf('return {', markerStart); const objectEnd = source.indexOf('\n };', objectStart); assert.ok(objectStart >= 0 && objectEnd > objectStart, `${locale}: locale object must be parseable`); return source.slice(objectStart, objectEnd); } -test('every non-English locale owns complete active Share studio copy', () => { - const translatedFields = copyFields().filter((field) => !field.startsWith('intensity')); +test('every non-English locale retains complete combined-presentation copy', () => { + const translatedFields = combinedCopyFields().filter((field) => !field.startsWith('intensity')); const missing: string[] = []; for (const locale of NON_ENGLISH) { - const branch = localeBranch(locale); + const branch = combinedLocaleBranch(locale); if (!branch) { missing.push(`${locale}:branch`); continue; } for (const field of translatedFields) { - if (!new RegExp(`\\b${field}:`).test(branch)) { - missing.push(`${locale}:${field}`); - } + if (!new RegExp(`\\b${field}:`).test(branch)) missing.push(`${locale}:${field}`); } } - assert.deepEqual(missing, [], `missing Share studio translations: ${missing.join(', ')}`); + assert.deepEqual(missing, [], `missing combined-presentation translations: ${missing.join(', ')}`); +}); + +test('every locale owns complete active sharing-workspace copy', () => { + const englishFields = Object.keys( + SHARING_WORKSPACE_TRANSLATIONS.en, + ) as (keyof SharingWorkspaceTranslations)[]; + assert.ok(englishFields.length >= 40, 'the unified sharing workspace copy contract is unexpectedly small'); + + for (const locale of LOCALES) { + const copy = SHARING_WORKSPACE_TRANSLATIONS[locale]; + assert.deepEqual( + Object.keys(copy).sort(), + [...englishFields].sort(), + `${locale}: sharing copy fields`, + ); + for (const field of englishFields) { + assert.ok(copy[field].trim().length > 0, `${locale}:${field}`); + } + } +}); + +test('I18n exposes the selected locale sharing-workspace copy without fallback', () => { + for (const locale of LOCALES) { + I18n.setLanguage(locale); + assert.strictEqual(I18n.sharingWorkspace, SHARING_WORKSPACE_TRANSLATIONS[locale]); + } }); diff --git a/src/test/dashboardOnlySettings.test.ts b/src/test/dashboardOnlySettings.test.ts index 9b3d7f70..3da9ab96 100644 --- a/src/test/dashboardOnlySettings.test.ts +++ b/src/test/dashboardOnlySettings.test.ts @@ -42,3 +42,18 @@ test('currency display settings reformat materialized data without rebuilding pr assert.match(branch, /return;/); assert.doesNotMatch(branch, /restart|createCodexProvider|startWatcher|scan/i); }); + +test('pricing changes also invalidate sharing projections before a dashboard refresh', () => { + const source = fs.readFileSync( + path.join(__dirname, '..', '..', 'src', 'extension.ts'), + 'utf8', + ); + assert.match( + source, + /private async refreshPricing\(\)[\s\S]*?this\.invalidateClaudeUsagePricingCache\(\);\s*this\.webviewProvider\.invalidateShareCardPreview\(\);/, + ); + assert.match( + source, + /if \(pricingBackendChanged\) \{\s*this\.invalidateClaudeUsagePricingCache\(\);\s*this\.webviewProvider\.invalidateShareCardPreview\(\);/, + ); +}); diff --git a/src/test/extensionWindowActivity.test.ts b/src/test/extensionWindowActivity.test.ts index dbad387b..6399b591 100644 --- a/src/test/extensionWindowActivity.test.ts +++ b/src/test/extensionWindowActivity.test.ts @@ -2328,7 +2328,7 @@ test('Claude refresh diagnostics drain the unnamed quota-watcher event count', a }); test('real Claude filesystem events flow through manifest, index, and dashboard refresh', { - timeout: 10_000, + timeout: 15_000, }, async (t) => { const extension = bareExtension(); const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ccu-claude-watch-e2e-')); @@ -2444,20 +2444,30 @@ test('real Claude filesystem events flow through manifest, index, and dashboard } const activeWatcher = extension.fileWatcher as fs.FSWatcher; + // Recursive fs.watch can drop the first event for a just-created nested + // file while macOS is attaching its directory handle. Rewriting that + // same test-only file exercises the real watcher path until it observes + // the change; no synthetic callback or polling-based refresh is used. + const nestedBody = `${usageLine('subagent', 20)}\n`; + let rewrite: ReturnType | undefined; const outcomePromise = new Promise<'refreshed' | 'watch-error'>((resolve, reject) => { const timeout = setTimeout(() => { + if (rewrite) clearInterval(rewrite); reject(new Error('timed out waiting for nested Claude refresh')); - }, 5_000); + }, 8_000); nestedRefresh.then(() => { clearTimeout(timeout); + if (rewrite) clearInterval(rewrite); resolve('refreshed'); }); activeWatcher.once('error', () => { clearTimeout(timeout); + if (rewrite) clearInterval(rewrite); resolve('watch-error'); }); + fs.writeFileSync(nestedFile, nestedBody, 'utf8'); + rewrite = setInterval(() => fs.writeFileSync(nestedFile, nestedBody, 'utf8'), 250); }); - fs.writeFileSync(nestedFile, `${usageLine('subagent', 20)}\n`, 'utf8'); const outcome = await outcomePromise; if (outcome === 'watch-error') { t.skip('the test host cannot allocate a recursive fs.watch handle'); diff --git a/src/test/githubHeatmapPublish.test.ts b/src/test/githubHeatmapPublish.test.ts index dc50b2d5..cd0b1a3a 100644 --- a/src/test/githubHeatmapPublish.test.ts +++ b/src/test/githubHeatmapPublish.test.ts @@ -1,10 +1,13 @@ import { test } from 'node:test'; import * as assert from 'node:assert/strict'; import { + GITHUB_HEATMAP_DESTINATION_KEY, GITHUB_PUBLIC_REPO_SCOPE, GitHubApiRequest, + completeSuccessfulGitHubPublish, createGitHubPublishPlan, githubPublishConfirmationDetail, + parseGitHubHeatmapDestination, probePublicGitHubPublishTarget, publishPublicGitHubFile, } from '../githubHeatmapPublish'; @@ -44,22 +47,34 @@ test('mocked public create probe and write use the verified branch and no sha', ); assert.equal(preview.action, 'create'); await publishPublicGitHubFile(preview, 'U0ZH', request); - assert.deepEqual(calls[2], { - method: 'PUT', - path: '/repos/owner/repo/contents/assets/activity.svg', - body: { - message: 'Update Claude Code usage heatmap', - content: 'U0ZH', - branch: 'main', + assert.deepEqual(calls, [ + { + method: 'GET', + path: '/repos/owner/repo', + body: undefined, }, - }); + { + method: 'GET', + path: '/repos/owner/repo/contents/assets/activity.svg?ref=main', + body: undefined, + }, + { + method: 'PUT', + path: '/repos/owner/repo/contents/assets/activity.svg', + body: { + message: 'Update Claude Code usage heatmap', + content: 'U0ZH', + branch: 'main', + }, + }, + ]); }); -test('mocked update carries only the exact probed sha', async () => { +test('mocked update carries only the exact probed endpoint, branch, and sha', async () => { const sha = 'a'.repeat(40); - const calls: Array<{ method: string; body?: any }> = []; - const request: GitHubApiRequest = async (method, _path, body) => { - calls.push({ method, body }); + const calls: Array<{ method: string; path: string; body?: any }> = []; + const request: GitHubApiRequest = async (method, path, body) => { + calls.push({ method, path, body }); if (calls.length === 1) return { status: 200, body: JSON.stringify({ private: false, default_branch: 'release/v2' }) }; if (calls.length === 2) return { status: 200, body: JSON.stringify({ sha }) }; return { status: 200, body: '{}' }; @@ -70,8 +85,24 @@ test('mocked update carries only the exact probed sha', async () => { ); assert.equal(preview.action, 'update'); await publishPublicGitHubFile(preview, 'U0ZH', request); - assert.equal(calls[2].body.sha, sha); - assert.equal(calls[2].body.branch, 'release/v2'); + assert.deepEqual(calls, [ + { method: 'GET', path: '/repos/owner/repo', body: undefined }, + { + method: 'GET', + path: '/repos/owner/repo/contents/activity.svg?ref=release%2Fv2', + body: undefined, + }, + { + method: 'PUT', + path: '/repos/owner/repo/contents/activity.svg', + body: { + message: 'Update Claude Code usage heatmap', + content: 'U0ZH', + branch: 'release/v2', + sha, + }, + }, + ]); }); test('private or unverifiable targets fail closed before any mock write', async () => { @@ -111,3 +142,62 @@ test('mocked request failures expose status only, never response-body canaries', (error: Error) => /status 503/.test(error.message) && !/PRIVACY_CANARY/.test(error.message), ); }); + +test('successful publication is reported before an atomic destination-save warning', async () => { + const events: string[] = []; + const result = await completeSuccessfulGitHubPublish( + { + ...createGitHubPublishPlan('owner/repo', 'images/activity.svg'), + visibility: 'public', + branch: 'main', + action: 'create', + browserUrl: 'https://github.com/owner/repo/blob/main/images/activity.svg', + }, + { + persistDestination: async (key, destination) => { + events.push(`persist:${key}:${JSON.stringify(destination)}`); + throw new Error('synthetic persistence failure'); + }, + showSuccess: async () => { + events.push('success'); + return true; + }, + showPersistenceWarning: async () => { + events.push('warning'); + }, + }, + ); + + assert.deepEqual(result, { openBrowser: true, preferenceSaved: false }); + assert.deepEqual(events, [ + `persist:${GITHUB_HEATMAP_DESTINATION_KEY}:${JSON.stringify({ + schemaVersion: 1, + repository: 'owner/repo', + filePath: 'images/activity.svg', + })}`, + 'success', + 'warning', + ]); +}); + +test('stored GitHub destination accepts only the exact atomic schema', () => { + assert.deepEqual(parseGitHubHeatmapDestination({ + schemaVersion: 1, + repository: 'owner/repo', + filePath: 'images/activity.svg', + }), { + schemaVersion: 1, + repository: 'owner/repo', + filePath: 'images/activity.svg', + }); + assert.equal(parseGitHubHeatmapDestination({ + schemaVersion: 1, + repository: 'owner/repo', + filePath: 'images/activity.svg', + token: 'must-not-survive', + }), undefined); + assert.equal(parseGitHubHeatmapDestination({ + schemaVersion: 1, + repository: 'owner/repo', + }), undefined); +}); diff --git a/src/test/heatmapSvg.test.ts b/src/test/heatmapSvg.test.ts index 504f7ed6..02b87a5d 100644 --- a/src/test/heatmapSvg.test.ts +++ b/src/test/heatmapSvg.test.ts @@ -4,6 +4,7 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; import { buildContributionGrid, renderHeatmapSvg, CLAUDE_ORANGE_SCALE } from '../heatmapSvg'; import { DayUsage } from '../heatmap'; +import { renderCombinedHeatmapSvg } from '../combinedHeatmapSvg'; const day = (tokens: number): DayUsage => ({ tokens, cost: tokens / 1000, sessions: 1 }); @@ -14,18 +15,48 @@ function fillForDate(svg: string, dateISO: string): string | undefined { return match?.[1]; } +function relativeLuminance(hex: string): number { + const channels = [1, 3, 5].map((start) => Number.parseInt(hex.slice(start, start + 2), 16) / 255); + const linear = channels.map((value) => + value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4, + ); + return 0.2126 * linear[0] + 0.7152 * linear[1] + 0.0722 * linear[2]; +} + +function contrastRatio(left: string, right: string): number { + const l1 = relativeLuminance(left); + const l2 = relativeLuminance(right); + return (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05); +} + +function assertNormalSvgLabelContrast(svg: string): void { + const background = /]*\bfill="(#[0-9a-f]{6})"/i.exec(svg)?.[1]; + assert.ok(background, 'SVG must expose a solid, auditable background'); + const labels = [...svg.matchAll(/]*)>/g)]; + assert.ok(labels.length > 0, 'SVG must contain labels'); + for (const [, attributes] of labels) { + const fill = /\bfill="(#[0-9a-f]{6})"/i.exec(attributes)?.[1]; + const fontSize = Number(/\bfont-size="([\d.]+)"/.exec(attributes)?.[1]); + assert.ok(fill, `normal SVG label at ${fontSize}px must use a solid fill`); + assert.ok( + contrastRatio(fill, background) >= 4.5, + `${fill} at ${fontSize}px must retain 4.5:1 contrast against ${background}`, + ); + } +} + test('the in-dashboard heatmap anchors its end date in the configured timezone', () => { const webviewSource = fs.readFileSync( path.join(__dirname, '..', '..', 'src', 'webview.ts'), 'utf8', ); - const start = webviewSource.indexOf("if (this.setting('showHeatmap', false)"); - const end = webviewSource.indexOf('const dailyBreakdown =', start); - assert.ok(start >= 0 && end > start, 'the live provider heatmap panel must remain discoverable'); + const start = webviewSource.indexOf('private renderClaudeHeatmapPresentation'); + const end = webviewSource.indexOf('private buildShareCardSvgFor', start); + assert.ok(start >= 0 && end > start, 'the Claude heatmap presentation must remain discoverable'); const panel = webviewSource.slice(start, end); assert.match( panel, - /renderHeatmapSvg\(daily,\s*\{\s*endDateISO:\s*dayKeyInZone\(new Date\(\),\s*I18n\.getTimezone\(\)\),?\s*\}\)/, + /renderHeatmapSvg\(daily,\s*\{\s*endDateISO:\s*dayKeyInZone\(new Date\(\),\s*I18n\.getTimezone\(\)\),\s*locale:\s*I18n\.getLocale\(\),?\s*\}\)/, ); }); @@ -90,6 +121,20 @@ test('has legend, watermark and the orange ramp; no crash on empty data', () => assert.ok(svg.includes(CLAUDE_ORANGE_SCALE[0])); }); +test('legacy and combined heatmaps give every normal SVG label at least 4.5:1 contrast', () => { + assertNormalSvgLabelContrast(renderHeatmapSvg({}, { endDateISO: '2026-07-01' })); + assertNormalSvgLabelContrast(renderCombinedHeatmapSvg({ + '2026-06-30': { + dateISO: '2026-06-30', + claudeProcessed: 1_000, + codexProcessed: 2_000, + combinedProcessed: 3_000, + }, + }, { + endDateISO: '2026-07-01', + })); +}); + test('short custom ramps keep a valid watermark and matching legend', () => { const svg = renderHeatmapSvg( { '2026-06-20': day(10) }, @@ -173,8 +218,8 @@ test('short-range legends stay inside a min-width share card', () => { }); assert.doesNotMatch(svg, /<(?:text|rect) x="-/); - assert.match(svg, /]*>Less<\/text>/); - assert.match(svg, /]*>More<\/text>/); + assert.match(svg, /]*>Less<\/text>/); + assert.match(svg, /]*>More<\/text>/); }); test('the default linear renderer preserves the original four active buckets', () => { @@ -211,3 +256,52 @@ test('renderer sanitizes caller-supplied SVG colors at the shared boundary', () assert.doesNotMatch(svg, /javascript|onload|url\(/i); }); + +test('renderer escapes quotes before caller text enters an SVG attribute', () => { + const svg = renderHeatmapSvg({}, { + endDateISO: '2026-07-01', + title: 'Safe title', + ariaLabel: `Safe title\" onload=\"alert(1)' data-canary='unsafe`, + }); + + assert.doesNotMatch(svg, /aria-label="Safe title"\s+onload=/i); + assert.match(svg, /aria-label="Safe title" onload="alert\(1\)' data-canary='unsafe"/); + assert.match(svg, /'/); +}); + +test('maximum ASCII and CJK titles are visually contained in the SVG viewBox', () => { + for (const title of ['W'.repeat(80), '综合活动热力图'.repeat(12)]) { + const svg = renderHeatmapSvg({}, { + startDateISO: '2026-06-23', + endDateISO: '2026-07-22', + minWidth: 720, + title, + }); + const width = Number(/viewBox="0 0 ([\d.]+)/.exec(svg)?.[1]); + const renderedTitle = /]*>(.*?)<\/text>/.exec(svg)?.[1] ?? ''; + assert.equal(width, 720); + assert.ok(renderedTitle.endsWith('…'), `expected a contained title: ${renderedTitle}`); + assert.ok(renderedTitle.length < title.length); + } +}); + +test('Claude heatmap labels and tooltips use all eight UI locales', () => { + const expected = { + en: ['Less', 'More', 'No tokens on June'], + 'de-DE': ['Weniger', 'Mehr', 'Keine Token am'], + 'zh-TW': ['較少', '較多', '無 Token 日期'], + 'zh-CN': ['较少', '较多', '无 Token 日期'], + ja: ['少ない', '多い', 'なし: トークン 日付'], + ko: ['적게', '많이', '없음: 토큰 날짜'], + 'pt-BR': ['Menos', 'Mais', 'Sem tokens em'], + id: ['Lebih sedikit', 'Lebih banyak', 'Tidak ada token pada'], + }; + for (const [locale, markers] of Object.entries(expected)) { + const svg = renderHeatmapSvg({}, { + startDateISO: '2026-06-20', + endDateISO: '2026-06-22', + locale, + }); + for (const marker of markers) assert.match(svg, new RegExp(marker)); + } +}); diff --git a/src/test/i18nAutoLanguage.test.ts b/src/test/i18nAutoLanguage.test.ts new file mode 100644 index 00000000..38efb5a1 --- /dev/null +++ b/src/test/i18nAutoLanguage.test.ts @@ -0,0 +1,24 @@ +import { test } from 'node:test'; +import * as assert from 'node:assert/strict'; +import { I18n } from '../i18n'; + +test('auto language follows the editor locale for every supported family', () => { + const previous = I18n.getCurrentLanguage(); + try { + for (const [editorLocale, expected] of [ + ['de-CH', 'de-DE'], + ['pt-BR', 'pt-BR'], + ['zh-HK', 'zh-TW'], + ['zh-CN', 'zh-CN'], + ['ja-JP', 'ja'], + ['ko-KR', 'ko'], + ['id-ID', 'id'], + ['en-US', 'en'], + ] as const) { + I18n.setLanguage('auto', editorLocale); + assert.equal(I18n.getCurrentLanguage(), expected, editorLocale); + } + } finally { + I18n.setLanguage(previous); + } +}); diff --git a/src/test/localDataControls.test.ts b/src/test/localDataControls.test.ts index 3a751a82..69fb0ae1 100644 --- a/src/test/localDataControls.test.ts +++ b/src/test/localDataControls.test.ts @@ -394,6 +394,11 @@ test('source policy keeps controls out of Settings while preserving safe command [], 'UI and sharing client allowlists must remain disjoint', ); + assert.doesNotMatch(webviewSource, /__ccuSharingSessionKeys|ccu\.sharing\.commandRevision/); + assert.doesNotMatch( + LOCAL_DATA_ACTION_TARGETS['reset-sharing-preferences'].join('\n'), + /ccu\.sharing\.commandRevision/, + ); const clearAll = sourceSlice( extensionSource, diff --git a/src/test/localDataExtension.test.ts b/src/test/localDataExtension.test.ts index 0fc6164e..052c565a 100644 --- a/src/test/localDataExtension.test.ts +++ b/src/test/localDataExtension.test.ts @@ -228,6 +228,7 @@ test('pending Webview reset tombstone survives failure and clears only after ver const acknowledgements = [false, true]; const extension = Object.create(ClaudeCodeUsageExtension.prototype) as any; extension.context = { globalState }; + extension.localDataActionWrite = Promise.resolve(); extension.pendingClientResetReplay = Promise.resolve(); extension.webviewProvider = { requestClientLocalDataAction: async (action: string) => { @@ -245,6 +246,63 @@ test('pending Webview reset tombstone survives failure and clears only after ver assert.equal(globalState.state.has('ccu.localData.pendingClientReset.v1'), false); }); +test('an acknowledged reset replay cannot clear a newer tombstone revision', async () => { + const key = 'ccu.localData.pendingClientReset.v1'; + const globalState = memoryGlobalState({ + [key]: 'clear-all-client-state', + }); + let releaseAck!: (ok: boolean) => void; + const ack = new Promise((resolve) => { releaseAck = resolve; }); + const extension = Object.create(ClaudeCodeUsageExtension.prototype) as any; + extension.context = { globalState }; + extension.localDataActionWrite = Promise.resolve(); + extension.pendingClientResetReplay = Promise.resolve(); + extension.webviewProvider = { + requestClientLocalDataAction: async () => ack, + }; + + const replay = extension.replayPendingClientReset(); + await new Promise((resolve) => setImmediate(resolve)); + const newer = { + schemaVersion: 1, + revision: 2, + action: 'reset-sharing-preferences', + }; + await globalState.update(key, newer); + releaseAck(true); + await replay; + + assert.deepEqual(globalState.state.get(key), newer); +}); + +test('reset replay is serialized with newly requested local-data actions', async () => { + const globalState = memoryGlobalState({ + 'ccu.localData.pendingClientReset.v1': 'clear-all-client-state', + }); + let releaseAck!: (ok: boolean) => void; + const ack = new Promise((resolve) => { releaseAck = resolve; }); + const extension = Object.create(ClaudeCodeUsageExtension.prototype) as any; + extension.context = { globalState }; + extension.localDataActionWrite = Promise.resolve(); + extension.pendingClientResetReplay = Promise.resolve(); + extension.webviewProvider = { + requestClientLocalDataAction: async () => ack, + }; + + const replay = extension.replayPendingClientReset(); + await new Promise((resolve) => setImmediate(resolve)); + let newerActionStarted = false; + const newerAction = extension.serializeLocalDataAction(async () => { + newerActionStarted = true; + }); + await new Promise((resolve) => setImmediate(resolve)); + assert.equal(newerActionStarted, false); + + releaseAck(true); + await Promise.all([replay, newerAction]); + assert.equal(newerActionStarted, true); +}); + test('clear-all uses the exact allowlist and preserves unrelated state', async () => { const globalState = memoryGlobalState({ 'ccu.setting.showCost': true, diff --git a/src/test/monthDetailRequest.test.ts b/src/test/monthDetailRequest.test.ts new file mode 100644 index 00000000..0f0d4e5b --- /dev/null +++ b/src/test/monthDetailRequest.test.ts @@ -0,0 +1,63 @@ +import { test } from 'node:test'; +import * as assert from 'node:assert/strict'; +import Module = require('node:module'); + +test('Claude month drill-down accepts the production first-day key and replies to that exact row', async () => { + const originalLoad = (Module as any)._load; + let onMessage: ((message: Record) => Promise) | undefined; + const replies: Array> = []; + const vscode = { + ViewColumn: { One: 1 }, + window: { + createWebviewPanel: () => ({ + webview: { + onDidReceiveMessage: (handler: typeof onMessage) => { onMessage = handler; }, + postMessage: async (message: Record) => { replies.push(message); return true; }, + }, + onDidDispose: () => undefined, + }), + }, + workspace: { workspaceFolders: [] }, + }; + (Module as any)._load = function(request: string, parent: unknown, isMain: boolean) { + if (request === 'vscode') return vscode; + return originalLoad.call(this, request, parent, isMain); + }; + + try { + const { UsageWebviewProvider } = require('../webview') as typeof import('../webview'); + const provider = new UsageWebviewProvider({} as any) as any; + provider.currentProvider = 'claude'; + provider.allRecords = new Proxy([], { + get() { throw new Error('month drill-down must not inspect raw records'); }, + }); + provider.dailyDataForEveryDay = [ + { date: '2026-06-30', data: { marker: 'june' } }, + { date: '2026-07-02', data: { marker: 'second' } }, + { date: '2026-07-01', data: { marker: 'first' } }, + { date: '2026-08-01', data: { marker: 'august' } }, + ]; + provider.updateWebview = () => undefined; + provider.show(); + assert.ok(onMessage); + + await onMessage!({ command: 'getDailyData', month: '2026-07-01', provider: 'claude' }); + assert.deepEqual(replies, [{ + command: 'dailyDataResponse', + provider: 'claude', + month: '2026-07-01', + data: [ + { date: '2026-07-01', data: { marker: 'first' } }, + { date: '2026-07-02', data: { marker: 'second' } }, + ], + }]); + + await onMessage!({ command: 'getDailyData', month: '2026-07-02', provider: 'claude' }); + assert.equal(replies.length, 1, 'only first-of-month keys are accepted'); + provider.currentProvider = 'codex'; + await onMessage!({ command: 'getDailyData', month: '2026-07-01', provider: 'codex' }); + assert.equal(replies.length, 1, 'Codex still requires its YYYY-MM key'); + } finally { + (Module as any)._load = originalLoad; + } +}); diff --git a/src/test/promptDedup.test.ts b/src/test/promptDedup.test.ts index c2231aec..0a59c35b 100644 --- a/src/test/promptDedup.test.ts +++ b/src/test/promptDedup.test.ts @@ -1,6 +1,6 @@ import { test } from 'node:test'; import * as assert from 'node:assert/strict'; -import { isRetryDuplicatePrompt, PROMPT_RETRY_WINDOW_MS } from '../promptDedup'; +import { detachedPromptPrefix, isRetryDuplicatePrompt, PROMPT_RETRY_WINDOW_MS } from '../promptDedup'; const t0 = Date.parse('2026-06-11T16:26:38.000Z'); @@ -40,7 +40,32 @@ test('boundary: exactly at the window edge is still a duplicate', () => { const seen = new Map(); isRetryDuplicatePrompt('x', t0, seen); assert.equal(isRetryDuplicatePrompt('x', t0 + PROMPT_RETRY_WINDOW_MS, seen), true); - assert.equal(isRetryDuplicatePrompt('x', t0 + PROMPT_RETRY_WINDOW_MS + 1, new Map([['x', t0]])), false); + const edge = new Map(); + isRetryDuplicatePrompt('x', t0, edge); + assert.equal(isRetryDuplicatePrompt('x', t0 + PROMPT_RETRY_WINDOW_MS + 1, edge), false); +}); + +test('large prompt dedup never retains the raw prompt as an index key', () => { + const seen = new Map(); + const prompt = 'synthetic-only-'.repeat(17_000); + assert.equal(isRetryDuplicatePrompt(prompt, t0, seen), false); + assert.equal(isRetryDuplicatePrompt(prompt, t0 + 30_000, seen), true); + assert.equal(isRetryDuplicatePrompt(`${prompt}different`, t0 + 31_000, seen), false); + assert.equal(seen.size, 2); + assert.ok([...seen.keys()].every((key) => key.length < 100)); +}); + +test('distinct unpaired UTF-16 surrogates do not become the same digest', () => { + const seen = new Map(); + assert.equal(isRetryDuplicatePrompt('a\ud800b', t0, seen), false); + assert.equal(isRetryDuplicatePrompt('a\udc00b', t0 + 1000, seen), false); +}); + +test('retained excerpts preserve exact UTF-16 prefix semantics', () => { + const source = '甲😀乙'.repeat(100_000); + assert.equal(detachedPromptPrefix(source, 5), source.slice(0, 5)); + assert.equal(detachedPromptPrefix(source, 2), source.slice(0, 2)); + assert.equal(detachedPromptPrefix(source, 2).length, 2); }); test('unusable timestamp is counted, never dropped', () => { diff --git a/src/test/providerSelection.test.ts b/src/test/providerSelection.test.ts index b4ef2e58..ced74686 100644 --- a/src/test/providerSelection.test.ts +++ b/src/test/providerSelection.test.ts @@ -112,7 +112,7 @@ test('Codex dashboard HTML uses only classes already rendered by the Claude dash provider.hourlyDataForRolling30DaysByDay = { '2026-07-20': [{ hour: '12:00', data: usage }], }; - provider.dailyDataForAllTime = [{ date: '2026-07', data: usage }]; + provider.dailyDataForAllTime = [{ date: '2026-07-01', data: usage }]; provider.sessionBreakdown = [session]; provider.projectBreakdown = [project]; provider.claudeProjectUsageMatrix = buildProjectUsageMatrixSnapshot( @@ -484,7 +484,7 @@ test('Codex settings and charts use the shared dashboard renderers', () => { assert.match(webview, /private renderCompositionChart\([\s\S]*provider: SettingProvider/); assert.match( webview, - /snap\.filter\(\(setting\)\s*=>\s*settingAppliesToProvider\(setting, provider\)\)/, + /snap\.filter\(\(setting\)\s*=>\s*setting\.visible\s*!==\s*false\s*&&\s*settingAppliesToProvider\(setting, provider\),?\s*\)/, ); assert.match( settings, @@ -648,8 +648,8 @@ test('provider and Codex view copy is complete in every UI locale', () => { .join('\n'); assert.doesNotMatch(englishVisibleCopy, /\b(?:fresh|new)\b/i); const settingsSource = readFileSync(path.resolve(__dirname, '..', '..', 'src', 'settings.ts'), 'utf8'); - assert.match(settingsSource, /key: 'codex\.statusMetric'[\s\S]*?enumValues: \['fresh', 'processed', 'output'\][\s\S]*?enumLabels: \['Uncached', 'Processed', 'Output'\]/); - assert.match(settingsSource, /help: "Today's uncached usage, processed tokens, or output tokens\."/); + assert.match(settingsSource, /key: 'codex\.statusMetric'[\s\S]*?enumValues: \['processed', 'fresh', 'output'\][\s\S]*?enumLabels: \['Processed', 'Uncached', 'Output'\]/); + assert.match(settingsSource, /help: "Today's processed tokens \(default\), uncached usage, or output tokens\."/); const extensionSource = readFileSync(path.resolve(__dirname, '..', '..', 'src', 'extension.ts'), 'utf8'); assert.match( extensionSource, diff --git a/src/test/repositoryPolicy.test.ts b/src/test/repositoryPolicy.test.ts index 17992ac0..1338d17e 100644 --- a/src/test/repositoryPolicy.test.ts +++ b/src/test/repositoryPolicy.test.ts @@ -1410,14 +1410,16 @@ test('git and VSIX ignores exclude private and development-only material', () => } }); -test('all seven README files credit both development tools', () => { +test('all nine README files credit both development tools', () => { const readmes = [ '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', ]; for (const readme of readmes) { @@ -1427,15 +1429,17 @@ test('all seven README files credit both development tools', () => { } }); -test('all seven README editions explain Codex Beta in their own language', () => { +test('all nine README files explain current Codex support without a Beta label', () => { const expectations: Record = { - 'README.md': [/Codex Beta/, /processed/i, /uncached usage/i, /cached/i, /last-observed/i], - 'README-en.md': [/Codex Beta/, /processed/i, /uncached usage/i, /cached/i, /last-observed/i], - 'README-zh-CN.md': [/Codex Beta/, /已处理/, /未缓存用量/, /缓存/, /最后观测/], - 'README-zh-TW.md': [/Codex Beta/, /已處理/, /未快取用量/, /快取/, /最後觀測/], - 'README-ja.md': [/Codex Beta/, /処理済み/, /非キャッシュ使用量/, /キャッシュ/, /最終観測/], - 'README-ko.md': [/Codex Beta/, /처리된/, /캐시되지 않은 사용량/, /캐시/, /마지막 관측/], - 'README-id.md': [/Codex Beta/, /diproses/i, /penggunaan tanpa cache/i, /cache/i, /terakhir diamati/i], + 'README.md': [/Codex/, /processed/i, /uncached usage/i, /cached/i, /last-observed/i], + 'README-en.md': [/Codex/, /processed/i, /uncached usage/i, /cached/i, /last-observed/i], + 'README-de-DE.md': [/Codex/, /verarbeitet/i, /nicht gecachte Nutzung/i, /gecachte/i, /letzte lokale Beobachtung/i], + 'README-zh-CN.md': [/Codex/, /已处理/, /未缓存用量/, /缓存/, /最后观测/], + 'README-zh-TW.md': [/Codex/, /已處理/, /未快取用量/, /快取/, /最後觀測/], + 'README-ja.md': [/Codex/, /処理済み/, /非キャッシュ使用量/, /キャッシュ/, /最終観測/], + 'README-ko.md': [/Codex/, /처리된/, /캐시되지 않은 사용량/, /캐시/, /마지막 관측/], + 'README-pt-BR.md': [/Codex/, /processado/i, /uso sem cache/i, /cache/i, /última observação local/i], + 'README-id.md': [/Codex/, /diproses/i, /penggunaan tanpa cache/i, /cache/i, /terakhir diamati/i], }; for (const [readme, patterns] of Object.entries(expectations)) { @@ -1443,10 +1447,21 @@ test('all seven README editions explain Codex Beta in their own language', () => for (const pattern of patterns) { assert.match(body, pattern, `${readme} is missing ${pattern}`); } + assert.doesNotMatch(body.slice(0, 2_000), /Codex Beta/, `${readme} still presents Codex as Beta`); assert.doesNotMatch(body, /showOpusWeekly/, `${readme} still documents the retired setting key`); } }); +test('all provider tabs and settings present Codex without a Beta badge', () => { + const i18n = repoFile('src/i18n.ts'); + const settings = repoFile('src/settings.ts'); + + assert.equal((i18n.match(/codexBeta: 'Codex'/g) ?? []).length, 8); + assert.doesNotMatch(i18n, /codexBeta: 'Codex Beta'/); + assert.match(settings, /label: 'Enable Codex'/); + assert.doesNotMatch(settings, /label: 'Enable Codex Beta'/); +}); + test('v2.3 README editions share release evidence and local-data boundaries', () => { const readmes = [ 'README.md', @@ -1514,6 +1529,91 @@ test('v2.3 README editions share release evidence and local-data boundaries', () assert.match(repoFile('LOCAL-DATA.zh-CN.md'), /OAuth access\/refresh token/); }); +test('v2.4 synthetic preview images are present and carry no text metadata', () => { + for (const image of [ + 'images/v2.4.0/compare-sharing-en-light.png', + 'images/v2.4.0/claude-sharing-zh-CN-dark.png', + 'images/v2.4.0/codex-month-zh-CN-dark.png', + 'images/v2.4.0/narrow-sharing-de-DE-dark.png', + ]) { + assert.ok(existsSync(resolve(REPO_ROOT, image)), `missing synthetic preview ${image}`); + const chunkTypes = privacySafePngChunkTypes(image); + for (const metadataChunk of ['eXIf', 'iTXt', 'tEXt', 'zTXt']) { + assert.equal(chunkTypes.includes(metadataChunk), false, `${image} retains ${metadataChunk} metadata`); + } + } +}); + +test('v2.4 README editions describe one preview-first sharing workspace', () => { + const expectations: Record = { + 'README.md': [ + /^## What's new in 2\.4$/m, + /preview-first/i, + /Combined activity heatmap/, + /Claude Share Card/, + /Claude token\s+heatmap/, + ], + 'README-en.md': [ + /^## What's new in v2\.4$/m, + /preview-first/i, + /Combined activity heatmap/, + /Claude Share Card/, + /Claude token\s+heatmap/, + ], + 'README-zh-CN.md': [ + /^## 2\.4 新功能$/m, + /预览优先/, + /综合活动热力图/, + /Claude 分享卡/, + /Claude token\s+热力图/, + ], + 'README-zh-TW.md': [ + /^## v2\.4 新功能$/m, + /預覽優先/, + /綜合活動熱力圖/, + /Claude 分享卡/, + /Claude token\s+熱力圖/, + ], + 'README-ja.md': [ + /^## v2\.4 の新機能$/m, + /プレビュー/, + /統合アクティビティヒートマップ/, + /Claude Share Card/, + /Claude トークンヒートマップ/, + ], + 'README-ko.md': [ + /^## v2\.4 새로운 기능$/m, + /미리보기/, + /통합 활동 히트맵/, + /Claude 공유 카드/, + /Claude 토큰 히트맵/, + ], + 'README-id.md': [ + /^## Yang baru di v2\.4$/m, + /pratinjau/i, + /heatmap aktivitas gabungan/i, + /Claude Share Card/, + /heatmap token Claude/i, + ], + }; + + for (const [readme, patterns] of Object.entries(expectations)) { + const body = repoFile(readme); + for (const pattern of patterns) { + assert.match(body, pattern, `${readme} is missing ${pattern}`); + } + for (const publicId of ['exportShareCard', 'exportHeatmap', 'publishHeatmapToGitHub']) { + assert.ok(body.includes(publicId), `${readme} is missing compatibility command ${publicId}`); + } + assert.match(body, /enableShareCard/, `${readme} is missing the single visible sharing switch`); + assert.doesNotMatch( + body, + /^##[^\n]*2\.4\.\d[^\n]*$/m, + `${readme} must keep What's new at minor-version granularity`, + ); + } +}); + test('Marketplace metadata presents Claude and Codex local usage support', () => { const packageJson = JSON.parse(repoFile('package.json')) as { description: string; @@ -1525,6 +1625,37 @@ test('Marketplace metadata presents Claude and Codex local usage support', () => assert.ok(packageJson.keywords.includes('local-usage')); }); +test('every supported UI locale has a discoverable README edition', () => { + const packageJson = JSON.parse(repoFile('package.json')) as { + description: string; + keywords: string[]; + contributes: { configuration: { properties: Record } }; + }; + const locales = packageJson.contributes.configuration.properties['claudeCodeUsage.language'].enum + ?.filter((value) => value !== 'auto') ?? []; + assert.equal(locales.length, 8); + const home = repoFile('README.md'); + for (const locale of locales) { + const filename = `README-${locale}.md`; + assert.ok(existsSync(resolve(REPO_ROOT, filename)), `${locale} has no README`); + assert.ok(home.includes(`](${filename})`), `Marketplace README does not link ${filename}`); + const body = repoFile(filename); + assert.match(body, /README\.md/, `${filename} cannot return to the main README`); + assert.match(body, /Codex/, `${filename} omits Codex`); + assert.match(body, /https:\/\/claude\.com\/claude-code/, `${filename} omits Claude Code credit`); + assert.match(body, /https:\/\/developers\.openai\.com\/codex\//, `${filename} omits Codex credit`); + } + for (const term of ['token', 'quota', 'status bar']) { + assert.match(packageJson.description, new RegExp(term, 'i')); + } + for (const keyword of ['codex-usage', 'token-usage', 'quota']) { + assert.ok(packageJson.keywords.includes(keyword), `Marketplace lacks ${keyword} tag`); + } + assert.ok(packageJson.keywords.length <= 30); + assert.ok(repoFile('README-en.md').split('\n').length > repoFile('README-de-DE.md').split('\n').length); + assert.ok(repoFile('README-zh-CN.md').split('\n').length > repoFile('README-pt-BR.md').split('\n').length); +}); + test('pull request checklist names the actual eight UI locales', () => { const packageJson = JSON.parse(repoFile('package.json')) as { contributes: { configuration: { properties: Record } }; @@ -1535,7 +1666,7 @@ test('pull request checklist names the actual eight UI locales', () => { const template = repoFile('.github/PULL_REQUEST_TEMPLATE.md'); assert.match(template, /all eight UI locales/); assert.doesNotMatch(template, /all seven languages/); - assert.match(template, /all seven README editions/); + assert.match(template, /all nine README files/); }); test('changelog records the V2.2.2 energy patch after the released V2.2.1 baseline', () => { @@ -1593,7 +1724,7 @@ test('release announcements are exact-version and user-disableable', () => { assert.match(settings, /key:\s*'releaseAnnouncements'/); assert.match(settings, /default:\s*true/); }); -test('Codex beta settings use safe provider-aware defaults', () => { +test('Codex settings default to today processed tokens and preserve provider-aware controls', () => { const settings = repoFile('src/settings.ts'); const webview = repoFile('src/webview.ts'); const packageJson = repoFile('package.json'); @@ -1601,7 +1732,7 @@ test('Codex beta settings use safe provider-aware defaults', () => { assert.match(settings, /key:\s*'codex\.enabled'[\s\S]*?default:\s*true/); assert.match(settings, /key:\s*'codex\.fileWatchSeconds'[\s\S]*?default:\s*'30'/); assert.match(settings, /key:\s*'statusBarProvider'[\s\S]*?default:\s*'auto'/); - assert.match(settings, /key:\s*'codex\.statusMetric'[\s\S]*?default:\s*'fresh'/); + assert.match(settings, /key:\s*'codex\.statusMetric'[\s\S]*?default:\s*'processed'/); assert.match(settings, /key:\s*'codex\.optimization\.enabled'[\s\S]*?default:\s*true/); assert.match(webview, /'general'[\s\S]*?'providers'[\s\S]*?'features'/); assert.match(packageJson, /claudeCodeUsage\.codex\.dataDirectory/); @@ -1768,8 +1899,8 @@ test('publish pins compatible registry CLIs and isolates all three delivery sink test('CONTRIBUTING documents provider-aware privacy and three testing layers', () => { const guide = repoFile('CONTRIBUTING.md'); - assert.match(guide, /Codex Beta is enabled by default and can be turned off in provider settings\./); - assert.doesNotMatch(guide, /(?:opt-in[\s\S]{0,80}Codex Beta|Codex Beta[\s\S]{0,80}opt-in)/i); + assert.match(guide, /Codex is enabled by default and can be turned off in provider settings\./); + assert.doesNotMatch(guide.slice(0, 1_500), /Codex Beta/); assert.doesNotMatch(guide, /Claude-only|Multi-provider monitoring[^\n]*out of scope/i); assert.match(guide, /Usage ingestion is read-only, and Codex data is never mutated\./); assert.match(guide, /Claude session actions are separately gated and disabled by default\./); diff --git a/src/test/settingsPrivacy.test.ts b/src/test/settingsPrivacy.test.ts index f07f8ec2..9903a9ae 100644 --- a/src/test/settingsPrivacy.test.ts +++ b/src/test/settingsPrivacy.test.ts @@ -493,6 +493,10 @@ test('the tracked bilingual data contract covers quota, retention, clearing, and '512', 'ccu.heatmapRepo', 'ccu.heatmapPath', + 'ccu.heatmapDestination.v1', + 'ccu.sharing.template', + 'enableShareCard', + 'showHeatmap', 'ccu.combinedHeatmap.title', 'ccu.combinedHeatmap.range', 'ccu.combinedHeatmap.privacyPreview', @@ -512,10 +516,10 @@ test('the tracked bilingual data contract covers quota, retention, clearing, and } }); -test('GitHub heatmap publication is public-only, exact-target confirmed, and persists destinations after success', () => { +test('GitHub heatmap publication is public-only, exact-target confirmed, and atomically persists its destination after success', () => { const source = fs.readFileSync(path.join(__dirname, '..', '..', 'src', 'extension.ts'), 'utf8'); const start = source.indexOf('private async publishHeatmapToGitHub'); - const end = source.indexOf('/** Export a one-page usage share card', start); + const end = source.indexOf('private async getAdvice', start); const publish = source.slice(start, end); assert.ok(start >= 0 && end > start); @@ -525,18 +529,22 @@ test('GitHub heatmap publication is public-only, exact-target confirmed, and per assert.match(publish, /githubPublishConfirmationDetail/); assert.match(publish, /Confirm the exact GitHub write/); const write = publish.indexOf('publishPublicGitHubFile'); - const persistRepo = publish.indexOf("globalState.update('ccu.heatmapRepo'"); - const persistPath = publish.indexOf("globalState.update('ccu.heatmapPath'"); - assert.ok(write >= 0 && persistRepo > write && persistPath > write); + const complete = publish.indexOf('completeSuccessfulGitHubPublish'); + assert.ok(write >= 0 && complete > write); + assert.match(publish, /globalState\.update\(key, destination\)/); + assert.doesNotMatch(publish.slice(write), /globalState\.update\('ccu\.heatmap(?:Repo|Path)'/); + assert.match(publish, /publishPreferenceSaveWarning/); }); -test('reset sharing preferences clears only the remembered GitHub destination', () => { - const source = fs.readFileSync(path.join(__dirname, '..', '..', 'src', 'webview.ts'), 'utf8'); - const start = source.indexOf("case 'resetCombinedHeatmapPreferences'"); - const end = source.indexOf("case 'buildShareCard'", start); +test('reset sharing preferences host step clears only sharing-owned state and remembered GitHub destination', () => { + const source = fs.readFileSync(path.join(__dirname, '..', '..', 'src', 'extension.ts'), 'utf8'); + const start = source.indexOf('private async resetSharingPreferencesHost'); + const end = source.indexOf('private pendingClientReset', start); const reset = source.slice(start, end); assert.ok(start >= 0 && end > start); + assert.match(reset, /settings\.resetSharingOwnedData\(\)/); + assert.match(reset, /globalState\.update\(GITHUB_HEATMAP_DESTINATION_KEY, undefined\)/); assert.match(reset, /globalState\.update\('ccu\.heatmapRepo', undefined\)/); assert.match(reset, /globalState\.update\('ccu\.heatmapPath', undefined\)/); assert.doesNotMatch(reset, /quota|index|secret|token|cookie|log/i); diff --git a/src/test/shareCardSvg.test.ts b/src/test/shareCardSvg.test.ts index 8abcaa7c..72f1b002 100644 --- a/src/test/shareCardSvg.test.ts +++ b/src/test/shareCardSvg.test.ts @@ -9,15 +9,78 @@ import { renderShareCardSvg, resolveShareCardTheme, SHARE_CARD_THEMES, BADGE_COP const base: ShareCardData = { range: 'month', watermark: true }; +type Rgb = [number, number, number]; + +function hexRgb(hex: string): Rgb { + assert.match(hex, /^#[0-9a-f]{6}$/i); + return [1, 3, 5].map((start) => Number.parseInt(hex.slice(start, start + 2), 16)) as Rgb; +} + +function composite(foreground: Rgb, background: Rgb, alpha: number): Rgb { + return foreground.map((value, index) => + Math.round(value * alpha + background[index] * (1 - alpha)), + ) as Rgb; +} + +function compositeCssColor(color: string, background: Rgb): Rgb { + const rgba = /^rgba\((\d+),(\d+),(\d+),([\d.]+)\)$/.exec(color.replace(/\s+/g, '')); + if (rgba) { + return composite( + [Number(rgba[1]), Number(rgba[2]), Number(rgba[3])], + background, + Number(rgba[4]), + ); + } + return hexRgb(color); +} + +function relativeLuminance(rgb: Rgb): number { + const channels = rgb.map((channel) => { + const value = channel / 255; + return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4; + }); + return 0.2126 * channels[0] + 0.7152 * channels[1] + 0.0722 * channels[2]; +} + +function contrastRatio(foreground: Rgb, background: Rgb): number { + const foregroundLuminance = relativeLuminance(foreground); + const backgroundLuminance = relativeLuminance(background); + return (Math.max(foregroundLuminance, backgroundLuminance) + 0.05) / + (Math.min(foregroundLuminance, backgroundLuminance) + 0.05); +} + +function possibleTextSurfaces(theme: (typeof SHARE_CARD_THEMES)[keyof typeof SHARE_CARD_THEMES]): Rgb[] { + const bases = [hexRgb(theme.bgTop), hexRgb(theme.bgBottom)]; + const decorated = bases.flatMap((background) => [ + background, + composite(hexRgb(theme.blobWarm), background, 0.55), + composite(hexRgb(theme.blobCool), background, 0.5), + ]); + return decorated.flatMap((background) => [ + background, + compositeCssColor(theme.panelFill, background), + compositeCssColor(theme.badgeFill, background), + ]); +} + test('renders a well-formed 1200x680 svg with the brand', () => { const svg = renderShareCardSvg(base); assert.match(svg, /^$/); assert.match(svg, /width="1200" height="680"/); + assert.match(svg, /role="img" aria-label="Claude Code Usage/); assert.match(svg, />Claude Code UsageAI coding usage snapshot { + const svg = renderShareCardSvg(base, { + avatarDataUri: `data:image/svg+xml,\" onload=\"alert(1)' data-canary='unsafe`, + }); + assert.doesNotMatch(svg, /href="data:image\/svg\+xml," onload=/i); + assert.match(svg, /href="data:image\/svg\+xml," onload="alert\(1\)' data-canary='unsafe"/); +}); + test('draws the total-tokens hero compactly with its label + one decimal', () => { const svg = renderShareCardSvg({ ...base, totalTokens: 5_000_000_000 }); assert.match(svg, />5\.0B { } }); -test('BADGE_COPY covers every badge id in en / zh-CN / zh-TW', () => { +test('BADGE_COPY covers every badge id in all eight UI locales', () => { for (const id of ['context-marathoner', 'cache-saver', 'token-sprinter', 'workflow-pilot', 'steady-builder']) { - for (const lang of ['en', 'zh-CN', 'zh-TW'] as const) { + for (const lang of ['en', 'de-DE', 'zh-TW', 'zh-CN', 'ja', 'ko', 'pt-BR', 'id'] as const) { assert.ok(BADGE_COPY[id]?.[lang]?.title && BADGE_COPY[id]?.[lang]?.line, `${id} ${lang}`); } } @@ -168,7 +231,91 @@ test('card text follows the UI language', () => { assert.match(zh, />总 token缓存命中total tokens${label}<`)); + } + // Unknown UI languages still fall back to English. const de = renderShareCardSvg(data, { lang: 'de-DE' }); - assert.match(de, />total tokenstotal tokenstotal tokens { + const data: ShareCardData = { + ...base, + range: 'week', + rhythm: [1, 2], + rhythmStart: '2026-06-01', + rhythmEnd: '2026-06-02', + }; + const expected = { + en: ['the last 7 days', 'Jun 1'], + 'de-DE': ['letzten 7 Tage', '1. Juni'], + 'zh-TW': ['最近 7 天', '6月1日'], + 'zh-CN': ['最近 7 天', '6月1日'], + ja: ['過去 7 日間', '6月1日'], + ko: ['최근 7일', '6월 1일'], + 'pt-BR': ['últimos 7 dias', '1 de jun.'], + id: ['7 hari terakhir', '1 Jun'], + }; + for (const [lang, markers] of Object.entries(expected)) { + const svg = renderShareCardSvg(data, { lang }); + for (const marker of markers) assert.match(svg, new RegExp(marker)); + } +}); + +test('every normal-size SVG label has at least 4.5:1 contrast on every possible card surface', () => { + const data: ShareCardData = { + ...base, + totalTokens: 5_000_000_000, + estimatedCost: 42, + cacheSharePct: 71, + topModelName: 'Opus 4.8', + sessions: 9, + messages: 20, + composition: { + input: 1_000_000, + output: 3_000_000, + cacheCreate: 2_000_000, + cacheRead: 4_000_000, + }, + rhythm: [1, 2, 5_300_000], + rhythmStart: '2026-06-01', + rhythmEnd: '2026-06-30', + badge: { id: 'cache-saver', label: 'Cache Saver' }, + }; + + for (const themeName of ['claudeClassic', 'claudeCream', 'auroraDark'] as const) { + const svg = renderShareCardSvg(data, { + theme: themeName, + avatarDataUri: 'data:image/png;base64,AAA', + username: 'octocat', + }); + const labels = [...svg.matchAll(/]*)>/g)].flatMap((match) => { + const fontSize = /font-size="([\d.]+)"/.exec(match[1])?.[1]; + const fill = /fill="([^"]+)"/.exec(match[1])?.[1]; + return fontSize && fill && Number(fontSize) <= 22 + ? [{ fontSize: Number(fontSize), fill }] + : []; + }); + assert.ok(labels.length > 0, `${themeName} should render normal-size labels`); + + for (const label of labels) { + assert.match(label.fill, /^#[0-9a-f]{6}$/i, `${themeName} normal text must use an auditable solid fill`); + for (const background of possibleTextSurfaces(SHARE_CARD_THEMES[themeName])) { + assert.ok( + contrastRatio(hexRgb(label.fill), background) >= 4.5, + `${themeName} ${label.fill} at ${label.fontSize}px must retain 4.5:1 contrast`, + ); + } + } + } }); diff --git a/src/test/sharePresentationCache.test.ts b/src/test/sharePresentationCache.test.ts new file mode 100644 index 00000000..9a21aa9a --- /dev/null +++ b/src/test/sharePresentationCache.test.ts @@ -0,0 +1,97 @@ +import { test } from 'node:test'; +import * as assert from 'node:assert/strict'; +import Module = require('node:module'); + +test('sharing renders only the active presentation and reuses materialized daily aggregates', () => { + const originalLoad = (Module as any)._load; + const activeColorTheme = { kind: 1 }; + (Module as any)._load = function(request: string, parent: unknown, isMain: boolean) { + if (request === 'vscode') { + return { + ColorThemeKind: { Light: 1, Dark: 2, HighContrast: 3, HighContrastLight: 4 }, + window: { activeColorTheme }, + workspace: { getConfiguration: () => ({ get: () => undefined }) }, + }; + } + return originalLoad.call(this, request, parent, isMain); + }; + + let shareBuilds = 0; + + try { + const { UsageWebviewProvider } = require('../webview') as typeof import('../webview'); + const { I18n } = require('../i18n') as typeof import('../i18n'); + const provider = new UsageWebviewProvider({ + globalState: { get: () => undefined, update: async () => undefined }, + } as any) as any; + provider.buildShareCardSvgFor = () => { + shareBuilds += 1; + return ''; + }; + const initialRecords: any[] = []; + const usage = { + totalInputTokens: 100, + totalOutputTokens: 20, + totalCacheCreationTokens: 30, + totalCacheReadTokens: 50, + totalCost: 1.25, + costBreakdown: { input: 0.1, output: 0.2, cacheWrite: 0.3, cacheRead: 0.65 }, + messageCount: 2, + modelBreakdown: {}, + }; + const initialDaily = [{ date: '2026-09-22', data: usage }]; + provider.updateData( + null, null, null, null, [], [], [], undefined, undefined, initialRecords, + [], [], null, [], [], [], {}, null, initialDaily, + ); + provider.providerAvailability = { claude: true, codex: true, codexData: true }; + + const firstDaily = provider.getClaudeDailyUsageMap(I18n.getTimezone()); + assert.deepEqual(firstDaily, { + '2026-09-22': { tokens: 200, cost: 1.25, sessions: 0 }, + }); + assert.strictEqual( + provider.getClaudeDailyUsageMap(I18n.getTimezone()), + firstDaily, + 'unchanged materialized days reuse the daily projection', + ); + + provider.sharingTemplate = 'combinedHeatmap'; + provider.renderSharingWorkspace(); + assert.equal(shareBuilds, 0, 'the hidden Share Card is not generated'); + + provider.sharingTemplate = 'claudeShareCard'; + provider.renderSharingWorkspace(); + assert.equal(shareBuilds, 1, 'selecting Share Card generates one preview'); + provider.renderSharingWorkspace(); + assert.equal(shareBuilds, 1, 'unchanged Share Card preview is cached'); + + const nextRecords: any[] = []; + const nextDaily = [{ date: '2026-09-23', data: { ...usage, totalInputTokens: 300 } }]; + provider.updateData( + null, null, null, null, [], [], [], undefined, undefined, nextRecords, + [], [], null, [], [], [], {}, null, nextDaily, + ); + const secondDaily = provider.getClaudeDailyUsageMap(I18n.getTimezone()); + assert.notStrictEqual(secondDaily, firstDaily); + assert.equal(secondDaily['2026-09-23'].tokens, 400); + provider.renderSharingWorkspace(); + assert.equal(shareBuilds, 2, 'changed indexed records invalidate the active Share Card once'); + + const alternateZone = I18n.getTimezone() === 'Pacific/Auckland' + ? 'America/Los_Angeles' : 'Pacific/Auckland'; + const alternateDaily = provider.getClaudeDailyUsageMap(alternateZone); + assert.strictEqual(provider.getClaudeDailyUsageMap(alternateZone), alternateDaily); + assert.notStrictEqual(alternateDaily, secondDaily, 'timezone remains part of the cache identity'); + + activeColorTheme.kind = 2; + provider.renderSharingWorkspace(); + assert.equal(shareBuilds, 3, 'theme changes rebuild the active theme-aware card'); + + provider.invalidateShareCardPreview(); + provider.renderSharingWorkspace(); + assert.equal(shareBuilds, 4, 'formatting changes invalidate the active preview once'); + } finally { + (Module as any)._load = originalLoad; + } +}); diff --git a/src/test/shareWorkspaceRouting.test.ts b/src/test/shareWorkspaceRouting.test.ts new file mode 100644 index 00000000..385216d6 --- /dev/null +++ b/src/test/shareWorkspaceRouting.test.ts @@ -0,0 +1,117 @@ +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { test } from 'node:test'; +import * as assert from 'node:assert/strict'; + +const source = (file: string): string => fs.readFileSync( + path.join(__dirname, '..', '..', 'src', file), + 'utf8', +); + +test('legacy public share commands enter the matching preview-first workspace presentation', () => { + const extension = source('extension.ts'); + const start = extension.indexOf('private setupCommands'); + const end = extension.indexOf('private buildQuotaScopeOptions', start); + assert.ok(start >= 0 && end > start, 'command setup must remain discoverable'); + const commands = extension.slice(start, end); + + const routes = [ + ['claudeCodeUsage.exportShareCard', 'claudeShareCard'], + ['claudeCodeUsage.exportHeatmap', 'claudeHeatmap'], + ['claudeCodeUsage.publishHeatmapToGitHub', 'claudeHeatmap'], + ] as const; + for (const [command, presentation] of routes) { + const registration = commands.indexOf(`registerCommand('${command}'`); + assert.ok(registration >= 0, `${command} remains public`); + const nextRegistration = commands.indexOf('registerCommand(', registration + 1); + const handler = commands.slice( + registration, + nextRegistration >= 0 ? nextRegistration : commands.length, + ); + assert.match( + handler, + new RegExp(`showSharingWorkspace\\('${presentation}'\\)`), + `${command} should route to ${presentation}`, + ); + } +}); + +test('sharing has one visible on-off control while the old heatmap preference remains compatibility-only', () => { + const settings = source('settings.ts'); + const block = (key: string): string => { + const start = settings.indexOf(`key: '${key}'`); + const end = settings.indexOf('\n },', start); + assert.ok(start >= 0 && end > start, `${key} setting remains catalogued`); + return settings.slice(start, end); + }; + const sharing = block('enableShareCard'); + const legacyHeatmap = block('showHeatmap'); + + assert.match(sharing, /default: true/); + assert.doesNotMatch(sharing, /visible: false/); + assert.match(legacyHeatmap, /visible: false/); + + const webview = source('webview.ts'); + assert.match( + webview, + /message\.key === 'enableShareCard'[\s\S]*?this\.sharingWorkspaceRequested = false/, + 'turning off the one visible switch must dismiss a command-opened workspace', + ); +}); + +test('the old Claude panels no longer render beside the unified workspace', () => { + const webview = source('webview.ts'); + const start = webview.indexOf('private renderAllTimeData'); + const end = webview.indexOf('private weeklyValuePoints', start); + assert.ok(start >= 0 && end > start, 'All-time renderer must remain discoverable'); + const allTime = webview.slice(start, end); + + assert.doesNotMatch(allTime, /setting\('showHeatmap'/); + assert.doesNotMatch(allTime, /renderShareCardPanel\(\) \+ heatmapPanel/); + assert.match(allTime, /renderSharingWorkspace/); +}); + +test('Share Card preview and local export have no GitHub identity or network path', () => { + const webview = source('webview.ts'); + const start = webview.indexOf("case 'buildShareCard'"); + const end = webview.indexOf("case 'setDashboardAutoRefresh'", start); + assert.ok(start >= 0 && end > start, 'Share Card request handlers must remain discoverable'); + const localArtifactHandlers = webview.slice(start, end); + + assert.doesNotMatch( + localArtifactHandlers, + /getGithubIdentity|authentication|getSession|httpsGet|api\.github\.com|message\.(?:avatar|username)/i, + ); + assert.doesNotMatch(webview, /import \* as https from ['"]https['"]/); + assert.doesNotMatch(webview, /private async getGithubIdentity|private httpsGet/); +}); + +test('the in-workspace sharing reset routes through the existing host request and client ACK protocol', () => { + const webview = source('webview.ts'); + const start = webview.indexOf('function resetCombinedHeatmapPreferences'); + const end = webview.indexOf('\nfunction refresh()', start); + assert.ok(start >= 0 && end > start, 'direct sharing reset must remain discoverable'); + const reset = webview.slice(start, end); + + assert.match(reset, /runLocalDataAction\(['"]reset-sharing-preferences['"]\)/); + assert.doesNotMatch(reset, /ccuResetLocalStorageKeys|ccuResetSessionStorageKeys/); + assert.doesNotMatch(reset, /command:\s*['"]resetCombinedHeatmapPreferences['"]/); + assert.doesNotMatch(webview, /case ['"]resetCombinedHeatmapPreferences['"]/); +}); + +test('Share Card drafts keep raw project paths and session IDs memory-only', () => { + const webview = source('webview.ts'); + const start = webview.indexOf('var __ccuShareCardDraft'); + const end = webview.indexOf('\nfunction scApplyDefaultControls', start); + assert.ok(start >= 0 && end > start, 'Share Card draft policy must remain discoverable'); + const draft = webview.slice(start, end); + + assert.match(draft, /var __ccuShareCardDraft = null/); + assert.match(draft, /scope: 'all'/); + assert.match(draft, /JSON\.stringify\(scPersistentDraftConfig\(cfg\)\)/); + assert.match(draft, /__ccuShareCardDraft = scReadConfig\(\)/); + assert.doesNotMatch( + draft, + /localStorage\.setItem\([^\n]*JSON\.stringify\(scReadConfig\(\)\)/, + ); +}); diff --git a/src/test/sharingCommandIntent.test.ts b/src/test/sharingCommandIntent.test.ts new file mode 100644 index 00000000..a4988729 --- /dev/null +++ b/src/test/sharingCommandIntent.test.ts @@ -0,0 +1,123 @@ +import { test } from 'node:test'; +import * as assert from 'node:assert/strict'; +import Module = require('node:module'); + +import { SharingCommandIntentLedger } from '../sharingCommandIntent'; + +test('consumed sharing command intent stays one-shot while revision history remains monotonic', () => { + const ledger = new SharingCommandIntentLedger(); + + const first = ledger.issue('claudeHeatmap'); + assert.deepEqual(first, { revision: 1, template: 'claudeHeatmap' }); + assert.deepEqual(ledger.pending(), first); + assert.equal(ledger.acknowledge(0), false, 'a stale ACK cannot consume live intent'); + assert.deepEqual(ledger.pending(), first); + + assert.equal(ledger.acknowledge(first.revision), true); + assert.equal(ledger.pending(), undefined, 'ordinary rerenders have no consumed intent to replay'); + assert.equal(ledger.acknowledge(first.revision), false, 'consumption is idempotent'); + + ledger.clearPending(); + assert.equal(ledger.pending(), undefined, 'reset and dispose/reopen keep consumed intent absent'); + assert.equal(ledger.latestRevision(), 1, 'reset must not reuse an old command revision'); + + const second = ledger.issue('claudeShareCard'); + assert.deepEqual(second, { revision: 2, template: 'claudeShareCard' }); + ledger.clearPending(); + assert.equal(ledger.pending(), undefined); + assert.equal(ledger.latestRevision(), 2); + + assert.deepEqual(ledger.issue('claudeHeatmap'), { + revision: 3, + template: 'claudeHeatmap', + }); +}); + +test('a delayed ACK cannot consume a newer sharing command', () => { + const ledger = new SharingCommandIntentLedger(); + const first = ledger.issue('claudeHeatmap'); + const second = ledger.issue('claudeShareCard'); + + assert.equal(ledger.acknowledge(first.revision), false); + assert.deepEqual(ledger.pending(), second); + assert.equal(ledger.acknowledge(second.revision), true); + assert.equal(ledger.pending(), undefined); +}); + +test('the provider consumes an acknowledged command across rerender, reset, and panel reopen', async () => { + const originalLoad = (Module as any)._load; + let messageHandler: ((message: Record) => Promise) | undefined; + let disposeHandler: (() => void) | undefined; + const panels: Array<{ webview: { html: string } }> = []; + const vscode = { + ColorThemeKind: { Light: 1, Dark: 2, HighContrast: 3, HighContrastLight: 4 }, + ViewColumn: { One: 1 }, + Uri: { file: (fsPath: string) => ({ fsPath }) }, + env: { language: 'en', uriScheme: 'vscode' }, + commands: { executeCommand: async () => undefined }, + extensions: { getExtension: () => undefined }, + window: { + activeColorTheme: { kind: 1 }, + createWebviewPanel: () => { + const webview = { + html: '', + onDidReceiveMessage: (handler: typeof messageHandler) => { + messageHandler = handler; + return { dispose: () => undefined }; + }, + postMessage: async () => true, + }; + const panel = { + webview, + reveal: () => undefined, + onDidDispose: (handler: () => void) => { + disposeHandler = handler; + return { dispose: () => undefined }; + }, + }; + panels.push(panel); + return panel; + }, + showWarningMessage: async () => undefined, + }, + workspace: { + workspaceFolders: [], + getConfiguration: () => ({ get: () => undefined, update: async () => undefined }), + fs: {}, + }, + }; + (Module as any)._load = function(request: string, parent: unknown, isMain: boolean) { + if (request === 'vscode') return vscode; + return originalLoad.call(this, request, parent, isMain); + }; + + try { + const { UsageWebviewProvider } = require('../webview') as typeof import('../webview'); + const provider = new UsageWebviewProvider({ + globalState: { get: () => undefined, update: async () => undefined }, + } as any) as any; + provider.allRecords = [{ timestamp: '2026-09-01T00:00:00.000Z' }]; + + provider.showSharingWorkspace('claudeHeatmap'); + assert.match(panels[panels.length - 1].webview.html, /__sharingTemplateCommand = \{"revision":1,"template":"claudeHeatmap"\}/); + assert.ok(messageHandler); + await messageHandler!({ command: 'sharingTemplateCommandAck', revision: 1 }); + + provider.updateWebview(); + assert.match(panels[panels.length - 1].webview.html, /__sharingTemplateCommand = null/); + provider.clearSharingRuntimeState(); + provider.updateWebview(); + assert.match(panels[panels.length - 1].webview.html, /__sharingTemplateCommand = null/); + + assert.ok(disposeHandler); + disposeHandler!(); + provider.show(); + assert.equal(panels.length, 2); + assert.match(panels[panels.length - 1].webview.html, /__sharingTemplateCommand = null/); + + provider.showSharingWorkspace('claudeShareCard'); + assert.match(panels[panels.length - 1].webview.html, /__sharingTemplateCommand = \{"revision":2,"template":"claudeShareCard"\}/); + } finally { + (Module as any)._load = originalLoad; + } +}); diff --git a/src/webview.ts b/src/webview.ts index b233f430..784dcd00 100644 --- a/src/webview.ts +++ b/src/webview.ts @@ -30,6 +30,10 @@ import { } from './combinedHeatmapSvg'; import { DEFAULT_SECTIONS, ShareSections, buildShareCardData, shareCardFilename } from './shareCard'; import { renderShareCardSvg, ShareCardTheme } from './shareCardSvg'; +import { + SharingCommandIntentLedger, + SharingTemplate, +} from './sharingCommandIntent'; import { parseConversation } from './conversationLog'; import { renderConversationViewer } from './conversationViewerHtml'; import { formatUsageDate, shortUsageDate } from './usageDateLabels'; @@ -76,7 +80,6 @@ import { } from './projectUsageMatrix'; import * as os from 'os'; import * as path from 'path'; -import * as https from 'https'; import { createHash, randomBytes } from 'crypto'; import { AdviceEffectivenessProvider, @@ -363,6 +366,8 @@ function localDataUiCopy(locale: string): LocalDataUiCopy { }; } +export type { SharingTemplate } from './sharingCommandIntent'; + interface CombinedHeatmapUiCopy { eyebrow: string; panelTitle: string; @@ -939,6 +944,7 @@ export class UsageWebviewProvider { private allTimeData: UsageData | null = null; private dailyDataForRolling30Days: { date: string; data: UsageData }[] = []; private dailyDataForAllTime: { date: string; data: UsageData }[] = []; + private dailyDataForEveryDay: { date: string; data: UsageData }[] = []; private hourlyDataForToday: { hour: string; data: UsageData }[] = []; private hourlyDataForRolling30DaysByDay: Record< string, @@ -949,6 +955,9 @@ export class UsageWebviewProvider { private dataDirectory: string | null = null; private currentTab: string = 'today'; private currentProvider: 'claude' | 'codex' | 'compare' = 'claude'; + private sharingTemplate: SharingTemplate = 'combinedHeatmap'; + private sharingWorkspaceRequested = false; + private readonly sharingCommandIntents = new SharingCommandIntentLedger(); private codexView: CodexUsageView | null = null; private codexInsights: CodexScopedInsights = emptyCodexScopedInsights(); private providerAvailability = { claude: false, codex: false, codexData: false }; @@ -967,7 +976,6 @@ export class UsageWebviewProvider { private branchBreakdown: BranchUsage[] = []; private workflowBreakdown: WorkflowUsage[] = []; private costliestMessages: CostlyMessage[] = []; - private githubIdentity?: { avatar?: string; name?: string }; // fetched on demand // Cache the (slow-moving) cache analyses so they recompute at most weekly, not // every render — Carl: a weekly refresh is plenty for these estimates. private analysisCache?: { @@ -983,16 +991,27 @@ export class UsageWebviewProvider { // One reusable conversation-viewer panel: each "view" click re-reads the file // and refreshes this panel rather than piling up new tabs. private conversationPanel?: vscode.WebviewPanel; - // Last generated share card + its config, kept so an auto-refresh re-render - // doesn't wipe the user's picks or preview (share card / heatmap / optimizer - // output should survive data refreshes — only usage numbers update). - private lastShareCardSvg?: string; + // Reuse expensive local previews only while their data and display context + // remain unchanged. Config is retained independently from a generated SVG. + private shareCardPreviewCache?: { + records: any[]; day: string; timeZone: string; locale: string; themeKind?: number; svg: string; + }; + private claudeHeatmapPreviewCache?: { + dailyRows: { date: string; data: UsageData }[]; + day: string; + timeZone: string; + locale: string; + svg: string; + }; + private claudeDailyUsageCache?: { + dailyRows: { date: string; data: UsageData }[]; + timeZone: string; + daily: ReturnType; + }; private lastShareCardConfig?: { range: string; scope: string; sections: Record; - avatar: boolean; - username: boolean; fullNumbers: boolean; theme: string; }; @@ -1040,7 +1059,8 @@ export class UsageWebviewProvider { action: LocalDataAction, quotaScopeToken?: string, ) => Promise; - public onResetSharingPreferences?: () => Promise; + public onExportClaudeHeatmap?: () => void | Promise; + public onPublishClaudeHeatmap?: () => void | Promise; public onLocalDataClientReady?: () => void; // Shared settings store + a callback to let extension.ts re-apply config when // the user edits a setting in the dashboard's ⚙ Settings tab. Both are set by @@ -1628,14 +1648,26 @@ export class UsageWebviewProvider { } public clearSharingRuntimeState(): void { - this.lastShareCardSvg = undefined; + this.invalidateSharingCaches(); this.lastShareCardConfig = undefined; + this.sharingTemplate = 'combinedHeatmap'; + this.sharingWorkspaceRequested = false; + // Reset clears any live command without rewinding its sequence. A later + // command therefore cannot collide with a command already seen by this + // provider's Webview. + this.sharingCommandIntents.clearPending(); } - /** A display-format change invalidates rendered money but not the user's + /** Pricing or display changes invalidate derived previews but not the user's * chosen share-card range, scope, sections, or theme. */ public invalidateShareCardPreview(): void { - this.lastShareCardSvg = undefined; + this.invalidateSharingCaches(); + } + + private invalidateSharingCaches(): void { + this.shareCardPreviewCache = undefined; + this.claudeHeatmapPreviewCache = undefined; + this.claudeDailyUsageCache = undefined; } private async handleClearAdviceLocalDataMessage(): Promise { @@ -2060,6 +2092,12 @@ export class UsageWebviewProvider { } break; } + case 'sharingTemplateCommandAck': { + if (Number.isSafeInteger(message.revision)) { + this.sharingCommandIntents.acknowledge(message.revision); + } + break; + } case 'requestLocalDataInventory': { const summary = message.clientSummary as Partial | undefined; const clientSummary: LocalDataClientSummary = { @@ -2126,10 +2164,22 @@ export class UsageWebviewProvider { break; } case 'exportHeatmap': - vscode.commands.executeCommand('claudeCodeUsage.exportHeatmap'); + await this.onExportClaudeHeatmap?.(); break; case 'publishHeatmap': - vscode.commands.executeCommand('claudeCodeUsage.publishHeatmapToGitHub'); + await this.onPublishClaudeHeatmap?.(); + break; + case 'sharingTemplateChanged': + if ( + message.template === 'combinedHeatmap' || + message.template === 'claudeShareCard' || + message.template === 'claudeHeatmap' + ) { + if (this.sharingTemplate !== message.template) { + this.sharingTemplate = message.template; + this.updateWebview(); + } + } break; case 'previewCombinedHeatmap': { if (!this.panel) { @@ -2213,51 +2263,31 @@ export class UsageWebviewProvider { } break; } - case 'resetCombinedHeatmapPreferences': { - try { - if (this.onResetSharingPreferences) { - await this.onResetSharingPreferences(); - } else { - await this.context.globalState.update('ccu.heatmapRepo', undefined); - await this.context.globalState.update('ccu.heatmapPath', undefined); - this.clearSharingRuntimeState(); - } - this.panel?.webview.postMessage({ - command: 'combinedHeatmapPreferencesReset', - ok: true, - }); - } catch { - this.panel?.webview.postMessage({ - command: 'combinedHeatmapPreferencesReset', - ok: false, - }); - } - break; - } case 'buildShareCard': { - // On-demand preview: build the SVG from the panel's config and send - // it back for injection (no full re-render). + // Preview is a pure local render using only the indexed usage data. if (this.panel && this.allRecords && this.allRecords.length > 0) { try { - const id = message.avatar || message.username ? await this.getGithubIdentity() : {}; const range = String(message.range || 'last30'); const scope = String(message.scope || 'all'); const sections = (message.sections || {}) as Record; const theme = (message.theme as ShareCardTheme) || 'claudeClassic'; const svg = this.buildShareCardSvgFor(range, scope, sections as Partial, { - avatarDataUri: message.avatar ? id.avatar : undefined, - username: message.username ? id.name : undefined, fullNumbers: !!message.fullNumbers, theme, }); // Remember it so a re-render restores the preview + picks. - this.lastShareCardSvg = svg; + this.shareCardPreviewCache = { + records: this.allRecords, + day: dayKeyInZone(new Date(), I18n.getTimezone()), + timeZone: I18n.getTimezone(), + locale: I18n.getLocale(), + themeKind: vscode.window.activeColorTheme?.kind, + svg, + }; this.lastShareCardConfig = { range, scope, sections, - avatar: !!message.avatar, - username: !!message.username, fullNumbers: !!message.fullNumbers, theme, }; @@ -2269,16 +2299,13 @@ export class UsageWebviewProvider { break; } case 'exportShareCard': { - // Export using the panel's config (range / scope / sections). + // Local export uses only already-indexed usage and the chosen config. if (!this.allRecords || this.allRecords.length === 0) { vscode.window.showWarningMessage(I18n.t.popup.noDataMessage); break; } const range = String(message.range || 'last30'); - const id = message.avatar || message.username ? await this.getGithubIdentity() : {}; const svg = this.buildShareCardSvgFor(range, String(message.scope || 'all'), (message.sections || {}) as Partial, { - avatarDataUri: message.avatar ? id.avatar : undefined, - username: message.username ? id.name : undefined, fullNumbers: !!message.fullNumbers, theme: (message.theme as ShareCardTheme) || 'claudeClassic', }); @@ -2413,6 +2440,12 @@ export class UsageWebviewProvider { case 'updateSetting': if (this.settings && typeof message.key === 'string') { await this.settings.set(message.key, message.value); + if ( + message.key === 'enableShareCard' && + this.settings.get('enableShareCard') === false + ) { + this.sharingWorkspaceRequested = false; + } // Re-apply config. Pass the key so status-bar-only toggles skip the // dashboard reload (which flickers); globalState changes don't fire // onDidChangeConfiguration, so this callback is the only signal. @@ -2480,7 +2513,13 @@ export class UsageWebviewProvider { const requestedProvider = message.provider === 'codex' || message.provider === 'claude' ? message.provider : ''; - if (!/^\d{4}-\d{2}$/.test(monthString) || !this.panel || requestedProvider !== this.currentProvider) { + // Claude's materialized all-time month rows use YYYY-MM-01, while + // Codex rows use YYYY-MM. Keep the response key unchanged so the + // matching detail row can consume it after the host round-trip. + const validMonth = requestedProvider === 'claude' + ? /^\d{4}-\d{2}(?:-01)?$/.test(monthString) + : /^\d{4}-\d{2}$/.test(monthString); + if (!validMonth || !this.panel || requestedProvider !== this.currentProvider) { break; } if (requestedProvider === 'codex') { @@ -2495,8 +2534,10 @@ export class UsageWebviewProvider { }); break; } - const { ClaudeDataLoader } = await import('./dataLoader'); - const dailyData = ClaudeDataLoader.getDailyDataForSpecificMonth(this.allRecords, monthString); + const monthKey = monthString.slice(0, 7); + const dailyData = this.dailyDataForEveryDay + .filter((row) => row.date.startsWith(`${monthKey}-`)) + .sort((left, right) => left.date.localeCompare(right.date)); await this.panel.webview.postMessage({ command: 'dailyDataResponse', provider: 'claude', @@ -2511,6 +2552,23 @@ export class UsageWebviewProvider { this.updateWebview(); } + /** Compatibility commands enter the same preview-first workspace instead of + * bypassing it with an immediate picker or network flow. */ + public showSharingWorkspace(template: SharingTemplate): void { + if (!this.allRecords || this.allRecords.length === 0) { + void vscode.window.showWarningMessage(I18n.t.popup.noDataMessage); + return; + } + this.sharingTemplate = template; + this.sharingWorkspaceRequested = true; + this.sharingCommandIntents.issue(template); + this.currentTab = 'all'; + this.currentProvider = this.providerAvailability.claude && this.providerAvailability.codexData + ? 'compare' + : 'claude'; + this.show(); + } + updateData( sessionData: SessionData | null, todayData: UsageData | null, @@ -2533,6 +2591,7 @@ export class UsageWebviewProvider { { hour: string; data: UsageData }[] > = {}, projectUsageMatrix: ProjectUsageMatrixSnapshot | null = null, + dailyDataForEveryDay: { date: string; data: UsageData }[] = [], ): void { this.currentSessionData = sessionData; this.todayData = todayData; @@ -2540,12 +2599,19 @@ export class UsageWebviewProvider { this.allTimeData = allTimeData; this.dailyDataForRolling30Days = dailyDataForRolling30Days; this.dailyDataForAllTime = dailyDataForAllTime; + if (this.dailyDataForEveryDay !== dailyDataForEveryDay) { + this.invalidateSharingCaches(); + } + this.dailyDataForEveryDay = dailyDataForEveryDay; this.hourlyDataForToday = hourlyDataForToday; this.hourlyDataForRolling30DaysByDay = hourlyDataForRolling30DaysByDay; this.error = error || null; this.dataDirectory = dataDirectory || null; this.isLoading = false; if (allRecords) { + if (this.allRecords !== allRecords) { + this.invalidateSharingCaches(); + } this.allRecords = allRecords; } this.sessionBreakdown = sessionBreakdown; @@ -2977,7 +3043,7 @@ export class UsageWebviewProvider { if (!endDateISO) { throw new Error('Could not resolve the configured timezone date.'); } - const claudeDaily = ClaudeDataLoader.getDailyUsageMap(this.allRecords ?? [], timeZone); + const claudeDaily = this.getClaudeDailyUsageMap(timeZone); const daily = mergeCombinedDailyUsage( claudeDailyPointsFromUsage(claudeDaily), codexDailyPointsFromUsage(this.codexView?.daily ?? []), @@ -2994,6 +3060,7 @@ export class UsageWebviewProvider { range, endDateISO, title, + locale: I18n.getLocale(), palette, customAccent, intensityMode, @@ -3007,12 +3074,25 @@ export class UsageWebviewProvider { }; } - private renderCombinedHeatmapPanel(): string { + private renderSharingWorkspace(): string { const copy = combinedHeatmapUiCopy(I18n.getLocale()); - const artifact = this.buildCombinedHeatmapArtifact('year', copy.defaultTitle); - const preview = artifact.hasData - ? artifact.svg - : '

' + this.escapeHtml(copy.noData) + '

'; + const workspaceCopy = I18n.sharingWorkspace; + const combinedAvailable = this.providerAvailability.claude && this.providerAvailability.codexData; + const selected: SharingTemplate = this.sharingTemplate === 'combinedHeatmap' && !combinedAvailable + ? 'claudeShareCard' + : this.sharingTemplate; + this.sharingTemplate = selected; + const selectedOption = (template: SharingTemplate): string => template === selected ? ' selected' : ''; + const presentationState = (template: SharingTemplate): string => + ' data-sharing-presentation="' + template + '"' + (template === selected ? '' : ' hidden'); + const artifact = selected === 'combinedHeatmap' + ? this.buildCombinedHeatmapArtifact('year', copy.defaultTitle) + : undefined; + const preview = artifact + ? (artifact.hasData + ? artifact.svg + : '

' + this.escapeHtml(copy.noData) + '

') + : ''; const paletteChoice = ( value: string, label: string, @@ -3030,12 +3110,12 @@ export class UsageWebviewProvider { paletteChoice('codexBlue', copy.codexBlue, ['#eff6ff', '#93c5fd', '#3b82f6', '#1d4ed8']) + paletteChoice('githubGreen', copy.githubGreen, ['#dafbe1', '#6fdd8b', '#2da44e', '#116329']) + paletteChoice('custom', copy.customPalette, ['#eee8f8', '#bca5e6', '#8668c7', '#4f2f87']); - return '
' + - '
' + - '' + this.escapeHtml(copy.eyebrow) + '' + - '

' + this.escapeHtml(copy.panelTitle) + '

' + - '

' + this.escapeHtml(copy.description) + '

' + - '◆ ' + this.escapeHtml(copy.privateBadge) + '
' + + const combinedPresentation = artifact ? ''; + '
' : this.renderSharingPresentationPlaceholder( + 'combinedHeatmap', + workspaceCopy.combinedPresentation, + workspaceCopy.combinedPresentationDescription, + ); + + const combinedOption = ''; + + return ''; + } + + private renderSharingPresentationPlaceholder( + template: SharingTemplate, + title: string, + description: string, + ): string { + return ''; } private renderCodexCompare(): string { @@ -3107,8 +3240,8 @@ export class UsageWebviewProvider { '' + this.escapeHtml(copy.cachedInput) + '' + I18n.formatNumber(cache) + '' + '' + this.escapeHtml(copy.output) + '' + I18n.formatNumber(output) + '' + ''; - const sharingWorkspace = this.setting('enableShareCard', true) - ? this.renderCombinedHeatmapPanel() + const sharingWorkspace = (this.setting('enableShareCard', true) || this.sharingWorkspaceRequested) + ? this.renderSharingWorkspace() : ''; const summaryGrid = '
' + card( @@ -3317,7 +3450,9 @@ export class UsageWebviewProvider { private renderSettingsPanel(provider: SettingProvider): string { const t = I18n.t.popup; const snap: SettingView[] = this.settings ? this.settings.snapshot() : []; - const visible = snap.filter((setting) => settingAppliesToProvider(setting, provider)); + const visible = snap.filter((setting) => + setting.visible !== false && settingAppliesToProvider(setting, provider), + ); const groups: { key: string; label: string }[] = [ { key: 'general', label: t.settingsGroupGeneral }, { key: 'providers', label: t.settingsGroupProviders }, @@ -4557,21 +4692,13 @@ export class UsageWebviewProvider { const allTimeSummary = this.renderUsageData(this.allTimeData, provider); - // Optional GitHub-style token heatmap (off by default; mainly a shareable - // view). Inline SVG renders with working hover tooltips inside the webview. - let heatmapPanel = ''; - if (this.setting('showHeatmap', false) && this.allRecords && this.allRecords.length > 0) { - const daily = ClaudeDataLoader.getDailyUsageMap(this.allRecords, I18n.getTimezone()); - heatmapPanel = - '

Token heatmap

' + - '
' + renderHeatmapSvg(daily, { - endDateISO: dayKeyInZone(new Date(), I18n.getTimezone()), - }) + '
' + - '
'; - } + // With both providers available, sharing has one home in Compare. A + // Claude-only installation receives the exact same workspace here. + const sharingWorkspace = + !this.providerAvailability.codexData && + (this.setting('enableShareCard', true) || this.sharingWorkspaceRequested) + ? this.renderSharingWorkspace() + : ''; const dailyBreakdown = this.dailyDataForAllTime.length > 0 @@ -4656,9 +4783,8 @@ export class UsageWebviewProvider { ` : ''; - // Heatmap sits right above the "monthly usage" breakdown, below the totals. return allTimeSummary + this.renderWeeklyValuePanel(provider) + - this.renderShareCardPanel() + heatmapPanel + dailyBreakdown; + sharingWorkspace + dailyBreakdown; } private weeklyValuePoints(provider: SettingProvider): WeeklyValuePoint[] { @@ -4834,18 +4960,12 @@ export class UsageWebviewProvider { tableRows + '
'; } - /** The "Share card" panel (All tab). On by default (`enableShareCard`); when - * enabled, a config form — range / scope / which metrics — with a Generate button. - * The preview is built on demand (no per-keystroke re-render), and export uses - * the same config. Privacy-safe: built from buildShareCardData, aggregate only. */ - private renderShareCardPanel(): string { + /** Claude's legacy Share Card remains a provider-truthful presentation inside + * the one sharing workspace. Its complete preview precedes every control. */ + private renderShareCardPanel(selected: SharingTemplate): string { const esc = (s: string): string => this.escapeHtml(s); - if (!this.setting('enableShareCard', true)) { - return ''; - } - if (!this.allRecords || this.allRecords.length === 0) { - return ''; - } + const copy = I18n.sharingWorkspace; + const combinedCopy = combinedHeatmapUiCopy(I18n.getLocale()); // Range: grouped like the scope dropdown — rolling presets vs. a specific // calendar month (last 12), so the two kinds are visually distinct. @@ -4854,8 +4974,8 @@ export class UsageWebviewProvider { for (let i = 1; i <= 12; i++) { const d = new Date(now.getFullYear(), now.getMonth() - i, 1); const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`; - const label = d.toLocaleDateString('en-US', { month: 'long', year: 'numeric' }); - monthOpts.push(``); + const label = d.toLocaleDateString(I18n.getLocale(), { month: 'long', year: 'numeric' }); + monthOpts.push(``); } // Rehydrate the last config so an auto-refresh re-render doesn't wipe the // user's picks or their generated preview (Carl: refresh shouldn't blow away @@ -4868,14 +4988,14 @@ export class UsageWebviewProvider { const rangeSelect = ''; @@ -4892,9 +5012,9 @@ export class UsageWebviewProvider { .join(''); const scopeSelect = ''; // Section toggles. Default ON: cost, cache-hit, model (+ the token hero, @@ -4903,57 +5023,156 @@ export class UsageWebviewProvider { ''; const toggles = - check('totalTokens', 'Total tokens', true) + - check('estimatedCost', 'Est. cost', true) + - check('cacheEfficiency', 'Cache-hit rate', true) + - check('topModel', 'Top model', true) + - check('sessions', 'Session count', true) + - check('tokenComposition', 'Token mix', true) + - check('rhythm', 'Daily pulse', true) + - check('badge', 'Badge', true) + - check('messages', 'Message count', false) + - check('projectName', 'Project name', false); - const avatarChecked = cfg?.avatar ? ' checked' : ''; - const nameChecked = cfg?.username ? ' checked' : ''; + check('totalTokens', copy.totalTokens, true) + + check('estimatedCost', copy.estimatedCost, true) + + check('cacheEfficiency', copy.cacheHitRate, true) + + check('topModel', copy.topModel, true) + + check('sessions', copy.sessionCount, true) + + check('tokenComposition', copy.tokenMix, true) + + check('rhythm', copy.dailyPulse, true) + + check('badge', copy.badge, true) + + check('messages', copy.messageCount, false) + + check('projectName', copy.projectName, false); const fullChecked = cfg?.fullNumbers ? ' checked' : ''; const curTheme = cfg?.theme || 'claudeClassic'; const themeSelect = ''; - const preview = this.lastShareCardSvg - ? this.lastShareCardSvg - : '

Preview appears here after you click Generate.

'; + const sections: Record = { + totalTokens: secOn('totalTokens', true), + estimatedCost: secOn('estimatedCost', true), + cacheEfficiency: secOn('cacheEfficiency', true), + topModel: secOn('topModel', true), + sessions: secOn('sessions', true), + tokenComposition: secOn('tokenComposition', true), + rhythm: secOn('rhythm', true), + badge: secOn('badge', true), + messages: secOn('messages', false), + projectName: secOn('projectName', false), + }; + const day = dayKeyInZone(now, I18n.getTimezone()); + const cachedPreview = this.shareCardPreviewCache; + let preview = cachedPreview?.records === this.allRecords && + cachedPreview.day === day && cachedPreview.timeZone === I18n.getTimezone() && + cachedPreview.locale === I18n.getLocale() && + cachedPreview.themeKind === vscode.window.activeColorTheme?.kind + ? cachedPreview.svg : undefined; + if (!preview) { + try { + preview = this.buildShareCardSvgFor(curRange, curScope, sections, { + fullNumbers: cfg?.fullNumbers, + theme: curTheme as ShareCardTheme, + }); + this.shareCardPreviewCache = { + records: this.allRecords, + day, + timeZone: I18n.getTimezone(), + locale: I18n.getLocale(), + themeKind: vscode.window.activeColorTheme?.kind, + svg: preview, + }; + } catch { + preview = '

' + esc(copy.previewPending) + '

'; + } + } return ( - '' ); } - /** Build the share-card SVG for a webview request ({range, scope, sections, - * size, avatar}). Shared by the preview (postMessage back) and export paths. */ + private renderClaudeHeatmapPresentation(selected: SharingTemplate): string { + const copy = I18n.sharingWorkspace; + const combinedCopy = combinedHeatmapUiCopy(I18n.getLocale()); + const day = dayKeyInZone(new Date(), I18n.getTimezone()); + const cachedPreview = this.claudeHeatmapPreviewCache; + let svg = cachedPreview?.dailyRows === this.dailyDataForEveryDay && + cachedPreview.day === day && cachedPreview.timeZone === I18n.getTimezone() && + cachedPreview.locale === I18n.getLocale() + ? cachedPreview.svg : undefined; + if (!svg) { + const daily = this.getClaudeDailyUsageMap(I18n.getTimezone()); + svg = renderHeatmapSvg(daily, { + endDateISO: dayKeyInZone(new Date(), I18n.getTimezone()), + locale: I18n.getLocale(), + }); + this.claudeHeatmapPreviewCache = { + dailyRows: this.dailyDataForEveryDay, + day, + timeZone: I18n.getTimezone(), + locale: I18n.getLocale(), + svg, + }; + } + return ''; + } + + /** The Compare and Claude heatmaps consume the same per-day projection. + * Share it across Webview renders; a changed record array is invalidated by + * updateData, and the timezone remains part of the cache identity. */ + private getClaudeDailyUsageMap(timeZone: string): ReturnType { + const cached = this.claudeDailyUsageCache; + if (cached?.dailyRows === this.dailyDataForEveryDay && cached.timeZone === timeZone) { + return cached.daily; + } + const daily = Object.fromEntries(this.dailyDataForEveryDay.map(({ date, data }) => [ + date, + { + tokens: data.totalInputTokens + data.totalOutputTokens + + data.totalCacheCreationTokens + data.totalCacheReadTokens, + cost: data.totalCost, + // The unified sharing surfaces currently render token activity only. + // Do not invent a distinct-session count from message aggregates. + sessions: 0, + }, + ])); + this.claudeDailyUsageCache = { + dailyRows: this.dailyDataForEveryDay, + timeZone, + daily, + }; + return daily; + } + + /** Build the share-card SVG from already-indexed local usage. Shared by the + * preview (postMessage back) and local export paths. */ private buildShareCardSvgFor( range: string, scope: string, @@ -4973,75 +5192,6 @@ export class UsageWebviewProvider { }); } - /** Fetch the signed-in GitHub user's avatar (data: URI) and display name, - * cached. Only called when the user ticks "GitHub avatar" / "GitHub name", so - * auth is on demand (just `read:user`). Missing parts come back undefined. */ - private async getGithubIdentity(): Promise<{ avatar?: string; name?: string }> { - if (this.githubIdentity) { - return this.githubIdentity; - } - let session: vscode.AuthenticationSession | undefined; - try { - session = await vscode.authentication.getSession('github', ['read:user'], { createIfNone: true }); - } catch { - return {}; - } - if (!session) { - return {}; - } - const out: { avatar?: string; name?: string } = {}; - try { - const user = await this.httpsGet('https://api.github.com/user', session.accessToken); - const parsed = JSON.parse(user.body.toString('utf8')) as { avatar_url?: string; name?: string; login?: string }; - out.name = (parsed.name && parsed.name.trim()) || parsed.login; - if (parsed.avatar_url) { - const url = parsed.avatar_url + (parsed.avatar_url.includes('?') ? '&' : '?') + 's=160'; - const img = await this.httpsGet(url); - out.avatar = `data:${img.contentType || 'image/png'};base64,${img.body.toString('base64')}`; - } - } catch { - /* keep whatever we got */ - } - this.githubIdentity = out; - return out; - } - - /** Minimal GET over https returning the raw body + content-type. Follows - * redirects (GitHub avatar URLs 302 to avatars.githubusercontent.com — not - * following them was why the avatar came back empty / broken). */ - private httpsGet(url: string, token?: string, hops = 0): Promise<{ body: Buffer; contentType: string }> { - return new Promise((resolve, reject) => { - const req = https.get( - url, - { - headers: { - 'User-Agent': 'ClaudeCodeUsage-VSCode', - ...(token ? { Authorization: `Bearer ${token}`, Accept: 'application/vnd.github+json' } : {}), - }, - timeout: 15000, - }, - (res) => { - const status = res.statusCode || 0; - const loc = res.headers.location; - if (status >= 300 && status < 400 && loc && hops < 4) { - res.resume(); // drain - // Don't forward the auth token to a redirected (CDN) host. - const next = new URL(loc, url).toString(); - this.httpsGet(next, undefined, hops + 1).then(resolve, reject); - return; - } - const chunks: Buffer[] = []; - res.on('data', (c) => chunks.push(c as Buffer)); - res.on('end', () => - resolve({ body: Buffer.concat(chunks), contentType: String(res.headers['content-type'] || '') }) - ); - } - ); - req.on('error', reject); - req.on('timeout', () => req.destroy(new Error('timeout'))); - }); - } - private renderSessionData(provider: SettingProvider = 'claude'): string { if (provider === 'codex') { const copy = I18n.t.providers.codex; @@ -9665,6 +9815,12 @@ export class UsageWebviewProvider { gap: 8px 10px; margin-bottom: 14px; padding-top: 12px; + padding-right: 0; + padding-bottom: 0; + padding-left: 0; + border-right: 0; + border-bottom: 0; + border-left: 0; border-top: 1px solid var(--vscode-panel-border); } .sc-check { @@ -9688,20 +9844,17 @@ export class UsageWebviewProvider { border: 1px solid var(--vscode-panel-border); border-radius: 10px; overflow: auto; + overscroll-behavior-inline: contain; + -webkit-overflow-scrolling: touch; max-height: 640px; - background: - linear-gradient(45deg, #f3f3f3 25%, transparent 25%, transparent 75%, #f3f3f3 75%), - linear-gradient(45deg, #f3f3f3 25%, #fff 25%, #fff 75%, #f3f3f3 75%); - background-size: 20px 20px; - background-position: 0 0, 10px 10px; + background: var(--vscode-editor-background); } .share-preview svg { display: block; width: 100%; height: auto; - max-width: 560px; + max-width: none; margin: 0 auto; - box-shadow: 0 2px 12px rgba(0,0,0,0.18); border-radius: 8px; } .share-preview p { @@ -9724,7 +9877,7 @@ export class UsageWebviewProvider { border: 1px solid var(--vscode-panel-border); border-radius: 6px; padding: 6px; - background: #ffffff; + background: var(--vscode-editor-background); } .heatmap-svg svg { display: block; @@ -9734,7 +9887,7 @@ export class UsageWebviewProvider { overflow: hidden; padding: var(--ccu-space-5); border: 1px solid var(--ccu-border); - border-left: 4px solid var(--vscode-charts-purple, #4f2f87); + border-left: 4px solid var(--vscode-charts-purple); border-radius: var(--ccu-radius-lg); background: var(--ccu-surface-raised); } @@ -9755,7 +9908,7 @@ export class UsageWebviewProvider { line-height: 1.5; } .combined-share-eyebrow { - color: var(--vscode-charts-purple, #8668c7); + color: var(--vscode-charts-purple); font-size: 10px; font-weight: 700; letter-spacing: 0.12em; @@ -9777,6 +9930,39 @@ export class UsageWebviewProvider { gap: var(--ccu-space-4); align-items: start; } + .sharing-template-field { + max-width: 520px; + margin: 0 0 var(--ccu-space-5); + } + .sharing-template-field select { + min-height: 34px; + font-family: var(--vscode-font-family); + } + .sharing-preview-stage, + .sharing-presentation { + min-width: 0; + } + .sharing-presentation[hidden] { + display: none; + } + .sharing-presentation-title { + margin: 0 0 var(--ccu-space-1); + font-size: 15px; + } + .sharing-presentation-description { + margin: 0 0 var(--ccu-space-3); + color: var(--vscode-descriptionForeground); + line-height: 1.45; + } + .sharing-controls-panel { + box-sizing: border-box; + width: 100%; + margin-top: var(--ccu-space-4); + } + .sharing-artifact-preview { + box-sizing: border-box; + width: 100%; + } .combined-preview-column, .combined-config-card { min-width: 0; @@ -9800,11 +9986,12 @@ export class UsageWebviewProvider { border-radius: var(--ccu-radius-panel); background: var(--ccu-surface-subtle); } - .combined-config-card h3 { + .combined-config-card h4, + .sc-config h4 { margin: 0 0 var(--ccu-space-3); font-size: 13px; } - .combined-config-card .sc-field > span { + .sharing-controls-panel .sc-field > span { color: var(--vscode-foreground); } .combined-heatmap-controls { @@ -9906,7 +10093,7 @@ export class UsageWebviewProvider { } .combined-privacy-preview { padding: var(--ccu-space-2) var(--ccu-space-3); - border-left: 3px solid var(--vscode-testing-iconPassed, #2ea043); + border-left: 3px solid var(--vscode-testing-iconPassed); border-radius: var(--ccu-radius-control); background: var(--ccu-surface-muted); color: var(--vscode-foreground); @@ -9963,7 +10150,10 @@ export class UsageWebviewProvider { max-width: none; height: auto; border-radius: 12px; - box-shadow: 0 8px 26px rgba(24, 16, 36, 0.18); + } + .claude-heatmap-actions { + padding-top: var(--ccu-space-3); + border-top: 1px solid var(--ccu-border); } /* Clickable month bars in the all-time composition chart (drill to daily). */ @@ -10792,6 +10982,10 @@ export class UsageWebviewProvider { width: auto; max-width: none; } + .claude-share-card-presentation .share-preview svg { + width: 1200px; + min-width: 1200px; + } .action-card-head { align-items: flex-start; flex-wrap: wrap; @@ -11158,8 +11352,20 @@ document.addEventListener('toggle', function(e){ }, true); // Restore the active tab from localStorage before first paint, so a reload can't change it. +function ccuSharingCommandPending() { + return !!__sharingTemplateCommand; +} function restoreActiveTab() { try { + if ( + ccuSharingCommandPending() && + document.getElementById('tab-all') && + document.getElementById('all') + ) { + showTab('all', true); + try { localStorage.setItem('ccu.activeTab', 'all'); } catch (e) {} + return; + } var t = localStorage.getItem('ccu.activeTab'); if (t && document.getElementById('tab-' + t) && document.getElementById(t)) { showTab(t, true); @@ -11181,7 +11387,9 @@ function restoreUi() { initializeHourlyOverviewSelections(); restoreHourlyOverviewSelections(); initializeStatusRegions(); + restoreSharingTemplate(); restoreCombinedHeatmapConfig(); + restoreShareCardDraft(); restoreProjectMatrixState(); requestLocalDataInventoryForVisibleSettings(); restoreScrollPosition(); @@ -11256,6 +11464,8 @@ const __currencyDisplay = ${JSON.stringify({ decimalPlaces: I18n.getDecimalPlaces(), })}; const __combinedHeatmapCopy = ${JSON.stringify(combinedHeatmapUiCopy(I18n.getLocale()))}; +const __sharingWorkspaceCopy = ${JSON.stringify(I18n.sharingWorkspace)}; +const __sharingTemplateCommand = ${JSON.stringify(this.sharingCommandIntents.pending() ?? null)}; const __localDataCopy = ${JSON.stringify(localDataUiCopy(I18n.getLocale()))}; const __dateOpts = (extra) => { const opts = Object.assign({}, extra || {}); @@ -11323,6 +11533,56 @@ function showProvider(provider, tab) { ${getProviderNavClientScript()} +function selectSharingTemplate(template, save) { + if (['combinedHeatmap', 'claudeShareCard', 'claudeHeatmap'].indexOf(template) < 0) { return; } + var selector = document.getElementById('sharingTemplate'); + var option = selector ? selector.querySelector('option[value="' + template + '"]') : null; + if (!selector || !option || option.disabled) { return; } + selector.value = template; + document.querySelectorAll('[data-sharing-presentation]').forEach(function(presentation) { + presentation.hidden = presentation.getAttribute('data-sharing-presentation') !== template; + }); + if (save) { + try { localStorage.setItem('ccu.sharing.template', template); } catch (e) {} + vscode.postMessage({ command: 'sharingTemplateChanged', template: template }); + } +} +function restoreSharingTemplate() { + var selector = document.getElementById('sharingTemplate'); + if (!selector) { return; } + var stored = ''; + var commandPending = ccuSharingCommandPending(); + try { + if (commandPending) { + stored = __sharingTemplateCommand.template; + localStorage.setItem('ccu.sharing.template', stored); + } else { + stored = localStorage.getItem('ccu.sharing.template') || ''; + } + } catch (e) { + if (commandPending) { stored = __sharingTemplateCommand.template; } + } + var option = stored ? selector.querySelector('option[value="' + stored + '"]') : null; + var initial = selector.value; + var resolved = option && !option.disabled ? stored : selector.value; + selectSharingTemplate(resolved, false); + if (!commandPending && resolved !== initial) { + vscode.postMessage({ command: 'sharingTemplateChanged', template: resolved }); + } + if (commandPending) { + vscode.postMessage({ + command: 'sharingTemplateCommandAck', + revision: __sharingTemplateCommand.revision + }); + } +} +function selectDefaultSharingTemplate() { + var selector = document.getElementById('sharingTemplate'); + if (!selector) { return; } + var combined = selector.querySelector('option[value="combinedHeatmap"]'); + selectSharingTemplate(combined && !combined.disabled ? 'combinedHeatmap' : 'claudeShareCard', false); +} + function scReadConfig() { var rangeEl = document.getElementById('scRange'); var scopeEl = document.getElementById('scScope'); @@ -11331,29 +11591,98 @@ function scReadConfig() { for (var i = 0; i < boxes.length; i++) { sections[boxes[i].getAttribute('data-sec')] = boxes[i].checked; } - var avatarEl = document.getElementById('scAvatar'); - var nameEl = document.getElementById('scName'); var fullEl = document.getElementById('scFull'); var themeEl = document.getElementById('scTheme'); return { range: rangeEl ? rangeEl.value : 'last30', scope: scopeEl ? scopeEl.value : 'all', theme: themeEl ? themeEl.value : 'claudeCream', - avatar: avatarEl ? avatarEl.checked : false, - username: nameEl ? nameEl.checked : false, fullNumbers: fullEl ? fullEl.checked : false, sections: sections }; } +// Project paths and session IDs are valid in-memory selector values, but they +// must not become durable UI preferences. Keep the full draft only for the +// lifetime of this Webview and persist the non-identifying controls. +var __ccuShareCardDraft = null; +function scPersistentDraftConfig(cfg) { + return { + range: cfg && cfg.range, + scope: 'all', + theme: cfg && cfg.theme, + fullNumbers: !!(cfg && cfg.fullNumbers), + sections: cfg && cfg.sections + }; +} +function scWritePersistentDraft(cfg) { + try { + localStorage.setItem( + 'ccu.sharing.shareCardDraft', + JSON.stringify(scPersistentDraftConfig(cfg)) + ); + } catch (e) {} +} +function scSaveDraft() { + __ccuShareCardDraft = scReadConfig(); + scWritePersistentDraft(__ccuShareCardDraft); +} +function scSetSelectValue(id, value) { + var select = document.getElementById(id); + if (!select || typeof value !== 'string') { return; } + for (var i = 0; i < select.options.length; i++) { + if (select.options[i].value === value) { select.value = value; return; } + } +} +function restoreShareCardDraft() { + var controls = document.querySelector('.claude-share-card-presentation .sc-config'); + if (!controls) { return; } + controls.addEventListener('change', scSaveDraft); + try { + var cfg = __ccuShareCardDraft; + if (!cfg) { + var raw = localStorage.getItem('ccu.sharing.shareCardDraft'); + if (!raw || raw.length > 4096) { return; } + cfg = JSON.parse(raw); + // Migrate older drafts that persisted a raw project path or session ID. + __ccuShareCardDraft = cfg; + scWritePersistentDraft(cfg); + } + if (!cfg || typeof cfg !== 'object' || Array.isArray(cfg)) { return; } + scSetSelectValue('scRange', cfg.range); + scSetSelectValue('scScope', cfg.scope); + scSetSelectValue('scTheme', cfg.theme); + var full = document.getElementById('scFull'); + if (full && typeof cfg.fullNumbers === 'boolean') { full.checked = cfg.fullNumbers; } + if (cfg.sections && typeof cfg.sections === 'object' && !Array.isArray(cfg.sections)) { + controls.querySelectorAll('.sc-sec').forEach(function(box) { + var key = box.getAttribute('data-sec'); + if (key && typeof cfg.sections[key] === 'boolean') { box.checked = cfg.sections[key]; } + }); + } + } catch (e) {} +} +function scApplyDefaultControls() { + scSetSelectValue('scRange', 'last30'); + scSetSelectValue('scScope', 'all'); + scSetSelectValue('scTheme', 'claudeClassic'); + var full = document.getElementById('scFull'); + if (full) { full.checked = false; } + var optional = { messages: true, projectName: true }; + document.querySelectorAll('.claude-share-card-presentation .sc-sec').forEach(function(box) { + box.checked = !optional[box.getAttribute('data-sec')]; + }); +} function generateShareCard() { var cfg = scReadConfig(); + scSaveDraft(); var prev = document.getElementById('scPreview'); - if (prev) { prev.innerHTML = '

Generating…

'; } - vscode.postMessage({ command: 'buildShareCard', range: cfg.range, scope: cfg.scope, theme: cfg.theme, avatar: cfg.avatar, username: cfg.username, fullNumbers: cfg.fullNumbers, sections: cfg.sections }); + if (prev) { prev.innerHTML = '

' + __sharingWorkspaceCopy.generating + '

'; } + vscode.postMessage({ command: 'buildShareCard', range: cfg.range, scope: cfg.scope, theme: cfg.theme, fullNumbers: cfg.fullNumbers, sections: cfg.sections }); } function exportShareCardConfigured() { var cfg = scReadConfig(); - vscode.postMessage({ command: 'exportShareCard', range: cfg.range, scope: cfg.scope, theme: cfg.theme, avatar: cfg.avatar, username: cfg.username, fullNumbers: cfg.fullNumbers, sections: cfg.sections }); + scSaveDraft(); + vscode.postMessage({ command: 'exportShareCard', range: cfg.range, scope: cfg.scope, theme: cfg.theme, fullNumbers: cfg.fullNumbers, sections: cfg.sections }); } function exportHeatmap() { vscode.postMessage({ command: 'exportHeatmap' }); @@ -11455,39 +11784,10 @@ function copyCombinedHeatmapMarkdown() { vscode.postMessage({ command: 'copyCombinedHeatmapMarkdown', title: config.title, range: config.range, intensityMode: config.intensityMode, palette: config.palette, customAccent: config.customAccent }); } function resetCombinedHeatmapPreferences() { - var title = document.getElementById('combinedHeatmapTitle'); - var range = document.getElementById('combinedHeatmapRange'); - var intensityMode = document.getElementById('combinedHeatmapIntensityMode'); - var privacy = document.getElementById('combinedHeatmapPrivacy'); - var customAccent = document.getElementById('combinedHeatmapCustomAccent'); - try { - localStorage.removeItem('ccu.combinedHeatmap.title'); - localStorage.removeItem('ccu.combinedHeatmap.range'); - localStorage.removeItem('ccu.combinedHeatmap.intensityMode'); - localStorage.removeItem('ccu.combinedHeatmap.privacyPreview'); - localStorage.removeItem('ccu.combinedHeatmap.palette'); - localStorage.removeItem('ccu.combinedHeatmap.customAccent'); - } catch (e) {} - if (title) { title.value = __combinedHeatmapCopy.defaultTitle; } - if (range) { range.value = 'year'; } - if (intensityMode) { intensityMode.value = 'quantile'; } - if (privacy) { privacy.checked = true; } - var defaultPalette = document.querySelector('input[name="combinedHeatmapPalette"][value="academicViolet"]'); - if (defaultPalette) { defaultPalette.checked = true; } - if (customAccent) { customAccent.value = '#4f2f87'; } - toggleCombinedCustomAccent(); - toggleCombinedHeatmapPrivacy(false); + __ccuDirectSharingResetPending = true; var status = document.getElementById('combinedHeatmapStatus'); if (status) { status.textContent = __combinedHeatmapCopy.resetSharing + '…'; } - vscode.postMessage({ - command: 'previewCombinedHeatmap', - title: __combinedHeatmapCopy.defaultTitle, - range: 'year', - intensityMode: 'quantile', - palette: 'academicViolet', - customAccent: '#4f2f87' - }); - vscode.postMessage({ command: 'resetCombinedHeatmapPreferences' }); + runLocalDataAction('reset-sharing-preferences'); } function refresh() { vscode.postMessage({ command: 'refresh' }); @@ -11549,6 +11849,8 @@ var __ccuUiPreferenceKeys = [ 'ccu.sessionModel' ]; var __ccuSharingPreferenceKeys = [ + 'ccu.sharing.template', + 'ccu.sharing.shareCardDraft', 'ccu.combinedHeatmap.title', 'ccu.combinedHeatmap.range', 'ccu.combinedHeatmap.intensityMode', @@ -11556,17 +11858,20 @@ var __ccuSharingPreferenceKeys = [ 'ccu.combinedHeatmap.palette', 'ccu.combinedHeatmap.customAccent' ]; - -function ccuCountPresentLocalStorageKeys(keys) { +function ccuCountPresentStorageKeys(storage, keys) { var count = 0; try { keys.forEach(function(key) { - if (localStorage.getItem(key) !== null) { count += 1; } + if (storage.getItem(key) !== null) { count += 1; } }); } catch (e) {} return count; } +function ccuCountPresentLocalStorageKeys(keys) { + try { return ccuCountPresentStorageKeys(localStorage, keys); } catch (e) { return 0; } +} + function ccuLocalDataClientSummary() { var state = ccuReadUiState(); return { @@ -11602,15 +11907,48 @@ function runLocalDataAction(action) { }); } -function ccuResetLocalStorageKeys(keys) { +function ccuResetStorageKeys(storage, keys) { try { - keys.forEach(function(key) { localStorage.removeItem(key); }); - return keys.every(function(key) { return localStorage.getItem(key) === null; }); + keys.forEach(function(key) { storage.removeItem(key); }); + return keys.every(function(key) { return storage.getItem(key) === null; }); } catch (e) { return false; } } +function ccuResetLocalStorageKeys(keys) { + try { return ccuResetStorageKeys(localStorage, keys); } catch (e) { return false; } +} + +var __ccuDirectSharingResetPending = false; + +function ccuApplyDefaultSharingControls() { + var title = document.getElementById('combinedHeatmapTitle'); + var range = document.getElementById('combinedHeatmapRange'); + var intensityMode = document.getElementById('combinedHeatmapIntensityMode'); + var privacy = document.getElementById('combinedHeatmapPrivacy'); + var customAccent = document.getElementById('combinedHeatmapCustomAccent'); + if (title) { title.value = __combinedHeatmapCopy.defaultTitle; } + if (range) { range.value = 'year'; } + if (intensityMode) { intensityMode.value = 'quantile'; } + if (privacy) { privacy.checked = true; } + var defaultPalette = document.querySelector('input[name="combinedHeatmapPalette"][value="academicViolet"]'); + if (defaultPalette) { defaultPalette.checked = true; } + if (customAccent) { customAccent.value = '#4f2f87'; } + selectDefaultSharingTemplate(); + scApplyDefaultControls(); + toggleCombinedCustomAccent(); + toggleCombinedHeatmapPrivacy(false); + vscode.postMessage({ + command: 'previewCombinedHeatmap', + title: __combinedHeatmapCopy.defaultTitle, + range: 'year', + intensityMode: 'quantile', + palette: 'academicViolet', + customAccent: '#4f2f87' + }); +} + function ccuApplyLocalDataClientAction(action) { var ok = true; if (action === 'reset-ui-state' || action === 'clear-all-client-state') { @@ -11624,8 +11962,17 @@ function ccuApplyLocalDataClientAction(action) { } } if (action === 'reset-sharing-preferences' || action === 'clear-all-client-state') { - ok = ccuResetLocalStorageKeys(__ccuSharingPreferenceKeys) && ok; - restoreCombinedHeatmapConfig(); + var sharingStorageReset = ccuResetLocalStorageKeys(__ccuSharingPreferenceKeys); + ok = sharingStorageReset && ok; + if (sharingStorageReset) { + ccuApplyDefaultSharingControls(); + var sharingMasterSwitch = document.getElementById('set_enableShareCard'); + if (sharingMasterSwitch && sharingMasterSwitch.type === 'checkbox') { + // Host-side reset has already restored this sole visible switch to its + // catalog default. Reflect that immediately without posting a new write. + sharingMasterSwitch.checked = true; + } + } } return ok; } @@ -12271,6 +12618,20 @@ function restoreClaudeDrilldownDetails() { toggleMonthlyDetail(date, true); } }); + restoreClaudeAllTimeHourlyDetails(document); + } catch (e) {} +} + +function restoreClaudeAllTimeHourlyDetails(root) { + try { + const scope = root || document; + const saved = ccuReadUiState().claudeDrilldownDetails || {}; + scope.querySelectorAll('[data-claude-alltime-hourly-detail-row]').forEach(function(row) { + const date = row.getAttribute('data-date'); + if (date && saved[claudeDrilldownStateKey(row, 'hourly')] === date) { + toggleClaudeAllTimeHourlyDetail(date, row, true); + } + }); } catch (e) {} } @@ -12349,6 +12710,63 @@ function toggleHourlyDetail(date, restoring) { } } +function claudeAllTimeHourlyElements(date, source) { + const sourceScope = source && source.closest + ? source.closest('[data-claude-alltime-daily]') + : null; + const searchScope = sourceScope || document.querySelector('#all [data-claude-alltime-daily]'); + if (!searchScope) { return {}; } + const detailRow = searchScope.querySelector( + '[data-claude-alltime-hourly-detail-row][data-date="' + date + '"]', + ); + return { + detailRow: detailRow, + button: searchScope.querySelector( + '[data-claude-alltime-hourly-toggle][data-date="' + date + '"]', + ), + container: detailRow + ? detailRow.querySelector('.hourly-detail-container[id]') + : null, + chartBar: searchScope.querySelector( + '.hc-col[data-date="' + date + '"] .chart-bar.clickable', + ), + }; +} + +function toggleClaudeAllTimeHourlyDetail(date, source, restoring) { + try { + const elements = claudeAllTimeHourlyElements(date, source); + const detailRow = elements.detailRow; + const button = elements.button; + const container = elements.container; + const chartBar = elements.chartBar; + if (!detailRow || !button || !container) { return; } + const isExpanded = detailRow.style.display !== 'none' && detailRow.style.display !== ''; + if (!isExpanded) { + closeAllHourlyDetails(); + detailRow.style.display = 'table-row'; + button.classList.add('expanded'); + setChartDrilldownExpanded('claude-alltime-hourly-detail-' + date, true); + if (chartBar) { chartBar.classList.add('selected'); } + persistClaudeDrilldown(detailRow, 'hourly', date); + if (!container.dataset.loaded) { + installMaterializedClaudeHourlyDetail(container, date); + } + if (!restoring) { + try { detailRow.scrollIntoView({ behavior: 'smooth', block: 'center' }); } catch (e) {} + } + } else { + detailRow.style.display = 'none'; + button.classList.remove('expanded'); + setChartDrilldownExpanded('claude-alltime-hourly-detail-' + date, false); + if (chartBar) { chartBar.classList.remove('selected'); } + persistClaudeDrilldown(detailRow, 'hourly', ''); + } + } catch (error) { + console.error('Error in toggleClaudeAllTimeHourlyDetail:', error); + } +} + function codexHourlyDetailElements(date, source) { const sourceScope = source && source.closest ? source.closest('[data-codex-alltime-daily], [data-codex-last30-daily]') @@ -12478,6 +12896,9 @@ function closeAllHourlyDetails() { document.querySelectorAll('[aria-controls^="hourly-detail-"]').forEach(function(control) { control.setAttribute('aria-expanded', 'false'); }); + document.querySelectorAll('[aria-controls^="claude-alltime-hourly-detail-"]').forEach(function(control) { + control.setAttribute('aria-expanded', 'false'); + }); } @@ -12749,7 +13170,7 @@ window.addEventListener('message', async function(event) { } if (message.command === 'dashboardDataPatch') { - ccuApplyDashboardDataPatch(message); + ccuQueueDashboardDataPatch(message); return; } @@ -12812,6 +13233,17 @@ window.addEventListener('message', async function(event) { ? localDataResult.message : __localDataCopy.unavailable; } + if (__ccuDirectSharingResetPending) { + var directSharingResetStatus = document.getElementById('combinedHeatmapStatus'); + if (directSharingResetStatus) { + directSharingResetStatus.textContent = typeof localDataResult.message === 'string' + ? localDataResult.message + : (localDataResult.ok === true + ? __combinedHeatmapCopy.resetComplete + : __combinedHeatmapCopy.resetFailed); + } + __ccuDirectSharingResetPending = false; + } requestLocalDataInventory(); } @@ -13019,7 +13451,7 @@ window.addEventListener('message', async function(event) { if (message.error) { var shareCardError = document.createElement('p'); shareCardError.className = 'table-hint'; - shareCardError.textContent = 'Could not build the card: ' + String(message.error); + shareCardError.textContent = __sharingWorkspaceCopy.cardBuildFailed + ' ' + String(message.error); prev.replaceChildren(shareCardError); } else { prev.innerHTML = message.svg || ''; @@ -13055,15 +13487,6 @@ window.addEventListener('message', async function(event) { } } - if (message.command === 'combinedHeatmapPreferencesReset') { - var resetStatus = document.getElementById('combinedHeatmapStatus'); - if (resetStatus) { - resetStatus.textContent = message.ok - ? __combinedHeatmapCopy.resetComplete - : __combinedHeatmapCopy.resetFailed; - } - } - if (message.command === 'dailyDataResponse') { const container = document.getElementById('monthly-detail-' + message.month); const responseProvider = message.provider === 'codex' ? 'codex' : 'claude'; @@ -13078,6 +13501,7 @@ window.addEventListener('message', async function(event) { restoreChartMetrics(container); initializeChartDrilldowns(container); restoreCodexHourlyDetails(); + restoreClaudeAllTimeHourlyDetails(container); initializeStatusRegions(container); } } @@ -13162,6 +13586,8 @@ function chartDrilldownInfo(element) { var containingTab = element && element.closest ? element.closest('.tab-content') : null; var kind = element.closest('[data-codex-alltime-daily]') ? 'codex-alltime-hourly' + : element.closest('[data-claude-alltime-daily]') + ? 'claude-alltime-hourly' : containingTab && containingTab.id === 'all' ? 'monthly' : element.closest('[data-codex-last30-daily]') @@ -13169,6 +13595,8 @@ function chartDrilldownInfo(element) { : 'hourly'; var prefix = kind === 'monthly' ? 'monthly-detail-' + : kind === 'claude-alltime-hourly' + ? 'claude-alltime-hourly-detail-' : kind === 'codex-alltime-hourly' ? 'codex-alltime-hourly-detail-' : kind === 'codex-hourly' @@ -13185,6 +13613,9 @@ function activateChartDrilldown(element) { var info = chartDrilldownInfo(element); if (!info) { return; } if (info.kind === 'monthly') { toggleMonthlyDetail(info.date); } + else if (info.kind === 'claude-alltime-hourly') { + toggleClaudeAllTimeHourlyDetail(info.date, element); + } else if (info.kind === 'codex-hourly' || info.kind === 'codex-alltime-hourly') { toggleCodexHourlyDetail(info.date, element); } @@ -13512,7 +13943,9 @@ function updateMainChart(metric, container) { // Hourly chart: tooltip shows the value only (the hour is on the x-axis). bar.title = valueWithHelp; } else if (date) { - bar.title = ccuFormatUsageDateKey(date) + ': ' + valueWithHelp; + // Claude's all-time month key includes its sentinel first day. The chart + // scope, not the presence of a day component, determines its label. + bar.title = ccuFormatUsageDateKey(date, null, !!bar.closest('#allTimeChart')) + ': ' + valueWithHelp; } // Drill-down bars keep their accessible name synchronized with the // currently selected metric; otherwise a keyboard user would continue to @@ -13731,7 +14164,12 @@ function renderDailyData(dailyData, monthDate) { return '
${I18n.t.popup.noDataMessage}
'; } - let html = '
'; + const expandableDays = new Set(dailyData + .filter(function(item) { + return Object.prototype.hasOwnProperty.call(__claudeLast30HoursByDay, item.date); + }) + .map(function(item) { return item.date; })); + let html = '
'; html += '

' + ccuFormatUsageDateKey(monthDate, null, true) + ' ${I18n.t.popup.dailyBreakdown}

'; html += '
'; @@ -13745,7 +14183,7 @@ function renderDailyData(dailyData, monthDate) { // hc-wrap is self-contained (own Y-axis + scroll); no chart-container. html += '
'; - html += renderDailyChart(dailyData, 'cost'); + html += renderDailyChart(dailyData, 'cost', expandableDays); html += '
'; // Per-day token composition for this month (the drill-down of the all-time @@ -13766,12 +14204,14 @@ function renderDailyData(dailyData, monthDate) { html += '${I18n.t.popup.cacheRead}'; html += '${I18n.t.popup.cacheHitRate}'; html += '${I18n.t.popup.messages}'; + html += ''; html += ''; dailyData.forEach(function(item) { const formattedDate = ccuFormatUsageDateKey(item.date, { month: 'numeric', day: 'numeric' }); - html += ''; + const canExpand = expandableDays.has(item.date); + html += ''; html += '' + formattedDate + ''; html += '' + formatValue(item.data.totalCost, 'cost') + ''; html += '' + item.data.totalInputTokens.toLocaleString(__locale) + ''; @@ -13780,7 +14220,24 @@ function renderDailyData(dailyData, monthDate) { html += '' + item.data.totalCacheReadTokens.toLocaleString(__locale) + ''; html += '' + cacheHitPct(item.data) + ''; html += '' + item.data.messageCount.toLocaleString(__locale) + ''; + html += ''; + if (canExpand) { + html += ''; + } + html += ''; html += ''; + if (canExpand) { + html += '' + + '
' + + '
${I18n.t.statusBar.loading}
'; + } }); html += '
'; @@ -13834,7 +14291,10 @@ function griddedChart(items, metric, opts) { inner = seg(cb.input, 'seg-input') + seg(cb.cacheRead, 'seg-cache-read') + seg(cb.cacheWrite, 'seg-cache-creation') + seg(cb.output, 'seg-output'); } - if (opts.clickable) { cls += ' clickable'; } + const clickable = typeof opts.clickable === 'function' + ? opts.clickable(it) + : opts.clickable; + if (clickable) { cls += ' clickable'; } const visibleValue = opts.hideZeroLabels && value === 0 ? '' @@ -13866,14 +14326,14 @@ function griddedChart(items, metric, opts) { '
'; } -function renderDailyChart(dailyData, metric) { +function renderDailyChart(dailyData, metric, expandableDays) { // Parse 'YYYY-MM-DD' textually: new Date('YYYY-MM-DD') is interpreted as // UTC midnight, so getMonth()/getDate() shift back a day in negative-UTC // timezones. String parts are timezone-proof. function parts(dateStr) { return dateStr.split('-').map(Number); } return griddedChart(dailyData, metric, { keyName: 'date', - clickable: false, + clickable: function(it) { return !!(expandableDays && expandableDays.has(it.date)); }, // Show month/day (not just the day number) so the axis isn't ambiguous. getLabel: function(it) { var p = parts(it.date); return p[1] + '/' + p[2]; }, getTitle: function(it, v) { diff --git a/tests/perf/measure-large-claude-index.mjs b/tests/perf/measure-large-claude-index.mjs new file mode 100644 index 00000000..925fcc0f --- /dev/null +++ b/tests/perf/measure-large-claude-index.mjs @@ -0,0 +1,195 @@ +// Opt-in local diagnostic for a large synthetic Claude transcript history. +// It never reads the user's Claude profile. The fixture is removed on exit. +// Run npm run compile first. For the retained-heap gate, use: +// node --expose-gc tests/perf/measure-large-claude-index.mjs --gib 2.4 --files 645 --analyze-content [--assistant-payload] +// Content analysis is off by default to isolate usage-index I/O. Use the +// opt-in flag to measure the more expensive default-on production path. +import assert from 'node:assert/strict'; +import { appendFile, mkdir, mkdtemp, rm, unlink, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { dirname, join, resolve } from 'node:path'; +import { createRequire } from 'node:module'; +import { fileURLToPath } from 'node:url'; + +const require = createRequire(import.meta.url); +const scriptDirectory = dirname(fileURLToPath(import.meta.url)); +const { createClaudeUsageIndex, updateClaudeUsageIndex } = require(resolve(scriptDirectory, '../../out/claudeIncrementalIndex.js')); + +function option(name, fallback) { + const at = process.argv.indexOf(name); + if (at < 0) return fallback; + const parsed = Number(process.argv[at + 1]); + if (!Number.isFinite(parsed)) throw new Error(`${name} needs a finite number`); + return parsed; +} + +const targetGiB = option('--gib', 2.4); +const fileCount = option('--files', 645); +const analyzeContent = process.argv.includes('--analyze-content'); +const assistantPayload = process.argv.includes('--assistant-payload'); +if (analyzeContent && typeof global.gc !== 'function') { + throw new Error('Content-analysis memory gate requires node --expose-gc'); +} +if (targetGiB <= 0 || targetGiB > 3 || !Number.isInteger(fileCount) || fileCount < 8 || fileCount > 1000) { + throw new Error('Expected 0 < --gib <= 3 and 8 <= --files <= 1000'); +} + +const timestamp = new Date().toISOString(); +const payload = 'synthetic-only-'.repeat(17_000); // ~255 KiB; safely below the 1 MiB line cap. +const fillerLine = `${JSON.stringify(assistantPayload ? { + type: 'assistant', timestamp, cwd: '/fixture/large-history', + message: { + role: 'assistant', model: 'claude-sonnet-4-5', content: [{ type: 'text', text: payload }], + usage: { input_tokens: 10, output_tokens: 1 }, + }, +} : { + type: 'user', timestamp, cwd: '/fixture/large-history', + message: { role: 'user', content: payload }, +})}\n`; +const fillerBytes = Buffer.byteLength(fillerLine); +const seedLine = (index) => `${JSON.stringify({ + type: 'assistant', timestamp, cwd: '/fixture/large-history', + requestId: `synthetic-request-${index}`, + message: { + id: `synthetic-message-${index}`, model: 'claude-sonnet-4-5', + content: [{ type: 'text', text: 'synthetic response' }], + usage: { input_tokens: 10, output_tokens: 1, cache_creation_input_tokens: 2, cache_read_input_tokens: 3 }, + }, +})}\n`; +const seedBytes = Buffer.byteLength(seedLine(0)); +const targetBytes = Math.ceil(targetGiB * 1024 ** 3); +const fillerCount = Math.ceil(Math.max(0, targetBytes - fileCount * seedBytes) / fillerBytes); +const root = await mkdtemp(join(tmpdir(), 'ccu-v240-large-synthetic-')); +const project = join(root, 'projects', '-fixture-large-history'); +const files = []; +let peakRss = process.memoryUsage().rss; +const sampler = setInterval(() => { + peakRss = Math.max(peakRss, process.memoryUsage().rss); +}, 50); + +async function measured(label, work) { + const start = performance.now(); + const result = await work(); + const memory = process.memoryUsage(); + return { + label, + elapsedMs: Math.round(performance.now() - start), + rssBytes: memory.rss, + heapUsedBytes: memory.heapUsed, + result, + }; +} + +try { + await mkdir(project, { recursive: true }); + let writtenBytes = 0; + const generated = await measured('generate', async () => { + for (let index = 0; index < fileCount; index += 1) { + const file = join(project, `session-${String(index).padStart(4, '0')}.jsonl`); + files.push(file); + const copies = Math.floor(fillerCount / fileCount) + (index < fillerCount % fileCount ? 1 : 0); + const body = seedLine(index) + fillerLine.repeat(copies); + writtenBytes += Buffer.byteLength(body); + await writeFile(file, body, 'utf8'); + if ((index + 1) % 100 === 0) process.stdout.write(`generated ${index + 1}/${fileCount} files\n`); + } + }); + assert.ok(writtenBytes >= targetBytes, 'fixture must meet the declared byte target'); + process.stdout.write(`fixture bytes=${writtenBytes} files=${fileCount} contentAnalysis=${analyzeContent} assistantPayload=${assistantPayload}\n`); + + const cold = await measured('cold', () => updateClaudeUsageIndex(createClaudeUsageIndex(), root, { + analyzeContent, + })); + assert.equal(cold.result.diagnostics.bodyReads, fileCount); + assert.ok(cold.result.records.length >= fileCount); + const coldRecords = cold.result.records.length; + const unchanged = await measured('unchanged', () => updateClaudeUsageIndex(cold.result.index, root, { + analyzeContent, + })); + assert.equal(unchanged.result.diagnostics.bodyReads, 0); + assert.equal(unchanged.result.diagnostics.bytesRead, 0); + + let appendedBytes = 0; + const tailFiles = fileCount >= 645 ? [7, 91, 183, 275, 367, 459, 644] : [0, 1, 2, 3, 4, 5, 6]; + for (const index of tailFiles) { + const line = seedLine(fileCount + index); + appendedBytes += Buffer.byteLength(line); + await appendFile(files[index], line, 'utf8'); + } + const newLine = seedLine(fileCount * 2); + await writeFile(join(project, 'session-new.jsonl'), newLine, 'utf8'); + const warm = await measured('seven-tails-plus-new', () => updateClaudeUsageIndex(unchanged.result.index, root, { + analyzeContent, + })); + assert.equal(warm.result.diagnostics.bodyReads, 8); + // The first-timestamp probe may read some/all of the new file before its + // one normal body scan; keep the bound without pinning that probe's size. + assert.ok(warm.result.diagnostics.bytesRead >= appendedBytes + Buffer.byteLength(newLine)); + assert.ok(warm.result.diagnostics.bytesRead <= appendedBytes + 2 * Buffer.byteLength(newLine)); + assert.equal(warm.result.records.length, coldRecords + 8); + + const replacementLine = seedLine(fileCount * 3); + await writeFile(files[0], replacementLine, 'utf8'); + const truncated = await measured('truncate-one-file', () => updateClaudeUsageIndex(warm.result.index, root, { + analyzeContent, + })); + // Content analysis currently falls back to a full ordered rebuild on an + // unsafe source mutation so cross-file UUID ownership stays correct. Do not + // lock that expensive implementation in as a desired future contract. + assert.ok(truncated.result.diagnostics.bodyReads >= 1 && + truncated.result.diagnostics.bodyReads <= fileCount + 1); + if (!analyzeContent) { + assert.equal(truncated.result.diagnostics.bodyReads, 1); + assert.deepEqual(truncated.result.diagnostics.changed, + { append: 0, rebuild: 1, move: 0, delete: 0 }); + assert.ok(truncated.result.diagnostics.bytesRead <= 4 * Buffer.byteLength(replacementLine), + 'usage-only truncation must not reread untouched large files'); + } + assert.ok(truncated.result.records.length <= warm.result.records.length); + + await unlink(files[1]); + const deleted = await measured('delete-one-file', () => updateClaudeUsageIndex(truncated.result.index, root, { + analyzeContent, + })); + assert.ok(deleted.result.diagnostics.bodyReads <= fileCount); + if (!analyzeContent) { + assert.equal(deleted.result.diagnostics.bodyReads, 0); + assert.equal(deleted.result.diagnostics.bytesRead, 0); + assert.deepEqual(deleted.result.diagnostics.changed, + { append: 0, rebuild: 0, move: 0, delete: 1 }); + } + assert.ok(deleted.result.records.length < truncated.result.records.length); + + const afterGc = typeof global.gc === 'function' + ? await measured('post-forced-gc', async () => { global.gc(); }) + : null; + if (analyzeContent && afterGc) { + assert.ok(afterGc.heapUsedBytes < Math.max(200 * 1024 ** 2, writtenBytes / 2), + 'retained heap must remain below the synthetic transcript memory ceiling'); + } + + process.stdout.write(`${JSON.stringify({ + fixtureBytes: writtenBytes, + files: fileCount, + coldRecords, + contentAnalysis: analyzeContent, + assistantPayload, + peakRssBytes: peakRss, + phases: [ + { label: generated.label, elapsedMs: generated.elapsedMs, + rssBytes: generated.rssBytes, heapUsedBytes: generated.heapUsedBytes }, + ...[cold, unchanged, warm, truncated, deleted].map(({ label, elapsedMs, rssBytes, heapUsedBytes, result }) => ({ + label, elapsedMs, rssBytes, heapUsedBytes, + bodyReads: result.diagnostics.bodyReads, + bytesRead: result.diagnostics.bytesRead, + linesParsed: result.diagnostics.linesParsed, + changed: result.diagnostics.changed, + })), + ...(afterGc ? [{ label: afterGc.label, elapsedMs: afterGc.elapsedMs, + rssBytes: afterGc.rssBytes, heapUsedBytes: afterGc.heapUsedBytes }] : []), + ], + })}\n`); +} finally { + clearInterval(sampler); + await rm(root, { recursive: true, force: true }); +} diff --git a/tests/perf/measure-large-codex-index.mjs b/tests/perf/measure-large-codex-index.mjs new file mode 100644 index 00000000..b2f2779c --- /dev/null +++ b/tests/perf/measure-large-codex-index.mjs @@ -0,0 +1,203 @@ +// Opt-in, synthetic-only Codex index diagnostic. No real Codex home is read. +// Run npm run compile first; for example: +// node --expose-gc tests/perf/measure-large-codex-index.mjs --gib 1.4 --files 645 --line-kib 16 +import assert from 'node:assert/strict'; +import { appendFile, mkdir, mkdtemp, rm, unlink, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { dirname, join, resolve } from 'node:path'; +import { createRequire } from 'node:module'; +import { fileURLToPath } from 'node:url'; + +const require = createRequire(import.meta.url); +const directory = dirname(fileURLToPath(import.meta.url)); +const { createEmptyCodexIndex, updateCodexIndex, + CODEX_REFRESH_BACKFILL_MAX_BYTES, + CODEX_REFRESH_BACKFILL_MAX_FILE_PASSES } = require(resolve(directory, '../../out/providers/codex/codexIndex.js')); +const { scanCodexManifest } = require(resolve(directory, '../../out/providers/codex/codexManifest.js')); +const { defaultCodexJsonlReader } = require(resolve(directory, '../../out/providers/codex/codexJsonlScanner.js')); + +function option(name, fallback) { + const position = process.argv.indexOf(name); + if (position < 0) return fallback; + const value = Number(process.argv[position + 1]); + if (!Number.isFinite(value)) throw new Error(`${name} requires a finite number`); + return value; +} + +const gib = option('--gib', 1.4); +const fileCount = option('--files', 645); +const lineKiB = option('--line-kib', 16); +if (gib <= 0 || gib > 3 || !Number.isInteger(fileCount) || fileCount < 8 || fileCount > 1000) { + throw new Error('Expected 0 < --gib <= 3 and 8 <= --files <= 1000'); +} +if (lineKiB < 1 || lineKiB > 256) { + throw new Error('Expected 1 <= --line-kib <= 256'); +} +const targetBytes = Math.ceil(gib * 1024 ** 3); +const timestamp = '2026-07-20T00:01:00.000Z'; +const now = Date.parse('2026-07-21T04:00:00.000Z'); +const salt = 'synthetic-test-salt'; +const timeZone = 'UTC'; +const filler = `${JSON.stringify({ timestamp, type: 'event_msg', payload: { + type: 'user_message', message: 'synthetic-only-'.repeat( + Math.floor(lineKiB * 1024 / Buffer.byteLength('synthetic-only-')), + ), +} })}\n`; +const fillerBytes = Buffer.byteLength(filler); +function seed(index, input = 100, output = 20) { + return [ + { timestamp, type: 'session_meta', payload: { + id: `synthetic-session-${index}`, cwd: '/fixture/codex-history', + } }, + { timestamp, type: 'turn_context', payload: { model: 'gpt-5.6-sol', effort: 'high' } }, + { timestamp, type: 'event_msg', payload: { type: 'token_count', info: { + total_token_usage: { + input_tokens: input, cached_input_tokens: Math.floor(input / 2), + output_tokens: output, reasoning_output_tokens: Math.floor(output / 2), + total_tokens: input + output, + }, + } } }, + ].map((row) => JSON.stringify(row)).join('\n') + '\n'; +} +function tail(input = 150, output = 30) { + return `${JSON.stringify({ timestamp, type: 'event_msg', payload: { + type: 'token_count', info: { total_token_usage: { + input_tokens: input, cached_input_tokens: Math.floor(input / 2), + output_tokens: output, reasoning_output_tokens: Math.floor(output / 2), + total_tokens: input + output, + } }, + } })}\n`; +} + +const root = await mkdtemp(join(tmpdir(), 'ccu-v240-codex-synthetic-')); +const sessions = join(root, 'sessions'); +const files = []; +let peakRssBytes = process.memoryUsage().rss; +const sampler = setInterval(() => { + peakRssBytes = Math.max(peakRssBytes, process.memoryUsage().rss); +}, 50); +const phases = []; +function instrumentedIo() { + const reads = []; + return { + reads, + async *read(entry, start, end) { + let bytes = 0; + for await (const chunk of defaultCodexJsonlReader.read(entry, start, end)) { + bytes += chunk.length; + yield chunk; + } + reads.push({ fileKey: entry.fileKey, start, bytes }); + }, + }; +} +async function measured(label, work) { + const io = instrumentedIo(); + const start = performance.now(); + const result = await work(io); + const rssBytes = process.memoryUsage().rss; + peakRssBytes = Math.max(peakRssBytes, rssBytes); + const phase = { + label, elapsedMs: Math.round(performance.now() - start), + rssBytes, + bodyReads: result.bodyReads, + readCalls: io.reads.length, + bytesRead: io.reads.reduce((sum, read) => sum + read.bytes, 0), + migration: result.migration, + }; + phases.push(phase); + return { result, phase, io }; +} +const budget = { + maxFilePasses: CODEX_REFRESH_BACKFILL_MAX_FILE_PASSES, + maxBytes: CODEX_REFRESH_BACKFILL_MAX_BYTES, +}; +async function update(index, io) { + const manifest = await scanCodexManifest(root, salt); + return updateCodexIndex(index, manifest, { + salt, timeZone, now: () => now, budget, io, + }); +} + +try { + await mkdir(sessions, { recursive: true }); + const seedBytes = Buffer.byteLength(seed(0)); + const fillerCount = Math.ceil(Math.max(0, targetBytes - fileCount * seedBytes) / fillerBytes); + let writtenBytes = 0; + for (let index = 0; index < fileCount; index += 1) { + const file = join(sessions, `rollout-${String(index).padStart(4, '0')}.jsonl`); + files.push(file); + const copies = Math.floor(fillerCount / fileCount) + + (index < fillerCount % fileCount ? 1 : 0); + const body = seed(index) + filler.repeat(copies); + writtenBytes += Buffer.byteLength(body); + await writeFile(file, body, 'utf8'); + if ((index + 1) % 100 === 0) process.stdout.write(`generated ${index + 1}/${fileCount} files\n`); + } + assert.ok(writtenBytes >= targetBytes); + process.stdout.write(`fixture bytes=${writtenBytes} files=${fileCount} fillerLines=${fillerCount} fillerLineBytes=${fillerBytes}\n`); + + const cold = await measured('cold', (io) => update(createEmptyCodexIndex(timeZone), io)); + assert.equal(cold.result.failedFiles, 0); + assert.equal(cold.result.index.coverage.complete, true); + assert.equal(cold.result.index.coverage.period.allTime.complete, true); + assert.equal(cold.result.index.coverage.hourly?.complete, true); + assert.equal(cold.result.index.aggregate.total.inputTotal, 100 * fileCount); + + const unchanged = await measured('unchanged', (io) => update(cold.result.index, io)); + assert.strictEqual(unchanged.result.index, cold.result.index); + assert.equal(unchanged.phase.readCalls, 0); + assert.equal(unchanged.phase.bytesRead, 0); + + const tailIndexes = fileCount >= 645 ? [7, 91, 183, 275, 367, 459, 644] : [0, 1, 2, 3, 4, 5, 6]; + for (const index of tailIndexes) await appendFile(files[index], tail(), 'utf8'); + const added = join(sessions, 'rollout-new.jsonl'); + await writeFile(added, seed(fileCount), 'utf8'); + const warm = await measured('seven-tails-plus-new', (io) => update(unchanged.result.index, io)); + assert.equal(warm.result.failedFiles, 0); + assert.equal(warm.result.index.coverage.complete, true); + assert.equal(warm.result.index.aggregate.total.inputTotal, 100 * (fileCount + 1) + 50 * 7); + const changedKeys = new Set((await scanCodexManifest(root, salt)).files + .filter((entry) => tailIndexes.some((index) => files[index] === entry.absolutePath) || + entry.absolutePath === added) + .map((entry) => entry.fileKey)); + assert.equal(changedKeys.size, 8); + assert.ok(warm.io.reads.every((read) => changedKeys.has(read.fileKey)), + 'append/new-file refresh must not read untouched JSONL bodies'); + assert.ok(warm.phase.bytesRead < 8 * 1024 * 1024); + + await writeFile(files[0], seed(0, 40, 10), 'utf8'); + const truncated = await measured('truncate-one-file', (io) => update(warm.result.index, io)); + assert.equal(truncated.result.failedFiles, 0); + assert.equal(truncated.result.index.aggregate.total.inputTotal, + warm.result.index.aggregate.total.inputTotal - + (100 + (tailIndexes.includes(0) ? 50 : 0) - 40)); + const truncatedKey = (await scanCodexManifest(root, salt)).files + .find((entry) => entry.absolutePath === files[0]).fileKey; + assert.ok(truncated.io.reads.every((read) => read.fileKey === truncatedKey), + 'one-file truncation must not rescan untouched JSONL bodies'); + + await unlink(files[1]); + const deleted = await measured('delete-one-file', (io) => update(truncated.result.index, io)); + assert.equal(deleted.result.failedFiles, 0); + assert.equal(deleted.result.index.aggregate.total.inputTotal, + truncated.result.index.aggregate.total.inputTotal - + (100 + (tailIndexes.includes(1) ? 50 : 0))); + assert.equal(deleted.phase.bytesRead, 0, + 'one-file deletion must not rescan unaffected JSONL bodies'); + + if (typeof global.gc === 'function') { + global.gc(); + assert.ok(process.memoryUsage().heapUsed < + Math.max(200 * 1024 ** 2, writtenBytes / 2), + 'retained heap must remain below the synthetic corpus memory ceiling'); + } + process.stdout.write(`${JSON.stringify({ + fixtureBytes: writtenBytes, files: fileCount, fillerLines: fillerCount, + fillerLineBytes: fillerBytes, peakRssBytes, + postGcHeapBytes: process.memoryUsage().heapUsed, phases, + })}\n`); +} finally { + clearInterval(sampler); + await rm(root, { recursive: true, force: true }); +} diff --git a/tests/ui/chart-date-labels.spec.mjs b/tests/ui/chart-date-labels.spec.mjs index dc0971ea..dc05df9e 100644 --- a/tests/ui/chart-date-labels.spec.mjs +++ b/tests/ui/chart-date-labels.spec.mjs @@ -15,7 +15,7 @@ test('chart metric switches preserve daily and monthly key labels west of UTC', await page.locator('#tab-all').click(); const monthlyBar = page.locator( - '#all #allTimeChart .hc-col[data-date="2026-07"] .chart-bar', + '#all #allTimeChart .hc-col[data-date="2026-07-01"] .chart-bar', ); await expect(monthlyBar).toHaveAttribute('title', /^July 2026:/); await page.locator('#all .chart-tab[data-metric="inputTokens"]').click(); diff --git a/tests/ui/codex-accessibility.spec.mjs b/tests/ui/codex-accessibility.spec.mjs index 6fc03574..7a3ea38c 100644 --- a/tests/ui/codex-accessibility.spec.mjs +++ b/tests/ui/codex-accessibility.spec.mjs @@ -198,7 +198,7 @@ test('shared chart controls expose and update aria-pressed from the keyboard', a for (const scenario of [ { name: 'Claude', label: 'Claude', open: openClaude }, - { name: 'Codex', label: 'Codex Beta', open: openCodex }, + { name: 'Codex', label: 'Codex', open: openCodex }, { name: 'Compare', label: 'Compare', open: openCompare }, ]) { test(`${scenario.name} chart regions have unique provider-qualified accessible names`, async ({ page }) => { diff --git a/tests/ui/codex-interactions.spec.mjs b/tests/ui/codex-interactions.spec.mjs index dd6276aa..5961d358 100644 --- a/tests/ui/codex-interactions.spec.mjs +++ b/tests/ui/codex-interactions.spec.mjs @@ -349,11 +349,25 @@ test('materialized Claude hourly detail sends no host message and survives a ful ))).toEqual([]); }); -test('Claude monthly chart drill-down survives a full webview reload', async ({ page }) => { +test('Claude all-time months drill into materialized days and supported hours', async ({ page }) => { await openClaude(page); await page.locator('#tab-all').click(); - const month = '2026-07'; + const month = '2026-07-01'; + const day = '2026-07-19'; + const dailyData = [{ + date: day, + data: { + totalInputTokens: 400, + totalOutputTokens: 80, + totalCacheCreationTokens: 120, + totalCacheReadTokens: 200, + totalCost: 1.6, + costBreakdown: { input: 0.2, output: 0.4, cacheWrite: 0.6, cacheRead: 0.4 }, + messageCount: 10, + modelBreakdown: {}, + }, + }]; const chartBar = page.locator( `#all #allTimeChart .hc-col[data-date="${month}"] .chart-bar.clickable`, ); @@ -367,6 +381,30 @@ test('Claude monthly chart drill-down survives a full webview reload', async ({ return state.claudeDrilldownDetails?.['claude:all:monthly']; })).toBe(month); + await page.evaluate(({ month, dailyData }) => { + window.dispatchEvent(new MessageEvent('message', { + data: { command: 'dailyDataResponse', provider: 'claude', month, data: dailyData }, + })); + }, { month, dailyData }); + const daily = detail.locator('[data-claude-alltime-daily]'); + const hourlyId = `claude-alltime-hourly-detail-${day}`; + const dailyBar = daily.locator(`.chart-content .hc-col[data-date="${day}"] .chart-bar.clickable`); + const dailyToggle = daily.locator(`[data-claude-alltime-hourly-toggle][data-date="${day}"]`); + await expect(dailyBar).toHaveAttribute('aria-controls', hourlyId); + await expect(dailyToggle).toHaveAttribute('aria-controls', hourlyId); + const postedBeforeHourlyOpen = await page.evaluate(() => window.__ccuPostedMessages.length); + await dailyBar.focus(); + await page.keyboard.press('Space'); + const hourlyDetail = daily.locator(`[data-claude-alltime-hourly-detail-row][data-date="${day}"]`); + await expect(hourlyDetail).toBeVisible(); + await expect(hourlyDetail.locator('[data-claude-materialized-hours="true"]')).toBeVisible(); + await expect(hourlyDetail.locator('.daily-table tbody tr')).toHaveCount(24); + expect(await page.evaluate(() => window.__ccuPostedMessages.length)).toBe(postedBeforeHourlyOpen); + await expect.poll(() => page.evaluate(() => { + const state = JSON.parse(localStorage.getItem('__ccu-vscode-state') || '{}'); + return state.claudeDrilldownDetails?.['claude:all:hourly']; + })).toBe(day); + await page.reload({ waitUntil: 'load' }); await expect(page.locator('#tab-all')).toHaveClass(/active/); @@ -377,6 +415,17 @@ test('Claude monthly chart drill-down survives a full webview reload', async ({ await expect.poll(() => page.evaluate(() => window.__ccuPostedMessages.find( (message) => message.command === 'getDailyData', ))).toEqual({ command: 'getDailyData', month, provider: 'claude' }); + await page.evaluate(({ month, dailyData }) => { + window.dispatchEvent(new MessageEvent('message', { + data: { command: 'dailyDataResponse', provider: 'claude', month, data: dailyData }, + })); + }, { month, dailyData }); + await expect(page.locator( + `#all [data-claude-alltime-daily] [data-claude-alltime-hourly-detail-row][data-date="${day}"]`, + )).toBeVisible(); + await expect(page.locator( + `#all [data-claude-alltime-daily] .chart-content .hc-col[data-date="${day}"] .chart-bar.clickable`, + )).toHaveAttribute('aria-expanded', 'true'); }); test('Codex all-time months drill into indexed days and available hours on demand', async ({ page }) => { diff --git a/tests/ui/codex-visual.spec.mjs b/tests/ui/codex-visual.spec.mjs index 5f23bf14..70080ede 100644 --- a/tests/ui/codex-visual.spec.mjs +++ b/tests/ui/codex-visual.spec.mjs @@ -219,8 +219,10 @@ test('Claude middle dashboard scope is labelled as a rolling 30-day range', asyn }); test('legacy share-card errors are rendered as text instead of executable HTML', async ({ page }) => { - await openClaude(page, { fixture: 'combined-heatmap' }); - await page.locator('#tab-all').click(); + await openCompare(page, { + fixture: 'combined-heatmap', + commandTemplate: 'claudeShareCard', + }); await expect(page.locator('#scPreview')).toBeVisible(); const hostileError = ''; @@ -701,7 +703,7 @@ test('weekly allowance value uses the shared all-time chart and exposes uncertai await page.locator('#tab-all').click(); const panel = page.locator('#all .daily-breakdown').filter({ - hasText: 'Weekly subscription allowance · API-equivalent estimate · Codex Beta', + hasText: 'Weekly subscription allowance · API-equivalent estimate · Codex', }); await expect(panel).toBeVisible(); await expect(panel).toContainText('not an official balance, bill, or cash value'); @@ -752,7 +754,7 @@ test('weekly allowance value can be hidden without hiding Codex API-equivalent c await openCodex(page, { weeklyValue: false }); await page.locator('#tab-all').click(); - await expect(page.locator('#all')).not.toContainText('Weekly subscription allowance · API-equivalent estimate · Codex Beta'); + await expect(page.locator('#all')).not.toContainText('Weekly subscription allowance · API-equivalent estimate · Codex'); await expect(page.locator('#all .summary-grid .summary-item').first()).toContainText( 'API-equivalent cost', ); @@ -769,22 +771,22 @@ test('weekly allowance value can be hidden from Claude All time', async ({ page test('Compare shows both weekly panels by default and hides only those panels when disabled', async ({ page }) => { await openCompare(page); await expect(page.locator('#provider-panel')).toContainText('Weekly subscription allowance · API-equivalent estimate · Claude'); - await expect(page.locator('#provider-panel')).toContainText('Weekly subscription allowance · API-equivalent estimate · Codex Beta'); + await expect(page.locator('#provider-panel')).toContainText('Weekly subscription allowance · API-equivalent estimate · Codex'); await expect(page.locator('#provider-panel .usage-summary .summary-item')).toHaveCount(2); await openCompare(page, { weeklyValue: false }); await expect(page.locator('#provider-panel')).not.toContainText('Weekly subscription allowance · API-equivalent estimate · Claude'); - await expect(page.locator('#provider-panel')).not.toContainText('Weekly subscription allowance · API-equivalent estimate · Codex Beta'); + await expect(page.locator('#provider-panel')).not.toContainText('Weekly subscription allowance · API-equivalent estimate · Codex'); await expect(page.locator('#provider-panel .usage-summary .summary-item')).toHaveCount(2); await expect(page.locator('#provider-panel .usage-summary')).toContainText('Claude'); - await expect(page.locator('#provider-panel .usage-summary')).toContainText('Codex Beta'); + await expect(page.locator('#provider-panel .usage-summary')).toContainText('Codex'); }); test('weekly allowance table fits at 1280px in the longest locale', async ({ page }) => { await openCodex(page, { locale: 'de-DE', width: 1280 }); await page.locator('#tab-all').click(); const panel = page.locator('#all .daily-breakdown').filter({ - hasText: 'Wöchentliches Abo-Kontingent · API-Äquivalenzschätzung · Codex Beta', + hasText: 'Wöchentliches Abo-Kontingent · API-Äquivalenzschätzung · Codex', }); await panel.locator('.weekly-value-details summary').click(); const container = panel.locator('.daily-table-container'); @@ -804,7 +806,7 @@ test('weekly chart uses a non-overflowing current-period label in German', async await openCodex(page, { locale: 'de-DE', width: 1280 }); await page.locator('#tab-all').click(); const panel = page.locator('#all .daily-breakdown').filter({ - hasText: 'Wöchentliches Abo-Kontingent · API-Äquivalenzschätzung · Codex Beta', + hasText: 'Wöchentliches Abo-Kontingent · API-Äquivalenzschätzung · Codex', }); const labels = panel.locator('.hc-xlabel'); const layout = await labels.evaluateAll((elements) => { @@ -837,7 +839,7 @@ test('weekly used-value history remains visible without historical quota samples await page.locator('#tab-all').click(); const panel = page.locator('#all .daily-breakdown').filter({ - hasText: 'Weekly subscription allowance · API-equivalent estimate · Codex Beta', + hasText: 'Weekly subscription allowance · API-equivalent estimate · Codex', }); await expect(panel).toContainText('Monday-to-Monday UTC calendar weeks'); await expect(panel.locator('.weekly-value-details')).not.toHaveAttribute('open', ''); diff --git a/tests/ui/combined-heatmap.spec.mjs b/tests/ui/combined-heatmap.spec.mjs index 8e5c5c9f..389778f0 100644 --- a/tests/ui/combined-heatmap.spec.mjs +++ b/tests/ui/combined-heatmap.spec.mjs @@ -1,12 +1,17 @@ import AxeBuilder from '@axe-core/playwright'; -import { test, expect, openCompare } from './support/app.mjs'; +import { createRequire } from 'node:module'; +import { test, expect, openClaude, openCompare } from './support/app.mjs'; -test('Compare opens with the combined Claude + Codex heatmap and privacy-bounded share card', async ({ page }) => { +const require = createRequire(import.meta.url); +const { renderCombinedHeatmapSvg } = require('../../out/combinedHeatmapSvg.js'); + +test('Compare opens one preview-first sharing workspace on the combined presentation', async ({ page }) => { await openCompare(page, { fixture: 'combined-heatmap', locale: 'en' }); - const panel = page.locator('.combined-heatmap-panel'); + const panel = page.locator('.sharing-workspace'); await expect(panel).toBeVisible(); - await expect(panel.getByRole('heading', { name: 'Combined activity heatmap and share card' })).toBeVisible(); + await expect(panel.getByRole('heading', { name: 'Sharing workspace' })).toBeVisible(); + await expect(page.getByLabel('Presentation')).toHaveValue('combinedHeatmap'); await expect(page.getByRole('region', { name: 'Live preview' })).toBeVisible(); await expect(page.locator('#combinedHeatmapPreview svg')).toHaveAttribute('role', 'img'); await expect(page.getByRole('radio', { name: 'Academic Violet' })).toBeChecked(); @@ -30,7 +35,7 @@ test('Compare opens with the combined Claude + Codex heatmap and privacy-bounded )).toBe(true); await expect(page.locator('#combinedHeatmapPrivacyPreview')).toContainText('accounts, projects, thread titles, local paths, and log content'); await expect(panel).toContainText('not productivity, subscription billing, or provider capability equivalence'); - const desktopPreviewGeometry = await page.locator('.combined-heatmap-preview').evaluate((preview) => { + const desktopPreviewGeometry = await page.locator('#combinedHeatmapPreview').evaluate((preview) => { const svg = preview.querySelector('svg'); const more = [...preview.querySelectorAll('text')].find((element) => element.textContent === 'More'); if (!svg || !more) { @@ -71,6 +76,181 @@ test('Compare opens with the combined Claude + Codex heatmap and privacy-bounded expect(verticalLayout.settingsWidth).toBeLessThanOrEqual(verticalLayout.panelWidth); }); +test('presentation selection swaps truthful previews and survives reload', async ({ page }) => { + await openCompare(page, { fixture: 'combined-heatmap', locale: 'en' }); + + const selector = page.getByLabel('Presentation'); + await expect(selector.locator('option')).toHaveText([ + 'Combined activity heatmap', + 'Claude Share Card', + 'Claude token heatmap', + ]); + + await selector.selectOption('claudeShareCard'); + await expect(page.locator('[data-sharing-presentation="combinedHeatmap"]')).toBeHidden(); + await expect(page.locator('[data-sharing-presentation="claudeShareCard"]')).toHaveClass(/sharing-presentation-placeholder/); + await expect(page.locator('[data-sharing-presentation="claudeShareCard"]')).toBeVisible(); + await expect(page.locator('[data-sharing-presentation="claudeShareCard"] svg')).toHaveCount(0); + await expect.poll(() => page.evaluate(() => window.__ccuPostedMessages.filter( + (message) => message.command === 'sharingTemplateChanged', + ))).toEqual([{ command: 'sharingTemplateChanged', template: 'claudeShareCard' }]); + + await openCompare(page, { + fixture: 'combined-heatmap', + locale: 'en', + commandTemplate: 'claudeShareCard', + }); + const card = page.locator('[data-sharing-presentation="claudeShareCard"]'); + await expect(card).toBeVisible(); + await expect(card).not.toHaveClass(/sharing-presentation-placeholder/); + await expect(card.locator('#scPreview svg')).toBeVisible(); + await expect(card.getByLabel('GitHub avatar')).toHaveCount(0); + await expect(card.getByLabel('GitHub name')).toHaveCount(0); + const cardLayout = await card.evaluate((element) => { + const preview = element.querySelector('#scPreview'); + const controls = element.querySelector('.sharing-controls-panel'); + if (!preview || !controls) return null; + return { + previewBottom: preview.getBoundingClientRect().bottom, + controlsTop: controls.getBoundingClientRect().top, + previewWidth: preview.getBoundingClientRect().width, + presentationWidth: element.getBoundingClientRect().width, + }; + }); + expect(cardLayout).not.toBeNull(); + expect(cardLayout.controlsTop).toBeGreaterThanOrEqual(cardLayout.previewBottom); + expect(cardLayout.previewWidth).toBeGreaterThanOrEqual(cardLayout.presentationWidth * 0.9); + await page.evaluate(() => { window.__ccuPostedMessages = []; }); + await card.getByRole('button', { name: 'Update preview' }).click(); + await card.getByRole('button', { name: 'Export SVG…' }).click(); + const cardMessages = await page.evaluate(() => window.__ccuPostedMessages); + expect(cardMessages.map(({ command }) => command)).toEqual(['buildShareCard', 'exportShareCard']); + expect(cardMessages.every((message) => !('avatar' in message) && !('username' in message))).toBe(true); + const cardA11y = await new AxeBuilder({ page }) + .include('[data-sharing-presentation="claudeShareCard"]') + .analyze(); + expect(cardA11y.violations).toEqual([]); + + await page.reload({ waitUntil: 'load' }); + await expect(page.getByLabel('Presentation')).toHaveValue('claudeShareCard'); + await expect(page.locator('[data-sharing-presentation="claudeShareCard"]')).toBeVisible(); + + await page.getByLabel('Presentation').selectOption('claudeHeatmap'); + await expect(page.locator('[data-sharing-presentation="claudeHeatmap"]')).toHaveClass(/sharing-presentation-placeholder/); + await expect.poll(() => page.evaluate(() => window.__ccuPostedMessages.filter( + (message) => message.command === 'sharingTemplateChanged', + ).at(-1))).toEqual({ command: 'sharingTemplateChanged', template: 'claudeHeatmap' }); + await openCompare(page, { + fixture: 'combined-heatmap', + locale: 'en', + commandTemplate: 'claudeHeatmap', + }); + const heatmap = page.locator('[data-sharing-presentation="claudeHeatmap"]'); + await expect(heatmap).toBeVisible(); + await expect(heatmap).not.toHaveClass(/sharing-presentation-placeholder/); + await expect(heatmap.locator('#claudeHeatmapPreview svg')).toBeVisible(); + const heatmapA11y = await new AxeBuilder({ page }) + .include('[data-sharing-presentation="claudeHeatmap"]') + .analyze(); + expect(heatmapA11y.violations).toEqual([]); + await page.evaluate(() => { window.__ccuPostedMessages = []; }); + await heatmap.getByRole('button', { name: 'Export SVG…' }).click(); + await heatmap.getByRole('button', { name: 'Publish to GitHub…' }).click(); + expect(await page.evaluate(() => window.__ccuPostedMessages)).toEqual([ + { command: 'exportHeatmap' }, + { command: 'publishHeatmap' }, + ]); +}); + +test('unapplied Share Card controls survive a dashboard refresh and can be reset', async ({ page }) => { + await openCompare(page, { + fixture: 'combined-heatmap', + locale: 'en', + commandTemplate: 'claudeShareCard', + }); + await page.locator('#scRange').selectOption('week'); + await page.locator('#scTheme').selectOption('auroraDark'); + await page.locator('.sc-sec[data-sec="messages"]').check(); + await page.reload(); + await expect(page.locator('#scRange')).toHaveValue('week'); + await expect(page.locator('#scTheme')).toHaveValue('auroraDark'); + await expect(page.locator('.sc-sec[data-sec="messages"]')).toBeChecked(); + await page.evaluate(() => { + window.dispatchEvent(new MessageEvent('message', { + data: { command: 'localDataClientAction', action: 'reset-sharing-preferences' }, + })); + }); + await expect(page.locator('#scRange')).toHaveValue('last30'); + await expect(page.locator('#scTheme')).toHaveValue('claudeClassic'); + await expect(page.locator('.sc-sec[data-sec="messages"]')).not.toBeChecked(); +}); + +test('explicit legacy command intent overrides a saved Claude tab after a full render', async ({ page }) => { + await openClaude(page, { fixture: 'combined-heatmap', claudeOnly: true }); + await page.getByRole('tab', { name: 'Last 30 Days' }).click(); + await expect(page.locator('#tab-month')).toHaveClass(/active/); + + await openClaude(page, { + fixture: 'combined-heatmap', + claudeOnly: true, + commandTemplate: 'claudeHeatmap', + }); + + await expect(page.locator('#tab-all')).toHaveClass(/active/); + await expect(page.locator('#all .sharing-workspace')).toBeVisible(); + await expect(page.getByLabel('Presentation')).toHaveValue('claudeHeatmap'); + await expect.poll(() => page.evaluate(() => localStorage.getItem('ccu.activeTab'))).toBe('all'); + expect(await page.evaluate(() => window.__ccuPostedMessages.filter( + (message) => message.command === 'sharingTemplateCommandAck', + ))).toEqual([{ command: 'sharingTemplateCommandAck', revision: 1 }]); +}); + +test('sharing reset cannot collide with the next legacy command revision', async ({ page }) => { + await openCompare(page, { + fixture: 'combined-heatmap', + commandTemplate: 'claudeHeatmap', + }); + await expect(page.getByLabel('Presentation')).toHaveValue('claudeHeatmap'); + expect(await page.evaluate(() => window.__ccuPostedMessages.filter( + (message) => message.command === 'sharingTemplateCommandAck', + ))).toEqual([{ command: 'sharingTemplateCommandAck', revision: 1 }]); + + await page.evaluate(() => { + window.dispatchEvent(new MessageEvent('message', { + data: { + command: 'localDataClientAction', + action: 'reset-sharing-preferences', + requestId: 'sharing-reset-between-commands', + }, + })); + }); + await page.getByLabel('Presentation').selectOption('claudeShareCard'); + + await openCompare(page, { + fixture: 'combined-heatmap', + commandTemplate: 'claudeHeatmap', + commandAfterReset: true, + }); + await expect(page.getByLabel('Presentation')).toHaveValue('claudeHeatmap'); + await expect(page.locator('[data-sharing-presentation="claudeHeatmap"]')).toBeVisible(); + expect(await page.evaluate(() => window.__ccuPostedMessages.filter( + (message) => message.command === 'sharingTemplateCommandAck', + ))).toEqual([{ command: 'sharingTemplateCommandAck', revision: 2 }]); + expect(await page.evaluate(() => sessionStorage.getItem('ccu.sharing.commandRevision'))).toBeNull(); +}); + +test('legacy Claude panels are consolidated and sharing exposes only one visible toggle', async ({ page }) => { + await openClaude(page, { fixture: 'combined-heatmap', locale: 'en' }); + + await page.getByRole('tab', { name: 'All Time' }).click(); + await expect(page.locator('.share-panel')).toHaveCount(0); + await expect(page.locator('#all .sharing-workspace')).toHaveCount(0); + + await page.getByRole('tab', { name: 'Settings' }).click(); + await expect(page.getByText('Enable sharing workspace', { exact: true })).toBeVisible(); + await expect(page.getByText('Show token heatmap (All-time tab)', { exact: true })).toHaveCount(0); +}); + test('sharing workspace is on by default and can be hidden without removing Compare data', async ({ page }) => { await openCompare(page, { fixture: 'combined-heatmap', locale: 'en' }); await expect(page.locator('.combined-heatmap-panel')).toBeVisible(); @@ -90,7 +270,7 @@ test('Compare keeps provider costs and allowance estimates separate instead of s const summaryCards = page.locator('#provider-panel > .usage-summary .summary-item'); await expect(summaryCards).toHaveCount(2); - await expect(summaryCards.locator(':scope > h3')).toHaveText(['Claude', 'Codex Beta']); + await expect(summaryCards.locator(':scope > h3')).toHaveText(['Claude', 'Codex']); const summaryText = (await summaryCards.allTextContents()).join('\n'); expect(summaryText).not.toContain('$'); expect(summaryText).not.toMatch(/quota|allowance/i); @@ -101,7 +281,7 @@ test('Compare keeps provider costs and allowance estimates separate instead of s await expect(allowancePanels).toHaveCount(2); await expect(allowancePanels.locator(':scope > .section-header > h3')).toHaveText([ 'Weekly subscription allowance · API-equivalent estimate · Claude', - 'Weekly subscription allowance · API-equivalent estimate · Codex Beta', + 'Weekly subscription allowance · API-equivalent estimate · Codex', ]); }); @@ -154,7 +334,35 @@ test('combined share preferences stay local and every export action is explicit' ]); expect(state.messages.some((message) => /publish/i.test(message.command))).toBe(false); + await page.evaluate(() => { window.__ccuPostedMessages = []; }); await page.getByRole('button', { name: 'Reset sharing preferences' }).click(); + expect(await page.evaluate(() => window.__ccuPostedMessages)).toEqual([ + { command: 'runLocalDataAction', action: 'reset-sharing-preferences' }, + ]); + await page.evaluate(() => { + window.dispatchEvent(new MessageEvent('message', { + data: { + command: 'localDataClientAction', + action: 'reset-sharing-preferences', + requestId: 'direct-sharing-reset-success', + }, + })); + }); + expect(await page.evaluate(() => window.__ccuPostedMessages.findLast( + (message) => message.command === 'localDataClientActionAck', + ))).toEqual({ + command: 'localDataClientActionAck', + requestId: 'direct-sharing-reset-success', + ok: true, + }); + await page.evaluate(() => { + window.dispatchEvent(new MessageEvent('message', { + data: { + command: 'localDataActionResult', + result: { ok: true, message: 'Sharing preferences and destination strings were reset.' }, + }, + })); + }); await expect(page.locator('#combinedHeatmapTitle')).toHaveValue('Claude + Codex local activity'); await expect(page.locator('#combinedHeatmapRange')).toHaveValue('year'); await expect(page.locator('#combinedHeatmapIntensityMode')).toHaveValue('quantile'); @@ -167,59 +375,140 @@ test('combined share preferences stay local and every export action is explicit' intensityMode: localStorage.getItem('ccu.combinedHeatmap.intensityMode'), palette: localStorage.getItem('ccu.combinedHeatmap.palette'), customAccent: localStorage.getItem('ccu.combinedHeatmap.customAccent'), - messages: window.__ccuPostedMessages.slice(-2), + messages: window.__ccuPostedMessages, })); - expect(reset).toEqual({ + expect(reset).toMatchObject({ title: null, range: null, privacy: null, intensityMode: null, palette: null, customAccent: null, - messages: [ - { command: 'previewCombinedHeatmap', title: 'Claude + Codex local activity', range: 'year', intensityMode: 'quantile', palette: 'academicViolet', customAccent: '#4f2f87' }, - { command: 'resetCombinedHeatmapPreferences' }, - ], }); + expect(reset.messages.some((message) => message.command === 'resetCombinedHeatmapPreferences')).toBe(false); + await expect(page.locator('#combinedHeatmapStatus')).toContainText('Sharing preferences and destination strings were reset.'); + + await page.reload({ waitUntil: 'load' }); + await expect(page.locator('#combinedHeatmapTitle')).toHaveValue('Claude + Codex local activity'); + await expect(page.locator('#combinedHeatmapRange')).toHaveValue('year'); + await expect(page.locator('#combinedHeatmapIntensityMode')).toHaveValue('quantile'); + await expect(page.getByRole('radio', { name: 'Academic Violet' })).toBeChecked(); + await expect(page.locator('#combinedHeatmapPrivacy')).toBeChecked(); +}); + +test('direct sharing reset reports browser-storage removal failure through the host protocol', async ({ page }) => { + await openCompare(page, { fixture: 'combined-heatmap', locale: 'en' }); + await page.locator('#combinedHeatmapTitle').fill('Must survive failed reset'); + await page.getByRole('button', { name: 'Update preview' }).click(); + await page.evaluate(() => { window.__ccuPostedMessages = []; }); + + await page.getByRole('button', { name: 'Reset sharing preferences' }).click(); + expect(await page.evaluate(() => window.__ccuPostedMessages)).toEqual([ + { command: 'runLocalDataAction', action: 'reset-sharing-preferences' }, + ]); + await page.evaluate(() => { + const original = Storage.prototype.removeItem; + Storage.prototype.removeItem = function failingSharingRemoveItem(key) { + if (key === 'ccu.combinedHeatmap.title') { + throw new Error('synthetic-sharing-storage-failure'); + } + return original.call(this, key); + }; + window.dispatchEvent(new MessageEvent('message', { + data: { + command: 'localDataClientAction', + action: 'reset-sharing-preferences', + requestId: 'direct-sharing-reset-failure', + }, + })); + Storage.prototype.removeItem = original; + }); + expect(await page.evaluate(() => window.__ccuPostedMessages.findLast( + (message) => message.command === 'localDataClientActionAck', + ))).toEqual({ + command: 'localDataClientActionAck', + requestId: 'direct-sharing-reset-failure', + ok: false, + }); + expect(await page.evaluate(() => localStorage.getItem('ccu.combinedHeatmap.title'))) + .toBe('Must survive failed reset'); + + await page.evaluate(() => { + window.dispatchEvent(new MessageEvent('message', { + data: { + command: 'localDataActionResult', + result: { + ok: false, + message: 'Host data was handled, but the Webview did not confirm its local reset.', + }, + }, + })); + }); + await expect(page.locator('#combinedHeatmapStatus')).toContainText( + 'Host data was handled, but the Webview did not confirm its local reset.', + ); }); for (const theme of ['light', 'dark']) { test(`combined heatmap controls and preview are accessible in ${theme} theme`, async ({ page }) => { await openCompare(page, { fixture: 'combined-heatmap', locale: 'zh-CN', theme }); - await expect(page.getByLabel('分享标题')).toBeVisible(); - await expect(page.getByLabel('时间范围')).toBeVisible(); - await expect(page.getByLabel('色阶映射')).toHaveValue('quantile'); - await expect(page.getByRole('group', { name: '热力图配色' })).toBeVisible(); - await expect(page.getByLabel('显示隐私预览')).toBeChecked(); + const presentation = page.locator('[data-sharing-presentation="combinedHeatmap"]'); + await expect(presentation.getByLabel('分享标题')).toBeVisible(); + await expect(presentation.getByLabel('时间范围')).toBeVisible(); + await expect(presentation.getByLabel('色阶映射')).toHaveValue('quantile'); + await expect(presentation.getByRole('group', { name: '热力图配色' })).toBeVisible(); + await expect(presentation.getByLabel('显示隐私预览')).toBeChecked(); await page.locator('.combined-output-panel').evaluate((element) => { element.open = true; }); await expect(page.getByLabel('README / Markdown 引用片段')).toBeVisible(); const results = await new AxeBuilder({ page }) .include('.combined-heatmap-panel') - // Chromium/axe cannot resolve overlapping SVG primitives reliably. - // Export-card text/background pairs have deterministic contrast tests - // in combinedHeatmap.test.ts; Axe owns the surrounding HTML controls. - .exclude('#combinedHeatmapPreview svg') .analyze(); expect(results.violations).toEqual([]); - expect(results.incomplete.filter((item) => item.id === 'color-contrast')).toEqual([]); + const unresolvedHtmlContrast = results.incomplete + .filter((item) => item.id === 'color-contrast') + .flatMap((item) => item.nodes) + .filter((node) => !/^ { const expected = { - en: ['Intensity scale', 'Combined activity heatmap and share card'], - 'de-DE': ['Intensitätsskala', 'Kombinierte Aktivitäts-Heatmap und Freigabekarte'], - 'zh-TW': ['色階映射', '綜合活動熱力圖與分享卡'], - 'zh-CN': ['色阶映射', '综合活动热力图与分享卡'], - ja: ['強度スケール', '統合アクティビティヒートマップと共有カード'], - ko: ['강도 스케일', '통합 활동 히트맵과 공유 카드'], - 'pt-BR': ['Escala de intensidade', 'Mapa de calor de atividade combinado e cartão de compartilhamento'], - id: ['Skala intensitas', 'Heatmap aktivitas gabungan dan kartu berbagi'], + en: ['Intensity scale', 'Sharing workspace', 'Presentation', 'More', 'total tokens'], + 'de-DE': ['Intensitätsskala', 'Freigabe-Arbeitsbereich', 'Darstellung', 'Mehr', 'Gesamttoken'], + 'zh-TW': ['色階映射', '分享工作區', '呈現方式', '較多', '總 token'], + 'zh-CN': ['色阶映射', '分享工作台', '呈现方式', '较多', '总 token'], + ja: ['強度スケール', '共有ワークスペース', '表示形式', '多い', '総トークン'], + ko: ['강도 스케일', '공유 작업 공간', '표시 형식', '많이', '총 토큰'], + 'pt-BR': ['Escala de intensidade', 'Espaço de compartilhamento', 'Apresentação', 'Mais', 'total de tokens'], + id: ['Skala intensitas', 'Ruang kerja berbagi', 'Presentasi', 'Lebih banyak', 'total token'], }; - for (const [locale, [intensityLabel, heading]] of Object.entries(expected)) { + for (const [locale, [intensityLabel, heading, presentationLabel, combinedMarker, cardMarker]] of Object.entries(expected)) { await openCompare(page, { fixture: 'combined-heatmap', locale }); await expect(page.getByLabel(intensityLabel)).toHaveValue('quantile'); await expect(page.getByRole('heading', { name: heading })).toBeVisible(); + await expect(page.getByLabel(presentationLabel)).toHaveValue('combinedHeatmap'); + await expect(page.locator('#combinedHeatmapPreview svg')).toContainText(combinedMarker); + const outOfBoundsLabels = await page.locator('#combinedHeatmapPreview svg').evaluate((svg) => { + const viewBox = svg.viewBox.baseVal; + return [...svg.querySelectorAll('text')].flatMap((label) => { + const box = label.getBBox(); + return box.x < -0.5 || box.x + box.width > viewBox.width + 0.5 + ? [label.textContent] + : []; + }); + }); + expect(outOfBoundsLabels, { locale, outOfBoundsLabels }).toEqual([]); + await openCompare(page, { + fixture: 'combined-heatmap', + locale, + commandTemplate: 'claudeShareCard', + }); + await expect(page.locator('#scPreview svg')).toContainText(cardMarker); + await page.evaluate(() => localStorage.setItem('ccu.sharing.template', 'combinedHeatmap')); } }); @@ -228,9 +517,118 @@ test('combined heatmap remains usable at a 360px webview width', async ({ page } await expect(page.locator('.combined-heatmap-controls')).toHaveCSS('grid-template-columns', /\d+(?:\.\d+)?px/); const pageOverflow = await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth); expect(pageOverflow).toBeLessThanOrEqual(1); - const previewOverflow = await page.locator('.combined-heatmap-preview').evaluate((node) => ({ - scrollWidth: node.scrollWidth, - clientWidth: node.clientWidth, - })); + const previewOverflow = await page.locator('#combinedHeatmapPreview').evaluate((node) => { + const svg = node.querySelector('svg'); + const viewBox = svg?.viewBox.baseVal; + const outOfBoundsLabels = svg && viewBox + ? [...svg.querySelectorAll('text')].flatMap((label) => { + const box = label.getBBox(); + const epsilon = 0.5; + return box.x < -epsilon || box.y < -epsilon || + box.x + box.width > viewBox.width + epsilon || + box.y + box.height > viewBox.height + epsilon + ? [label.textContent] + : []; + }) + : ['missing-svg']; + return { + scrollWidth: node.scrollWidth, + clientWidth: node.clientWidth, + outOfBoundsLabels, + }; + }); expect(previewOverflow.scrollWidth).toBeGreaterThan(previewOverflow.clientWidth); + expect(previewOverflow.outOfBoundsLabels, previewOverflow).toEqual([]); +}); + +test('maximum ASCII and CJK titles remain inside the rendered SVG viewBox', async ({ page }) => { + await openCompare(page, { fixture: 'combined-heatmap', locale: 'en', width: 360, height: 900 }); + for (const title of ['W'.repeat(80), '综合活动热力图'.repeat(12)]) { + const svg = renderCombinedHeatmapSvg({}, { + range: '30d', + endDateISO: '2026-07-22', + title, + }); + await page.evaluate((renderedSvg) => { + window.dispatchEvent(new MessageEvent('message', { + data: { + command: 'combinedHeatmapResult', + svg: renderedSvg, + markdown: '', + filename: 'contained.svg', + hasData: true, + }, + })); + }, svg); + const geometry = await page.locator('#combinedHeatmapPreview svg').evaluate((element) => { + const viewBox = element.viewBox.baseVal; + const titleNode = element.querySelector('text'); + const box = titleNode?.getBBox(); + return { + text: titleNode?.textContent, + right: box ? box.x + box.width : Number.POSITIVE_INFINITY, + width: viewBox.width, + }; + }); + expect(geometry.text).toMatch(/…$/); + expect(geometry.right, geometry).toBeLessThanOrEqual(geometry.width + 0.5); + } }); + +for (const template of ['claudeShareCard', 'claudeHeatmap']) { + test(`${template} remains preview-first and accessible at a 360px webview width`, async ({ page }) => { + await openCompare(page, { + fixture: 'combined-heatmap', + commandTemplate: template, + width: 360, + height: 900, + }); + const presentation = page.locator(`[data-sharing-presentation="${template}"]`); + await expect(presentation).toBeVisible(); + const geometry = await presentation.evaluate((element) => { + const preview = element.querySelector('.sharing-artifact-preview'); + const controls = element.querySelector('.sharing-controls-panel'); + const svg = preview?.querySelector('svg'); + const viewBox = svg?.viewBox.baseVal; + const scale = svg && viewBox?.width + ? svg.getBoundingClientRect().width / viewBox.width + : null; + const outOfBoundsLabels = svg && viewBox + ? [...svg.querySelectorAll('text')].flatMap((label) => { + const box = label.getBBox(); + const epsilon = 0.5; + return box.x < -epsilon || box.y < -epsilon || + box.x + box.width > viewBox.width + epsilon || + box.y + box.height > viewBox.height + epsilon + ? [label.textContent] + : []; + }) + : ['missing-svg']; + return { + pageOverflow: document.documentElement.scrollWidth - document.documentElement.clientWidth, + previewBottom: preview?.getBoundingClientRect().bottom, + previewWidth: preview?.getBoundingClientRect().width, + previewClientWidth: preview?.clientWidth, + previewScrollWidth: preview?.scrollWidth, + controlsTop: controls?.getBoundingClientRect().top, + presentationWidth: element.getBoundingClientRect().width, + intrinsicWidth: viewBox?.width, + scale, + outOfBoundsLabels, + }; + }); + expect(geometry.pageOverflow).toBeLessThanOrEqual(1); + expect(geometry.controlsTop).toBeGreaterThanOrEqual(geometry.previewBottom); + expect(geometry.previewWidth).toBeGreaterThanOrEqual(geometry.presentationWidth * 0.9); + expect(geometry.previewScrollWidth).toBeGreaterThan(geometry.previewClientWidth); + expect(geometry.outOfBoundsLabels, geometry).toEqual([]); + if (template === 'claudeShareCard') { + expect(geometry.intrinsicWidth).toBe(1200); + expect(geometry.scale, geometry).toBeGreaterThanOrEqual(0.99); + } + const results = await new AxeBuilder({ page }) + .include(`[data-sharing-presentation="${template}"]`) + .analyze(); + expect(results.violations).toEqual([]); + }); +} diff --git a/tests/ui/currency-display.spec.mjs b/tests/ui/currency-display.spec.mjs index 77128961..88f2cf01 100644 --- a/tests/ui/currency-display.spec.mjs +++ b/tests/ui/currency-display.spec.mjs @@ -25,7 +25,7 @@ test('currency preset reaches Codex summaries, charts, weekly values, and settin await page.locator('#tab-all').click(); const weekly = page.locator('#all .daily-breakdown').filter({ - hasText: 'Weekly subscription allowance · API-equivalent estimate · Codex Beta', + hasText: 'Weekly subscription allowance · API-equivalent estimate · Codex', }); await expect(weekly.locator('.hc-yaxis .hc-yval').first()).toHaveText(/^≈EUR \d/); await weekly.locator('.weekly-value-details summary').click(); diff --git a/tests/ui/dashboard-live-refresh.spec.mjs b/tests/ui/dashboard-live-refresh.spec.mjs index 546d6a0a..194aad63 100644 --- a/tests/ui/dashboard-live-refresh.spec.mjs +++ b/tests/ui/dashboard-live-refresh.spec.mjs @@ -1,4 +1,95 @@ -import { test, expect, openClaude } from './support/app.mjs'; +import { test, expect, openClaude, openCodex } from './support/app.mjs'; + +test('a live Codex refresh waits for an active scroll burst before replacing the panel', async ({ page }) => { + await openCodex(page, { height: 420 }); + const before = await page.evaluate(() => { + const panel = document.getElementById('provider-panel'); + const filler = document.createElement('div'); + filler.style.height = '1600px'; + panel.appendChild(filler); + scrollTo(0, 160); + window.dispatchEvent(new Event('scroll')); + window.__ccuScrollPanel = panel.firstElementChild; + window.dispatchEvent(new MessageEvent('message', { data: { + command: 'dashboardDataPatch', + provider: 'codex', + tab: 'today', + revision: 1, + html: panel.innerHTML, + claudeLast30HoursByDay: {}, + } })); + return { scrollY, calls: window.__ccuSetStateCalls }; + }); + expect(before.scrollY).toBeGreaterThan(0); + expect(await page.evaluate(() => document.getElementById('provider-panel').firstElementChild === window.__ccuScrollPanel)).toBe(true); + expect(await page.evaluate(() => window.__ccuPostedMessages.some((message) => message.command === 'dashboardDataPatchAck'))).toBe(false); + await expect.poll(() => page.evaluate(() => + window.__ccuPostedMessages.some((message) => message.command === 'dashboardDataPatchAck' && message.revision === 1 && message.ok), + )).toBe(true); + expect(await page.evaluate((calls) => window.__ccuSetStateCalls - calls, before.calls)).toBeLessThanOrEqual(1); +}); + +test('continued Codex scrolling coalesces updates but cannot starve the latest patch', async ({ page }) => { + await openCodex(page, { height: 420 }); + await page.evaluate(() => { + const panel = document.getElementById('provider-panel'); + const filler = document.createElement('div'); + filler.style.height = '1600px'; + panel.appendChild(filler); + scrollTo(0, 160); + window.dispatchEvent(new Event('scroll')); + window.__ccuScrollPulse = setInterval(() => window.dispatchEvent(new Event('scroll')), 35); + for (let revision = 1; revision <= 4; revision += 1) { + window.dispatchEvent(new MessageEvent('message', { data: { + command: 'dashboardDataPatch', provider: 'codex', tab: 'today', revision, + html: panel.innerHTML, claudeLast30HoursByDay: {}, + } })); + } + }); + try { + await expect.poll(() => page.evaluate(() => + window.__ccuPostedMessages.filter((message) => message.command === 'dashboardDataPatchAck'), + )).toEqual([{ command: 'dashboardDataPatchAck', revision: 4, ok: true }]); + } finally { + await page.evaluate(() => clearInterval(window.__ccuScrollPulse)); + } +}); + +test('a 20-update scroll storm swaps the provider panel at most twice', async ({ page }) => { + await openCodex(page, { height: 420 }); + const result = await page.evaluate(async () => { + const panel = document.getElementById('provider-panel'); + const filler = document.createElement('div'); + filler.style.height = '1600px'; + panel.appendChild(filler); + const html = panel.innerHTML; + let swaps = 0; + const observer = new MutationObserver((records) => { + swaps += records.filter((record) => record.type === 'childList').length; + }); + observer.observe(panel, { childList: true }); + scrollTo(0, 160); + for (let revision = 1; revision <= 20; revision += 1) { + window.dispatchEvent(new Event('scroll')); + window.dispatchEvent(new MessageEvent('message', { data: { + command: 'dashboardDataPatch', provider: 'codex', tab: 'today', revision, + html, claudeLast30HoursByDay: {}, + } })); + await new Promise((done) => setTimeout(done, 25)); + } + await new Promise((done) => setTimeout(done, 650)); + observer.disconnect(); + return { + swaps, + acks: window.__ccuPostedMessages + .filter((message) => message.command === 'dashboardDataPatchAck') + .map((message) => message.revision), + }; + }); + expect(result.swaps).toBeGreaterThan(0); + expect(result.swaps).toBeLessThanOrEqual(2); + expect(result.acks.at(-1)).toBe(20); +}); const scrollerFixtures = [ ['dashboard-tabs', 'tabs'], diff --git a/tests/ui/local-data-controls.spec.mjs b/tests/ui/local-data-controls.spec.mjs index 881a4328..f4cc7250 100644 --- a/tests/ui/local-data-controls.spec.mjs +++ b/tests/ui/local-data-controls.spec.mjs @@ -1,4 +1,4 @@ -import { test, expect, openCodex } from './support/app.mjs'; +import { test, expect, openClaude, openCodex } from './support/app.mjs'; test('Settings stays concise: repository privacy controls are not rendered in the plugin', async ({ page }) => { await openCodex(page); @@ -61,6 +61,7 @@ test('UI and sharing resets are independent and preserve unrelated localStorage' await page.evaluate(() => { localStorage.setItem('ccu.activeTab', 'settings'); localStorage.setItem('ccu.sessionRange', '30'); + localStorage.setItem('ccu.sharing.template', 'claudeShareCard'); localStorage.setItem('ccu.combinedHeatmap.title', 'Synthetic title'); localStorage.setItem('ccu.combinedHeatmap.range', '90d'); localStorage.setItem('unrelated-private-canary', 'preserve'); @@ -83,12 +84,14 @@ test('UI and sharing resets are independent and preserve unrelated localStorage' expect(await page.evaluate(() => ({ active: localStorage.getItem('ccu.activeTab'), range: localStorage.getItem('ccu.sessionRange'), + sharingTemplate: localStorage.getItem('ccu.sharing.template'), shareTitle: localStorage.getItem('ccu.combinedHeatmap.title'), state: JSON.parse(localStorage.getItem('__ccu-vscode-state') || '{}'), unrelated: localStorage.getItem('unrelated-private-canary'), }))).toEqual({ active: null, range: null, + sharingTemplate: 'claudeShareCard', shareTitle: 'Synthetic title', state: {}, unrelated: 'preserve', @@ -100,10 +103,49 @@ test('UI and sharing resets are independent and preserve unrelated localStorage' })); }); expect(await page.evaluate(() => ({ + sharingTemplate: localStorage.getItem('ccu.sharing.template'), title: localStorage.getItem('ccu.combinedHeatmap.title'), range: localStorage.getItem('ccu.combinedHeatmap.range'), unrelated: localStorage.getItem('unrelated-private-canary'), - }))).toEqual({ title: null, range: null, unrelated: 'preserve' }); + }))).toEqual({ sharingTemplate: null, title: null, range: null, unrelated: 'preserve' }); +}); + +test('sharing reset refreshes the visible master switch without browser command-coordination state', async ({ page }) => { + await openClaude(page); + await page.locator('#tab-settings').click(); + const masterSwitch = page.locator('#set_enableShareCard'); + await masterSwitch.evaluate((element) => { + element.checked = false; + element.dispatchEvent(new Event('change', { bubbles: true })); + }); + await page.evaluate(() => { + localStorage.setItem('ccu.sharing.template', 'claudeShareCard'); + sessionStorage.setItem('unrelated-session-canary', 'preserve'); + window.dispatchEvent(new MessageEvent('message', { + data: { + command: 'localDataClientAction', + action: 'reset-sharing-preferences', + requestId: 'sharing-reset-runtime-1', + }, + })); + }); + + await expect(masterSwitch).toBeChecked(); + expect(await page.evaluate(() => ({ + template: localStorage.getItem('ccu.sharing.template'), + unrelated: sessionStorage.getItem('unrelated-session-canary'), + ack: window.__ccuPostedMessages.findLast( + (message) => message.requestId === 'sharing-reset-runtime-1', + ), + }))).toEqual({ + template: null, + unrelated: 'preserve', + ack: { + command: 'localDataClientActionAck', + requestId: 'sharing-reset-runtime-1', + ok: true, + }, + }); }); test('client reset ACK is false when allowlisted browser storage cannot be deleted', async ({ page }) => { diff --git a/tests/ui/support/app.mjs b/tests/ui/support/app.mjs index 3acadf5e..5bba5fb8 100644 --- a/tests/ui/support/app.mjs +++ b/tests/ui/support/app.mjs @@ -44,11 +44,11 @@ export async function openCodex( export async function openClaude( page, - { locale = 'en', theme = 'light', fixture = 'default', autoRefresh = false, weeklyValue = true, shareStudio = true, projectMatrix = true, timeZone = 'Asia/Hong_Kong', width = 1280, height = 900 } = {}, + { locale = 'en', theme = 'light', fixture = 'default', autoRefresh = false, weeklyValue = true, shareStudio = true, projectMatrix = true, timeZone = 'Asia/Hong_Kong', claudeOnly = false, commandTemplate = '', commandAfterReset = false, width = 1280, height = 900 } = {}, ) { await page.setViewportSize({ width, height }); await page.goto( - `${uiBaseUrl}/?provider=claude&locale=${encodeURIComponent(locale)}&theme=${theme}&fixture=${encodeURIComponent(fixture)}&autoRefresh=${autoRefresh}&weeklyValue=${weeklyValue}&shareStudio=${shareStudio}&projectMatrix=${projectMatrix}&timeZone=${encodeURIComponent(timeZone)}`, + `${uiBaseUrl}/?provider=claude&locale=${encodeURIComponent(locale)}&theme=${theme}&fixture=${encodeURIComponent(fixture)}&autoRefresh=${autoRefresh}&weeklyValue=${weeklyValue}&shareStudio=${shareStudio}&projectMatrix=${projectMatrix}&timeZone=${encodeURIComponent(timeZone)}&claudeOnly=${claudeOnly}&commandTemplate=${encodeURIComponent(commandTemplate)}&commandAfterReset=${commandAfterReset}`, { waitUntil: 'load' }, ); await page.locator('.tab-content.active').waitFor(); @@ -56,11 +56,11 @@ export async function openClaude( export async function openCompare( page, - { locale = 'en', theme = 'light', fixture = 'default', autoRefresh = false, weeklyValue = true, shareStudio = true, width = 1280, height = 900 } = {}, + { locale = 'en', theme = 'light', fixture = 'default', autoRefresh = false, weeklyValue = true, shareStudio = true, commandTemplate = '', commandAfterReset = false, width = 1280, height = 900 } = {}, ) { await page.setViewportSize({ width, height }); await page.goto( - `${uiBaseUrl}/?provider=compare&locale=${encodeURIComponent(locale)}&theme=${theme}&fixture=${encodeURIComponent(fixture)}&autoRefresh=${autoRefresh}&weeklyValue=${weeklyValue}&shareStudio=${shareStudio}`, + `${uiBaseUrl}/?provider=compare&locale=${encodeURIComponent(locale)}&theme=${theme}&fixture=${encodeURIComponent(fixture)}&autoRefresh=${autoRefresh}&weeklyValue=${weeklyValue}&shareStudio=${shareStudio}&commandTemplate=${encodeURIComponent(commandTemplate)}&commandAfterReset=${commandAfterReset}`, { waitUntil: 'load' }, ); await page.locator('#provider-panel').waitFor(); diff --git a/tests/ui/support/capture-readme.mjs b/tests/ui/support/capture-readme.mjs index e086abef..8a90e02a 100644 --- a/tests/ui/support/capture-readme.mjs +++ b/tests/ui/support/capture-readme.mjs @@ -80,7 +80,7 @@ try { } else if (view === 'weekly') { await page.locator('#tab-all').click(); const panel = page.locator('#all .daily-breakdown').filter({ - hasText: 'Weekly subscription allowance · API-equivalent estimate · Codex Beta', + hasText: 'Weekly subscription allowance · API-equivalent estimate · Codex', }); await expect(panel.locator('.weekly-value-details')).not.toHaveAttribute('open', ''); await expect(panel.locator('.seg-cache-read')).not.toHaveCount(0); diff --git a/tests/ui/support/capture-v240.mjs b/tests/ui/support/capture-v240.mjs new file mode 100644 index 00000000..057947ed --- /dev/null +++ b/tests/ui/support/capture-v240.mjs @@ -0,0 +1,82 @@ +// Reproducible synthetic documentation captures from the production renderer. +// These are not evidence of an installed VSIX or a user's actual usage. +import { createServer } from 'node:http'; +import { createRequire } from 'node:module'; +import { mkdir } from 'node:fs/promises'; +import { resolve } from 'node:path'; +import { chromium, expect } from '@playwright/test'; + +const require = createRequire(import.meta.url); +const { renderHarness } = require('./render-harness.cjs'); +const fixtureNow = Date.parse('2026-07-20T12:00:00.000Z'); +const output = resolve('images/v2.4.0'); +const views = { + compare: { provider: 'compare', locale: 'en', theme: 'light', fixture: 'combined-heatmap', width: 1280 }, + claude: { provider: 'claude', locale: 'zh-CN', theme: 'dark', fixture: 'combined-heatmap', claudeOnly: true, commandTemplate: 'claudeShareCard', width: 1280 }, + codex: { provider: 'codex', locale: 'zh-CN', theme: 'dark', fixture: 'default', width: 1280 }, + narrow: { provider: 'compare', locale: 'de-DE', theme: 'dark', fixture: 'combined-heatmap', width: 360 }, +}; +const server = createServer(async (request, response) => { + const options = views[request.url?.slice(1)]; + if (!options) { response.writeHead(404); response.end(); return; } + const NativeDate = Date; + try { + globalThis.Date = class extends NativeDate { + constructor(...args) { super(...(args.length ? args : [fixtureNow])); } + static now() { return fixtureNow; } + }; + const html = await renderHarness({ ...options, autoRefresh: false, weeklyValue: true, shareStudio: true }); + response.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }); + response.end(html); + } catch { + response.writeHead(500); response.end('Fixture rendering failed'); + } finally { + globalThis.Date = NativeDate; + } +}); +await new Promise((done) => server.listen(0, '127.0.0.1', done)); +const browser = await chromium.launch(); +try { + await mkdir(output, { recursive: true }); + for (const [view, options] of Object.entries(views)) { + const context = await browser.newContext({ + viewport: { width: options.width, height: 900 }, + deviceScaleFactor: 1, + locale: options.locale, + timezoneId: 'Asia/Hong_Kong', + reducedMotion: 'reduce', + colorScheme: options.theme, + }); + const page = await context.newPage(); + await page.addInitScript(() => { + window.acquireVsCodeApi = () => ({ + getState: () => undefined, + setState: () => undefined, + postMessage: () => Promise.resolve(true), + }); + }); + await page.clock.install({ time: new Date(fixtureNow) }); + await page.goto(`http://127.0.0.1:${server.address().port}/${view}`); + const screenshot = { animations: 'disabled', caret: 'hide' }; + if (view === 'codex') { + await page.locator('#tab-month').click(); + await expect(page.locator('#month')).toContainText('未缓存用量'); + await page.screenshot({ ...screenshot, path: resolve(output, 'codex-month-zh-CN-dark.png') }); + } else { + if (view === 'claude') { + await page.locator('#tab-all').click(); + await expect(page.locator('[data-sharing-presentation="claudeShareCard"]')).toBeVisible(); + } else { + await expect(page.locator('[data-sharing-presentation="combinedHeatmap"]')).toBeVisible(); + } + await page.locator('.sharing-workspace').screenshot({ + ...screenshot, + path: resolve(output, `${view}-sharing-${options.locale}-${options.theme}.png`), + }); + } + await context.close(); + } +} finally { + await browser.close(); + await new Promise((done) => server.close(done)); +} diff --git a/tests/ui/support/measure-scroll-refresh.mjs b/tests/ui/support/measure-scroll-refresh.mjs new file mode 100644 index 00000000..50e7b9d8 --- /dev/null +++ b/tests/ui/support/measure-scroll-refresh.mjs @@ -0,0 +1,88 @@ +// Local diagnostic, not a pass/fail test or an installed-VSIX measurement. +// Usage: node tests/ui/support/measure-scroll-refresh.mjs BASE_ROOT CANDIDATE_ROOT +import { createServer } from 'node:http'; +import { createRequire } from 'node:module'; +import { resolve } from 'node:path'; +import { chromium } from '@playwright/test'; + +const roots = process.argv.slice(2).map((root) => resolve(root)); +if (roots.length !== 2) throw new Error('Pass baseline and candidate checkout roots'); +const fixtureNow = Date.parse('2026-07-20T12:00:00.000Z'); +const browser = await chromium.launch(); + +async function measure(root) { + const require = createRequire(resolve(root, 'package.json')); + const { renderHarness } = require(resolve(root, 'tests/ui/support/render-harness.cjs')); + const NativeDate = Date; + let html; + try { + globalThis.Date = class extends NativeDate { + constructor(...args) { super(...(args.length ? args : [fixtureNow])); } + static now() { return fixtureNow; } + }; + html = await renderHarness({ provider: 'codex', locale: 'en', theme: 'dark', fixture: 'default', autoRefresh: false }); + } finally { + globalThis.Date = NativeDate; + } + const server = createServer((_request, response) => { + response.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }); + response.end(html); + }); + await new Promise((done) => server.listen(0, '127.0.0.1', done)); + try { + const trials = []; + for (let trial = 0; trial < 3; trial += 1) { + const context = await browser.newContext({ viewport: { width: 1280, height: 420 }, reducedMotion: 'reduce' }); + const page = await context.newPage(); + await page.addInitScript(() => { + window.__ccuAcks = []; + window.acquireVsCodeApi = () => ({ + getState: () => undefined, + setState: () => undefined, + postMessage: (message) => { if (message.command === 'dashboardDataPatchAck') window.__ccuAcks.push(message); }, + }); + }); + await page.clock.install({ time: new Date(fixtureNow) }); + await page.goto(`http://127.0.0.1:${server.address().port}/`); + const result = await page.evaluate(async () => { + const panel = document.getElementById('provider-panel'); + if (!panel) throw new Error('Provider panel did not render'); + const filler = document.createElement('div'); + filler.style.height = '1600px'; + panel.appendChild(filler); + const patchHtml = panel.innerHTML; + let swaps = 0; + const observer = new MutationObserver((records) => { + swaps += records.filter((record) => record.type === 'childList').length; + }); + observer.observe(panel, { childList: true }); + scrollTo(0, 160); + let dispatchMs = 0; + for (let revision = 1; revision <= 20; revision += 1) { + window.dispatchEvent(new Event('scroll')); + const start = performance.now(); + window.dispatchEvent(new MessageEvent('message', { data: { + command: 'dashboardDataPatch', provider: 'codex', tab: 'today', revision, + html: patchHtml, claudeLast30HoursByDay: {}, + } })); + dispatchMs += performance.now() - start; + await new Promise((done) => setTimeout(done, 25)); + } + await new Promise((done) => setTimeout(done, 650)); + observer.disconnect(); + return { swaps, dispatchMs: Number(dispatchMs.toFixed(1)), acks: window.__ccuAcks.map((ack) => ack.revision) }; + }); + trials.push(result); + await context.close(); + } + return { root, trials }; + } finally { + await new Promise((done) => server.close(done)); + } +} + +try { + for (const root of roots) process.stdout.write(`${JSON.stringify(await measure(root))}\n`); +} finally { + await browser.close(); +} diff --git a/tests/ui/support/render-harness.cjs b/tests/ui/support/render-harness.cjs index 4cf96423..b00b8ff7 100644 --- a/tests/ui/support/render-harness.cjs +++ b/tests/ui/support/render-harness.cjs @@ -3,6 +3,23 @@ const Module = require('node:module'); const originalLoad = Module._load; +let latestHarnessPanel; + +function createHarnessPanel() { + const webview = { + html: '', + onDidReceiveMessage: () => ({ dispose: () => undefined }), + postMessage: async () => true, + }; + const panel = { + webview, + reveal: () => undefined, + onDidDispose: () => ({ dispose: () => undefined }), + }; + latestHarnessPanel = panel; + return panel; +} + const vscodeHost = { ColorThemeKind: { Light: 1, Dark: 2, HighContrast: 3, HighContrastLight: 4 }, ViewColumn: { One: 1 }, @@ -13,7 +30,7 @@ const vscodeHost = { authentication: { getSession: async () => undefined }, window: { activeColorTheme: { kind: 1 }, - createWebviewPanel: () => { throw new Error('UI harness does not create VS Code panels'); }, + createWebviewPanel: () => createHarnessPanel(), }, workspace: { workspaceFolders: undefined, @@ -318,8 +335,8 @@ function addClaudeData(provider, { fixture = 'default', enableContent = false } { date: '2026-07-20', data: claudeUsage(0.6) }, ], [ - { date: '2026-06', data: claudeUsage(5) }, - { date: '2026-07', data: claudeUsage(13) }, + { date: '2026-06-01', data: claudeUsage(5) }, + { date: '2026-07-01', data: claudeUsage(13) }, ], [ { hour: '18:00', data: claudeUsage(0.35) }, @@ -349,6 +366,11 @@ function addClaudeData(provider, { fixture = 'default', enableContent = false } '2026-07-20': [{ hour: '18:00', data: claudeUsage(0.6) }], }, claudeProjectUsageMatrix(), + [ + { date: '2026-06-01', data: claudeUsage(5) }, + { date: '2026-07-19', data: claudeUsage(0.4) }, + { date: '2026-07-20', data: claudeUsage(0.6) }, + ], ); if (completedWeeklyFixture) { provider.updateWeeklyQuotaHistory([{ @@ -431,6 +453,9 @@ exports.renderHarness = async function renderHarness({ adviceFeedback = 'none', timeZone = 'Asia/Hong_Kong', codexMonth = '', + claudeOnly = false, + commandTemplate = '', + commandAfterReset = false, } = {}) { I18n.setLanguage(locale); I18n.setTimezone(timeZone); @@ -535,9 +560,11 @@ exports.renderHarness = async function renderHarness({ }; } provider.updateProviderData( - view, + claudeOnly ? null : view, buildScopedCodexInsights(view), - { claude: true, codex: true }, + claudeOnly + ? { claude: true, codex: false, codexData: false } + : { claude: true, codex: true }, ); provider.currentProvider = selectedProvider; @@ -548,7 +575,17 @@ exports.renderHarness = async function renderHarness({ return provider.renderCodexMonthDailyDetail(codexMonth, rows); } - const html = provider.getWebviewContent(); + latestHarnessPanel = undefined; + if (commandTemplate) { + provider.showSharingWorkspace(commandTemplate); + if (commandAfterReset) { + provider.clearSharingRuntimeState(); + provider.showSharingWorkspace(commandTemplate); + } + } + const html = latestHarnessPanel + ? latestHarnessPanel.webview.html + : provider.getWebviewContent(); const fixtureHtml = persistedDetailsFixture ? html.replace('
{ const codexMonth = /^\d{4}-\d{2}$/.test(requestedCodexMonth) ? requestedCodexMonth : ''; + const claudeOnly = url.searchParams.get('claudeOnly') === 'true'; + const requestedCommandTemplate = url.searchParams.get('commandTemplate') ?? ''; + const commandTemplate = ['claudeShareCard', 'claudeHeatmap'].includes(requestedCommandTemplate) + ? requestedCommandTemplate + : ''; + const commandAfterReset = url.searchParams.get('commandAfterReset') === 'true'; const html = await renderHarness({ provider, locale, @@ -77,6 +83,9 @@ const server = createServer(async (request, response) => { adviceFeedback, timeZone, codexMonth, + claudeOnly, + commandTemplate, + commandAfterReset, }); response.writeHead(200, { 'content-type': 'text/html; charset=utf-8',