From 47927d019e57b3ce4fd818d4d36c42c24ff0f85e Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Wed, 10 Jun 2026 13:38:51 -0700 Subject: [PATCH] Add feature interaction usage bucket telemetry (#5119) Co-authored-by: Orca --- config/scripts/localize-renderer-strings.mjs | 8 +- src/main/ipc/telemetry.test.ts | 6 + src/main/ipc/telemetry.ts | 5 +- src/main/persistence.test.ts | 246 ++++++++++++++- src/main/persistence.ts | 102 ++++-- .../src/components/CodexRestartChip.tsx | 12 +- .../src/components/FirstLaunchBanner.tsx | 25 +- .../src/components/GitHubItemDialog.tsx | 12 + .../src/components/GitLabItemDialog.tsx | 295 ++++++++++++++---- .../src/components/JiraIssueWorkspace.tsx | 138 +++++--- .../LinearIssueMarkdownDescriptionEditor.tsx | 87 +++++- .../src/components/LinearIssueTextEditor.tsx | 27 +- .../src/components/LinearIssueWorkspace.tsx | 186 ++++++++--- .../src/components/LinearItemDrawer.tsx | 191 +++++++++--- .../components/NewWorkspaceComposerModal.tsx | 6 +- src/renderer/src/components/QuickOpen.tsx | 56 +++- .../src/components/SelectedTextCopyMenu.tsx | 3 +- src/renderer/src/components/StarNagCard.tsx | 26 +- src/renderer/src/components/TaskPage.tsx | 51 ++- src/renderer/src/components/Terminal.tsx | 61 +++- .../src/components/TerminalSearch.tsx | 12 +- .../src/components/WorktreeJumpPalette.tsx | 26 +- .../activity/ActivityPrototypePage.tsx | 172 ++++++++-- .../activity/ActivityTitlebarControls.tsx | 19 +- .../src/components/agent/AgentCombobox.tsx | 28 +- .../components/agent/AgentSettingsDialog.tsx | 10 +- .../automations/AutomationCustomCronPanel.tsx | 25 +- .../AutomationEditorDialogHeader.tsx | 48 ++- .../AutomationMissedRunGraceField.tsx | 66 +++- .../automations/AutomationPrecheckFields.tsx | 54 +++- .../automations/AutomationRunHistory.tsx | 38 ++- .../automations/AutomationRunPageFrame.tsx | 10 +- .../automations/AutomationSchedulePicker.tsx | 49 ++- .../automations/AutomationSessionField.tsx | 20 +- .../automations/CreateFromPicker.tsx | 33 +- .../ExternalAutomationManagers.tsx | 130 ++++++-- .../ExternalAutomationRunTable.tsx | 57 +++- .../automations/HermesCronOutputView.tsx | 15 +- .../automations/WorkspaceCombobox.tsx | 21 +- .../external-automation-schedule-display.ts | 5 +- .../components/browser-pane/BrowserFind.tsx | 19 +- .../BrowserMobileDriverOverlay.tsx | 26 +- .../components/browser-pane/BrowserPane.tsx | 6 + .../browser-pane/GrabConfirmationSheet.tsx | 72 ++++- .../browser-address-bar-suggestions.ts | 6 +- .../src/components/confirmation-dialog.tsx | 6 +- .../ContextualTourControl.tsx | 18 +- .../ContextualTourOverlaySurface.tsx | 31 +- .../ContextualTourProgressDots.tsx | 10 +- .../contextual-tour-overlay-measurement.ts | 8 +- .../DashboardAgentChildDisclosure.tsx | 10 +- .../dashboard/DashboardAgentRowMessage.tsx | 11 +- .../diff-comments/DiffCommentCard.tsx | 48 ++- .../diff-comments/DiffCommentPopover.tsx | 15 +- .../diff-comments/useDiffCommentDecorator.tsx | 5 +- .../src/components/editor/ChangesModeView.tsx | 19 +- .../components/editor/CodeBlockCopyButton.tsx | 8 +- .../editor/CombinedDiffFileTree.tsx | 45 ++- .../components/editor/CombinedDiffViewer.tsx | 157 ++++++++-- .../components/editor/ConflictComponents.tsx | 92 ++++-- .../editor/ConflictReviewFileTree.tsx | 20 +- .../src/components/editor/CsvViewer.tsx | 12 +- .../components/editor/DiffNotesSendMenu.tsx | 4 +- .../src/components/editor/DiffSectionBody.tsx | 24 +- .../components/editor/DiffSectionHeader.tsx | 2 +- .../src/components/editor/EditorContent.tsx | 77 +++-- .../editor/EditorPanelMarkdownActionsMenu.tsx | 26 +- .../components/editor/EditorPanelShell.tsx | 3 +- .../components/editor/EditorViewToggle.tsx | 16 +- .../src/components/editor/ImageDiffViewer.tsx | 7 +- .../src/components/editor/ImageViewer.tsx | 18 +- .../components/editor/ImageViewerPopup.tsx | 13 +- .../src/components/editor/IpynbViewer.tsx | 104 ++++-- .../src/components/editor/MarkdownPreview.tsx | 129 ++++++-- .../editor/MarkdownTableOfContentsPanel.tsx | 79 ++++- .../editor/MarkdownTemplatePicker.tsx | 35 ++- .../src/components/editor/MermaidBlock.tsx | 5 +- .../src/components/editor/MonacoEditor.tsx | 12 +- .../editor/MonacoGutterContextMenu.tsx | 18 +- .../src/components/editor/NotesSendMenu.tsx | 14 +- .../src/components/editor/PdfFind.tsx | 15 +- .../src/components/editor/PdfViewer.tsx | 16 +- .../editor/ReviewNotesSendMenuContent.tsx | 35 ++- .../editor/RichMarkdownAnnotationOverlay.tsx | 15 +- .../editor/RichMarkdownCodeBlock.tsx | 131 ++++++-- .../editor/RichMarkdownDocLinkMenu.tsx | 23 +- .../editor/RichMarkdownErrorBoundary.tsx | 15 +- .../editor/RichMarkdownLinkBubble.tsx | 20 +- .../editor/RichMarkdownReviewNoteLayer.tsx | 35 ++- .../editor/RichMarkdownReviewRailActions.tsx | 48 ++- .../editor/RichMarkdownSearchBar.tsx | 36 ++- .../editor/RichMarkdownSlashMenu.tsx | 22 +- .../components/editor/RichMarkdownToolbar.tsx | 34 +- .../editor/UntitledFileRenameDialog.tsx | 42 ++- .../editor/export-active-markdown.ts | 22 +- .../components/editor/markdown-rich-mode.ts | 15 +- .../rich-markdown-editor-click-routing.ts | 8 +- .../editor/useContextualCopySetup.tsx | 3 +- .../components/editor/useLocalImagePick.ts | 14 +- .../editor/useRichMarkdownReviewData.ts | 5 +- .../components/emulator-pane/EmulatorPane.tsx | 7 +- .../emulator-pane/emulator-device-frame.tsx | 12 +- .../emulator-pane/emulator-pane-toolbar.tsx | 51 ++- .../emulator-screen-stream-content.tsx | 26 +- .../emulator-unavailable-pane.tsx | 13 +- .../use-emulator-frame-stream.ts | 8 +- .../RecoverableRenderErrorBoundary.tsx | 17 +- ...ture-interaction-writer-boundaries.test.ts | 227 ++++++++++++++ .../feature-tips/CliFeatureTipVisual.tsx | 11 +- .../feature-tips/CliSkillSetupTerminal.tsx | 42 ++- .../CmdJPaletteFeatureTipVisual.tsx | 10 +- .../feature-tips/FeatureTipActions.tsx | 3 +- .../feature-tips/FeatureTipsModal.tsx | 73 ++++- .../feature-wall/BrowserAnimatedVisual.tsx | 117 +++++-- .../feature-wall/BrowserUseSkillSetupCard.tsx | 10 +- .../ComputerUseAnimatedVisual.tsx | 72 ++++- .../feature-wall/EditorAnimatedVisual.tsx | 98 +++++- .../feature-wall/FeatureTourWorkspaceCard.tsx | 8 +- .../feature-wall/FeatureWallBody.tsx | 3 +- .../feature-wall/FeatureWallModal.tsx | 13 +- .../feature-wall/FeatureWallPreview.tsx | 6 +- .../feature-wall/FeatureWallRail.tsx | 20 +- .../FeatureWallSetupChecklist.tsx | 28 +- .../FeatureWallSetupWorkflowActions.tsx | 77 ++++- .../feature-wall/FeatureWallTourPanel.tsx | 6 +- .../feature-wall/ReviewAnimatedVisual.tsx | 4 +- .../ReviewNotesAnimatedVisual.tsx | 49 ++- .../feature-wall/ReviewShipAnimatedVisual.tsx | 109 ++++++- .../feature-wall/TasksAnimatedVisual.tsx | 42 ++- .../feature-wall/WorkbenchAnimatedVisual.tsx | 160 ++++++++-- .../OrchestrationPage.tsx | 12 +- .../UsageAccountsCard.tsx | 58 +++- .../agents-orchestration/UsagePage.tsx | 62 +++- .../orchestration-cards.tsx | 2 +- .../feature-wall-usage-tracking.ts | 25 +- .../review-animated-visual-notes-styles.tsx | 7 +- .../review-animated-visual-pr-view-styles.tsx | 7 +- .../review-animated-visual-shared.tsx | 21 +- .../review-animated-visual-ship-styles.tsx | 7 +- .../feature-wall/review-notes-diff-rows.tsx | 6 +- .../FloatingTerminalIconContextMenu.tsx | 17 +- .../FloatingTerminalOrchestrationDialog.tsx | 44 ++- .../FloatingTerminalToggleButton.tsx | 23 +- .../src/components/github-pr-merge-state.ts | 112 +++++-- .../github-project/ColumnResizeHandle.tsx | 5 +- .../github-project/GhAuthErrorHelp.tsx | 95 +++++- .../components/github-project/ProjectCell.tsx | 135 ++++++-- .../github-project/ProjectGroupHeader.tsx | 8 +- .../github-project/ProjectItemSlugDialog.tsx | 14 +- .../github-project/ProjectPicker.tsx | 101 ++++-- .../components/github-project/ProjectRow.tsx | 20 +- .../github-project/ProjectViewList.tsx | 20 +- .../github-project/ProjectViewWrapper.tsx | 140 +++++++-- .../slug-dialog/AssigneesEditor.tsx | 18 +- .../github-project/slug-dialog/Comments.tsx | 31 +- .../slug-dialog/LabelsEditor.tsx | 18 +- .../slug-dialog/SlugDialogBody.tsx | 47 ++- .../github/GitHubMarkdownComposer.tsx | 18 +- .../components/github/IssueSourceSelector.tsx | 17 +- .../components/github/PRFilterDropdowns.tsx | 23 +- .../src/components/github/PRFilterPickers.tsx | 10 +- .../components/github/PRFilterSections.tsx | 106 +++++-- .../github/github-rate-limit-display.tsx | 67 +++- .../gitlab/gitlab-rate-limit-display.tsx | 50 ++- .../src/components/linear-api-key-dialog.tsx | 47 ++- .../src/components/linear-priority-icon.tsx | 15 +- .../linear-project-view-surfaces.tsx | 251 ++++++++++++--- .../src/components/linear-scope-selector.tsx | 36 ++- .../components/mobile/MobilePageToolbar.tsx | 12 +- .../src/components/mobile/PhoneCarousel.tsx | 24 +- .../components/mobile/mobile-platform-copy.ts | 10 +- .../components/mobile/slides/HomeSlide.tsx | 112 +++++-- .../mobile/slides/TerminalSlide.tsx | 208 +++++++++--- .../mobile/slides/WorktreeListSlide.tsx | 35 ++- .../new-workspace/SmartWorkspaceNameField.tsx | 148 +++++++-- .../onboarding/AgentFeatureSetupStep.tsx | 6 +- .../onboarding/FeatureSetupChecklist.tsx | 27 +- .../onboarding/FeatureSetupInlineTerminal.tsx | 15 +- .../onboarding/IntegrationsStep.tsx | 121 +++++-- .../onboarding/NotificationStep.tsx | 79 ++++- .../components/onboarding/OnboardingFlow.tsx | 68 ++-- .../onboarding/OnboardingFooter.tsx | 9 +- .../OnboardingInlineCommandTerminal.tsx | 6 +- .../OnboardingSkipConfirmationDialog.tsx | 10 +- .../onboarding/OnboardingTourStep.tsx | 6 +- .../src/components/pet/PetOverlay.tsx | 15 +- src/renderer/src/components/pet/pet-models.ts | 6 +- .../components/repo/NestedRepoChecklist.tsx | 14 +- .../repo/NestedRepoScanLimitNotice.tsx | 17 +- .../right-sidebar/BulkActionBar.tsx | 11 +- .../components/right-sidebar/FileExplorer.tsx | 6 +- .../FileExplorerBackgroundMenu.tsx | 12 +- .../right-sidebar/FileExplorerRow.tsx | 108 +++++-- .../right-sidebar/FileExplorerToolbar.tsx | 45 ++- .../right-sidebar/FileExplorerTreeStatus.tsx | 12 +- .../right-sidebar/GitHistoryGraphSvg.tsx | 6 +- .../right-sidebar/SearchFilters.tsx | 16 +- .../components/right-sidebar/SearchHeader.tsx | 26 +- .../right-sidebar/SearchResultItems.tsx | 12 +- .../SourceControlTextGenerationDialog.tsx | 23 +- .../right-sidebar/activity-bar-buttons.tsx | 7 +- .../right-sidebar/checks-panel-empty-state.ts | 86 ++++- .../right-sidebar/gitlab-mr-merge-state.ts | 56 +++- .../right-panel-comment-composer.tsx | 58 +++- ...source-control-ai-commit-failure-launch.ts | 49 ++- .../source-control-discard-confirmation.ts | 50 ++- .../source-control-discard-dialog.tsx | 35 ++- .../right-sidebar/use-source-control-ai.ts | 7 +- .../right-sidebar/useFileDeletion.ts | 37 ++- .../right-sidebar/useFileExplorerHandlers.ts | 14 +- .../right-sidebar/useFileExplorerImport.ts | 16 +- .../right-sidebar/useFileExplorerKeys.ts | 9 +- .../settings/AgentLocationSetting.tsx | 54 +++- .../settings/AgentSkillSetupPanel.tsx | 56 +++- .../components/settings/AppIconSelector.tsx | 12 +- .../settings/AutoRenameBranchPromptEditor.tsx | 65 +++- .../src/components/settings/BaseRefPicker.tsx | 62 +++- .../settings/BrowserDefaultZoomSetting.tsx | 24 +- .../settings/BrowserHomePageSetting.tsx | 38 ++- .../settings/BrowserUseComputerUseNotice.tsx | 19 +- .../settings/BrowserUseEnableSwitch.tsx | 5 +- .../settings/BrowserUseExamples.tsx | 38 ++- .../settings/BrowserUseSkillStep.tsx | 10 +- .../settings/CliSkillRuntimeSetup.tsx | 69 +++- .../settings/FloatingWorkspacePane.tsx | 2 + .../settings/GeneralEditorSettingsSection.tsx | 187 +++++++++-- .../GeneralNetworkSettingsSection.tsx | 72 ++++- .../settings/GeneralSupportSection.tsx | 36 ++- .../settings/GeneralUpdateSettingsSection.tsx | 119 +++++-- .../GeneralWorkspaceSettingsSection.tsx | 109 +++++-- .../settings/GhosttyImportModal.tsx | 75 ++++- .../settings/HiddenExperimentalGroup.tsx | 30 +- .../settings/KagiSessionLinkForm.tsx | 50 ++- .../settings/KeybindingsFileActions.tsx | 73 ++++- .../settings/ManageSessionKillDialog.tsx | 32 +- .../components/settings/McpConfigFileRow.tsx | 7 +- .../MobileEmulatorAgentControlRow.tsx | 70 ++++- .../settings/MobileEmulatorExamples.tsx | 39 ++- .../MobileNetworkInterfaceSection.tsx | 90 +++++- .../settings/OpenAiTranscriptionKeyDialog.tsx | 50 ++- .../OpenAiTranscriptionSettingsRow.tsx | 40 ++- .../settings/OrchestrationExamplesDialog.tsx | 31 +- .../settings/OrchestrationSetupCard.tsx | 10 +- .../OrchestrationSkillAgentCoverage.tsx | 17 +- .../OrchestrationSkillPromptDialog.tsx | 45 ++- .../PrivacyDiagnosticBundleControls.tsx | 39 ++- .../src/components/settings/PrivacyPane.tsx | 40 ++- .../settings/RecentTabOrderControl.tsx | 23 +- ...RepositorySourceControlAiCustomCommand.tsx | 35 ++- .../RepositorySourceControlAiEnablement.tsx | 43 ++- ...orySourceControlAiHostedReviewDefaults.tsx | 66 +++- .../RepositorySourceControlAiSection.tsx | 39 ++- .../settings/RuntimeAccessGrantList.tsx | 67 +++- .../RuntimePairingGeneratedUrlRows.tsx | 6 +- .../settings/ShortcutBindingRow.tsx | 61 +++- .../settings/ShortcutFilterRail.tsx | 30 +- .../components/settings/ShortcutRowsList.tsx | 6 +- .../ShortcutTerminalPolicyControl.tsx | 34 +- .../settings/SshDestructiveActionDialog.tsx | 3 +- .../settings/SshPassphraseDialog.tsx | 68 +++- .../src/components/settings/SshTargetCard.tsx | 62 +++- .../settings/SshTargetDestructiveActions.tsx | 30 +- .../src/components/settings/SshTargetForm.tsx | 109 +++++-- .../src/components/settings/TasksPane.tsx | 42 ++- .../settings/TerminalFontSizeSetting.tsx | 21 +- .../src/components/settings/UIZoomControl.tsx | 3 +- .../settings/WorktreeSymlinksSection.tsx | 79 ++++- .../settings/WslCliRegistration.tsx | 123 ++++++-- .../settings/jira-integration-card.tsx | 96 ++++-- .../setup-guide/SetupGuideModal.tsx | 24 +- .../setup-guide/SetupGuideProgressRing.tsx | 6 +- .../sidebar/AddProjectFromFolderDialog.tsx | 31 +- .../components/sidebar/AddRepoCreateStep.tsx | 62 +++- .../sidebar/AddRepoNestedImportStep.tsx | 69 +++- .../components/sidebar/AddRepoRemoteStep.tsx | 59 +++- .../sidebar/AddRepoServerStartStep.tsx | 118 +++++-- .../sidebar/AddRepoStepIndicator.tsx | 3 +- .../sidebar/AutoRenameFailedDialog.tsx | 37 ++- .../src/components/sidebar/CacheTimer.tsx | 6 +- .../sidebar/CreateProjectLocationField.tsx | 47 ++- .../sidebar/DeleteWorktreeDialogFooter.tsx | 4 +- .../sidebar/DeleteWorktreeDirtyChangeHint.tsx | 6 +- .../sidebar/DeleteWorktreeLineageNotice.tsx | 26 +- .../DeleteWorktreeSkipConfirmOption.tsx | 6 +- .../sidebar/DeleteWorktreeWarningPanels.tsx | 16 +- .../ImportedWorktreesVisibilityLine.tsx | 53 +++- .../components/sidebar/NonGitFolderDialog.tsx | 26 +- .../sidebar/OrcaYamlTrustDialog.tsx | 65 +++- .../components/sidebar/PendingWorktreeRow.tsx | 7 +- .../sidebar/ProjectGroupDeleteDialog.tsx | 25 +- .../sidebar/ProjectGroupNameDialog.tsx | 13 +- .../components/sidebar/RemoteFileBrowser.tsx | 34 +- .../components/sidebar/RemoveFolderDialog.tsx | 22 +- .../ScrollToCurrentWorkspaceToolbarButton.tsx | 11 +- .../sidebar/SetupGuideSidebarEntry.tsx | 13 +- .../sidebar/SetupScriptPromptCardViews.tsx | 106 ++++++- .../sidebar/SidebarFeedbackDialog.tsx | 86 ++++- .../src/components/sidebar/SidebarFilter.tsx | 52 ++- .../SidebarRepositoryFilterSection.tsx | 40 ++- .../sidebar/SidebarSettingsHelpMenu.tsx | 75 ++++- .../src/components/sidebar/SidebarToolbar.tsx | 10 +- .../sidebar/SidebarWorkspaceFilterSection.tsx | 14 +- .../sidebar/SshDisconnectedDialog.tsx | 35 ++- .../src/components/sidebar/SshTargetRow.tsx | 5 +- .../sidebar/WorkspaceKanbanCard.tsx | 2 +- .../sidebar/WorkspaceKanbanDrawerHeader.tsx | 30 +- .../sidebar/WorkspaceKanbanPinDropTarget.tsx | 11 +- .../sidebar/WorkspaceKanbanSettingsMenu.tsx | 45 ++- .../sidebar/WorkspaceKanbanStatusLane.tsx | 8 +- .../components/sidebar/WorktreeCardAgents.tsx | 4 +- .../WorktreeCardMetadataStatusBadges.tsx | 42 ++- .../components/sidebar/WorktreeCardPorts.tsx | 82 ++++- .../sidebar/WorktreeContextMenu.tsx | 97 ++++-- .../components/sidebar/WorktreeOpenInMenu.tsx | 45 ++- .../sidebar/WorktreeTitleInlineRename.tsx | 20 +- .../sidebar/WorktreeVisibilityDialog.tsx | 33 +- .../sidebar/add-repo-local-start-actions.ts | 40 ++- .../sidebar/delete-worktree-flow.ts | 76 +++-- .../sidebar/delete-worktree-toast.ts | 40 ++- .../sidebar/remote-file-browser-helpers.ts | 36 ++- .../sidebar/repo-header-create-state.ts | 35 ++- .../sidebar/sidebar-project-drop.ts | 30 +- .../components/sidebar/sleep-worktree-flow.ts | 10 +- .../components/sidebar/useAddRepoCloneFlow.ts | 12 +- .../sidebar/useAddRepoLocalFolderFlow.ts | 7 +- .../sidebar/useAddRepoNestedImportFlow.ts | 28 +- .../sidebar/useSidebarProjectDrop.ts | 41 ++- .../sidebar/worktree-card-compact-agents.tsx | 22 +- .../sidebar/worktree-list-groups.ts | 12 +- .../src/components/skills/SkillsPage.tsx | 100 ++++-- .../stats/ClaudeUsageDailyChart.tsx | 48 ++- .../components/stats/CodexUsageDailyChart.tsx | 43 ++- .../src/components/stats/ShareUsageButton.tsx | 21 +- .../src/components/stats/ShareUsageCard.tsx | 30 +- .../components/stats/UsageOverviewPane.tsx | 81 ++++- .../src/components/stats/share-card-utils.tsx | 53 +++- .../components/stats/usage-overview-model.ts | 6 +- .../status-bar/PetStatusSegment.tsx | 84 ++++- .../status-bar/SshStatusSegment.tsx | 41 ++- .../status-bar/StatusBarUsageEmptyCta.tsx | 49 ++- .../status-bar/WorkspaceSpaceCompactPanel.tsx | 99 +++++- .../status-bar/ports-status-popover-rows.tsx | 75 ++++- .../src/components/status-bar/tooltip.tsx | 53 +++- .../src/components/tab-bar/BrowserTab.tsx | 28 +- .../src/components/tab-bar/EditorFileTab.tsx | 6 +- .../tab-bar/EditorFileTabContextMenu.tsx | 50 ++- .../components/tab-bar/RecentTabSwitcher.tsx | 8 +- .../src/components/tab-bar/SortableTab.tsx | 16 +- .../tab-bar/SortableTabContextMenu.tsx | 86 +++-- .../tab-bar/TabBarQuickCommandsButton.tsx | 98 +++++- .../src/components/tab-bar/shell-icons.tsx | 6 +- .../components/tab-group/TabGroupPanel.tsx | 40 ++- .../terminal-pane/CloseTerminalDialog.tsx | 19 +- .../terminal-pane/MobileDriverOverlay.tsx | 25 +- .../TerminalAgentSessionForkDialog.tsx | 42 ++- .../terminal-pane/TerminalContextMenu.tsx | 90 +++++- .../terminal-pane/TerminalErrorToast.tsx | 23 +- .../TerminalSessionStateSaveFailureDialog.tsx | 31 +- .../osc52-clipboard-blocked-toast.ts | 47 +-- .../terminal-pane/stale-agent-row.ts | 16 +- .../terminal-agent-session-fork.ts | 53 +++- .../terminal-pane/terminal-drop-handler.ts | 46 ++- .../TerminalQuickCommandActionToggle.tsx | 12 +- .../TerminalQuickCommandAppendEnterSwitch.tsx | 18 +- .../TerminalQuickCommandDialogFooter.tsx | 18 +- .../TerminalQuickCommandLabelField.tsx | 12 +- .../TerminalQuickCommandScopeField.tsx | 39 ++- .../src/components/ui/color-picker.tsx | 16 +- src/renderer/src/components/ui/dialog.tsx | 8 +- .../src/components/ui/repo-multi-combobox.tsx | 31 +- src/renderer/src/components/ui/sheet.tsx | 4 +- .../src/components/ui/team-multi-combobox.tsx | 28 +- .../workspace-space/WorkspaceSpacePage.tsx | 20 +- .../WorktreeCreationPanel.tsx | 35 ++- .../src/hooks/useAutomationDispatchEvents.ts | 20 +- src/renderer/src/hooks/useGlobalFileDrop.ts | 21 +- src/renderer/src/i18n/locales/es.json | 81 ++++- src/renderer/src/i18n/locales/ko.json | 61 +++- src/renderer/src/i18n/locales/zh.json | 61 +++- .../src/lib/agent-skill-cli-prerequisite.ts | 61 +++- src/renderer/src/lib/ensure-simulator-tab.ts | 6 +- .../src/lib/fix-checks-agent-launch.ts | 63 +++- .../lib/floating-workspace-tab-creation.ts | 2 +- .../lib/launch-agent-background-session.ts | 7 +- .../src/lib/launch-agent-in-new-tab.ts | 16 +- .../src/lib/launch-work-item-direct-agent.ts | 6 +- src/renderer/src/lib/local-path-open-guard.ts | 7 +- .../src/lib/resume-sleeping-agent-session.ts | 7 +- .../lib/source-control-agent-action-plan.ts | 40 ++- .../src/lib/source-control-generation-plan.ts | 8 +- src/renderer/src/lib/sparse-preset-draft.ts | 12 +- ...terminal-shortcut-capture-notification.tsx | 45 +-- .../src/lib/workspace-create-error-format.ts | 7 +- src/renderer/src/main.tsx | 5 +- src/renderer/src/store/slices/browser.test.ts | 16 + src/renderer/src/store/slices/browser.ts | 6 +- src/renderer/src/store/slices/editor.test.ts | 18 ++ src/renderer/src/store/slices/editor.ts | 30 +- src/renderer/src/store/slices/repos.ts | 36 ++- .../src/store/slices/sparse-presets.ts | 49 ++- .../src/store/slices/store-test-helpers.ts | 4 +- src/renderer/src/store/slices/ui.test.ts | 61 ++++ src/renderer/src/store/slices/ui.ts | 42 ++- .../src/store/slices/workspace-cleanup.ts | 5 +- src/renderer/src/web/WebConnect.tsx | 28 +- src/renderer/src/web/main.tsx | 5 +- src/renderer/src/web/web-preload-api.test.ts | 42 ++- src/renderer/src/web/web-preload-api.ts | 9 +- .../src/web/web-runtime-environment.ts | 2 +- src/shared/constants.ts | 3 +- src/shared/feature-interaction-catalog.ts | 162 ++++++++++ src/shared/feature-interaction-categories.ts | 79 +++++ .../feature-interaction-usage-buckets.ts | 69 ++++ src/shared/feature-interactions.test.ts | 91 +++++- src/shared/feature-interactions.ts | 243 +++------------ src/shared/telemetry-events.test.ts | 109 +++++++ src/shared/telemetry-events.ts | 48 ++- src/shared/types.ts | 7 +- 418 files changed, 13870 insertions(+), 3196 deletions(-) create mode 100644 src/renderer/src/components/feature-interaction-writer-boundaries.test.ts create mode 100644 src/shared/feature-interaction-catalog.ts create mode 100644 src/shared/feature-interaction-categories.ts create mode 100644 src/shared/feature-interaction-usage-buckets.ts diff --git a/config/scripts/localize-renderer-strings.mjs b/config/scripts/localize-renderer-strings.mjs index 34b01c7370c..459a16ee387 100644 --- a/config/scripts/localize-renderer-strings.mjs +++ b/config/scripts/localize-renderer-strings.mjs @@ -74,7 +74,9 @@ function editForCandidate(candidate, key, translation, sourceFile) { } function sourceKindForPath(filePath) { - return filePath.endsWith('.tsx') || filePath.endsWith('.jsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS + return filePath.endsWith('.tsx') || filePath.endsWith('.jsx') + ? ts.ScriptKind.TSX + : ts.ScriptKind.TS } function findNodeByRange(sourceFile, start, end) { @@ -201,7 +203,9 @@ async function collectCandidateFiles(root) { for (const entry of entries) { const fullPath = path.join(dir, entry.name) if (entry.isDirectory()) { - if (!['.git', 'assets', 'dist', 'node_modules', 'out', '__snapshots__'].includes(entry.name)) { + if ( + !['.git', 'assets', 'dist', 'node_modules', 'out', '__snapshots__'].includes(entry.name) + ) { stack.push(fullPath) } continue diff --git a/src/main/ipc/telemetry.test.ts b/src/main/ipc/telemetry.test.ts index 4d02a8d1df6..162ee86ba7a 100644 --- a/src/main/ipc/telemetry.test.ts +++ b/src/main/ipc/telemetry.test.ts @@ -160,6 +160,12 @@ describe('telemetry IPC handlers', () => { registerWith({ installId: 'x', existedBeforeTelemetryRelease: false, optedIn: true }) const handler = handlers.get('telemetry:track')! handler({}, 'app_starred_orca', { source: 'settings' }) + handler({}, 'feature_interaction_usage_bucket_reached', { + feature_id: 'tasks', + feature_category: 'task_management', + count_bucket: 'count_1', + bucket_source: 'crossed_now' + }) expect(trackMock).not.toHaveBeenCalled() expect(getCohortAtEmitMock).not.toHaveBeenCalled() }) diff --git a/src/main/ipc/telemetry.ts b/src/main/ipc/telemetry.ts index c5fe79b833d..a4faa9d7927 100644 --- a/src/main/ipc/telemetry.ts +++ b/src/main/ipc/telemetry.ts @@ -53,7 +53,10 @@ import type { OptInVia } from '../../shared/telemetry-events' // mirrors how other core-handlers accept the store explicitly. let storeRef: Store | null = null -const MAIN_OWNED_TELEMETRY_EVENTS = new Set(['app_starred_orca']) +const MAIN_OWNED_TELEMETRY_EVENTS = new Set([ + 'app_starred_orca', + 'feature_interaction_usage_bucket_reached' +]) /** * Derive the `via` discriminator for a `telemetry:setOptIn` call from diff --git a/src/main/persistence.test.ts b/src/main/persistence.test.ts index 3dfe7ab4ec2..4f9f9b8179c 100644 --- a/src/main/persistence.test.ts +++ b/src/main/persistence.test.ts @@ -81,6 +81,11 @@ const WORKFLOW_DEFAULT_WORKSPACE_STATUSES = [ { id: 'todo', label: 'Todo', color: 'neutral', icon: 'circle' } ] +const { trackMock, getCohortAtEmitMock } = vi.hoisted(() => ({ + trackMock: vi.fn(), + getCohortAtEmitMock: vi.fn() +})) + vi.mock('electron', () => ({ app: { getPath: () => testState.dir @@ -102,6 +107,14 @@ vi.mock('./git/repo', () => ({ getGitUsername: vi.fn().mockReturnValue('testuser') })) +vi.mock('./telemetry/client', () => ({ + track: trackMock +})) + +vi.mock('./telemetry/cohort-classifier', () => ({ + getCohortAtEmit: getCohortAtEmitMock +})) + /** Reset modules and dynamically import Store so the data-file path picks up the current testState.dir */ async function createStore() { vi.resetModules() @@ -257,6 +270,9 @@ function makeBalancedLegacyPaneLayout(start: number, end: number): TerminalPaneL describe('Store', () => { beforeEach(() => { testState.dir = mkdtempSync(join(tmpdir(), 'orca-test-')) + trackMock.mockReset() + getCohortAtEmitMock.mockReset() + getCohortAtEmitMock.mockReturnValue({ nth_repo_added: 2 }) }) afterEach(() => { @@ -268,7 +284,7 @@ describe('Store', () => { it('returns empty repos when no data file exists', async () => { const store = await createStore() expect(store.getRepos()).toEqual([]) - }) + }, 15_000) it('returns default settings when no data file exists', async () => { const store = await createStore() @@ -3037,6 +3053,61 @@ describe('Store', () => { }) }) + it('normalizes malformed main-owned feature telemetry bucket markers on read', async () => { + writeDataFile({ + schemaVersion: 1, + repos: [], + worktreeMeta: {}, + settings: {}, + ui: {}, + githubCache: { pr: {}, issue: {} }, + workspaceSession: {}, + featureInteractionTelemetryBuckets: { + tasks: 'count_2', + browser: 'count_4', + unknown: 'count_1' + } + }) + + const store = await createStore() + store.flush() + + const persisted = readDataFile() as PersistedState + expect(persisted.featureInteractionTelemetryBuckets).toEqual({ tasks: 'count_2' }) + }) + + it('does not expose or accept UI shadow writes for main-owned feature telemetry markers', async () => { + writeDataFile({ + schemaVersion: 1, + repos: [], + worktreeMeta: {}, + settings: {}, + ui: { + featureInteractionTelemetryBuckets: { tasks: 'count_1000_plus' } + }, + githubCache: { pr: {}, issue: {} }, + workspaceSession: {}, + featureInteractionTelemetryBuckets: { tasks: 'count_2' } + }) + + const store = await createStore() + + expect('featureInteractionTelemetryBuckets' in (store.getUI() as Record)).toBe( + false + ) + + store.updateUI({ + featureInteractionTelemetryBuckets: { tasks: 'count_500_999' } + } as never) + store.flush() + + const persisted = readDataFile() as PersistedState & { + ui: Record + } + expect(persisted.featureInteractionTelemetryBuckets).toEqual({ tasks: 'count_2' }) + expect(persisted.ui.featureInteractionTelemetryBuckets).toBeUndefined() + }) + it('normalizes feature tip ids from direct UI writes', async () => { const store = await createStore() @@ -3068,6 +3139,179 @@ describe('Store', () => { }) }) + it('emits feature interaction telemetry only when a higher bucket is reached', async () => { + const store = await createStore() + + store.recordFeatureInteraction('tasks') + store.recordFeatureInteraction('tasks') + store.recordFeatureInteraction('tasks') + store.recordFeatureInteraction('tasks') + store.flush() + + expect(trackMock).toHaveBeenCalledTimes(3) + expect(trackMock).toHaveBeenNthCalledWith(1, 'feature_interaction_usage_bucket_reached', { + feature_id: 'tasks', + feature_category: 'task_management', + count_bucket: 'count_1', + bucket_source: 'crossed_now', + nth_repo_added: 2 + }) + expect(trackMock).toHaveBeenNthCalledWith(2, 'feature_interaction_usage_bucket_reached', { + feature_id: 'tasks', + feature_category: 'task_management', + count_bucket: 'count_2', + bucket_source: 'crossed_now', + nth_repo_added: 2 + }) + expect(trackMock).toHaveBeenNthCalledWith(3, 'feature_interaction_usage_bucket_reached', { + feature_id: 'tasks', + feature_category: 'task_management', + count_bucket: 'count_3_4', + bucket_source: 'crossed_now', + nth_repo_added: 2 + }) + expect((readDataFile() as PersistedState).featureInteractionTelemetryBuckets).toEqual({ + tasks: 'count_3_4' + }) + }) + + it('emits one observed-existing bucket for pre-rollout interaction counts', async () => { + const store = await createStore() + store.updateUI({ + featureInteractions: { + tasks: { firstInteractedAt: 100, interactionCount: 137 } + } + }) + trackMock.mockClear() + + store.recordFeatureInteraction('tasks') + store.recordFeatureInteraction('tasks') + store.flush() + + expect(trackMock).toHaveBeenCalledTimes(1) + expect(trackMock).toHaveBeenCalledWith('feature_interaction_usage_bucket_reached', { + feature_id: 'tasks', + feature_category: 'task_management', + count_bucket: 'count_100_199', + bucket_source: 'observed_existing', + nth_repo_added: 2 + }) + expect((readDataFile() as PersistedState).featureInteractionTelemetryBuckets).toEqual({ + tasks: 'count_100_199' + }) + }) + + it('emits only the top-coded observed-existing bucket for pre-rollout power users', async () => { + const store = await createStore() + store.updateUI({ + featureInteractions: { + tasks: { firstInteractedAt: 100, interactionCount: 1200 } + } + }) + trackMock.mockClear() + + store.recordFeatureInteraction('tasks') + + expect(trackMock).toHaveBeenCalledTimes(1) + expect(trackMock).toHaveBeenCalledWith('feature_interaction_usage_bucket_reached', { + feature_id: 'tasks', + feature_category: 'task_management', + count_bucket: 'count_1000_plus', + bucket_source: 'observed_existing', + nth_repo_added: 2 + }) + }) + + it('emits high bucket crossings once and ignores same-range increments', async () => { + writeDataFile({ + schemaVersion: 1, + repos: [], + worktreeMeta: {}, + settings: {}, + ui: { + featureInteractions: { + tasks: { firstInteractedAt: 100, interactionCount: 198 } + } + }, + githubCache: { pr: {}, issue: {} }, + workspaceSession: {}, + featureInteractionTelemetryBuckets: { tasks: 'count_100_199' } + }) + const store = await createStore() + + store.recordFeatureInteraction('tasks') + store.recordFeatureInteraction('tasks') + + expect(trackMock).toHaveBeenCalledTimes(1) + expect(trackMock).toHaveBeenCalledWith('feature_interaction_usage_bucket_reached', { + feature_id: 'tasks', + feature_category: 'task_management', + count_bucket: 'count_200_499', + bucket_source: 'crossed_now', + nth_repo_added: 2 + }) + }) + + it('does not emit for count 4 but emits the count_1000_plus crossing', async () => { + const store = await createStore() + + store.recordFeatureInteraction('tasks') + store.recordFeatureInteraction('tasks') + store.recordFeatureInteraction('tasks') + trackMock.mockClear() + + store.recordFeatureInteraction('tasks') + expect(trackMock).not.toHaveBeenCalled() + + writeDataFile({ + schemaVersion: 1, + repos: [], + worktreeMeta: {}, + settings: {}, + ui: { + featureInteractions: { + tasks: { firstInteractedAt: 100, interactionCount: 999 } + } + }, + githubCache: { pr: {}, issue: {} }, + workspaceSession: {}, + featureInteractionTelemetryBuckets: { tasks: 'count_500_999' } + }) + const reloaded = await createStore() + + reloaded.recordFeatureInteraction('tasks') + expect(trackMock).toHaveBeenCalledTimes(1) + expect(trackMock).toHaveBeenCalledWith('feature_interaction_usage_bucket_reached', { + feature_id: 'tasks', + feature_category: 'task_management', + count_bucket: 'count_1000_plus', + bucket_source: 'crossed_now', + nth_repo_added: 2 + }) + }) + + it('dedupes against the persisted bucket marker', async () => { + writeDataFile({ + schemaVersion: 1, + repos: [], + worktreeMeta: {}, + settings: {}, + ui: { + featureInteractions: { + tasks: { firstInteractedAt: 100, interactionCount: 100 } + } + }, + githubCache: { pr: {}, issue: {} }, + workspaceSession: {}, + featureInteractionTelemetryBuckets: { tasks: 'count_100_199' } + }) + const store = await createStore() + + store.recordFeatureInteraction('tasks') + + expect(trackMock).not.toHaveBeenCalled() + }) + it('updateUI restores fixed card properties from direct UI writes', async () => { const store = await createStore() store.updateUI({ worktreeCardProperties: ['inline-agents'] }) diff --git a/src/main/persistence.ts b/src/main/persistence.ts index 395049ca32b..71aeb870e83 100644 --- a/src/main/persistence.ts +++ b/src/main/persistence.ts @@ -94,7 +94,11 @@ import { normalizeTerminalShortcutPolicy } from '../shared/keybindings' import { normalizeAppIconId } from '../shared/app-icon' import { normalizeTerminalCustomThemes } from '../shared/terminal-custom-themes' import { + compareFeatureInteractionUsageBuckets, + getFeatureInteractionCategory, + getFeatureInteractionUsageBucket, normalizeFeatureInteractions, + normalizeFeatureInteractionTelemetryBuckets, type FeatureInteractionId } from '../shared/feature-interactions' import { normalizeContextualTourIds } from '../shared/contextual-tours' @@ -134,6 +138,8 @@ import { migrateWorkspaceSessionTerminalScrollbackSnapshots, readTerminalScrollbackSnapshotSync } from './terminal-scrollback-snapshots' +import { track } from './telemetry/client' +import { getCohortAtEmit } from './telemetry/cohort-classifier' function encrypt(plaintext: string): string { if (!plaintext || !safeStorage.isEncryptionAvailable()) { @@ -324,6 +330,21 @@ function mergeContextualTourSeenIds( return [...merged] } +function stripMainOwnedTelemetryMarkerFromUI( + value: Partial | undefined +): Partial { + if (!value || typeof value !== 'object') { + return {} + } + const { featureInteractionTelemetryBuckets: _reserved, ...ui } = value as Partial< + PersistedState['ui'] + > & { + featureInteractionTelemetryBuckets?: unknown + } + void _reserved + return ui +} + function normalizeSortBy(sortBy: unknown): PersistedState['ui']['sortBy'] { if ( sortBy === 'smart' || @@ -1900,6 +1921,9 @@ export class Store { result = { ...defaults, ...parsed, + featureInteractionTelemetryBuckets: normalizeFeatureInteractionTelemetryBuckets( + parsed.featureInteractionTelemetryBuckets + ), projectGroups: normalizeProjectGroups(parsed.projectGroups), worktreeLineageById: parsed.worktreeLineageById ?? {}, settings: { @@ -2106,7 +2130,7 @@ export class Store { } return { ...defaults.ui, - ...parsed.ui, + ...stripMainOwnedTelemetryMarkerFromUI(parsed.ui), // Why: migrate once from the retired Appearance setting only // when no explicit persisted chrome preference exists yet. rightSidebarOpen, @@ -3175,9 +3199,10 @@ export class Store { // ── UI State ─────────────────────────────────────────────────────── getUI(): PersistedState['ui'] { + const uiState = stripMainOwnedTelemetryMarkerFromUI(this.state.ui) return { ...getDefaultUIState(), - ...this.state.ui, + ...uiState, groupBy: normalizeGroupBy(this.state.ui?.groupBy), sortBy: normalizeSortBy(this.state.ui?.sortBy), projectOrderBy: normalizeProjectOrderBy(this.state.ui?.projectOrderBy), @@ -3206,39 +3231,44 @@ export class Store { } updateUI(updates: Partial): void { + const sanitizedUpdates = stripMainOwnedTelemetryMarkerFromUI(updates) + const currentUI = { + ...getDefaultUIState(), + ...stripMainOwnedTelemetryMarkerFromUI(this.state.ui) + } this.state.ui = { - ...this.state.ui, - ...updates, - groupBy: updates.groupBy - ? normalizeGroupBy(updates.groupBy) + ...currentUI, + ...sanitizedUpdates, + groupBy: sanitizedUpdates.groupBy + ? normalizeGroupBy(sanitizedUpdates.groupBy) : normalizeGroupBy(this.state.ui?.groupBy), - sortBy: updates.sortBy - ? normalizeSortBy(updates.sortBy) + sortBy: sanitizedUpdates.sortBy + ? normalizeSortBy(sanitizedUpdates.sortBy) : normalizeSortBy(this.state.ui?.sortBy), projectOrderBy: updates.projectOrderBy ? normalizeProjectOrderBy(updates.projectOrderBy) : normalizeProjectOrderBy(this.state.ui?.projectOrderBy), rightSidebarTab: - updates.rightSidebarTab !== undefined - ? normalizeRightSidebarTab(updates.rightSidebarTab) + sanitizedUpdates.rightSidebarTab !== undefined + ? normalizeRightSidebarTab(sanitizedUpdates.rightSidebarTab) : normalizeRightSidebarTab(this.state.ui?.rightSidebarTab), worktreeCardProperties: - updates.worktreeCardProperties !== undefined - ? normalizeWorktreeCardProperties(updates.worktreeCardProperties) + sanitizedUpdates.worktreeCardProperties !== undefined + ? normalizeWorktreeCardProperties(sanitizedUpdates.worktreeCardProperties) : normalizeWorktreeCardProperties(this.state.ui?.worktreeCardProperties), agentActivityDisplayMode: updates.agentActivityDisplayMode !== undefined ? normalizeAgentActivityDisplayMode(updates.agentActivityDisplayMode) : normalizeAgentActivityDisplayMode(this.state.ui?.agentActivityDisplayMode), workspaceStatuses: - updates.workspaceStatuses !== undefined - ? normalizeWorkspaceStatuses(updates.workspaceStatuses) + sanitizedUpdates.workspaceStatuses !== undefined + ? normalizeWorkspaceStatuses(sanitizedUpdates.workspaceStatuses) : normalizeWorkspaceStatuses(this.state.ui?.workspaceStatuses), workspaceBoardOpacity: clampWorkspaceBoardOpacity( - updates.workspaceBoardOpacity ?? this.state.ui?.workspaceBoardOpacity + sanitizedUpdates.workspaceBoardOpacity ?? this.state.ui?.workspaceBoardOpacity ), workspaceBoardColumnWidth: clampWorkspaceBoardColumnWidth( - updates.workspaceBoardColumnWidth ?? this.state.ui?.workspaceBoardColumnWidth + sanitizedUpdates.workspaceBoardColumnWidth ?? this.state.ui?.workspaceBoardColumnWidth ), browserDefaultZoomLevel: normalizeBrowserPageZoomLevel( updates.browserDefaultZoomLevel ?? this.state.ui?.browserDefaultZoomLevel @@ -3248,8 +3278,8 @@ export class Store { ? normalizeShowDotfilesByWorktree(updates.showDotfilesByWorktree) : normalizeShowDotfilesByWorktree(this.state.ui?.showDotfilesByWorktree), featureTipsSeenIds: - updates.featureTipsSeenIds !== undefined - ? normalizeFeatureTipIds(updates.featureTipsSeenIds) + sanitizedUpdates.featureTipsSeenIds !== undefined + ? normalizeFeatureTipIds(sanitizedUpdates.featureTipsSeenIds) : normalizeFeatureTipIds(this.state.ui?.featureTipsSeenIds), // Why: renderer and paired clients can mark different tours seen from // stale UI snapshots; union them so completed tours stay suppressed. @@ -3264,10 +3294,10 @@ export class Store { // Merge instead of replacing so a stale renderer snapshot cannot erase // runtime-only feature interactions. featureInteractions: - updates.featureInteractions !== undefined + sanitizedUpdates.featureInteractions !== undefined ? mergeFeatureInteractions( this.state.ui?.featureInteractions, - updates.featureInteractions + sanitizedUpdates.featureInteractions ) : normalizeFeatureInteractions(this.state.ui?.featureInteractions) } @@ -3276,16 +3306,46 @@ export class Store { recordFeatureInteraction(id: FeatureInteractionId): PersistedState['ui'] { const featureInteractions = normalizeFeatureInteractions(this.state.ui?.featureInteractions) + const telemetryBuckets = normalizeFeatureInteractionTelemetryBuckets( + this.state.featureInteractionTelemetryBuckets + ) const existing = featureInteractions[id] + const previousCount = existing?.interactionCount ?? 0 + const nextCount = previousCount + 1 + const previousBucket = getFeatureInteractionUsageBucket(previousCount) + const nextBucket = getFeatureInteractionUsageBucket(nextCount) + const lastEmittedBucket = telemetryBuckets[id] ?? null + const shouldEmit = + nextBucket !== null && + (lastEmittedBucket === null || + compareFeatureInteractionUsageBuckets(nextBucket, lastEmittedBucket) > 0) + this.updateUI({ featureInteractions: { ...featureInteractions, [id]: { firstInteractedAt: existing?.firstInteractedAt ?? Date.now(), - interactionCount: (existing?.interactionCount ?? 0) + 1 + interactionCount: nextCount } } }) + this.state.featureInteractionTelemetryBuckets = shouldEmit + ? { ...telemetryBuckets, [id]: nextBucket } + : telemetryBuckets + this.scheduleSave() + + if (shouldEmit) { + track('feature_interaction_usage_bucket_reached', { + feature_id: id, + feature_category: getFeatureInteractionCategory(id), + count_bucket: nextBucket, + bucket_source: + lastEmittedBucket === null && previousBucket !== null && previousBucket === nextBucket + ? 'observed_existing' + : 'crossed_now', + ...getCohortAtEmit() + }) + } return this.getUI() } diff --git a/src/renderer/src/components/CodexRestartChip.tsx b/src/renderer/src/components/CodexRestartChip.tsx index 89c42cb2398..d2455f0c7e0 100644 --- a/src/renderer/src/components/CodexRestartChip.tsx +++ b/src/renderer/src/components/CodexRestartChip.tsx @@ -74,7 +74,11 @@ export default function CodexRestartChip({
- {translate("auto.components.CodexRestartChip.9263e75f49", "Codex is using the previous account")} + {translate( + 'auto.components.CodexRestartChip.9263e75f49', + 'Codex is using the previous account' + )} +
+ {translate('auto.components.CodexRestartChip.c72a5fb234', 'Restart')} + + {translate('auto.components.CodexRestartChip.9132779820', 'Dismiss')} +
diff --git a/src/renderer/src/components/FirstLaunchBanner.tsx b/src/renderer/src/components/FirstLaunchBanner.tsx index 9a44ed3a6ed..2237770e2f2 100644 --- a/src/renderer/src/components/FirstLaunchBanner.tsx +++ b/src/renderer/src/components/FirstLaunchBanner.tsx @@ -128,21 +128,30 @@ export function FirstLaunchBanner({
{/* Text column — title + body stack on the left, takes remaining width so the action column never pushes copy into a wrap. */}
-

{translate("auto.components.FirstLaunchBanner.9784b4d7bc", "Help us decide what to build next")}

+

+ {translate( + 'auto.components.FirstLaunchBanner.9784b4d7bc', + 'Help us decide what to build next' + )} +

- {translate("auto.components.FirstLaunchBanner.958d2cc31b", "Anonymous counts of which features you use help us prioritize what to build. No file contents, prompts, terminal output, or anything that identifies you. Change anytime in Settings -> Privacy & Telemetry.")}{' '} + {translate( + 'auto.components.FirstLaunchBanner.958d2cc31b', + 'Anonymous counts of which features you use help us prioritize what to build. No file contents, prompts, terminal output, or anything that identifies you. Change anytime in Settings -> Privacy & Telemetry.' + )}{' '} + {translate('auto.components.FirstLaunchBanner.d1deebb050', 'Privacy policy')} + .

@@ -160,15 +169,17 @@ export function FirstLaunchBanner({ disabled={inFlight} className="border-border/60 text-muted-foreground" > - {translate("auto.components.FirstLaunchBanner.fc5cc29955", "Opt out")} + {translate('auto.components.FirstLaunchBanner.fc5cc29955', 'Opt out')} + + {translate('auto.components.FirstLaunchBanner.94cc673726', 'Got it')} +
{/* aria-label says "Dismiss" — the action persists silent opt-in, not just hides the UI. */} ) : null} {comment.createdAt ? new Date(comment.createdAt).toLocaleDateString() : ''} @@ -260,7 +264,8 @@ function PipelineJobRow({ className="h-6" > {retrying ? : null} - {translate("auto.components.GitLabItemDialog.fa3e042203", "Retry")} + {translate('auto.components.GitLabItemDialog.fa3e042203', 'Retry')} + ) : null} {job.webUrl ? ( @@ -278,19 +283,23 @@ function PipelineJobRow({ {expanded ? (
- {translate("auto.components.GitLabItemDialog.2f9b27f838", "Job log")} + {translate('auto.components.GitLabItemDialog.2f9b27f838', 'Job log')} + {translate('auto.components.GitLabItemDialog.028bde664e', 'Hide')} +
{traceState?.loading ? (
- {translate("auto.components.GitLabItemDialog.d600c2619a", "Loading log")}
+ {translate('auto.components.GitLabItemDialog.d600c2619a', 'Loading log')} +
) : traceState?.error ? (
{traceState.error}
) : (
-              {traceState?.trace?.trim() ? traceState.trace : translate("auto.components.GitLabItemDialog.32f8bef818", "No log output.")}
+              {traceState?.trace?.trim()
+                ? traceState.trace
+                : translate('auto.components.GitLabItemDialog.32f8bef818', 'No log output.')}
             
)} @@ -484,7 +493,7 @@ export default function GitLabItemDialog({ const nextBody = bodyDraft const nextLabels = parseGitLabLabelDraft(labelDraft) if (!nextTitle) { - toast.error(translate("auto.components.GitLabItemDialog.98718490e4", "MR title is required.")) + toast.error(translate('auto.components.GitLabItemDialog.98718490e4', 'MR title is required.')) return } @@ -531,6 +540,7 @@ export default function GitLabItemDialog({ setTitleDraft('') setBodyDraft('') setLabelDraft('') + useAppStore.getState().recordFeatureInteraction('gitlab-tasks') } } else if (mountedRef.current) { toast.error(res.error) @@ -611,7 +621,11 @@ export default function GitLabItemDialog({ return } if (result.ok) { - toast.success(translate("auto.components.GitLabItemDialog.f7cb495a12", "Retried {{value0}}", { value0: job.name })) + toast.success( + translate('auto.components.GitLabItemDialog.f7cb495a12', 'Retried {{value0}}', { + value0: job.name + }) + ) if (result.job) { setDetails((current) => current @@ -646,7 +660,12 @@ export default function GitLabItemDialog({ .map((reviewer) => reviewer.id) .filter((id): id is number => typeof id === 'number') if (reviewerIds.length !== nextReviewers.length) { - toast.error(translate("auto.components.GitLabItemDialog.ceaf7c30c7", "Reviewer id is unavailable for this GitLab user.")) + toast.error( + translate( + 'auto.components.GitLabItemDialog.ceaf7c30c7', + 'Reviewer id is unavailable for this GitLab user.' + ) + ) return } setReviewerUpdating(true) @@ -668,6 +687,7 @@ export default function GitLabItemDialog({ setReviewerOptions((current) => current ? dedupeGitLabUsers([...current, ...result.reviewers]) : current ) + useAppStore.getState().recordFeatureInteraction('gitlab-tasks') } else { toast.error(result.error) } @@ -688,11 +708,21 @@ export default function GitLabItemDialog({ const line = Number.parseInt(inlineCommentLine, 10) const body = inlineCommentBody.trim() if (!file || !Number.isFinite(line) || line <= 0 || !body) { - toast.error(translate("auto.components.GitLabItemDialog.00d0d25825", "File, line, and comment are required.")) + toast.error( + translate( + 'auto.components.GitLabItemDialog.00d0d25825', + 'File, line, and comment are required.' + ) + ) return } if (!details.baseSha || !details.startSha || !details.headSha) { - toast.error(translate("auto.components.GitLabItemDialog.ffdd9a78e1", "MR diff refs are unavailable for inline comments.")) + toast.error( + translate( + 'auto.components.GitLabItemDialog.ffdd9a78e1', + 'MR diff refs are unavailable for inline comments.' + ) + ) return } setInlineCommentSubmitting(true) @@ -719,7 +749,10 @@ export default function GitLabItemDialog({ current ? { ...current, comments: [...current.comments, result.comment] } : current ) setInlineCommentBody('') - toast.success(translate("auto.components.GitLabItemDialog.60c13320c4", "Inline comment added")) + useAppStore.getState().recordFeatureInteraction('gitlab-tasks') + toast.success( + translate('auto.components.GitLabItemDialog.60c13320c4', 'Inline comment added') + ) } else { toast.error(result.error) } @@ -747,7 +780,12 @@ export default function GitLabItemDialog({ const res = await window.api.gl.closeMR({ repoPath, iid: item.number }) if (res.ok) { if (mountedRef.current) { - toast.success(translate("auto.components.GitLabItemDialog.9b11cd233f", "Closed MR !{{value0}}", { value0: item.number })) + useAppStore.getState().recordFeatureInteraction('gitlab-tasks') + toast.success( + translate('auto.components.GitLabItemDialog.9b11cd233f', 'Closed MR !{{value0}}', { + value0: item.number + }) + ) handleRefresh() } } else { @@ -771,7 +809,12 @@ export default function GitLabItemDialog({ const res = await window.api.gl.reopenMR({ repoPath, iid: item.number }) if (res.ok) { if (mountedRef.current) { - toast.success(translate("auto.components.GitLabItemDialog.865ea2703e", "Reopened MR !{{value0}}", { value0: item.number })) + useAppStore.getState().recordFeatureInteraction('gitlab-tasks') + toast.success( + translate('auto.components.GitLabItemDialog.865ea2703e', 'Reopened MR !{{value0}}', { + value0: item.number + }) + ) handleRefresh() } } else { @@ -795,7 +838,12 @@ export default function GitLabItemDialog({ const res = await window.api.gl.mergeMR({ repoPath, iid: item.number }) if (res.ok) { if (mountedRef.current) { - toast.success(translate("auto.components.GitLabItemDialog.e089f62594", "Merged MR !{{value0}}", { value0: item.number })) + useAppStore.getState().recordFeatureInteraction('gitlab-tasks') + toast.success( + translate('auto.components.GitLabItemDialog.e089f62594', 'Merged MR !{{value0}}', { + value0: item.number + }) + ) handleRefresh() } } else { @@ -828,6 +876,7 @@ export default function GitLabItemDialog({ setCommentDraftState((current) => current.itemId === itemId ? { itemId, value: '' } : current ) + useAppStore.getState().recordFeatureInteraction('gitlab-tasks') handleRefresh() } } else { @@ -867,6 +916,7 @@ export default function GitLabItemDialog({ } : current ) + useAppStore.getState().recordFeatureInteraction('gitlab-tasks') } } else if (mountedRef.current) { toast.error(res.error) @@ -907,8 +957,14 @@ export default function GitLabItemDialog({ !open && onClose()}> - {item ? visibleTitle : translate("auto.components.GitLabItemDialog.3a051b8ade", "Work item")} - {translate("auto.components.GitLabItemDialog.30c97083c2", "GitLab work item detail")} + + {item + ? visibleTitle + : translate('auto.components.GitLabItemDialog.3a051b8ade', 'Work item')} + + + {translate('auto.components.GitLabItemDialog.30c97083c2', 'GitLab work item detail')} + {item ? ( @@ -923,7 +979,12 @@ export default function GitLabItemDialog({ {item.number} - {item.author ? {translate("auto.components.GitLabItemDialog.9bfb4a24d7", "by")}{item.author} : null} + {item.author ? ( + + {translate('auto.components.GitLabItemDialog.9bfb4a24d7', 'by')} + {item.author} + + ) : null}

{visibleTitle} @@ -944,7 +1005,7 @@ export default function GitLabItemDialog({ + {translate('auto.components.GitLabItemDialog.cb55b0390f', 'Manage')} +
{currentReviewers.length > 0 ? ( @@ -1043,14 +1126,23 @@ export default function GitLabItemDialog({ ) } className="rounded-full p-0.5 text-muted-foreground hover:bg-muted hover:text-foreground disabled:opacity-50" - aria-label={translate("auto.components.GitLabItemDialog.1b19cdc510", "Remove reviewer {{value0}}", { value0: reviewer.username })} + aria-label={translate( + 'auto.components.GitLabItemDialog.1b19cdc510', + 'Remove reviewer {{value0}}', + { value0: reviewer.username } + )} > )) ) : ( - {translate("auto.components.GitLabItemDialog.474b50d988", "No reviewers.")} + + {translate( + 'auto.components.GitLabItemDialog.474b50d988', + 'No reviewers.' + )} + )}
{reviewerOptions ? ( @@ -1061,7 +1153,12 @@ export default function GitLabItemDialog({ onChange={(event) => setReviewerDraftId(event.target.value)} className="h-8 min-w-0 flex-1 rounded-md border border-input bg-background px-2 text-xs text-foreground" > - + {reviewerOptionRows.map((reviewer) => (