From 286c21005b0be89f204fb75034980803525dc1ce Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sun, 30 Aug 2026 20:26:45 -0700 Subject: [PATCH] refactor(renderer): split oversized UI surfaces (cherry picked from commit da89be434509f8ba5c4156c4a8193ef0bcdbc70e) --- src/renderer/src/app-startup-routing.test.ts | 10 +- src/renderer/src/components/TaskPage.tsx | 3179 +------------- .../src/components/TaskPageContent.tsx | 75 + src/renderer/src/components/TaskPageFrame.tsx | 19 + .../TaskPageGitHubAssigneesCell.tsx | 295 ++ .../src/components/TaskPageGitHubAvatars.tsx | 54 + .../components/TaskPageGitHubChecksCell.tsx | 86 + .../TaskPageGitHubDuplicatePicker.tsx | 118 + .../src/components/TaskPageGitHubFilters.tsx | 256 ++ .../components/TaskPageGitHubIssueDialog.tsx | 241 ++ .../TaskPageGitHubIssueSelectors.tsx | 207 + .../src/components/TaskPageGitHubList.tsx | 320 ++ .../components/TaskPageGitHubMergeCell.tsx | 266 ++ .../components/TaskPageGitHubModeControls.tsx | 150 + .../components/TaskPageGitHubReviewCell.tsx | 345 ++ .../TaskPageGitHubReviewerPicker.tsx | 222 + .../src/components/TaskPageGitHubRows.tsx | 388 ++ .../components/TaskPageGitHubStatusCell.tsx | 396 ++ .../src/components/TaskPageGitLabDialog.tsx | 29 + .../src/components/TaskPageGitLabFilters.tsx | 167 + .../src/components/TaskPageGitLabItemList.tsx | 148 + .../src/components/TaskPageGitLabTodoList.tsx | 104 + .../components/TaskPageJiraConnectDialog.tsx | 10 + .../src/components/TaskPageJiraContent.tsx | 158 + .../src/components/TaskPageJiraFilters.tsx | 183 + .../components/TaskPageJiraIssueDialog.tsx | 380 ++ .../TaskPageLinearConnectDialog.tsx | 23 + .../src/components/TaskPageLinearContent.tsx | 79 + .../TaskPageLinearCustomViewList.tsx | 56 + .../TaskPageLinearCustomViewProjects.tsx | 102 + .../src/components/TaskPageLinearFilters.tsx | 316 ++ .../TaskPageLinearIssueAttributes.tsx | 372 ++ .../components/TaskPageLinearIssueBoard.tsx | 185 + .../TaskPageLinearIssueBoardColumns.tsx | 227 + .../components/TaskPageLinearIssueDialog.tsx | 171 + .../components/TaskPageLinearIssueList.tsx | 133 + .../components/TaskPageLinearIssueRows.tsx | 9 + .../components/TaskPageLinearIssueTable.tsx | 280 ++ .../components/TaskPageLinearIssueToolbar.tsx | 197 + .../TaskPageLinearProjectDialog.tsx | 159 + .../TaskPageLinearProjectFields.tsx | 375 ++ .../components/TaskPageLinearProjectList.tsx | 63 + .../TaskPageLinearProjectOverview.tsx | 73 + .../src/components/TaskPageListChrome.tsx | 37 + .../src/components/TaskPagePaginationBar.tsx | 87 + .../components/TaskPageProviderFilters.tsx | 23 + .../src/components/TaskPageSourceBar.tsx | 231 + .../src/components/TaskPageSurface.tsx | 34 + src/renderer/src/components/Terminal.tsx | 2968 +------------ .../components/TerminalLegacyBrowserPanes.tsx | 59 + .../TerminalLegacyEditorSurface.tsx | 28 + .../TerminalLegacyTerminalPanes.tsx | 112 + .../TerminalLegacyWorkspaceSurface.tsx | 21 + .../TerminalSplitWorkspaceSurfaces.tsx | 69 + .../src/components/TerminalSurface.tsx | 28 + .../src/components/TerminalTitlebarTabs.tsx | 105 + .../components/TerminalWorkspaceDialogs.tsx | 117 + .../TerminalWorktreeSplitSurface.tsx | 96 + .../src/components/WorktreeJumpPalette.tsx | 3856 +---------------- .../automations/AutomationsListPanel.tsx | 4 + .../automations/AutomationsPage.tsx | 2834 +----------- .../automations/AutomationsPageSurface.tsx | 381 ++ .../automations/automation-hermes-save.ts | 89 + .../automation-management-actions.ts | 102 + .../automation-orca-save-operations.ts | 264 ++ .../automations/automation-orca-save.ts | 288 ++ .../automations/automation-run-actions.ts | 122 + ...automation-run-completion-evidence.test.ts | 88 + .../automation-run-completion-evidence.ts | 38 + .../automation-run-workspace-action.ts | 71 + .../automations/automation-save-action.ts | 118 + .../automations/automation-save-context.ts | 17 + .../automations-page-action-context.ts | 21 + .../use-automation-draft-effects.ts | 179 + .../use-automation-editor-actions.ts | 263 ++ .../use-automation-run-page-state.ts | 236 + .../use-automation-source-availability.ts | 13 + .../use-automations-page-controller.ts | 109 + .../use-automations-page-destination-form.ts | 237 + .../use-automations-page-destination-state.ts | 214 + .../use-automations-page-escape.ts | 54 + .../use-automations-page-list-state.ts | 161 + .../use-automations-page-local-state.ts | 229 + ...use-automations-page-presentation-state.ts | 121 + .../use-automations-page-refresh.ts | 166 + .../use-automations-page-setup-state.ts | 199 + .../use-automations-page-store-state.ts | 98 + .../use-external-automation-actions.ts | 155 + .../palette-activation-focus-routing.test.ts | 16 +- .../src/components/editor/MarkdownPreview.tsx | 2181 +--------- .../MarkdownPreviewAnnotationComposer.tsx | 144 + .../components/editor/MarkdownPreviewBody.tsx | 90 + .../editor/MarkdownPreviewReviewToolbar.tsx | 80 + .../editor/MarkdownPreviewSearchBar.tsx | 100 + .../editor/MarkdownPreviewSurface.tsx | 83 + .../markdown-preview-anchor-navigation.ts | 16 + .../editor/markdown-preview-block-model.ts | 51 + .../editor/markdown-preview-editor-reveal.ts | 42 + .../editor/markdown-preview-link-actions.ts | 263 ++ .../editor/markdown-preview-source-routing.ts | 175 + .../markdown-preview-system-link-action.ts | 93 + .../editor/markdown-preview-types.ts | 44 + ...-markdown-preview-annotation-renderers.tsx | 255 ++ .../use-markdown-preview-components.tsx | 300 ++ .../editor/use-markdown-preview-foundation.ts | 151 + .../use-markdown-preview-review-actions.ts | 206 + .../use-markdown-preview-scroll-viewport.ts | 73 + .../use-markdown-preview-source-foundation.ts | 205 + .../editor/use-markdown-preview-viewport.ts | 321 ++ ...ture-interaction-writer-boundaries.test.ts | 49 +- .../FloatingTerminalEmptyState.tsx | 144 + .../FloatingTerminalOrchestrationCard.tsx | 55 + .../FloatingTerminalPanel.tsx | 2266 +--------- .../FloatingTerminalPanelSurface.tsx | 349 ++ .../FloatingTerminalSaveDialog.tsx | 92 + .../floating-terminal-focus-reporting.ts | 36 + .../floating-terminal-panel-drag-actions.ts | 77 + .../floating-terminal-panel-drag-target.ts | 6 + .../floating-terminal-panel-initial-bounds.ts | 54 + .../floating-terminal-panel-types.ts | 28 + .../use-floating-terminal-close-actions.ts | 224 + .../use-floating-terminal-create-actions.ts | 152 + ...se-floating-terminal-editor-close-queue.ts | 121 + .../use-floating-terminal-focus-lifecycle.ts | 108 + ...ting-terminal-global-shortcut-listeners.ts | 150 + .../use-floating-terminal-guest-bridge.ts | 49 + ...floating-terminal-initial-focus-effects.ts | 35 + ...oating-terminal-orchestration-dismissal.ts | 18 + ...ating-terminal-orchestration-visibility.ts | 58 + .../use-floating-terminal-panel-controller.ts | 100 + ...e-floating-terminal-panel-focus-reclaim.ts | 116 + .../use-floating-terminal-panel-geometry.ts | 130 + .../use-floating-terminal-panel-items.ts | 225 + ...use-floating-terminal-panel-local-state.ts | 107 + .../use-floating-terminal-panel-maximize.ts | 116 + .../use-floating-terminal-panel-shortcuts.ts | 247 ++ ...use-floating-terminal-panel-store-state.ts | 64 + .../use-floating-terminal-shortcut-details.ts | 17 + ...rkspaceNameField-source-boundaries.test.ts | 119 +- .../new-workspace/SmartWorkspaceNameField.tsx | 2532 +---------- .../smart-workspace-cross-repo-dialog.tsx | 92 + .../smart-workspace-github-direct-link.ts | 111 + .../smart-workspace-name-field-copy.ts | 119 + .../smart-workspace-name-field-model.ts | 105 + .../smart-workspace-name-field-surface.tsx | 228 + .../smart-workspace-name-input-surface.tsx | 307 ++ .../smart-workspace-provider-availability.ts | 17 + .../smart-workspace-repo-slug.ts | 61 + ...smart-workspace-source-results-surface.tsx | 156 + .../smart-workspace-source-row-content.tsx | 168 + .../use-smart-workspace-field-availability.ts | 255 ++ ...se-smart-workspace-field-focus-controls.ts | 94 + .../use-smart-workspace-github-search.ts | 240 + .../use-smart-workspace-gitlab-search.ts | 173 + .../use-smart-workspace-name-field-actions.ts | 235 + ...e-smart-workspace-name-field-controller.ts | 151 + ...e-smart-workspace-name-field-foundation.ts | 201 + ...smart-workspace-name-field-presentation.ts | 301 ++ .../use-smart-workspace-name-field-state.ts | 99 + .../use-smart-workspace-secondary-searches.ts | 259 ++ .../refactor-hook-contract.test-support.ts | 163 + .../status-bar/ClaudeSwitcherMenu.tsx | 323 ++ .../status-bar/CodexSwitcherMenu.tsx | 298 ++ .../status-bar/InlineProviderUsage.tsx | 140 + .../status-bar/ProviderDetailsMenu.tsx | 150 + .../status-bar/ResourceUsageAppSection.tsx | 98 + .../status-bar/ResourceUsageKillDialog.tsx | 98 + .../status-bar/ResourceUsageMetrics.tsx | 130 + .../ResourceUsagePopoverContent.tsx | 378 ++ .../status-bar/ResourceUsageTree.tsx | 127 + .../status-bar/ResourceUsageTrigger.tsx | 95 + .../status-bar/ResourceUsageWorktreeRow.tsx | 260 ++ .../src/components/status-bar/StatusBar.tsx | 2615 +---------- .../status-bar/StatusBarAccountControls.tsx | 116 + .../status-bar/StatusBarProviderSegment.tsx | 253 ++ .../status-bar/StatusBarSurface.tsx | 300 ++ .../status-bar/StatusBarVisibilityMenu.tsx | 169 + .../status-bar/WorkspaceSpaceManagerPanel.tsx | 2319 +--------- .../status-bar/codex-switcher-projection.ts | 32 + .../status-bar/resource-usage-sort.ts | 47 + .../status-bar/status-bar-claude-accounts.ts | 184 + .../status-bar/status-bar-codex-accounts.ts | 186 + .../status-bar-container-observer.ts | 12 + .../status-bar/status-bar-runtime-targets.ts | 99 + .../use-codex-switcher-controller.ts | 325 ++ .../status-bar/use-resource-usage-actions.ts | 2 + .../use-resource-usage-foundation.ts | 213 + .../use-resource-usage-projection.ts | 150 + .../status-bar/use-status-bar-controller.ts | 273 ++ ...use-workspace-space-decision-projection.ts | 133 + .../use-workspace-space-git-refresh-action.ts | 109 + .../use-workspace-space-manager-bindings.ts | 122 + .../use-workspace-space-manager-panel.ts | 239 + .../use-workspace-space-manager-projection.ts | 215 + .../workspace-space-breakdown-list.tsx | 127 + .../workspace-space-decision-details.ts | 204 + .../workspace-space-decision-hover-card.tsx | 265 ++ .../workspace-space-manager-overview.tsx | 180 + ...pace-space-manager-source-boundary.test.ts | 79 + .../workspace-space-manager-state-types.ts | 15 + .../workspace-space-manager-table.tsx | 173 + .../workspace-space-manager-toolbar.tsx | 188 + .../workspace-space-manager-view.tsx | 20 + .../status-bar/workspace-space-metrics.tsx | 67 + .../workspace-space-presentation.test.ts | 138 +- .../workspace-space-selection-controls.tsx | 67 + .../workspace-space-state-resolution.ts | 37 + .../workspace-space-status-badge.tsx | 126 + .../status-bar/workspace-space-treemap.tsx | 187 + .../workspace-space-worktree-row.tsx | 197 + .../components/task-page-draft-storage.tsx | 56 + .../task-page-github-issue-creation.test.ts | 14 +- .../task-page-github-landing-refresh-run.tsx | 398 ++ .../task-page-github-quiet-refresh-run.tsx | 314 ++ .../task-page-github-review-model.tsx | 78 + .../task-page-github-reviewer-actions.ts | 254 ++ .../task-page-linear-issue-model.tsx | 267 ++ .../task-page-linear-jira-list-model.tsx | 194 + .../components/task-page-source-context.tsx | 176 + .../task-page-source-family.test-support.ts | 65 + .../task-page-source-switch-boundary.test.ts | 80 +- .../task-page-task-creation-drafts.test.ts | 22 +- ...k-page-workspace-composer-boundary.test.ts | 35 +- .../components/terminal-agent-tab-shortcut.ts | 42 + .../components/terminal-cold-activation.ts | 172 + .../components/terminal-pane/TerminalPane.tsx | 3507 +-------------- .../TerminalPaneNativeChatPortal.tsx | 91 + .../TerminalPaneRuntimePortals.tsx | 197 + .../terminal-pane/TerminalPaneSurface.tsx | 309 ++ .../TerminalQuickCommandEditorDialog.tsx | 32 + ...nal-ios-hangul-preedit-coexistence.test.ts | 2 +- .../terminal-pane-hook-order-parity.test.ts | 108 + .../terminal-pane-layout-restore.ts | 84 + .../terminal-pane-lifecycle-close.ts | 103 + .../terminal-pane-lifecycle-primitives.ts | 283 ++ .../terminal-pane-lifecycle-types.ts | 132 + ...erminal-pane-listener-order-parity.test.ts | 94 + .../terminal-pane-manager-options.ts | 176 + .../terminal-pane-mount-bootstrap.ts | 47 + .../terminal-pane-mount-cleanup.ts | 139 + .../terminal-pane-mount-context.ts | 57 + .../terminal-pane-mount-events.ts | 118 + .../terminal-pane-mount-preparation.ts | 294 ++ .../terminal-pane-pane-closed.ts | 169 + .../terminal-pane-pane-created.ts | 226 + .../terminal-pane/terminal-pane-pane-input.ts | 205 + .../terminal-pane/terminal-pane-pane-links.ts | 206 + .../terminal-pane-paste-execution.ts | 150 + .../terminal-pane-paste-listeners.ts | 248 ++ .../terminal-pane/terminal-pane-pty-deps.ts | 112 + .../terminal-pane/terminal-pane-types.ts | 18 + .../use-terminal-pane-chat-state.ts | 268 ++ .../use-terminal-pane-close-actions.ts | 226 + .../use-terminal-pane-context-actions.ts | 245 ++ .../use-terminal-pane-controller.ts | 41 + .../use-terminal-pane-foundation.ts | 231 + .../use-terminal-pane-global-listeners.ts | 247 ++ .../use-terminal-pane-layout-bindings.ts | 173 + .../use-terminal-pane-layout-persistence.ts | 226 + .../use-terminal-pane-lifecycle-refs.ts | 36 + .../use-terminal-pane-lifecycle-stage.ts | 147 + .../use-terminal-pane-lifecycle.ts | 2494 +---------- .../use-terminal-pane-mobile-actions.ts | 254 ++ .../use-terminal-pane-mount-lifecycle.ts | 131 + .../use-terminal-pane-paste-listeners.ts | 40 + .../use-terminal-pane-process-exit-actions.ts | 254 ++ .../use-terminal-pane-projection.ts | 202 + .../use-terminal-pane-reconciliation.ts | 154 + .../use-terminal-pane-startup-actions.ts | 233 + .../use-terminal-pane-store-bindings.ts | 85 + .../use-terminal-pane-title-effects.ts | 255 ++ .../use-terminal-pane-title-state.ts | 155 + .../terminal-parking-pass-candidates.ts | 152 + .../components/terminal-workspace-keydown.ts | 268 ++ .../components/terminal-workspace-model.ts | 106 + .../terminal/initial-terminal-wiring.test.ts | 13 +- .../use-task-page-composer-actions.ts | 264 ++ .../use-task-page-detail-routing.ts | 241 ++ ...e-task-page-github-cache-reconciliation.ts | 160 + .../components/use-task-page-github-detail.ts | 292 ++ .../use-task-page-github-issue-creation.ts | 196 + .../use-task-page-github-issue-draft.ts | 169 + .../use-task-page-github-landing-refresh.ts | 35 + .../use-task-page-github-list-projection.ts | 201 + .../use-task-page-github-list-state.ts | 290 ++ .../use-task-page-github-mutation-state.ts | 171 + ...e-task-page-github-quiet-refresh-effect.ts | 17 + .../use-task-page-github-quiet-refresh.ts | 42 + .../use-task-page-github-search-pagination.ts | 223 + .../use-task-page-gitlab-loading.ts | 194 + .../use-task-page-global-effects.ts | 124 + .../use-task-page-jira-creation-metadata.ts | 132 + .../use-task-page-jira-creation-projects.ts | 179 + .../use-task-page-jira-creation-state.ts | 182 + .../use-task-page-jira-issue-creation.ts | 135 + .../use-task-page-jira-list-effects.ts | 169 + .../use-task-page-jira-list-projection.ts | 70 + .../use-task-page-jira-list-state.ts | 141 + .../components/use-task-page-linear-board.ts | 258 ++ ...use-task-page-linear-collection-effects.ts | 204 + .../use-task-page-linear-context-actions.ts | 245 ++ .../use-task-page-linear-context-state.ts | 129 + .../use-task-page-linear-creation-state.ts | 305 ++ ...se-task-page-linear-custom-view-effects.ts | 210 + .../use-task-page-linear-filter-selection.ts | 158 + .../use-task-page-linear-in-orca-effects.ts | 143 + .../use-task-page-linear-issue-creation.ts | 162 + .../use-task-page-linear-list-effects.ts | 293 ++ .../use-task-page-linear-list-presentation.ts | 100 + .../use-task-page-linear-list-projection.ts | 248 ++ .../use-task-page-linear-list-selection.ts | 239 + .../use-task-page-linear-project-creation.ts | 151 + .../use-task-page-linear-view-state.ts | 251 ++ .../use-task-page-provider-metadata.ts | 127 + .../use-task-page-provider-state.ts | 174 + .../use-task-page-repo-selection.ts | 266 ++ .../use-task-page-resume-restoration.ts | 239 + .../components/use-task-page-runtime-hosts.ts | 197 + .../use-task-page-search-actions.ts | 257 ++ .../use-task-page-source-availability.ts | 175 + .../use-task-page-source-summary.ts | 189 + .../use-task-page-store-bindings.ts | 155 + .../use-task-page-workspace-actions.ts | 131 + .../use-terminal-activation-actions.ts | 67 + .../use-terminal-browser-retention.ts | 106 + .../use-terminal-bulk-close-actions.ts | 152 + .../components/use-terminal-close-actions.ts | 120 + .../src/components/use-terminal-controller.ts | 40 + .../components/use-terminal-create-actions.ts | 215 + ...se-terminal-editor-close-dialog-actions.ts | 153 + .../use-terminal-editor-close-foundation.ts | 89 + .../use-terminal-editor-close-queue.ts | 137 + .../use-terminal-keyboard-shortcuts.ts | 54 + .../use-terminal-parking-foundation.ts | 171 + .../components/use-terminal-parking-pass.ts | 174 + .../use-terminal-watcher-effects.ts | 200 + .../use-terminal-window-lifecycle.ts | 117 + .../use-terminal-workspace-foundation.ts | 94 + .../use-terminal-workspace-projection.ts | 110 + .../use-terminal-workspace-store-bindings.ts | 118 + .../use-worktree-jump-palette-controller.ts | 127 + ...use-worktree-jump-palette-create-action.ts | 225 + .../use-worktree-jump-palette-filter.ts | 126 + .../use-worktree-jump-palette-list-entries.ts | 230 + .../use-worktree-jump-palette-local-state.ts | 65 + .../use-worktree-jump-palette-open-tabs.ts | 275 ++ ...e-worktree-jump-palette-project-targets.ts | 134 + ...use-worktree-jump-palette-quick-actions.ts | 148 + .../use-worktree-jump-palette-recent-tabs.ts | 200 + .../use-worktree-jump-palette-sections.ts | 134 + ...worktree-jump-palette-selection-actions.ts | 265 ++ ...rktree-jump-palette-selection-lifecycle.ts | 237 + .../use-worktree-jump-palette-store-state.ts | 170 + .../use-worktree-jump-palette-worktrees.ts | 226 + .../inactive-workspace-estimate.ts | 38 + .../workspace-surface-projection.test.ts | 53 +- ...ee-jump-palette-browser-simulator-rows.tsx | 177 + .../worktree-jump-palette-empty-state.ts | 71 + .../worktree-jump-palette-entry.tsx | 78 + .../components/worktree-jump-palette-model.ts | 108 + .../worktree-jump-palette-primitives.tsx | 173 + ...ktree-jump-palette-project-action-rows.tsx | 114 + .../worktree-jump-palette-recent-inclusion.ts | 49 + ...ktree-jump-palette-sleeping-filter.test.ts | 16 +- ...mp-palette-source-context-boundary.test.ts | 20 +- ...rktree-jump-palette-source.test-support.ts | 6 + .../worktree-jump-palette-surface.tsx | 166 + ...orktree-jump-palette-workspace-tab-row.tsx | 103 + .../worktree-jump-palette-worktree-row.tsx | 135 + src/renderer/src/lib/cmd-j-match-relevance.ts | 96 + 370 files changed, 53838 insertions(+), 30703 deletions(-) create mode 100644 src/renderer/src/components/TaskPageContent.tsx create mode 100644 src/renderer/src/components/TaskPageFrame.tsx create mode 100644 src/renderer/src/components/TaskPageGitHubAssigneesCell.tsx create mode 100644 src/renderer/src/components/TaskPageGitHubAvatars.tsx create mode 100644 src/renderer/src/components/TaskPageGitHubChecksCell.tsx create mode 100644 src/renderer/src/components/TaskPageGitHubDuplicatePicker.tsx create mode 100644 src/renderer/src/components/TaskPageGitHubFilters.tsx create mode 100644 src/renderer/src/components/TaskPageGitHubIssueDialog.tsx create mode 100644 src/renderer/src/components/TaskPageGitHubIssueSelectors.tsx create mode 100644 src/renderer/src/components/TaskPageGitHubList.tsx create mode 100644 src/renderer/src/components/TaskPageGitHubMergeCell.tsx create mode 100644 src/renderer/src/components/TaskPageGitHubModeControls.tsx create mode 100644 src/renderer/src/components/TaskPageGitHubReviewCell.tsx create mode 100644 src/renderer/src/components/TaskPageGitHubReviewerPicker.tsx create mode 100644 src/renderer/src/components/TaskPageGitHubRows.tsx create mode 100644 src/renderer/src/components/TaskPageGitHubStatusCell.tsx create mode 100644 src/renderer/src/components/TaskPageGitLabDialog.tsx create mode 100644 src/renderer/src/components/TaskPageGitLabFilters.tsx create mode 100644 src/renderer/src/components/TaskPageGitLabItemList.tsx create mode 100644 src/renderer/src/components/TaskPageGitLabTodoList.tsx create mode 100644 src/renderer/src/components/TaskPageJiraConnectDialog.tsx create mode 100644 src/renderer/src/components/TaskPageJiraContent.tsx create mode 100644 src/renderer/src/components/TaskPageJiraFilters.tsx create mode 100644 src/renderer/src/components/TaskPageJiraIssueDialog.tsx create mode 100644 src/renderer/src/components/TaskPageLinearConnectDialog.tsx create mode 100644 src/renderer/src/components/TaskPageLinearContent.tsx create mode 100644 src/renderer/src/components/TaskPageLinearCustomViewList.tsx create mode 100644 src/renderer/src/components/TaskPageLinearCustomViewProjects.tsx create mode 100644 src/renderer/src/components/TaskPageLinearFilters.tsx create mode 100644 src/renderer/src/components/TaskPageLinearIssueAttributes.tsx create mode 100644 src/renderer/src/components/TaskPageLinearIssueBoard.tsx create mode 100644 src/renderer/src/components/TaskPageLinearIssueBoardColumns.tsx create mode 100644 src/renderer/src/components/TaskPageLinearIssueDialog.tsx create mode 100644 src/renderer/src/components/TaskPageLinearIssueList.tsx create mode 100644 src/renderer/src/components/TaskPageLinearIssueRows.tsx create mode 100644 src/renderer/src/components/TaskPageLinearIssueTable.tsx create mode 100644 src/renderer/src/components/TaskPageLinearIssueToolbar.tsx create mode 100644 src/renderer/src/components/TaskPageLinearProjectDialog.tsx create mode 100644 src/renderer/src/components/TaskPageLinearProjectFields.tsx create mode 100644 src/renderer/src/components/TaskPageLinearProjectList.tsx create mode 100644 src/renderer/src/components/TaskPageLinearProjectOverview.tsx create mode 100644 src/renderer/src/components/TaskPageListChrome.tsx create mode 100644 src/renderer/src/components/TaskPagePaginationBar.tsx create mode 100644 src/renderer/src/components/TaskPageProviderFilters.tsx create mode 100644 src/renderer/src/components/TaskPageSourceBar.tsx create mode 100644 src/renderer/src/components/TaskPageSurface.tsx create mode 100644 src/renderer/src/components/TerminalLegacyBrowserPanes.tsx create mode 100644 src/renderer/src/components/TerminalLegacyEditorSurface.tsx create mode 100644 src/renderer/src/components/TerminalLegacyTerminalPanes.tsx create mode 100644 src/renderer/src/components/TerminalLegacyWorkspaceSurface.tsx create mode 100644 src/renderer/src/components/TerminalSplitWorkspaceSurfaces.tsx create mode 100644 src/renderer/src/components/TerminalSurface.tsx create mode 100644 src/renderer/src/components/TerminalTitlebarTabs.tsx create mode 100644 src/renderer/src/components/TerminalWorkspaceDialogs.tsx create mode 100644 src/renderer/src/components/TerminalWorktreeSplitSurface.tsx create mode 100644 src/renderer/src/components/automations/AutomationsPageSurface.tsx create mode 100644 src/renderer/src/components/automations/automation-hermes-save.ts create mode 100644 src/renderer/src/components/automations/automation-management-actions.ts create mode 100644 src/renderer/src/components/automations/automation-orca-save-operations.ts create mode 100644 src/renderer/src/components/automations/automation-orca-save.ts create mode 100644 src/renderer/src/components/automations/automation-run-actions.ts create mode 100644 src/renderer/src/components/automations/automation-run-completion-evidence.test.ts create mode 100644 src/renderer/src/components/automations/automation-run-completion-evidence.ts create mode 100644 src/renderer/src/components/automations/automation-run-workspace-action.ts create mode 100644 src/renderer/src/components/automations/automation-save-action.ts create mode 100644 src/renderer/src/components/automations/automation-save-context.ts create mode 100644 src/renderer/src/components/automations/automations-page-action-context.ts create mode 100644 src/renderer/src/components/automations/use-automation-draft-effects.ts create mode 100644 src/renderer/src/components/automations/use-automation-editor-actions.ts create mode 100644 src/renderer/src/components/automations/use-automation-run-page-state.ts create mode 100644 src/renderer/src/components/automations/use-automation-source-availability.ts create mode 100644 src/renderer/src/components/automations/use-automations-page-controller.ts create mode 100644 src/renderer/src/components/automations/use-automations-page-destination-form.ts create mode 100644 src/renderer/src/components/automations/use-automations-page-destination-state.ts create mode 100644 src/renderer/src/components/automations/use-automations-page-escape.ts create mode 100644 src/renderer/src/components/automations/use-automations-page-list-state.ts create mode 100644 src/renderer/src/components/automations/use-automations-page-local-state.ts create mode 100644 src/renderer/src/components/automations/use-automations-page-presentation-state.ts create mode 100644 src/renderer/src/components/automations/use-automations-page-refresh.ts create mode 100644 src/renderer/src/components/automations/use-automations-page-setup-state.ts create mode 100644 src/renderer/src/components/automations/use-automations-page-store-state.ts create mode 100644 src/renderer/src/components/automations/use-external-automation-actions.ts create mode 100644 src/renderer/src/components/editor/MarkdownPreviewAnnotationComposer.tsx create mode 100644 src/renderer/src/components/editor/MarkdownPreviewBody.tsx create mode 100644 src/renderer/src/components/editor/MarkdownPreviewReviewToolbar.tsx create mode 100644 src/renderer/src/components/editor/MarkdownPreviewSearchBar.tsx create mode 100644 src/renderer/src/components/editor/MarkdownPreviewSurface.tsx create mode 100644 src/renderer/src/components/editor/markdown-preview-anchor-navigation.ts create mode 100644 src/renderer/src/components/editor/markdown-preview-block-model.ts create mode 100644 src/renderer/src/components/editor/markdown-preview-editor-reveal.ts create mode 100644 src/renderer/src/components/editor/markdown-preview-link-actions.ts create mode 100644 src/renderer/src/components/editor/markdown-preview-source-routing.ts create mode 100644 src/renderer/src/components/editor/markdown-preview-system-link-action.ts create mode 100644 src/renderer/src/components/editor/markdown-preview-types.ts create mode 100644 src/renderer/src/components/editor/use-markdown-preview-annotation-renderers.tsx create mode 100644 src/renderer/src/components/editor/use-markdown-preview-components.tsx create mode 100644 src/renderer/src/components/editor/use-markdown-preview-foundation.ts create mode 100644 src/renderer/src/components/editor/use-markdown-preview-review-actions.ts create mode 100644 src/renderer/src/components/editor/use-markdown-preview-scroll-viewport.ts create mode 100644 src/renderer/src/components/editor/use-markdown-preview-source-foundation.ts create mode 100644 src/renderer/src/components/editor/use-markdown-preview-viewport.ts create mode 100644 src/renderer/src/components/floating-terminal/FloatingTerminalEmptyState.tsx create mode 100644 src/renderer/src/components/floating-terminal/FloatingTerminalOrchestrationCard.tsx create mode 100644 src/renderer/src/components/floating-terminal/FloatingTerminalPanelSurface.tsx create mode 100644 src/renderer/src/components/floating-terminal/FloatingTerminalSaveDialog.tsx create mode 100644 src/renderer/src/components/floating-terminal/floating-terminal-focus-reporting.ts create mode 100644 src/renderer/src/components/floating-terminal/floating-terminal-panel-drag-actions.ts create mode 100644 src/renderer/src/components/floating-terminal/floating-terminal-panel-drag-target.ts create mode 100644 src/renderer/src/components/floating-terminal/floating-terminal-panel-initial-bounds.ts create mode 100644 src/renderer/src/components/floating-terminal/floating-terminal-panel-types.ts create mode 100644 src/renderer/src/components/floating-terminal/use-floating-terminal-close-actions.ts create mode 100644 src/renderer/src/components/floating-terminal/use-floating-terminal-create-actions.ts create mode 100644 src/renderer/src/components/floating-terminal/use-floating-terminal-editor-close-queue.ts create mode 100644 src/renderer/src/components/floating-terminal/use-floating-terminal-focus-lifecycle.ts create mode 100644 src/renderer/src/components/floating-terminal/use-floating-terminal-global-shortcut-listeners.ts create mode 100644 src/renderer/src/components/floating-terminal/use-floating-terminal-guest-bridge.ts create mode 100644 src/renderer/src/components/floating-terminal/use-floating-terminal-initial-focus-effects.ts create mode 100644 src/renderer/src/components/floating-terminal/use-floating-terminal-orchestration-dismissal.ts create mode 100644 src/renderer/src/components/floating-terminal/use-floating-terminal-orchestration-visibility.ts create mode 100644 src/renderer/src/components/floating-terminal/use-floating-terminal-panel-controller.ts create mode 100644 src/renderer/src/components/floating-terminal/use-floating-terminal-panel-focus-reclaim.ts create mode 100644 src/renderer/src/components/floating-terminal/use-floating-terminal-panel-geometry.ts create mode 100644 src/renderer/src/components/floating-terminal/use-floating-terminal-panel-items.ts create mode 100644 src/renderer/src/components/floating-terminal/use-floating-terminal-panel-local-state.ts create mode 100644 src/renderer/src/components/floating-terminal/use-floating-terminal-panel-maximize.ts create mode 100644 src/renderer/src/components/floating-terminal/use-floating-terminal-panel-shortcuts.ts create mode 100644 src/renderer/src/components/floating-terminal/use-floating-terminal-panel-store-state.ts create mode 100644 src/renderer/src/components/floating-terminal/use-floating-terminal-shortcut-details.ts create mode 100644 src/renderer/src/components/new-workspace/smart-workspace-cross-repo-dialog.tsx create mode 100644 src/renderer/src/components/new-workspace/smart-workspace-github-direct-link.ts create mode 100644 src/renderer/src/components/new-workspace/smart-workspace-name-field-copy.ts create mode 100644 src/renderer/src/components/new-workspace/smart-workspace-name-field-model.ts create mode 100644 src/renderer/src/components/new-workspace/smart-workspace-name-field-surface.tsx create mode 100644 src/renderer/src/components/new-workspace/smart-workspace-name-input-surface.tsx create mode 100644 src/renderer/src/components/new-workspace/smart-workspace-provider-availability.ts create mode 100644 src/renderer/src/components/new-workspace/smart-workspace-repo-slug.ts create mode 100644 src/renderer/src/components/new-workspace/smart-workspace-source-results-surface.tsx create mode 100644 src/renderer/src/components/new-workspace/smart-workspace-source-row-content.tsx create mode 100644 src/renderer/src/components/new-workspace/use-smart-workspace-field-availability.ts create mode 100644 src/renderer/src/components/new-workspace/use-smart-workspace-field-focus-controls.ts create mode 100644 src/renderer/src/components/new-workspace/use-smart-workspace-github-search.ts create mode 100644 src/renderer/src/components/new-workspace/use-smart-workspace-gitlab-search.ts create mode 100644 src/renderer/src/components/new-workspace/use-smart-workspace-name-field-actions.ts create mode 100644 src/renderer/src/components/new-workspace/use-smart-workspace-name-field-controller.ts create mode 100644 src/renderer/src/components/new-workspace/use-smart-workspace-name-field-foundation.ts create mode 100644 src/renderer/src/components/new-workspace/use-smart-workspace-name-field-presentation.ts create mode 100644 src/renderer/src/components/new-workspace/use-smart-workspace-name-field-state.ts create mode 100644 src/renderer/src/components/new-workspace/use-smart-workspace-secondary-searches.ts create mode 100644 src/renderer/src/components/refactor-hook-contract.test-support.ts create mode 100644 src/renderer/src/components/status-bar/ClaudeSwitcherMenu.tsx create mode 100644 src/renderer/src/components/status-bar/CodexSwitcherMenu.tsx create mode 100644 src/renderer/src/components/status-bar/InlineProviderUsage.tsx create mode 100644 src/renderer/src/components/status-bar/ProviderDetailsMenu.tsx create mode 100644 src/renderer/src/components/status-bar/ResourceUsageAppSection.tsx create mode 100644 src/renderer/src/components/status-bar/ResourceUsageKillDialog.tsx create mode 100644 src/renderer/src/components/status-bar/ResourceUsageMetrics.tsx create mode 100644 src/renderer/src/components/status-bar/ResourceUsagePopoverContent.tsx create mode 100644 src/renderer/src/components/status-bar/ResourceUsageTree.tsx create mode 100644 src/renderer/src/components/status-bar/ResourceUsageTrigger.tsx create mode 100644 src/renderer/src/components/status-bar/ResourceUsageWorktreeRow.tsx create mode 100644 src/renderer/src/components/status-bar/StatusBarAccountControls.tsx create mode 100644 src/renderer/src/components/status-bar/StatusBarProviderSegment.tsx create mode 100644 src/renderer/src/components/status-bar/StatusBarSurface.tsx create mode 100644 src/renderer/src/components/status-bar/StatusBarVisibilityMenu.tsx create mode 100644 src/renderer/src/components/status-bar/codex-switcher-projection.ts create mode 100644 src/renderer/src/components/status-bar/resource-usage-sort.ts create mode 100644 src/renderer/src/components/status-bar/status-bar-claude-accounts.ts create mode 100644 src/renderer/src/components/status-bar/status-bar-codex-accounts.ts create mode 100644 src/renderer/src/components/status-bar/status-bar-container-observer.ts create mode 100644 src/renderer/src/components/status-bar/status-bar-runtime-targets.ts create mode 100644 src/renderer/src/components/status-bar/use-codex-switcher-controller.ts create mode 100644 src/renderer/src/components/status-bar/use-resource-usage-foundation.ts create mode 100644 src/renderer/src/components/status-bar/use-resource-usage-projection.ts create mode 100644 src/renderer/src/components/status-bar/use-status-bar-controller.ts create mode 100644 src/renderer/src/components/status-bar/use-workspace-space-decision-projection.ts create mode 100644 src/renderer/src/components/status-bar/use-workspace-space-git-refresh-action.ts create mode 100644 src/renderer/src/components/status-bar/use-workspace-space-manager-bindings.ts create mode 100644 src/renderer/src/components/status-bar/use-workspace-space-manager-panel.ts create mode 100644 src/renderer/src/components/status-bar/use-workspace-space-manager-projection.ts create mode 100644 src/renderer/src/components/status-bar/workspace-space-breakdown-list.tsx create mode 100644 src/renderer/src/components/status-bar/workspace-space-decision-details.ts create mode 100644 src/renderer/src/components/status-bar/workspace-space-decision-hover-card.tsx create mode 100644 src/renderer/src/components/status-bar/workspace-space-manager-overview.tsx create mode 100644 src/renderer/src/components/status-bar/workspace-space-manager-source-boundary.test.ts create mode 100644 src/renderer/src/components/status-bar/workspace-space-manager-state-types.ts create mode 100644 src/renderer/src/components/status-bar/workspace-space-manager-table.tsx create mode 100644 src/renderer/src/components/status-bar/workspace-space-manager-toolbar.tsx create mode 100644 src/renderer/src/components/status-bar/workspace-space-manager-view.tsx create mode 100644 src/renderer/src/components/status-bar/workspace-space-metrics.tsx create mode 100644 src/renderer/src/components/status-bar/workspace-space-selection-controls.tsx create mode 100644 src/renderer/src/components/status-bar/workspace-space-state-resolution.ts create mode 100644 src/renderer/src/components/status-bar/workspace-space-status-badge.tsx create mode 100644 src/renderer/src/components/status-bar/workspace-space-treemap.tsx create mode 100644 src/renderer/src/components/status-bar/workspace-space-worktree-row.tsx create mode 100644 src/renderer/src/components/task-page-draft-storage.tsx create mode 100644 src/renderer/src/components/task-page-github-landing-refresh-run.tsx create mode 100644 src/renderer/src/components/task-page-github-quiet-refresh-run.tsx create mode 100644 src/renderer/src/components/task-page-github-review-model.tsx create mode 100644 src/renderer/src/components/task-page-github-reviewer-actions.ts create mode 100644 src/renderer/src/components/task-page-linear-issue-model.tsx create mode 100644 src/renderer/src/components/task-page-linear-jira-list-model.tsx create mode 100644 src/renderer/src/components/task-page-source-context.tsx create mode 100644 src/renderer/src/components/task-page-source-family.test-support.ts create mode 100644 src/renderer/src/components/terminal-agent-tab-shortcut.ts create mode 100644 src/renderer/src/components/terminal-cold-activation.ts create mode 100644 src/renderer/src/components/terminal-pane/TerminalPaneNativeChatPortal.tsx create mode 100644 src/renderer/src/components/terminal-pane/TerminalPaneRuntimePortals.tsx create mode 100644 src/renderer/src/components/terminal-pane/TerminalPaneSurface.tsx create mode 100644 src/renderer/src/components/terminal-pane/TerminalQuickCommandEditorDialog.tsx create mode 100644 src/renderer/src/components/terminal-pane/terminal-pane-hook-order-parity.test.ts create mode 100644 src/renderer/src/components/terminal-pane/terminal-pane-layout-restore.ts create mode 100644 src/renderer/src/components/terminal-pane/terminal-pane-lifecycle-close.ts create mode 100644 src/renderer/src/components/terminal-pane/terminal-pane-lifecycle-primitives.ts create mode 100644 src/renderer/src/components/terminal-pane/terminal-pane-lifecycle-types.ts create mode 100644 src/renderer/src/components/terminal-pane/terminal-pane-listener-order-parity.test.ts create mode 100644 src/renderer/src/components/terminal-pane/terminal-pane-manager-options.ts create mode 100644 src/renderer/src/components/terminal-pane/terminal-pane-mount-bootstrap.ts create mode 100644 src/renderer/src/components/terminal-pane/terminal-pane-mount-cleanup.ts create mode 100644 src/renderer/src/components/terminal-pane/terminal-pane-mount-context.ts create mode 100644 src/renderer/src/components/terminal-pane/terminal-pane-mount-events.ts create mode 100644 src/renderer/src/components/terminal-pane/terminal-pane-mount-preparation.ts create mode 100644 src/renderer/src/components/terminal-pane/terminal-pane-pane-closed.ts create mode 100644 src/renderer/src/components/terminal-pane/terminal-pane-pane-created.ts create mode 100644 src/renderer/src/components/terminal-pane/terminal-pane-pane-input.ts create mode 100644 src/renderer/src/components/terminal-pane/terminal-pane-pane-links.ts create mode 100644 src/renderer/src/components/terminal-pane/terminal-pane-paste-execution.ts create mode 100644 src/renderer/src/components/terminal-pane/terminal-pane-paste-listeners.ts create mode 100644 src/renderer/src/components/terminal-pane/terminal-pane-pty-deps.ts create mode 100644 src/renderer/src/components/terminal-pane/terminal-pane-types.ts create mode 100644 src/renderer/src/components/terminal-pane/use-terminal-pane-chat-state.ts create mode 100644 src/renderer/src/components/terminal-pane/use-terminal-pane-close-actions.ts create mode 100644 src/renderer/src/components/terminal-pane/use-terminal-pane-context-actions.ts create mode 100644 src/renderer/src/components/terminal-pane/use-terminal-pane-controller.ts create mode 100644 src/renderer/src/components/terminal-pane/use-terminal-pane-foundation.ts create mode 100644 src/renderer/src/components/terminal-pane/use-terminal-pane-global-listeners.ts create mode 100644 src/renderer/src/components/terminal-pane/use-terminal-pane-layout-bindings.ts create mode 100644 src/renderer/src/components/terminal-pane/use-terminal-pane-layout-persistence.ts create mode 100644 src/renderer/src/components/terminal-pane/use-terminal-pane-lifecycle-refs.ts create mode 100644 src/renderer/src/components/terminal-pane/use-terminal-pane-lifecycle-stage.ts create mode 100644 src/renderer/src/components/terminal-pane/use-terminal-pane-mobile-actions.ts create mode 100644 src/renderer/src/components/terminal-pane/use-terminal-pane-mount-lifecycle.ts create mode 100644 src/renderer/src/components/terminal-pane/use-terminal-pane-paste-listeners.ts create mode 100644 src/renderer/src/components/terminal-pane/use-terminal-pane-process-exit-actions.ts create mode 100644 src/renderer/src/components/terminal-pane/use-terminal-pane-projection.ts create mode 100644 src/renderer/src/components/terminal-pane/use-terminal-pane-reconciliation.ts create mode 100644 src/renderer/src/components/terminal-pane/use-terminal-pane-startup-actions.ts create mode 100644 src/renderer/src/components/terminal-pane/use-terminal-pane-store-bindings.ts create mode 100644 src/renderer/src/components/terminal-pane/use-terminal-pane-title-effects.ts create mode 100644 src/renderer/src/components/terminal-pane/use-terminal-pane-title-state.ts create mode 100644 src/renderer/src/components/terminal-parking-pass-candidates.ts create mode 100644 src/renderer/src/components/terminal-workspace-keydown.ts create mode 100644 src/renderer/src/components/terminal-workspace-model.ts create mode 100644 src/renderer/src/components/use-task-page-composer-actions.ts create mode 100644 src/renderer/src/components/use-task-page-detail-routing.ts create mode 100644 src/renderer/src/components/use-task-page-github-cache-reconciliation.ts create mode 100644 src/renderer/src/components/use-task-page-github-detail.ts create mode 100644 src/renderer/src/components/use-task-page-github-issue-creation.ts create mode 100644 src/renderer/src/components/use-task-page-github-issue-draft.ts create mode 100644 src/renderer/src/components/use-task-page-github-landing-refresh.ts create mode 100644 src/renderer/src/components/use-task-page-github-list-projection.ts create mode 100644 src/renderer/src/components/use-task-page-github-list-state.ts create mode 100644 src/renderer/src/components/use-task-page-github-mutation-state.ts create mode 100644 src/renderer/src/components/use-task-page-github-quiet-refresh-effect.ts create mode 100644 src/renderer/src/components/use-task-page-github-quiet-refresh.ts create mode 100644 src/renderer/src/components/use-task-page-github-search-pagination.ts create mode 100644 src/renderer/src/components/use-task-page-gitlab-loading.ts create mode 100644 src/renderer/src/components/use-task-page-global-effects.ts create mode 100644 src/renderer/src/components/use-task-page-jira-creation-metadata.ts create mode 100644 src/renderer/src/components/use-task-page-jira-creation-projects.ts create mode 100644 src/renderer/src/components/use-task-page-jira-creation-state.ts create mode 100644 src/renderer/src/components/use-task-page-jira-issue-creation.ts create mode 100644 src/renderer/src/components/use-task-page-jira-list-effects.ts create mode 100644 src/renderer/src/components/use-task-page-jira-list-projection.ts create mode 100644 src/renderer/src/components/use-task-page-jira-list-state.ts create mode 100644 src/renderer/src/components/use-task-page-linear-board.ts create mode 100644 src/renderer/src/components/use-task-page-linear-collection-effects.ts create mode 100644 src/renderer/src/components/use-task-page-linear-context-actions.ts create mode 100644 src/renderer/src/components/use-task-page-linear-context-state.ts create mode 100644 src/renderer/src/components/use-task-page-linear-creation-state.ts create mode 100644 src/renderer/src/components/use-task-page-linear-custom-view-effects.ts create mode 100644 src/renderer/src/components/use-task-page-linear-filter-selection.ts create mode 100644 src/renderer/src/components/use-task-page-linear-in-orca-effects.ts create mode 100644 src/renderer/src/components/use-task-page-linear-issue-creation.ts create mode 100644 src/renderer/src/components/use-task-page-linear-list-effects.ts create mode 100644 src/renderer/src/components/use-task-page-linear-list-presentation.ts create mode 100644 src/renderer/src/components/use-task-page-linear-list-projection.ts create mode 100644 src/renderer/src/components/use-task-page-linear-list-selection.ts create mode 100644 src/renderer/src/components/use-task-page-linear-project-creation.ts create mode 100644 src/renderer/src/components/use-task-page-linear-view-state.ts create mode 100644 src/renderer/src/components/use-task-page-provider-metadata.ts create mode 100644 src/renderer/src/components/use-task-page-provider-state.ts create mode 100644 src/renderer/src/components/use-task-page-repo-selection.ts create mode 100644 src/renderer/src/components/use-task-page-resume-restoration.ts create mode 100644 src/renderer/src/components/use-task-page-runtime-hosts.ts create mode 100644 src/renderer/src/components/use-task-page-search-actions.ts create mode 100644 src/renderer/src/components/use-task-page-source-availability.ts create mode 100644 src/renderer/src/components/use-task-page-source-summary.ts create mode 100644 src/renderer/src/components/use-task-page-store-bindings.ts create mode 100644 src/renderer/src/components/use-task-page-workspace-actions.ts create mode 100644 src/renderer/src/components/use-terminal-activation-actions.ts create mode 100644 src/renderer/src/components/use-terminal-browser-retention.ts create mode 100644 src/renderer/src/components/use-terminal-bulk-close-actions.ts create mode 100644 src/renderer/src/components/use-terminal-close-actions.ts create mode 100644 src/renderer/src/components/use-terminal-controller.ts create mode 100644 src/renderer/src/components/use-terminal-create-actions.ts create mode 100644 src/renderer/src/components/use-terminal-editor-close-dialog-actions.ts create mode 100644 src/renderer/src/components/use-terminal-editor-close-foundation.ts create mode 100644 src/renderer/src/components/use-terminal-editor-close-queue.ts create mode 100644 src/renderer/src/components/use-terminal-keyboard-shortcuts.ts create mode 100644 src/renderer/src/components/use-terminal-parking-foundation.ts create mode 100644 src/renderer/src/components/use-terminal-parking-pass.ts create mode 100644 src/renderer/src/components/use-terminal-watcher-effects.ts create mode 100644 src/renderer/src/components/use-terminal-window-lifecycle.ts create mode 100644 src/renderer/src/components/use-terminal-workspace-foundation.ts create mode 100644 src/renderer/src/components/use-terminal-workspace-projection.ts create mode 100644 src/renderer/src/components/use-terminal-workspace-store-bindings.ts create mode 100644 src/renderer/src/components/use-worktree-jump-palette-controller.ts create mode 100644 src/renderer/src/components/use-worktree-jump-palette-create-action.ts create mode 100644 src/renderer/src/components/use-worktree-jump-palette-filter.ts create mode 100644 src/renderer/src/components/use-worktree-jump-palette-list-entries.ts create mode 100644 src/renderer/src/components/use-worktree-jump-palette-local-state.ts create mode 100644 src/renderer/src/components/use-worktree-jump-palette-open-tabs.ts create mode 100644 src/renderer/src/components/use-worktree-jump-palette-project-targets.ts create mode 100644 src/renderer/src/components/use-worktree-jump-palette-quick-actions.ts create mode 100644 src/renderer/src/components/use-worktree-jump-palette-recent-tabs.ts create mode 100644 src/renderer/src/components/use-worktree-jump-palette-sections.ts create mode 100644 src/renderer/src/components/use-worktree-jump-palette-selection-actions.ts create mode 100644 src/renderer/src/components/use-worktree-jump-palette-selection-lifecycle.ts create mode 100644 src/renderer/src/components/use-worktree-jump-palette-store-state.ts create mode 100644 src/renderer/src/components/use-worktree-jump-palette-worktrees.ts create mode 100644 src/renderer/src/components/workspace-cleanup/inactive-workspace-estimate.ts create mode 100644 src/renderer/src/components/worktree-jump-palette-browser-simulator-rows.tsx create mode 100644 src/renderer/src/components/worktree-jump-palette-empty-state.ts create mode 100644 src/renderer/src/components/worktree-jump-palette-entry.tsx create mode 100644 src/renderer/src/components/worktree-jump-palette-model.ts create mode 100644 src/renderer/src/components/worktree-jump-palette-primitives.tsx create mode 100644 src/renderer/src/components/worktree-jump-palette-project-action-rows.tsx create mode 100644 src/renderer/src/components/worktree-jump-palette-recent-inclusion.ts create mode 100644 src/renderer/src/components/worktree-jump-palette-source.test-support.ts create mode 100644 src/renderer/src/components/worktree-jump-palette-surface.tsx create mode 100644 src/renderer/src/components/worktree-jump-palette-workspace-tab-row.tsx create mode 100644 src/renderer/src/components/worktree-jump-palette-worktree-row.tsx create mode 100644 src/renderer/src/lib/cmd-j-match-relevance.ts diff --git a/src/renderer/src/app-startup-routing.test.ts b/src/renderer/src/app-startup-routing.test.ts index a64fcc3cbff..24a9cc75120 100644 --- a/src/renderer/src/app-startup-routing.test.ts +++ b/src/renderer/src/app-startup-routing.test.ts @@ -21,7 +21,7 @@ const PERSISTED_UI_WRITER_PATH = 'src/renderer/src/app-shell/use-persisted-ui-wr describe('renderer startup runtime routing', () => { it('routes packaged terminal restore through the daemon adoption gate', () => { const source = readFileSync( - join(process.cwd(), 'src/renderer/src/components/Terminal.tsx'), + join(process.cwd(), 'src/renderer/src/components/use-terminal-watcher-effects.ts'), 'utf8' ) const gateStart = source.indexOf('const startupActivationGateWorktreeIdsRef') @@ -226,7 +226,9 @@ describe('renderer startup runtime routing', () => { }) it('keeps the persisted Automations view from starting its own bootstrap worktree scan', () => { - const source = readSource('src/renderer/src/components/automations/AutomationsPage.tsx') + const source = readSource( + 'src/renderer/src/components/automations/use-automations-page-refresh.ts' + ) const fullRefreshStart = source.indexOf('const mountedBeforeStartupWorktreeRefreshRef') const fullRefreshEffect = source.slice( fullRefreshStart, @@ -348,7 +350,7 @@ describe('renderer startup runtime routing', () => { 'utf8' ) const terminalSource = readFileSync( - join(process.cwd(), 'src/renderer/src/components/Terminal.tsx'), + join(process.cwd(), 'src/renderer/src/components/use-terminal-watcher-effects.ts'), 'utf8' ) const hydrateIndex = appSource.indexOf("timeRendererStartupSyncStep('hydrate-session-stores'") @@ -468,7 +470,7 @@ describe('renderer startup runtime routing', () => { }) it('does not eagerly import optional status-bar segments on startup', () => { - const source = readSource('src/renderer/src/components/status-bar/StatusBar.tsx') + const source = readSource('src/renderer/src/components/status-bar/StatusBarSurface.tsx') expect(source).toContain("import('./ResourceUsageStatusSegment').then") expect(source).toContain("import('./PortsStatusSegment').then") diff --git a/src/renderer/src/components/TaskPage.tsx b/src/renderer/src/components/TaskPage.tsx index 6138948e88d..fcd84c20856 100644 --- a/src/renderer/src/components/TaskPage.tsx +++ b/src/renderer/src/components/TaskPage.tsx @@ -1,3101 +1,84 @@ -/* eslint-disable max-lines -- Why: repo selector, task-source controls, and task list stay co-located so their wiring reads in one place. */ -import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' -import { useShallow } from 'zustand/react/shallow' -import { toast } from 'sonner' - -import { useAppStore } from '@/store' - -import { parseTaskQuery, stripRepoQualifiers, withQualifier } from '../../../shared/task-query' -import type { PRFilterChange } from '@/components/github/PRFilterDropdowns' - -import { - overlayPendingOnTaskPagePages, - patchTaskPageGitHubWorkItemPages -} from '@/components/task-page-github-work-item-mutations' - -import type { ItemDialogTab } from '@/components/GitHubItemDialog' -import { getTaskPresetQuery } from '../../../shared/task-preset-query' - -import { - buildTaskPageRepoSourceState, - findTaskPageDialogWorkItem, - reconcileTaskPagePagesWithWorkItemsCache, - selectTaskPageUnresolvedSourceRepos, - selectTaskPageWorkItemsCacheEntries, - type TaskPageRepoSourceState -} from '@/components/task-page-cache-selectors' -import { shouldHideTaskPageListChrome } from '@/components/task-page-list-chrome-visibility' -import { taskPageGitHubResumeCache } from '@/components/task-page-github-resume-cache' -import { - filterLinearIssuesForInOrcaWorkspace, - readLinkedLinearIssuesWithLimit -} from '@/components/task-page-linear-in-orca-issues' -import { serializeLinearIssueViewResumeState } from '../../../shared/linear/issue-view-resume-state' -import { saveLinearIssueView } from './linear-issue-view-storage' - -import type { GitHubAssignableUser } from '../../../shared/github/pull-request-types' -import type { GitHubWorkItem } from '../../../shared/github/work-item-types' -import type { GitLabWorkItem } from '../../../shared/gitlab-types' - -import type { LinearIssue } from '../../../shared/linear/issue-types' -import type { - LinearCustomViewSummary, - LinearProjectSummary -} from '../../../shared/linear/project-types' -import type { LinearCollectionResult } from '../../../shared/linear/workspace-types' -import type { TaskViewPresetId } from '../../../shared/ui-chrome-types' -import { clampLinearIssueListLimit } from '../../../shared/linear/issue-read-limits' -import { shouldSuppressEnterSubmit } from '@/lib/new-workspace-enter-guard' -import { useContextualTour } from '@/components/contextual-tours/use-contextual-tour' - -import { translate } from '@/i18n/i18n' -import type { GitHubTaskKind } from '@/components/task-page-localized-options' -import { useGitHubTaskSearchCommit } from '@/components/use-github-task-search-commit' -import { - getDefaultPresetForGitHubTaskKind, - getGitHubTaskKind, - scopeGitHubTaskSearch -} from '@/components/task-page-github-task-kind' - -import { - LINEAR_ITEM_LIMIT, - TASK_SEARCH_DEBOUNCE_MS -} from '@/components/task-page/task-page-list-limits' - -import { - getTaskPageRepoCacheInput, - getTaskPageRepoSourceContext -} from '@/components/task-page/source/repo-source-context' - -import { - LINEAR_CUSTOM_VIEW_MODELS, - mergeLinearCollectionResults -} from '@/components/task-page/linear/linear-issue-grouping' - -import type { TaskPageSourceToolbarProps } from '@/components/task-page/chrome/task-page-source-toolbar' -import type { TaskPageGithubModeBarProps } from '@/components/task-page/chrome/task-page-github-mode-bar' -import type { TaskPageGithubItemFiltersProps } from '@/components/task-page/chrome/task-page-github-item-filters' -import type { TaskPageLinearFiltersProps } from '@/components/task-page/chrome/task-page-linear-filters' -import type { TaskPageJiraFiltersProps } from '@/components/task-page/chrome/task-page-jira-filters' -import type { TaskPageGitlabFiltersProps } from '@/components/task-page/chrome/task-page-gitlab-filters' -import type { GithubDetailHostProps } from '@/components/task-page/github/github-detail-host' -import type { GithubWorkItemTableProps } from '@/components/task-page/github/github-work-item-table' -import { startGitHubListScrollRestore } from '@/components/task-page/github/github-list-scroll-restore' -import type { GitlabWorkItemListProps } from '@/components/task-page/gitlab/gitlab-work-item-list' -import type { JiraIssueListHostProps } from '@/components/task-page/jira/jira-issue-list-host' -import type { NewGithubIssueDialogProps } from '@/components/task-page/dialogs/new-github-issue-dialog' -import type { NewLinearProjectDialogProps } from '@/components/task-page/dialogs/new-linear-project-dialog' -import type { NewLinearIssueDialogProps } from '@/components/task-page/dialogs/new-linear-issue-dialog' -import type { NewJiraIssueDialogProps } from '@/components/task-page/dialogs/new-jira-issue-dialog' -import type { TaskPageConnectDialogsProps } from '@/components/task-page/dialogs/task-page-connect-dialogs' -import type { LinearViewsHostProps } from '@/components/task-page/linear/linear-views-host' -import { useTaskPageStoreBindings } from '@/components/task-page/hooks/use-task-page-store-bindings' -import { useTaskPageRepoSelection } from '@/components/task-page/source/use-task-page-repo-selection' -import { useTaskPageAccountScopes } from '@/components/task-page/hooks/use-task-page-account-scopes' -import { useTaskPageVisibleSources } from '@/components/task-page/hooks/use-task-page-visible-sources' -import { useTaskPageTaskSourceState } from '@/components/task-page/hooks/use-task-page-task-source-state' -import { useTaskPageRuntimePreflight } from '@/components/task-page/hooks/use-task-page-runtime-preflight' -import { useTaskPageSourceAvailability } from '@/components/task-page/hooks/use-task-page-source-availability' -import { useTaskPageSourceNotices } from '@/components/task-page/hooks/use-task-page-source-notices' -import { useTaskPageSourceSync } from '@/components/task-page/hooks/use-task-page-source-sync' -import { useTaskPageGitLabListState } from '@/components/task-page/hooks/use-task-page-gitlab-list-state' -import { useTaskPageGitHubListState } from '@/components/task-page/hooks/use-task-page-github-list-state' -import { useTaskPageLinearListState } from '@/components/task-page/hooks/use-task-page-linear-list-state' -import { useTaskPageJiraListState } from '@/components/task-page/hooks/use-task-page-jira-list-state' -import { useTaskPageGitLabFetch } from '@/components/task-page/hooks/use-task-page-gitlab-fetch' -import { useTaskPageGitHubNewIssueState } from '@/components/task-page/hooks/use-task-page-github-new-issue-state' -import { useTaskPageSessionResume } from '@/components/task-page/hooks/use-task-page-session-resume' -import { useTaskPageSelectedIssueState } from '@/components/task-page/hooks/use-task-page-selected-issue-state' -import { useTaskPageLinearTeams } from '@/components/task-page/hooks/use-task-page-linear-teams' -import { useTaskPageJiraProjects } from '@/components/task-page/hooks/use-task-page-jira-projects' -import { useTaskPageLinearActiveCollection } from '@/components/task-page/hooks/use-task-page-linear-active-collection' -import { useTaskPageLinearBoard } from '@/components/task-page/hooks/use-task-page-linear-board' -import { useTaskPageLinearCreateDialogs } from '@/components/task-page/hooks/use-task-page-linear-create-dialogs' -import { useTaskPageGitHubMutationSession } from '@/components/task-page/hooks/use-task-page-github-mutation-session' -import { useTaskPageCreateGithubSubmit } from '@/components/task-page/hooks/use-task-page-create-github-submit' -import { useTaskPageLinearFetch } from '@/components/task-page/hooks/use-task-page-linear-fetch' -import { useTaskPageLinearIssueWindow } from '@/components/task-page/hooks/use-task-page-linear-issue-window' -import { useTaskPageLinearActions } from '@/components/task-page/hooks/use-task-page-linear-actions' -import { useTaskPageJiraFetch } from '@/components/task-page/hooks/use-task-page-jira-fetch' -import { useTaskPageJiraActions } from '@/components/task-page/hooks/use-task-page-jira-actions' -import { useTaskPageGitHubFilteredItems } from '@/components/task-page/hooks/use-task-page-github-filtered-items' -import { useTaskPageGitHubPageLoader } from '@/components/task-page/hooks/use-task-page-github-page-loader' -import { useTaskPageCreateLinearSubmits } from '@/components/task-page/hooks/use-task-page-create-linear-submits' -import { useTaskPageCreateJiraSubmit } from '@/components/task-page/hooks/use-task-page-create-jira-submit' -import { useTaskPageJiraDisplayedIssues } from '@/components/task-page/hooks/use-task-page-jira-displayed-issues' -import { useTaskPageJiraCreateDialog } from '@/components/task-page/hooks/use-task-page-jira-create-dialog' -import { useTaskPageCreateDialogResets } from '@/components/task-page/hooks/use-task-page-create-dialog-resets' -import { useTaskPageUseItemActions } from '@/components/task-page/hooks/use-task-page-use-item-actions' -import { TaskPageLayout } from '@/components/task-page/task-page-layout' -import { useTaskPageGitHubFetch } from '@/components/task-page/hooks/use-task-page-github-fetch' -import { useTaskPageGitHubQuietRevalidate } from '@/components/task-page/hooks/use-task-page-github-quiet-revalidate' +import React from 'react' +import { useTaskPageStoreBindings } from './use-task-page-store-bindings' +import { useTaskPageRepoSelection } from './use-task-page-repo-selection' +import { useTaskPageRuntimeHosts } from './use-task-page-runtime-hosts' +import { useTaskPageSourceAvailability } from './use-task-page-source-availability' +import { useTaskPageProviderState } from './use-task-page-provider-state' +import { useTaskPageGitHubListState } from './use-task-page-github-list-state' +import { useTaskPageGitHubDetail } from './use-task-page-github-detail' +import { useTaskPageGitHubCacheReconciliation } from './use-task-page-github-cache-reconciliation' +import { useTaskPageGitHubIssueDraft } from './use-task-page-github-issue-draft' +import { useTaskPageDetailRouting } from './use-task-page-detail-routing' +import { useTaskPageLinearViewState } from './use-task-page-linear-view-state' +import { useTaskPageJiraListState } from './use-task-page-jira-list-state' +import { useTaskPageResumeRestoration } from './use-task-page-resume-restoration' +import { useTaskPageProviderMetadata } from './use-task-page-provider-metadata' +import { useTaskPageGitLabLoading } from './use-task-page-gitlab-loading' +import { useTaskPageLinearListSelection } from './use-task-page-linear-list-selection' +import { useTaskPageLinearListProjection } from './use-task-page-linear-list-projection' +import { useTaskPageLinearBoard } from './use-task-page-linear-board' +import { useTaskPageJiraListProjection } from './use-task-page-jira-list-projection' +import { useTaskPageLinearCreationState } from './use-task-page-linear-creation-state' +import { useTaskPageGitHubMutationState } from './use-task-page-github-mutation-state' +import { useTaskPageJiraCreationState } from './use-task-page-jira-creation-state' +import { useTaskPageJiraCreationMetadata } from './use-task-page-jira-creation-metadata' +import { useTaskPageGitHubListProjection } from './use-task-page-github-list-projection' +import { useTaskPageGitHubSearchPagination } from './use-task-page-github-search-pagination' +import { useTaskPageGitHubLandingRefresh } from './use-task-page-github-landing-refresh' +import { useTaskPageGitHubQuietRefresh } from './use-task-page-github-quiet-refresh' +import { useTaskPageSearchActions } from './use-task-page-search-actions' +import { useTaskPageWorkspaceActions } from './use-task-page-workspace-actions' +import { useTaskPageGitHubIssueCreation } from './use-task-page-github-issue-creation' +import { useTaskPageLinearProjectCreation } from './use-task-page-linear-project-creation' +import { useTaskPageLinearIssueCreation } from './use-task-page-linear-issue-creation' +import { useTaskPageJiraIssueCreation } from './use-task-page-jira-issue-creation' +import { useTaskPageGlobalEffects } from './use-task-page-global-effects' +import { useTaskPageLinearListEffects } from './use-task-page-linear-list-effects' +import { useTaskPageLinearInOrcaEffects } from './use-task-page-linear-in-orca-effects' +import { useTaskPageLinearCollectionEffects } from './use-task-page-linear-collection-effects' +import { useTaskPageJiraListEffects } from './use-task-page-jira-list-effects' +import { useTaskPageComposerActions } from './use-task-page-composer-actions' +import { TaskPageSurface } from './TaskPageSurface' export default function TaskPage(): React.JSX.Element { - const { - settings, - persistedUIReady, - taskResumeState, - setTaskResumeState, - pageData, - openTaskPage, - closeTaskPage, - activeModal, - repos, - sshConnectionStates, - sshTargetLabels, - runtimeEnvironments, - runtimeStatusByEnvironmentId, - repoMap, - allWorktrees, - openModal, - updateSettings, - fetchWorkItemsAcrossRepos, - fetchPRChecks, - getCachedWorkItems, - setIssueSourcePreference, - workItemsInvalidationNonce, - linearStatus, - linearStatusContextKey, - preflightStatus, - preflightStatusChecked, - preflightStatusContextKey, - selectLinearWorkspace, - searchLinearIssues, - listLinearIssues, - linearListInvalidationToken, - folderWorkspaces, - invalidateLinearIssueLists, - getCachedLinearIssues, - fetchLinearIssue, - refreshLinearIssue, - getCachedLinearTeams, - listLinearTeams, - getCachedLinearProjects, - listLinearProjectsFromStore, - fetchLinearProject, - listLinearProjectIssues, - getCachedLinearCustomViews, - listLinearCustomViews, - fetchLinearCustomView, - listLinearCustomViewIssues, - listLinearCustomViewProjects, - patchLinearIssue, - checkLinearConnection, - refreshPreflightStatus, - expectedPreflightContextKey, - jiraStatus, - jiraStatusContextKey, - selectJiraSite, - searchJiraIssues, - listJiraIssues, - checkJiraConnection, - providerRuntimeContextKey, - providerRuntimeContextKeyRef, - preflightStatusCurrent, - linearStatusReady, - jiraStatusReady, - linearConnected, - jiraConnected, - submitShortcutLabel, - eligibleRepos - } = useTaskPageStoreBindings() - - const { - resolvedInitialSelection, - repoSelection, - setRepoSelection, - taskPickerGroups, - taskPickerRepos, - selectedRepos, - selectedReposKey, - primaryRepo - } = useTaskPageRepoSelection({ - eligibleRepos, - pageDataPreselectedRepoId: pageData.preselectedRepoId, - defaultRepoSelection: settings?.defaultRepoSelection - }) - const { - linearWorkspaces, - selectedLinearWorkspaceId, - selectedLinearWorkspace, - jiraSites, - selectedJiraSiteId, - selectedJiraSite - } = useTaskPageAccountScopes({ linearStatus, jiraStatus }) - const { - visibleTaskProviders, - sourceOptions, - githubModeButtons, - linearModeOptions, - jiraPresets, - gitLabIssueFilters, - gitLabMRFilters, - linearViewOptions, - linearGroupOptions, - linearOrderOptions, - linearDisplayPropertyOptions, - visibleSourceOptions, - hideTaskSource, - defaultTaskViewPreset, - initialTaskQuery, - preferredTaskSource - } = useTaskPageVisibleSources({ - settings, - preflightStatusCurrent, - preflightStatus, - linearConnected, - updateSettings, - pageData - }) - const { taskSource, setTaskSource } = useTaskPageTaskSourceState({ - preferredTaskSource, - visibleTaskProviders - }) - const { - runtimePreflightStatusByHostId, - taskSourceRepoContexts, - hostRegistryById, - hostLabelById, - getTaskPickerRepoHostLabel - } = useTaskPageRuntimePreflight({ - taskSource, - selectedRepos, - repos, - settings, - sshTargetLabels, - sshConnectionStates, - runtimeEnvironments, - runtimeStatusByEnvironmentId - }) - const { - taskSourceHostAvailability, - accountBackedTaskSourceHostId, - linearTaskSourceContext, - linearListInvalidationVersionForSource, - jiraTaskSourceContext, - jiraTaskSourceScopeKey, - accountBackedTaskSourceHostAvailability - } = useTaskPageSourceAvailability({ - taskSource, - selectedRepos, - hostRegistryById, - taskSourceRepoContexts, - runtimePreflightStatusByHostId, - preflightStatus, - preflightStatusChecked, - preflightStatusCurrent, - settings, - linearListInvalidationToken, - providerRuntimeContextKey, - selectedLinearWorkspace, - selectedLinearWorkspaceId, - selectedJiraSite, - selectedJiraSiteId - }) - const { - taskSourceAvailabilityNoticeByProvider, - taskSourceContextSummary, - taskSourceAvailabilityNotice, - githubEmptyState - } = useTaskPageSourceNotices({ - taskSource, - selectedRepos, - hostRegistryById, - hostLabelById, - preflightStatus, - preflightStatusCurrent, - preflightStatusChecked, - runtimePreflightStatusByHostId, - taskSourceRepoContexts, - accountBackedTaskSourceHostId, - accountBackedTaskSourceHostAvailability, - taskSourceHostAvailability, - selectedLinearWorkspace, - selectedJiraSite, - sourceOptions - }) - const { - taskSourceManuallyChangedRef, - taskResumeAppliedRef, - githubSearchPersistReadyRef, - linearSearchPersistReadyRef, - linearViewPersistReadyRef, - jiraSearchPersistReadyRef, - taskResumeApplied, - setTaskResumeApplied - } = useTaskPageSourceSync({ - pageData, - preferredTaskSource, - taskSource, - setTaskSource, - visibleTaskProviders, - settings - }) - - // Why: Project mode is a GitHub sub-tab — visible on the GitHub source, but actual entry is gated on a non-null activeProject. - const projectModeVisible = taskSource === 'github' - const [githubMode, setGithubMode] = useState<'items' | 'project'>('items') - - const { - setGitlabFilter, - gitlabItems, - setGitlabItems, - gitlabLoading, - setGitlabLoading, - gitlabError, - setGitlabError, - gitlabRefreshNonce, - setGitlabRefreshNonce, - gitlabDialogItem, - setGitlabDialogItem, - gitlabView, - setGitlabView, - gitlabIssuePage, - setGitlabIssuePage, - gitlabIssueTotalPages, - setGitlabIssueTotalPages, - gitlabIssueLoadingTargetPage, - setGitlabIssueLoadingTargetPage, - gitlabTodos, - setGitlabTodos, - gitlabTodosLoading, - setGitlabTodosLoading, - gitlabEmptyState, - activeGitlabFilter, - displayedGitLabItems - } = useTaskPageGitLabListState({ taskSource, selectedRepos, selectedReposKey }) - const { - taskSearchInput, - setTaskSearchInput, - appliedTaskSearch, - setAppliedTaskSearch, - taskSearchInputRef, - activeTaskPreset, - setActiveTaskPreset, - tasksLoading, - setTasksLoading, - tasksRefreshing, - setTasksRefreshing, - tasksFiltering, - setTasksFiltering, - tasksError, - setTasksError, - failedCount, - setFailedCount, - githubUnavailable, - setGithubUnavailable, - taskRefreshNonce, - setTaskRefreshNonce, - quietRefreshNonce, - setQuietRefreshNonce, - githubViewerLogin, - setGitHubViewerLogin, - lastFetchedNonceRef, - lastFetchedInvalidationNonceRef, - paginationGenerationRef, - landingGitHubRefreshKeysRef, - githubPerRepoPageLimit, - githubPageSize, - githubResumeContextKey, - pages, - setPages, - currentPage, - setCurrentPage, - pagesRef, - currentPageRef, - githubResumeConsumedRef, - githubResumeContextRef, - githubListScrollRef, - githubListScrollTopRef, - pendingGithubScrollRestoreRef, - githubRestoreScrollWriteRef, - paginationLoading, - setPaginationLoading, - loadingTargetPage, - setLoadingTargetPage, - countedTotalPages, - setCountedTotalPages, - provenPageLimit, - setProvenPageLimit, - countedTotalPagesRef, - hardRefreshEpochRef, - fetchWorkItemsNextPage, - countWorkItemsAcrossRepos - } = useTaskPageGitHubListState({ - initialTaskQuery, - defaultTaskViewPreset, - selectedRepos, - selectedReposKey, - getCachedWorkItems - }) - - useEffect(() => { - const page = pages[currentPage] - if (!taskResumeApplied || taskSource !== 'github' || githubMode !== 'items' || !page) { - return - } - taskPageGitHubResumeCache.write(githubResumeContextKey, currentPage, page) - }, [currentPage, githubMode, githubResumeContextKey, pages, taskResumeApplied, taskSource]) - - const taskListPositionRef = useRef<{ - contextKey: string - page: number - scrollTop: number - } | null>(null) - useLayoutEffect(() => { - if ( - taskSource !== 'github' || - githubMode !== 'items' || - pageData.openGitHubWorkItem || - pendingGithubScrollRestoreRef.current !== null - ) { - return - } - taskListPositionRef.current = { - contextKey: githubResumeContextKey, - page: currentPage, - scrollTop: githubListScrollTopRef.current - } - }, [ - currentPage, - githubMode, - githubResumeContextKey, - pageData.openGitHubWorkItem, - taskSource, - githubListScrollTopRef, - pendingGithubScrollRestoreRef - ]) - - useEffect( - () => () => { - const position = taskListPositionRef.current - const state = useAppStore.getState() - if (position && !state.taskPageData.openGitHubWorkItem) { - state.setTaskListPosition({ - contextKey: position.contextKey, - page: position.page, - scrollTop: position.scrollTop - }) - } - }, - [] - ) - - // Why: keyed on selectedReposKey, not the selectedRepos array — a background - // repos:changed refresh mid-flight would otherwise bump the generation and - // silently discard the user's page navigation (#11485). Mirrors every dep of - // the fetch effect that resets page state, so a reset always invalidates - // in-flight page requests. - useEffect(() => { - paginationGenerationRef.current += 1 - setPaginationLoading(false) - setLoadingTargetPage(null) - }, [ - selectedReposKey, - appliedTaskSearch, - workItemsInvalidationNonce, - taskRefreshNonce, - taskSource, - githubMode, - taskResumeApplied, - - setPaginationLoading, - paginationGenerationRef, - setLoadingTargetPage - ]) - - // Why: the dialog's "Use" button routes through the same direct-launch flow as the row-level "Use" CTA so behavior is consistent regardless of entry point. - const githubTaskDrawerWorkItem = useAppStore((s) => s.githubTaskDrawerWorkItem) - const setGithubTaskDrawerWorkItem = useAppStore((s) => s.setGithubTaskDrawerWorkItem) - const [dialogInitialTab, setDialogInitialTab] = useState('conversation') - const dialogWorkItemKey = githubTaskDrawerWorkItem - ? { id: githubTaskDrawerWorkItem.id, repoId: githubTaskDrawerWorkItem.repoId } - : null - - const appliedWorkItemsCacheQuery = useMemo( - () => stripRepoQualifiers(appliedTaskSearch.trim()), - [appliedTaskSearch] - ) - const selectedWorkItemsCacheEntries = useAppStore( - useShallow((s) => - selectTaskPageWorkItemsCacheEntries( - s.workItemsCache, - selectedRepos.map(getTaskPageRepoCacheInput), - githubPerRepoPageLimit, - appliedWorkItemsCacheQuery - ) - ) - ) - - // Why: derive the dialog item from the cache for optimistic patches, falling back to the click-time snapshot for new stubs; key by repoId so same-number issues across repos resolve to the clicked row. - const cachedDialogWorkItem = useAppStore((s) => - findTaskPageDialogWorkItem(s.workItemsCache, dialogWorkItemKey) - ) - const dialogWorkItem = dialogWorkItemKey - ? (cachedDialogWorkItem ?? githubTaskDrawerWorkItem) - : null - - useLayoutEffect(() => { - const target = pendingGithubScrollRestoreRef.current - // Start observing as soon as the list mounts; its page rows may be committed - // by a later render, and the restore helper will retry when they appear. - // Keep the target armed while the detail route is transitioning and the list is still mounted. - if (target === null || pageData.openGitHubWorkItem) { - return - } - return startGitHubListScrollRestore({ - target, - scrollElementRef: githubListScrollRef, - pendingRestoreRef: pendingGithubScrollRestoreRef, - restoreWriteRef: githubRestoreScrollWriteRef, - onScrollTopApplied: (scrollTop) => { - githubListScrollTopRef.current = scrollTop - taskListPositionRef.current = { - contextKey: githubResumeContextKey, - page: currentPage, - scrollTop - } - } - }) - }, [ - currentPage, - dialogWorkItem, - githubResumeContextKey, - pageData.openGitHubWorkItem, - pages, - githubListScrollTopRef, - pendingGithubScrollRestoreRef, - githubRestoreScrollWriteRef, - githubListScrollRef.current?.scrollTop, - githubListScrollRef - ]) - - const dialogRepoPath = dialogWorkItem ? (repoMap.get(dialogWorkItem.repoId)?.path ?? null) : null - const dialogSourceContext = useMemo(() => { - if (!dialogWorkItem) { - return null - } - if ( - pageData.openGitHubSourceContext?.provider === 'github' && - pageData.openGitHubWorkItem?.id === dialogWorkItem.id && - pageData.openGitHubWorkItem.repoId === dialogWorkItem.repoId - ) { - return pageData.openGitHubSourceContext - } - return getTaskPageRepoSourceContext(repoMap.get(dialogWorkItem.repoId), 'github') - }, [dialogWorkItem, pageData.openGitHubSourceContext, pageData.openGitHubWorkItem, repoMap]) - const gitlabDialogRepo = useMemo( - () => - gitlabDialogItem - ? (selectedRepos.find((r) => r.id === gitlabDialogItem.repoId) ?? primaryRepo) - : null, - [gitlabDialogItem, primaryRepo, selectedRepos] - ) - const gitlabDialogSourceContext = useMemo(() => { - if (!gitlabDialogItem) { - return null - } - if ( - pageData.openGitLabSourceContext?.provider === 'gitlab' && - pageData.openGitLabWorkItem?.id === gitlabDialogItem.id && - pageData.openGitLabWorkItem?.repoId === gitlabDialogItem.repoId - ) { - return pageData.openGitLabSourceContext - } - return getTaskPageRepoSourceContext(gitlabDialogRepo, 'gitlab', gitlabDialogItem.projectRef) - }, [ - gitlabDialogItem, - gitlabDialogRepo, - pageData.openGitLabSourceContext, - pageData.openGitLabWorkItem - ]) - - const setDialogWorkItem = useCallback( - (item: GitHubWorkItem | null, initialTab: ItemDialogTab = 'conversation') => { - setDialogInitialTab(item ? initialTab : 'conversation') - setGithubTaskDrawerWorkItem(item) - }, - [setGithubTaskDrawerWorkItem] - ) - - useEffect(() => { - if (!pageData.openGitHubWorkItem) { - setDialogWorkItem(null) - return - } - setGithubMode('items') - setDialogWorkItem(pageData.openGitHubWorkItem, pageData.openGitHubInitialTab) - }, [pageData.openGitHubInitialTab, pageData.openGitHubWorkItem, setDialogWorkItem]) - - useEffect(() => { - setGitlabDialogItem(pageData.openGitLabWorkItem ?? null) - }, [pageData.openGitLabWorkItem, setGitlabDialogItem]) - - const openGitHubDetailPage = useCallback( - (item: GitHubWorkItem, initialTab: ItemDialogTab = 'conversation') => { - const scrollTop = githubListScrollRef.current?.scrollTop ?? githubListScrollTopRef.current - githubListScrollTopRef.current = scrollTop - pendingGithubScrollRestoreRef.current = scrollTop - taskListPositionRef.current = { - contextKey: githubResumeContextKey, - page: currentPageRef.current, - scrollTop - } - useAppStore.getState().setTaskListPosition(taskListPositionRef.current) - openTaskPage( - { - taskSource: 'github', - preselectedRepoId: item.repoId, - openGitHubWorkItem: item, - openGitHubSourceContext: getTaskPageRepoSourceContext(repoMap.get(item.repoId), 'github'), - openGitHubInitialTab: initialTab - }, - { recordTasksInteraction: false } - ) - }, - [ - githubResumeContextKey, - openTaskPage, - repoMap, - githubListScrollTopRef, - pendingGithubScrollRestoreRef, - currentPageRef, - githubListScrollRef.current?.scrollTop - ] - ) - - const openGitLabDetailPage = useCallback( - (item: GitLabWorkItem) => { - openTaskPage( - { - taskSource: 'gitlab', - preselectedRepoId: item.repoId, - openGitLabWorkItem: item, - openGitLabSourceContext: getTaskPageRepoSourceContext( - repoMap.get(item.repoId), - 'gitlab', - item.projectRef - ) - }, - { recordTasksInteraction: false } - ) - }, - [openTaskPage, repoMap] - ) - - const patchTaskPageWorkItemRows = useCallback( - ( - itemKey: { id: string; repoId: string }, - patch: Partial, - shouldPatch?: (item: GitHubWorkItem) => boolean - ): void => { - setPages((current) => { - return patchTaskPageGitHubWorkItemPages(current, itemKey, patch, shouldPatch) - }) - }, - [setPages] - ) - const handleDialogReviewRequestsChange = useCallback( - (itemKey: { id: string; repoId: string }, reviewRequests: GitHubAssignableUser[]): void => { - patchTaskPageWorkItemRows(itemKey, { reviewRequests }) - }, - [patchTaskPageWorkItemRows] - ) - - // Why: the per-repo issue-source indicator and retry banner both derive from the same workItemsCache entry, so no extra IPC. - // Why: subscribe only to entries this page renders; the selector returns entry refs so shallow equality filters unrelated cache writes. - const perRepoSourceState = useMemo( - () => buildTaskPageRepoSourceState(selectedRepos, selectedWorkItemsCacheEntries), - [selectedRepos, selectedWorkItemsCacheEntries] - ) - - // Why: repos that fetched but resolved no GitHub source (#9660) show empty like a genuine zero-result; surface them explicitly with Retry. - const unresolvedSourceRepos = useMemo( - () => selectTaskPageUnresolvedSourceRepos(selectedRepos, perRepoSourceState), - [selectedRepos, perRepoSourceState] - ) - - useEffect(() => { - if (taskSource !== 'github' || githubMode !== 'items') { - return - } - // Why: inline/dialog edits patch `workItemsCache`; the paged table renders - // from a local snapshot so it needs the patched row objects copied across. - // Hard guarantee (K4): always overlay pending after reconcile so list - // fetch clobbers never paint unprotected coordinator fields. - setPages((current) => - reconcileTaskPagePagesWithWorkItemsCache(current, selectedWorkItemsCacheEntries).map( - (page) => (page ? (overlayPendingOnTaskPagePages([page])[0] ?? []) : null) - ) - ) - }, [githubMode, selectedWorkItemsCacheEntries, taskSource, setPages]) - - // Why: one-time toast per repo when the 'upstream' preference fell back to origin (ref-gated); deliberately don't auto-reset the preference so re-adding upstream later still applies. - const fellBackToastedRef = useRef>(new Set()) - useEffect(() => { - if (taskSource !== 'github') { - return - } - for (const [index, r] of selectedRepos.entries()) { - const entry = selectedWorkItemsCacheEntries[index] - if (!entry?.issueSourceFellBack) { - continue - } - if (fellBackToastedRef.current.has(r.id)) { - continue - } - const prSlug = entry.sources?.prs - ? `${entry.sources.prs.owner}/${entry.sources.prs.repo}` - : r.displayName - toast.message( - translate( - 'auto.components.TaskPage.f4374519ae', - 'Your preferred issue source (upstream) is no longer configured for {{value0}}. Using origin.', - { value0: prSlug } - ) - ) - fellBackToastedRef.current.add(r.id) - } - }, [selectedRepos, selectedWorkItemsCacheEntries, taskSource]) - - // Why: partial-failure retry leaves the cache populated so tasksLoading never flips, giving no feedback; track retry-in-flight per source so only the clicked banner shows "Retrying…". - const [retryingSourceKeys, setRetryingSourceKeys] = useState>(() => new Set()) - - const handleRetryIssuesFetch = useCallback( - (sourceKey: string) => { - const source = perRepoSourceState.find((s) => s.sourceKey === sourceKey) - if (!source) { - return - } - // Why: nonce bump reuses the fetch path as force=true so retry doesn't dedupe onto a still-failing in-flight request (refreshes all repos; Retrying… stays scoped to the clicked source). - setRetryingSourceKeys((prev) => { - const next = new Set(prev) - next.add(source.sourceKey) - return next - }) - setTaskRefreshNonce((n) => n + 1) - }, - [perRepoSourceState, setTaskRefreshNonce] - ) - const handleRefreshGithubTasks = useCallback((): void => { - setTasksRefreshing(true) - setTaskRefreshNonce((current) => current + 1) - }, [setTasksRefreshing, setTaskRefreshNonce]) - const { - newIssueOpen, - setNewIssueOpen, - newIssueTitle, - setNewIssueTitle, - newIssueBody, - setNewIssueBody, - newIssueLabels, - setNewIssueLabels, - newIssueAssignees, - setNewIssueAssignees, - newIssueSubmitting, - setNewIssueSubmitting, - newIssueRepoId, - setNewIssueRepoId, - setNewIssueDraft, - clearNewIssueDraft, - newIssueTargetRepo, - newIssueSourceContext, - newIssueRuntimeTarget, - newIssueRepoLabels, - newIssueRepoAssignees - } = useTaskPageGitHubNewIssueState({ - selectedRepos, - settings, - repos - }) - - const { - selectedLinearIssueId, - setSelectedLinearIssueFallback, - selectedLinearIssueCanFloat, - linearCacheSnapshot, - jiraCacheSnapshot, - selectedLinearIssue, - linearDetailSourceContext, - setSelectedLinearIssue, - clearSelectedLinearIssue, - openLinearDetailPage, - openRelatedLinearIssue, - closeTaskDetailPage, - selectedJiraIssueKey, - setSelectedJiraIssueKey, - selectedJiraIssueFallback, - setSelectedJiraIssueFallback, - selectedJiraIssue, - jiraDetailSourceContext, - setSelectedJiraIssue, - openJiraDetailPage - } = useTaskPageSelectedIssueState({ - pageData, - linearTaskSourceContext, - jiraTaskSourceContext, - openTaskPage, - setDialogWorkItem - }) - - const { - linearMode, - setLinearMode, - linearIssues, - setLinearIssues, - linearIssueLimit, - setLinearIssueLimit, - linearIssuePage, - setLinearIssuePage, - linearIssueLoadingTargetPage, - setLinearIssueLoadingTargetPage, - linearIssuesHasMore, - setLinearIssuesHasMore, - linearLoading, - setLinearLoading, - linearError, - setLinearError, - linearSearchInput, - setLinearSearchInput, - appliedLinearSearch, - setAppliedLinearSearch, - linearIssueFiltersByWorkspaceId, - setLinearIssueFiltersByWorkspaceId, - linearAttributeFilterWorkspaceId, - linearAttributeFilter, - linearAttributeFilterReadRef, - linearPrimaryTeamRef, - linearViewMode, - setLinearViewMode, - linearGroupBy, - setLinearGroupBy, - linearOrderBy, - setLinearOrderBy, - linearDisplayProperties, - setLinearDisplayProperties, - linearTeamPropertyTouched, - setLinearTeamPropertyTouched, - linearRefreshNonce, - setLinearRefreshNonce, - linearProjectSearchInput, - setLinearProjectSearchInput, - appliedLinearProjectSearch, - setAppliedLinearProjectSearch, - linearProjectsResult, - setLinearProjectsResult, - linearProjectsLoading, - setLinearProjectsLoading, - linearProjectsError, - setLinearProjectsError, - selectedLinearProject, - setSelectedLinearProject, - selectedLinearProjectDetail, - setSelectedLinearProjectDetail, - linearProjectDetailLoading, - setLinearProjectDetailLoading, - linearProjectDetailError, - setLinearProjectDetailError, - linearProjectTab, - setLinearProjectTab, - linearProjectIssuesResult, - setLinearProjectIssuesResult, - linearProjectIssueLimit, - setLinearProjectIssueLimit, - linearProjectIssuePage, - setLinearProjectIssuePage, - linearProjectIssueLoadingTargetPage, - setLinearProjectIssueLoadingTargetPage, - linearProjectIssuesLoading, - setLinearProjectIssuesLoading, - linearProjectIssuesError, - setLinearProjectIssuesError, - linearCustomViewsResult, - setLinearCustomViewsResult, - linearCustomViewsLoading, - setLinearCustomViewsLoading, - linearCustomViewsError, - setLinearCustomViewsError, - selectedLinearCustomView, - setSelectedLinearCustomView, - linearProjectParentView, - setLinearProjectParentView, - linearCustomViewIssuesResult, - setLinearCustomViewIssuesResult, - linearCustomViewIssueLimit, - setLinearCustomViewIssueLimit, - linearCustomViewIssuePage, - setLinearCustomViewIssuePage, - linearCustomViewIssueLoadingTargetPage, - setLinearCustomViewIssueLoadingTargetPage, - linearCustomViewProjectsResult, - setLinearCustomViewProjectsResult, - linearCustomViewContentsLoading, - setLinearCustomViewContentsLoading, - linearCustomViewContentsError, - setLinearCustomViewContentsError, - openLinearProjectContext, - openLinearCustomViewContext, - linearBoardDraggingIssueId, - setLinearBoardDraggingIssueId, - linearBoardDragOverKey, - setLinearBoardDragOverKey, - linearBoardUpdatingIssueIds, - setLinearBoardUpdatingIssueIds, - lastLinearRequestRef, - landingLinearRefreshKeysRef, - linearContextResumeAttemptedRef, - patchScopedLinearIssue, - selectLinearMode - } = useTaskPageLinearListState({ - selectedLinearWorkspaceId, - clearSelectedLinearIssue, - setTaskResumeState - }) - - const { - jiraIssues, - setJiraIssues, - jiraLoading, - setJiraLoading, - jiraError, - setJiraError, - jiraErrorDetailsOpen, - setJiraErrorDetailsOpen, - jiraSearchInput, - setJiraSearchInput, - appliedJiraSearch, - setAppliedJiraSearch, - activeJiraPreset, - setActiveJiraPreset, - jiraRefreshNonce, - setJiraRefreshNonce, - jiraProjectStatusOrder, - setJiraProjectStatusOrder, - jiraOrderBy, - jiraOrderDirection, - jiraPrioritiesBySite, - handleJiraSort - } = useTaskPageJiraListState({ - selectedJiraSiteId, - taskSource, - jiraConnected, - jiraTaskSourceContext, - settings - }) - - useTaskPageSessionResume({ - persistedUIReady, - settings, - pageData, - resolvedInitialSelection, - taskResumeState, - visibleTaskProviders, - setTaskSource, - setRepoSelection, - setGithubMode, - setTaskSearchInput, - setAppliedTaskSearch, - setActiveTaskPreset, - setLinearMode, - setLinearSearchInput, - setAppliedLinearSearch, - setLinearViewMode, - setLinearGroupBy, - setLinearOrderBy, - setLinearDisplayProperties, - setLinearTeamPropertyTouched, - setLinearIssueFiltersByWorkspaceId, - setActiveJiraPreset, - setJiraSearchInput, - setAppliedJiraSearch, - taskResumeAppliedRef, - setTaskResumeApplied, - linearContextResumeAttemptedRef, - taskResumeApplied, - taskSource, - linearConnected, - fetchLinearProject, - fetchLinearCustomView, - listLinearCustomViews, - linearTaskSourceContext, - setTaskResumeState, - setSelectedLinearProject, - setSelectedLinearProjectDetail, - setLinearProjectParentView, - setLinearProjectsError, - setLinearCustomViewsLoading, - setLinearCustomViewsError, - setSelectedLinearCustomView - }) - - const { availableTeams, setAvailableTeams, setLinearTeamRefreshNonce } = useTaskPageLinearTeams({ - taskResumeApplied, - taskSource, - linearConnected, - selectedLinearWorkspaceId, - getCachedLinearTeams, - linearTaskSourceContext, - listLinearTeams - }) - - const { availableJiraProjects, jiraProjectsLoading } = useTaskPageJiraProjects({ - taskResumeApplied, - taskSource, - jiraConnected, - jiraTaskSourceContext, - settings, - selectedJiraSiteId - }) - - useTaskPageGitLabFetch({ - taskSource, - gitlabView, - activeGitlabFilter, - gitlabRefreshNonce, - selectedRepos, - selectedReposKey, - primaryRepo, - gitlabIssuePage, - setGitlabItems, - setGitlabLoading, - setGitlabError, - setGitlabIssuePage, - setGitlabIssueTotalPages, - setGitlabIssueLoadingTargetPage, - setGitlabTodos, - setGitlabTodosLoading - }) - - const { - defaultLinearTeamSelection, - linearTeamSelection, - setLinearTeamSelection, - activeLinearIssues, - activeLinearIssueLoading, - activeLinearIssueError, - activeLinearIssueHasCollectionError, - activeLinearIssueContextLabel, - activeLinearIssuePage, - activeLinearIssueLoadingTargetPage, - activeLinearIssueCanRequestMore, - activeLinearIssueLimit - } = useTaskPageLinearActiveCollection({ - settings, - selectedLinearProject, - linearProjectTab, - linearProjectIssuesResult, - selectedLinearCustomView, - linearCustomViewIssuesResult, - linearIssues, - linearProjectIssuesLoading, - linearCustomViewContentsLoading, - linearLoading, - linearStatus, - linearProjectIssuesError, - linearCustomViewContentsError, - linearError, - appliedLinearSearch, - linearIssuesHasMore, - linearIssueLimit, - linearProjectIssueLimit, - linearCustomViewIssueLimit, - linearProjectIssuePage, - linearCustomViewIssuePage, - linearIssuePage, - linearProjectIssueLoadingTargetPage, - linearCustomViewIssueLoadingTargetPage, - linearIssueLoadingTargetPage - }) - - const { - linearTeamOptions, - linearAttributePrimaryTeam, - applyLinearAttributeFilter, - linearSearchActive, - showLinearAttributeFilters, - linearIssueAttachmentIndex, - inOrcaLinkedLinearRefsSignature, - inOrcaLinkedLinearRefsRef, - filteredLinearIssues, - linearIssueTotalPages, - visibleLinearIssuePage, - pagedLinearIssues, - showLinearIssuePagination, - handleLinearIssuePageChange, - showLinearEmptyFilteredLoadMore, - handleLinearEmptyFilteredLoadMore - } = useTaskPageLinearIssueWindow({ - activeLinearIssues, - linearCacheSnapshot, - availableTeams, - defaultLinearTeamSelection, - linearTeamSelection, - setLinearTeamSelection, - linearAttributeFilterWorkspaceId, - setLinearIssueFiltersByWorkspaceId, - setLinearIssueLimit, - setLinearIssuePage, - setLinearIssueLoadingTargetPage, - linearPrimaryTeamRef, - linearAttributeFilter, - linearSearchInput, - appliedLinearSearch, - linearMode, - activeLinearIssueContextLabel, - allWorktrees, - folderWorkspaces, - selectedLinearWorkspaceId, - linearStatus, - linearOrderBy, - activeLinearIssueCanRequestMore, - activeLinearIssuePage, - activeLinearIssueError, - activeLinearIssueLoading, - selectedLinearProject, - linearProjectTab, - selectedLinearCustomView, - setLinearProjectIssuePage, - setLinearCustomViewIssuePage, - setLinearProjectIssueLoadingTargetPage, - setLinearCustomViewIssueLoadingTargetPage, - setLinearProjectIssueLimit, - setLinearCustomViewIssueLimit, - activeLinearIssueLimit, - activeLinearIssueLoadingTargetPage, - activeLinearIssueHasCollectionError - }) - - const { - selectedLinearTeamForExternalLink, - effectiveLinearDisplayProperties, - linearIssueGridStyle, - linearIssueListRows, - linearBoardSections, - linearStatusBoardEnabled, - handleLinearBoardCardDragStart, - handleLinearBoardDragOver, - handleLinearBoardDrop, - toggleLinearDisplayProperty - } = useTaskPageLinearBoard({ - linearTeamSelection, - linearTeamOptions, - linearDisplayProperties, - linearGroupBy, - linearTeamPropertyTouched, - pagedLinearIssues, - linearOrderBy, - linearBoardUpdatingIssueIds, - linearBoardDraggingIssueId, - filteredLinearIssues, - linearTaskSourceContext, - settings, - patchLinearIssue, - patchScopedLinearIssue, - invalidateLinearIssueLists, - setLinearBoardDraggingIssueId, - setLinearBoardDragOverKey, - setLinearBoardUpdatingIssueIds, - setSelectedLinearIssueFallback, - setLinearDisplayProperties, - setLinearTeamPropertyTouched - }) - - const { displayedJiraIssues, displayedJiraStatusOrder, sortedJiraIssues } = - useTaskPageJiraDisplayedIssues({ - jiraIssues, - jiraIssueCache: jiraCacheSnapshot.issueCache, - jiraSearchCache: jiraCacheSnapshot.searchCache, - jiraTaskSourceContext, - jiraTaskSourceScopeKey, - jiraProjectStatusOrder, - jiraOrderBy, - jiraOrderDirection, - jiraPrioritiesBySite - }) - - const { - newLinearProjectOpen, - setNewLinearProjectOpen, - newLinearProjectName, - setNewLinearProjectName, - newLinearProjectDescription, - setNewLinearProjectDescription, - newLinearProjectContent, - setNewLinearProjectContent, - setNewLinearProjectTeamId, - newLinearProjectLeadId, - setNewLinearProjectLeadId, - newLinearProjectMemberIds, - setNewLinearProjectMemberIds, - newLinearProjectLabelIds, - setNewLinearProjectLabelIds, - newLinearProjectPriority, - setNewLinearProjectPriority, - newLinearProjectStartDate, - setNewLinearProjectStartDate, - newLinearProjectTargetDate, - setNewLinearProjectTargetDate, - newLinearProjectSubmitting, - setNewLinearProjectSubmitting, - newLinearProjectTargetTeam, - newLinearProjectMembers, - newLinearProjectLabels, - discardNewLinearProjectDraft, - newLinearIssueOpen, - setNewLinearIssueOpen, - newLinearIssueTitle, - setNewLinearIssueTitle, - newLinearIssueBody, - setNewLinearIssueBody, - newLinearIssueTeamId, - setNewLinearIssueTeamId, - newLinearIssueSubmitting, - setNewLinearIssueSubmitting, - newLinearIssueStateId, - setNewLinearIssueStateId, - newLinearIssueAssigneeId, - setNewLinearIssueAssigneeId, - newLinearIssuePriority, - setNewLinearIssuePriority, - newLinearIssueProjectId, - setNewLinearIssueProjectId, - newLinearIssueLabelIds, - setNewLinearIssueLabelIds, - discardNewLinearIssueDraft, - newLinearIssueTargetTeam, - newLinearIssueProjects, - setNewLinearIssueProjects, - newLinearIssueProjectsLoading, - setNewLinearIssueProjectsLoading, - newLinearStates, - newLinearMembers, - newLinearLabels, - linearConnectOpen, - setLinearConnectOpen - } = useTaskPageLinearCreateDialogs({ - availableTeams, - settings, - linearConnected, - selectedLinearWorkspaceId, - linearTaskSourceContext, - selectedLinearProject - }) - - const [jiraConnectOpen, setJiraConnectOpen] = useState(false) - useContextualTour( - 'tasks', - !dialogWorkItem && - !gitlabDialogItem && - !selectedLinearIssue && - !newIssueOpen && - !newLinearProjectOpen && - !newLinearIssueOpen && - !linearConnectOpen && - !jiraConnectOpen && - activeModal === 'none', - 'tasks_open' - ) - - const activeGithubTaskKind = getGitHubTaskKind(activeTaskPreset, appliedTaskSearch) - const appliedTaskQuery = useMemo(() => parseTaskQuery(appliedTaskSearch), [appliedTaskSearch]) - - const { githubWorkItemMutationQueryKey, githubWorkItemMutation, selectedGitHubRepoExternalLink } = - useTaskPageGitHubMutationSession({ - selectedRepos, - githubMode, - appliedTaskSearch, - appliedTaskQuery, - githubViewerLogin, - setGitHubViewerLogin, - patchTaskPageWorkItemRows, - pages, - taskSource, - perRepoSourceState, - activeGithubTaskKind, - setQuietRefreshNonce - }) - - const { - newJiraIssueOpen, - setNewJiraIssueOpen, - newJiraIssueTitle, - setNewJiraIssueTitle, - newJiraIssueBody, - setNewJiraIssueBody, - setNewJiraIssueProjectId, - newJiraIssueProjectComboboxOpen, - setNewJiraIssueProjectComboboxOpen, - newJiraIssueProjectQuery, - setNewJiraIssueProjectQuery, - newJiraIssueProjectCommandValue, - setNewJiraIssueProjectCommandValue, - newJiraIssueTypeId, - setNewJiraIssueTypeId, - newJiraIssueSubmitting, - setNewJiraIssueSubmitting, - newJiraIssueProjectSearchInputRef, - availableJiraIssueTypes, - setAvailableJiraIssueTypes, - jiraIssueTypesLoading, - setJiraIssueTypesLoading, - setJiraCreateFields, - jiraCreateFieldsLoading, - setJiraCreateFieldsLoading, - jiraCreateFieldsError, - setJiraCreateFieldsError, - newJiraIssueCustomFieldValues, - setNewJiraIssueCustomFieldValues, - jiraUserFieldSelections, - setJiraUserFieldSelections, - discardNewJiraIssueDraft, - includeJiraSiteNameInProjectLabel, - sortedAvailableJiraProjects, - filteredNewJiraIssueProjects, - newJiraIssueTargetProject, - newJiraIssueTargetProjectSelectionKey, - newJiraIssueTargetType, - visibleJiraCreateFields, - hasMissingJiraCreateField, - handleNewJiraIssueProjectComboboxOpenChange, - handleNewJiraIssueProjectSelect, - handleNewJiraIssueProjectTriggerKeyDown - } = useTaskPageJiraCreateDialog({ - selectedJiraSiteId, - availableJiraProjects, - jiraConnected, - jiraViewer: jiraConnected ? jiraStatus.viewer : null, - jiraViewerSiteId: jiraConnected ? (jiraStatus.activeSiteId ?? null) : null, - settings, - jiraTaskSourceContext - }) - useTaskPageCreateDialogResets({ - providerRuntimeContextKey, - setNewLinearIssueOpen, - setNewLinearIssueTitle, - setNewLinearIssueBody, - setNewLinearIssueTeamId, - setNewLinearIssueStateId, - setNewLinearIssueAssigneeId, - setNewLinearIssuePriority, - setNewLinearIssueProjectId, - setNewLinearIssueLabelIds, - setNewLinearIssueProjects, - setNewLinearIssueProjectsLoading, - setNewLinearIssueSubmitting, - setNewJiraIssueOpen, - setNewJiraIssueTitle, - setNewJiraIssueBody, - setNewJiraIssueProjectId, - setNewJiraIssueProjectComboboxOpen, - setNewJiraIssueProjectQuery, - setNewJiraIssueProjectCommandValue, - setNewJiraIssueTypeId, - setAvailableJiraIssueTypes, - setJiraIssueTypesLoading, - setJiraCreateFields, - setJiraCreateFieldsLoading, - setJiraCreateFieldsError, - setNewJiraIssueCustomFieldValues, - setNewJiraIssueSubmitting - }) - - const { - filteredWorkItems, - softHiddenVisibleCount, - showGitHubTaskSkeletons, - loadedGitHubAuthorLogins, - primaryGithubFilterSlug, - showPRManagementColumns, - githubTaskGridClass, - ensurePRChecksLoaded, - totalPages - } = useTaskPageGitHubFilteredItems({ - activeGithubTaskKind, - pages, - currentPage, - githubWorkItemMutation, - tasksFiltering, - tasksLoading, - perRepoSourceState, - repoMap, - fetchPRChecks, - patchTaskPageWorkItemRows, - taskSource, - githubMode, - githubPageSize, - countedTotalPages, - provenPageLimit - }) - - const { handleLoadNextPage } = useTaskPageGitHubPageLoader({ - paginationLoading, - selectedRepos, - appliedTaskSearch, - paginationGenerationRef, - currentPage, - setPaginationLoading, - setLoadingTargetPage, - fetchWorkItemsNextPage, - githubPerRepoPageLimit, - githubPageSize, - countedTotalPagesRef, - setProvenPageLimit, - setCountedTotalPages, - pagesRef, - currentPageRef, - setPages, - setCurrentPage - }) - - const commitTaskSearch = useCallback( - (value: string): void => { - const scoped = scopeGitHubTaskSearch(value, activeGithubTaskKind) - if (scoped !== appliedTaskSearch) { - setTasksFiltering(true) - } - setAppliedTaskSearch(scoped) - }, - [activeGithubTaskKind, appliedTaskSearch, setAppliedTaskSearch, setTasksFiltering] - ) - useGitHubTaskSearchCommit({ - enabled: taskResumeApplied, - onCommit: commitTaskSearch, - value: taskSearchInput - }) - - useEffect(() => { - if (!taskResumeApplied) { - return - } - if (!githubSearchPersistReadyRef.current) { - githubSearchPersistReadyRef.current = true - return - } - // Why: persist the applied query unconditionally to cover paths that change appliedTaskSearch outside the preset handler. - setTaskResumeState({ - githubItemsPreset: activeTaskPreset, - githubItemsQuery: appliedTaskSearch.trim() - }) - }, [ - activeTaskPreset, - appliedTaskSearch, - setTaskResumeState, - taskResumeApplied, - githubSearchPersistReadyRef - ]) - - useTaskPageGitHubFetch({ - taskResumeApplied, - taskSource, - githubMode, - selectedRepos, - setRetryingSourceKeys, - setTasksRefreshing, - setTasksFiltering, - appliedTaskSearch, - githubResumeContextRef, - githubResumeContextKey, - githubResumeConsumedRef, - currentPageRef, - pagesRef, - pendingGithubScrollRestoreRef, - getCachedWorkItems, - githubPerRepoPageLimit, - githubPageSize, - githubWorkItemMutationQueryKey, - setPages, - setCurrentPage, - setCountedTotalPages, - countedTotalPagesRef, - setProvenPageLimit, - setTasksError, - setFailedCount, - setGithubUnavailable, - setTasksLoading, - taskRefreshNonce, - lastFetchedNonceRef, - workItemsInvalidationNonce, - lastFetchedInvalidationNonceRef, - hardRefreshEpochRef, - landingGitHubRefreshKeysRef, - paginationGenerationRef, - setPaginationLoading, - setLoadingTargetPage, - fetchWorkItemsNextPage, - fetchWorkItemsAcrossRepos, - countWorkItemsAcrossRepos, - retryingSourceKeys, - selectedReposKey - }) - - useTaskPageGitHubQuietRevalidate({ - githubWorkItemMutationQueryKey, - quietRefreshNonce, - taskSource, - githubMode, - selectedRepos, - pagesRef, - pages, - currentPage, - currentPageRef, - appliedTaskSearch, - hardRefreshEpochRef, - fetchWorkItemsAcrossRepos, - fetchWorkItemsNextPage, - githubPerRepoPageLimit, - githubPageSize, - setPages, - setQuietRefreshNonce - }) - - const applyPRFilterChange = useCallback( - (change: PRFilterChange): void => { - let next = scopeGitHubTaskSearch(taskSearchInput, activeGithubTaskKind) - // Why: withQualifier round-trips through parseTaskQuery so each dropdown's patch preserves prior filters and free-text. - if ('author' in change) { - next = withQualifier(next, 'author', change.author ?? null) - } - if ('assignee' in change) { - next = withQualifier(next, 'assignee', change.assignee ?? null) - } - if ('labels' in change) { - next = withQualifier(next, 'labels', change.labels ?? []) - } - if ('state' in change && change.state) { - next = withQualifier(next, 'state', change.state) - if (change.state !== 'open') { - next = withQualifier(next, 'draft', null) - } - } - if ('draft' in change) { - next = withQualifier(next, 'draft', change.draft ? 'true' : 'false') - } - if ('reviewer' in change) { - // Why: the two reviewer qualifiers are mutually exclusive — clear the other whenever one is set so the chip matches the query. - const reviewer = change.reviewer ?? null - if (reviewer === null) { - next = withQualifier(next, 'reviewRequested', null) - next = withQualifier(next, 'reviewedBy', null) - } else if (reviewer.kind === 'requested') { - next = withQualifier(next, 'reviewedBy', null) - next = withQualifier(next, 'reviewRequested', reviewer.login) - } else { - next = withQualifier(next, 'reviewRequested', null) - next = withQualifier(next, 'reviewedBy', reviewer.login) - } - } - setTaskSearchInput(next) - setAppliedTaskSearch(next) - setActiveTaskPreset(null) - setTaskResumeState({ githubItemsPreset: null, githubItemsQuery: next }) - // Why: a filter change replaces every row's meaning; show the load skeleton so stale rows don't read as if the filter did nothing. - setTasksFiltering(true) - setTaskRefreshNonce((current) => current + 1) - }, - [ - activeGithubTaskKind, - setTaskResumeState, - taskSearchInput, - setTaskSearchInput, - setActiveTaskPreset, - setTasksFiltering, - setAppliedTaskSearch, - setTaskRefreshNonce - ] - ) - - const handleApplyTaskSearch = useCallback((): void => { - const scoped = scopeGitHubTaskSearch(taskSearchInput, activeGithubTaskKind) - setTaskSearchInput(scoped) - setAppliedTaskSearch(scoped) - setActiveTaskPreset(null) - setTaskResumeState({ githubItemsPreset: null, githubItemsQuery: scoped }) - setTasksFiltering(true) - setTaskRefreshNonce((current) => current + 1) - }, [ - activeGithubTaskKind, - setTaskResumeState, - taskSearchInput, - setAppliedTaskSearch, - setTaskRefreshNonce, - setTaskSearchInput, - setActiveTaskPreset, - setTasksFiltering - ]) - - const handleTaskSearchChange = useCallback( - (event: React.ChangeEvent): void => { - const next = event.target.value - setTaskSearchInput(next) - setActiveTaskPreset(null) - }, - [setTaskSearchInput, setActiveTaskPreset] - ) - - const handleSetDefaultTaskPreset = useCallback( - (presetId: TaskViewPresetId): void => { - // Why: the default task view is a durable preference, so persist it instead of only changing page state. - void updateSettings({ defaultTaskViewPreset: presetId }).catch(() => { - toast.error( - translate('auto.components.TaskPage.fe380f306c', 'Failed to save default task view.') - ) - }) - }, - [updateSettings] - ) - - const handleSelectGithubTaskKind = useCallback( - (kind: GitHubTaskKind): void => { - const preset = getDefaultPresetForGitHubTaskKind(kind) - const query = getTaskPresetQuery(preset) - setTaskSearchInput(query) - setAppliedTaskSearch(query) - setActiveTaskPreset(preset) - setTaskResumeState({ - githubItemsPreset: preset, - githubItemsQuery: query - }) - setTasksFiltering(true) - setTaskRefreshNonce((current) => current + 1) - }, - [ - setTaskResumeState, - setAppliedTaskSearch, - setTaskRefreshNonce, - setTaskSearchInput, - setActiveTaskPreset, - setTasksFiltering - ] - ) - - const handleResetGithubTaskSearch = useCallback((): void => { - handleSelectGithubTaskKind(activeGithubTaskKind) - }, [activeGithubTaskKind, handleSelectGithubTaskKind]) - - const handleTaskSearchKeyDown = useCallback( - (event: React.KeyboardEvent): void => { - if (event.key === 'Enter') { - // React SyntheticEvent does not expose isComposing; use nativeEvent. - if ( - shouldSuppressEnterSubmit( - { isComposing: event.nativeEvent.isComposing, shiftKey: event.shiftKey }, - false - ) - ) { - return - } - event.preventDefault() - handleApplyTaskSearch() - } - }, - [handleApplyTaskSearch] - ) - - useEffect(() => { - if ( - taskSource !== 'github' || - githubMode !== 'items' || - dialogWorkItem || - newIssueOpen || - newLinearProjectOpen || - newLinearIssueOpen || - newJiraIssueOpen || - activeModal !== 'none' - ) { - return - } - - const onKeyDown = (event: KeyboardEvent): void => { - const isMac = navigator.userAgent.includes('Mac') - const modifierPressed = isMac ? event.metaKey : event.ctrlKey - if (!modifierPressed || event.altKey || event.shiftKey || event.key.toLowerCase() !== 'f') { - return - } - - const input = taskSearchInputRef.current - if (!input) { - return - } - const target = event.target - if ( - target instanceof HTMLElement && - target !== input && - (target instanceof HTMLInputElement || - target instanceof HTMLTextAreaElement || - target.isContentEditable) - ) { - return - } - - event.preventDefault() - event.stopPropagation() - input.focus() - input.select() - } - - window.addEventListener('keydown', onKeyDown, { capture: true }) - return () => window.removeEventListener('keydown', onKeyDown, { capture: true }) - }, [ - activeModal, - dialogWorkItem, - githubMode, - newIssueOpen, - newLinearProjectOpen, - newLinearIssueOpen, - newJiraIssueOpen, - taskSource, - - taskSearchInputRef - ]) - - const { handleUseWorkItem, handleOpenOrUseGitHubWorkItem, handleUseGitLabItem } = - useTaskPageUseItemActions({ - openModal, - repoMap - }) - - const { handleCreateNewIssue } = useTaskPageCreateGithubSubmit({ - newIssueTargetRepo, - newIssueTitle, - newIssueSubmitting, - newIssueRuntimeTarget, - newIssueSourceContext, - newIssueBody, - newIssueLabels, - newIssueAssignees, - setNewIssueSubmitting, - setNewIssueOpen, - setNewIssueTitle, - setNewIssueDraft, - setNewIssueBody, - setNewIssueLabels, - setNewIssueAssignees, - clearNewIssueDraft, - setTaskRefreshNonce, - openGitHubDetailPage, - setDialogWorkItem - }) - - const { handleCreateNewLinearProject, handleCreateNewLinearIssue } = - useTaskPageCreateLinearSubmits({ - newLinearProjectTargetTeam, - newLinearProjectName, - newLinearProjectSubmitting, - linearTaskSourceContext, - settings, - newLinearProjectDescription, - newLinearProjectContent, - newLinearProjectLeadId, - newLinearProjectMemberIds, - newLinearProjectLabelIds, - newLinearProjectPriority, - newLinearProjectStartDate, - newLinearProjectTargetDate, - discardNewLinearProjectDraft, - setNewLinearProjectSubmitting, - setNewLinearProjectOpen, - setNewLinearProjectName, - setNewLinearProjectDescription, - setNewLinearProjectContent, - setNewLinearProjectLeadId, - setNewLinearProjectMemberIds, - setNewLinearProjectLabelIds, - setNewLinearProjectPriority, - setNewLinearProjectStartDate, - setNewLinearProjectTargetDate, - setAppliedLinearProjectSearch, - setLinearProjectSearchInput, - setLinearProjectsResult, - setSelectedLinearProjectDetail, - openLinearProjectContext, - setLinearRefreshNonce, - newLinearIssueTargetTeam, - newLinearIssueTitle, - newLinearIssueSubmitting, - selectedLinearProject, - newLinearIssueProjectId, - providerRuntimeContextKey, - providerRuntimeContextKeyRef, - newLinearIssueBody, - newLinearIssueStateId, - newLinearIssuePriority, - newLinearIssueAssigneeId, - newLinearIssueLabelIds, - discardNewLinearIssueDraft, - setNewLinearIssueSubmitting, - setNewLinearIssueOpen, - setNewLinearIssueTitle, - setNewLinearIssueBody, - setNewLinearIssueStateId, - setNewLinearIssueAssigneeId, - setNewLinearIssuePriority, - setNewLinearIssueProjectId, - setNewLinearIssueLabelIds, - setSelectedLinearIssue - }) - - const { handleCreateNewJiraIssue } = useTaskPageCreateJiraSubmit({ - newJiraIssueTargetProject, - newJiraIssueTargetType, - newJiraIssueTitle, - newJiraIssueSubmitting, - hasMissingJiraCreateField, - jiraCreateFieldsLoading, - visibleJiraCreateFields, - newJiraIssueCustomFieldValues, - providerRuntimeContextKey, - providerRuntimeContextKeyRef, - jiraTaskSourceContext, - settings, - newJiraIssueBody, - discardNewJiraIssueDraft, - setNewJiraIssueSubmitting, - setNewJiraIssueOpen, - setNewJiraIssueTitle, - setNewJiraIssueBody, - setNewJiraIssueCustomFieldValues, - setJiraRefreshNonce, - setJiraIssues, - setSelectedJiraIssue - }) - - const githubTasksBusy = tasksLoading || tasksRefreshing || tasksFiltering - - useEffect(() => { - // Why: when a modal is open, let it own Esc dismissal. - if ( - dialogWorkItem || - selectedJiraIssue || - selectedLinearIssue || - newIssueOpen || - newLinearIssueOpen || - newJiraIssueOpen || - activeModal !== 'none' - ) { - return - } - - const onKeyDown = (event: KeyboardEvent): void => { - if (event.key !== 'Escape') { - return - } - - const target = event.target - if (!(target instanceof HTMLElement)) { - return - } - - // Why: open menus/popovers/selects own Esc; capture-phase leave would steal it from Radix. - if ( - document.querySelector( - '[data-slot="dropdown-menu-content"], [data-slot="popover-content"], [data-slot="select-content"], [role="menu"]' - ) - ) { - return - } - - // Why: Esc first blurs a focused input so it doesn't accidentally close the whole page; only closes once focus is outside an input. - if ( - target instanceof HTMLInputElement || - target instanceof HTMLTextAreaElement || - target instanceof HTMLSelectElement || - target.isContentEditable - ) { - event.preventDefault() - target.blur() - return - } - - event.preventDefault() - closeTaskPage() - } - - window.addEventListener('keydown', onKeyDown, { capture: true }) - return () => window.removeEventListener('keydown', onKeyDown, { capture: true }) - }, [ - activeModal, - closeTaskPage, - dialogWorkItem, - newIssueOpen, - newLinearIssueOpen, - newJiraIssueOpen, - selectedLinearIssue, - selectedJiraIssue - ]) - - useEffect(() => { - if (!preflightStatusCurrent || !preflightStatusChecked) { - void refreshPreflightStatus() - } - if (!linearStatusReady) { - void checkLinearConnection() - } - if (!jiraStatusReady) { - void checkJiraConnection() - } - }, [ - checkJiraConnection, - checkLinearConnection, - expectedPreflightContextKey, - jiraStatusContextKey, - jiraStatusReady, - linearStatusContextKey, - linearStatusReady, - providerRuntimeContextKey, - preflightStatusContextKey, - preflightStatusChecked, - preflightStatusCurrent, - refreshPreflightStatus - ]) - - // Why: debounce the Linear search input so we don't fire a request per keystroke (300ms, matching GitHub search). - useEffect(() => { - if (!taskResumeApplied) { - return - } - const timeout = window.setTimeout(() => { - setAppliedLinearSearch(linearSearchInput) - }, TASK_SEARCH_DEBOUNCE_MS) - return () => window.clearTimeout(timeout) - }, [linearSearchInput, taskResumeApplied, setAppliedLinearSearch]) - - useEffect(() => { - if (!taskResumeApplied) { - return - } - if (!linearSearchPersistReadyRef.current) { - linearSearchPersistReadyRef.current = true - return - } - setTaskResumeState({ linearQuery: appliedLinearSearch.trim() }) - }, [appliedLinearSearch, setTaskResumeState, taskResumeApplied, linearSearchPersistReadyRef]) - - useEffect(() => { - if (!taskResumeApplied) { - return - } - if (!linearViewPersistReadyRef.current) { - linearViewPersistReadyRef.current = true - return - } - saveLinearIssueView( - serializeLinearIssueViewResumeState({ - viewMode: linearViewMode, - groupBy: linearGroupBy, - orderBy: linearOrderBy, - displayProperties: linearDisplayProperties, - teamPropertyTouched: linearTeamPropertyTouched, - filtersByWorkspaceId: linearIssueFiltersByWorkspaceId - }) - ) - }, [ - linearDisplayProperties, - linearGroupBy, - linearIssueFiltersByWorkspaceId, - linearOrderBy, - linearTeamPropertyTouched, - linearViewMode, - taskResumeApplied, - - linearViewPersistReadyRef - ]) - - useEffect(() => { - setLinearIssueLimit(LINEAR_ITEM_LIMIT) - setLinearIssuePage(0) - setLinearIssueLoadingTargetPage(null) - }, [ - appliedLinearSearch, - linearMode, - selectedLinearCustomView?.id, - selectedLinearProject?.id, - selectedLinearWorkspaceId, - taskSource, - setLinearIssueLoadingTargetPage, - setLinearIssueLimit, - setLinearIssuePage - ]) - - useTaskPageLinearFetch({ - taskResumeApplied, - taskSource, - linearMode, - linearConnected, - appliedLinearSearch, - linearIssueLimit, - linearAttributeFilter, - selectedLinearWorkspaceId, - getCachedLinearIssues, - linearTaskSourceContext, - linearAttributeFilterWorkspaceId, - linearAttributeFilterReadRef, - lastLinearRequestRef, - landingLinearRefreshKeysRef, - linearRefreshNonce, - searchLinearIssues, - listLinearIssues, - setLinearError, - setLinearIssuesHasMore, - setLinearIssues, - setLinearLoading, - linearListInvalidationVersionForSource - }) - - // Why: Has Worktree loads Linear tickets linked on local worktrees, not a Linear list/search query. - useEffect(() => { - if (!taskResumeApplied) { - return - } - if (taskSource !== 'linear' || linearMode !== 'in-orca' || !linearConnected) { - return - } - - let cancelled = false - const linkedRefs = inOrcaLinkedLinearRefsRef.current - const requestSignature = `in-orca::${selectedLinearWorkspaceId ?? 'default'}::${inOrcaLinkedLinearRefsSignature}` - const previousRequest = lastLinearRequestRef.current - const isNewSignature = previousRequest?.signature !== requestSignature - const forceRefresh = linearRefreshNonce > 0 && previousRequest?.nonce !== linearRefreshNonce - lastLinearRequestRef.current = { nonce: linearRefreshNonce, signature: requestSignature } - setLinearIssuesHasMore(false) - setLinearError(null) - - if (linkedRefs.length === 0) { - setLinearIssues([]) - setLinearLoading(false) - return () => { - cancelled = true - } - } - - if (isNewSignature) { - setLinearIssues([]) - } - setLinearLoading(true) - // Why: fetchLinearIssue serves anything under the 60s TTL and ignores `force`, so an - // explicit refresh has to go through refreshLinearIssue or the button does nothing. - void readLinkedLinearIssuesWithLimit(linkedRefs, (ref) => { - const read = forceRefresh ? refreshLinearIssue : fetchLinearIssue - return read(ref.identifier, ref.workspaceId ?? selectedLinearWorkspaceId, { - sourceContext: ref.sourceContext ?? linearTaskSourceContext - }) - }) - .then((results) => { - if ( - cancelled || - lastLinearRequestRef.current?.signature !== requestSignature || - lastLinearRequestRef.current?.nonce !== linearRefreshNonce - ) { - return - } - const loaded = results.filter((issue): issue is LinearIssue => issue != null) - // Why: reads resolve to null instead of throwing, so an all-null result with links - // present is a load failure — not the "nothing linked yet" empty state. - if (loaded.length === 0) { - setLinearError( - translate( - 'auto.components.TaskPage.linearHasWorktreeLoadFailed', - 'Unable to load Linear issues linked to an Orca workspace.' - ) - ) - setLinearIssues([]) - setLinearLoading(false) - return - } - if (loaded.length !== results.length) { - setLinearError( - translate( - 'auto.components.TaskPage.linearHasWorktreePartialLoadFailed', - 'Some Linear issues linked to an Orca workspace could not be loaded. Refresh to try again.' - ) - ) - } - setLinearIssues(filterLinearIssuesForInOrcaWorkspace(loaded, selectedLinearWorkspaceId)) - setLinearLoading(false) - }) - .catch((err) => { - if ( - cancelled || - lastLinearRequestRef.current?.signature !== requestSignature || - lastLinearRequestRef.current?.nonce !== linearRefreshNonce - ) { - return - } - setLinearError(err instanceof Error ? err.message : 'Failed to load Linear issues.') - setLinearLoading(false) - }) - - return () => { - cancelled = true - } - // Why: linkedRefs are read from a ref keyed by their signature, so unrelated worktree - // churn (activity stamps, unread flags) can't re-issue one read per linked ticket. - }, [ - fetchLinearIssue, - inOrcaLinkedLinearRefsSignature, - linearConnected, - linearMode, - linearRefreshNonce, - linearTaskSourceContext, - inOrcaLinkedLinearRefsRef, - lastLinearRequestRef, - lastLinearRequestRef.current?.nonce, - lastLinearRequestRef.current?.signature, - refreshLinearIssue, - setLinearError, - setLinearIssues, - setLinearIssuesHasMore, - setLinearLoading, - selectedLinearWorkspaceId, - taskResumeApplied, - taskSource - ]) - - useEffect(() => { - if (!taskResumeApplied) { - return - } - const timeout = window.setTimeout(() => { - setAppliedLinearProjectSearch(linearProjectSearchInput) - }, TASK_SEARCH_DEBOUNCE_MS) - return () => window.clearTimeout(timeout) - }, [linearProjectSearchInput, setAppliedLinearProjectSearch, taskResumeApplied]) - - useEffect(() => { - if (!taskResumeApplied || taskSource !== 'linear' || linearMode !== 'projects') { - return - } - if (!linearConnected || selectedLinearProject) { - return - } - let cancelled = false - const query = appliedLinearProjectSearch.trim() - const cached = getCachedLinearProjects(query || undefined, LINEAR_ITEM_LIMIT, undefined, { - sourceContext: linearTaskSourceContext - }) - if (cached) { - setLinearProjectsResult(cached) - } - const force = linearRefreshNonce > 0 - setLinearProjectsLoading(force || cached === null) - setLinearProjectsError(null) - void listLinearProjectsFromStore(query || undefined, LINEAR_ITEM_LIMIT, undefined, { - force, - sourceContext: linearTaskSourceContext - }) - .then((result) => { - if (!cancelled) { - setLinearProjectsResult(result) - setLinearProjectsLoading(false) - } - }) - .catch((error) => { - if (!cancelled) { - setLinearProjectsError( - error instanceof Error ? error.message : 'Failed to load projects.' - ) - setLinearProjectsLoading(false) - } - }) - return () => { - cancelled = true - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ - taskResumeApplied, - taskSource, - linearMode, - linearConnected, - selectedLinearWorkspaceId, - selectedLinearProject, - appliedLinearProjectSearch, - linearRefreshNonce, - getCachedLinearProjects, - linearTaskSourceContext - ]) - - useEffect(() => { - if (!selectedLinearProject?.workspaceId) { - setSelectedLinearProjectDetail(null) - return - } - let cancelled = false - setLinearProjectDetailLoading(true) - setLinearProjectDetailError(null) - void fetchLinearProject(selectedLinearProject.id, selectedLinearProject.workspaceId, { - force: linearRefreshNonce > 0, - sourceContext: linearTaskSourceContext - }) - .then((project) => { - if (!cancelled) { - setSelectedLinearProjectDetail(project) - setLinearProjectDetailLoading(false) - if (!project) { - setSelectedLinearProject(null) - setLinearProjectParentView(null) - setLinearProjectDetailError(null) - setLinearProjectsError('Project was not found.') - setTaskResumeState({ linearContext: undefined }) - } - } - }) - .catch((error) => { - if (!cancelled) { - setLinearProjectDetailError( - error instanceof Error ? error.message : 'Failed to load project.' - ) - setLinearProjectDetailLoading(false) - } - }) - return () => { - cancelled = true - } - }, [ - fetchLinearProject, - linearRefreshNonce, - selectedLinearProject, - setLinearProjectDetailError, - setLinearProjectDetailLoading, - setLinearProjectParentView, - setLinearProjectsError, - setSelectedLinearProject, - setSelectedLinearProjectDetail, - setTaskResumeState, - linearTaskSourceContext - ]) - - useEffect(() => { - if (!selectedLinearProject?.workspaceId || linearProjectTab !== 'issues') { - return - } - let cancelled = false - setLinearProjectIssuesLoading(true) - setLinearProjectIssuesError(null) - const effectiveLimit = clampLinearIssueListLimit(linearProjectIssueLimit) - void listLinearProjectIssues( - selectedLinearProject.id, - selectedLinearProject.workspaceId, - effectiveLimit, - { force: linearRefreshNonce > 0, sourceContext: linearTaskSourceContext } - ) - .then((result) => { - if (!cancelled) { - setLinearProjectIssuesResult(result) - setLinearProjectIssuesLoading(false) - } - }) - .catch((error) => { - if (!cancelled) { - setLinearProjectIssuesError( - error instanceof Error ? error.message : 'Failed to load project issues.' - ) - setLinearProjectIssuesLoading(false) - } - }) - return () => { - cancelled = true - } - }, [ - linearProjectIssueLimit, - linearProjectTab, - linearRefreshNonce, - listLinearProjectIssues, - linearTaskSourceContext, - selectedLinearProject, - setLinearProjectIssuesError, - setLinearProjectIssuesLoading, - setLinearProjectIssuesResult - ]) - - useEffect(() => { - if (!taskResumeApplied || taskSource !== 'linear' || linearMode !== 'views') { - return - } - if (!linearConnected || selectedLinearCustomView) { - return - } - let cancelled = false - const cachedResults = LINEAR_CUSTOM_VIEW_MODELS.map((model) => - getCachedLinearCustomViews(model, LINEAR_ITEM_LIMIT, undefined, { - sourceContext: linearTaskSourceContext - }) - ) - const allCached = cachedResults.every( - (result): result is LinearCollectionResult => result !== null - ) - if (allCached) { - setLinearCustomViewsResult(mergeLinearCollectionResults(cachedResults)) - } - const force = linearRefreshNonce > 0 - setLinearCustomViewsLoading(force || !allCached) - setLinearCustomViewsError(null) - // Why: the Views tab already has a Model column, so list both models rather than add a redundant Issues/Projects switch. - void Promise.all( - LINEAR_CUSTOM_VIEW_MODELS.map((model) => - listLinearCustomViews(model, LINEAR_ITEM_LIMIT, undefined, { - force, - sourceContext: linearTaskSourceContext - }) - ) - ) - .then((result) => { - if (!cancelled) { - setLinearCustomViewsResult(mergeLinearCollectionResults(result)) - setLinearCustomViewsLoading(false) - } - }) - .catch((error) => { - if (!cancelled) { - setLinearCustomViewsError( - error instanceof Error ? error.message : 'Failed to load views.' - ) - setLinearCustomViewsLoading(false) - } - }) - return () => { - cancelled = true - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ - taskResumeApplied, - taskSource, - linearMode, - linearConnected, - selectedLinearWorkspaceId, - selectedLinearCustomView, - linearRefreshNonce, - getCachedLinearCustomViews, - listLinearCustomViews, - linearTaskSourceContext - ]) - - useEffect(() => { - if (!selectedLinearCustomView?.workspaceId) { - setLinearCustomViewIssuesResult({ items: [] }) - setLinearCustomViewProjectsResult({ items: [] }) - return - } - let cancelled = false - setLinearCustomViewContentsLoading(true) - setLinearCustomViewContentsError(null) - const issueLimit = clampLinearIssueListLimit(linearCustomViewIssueLimit) - const request = - selectedLinearCustomView.model === 'issue' - ? listLinearCustomViewIssues( - selectedLinearCustomView.id, - selectedLinearCustomView.workspaceId, - issueLimit, - { force: linearRefreshNonce > 0, sourceContext: linearTaskSourceContext } - ) - : listLinearCustomViewProjects( - selectedLinearCustomView.id, - selectedLinearCustomView.workspaceId, - LINEAR_ITEM_LIMIT, - { force: linearRefreshNonce > 0, sourceContext: linearTaskSourceContext } - ) - void request - .then((result) => { - if (cancelled) { - return - } - if (selectedLinearCustomView.model === 'issue') { - setLinearCustomViewIssuesResult(result as LinearCollectionResult) - } else { - setLinearCustomViewProjectsResult(result as LinearCollectionResult) - } - setLinearCustomViewContentsLoading(false) - }) - .catch((error) => { - if (!cancelled) { - setLinearCustomViewContentsError( - error instanceof Error ? error.message : 'Failed to load view contents.' - ) - setLinearCustomViewContentsLoading(false) - } - }) - return () => { - cancelled = true - } - }, [ - linearRefreshNonce, - linearCustomViewIssueLimit, - listLinearCustomViewIssues, - listLinearCustomViewProjects, - linearTaskSourceContext, - selectedLinearCustomView, - setLinearCustomViewContentsError, - setLinearCustomViewContentsLoading, - setLinearCustomViewIssuesResult, - setLinearCustomViewProjectsResult - ]) - - useEffect(() => { - if (!taskResumeApplied || taskSource !== 'linear') { - return - } - - if (!linearConnected) { - clearSelectedLinearIssue() - return - } - - if (filteredLinearIssues.length === 0) { - if (!selectedLinearIssueCanFloat) { - clearSelectedLinearIssue() - } - return - } - - // Why: list-first — keep an open inspector only while its issue stays in the filter, not auto-open row 1; user-directed sub-issue nav stays. - if ( - selectedLinearIssueId && - !selectedLinearIssueCanFloat && - !filteredLinearIssues.some((issue) => issue.id === selectedLinearIssueId) - ) { - clearSelectedLinearIssue() - } - }, [ - clearSelectedLinearIssue, - filteredLinearIssues, - linearConnected, - selectedLinearIssueCanFloat, - selectedLinearIssueId, - taskResumeApplied, - taskSource - ]) - - useTaskPageJiraFetch({ - taskResumeApplied, - jiraSearchInput, - setAppliedJiraSearch, - jiraSearchPersistReadyRef, - appliedJiraSearch, - setTaskResumeState, - taskSource, - jiraConnected, - setJiraLoading, - setJiraError, - setJiraErrorDetailsOpen, - searchJiraIssues, - listJiraIssues, - activeJiraPreset, - jiraTaskSourceContext, - selectedJiraSiteId, - jiraRefreshNonce, - jiraTaskSourceScopeKey, - settings, - setJiraIssues, - setJiraProjectStatusOrder, - displayedJiraIssues, - selectedJiraIssueKey, - setSelectedJiraIssueKey, - selectedJiraIssueFallback, - setSelectedJiraIssueFallback - }) - - const { - handleUseLinearItem, - handleOpenOrUseLinearItem, - handleLinearWorkspaceChange, - handleLinearTeamSelectionChange, - handleLinearScopeOpen, - handleLinearAccessConnected - } = useTaskPageLinearActions({ - linearTaskSourceContext, - openModal, - clearSelectedLinearIssue, - setSelectedLinearProject, - setSelectedLinearProjectDetail, - setSelectedLinearCustomView, - setLinearProjectParentView, - setLinearProjectTab, - setLinearProjectsResult, - setLinearCustomViewsResult, - setLinearProjectIssuesResult, - setLinearCustomViewIssuesResult, - setLinearCustomViewProjectsResult, - setLinearProjectDetailError, - setLinearProjectsError, - setLinearCustomViewsError, - setLinearCustomViewContentsError, - setTaskResumeState, - linearMode, - linearContextResumeAttemptedRef, - setLinearIssues, - setLinearError, - setLinearLoading, - selectLinearWorkspace, - setLinearTeamRefreshNonce, - setLinearTeamSelection, - updateSettings, - checkLinearConnection, - listLinearTeams, - selectedLinearWorkspaceId, - setAvailableTeams, - setLinearRefreshNonce - }) - - const { handleUseJiraItem } = useTaskPageJiraActions({ - jiraSites, - jiraTaskSourceContext, - openModal - }) - - const taskPageListChromeHidden = shouldHideTaskPageListChrome({ - taskSource, - hasGitHubDetail: Boolean(dialogWorkItem), - hasGitLabDetail: Boolean(gitlabDialogItem), - hasJiraDetail: Boolean(selectedJiraIssue), - hasLinearIssueDetail: Boolean(selectedLinearIssue), - hasLinearProjectContext: Boolean(selectedLinearProject), - hasLinearViewContext: Boolean(selectedLinearCustomView) - }) - - const sourceToolbar: TaskPageSourceToolbarProps = { - closeTaskPage, - visibleSourceOptions, - taskSource, - taskSourceAvailabilityNoticeByProvider, - taskSourceManuallyChangedRef, - openTaskPage, - updateSettings, - taskSourceContextSummary, - linearConnected, - linearWorkspaces, - selectedLinearWorkspaceId, - linearTeamOptions, - linearTeamSelection, - defaultLinearTeamSelection, - onLinearWorkspaceChange: handleLinearWorkspaceChange, - onLinearTeamSelectionChange: handleLinearTeamSelectionChange, - onOpenLinearConnect: () => setLinearConnectOpen(true), - onLinearScopeOpen: handleLinearScopeOpen, - selectedLinearTeamForExternalLink, - jiraConnected, - jiraSites, - selectedJiraSiteId, - selectJiraSite, - setSelectedJiraIssueKey, - setSelectedJiraIssueFallback, - setJiraIssues, - setJiraError, - setJiraLoading, - taskSourceAvailabilityNotice - } - const githubModeBar: TaskPageGithubModeBarProps = { - projectModeVisible, - githubModeButtons, - githubMode, - activeGithubTaskKind, - setGithubMode, - setTaskResumeState, - handleSelectGithubTaskKind, - taskPickerGroups, - repoSelection, - getTaskPickerRepoHostLabel, - eligibleRepos, - setRepoSelection, - updateSettings, - taskPickerRepos, - selectedGitHubRepoExternalLink - } - const githubItemFilters: TaskPageGithubItemFiltersProps = { - activeGithubTaskKind, - activeTaskPreset, - setTaskSearchInput, - setAppliedTaskSearch, - setActiveTaskPreset, - setTaskResumeState, - setTaskRefreshNonce, - handleSetDefaultTaskPreset, - appliedTaskQuery, - loadedGitHubAuthorLogins, - primaryGithubFilterSlug, - settings, - applyPRFilterChange, - taskSearchInputRef, - taskSearchInput, - handleTaskSearchChange, - handleTaskSearchKeyDown, - appliedTaskSearch, - handleResetGithubTaskSearch, - selectedRepos, - setNewIssueTitle, - setNewIssueBody, - setNewIssueLabels, - setNewIssueAssignees, - setNewIssueRepoId, - setNewIssueOpen, - newIssueTargetRepo, - handleRefreshGithubTasks, - githubTasksBusy, - perRepoSourceState, - setIssueSourcePreference - } - const linearFilters: TaskPageLinearFiltersProps = { - linearModeOptions, - linearMode, - selectLinearMode, - selectedLinearProject, - availableTeams, - setNewLinearProjectName, - setNewLinearProjectDescription, - setNewLinearProjectContent, - setNewLinearProjectTeamId, - setNewLinearProjectLeadId, - setNewLinearProjectMemberIds, - setNewLinearProjectLabelIds, - setNewLinearProjectPriority, - setNewLinearProjectStartDate, - setNewLinearProjectTargetDate, - setNewLinearProjectOpen, - setNewLinearIssueTitle, - setNewLinearIssueBody, - setNewLinearIssueTeamId, - setNewLinearIssueProjectId, - setNewLinearIssueOpen, - setLinearRefreshNonce, - linearLoading, - linearProjectsLoading, - linearProjectDetailLoading, - linearCustomViewsLoading, - linearCustomViewContentsLoading, - showLinearAttributeFilters, - linearAttributeFilter, - applyLinearAttributeFilter, - linearAttributeFilterWorkspaceId, - linearAttributePrimaryTeam, - linearTeamSelection, - linearTeamOptions, - linearTaskSourceContext, - settings, - linearSearchInput, - setLinearSearchInput, - setAppliedLinearSearch, - setTaskResumeState, - linearProjectSearchInput, - setLinearProjectSearchInput, - setAppliedLinearProjectSearch - } - const jiraFilters: TaskPageJiraFiltersProps = { - jiraPresets, - jiraSearchInput, - activeJiraPreset, - setJiraSearchInput, - setAppliedJiraSearch, - setActiveJiraPreset, - setTaskResumeState, - setJiraRefreshNonce, - sortedAvailableJiraProjects, - setNewJiraIssueTitle, - setNewJiraIssueBody, - setNewJiraIssueProjectId, - setNewJiraIssueProjectQuery, - setNewJiraIssueProjectCommandValue, - setNewJiraIssueTypeId, - setNewJiraIssueOpen, - jiraProjectsLoading, - jiraLoading - } - const gitlabFilters: TaskPageGitlabFiltersProps = { - gitlabView, - setGitlabView, - taskPickerGroups, - repoSelection, - getTaskPickerRepoHostLabel, - eligibleRepos, - setRepoSelection, - updateSettings, - taskPickerRepos, - gitLabIssueFilters, - gitLabMRFilters, - activeGitlabFilter, - setGitlabFilter, - setGitlabRefreshNonce, - gitlabLoading, - gitlabTodosLoading - } - const githubDetail: GithubDetailHostProps | null = dialogWorkItem - ? { - dialogWorkItem, - dialogInitialTab, - dialogRepoPath, - dialogSourceContext, - setDialogWorkItem, - handleUseWorkItem, - handleDialogReviewRequestsChange, - closeTaskDetailPage - } - : null - const githubTable: GithubWorkItemTableProps = { - githubListScrollRef, - githubResumeContextKey, - currentPageRef, - pendingGithubScrollRestoreRef, - githubRestoreScrollWriteRef, - githubListScrollTopRef, - taskListPositionRef, - githubTaskGridClass, - activeGithubTaskKind, - showPRManagementColumns, - tasksError, - githubUnavailable, - failedCount, - selectedRepos, - perRepoSourceState, - handleRetryIssuesFetch, - tasksLoading, - retryingSourceKeys, - unresolvedSourceRepos, - showGitHubTaskSkeletons, - filteredWorkItems, - softHiddenVisibleCount, - totalPages, - githubEmptyState, - repoMap, - allWorktrees, - openGitHubDetailPage, - githubWorkItemMutation, - ensurePRChecksLoaded, - handleOpenOrUseGitHubWorkItem, - handleUseWorkItem, - currentPage, - loadingTargetPage, - pages, - handleLoadNextPage, - setCurrentPage - } - const gitlabList: GitlabWorkItemListProps = { - gitlabError, - gitlabLoading, - gitlabItems, - displayedGitLabItems, - gitlabEmptyState, - openGitLabDetailPage, - handleUseGitLabItem, - showGitlabIssuePagination: gitlabView === 'issues' && gitlabIssueTotalPages > 1, - gitlabIssuePage, - gitlabIssueTotalPages, - gitlabIssueLoadingTargetPage, - onGitlabIssuePageChange: (page: number) => { - if (page === gitlabIssuePage) { - return - } - setGitlabIssueLoadingTargetPage(page) - setGitlabIssuePage(page) - } - } - const jiraList: JiraIssueListHostProps = { - jiraStatusReady, - jiraConnected, - setJiraConnectOpen, - hideTaskSource, - displayedJiraIssues, - jiraOrderDirection, - handleJiraSort, - jiraOrderBy, - jiraStatusCredentialError: jiraStatus.credentialError, - jiraError, - jiraErrorDetailsOpen, - setJiraErrorDetailsOpen, - jiraLoading, - jiraIssues, - jiraSearchInput, - sortedJiraIssues, - openJiraDetailPage, - handleUseJiraItem, - selectedJiraIssue, - selectedJiraSiteId, - displayedJiraStatusOrder, - closeTaskDetailPage, - jiraDetailSourceContext - } - const linearViews: LinearViewsHostProps = { - selectedLinearIssue, - activeLinearIssueContextLabel, - handleUseLinearItem, - openRelatedLinearIssue, - closeTaskDetailPage, - linearDetailSourceContext, - linearStatusReady, - linearConnected, - setLinearConnectOpen, - selectedLinearProject, - linearProjectTab, - selectedLinearProjectDetail, - linearProjectDetailLoading, - linearProjectDetailError, - linearProjectParentView, - setSelectedLinearProject, - setSelectedLinearProjectDetail, - setLinearProjectTab, - setLinearMode, - setSelectedLinearCustomView, - setTaskResumeState, - setLinearProjectParentView, - setLinearRefreshNonce, - linearMode, - linearProjectsError, - linearProjectsResult, - linearProjectsLoading, - selectedLinearWorkspaceId, - openLinearProjectContext, - selectedLinearCustomView, - linearCustomViewsError, - linearCustomViewsResult, - linearCustomViewsLoading, - openLinearCustomViewContext, - linearCustomViewContentsError, - linearCustomViewProjectsResult, - linearCustomViewContentsLoading, - issueList: { - toolbar: { - activeLinearIssueContextLabel, - selectedLinearProject, - setLinearProjectTab, - setSelectedLinearCustomView, - setLinearProjectParentView, - setTaskResumeState, - linearMode, - linearViewOptions, - linearViewMode, - setLinearViewMode, - linearGroupBy, - setLinearGroupBy, - linearGroupOptions, - linearOrderBy, - setLinearOrderBy, - linearOrderOptions, - linearDisplayPropertyOptions, - effectiveLinearDisplayProperties, - toggleLinearDisplayProperty, - linearIssueGridStyle - }, - empty: { - activeLinearIssueError, - activeLinearIssueLoading, - activeLinearIssues, - activeLinearIssueHasCollectionError, - linearMode, - linearSearchActive, - activeLinearIssueContextLabel, - linearAttributeFilter, - filteredLinearIssues, - linearIssuesHasMore, - setLinearIssueLimit - }, - board: { - linearBoardSections, - handleLinearBoardDragOver, - handleLinearBoardDrop, - linearBoardDragOverKey, - selectedLinearIssueId, - linearBoardDraggingIssueId, - linearBoardUpdatingIssueIds, - selectedLinearWorkspaceId, - linearIssueAttachmentIndex, - linearStatusBoardEnabled, - handleLinearBoardCardDragStart, - setLinearBoardDraggingIssueId, - setLinearBoardDragOverKey, - openLinearDetailPage, - effectiveLinearDisplayProperties, - handleOpenOrUseLinearItem, - linearTaskSourceContext - }, - list: { - linearIssueListRows, - selectedLinearIssueId, - selectedLinearWorkspaceId, - linearIssueAttachmentIndex, - openLinearDetailPage, - linearIssueGridStyle, - effectiveLinearDisplayProperties, - linearTaskSourceContext, - handleOpenOrUseLinearItem - }, - linearViewMode, - selectedLinearProject, - linearProjectTab, - selectedLinearCustomView, - linearProjectIssuesResult, - linearCustomViewIssuesResult, - linearIssues, - showLinearEmptyFilteredLoadMore, - handleLinearEmptyFilteredLoadMore, - activeLinearIssueLoading, - showLinearIssuePagination, - visibleLinearIssuePage, - linearIssueTotalPages, - activeLinearIssueLoadingTargetPage, - handleLinearIssuePageChange, - pagedLinearIssuesCount: pagedLinearIssues.length - } - } - const newGithubIssue: NewGithubIssueDialogProps = { - newIssueOpen, - newIssueSubmitting, - setNewIssueOpen, - handleCreateNewIssue, - newIssueTargetRepo, - perRepoSourceState, - setIssueSourcePreference, - selectedRepos, - newIssueRepoId, - setNewIssueRepoId, - setNewIssueLabels, - setNewIssueAssignees, - newIssueTitle, - setNewIssueTitle, - newIssueBody, - setNewIssueBody, - newIssueRepoLabels, - newIssueLabels, - newIssueRepoAssignees, - newIssueAssignees, - submitShortcutLabel - } - const newLinearProject: NewLinearProjectDialogProps = { - newLinearProjectOpen, - newLinearProjectSubmitting, - setNewLinearProjectOpen, - handleCreateNewLinearProject, - availableTeams, - newLinearProjectTargetTeam, - setNewLinearProjectTeamId, - newLinearProjectName, - setNewLinearProjectName, - newLinearProjectDescription, - setNewLinearProjectDescription, - newLinearProjectContent, - setNewLinearProjectContent, - submitShortcutLabel, - newLinearProjectPriority, - setNewLinearProjectPriority, - newLinearProjectMembers, - newLinearProjectLeadId, - setNewLinearProjectLeadId, - newLinearProjectMemberIds, - setNewLinearProjectMemberIds, - newLinearProjectLabelIds, - setNewLinearProjectLabelIds, - newLinearProjectLabels, - newLinearProjectStartDate, - setNewLinearProjectStartDate, - newLinearProjectTargetDate, - setNewLinearProjectTargetDate - } - const newLinearIssue: NewLinearIssueDialogProps = { - newLinearIssueOpen, - newLinearIssueSubmitting, - setNewLinearIssueOpen, - handleCreateNewLinearIssue, - availableTeams, - newLinearIssueTargetTeam, - newLinearIssueTeamId, - setNewLinearIssueTeamId, - newLinearIssueTitle, - setNewLinearIssueTitle, - newLinearIssueBody, - setNewLinearIssueBody, - submitShortcutLabel, - newLinearStates, - newLinearIssueStateId, - setNewLinearIssueStateId, - newLinearMembers, - newLinearIssueAssigneeId, - setNewLinearIssueAssigneeId, - newLinearIssuePriority, - setNewLinearIssuePriority, - newLinearIssueProjects, - newLinearIssueProjectId, - setNewLinearIssueProjectId, - newLinearIssueProjectsLoading, - newLinearIssueLabelIds, - setNewLinearIssueLabelIds, - newLinearLabels - } - const newJiraIssue: NewJiraIssueDialogProps = { - newJiraIssueOpen, - newJiraIssueSubmitting, - setNewJiraIssueOpen, - handleCreateNewJiraIssue, - newJiraIssueTargetProject, - newJiraIssueProjectComboboxOpen, - handleNewJiraIssueProjectComboboxOpenChange, - handleNewJiraIssueProjectTriggerKeyDown, - sortedAvailableJiraProjects, - includeJiraSiteNameInProjectLabel, - newJiraIssueProjectCommandValue, - setNewJiraIssueProjectCommandValue, - newJiraIssueProjectSearchInputRef, - newJiraIssueProjectQuery, - setNewJiraIssueProjectQuery, - filteredNewJiraIssueProjects, - newJiraIssueTargetProjectSelectionKey, - handleNewJiraIssueProjectSelect, - newJiraIssueTypeId, - newJiraIssueTargetType, - setNewJiraIssueTypeId, - jiraIssueTypesLoading, - availableJiraIssueTypes, - newJiraIssueTitle, - setNewJiraIssueTitle, - newJiraIssueBody, - setNewJiraIssueBody, - jiraCreateFieldsLoading, - jiraCreateFieldsError, - visibleJiraCreateFields, - newJiraIssueCustomFieldValues, - setNewJiraIssueCustomFieldValues, - jiraUserFieldSelections, - setJiraUserFieldSelections, - jiraProviderSettings: jiraTaskSourceContext ?? settings, - submitShortcutLabel, - hasMissingJiraCreateField - } - const connectDialogs: TaskPageConnectDialogsProps = { - gitlabDialogItem, - gitlabDialogRepo, - gitlabDialogSourceContext, - setGitlabDialogItem, - handleUseGitLabItem, - linearConnectOpen, - setLinearConnectOpen, - selectedLinearWorkspace, - handleLinearAccessConnected, - jiraConnectOpen, - setJiraConnectOpen - } - - return ( - - ) + const stage1 = useTaskPageStoreBindings() + const stage2 = useTaskPageRepoSelection(stage1) + const stage3 = useTaskPageRuntimeHosts(stage2) + const stage4 = useTaskPageSourceAvailability(stage3) + const stage5 = useTaskPageProviderState(stage4) + const stage6 = useTaskPageGitHubListState(stage5) + const stage7 = useTaskPageGitHubDetail(stage6) + const stage8 = useTaskPageGitHubCacheReconciliation(stage7) + const stage9 = useTaskPageGitHubIssueDraft(stage8) + const stage10 = useTaskPageDetailRouting(stage9) + const stage11 = useTaskPageLinearViewState(stage10) + const stage12 = useTaskPageJiraListState(stage11) + const stage13 = useTaskPageResumeRestoration(stage12) + const stage14 = useTaskPageProviderMetadata(stage13) + const stage15 = useTaskPageGitLabLoading(stage14) + const stage16 = useTaskPageLinearListSelection(stage15) + const stage17 = useTaskPageLinearListProjection(stage16) + const stage18 = useTaskPageLinearBoard(stage17) + const stage19 = useTaskPageJiraListProjection(stage18) + const stage20 = useTaskPageLinearCreationState(stage19) + const stage21 = useTaskPageGitHubMutationState(stage20) + const stage22 = useTaskPageJiraCreationState(stage21) + const stage23 = useTaskPageJiraCreationMetadata(stage22) + const stage24 = useTaskPageGitHubListProjection(stage23) + const stage25 = useTaskPageGitHubSearchPagination(stage24) + const stage26 = useTaskPageGitHubLandingRefresh(stage25) + const stage27 = useTaskPageGitHubQuietRefresh(stage26) + const stage28 = useTaskPageSearchActions(stage27) + const stage29 = useTaskPageWorkspaceActions(stage28) + const stage30 = useTaskPageGitHubIssueCreation(stage29) + const stage31 = useTaskPageLinearProjectCreation(stage30) + const stage32 = useTaskPageLinearIssueCreation(stage31) + const stage33 = useTaskPageJiraIssueCreation(stage32) + const stage34 = useTaskPageGlobalEffects(stage33) + const stage35 = useTaskPageLinearListEffects(stage34) + const stage36 = useTaskPageLinearInOrcaEffects(stage35) + const stage37 = useTaskPageLinearCollectionEffects(stage36) + const stage38 = useTaskPageJiraListEffects(stage37) + const stage39 = useTaskPageComposerActions(stage38) + return } diff --git a/src/renderer/src/components/TaskPageContent.tsx b/src/renderer/src/components/TaskPageContent.tsx new file mode 100644 index 00000000000..7c6419c6742 --- /dev/null +++ b/src/renderer/src/components/TaskPageContent.tsx @@ -0,0 +1,75 @@ +import type { TaskPageComposerActionsModel } from './use-task-page-composer-actions' +import PullRequestPage from '@/components/PullRequestPage' +import GitHubItemDialog from '@/components/GitHubItemDialog' +import ProjectViewWrapper from '@/components/github-project/ProjectViewWrapper' +import { TaskPageGitHubList } from './TaskPageGitHubList' +import { TaskPageGitLabTodoList } from './TaskPageGitLabTodoList' +import { TaskPageGitLabItemList } from './TaskPageGitLabItemList' +import { TaskPageJiraContent } from './TaskPageJiraContent' +export function TaskPageContent({ + model +}: { + model: TaskPageComposerActionsModel +}): React.JSX.Element | null { + const { + repoSelection, + taskSource, + githubMode, + gitlabView, + dialogInitialTab, + dialogWorkItem, + dialogRepoPath, + dialogSourceContext, + setDialogWorkItem, + handleDialogReviewRequestsChange, + closeTaskDetailPage, + handleUseWorkItem + } = model + return taskSource === 'github' && dialogWorkItem ? ( + dialogWorkItem.type === 'pr' ? ( + { + setDialogWorkItem(null) + handleUseWorkItem(item) + }} + onReviewRequestsChange={handleDialogReviewRequestsChange} + onClose={closeTaskDetailPage} + /> + ) : ( + { + setDialogWorkItem(null) + handleUseWorkItem(item) + }} + onReviewRequestsChange={handleDialogReviewRequestsChange} + onClose={closeTaskDetailPage} + /> + ) + ) : taskSource === 'github' && githubMode === 'project' ? ( +
+ +
+ ) : taskSource === 'github' ? ( + // Why: bottom of the joined GitHub list card — flush under the filter + // chrome (no gap, no top border/radius) so toolbar + table read as one. + + ) : taskSource === 'gitlab' && gitlabView === 'todos' ? ( + + ) : taskSource === 'gitlab' ? ( + + ) : ( + + ) +} diff --git a/src/renderer/src/components/TaskPageFrame.tsx b/src/renderer/src/components/TaskPageFrame.tsx new file mode 100644 index 00000000000..07d86bc07e2 --- /dev/null +++ b/src/renderer/src/components/TaskPageFrame.tsx @@ -0,0 +1,19 @@ +import type { TaskPageComposerActionsModel } from './use-task-page-composer-actions' +import { TaskPageListChrome } from './TaskPageListChrome' +import { TaskPageContent } from './TaskPageContent' +export function TaskPageFrame({ + model +}: { + model: TaskPageComposerActionsModel +}): React.JSX.Element | null { + return ( +
+ {/* Why: pt-1.5 (6px) aligns this 32px icon cluster's center with the sidebar Tasks row, 22px below the titlebar. */} +
+ + + +
+
+ ) +} diff --git a/src/renderer/src/components/TaskPageGitHubAssigneesCell.tsx b/src/renderer/src/components/TaskPageGitHubAssigneesCell.tsx new file mode 100644 index 00000000000..3d8bad1ed6d --- /dev/null +++ b/src/renderer/src/components/TaskPageGitHubAssigneesCell.tsx @@ -0,0 +1,295 @@ +import type { GitHubWorkItem } from '../../../shared/github/work-item-types' +import type { GitHubAssignableUser } from '../../../shared/github/pull-request-types' +import type { Repo } from '../../../shared/repo-types' +import { + type TaskSourceContext, + getTaskSourceRuntimeSettings +} from '../../../shared/task-source-context' +import React, { useMemo, useState, useCallback } from 'react' +import { useAppStore } from '@/store' +import { useShallow } from 'zustand/react/shallow' +import { getSettingsForRepoRuntimeOwner } from '@/lib/repo-runtime-owner' +import { parseGitHubIssueOrPRLink } from '@/lib/github-links' +import { useRepoAssigneesBySlug } from '@/hooks/useGitHubSlugMetadata' +import { translate } from '@/i18n/i18n' +import { getActiveRuntimeTarget, callRuntimeRpc } from '@/runtime/runtime-rpc-client' +import { githubProjectHost } from '../../../shared/github/project-identity' +import { Popover, PopoverTrigger, PopoverContent } from '@/components/ui/popover' +import { cn } from '@/lib/utils' +import { LoaderCircle, ChevronDown, Check } from 'lucide-react' +import type { TaskPageGitHubWorkItemMutationRunner } from './task-page-linear-jira-list-model' +import { GitHubAssigneeAvatar } from './TaskPageGitHubAvatars' +export function GHAssigneesCell({ + item, + repo, + sourceContext, + workItemMutation +}: { + item: GitHubWorkItem + repo: Repo | null + sourceContext?: TaskSourceContext | null + workItemMutation: TaskPageGitHubWorkItemMutationRunner +}): React.JSX.Element { + const repoOwnerSettings = useAppStore( + useShallow((s) => getSettingsForRepoRuntimeOwner(s, repo?.id ?? null)) + ) + const sourceSettings = useMemo( + () => + sourceContext?.provider === 'github' + ? ({ + ...repoOwnerSettings, + ...getTaskSourceRuntimeSettings(sourceContext) + } as typeof repoOwnerSettings) + : repoOwnerSettings, + [repoOwnerSettings, sourceContext] + ) + const [open, setOpen] = useState(false) + const [pendingLogin, setPendingLogin] = useState(null) + const assignees = useMemo(() => item.assignees ?? [], [item.assignees]) + const parsed = useMemo(() => parseGitHubIssueOrPRLink(item.url), [item.url]) + const owner = parsed?.slug.owner ?? null + const repoName = parsed?.slug.repo ?? null + const seedLogins = useMemo( + () => + assignees + .map((a) => a.login) + .sort() + .filter(Boolean), + [assignees] + ) + const metadata = useRepoAssigneesBySlug( + open ? owner : null, + open ? repoName : null, + seedLogins, + sourceSettings, + parsed?.slug.host + ) + const toggleAssignee = useCallback( + async (user: GitHubAssignableUser): Promise => { + if (item.type !== 'issue') { + return + } + const userLoginKey = user.login.toLowerCase() + const isOn = assignees.some((a) => a.login.toLowerCase() === userLoginKey) + if ( + workItemMutation.isIntentPending({ + item, + intent: { + type: 'toggleAssignee', + user + }, + sourceContext + }) + ) { + return + } + setPendingLogin(user.login) + try { + await workItemMutation.run({ + item, + intent: { + type: 'toggleAssignee', + user + }, + sourceContext, + errorToast: translate( + 'auto.components.TaskPage.ca63694b4c', + 'Failed to update assignees.' + ), + mutate: async () => { + const updates = isOn + ? { + removeAssignees: [user.login] + } + : { + addAssignees: [user.login] + } + const target = getActiveRuntimeTarget(sourceSettings) + if (owner && repoName) { + const args = { + owner, + repo: repoName, + host: githubProjectHost(parsed?.slug.host), + number: item.number, + updates + } + const res = + target.kind === 'environment' + ? await callRuntimeRpc< + Awaited> + >(target, 'github.project.updateIssueBySlug', args, { + timeoutMs: 30_000 + }) + : await window.api.gh.updateIssueBySlug(args) + if (!res.ok) { + throw new Error(res.error.message) + } + return res + } + if (repo) { + const runtimeRepoId = + sourceContext?.provider === 'github' ? (sourceContext.repoId ?? repo.id) : repo.id + const res = + target.kind === 'environment' + ? await callRuntimeRpc<{ + ok?: boolean + error?: string + }>( + target, + 'github.updateIssue', + { + repo: runtimeRepoId, + number: item.number, + updates + }, + { + timeoutMs: 30_000 + } + ) + : await window.api.gh.updateIssue({ + repoPath: repo.path, + repoId: repo.id, + sourceContext, + number: item.number, + updates + }) + if (res && res.ok === false) { + throw new Error(res.error) + } + return res + } + throw new Error('No GitHub repository context available for this issue.') + } + }) + } finally { + setPendingLogin(null) + } + }, + [ + assignees, + item, + owner, + parsed?.slug.host, + repo, + repoName, + sourceContext, + sourceSettings, + workItemMutation + ] + ) + const triggerContent = + assignees.length > 0 ? ( + <> +
+ {assignees.slice(0, 3).map((assignee) => ( + + ))} +
+ {assignees.length > 3 ? ( + + +{assignees.length - 3} + + ) : null} + + ) : ( + - + ) + return ( + + + + + event.stopPropagation()} + > + {!owner || !repoName ? ( +
+ {translate('auto.components.TaskPage.53e002d895', 'Issue has no repo slug.')} +
+ ) : metadata.loading ? ( +
+ {translate('auto.components.TaskPage.0eacf48491', 'Loading…')} +
+ ) : metadata.error ? ( +
{metadata.error}
+ ) : metadata.data.length === 0 ? ( +
+ {translate('auto.components.TaskPage.edf4bc4135', 'No assignable users.')} +
+ ) : ( + metadata.data.map((user) => { + const isOn = assignees.some((a) => a.login.toLowerCase() === user.login.toLowerCase()) + const pending = pendingLogin === user.login + return ( + + ) + }) + )} +
+
+ ) +} diff --git a/src/renderer/src/components/TaskPageGitHubAvatars.tsx b/src/renderer/src/components/TaskPageGitHubAvatars.tsx new file mode 100644 index 00000000000..14b8bacaee8 --- /dev/null +++ b/src/renderer/src/components/TaskPageGitHubAvatars.tsx @@ -0,0 +1,54 @@ +import type { GitHubPRPrimaryReviewer } from '@/components/github-pr-reviewer-display' +import React from 'react' +import { GitHubUserAvatar } from '@/components/github/github-user-avatar' +import { Users } from 'lucide-react' +import type { GitHubAssignableUser } from '../../../shared/github/pull-request-types' +export function ReviewChipAvatar({ + reviewer, + avatarHost +}: { + reviewer: GitHubPRPrimaryReviewer | null + avatarHost?: string +}): React.JSX.Element { + if (reviewer?.login) { + // Why: review requests may contain only logins; use the PR host before falling back to initials. + const avatarUrl = + reviewer.avatarUrl || `https://${avatarHost ?? 'github.com'}/${reviewer.login}.png?size=40` + return ( + + ) + } + return +} +export function GitHubAssigneeAvatar({ + assignee +}: { + assignee: GitHubAssignableUser +}): React.JSX.Element { + if (assignee.avatarUrl) { + return ( + {assignee.login} + ) + } + return ( + + {assignee.login.slice(0, 1).toUpperCase()} + + ) +} diff --git a/src/renderer/src/components/TaskPageGitHubChecksCell.tsx b/src/renderer/src/components/TaskPageGitHubChecksCell.tsx new file mode 100644 index 00000000000..7f782a21371 --- /dev/null +++ b/src/renderer/src/components/TaskPageGitHubChecksCell.tsx @@ -0,0 +1,86 @@ +import type { GitHubWorkItem } from '../../../shared/github/work-item-types' +import React, { useRef, useEffect } from 'react' +import { translate } from '@/i18n/i18n' +import { CheckCircle2, AlertCircle, Clock3, Minus } from 'lucide-react' +import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip' +import { cn } from '@/lib/utils' +import { getChecksPillTone, getChecksLabel } from '@/components/task-page-checks-pill' +export function PRChecksCell({ + item, + onOpen, + onLoadChecks +}: { + item: GitHubWorkItem + onOpen: () => void + onLoadChecks: () => void +}): React.JSX.Element { + const triggerRef = useRef(null) + useEffect(() => { + if (item.type !== 'pr' || item.checksSummary) { + return + } + const node = triggerRef.current + if (!node || typeof IntersectionObserver === 'undefined') { + return + } + let requested = false + const observer = new IntersectionObserver( + (entries) => { + if (requested || !entries.some((entry) => entry.isIntersecting)) { + return + } + requested = true + onLoadChecks() + observer.disconnect() + }, + { + rootMargin: '160px 0px' + } + ) + observer.observe(node) + return () => observer.disconnect() + }, [item.checksSummary, item.type, onLoadChecks]) + if (item.type !== 'pr') { + return ( + + {translate('auto.components.TaskPage.b1eaa18ace', 'Issue')} + + ) + } + const summary = item.checksSummary + const Icon = + summary?.state === 'success' + ? CheckCircle2 + : summary?.state === 'failure' + ? AlertCircle + : summary?.state === 'pending' + ? Clock3 + : Minus + return ( + + + + + + {translate('auto.components.TaskPage.995dd6af9b', 'Open PR checks')} + + + ) +} diff --git a/src/renderer/src/components/TaskPageGitHubDuplicatePicker.tsx b/src/renderer/src/components/TaskPageGitHubDuplicatePicker.tsx new file mode 100644 index 00000000000..31ba2a4d75a --- /dev/null +++ b/src/renderer/src/components/TaskPageGitHubDuplicatePicker.tsx @@ -0,0 +1,118 @@ +import type React from 'react' +import { CheckCircle2, ChevronLeft, CircleDot, Copy, Search } from 'lucide-react' + +import { translate } from '@/i18n/i18n' +import type { GitHubWorkItem } from '../../../shared/github/work-item-types' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' + +export function TaskPageGitHubDuplicatePicker({ + closeAsDuplicate, + directDuplicateTarget, + duplicateError, + duplicatePickerTitle, + duplicateSearch, + filteredDuplicateCandidates, + handleDuplicateSearchSubmit, + setDuplicateError, + setDuplicatePickerOpen, + setDuplicateSearch +}: { + closeAsDuplicate: (targetIssueNumber: number | string) => void + directDuplicateTarget: number | null + duplicateError: string | null + duplicatePickerTitle: string + duplicateSearch: string + filteredDuplicateCandidates: GitHubWorkItem[] + handleDuplicateSearchSubmit: () => void + setDuplicateError: React.Dispatch> + setDuplicatePickerOpen: React.Dispatch> + setDuplicateSearch: React.Dispatch> +}): React.JSX.Element { + return ( +
+
+ + {duplicatePickerTitle} +
+
+ + { + setDuplicateSearch(event.target.value) + setDuplicateError(null) + }} + onKeyDown={(event) => { + if (event.key === 'Enter') { + event.preventDefault() + handleDuplicateSearchSubmit() + } + }} + placeholder={translate('auto.components.TaskPage.searchIssues', 'Search issues')} + className="h-9 pl-8 text-[12px]" + aria-invalid={duplicateError ? true : undefined} + /> +
+ {duplicateError ? ( +

{duplicateError}

+ ) : null} +
+ {directDuplicateTarget ? ( + + ) : null} + {filteredDuplicateCandidates.map((candidate) => ( + + ))} + {!directDuplicateTarget && filteredDuplicateCandidates.length === 0 ? ( +

+ {translate( + 'auto.components.TaskPage.noMatchingIssuesLoaded', + 'No matching issues loaded.' + )} +

+ ) : null} +
+
+ ) +} diff --git a/src/renderer/src/components/TaskPageGitHubFilters.tsx b/src/renderer/src/components/TaskPageGitHubFilters.tsx new file mode 100644 index 00000000000..918808b1763 --- /dev/null +++ b/src/renderer/src/components/TaskPageGitHubFilters.tsx @@ -0,0 +1,256 @@ +import type { TaskPageComposerActionsModel } from './use-task-page-composer-actions' +import { getGitHubTaskKindPresets } from '@/components/task-page-localized-options' +import { cn } from '@/lib/utils' +import PRFilterDropdowns from '@/components/github/PRFilterDropdowns' +import { Search, X, Plus, LoaderCircle, RefreshCw } from 'lucide-react' +import { Input } from '@/components/ui/input' +import { translate } from '@/i18n/i18n' +import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip' +import { Button } from '@/components/ui/button' +import { resolveNewIssueOpenSeed } from '@/components/task-page-new-issue-draft' +import { useAppStore } from '@/store' +import { hasUpstreamCandidateDivergence, hasDivergentSources } from './task-page-draft-storage' +import IssueSourceIndicator from '@/components/github/IssueSourceIndicator' +import IssueSourceSelector, { issueSourceChipClass } from '@/components/github/IssueSourceSelector' +import RepoBadgeLabel from '@/components/repo/RepoBadgeLabel' +export function TaskPageGitHubFilters({ + model +}: { + model: TaskPageComposerActionsModel +}): React.JSX.Element | null { + const { + settings, + setTaskResumeState, + setIssueSourcePreference, + selectedRepos, + taskSearchInput, + setTaskSearchInput, + appliedTaskSearch, + setAppliedTaskSearch, + taskSearchInputRef, + activeTaskPreset, + setActiveTaskPreset, + setTaskRefreshNonce, + perRepoSourceState, + handleRefreshGithubTasks, + setNewIssueOpen, + setNewIssueTitle, + setNewIssueBody, + setNewIssueLabels, + setNewIssueAssignees, + setNewIssueRepoId, + newIssueTargetRepo, + activeGithubTaskKind, + appliedTaskQuery, + loadedGitHubAuthorLogins, + primaryGithubFilterSlug, + applyPRFilterChange, + handleTaskSearchChange, + handleSetDefaultTaskPreset, + handleResetGithubTaskSearch, + handleTaskSearchKeyDown, + githubTasksBusy + } = model + return ( + // Why: top of the joined GitHub list card — pairs with the + // table shell below (rounded-t-none border-t-0) as one surface. +
+
+ {getGitHubTaskKindPresets(activeGithubTaskKind).map((option) => { + const active = activeTaskPreset === option.id + return ( + + ) + })} +
+
+ applyPRFilterChange(change)} + /> +
+ + + {taskSearchInput || appliedTaskSearch ? ( + + ) : null} +
+
+ + + + + + {translate('auto.components.TaskPage.d3d0998b7d', 'New GitHub issue')} + + + + + + + + {githubTasksBusy + ? translate('auto.components.TaskPage.31f81cc334', 'Refreshing GitHub work…') + : translate('auto.components.TaskPage.ff53631e6f', 'Refresh GitHub work')} + + +
+
+ + {(() => { + // Why: show the source-slug chip only when the selector can't render (no upstream to toggle); otherwise it duplicates the selector. + const rows = perRepoSourceState.filter( + (s) => hasUpstreamCandidateDivergence(s) || hasDivergentSources(s) + ) + if (rows.length === 0) { + return null + } + return ( +
+ {rows.map((s) => { + const repo = selectedRepos.find((r) => r.id === s.repoId) + const showRepoBadgeLabel = selectedRepos.length > 1 && repo + const selectorRenderable = hasUpstreamCandidateDivergence(s) + // Why: render the indicator standalone — it has its own chip styles, so nesting it in our chip would double-border it. + if (!selectorRenderable && hasDivergentSources(s)) { + return ( + + ) + } + if (!selectorRenderable || !repo) { + return null + } + // Why:
not — the child selector renders a block
(div-in-span is invalid HTML); inline-flex class looks identical. + return ( +
+ {showRepoBadgeLabel ? ( + + ) : null} + { + void setIssueSourcePreference(repo.id, repo.path, next) + }} + /> +
+ ) + })} +
+ ) + })()} +
+ ) +} diff --git a/src/renderer/src/components/TaskPageGitHubIssueDialog.tsx b/src/renderer/src/components/TaskPageGitHubIssueDialog.tsx new file mode 100644 index 00000000000..441f07e9b5e --- /dev/null +++ b/src/renderer/src/components/TaskPageGitHubIssueDialog.tsx @@ -0,0 +1,241 @@ +import type { TaskPageComposerActionsModel } from './use-task-page-composer-actions' +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogFooter +} from '@/components/ui/dialog' +import { isScreenSubmitShortcut } from '@/lib/screen-submit-shortcut' +import { translate } from '@/i18n/i18n' +import { sameGitHubOwnerRepo } from '@/components/github/IssueSourceIndicator' +import IssueSourceSelector from '@/components/github/IssueSourceSelector' +import { + Select, + SelectTrigger, + SelectValue, + SelectContent, + SelectItem +} from '@/components/ui/select' +import { resolveUserRepoSwitchReset } from '@/components/task-page-new-issue-draft' +import RepoBadgeLabel from '@/components/repo/RepoBadgeLabel' +import { Input } from '@/components/ui/input' +import { GitHubMarkdownComposer } from '@/components/github/GitHubMarkdownComposer' +import { + GitHubIssueLabelSelector, + GitHubIssueAssigneeSelector +} from './TaskPageGitHubIssueSelectors' +import { Button } from '@/components/ui/button' +import { LoaderCircle } from 'lucide-react' +export function TaskPageGitHubIssueDialog({ + model +}: { + model: TaskPageComposerActionsModel +}): React.JSX.Element | null { + const { + setIssueSourcePreference, + submitShortcutLabel, + selectedRepos, + perRepoSourceState, + newIssueOpen, + setNewIssueOpen, + newIssueTitle, + setNewIssueTitle, + newIssueBody, + setNewIssueBody, + newIssueLabels, + setNewIssueLabels, + newIssueAssignees, + setNewIssueAssignees, + newIssueSubmitting, + newIssueRepoId, + setNewIssueRepoId, + newIssueTargetRepo, + newIssueRepoLabels, + newIssueRepoAssignees, + handleCreateNewIssue + } = model + return ( + { + if (!newIssueSubmitting) { + setNewIssueOpen(open) + } + }} + > + { + if (isScreenSubmitShortcut(event)) { + event.preventDefault() + void handleCreateNewIssue() + } + }} + > + + + {translate('auto.components.TaskPage.d3d0998b7d', 'New GitHub issue')} + + {(() => { + // Why: inline the resolved {owner}/{repo} slug as the source indicator; fall back to displayName when unresolved. + const entry = newIssueTargetRepo + ? perRepoSourceState.find((s) => s.repoId === newIssueTargetRepo.id) + : undefined + const issuesSlug = entry?.sources?.issues + ? `${entry.sources.issues.owner}/${entry.sources.issues.repo}` + : null + const fallback = newIssueTargetRepo?.displayName ?? 'this repository' + return ( + + {translate('auto.components.TaskPage.9f2b4c03a6', 'Filing in')} + {issuesSlug ?? fallback} + + ) + })()} + {(() => { + // Why: mirror the Tasks-view target selector so a fork contributor can flip target at filing time (fork-routing regression #1076). + // Sibling (not nested) because DialogDescription renders a

and the selector a

— nesting is invalid HTML. + if (!newIssueTargetRepo) { + return null + } + const entry = perRepoSourceState.find((s) => s.repoId === newIssueTargetRepo.id) + if (!entry || !entry.sources?.upstreamCandidate || !entry.sources?.originCandidate) { + return null + } + if ( + sameGitHubOwnerRepo(entry.sources.originCandidate, entry.sources.upstreamCandidate) + ) { + return null + } + return ( +
+ { + void setIssueSourcePreference( + newIssueTargetRepo.id, + newIssueTargetRepo.path, + next + ) + }} + /> +
+ ) + })()} + +
+ {selectedRepos.length > 1 ? ( +
+ + +
+ ) : null} +
+ + setNewIssueTitle(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter' && !e.nativeEvent.isComposing) { + e.preventDefault() + void handleCreateNewIssue() + } + }} + placeholder={translate('auto.components.TaskPage.578f730c16', 'Short summary')} + disabled={newIssueSubmitting} + /> +
+
+ + void handleCreateNewIssue()} + /> +
+
+ + +
+

