mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 00:02:31 +00:00
refactor(usage): share attribution and scope filtering
This commit is contained in:
@@ -1,67 +1,13 @@
|
||||
import { normalizeComparablePath } from '../usage/usage-path-comparison'
|
||||
import { attributeUsageEvent } from '../usage/usage-event-attribution'
|
||||
import type { UsageScanWorktreeRef } from '../usage/usage-provider-contract'
|
||||
import type { UsageWorktreeResolver } from '../usage/usage-worktree-resolver'
|
||||
import type { CodexUsageAttributedEvent, CodexUsageParsedEvent } from './types'
|
||||
|
||||
export type CodexUsageWorktreeRef = UsageScanWorktreeRef
|
||||
|
||||
function getDefaultProjectLabel(cwd: string | null): string {
|
||||
if (!cwd) {
|
||||
return 'Unknown location'
|
||||
}
|
||||
const parts = cwd.replace(/\\/g, '/').split('/').filter(Boolean)
|
||||
if (parts.length >= 2) {
|
||||
return parts.slice(-2).join('/')
|
||||
}
|
||||
return parts.at(-1) ?? cwd
|
||||
}
|
||||
|
||||
function localDayFromTimestamp(timestamp: string): string | null {
|
||||
const parsed = new Date(timestamp)
|
||||
if (Number.isNaN(parsed.getTime())) {
|
||||
return null
|
||||
}
|
||||
const year = parsed.getFullYear()
|
||||
const month = String(parsed.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(parsed.getDate()).padStart(2, '0')
|
||||
return `${year}-${month}-${day}`
|
||||
}
|
||||
|
||||
export async function attributeCodexUsageEvent(
|
||||
event: CodexUsageParsedEvent,
|
||||
resolveWorktree: UsageWorktreeResolver
|
||||
): Promise<CodexUsageAttributedEvent | null> {
|
||||
const day = localDayFromTimestamp(event.timestamp)
|
||||
if (!day) {
|
||||
return null
|
||||
}
|
||||
|
||||
let repoId: string | null = null
|
||||
let worktreeId: string | null = null
|
||||
let projectKey = 'unscoped'
|
||||
let projectLabel = getDefaultProjectLabel(event.cwd)
|
||||
|
||||
if (event.cwd) {
|
||||
const worktree = resolveWorktree(event.cwd)
|
||||
if (worktree) {
|
||||
repoId = worktree.repoId
|
||||
worktreeId = worktree.worktreeId
|
||||
projectKey = `worktree:${worktree.worktreeId}`
|
||||
projectLabel = worktree.displayName
|
||||
} else {
|
||||
// Why: all-local mode should still collapse repeated off-Orca sessions by
|
||||
// location, but those keys must normalize slash/case differences so the
|
||||
// same folder does not fragment into multiple "projects" across platforms.
|
||||
projectKey = `cwd:${normalizeComparablePath(event.cwd)}`
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...event,
|
||||
day,
|
||||
projectKey,
|
||||
projectLabel,
|
||||
repoId,
|
||||
worktreeId
|
||||
}
|
||||
return attributeUsageEvent(event, resolveWorktree)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { CodexUsageRange, CodexUsageScope } from '../../shared/codex-usage-types'
|
||||
import type { CodexUsagePersistedState } from './types'
|
||||
import { getLocalUsageDay, getUsageRangeCutoff } from '../usage/usage-calendar-range'
|
||||
import { filterUsageDaily, filterUsageSessions } from '../usage/usage-scope-filters'
|
||||
|
||||
export type ScopedCodexUsageModelRow = {
|
||||
modelKey: string
|
||||
@@ -19,16 +19,7 @@ export function getFilteredDaily(
|
||||
scope: CodexUsageScope,
|
||||
range: CodexUsageRange
|
||||
) {
|
||||
const cutoff = getUsageRangeCutoff(range)
|
||||
return state.dailyAggregates.filter((entry) => {
|
||||
if (cutoff && entry.day < cutoff) {
|
||||
return false
|
||||
}
|
||||
if (scope === 'orca' && entry.worktreeId === null) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
return filterUsageDaily(state.dailyAggregates, scope, range)
|
||||
}
|
||||
|
||||
export function getFilteredSessions(
|
||||
@@ -36,20 +27,7 @@ export function getFilteredSessions(
|
||||
scope: CodexUsageScope,
|
||||
range: CodexUsageRange
|
||||
) {
|
||||
const cutoff = getUsageRangeCutoff(range)
|
||||
return state.sessions.filter((session) => {
|
||||
const day = getLocalUsageDay(session.lastTimestamp)
|
||||
if (!day) {
|
||||
return false
|
||||
}
|
||||
if (cutoff && day < cutoff) {
|
||||
return false
|
||||
}
|
||||
if (scope === 'orca') {
|
||||
return session.locationBreakdown.some((entry) => entry.worktreeId !== null)
|
||||
}
|
||||
return true
|
||||
})
|
||||
return filterUsageSessions(state.sessions, scope, range)
|
||||
}
|
||||
|
||||
export function getScopedSessionModels(
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { getLocalUsageDay } from './usage-calendar-range'
|
||||
import { normalizeComparablePath } from './usage-path-comparison'
|
||||
import type { UsageWorktreeResolver } from './usage-worktree-resolver'
|
||||
|
||||
export type UnattributedUsageEvent = {
|
||||
timestamp: string
|
||||
cwd: string | null
|
||||
}
|
||||
|
||||
export type UsageEventAttribution = {
|
||||
day: string
|
||||
projectKey: string
|
||||
projectLabel: string
|
||||
repoId: string | null
|
||||
worktreeId: string | null
|
||||
}
|
||||
|
||||
function defaultProjectLabel(cwd: string | null): string {
|
||||
if (!cwd) {
|
||||
return 'Unknown location'
|
||||
}
|
||||
const parts = cwd.replace(/\\/g, '/').split('/').filter(Boolean)
|
||||
return parts.length >= 2 ? parts.slice(-2).join('/') : (parts.at(-1) ?? cwd)
|
||||
}
|
||||
|
||||
export function attributeUsageEvent<T extends UnattributedUsageEvent>(
|
||||
event: T,
|
||||
resolveWorktree: UsageWorktreeResolver
|
||||
): (T & UsageEventAttribution) | null {
|
||||
const day = getLocalUsageDay(event.timestamp)
|
||||
if (!day) {
|
||||
return null
|
||||
}
|
||||
const worktree = event.cwd ? resolveWorktree(event.cwd) : null
|
||||
return {
|
||||
...event,
|
||||
day,
|
||||
projectKey: worktree
|
||||
? `worktree:${worktree.worktreeId}`
|
||||
: event.cwd
|
||||
? `cwd:${normalizeComparablePath(event.cwd)}`
|
||||
: 'unscoped',
|
||||
projectLabel: worktree?.displayName ?? defaultProjectLabel(event.cwd),
|
||||
repoId: worktree?.repoId ?? null,
|
||||
worktreeId: worktree?.worktreeId ?? null
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
* consumer, so only the scan envelope is shared.
|
||||
*/
|
||||
|
||||
export type UsageProviderId = 'claude' | 'codex' | 'opencode' | `plugin:${string}`
|
||||
export type UsageProviderId = 'claude' | 'codex' | 'devin' | 'opencode' | `plugin:${string}`
|
||||
|
||||
/** Scan input. Distinct from `UsageWorktreeRef` in usage-worktree-metadata, which lacks `repoId`. */
|
||||
export type UsageScanWorktreeRef = {
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { getLocalUsageDay, getUsageRangeCutoff } from './usage-calendar-range'
|
||||
|
||||
export function filterUsageDaily<T extends { day: string; worktreeId: string | null }>(
|
||||
daily: readonly T[],
|
||||
scope: 'orca' | 'all',
|
||||
range: '7d' | '30d' | '90d' | 'all'
|
||||
): T[] {
|
||||
const cutoff = getUsageRangeCutoff(range)
|
||||
return daily.filter((entry) => {
|
||||
if (cutoff && entry.day < cutoff) {
|
||||
return false
|
||||
}
|
||||
return scope === 'all' || entry.worktreeId !== null
|
||||
})
|
||||
}
|
||||
|
||||
export function filterUsageSessions<
|
||||
T extends {
|
||||
lastTimestamp: string
|
||||
locationBreakdown: readonly { worktreeId: string | null }[]
|
||||
}
|
||||
>(sessions: readonly T[], scope: 'orca' | 'all', range: '7d' | '30d' | '90d' | 'all'): T[] {
|
||||
const cutoff = getUsageRangeCutoff(range)
|
||||
return sessions.filter((session) => {
|
||||
const day = getLocalUsageDay(session.lastTimestamp)
|
||||
if (!day || (cutoff && day < cutoff)) {
|
||||
return false
|
||||
}
|
||||
return scope === 'all' || session.locationBreakdown.some((entry) => entry.worktreeId !== null)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user