Files
orca/src/shared/feature-interactions.ts
T
2026-05-27 17:43:45 -07:00

253 lines
6.5 KiB
TypeScript

export type FeatureInteractionId =
| 'workspace-board'
| 'workspace-board-actions'
| 'browser'
| 'tasks'
| 'automations'
| 'automation-created'
| 'automation-run'
| 'browser-annotations'
| 'browser-grab'
| 'workspace-creation'
| 'agent-browser-setup'
| 'agent-browser-use'
| 'agent-orchestration-setup'
| 'agent-orchestration'
| 'ai-commit-generation'
| 'ai-pr-generation'
| 'claude-account-switching'
| 'computer-use-setup'
| 'computer-use'
| 'codex-account-switching'
| 'cookie-import'
| 'floating-workspace'
| 'mobile-pairing'
| 'notifications'
| 'ports'
| 'quick-commands'
| 'resource-manager'
| 'review-notes'
| 'ssh'
| 'terminal-panes'
| 'terminal-tabs'
| 'tab-splits'
| 'usage-tracking'
| 'voice-dictation'
| 'workspace-cleanup'
export type FeatureInteractionDefinition = {
id: FeatureInteractionId
/** The product action that counts as "the user has interacted with this feature." */
interaction: string
}
export type FeatureInteractionRecord = {
/** Unix timestamp in milliseconds for the first local interaction. */
firstInteractedAt: number
/** Number of local interactions recorded for this feature. */
interactionCount: number
}
export type FeatureInteractionState = Partial<
Record<FeatureInteractionId, FeatureInteractionRecord>
>
// Why: education state has three separate meanings; see
// docs/reference/feature-education-state.md before adding or reusing ids here.
export const FEATURE_INTERACTIONS = [
{
id: 'workspace-board',
interaction: 'workspace board opened'
},
{
id: 'workspace-board-actions',
interaction: 'workspace board card, lane, density, or status action used'
},
{
id: 'browser',
interaction: 'non-blank browser page viewed'
},
{
id: 'tasks',
interaction: 'Tasks page opened'
},
{
id: 'automations',
interaction: 'Automations page opened'
},
{
id: 'automation-created',
interaction: 'automation created'
},
{
id: 'automation-run',
interaction: 'automation run queued'
},
{
id: 'browser-annotations',
interaction: 'browser annotation added, copied, or cleared'
},
{
id: 'browser-grab',
interaction: 'browser element grab or screenshot used'
},
{
id: 'workspace-creation',
interaction: 'workspace creation flow opened'
},
{
id: 'agent-browser-setup',
interaction: 'Agent Browser Use setup enabled or opened'
},
{
id: 'agent-browser-use',
interaction: 'agent browser runtime method used'
},
{
id: 'agent-orchestration-setup',
interaction: 'Agent Orchestration setup enabled or opened'
},
{
id: 'agent-orchestration',
interaction: 'agent orchestration runtime method used'
},
{
id: 'ai-commit-generation',
interaction: 'AI commit message generation enabled or used'
},
{
id: 'ai-pr-generation',
interaction: 'AI pull request generation used'
},
{
id: 'claude-account-switching',
interaction: 'Claude managed account added, selected, reauthenticated, or removed'
},
{
id: 'computer-use-setup',
interaction: 'Computer Use setup or permission flow opened'
},
{
id: 'computer-use',
interaction: 'computer-use runtime method used'
},
{
id: 'codex-account-switching',
interaction: 'Codex managed account added, selected, reauthenticated, or removed'
},
{
id: 'cookie-import',
interaction: 'browser cookies imported or cleared'
},
{
id: 'floating-workspace',
interaction: 'Floating Workspace opened or configured'
},
{
id: 'mobile-pairing',
interaction: 'mobile pairing enabled or QR code generated'
},
{
id: 'notifications',
interaction: 'desktop notifications enabled or tested'
},
{
id: 'ports',
interaction: 'Ports popover opened, configured, or port action used'
},
{
id: 'quick-commands',
interaction: 'terminal quick command created or edited'
},
{
id: 'resource-manager',
interaction: 'Resource Manager opened or configured'
},
{
id: 'review-notes',
interaction: 'review note added or sent to an agent'
},
{
id: 'ssh',
interaction: 'SSH target added, imported, tested, connected, disconnected, or configured'
},
{
id: 'terminal-panes',
interaction: 'terminal/editor/browser pane created, resized, or merged'
},
{
id: 'terminal-tabs',
interaction: 'workspace tab created, moved, reordered, pinned, renamed, recolored, or closed'
},
{
id: 'tab-splits',
interaction: 'workspace tab split into another pane'
},
{
id: 'usage-tracking',
interaction: 'Stats & Usage or provider usage details opened or configured'
},
{
id: 'voice-dictation',
interaction: 'dictation session started'
},
{
id: 'workspace-cleanup',
interaction: 'workspace disk space scan, review, or cleanup action used'
}
] as const satisfies readonly FeatureInteractionDefinition[]
export const FEATURE_INTERACTION_IDS = FEATURE_INTERACTIONS.map((feature) => feature.id)
export function isFeatureInteractionId(value: unknown): value is FeatureInteractionId {
return (
typeof value === 'string' && FEATURE_INTERACTION_IDS.includes(value as FeatureInteractionId)
)
}
export function hasFeatureInteraction(
state: FeatureInteractionState | null | undefined,
id: FeatureInteractionId
): boolean {
return normalizeFeatureInteractionRecord(state?.[id]) !== null
}
export function normalizeFeatureInteractions(value: unknown): FeatureInteractionState {
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
return {}
}
const input = value as Record<string, unknown>
const out: FeatureInteractionState = {}
for (const id of FEATURE_INTERACTION_IDS) {
const record = normalizeFeatureInteractionRecord(input[id])
if (record) {
out[id] = record
}
}
return out
}
function normalizeFeatureInteractionRecord(value: unknown): FeatureInteractionRecord | null {
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
return null
}
const input = value as Record<string, unknown>
const firstInteractedAt = input.firstInteractedAt
if (
typeof firstInteractedAt !== 'number' ||
!Number.isFinite(firstInteractedAt) ||
firstInteractedAt < 0
) {
return null
}
const rawInteractionCount = input.interactionCount
const interactionCount =
typeof rawInteractionCount === 'number' &&
Number.isInteger(rawInteractionCount) &&
rawInteractionCount > 0
? rawInteractionCount
: 1
return { firstInteractedAt, interactionCount }
}