+ {submitShortcutLabel} {translate('auto.components.TaskPage.fc0d8a1fa4', 'to submit.')} +

+
+ + + + + +
+ ) +} diff --git a/src/renderer/src/components/TaskPageGitHubIssueSelectors.tsx b/src/renderer/src/components/TaskPageGitHubIssueSelectors.tsx new file mode 100644 index 00000000000..5c39c9c7b98 --- /dev/null +++ b/src/renderer/src/components/TaskPageGitHubIssueSelectors.tsx @@ -0,0 +1,207 @@ +import React, { useMemo, useCallback } from 'react' +import { translate } from '@/i18n/i18n' +import { Popover, PopoverTrigger, PopoverContent } from '@/components/ui/popover' +import { Button } from '@/components/ui/button' +import { LoaderCircle, Check } from 'lucide-react' +import { cn } from '@/lib/utils' +import type { GitHubAssignableUser } from '../../../shared/github/pull-request-types' +import { GitHubAssigneeAvatar } from './TaskPageGitHubAvatars' +export function GitHubIssueLabelSelector({ + labels, + selectedLabels, + loading, + error, + disabled, + onChange +}: { + labels: string[] + selectedLabels: string[] + loading: boolean + error: string | null + disabled: boolean + onChange: (labels: string[]) => void +}): React.JSX.Element { + const selectedSet = useMemo(() => new Set(selectedLabels), [selectedLabels]) + const toggleLabel = useCallback( + (label: string) => { + onChange( + selectedSet.has(label) + ? selectedLabels.filter((name) => name !== label) + : [...selectedLabels, label] + ) + }, + [onChange, selectedLabels, selectedSet] + ) + return ( +
+ + + + + + + {error ? ( +
{error}
+ ) : labels.length === 0 ? ( +
+ {translate('auto.components.TaskPage.b36f4bf9de', 'No labels.')} +
+ ) : ( + labels.map((label) => ( + + )) + )} +
+
+
+ ) +} +export function GitHubIssueAssigneeSelector({ + assignees, + selectedAssignees, + loading, + error, + disabled, + onChange +}: { + assignees: GitHubAssignableUser[] + selectedAssignees: GitHubAssignableUser[] + loading: boolean + error: string | null + disabled: boolean + onChange: (assignees: GitHubAssignableUser[]) => void +}): React.JSX.Element { + const selectedLogins = useMemo( + () => new Set(selectedAssignees.map((assignee) => assignee.login.toLowerCase())), + [selectedAssignees] + ) + const toggleAssignee = useCallback( + (assignee: GitHubAssignableUser) => { + const key = assignee.login.toLowerCase() + onChange( + selectedLogins.has(key) + ? selectedAssignees.filter((current) => current.login.toLowerCase() !== key) + : [...selectedAssignees, assignee] + ) + }, + [onChange, selectedAssignees, selectedLogins] + ) + return ( +
+ + + + + + + {error ? ( +
{error}
+ ) : assignees.length === 0 ? ( +
+ {translate('auto.components.TaskPage.edf4bc4135', 'No assignable users.')} +
+ ) : ( + assignees.map((assignee) => { + const selected = selectedLogins.has(assignee.login.toLowerCase()) + return ( + + ) + }) + )} +
+
+
+ ) +} diff --git a/src/renderer/src/components/TaskPageGitHubList.tsx b/src/renderer/src/components/TaskPageGitHubList.tsx new file mode 100644 index 00000000000..aa53374ffa7 --- /dev/null +++ b/src/renderer/src/components/TaskPageGitHubList.tsx @@ -0,0 +1,320 @@ +import type { TaskPageComposerActionsModel } from './use-task-page-composer-actions' +import { useAppStore } from '@/store' +import { cn } from '@/lib/utils' +import { + GITHUB_TASK_HEADER_SURFACE_CLASS, + GITHUB_TASK_STICKY_ID_HEADER_CLASS, + GITHUB_TASK_STICKY_TITLE_HEADER_CLASS, + GITHUB_TASK_STICKY_ID_CELL_CLASS, + GITHUB_TASK_STICKY_TITLE_CELL_CLASS +} from './task-page-source-context' +import { translate } from '@/i18n/i18n' +import { Button } from '@/components/ui/button' +import { LoaderCircle } from 'lucide-react' +import { TaskPageGitHubRows } from './TaskPageGitHubRows' +import { PaginationBar } from './TaskPagePaginationBar' +export function TaskPageGitHubList({ + model +}: { + model: TaskPageComposerActionsModel +}): React.JSX.Element | null { + const { + selectedRepos, + githubEmptyState, + tasksLoading, + tasksError, + failedCount, + githubUnavailable, + githubResumeContextKey, + pages, + currentPage, + setCurrentPage, + currentPageRef, + githubListScrollRef, + githubListScrollTopRef, + pendingGithubScrollRestoreRef, + loadingTargetPage, + taskListPositionRef, + perRepoSourceState, + unresolvedSourceRepos, + retryingSourceKeys, + handleRetryIssuesFetch, + activeGithubTaskKind, + filteredWorkItems, + softHiddenVisibleCount, + showGitHubTaskSkeletons, + showPRManagementColumns, + githubTaskGridClass, + totalPages, + handleLoadNextPage + } = model + return ( + // Why: bottom of the joined GitHub list card — flush under the filter + // chrome (no gap, no top border/radius) so toolbar + table read as one. +
+
{ + const state = useAppStore.getState() + if ( + state.activeView !== 'tasks' || + state.taskPageData.openGitHubWorkItem || + pendingGithubScrollRestoreRef.current !== null + ) { + return + } + const scrollTop = event.currentTarget.scrollTop + githubListScrollTopRef.current = scrollTop + taskListPositionRef.current = { + contextKey: githubResumeContextKey, + page: currentPageRef.current, + scrollTop + } + }} + > +
span]:flex [&>span]:items-center', + GITHUB_TASK_HEADER_SURFACE_CLASS, + githubTaskGridClass + )} + > + + {translate('auto.components.TaskPage.eb10c32872', 'ID')} + + + {translate('auto.components.TaskPage.5eccb3c841', 'Title / Context')} + + {activeGithubTaskKind === 'issues' ? ( + {translate('auto.components.TaskPage.8aba10579d', 'Assignees')} + ) : null} + {showPRManagementColumns ? ( + <> + {translate('auto.components.TaskPage.f6fa3c97d0', 'Reviewers')} + {translate('auto.components.TaskPage.a7396b05c6', 'Checks')} + {translate('auto.components.TaskPage.443f7dd928', 'Merge')} + + ) : ( + {translate('auto.components.TaskPage.154b0fa623', 'Status')} + )} + {translate('auto.components.TaskPage.f362667d55', 'Updated')} + +
+ + {tasksError ? ( +
+ {tasksError} +
+ ) : null} + + {!tasksError && githubUnavailable ? ( + // Why: name the GitHub outage explicitly so an empty list isn't misread as an Orca bug; takes priority over the count banner. +
+ {translate( + 'auto.components.TaskPage.75a38d7df8', + 'GitHub data is temporarily unavailable. Its API may be down, rate-limited, or unreachable. Please try again shortly.' + )} +
+ ) : null} + + {!tasksError && !githubUnavailable && failedCount > 0 ? ( + // Why: per-repo partial-failure signal, distinct from a hard IPC reject (tasksError); the two are mutually exclusive. +
+ {failedCount} {translate('auto.components.TaskPage.7762f4b03a', 'of')}{' '} + {selectedRepos.length}{' '} + {translate('auto.components.TaskPage.d1766fd62d', 'projects failed to load')} +
+ ) : null} + + {perRepoSourceState + .filter((s) => s.error) + .map((s) => { + const err = s.error! + // Why: Retry re-fetches force=true via the shared refresh nonce, invalidating any still-failing in-flight request first. + return ( +
+ + {translate('auto.components.TaskPage.0c0de0fc0e', "Couldn't load issues from")}{' '} + + {err.source.owner}/{err.source.repo} + {' '} + — {err.message} + + +
+ ) + })} + + {unresolvedSourceRepos.map((r) => ( + // Why: null-source repos (#9660) render empty like genuine zero — name the repo and offer Retry so a transient resolve blip is recoverable. +
+ + {translate( + 'auto.components.TaskPage.noGithubSourceDetected', + 'No GitHub source detected for' + )}{' '} + {r.label} —{' '} + {translate( + 'auto.components.TaskPage.noGithubSourceDetectedHint', + 'it may have no GitHub remote, or the source could not be resolved.' + )} + + +
+ ))} + + {showGitHubTaskSkeletons ? ( + // Why: render enough shimmer rows to fill a typical viewport + // so the table doesn't visibly grow when results land. A + // 3-row stub jumps to ~30 real rows; matching the steady- + // state height keeps layout stable across the load. +
+ {Array.from({ + length: 12 + }).map((_, i) => ( +
+
+
+
+
+
+
+
+ {!showPRManagementColumns ? ( +
+
+
+ ) : null} + {showPRManagementColumns ? ( + <> +
+
+
+
+
+
+
+
+
+ + ) : ( +
+
+
+ )} +
+
+
+
+
+
+
+ ))} +
+ ) : null} + + {/* Why: suppress the generic empty state when any error banner is + visible (IPC reject via tasksError, cross-repo partial failure + via failedCount, or per-repo issue-side error). Showing + "No matching GitHub work" next to "Couldn't load issues from X/Y" + is contradictory and misleads the user into thinking they + typed the wrong query. */} + {/* Why: soft-hidden membership exits must not look like an empty + query when pager/count still implies items remain — but on a + single page (totalPages <= 1) there's nothing else to show, so + surface the empty state instead of a blank panel. */} + {!showGitHubTaskSkeletons && + filteredWorkItems.length === 0 && + (softHiddenVisibleCount === 0 || totalPages <= 1) && + !tasksError && + !githubUnavailable && + failedCount === 0 && + unresolvedSourceRepos.length === 0 && + perRepoSourceState.every((s) => !s.error) ? ( +
+

{githubEmptyState.title}

+

{githubEmptyState.description}

+
+ ) : null} + + +
+ + {/* Why: pagination sits outside the scroll container so it + remains pinned at the bottom of the panel rather than + hiding below the last row inside the scrolling region. */} + {(filteredWorkItems.length > 0 || softHiddenVisibleCount > 0) && + !showGitHubTaskSkeletons && + totalPages > 1 ? ( +
+ { + pendingGithubScrollRestoreRef.current = null + githubListScrollTopRef.current = 0 + if (githubListScrollRef.current) { + githubListScrollRef.current.scrollTop = 0 + } + if (pages[page] !== null && pages[page] !== undefined) { + currentPageRef.current = page + setCurrentPage(page) + } else { + void handleLoadNextPage(page) + } + }} + /> +
+ ) : null} +
+ ) +} diff --git a/src/renderer/src/components/TaskPageGitHubMergeCell.tsx b/src/renderer/src/components/TaskPageGitHubMergeCell.tsx new file mode 100644 index 00000000000..cca2d2b2a96 --- /dev/null +++ b/src/renderer/src/components/TaskPageGitHubMergeCell.tsx @@ -0,0 +1,266 @@ +import type { GitHubWorkItem } from '../../../shared/github/work-item-types' +import type { GitHubPRMergeMethod } from '../../../shared/github/pull-request-types' +import type { Repo } from '../../../shared/repo-types' +import { + type TaskSourceContext, + getTaskSourceRuntimeSettings +} from '../../../shared/task-source-context' +import React, { useState, useMemo } from 'react' +import { useConfirmationDialog } from '@/components/confirmation-dialog-context' +import { useAppStore } from '@/store' +import { useShallow } from 'zustand/react/shallow' +import { getSettingsForRepoRuntimeOwner } from '@/lib/repo-runtime-owner' +import { translate } from '@/i18n/i18n' +import { presentGitHubPRMergeState } from '@/components/github-pr-merge-state' +import { + resolveGitHubPRMergeMethods, + GITHUB_PR_MERGE_METHOD_LABELS +} from '../../../shared/github/pull-request-merge-methods' +import { getActiveRuntimeTarget, callRuntimeRpc } from '@/runtime/runtime-rpc-client' +import { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator +} from '@/components/ui/dropdown-menu' +import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip' +import { cn } from '@/lib/utils' +import { LoaderCircle, GitMerge, ChevronDown, ExternalLink } from 'lucide-react' +import type { TaskPageGitHubWorkItemMutationRunner } from './task-page-linear-jira-list-model' +import { resolveTaskPullRequestRepo } from './task-page-github-review-model' +export function PRMergeCell({ + item, + repo, + sourceContext, + workItemMutation +}: { + item: GitHubWorkItem + repo: Repo | null + sourceContext?: TaskSourceContext | null + workItemMutation: TaskPageGitHubWorkItemMutationRunner +}): React.JSX.Element { + const [merging, setMerging] = useState(false) + const confirm = useConfirmationDialog() + const repoOwnerSettings = useAppStore( + useShallow((s) => getSettingsForRepoRuntimeOwner(s, repo?.id ?? null)) + ) + const sourceSettings = useMemo( + () => + sourceContext?.provider === 'github' + ? ({ + ...repoOwnerSettings, + ...getTaskSourceRuntimeSettings(sourceContext) + } as typeof repoOwnerSettings) + : repoOwnerSettings, + [repoOwnerSettings, sourceContext] + ) + if (item.type !== 'pr') { + return ( + + {translate('auto.components.TaskPage.b1eaa18ace', 'Issue')} + + ) + } + const mergePresentation = presentGitHubPRMergeState(item) + const mergeMethods = resolveGitHubPRMergeMethods(item.mergeMethodSettings) + const prRepo = resolveTaskPullRequestRepo(item) + const mergeMutationPending = workItemMutation.isIntentPending({ + item, + intent: { + type: 'merge' + }, + sourceContext + }) + const autoMergeMutationPending = mergePresentation.autoMergeAction + ? workItemMutation.isIntentPending({ + item, + intent: { + type: 'setAutoMerge', + enabled: mergePresentation.autoMergeAction.kind === 'enable' + }, + sourceContext + }) + : false + const mergeDisabled = + !repo || merging || mergeMutationPending || !mergePresentation.directMergeAvailable + const handleMerge = async (method: GitHubPRMergeMethod): Promise => { + if (!repo || mergeDisabled) { + return + } + const label = GITHUB_PR_MERGE_METHOD_LABELS[method] + const confirmed = await confirm({ + title: translate('auto.components.TaskPage.844dc193c7', '{{value0}} PR #{{value1}}?', { + value0: label, + value1: item.number + }), + description: translate( + 'auto.components.TaskPage.0506a78337', + 'This will update the pull request on GitHub.' + ), + confirmLabel: label + }) + if (!confirmed) { + return + } + setMerging(true) + try { + await workItemMutation.run({ + item, + intent: { + type: 'merge' + }, + sourceContext, + successToast: translate('auto.components.TaskPage.a161925adc', 'Pull request merged'), + errorToast: translate( + 'auto.components.TaskPage.88f478cdef', + 'Failed to merge pull request' + ), + mutate: async () => { + const target = getActiveRuntimeTarget(sourceSettings) + const runtimeRepoId = + sourceContext?.provider === 'github' ? (sourceContext.repoId ?? repo.id) : repo.id + return target.kind === 'environment' + ? callRuntimeRpc<{ + ok: boolean + error?: string + }>( + target, + 'github.mergePR', + { + repo: runtimeRepoId, + prNumber: item.number, + method, + prRepo + }, + { + timeoutMs: 30_000 + } + ) + : window.api.gh.mergePR({ + repoPath: repo.path, + repoId: repo.id, + sourceContext, + prNumber: item.number, + method, + prRepo + }) + } + }) + } finally { + setMerging(false) + } + } + const handleAutoMerge = async (): Promise => { + if (!repo || autoMergeMutationPending || !mergePresentation.autoMergeAction) { + return + } + const enabled = mergePresentation.autoMergeAction.kind === 'enable' + setMerging(true) + try { + await workItemMutation.run({ + item, + intent: { + type: 'setAutoMerge', + enabled + }, + sourceContext, + successToast: enabled + ? translate('auto.components.TaskPage.fed317634c', 'Auto-merge enabled') + : translate('auto.components.TaskPage.a5bf86defe', 'Auto-merge disabled'), + errorToast: enabled + ? translate('auto.components.TaskPage.a3318684bc', 'Failed to enable auto-merge') + : translate('auto.components.TaskPage.1a9ea003dc', 'Failed to disable auto-merge'), + mutate: async () => { + const target = getActiveRuntimeTarget(sourceSettings) + const runtimeRepoId = + sourceContext?.provider === 'github' ? (sourceContext.repoId ?? repo.id) : repo.id + return target.kind === 'environment' + ? callRuntimeRpc<{ + ok: boolean + error?: string + }>( + target, + 'github.setPRAutoMerge', + { + repo: runtimeRepoId, + prNumber: item.number, + enabled, + method: enabled ? mergeMethods.defaultMethod : undefined, + prRepo + }, + { + timeoutMs: 30_000 + } + ) + : window.api.gh.setPRAutoMerge({ + repoPath: repo.path, + repoId: repo.id, + sourceContext, + prNumber: item.number, + enabled, + method: enabled ? mergeMethods.defaultMethod : undefined, + prRepo + }) + } + }) + } finally { + setMerging(false) + } + } + return ( + + + + + + + + + {mergePresentation.tooltip} + + + event.stopPropagation()}> + {mergePresentation.autoMergeAction && ( + void handleAutoMerge()} + > + + {mergePresentation.autoMergeAction.label} + + )} + {mergePresentation.autoMergeAction && } + {mergeMethods.methods.map(({ method, label }) => ( + void handleMerge(method)} + > + + {label} + + ))} + window.api.shell.openUrl(item.url)}> + + {translate('auto.components.TaskPage.37d60046e3', 'Open GitHub merge box')} + + + + ) +} diff --git a/src/renderer/src/components/TaskPageGitHubModeControls.tsx b/src/renderer/src/components/TaskPageGitHubModeControls.tsx new file mode 100644 index 00000000000..d1252bf4bfc --- /dev/null +++ b/src/renderer/src/components/TaskPageGitHubModeControls.tsx @@ -0,0 +1,150 @@ +import type { TaskPageComposerActionsModel } from './use-task-page-composer-actions' +import { cn } from '@/lib/utils' +import TaskProjectSourceCombobox from '@/components/task-project-source-combobox' +import { normalizeTaskRepoSelection } from '@/components/task-page-default-repo-selection' +import { toast } from 'sonner' +import { translate } from '@/i18n/i18n' +import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip' +import { Button } from '@/components/ui/button' +import { ExternalLink } from 'lucide-react' +export function TaskPageGitHubModeControls({ + model +}: { + model: TaskPageComposerActionsModel +}): React.JSX.Element | null { + const { + setTaskResumeState, + updateSettings, + eligibleRepos, + repoSelection, + setRepoSelection, + taskPickerGroups, + taskPickerRepos, + githubModeButtons, + taskSource, + getTaskPickerRepoHostLabel, + projectModeVisible, + githubMode, + setGithubMode, + activeGithubTaskKind, + selectedGitHubRepoExternalLink, + handleSelectGithubTaskKind + } = model + return taskSource === 'github' ? ( +
+ {projectModeVisible ? ( +
+ {githubModeButtons.map((mode) => { + const active = + mode.id === 'project' + ? githubMode === 'project' + : githubMode === 'items' && activeGithubTaskKind === mode.id + return ( + + ) + })} +
+ ) : null} + {/* Why: Project rows are repo-scoped, so the selection must stay visible in both GitHub modes. */} +
+ { + const normalized = normalizeTaskRepoSelection(eligibleRepos, next) + setRepoSelection(normalized) + void updateSettings({ + defaultRepoSelection: [...normalized] + }).catch(() => { + toast.error( + translate( + 'auto.components.TaskPage.dfd72673e7', + 'Failed to save project selection.' + ) + ) + }) + }} + onSelectAll={() => { + const allIds = new Set(taskPickerRepos.map((r) => r.id)) + setRepoSelection(allIds) + void updateSettings({ + defaultRepoSelection: null + }).catch(() => { + toast.error( + translate( + 'auto.components.TaskPage.dfd72673e7', + 'Failed to save project selection.' + ) + ) + }) + }} + triggerClassName="h-8 w-auto max-w-[220px] rounded-md border border-border/50 bg-muted/50 px-2 text-xs font-medium shadow-sm transition hover:bg-muted/50 focus:ring-2 focus:ring-ring/20 focus:outline-none" + /> +
+ + + + + + {selectedGitHubRepoExternalLink + ? translate('auto.components.TaskPage.8d1e17a3ef', 'Open {{value0}} in GitHub', { + value0: selectedGitHubRepoExternalLink.label + }) + : translate( + 'auto.components.TaskPage.bc46d8204e', + 'Select one project to open in GitHub' + )} + + +
+ ) : null +} diff --git a/src/renderer/src/components/TaskPageGitHubReviewCell.tsx b/src/renderer/src/components/TaskPageGitHubReviewCell.tsx new file mode 100644 index 00000000000..288db753496 --- /dev/null +++ b/src/renderer/src/components/TaskPageGitHubReviewCell.tsx @@ -0,0 +1,345 @@ +import type { GitHubWorkItem } from '../../../shared/github/work-item-types' +import type { GitHubAssignableUser } from '../../../shared/github/pull-request-types' +import type { Repo } from '../../../shared/repo-types' +import { + type TaskSourceContext, + getTaskSourceRuntimeSettings +} from '../../../shared/task-source-context' +import React, { useState, useMemo, useRef, useCallback } from 'react' +import { useAppStore } from '@/store' +import { useShallow } from 'zustand/react/shallow' +import { getSettingsForRepoRuntimeOwner } from '@/lib/repo-runtime-owner' +import { useRepoAssigneesBySlug } from '@/hooks/useGitHubSlugMetadata' +import { + getGitHubPRReviewerQueryState, + filterGitHubPRReviewerCandidates +} from '@/components/github/github-pr-reviewer-candidate-filter' +import { translate } from '@/i18n/i18n' +import { + getGitHubPRPrimaryReviewer, + getGitHubPRReviewerRows, + getGitHubPRReviewLabel +} from '@/components/github-pr-reviewer-display' +import { cn } from '@/lib/utils' +import { ChevronDown } from 'lucide-react' +import { Popover, PopoverTrigger } from '@/components/ui/popover' +import type { TaskPageGitHubWorkItemMutationRunner } from './task-page-linear-jira-list-model' +import { + mergeReviewerSuggestions, + resolveTaskPullRequestRepo +} from './task-page-github-review-model' +import { ReviewChipAvatar } from './TaskPageGitHubAvatars' +import { createTaskPageGitHubReviewerActions } from './task-page-github-reviewer-actions' +import { TaskPageGitHubReviewerPicker } from './TaskPageGitHubReviewerPicker' +export function PRReviewCell({ + item, + repo, + sourceContext, + workItemMutation +}: { + item: GitHubWorkItem + repo: Repo | null + sourceContext?: TaskSourceContext | null + workItemMutation: TaskPageGitHubWorkItemMutationRunner +}): React.JSX.Element { + const [open, setOpen] = useState(false) + const [reviewerInput, setReviewerInput] = useState('') + const [localReviewRequests, setLocalReviewRequests] = useState( + () => item.reviewRequests ?? [] + ) + const [reviewerPickerSide, setReviewerPickerSide] = useState<'top' | 'bottom'>('bottom') + const [reviewerPickerMaxHeight, setReviewerPickerMaxHeight] = useState(null) + const [reviewRequestsSource, setReviewRequestsSource] = useState(() => ({ + itemId: item.id, + repoId: item.repoId, + reviewRequests: item.reviewRequests + })) + const [activeReviewerCursor, setActiveReviewerCursor] = useState({ + resetKey: '', + index: 0 + }) + const [submitting, setSubmitting] = useState(false) + const repoOwnerSettings = useAppStore( + useShallow((s) => getSettingsForRepoRuntimeOwner(s, repo?.id ?? null)) + ) + const sourceSettings = useMemo( + () => + sourceContext?.provider === 'github' + ? ({ + ...repoOwnerSettings, + ...getTaskSourceRuntimeSettings(sourceContext) + } as typeof repoOwnerSettings) + : repoOwnerSettings, + [repoOwnerSettings, sourceContext] + ) + const reviewerInputRef = useRef(null) + const reviewerTriggerRef = useRef(null) + const reviewerInputFocusFrameRef = useRef(null) + const cancelReviewerInputFocusFrame = useCallback((): void => { + if (reviewerInputFocusFrameRef.current === null) { + return + } + cancelAnimationFrame(reviewerInputFocusFrameRef.current) + reviewerInputFocusFrameRef.current = null + }, []) + const setReviewerInputNode = useCallback( + (node: HTMLInputElement | null): void => { + // Why: the queued picker focus is only valid while this input is mounted. + if (!node) { + cancelReviewerInputFocusFrame() + } + reviewerInputRef.current = node + }, + [cancelReviewerInputFocusFrame] + ) + + // Why: reviewer edits are optimistic, but item switches/refetches must clear stale local requests before paint (a passive Effect leaves one stale frame). + if ( + reviewRequestsSource.itemId !== item.id || + reviewRequestsSource.repoId !== item.repoId || + reviewRequestsSource.reviewRequests !== item.reviewRequests + ) { + setReviewRequestsSource({ + itemId: item.id, + repoId: item.repoId, + reviewRequests: item.reviewRequests + }) + setLocalReviewRequests(item.reviewRequests ?? []) + } + const reviewerSeedUsers = useMemo(() => { + const byLogin = new Map() + const add = (user: GitHubAssignableUser): void => { + if (!user.login) { + return + } + byLogin.set(user.login.toLowerCase(), user) + } + for (const user of localReviewRequests) { + add(user) + } + for (const review of item.latestReviews ?? []) { + add({ + login: review.login, + name: null, + avatarUrl: review.avatarUrl ?? '' + }) + } + if (item.author) { + add({ + login: item.author, + name: null, + avatarUrl: '' + }) + } + return Array.from(byLogin.values()) + }, [item.author, item.latestReviews, localReviewRequests]) + const reviewRepo = useMemo(() => resolveTaskPullRequestRepo(item), [item]) + const reviewerMetadata = useRepoAssigneesBySlug( + open && reviewRepo ? reviewRepo.owner : null, + open && reviewRepo ? reviewRepo.repo : null, + reviewerSeedUsers.map((user) => user.login), + sourceSettings, + reviewRepo?.host + ) + const authorLogin = item.author?.toLowerCase() ?? null + const reviewerCandidates = useMemo( + () => + mergeReviewerSuggestions(reviewerMetadata.data, reviewerSeedUsers).filter( + (user) => user.login.toLowerCase() !== authorLogin + ), + [authorLogin, reviewerMetadata.data, reviewerSeedUsers] + ) + const reviewerCandidatesByLogin = useMemo( + () => new Map(reviewerCandidates.map((user) => [user.login.toLowerCase(), user])), + [reviewerCandidates] + ) + const selectedReviewerLogins = useMemo( + () => + new Set( + localReviewRequests.map((reviewer) => reviewer.login.trim().toLowerCase()).filter(Boolean) + ), + [localReviewRequests] + ) + const reviewerQueryState = useMemo( + () => getGitHubPRReviewerQueryState(reviewerInput), + [reviewerInput] + ) + const reviewerQuery = reviewerQueryState.query + const filteredReviewerCandidates = useMemo( + () => + filterGitHubPRReviewerCandidates({ + candidates: reviewerCandidates, + queryState: reviewerQueryState + }), + [reviewerCandidates, reviewerQueryState] + ) + const suggestedReviewerRows = useMemo( + () => + reviewerQuery.length === 0 && !reviewerQueryState.isTooLarge + ? reviewerSeedUsers + .filter((user) => !selectedReviewerLogins.has(user.login.toLowerCase())) + .filter((user) => user.login.toLowerCase() !== authorLogin) + .map((user) => reviewerCandidatesByLogin.get(user.login.toLowerCase()) ?? user) + .slice(0, 1) + : [], + [ + authorLogin, + reviewerCandidatesByLogin, + reviewerQuery.length, + reviewerQueryState.isTooLarge, + reviewerSeedUsers, + selectedReviewerLogins + ] + ) + const everyoneElseReviewerRows = useMemo(() => { + const suggestedLogins = new Set(suggestedReviewerRows.map((user) => user.login.toLowerCase())) + return filteredReviewerCandidates.filter( + (user) => !suggestedLogins.has(user.login.toLowerCase()) + ) + }, [filteredReviewerCandidates, suggestedReviewerRows]) + const actionableReviewerRows = useMemo( + () => [...suggestedReviewerRows, ...everyoneElseReviewerRows], + [everyoneElseReviewerRows, suggestedReviewerRows] + ) + const reviewerCursorResetKey = `${reviewerQuery}\u0000${actionableReviewerRows.length}` + if (activeReviewerCursor.resetKey !== reviewerCursorResetKey) { + setActiveReviewerCursor({ + resetKey: reviewerCursorResetKey, + index: 0 + }) + } + const activeReviewerIndex = + activeReviewerCursor.resetKey === reviewerCursorResetKey ? activeReviewerCursor.index : 0 + const setActiveReviewerIndex = useCallback( + (nextIndex: number | ((current: number) => number)): void => { + setActiveReviewerCursor((current) => { + const currentIndex = current.resetKey === reviewerCursorResetKey ? current.index : 0 + return { + resetKey: reviewerCursorResetKey, + index: typeof nextIndex === 'function' ? nextIndex(currentIndex) : nextIndex + } + }) + }, + [reviewerCursorResetKey] + ) + if (item.type !== 'pr') { + return ( + + {translate('auto.components.TaskPage.b1eaa18ace', 'Issue')} + + ) + } + const itemWithLocalReviewRequests = { + ...item, + reviewRequests: localReviewRequests + } + const primaryReviewer = getGitHubPRPrimaryReviewer(itemWithLocalReviewRequests) + const reviewerRows = getGitHubPRReviewerRows(itemWithLocalReviewRequests) + const extraReviewerCount = Math.max(0, reviewerRows.length - 1) + const hasReviewerMetadata = + item.reviewDecision !== undefined || + localReviewRequests.length > 0 || + item.reviewRequests !== undefined || + item.latestReviews !== undefined + const { handleRequestReview, requestReviewer } = createTaskPageGitHubReviewerActions({ + item, + localReviewRequests, + repo, + reviewRepo, + reviewerCandidates, + reviewerInput, + selectedReviewerLogins, + setLocalReviewRequests, + setOpen, + setReviewerInput, + setSubmitting, + sourceContext, + sourceSettings, + submitting, + workItemMutation + }) + const handleReviewerPickerOpenChange = (nextOpen: boolean): void => { + if (nextOpen) { + const rect = reviewerTriggerRef.current?.getBoundingClientRect() + const gap = 8 + const availableBelow = rect ? window.innerHeight - rect.bottom - gap : 0 + const availableAbove = rect ? rect.top - gap : 0 + const nextSide = availableBelow < 240 && availableAbove > availableBelow ? 'top' : 'bottom' + const available = nextSide === 'top' ? availableAbove : availableBelow + setReviewerPickerSide(nextSide) + setReviewerPickerMaxHeight(Math.max(180, Math.min(360, available || 360))) + } + setOpen(nextOpen) + if (nextOpen) { + cancelReviewerInputFocusFrame() + reviewerInputFocusFrameRef.current = requestAnimationFrame(() => { + reviewerInputFocusFrameRef.current = null + reviewerInputRef.current?.focus() + }) + return + } + cancelReviewerInputFocusFrame() + setReviewerInput('') + } + return ( + + + + + + + ) +} diff --git a/src/renderer/src/components/TaskPageGitHubReviewerPicker.tsx b/src/renderer/src/components/TaskPageGitHubReviewerPicker.tsx new file mode 100644 index 00000000000..2a18fcf15ad --- /dev/null +++ b/src/renderer/src/components/TaskPageGitHubReviewerPicker.tsx @@ -0,0 +1,222 @@ +import type { Dispatch, RefCallback, SetStateAction } from 'react' +import { Check } from 'lucide-react' + +import { translate } from '@/i18n/i18n' +import { cn } from '@/lib/utils' +import { Input } from '@/components/ui/input' +import { PopoverContent } from '@/components/ui/popover' + +import type { GitHubAssignableUser } from '../../../shared/github/pull-request-types' +import type { Repo } from '../../../shared/repo-types' + +type ReviewerPickerProps = { + actionableReviewerRows: GitHubAssignableUser[] + activeReviewerIndex: number + everyoneElseReviewerRows: GitHubAssignableUser[] + filteredReviewerCandidates: GitHubAssignableUser[] + handleRequestReview: (requestedLogins?: string[]) => Promise + handleReviewerPickerOpenChange: (nextOpen: boolean) => void + hasReviewerMetadata: boolean + repo: Repo | null + requestReviewer: (reviewer: GitHubAssignableUser) => Promise + reviewerInput: string + reviewerMetadataError: string | null + reviewerMetadataLoading: boolean + reviewerPickerMaxHeight: number | null + reviewerPickerSide: 'top' | 'bottom' + selectedReviewerLogins: ReadonlySet + setActiveReviewerIndex: (nextIndex: number | ((current: number) => number)) => void + setReviewerInput: Dispatch> + setReviewerInputNode: RefCallback + submitting: boolean + suggestedReviewerRows: GitHubAssignableUser[] +} + +export function TaskPageGitHubReviewerPicker({ + actionableReviewerRows, + activeReviewerIndex, + everyoneElseReviewerRows, + filteredReviewerCandidates, + handleRequestReview, + handleReviewerPickerOpenChange, + hasReviewerMetadata, + repo, + requestReviewer, + reviewerInput, + reviewerMetadataError, + reviewerMetadataLoading, + reviewerPickerMaxHeight, + reviewerPickerSide, + selectedReviewerLogins, + setActiveReviewerIndex, + setReviewerInput, + setReviewerInputNode, + submitting, + suggestedReviewerRows +}: ReviewerPickerProps): React.JSX.Element { + const renderReviewerPickerRow = ( + reviewer: GitHubAssignableUser, + options: { + suggested: boolean + activeIndex: number + } + ): React.JSX.Element => { + const selected = selectedReviewerLogins.has(reviewer.login.toLowerCase()) + const active = actionableReviewerRows[activeReviewerIndex]?.login === reviewer.login + return ( + + ) + } + + return ( + event.stopPropagation()} + onOpenAutoFocus={(event) => { + event.preventDefault() + }} + > +
+
+ {translate('auto.components.TaskPage.62c7bd789f', 'Request up to 15 reviewers')} +
+
+
+ setReviewerInput(event.target.value)} + placeholder={translate('auto.components.TaskPage.0b9b04f4b5', 'Type or choose a user')} + disabled={!repo || submitting} + className="h-8 rounded-md bg-background px-2 text-[13px]" + aria-label={translate('auto.components.TaskPage.0b9b04f4b5', 'Type or choose a user')} + aria-autocomplete="list" + onKeyDown={(event) => { + if (event.key === 'ArrowDown' && actionableReviewerRows.length > 0) { + event.preventDefault() + setActiveReviewerIndex((current) => (current + 1) % actionableReviewerRows.length) + return + } + if (event.key === 'ArrowUp' && actionableReviewerRows.length > 0) { + event.preventDefault() + setActiveReviewerIndex( + (current) => + (current - 1 + actionableReviewerRows.length) % actionableReviewerRows.length + ) + return + } + if (event.key === 'Enter') { + event.preventDefault() + const activeReviewer = actionableReviewerRows[activeReviewerIndex] + if (activeReviewer) { + void requestReviewer(activeReviewer) + return + } + void handleRequestReview() + return + } + if (event.key === 'Escape') { + event.preventDefault() + handleReviewerPickerOpenChange(false) + } + }} + /> +
+
+ {reviewerMetadataLoading ? ( +
+ {translate('auto.components.TaskPage.0eacf48491', 'Loading…')} +
+ ) : filteredReviewerCandidates.length > 0 ? ( + <> + {suggestedReviewerRows.length > 0 ? ( + <> +
+ {translate('auto.components.TaskPage.3ace2e6bcf', 'Suggestions')} +
+ {suggestedReviewerRows.map((reviewer, index) => + renderReviewerPickerRow(reviewer, { + suggested: true, + activeIndex: index + }) + )} + + ) : null} +
+ {translate('auto.components.TaskPage.67755a83a1', 'Everyone else')} +
+ {everyoneElseReviewerRows.length > 0 ? ( + everyoneElseReviewerRows.map((reviewer, index) => + renderReviewerPickerRow(reviewer, { + suggested: false, + activeIndex: suggestedReviewerRows.length + index + }) + ) + ) : ( +
+ {translate('auto.components.TaskPage.8a22eb3f7b', 'No matching reviewers.')} +
+ )} + + ) : ( +
+ {reviewerMetadataError ?? + (hasReviewerMetadata + ? translate('auto.components.TaskPage.8a22eb3f7b', 'No matching reviewers.') + : translate( + 'auto.components.TaskPage.9e03c17847', + 'Open the PR details to view current reviewers.' + ))} +
+ )} +
+
+ ) +} diff --git a/src/renderer/src/components/TaskPageGitHubRows.tsx b/src/renderer/src/components/TaskPageGitHubRows.tsx new file mode 100644 index 00000000000..39d7da64803 --- /dev/null +++ b/src/renderer/src/components/TaskPageGitHubRows.tsx @@ -0,0 +1,388 @@ +import type { TaskPageComposerActionsModel } from './use-task-page-composer-actions' +import { findGithubWorkItemWorkspaceAttachment } from '@/lib/github-work-item-workspace-attachment' +import { getWorktreeAttachmentLabel } from '@/lib/worktree-attachment-label' +import { formatPRDelta } from '@/components/task-page-pr-delta-summary' +import { + isTaskPageGitHubDraftPR, + getTaskPageGitHubPRIconTone +} from '@/components/task-page-github-work-item-status' +import { + GitPullRequestDraft, + GitPullRequest, + CircleDot, + Files, + FolderKanban, + ArrowRight, + ChevronDown, + Plus, + ExternalLink, + EllipsisVertical +} from 'lucide-react' +import { cn } from '@/lib/utils' +import { + GITHUB_TASK_STICKY_ID_CELL_CLASS, + GITHUB_TASK_STICKY_TITLE_CELL_CLASS, + getTaskPageRepoSourceContext, + formatRelativeTime +} from './task-page-source-context' +import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip' +import { translate } from '@/i18n/i18n' +import { TaskPageGitHubWorkItemStateBadge } from '@/components/task-page-github-work-item-status-badge' +import RepoBadgeLabel from '@/components/repo/RepoBadgeLabel' +import { GHAssigneesCell } from './TaskPageGitHubAssigneesCell' +import { PRReviewCell } from './TaskPageGitHubReviewCell' +import { PRChecksCell } from './TaskPageGitHubChecksCell' +import { PRMergeCell } from './TaskPageGitHubMergeCell' +import { GHStatusCell } from './TaskPageGitHubStatusCell' +import { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem +} from '@/components/ui/dropdown-menu' +import { ButtonGroup } from '@/components/ui/button-group' +import { Button } from '@/components/ui/button' +export function TaskPageGitHubRows({ + model +}: { + model: TaskPageComposerActionsModel +}): React.JSX.Element | null { + const { + repoMap, + allWorktrees, + selectedRepos, + openGitHubDetailPage, + githubWorkItemMutation, + filteredWorkItems, + showGitHubTaskSkeletons, + showPRManagementColumns, + githubTaskGridClass, + ensurePRChecksLoaded, + handleUseWorkItem, + handleOpenOrUseGitHubWorkItem + } = model + return ( +
+ {!showGitHubTaskSkeletons && + filteredWorkItems.map((item) => { + const itemRepo = repoMap.get(item.repoId) ?? null + const attachedWorkspace = findGithubWorkItemWorkspaceAttachment( + allWorktrees, + item.repoId, + item.type, + item.number + ) + const attachedWorkspaceLabel = attachedWorkspace + ? getWorktreeAttachmentLabel(attachedWorkspace) + : null + const prDelta = item.type === 'pr' ? formatPRDelta(item) : null + const githubTaskIdPill = ( + + {item.type === 'pr' ? ( + isTaskPageGitHubDraftPR(item) ? ( + + ) + return ( + // Why: clickable div not a + + + + + event.stopPropagation()}> + {attachedWorkspace ? ( + handleUseWorkItem(item)}> + + {translate('auto.components.TaskPage.b6329379ca', 'Start new workspace')} + + ) : null} + window.api.shell.openUrl(item.url)}> + + {translate('auto.components.TaskPage.c1d1600362', 'Open in browser')} + + + + ) : ( + + )} + {item.type !== 'pr' ? ( + + + + + e.stopPropagation()}> + {attachedWorkspace ? ( + handleUseWorkItem(item)}> + + {translate('auto.components.TaskPage.b6329379ca', 'Start new workspace')} + + ) : null} + window.api.shell.openUrl(item.url)}> + + {translate('auto.components.TaskPage.c1d1600362', 'Open in browser')} + + + + ) : null} +
+
+ ) + })} +
+ ) +} diff --git a/src/renderer/src/components/TaskPageGitHubStatusCell.tsx b/src/renderer/src/components/TaskPageGitHubStatusCell.tsx new file mode 100644 index 00000000000..70fe2df2b00 --- /dev/null +++ b/src/renderer/src/components/TaskPageGitHubStatusCell.tsx @@ -0,0 +1,396 @@ +import type { GitHubWorkItem } from '../../../shared/github/work-item-types' +import type { GitHubIssueUpdate } from '../../../shared/issue-mutation-types' +import type { Repo } from '../../../shared/repo-types' +import { + type TaskSourceContext, + getTaskSourceRuntimeSettings +} from '../../../shared/task-source-context' +import React, { useState, useMemo, useCallback } from 'react' +import { + createTaskPageGitHubStatusStateDraft, + resolveTaskPageGitHubStatusStateDraft, + updateTaskPageGitHubStatusLocalState +} from '@/components/task-page-github-status-state' +import { useAppStore } from '@/store' +import { useShallow } from 'zustand/react/shallow' +import { getSettingsForRepoRuntimeOwner } from '@/lib/repo-runtime-owner' +import { parseGitHubIssueOrPRLink } from '@/lib/github-links' +import { + getTaskPageGitHubDuplicateCandidates, + validateTaskPageGitHubDuplicateTarget, + type TaskPageGitHubCloseAction, + buildTaskPageGitHubCloseUpdate, + getTaskPageGitHubDuplicateTargetErrorMessage +} from '@/components/task-page-github-status-actions' +import { translate } from '@/i18n/i18n' +import { getActiveRuntimeTarget, callRuntimeRpc } from '@/runtime/runtime-rpc-client' +import { githubProjectHost } from '../../../shared/github/project-identity' +import { TaskPageGitHubWorkItemStateBadge } from '@/components/task-page-github-work-item-status-badge' +import { Popover, PopoverTrigger, PopoverContent } from '@/components/ui/popover' +import { cn } from '@/lib/utils' +import { CircleDot, ChevronDown, Copy, CheckCircle2, Ban, ChevronRight } from 'lucide-react' +import type { TaskPageGitHubWorkItemMutationRunner } from './task-page-linear-jira-list-model' +import { TaskPageGitHubDuplicatePicker } from './TaskPageGitHubDuplicatePicker' +export function GHStatusCell({ + item, + repo, + sourceContext, + workItemMutation +}: { + item: GitHubWorkItem + repo: Repo | null + sourceContext?: TaskSourceContext | null + workItemMutation: TaskPageGitHubWorkItemMutationRunner +}): React.JSX.Element { + const [statusStateDraft, setStatusStateDraft] = useState(() => + createTaskPageGitHubStatusStateDraft(item) + ) + const [open, setOpen] = useState(false) + const [statusUpdating, setStatusUpdating] = useState(false) + const [duplicatePickerOpen, setDuplicatePickerOpen] = useState(false) + const [duplicateSearch, setDuplicateSearch] = useState('') + const [duplicateError, setDuplicateError] = useState(null) + const duplicateIssueCandidates = useAppStore( + useShallow((s) => { + if (!duplicatePickerOpen) { + return [] + } + const deduped = new Map() + for (const entry of Object.values(s.workItemsCache)) { + for (const candidate of entry.data ?? []) { + if ( + candidate.type === 'issue' && + candidate.repoId === item.repoId && + candidate.number !== item.number && + !deduped.has(candidate.number) + ) { + deduped.set(candidate.number, candidate) + } + } + } + return Array.from(deduped.values()).sort((a, b) => b.number - a.number) + }) + ) + const repoOwnerSettings = useAppStore( + useShallow((s) => getSettingsForRepoRuntimeOwner(s, repo?.id ?? null)) + ) + const sourceSettings = useMemo( + () => + sourceContext?.provider === 'github' + ? ({ + ...repoOwnerSettings, + ...getTaskSourceRuntimeSettings(sourceContext) + } as typeof repoOwnerSettings) + : repoOwnerSettings, + [repoOwnerSettings, sourceContext] + ) + const parsedIssueLink = useMemo(() => parseGitHubIssueOrPRLink(item.url), [item.url]) + const filteredDuplicateCandidates = useMemo( + () => + getTaskPageGitHubDuplicateCandidates(duplicateIssueCandidates, item.number, duplicateSearch), + [duplicateIssueCandidates, duplicateSearch, item.number] + ) + const directDuplicateTarget = useMemo(() => { + const trimmed = duplicateSearch.trim() + const validation = validateTaskPageGitHubDuplicateTarget(trimmed, item.number) + if (!trimmed || !validation.ok) { + return null + } + if ( + filteredDuplicateCandidates.some((candidate) => candidate.number === validation.duplicateOf) + ) { + return null + } + return validation.duplicateOf + }, [duplicateSearch, filteredDuplicateCandidates, item.number]) + const duplicatePickerTitle = parsedIssueLink?.slug + ? `${parsedIssueLink.slug.owner}/${parsedIssueLink.slug.repo}` + : (repo?.displayName ?? translate('auto.components.TaskPage.repository', 'Repository')) + const resolvedStatusStateDraft = resolveTaskPageGitHubStatusStateDraft(statusStateDraft, item) + if (resolvedStatusStateDraft !== statusStateDraft) { + // Why: item rows can refresh from the cache while this cell is mounted; reconcile before paint to avoid one stale status frame. + setStatusStateDraft(resolvedStatusStateDraft) + } + const localState = resolvedStatusStateDraft.localState + const stateMutationPending = workItemMutation.isIntentPending({ + item, + intent: { + type: 'setState', + state: localState === 'open' ? 'closed' : 'open' + }, + sourceContext + }) + const updateLocalState = useCallback( + (nextState: GitHubWorkItem['state']) => { + setStatusStateDraft((current) => + updateTaskPageGitHubStatusLocalState(current, item, nextState) + ) + }, + [item] + ) + const handleStateChange = useCallback( + async (newState: 'open' | 'closed', closeAction?: TaskPageGitHubCloseAction) => { + if ( + statusUpdating || + stateMutationPending || + newState === localState || + item.type !== 'issue' + ) { + return + } + const parsedOwnerRepo = parsedIssueLink?.slug + if (!repo && !parsedOwnerRepo) { + return + } + const updates: GitHubIssueUpdate = + newState === 'closed' && closeAction + ? buildTaskPageGitHubCloseUpdate(closeAction) + : { + state: newState + } + updateLocalState(newState) + // Why: coordinator owns durable patch + soft-hide + quiet revalidate; keep + // the status draft so one-frame flash is still covered until proven safe. + setStatusUpdating(true) + try { + await workItemMutation.run({ + item, + intent: { + type: 'setState', + state: newState, + closeAction + }, + sourceContext, + errorToast: translate('auto.components.TaskPage.1c893195ac', 'Failed to update state'), + mutate: async () => { + const target = getActiveRuntimeTarget(sourceSettings) + // Why: issue rows can be sourced by owner/repo URL instead of the local + // repo context; slug-aware writes preserve close reasons and duplicates. + if (parsedOwnerRepo) { + return target.kind === 'environment' + ? callRuntimeRpc<{ + ok?: boolean + error?: + | { + message?: string + } + | string + }>( + target, + 'github.project.updateIssueBySlug', + { + owner: parsedOwnerRepo.owner, + repo: parsedOwnerRepo.repo, + host: githubProjectHost(parsedOwnerRepo.host), + number: item.number, + updates + }, + { + timeoutMs: 30_000 + } + ) + : window.api.gh.updateIssueBySlug({ + owner: parsedOwnerRepo.owner, + repo: parsedOwnerRepo.repo, + host: githubProjectHost(parsedOwnerRepo.host), + number: item.number, + updates + }) + } + if (!repo) { + throw new Error('No GitHub repository context available for this issue.') + } + const runtimeRepoId = + sourceContext?.provider === 'github' ? (sourceContext.repoId ?? repo.id) : repo.id + return target.kind === 'environment' + ? callRuntimeRpc<{ + ok?: boolean + error?: string + }>( + target, + 'github.updateIssue', + { + repo: runtimeRepoId, + number: item.number, + updates + }, + { + timeoutMs: 30_000 + } + ) + : window.api.gh.updateIssue({ + repoPath: repo.path, + repoId: repo.id, + sourceContext, + number: item.number, + updates + }) + } + }) + } finally { + setStatusUpdating(false) + } + // Why: draft realigns from item.state via resolveTaskPageGitHubStatusStateDraft + // when patchWorkItem (begin/rollback) updates the cache-backed row. + }, + [ + item, + localState, + parsedIssueLink, + repo, + sourceContext, + sourceSettings, + stateMutationPending, + statusUpdating, + updateLocalState, + workItemMutation + ] + ) + const closeAsDuplicate = useCallback( + (targetIssueNumber: number | string) => { + const validation = validateTaskPageGitHubDuplicateTarget( + String(targetIssueNumber), + item.number + ) + if (!validation.ok) { + setDuplicateError(getTaskPageGitHubDuplicateTargetErrorMessage(validation, translate)) + return + } + setDuplicateError(null) + handleStateChange('closed', { + stateReason: 'duplicate', + duplicateOf: validation.duplicateOf + }) + setOpen(false) + setDuplicatePickerOpen(false) + }, + [handleStateChange, item.number] + ) + const handleDuplicateSearchSubmit = useCallback(() => { + const validation = validateTaskPageGitHubDuplicateTarget(duplicateSearch, item.number) + if (!validation.ok) { + setDuplicateError(getTaskPageGitHubDuplicateTargetErrorMessage(validation, translate)) + return + } + closeAsDuplicate(validation.duplicateOf) + }, [closeAsDuplicate, duplicateSearch, item.number]) + const handlePopoverOpenChange = useCallback((nextOpen: boolean) => { + setOpen(nextOpen) + if (!nextOpen) { + setDuplicatePickerOpen(false) + setDuplicateSearch('') + setDuplicateError(null) + } + }, []) + if (item.type !== 'issue' || (!repo && !parsedIssueLink?.slug)) { + return + } + return ( + + + + + e.stopPropagation()} + onKeyDown={(e) => e.stopPropagation()} + > + {duplicatePickerOpen ? ( + + ) : ( + <> + + + + + + )} + + + ) +} diff --git a/src/renderer/src/components/TaskPageGitLabDialog.tsx b/src/renderer/src/components/TaskPageGitLabDialog.tsx new file mode 100644 index 00000000000..c8b9a588e70 --- /dev/null +++ b/src/renderer/src/components/TaskPageGitLabDialog.tsx @@ -0,0 +1,29 @@ +import type { TaskPageComposerActionsModel } from './use-task-page-composer-actions' +import GitLabItemDialog from '@/components/GitLabItemDialog' +export function TaskPageGitLabDialog({ + model +}: { + model: TaskPageComposerActionsModel +}): React.JSX.Element | null { + const { + gitlabDialogItem, + setGitlabDialogItem, + gitlabDialogRepo, + gitlabDialogSourceContext, + handleUseGitLabItem + } = model + return ( + { + setGitlabDialogItem(null) + handleUseGitLabItem(item) + }} + onClose={() => setGitlabDialogItem(null)} + /> + ) +} diff --git a/src/renderer/src/components/TaskPageGitLabFilters.tsx b/src/renderer/src/components/TaskPageGitLabFilters.tsx new file mode 100644 index 00000000000..330ea8cf00c --- /dev/null +++ b/src/renderer/src/components/TaskPageGitLabFilters.tsx @@ -0,0 +1,167 @@ +import type { TaskPageComposerActionsModel } from './use-task-page-composer-actions' +import { cn } from '@/lib/utils' +import TaskProjectSourceCombobox from '@/components/task-project-source-combobox' +import { normalizeTaskRepoSelection } from '@/components/task-page-default-repo-selection' +import { toast } from 'sonner' +import { translate } from '@/i18n/i18n' +import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip' +import { Button } from '@/components/ui/button' +import { LoaderCircle, RefreshCw } from 'lucide-react' +export function TaskPageGitLabFilters({ + model +}: { + model: TaskPageComposerActionsModel +}): React.JSX.Element | null { + const { + updateSettings, + eligibleRepos, + repoSelection, + setRepoSelection, + taskPickerGroups, + taskPickerRepos, + gitLabIssueFilters, + gitLabMRFilters, + getTaskPickerRepoHostLabel, + setGitlabFilter, + gitlabLoading, + setGitlabRefreshNonce, + gitlabView, + setGitlabView, + gitlabTodosLoading, + activeGitlabFilter + } = model + return ( + <> +
+
+ {(['issues', 'mrs', 'todos'] as const).map((view) => { + const active = gitlabView === view + const label = view === 'issues' ? 'Issues' : view === 'mrs' ? 'MRs' : 'My Todos' + return ( + + ) + })} +
+
+ { + const normalized = normalizeTaskRepoSelection(eligibleRepos, next) + setRepoSelection(normalized) + void updateSettings({ + defaultRepoSelection: [...normalized] + }).catch(() => { + toast.error( + translate( + 'auto.components.TaskPage.dfd72673e7', + 'Failed to save project selection.' + ) + ) + }) + }} + onSelectAll={() => { + const allIds = new Set(taskPickerRepos.map((r) => r.id)) + setRepoSelection(allIds) + void updateSettings({ + defaultRepoSelection: null + }).catch(() => { + toast.error( + translate( + 'auto.components.TaskPage.dfd72673e7', + 'Failed to save project selection.' + ) + ) + }) + }} + triggerClassName="h-8 w-full rounded-md border border-border/50 bg-muted/50 px-2 text-xs font-medium shadow-sm transition hover:bg-muted/50 focus:ring-2 focus:ring-ring/20 focus:outline-none" + /> +
+
+
+
+
+
+ {gitlabView === 'issues' || gitlabView === 'mrs' + ? (gitlabView === 'issues' ? gitLabIssueFilters : gitLabMRFilters).map( + ({ id, label }) => { + const active = activeGitlabFilter === id + return ( + + ) + } + ) + : null} +
+
+
+ + + + + + {gitlabView === 'todos' + ? translate('auto.components.TaskPage.c679af7ad9', 'Refresh My Todos') + : translate('auto.components.TaskPage.d4c2830063', 'Refresh GitLab work items')} + + +
+
+
+ + ) +} diff --git a/src/renderer/src/components/TaskPageGitLabItemList.tsx b/src/renderer/src/components/TaskPageGitLabItemList.tsx new file mode 100644 index 00000000000..5799c131e8a --- /dev/null +++ b/src/renderer/src/components/TaskPageGitLabItemList.tsx @@ -0,0 +1,148 @@ +import type { TaskPageComposerActionsModel } from './use-task-page-composer-actions' +import { translate } from '@/i18n/i18n' +import { useAppStore } from '@/store' +import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip' +import { Button } from '@/components/ui/button' +import { ArrowRight, ExternalLink } from 'lucide-react' +export function TaskPageGitLabItemList({ + model +}: { + model: TaskPageComposerActionsModel +}): React.JSX.Element | null { + const { + gitlabItems, + gitlabLoading, + gitlabError, + gitlabEmptyState, + displayedGitLabItems, + openGitLabDetailPage, + handleUseGitLabItem + } = model + return ( +
+
+ {translate('auto.components.TaskPage.eb10c32872', 'ID')} + {translate('auto.components.TaskPage.16cba35bee', 'Title')} + {translate('auto.components.TaskPage.00b7ffb952', 'Type / State')} + {translate('auto.components.TaskPage.f362667d55', 'Updated')} + +
+
+ {gitlabError ? ( +
+ {gitlabError} +
+ ) : null} + {gitlabLoading && gitlabItems.length === 0 ? ( + // Why: shimmer rows fill the viewport so the card never flashes empty and the table doesn't jump when real rows land. +
+ {Array.from({ + length: 12 + }).map((_, i) => ( +
+
+
+
+
+
+
+
+
+ ))} +
+ ) : null} + {!gitlabLoading && displayedGitLabItems.length === 0 && !gitlabError ? ( +
+

{gitlabEmptyState.title}

+

{gitlabEmptyState.description}

+
+ ) : null} +
+ {displayedGitLabItems.map((item) => ( + // Why: div role="button" not a + + + {translate('auto.components.TaskPage.9497f2787c', 'Start workspace')} + + + +
+
+ ))} +
+
+
+ ) +} diff --git a/src/renderer/src/components/TaskPageGitLabTodoList.tsx b/src/renderer/src/components/TaskPageGitLabTodoList.tsx new file mode 100644 index 00000000000..99b68710c92 --- /dev/null +++ b/src/renderer/src/components/TaskPageGitLabTodoList.tsx @@ -0,0 +1,104 @@ +import type { TaskPageComposerActionsModel } from './use-task-page-composer-actions' +import { translate } from '@/i18n/i18n' +import { ExternalLink } from 'lucide-react' +export function TaskPageGitLabTodoList({ + model +}: { + model: TaskPageComposerActionsModel +}): React.JSX.Element | null { + const { primaryRepo, gitlabTodos, gitlabTodosLoading } = model + return ( +
+
+ {translate('auto.components.TaskPage.8396825a14', 'Action')} + {translate('auto.components.TaskPage.16cba35bee', 'Title')} + {translate('auto.components.TaskPage.00022ec0ba', 'Project')} + {translate('auto.components.TaskPage.f362667d55', 'Updated')} + +
+
+ {gitlabTodosLoading && gitlabTodos.length === 0 ? ( +
+ {Array.from({ + length: 12 + }).map((_, i) => ( +
+
+
+
+
+
+
+
+
+ ))} +
+ ) : null} + {!gitlabTodosLoading && gitlabTodos.length === 0 ? ( +
+ {primaryRepo + ? translate( + 'auto.components.TaskPage.d591aac6ae', + 'No pending todos. You’re all caught up!' + ) + : translate( + 'auto.components.TaskPage.03da966159', + 'Select a project so we can authenticate to GitLab.' + )} +
+ ) : null} +
+ {gitlabTodos.map((todo) => ( +
void window.api.shell.openUrl(todo.targetUrl)} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + void window.api.shell.openUrl(todo.targetUrl) + } + }} + className="grid w-full cursor-pointer gap-3 px-3 py-2 text-left grid-cols-[110px_minmax(0,3fr)_minmax(120px,1.2fr)_110px_50px] hover:bg-muted/50" + title={ + todo.targetType === 'MergeRequest' + ? translate('auto.components.TaskPage.a0544fb653', 'MR !{{value0}}', { + value0: todo.targetIid ?? '' + }) + : todo.targetType === 'Issue' + ? translate('auto.components.TaskPage.e9b6955dcd', 'Issue #{{value0}}', { + value0: todo.targetIid ?? '' + }) + : todo.targetType + } + > + + {/* Why: GitLab action_name is snake_case (review_requested); swap _ for space so the row reads like a sentence. */} + {todo.actionName.replace(/_/g, ' ')} + + {todo.targetTitle} + + {todo.projectPath} + + + {todo.updatedAt ? new Date(todo.updatedAt).toLocaleDateString() : ''} + + + + +
+ ))} +
+
+
+ ) +} diff --git a/src/renderer/src/components/TaskPageJiraConnectDialog.tsx b/src/renderer/src/components/TaskPageJiraConnectDialog.tsx new file mode 100644 index 00000000000..219e33c4637 --- /dev/null +++ b/src/renderer/src/components/TaskPageJiraConnectDialog.tsx @@ -0,0 +1,10 @@ +import type { TaskPageComposerActionsModel } from './use-task-page-composer-actions' +import { JiraConnectDialog } from '@/components/jira-connect-dialog' +export function TaskPageJiraConnectDialog({ + model +}: { + model: TaskPageComposerActionsModel +}): React.JSX.Element | null { + const { jiraConnectOpen, setJiraConnectOpen } = model + return +} diff --git a/src/renderer/src/components/TaskPageJiraContent.tsx b/src/renderer/src/components/TaskPageJiraContent.tsx new file mode 100644 index 00000000000..4f2f5d0aa2b --- /dev/null +++ b/src/renderer/src/components/TaskPageJiraContent.tsx @@ -0,0 +1,158 @@ +import type { TaskPageComposerActionsModel } from './use-task-page-composer-actions' +import { LoaderCircle } from 'lucide-react' +import { JiraIcon } from '@/components/icons/JiraIcon' +import { translate } from '@/i18n/i18n' +import { Button } from '@/components/ui/button' +import { TaskPageJiraSortControls } from './task-page-jira-sort-controls' +import { TaskPageJiraErrorBanner } from './task-page-linear-jira-list-model' +import { TaskPageJiraIssueList } from '@/components/task-page-jira-issue-list' +import { formatRelativeTime } from './task-page-source-context' +import { getJiraStatusTone } from '@/components/task-page-jira-status-tone' +import JiraIssueWorkspace from '@/components/JiraIssueWorkspace' +import { TaskPageLinearContent } from './TaskPageLinearContent' +export function TaskPageJiraContent({ + model +}: { + model: TaskPageComposerActionsModel +}): React.JSX.Element | null { + const { + jiraStatus, + jiraStatusReady, + jiraConnected, + selectedJiraSiteId, + hideTaskSource, + taskSource, + closeTaskDetailPage, + selectedJiraIssue, + jiraDetailSourceContext, + openJiraDetailPage, + jiraIssues, + jiraLoading, + jiraError, + jiraErrorDetailsOpen, + setJiraErrorDetailsOpen, + jiraSearchInput, + jiraOrderBy, + jiraOrderDirection, + handleJiraSort, + displayedJiraIssues, + displayedJiraStatusOrder, + sortedJiraIssues, + setJiraConnectOpen, + handleUseJiraItem + } = model + return taskSource === 'jira' ? ( + !jiraStatusReady ? ( +
+ +
+ ) : !jiraConnected ? ( +
+ +

+ {translate('auto.components.TaskPage.a150c59da7', 'Connect your Jira site')} +

+

+ {translate( + 'auto.components.TaskPage.b518ae6307', + 'Browse, edit, create, and start work from Jira issues directly from here.' + )} +

+
+ + +
+
+ ) : ( +
+
+
+ {translate('auto.components.TaskPage.63b2abd3aa', 'Jira issues')} +
+
+ {displayedJiraIssues.length} {translate('auto.components.TaskPage.b7bae28b6a', 'shown')} +
+
+ + + +
+ {jiraStatus.credentialError ? ( +
+ {jiraStatus.credentialError} +
+ ) : null} + {!jiraStatus.credentialError && jiraError ? ( + + ) : null} + + {jiraLoading && jiraIssues.length === 0 ? ( +
+ {Array.from({ + length: 6 + }).map((_, i) => ( +
+
+
+
+ ))} +
+ ) : null} + + {!jiraLoading && jiraIssues.length === 0 && !jiraError && !jiraStatus.credentialError ? ( +
+

+ {translate('auto.components.TaskPage.eba87f2edb', 'No Jira issues found')} +

+

+ {jiraSearchInput + ? translate('auto.components.TaskPage.f51e254d35', 'Try a different JQL query.') + : translate( + 'auto.components.TaskPage.94d900518d', + 'No issues match the selected preset.' + )} +

+
+ ) : null} + + +
+ +
+ ) + ) : ( + + ) +} diff --git a/src/renderer/src/components/TaskPageJiraFilters.tsx b/src/renderer/src/components/TaskPageJiraFilters.tsx new file mode 100644 index 00000000000..b97fad08c46 --- /dev/null +++ b/src/renderer/src/components/TaskPageJiraFilters.tsx @@ -0,0 +1,183 @@ +import type { TaskPageComposerActionsModel } from './use-task-page-composer-actions' +import { cn } from '@/lib/utils' +import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip' +import { Button } from '@/components/ui/button' +import { useAppStore } from '@/store' +import { getJiraProjectSelectionKey } from '@/components/task-page-jira-project-selection' +import { translate } from '@/i18n/i18n' +import { LoaderCircle, Plus, RefreshCw, Search, X } from 'lucide-react' +import { Input } from '@/components/ui/input' +import { shouldSuppressEnterSubmit } from '@/lib/new-workspace-enter-guard' +export function TaskPageJiraFilters({ + model +}: { + model: TaskPageComposerActionsModel +}): React.JSX.Element | null { + const { + setTaskResumeState, + jiraPresets, + jiraLoading, + jiraSearchInput, + setJiraSearchInput, + setAppliedJiraSearch, + activeJiraPreset, + setActiveJiraPreset, + setJiraRefreshNonce, + jiraProjectsLoading, + setNewJiraIssueOpen, + setNewJiraIssueTitle, + setNewJiraIssueBody, + setNewJiraIssueProjectId, + setNewJiraIssueProjectQuery, + setNewJiraIssueProjectCommandValue, + setNewJiraIssueTypeId, + sortedAvailableJiraProjects + } = model + return ( +
+
+
+ {jiraPresets.map((preset) => { + const active = !jiraSearchInput && activeJiraPreset === preset.id + return ( + + ) + })} +
+
+ + + + + + {translate('auto.components.TaskPage.0c11ca0b6d', 'New Jira issue')} + + + + + + + + {translate('auto.components.TaskPage.2ff9fd71fd', 'Refresh Jira issues')} + + +
+
+
+
+ + setJiraSearchInput(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + if ( + shouldSuppressEnterSubmit( + { + isComposing: e.nativeEvent.isComposing, + shiftKey: e.shiftKey + }, + false + ) + ) { + return + } + e.preventDefault() + const trimmed = jiraSearchInput.trim() + setJiraSearchInput(trimmed) + setAppliedJiraSearch(trimmed) + setTaskResumeState({ + jiraQuery: trimmed + }) + setJiraRefreshNonce((n) => n + 1) + } + }} + placeholder={translate( + 'auto.components.TaskPage.99c2755218', + 'Jira JQL, e.g. project = ABC AND statusCategory != Done' + )} + className="h-8 rounded-md border-border/50 bg-background pl-8 pr-8 text-xs" + /> + {jiraSearchInput ? ( + + ) : null} +
+
+
+ ) +} diff --git a/src/renderer/src/components/TaskPageJiraIssueDialog.tsx b/src/renderer/src/components/TaskPageJiraIssueDialog.tsx new file mode 100644 index 00000000000..51e9a93827e --- /dev/null +++ b/src/renderer/src/components/TaskPageJiraIssueDialog.tsx @@ -0,0 +1,380 @@ +import type { TaskPageComposerActionsModel } from './use-task-page-composer-actions' +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogFooter +} from '@/components/ui/dialog' +import { isScreenSubmitShortcut } from '@/lib/screen-submit-shortcut' +import { translate } from '@/i18n/i18n' +import { Popover, PopoverTrigger, PopoverContent } from '@/components/ui/popover' +import { Button } from '@/components/ui/button' +import { getJiraProjectPickerDisplayLabel as getJiraProjectDisplayLabel } from '@/components/jira-project-picker-filter' +import { ChevronDown, Check, LoaderCircle } from 'lucide-react' +import { + Command, + CommandInput, + CommandList, + CommandEmpty, + CommandItem +} from '@/components/ui/command' +import { getJiraProjectSelectionKey } from '@/components/task-page-jira-project-selection' +import { cn } from '@/lib/utils' +import { + Select, + SelectTrigger, + SelectValue, + SelectContent, + SelectItem +} from '@/components/ui/select' +import { Input } from '@/components/ui/input' +import { getJiraCreateAllowedValueLabel } from '@/components/task-page-jira-create-fields' +export function TaskPageJiraIssueDialog({ + model +}: { + model: TaskPageComposerActionsModel +}): React.JSX.Element | null { + const { + submitShortcutLabel, + newJiraIssueOpen, + setNewJiraIssueOpen, + newJiraIssueTitle, + setNewJiraIssueTitle, + newJiraIssueBody, + setNewJiraIssueBody, + newJiraIssueProjectComboboxOpen, + newJiraIssueProjectQuery, + setNewJiraIssueProjectQuery, + newJiraIssueProjectCommandValue, + setNewJiraIssueProjectCommandValue, + newJiraIssueTypeId, + setNewJiraIssueTypeId, + newJiraIssueSubmitting, + newJiraIssueProjectSearchInputRef, + availableJiraIssueTypes, + jiraIssueTypesLoading, + jiraCreateFieldsLoading, + jiraCreateFieldsError, + newJiraIssueCustomFieldValues, + setNewJiraIssueCustomFieldValues, + includeJiraSiteNameInProjectLabel, + sortedAvailableJiraProjects, + filteredNewJiraIssueProjects, + newJiraIssueTargetProject, + newJiraIssueTargetProjectSelectionKey, + newJiraIssueTargetType, + visibleJiraCreateFields, + hasMissingJiraCreateField, + handleNewJiraIssueProjectComboboxOpenChange, + handleNewJiraIssueProjectSelect, + handleNewJiraIssueProjectTriggerKeyDown, + handleCreateNewJiraIssue + } = model + return ( + { + if (!newJiraIssueSubmitting) { + setNewJiraIssueOpen(open) + } + }} + > + { + if (isScreenSubmitShortcut(event)) { + event.preventDefault() + void handleCreateNewJiraIssue() + } + }} + > + + + {translate('auto.components.TaskPage.0c11ca0b6d', 'New Jira issue')} + + + {newJiraIssueTargetProject + ? translate( + 'auto.components.TaskPage.0f7b0d964a', + 'Creates a new issue in {{value0}}.', + { + value0: newJiraIssueTargetProject.key + } + ) + : translate( + 'auto.components.TaskPage.e178c0a953', + 'Choose a Jira project before creating the issue.' + )} + + +
+
+
+ + + + + + event.preventDefault()} + > + + + + + {translate('auto.components.TaskPage.93c57f15e5', 'No projects found.')} + + {filteredNewJiraIssueProjects.map((project) => { + const selectionKey = getJiraProjectSelectionKey(project) + const selected = selectionKey === newJiraIssueTargetProjectSelectionKey + return ( + handleNewJiraIssueProjectSelect(selectionKey)} + className="items-center gap-2 px-3 py-2 text-xs" + > + + + {getJiraProjectDisplayLabel( + project, + includeJiraSiteNameInProjectLabel + )} + + + ) + })} + + + + +
+
+ + +
+
+
+ + setNewJiraIssueTitle(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter' && !e.nativeEvent.isComposing) { + e.preventDefault() + void handleCreateNewJiraIssue() + } + }} + placeholder={translate('auto.components.TaskPage.578f730c16', 'Short summary')} + disabled={newJiraIssueSubmitting} + /> +
+
+ +