Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
7af6292
feat(stats): define usage event and message contracts
k1yt Jul 18, 2026
cbfab67
feat(stats): add append-only local usage store and aggregation
k1yt Jul 18, 2026
87570b2
feat(stats): record final usage for each API attempt
k1yt Jul 18, 2026
0b7e1de
feat(stats): expose stats query export and clear handlers
k1yt Jul 18, 2026
b786b73
feat(stats): add slash entry and statistics webview
k1yt Jul 18, 2026
0a3a902
fix(stats): resolve blockers B1/B2/B3 and highs H1/H3
k1yt Jul 19, 2026
0d44f57
feat(stats): add autocomplete entry and time-axis groupBy in UI
k1yt Jul 19, 2026
e199309
test(stats): add coverage tests for UsageStatsService, UsageHeatmap, …
k1yt Jul 19, 2026
b3c7027
i18n(stats): add translations for 17 languages
k1yt Jul 19, 2026
22dbf9a
fix(i18n): remove BOM from package.nls.ca.json
k1yt Jul 19, 2026
b8657f4
fix(i18n): remove BOM from all package.nls locale files
k1yt Jul 19, 2026
bc9df7b
fix(i18n): restore missing opening brace in all package.nls locale files
k1yt Jul 19, 2026
c161365
i18n(stats): apply CodeRabbit translation review fixes (de, fr, vi, z…
k1yt Jul 19, 2026
9899f4f
refactor(stats): convert all Korean comments to English
k1yt Jul 19, 2026
d4c838d
feat(dashboard): remove /stats command and add Dashboard sidebar entry
k1yt Jul 19, 2026
2e250c3
feat(dashboard): add DashboardView with summary, time range, and brea…
k1yt Jul 19, 2026
2889921
feat(dashboard): add session list with titles and model/provider filters
k1yt Jul 19, 2026
c1981a4
feat(dashboard): add session detail with expandable API call list
k1yt Jul 19, 2026
7acc894
feat(dashboard): add translations for all 17 languages
k1yt Jul 19, 2026
138aa81
test(stats): remove stale 'stats' command test assertions
k1yt Jul 19, 2026
b2a7cba
refactor(dashboard): remove orphaned StatsView, i18n relative time, e…
k1yt Jul 19, 2026
995c7c8
feat(dashboard): default Custom date range to yesterday-today
k1yt Jul 19, 2026
f0a7c9c
fix(providers): add totalCost calculation using user-configured pricing
k1yt Jul 19, 2026
ad71891
feat(dashboard): compute missing costs at query time and fix session …
k1yt Jul 19, 2026
968d735
feat(dashboard): add usage dashboard with mode column, multi-model ag…
k1yt Jul 20, 2026
70d093f
feat(heatmap): blue gradient 6 levels, white borders, and 221 new tests
k1yt Jul 20, 2026
a940461
feat(dashboard): responsive heatmap, 30d/60d/120d/360d ranges, CI fix…
k1yt Jul 20, 2026
b447fd3
feat(stats): make UsageHeatmap self-fetching for independent range se…
k1yt Jul 20, 2026
eae70ea
test(stats): add comprehensive DashboardView test suite for codecov p…
k1yt Jul 20, 2026
415b7e7
fix(stats): remove unused variables in DashboardView.spec.tsx to fix …
k1yt Jul 20, 2026
3b86708
fix(stats): correct totalTokens calculation, provider pricing, and da…
k1yt Jul 20, 2026
7e7fbe4
fix(stats): remove day axis from breakdown groupBy to eliminate dupli…
k1yt Jul 20, 2026
4a49d3e
feat(stats): add endpoint domain extraction for provider identificati…
k1yt Jul 20, 2026
0e24866
fix(terminal): retry with execa when shell integration loses command
k1yt Jul 20, 2026
21465e4
fix(stats): update MiMo pricing, remove session filters, add NDJSON c…
k1yt Jul 21, 2026
5f375bf
feat(dashboard): add multi-window refresh, cache ratio estimation, an…
k1yt Jul 24, 2026
76be6eb
fix(stats): pass all CI checks after rebase onto main
k1yt Jul 24, 2026
59ac789
fix(dashboard): remove unknownEventCount display and utility scripts
k1yt Jul 24, 2026
fa8f19a
fix(ci): increase e2e timeout and add provider totalCost tests
k1yt Jul 24, 2026
99c7bf0
fix(ci): pass test:coverage
k1yt Jul 24, 2026
c17d09e
fix(ci): revert e2e timeout + add coverage tests
k1yt Jul 24, 2026
5c5debe
fix(ci): break terminal-reuse fixture re-match loop
k1yt Jul 24, 2026
1d1eb91
fix(task): guard saveClineMessages against abandoned tasks to prevent…
k1yt Jul 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion apps/vscode-e2e/src/fixtures/terminal-reuse-shell-race.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,20 @@ import { toolResultContains } from "./tool-result"
export function addTerminalReuseShellRaceFixtures(mock: InstanceType<typeof LLMock>) {
// First command completes — model issues a second command on the same terminal.
// With the temp-script fix, both commands now deliver real output.
// Single-fire guard: `toolResultContains` is a permanent-history existence check, so once the
// first command's result is in history this predicate would match on EVERY subsequent re-prompt.
// If the second command's result does not satisfy Fixture B, Fixture A would keep re-emitting the
// second command forever (60s test timeout). Fire exactly once.
let secondCommandIssued = false
mock.addFixture({
match: {
predicate: (req) => toolResultContains(req, "call_terminal_reuse_001", ["first", "Exit code: 0"]),
predicate: (req) => {
if (secondCommandIssued) return false
if (!toolResultContains(req, "call_terminal_reuse_001", ["first", "Exit code: 0"])) return false
// Match confirmed — the response below will be emitted. Latch so we never match again.
secondCommandIssued = true
return true
},
},
response: {
toolCalls: [
Expand Down
323 changes: 323 additions & 0 deletions packages/types/src/__tests__/usage-stats.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,323 @@
import {
UsageEventStatus,
UsageValueSource,
InclusionRule,
SourcedNumber,
UsageEventV1,
StatsQuery,
StatsBucket,
StatsSnapshot,
} from "../usage-stats.js"

describe("usage-stats schemas", () => {
// ── Enums ────────────────────────────────────────────────────────────

describe("UsageEventStatus", () => {
it("should accept all valid statuses", () => {
expect(UsageEventStatus.parse("completed")).toBe("completed")
expect(UsageEventStatus.parse("failed")).toBe("failed")
expect(UsageEventStatus.parse("cancelled")).toBe("cancelled")
})

it("should reject invalid status", () => {
expect(() => UsageEventStatus.parse("success")).toThrow()
})
})

describe("UsageValueSource", () => {
it("should accept all valid sources", () => {
expect(UsageValueSource.parse("provider")).toBe("provider")
expect(UsageValueSource.parse("estimated")).toBe("estimated")
expect(UsageValueSource.parse("backfilled")).toBe("backfilled")
})

it("should reject invalid source", () => {
expect(() => UsageValueSource.parse("guessed")).toThrow()
})
})

describe("InclusionRule", () => {
it("should accept all valid rules", () => {
expect(InclusionRule.parse("included")).toBe("included")
expect(InclusionRule.parse("excluded")).toBe("excluded")
expect(InclusionRule.parse("unknown")).toBe("unknown")
})
})

// ── SourcedNumber ─────────────────────────────────────────────────────

describe("SourcedNumber", () => {
it("should parse a valid SourcedNumber", () => {
const result = SourcedNumber.parse({ value: 42, source: "provider" })
expect(result).toEqual({ value: 42, source: "provider" })
})

it("should reject missing source", () => {
expect(() => SourcedNumber.parse({ value: 42 })).toThrow()
})

it("should reject missing value", () => {
expect(() => SourcedNumber.parse({ source: "estimated" })).toThrow()
})
})

// ── UsageEventV1 ────────────────────────────────────────────────────────

describe("UsageEventV1", () => {
const validEvent = {
schemaVersion: 1,
eventId: "evt-001",
idempotencyKey: "idem-001",
occurredAt: "2026-07-18T12:00:00.000Z",
timezoneOffsetMinutes: -540,
status: "completed",
attempt: 1,
taskId: "task-001",
provider: "anthropic",
model: "claude-sonnet-4-20250514",
mode: "code",
usage: {
inputTokens: { value: 1000, source: "provider" },
outputTokens: { value: 500, source: "provider" },
costUsd: { value: 0.015, source: "provider" },
},
semantics: {
cacheReadInInput: "included",
cacheWriteInInput: "included",
reasoningInOutput: "excluded",
},
provenance: "live",
}

it("should parse a valid complete event", () => {
const result = UsageEventV1.parse(validEvent)
expect(result.eventId).toBe("evt-001")
expect(result.schemaVersion).toBe(1)
expect(result.usage.inputTokens?.value).toBe(1000)
})

it("should accept optional parentTaskId", () => {
const result = UsageEventV1.parse({ ...validEvent, parentTaskId: "task-000" })
expect(result.parentTaskId).toBe("task-000")
})

it("should work without optional usage fields", () => {
const minimal = { ...validEvent, usage: {} }
const result = UsageEventV1.parse(minimal)
expect(result.usage.inputTokens).toBeUndefined()
})

it("should accept backfilled provenance", () => {
const result = UsageEventV1.parse({ ...validEvent, provenance: "history-backfill" })
expect(result.provenance).toBe("history-backfill")
})

it("should reject schemaVersion !== 1", () => {
expect(() => UsageEventV1.parse({ ...validEvent, schemaVersion: 2 })).toThrow()
})

it("should reject missing semantics", () => {
const { semantics: _semantics, ...withoutSemantics } = validEvent
expect(() => UsageEventV1.parse(withoutSemantics)).toThrow()
})

it("should reject invalid provenance", () => {
expect(() => UsageEventV1.parse({ ...validEvent, provenance: "imported" })).toThrow()
})

it("should reject missing required fields (eventId)", () => {
const { eventId: _eventId, ...withoutEventId } = validEvent
expect(() => UsageEventV1.parse(withoutEventId)).toThrow()
})

it("should accept attempt of 0 (no min constraint in V1)", () => {
// z.number() accepts negatives, but attempt should be >= 0 logically
// This test confirms the schema accepts any number (no min constraint in V1)
const result = UsageEventV1.parse({ ...validEvent, attempt: 0 })
expect(result.attempt).toBe(0)
})
})

// ── StatsQuery ───────────────────────────────────────────────────────

describe("StatsQuery", () => {
it("should parse a valid query with preset", () => {
const result = StatsQuery.parse({
preset: "7d",
timezone: "Asia/Seoul",
groupBy: ["day"],
})
expect(result.preset).toBe("7d")
expect(result.includeCancelled).toBe(false) // default
})

it("should parse a query with from/to range", () => {
const result = StatsQuery.parse({
from: "2026-07-01T00:00:00Z",
to: "2026-07-18T00:00:00Z",
timezone: "UTC",
groupBy: ["provider", "model"],
})
expect(result.from).toBe("2026-07-01T00:00:00Z")
expect(result.groupBy).toHaveLength(2)
})

it("should default includeCancelled to false", () => {
const result = StatsQuery.parse({
timezone: "UTC",
groupBy: [],
})
expect(result.includeCancelled).toBe(false)
})

it("should accept includeCancelled: true", () => {
const result = StatsQuery.parse({
timezone: "UTC",
groupBy: [],
includeCancelled: true,
})
expect(result.includeCancelled).toBe(true)
})

it("should reject more than 3 groupBy dimensions", () => {
expect(() =>
StatsQuery.parse({
timezone: "UTC",
groupBy: ["day", "week", "month", "provider"],
}),
).toThrow()
})

it("should reject invalid preset", () => {
expect(() =>
StatsQuery.parse({
preset: "90d",
timezone: "UTC",
groupBy: [],
}),
).toThrow()
})

it("should reject missing timezone", () => {
expect(() =>
StatsQuery.parse({
groupBy: [],
}),
).toThrow()
})

it("should reject invalid groupBy dimension", () => {
expect(() =>
StatsQuery.parse({
timezone: "UTC",
groupBy: ["hour"],
}),
).toThrow()
})
})

// ── StatsBucket ──────────────────────────────────────────────────────

describe("StatsBucket", () => {
const validBucket = {
key: { day: "2026-07-18" },
events: 10,
completedCalls: 8,
failedCalls: 1,
cancelledCalls: 1,
inputTokens: 5000,
outputTokens: 2500,
cacheReadTokens: 1000,
cacheWriteTokens: 500,
reasoningTokens: 200,
totalTokens: 7500,
costUsd: 0.075,
unknownEventCount: 0,
}

it("should parse a valid bucket", () => {
const result = StatsBucket.parse(validBucket)
expect(result.events).toBe(10)
expect(result.key.day).toBe("2026-07-18")
})

it("should reject missing required numeric field", () => {
const { costUsd: _costUsd, ...withoutCost } = validBucket
expect(() => StatsBucket.parse(withoutCost)).toThrow()
})

it("should accept empty key record", () => {
const result = StatsBucket.parse({ ...validBucket, key: {} })
expect(Object.keys(result.key)).toHaveLength(0)
})
})

// ── StatsSnapshot ─────────────────────────────────────────────────────

describe("StatsSnapshot", () => {
const validQuery = {
timezone: "UTC",
groupBy: ["day"],
}
const validBucket = {
key: { day: "2026-07-18" },
events: 5,
completedCalls: 4,
failedCalls: 1,
cancelledCalls: 0,
inputTokens: 2000,
outputTokens: 1000,
cacheReadTokens: 0,
cacheWriteTokens: 0,
reasoningTokens: 0,
totalTokens: 3000,
costUsd: 0.03,
unknownEventCount: 0,
}
const validSnapshot = {
query: validQuery,
generatedAt: "2026-07-18T12:00:00.000Z",
buckets: [validBucket],
totals: validBucket,
coverage: {
firstEventAt: "2026-07-01T00:00:00.000Z",
lastEventAt: "2026-07-18T12:00:00.000Z",
recordingPaused: false,
backfilledEventCount: 0,
},
}

it("should parse a valid snapshot", () => {
const result = StatsSnapshot.parse(validSnapshot)
expect(result.buckets).toHaveLength(1)
expect(result.coverage.recordingPaused).toBe(false)
})

it("should accept empty buckets array", () => {
const result = StatsSnapshot.parse({ ...validSnapshot, buckets: [] })
expect(result.buckets).toHaveLength(0)
})

it("should accept optional firstEventAt/lastEventAt omitted", () => {
const result = StatsSnapshot.parse({
...validSnapshot,
coverage: {
recordingPaused: true,
backfilledEventCount: 0,
},
})
expect(result.coverage.firstEventAt).toBeUndefined()
expect(result.coverage.lastEventAt).toBeUndefined()
})

it("should reject missing coverage", () => {
const { coverage: _coverage, ...withoutCoverage } = validSnapshot
expect(() => StatsSnapshot.parse(withoutCoverage)).toThrow()
})

it("should reject missing totals", () => {
const { totals: _totals, ...withoutTotals } = validSnapshot
expect(() => StatsSnapshot.parse(withoutTotals)).toThrow()
})
})
})
1 change: 1 addition & 0 deletions packages/types/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export * from "./provider-settings.js"
export * from "./task.js"
export * from "./todo.js"
export * from "./skills.js"
export * from "./usage-stats.js"
export * from "./rules.js"
export * from "./marketplace.js"
export * from "./telemetry.js"
Expand Down
26 changes: 6 additions & 20 deletions packages/types/src/providers/mimo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,10 @@ export const mimoModels = {
supportsImages: false, // Pro series is text-only
supportsPromptCache: false,
preserveReasoning: true,
inputPrice: 1.0, // $1.00/1M tokens (cache miss, ≤256K)
outputPrice: 3.0, // $3.00/1M tokens (≤256K)
cacheReadsPrice: 0.2, // $0.20/1M tokens (cache hit, ≤256K)
inputPrice: 0.435, // $0.435/1M tokens
outputPrice: 0.87, // $0.87/1M tokens
cacheReadsPrice: 0.0036, // $0.0036/1M tokens
cacheWritesPrice: 0, // Free for limited time
// MiMo charges 2x above 256K context
longContextPricing: {
thresholdTokens: 256_000,
inputPriceMultiplier: 2,
outputPriceMultiplier: 2,
cacheReadsPriceMultiplier: 2,
},
description:
"MiMo V2.5 Pro - Xiaomi's flagship reasoning model with 1M context, deep thinking, tool calling, and structured output.",
},
Expand All @@ -41,17 +34,10 @@ export const mimoModels = {
supportsImages: true, // Full-modal: text, image, audio, video input
supportsPromptCache: false,
preserveReasoning: true,
inputPrice: 0.4, // $0.40/1M tokens (cache miss, ≤256K)
outputPrice: 2.0, // $2.00/1M tokens (≤256K)
cacheReadsPrice: 0.08, // $0.08/1M tokens (cache hit, ≤256K)
inputPrice: 0.14, // $0.14/1M tokens
outputPrice: 0.28, // $0.28/1M tokens
cacheReadsPrice: 0.0028, // $0.0028/1M tokens
cacheWritesPrice: 0, // Free for limited time
// MiMo charges 2x above 256K context
longContextPricing: {
thresholdTokens: 256_000,
inputPriceMultiplier: 2,
outputPriceMultiplier: 2,
cacheReadsPriceMultiplier: 2,
},
description:
"MiMo V2.5 - Full-modal understanding model (text, image, audio, video) with 1M context, deep thinking, tool calling, and structured output.",
},
Expand Down
Loading
Loading