diff --git a/.migration/accordion.md b/.migration/accordion.md new file mode 100644 index 00000000000..c4e962347f4 --- /dev/null +++ b/.migration/accordion.md @@ -0,0 +1,19 @@ +# accordion + +2026-07-16. Transformation engine (legacy `new-york-v4`). Migrated cleanly. + +## Changed +- `ui/accordion.tsx`: `radix-ui` → `@base-ui/react/accordion`; `Content` → `Panel`; + trigger `[&[data-state=open]>svg]:rotate-180` → `[&[data-panel-open]>svg]:rotate-180`. +- Call sites: removed `type="single" collapsible` (single is Base UI default and + always collapsible); per-item string `value`s unchanged. + +## Left alone +- No height-animation classes existed. + +## Behavior changes +- None for these uncontrolled single-mode accordions (controlled would use arrays). + +## Verify by hand +- Open each accordion (PR resolved comments, GitHub item dialog, mobile pairing + guide, checks panel): toggles, chevron rotates, one section open at a time. diff --git a/.migration/badge.md b/.migration/badge.md new file mode 100644 index 00000000000..9f5bc65a323 --- /dev/null +++ b/.migration/badge.md @@ -0,0 +1,17 @@ +# badge + +2026-07-16. Transformation engine (legacy `new-york-v4`). Slot → useRender. + +## Changed +- `ui/badge.tsx`: `Slot`/`asChild` → `useRender` + `mergeProps`; `data-slot` object + literal cast to `React.ComponentProps<'span'>`; `badgeVariants` kept. +- `asChild` call sites → `render`. + +## Left alone +- Nothing relevant. + +## Behavior changes +- `asChild` removed (use `render`). + +## Verify by hand +- Badges render; `render`-composed badges look correct. diff --git a/.migration/button-group.md b/.migration/button-group.md new file mode 100644 index 00000000000..6077198c719 --- /dev/null +++ b/.migration/button-group.md @@ -0,0 +1,16 @@ +# button-group + +2026-07-16. Transformation engine (legacy `new-york-v4`). Slot → useRender. + +## Changed +- `ui/button-group.tsx`: `ButtonGroupText` (the only Slot user) → `useRender` + + `mergeProps` on a `div`. `ButtonGroup`/`ButtonGroupSeparator` unchanged. + +## Left alone +- `ButtonGroupSeparator` wraps the migrated Separator (no radix). + +## Behavior changes +- `ButtonGroupText` `asChild` removed (use `render`). + +## Verify by hand +- Button groups lay out; separators render between items. diff --git a/.migration/button.md b/.migration/button.md new file mode 100644 index 00000000000..392dc1db6de --- /dev/null +++ b/.migration/button.md @@ -0,0 +1,17 @@ +# button + +2026-07-16. Transformation engine (legacy `new-york-v4`). Real Base UI Button primitive. + +## Changed +- `ui/button.tsx`: `Slot`/`asChild` → `@base-ui/react/button` (`render` native); + cva variants/`buttonVariants` kept; `className` narrowed to `string`. +- `asChild` call sites → `render={}`; non-button targets get `nativeButton={false}`. + +## Left alone +- Nothing relevant. + +## Behavior changes +- `asChild` removed (use `render`); ref type `HTMLButtonElement` → `HTMLElement`. + +## Verify by hand +- Buttons click normally; ` - + window.api.ui.popupMenu()} + > + + + } + /> {translate('auto.App.8b0b8eb54f', 'Application menu')} @@ -2052,19 +2070,21 @@ function App(): React.JSX.Element { <> {settings?.showTitlebarAppName !== false && ( - -
- - {translate('auto.App.5096cbbc86', 'Orca')} - -
-
+ + + {translate('auto.App.5096cbbc86', 'Orca')} + + + } + /> { + onClick={() => { void actions.updateSettings({ showTitlebarAppName: false }) }} > @@ -2077,15 +2097,17 @@ function App(): React.JSX.Element { )} {showSidebar && ( - - - + + + + } + /> {translate('auto.App.ce37cf5279', 'Toggle sidebar ({{value0}})', { value0: leftSidebarShortcutLabel @@ -2102,16 +2124,18 @@ function App(): React.JSX.Element { // and ml-auto has no spare width; keep a fixed gutter before Back.
- - - + useAppStore.getState().goBackWorktree()} + disabled={!canGoBackWorktree} + aria-label={translate('auto.App.064bd07810', 'Go back')} + > + + + } + /> {translate('auto.App.fe21e8f6f5', 'Go back ({{value0}})', { value0: historyBackShortcutLabel @@ -2119,16 +2143,18 @@ function App(): React.JSX.Element { - - - + useAppStore.getState().goForwardWorktree()} + disabled={!canGoForwardWorktree} + aria-label={translate('auto.App.cf9099fe98', 'Go forward')} + > + + + } + /> {translate('auto.App.f7aa73e785', 'Go forward ({{value0}})', { value0: historyForwardShortcutLabel @@ -2142,15 +2168,17 @@ function App(): React.JSX.Element { const rightSidebarToggle = showRightSidebarControls ? ( - - - + + + + } + /> {translate('auto.App.c184e056de', 'Toggle right sidebar ({{value0}})', { value0: rightSidebarShortcutLabel @@ -2171,16 +2199,18 @@ function App(): React.JSX.Element { )} {showTitlebarExpandButton && ( - - - + + + + } + /> {translate('auto.App.c1cf0b0e4a', 'Collapse pane')} @@ -2199,6 +2229,7 @@ function App(): React.JSX.Element { const workspaceProfileSwitcher = showProfileSwitcherInTopRight && workspaceChromeActive && + !hasActiveSpoolWorkspace && leftTitlebarChromeLayout.shouldMount && !stackedSidebarOpen ? (
- + + {/* Why: leaf-mounted retention sync keeps agent-status retention subscriptions from re-rendering the App tree. */} @@ -2439,6 +2471,7 @@ function App(): React.JSX.Element { {activeView === 'activity' ? : null} {activeView === 'space' ? : null} {activeView === 'mobile' ? : null} + {hasActiveSpoolWorkspace ? : null} {activeView === 'terminal' && creationLayoutActive && activePendingCreationId ? ( @@ -2451,6 +2484,7 @@ function App(): React.JSX.Element { ) : null} {activeView === 'terminal' && !activeWorktreeId && + !hasActiveSpoolWorkspace && !creationLayoutActive ? ( ) : null} @@ -2469,16 +2503,23 @@ function App(): React.JSX.Element {
{/* Why: keep the right-sidebar shell mounted for layout stability. Its heavy panels disconnect while closed so workspace wake stays - responsive. Unmount on the tasks view since that surface is - intentionally distraction-free. */} + responsive. Remote panel state is route-scoped, so its boundary + also resets when the owning Desktop/worktree binding changes. */} {showRightSidebarControls ? ( - + + {hasActiveSpoolWorkspace && activeSpoolWorkspaceRoute ? ( + + ) : ( + + )} + ) : null}
@@ -2512,7 +2559,7 @@ function App(): React.JSX.Element { ) : null} - {statusBarVisible ? ( + {statusBarVisible && !hasActiveSpoolWorkspace ? ( diff --git a/src/renderer/src/components/DetachedHeadBadge.tsx b/src/renderer/src/components/DetachedHeadBadge.tsx index 58a12e6395e..3428abce750 100644 --- a/src/renderer/src/components/DetachedHeadBadge.tsx +++ b/src/renderer/src/components/DetachedHeadBadge.tsx @@ -24,19 +24,21 @@ export function DetachedHeadBadge({ return ( - - - - {visibleLabel} - - + + + {visibleLabel} + + } + /> {display.tooltip} diff --git a/src/renderer/src/components/GitHubItemDialog.tsx b/src/renderer/src/components/GitHubItemDialog.tsx index d076f0b325d..6f81cb1f417 100644 --- a/src/renderer/src/components/GitHubItemDialog.tsx +++ b/src/renderer/src/components/GitHubItemDialog.tsx @@ -621,23 +621,25 @@ function PRAssigneesPanel({
{translate('auto.components.GitHubItemDialog.83ac703dda', 'Assignees')} - - - + + {isPending('assignees') ? ( + + ) : ( + + )} + + } + /> {repoAssignees.error ? (
@@ -1199,28 +1201,28 @@ function PRReviewersPanel({
{translate('auto.components.GitHubItemDialog.dc8a092c57', 'Reviewers')} - - - + + {submitting ? ( + + ) : ( + + )} + + } + /> { - event.preventDefault() - }} + initialFocus={false} >
{canRemoveReviewer ? ( - - - + { + void handleRemoveReviewers([reviewer.login]) + }} + > + + + } + /> {translate('auto.components.GitHubItemDialog.5c1c973855', 'Remove reviewer')} @@ -2024,44 +2028,46 @@ function PRViewedCheckbox({ }): React.JSX.Element { return ( - - - + + {pending ? ( + + ) : checked ? ( + + ) : null} + + {translate('auto.components.GitHubItemDialog.af924014f8', 'Viewed')} + + } + /> {checked ? translate('auto.components.GitHubItemDialog.ba8e329d92', 'Unmark viewed') @@ -2602,20 +2608,22 @@ function PRFilesCombinedDiffViewer({
{fileTreeCollapsed && ( - - - + setFileTreeCollapsed(false)} + > + + + } + /> {translate('auto.components.GitHubItemDialog.1257d1435d', 'Show file tree')} @@ -2879,102 +2887,110 @@ function CommentCodeContext({ > {(contextBefore > 0 || contextAfter > 0) && ( - + { + setContextBefore(0) + setContextAfter(0) + }} + aria-label={translate( + 'auto.components.GitHubItemDialog.b1574e8ac2', + 'Reset code context' + )} + > + + + } + /> + + {translate('auto.components.GitHubItemDialog.b1574e8ac2', 'Reset code context')} + + + )} + + { - setContextBefore(0) - setContextAfter(0) - }} + disabled={!canExpandAbove} + onClick={() => + setContextBefore((current) => + Math.min(current + CODE_CONTEXT_EXPAND_STEP, commentFrom - 1) + ) + } aria-label={translate( - 'auto.components.GitHubItemDialog.b1574e8ac2', - 'Reset code context' + 'auto.components.GitHubItemDialog.307c98e8e3', + 'Show {{value0}} more lines above', + { value0: CODE_CONTEXT_EXPAND_STEP } )} > - + - - - {translate('auto.components.GitHubItemDialog.b1574e8ac2', 'Reset code context')} - - - )} - - - - + } + /> {translate('auto.components.GitHubItemDialog.5664681624', 'Show more lines above')} - - - + + setContextAfter((current) => + Math.min(current + CODE_CONTEXT_EXPAND_STEP, totalLines - commentTo) + ) + } + aria-label={translate( + 'auto.components.GitHubItemDialog.307c98e8e3', + 'Show {{value0}} more lines below', + { value0: CODE_CONTEXT_EXPAND_STEP } + )} + > + + + } + /> {translate('auto.components.GitHubItemDialog.06c06e58ba', 'Show more lines below')} - - - + { + setContextBefore((current) => + Math.max(current, Math.max(0, commentFrom - blockRange.startLine)) + ) + setContextAfter((current) => + Math.max(current, Math.max(0, blockRange.endLine - commentTo)) + ) + }} + aria-label={blockTooltip} + > + + + } + /> {blockTooltip} @@ -3357,43 +3373,47 @@ function ConversationTab({ )}
- - - - - {translate('auto.components.GitHubItemDialog.bca8eb39ac', 'Reply to comment')} - - - {comment.url && ( - - + window.api.shell.openUrl(comment.url)} + onClick={() => + setReplyingTo((current) => (current === comment.id ? null : comment.id)) + } aria-label={translate( - 'auto.components.GitHubItemDialog.a154ec5224', - 'Open comment on GitHub' + 'auto.components.GitHubItemDialog.bca8eb39ac', + 'Reply to comment' )} > - + - + } + /> + + {translate('auto.components.GitHubItemDialog.bca8eb39ac', 'Reply to comment')} + + + {comment.url && ( + + window.api.shell.openUrl(comment.url)} + aria-label={translate( + 'auto.components.GitHubItemDialog.a154ec5224', + 'Open comment on GitHub' + )} + > + + + } + /> {translate('auto.components.GitHubItemDialog.a154ec5224', 'Open comment on GitHub')} @@ -3460,7 +3480,7 @@ function ConversationTab({ const root = getPRCommentGroupRoot(group) const count = getPRCommentGroupCount(group) return ( - + ) : ( - - - + { + setBodyDraft(body) + setBodyEditing(true) + }} + aria-label={translate( + 'auto.components.GitHubItemDialog.4d555d3796', + 'Edit description' + )} + > + + + } + /> {translate('auto.components.GitHubItemDialog.4d555d3796', 'Edit description')} @@ -4098,29 +4120,33 @@ function PRActionsPanel({
- - - - - + + {mergePending ? ( + + ) : ( + + )} + {mergePresentation.autoMergeAction?.label ?? + (mergePresentation.directMergeAvailable + ? mergeMethods.defaultLabel + : mergePresentation.label)} + + + } + /> + } + /> {!canMergeWithRepoContext ? translate( @@ -4134,7 +4160,7 @@ function PRActionsPanel({ {mergePresentation.autoMergeAction && ( void handleAutoMerge()} + onClick={() => void handleAutoMerge()} > {mergePresentation.autoMergeAction.label} @@ -4145,13 +4171,13 @@ function PRActionsPanel({ void handleMerge(method)} + onClick={() => void handleMerge(method)} > {label} ))} - window.api.shell.openUrl(item.url)}> + window.api.shell.openUrl(item.url)}> {translate('auto.components.GitHubItemDialog.53fe19aefc', 'Open GitHub merge box')} @@ -4739,19 +4765,21 @@ function ChecksTab({ const refreshAction = ( - - - + void handleRefresh()} + aria-label={translate('auto.components.GitHubItemDialog.9a1004fc76', 'Refresh checks')} + > + + + } + /> {translate('auto.components.GitHubItemDialog.9a1004fc76', 'Refresh checks')} @@ -4760,25 +4788,27 @@ function ChecksTab({ const fixBrokenChecksAction = failedChecks.length > 0 || fixingChecks ? ( - - - + void handleFixBrokenChecks()} + > + {fixingChecks ? ( + + ) : ( + + )} + {variant === 'compact' + ? translate('auto.components.GitHubItemDialog.9157d48ddb', 'Fix checks') + : translate('auto.components.GitHubItemDialog.2511f44bb7', 'Fix broken checks')} + + } + /> {translate( 'auto.components.GitHubItemDialog.f4b1292569', @@ -4790,32 +4820,34 @@ function ChecksTab({ const rerunAction = list.length > 0 || rerunning ? ( - - - + + {rerunning ? ( + + ) : ( + + )} + {translate('auto.components.GitHubItemDialog.1b56e28faa', 'Rerun')} + + + } + /> void handleRerun(true)} + onClick={() => void handleRerun(true)} > {translate('auto.components.GitHubItemDialog.e31651a224', 'Rerun failed checks')} - void handleRerun(false)}> + void handleRerun(false)}> {translate('auto.components.GitHubItemDialog.71c11aff84', 'Rerun all checks')} @@ -5953,30 +5985,34 @@ function GHEditSection({ const isSidebar = variant === 'sidebar' return ( - - - + > + + {localState === 'closed' ? ( + + ) : ( + + )} + {getStateLabel({ ...item, state: localState })} + + + + } + /> {translate('auto.components.GitHubItemDialog.83ac703dda', 'Assignees')} - - - + + {isPending('assignees') ? ( + + ) : ( + + )} + + } + /> {translate('auto.components.GitHubItemDialog.217e55d87c', 'Labels')} - - - + + {isPending('labels') ? ( + + ) : ( + + )} + + } + /> - - - + ) : ( + localLabels.map((name) => ( + + {name} + + )) + )} + {isPending('labels') ? ( + + ) : ( + + )} + + } + /> {repoLabels.error ? (
@@ -6434,30 +6476,32 @@ function GHEditSection({ {/* Assignees */} - - - + ) : ( + localAssignees.map((login) => ( + + {login} + + )) + )} + {isPending('assignees') ? ( + + ) : ( + + )} + + } + /> {repoAssignees.error ? (
@@ -6520,21 +6564,23 @@ function GHEditSection({ {translate('auto.components.GitHubItemDialog.726db41722', 'Open workspace')} - - - + + + + } + /> - onUse(item)}> + onUse(item)}> {translate('auto.components.GitHubItemDialog.36182aa57f', 'Start new workspace')} @@ -6647,22 +6693,24 @@ function GHCommentComposer({ onSubmitShortcut={() => void handleSubmit()} /> - - - + + {submitting ? ( + + ) : ( + + )} + + } + /> {translate('auto.components.GitHubItemDialog.0a73f59e85', 'Send comment')} @@ -7302,25 +7350,27 @@ export default function GitHubItemDialog({ #{workItem.number}
- - - + void handleCopyWorkItemLink()} + aria-label={translate( + 'auto.components.GitHubItemDialog.c43fe79ee0', + 'Copy GitHub link' + )} + > + {linkCopied ? ( + + ) : ( + + )} + + } + /> {linkCopied ? translate('auto.components.GitHubItemDialog.038b3d39b1', 'Copied') @@ -7331,19 +7381,21 @@ export default function GitHubItemDialog({ - - - + window.api.shell.openUrl(workItem.url)} + aria-label={translate( + 'auto.components.GitHubItemDialog.3fdf777817', + 'Open on GitHub' + )} + > + + + } + /> {translate('auto.components.GitHubItemDialog.3fdf777817', 'Open on GitHub')} @@ -7378,28 +7430,30 @@ export default function GitHubItemDialog({ {translate('auto.components.GitHubItemDialog.726db41722', 'Open workspace')} - - - + + + + } + /> - onUse(workItem)}> + onUse(workItem)}> {translate( 'auto.components.GitHubItemDialog.36182aa57f', 'Start new workspace' )} - window.api.shell.openUrl(workItem.url)}> + window.api.shell.openUrl(workItem.url)}> {translate('auto.components.GitHubItemDialog.3fdf777817', 'Open on GitHub')} @@ -7547,25 +7601,27 @@ export default function GitHubItemDialog({ )} - - - + void handleCopyWorkItemLink()} + aria-label={translate( + 'auto.components.GitHubItemDialog.c43fe79ee0', + 'Copy GitHub link' + )} + > + {linkCopied ? ( + + ) : ( + + )} + + } + /> {linkCopied ? translate('auto.components.GitHubItemDialog.038b3d39b1', 'Copied') @@ -7573,19 +7629,21 @@ export default function GitHubItemDialog({ - - - + window.api.shell.openUrl(workItem.url)} + aria-label={translate( + 'auto.components.GitHubItemDialog.3fdf777817', + 'Open on GitHub' + )} + > + + + } + /> {translate('auto.components.GitHubItemDialog.3fdf777817', 'Open on GitHub')} diff --git a/src/renderer/src/components/GitLabItemDialog.tsx b/src/renderer/src/components/GitLabItemDialog.tsx index b71386b4c34..b45bc43ec04 100644 --- a/src/renderer/src/components/GitLabItemDialog.tsx +++ b/src/renderer/src/components/GitLabItemDialog.tsx @@ -26,7 +26,6 @@ import { toast } from 'sonner' import { Button } from '@/components/ui/button' import { Sheet, SheetContent, SheetDescription, SheetTitle } from '@/components/ui/sheet' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' -import { VisuallyHidden } from 'radix-ui' import CommentMarkdown from '@/components/sidebar/CommentMarkdown' import { isScreenSubmitShortcut } from '@/lib/screen-submit-shortcut' import { useMountedRef } from '@/hooks/useMountedRef' @@ -1009,16 +1008,14 @@ export default function GitLabItemDialog({ return ( !open && onClose()}> - - - {item - ? visibleTitle - : translate('auto.components.GitLabItemDialog.3a051b8ade', 'Work item')} - - - {translate('auto.components.GitLabItemDialog.30c97083c2', 'GitLab work item detail')} - - + + {item + ? visibleTitle + : translate('auto.components.GitLabItemDialog.3a051b8ade', 'Work item')} + + + {translate('auto.components.GitLabItemDialog.30c97083c2', 'GitLab work item detail')} + {item ? ( <> diff --git a/src/renderer/src/components/JiraIssueWorkspace.tsx b/src/renderer/src/components/JiraIssueWorkspace.tsx index 857df73e599..0664019e1c1 100644 --- a/src/renderer/src/components/JiraIssueWorkspace.tsx +++ b/src/renderer/src/components/JiraIssueWorkspace.tsx @@ -14,7 +14,6 @@ import { X } from 'lucide-react' import { toast } from 'sonner' -import { VisuallyHidden } from 'radix-ui' import CommentMarkdown from '@/components/sidebar/CommentMarkdown' import { JiraIcon } from '@/components/icons/JiraIcon' @@ -398,22 +397,18 @@ export default function JiraIssueWorkspace({ side="right" showCloseButton={false} className="w-[min(92vw,780px)] p-0 sm:max-w-[780px]" - onOpenAutoFocus={(event) => event.preventDefault()} + initialFocus={false} > - - - {displayed?.title ?? - translate('auto.components.JiraIssueWorkspace.ef21405c6d', 'Jira issue')} - - - - - {translate( - 'auto.components.JiraIssueWorkspace.857bd2f88f', - 'Preview, edit, and start work from the selected issue.' - )} - - + + {displayed?.title ?? + translate('auto.components.JiraIssueWorkspace.ef21405c6d', 'Jira issue')} + + + {translate( + 'auto.components.JiraIssueWorkspace.857bd2f88f', + 'Preview, edit, and start work from the selected issue.' + )} + {displayed ? (
@@ -440,20 +435,22 @@ export default function JiraIssueWorkspace({ - - - + + + + } + /> {translate('auto.components.JiraIssueWorkspace.7a96985ca0', 'Close')} @@ -463,21 +460,23 @@ export default function JiraIssueWorkspace({
- - - + + {displayed.status.name} + {pendingField === 'transition' ? ( + + ) : null} + + } + /> - - - + + {displayed.priority?.name ?? + translate('auto.components.JiraIssueWorkspace.51bed73f88', 'No priority')} + {pendingField === 'priority' ? ( + + ) : null} + + } + /> - - - + + {displayed.assignee?.displayName ?? + translate('auto.components.JiraIssueWorkspace.54649eaeab', '+ Assignee')} + {pendingField === 'assignee' ? ( + + ) : null} + + } + /> - - - + + + {item.label} + + } + /> {item.label} diff --git a/src/renderer/src/components/LinearIssueMarkdownToolbar.tsx b/src/renderer/src/components/LinearIssueMarkdownToolbar.tsx index 8ecdf5673c1..d7b08a804dd 100644 --- a/src/renderer/src/components/LinearIssueMarkdownToolbar.tsx +++ b/src/renderer/src/components/LinearIssueMarkdownToolbar.tsx @@ -37,18 +37,20 @@ function LinearIssueMarkdownToolbarButton({ }: LinearIssueMarkdownToolbarButtonProps): React.JSX.Element { return ( - - - + event.preventDefault()} + onClick={onClick} + > + {children} + + } + /> {label} diff --git a/src/renderer/src/components/LinearIssueWorkspace.tsx b/src/renderer/src/components/LinearIssueWorkspace.tsx index ccb1f03c0e5..6424251ca71 100644 --- a/src/renderer/src/components/LinearIssueWorkspace.tsx +++ b/src/renderer/src/components/LinearIssueWorkspace.tsx @@ -19,7 +19,6 @@ import { X } from 'lucide-react' import { toast } from 'sonner' -import { VisuallyHidden } from 'radix-ui' import { LinearIcon } from '@/components/icons/LinearIcon' import CommentMarkdown from '@/components/sidebar/CommentMarkdown' @@ -272,17 +271,19 @@ function LinearIssueSubIssueButton({ ) : null} - - - + + + + {translate('auto.components.LinearIssueWorkspace.8c55d6696a', 'Add sub-issues')} + + + } + />
- - - + + + + {issue.project?.name ?? + translate('auto.components.LinearIssueWorkspace.519c3587f3', 'Add to project')} + + + + } + />
2 / 17 - - - + void copyTextToClipboard(displayed.url, 'URL')} + aria-label={translate( + 'auto.components.LinearIssueWorkspace.97c19a84f1', + 'Copy Linear URL' + )} + > + + + } + /> {translate('auto.components.LinearIssueWorkspace.9a9a884236', 'Copy URL')} - - - + void copyTextToClipboard(displayed.identifier, 'Identifier')} + aria-label={translate( + 'auto.components.LinearIssueWorkspace.9e3c49beb8', + 'Copy issue identifier' + )} + > + + + } + /> {translate('auto.components.LinearIssueWorkspace.30c1242f3a', 'Copy identifier')} - - - - - {translate('auto.components.LinearIssueWorkspace.e1e0a9bca9', 'Start workspace')} - - - {variant === 'sheet' ? ( - - + - + - + } + /> + + {translate('auto.components.LinearIssueWorkspace.e1e0a9bca9', 'Start workspace')} + + + {variant === 'sheet' ? ( + + + + + } + /> {translate('auto.components.LinearIssueWorkspace.df4c86ed12', 'Close')} @@ -961,16 +972,18 @@ export default function LinearIssueWorkspace({ const Icon = item.icon return ( - - - + + + {item.label} + + } + /> {item.label} @@ -999,24 +1012,18 @@ export default function LinearIssueWorkspace({ side="right" showCloseButton={false} className="w-[min(92vw,1180px)] bg-background p-0 sm:max-w-[1180px]" - onOpenAutoFocus={(event) => { - event.preventDefault() - }} + initialFocus={false} > - - - {displayed?.title ?? - translate('auto.components.LinearIssueWorkspace.61f424f8ca', 'Linear issue')} - - - - - {translate( - 'auto.components.LinearIssueWorkspace.ad5dec37b7', - 'Preview, edit, and start work from the selected issue.' - )} - - + + {displayed?.title ?? + translate('auto.components.LinearIssueWorkspace.61f424f8ca', 'Linear issue')} + + + {translate( + 'auto.components.LinearIssueWorkspace.ad5dec37b7', + 'Preview, edit, and start work from the selected issue.' + )} + {content} diff --git a/src/renderer/src/components/LinearItemDrawer.tsx b/src/renderer/src/components/LinearItemDrawer.tsx index 25ee1ffef16..f76efbbef4b 100644 --- a/src/renderer/src/components/LinearItemDrawer.tsx +++ b/src/renderer/src/components/LinearItemDrawer.tsx @@ -20,7 +20,6 @@ import { LinearIssueTextEditor } from '@/components/LinearIssueTextEditor' import { Sheet, SheetContent, SheetDescription, SheetTitle } from '@/components/ui/sheet' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' -import { VisuallyHidden } from 'radix-ui' import CommentMarkdown from '@/components/sidebar/CommentMarkdown' import { cn } from '@/lib/utils' import { @@ -447,21 +446,23 @@ export function LinearIssueEditSection({
- - - + + + {localState.name} + + + } + /> - - - + + + + {PRIORITY_LABELS[localPriority] ?? `P${localPriority}`} + + + + } + /> {[0, 1, 2, 3, 4].map((p) => ( - + + {localAssignee?.avatarUrl ? ( + + ) : ( + + )} + + {localAssignee + ? localAssignee.displayName + : translate('auto.components.LinearItemDrawer.866316f22c', 'Unassigned')} + + + + } + /> - - - + + + + {formatLinearEstimateLabel(localEstimate)} + + + + } + />
@@ -681,31 +688,33 @@ export function LinearIssueEditSection({
- - - + + + + {localLabels.length + ? labelSummary + : translate('auto.components.LinearItemDrawer.23886c7eec', 'Add label')} + + + + } + /> {/* Status */} - - - + + + {localState.name} + + + } + /> {states.error ? (
{states.error}
@@ -817,20 +828,22 @@ export function LinearIssueEditSection({ {/* Priority */} - - - + + + + {PRIORITY_LABELS[localPriority] ?? `P${localPriority}`} + + + + } + /> {[0, 1, 2, 3, 4].map((p) => ( - + + {formatLinearEstimateLabel(localEstimate)} + + + } + />
@@ -920,21 +935,23 @@ export function LinearIssueEditSection({ {/* Assignee */} - - - + + + {localAssignee + ? localAssignee.displayName + : translate('auto.components.LinearItemDrawer.d71cd3003e', '+ Assignee')} + + + + } + />
- + + {labelSummary} + + + } + /> {labels.error ? (
{labels.error}
@@ -1378,24 +1397,18 @@ export default function LinearItemDrawer({ side="right" showCloseButton={false} className="w-full p-0 sm:max-w-[640px]" - onOpenAutoFocus={(event) => { - event.preventDefault() - }} + initialFocus={false} > - - - {displayed?.title ?? - translate('auto.components.LinearItemDrawer.39883467f4', 'Linear issue')} - - - - - {translate( - 'auto.components.LinearItemDrawer.04a442f796', - 'Preview and edit the selected Linear issue.' - )} - - + + {displayed?.title ?? + translate('auto.components.LinearItemDrawer.39883467f4', 'Linear issue')} + + + {translate( + 'auto.components.LinearItemDrawer.04a442f796', + 'Preview and edit the selected Linear issue.' + )} + {displayed && (
@@ -1424,39 +1437,43 @@ export default function LinearItemDrawer({
- - - + window.api.shell.openUrl(displayed.url)} + aria-label={translate( + 'auto.components.LinearItemDrawer.0190b760c1', + 'Open on Linear' + )} + > + + + } + /> {translate('auto.components.LinearItemDrawer.0190b760c1', 'Open on Linear')} - - - + + + + } + /> {translate('auto.components.LinearItemDrawer.9dc54172db', 'Close · Esc')} diff --git a/src/renderer/src/components/NewWorkspaceComposerCard.tsx b/src/renderer/src/components/NewWorkspaceComposerCard.tsx index c7d96c14fef..632ebfc0b4f 100644 --- a/src/renderer/src/components/NewWorkspaceComposerCard.tsx +++ b/src/renderer/src/components/NewWorkspaceComposerCard.tsx @@ -279,37 +279,39 @@ function WorkspaceRunTargetCombobox({ return ( - - - + ) : selectedHost ? ( + + + {selectedHost.label} + + ) : ( + + {translate( + 'auto.components.NewWorkspaceComposerCard.chooseRunTarget', + 'Choose target' + )} + + )} + + + } + /> 0 ? ( - - {/* Why: a real CommandItem (not a raw button) so cmdk registers it — fixes the row - only rendering under the first host, the uneven height, and the double-highlight. */} - setVmRecipesOpen(true)} - className="items-center gap-2 px-3 py-2" - > - - -
-
{ephemeralVmLabel}
- {/* Why: a second line so this row matches the two-line height of the host - options above, and to hint what choosing it opens. */} -
- {translate( - 'auto.components.NewWorkspaceComposerCard.perWorkspaceEnvHint', - 'Provision an on-demand environment from a recipe' + {/* Why: a real CommandItem (not a raw button) so cmdk registers it — fixes the row + only rendering under the first host, the uneven height, and the double-highlight. */} + setVmRecipesOpen(true)} + className="items-center gap-2 px-3 py-2" + > + + +
+
{ephemeralVmLabel}
+ {/* Why: a second line so this row matches the two-line height of the host + options above, and to hint what choosing it opens. */} +
+ {translate( + 'auto.components.NewWorkspaceComposerCard.perWorkspaceEnvHint', + 'Provision an on-demand environment from a recipe' + )} +
-
- - - + + + } + /> @@ -810,21 +814,23 @@ export default function NewWorkspaceComposerCard({ {showAddProjectButton ? ( - - - + + + + } + /> {translate('auto.components.NewWorkspaceComposerCard.d6b0a96f32', 'Add project')} @@ -1043,25 +1049,27 @@ export default function NewWorkspaceComposerCard({ {translate('auto.components.NewWorkspaceComposerCard.01d1e8f601', 'Agent')} - - - + + + + } + /> {translate( 'auto.components.NewWorkspaceComposerCard.ba64270bdb', diff --git a/src/renderer/src/components/NewWorkspaceComposerModal.tsx b/src/renderer/src/components/NewWorkspaceComposerModal.tsx index 3bbac8e9676..5637ef56707 100644 --- a/src/renderer/src/components/NewWorkspaceComposerModal.tsx +++ b/src/renderer/src/components/NewWorkspaceComposerModal.tsx @@ -84,18 +84,20 @@ function ComposerModalBody({ onClose: () => void onOpenChange: (open: boolean) => void }): React.JSX.Element { + const contentRef = React.useRef(null) return ( { - // Why: Radix's FocusScope fires this once the dialog has mounted. - // preventDefault stops it from focusing whatever first-tabbable it - // picks (close button), and we instead focus the name/source field - // so users can start typing immediately. - event.preventDefault() - const content = event.currentTarget as HTMLElement - getWorkspaceComposerInitialFocusTarget(content)?.focus({ preventScroll: true }) + initialFocus={() => { + // Why: skip Base UI's default auto-focus (the close button) and focus + // the name/source field instead so users can start typing immediately. + const content = contentRef.current + if (content) { + getWorkspaceComposerInitialFocusTarget(content)?.focus({ preventScroll: true }) + } + return false }} > diff --git a/src/renderer/src/components/PullRequestPage.tsx b/src/renderer/src/components/PullRequestPage.tsx index d807dacf89f..af06bf875b0 100644 --- a/src/renderer/src/components/PullRequestPage.tsx +++ b/src/renderer/src/components/PullRequestPage.tsx @@ -668,20 +668,25 @@ function PRAssigneesPanel({
{translate('auto.components.PullRequestPage.8ff5ae8866', 'Assignees')} - - - + + {isPending('assignees') ? ( + + ) : ( + + )} + + } + /> {repoAssignees.error ? (
@@ -1241,28 +1246,28 @@ function PRReviewersPanel({
{translate('auto.components.PullRequestPage.00d3be6bcd', 'Reviewers')} - - - + + {submitting ? ( + + ) : ( + + )} + + } + /> { - event.preventDefault() - }} + initialFocus={false} >
{canRemoveReviewer ? ( - - - + { + void handleRemoveReviewers([reviewer.login]) + }} + > + + + } + /> {translate('auto.components.PullRequestPage.7f964a365a', 'Remove reviewer')} @@ -2066,44 +2073,46 @@ function PRViewedCheckbox({ }): React.JSX.Element { return ( - - - + + {pending ? ( + + ) : checked ? ( + + ) : null} + + {translate('auto.components.PullRequestPage.2e528e1c2d', 'Viewed')} + + } + /> {checked ? translate('auto.components.PullRequestPage.2b4fdb880c', 'Unmark viewed') @@ -2770,20 +2779,22 @@ function PRFilesCombinedDiffViewer({
{fileTreeCollapsed && ( - - - + setFileTreeCollapsed(false)} + > + + + } + /> {translate('auto.components.PullRequestPage.319cf2d54b', 'Show file tree')} @@ -3047,102 +3058,110 @@ function CommentCodeContext({ > {(contextBefore > 0 || contextAfter > 0) && ( - + { + setContextBefore(0) + setContextAfter(0) + }} + aria-label={translate( + 'auto.components.PullRequestPage.5f3e293517', + 'Reset code context' + )} + > + + + } + /> + + {translate('auto.components.PullRequestPage.5f3e293517', 'Reset code context')} + + + )} + + { - setContextBefore(0) - setContextAfter(0) - }} + disabled={!canExpandAbove} + onClick={() => + setContextBefore((current) => + Math.min(current + CODE_CONTEXT_EXPAND_STEP, commentFrom - 1) + ) + } aria-label={translate( - 'auto.components.PullRequestPage.5f3e293517', - 'Reset code context' + 'auto.components.PullRequestPage.e295a78c11', + 'Show {{value0}} more lines above', + { value0: CODE_CONTEXT_EXPAND_STEP } )} > - + - - - {translate('auto.components.PullRequestPage.5f3e293517', 'Reset code context')} - - - )} - - - - + } + /> {translate('auto.components.PullRequestPage.c9de94b07a', 'Show more lines above')} - - - + + setContextAfter((current) => + Math.min(current + CODE_CONTEXT_EXPAND_STEP, totalLines - commentTo) + ) + } + aria-label={translate( + 'auto.components.PullRequestPage.e295a78c11', + 'Show {{value0}} more lines below', + { value0: CODE_CONTEXT_EXPAND_STEP } + )} + > + + + } + /> {translate('auto.components.PullRequestPage.51ed0cf38b', 'Show more lines below')} - - - + { + setContextBefore((current) => + Math.max(current, Math.max(0, commentFrom - blockRange.startLine)) + ) + setContextAfter((current) => + Math.max(current, Math.max(0, blockRange.endLine - commentTo)) + ) + }} + aria-label={blockTooltip} + > + + + } + /> {blockTooltip} @@ -3498,43 +3517,47 @@ function ConversationTab({ )}
- - - - - {translate('auto.components.PullRequestPage.d6c6679de7', 'Reply to comment')} - - - {comment.url && ( - - + window.api.shell.openUrl(comment.url)} + onClick={() => + setReplyingTo((current) => (current === comment.id ? null : comment.id)) + } aria-label={translate( - 'auto.components.PullRequestPage.0ac19bb52e', - 'Open comment on GitHub' + 'auto.components.PullRequestPage.d6c6679de7', + 'Reply to comment' )} > - + - + } + /> + + {translate('auto.components.PullRequestPage.d6c6679de7', 'Reply to comment')} + + + {comment.url && ( + + window.api.shell.openUrl(comment.url)} + aria-label={translate( + 'auto.components.PullRequestPage.0ac19bb52e', + 'Open comment on GitHub' + )} + > + + + } + /> {translate('auto.components.PullRequestPage.0ac19bb52e', 'Open comment on GitHub')} @@ -3602,7 +3625,7 @@ function ConversationTab({ const root = getPRCommentGroupRoot(group) const count = getPRCommentGroupCount(group) return ( - + ) : ( - - - + { + setBodyDraft(body) + setBodyEditing(true) + }} + aria-label={translate( + 'auto.components.PullRequestPage.da9aaa8bcf', + 'Edit description' + )} + > + + + } + /> {translate('auto.components.PullRequestPage.da9aaa8bcf', 'Edit description')} @@ -4086,29 +4111,33 @@ function PRActionsPanel({
- - - - - + + {mergePending ? ( + + ) : ( + + )} + {mergePresentation.autoMergeAction?.label ?? + (mergePresentation.directMergeAvailable + ? mergeMethods.defaultLabel + : mergePresentation.label)} + + + } + /> + } + /> {!canMergeWithRepoContext ? translate( @@ -4122,7 +4151,7 @@ function PRActionsPanel({ {mergePresentation.autoMergeAction && ( void handleAutoMerge()} + onClick={() => void handleAutoMerge()} > {mergePresentation.autoMergeAction.label} @@ -4133,13 +4162,13 @@ function PRActionsPanel({ void handleMerge(method)} + onClick={() => void handleMerge(method)} > {label} ))} - window.api.shell.openUrl(item.url)}> + window.api.shell.openUrl(item.url)}> {translate('auto.components.PullRequestPage.7df8d5fc60', 'Open GitHub merge box')} @@ -4834,19 +4863,21 @@ function ChecksTab({ const refreshAction = ( - - - + void handleRefresh()} + aria-label={translate('auto.components.PullRequestPage.5d0f42766d', 'Refresh checks')} + > + + + } + /> {translate('auto.components.PullRequestPage.5d0f42766d', 'Refresh checks')} @@ -4855,25 +4886,27 @@ function ChecksTab({ const fixBrokenChecksAction = failedChecks.length > 0 || fixingChecks ? ( - - - + void handleFixBrokenChecks()} + > + {fixingChecks ? ( + + ) : ( + + )} + {variant === 'compact' + ? translate('auto.components.PullRequestPage.c808db1dd1', 'Fix checks') + : translate('auto.components.PullRequestPage.a4541fd3db', 'Fix broken checks')} + + } + /> {translate( 'auto.components.PullRequestPage.0fa8b8faec', @@ -4885,32 +4918,34 @@ function ChecksTab({ const rerunAction = list.length > 0 || rerunning ? ( - - - + + {rerunning ? ( + + ) : ( + + )} + {translate('auto.components.PullRequestPage.522d9353e1', 'Rerun')} + + + } + /> void handleRerun(true)} + onClick={() => void handleRerun(true)} > {translate('auto.components.PullRequestPage.68605516dd', 'Rerun failed checks')} - void handleRerun(false)}> + void handleRerun(false)}> {translate('auto.components.PullRequestPage.54cddd1858', 'Rerun all checks')} @@ -6101,18 +6136,20 @@ function GHEditSection({
{/* State */} - - - + + {getStateLabel({ ...item, state: localState })} + + + } + /> - + ) : ( + localLabels.map((name) => ( + + {name} + + )) + )} + {isPending('labels') ? ( + + ) : ( + + )} + + } + /> {repoLabels.error ? (
@@ -6199,30 +6238,32 @@ function GHEditSection({ {/* Assignees */} - - - + ) : ( + localAssignees.map((login) => ( + + {login} + + )) + )} + {isPending('assignees') ? ( + + ) : ( + + )} + + } + /> {repoAssignees.error ? (
@@ -6403,22 +6444,24 @@ function GHCommentComposer({ className="scrollbar-sleek block h-20 max-h-[240px] min-h-20 w-full resize-none overflow-y-auto rounded-md border border-input bg-card px-3 py-2 pb-12 pr-12 text-[13px] leading-5 placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring" /> - - - + + {submitting ? ( + + ) : ( + + )} + + } + /> {translate('auto.components.PullRequestPage.161d91ef02', 'Send comment')} @@ -7026,25 +7069,27 @@ export default function PullRequestPage({ #{workItem.number}
- - - + void handleCopyWorkItemLink()} + aria-label={translate( + 'auto.components.PullRequestPage.347034903a', + 'Copy GitHub link' + )} + > + {linkCopied ? ( + + ) : ( + + )} + + } + /> {linkCopied ? translate('auto.components.PullRequestPage.3b6886b2ee', 'Copied') @@ -7052,19 +7097,21 @@ export default function PullRequestPage({ - - - + window.api.shell.openUrl(workItem.url)} + aria-label={translate( + 'auto.components.PullRequestPage.8ecda455a0', + 'Open on GitHub' + )} + > + + + } + /> {translate('auto.components.PullRequestPage.8ecda455a0', 'Open on GitHub')} @@ -7108,27 +7155,29 @@ export default function PullRequestPage({ : translate('auto.components.PullRequestPage.71a3c0f9d2', 'Start workspace')} - - - + + + + } + /> {attachedWorkspace ? ( - + {translate('auto.components.PullRequestPage.1a2570e18e', 'Start new workspace')} ) : null} - window.api.shell.openUrl(workItem.url)}> + window.api.shell.openUrl(workItem.url)}> {translate('auto.components.PullRequestPage.8ecda455a0', 'Open on GitHub')} diff --git a/src/renderer/src/components/TaskPage.tsx b/src/renderer/src/components/TaskPage.tsx index 0ade42f2811..94babfedf74 100644 --- a/src/renderer/src/components/TaskPage.tsx +++ b/src/renderer/src/components/TaskPage.tsx @@ -738,38 +738,40 @@ function LinearStateCell({ return ( - - - + e.stopPropagation()} + className={cn( + 'inline-flex min-w-0 cursor-pointer! items-center gap-1 rounded-full border text-[11px] font-medium transition-[background-color,border-color,color,box-shadow] hover:[--linear-state-pill-current-background:var(--linear-state-pill-hover-background)] hover:[--linear-state-pill-current-border:var(--linear-state-pill-hover-border)] hover:[--linear-state-pill-current-foreground:var(--linear-state-pill-hover-foreground)] hover:ring-1 hover:ring-foreground/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 disabled:cursor-default! disabled:opacity-80 [&_*]:cursor-pointer! disabled:[&_*]:cursor-default!', + className + )} + style={{ + ...getLinearStatePillStyle(issue.state.color), + cursor: pending ? 'default' : 'pointer' + }} + aria-label={translate( + 'auto.components.TaskPage.d45a910c4a', + 'Change Linear state from {{value0}}', + { value0: issue.state.name } + )} + aria-busy={pending || states.loading} + > + + {issue.state.name} + {pending || states.loading ? ( + + ) : ( + + )} + + } + /> {error.title}
{error.details ? ( <> - - - + + {open ? ( + + ) : ( + + )} + {translate('auto.components.TaskPage.40eaf2c27c', 'Details')} + + } + />
{error.details} @@ -1319,27 +1327,29 @@ function GHStatusCell({ return ( - - - + e.stopPropagation()} + onKeyDown={(e) => e.stopPropagation()} + className={cn( + 'group/status inline-flex cursor-pointer items-center gap-1 rounded-full border px-2 py-0.5 text-[10px] font-medium transition hover:brightness-125 hover:ring-1 hover:ring-white/10', + localState === 'closed' + ? 'border-primary/40 bg-primary/10 text-primary' + : 'border-emerald-500/40 bg-emerald-500/10 text-emerald-700 dark:text-emerald-200' + )} + > + {localState === 'open' ? : null} + + {localState === 'closed' + ? translate('auto.components.TaskPage.d09bf34db7', 'Closed') + : translate('auto.components.TaskPage.606a85c774', 'Open')} + + + + } + /> - - - + + {selectedLabels.length === 0 ? ( + + {translate('auto.components.TaskPage.5ebff3a0aa', 'None')} + + ) : ( + + {selectedLabels.map((label) => ( + + {label} + + ))} + + )} + {loading ? : null} + + } + /> {error ? (
{error}
@@ -1691,32 +1703,34 @@ function GitHubIssueAssigneeSelector({ {translate('auto.components.TaskPage.8aba10579d', 'Assignees')} - - - + )} + {loading ? : null} + + } + /> {error ? (
{error}
@@ -1915,34 +1929,36 @@ function GHAssigneesCell({ return ( - - - + a.login).join(', ') + }) + : translate('auto.components.TaskPage.7f94eb6395', 'Assign issue') + } + aria-busy={pendingLogin !== null} + onClick={(event) => event.stopPropagation()} + onKeyDown={(event) => event.stopPropagation()} + className={cn( + 'inline-flex h-6 max-w-full items-center gap-1 text-left transition disabled:opacity-60', + assignees.length > 0 + ? 'rounded-full border border-border/40 bg-background/70 px-1.5 hover:bg-muted/60' + : 'w-full rounded-sm border border-transparent bg-transparent px-1 hover:bg-muted/40' + )} + > + {triggerContent} + {pendingLogin ? ( + + ) : assignees.length > 0 ? ( + + ) : null} + + } + /> - - - + event.stopPropagation()} + className={cn( + 'inline-flex h-7 max-w-full items-center justify-center text-[12px] font-medium transition hover:brightness-110', + primaryReviewer + ? 'gap-1 rounded-full border border-border/40 bg-background/70 px-1.5 text-muted-foreground hover:text-foreground' + : 'min-w-7 text-muted-foreground hover:text-foreground' + )} + aria-label={translate( + 'auto.components.TaskPage.editReviewersWithCurrent', + 'Edit reviewers: {{value0}}', + { value0: getGitHubPRReviewLabel(itemWithLocalReviewRequests) } + )} + title={getGitHubPRReviewLabel(itemWithLocalReviewRequests)} + > + {primaryReviewer ? ( + <> + + {extraReviewerCount > 0 ? ( + + +{extraReviewerCount} + + ) : null} + + + ) : ( + + )} + + } + /> event.stopPropagation()} - onOpenAutoFocus={(event) => { - event.preventDefault() - }} + initialFocus={false} >
@@ -2697,26 +2712,28 @@ function PRChecksCell({ : Minus return ( - - - + { + event.stopPropagation() + onLoadChecks() + onOpen() + }} + className={cn( + 'inline-flex max-w-full items-center gap-1 rounded-full border px-2 py-0.5 text-[10px] font-medium transition hover:brightness-110', + getChecksPillTone(item) + )} + > + + {getChecksLabel(item)} + + } + /> {translate('auto.components.TaskPage.995dd6af9b', 'Open PR checks')} @@ -2878,33 +2895,37 @@ function PRMergeCell({ return ( - - - - - + event.stopPropagation()} + className={cn( + 'inline-flex max-w-full items-center gap-1 rounded-full border px-2 py-0.5 text-[10px] font-medium transition hover:brightness-110', + mergePresentation.tone + )} + > + {merging ? ( + + ) : ( + + )} + {mergePresentation.label} + + + } + /> + } + /> {mergePresentation.tooltip} event.stopPropagation()}> {mergePresentation.autoMergeAction && ( - void handleAutoMerge()}> + void handleAutoMerge()}> {mergePresentation.autoMergeAction.label} @@ -2914,13 +2935,13 @@ function PRMergeCell({ void handleMerge(method)} + onClick={() => void handleMerge(method)} > {label} ))} - window.api.shell.openUrl(item.url)}> + window.api.shell.openUrl(item.url)}> {translate('auto.components.TaskPage.37d60046e3', 'Open GitHub merge box')} @@ -8176,20 +8197,22 @@ export default function TaskPage(): React.JSX.Element { Left-aligned keeps it clear of the app sidebar on the right edge. */} - - - + + + + } + /> {translate('auto.components.TaskPage.4826fd1ad8', 'Close · Esc')} @@ -8202,42 +8225,46 @@ export default function TaskPage(): React.JSX.Element { const sourceDisabled = source.disabled || sourceAvailabilityNotice?.blocking return ( - - - + void updateSettings({ defaultTaskSource: source.id }).catch( + () => { + toast.error( + translate( + 'auto.components.TaskPage.609532fae7', + 'Failed to save default task source.' + ) + ) + } + ) + }} + data-task-source={source.id} + aria-label={sourceAvailabilityNotice?.label ?? source.label} + aria-pressed={active} + className={cn( + 'group flex h-8 w-8 items-center justify-center rounded-md border transition', + active + ? 'border-foreground/40 bg-muted/70 text-foreground shadow-sm' + : 'border-border/40 bg-transparent text-muted-foreground hover:bg-muted/40 hover:text-foreground', + sourceDisabled && 'cursor-not-allowed opacity-55' + )} + > + + + } + /> {sourceAvailabilityNotice?.label ?? source.label} @@ -8265,35 +8292,37 @@ export default function TaskPage(): React.JSX.Element { onOpen={handleLinearScopeOpen} /> - - - + className="h-8 w-8 rounded-md border-border/50 bg-muted/50 text-foreground shadow-sm transition hover:bg-muted/50" + > + + + } + /> {selectedLinearTeamForExternalLink ? translate( @@ -8315,6 +8344,9 @@ export default function TaskPage(): React.JSX.Element { - - setReadFilter(pressed ? 'unread' : 'all')} - variant="outline" - size="sm" - className={cn( - 'size-8 shrink-0 p-0', - readFilter === 'unread' - ? '!border-primary !bg-primary !text-primary-foreground shadow-xs ring-2 ring-primary/35 hover:!bg-primary/90 hover:!text-primary-foreground' - : 'text-muted-foreground hover:text-foreground' - )} - aria-label={translate( - 'auto.components.activity.ActivityPrototypePage.d1a88df9a8', - 'Show unread threads only' - )} - > - - - + setReadFilter(pressed ? 'unread' : 'all')} + variant="outline" + size="sm" + className={cn( + 'size-8 shrink-0 p-0', + readFilter === 'unread' + ? '!border-primary !bg-primary !text-primary-foreground shadow-xs ring-2 ring-primary/35 hover:!bg-primary/90 hover:!text-primary-foreground' + : 'text-muted-foreground hover:text-foreground' + )} + aria-label={translate( + 'auto.components.activity.ActivityPrototypePage.d1a88df9a8', + 'Show unread threads only' + )} + > + + + } + /> {translate( 'auto.components.activity.ActivityPrototypePage.d1a88df9a8', diff --git a/src/renderer/src/components/activity/ActivityTitlebarControls.tsx b/src/renderer/src/components/activity/ActivityTitlebarControls.tsx index 93ca4c65c1c..5c956d10e74 100644 --- a/src/renderer/src/components/activity/ActivityTitlebarControls.tsx +++ b/src/renderer/src/components/activity/ActivityTitlebarControls.tsx @@ -21,20 +21,22 @@ export function ActivityTitlebarControls(): React.JSX.Element { sidebar's nav row isn't available as the back path. This Back button is the dedicated exit, mirroring Settings' onBack pattern. */} - - - + + + + } + /> {translate( 'auto.components.activity.ActivityTitlebarControls.dc708f3eff', diff --git a/src/renderer/src/components/agent/AgentCombobox.tsx b/src/renderer/src/components/agent/AgentCombobox.tsx index fb15e5722a5..6713cf7dee7 100644 --- a/src/renderer/src/components/agent/AgentCombobox.tsx +++ b/src/renderer/src/components/agent/AgentCombobox.tsx @@ -98,9 +98,9 @@ function renderItem({ // Why: z-[70] sits above PopoverContent's z-[60] so the right-click menu // renders in front of the still-open combobox popover instead of behind it. - {row} + - + {isDefault ? translate('auto.components.agent.AgentCombobox.1b0d6965fa', 'Current default') @@ -263,39 +263,41 @@ export default function AgentCombobox({ return (
- - - + )} + + + } + /> { - event.preventDefault() + // Why: return false suppresses Base UI's default popup focus so our + // custom rAF focus (caret-at-end) runs instead — mirrors the old + // onOpenAutoFocus preventDefault + focusSearchInput. + initialFocus={() => { focusSearchInput() + return false }} > diff --git a/src/renderer/src/components/automations/AutomationDetail.tsx b/src/renderer/src/components/automations/AutomationDetail.tsx index b0ffad51b3f..45c636006bc 100644 --- a/src/renderer/src/components/automations/AutomationDetail.tsx +++ b/src/renderer/src/components/automations/AutomationDetail.tsx @@ -75,18 +75,20 @@ function ToolbarIconButton({ }): React.JSX.Element { return ( - - - + + {children} + + } + /> {label} @@ -152,19 +154,24 @@ export function AutomationDetail({
- - - - - + + + + } + /> {runNowDisabled ? ( {runNowAvailability.message} diff --git a/src/renderer/src/components/automations/AutomationEditorDialog.tsx b/src/renderer/src/components/automations/AutomationEditorDialog.tsx index f595290d2a2..c26addb3f7f 100644 --- a/src/renderer/src/components/automations/AutomationEditorDialog.tsx +++ b/src/renderer/src/components/automations/AutomationEditorDialog.tsx @@ -137,9 +137,7 @@ export function AutomationEditorDialog({ { - event.preventDefault() - }} + initialFocus={false} > - - - + + + + } + /> {translate( 'auto.components.automations.AutomationEditorDialog.6f9610e667', @@ -141,9 +143,8 @@ export function AutomationEditorDialogFooter({ ) : (
+ value={[draft.workspaceMode]} + onValueChange={([workspaceMode]) => workspaceMode && onDraftChange((current) => ({ ...current, diff --git a/src/renderer/src/components/automations/AutomationEditorDialogHeader.tsx b/src/renderer/src/components/automations/AutomationEditorDialogHeader.tsx index b1712f5250c..bfc0bb40049 100644 --- a/src/renderer/src/components/automations/AutomationEditorDialogHeader.tsx +++ b/src/renderer/src/components/automations/AutomationEditorDialogHeader.tsx @@ -105,10 +105,9 @@ export function AutomationEditorDialogHeader({ {isCreateMode ? (
- value && onCreateTargetChange(value as AutomationCreateTarget) + value[0] && onCreateTargetChange(value[0] as AutomationCreateTarget) } variant="outline" size="sm" @@ -128,20 +127,22 @@ export function AutomationEditorDialogHeader({ - - - + + + {translate( + 'auto.components.automations.AutomationEditorDialogHeader.31f9253920', + 'Use template' + )} + + } + />
{templates.map((template) => ( diff --git a/src/renderer/src/components/automations/AutomationMissedRunGraceField.tsx b/src/renderer/src/components/automations/AutomationMissedRunGraceField.tsx index 01e68d038df..f5af26f709a 100644 --- a/src/renderer/src/components/automations/AutomationMissedRunGraceField.tsx +++ b/src/renderer/src/components/automations/AutomationMissedRunGraceField.tsx @@ -33,18 +33,20 @@ export function AutomationMissedRunGraceField({ 'Grace' )} - - - + + + + } + /> {translate( 'auto.components.automations.AutomationMissedRunGraceField.3d70c185c8', @@ -58,14 +60,17 @@ export function AutomationMissedRunGraceField({ - onDraftChange((current) => ({ ...current, precheckTimeoutSeconds })) - } + onValueChange={(precheckTimeoutSeconds) => { + if (precheckTimeoutSeconds) { + onDraftChange((current) => ({ ...current, precheckTimeoutSeconds })) + } + }} > - + {translate( 'auto.components.automations.AutomationPrecheckFields.51e28cdad9', diff --git a/src/renderer/src/components/automations/AutomationProjectCombobox.tsx b/src/renderer/src/components/automations/AutomationProjectCombobox.tsx index b63bc5821f8..bd08e357bf3 100644 --- a/src/renderer/src/components/automations/AutomationProjectCombobox.tsx +++ b/src/renderer/src/components/automations/AutomationProjectCombobox.tsx @@ -204,37 +204,42 @@ export default function AutomationProjectCombobox({ return ( - - - + + {selectedRepo ? ( + + + + ) : ( + {placeholder} + )} + + + } + /> { - event.preventDefault() + // Why: return false suppresses Base UI's default popup focus so our + // custom focus of the search input runs instead — mirrors the old + // onOpenAutoFocus preventDefault + focusSearchInput. + initialFocus={() => { focusSearchInput() + return false }} > @@ -311,23 +316,25 @@ export default function AutomationProjectCombobox({ setHostMenuProjectKey(nextOpen ? group.projectKey : null) } > - - - + { + event.preventDefault() + event.stopPropagation() + }} + onMouseDown={(event) => event.preventDefault()} + className="flex w-7 shrink-0 items-center justify-center text-muted-foreground" + > + + + } + /> - - - + + + + {label} + + + + } + /> + dayOfWeek && onDraftChange((current) => ({ ...current, dayOfWeek, scheduleWarning: null })) } > diff --git a/src/renderer/src/components/automations/AutomationSessionField.tsx b/src/renderer/src/components/automations/AutomationSessionField.tsx index b6aef00ebd8..d529d510052 100644 --- a/src/renderer/src/components/automations/AutomationSessionField.tsx +++ b/src/renderer/src/components/automations/AutomationSessionField.tsx @@ -23,18 +23,20 @@ export function AutomationSessionField({ {translate('auto.components.automations.AutomationSessionField.5ad314118e', 'Session')} - - - + + + + } + /> {translate( 'auto.components.automations.AutomationSessionField.b675112193', @@ -46,9 +48,8 @@ export function AutomationSessionField({ } > { + value={[draft.workspaceMode === 'existing' && draft.reuseSession ? 'reuse' : 'fresh']} + onValueChange={([value]) => { if (!value) { return } diff --git a/src/renderer/src/components/automations/AutomationsPage.tsx b/src/renderer/src/components/automations/AutomationsPage.tsx index 98a78ef9845..0d9416b01ce 100644 --- a/src/renderer/src/components/automations/AutomationsPage.tsx +++ b/src/renderer/src/components/automations/AutomationsPage.tsx @@ -2120,20 +2120,22 @@ export default function AutomationsPage(): React.JSX.Element {
- - - + + + + } + /> {translate('auto.components.automations.AutomationsPage.0329f9bef1', 'Close · Esc')} @@ -2144,21 +2146,23 @@ export default function AutomationsPage(): React.JSX.Element { {translate('auto.components.automations.AutomationsPage.77c2778945', 'Automations')} - - - + openCreateDialog()} + className="border border-border/50 bg-transparent hover:bg-muted/50" + data-contextual-tour-target="automations-create" + > + + + } + /> {translate( 'auto.components.automations.AutomationsPage.8d1afa8269', @@ -2169,21 +2173,23 @@ export default function AutomationsPage(): React.JSX.Element {
- - - + + + + } + /> {translate( 'auto.components.automations.AutomationsPage.19a6e30eae', @@ -2229,13 +2235,7 @@ export default function AutomationsPage(): React.JSX.Element { setDontAskDeleteAgain(false) }} > - { - event.preventDefault() - deleteConfirmButtonRef.current?.focus() - }} - > + {translate( @@ -2316,13 +2316,7 @@ export default function AutomationsPage(): React.JSX.Element { } }} > - { - event.preventDefault() - deleteConfirmButtonRef.current?.focus() - }} - > + {translate( @@ -2432,73 +2426,76 @@ export default function AutomationsPage(): React.JSX.Element { const scheduleLabel = formatAutomationSchedule(automation.rrule) return ( - - - + + } + /> { + onClick={(event) => { if (!automationRunAvailability.canRunNow) { event.preventDefault() return } void runNow(automation) }} + closeOnClick={false} > @@ -2510,11 +2507,11 @@ export default function AutomationsPage(): React.JSX.Element { : automationRunAvailability.message} - void openEditDialog(automation)}> + void openEditDialog(automation)}> {translate('auto.components.automations.AutomationsPage.f4612e3f78', 'Edit')} - void toggleAutomation(automation)}> + void toggleAutomation(automation)}> {automation.enabled ? ( ) : ( @@ -2533,7 +2530,7 @@ export default function AutomationsPage(): React.JSX.Element { requestDeleteAutomation(automation)} + onClick={() => requestDeleteAutomation(automation)} > {translate( @@ -2619,63 +2616,65 @@ export default function AutomationsPage(): React.JSX.Element { const scheduleDisplay = getExternalAutomationScheduleDisplay(entry.manager, entry.job) return ( - - - + + + {nextRunLabel} + + + } + /> requestExternalAction(entry.manager, entry.job, 'run')} + onClick={() => requestExternalAction(entry.manager, entry.job, 'run')} > @@ -2689,7 +2688,7 @@ export default function AutomationsPage(): React.JSX.Element { {entry.manager.provider === 'hermes' ? ( openEditExternalDialog(entry.manager, entry.job)} + onClick={() => openEditExternalDialog(entry.manager, entry.job)} > {translate( @@ -2700,7 +2699,7 @@ export default function AutomationsPage(): React.JSX.Element { ) : null} + onClick={() => requestExternalAction( entry.manager, entry.job, @@ -2727,7 +2726,7 @@ export default function AutomationsPage(): React.JSX.Element { requestExternalAction(entry.manager, entry.job, 'delete')} + onClick={() => requestExternalAction(entry.manager, entry.job, 'delete')} > {translate( diff --git a/src/renderer/src/components/automations/CreateFromPicker.tsx b/src/renderer/src/components/automations/CreateFromPicker.tsx index 083ba015da7..a161183ace5 100644 --- a/src/renderer/src/components/automations/CreateFromPicker.tsx +++ b/src/renderer/src/components/automations/CreateFromPicker.tsx @@ -174,32 +174,39 @@ export function CreateFromPicker({ return (
- - - + + + } + /> { - event.preventDefault() + initialFocus={() => { + // Why: skip Base UI's default autofocus and focus the search input + // on the next frame (mirrors the prior onOpenAutoFocus behavior). focusSearchInput() + return false }} > diff --git a/src/renderer/src/components/automations/ExternalAutomationManagers.tsx b/src/renderer/src/components/automations/ExternalAutomationManagers.tsx index bd6e1b67549..885aded7916 100644 --- a/src/renderer/src/components/automations/ExternalAutomationManagers.tsx +++ b/src/renderer/src/components/automations/ExternalAutomationManagers.tsx @@ -79,18 +79,20 @@ function ExternalActionButton({ }): React.JSX.Element { return ( - - - + + {children} + + } + /> {label} @@ -194,26 +196,28 @@ export function ExternalAutomationManagers({ )} - - {/* Absolutely pinned to the row's top-right corner (above the action - icons) so it reaches the true right edge, not just the grid column. */} - - {/* Match either key: job.enabled flips mid-flight, so a single-action + + {/* Match either key: job.enabled flips mid-flight, so a single-action match would drop the spinner a render early. */} - {runningActionKey === actionKey(manager, job, 'pause') || - runningActionKey === actionKey(manager, job, 'resume') ? ( - - ) : null} - - onAction(manager, job, job.enabled ? 'pause' : 'resume') - } - disabled={disabledMessage !== null} - ariaLabelledBy={`automation-name-${manager.id}-${job.id}`} - /> - - + {runningActionKey === actionKey(manager, job, 'pause') || + runningActionKey === actionKey(manager, job, 'resume') ? ( + + ) : null} + + onAction(manager, job, job.enabled ? 'pause' : 'resume') + } + disabled={disabledMessage !== null} + ariaLabelledBy={`automation-name-${manager.id}-${job.id}`} + /> + + } + /> {disabledMessage ?? (job.enabled diff --git a/src/renderer/src/components/automations/ExternalAutomationRunTable.tsx b/src/renderer/src/components/automations/ExternalAutomationRunTable.tsx index aa8ff9666c7..ec8f1ea4798 100644 --- a/src/renderer/src/components/automations/ExternalAutomationRunTable.tsx +++ b/src/renderer/src/components/automations/ExternalAutomationRunTable.tsx @@ -189,9 +189,7 @@ export function ExternalAutomationRunTable({ {isLoading ? : null} {fetchError ? ( - - - + } /> {fetchError} diff --git a/src/renderer/src/components/automations/WorkspaceCombobox.tsx b/src/renderer/src/components/automations/WorkspaceCombobox.tsx index 0951a2e23b2..6e3b2a7ee30 100644 --- a/src/renderer/src/components/automations/WorkspaceCombobox.tsx +++ b/src/renderer/src/components/automations/WorkspaceCombobox.tsx @@ -66,30 +66,35 @@ export function WorkspaceCombobox({ return ( - - - + + + {selected?.displayName ?? + translate( + 'auto.components.automations.WorkspaceCombobox.66a0cd9628', + 'Select workspace' + )} + + + + } + /> { - event.preventDefault() + // Why: return false suppresses Base UI's default popup focus so our + // custom focus of the search input runs instead — mirrors the old + // onOpenAutoFocus preventDefault + focusSearchInput. + initialFocus={() => { focusSearchInput() + return false }} > diff --git a/src/renderer/src/components/browser-pane/BrowserAddressBar.tsx b/src/renderer/src/components/browser-pane/BrowserAddressBar.tsx index 973f2089740..3b5e59f41e7 100644 --- a/src/renderer/src/components/browser-pane/BrowserAddressBar.tsx +++ b/src/renderer/src/components/browser-pane/BrowserAddressBar.tsx @@ -355,59 +355,59 @@ export default function BrowserAddressBar({ setOpen(next) }} > - -
{ - event.preventDefault() - setOpen(false) - clearSuggestionPreview() - setAutocompleteQuery(value) - onSubmit() - }} - > - - { - const nextValue = event.target.value - // Why: typing creates a new suggestion list, so keyboard selection - // should return to the derived top match instead of a stale row. - // Clearing preview state here also prevents stale hover/selection - // from repopulating the input after Cmd+A → Delete. - prePreviewValueRef.current = null - setSelectedValueOverride(null) - setAutocompleteQuery(nextValue) - onChange(nextValue) + { + event.preventDefault() + setOpen(false) + clearSuggestionPreview() + setAutocompleteQuery(value) + onSubmit() }} - role="combobox" - aria-expanded={open} - aria-controls="browser-history-listbox" - aria-autocomplete="list" - /> - - + > + + { + const nextValue = event.target.value + // Why: typing creates a new suggestion list, so keyboard selection + // should return to the derived top match instead of a stale row. + // Clearing preview state here also prevents stale hover/selection + // from repopulating the input after Cmd+A → Delete. + prePreviewValueRef.current = null + setSelectedValueOverride(null) + setAutocompleteQuery(nextValue) + onChange(nextValue) + }} + role="combobox" + aria-expanded={open} + aria-controls="browser-history-listbox" + aria-autocomplete="list" + /> + + } + /> {suggestions.length > 0 && ( { - // Why: prevent the popover from stealing focus away from the - // address bar input. The user is still typing; the popover is - // an overlay of suggestions, not a focus target. - e.preventDefault() - }} + // Why: prevent the popover from stealing focus away from the + // address bar input. The user is still typing; the popover is + // an overlay of suggestions, not a focus target. + initialFocus={false} > - - - + + + {translate('auto.components.browser.pane.BrowserImportHintButton.b24fef25be', 'Import')} + + } + />
@@ -194,20 +196,22 @@ export function BrowserImportHintButton({
- - - + + {translate( + 'auto.components.browser.pane.BrowserImportHintButton.244266c122', + 'Import…' + )} + + } + /> {detectedBrowsers.map((browser) => browser.profiles.length > 1 ? ( @@ -224,7 +228,7 @@ export function BrowserImportHintButton({ {browser.profiles.map((profile) => ( + onClick={() => void handleImportFromBrowser(browser.family, profile.directory) } > @@ -237,7 +241,7 @@ export function BrowserImportHintButton({ ) : ( void handleImportFromBrowser(browser.family)} + onClick={() => void handleImportFromBrowser(browser.family)} > {translate( 'auto.components.browser.pane.BrowserImportHintButton.0c6d254eca', @@ -248,7 +252,7 @@ export function BrowserImportHintButton({ ) )} {detectedBrowsers.length > 0 ? : null} - void handleImportFromFile()}> + void handleImportFromFile()}> {translate( 'auto.components.browser.pane.BrowserImportHintButton.e0e125e074', 'From File…' diff --git a/src/renderer/src/components/browser-pane/BrowserPane.tsx b/src/renderer/src/components/browser-pane/BrowserPane.tsx index 20cde69e56f..d62e1671fd6 100644 --- a/src/renderer/src/components/browser-pane/BrowserPane.tsx +++ b/src/renderer/src/components/browser-pane/BrowserPane.tsx @@ -10,6 +10,7 @@ import { type DragEvent } from 'react' import { createPortal } from 'react-dom' +import type { Menu } from '@base-ui/react/menu' import { cn } from '@/lib/utils' import { createBrowserUuid } from '@/lib/browser-uuid' import { getConnectionId } from '@/lib/connection-context' @@ -406,7 +407,7 @@ function PendingBrowserAnnotationCard({ } }} > - + { - event.preventDefault() - onCancel() - }} >
@@ -474,11 +469,10 @@ function PendingBrowserAnnotationCard({ {translate('auto.components.browser.pane.BrowserPane.8f87e6c2e5', 'Intent')} { + value={[intent]} + onValueChange={([value]) => { if (value) { setIntent(value as BrowserAnnotationIntent) } @@ -2605,23 +2599,25 @@ function RemoteBrowserPagePane({ inputRef={addressBarInputRef} /> - - - + { + event.preventDefault() + }} + > + + + } + /> {translate( 'auto.components.browser.pane.BrowserPane.8b7e6d1f5a', @@ -2702,18 +2698,17 @@ function RemoteBrowserPagePane({ ) } -function preventAgentSendTargetOutsideDismiss(event: CustomEvent<{ originalEvent: Event }>) { - const target = event.detail.originalEvent.target +// Why: keep the send menu open when the outside press lands on an agent-send +// target — Base UI signals outside dismissals through onOpenChange's reason. +function isAgentSendTargetDismiss(target: EventTarget | null): boolean { if (!(target instanceof Element)) { - return + return false } - if ( + return Boolean( target.closest( '[data-agent-send-target="eligible"], [data-agent-send-target="disabled"], [data-agent-send-target="sending"]' ) - ) { - event.preventDefault() - } + ) } function BrowserPagePane({ @@ -4458,7 +4453,15 @@ function BrowserPagePane({ }, [browserAnnotationsPrompt, recordFeatureInteraction]) const handleAnnotationBannerSendOpenChange = useCallback( - (open: boolean): void => { + (open: boolean, eventDetails: Menu.Root.ChangeEventDetails): void => { + if ( + !open && + (eventDetails.reason === 'outside-press' || eventDetails.reason === 'focus-out') && + isAgentSendTargetDismiss(eventDetails.event.target) + ) { + eventDetails.cancel() + return + } setAnnotationBannerSendOpen(open) if (open) { openAgentSendPopoverTargetMode({ @@ -4486,7 +4489,15 @@ function BrowserPagePane({ ) const handleAnnotationTraySendOpenChange = useCallback( - (open: boolean): void => { + (open: boolean, eventDetails: Menu.Root.ChangeEventDetails): void => { + if ( + !open && + (eventDetails.reason === 'outside-press' || eventDetails.reason === 'focus-out') && + isAgentSendTargetDismiss(eventDetails.event.target) + ) { + eventDetails.cancel() + return + } setAnnotationTraySendOpen(open) if (open) { openAgentSendPopoverTargetMode({ @@ -5043,29 +5054,31 @@ function BrowserPagePane({ - - - - - + + + + } + /> {translate( 'auto.components.browser.pane.BrowserPane.acbe79fd01', @@ -5076,38 +5089,42 @@ function BrowserPagePane({ - - {/* Why: wrap the disabled button in a span so pointer events still + {/* Why: wrap the disabled button in a span so pointer events still reach the tooltip trigger — Radix (and the DOM) drop hover events on disabled - - + + + + } + /> {translate( 'auto.components.browser.pane.BrowserPane.fc9be38f6f', @@ -5369,14 +5386,21 @@ function BrowserPagePane({ onOpenChange={handleAnnotationBannerSendOpenChange} > - - - - - + + + {translate( + 'auto.components.browser.pane.BrowserPane.ac39b9366b', + 'Send' + )} + + } + /> + } + /> {translate( 'auto.components.browser.pane.BrowserPane.95af781091', @@ -5384,12 +5408,7 @@ function BrowserPagePane({ )} - + - - - + + + + } + /> {translate( 'auto.components.browser.pane.BrowserPane.11c5084aa2', @@ -5645,17 +5666,21 @@ function BrowserPagePane({ onOpenChange={handleAnnotationTraySendOpenChange} > - - - - - + + + {translate( + 'auto.components.browser.pane.BrowserPane.ac39b9366b', + 'Send' + )} + + } + /> + } + /> {translate( 'auto.components.browser.pane.BrowserPane.95af781091', @@ -5663,12 +5688,7 @@ function BrowserPagePane({ )} - + - - - + + + + } + /> {translate( 'auto.components.browser.pane.BrowserPane.11c5084aa2', @@ -5771,30 +5793,32 @@ function BrowserPagePane({ } }} > - - - + + ··· + + } + /> { + onClick={() => { const dataUrl = grabToast.payload?.screenshot?.dataUrl if (dataUrl?.startsWith('data:image/png;base64,')) { void window.api.ui.writeClipboardImage(dataUrl) diff --git a/src/renderer/src/components/browser-pane/browser-toolbar-menu-dropdown.tsx b/src/renderer/src/components/browser-pane/browser-toolbar-menu-dropdown.tsx index b34453ba3e3..d73274c4ef8 100644 --- a/src/renderer/src/components/browser-pane/browser-toolbar-menu-dropdown.tsx +++ b/src/renderer/src/components/browser-pane/browser-toolbar-menu-dropdown.tsx @@ -59,26 +59,28 @@ export function BrowserToolbarMenuDropdown({ }: BrowserToolbarMenuDropdownProps): React.JSX.Element { return ( - - - + + + + } + /> {allProfiles.map((profile) => { const isSelectedProfile = profile.id === effectiveProfileId return ( onSwitchProfile(profile.id === 'default' ? null : profile.id)} + onClick={() => onSwitchProfile(profile.id === 'default' ? null : profile.id)} > - + {translate('auto.components.browser.pane.BrowserToolbarMenu.cf7cdc67ef', 'New Profile…')} @@ -142,7 +144,7 @@ export function BrowserToolbarMenuDropdown({ {browser.profiles.map((profile) => ( onImportFromBrowser(browser.family, profile.directory)} + onClick={() => onImportFromBrowser(browser.family, profile.directory)} > {profile.name} @@ -153,7 +155,7 @@ export function BrowserToolbarMenuDropdown({ ) : ( onImportFromBrowser(browser.family)} + onClick={() => onImportFromBrowser(browser.family)} > {translate( 'auto.components.browser.pane.BrowserToolbarMenu.eb280bfb11', @@ -164,7 +166,7 @@ export function BrowserToolbarMenuDropdown({ ) )} {detectedBrowsers.length > 0 && } - + {translate( 'auto.components.browser.pane.BrowserToolbarMenu.56f94f4ffa', 'From File…' @@ -217,7 +219,7 @@ export function BrowserToolbarMenuDropdown({ { + onClick={() => { useAppStore.getState().openSettingsTarget({ pane: 'browser', repoId: null }) useAppStore.getState().openSettingsPage() }} diff --git a/src/renderer/src/components/browser-pane/markup/MarkupDrawButton.tsx b/src/renderer/src/components/browser-pane/markup/MarkupDrawButton.tsx index ec700170eb7..2e2065593f7 100644 --- a/src/renderer/src/components/browser-pane/markup/MarkupDrawButton.tsx +++ b/src/renderer/src/components/browser-pane/markup/MarkupDrawButton.tsx @@ -97,12 +97,16 @@ export function MarkupDrawButton({ }} > - - - {/* Why: wrap in a span so the trigger still fires when disabled. */} - {button} - - + {button} + } + /> + } + /> {label} @@ -112,10 +116,8 @@ export function MarkupDrawButton({ align="center" sideOffset={6} className="w-72 p-3" - onOpenAutoFocus={(event) => { - // Why: don't steal focus from the address bar / webview on first show. - event.preventDefault() - }} + // Why: don't steal focus from the address bar / webview on first show. + initialFocus={false} >
diff --git a/src/renderer/src/components/browser-pane/markup/MarkupToolbar.tsx b/src/renderer/src/components/browser-pane/markup/MarkupToolbar.tsx index ee616b0ce28..5129e7be0fb 100644 --- a/src/renderer/src/components/browser-pane/markup/MarkupToolbar.tsx +++ b/src/renderer/src/components/browser-pane/markup/MarkupToolbar.tsx @@ -95,7 +95,7 @@ export const MarkupToolbar = React.memo(function MarkupToolbar({ onClear }: MarkupToolbarProps) { return ( - +
{toolItems().map((item) => ( - - - - - + + + + } + /> + } + /> {translate('auto.components.browser-pane.markup.style', 'Color and thickness')} @@ -178,23 +182,27 @@ export const MarkupToolbar = React.memo(function MarkupToolbar({ - - - - - + + + {fontSize} + + } + /> + } + /> {translate('auto.components.browser-pane.markup.fontSize', 'Font size')} @@ -272,19 +280,21 @@ function IconButton({ }: IconButtonProps): React.JSX.Element { return ( - - - + + {children} + + } + /> {label} diff --git a/src/renderer/src/components/dashboard/DashboardAgentRow.tsx b/src/renderer/src/components/dashboard/DashboardAgentRow.tsx index 1a84a985ba5..d593723e9e8 100644 --- a/src/renderer/src/components/dashboard/DashboardAgentRow.tsx +++ b/src/renderer/src/components/dashboard/DashboardAgentRow.tsx @@ -323,14 +323,16 @@ const DashboardAgentRow = React.memo(function DashboardAgentRow({ gives the glyph enough presence for the leading slot without overpowering the prompt text. */} - - - - - + + + + } + /> {dotTooltipLabel} diff --git a/src/renderer/src/components/diff-comments/useDiffCommentDecorator.tsx b/src/renderer/src/components/diff-comments/useDiffCommentDecorator.tsx index 3bcb4366e89..0786723ac92 100644 --- a/src/renderer/src/components/diff-comments/useDiffCommentDecorator.tsx +++ b/src/renderer/src/components/diff-comments/useDiffCommentDecorator.tsx @@ -518,7 +518,7 @@ export function useDiffCommentDecorator({ root.render( // Why: Monaco view zones are separate React roots outside the app root, // so context providers from App.tsx do not reach tooltip-using actions. - +
- - - + 0 && 'border-foreground/30 text-foreground')} + > + + + } + />
{translate( diff --git a/src/renderer/src/components/editor/CombinedDiffViewer.tsx b/src/renderer/src/components/editor/CombinedDiffViewer.tsx index 9ae116de477..2b4c6303cd4 100644 --- a/src/renderer/src/components/editor/CombinedDiffViewer.tsx +++ b/src/renderer/src/components/editor/CombinedDiffViewer.tsx @@ -1629,14 +1629,16 @@ export default function CombinedDiffViewer({
{commitCompare.subject && ( - -
- {commitCompare.subject} -
-
+ + {commitCompare.subject} +
+ } + /> {commitCompare.subject} @@ -1769,20 +1771,22 @@ export default function CombinedDiffViewer({
{fileTreeCollapsed && ( - - - + setFileTreeCollapsed(false)} + > + + + } + /> {translate( 'auto.components.editor.CombinedDiffViewer.b6c3b84476', @@ -1812,31 +1816,33 @@ export default function CombinedDiffViewer({ {diffCommentCount > 0 && (
- - - + > + + + {translate( + 'auto.components.editor.CombinedDiffViewer.bb84b4c374', + 'AI notes' + )} + + + {diffCommentCount} + + + } + /> 0 && (
- - - + conflictNavigation.onJump('previous')} + > + + + } + /> {translate( 'auto.components.editor.ConflictComponents.41d9af2e7a', @@ -135,20 +137,22 @@ export function ConflictBanner({ - - - + conflictNavigation.onJump('next')} + > + + + } + /> {translate('auto.components.editor.ConflictComponents.9c2901ef8a', 'Next conflict')} @@ -298,20 +302,22 @@ export function ConflictReviewPanel({
{fileTreeCollapsed && ( - - - + setFileTreeCollapsed(false)} + > + + + } + /> {translate( 'auto.components.editor.ConflictComponents.c8ca989aea', diff --git a/src/renderer/src/components/editor/EditorPanelHeader.tsx b/src/renderer/src/components/editor/EditorPanelHeader.tsx index c9f8c6e4d1b..182087ed2e1 100644 --- a/src/renderer/src/components/editor/EditorPanelHeader.tsx +++ b/src/renderer/src/components/editor/EditorPanelHeader.tsx @@ -110,21 +110,23 @@ export function EditorPanelHeader({ onOpenContainingFolder={onOpenContainingFolder} /> {canOpenPreviewToSide && ( - + - - - + + + + } + /> {translate( 'auto.components.editor.EditorPanelHeader.fb8331694e', @@ -135,22 +137,24 @@ export function EditorPanelHeader({ )} {isSingleDiff && ( - + - - - + onOpenDiffTargetFile(isMarkdown ? 'rich' : undefined)} + aria-label={translate( + 'auto.components.editor.EditorPanelHeader.a10d9b8337', + 'Open file' + )} + disabled={!openFileState.canOpen} + > + + + } + /> {openFileState.canOpen ? isMarkdown @@ -186,17 +190,19 @@ export function EditorPanelHeader({ {isDiffSurface && ( // Why: the adjacent diff controls use the same tooltip timing, so they // share one provider instead of creating redundant Radix contexts. - + - - - + + {sideBySide ? : } + + } + /> {sideBySide ? translate( @@ -210,20 +216,22 @@ export function EditorPanelHeader({ - - - + + + + } + /> {translate('auto.components.editor.EditorPanelHeader.2076ecfc9c', 'Previous change')} {previousChangeShortcut.keys.length > 0 && ( @@ -236,20 +244,22 @@ export function EditorPanelHeader({ - - - + + + + } + /> {translate('auto.components.editor.EditorPanelHeader.631dab0df3', 'Next change')} {nextChangeShortcut.keys.length > 0 && ( @@ -274,27 +284,29 @@ export function EditorPanelHeader({ /> )} {canShowMarkdownTableOfContents && ( - + - - - + + + + } + /> {isMarkdownTableOfContentsDisabled ? translate( diff --git a/src/renderer/src/components/editor/EditorPanelHeaderPath.tsx b/src/renderer/src/components/editor/EditorPanelHeaderPath.tsx index f1d0ae5e7b6..a33c717416a 100644 --- a/src/renderer/src/components/editor/EditorPanelHeaderPath.tsx +++ b/src/renderer/src/components/editor/EditorPanelHeaderPath.tsx @@ -127,29 +127,32 @@ export function EditorPanelHeaderPath({
- - - + + + + } + /> {isDiffSurface ? ( <> @@ -74,10 +76,11 @@ export function EditorPanelMarkdownActionsMenu({ {canShowMarkdownFrontmatterToggle ? ( <> { + onClick={(event) => { event.preventDefault() onToggleMarkdownFrontmatter() }} + closeOnClick={false} > {markdownFrontmatterVisible ? translate( @@ -96,7 +99,7 @@ export function EditorPanelMarkdownActionsMenu({ {translate( 'auto.components.editor.EditorPanelMarkdownActionsMenu.3e0ce48c24', diff --git a/src/renderer/src/components/editor/EditorViewToggle.tsx b/src/renderer/src/components/editor/EditorViewToggle.tsx index 2a097af088a..62387d8751c 100644 --- a/src/renderer/src/components/editor/EditorViewToggle.tsx +++ b/src/renderer/src/components/editor/EditorViewToggle.tsx @@ -104,16 +104,16 @@ export default function EditorViewToggle({ // control to repaint when the active language changes. useTranslation() return ( - + { - if (v) { - onChange(v as EditorToggleValue) + const next = v[0] + if (next) { + onChange(next as EditorToggleValue) } }} > @@ -130,15 +130,17 @@ export default function EditorViewToggle({ const tooltipLabel = metadata.title ?? metadata.label return ( - - - - - + + + + } + /> {tooltipLabel} diff --git a/src/renderer/src/components/editor/IpynbViewer.tsx b/src/renderer/src/components/editor/IpynbViewer.tsx index fc19ca04175..b858c4af795 100644 --- a/src/renderer/src/components/editor/IpynbViewer.tsx +++ b/src/renderer/src/components/editor/IpynbViewer.tsx @@ -281,19 +281,21 @@ function NotebookHeaderButton({ }): React.JSX.Element { return ( - - - + + {children} + + } + /> {label} diff --git a/src/renderer/src/components/editor/MonacoGutterContextMenu.tsx b/src/renderer/src/components/editor/MonacoGutterContextMenu.tsx index 81fc40367de..4dbd56ed2ed 100644 --- a/src/renderer/src/components/editor/MonacoGutterContextMenu.tsx +++ b/src/renderer/src/components/editor/MonacoGutterContextMenu.tsx @@ -32,17 +32,19 @@ export function MonacoGutterContextMenu({ }: MonacoGutterContextMenuProps): React.JSX.Element { return ( - - - - + + {actionLabel ? {actionLabel} : null} + + } + /> + } + /> {hasDeliverableNotes ? ENABLED_SEND_TOOLTIP : disabledTooltip} - + {scopes.length > 1 ? ( <> @@ -237,18 +246,16 @@ export function NotesSendMenu({ ) } -function preventAgentSendTargetOutsideDismiss(event: CustomEvent<{ originalEvent: Event }>) { - const target = event.detail.originalEvent.target +function shouldKeepAgentSendMenuOpen(event: Event | undefined): boolean { + const target = event?.target if (!(target instanceof Element)) { - return + return false } - if ( + return Boolean( target.closest( '[data-agent-send-target="eligible"], [data-agent-send-target="disabled"], [data-agent-send-target="sending"]' ) - ) { - event.preventDefault() - } + ) } function NoteScopeMenuRow({ label, count }: { label: string; count: number }): React.JSX.Element { diff --git a/src/renderer/src/components/editor/ReviewNotesSendMenuContent.tsx b/src/renderer/src/components/editor/ReviewNotesSendMenuContent.tsx index 39220edb95b..387ab87ab53 100644 --- a/src/renderer/src/components/editor/ReviewNotesSendMenuContent.tsx +++ b/src/renderer/src/components/editor/ReviewNotesSendMenuContent.tsx @@ -251,7 +251,7 @@ function AgentTargetMenuItem({ return ( onSend(target)} + onClick={() => onSend(target)} // Why: surface the ineligibility reason (permission/stale/no-terminal) as a // hover tooltip rather than inline text, matching DashboardAgentRow's // title-attribute treatment of the same disabledReason. diff --git a/src/renderer/src/components/editor/RichMarkdownLinkBubble.tsx b/src/renderer/src/components/editor/RichMarkdownLinkBubble.tsx index aafffc60376..f165709b1ea 100644 --- a/src/renderer/src/components/editor/RichMarkdownLinkBubble.tsx +++ b/src/renderer/src/components/editor/RichMarkdownLinkBubble.tsx @@ -385,19 +385,21 @@ function LinkBubbleAction({ }): React.JSX.Element { return ( - - - + + {children} + + } + /> {label} diff --git a/src/renderer/src/components/editor/RichMarkdownToolbar.tsx b/src/renderer/src/components/editor/RichMarkdownToolbar.tsx index 18dbb20695c..ef93c2a42dc 100644 --- a/src/renderer/src/components/editor/RichMarkdownToolbar.tsx +++ b/src/renderer/src/components/editor/RichMarkdownToolbar.tsx @@ -46,18 +46,22 @@ function RichMarkdownMoreBlocksMenu({ editor }: { editor: Editor | null }): Reac - - - - - + event.preventDefault()} + > + + + } + /> + } + /> {label} @@ -67,20 +71,16 @@ function RichMarkdownMoreBlocksMenu({ editor }: { editor: Editor | null }): Reac {translate('auto.components.editor.RichMarkdownToolbar.2cd9e0bbb3', 'Headings')} - editor?.chain().focus().toggleHeading({ level: 4 }).run()} - > + editor?.chain().focus().toggleHeading({ level: 4 }).run()}> {translate('auto.components.editor.RichMarkdownToolbar.b05e14620d', 'Heading 4')} - editor?.chain().focus().toggleHeading({ level: 5 }).run()} - > + editor?.chain().focus().toggleHeading({ level: 5 }).run()}> {translate('auto.components.editor.RichMarkdownToolbar.6bbf827ef5', 'Heading 5')} - (editor ? insertToggle(editor) : undefined)}> + (editor ? insertToggle(editor) : undefined)}> {translate( 'auto.components.editor.RichMarkdownToolbar.d1bbf9a835', diff --git a/src/renderer/src/components/editor/RichMarkdownToolbarButton.tsx b/src/renderer/src/components/editor/RichMarkdownToolbarButton.tsx index 2234e044eb8..567a731e733 100644 --- a/src/renderer/src/components/editor/RichMarkdownToolbarButton.tsx +++ b/src/renderer/src/components/editor/RichMarkdownToolbarButton.tsx @@ -18,17 +18,19 @@ export function RichMarkdownToolbarButton({ return ( - - - + event.preventDefault()} + onClick={onClick} + > + {children} + + } + /> {label} diff --git a/src/renderer/src/components/editor/UntitledFileRenameDialog.tsx b/src/renderer/src/components/editor/UntitledFileRenameDialog.tsx index e9e6d9c018c..0c94186eee9 100644 --- a/src/renderer/src/components/editor/UntitledFileRenameDialog.tsx +++ b/src/renderer/src/components/editor/UntitledFileRenameDialog.tsx @@ -122,14 +122,16 @@ export function UntitledFileRenameDialog({ { - event.preventDefault() + initialFocus={() => { cancelFocusFrame() focusFrameRef.current = requestAnimationFrame(() => { focusFrameRef.current = null nameInputRef.current?.focus() nameInputRef.current?.select() }) + // Why: return false so Base UI skips its own auto-focus; the rAF + // above focuses and selects the name input instead. + return false }} > diff --git a/src/renderer/src/components/emulator-pane/MobileEmulatorAgentSetupGuideSteps.tsx b/src/renderer/src/components/emulator-pane/MobileEmulatorAgentSetupGuideSteps.tsx index f169591d5e1..981b0cc282c 100644 --- a/src/renderer/src/components/emulator-pane/MobileEmulatorAgentSetupGuideSteps.tsx +++ b/src/renderer/src/components/emulator-pane/MobileEmulatorAgentSetupGuideSteps.tsx @@ -94,29 +94,31 @@ export function MobileEmulatorAgentSetupGuideSteps({

{setup.cliInstallStatus.detail}

) : null}
- + - - - - - + + + + } + /> {!setup.cliSupported && !setup.cliLoading && setup.cliInstallStatus?.detail ? ( {setup.cliInstallStatus.detail} diff --git a/src/renderer/src/components/emulator-pane/MobileEmulatorTabIntroCallout.tsx b/src/renderer/src/components/emulator-pane/MobileEmulatorTabIntroCallout.tsx index e97bdaab1da..66ec524e88a 100644 --- a/src/renderer/src/components/emulator-pane/MobileEmulatorTabIntroCallout.tsx +++ b/src/renderer/src/components/emulator-pane/MobileEmulatorTabIntroCallout.tsx @@ -47,21 +47,23 @@ export function MobileEmulatorTabIntroCallout(): React.JSX.Element { )} - - - + + + + } + /> {translate( 'auto.components.emulator.pane.MobileEmulatorTabIntroCallout.1924982130', diff --git a/src/renderer/src/components/emulator-pane/emulator-pane-toolbar.tsx b/src/renderer/src/components/emulator-pane/emulator-pane-toolbar.tsx index 764a9e579d7..4b1d214ef56 100644 --- a/src/renderer/src/components/emulator-pane/emulator-pane-toolbar.tsx +++ b/src/renderer/src/components/emulator-pane/emulator-pane-toolbar.tsx @@ -61,7 +61,7 @@ export function EmulatorPaneToolbar({
- - - + > + + + {translate( + 'auto.components.emulator.pane.emulator.pane.toolbar.6bd8dff42a', + 'Rotate' + )} + + + } + /> {translate('auto.components.emulator.pane.emulator.pane.toolbar.6bd8dff42a', 'Rotate')} - - - - - {translate('auto.components.emulator.pane.emulator.pane.toolbar.e7a0d1897e', 'Home')} - - - {isLive ? ( - - + - + - + } + /> + + {translate('auto.components.emulator.pane.emulator.pane.toolbar.e7a0d1897e', 'Home')} + + + {isLive ? ( + + + + + } + /> {translate( 'auto.components.emulator.pane.emulator.pane.toolbar.06e10d7356', diff --git a/src/renderer/src/components/feature-tips/CliSkillSetupTerminal.tsx b/src/renderer/src/components/feature-tips/CliSkillSetupTerminal.tsx index b9e9f1fac88..b149aa355e6 100644 --- a/src/renderer/src/components/feature-tips/CliSkillSetupTerminal.tsx +++ b/src/renderer/src/components/feature-tips/CliSkillSetupTerminal.tsx @@ -35,20 +35,22 @@ export function CliSkillSetupTerminal(): React.JSX.Element { {ORCA_CLI_ORCHESTRATION_SKILL_INSTALL_COMMAND} - - - + void handleCopySkillCommand()} + aria-label={translate( + 'auto.components.feature.tips.CliSkillSetupTerminal.5eca672aac', + 'Copy skill install command' + )} + > + + + } + /> {translate( 'auto.components.feature.tips.CliSkillSetupTerminal.5c3aee22c0', diff --git a/src/renderer/src/components/feature-tips/CmdJPaletteTipDialog.tsx b/src/renderer/src/components/feature-tips/CmdJPaletteTipDialog.tsx index 427bb947ff0..c460ebf12eb 100644 --- a/src/renderer/src/components/feature-tips/CmdJPaletteTipDialog.tsx +++ b/src/renderer/src/components/feature-tips/CmdJPaletteTipDialog.tsx @@ -53,9 +53,9 @@ export function CmdJPaletteTipDialog({ event.preventDefault()} + initialFocus={false} >
diff --git a/src/renderer/src/components/feature-wall/AiCommitPrSettingsFields.tsx b/src/renderer/src/components/feature-wall/AiCommitPrSettingsFields.tsx index 52460dbadbc..5c171854b58 100644 --- a/src/renderer/src/components/feature-wall/AiCommitPrSettingsFields.tsx +++ b/src/renderer/src/components/feature-wall/AiCommitPrSettingsFields.tsx @@ -51,7 +51,7 @@ export function AiCommitPrSettingsFields({ - v && onAgentChange(v)}> - + {listCommitMessageAgentCapabilities().map((capability) => ( @@ -130,11 +134,15 @@ export function AiCommitPrSettingsFields({ - v && onModelChange(v)}> - + {activeCapability.models.map((model) => ( {model.label} @@ -153,11 +161,15 @@ export function AiCommitPrSettingsFields({ 'Thinking effort' )} - v && onThinkingChange(v)}> - + {activeModel.thinkingLevels.map((level) => ( {level.label} diff --git a/src/renderer/src/components/floating-terminal/FloatingTerminalIconContextMenu.tsx b/src/renderer/src/components/floating-terminal/FloatingTerminalIconContextMenu.tsx index 8947efc01f3..1c7e4f0c7c3 100644 --- a/src/renderer/src/components/floating-terminal/FloatingTerminalIconContextMenu.tsx +++ b/src/renderer/src/components/floating-terminal/FloatingTerminalIconContextMenu.tsx @@ -89,18 +89,20 @@ export function FloatingTerminalIconContextMenu({ {children} - - - + : showAttentionDot + ? // Why: announce pending activity to assistive tech; the dot + // itself is aria-hidden decoration. + translate( + 'auto.components.floating.terminal.FloatingTerminalToggleButton.4cb418b991', + 'Show floating workspace, new activity' + ) + : translate( + 'auto.components.floating.terminal.FloatingTerminalToggleButton.3b04b065b5', + 'Show floating workspace' + ) + } + aria-pressed={open} + onPointerDown={handlePointerDown} + onPointerMove={handlePointerMove} + onPointerUp={handlePointerEnd} + onPointerCancel={handlePointerEnd} + onClick={handleClick} + > + + {showAttentionDot ? ( + // Why: amber matches Orca's "needs attention / unread" convention + // (the tab-unread bell); the ring matches the button fill so the + // dot reads on both light (bg-card) and dark (dark:bg-accent). + + ) : null} + + } + /> {translate( 'auto.components.floating.terminal.FloatingTerminalToggleButton.bfe7809a70', diff --git a/src/renderer/src/components/floating-terminal/FloatingTerminalWindowControls.tsx b/src/renderer/src/components/floating-terminal/FloatingTerminalWindowControls.tsx index 03213e9820e..704ae295aad 100644 --- a/src/renderer/src/components/floating-terminal/FloatingTerminalWindowControls.tsx +++ b/src/renderer/src/components/floating-terminal/FloatingTerminalWindowControls.tsx @@ -126,22 +126,24 @@ export function FloatingTerminalWindowControls({
{defaultAgent ? ( - - - + + + + } + /> {translate( 'auto.components.floating.terminal.FloatingTerminalWindowControls.648352c51f', @@ -152,29 +154,31 @@ export function FloatingTerminalWindowControls({ ) : null} - - - + + {maximized ? : } + + } + /> {maximized ? withShortcutHint( @@ -194,21 +198,23 @@ export function FloatingTerminalWindowControls({ - - - + + + + } + /> {withShortcutHint( translate( diff --git a/src/renderer/src/components/github-project/ProjectCell.tsx b/src/renderer/src/components/github-project/ProjectCell.tsx index b6875672697..ace03dd4916 100644 --- a/src/renderer/src/components/github-project/ProjectCell.tsx +++ b/src/renderer/src/components/github-project/ProjectCell.tsx @@ -397,18 +397,20 @@ function IssueTypeCell({ return ( - - - + + {trigger} + + } + /> {!owner || !repo ? (
@@ -511,19 +513,21 @@ function SingleSelectCell({ } return ( - - - + + {label ?? ( + + )} + + } + /> {options.map((o) => ( - + + {label ?? ( + + )} + + } + /> {completed.length > 0 ? (
@@ -865,24 +871,26 @@ function AssigneesCell({ return ( - - - + + {labelContent ?? ( + + )} + + } + /> {!owner || !repo ? (
@@ -956,22 +964,27 @@ function LabelsCell({ return ( - - - + + {labelContent ?? ( + + )} + + } + /> {!owner || !repo ? (
diff --git a/src/renderer/src/components/github-project/ProjectItemSlugDialog.tsx b/src/renderer/src/components/github-project/ProjectItemSlugDialog.tsx index 2b778e29c10..d324ce84789 100644 --- a/src/renderer/src/components/github-project/ProjectItemSlugDialog.tsx +++ b/src/renderer/src/components/github-project/ProjectItemSlugDialog.tsx @@ -8,7 +8,6 @@ // simplified surface; it also routes every write through slug-addressed // mutation helpers and patches the Project table cache on success. import React from 'react' -import { VisuallyHidden } from 'radix-ui' import { Sheet, SheetContent, SheetDescription, SheetTitle } from '@/components/ui/sheet' import type { GitHubItemDialogProjectOrigin } from '@/components/GitHubItemDialog' import { SlugDialogBody } from './slug-dialog/SlugDialogBody' @@ -34,24 +33,20 @@ export default function ProjectItemSlugDialog({ side="right" showCloseButton={false} className="flex w-full flex-col gap-0 overflow-hidden p-0 sm:max-w-[720px] lg:max-w-[860px]" - onOpenAutoFocus={(e) => e.preventDefault()} + initialFocus={false} > - - - {translate( - 'auto.components.github.project.ProjectItemSlugDialog.4450efea9c', - 'GitHub item' - )} - - - - - {translate( - 'auto.components.github.project.ProjectItemSlugDialog.e55a5c4e68', - 'Project row preview.' - )} - - + + {translate( + 'auto.components.github.project.ProjectItemSlugDialog.4450efea9c', + 'GitHub item' + )} + + + {translate( + 'auto.components.github.project.ProjectItemSlugDialog.e55a5c4e68', + 'Project row preview.' + )} + {projectOrigin ? ( - - - + + {buttonLabel} + + + } + /> {viewPickFor ? ( {row.content.url ? ( - - - + + + + } + /> {translate('auto.components.github.project.ProjectRow.e12be8b4d4', 'Open in GitHub')} @@ -138,19 +140,21 @@ export default function ProjectRow({ ) : null} {!disabled && row.itemType !== 'DRAFT_ISSUE' && row.content.number != null ? ( - - - + + + + } + /> {translate('auto.components.github.project.ProjectRow.75b5d816e3', 'Start work')} @@ -162,8 +166,8 @@ export default function ProjectRow({ if (draftBody) { return ( - - {rowInner} + + - - - + + + + } + />
{translate('auto.components.github.project.ProjectViewList.989f81dc2a', 'Columns')} diff --git a/src/renderer/src/components/github-project/ProjectViewWrapper.tsx b/src/renderer/src/components/github-project/ProjectViewWrapper.tsx index 09578f05c2f..caad77158b0 100644 --- a/src/renderer/src/components/github-project/ProjectViewWrapper.tsx +++ b/src/renderer/src/components/github-project/ProjectViewWrapper.tsx @@ -1249,20 +1249,24 @@ function ViewTabStrip({ } const unsupportedMessage = `Orca doesn't support ${layoutLabel} project views yet.` return ( - - - - {tab} - - + + + {tab} + + } + />

diff --git a/src/renderer/src/components/github-project/slug-dialog/AssigneesEditor.tsx b/src/renderer/src/components/github-project/slug-dialog/AssigneesEditor.tsx index ded043e3d68..ede4e43c8e3 100644 --- a/src/renderer/src/components/github-project/slug-dialog/AssigneesEditor.tsx +++ b/src/renderer/src/components/github-project/slug-dialog/AssigneesEditor.tsx @@ -33,24 +33,26 @@ export function AssigneesEditor({ ) return ( !disabled && setOpen(o)}> - - - + + {translate( + 'auto.components.github.project.slug.dialog.AssigneesEditor.98914e6b36', + 'Assignees:' + )} + {selected.length === 0 + ? translate( + 'auto.components.github.project.slug.dialog.AssigneesEditor.94a4e6e4fa', + 'none' + ) + : selected.join(', ')} + + } + /> {metadata.loading ? (

diff --git a/src/renderer/src/components/github-project/slug-dialog/LabelsEditor.tsx b/src/renderer/src/components/github-project/slug-dialog/LabelsEditor.tsx index 1c87fc3fd28..0546ca8232b 100644 --- a/src/renderer/src/components/github-project/slug-dialog/LabelsEditor.tsx +++ b/src/renderer/src/components/github-project/slug-dialog/LabelsEditor.tsx @@ -24,24 +24,26 @@ export function LabelsEditor({ const metadata = useRepoLabelsBySlug(open ? owner : null, open ? repo : null, sourceSettings) return ( !disabled && setOpen(o)}> - - - + + {translate( + 'auto.components.github.project.slug.dialog.LabelsEditor.a7b182fcda', + 'Labels:' + )} + {selected.length === 0 + ? translate( + 'auto.components.github.project.slug.dialog.LabelsEditor.1a5366b5be', + 'none' + ) + : selected.join(', ')} + + } + /> {metadata.loading ? (
diff --git a/src/renderer/src/components/github/IssueSourceSelector.tsx b/src/renderer/src/components/github/IssueSourceSelector.tsx index e428d53f525..c46ce85ba93 100644 --- a/src/renderer/src/components/github/IssueSourceSelector.tsx +++ b/src/renderer/src/components/github/IssueSourceSelector.tsx @@ -176,7 +176,7 @@ export default function IssueSourceSelector({ return ( - {group} + {translate('auto.components.github.IssueSourceSelector.d6aeb2012b', 'Showing issues from')}{' '} {effective === 'upstream' ? upstreamSlug : originSlug} diff --git a/src/renderer/src/components/github/PRFilterDropdowns.tsx b/src/renderer/src/components/github/PRFilterDropdowns.tsx index 6085b8e750e..413d29848e9 100644 --- a/src/renderer/src/components/github/PRFilterDropdowns.tsx +++ b/src/renderer/src/components/github/PRFilterDropdowns.tsx @@ -169,26 +169,28 @@ export default function PRFilterDropdowns({ } }} > - - - + 0 && 'border-border' + )} + > + + {translate('auto.components.github.PRFilterDropdowns.79c54552f7', 'Filters')} + {activeCount > 0 ? ( + + {activeCount} + + ) : null} + + } + /> {openSection === null ? ( - - - + + + {translate( + 'auto.components.linear-issue-attribute-filter-dropdowns.filters', + 'Filters' + )} + {activeCount > 0 ? ( + + {activeCount} + + ) : null} + + } + /> {isAllWorkspaces ? (
diff --git a/src/renderer/src/components/linear-project-view-surfaces.tsx b/src/renderer/src/components/linear-project-view-surfaces.tsx index 58970a8215a..fe63cb9a4f5 100644 --- a/src/renderer/src/components/linear-project-view-surfaces.tsx +++ b/src/renderer/src/components/linear-project-view-surfaces.tsx @@ -367,46 +367,50 @@ export function LinearProjectTable({
{onUseProjectIssues ? ( - + { + event.stopPropagation() + onUseProjectIssues(project) + }} + aria-label={translate( + 'auto.components.linear.project.view.surfaces.7616c986c6', + 'Open {{value0}} issues', + { value0: project.name } + )} + > + + + } + /> + + {translate('auto.components.linear.project.view.surfaces.ee3d2caabd', 'Issues')} + + + ) : null} + + { event.stopPropagation() - onUseProjectIssues(project) + onOpenProject(project) }} aria-label={translate( 'auto.components.linear.project.view.surfaces.7616c986c6', - 'Open {{value0}} issues', + 'Open {{value0}} in Linear', { value0: project.name } )} > - + - - - {translate('auto.components.linear.project.view.surfaces.ee3d2caabd', 'Issues')} - - - ) : null} - - - - + } + /> {translate( 'auto.components.linear.project.view.surfaces.aac9a4afc6', @@ -540,23 +544,25 @@ export function LinearCustomViewTable({
- - - + { + event.stopPropagation() + onOpenView(view) + }} + aria-label={translate( + 'auto.components.linear.project.view.surfaces.7616c986c6', + 'Open {{value0}} in Linear', + { value0: view.name } + )} + > + + + } + /> {translate( 'auto.components.linear.project.view.surfaces.aac9a4afc6', diff --git a/src/renderer/src/components/linear-scope-selector.tsx b/src/renderer/src/components/linear-scope-selector.tsx index 26bfb33df2e..2eee8a8554b 100644 --- a/src/renderer/src/components/linear-scope-selector.tsx +++ b/src/renderer/src/components/linear-scope-selector.tsx @@ -234,21 +234,23 @@ export function LinearScopeSelector({ return ( - - - + + {triggerLabel} + + + } + />
- - - + + {sidebarToggleLabel} + + } + /> {sidebarToggleTooltip} @@ -49,20 +51,22 @@ export function MobilePageToolbar({
- - - + + + + } + /> {translate('auto.components.mobile.MobilePageToolbar.ad2284a9e2', 'Close · Esc')} diff --git a/src/renderer/src/components/native-chat/NativeChatComposerActions.tsx b/src/renderer/src/components/native-chat/NativeChatComposerActions.tsx index fb67917b332..a41c8fcd264 100644 --- a/src/renderer/src/components/native-chat/NativeChatComposerActions.tsx +++ b/src/renderer/src/components/native-chat/NativeChatComposerActions.tsx @@ -38,64 +38,68 @@ export function NativeChatComposerActions({ return (
- - - - - {translate('components.native-chat.composer.attach', 'Attach file')} - - -
- - + { - if (!isDictationHoldMode || dictationDisabled) { - return - } - event.preventDefault() - onDictationHoldStart() - }} - onPointerUp={() => { - if (isDictationHoldMode && !dictationDisabled) { - onDictationHoldEnd() - } - }} - onPointerCancel={() => { - if (isDictationHoldMode && !dictationDisabled) { - onDictationHoldEnd() - } - }} - onPointerLeave={(event) => { - if (isDictationHoldMode && event.buttons === 1 && !dictationDisabled) { - onDictationHoldEnd() - } - }} + aria-label={translate('components.native-chat.composer.attach', 'Attach file')} + disabled={attachDisabled} + onClick={onAttach} className="pointer-coarse:size-11" > - {isDictating ? ( - - ) : ( - - )} + - + } + /> + + {translate('components.native-chat.composer.attach', 'Attach file')} + + +
+ + { + if (!isDictationHoldMode || dictationDisabled) { + return + } + event.preventDefault() + onDictationHoldStart() + }} + onPointerUp={() => { + if (isDictationHoldMode && !dictationDisabled) { + onDictationHoldEnd() + } + }} + onPointerCancel={() => { + if (isDictationHoldMode && !dictationDisabled) { + onDictationHoldEnd() + } + }} + onPointerLeave={(event) => { + if (isDictationHoldMode && event.buttons === 1 && !dictationDisabled) { + onDictationHoldEnd() + } + }} + className="pointer-coarse:size-11" + > + {isDictating ? ( + + ) : ( + + )} + + } + /> {dictationLabel} diff --git a/src/renderer/src/components/native-chat/use-native-chat-context-menu.tsx b/src/renderer/src/components/native-chat/use-native-chat-context-menu.tsx index affaa63ab8d..d8db7f13567 100644 --- a/src/renderer/src/components/native-chat/use-native-chat-context-menu.tsx +++ b/src/renderer/src/components/native-chat/use-native-chat-context-menu.tsx @@ -134,34 +134,31 @@ export function useNativeChatContextMenu({ onSelectionCapture: rememberCurrentSelection, menu: ( - - - + {placeholder} + )} + + + } + /> { - event.preventDefault() + initialFocus={() => { + // Suppress default focus (return false); focusSearchInput schedules + // the rAF focus onto the search box itself. focusSearchInput() + return false }} > diff --git a/src/renderer/src/components/new-workspace/ProjectHostSetupCombobox.tsx b/src/renderer/src/components/new-workspace/ProjectHostSetupCombobox.tsx index 22df2d5f192..dae5fd28f60 100644 --- a/src/renderer/src/components/new-workspace/ProjectHostSetupCombobox.tsx +++ b/src/renderer/src/components/new-workspace/ProjectHostSetupCombobox.tsx @@ -39,30 +39,32 @@ export default function ProjectHostSetupCombobox({ return ( - - - + + {selected ? ( + + + {selected.label} + + ) : ( + + {translate( + 'auto.components.new.workspace.ProjectHostSetupCombobox.placeholder', + 'Choose host' + )} + + )} + + + } + /> { + (next: boolean, eventDetails: PopoverPrimitive.Root.ChangeEventDetails): void => { + // Why: the input is a PopoverAnchor, not a PopoverTrigger, so Base UI + // treats clicks/focus on it (and the mode tabs) as outside presses. + // Cancel those closes to keep results open — replaces the old + // onPointerDownOutside/onFocusOutside handlers on the content. + if ( + !next && + (eventDetails.reason === 'outside-press' || eventDetails.reason === 'focus-out') + ) { + const target = eventDetails.event.target as Node | null + if ( + target && + (localInputRef.current?.contains(target) || tabsListRef.current?.contains(target)) + ) { + eventDetails.cancel() + return + } + } if (disabled || selectedSource) { setOpen(false) return @@ -1371,7 +1389,7 @@ export default function SmartWorkspaceNameField({ shouldFilter={false} className="overflow-visible bg-transparent" > - +
{selectedSource ? ( // Why: min-w-0 + w-full lets the pill shrink to its flex @@ -1404,21 +1422,23 @@ export default function SmartWorkspaceNameField({ {selectedSource.url ? ( - - - + void window.api.shell.openUrl(selectedSource.url!)} + className="size-6 shrink-0 rounded-sm text-muted-foreground hover:text-foreground" + aria-label={translate( + 'auto.components.new.workspace.SmartWorkspaceNameField.2c69728c2a', + 'Open link in browser' + )} + > + + + } + /> {translate( 'auto.components.new.workspace.SmartWorkspaceNameField.370a1faf67', @@ -1428,21 +1448,23 @@ export default function SmartWorkspaceNameField({ ) : null} - - - + + + + } + /> {translate( 'auto.components.new.workspace.SmartWorkspaceNameField.0c9e668e3a', @@ -1539,28 +1561,9 @@ export default function SmartWorkspaceNameField({ // Why: this popover lives inside the create-workspace dialog; a // taller result list can cover the submit footer while typing. style={{ maxHeight: 'min(var(--radix-popover-content-available-height,7rem),7rem)' }} - onOpenAutoFocus={(event) => event.preventDefault()} - onPointerDownOutside={(event) => { - // Why: the input is a PopoverAnchor, not a PopoverTrigger, so - // Radix treats clicks on it as outside the popover. Keep focus - // clicks and mode-tab clicks from immediately closing results. - const target = event.target as Node - if ( - localInputRef.current?.contains(target) || - tabsListRef.current?.contains(target) - ) { - event.preventDefault() - } - }} - onFocusOutside={(event) => { - const target = event.target as Node - if ( - localInputRef.current?.contains(target) || - tabsListRef.current?.contains(target) - ) { - event.preventDefault() - } - }} + // Why: outside-press/focus-out cancellation now lives on the Popover + // root's onOpenChange (see handleSourcePopoverOpenChange). + initialFocus={false} > {mode === 'gitlab' ? ( // Why: GitLab MR-state filter — Open / Merged / Closed / All — diff --git a/src/renderer/src/components/onboarding/AgentStep.tsx b/src/renderer/src/components/onboarding/AgentStep.tsx index d5e18e8070f..31d6269de07 100644 --- a/src/renderer/src/components/onboarding/AgentStep.tsx +++ b/src/renderer/src/components/onboarding/AgentStep.tsx @@ -225,19 +225,21 @@ function YoloPermissionsControl({ - - - + event.preventDefault()} + className="grid size-6 shrink-0 place-items-center rounded-md text-muted-foreground outline-none transition-colors hover:bg-muted hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50" + > + + + } + /> {translate( 'auto.components.onboarding.AgentStep.yoloPermissionsTooltip', diff --git a/src/renderer/src/components/onboarding/OnboardingFlow.tsx b/src/renderer/src/components/onboarding/OnboardingFlow.tsx index c5546aa95e6..a098e732be6 100644 --- a/src/renderer/src/components/onboarding/OnboardingFlow.tsx +++ b/src/renderer/src/components/onboarding/OnboardingFlow.tsx @@ -173,7 +173,7 @@ export default function OnboardingFlow({ }, [requestSkipConfirmation, skipConfirmOpen]) return ( - +
- - - + + + + + } + /> {translate( 'auto.components.onboarding.RepoStep.e8fdb36338', diff --git a/src/renderer/src/components/onboarding/RepoStepNestedImportPanel.tsx b/src/renderer/src/components/onboarding/RepoStepNestedImportPanel.tsx index e7b24e2b71e..4cf99b76af9 100644 --- a/src/renderer/src/components/onboarding/RepoStepNestedImportPanel.tsx +++ b/src/renderer/src/components/onboarding/RepoStepNestedImportPanel.tsx @@ -49,26 +49,28 @@ export function RepoStepNestedImportPanel({
{nestedScanInProgress ? ( - - - + + + + + } + /> {translate( 'auto.components.onboarding.RepoStep.e8fdb36338', diff --git a/src/renderer/src/components/orca-profiles/OrcaProfileCloudMenuItems.tsx b/src/renderer/src/components/orca-profiles/OrcaProfileCloudMenuItems.tsx index a21681a48b8..4523511bb36 100644 --- a/src/renderer/src/components/orca-profiles/OrcaProfileCloudMenuItems.tsx +++ b/src/renderer/src/components/orca-profiles/OrcaProfileCloudMenuItems.tsx @@ -77,7 +77,7 @@ export function OrcaProfileCloudMenuItems({ { + onClick={() => { if (organization.orgId !== activeOrgId) { onSelectOrg(organization.orgId) } @@ -103,7 +103,7 @@ export function OrcaProfileCloudMenuItems({ {organizations.map((organization) => ( onCreateProfileForOrg(organization)} + onClick={() => onCreateProfileForOrg(organization)} className="min-w-0" > @@ -120,13 +120,13 @@ export function OrcaProfileCloudMenuItems({ ) : null} {showSignIn ? ( - + {connecting ? : } {getConnectLabel(authStatus, connecting)} ) : null} {activeProfile.kind === 'cloud-linked' ? ( - + {translate('auto.components.orca.profiles.switcher.signout', 'Sign out')} diff --git a/src/renderer/src/components/orca-profiles/OrcaProfileManagementDialog.tsx b/src/renderer/src/components/orca-profiles/OrcaProfileManagementDialog.tsx index 8f1577c7998..23f9571f5fb 100644 --- a/src/renderer/src/components/orca-profiles/OrcaProfileManagementDialog.tsx +++ b/src/renderer/src/components/orca-profiles/OrcaProfileManagementDialog.tsx @@ -74,7 +74,7 @@ function ProjectTransferMenu({ onTransfer(repo, profile, mode)} + onClick={() => onTransfer(repo, profile, mode)} > {mode === 'move' ? : } @@ -86,12 +86,14 @@ function ProjectTransferMenu({ return ( - - - + + {repoPending ? : } + {translate('auto.components.orca.profiles.management.04e7bd2a23', 'Transfer')} + + } + /> {translate('auto.components.orca.profiles.management.128c7dfe64', 'Copy to')} diff --git a/src/renderer/src/components/orca-profiles/OrcaProfileOrgMemberRow.tsx b/src/renderer/src/components/orca-profiles/OrcaProfileOrgMemberRow.tsx index 894e21c8481..98bcbf9993d 100644 --- a/src/renderer/src/components/orca-profiles/OrcaProfileOrgMemberRow.tsx +++ b/src/renderer/src/components/orca-profiles/OrcaProfileOrgMemberRow.tsx @@ -80,9 +80,7 @@ export function OrcaProfileOrgMemberRow({ {neverSignedIn && canManage ? ( - - {roleControl} - + {roleControl}} /> {translate( 'auto.components.orca.profiles.org.members.not.signed.in', diff --git a/src/renderer/src/components/orca-profiles/OrcaProfileSwitcher.tsx b/src/renderer/src/components/orca-profiles/OrcaProfileSwitcher.tsx index f5858d4aa7d..743f56bfa02 100644 --- a/src/renderer/src/components/orca-profiles/OrcaProfileSwitcher.tsx +++ b/src/renderer/src/components/orca-profiles/OrcaProfileSwitcher.tsx @@ -215,47 +215,51 @@ export function OrcaProfileSwitcher({ <> - - - - - + + {sidebarPlacement && switching ? ( + + ) : !multiProfileUi ? ( + + ) : ( + + )} + {!sidebarPlacement ? ( + <> + + {multiProfileUi + ? activeProfile.name + : showAccountIdentity + ? accountIdentity.title + : triggerLabel} + + {switching ? : } + + ) : null} + + } + /> + } + /> {triggerLabel} @@ -286,7 +290,7 @@ export function OrcaProfileSwitcher({ handleSwitchProfile(profile.id)} + onClick={() => handleSwitchProfile(profile.id)} className="min-w-0" > @@ -307,7 +311,7 @@ export function OrcaProfileSwitcher({ setOrgMembersOpen(true)} + onClick={() => setOrgMembersOpen(true)} > {translate( @@ -340,7 +344,7 @@ export function OrcaProfileSwitcher({ { + onClick={() => { setManagementOpen(true) }} > @@ -349,7 +353,7 @@ export function OrcaProfileSwitcher({ { + onClick={() => { setDialogOpen(true) }} > diff --git a/src/renderer/src/components/repo/NestedRepoScanLimitNotice.tsx b/src/renderer/src/components/repo/NestedRepoScanLimitNotice.tsx index 59066a6675b..c6a25a5f0e4 100644 --- a/src/renderer/src/components/repo/NestedRepoScanLimitNotice.tsx +++ b/src/renderer/src/components/repo/NestedRepoScanLimitNotice.tsx @@ -43,31 +43,33 @@ export function NestedRepoScanLimitNotice({ scan }: { scan: NestedRepoScanResult )} - - - + { + event.stopPropagation() + setDetailsOpen(true) + }} + > + + + } + /> event.preventDefault()} + initialFocus={false} > {detailsText} diff --git a/src/renderer/src/components/repo/RepoCombobox.tsx b/src/renderer/src/components/repo/RepoCombobox.tsx index f9fef8ad902..2ac4fe2b0f8 100644 --- a/src/renderer/src/components/repo/RepoCombobox.tsx +++ b/src/renderer/src/components/repo/RepoCombobox.tsx @@ -183,49 +183,54 @@ export default function RepoCombobox({ return (
- - - + + {selectedRepo ? ( + + + {selectedRepo.connectionId && ( + + + {translate('auto.components.repo.RepoCombobox.3639fd9da2', 'SSH')} + + )} + + ) : ( + {placeholder} + )} + + + } + /> { - event.preventDefault() + // Why: return false suppresses Base UI's default popup focus so our + // custom focus of the search input runs instead — mirrors the old + // onOpenAutoFocus preventDefault + focusSearchInput. + initialFocus={() => { focusSearchInput() + return false }} > diff --git a/src/renderer/src/components/repo/repo-fork-indicator.tsx b/src/renderer/src/components/repo/repo-fork-indicator.tsx index 896a5217730..04670b22dbc 100644 --- a/src/renderer/src/components/repo/repo-fork-indicator.tsx +++ b/src/renderer/src/components/repo/repo-fork-indicator.tsx @@ -21,14 +21,16 @@ export function RepoForkIndicator({ const label = `Fork of ${upstream.owner}/${upstream.repo}` return ( - - - - + +
+ } + /> {fullPath} diff --git a/src/renderer/src/components/right-sidebar/AiVaultSessionRow.tsx b/src/renderer/src/components/right-sidebar/AiVaultSessionRow.tsx index 448be77c122..5a9266d99bd 100644 --- a/src/renderer/src/components/right-sidebar/AiVaultSessionRow.tsx +++ b/src/renderer/src/components/right-sidebar/AiVaultSessionRow.tsx @@ -109,99 +109,103 @@ export function VaultSessionRow({ return ( - - + } + /> {showPullRequestMenu && ( - - - + + + + } + /> - + {translate('auto.components.right.sidebar.ChecksPanel.7202f4a40a', 'unlink PR')} - + {translate('auto.components.right.sidebar.ChecksPanel.07871c0589', 'Link another PR')} diff --git a/src/renderer/src/components/right-sidebar/CreateHostedReviewComposer.tsx b/src/renderer/src/components/right-sidebar/CreateHostedReviewComposer.tsx index 9656487a72d..289f5749e4e 100644 --- a/src/renderer/src/components/right-sidebar/CreateHostedReviewComposer.tsx +++ b/src/renderer/src/components/right-sidebar/CreateHostedReviewComposer.tsx @@ -213,11 +213,15 @@ export function CreateHostedReviewComposer({ {aiGenerationEnabled ? ( {!generating && generateDisabled ? ( - - {generateButton} - + + {generateButton} + + } + /> ) : ( - {generateButton} + )} {generateTooltipLabel} @@ -279,27 +283,29 @@ export function CreateHostedReviewComposer({ {showDropdown ? ( - - - + + + + } + /> {effectiveDropdownItems.map((entry, index) => entry.kind === 'separator' ? ( @@ -310,13 +316,14 @@ export function CreateHostedReviewComposer({ disabled={entry.disabled} title={entry.title} variant={entry.variant} - onSelect={(event) => { + onClick={(event) => { if (entry.disabled) { event.preventDefault() return } onDropdownAction(entry.kind) }} + closeOnClick={false} > {entry.label} diff --git a/src/renderer/src/components/right-sidebar/FileExplorerBackgroundMenu.tsx b/src/renderer/src/components/right-sidebar/FileExplorerBackgroundMenu.tsx index 8e7e3218ef1..63609268dd0 100644 --- a/src/renderer/src/components/right-sidebar/FileExplorerBackgroundMenu.tsx +++ b/src/renderer/src/components/right-sidebar/FileExplorerBackgroundMenu.tsx @@ -40,29 +40,31 @@ export function FileExplorerBackgroundMenu({ return ( - - - + {nodeStatus ? ( + + {STATUS_LABELS[nodeStatus]} + + ) : isIgnored ? ( + + ) : null} + + } + /> e.preventDefault()} + finalFocus={false} > - onStartNew('file', targetDir, targetDepth)}> + onStartNew('file', targetDir, targetDepth)}> {translate('auto.components.right.sidebar.FileExplorerRow.37c875d827', 'New File')} - onStartNew('folder', targetDir, targetDepth)}> + onStartNew('folder', targetDir, targetDepth)}> {translate('auto.components.right.sidebar.FileExplorerRow.f61af83316', 'New Folder')} {showCopyFileAction && ( - + {translate('auto.components.right.sidebar.FileExplorerRow.98a79948b3', 'Copy')} )} - onCopyPaths('absolute')}> + onCopyPaths('absolute')}> {selectionSize > 1 ? translate('auto.components.right.sidebar.FileExplorerRow.f9d7ca753d', 'Copy Paths') @@ -679,7 +684,7 @@ export function FileExplorerRow({ {copyPathShortcutLabel} ) : null} - onCopyPaths('relative')}> + onCopyPaths('relative')}> {selectionSize > 1 ? translate( @@ -695,13 +700,13 @@ export function FileExplorerRow({ ) : null} {!node.isDirectory && ( - onDuplicate(node)}> + onDuplicate(node)}> {translate('auto.components.right.sidebar.FileExplorerRow.0fec99bfd7', 'Duplicate')} )} {canAddAsProject && ( - + {translate( 'auto.components.right.sidebar.FileExplorerRow.1bb9be455c', @@ -710,7 +715,7 @@ export function FileExplorerRow({ )} {shouldShowOpenInTerminalAction(node) && ( - + {translate( 'auto.components.right.sidebar.FileExplorerRow.e887fa4b2e', @@ -719,13 +724,13 @@ export function FileExplorerRow({ )} {shouldShowViewFileAction(node) && ( - + {translate('auto.components.right.sidebar.FileExplorerRow.1d8e182c32', 'View File')} )} {!node.isDirectory && activeWorktreeId && ( - + {translate( 'auto.components.right.sidebar.FileExplorerRow.dd112c81d2', @@ -735,7 +740,7 @@ export function FileExplorerRow({ )} {!node.isDirectory && activeWorktreeId && detectLanguage(node.path) === 'markdown' && ( + onClick={() => openMarkdownPreview({ filePath: node.path, relativePath: node.relativePath, @@ -752,13 +757,13 @@ export function FileExplorerRow({ )} {showRemoteDownloadAction && ( - + {translate('auto.components.right.sidebar.FileExplorerRow.c2112579f6', 'Download')} )} {canCollapseFolderSubtree && shouldShowCollapseFolderAction(node, isExpanded) && ( - + {translate( 'auto.components.right.sidebar.FileExplorerRow.d6a25618aa', @@ -767,7 +772,7 @@ export function FileExplorerRow({ )} {shouldShowFindInFolderAction(node) && ( - + {translate( 'auto.components.right.sidebar.FileExplorerRow.0df0e5abac', @@ -779,7 +784,7 @@ export function FileExplorerRow({ )} { + onClick={() => { const state = useAppStore.getState() const activeWorktree = Object.values(state.worktreesByRepo) .flat() @@ -802,7 +807,7 @@ export function FileExplorerRow({ {revealLabel} - onStartRename(node)}> + onStartRename(node)}> {translate('auto.components.right.sidebar.FileExplorerRow.fc747429bf', 'Rename')} @@ -811,7 +816,7 @@ export function FileExplorerRow({ : translate('auto.components.right.sidebar.FileExplorerRow.a06551beee', 'Enter')} - + {translate('auto.components.right.sidebar.FileExplorerRow.addc01145f', 'Delete')} {deleteShortcutLabel} diff --git a/src/renderer/src/components/right-sidebar/FileExplorerToolbar.tsx b/src/renderer/src/components/right-sidebar/FileExplorerToolbar.tsx index 85ef2277dbb..217a5c4d81b 100644 --- a/src/renderer/src/components/right-sidebar/FileExplorerToolbar.tsx +++ b/src/renderer/src/components/right-sidebar/FileExplorerToolbar.tsx @@ -55,32 +55,34 @@ export function FileExplorerToolbar({ {repoName} - - - + { + if (!canCollapseAll) { + event.preventDefault() + return + } + onCollapseAll() + }} + > + + + } + /> {translate( 'auto.components.right.sidebar.FileExplorerToolbar.6026b16950', @@ -89,36 +91,38 @@ export function FileExplorerToolbar({ - - - + { + if (!canRefresh) { + event.preventDefault() + return + } + refresh.handleRefresh() + }} + > + {refresh.showRefreshSpinner ? ( + + ) : ( + + )} + + } + /> {translate( 'auto.components.right.sidebar.FileExplorerToolbar.d95e30fe28', @@ -128,22 +132,26 @@ export function FileExplorerToolbar({ - - - - - + + + + } + /> + } + /> {translate( 'auto.components.right.sidebar.FileExplorerToolbar.31b4c3195d', diff --git a/src/renderer/src/components/right-sidebar/FileExplorerViewSwitch.tsx b/src/renderer/src/components/right-sidebar/FileExplorerViewSwitch.tsx index ec64942f1cf..5566c75a869 100644 --- a/src/renderer/src/components/right-sidebar/FileExplorerViewSwitch.tsx +++ b/src/renderer/src/components/right-sidebar/FileExplorerViewSwitch.tsx @@ -45,11 +45,11 @@ export function FileExplorerViewSwitch({ return ( { - if (value === 'files' || value === 'search') { - onSelectView(value) + const next = value[0] + if (next === 'files' || next === 'search') { + onSelectView(next) } }} aria-label={translate( diff --git a/src/renderer/src/components/right-sidebar/FolderWorkspacePrChecksPanel.tsx b/src/renderer/src/components/right-sidebar/FolderWorkspacePrChecksPanel.tsx index 0c64561edec..af967cfc594 100644 --- a/src/renderer/src/components/right-sidebar/FolderWorkspacePrChecksPanel.tsx +++ b/src/renderer/src/components/right-sidebar/FolderWorkspacePrChecksPanel.tsx @@ -216,21 +216,23 @@ export default function FolderWorkspacePrChecksPanel({ ) : null}
- - - + setManualRefreshGeneration((generation) => generation + 1)} + disabled={childWorktrees.length === 0 || isRefreshing} + aria-label={translate( + 'auto.components.rightSidebar.FolderWorkspacePrChecksPanel.refresh', + 'Refresh PR checks' + )} + > + + + } + /> {translate( 'auto.components.rightSidebar.FolderWorkspacePrChecksPanel.refresh', diff --git a/src/renderer/src/components/right-sidebar/FolderWorkspacePrChecksRow.tsx b/src/renderer/src/components/right-sidebar/FolderWorkspacePrChecksRow.tsx index 29d82748467..71f3c650cd1 100644 --- a/src/renderer/src/components/right-sidebar/FolderWorkspacePrChecksRow.tsx +++ b/src/renderer/src/components/right-sidebar/FolderWorkspacePrChecksRow.tsx @@ -103,20 +103,22 @@ export function FolderWorkspacePrChecksRow({
{row.reviewUrl ? ( - - - + { + event.stopPropagation() + void openHttpLink(row.reviewUrl!) + }} + onKeyDown={(event) => event.stopPropagation()} + > + + + } + /> {openExternalLabel} ) : null} diff --git a/src/renderer/src/components/right-sidebar/GitHistoryCommitContextMenu.tsx b/src/renderer/src/components/right-sidebar/GitHistoryCommitContextMenu.tsx index 4746a1cacf8..c7c4ba9627e 100644 --- a/src/renderer/src/components/right-sidebar/GitHistoryCommitContextMenu.tsx +++ b/src/renderer/src/components/right-sidebar/GitHistoryCommitContextMenu.tsx @@ -19,21 +19,21 @@ export function GitHistoryCommitContextMenu({ }): React.JSX.Element { return ( - onAction('open-remote', item)}> + onAction('open-remote', item)}> {translate( 'auto.components.right.sidebar.GitHistoryCommitContextMenu.7b1c4e9a02', 'Open commit in browser' )} - onAction('copy-hash', item)}> + onAction('copy-hash', item)}> {translate( 'auto.components.right.sidebar.GitHistoryCommitContextMenu.8c2d5fab13', 'Copy commit hash' )} - onAction('copy-message', item)}> + onAction('copy-message', item)}> {translate( 'auto.components.right.sidebar.GitHistoryCommitContextMenu.9d3e60bc24', @@ -41,7 +41,7 @@ export function GitHistoryCommitContextMenu({ )} - onAction('explain', item)}> + onAction('explain', item)}> {translate( 'auto.components.right.sidebar.GitHistoryCommitContextMenu.ae4f71cd35', diff --git a/src/renderer/src/components/right-sidebar/GitHistoryPanel.tsx b/src/renderer/src/components/right-sidebar/GitHistoryPanel.tsx index bc60150cd77..07b9b8f244b 100644 --- a/src/renderer/src/components/right-sidebar/GitHistoryPanel.tsx +++ b/src/renderer/src/components/right-sidebar/GitHistoryPanel.tsx @@ -250,23 +250,25 @@ export function GitHistoryPanel({ {result?.hasMore && +} - - - + { + event.stopPropagation() + }} + > + + + } + /> {translate( 'auto.components.right.sidebar.GitHistoryPanel.9f7535d22b', @@ -275,28 +277,30 @@ export function GitHistoryPanel({ - - - + { + event.stopPropagation() + if (collapsed) { + onToggle() + return + } + onRefresh() + }} + aria-label={translate( + 'auto.components.right.sidebar.GitHistoryPanel.d0fb0f4bf2', + 'Refresh commits' + )} + > + + + } + /> {translate( 'auto.components.right.sidebar.GitHistoryPanel.d0fb0f4bf2', @@ -361,7 +365,7 @@ export function GitHistoryPanel({ {onCommitAction && !isBoundaryNode ? ( - {row} + ) : ( diff --git a/src/renderer/src/components/right-sidebar/GitHistoryRow.tsx b/src/renderer/src/components/right-sidebar/GitHistoryRow.tsx index b236d15533c..490ae03798d 100644 --- a/src/renderer/src/components/right-sidebar/GitHistoryRow.tsx +++ b/src/renderer/src/components/right-sidebar/GitHistoryRow.tsx @@ -13,18 +13,20 @@ function GitHistoryRefBadge({ itemRef }: { itemRef: GitHistoryItemRef }): React. return ( - - - {itemRef.name} - - + + {itemRef.name} + + } + /> {refLabel} @@ -89,11 +91,13 @@ export const GitHistoryRow = React.forwardRef( /> )} - - - {item.subject} - - + + {item.subject} + + } + /> {rowTooltip} @@ -106,14 +110,16 @@ export const GitHistoryRow = React.forwardRef( ))} {hiddenRefs.length > 0 && ( - - ref.name).join(', ')} - > - +{hiddenRefs.length} - - + ref.name).join(', ')} + > + +{hiddenRefs.length} + + } + /> {hiddenRefs.map((ref) => ref.name).join(', ')} diff --git a/src/renderer/src/components/right-sidebar/HostedReviewActions.tsx b/src/renderer/src/components/right-sidebar/HostedReviewActions.tsx index 3c625e2003c..213f97cfe16 100644 --- a/src/renderer/src/components/right-sidebar/HostedReviewActions.tsx +++ b/src/renderer/src/components/right-sidebar/HostedReviewActions.tsx @@ -105,92 +105,96 @@ export default function HostedReviewActions({ if (review.state === 'open') { return (
- +
- - {/* Why: wrapping in a so the tooltip trigger receives pointer + {/* Why: wrapping in a so the tooltip trigger receives pointer events even when the merge button inside is disabled. */} - + + + + } + /> + {primaryMergeDisabled && ( + + {mergePresentation.tooltip} + + )} + + + - mergePresentation.autoMergeAction && !mergePresentation.directMergeAvailable - ? void handleAutoMerge() - : void handleMerge(mergeMethods.defaultMethod) - } - disabled={primaryMergeDisabled} + disabled={menuDisabled} + aria-label={translate( + 'auto.components.right.sidebar.HostedReviewActions.2bfaf4379c', + 'More {{value0}} actions', + { value0: reviewLabel } + )} + title={translate( + 'auto.components.right.sidebar.HostedReviewActions.9845a71e17', + 'More actions' + )} > - {merging ? ( + {stateUpdating === 'closed' ? ( ) : ( - + )} - - {merging - ? translate( - 'auto.components.right.sidebar.HostedReviewActions.d2ca293f3d', - 'Working...' - ) - : mergePresentation.directMergeAvailable - ? mergeMethods.defaultLabel - : (mergePresentation.autoMergeAction?.label ?? mergePresentation.label)} - - - - {primaryMergeDisabled && ( - - {mergePresentation.tooltip} - - )} - - - - - + } + /> {mergePresentation.autoMergeAction && ( <> void handleAutoMerge()} + onClick={() => void handleAutoMerge()} > {mergePresentation.autoMergeAction.label} @@ -202,7 +206,7 @@ export default function HostedReviewActions({ void handleMerge(method)} + onClick={() => void handleMerge(method)} > {label} @@ -212,7 +216,7 @@ export default function HostedReviewActions({ void handleCloseReview()} + onClick={() => void handleCloseReview()} > {translate( diff --git a/src/renderer/src/components/right-sidebar/PortsPanel.tsx b/src/renderer/src/components/right-sidebar/PortsPanel.tsx index b0c1dae3a4d..1cefc8ee3ea 100644 --- a/src/renderer/src/components/right-sidebar/PortsPanel.tsx +++ b/src/renderer/src/components/right-sidebar/PortsPanel.tsx @@ -336,22 +336,24 @@ function LocalWorkspacePortsPanel({ isVisible }: { isVisible: boolean }): React. {translate('auto.components.right.sidebar.PortsPanel.6bc058dbe1', 'Ports')} - - - + void refresh()} + disabled={refreshing} + aria-label={translate( + 'auto.components.right.sidebar.PortsPanel.7822e3edc6', + 'Refresh Ports' + )} + > + + + } + /> {translate('auto.components.right.sidebar.PortsPanel.7822e3edc6', 'Refresh Ports')} @@ -579,72 +581,78 @@ function LocalPortRow({ return (
- -
-
- {port.kind === 'container' ? : } -
-
-
- :{port.port} - {processLabel} -
-
- {address} + +
+ {port.kind === 'container' ? : }
-
- {ownerLabel} - {confidenceLabel && ( - {confidenceLabel} - )} +
+
+ :{port.port} + {processLabel} +
+
+ {address} +
+
+ {ownerLabel} + {confidenceLabel && ( + {confidenceLabel} + )} +
-
- - + } + /> +
- - - + + + + } + /> {getPortOpenBrowserTooltipLabel(openBrowserLabel)} - - - + + + + } + /> {translate( 'auto.components.right.sidebar.PortsPanel.1004af16ab', @@ -655,21 +663,23 @@ function LocalPortRow({ {canStopProcess && ( - - - + + + + } + /> {translate('auto.components.right.sidebar.PortsPanel.f9528da632', 'Stop Process')} @@ -682,30 +692,24 @@ function LocalPortRow({ {`:${port.port}`} - handleOpenBrowser()} - > + handleOpenBrowser()}> {openBrowserLabel} - + {translate('auto.components.right.sidebar.PortsPanel.792baeb7ed', 'Copy Address')} { + onClick={() => { void window.api.ui.writeClipboardText(JSON.stringify(port, null, 2)) }} > {translate('auto.components.right.sidebar.PortsPanel.bdac206faf', 'Copy Details')} - onShowDetails(port)} - > + onShowDetails(port)}> {translate('auto.components.right.sidebar.PortsPanel.a223459512', 'Show Details')} @@ -714,7 +718,7 @@ function LocalPortRow({ className={LOCAL_PORT_MENU_ITEM_CLASS} variant="destructive" disabled={!canStopProcess} - onSelect={() => onStop(port)} + onClick={() => onStop(port)} > {translate('auto.components.right.sidebar.PortsPanel.f9528da632', 'Stop Process')} diff --git a/src/renderer/src/components/right-sidebar/RightSidebarFrame.tsx b/src/renderer/src/components/right-sidebar/RightSidebarFrame.tsx new file mode 100644 index 00000000000..2a45cec5c5f --- /dev/null +++ b/src/renderer/src/components/right-sidebar/RightSidebarFrame.tsx @@ -0,0 +1,338 @@ +import React, { useEffect, useMemo, useState } from 'react' +import { PanelRight } from 'lucide-react' +import type { CheckStatus } from '../../../../shared/types' +import type { ActiveRightSidebarTab, ActivityBarPosition } from '@/store/slices/editor' +import { cn } from '@/lib/utils' +import { useSidebarResize } from '@/hooks/useSidebarResize' +import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' +import { + ContextMenu, + ContextMenuContent, + ContextMenuLabel, + ContextMenuRadioGroup, + ContextMenuRadioItem, + ContextMenuTrigger +} from '@/components/ui/context-menu' +import { translate } from '@/i18n/i18n' +import { + isPairedWebClientWindow, + shouldRenderDesktopWindowChrome +} from '@/lib/desktop-window-chrome' +import { getRendererAppPlatform } from '@/lib/renderer-app-platform' +import { getTopActivityBarLayout } from './activity-bar-overflow' +import { + ActivityBarButton, + TopActivityOverflowMenu, + type ActivityBarItem +} from './activity-bar-buttons' +import { + RIGHT_SIDEBAR_HEADER_NO_DRAG_CLASS_NAME, + RIGHT_SIDEBAR_TOP_ACTIVITY_STRIP_CLASS_NAME, + RIGHT_SIDEBAR_WINDOWS_TOP_ACTIVITY_STRIP_CLASS_NAME +} from './right-sidebar-titlebar-drag-regions' +import { + RIGHT_SIDEBAR_MIN_WIDTH, + clampRightSidebarPanelWidth, + computeMaxRightSidebarPanelWidth +} from './right-sidebar-width' +import { useMeasuredWidth } from './right-sidebar-measured-width' + +const ACTIVITY_BAR_SIDE_WIDTH = 40 + +export type RightSidebarFrameProps = { + activeTab: ActiveRightSidebarTab + activityBarPosition: ActivityBarPosition + children: React.ReactNode + checksStatus?: CheckStatus | null + isOpen: boolean + items: readonly ActivityBarItem[] + onActivityBarPositionChange: (position: ActivityBarPosition) => void + onSelectTab: (tab: ActiveRightSidebarTab) => void + onToggle: () => void + onWidthChange: (width: number) => void + toggleShortcut: string + width: number +} + +export function RightSidebarFrame({ + activeTab, + activityBarPosition, + children, + checksStatus, + isOpen, + items, + onActivityBarPositionChange, + onSelectTab, + onToggle, + onWidthChange, + toggleShortcut, + width +}: RightSidebarFrameProps): React.JSX.Element { + const hasDesktopWindowChrome = shouldRenderDesktopWindowChrome({ + platform: getRendererAppPlatform(), + isWebClient: isPairedWebClientWindow() + }) + const [topActivityStripWidth, setTopActivityStripWidth] = useState(null) + const activityBarSideWidth = activityBarPosition === 'side' ? ACTIVITY_BAR_SIDE_WIDTH : 0 + const windowWidth = useWindowWidth() + const maxWidth = computeMaxRightSidebarPanelWidth(windowWidth, activityBarSideWidth) + const renderedWidth = clampRightSidebarPanelWidth(width, windowWidth, activityBarSideWidth) + const { containerRef, onResizeStart } = useSidebarResize({ + isOpen, + width: renderedWidth, + minWidth: RIGHT_SIDEBAR_MIN_WIDTH, + maxWidth, + deltaSign: -1, + renderedExtraWidth: activityBarSideWidth, + setWidth: onWidthChange + }) + const topActivityStripRef = useMeasuredWidth(setTopActivityStripWidth) + const topActivityLayout = useMemo( + () => getTopActivityBarLayout(items, topActivityStripWidth, activeTab), + [activeTab, items, topActivityStripWidth] + ) + const closeButton = isOpen ? ( + + + + + } + /> + + {translate( + 'auto.components.right.sidebar.index.9fffaf17c1', + 'Toggle right sidebar ({{value0}})', + { value0: toggleShortcut } + )} + + + ) : null + + return ( +
+
+ {activityBarPosition === 'top' ? ( + +
+ {!hasDesktopWindowChrome ? ( + <> + + +
+ } + /> +
+ {closeButton} +
+ + ) : ( +
+ {closeButton} +
+ )} +
+ {hasDesktopWindowChrome ? ( + + {/* Why: desktop controls own the titlebar; navigation moves below them. */} + +
+ } + /> + ) : null} + + + ) : ( +
+ + {items.find((item) => item.id === activeTab)?.title ?? ''} + +
{closeButton}
+
+ )} + + {isOpen ? ( +
+ {children} +
+ ) : null} + +
+
+ + {activityBarPosition === 'side' ? ( + + + {items.map((item) => ( + onSelectTab(item.id)} + layout="side" + statusIndicator={item.id === 'checks' ? checksStatus : null} + /> + ))} +
+ } + /> + + + ) : null} +
+ ) +} + +function TopActivityItems({ + activeTab, + checksStatus, + containerNoDrag = false, + layout, + onSelectTab +}: { + activeTab: ActiveRightSidebarTab + checksStatus?: CheckStatus | null + containerNoDrag?: boolean + layout: { visibleItems: ActivityBarItem[]; overflowItems: ActivityBarItem[] } + onSelectTab: (tab: ActiveRightSidebarTab) => void +}): React.JSX.Element { + return ( +
+
+ {layout.visibleItems.map((item) => ( + onSelectTab(item.id)} + layout="top" + statusIndicator={item.id === 'checks' ? checksStatus : null} + /> + ))} +
+ {layout.overflowItems.length > 0 ? ( + + ) : null} +
+ ) +} + +function ActivityBarPositionMenu({ + currentPosition, + onChangePosition +}: { + currentPosition: ActivityBarPosition + onChangePosition: (position: ActivityBarPosition) => void +}): React.JSX.Element { + return ( + + + {translate('auto.components.right.sidebar.index.864111caa2', 'Activity Bar Position')} + + onChangePosition(value as ActivityBarPosition)} + > + + {translate('auto.components.right.sidebar.index.7b415c39e9', 'Top')} + + + {translate('auto.components.right.sidebar.index.70893f017b', 'Side')} + + + + ) +} + +// Why: persisted widths must be reclamped when the host window changes size. +function useWindowWidth(): number | null { + const [windowWidth, setWindowWidth] = useState(() => getWindowWidth()) + + useEffect(() => { + function update(): void { + setWindowWidth(getWindowWidth()) + } + window.addEventListener('resize', update) + return () => window.removeEventListener('resize', update) + }, []) + + return windowWidth +} + +function getWindowWidth(): number | null { + if (typeof window === 'undefined' || !Number.isFinite(window.innerWidth)) { + return null + } + return window.innerWidth +} diff --git a/src/renderer/src/components/right-sidebar/SearchResultItems.tsx b/src/renderer/src/components/right-sidebar/SearchResultItems.tsx index a7499929ea9..4b840b5ac52 100644 --- a/src/renderer/src/components/right-sidebar/SearchResultItems.tsx +++ b/src/renderer/src/components/right-sidebar/SearchResultItems.tsx @@ -70,38 +70,44 @@ export function FileResultRow({ return (
{/* File header with context menu */} - + - - - - - + + + +
+ + {fileName} + {dirPath && ( + + {dirPath} + + )} + +
+ + {matchCount} + + + } + /> + } + /> window.api.ui.writeClipboardText(fileResult.relativePath)} @@ -172,35 +178,37 @@ export function MatchResultRow({ return ( - - - + { + // Why: clicking a result should move focus into the opened editor. + // If the sidebar button takes focus first, the browser can restore + // it after the click and make the initial reveal feel flaky. + if (event.button === 0) { + event.preventDefault() + } + }} + onClick={onClick} + > + + {match.line} + + + {parts.before} + {parts.match && ( + + {parts.match} + + )} + {parts.after} + + + } + /> window.api.ui.writeClipboardText(`${relativePath}#L${match.line}`)} diff --git a/src/renderer/src/components/right-sidebar/SessionRowTrailingActions.tsx b/src/renderer/src/components/right-sidebar/SessionRowTrailingActions.tsx index dbb5cfaf7dd..0dc277a0bd8 100644 --- a/src/renderer/src/components/right-sidebar/SessionRowTrailingActions.tsx +++ b/src/renderer/src/components/right-sidebar/SessionRowTrailingActions.tsx @@ -83,26 +83,28 @@ export function SessionRowTrailingActions({
{onJumpToOriginalPane ? ( - - - + { + event.stopPropagation() + onJumpToOriginalPane() + }} + data-testid="ai-vault-session-jump-original-pane" + className="can-hover:pointer-events-none group-hover/session-row:pointer-events-auto group-focus-within/session-row:pointer-events-auto focus-visible:pointer-events-auto" + > + + + } + /> {translate( 'auto.components.right.sidebar.AiVaultSessionRow.jumpToOriginalPane', @@ -124,81 +126,89 @@ export function SessionRowTrailingActions({ ) : null} - + { + event.stopPropagation() + onResume() + }} + data-testid="ai-vault-session-resume" + // Why: on touch (no hover) these controls stay visible and + // tappable; on hover-capable devices the session row gates both + // visibility and hit targets until it is hovered. + className="can-hover:pointer-events-none group-hover/session-row:pointer-events-auto group-focus-within/session-row:pointer-events-auto focus-visible:pointer-events-auto" + > + + + } + /> + + {resumeLabel} + + +
+ + { event.stopPropagation() - onResume() + onToggleDetails() }} - data-testid="ai-vault-session-resume" - // Why: on touch (no hover) these controls stay visible and - // tappable; on hover-capable devices the session row gates both - // visibility and hit targets until it is hovered. - className="can-hover:pointer-events-none group-hover/session-row:pointer-events-auto group-focus-within/session-row:pointer-events-auto focus-visible:pointer-events-auto" + data-testid="ai-vault-session-toggle-details" > - + - - - {resumeLabel} - - -
- - - - + } + /> {detailsTooltip} - - - - - + event.stopPropagation()} + > + + + } + /> + } + /> {translate( 'auto.components.right.sidebar.AiVaultSessionRow.moreActions', @@ -259,17 +269,19 @@ function WorktreeJumpTooltipTrigger({ ) if (!disabled) { - return {button} + return } return ( - - event.stopPropagation()} - > - {button} - - + event.stopPropagation()} + > + {button} + + } + /> ) } diff --git a/src/renderer/src/components/right-sidebar/SourceControl.tsx b/src/renderer/src/components/right-sidebar/SourceControl.tsx index 45a9e212c99..2028b2be363 100644 --- a/src/renderer/src/components/right-sidebar/SourceControl.tsx +++ b/src/renderer/src/components/right-sidebar/SourceControl.tsx @@ -5643,25 +5643,27 @@ function SourceControlInner(): React.JSX.Element { triggerClassName="size-6" /> {diffCommentCount > 0 && ( - + - - - + void handleCopyDiffComments()} + aria-label={translate( + 'auto.components.right.sidebar.SourceControl.3baf6c77b4', + 'Copy all notes to clipboard' + )} + > + {diffCommentsCopied ? ( + + ) : ( + + )} + + } + /> {translate( 'auto.components.right.sidebar.SourceControl.eae2d051af', @@ -5672,22 +5674,26 @@ function SourceControlInner(): React.JSX.Element { )} - + - - - - - + + + + } + /> + } + /> {translate( 'auto.components.right.sidebar.SourceControl.2fe2a67580', @@ -5700,7 +5706,7 @@ function SourceControlInner(): React.JSX.Element { { + onClick={() => { if (!activeWorktreeId || diffCommentCount === 0) { return } @@ -6710,32 +6716,35 @@ export function CommitArea({ ) : ( - -
- { - if (entry.disabled) { - event.preventDefault() - return - } - onDropdownAction(entry.kind) - }} - > - - {entry.label} - {entry.hint ? ( - - {entry.hint} - - ) : null} - - -
-
+ + { + if (entry.disabled) { + event.preventDefault() + return + } + onDropdownAction(entry.kind) + }} + closeOnClick={false} + > + + {entry.label} + {entry.hint ? ( + + {entry.hint} + + ) : null} + + +
+ } + /> {entry.title} @@ -6779,24 +6788,26 @@ export function CommitArea({ // sees that clicking will abort the run. Group/group-hover toggles // keep this stateless on the React side. - - - + onCancelGenerate()} + title={translate( + 'auto.components.right.sidebar.SourceControl.527e130b6f', + 'Stop generating' + )} + aria-label={translate( + 'auto.components.right.sidebar.SourceControl.ddc1fbd690', + 'Stop generating commit message' + )} + className="group absolute right-1.5 top-1.5 inline-flex size-5 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-destructive/10 hover:text-destructive focus-visible:bg-destructive/10 focus-visible:text-destructive focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-destructive/40" + > + + + + } + /> {translate( 'auto.components.right.sidebar.SourceControl.37a81f29ad', @@ -6806,37 +6817,39 @@ export function CommitArea({ ) : ( - - - + aria-label={translate( + 'auto.components.right.sidebar.SourceControl.461575b9bc', + 'Generate commit message with AI' + )} + className={cn( + 'absolute right-1.5 top-1.5 inline-flex size-5 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-muted/60 hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring', + isGenerateDisabled && + 'cursor-not-allowed opacity-40 hover:bg-transparent hover:text-muted-foreground' + )} + > + + + } + /> {generateDisabledReason ?? translate( @@ -6860,60 +6873,66 @@ export function CommitArea({ (size="xs", px-3 text-[11px]) so the sidebar has a consistent action-button shape across Source Control and Checks. */} - - - - - + + + + } + /> {primaryAction.title} - - - - - - - + + + {showChevronSpinner ? ( + + ) : ( + + )} + + } + /> + + } + /> {moreCommitAndRemoteActionsLabel} @@ -7194,18 +7213,20 @@ export function CompareSummaryToolbarButton({ }): React.JSX.Element { return ( - - - + + + + } + /> {label} @@ -8185,9 +8206,9 @@ function ConflictBadge({ entry }: { entry: GitStatusEntry }): React.JSX.Element } return ( - + - {badge} + {translate( 'auto.components.right.sidebar.SourceControl.03194cfff4', @@ -8323,28 +8344,30 @@ export function ActionButton({ // `aria-disabled` + visual dimming preserves the disabled affordance. return ( - - - + { + if (disabled) { + event.preventDefault() + return + } + onClick(event) + }} + > + + + } + /> {title} diff --git a/src/renderer/src/components/right-sidebar/SourceControlAgentActionDialogForm.tsx b/src/renderer/src/components/right-sidebar/SourceControlAgentActionDialogForm.tsx index 4f2caf0133b..b2a787882db 100644 --- a/src/renderer/src/components/right-sidebar/SourceControlAgentActionDialogForm.tsx +++ b/src/renderer/src/components/right-sidebar/SourceControlAgentActionDialogForm.tsx @@ -337,7 +337,10 @@ export function SourceControlAgentActionDialogForm({ 'Save for' )} - value && onSaveAgentDefaultChange(value)} + > diff --git a/src/renderer/src/components/right-sidebar/SourceControlTextGenerationDialogForm.tsx b/src/renderer/src/components/right-sidebar/SourceControlTextGenerationDialogForm.tsx index 7315ba66207..ecc23afa8a8 100644 --- a/src/renderer/src/components/right-sidebar/SourceControlTextGenerationDialogForm.tsx +++ b/src/renderer/src/components/right-sidebar/SourceControlTextGenerationDialogForm.tsx @@ -305,7 +305,10 @@ export function SourceControlTextGenerationDialogForm({ 'Save recipe' )} - value != null && setSaveTargetKey(value)} + > diff --git a/src/renderer/src/components/right-sidebar/activity-bar-buttons.tsx b/src/renderer/src/components/right-sidebar/activity-bar-buttons.tsx index 8ce09527994..694ad9d435e 100644 --- a/src/renderer/src/components/right-sidebar/activity-bar-buttons.tsx +++ b/src/renderer/src/components/right-sidebar/activity-bar-buttons.tsx @@ -52,29 +52,31 @@ export function TopActivityOverflowMenu({ return ( - - - + + + {hiddenChecksStatus && ( +
+ )} + + } + /> {items.map((item) => { const Icon = item.icon @@ -82,7 +84,7 @@ export function TopActivityOverflowMenu({ return ( onSelect(item.id)} + onClick={() => onSelect(item.id)} className={cn(active && 'bg-accent text-accent-foreground')} aria-current={active ? 'page' : undefined} > @@ -115,38 +117,40 @@ export function ActivityBarButton({ return ( - - - + {active && isTop && ( +
+ )} + {active && !isTop && ( +
+ )} + + } + /> {item.shortcut ? `${item.title} (${item.shortcut})` : item.title} diff --git a/src/renderer/src/components/right-sidebar/check-status-presentation.ts b/src/renderer/src/components/right-sidebar/check-status-presentation.ts new file mode 100644 index 00000000000..0530a18f746 --- /dev/null +++ b/src/renderer/src/components/right-sidebar/check-status-presentation.ts @@ -0,0 +1,31 @@ +import type React from 'react' +import { + AlertTriangle, + CircleCheck, + CircleDashed, + CircleMinus, + CircleX, + LoaderCircle +} from 'lucide-react' + +export const CHECK_ICON: Record> = { + success: CircleCheck, + failure: CircleX, + pending: LoaderCircle, + neutral: CircleDashed, + skipped: CircleMinus, + cancelled: CircleX, + timed_out: CircleX, + action_required: AlertTriangle +} + +export const CHECK_COLOR: Record = { + success: 'text-status-success', + failure: 'text-destructive', + pending: 'text-amber-500', + neutral: 'text-muted-foreground', + skipped: 'text-muted-foreground/60', + cancelled: 'text-muted-foreground/60', + timed_out: 'text-destructive', + action_required: 'text-amber-500' +} diff --git a/src/renderer/src/components/right-sidebar/checks-panel-content.tsx b/src/renderer/src/components/right-sidebar/checks-panel-content.tsx index 84798c944f8..940a2cb9d4d 100644 --- a/src/renderer/src/components/right-sidebar/checks-panel-content.tsx +++ b/src/renderer/src/components/right-sidebar/checks-panel-content.tsx @@ -1,12 +1,11 @@ -/* eslint-disable max-lines -- Why: co-locating all checks-panel sub-components (checks list, -conflict sections, threaded PR comments) keeps the shared icon/color maps in one place. */ +/* eslint-disable max-lines -- Why: legacy checks-panel sub-components for checks, conflicts, +and threaded PR comments still share tightly coupled selection and composer state. */ import React, { useCallback, useEffect, useRef, useState } from 'react' import { CircleCheck, CircleX, LoaderCircle, CircleDashed, - CircleMinus, GitPullRequest, Files, Copy, @@ -99,6 +98,9 @@ import { import { translate } from '@/i18n/i18n' import { useActiveWorktree } from '@/store/selectors' import { useAppStore } from '@/store' +import { CHECK_COLOR, CHECK_ICON } from './check-status-presentation' + +export { CHECK_COLOR, CHECK_ICON } from './check-status-presentation' export const PullRequestIcon = GitPullRequest @@ -112,28 +114,6 @@ function getPRCommentsListDisplayModeLabel(mode: PRCommentsListDisplayMode): str : translate('auto.components.right.sidebar.checks.panel.content.b13f85d75c', 'Timeline') } -export const CHECK_ICON: Record> = { - success: CircleCheck, - failure: CircleX, - pending: LoaderCircle, - neutral: CircleDashed, - skipped: CircleMinus, - cancelled: CircleX, - timed_out: CircleX, - action_required: AlertTriangle -} - -export const CHECK_COLOR: Record = { - success: 'text-emerald-500', - failure: 'text-rose-500', - pending: 'text-amber-500', - neutral: 'text-muted-foreground', - skipped: 'text-muted-foreground/60', - cancelled: 'text-muted-foreground/60', - timed_out: 'text-rose-500', - action_required: 'text-amber-500' -} - type ConflictReview = { mergeable: PRMergeableState conflictSummary?: PRConflictSummary @@ -1264,24 +1244,26 @@ export function ChecksList({ {openUrl && ( - - - + { + event.stopPropagation() + window.api.shell.openUrl(openUrl) + }} + > + + + } + /> {translate( 'auto.components.right.sidebar.checks.panel.content.0dca6bfab5', @@ -1502,23 +1484,28 @@ function CommentMoreMenu({ return ( - - - + event.stopPropagation()} + > + + + } + /> {hasQueue ? ( - onQueueForAgent?.()}> + onQueueForAgent?.()}> {translate( 'auto.components.right.sidebar.checks.panel.content.f8a2c91d04', @@ -1528,7 +1515,7 @@ function CommentMoreMenu({ ) : null} {hasQueue && (hasGoToComment || hasEdit || hasDelete) ? : null} {hasGoToComment && ( - window.api.shell.openUrl(comment.url)}> + window.api.shell.openUrl(comment.url)}> {translate( 'auto.components.right.sidebar.checks.panel.content.d3923d18fe', @@ -1539,17 +1526,18 @@ function CommentMoreMenu({ {hasGoToComment && (hasEdit || hasDelete) ? : null} {hasEdit ? ( { + onClick={(event) => { event.preventDefault() onStartEdit?.() }} + closeOnClick={false} > {translate('auto.components.right.sidebar.checks.panel.content.03ca88f623', 'Edit')} ) : null} {hasDelete ? ( - void onDelete?.()}> + void onDelete?.()}> {translate('auto.components.right.sidebar.checks.panel.content.6cc6eace26', 'Delete')} @@ -1558,7 +1546,7 @@ function CommentMoreMenu({ <> {(hasQueue || hasGoToComment || hasEdit || hasDelete) && } setPRBotAuthorOverride(comment.author, !isOverriddenBot)} + onClick={() => setPRBotAuthorOverride(comment.author, !isOverriddenBot)} > {isOverriddenBot @@ -2077,9 +2065,9 @@ function PRCommentGroupView({ return ( - {content} + - onQueueForAgent()}> + onQueueForAgent()}> {translate( 'auto.components.right.sidebar.checks.panel.content.f8a2c91d04', @@ -2123,7 +2111,7 @@ function ResolvedCommentGroupsSection({ } return (
- + @@ -2421,28 +2409,30 @@ export function PRCommentsList({ {canShowResolveWithAI && ( <> - - - + onResolveSelectedCommentsWithAI?.(selectableGroups)} + > + + + } + /> {resolveCommentsWithAIDisabled && resolveCommentsWithAIDisabledReason ? resolveCommentsWithAIDisabledReason @@ -2456,35 +2446,37 @@ export function PRCommentsList({ {isSelectingForAI && ( <> - - - + onResolveSelectedCommentsWithAI?.(selectedGroups)} + > + + + {selectedCommentQueueCount} + + + } + /> {resolveCommentsWithAIDisabled && resolveCommentsWithAIDisabledReason ? resolveCommentsWithAIDisabledReason @@ -2496,21 +2488,23 @@ export function PRCommentsList({ - - - + + + + } + /> {translate( 'auto.components.right.sidebar.checks.panel.content.a6de3e5a20', @@ -2524,20 +2518,22 @@ export function PRCommentsList({ )} {comments.length > 0 && ( - - - + + + + } + /> {translate( @@ -2560,30 +2556,32 @@ export function PRCommentsList({ )} {onAddComment && !isAddingComment && ( - - - + + + + } + /> {commentsDisabled && commentsDisabledReason ? commentsDisabledReason diff --git a/src/renderer/src/components/right-sidebar/index.tsx b/src/renderer/src/components/right-sidebar/index.tsx index cc0eb6a36b8..37f22823d2f 100644 --- a/src/renderer/src/components/right-sidebar/index.tsx +++ b/src/renderer/src/components/right-sidebar/index.tsx @@ -1,83 +1,44 @@ -/* eslint-disable max-lines -- Why: the right sidebar owns activity-bar visibility, routing, and resize behavior as one interaction surface; splitting the tab table away would make hidden-tab fallbacks harder to audit. */ -import React, { useEffect, useMemo, useRef, useState } from 'react' -import { Plug, Files, GitBranch, ListChecks, PanelRight, Workflow } from 'lucide-react' +import React, { useEffect, useMemo, useRef } from 'react' import { useAppStore } from '@/store' import type { ActiveRightSidebarTab } from '@/store/slices/editor' import { useRepoById } from '@/store/selectors' -import { cn } from '@/lib/utils' -import { useSidebarResize } from '@/hooks/useSidebarResize' -import type { ActivityBarPosition } from '@/store/slices/editor' import { isFolderRepo } from '../../../../shared/repo-kind' import { parseWorkspaceKey } from '../../../../shared/workspace-scope' -import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from '@/components/ui/tooltip' -import { - ContextMenu, - ContextMenuTrigger, - ContextMenuContent, - ContextMenuLabel, - ContextMenuRadioGroup, - ContextMenuRadioItem -} from '@/components/ui/context-menu' -import { getTopActivityBarLayout } from './activity-bar-overflow' -import { - ActivityBarButton, - TopActivityOverflowMenu, - type ActivityBarItem -} from './activity-bar-buttons' import { getActiveChecksStatus } from './active-checks-status' import { getVisibleRightSidebarActivityItems } from './right-sidebar-activity-visibility' import { useShortcutLabel } from '@/hooks/useShortcutLabel' -import { - RIGHT_SIDEBAR_HEADER_NO_DRAG_CLASS_NAME, - RIGHT_SIDEBAR_TOP_ACTIVITY_STRIP_CLASS_NAME, - RIGHT_SIDEBAR_WINDOWS_TOP_ACTIVITY_STRIP_CLASS_NAME -} from './right-sidebar-titlebar-drag-regions' -import { - RIGHT_SIDEBAR_MIN_WIDTH, - clampRightSidebarPanelWidth, - computeMaxRightSidebarPanelWidth -} from './right-sidebar-width' -import { translate } from '@/i18n/i18n' import { RightSidebarPanelContent } from './right-sidebar-panel-content' -import { useMeasuredWidth } from './right-sidebar-measured-width' import { normalizeRightSidebarRoute } from '@/store/right-sidebar-route' -import { AgentSessionHistoryIcon } from './agent-session-history-icon' import { resolveRightSidebarEffectiveTab } from './right-sidebar-effective-tab' -import { - isPairedWebClientWindow, - shouldRenderDesktopWindowChrome -} from '@/lib/desktop-window-chrome' -import { getRendererAppPlatform } from '@/lib/renderer-app-platform' - -const ACTIVITY_BAR_SIDE_WIDTH = 40 +import type { ActivityBarItem } from './activity-bar-buttons' +import { RightSidebarFrame } from './RightSidebarFrame' +import { createRightSidebarActivityItems } from './right-sidebar-activity-items' function RightSidebarInner(): React.JSX.Element { - const hasDesktopWindowChrome = shouldRenderDesktopWindowChrome({ - platform: getRendererAppPlatform(), - isWebClient: isPairedWebClientWindow() - }) const rightSidebarShortcut = useShortcutLabel('sidebar.right.toggle') const explorerShortcut = useShortcutLabel('sidebar.explorer.toggle') const sourceControlShortcut = useShortcutLabel('sidebar.sourceControl.toggle') const checksShortcut = useShortcutLabel('sidebar.checks.toggle') const portsShortcut = useShortcutLabel('sidebar.ports.toggle') - const rightSidebarOpen = useAppStore((s) => s.rightSidebarOpen) - const rightSidebarWidth = useAppStore((s) => s.rightSidebarWidth) - const setRightSidebarWidth = useAppStore((s) => s.setRightSidebarWidth) - const rightSidebarTab = useAppStore((s) => s.rightSidebarTab) - const rightSidebarRouteRequestId = useAppStore((s) => s.rightSidebarRouteRequestId) - const setRightSidebarTab = useAppStore((s) => s.setRightSidebarTab) - const showRightSidebarFiles = useAppStore((s) => s.showRightSidebarFiles) - const toggleRightSidebar = useAppStore((s) => s.toggleRightSidebar) - const checksStatus = useAppStore((s) => (s.rightSidebarOpen ? getActiveChecksStatus(s) : null)) - const activityBarPosition = useAppStore((s) => s.activityBarPosition) - const setActivityBarPosition = useAppStore((s) => s.setActivityBarPosition) - const [topActivityStripWidth, setTopActivityStripWidth] = useState(null) - const activeWorktreeId = useAppStore((s) => (rightSidebarOpen ? s.activeWorktreeId : null)) + const rightSidebarOpen = useAppStore((state) => state.rightSidebarOpen) + const rightSidebarWidth = useAppStore((state) => state.rightSidebarWidth) + const setRightSidebarWidth = useAppStore((state) => state.setRightSidebarWidth) + const rightSidebarTab = useAppStore((state) => state.rightSidebarTab) + const rightSidebarRouteRequestId = useAppStore((state) => state.rightSidebarRouteRequestId) + const setRightSidebarTab = useAppStore((state) => state.setRightSidebarTab) + const showRightSidebarFiles = useAppStore((state) => state.showRightSidebarFiles) + const toggleRightSidebar = useAppStore((state) => state.toggleRightSidebar) + const checksStatus = useAppStore((state) => + state.rightSidebarOpen ? getActiveChecksStatus(state) : null + ) + const activityBarPosition = useAppStore((state) => state.activityBarPosition) + const setActivityBarPosition = useAppStore((state) => state.setActivityBarPosition) + const activeWorktreeId = useAppStore((state) => + rightSidebarOpen ? state.activeWorktreeId : null + ) // Why: source control and checks are meaningless for non-git folders. - // Hide those tabs so the activity bar only shows relevant actions. - const activeWorktree = useAppStore((s) => - activeWorktreeId ? (s.getKnownWorktreeById(activeWorktreeId) ?? null) : null + const activeWorktree = useAppStore((state) => + activeWorktreeId ? (state.getKnownWorktreeById(activeWorktreeId) ?? null) : null ) const activeRepo = useRepoById(activeWorktree?.repoId ?? null) const activeWorkspaceScope = parseWorkspaceKey(activeWorktreeId ?? '') @@ -86,61 +47,15 @@ function RightSidebarInner(): React.JSX.Element { const isSshRepo = Boolean(activeRepo?.connectionId) const activityItems = useMemo( - () => [ - { - id: 'explorer', - icon: Files, - title: translate('auto.components.right.sidebar.index.8bc2bbc3a0', 'Explorer'), - shortcut: explorerShortcut === 'Unassigned' ? '' : explorerShortcut - }, - { - id: 'vault', - icon: AgentSessionHistoryIcon, - title: translate('auto.components.right.sidebar.index.aiVaultSessionHistory', 'Agents'), - shortcut: '' - }, - { - id: 'workspaces', - icon: Workflow, - title: translate( - 'auto.components.right.sidebar.index.folderWorkspaces', - 'Attached worktrees' - ), - shortcut: '', - folderOnly: true - }, - { - id: 'pr-checks', - icon: ListChecks, - title: translate('auto.components.right.sidebar.index.parentPrChecks', 'PR Checks'), - shortcut: '', - folderOnly: true - }, - { - id: 'source-control', - icon: GitBranch, - title: translate('auto.components.right.sidebar.index.0314901467', 'Source Control'), - shortcut: sourceControlShortcut === 'Unassigned' ? '' : sourceControlShortcut, - gitOnly: true - }, - { - id: 'checks', - icon: ListChecks, - title: translate('auto.components.right.sidebar.index.83a10e3c44', 'Checks'), - shortcut: checksShortcut === 'Unassigned' ? '' : checksShortcut, - gitOnly: true - }, - { - id: 'ports', - icon: Plug, - title: translate('auto.components.right.sidebar.index.441733b630', 'Ports'), - shortcut: portsShortcut === 'Unassigned' ? '' : portsShortcut, - sshOnly: true - } - ], + () => + createRightSidebarActivityItems({ + explorer: explorerShortcut, + sourceControl: sourceControlShortcut, + checks: checksShortcut, + ports: portsShortcut + }), [checksShortcut, explorerShortcut, portsShortcut, sourceControlShortcut] ) - const visibleItems = useMemo( () => getVisibleRightSidebarActivityItems(activityItems, { @@ -150,15 +65,9 @@ function RightSidebarInner(): React.JSX.Element { }), [activityItems, isFolder, isFolderWorkspace, isSshRepo] ) - const rememberedFolderTabByWorkspaceKeyRef = useRef>({}) const lastRightSidebarRouteRequestIdRef = useRef(rightSidebarRouteRequestId) const activeFolderWorkspaceKey = isFolderWorkspace ? (activeWorktreeId ?? null) : null - - // If the active tab is hidden (e.g. switched from a folder workspace to a git - // worktree), render a visible fallback without overwriting the stored route. - // Folder workspaces keep a session-local effective-tab memory so a PR Checks - // row can open a child Checks tab without erasing the parent's overview tab. const normalizedActiveTab = normalizeRightSidebarRoute(rightSidebarTab).rightSidebarTab const rememberedFolderTab = activeFolderWorkspaceKey ? rememberedFolderTabByWorkspaceKeyRef.current[activeFolderWorkspaceKey] @@ -185,6 +94,7 @@ function RightSidebarInner(): React.JSX.Element { } rememberedFolderTabByWorkspaceKeyRef.current[activeFolderWorkspaceKey] = effectiveTab }, [activeFolderWorkspaceKey, effectiveTab, visibleItems]) + const selectActivityTab = (tab: ActiveRightSidebarTab): void => { if (activeFolderWorkspaceKey) { rememberedFolderTabByWorkspaceKeyRef.current[activeFolderWorkspaceKey] = tab @@ -196,306 +106,25 @@ function RightSidebarInner(): React.JSX.Element { setRightSidebarTab(tab) } - const activityBarSideWidth = activityBarPosition === 'side' ? ACTIVITY_BAR_SIDE_WIDTH : 0 - const windowWidth = useWindowWidth() - const maxWidth = computeMaxRightSidebarPanelWidth(windowWidth, activityBarSideWidth) - const renderedRightSidebarWidth = clampRightSidebarPanelWidth( - rightSidebarWidth, - windowWidth, - activityBarSideWidth - ) - const { containerRef, onResizeStart } = useSidebarResize({ - isOpen: rightSidebarOpen, - width: renderedRightSidebarWidth, - minWidth: RIGHT_SIDEBAR_MIN_WIDTH, - maxWidth, - deltaSign: -1, - renderedExtraWidth: activityBarSideWidth, - setWidth: setRightSidebarWidth - }) - const topActivityStripRef = useMeasuredWidth(setTopActivityStripWidth) - - const panelContent = rightSidebarOpen ? ( -
- {/* Why: sidebar panels no longer use key={activeWorktreeId} because - the full unmount/remount cycle on every worktree switch triggered - an IPC storm (watchWorktree + readDir + git:branchCompare + …) - that froze the app for seconds on Windows. Each panel now reacts - to activeWorktreeId changes via store subscriptions and reset - effects, keeping the component instance alive across switches. */} - {/* Why: live agent activity now renders inline inside each workspace - card (WorktreeCardAgents, toggled by the 'inline-agents' card - property) rather than in a bottom-docked dashboard panel that - competed with file Explorer/Search for vertical space. The right - sidebar is back to tab-only content. */} - -
- ) : null - - const topActivityLayout = useMemo( - () => getTopActivityBarLayout(visibleItems, topActivityStripWidth, effectiveTab), - [visibleItems, topActivityStripWidth, effectiveTab] - ) - - const sideActivityBarIcons = visibleItems.map((item) => ( - selectActivityTab(item.id)} - layout="side" - statusIndicator={item.id === 'checks' ? checksStatus : null} - /> - )) - - const closeButton = rightSidebarOpen ? ( - - - - - - {translate( - 'auto.components.right.sidebar.index.9fffaf17c1', - 'Toggle right sidebar ({{value0}})', - { value0: rightSidebarShortcut } - )} - - - ) : null - return ( -
- {/* Panel content area */} -
- {activityBarPosition === 'top' ? ( - /* ── Top activity bar: horizontal icon row ── */ - -
- {!hasDesktopWindowChrome && ( - - -
-
- {/* Why: the top strip shares a narrow titlebar with the close - button and desktop window controls. Overflow goes - behind More instead of creating a horizontally - scrollable toolbar. */} -
- {topActivityLayout.visibleItems.map((item) => ( - selectActivityTab(item.id)} - layout="top" - statusIndicator={item.id === 'checks' ? checksStatus : null} - /> - ))} -
- {topActivityLayout.overflowItems.length > 0 && ( - - )} -
-
-
-
- {closeButton} -
-
- )} - {hasDesktopWindowChrome && ( - -
- {closeButton} -
-
- )} -
- {hasDesktopWindowChrome && ( - - -
- {/* Why: custom desktop chrome has fixed native-style controls - in the titlebar area; keep sidebar navigation in the - sidebar body so the titlebar stays visually native - instead of crowded. */} -
- {topActivityLayout.visibleItems.map((item) => ( - selectActivityTab(item.id)} - layout="top" - statusIndicator={item.id === 'checks' ? checksStatus : null} - /> - ))} -
- {topActivityLayout.overflowItems.length > 0 && ( - - )} -
-
-
- )} - -
- ) : ( - /* ── Side layout: static title header ── */ - /* Why: the 40px side activity bar absorbs the rightmost 40px of the - 138px window-controls overlay when custom desktop chrome is active, - but the remaining 98px still overlaps the panel header. - right-sidebar-header-side-inset applies exactly that remainder - (138-40=98px) as padding-right so the close button clears the - minimize button without the full 138px gap. */ -
- - {visibleItems.find((item) => item.id === effectiveTab)?.title ?? ''} - - -
{closeButton}
-
-
- )} - - {panelContent} - - {/* Resize handle on LEFT side */} -
-
- - {/* Side Activity Bar (icon strip on right edge) — only for 'side' position */} - {activityBarPosition === 'side' && ( - - -
- {sideActivityBarIcons} -
-
- -
- )} -
+ {/* Why: panels react to worktree changes themselves; remounting here creates an IPC storm. */} + + ) } const RightSidebar = React.memo(RightSidebarInner) export default RightSidebar - -// Why: persisted right-sidebar widths can outlive the window size they were -// chosen in. Clamp from the current window so the terminal/editor never render -// underneath the sidebar after resize or hydration. -function useWindowWidth(): number | null { - const [windowWidth, setWindowWidth] = useState(() => getWindowWidth()) - - useEffect(() => { - function update(): void { - setWindowWidth(getWindowWidth()) - } - window.addEventListener('resize', update) - return () => window.removeEventListener('resize', update) - }, []) - - return windowWidth -} - -function getWindowWidth(): number | null { - if (typeof window === 'undefined' || !Number.isFinite(window.innerWidth)) { - return null - } - return window.innerWidth -} - -// ─── Context Menu for Activity Bar Position ─────────── -function ActivityBarPositionMenu({ - currentPosition, - onChangePosition -}: { - currentPosition: ActivityBarPosition - onChangePosition: (pos: ActivityBarPosition) => void -}): React.JSX.Element { - return ( - - - {translate('auto.components.right.sidebar.index.864111caa2', 'Activity Bar Position')} - - onChangePosition(v as ActivityBarPosition)} - > - - {translate('auto.components.right.sidebar.index.7b415c39e9', 'Top')} - - - {translate('auto.components.right.sidebar.index.70893f017b', 'Side')} - - - - ) -} diff --git a/src/renderer/src/components/right-sidebar/right-panel-comment-composer.tsx b/src/renderer/src/components/right-sidebar/right-panel-comment-composer.tsx index a3f1e7bae5a..7fdde3c64e0 100644 --- a/src/renderer/src/components/right-sidebar/right-panel-comment-composer.tsx +++ b/src/renderer/src/components/right-sidebar/right-panel-comment-composer.tsx @@ -248,18 +248,20 @@ export function RightPanelCommentComposer({
{toolbar.map(({ action, label, icon: Icon }) => ( - - - + applyAction(action)} + > + + + } + /> {label} @@ -281,22 +283,24 @@ export function RightPanelCommentComposer({ )} - - - + void submit()} + > + {submitting + ? translate( + 'auto.components.right.sidebar.right.panel.comment.composer.87aff03d63', + 'Sending...' + ) + : submitLabel} + + } + /> {disabled && disabledReason ? ( {disabledReason} diff --git a/src/renderer/src/components/right-sidebar/right-sidebar-activity-items.ts b/src/renderer/src/components/right-sidebar/right-sidebar-activity-items.ts new file mode 100644 index 00000000000..af29b9baa8e --- /dev/null +++ b/src/renderer/src/components/right-sidebar/right-sidebar-activity-items.ts @@ -0,0 +1,72 @@ +import { Files, GitBranch, ListChecks, Plug, Workflow } from 'lucide-react' +import { translate } from '@/i18n/i18n' +import { AgentSessionHistoryIcon } from './agent-session-history-icon' +import type { ActivityBarItem } from './activity-bar-buttons' + +export type RightSidebarActivityShortcuts = { + explorer: string + sourceControl: string + checks: string + ports: string +} + +export function createRightSidebarActivityItems( + shortcuts: RightSidebarActivityShortcuts +): ActivityBarItem[] { + return [ + { + id: 'explorer', + icon: Files, + title: translate('auto.components.right.sidebar.index.8bc2bbc3a0', 'Explorer'), + shortcut: assignedShortcut(shortcuts.explorer) + }, + { + id: 'vault', + icon: AgentSessionHistoryIcon, + title: translate('auto.components.right.sidebar.index.aiVaultSessionHistory', 'Agents'), + shortcut: '' + }, + { + id: 'workspaces', + icon: Workflow, + title: translate( + 'auto.components.right.sidebar.index.folderWorkspaces', + 'Attached worktrees' + ), + shortcut: '', + folderOnly: true + }, + { + id: 'pr-checks', + icon: ListChecks, + title: translate('auto.components.right.sidebar.index.parentPrChecks', 'PR Checks'), + shortcut: '', + folderOnly: true + }, + { + id: 'source-control', + icon: GitBranch, + title: translate('auto.components.right.sidebar.index.0314901467', 'Source Control'), + shortcut: assignedShortcut(shortcuts.sourceControl), + gitOnly: true + }, + { + id: 'checks', + icon: ListChecks, + title: translate('auto.components.right.sidebar.index.83a10e3c44', 'Checks'), + shortcut: assignedShortcut(shortcuts.checks), + gitOnly: true + }, + { + id: 'ports', + icon: Plug, + title: translate('auto.components.right.sidebar.index.441733b630', 'Ports'), + shortcut: assignedShortcut(shortcuts.ports), + sshOnly: true + } + ] +} + +function assignedShortcut(shortcut: string): string { + return shortcut === 'Unassigned' ? '' : shortcut +} diff --git a/src/renderer/src/components/right-sidebar/source-control-branch-context-row.tsx b/src/renderer/src/components/right-sidebar/source-control-branch-context-row.tsx index 8cd5f078e04..ef89c036cec 100644 --- a/src/renderer/src/components/right-sidebar/source-control-branch-context-row.tsx +++ b/src/renderer/src/components/right-sidebar/source-control-branch-context-row.tsx @@ -50,9 +50,7 @@ function ContextStat({ return ( - - {stat.label} - + {stat.label}} /> {stat.title} diff --git a/src/renderer/src/components/right-sidebar/source-control-discard-dialog.tsx b/src/renderer/src/components/right-sidebar/source-control-discard-dialog.tsx index 6741a72ac55..1d1f77233ac 100644 --- a/src/renderer/src/components/right-sidebar/source-control-discard-dialog.tsx +++ b/src/renderer/src/components/right-sidebar/source-control-discard-dialog.tsx @@ -66,9 +66,9 @@ export function SourceControlDiscardDialog({ > - focusDiscardDialogConfirmButton(event, confirmButtonRef.current) - } + // Why: focus the destructive confirm button on open so Enter confirms + // rather than the default Cancel focus. See focusDiscardDialogConfirmButton. + initialFocus={confirmButtonRef} > diff --git a/src/renderer/src/components/right-sidebar/source-control-entry-context-menu.tsx b/src/renderer/src/components/right-sidebar/source-control-entry-context-menu.tsx index b333d887587..809fb50bba4 100644 --- a/src/renderer/src/components/right-sidebar/source-control-entry-context-menu.tsx +++ b/src/renderer/src/components/right-sidebar/source-control-entry-context-menu.tsx @@ -27,7 +27,8 @@ type SourceControlEntryContextMenuProps = { onView?: () => void onRevealInExplorer: (worktreeId: string, absolutePath: string) => void onOpenChange?: (open: boolean) => void - children: React.ReactNode + // Base UI's ContextMenuTrigger `render` needs a single element, not arbitrary ReactNode. + children: React.ReactElement } export function SourceControlEntryContextMenu({ @@ -77,9 +78,9 @@ export function SourceControlEntryContextMenu({ return ( - {children} + - + {translate( 'auto.components.right.sidebar.SourceControlEntryContextMenu.a1f2c8d901', @@ -87,7 +88,7 @@ export function SourceControlEntryContextMenu({ )} - + {translate('auto.components.right.sidebar.FileExplorerRow.b5d436aa30', 'Copy Path')} @@ -101,7 +102,7 @@ export function SourceControlEntryContextMenu({ {openInEntries.map((entry) => ( handleOpenInExternal(entry.target, entry.command)} + onClick={() => handleOpenInExternal(entry.target, entry.command)} disabled={!absolutePath} > {entry.target === 'file-manager' ? ( @@ -115,7 +116,7 @@ export function SourceControlEntryContextMenu({ ))} - + {translate( 'auto.components.sidebar.WorktreeOpenInMenu.1417fd8380', 'Customize apps...' @@ -124,7 +125,7 @@ export function SourceControlEntryContextMenu({ - + {translate( 'auto.components.right.sidebar.SourceControl.cc05b2d088', diff --git a/src/renderer/src/components/right-sidebar/source-control-fix-split-button.tsx b/src/renderer/src/components/right-sidebar/source-control-fix-split-button.tsx index 26fed796035..cb59d5d580a 100644 --- a/src/renderer/src/components/right-sidebar/source-control-fix-split-button.tsx +++ b/src/renderer/src/components/right-sidebar/source-control-fix-split-button.tsx @@ -112,24 +112,26 @@ export function SourceControlFixSplitButton({ )} {label} - - - + + + + } + />
{worktreeId && groupId && prompt && !disabledReason ? ( setComposerOpen(true)} + onClick={() => setComposerOpen(true)} className="gap-2 rounded-[7px] px-2 py-1.5 text-[12px] leading-5 font-medium" > diff --git a/src/renderer/src/components/right-sidebar/source-control-header-icon-button.tsx b/src/renderer/src/components/right-sidebar/source-control-header-icon-button.tsx index c6905771889..5732296d73b 100644 --- a/src/renderer/src/components/right-sidebar/source-control-header-icon-button.tsx +++ b/src/renderer/src/components/right-sidebar/source-control-header-icon-button.tsx @@ -16,20 +16,22 @@ export function SourceControlHeaderIconButton({ }): React.JSX.Element { return ( - - - + + + + } + /> {label} diff --git a/src/renderer/src/components/right-sidebar/source-control-header-overflow-menu.tsx b/src/renderer/src/components/right-sidebar/source-control-header-overflow-menu.tsx index 8959c50efa0..e7d4a798ea8 100644 --- a/src/renderer/src/components/right-sidebar/source-control-header-overflow-menu.tsx +++ b/src/renderer/src/components/right-sidebar/source-control-header-overflow-menu.tsx @@ -39,24 +39,28 @@ export function SourceControlHeaderOverflowMenu({ return ( - - - - - - - + + + + + } + /> + + } + /> {translate( 'auto.components.right.sidebar.SourceControl.f71c4a8d90', @@ -65,7 +69,7 @@ export function SourceControlHeaderOverflowMenu({ - + {sourceControlViewMode === 'tree' ? ( ) : ( @@ -73,11 +77,11 @@ export function SourceControlHeaderOverflowMenu({ )} {viewModeLabel} - + {translate('auto.components.right.sidebar.SourceControl.476b77745b', 'Change Base Ref')}… - + {translate( 'auto.components.right.sidebar.SourceControl.ed34038d0d', @@ -87,7 +91,7 @@ export function SourceControlHeaderOverflowMenu({ {diffCommentCount > 0 ? ( <> - + {translate('auto.components.right.sidebar.SourceControl.cc474e0b8c', 'Notes')} diff --git a/src/renderer/src/components/right-sidebar/source-control-header-toolbar.tsx b/src/renderer/src/components/right-sidebar/source-control-header-toolbar.tsx index 3cac51e1e9f..4021c88a1b7 100644 --- a/src/renderer/src/components/right-sidebar/source-control-header-toolbar.tsx +++ b/src/renderer/src/components/right-sidebar/source-control-header-toolbar.tsx @@ -81,25 +81,27 @@ function CreatePrHeaderButton({ }): React.JSX.Element { return ( - - - - - + + + + } + /> {action.title} diff --git a/src/renderer/src/components/right-sidebar/source-control-pull-policy-error-notice.tsx b/src/renderer/src/components/right-sidebar/source-control-pull-policy-error-notice.tsx index a0b6f40ef9c..6d55dfd0730 100644 --- a/src/renderer/src/components/right-sidebar/source-control-pull-policy-error-notice.tsx +++ b/src/renderer/src/components/right-sidebar/source-control-pull-policy-error-notice.tsx @@ -108,26 +108,28 @@ export function PullPolicyRemoteActionNotice({

{description}

- - - + handleCopyCommand(option.command)} + > + {copied ? ( +
diff --git a/src/renderer/src/components/settings/AccountsPane.tsx b/src/renderer/src/components/settings/AccountsPane.tsx index 5268a890e36..7fade6b2954 100644 --- a/src/renderer/src/components/settings/AccountsPane.tsx +++ b/src/renderer/src/components/settings/AccountsPane.tsx @@ -1663,16 +1663,18 @@ export function AccountsPane({
- - - + + + {translate('auto.components.settings.AccountsPane.43d7a45b97', 'How to copy')} + + } + /> diff --git a/src/renderer/src/components/settings/AdvancedNetworkSettingsSection.tsx b/src/renderer/src/components/settings/AdvancedNetworkSettingsSection.tsx index e5b8a3b81fe..405e36ba7aa 100644 --- a/src/renderer/src/components/settings/AdvancedNetworkSettingsSection.tsx +++ b/src/renderer/src/components/settings/AdvancedNetworkSettingsSection.tsx @@ -237,22 +237,24 @@ export function AdvancedNetworkSettingsSection({
- - - + + {translate( + 'auto.components.settings.AdvancedNetworkSettingsSection.configureProxy', + 'Configure proxy' + )} + + + } + />
diff --git a/src/renderer/src/components/settings/AdvancedPane.tsx b/src/renderer/src/components/settings/AdvancedPane.tsx index f2b014e2805..ebac0d09976 100644 --- a/src/renderer/src/components/settings/AdvancedPane.tsx +++ b/src/renderer/src/components/settings/AdvancedPane.tsx @@ -67,20 +67,22 @@ export function AdvancedPane({ settings, updateSettings }: AdvancedPaneProps): R 'HTTP/1.1 Compatibility' )} - + - - - + + + + } + /> - - - + void copyActiveCommand()} + > + + + } + /> {translate( 'auto.components.settings.AgentSkillSetupPanel.ed197f59a2', diff --git a/src/renderer/src/components/settings/AgentsPane.tsx b/src/renderer/src/components/settings/AgentsPane.tsx index 99718de46fe..8f592963dbf 100644 --- a/src/renderer/src/components/settings/AgentsPane.tsx +++ b/src/renderer/src/components/settings/AgentsPane.tsx @@ -207,18 +207,20 @@ export function AgentPermissionsSetting({ {translate('auto.components.settings.AgentsPane.agentPermissions', 'Agent Permissions')} - - - + + + + } + /> {translate( 'auto.components.settings.AgentsPane.agentPermissionsTooltip', diff --git a/src/renderer/src/components/settings/AppIconSelector.tsx b/src/renderer/src/components/settings/AppIconSelector.tsx index 8eb653ae799..880d5f6a8d2 100644 --- a/src/renderer/src/components/settings/AppIconSelector.tsx +++ b/src/renderer/src/components/settings/AppIconSelector.tsx @@ -39,11 +39,13 @@ type IconCycleButtonProps = { function IconCycleButton({ label, onClick, children }: IconCycleButtonProps): React.JSX.Element { return ( - - - + + {children} + + } + /> {label} diff --git a/src/renderer/src/components/settings/AutoRenameBranchFromWorkSetting.tsx b/src/renderer/src/components/settings/AutoRenameBranchFromWorkSetting.tsx index b8540cdefa9..6119c9500da 100644 --- a/src/renderer/src/components/settings/AutoRenameBranchFromWorkSetting.tsx +++ b/src/renderer/src/components/settings/AutoRenameBranchFromWorkSetting.tsx @@ -193,22 +193,24 @@ export function AutoRenameBranchFromWorkSetting({
- - - + + {translate( + 'auto.components.settings.AutoRenameBranchFromWorkSetting.e784ea62dc', + 'Advanced' + )} + + + } + />
@@ -235,17 +237,19 @@ export function AutoRenameBranchFromWorkSetting({ "to include Orca's" )}{' '} - - - + + {translate( + 'auto.components.settings.AutoRenameBranchFromWorkSetting.9c9b54e4ea', + 'built-in branch-name prompt' + )} + + } + /> - - - + + {isImporting ? ( + + ) : ( + + )} + {translate( + 'auto.components.settings.BrowserProfileRow.cdec84552f', + 'Import Cookies' + )} + + } + /> {detectedBrowsers.map((browser) => browser.profiles.length > 1 ? ( @@ -185,9 +190,7 @@ export function BrowserProfileRow({ {browser.profiles.map((bp) => ( - void handleImportFromBrowser(browser.family, bp.directory) - } + onClick={() => void handleImportFromBrowser(browser.family, bp.directory)} > {bp.name} @@ -198,7 +201,7 @@ export function BrowserProfileRow({ ) : ( void handleImportFromBrowser(browser.family)} + onClick={() => void handleImportFromBrowser(browser.family)} > {translate( 'auto.components.settings.BrowserProfileRow.c5a273a809', @@ -209,7 +212,7 @@ export function BrowserProfileRow({ ) )} {detectedBrowsers.length > 0 && } - void handleImportFromFile()}> + void handleImportFromFile()}> {translate('auto.components.settings.BrowserProfileRow.ebb78dfd6f', 'From File…')} diff --git a/src/renderer/src/components/settings/BrowserUseCliStep.tsx b/src/renderer/src/components/settings/BrowserUseCliStep.tsx index e3e2070ec2d..867a8209919 100644 --- a/src/renderer/src/components/settings/BrowserUseCliStep.tsx +++ b/src/renderer/src/components/settings/BrowserUseCliStep.tsx @@ -57,32 +57,37 @@ export function BrowserUseCliStep({

{cliStatus.detail}

) : null}
- + - - - - - + + + + } + /> {!cliSupported && !cliLoading && cliStatus?.detail ? ( {cliStatus.detail} diff --git a/src/renderer/src/components/settings/BrowserUseCookieImportStep.tsx b/src/renderer/src/components/settings/BrowserUseCookieImportStep.tsx index f73668fa908..52406304604 100644 --- a/src/renderer/src/components/settings/BrowserUseCookieImportStep.tsx +++ b/src/renderer/src/components/settings/BrowserUseCookieImportStep.tsx @@ -141,23 +141,25 @@ export function BrowserUseCookieImportStep({ } }} > - - - + + {isImportingDefault ? ( + + ) : ( + + )} + {cookiesImported + ? translate('auto.components.settings.BrowserUsePane.0462565413', 'Re-import') + : translate('auto.components.settings.BrowserUsePane.2ccfc9cff8', 'Import')} + + } + /> {detectedBrowsers.map((browser) => browser.profiles.length > 1 ? ( @@ -174,9 +176,7 @@ export function BrowserUseCookieImportStep({ {browser.profiles.map((bp) => ( - void handleImportFromBrowser(browser.family, bp.directory) - } + onClick={() => void handleImportFromBrowser(browser.family, bp.directory)} > {bp.name} @@ -187,7 +187,7 @@ export function BrowserUseCookieImportStep({ ) : ( void handleImportFromBrowser(browser.family)} + onClick={() => void handleImportFromBrowser(browser.family)} > {translate( 'auto.components.settings.BrowserUsePane.5301857d88', @@ -198,7 +198,7 @@ export function BrowserUseCookieImportStep({ ) )} {detectedBrowsers.length > 0 ? : null} - void handleImportFromFile()}> + void handleImportFromFile()}> {translate('auto.components.settings.BrowserUsePane.be6df68384', 'From File…')} diff --git a/src/renderer/src/components/settings/BrowserUseExamples.tsx b/src/renderer/src/components/settings/BrowserUseExamples.tsx index 1bcbad97a28..6eec9ab8e7b 100644 --- a/src/renderer/src/components/settings/BrowserUseExamples.tsx +++ b/src/renderer/src/components/settings/BrowserUseExamples.tsx @@ -56,21 +56,23 @@ export function BrowserUseExamples(): React.JSX.Element { {prompt} {translate('auto.components.settings.BrowserUseExamples.b84807f228', '"')}

- + - - - + void handleCopyText(prompt, 'prompt')} + aria-label={translate( + 'auto.components.settings.BrowserUseExamples.1188e56af4', + 'Copy example prompt' + )} + > + + + } + /> {translate('auto.components.settings.BrowserUseExamples.1199258ace', 'Copy')} diff --git a/src/renderer/src/components/settings/CliSection.tsx b/src/renderer/src/components/settings/CliSection.tsx index 20380332009..4c8d984207b 100644 --- a/src/renderer/src/components/settings/CliSection.tsx +++ b/src/renderer/src/components/settings/CliSection.tsx @@ -261,22 +261,24 @@ export function CliSection({

- + - - - + void refreshStatus()} + disabled={loading || busyAction !== null} + aria-label={translate( + 'auto.components.settings.CliSection.52e640f3a0', + 'Refresh CLI status' + )} + > + + + } + /> {translate('auto.components.settings.CliSection.5dae812f50', 'Refresh')} diff --git a/src/renderer/src/components/settings/FloatingWorkspacePane.tsx b/src/renderer/src/components/settings/FloatingWorkspacePane.tsx index acffb2d007c..437652d50b2 100644 --- a/src/renderer/src/components/settings/FloatingWorkspacePane.tsx +++ b/src/renderer/src/components/settings/FloatingWorkspacePane.tsx @@ -167,14 +167,13 @@ export function FloatingWorkspacePane({ )} control={ { - if (!value) { + if (!value[0]) { return } updateSettings({ - floatingTerminalTriggerLocation: value as FloatingTerminalTriggerLocation + floatingTerminalTriggerLocation: value[0] as FloatingTerminalTriggerLocation }) useAppStore.getState().recordFeatureInteraction('floating-workspace') }} diff --git a/src/renderer/src/components/settings/KeybindingsFileActions.tsx b/src/renderer/src/components/settings/KeybindingsFileActions.tsx index 24a6a5116da..02979f44cda 100644 --- a/src/renderer/src/components/settings/KeybindingsFileActions.tsx +++ b/src/renderer/src/components/settings/KeybindingsFileActions.tsx @@ -167,36 +167,38 @@ export function KeybindingsFileActions(): React.JSX.Element { )} - - - + + + + } + /> - void openKeybindingsFile()}> + void openKeybindingsFile()}> {translate( 'auto.components.settings.KeybindingsFileActions.98f1a23e1c', 'Open with Default App' )} - void openKeybindingsInExternalEditor('code')}> + void openKeybindingsInExternalEditor('code')}> {translate( 'auto.components.settings.KeybindingsFileActions.1637f64033', 'Open in VS Code' )} - void openKeybindingsInExternalEditor('cursor')}> + void openKeybindingsInExternalEditor('cursor')}> {translate( 'auto.components.settings.KeybindingsFileActions.9e24c0e858', @@ -204,14 +206,14 @@ export function KeybindingsFileActions(): React.JSX.Element { )} - void revealKeybindingsFile()}> + void revealKeybindingsFile()}> {translate( 'auto.components.settings.KeybindingsFileActions.a8a8d6b9d3', 'Reveal in File Manager' )} - void reloadKeybindings()}> + void reloadKeybindings()}> {translate( 'auto.components.settings.KeybindingsFileActions.abc49853fb', diff --git a/src/renderer/src/components/settings/ManageSessionKillDialog.tsx b/src/renderer/src/components/settings/ManageSessionKillDialog.tsx index 56ddb9845ee..5670abb8780 100644 --- a/src/renderer/src/components/settings/ManageSessionKillDialog.tsx +++ b/src/renderer/src/components/settings/ManageSessionKillDialog.tsx @@ -27,32 +27,20 @@ export function ManageSessionKillDialog({ return ( { + onOpenChange={(open, eventDetails) => { if (open) { return } // Why: destructive terminal mutations should keep their progress // dialog open until the daemon responds, matching other confirm flows. if (isBusy) { + eventDetails.cancel() return } onCancel() }} > - { - if (isBusy) { - event.preventDefault() - } - }} - onEscapeKeyDown={(event) => { - if (isBusy) { - event.preventDefault() - } - }} - > + {session ? ( <> diff --git a/src/renderer/src/components/settings/ManageSessionsTable.tsx b/src/renderer/src/components/settings/ManageSessionsTable.tsx index ed44276abb3..f2ebcd1e03b 100644 --- a/src/renderer/src/components/settings/ManageSessionsTable.tsx +++ b/src/renderer/src/components/settings/ManageSessionsTable.tsx @@ -58,25 +58,27 @@ export function ManageSessionsTable({
- - - + + {daemonBusyKind === 'killAll' ? ( + + ) : ( + + )} + + } + /> {translate( 'auto.components.settings.ManageSessionsSection.3282db098c', @@ -85,25 +87,27 @@ export function ManageSessionsTable({ - - - + + {daemonBusyKind === 'restart' ? ( + + ) : ( + + )} + + } + /> {translate( 'auto.components.settings.ManageSessionsSection.5ed15e778c', diff --git a/src/renderer/src/components/settings/MobileEmulatorAgentControlRow.tsx b/src/renderer/src/components/settings/MobileEmulatorAgentControlRow.tsx index 17946134bc1..196c80dec2c 100644 --- a/src/renderer/src/components/settings/MobileEmulatorAgentControlRow.tsx +++ b/src/renderer/src/components/settings/MobileEmulatorAgentControlRow.tsx @@ -112,26 +112,28 @@ export function MobileEmulatorAgentControlRow(): React.JSX.Element {

) : null}
- + - - - - - + + + + } + /> {!setup.cliSupported && !setup.cliLoading && setup.cliInstallStatus?.detail ? ( {setup.cliInstallStatus.detail} diff --git a/src/renderer/src/components/settings/MobileEmulatorExamples.tsx b/src/renderer/src/components/settings/MobileEmulatorExamples.tsx index 82b454b98e5..918a397ba8d 100644 --- a/src/renderer/src/components/settings/MobileEmulatorExamples.tsx +++ b/src/renderer/src/components/settings/MobileEmulatorExamples.tsx @@ -68,22 +68,24 @@ export function MobileEmulatorExamples({ {prompt} {translate('auto.components.settings.MobileEmulatorExamples.d151e25078', '"')}

- + - - - + void copyPrompt(prompt)} + > + + + } + /> {translate('auto.components.settings.MobileEmulatorExamples.edf13dd03b', 'Copy')} diff --git a/src/renderer/src/components/settings/MobileEmulatorSettingsPane.tsx b/src/renderer/src/components/settings/MobileEmulatorSettingsPane.tsx index 69d420bbdca..e5e5a51fa8f 100644 --- a/src/renderer/src/components/settings/MobileEmulatorSettingsPane.tsx +++ b/src/renderer/src/components/settings/MobileEmulatorSettingsPane.tsx @@ -274,13 +274,13 @@ export function MobileEmulatorSettingsPane({ - + {AUTOMATIC_DEVICE_LABEL} {devices.map((device) => ( diff --git a/src/renderer/src/components/settings/MobilePairingSetupSection.tsx b/src/renderer/src/components/settings/MobilePairingSetupSection.tsx index caf1f6b6873..b5cfe06c410 100644 --- a/src/renderer/src/components/settings/MobilePairingSetupSection.tsx +++ b/src/renderer/src/components/settings/MobilePairingSetupSection.tsx @@ -96,22 +96,24 @@ export function MobilePairingSetupSection({ className="min-w-[220px] justify-between font-normal" /> - - - + + + + } + /> {translate( 'auto.components.settings.MobilePairingSetupSection.refresh', @@ -122,7 +124,7 @@ export function MobilePairingSetupSection({
- + {translate( diff --git a/src/renderer/src/components/settings/NativeChatExperimentalSetting.tsx b/src/renderer/src/components/settings/NativeChatExperimentalSetting.tsx index a8c25191172..469fe6687f7 100644 --- a/src/renderer/src/components/settings/NativeChatExperimentalSetting.tsx +++ b/src/renderer/src/components/settings/NativeChatExperimentalSetting.tsx @@ -76,7 +76,7 @@ export function NativeChatExperimentalSetting({
{ + if (value) { + handleDistroChange(value) + } + }} disabled={wslCapabilitiesLoading || !wslAvailable} > diff --git a/src/renderer/src/components/settings/QuickCommandsScopeFilter.tsx b/src/renderer/src/components/settings/QuickCommandsScopeFilter.tsx index e845b9dd0b4..cc5da37b07c 100644 --- a/src/renderer/src/components/settings/QuickCommandsScopeFilter.tsx +++ b/src/renderer/src/components/settings/QuickCommandsScopeFilter.tsx @@ -68,22 +68,24 @@ export function QuickCommandsScopeFilter({ return (
- - - + + + + + } + /> - +
{envVars.map(({ name, description }) => ( - - - {name} - - + + {name} + + } + /> {description} diff --git a/src/renderer/src/components/settings/RepositoryHostSetupsSection.tsx b/src/renderer/src/components/settings/RepositoryHostSetupsSection.tsx index 4506d04a0a8..1e5afcc2229 100644 --- a/src/renderer/src/components/settings/RepositoryHostSetupsSection.tsx +++ b/src/renderer/src/components/settings/RepositoryHostSetupsSection.tsx @@ -127,7 +127,7 @@ export function RepositoryHostSetupsSection({ onActionModeChange(actionId, value)} + onValueChange={(value) => value && onActionModeChange(actionId, value)} > @@ -168,7 +168,7 @@ export function RepositorySourceControlAiActionRows({ onChange(row.key, nextValue)} + onValueChange={(nextValue) => nextValue && onChange(row.key, nextValue)} > diff --git a/src/renderer/src/components/settings/RuntimeAccessGrantList.tsx b/src/renderer/src/components/settings/RuntimeAccessGrantList.tsx index 82e4e4d8897..517746e4d9a 100644 --- a/src/renderer/src/components/settings/RuntimeAccessGrantList.tsx +++ b/src/renderer/src/components/settings/RuntimeAccessGrantList.tsx @@ -42,21 +42,23 @@ export function RuntimeAccessGrantList({ )} - - - + + + + } + /> {translate( 'auto.components.settings.RuntimeAccessGrantList.27cf8507ad', @@ -114,27 +116,29 @@ export function RuntimeAccessGrantList({
- - - + onRevoke(grant)} + disabled={isRevoking} + aria-label={translate( + 'auto.components.settings.RuntimeAccessGrantList.6f6d5188ed', + 'Revoke {{value0}}', + { value0: grant.name } + )} + > + {isRevoking ? ( + + ) : ( + + )} + + } + /> {translate( 'auto.components.settings.RuntimeAccessGrantList.68ec21309f', diff --git a/src/renderer/src/components/settings/RuntimePairingGeneratorForm.tsx b/src/renderer/src/components/settings/RuntimePairingGeneratorForm.tsx index 5557f61765f..50b4efb902e 100644 --- a/src/renderer/src/components/settings/RuntimePairingGeneratorForm.tsx +++ b/src/renderer/src/components/settings/RuntimePairingGeneratorForm.tsx @@ -122,22 +122,24 @@ export function RuntimePairingGeneratorForm({ {/* Why: server sharing uses the same interface list as Mobile, and VPN/tailnet addresses can appear after Settings opens. */} - - - + + + + } + /> {translate( 'auto.components.settings.RuntimePairingUrlGenerator.360c548cf3', diff --git a/src/renderer/src/components/settings/SettingsFormControls.tsx b/src/renderer/src/components/settings/SettingsFormControls.tsx index 146e0000f62..db2d26c842e 100644 --- a/src/renderer/src/components/settings/SettingsFormControls.tsx +++ b/src/renderer/src/components/settings/SettingsFormControls.tsx @@ -3,6 +3,7 @@ co-locate shared layout and keyboard interaction logic, which keeps the settings panel wiring simple even though the file exceeds the default line limit. */ import type React from 'react' import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react' +import type { Popover as PopoverPrimitive } from '@base-ui/react/popover' import { ScrollArea } from '../ui/scroll-area' import { Input } from '../ui/input' import { Label } from '../ui/label' @@ -626,7 +627,20 @@ export function FontAutocomplete({ onRequestSuggestions?.() }, [onRequestSuggestions]) - const handleOpenChange = (nextOpen: boolean): void => { + const handleOpenChange = ( + nextOpen: boolean, + eventDetails: PopoverPrimitive.Root.ChangeEventDetails + ): void => { + // Why: the input and its clear/toggle buttons are the anchor, not the + // content, so an outside-press on them must not dismiss the popup. + if ( + !nextOpen && + eventDetails.reason === 'outside-press' && + rootRef.current?.contains(eventDetails.event.target as Node) + ) { + eventDetails.cancel() + return + } setOpen(nextOpen) if (nextOpen) { requestSuggestions() @@ -706,130 +720,132 @@ export function FontAutocomplete({ return (
- -
- { - const next = e.target.value - requestSuggestions() - setQuery(next) - setIsFilteringQuery(true) - onChange(next) - setOpen(true) - }} - onFocus={() => { - requestSuggestions() - setIsFilteringQuery(false) - setOpen(true) - }} - onKeyDown={(e) => { - if (e.key === 'Escape') { - if (open) { - e.preventDefault() - setOpen(false) - setIsFilteringQuery(false) - } - return - } - - if (e.key === 'ArrowDown') { - e.preventDefault() + + { + const next = e.target.value + requestSuggestions() + setQuery(next) + setIsFilteringQuery(true) + onChange(next) + setOpen(true) + }} + onFocus={() => { + requestSuggestions() + setIsFilteringQuery(false) setOpen(true) - if (visibleSuggestions.length > 0) { - setHighlightedIndex((current) => - current < 0 ? 0 : Math.min(current + 1, visibleSuggestions.length - 1) - ) + }} + onKeyDown={(e) => { + if (e.key === 'Escape') { + if (open) { + e.preventDefault() + setOpen(false) + setIsFilteringQuery(false) + } + return } - return - } - if (e.key === 'ArrowUp') { - e.preventDefault() - setOpen(true) - if (visibleSuggestions.length > 0) { - setHighlightedIndex((current) => - current < 0 ? visibleSuggestions.length - 1 : Math.max(current - 1, 0) - ) + if (e.key === 'ArrowDown') { + e.preventDefault() + setOpen(true) + if (visibleSuggestions.length > 0) { + setHighlightedIndex((current) => + current < 0 ? 0 : Math.min(current + 1, visibleSuggestions.length - 1) + ) + } + return } - return - } - if (e.key === 'Enter' && open && highlightedIndex >= 0) { - const highlightedFont = visibleSuggestions[highlightedIndex] - if (highlightedFont) { + if (e.key === 'ArrowUp') { e.preventDefault() - commitValue(highlightedFont) + setOpen(true) + if (visibleSuggestions.length > 0) { + setHighlightedIndex((current) => + current < 0 ? visibleSuggestions.length - 1 : Math.max(current - 1, 0) + ) + } + return + } + + if (e.key === 'Enter' && open && highlightedIndex >= 0) { + const highlightedFont = visibleSuggestions[highlightedIndex] + if (highlightedFont) { + e.preventDefault() + commitValue(highlightedFont) + } } + }} + placeholder={placeholder} + className="pr-18" + role="combobox" + aria-autocomplete="list" + aria-expanded={open} + aria-controls={listboxId} + aria-activedescendant={ + open && highlightedIndex >= 0 + ? `${listboxId}-option-${highlightedIndex}` + : undefined } - }} - placeholder={placeholder} - className="pr-18" - role="combobox" - aria-autocomplete="list" - aria-expanded={open} - aria-controls={listboxId} - aria-activedescendant={ - open && highlightedIndex >= 0 - ? `${listboxId}-option-${highlightedIndex}` - : undefined - } - /> -
- {query ? ( + /> +
+ {query ? ( + + ) : null} - ) : null} - +
-
-
+ } + /> {/* Why: portal the dropdown outside the settings section — an in-flow absolute panel makes the highlighted option's scrollIntoView scroll @@ -837,15 +853,8 @@ export function FontAutocomplete({ e.preventDefault()} - onCloseAutoFocus={(e) => e.preventDefault()} - onInteractOutside={(e) => { - // Why: the input and its clear/toggle buttons are the anchor, not - // the content, so Radix would otherwise dismiss on every click there. - if (rootRef.current?.contains(e.target as Node)) { - e.preventDefault() - } - }} + initialFocus={false} + finalFocus={false} > 8 ? 'h-64' : undefined} diff --git a/src/renderer/src/components/settings/ShortcutCommandBlock.tsx b/src/renderer/src/components/settings/ShortcutCommandBlock.tsx index 853e6ae3c62..3c3b858875e 100644 --- a/src/renderer/src/components/settings/ShortcutCommandBlock.tsx +++ b/src/renderer/src/components/settings/ShortcutCommandBlock.tsx @@ -139,15 +139,17 @@ export function ShortcutCommandBlock({ ) : null} {terminalStatus && hasBinding ? ( - - - - {terminalStatus.label} - - + + + {terminalStatus.label} + + } + /> {terminalStatus.description} @@ -162,22 +164,24 @@ export function ShortcutCommandBlock({
{hasBinding && !showAppendSlot ? ( - - - + onAppendBinding(item.id)} + > + + + } + /> {translate( 'auto.components.settings.ShortcutCommandBlock.245c83af24', @@ -188,22 +192,24 @@ export function ShortcutCommandBlock({ ) : null} {modified ? ( - - - + onResetAction(item.id)} + > + + + } + /> {translate( 'auto.components.settings.ShortcutCommandBlock.9b02917027', @@ -214,22 +220,24 @@ export function ShortcutCommandBlock({ ) : null} {hasBinding ? ( - - - + onDisableAction(item.id)} + > + + + } + /> {translate( 'auto.components.settings.ShortcutCommandBlock.25e6e76618', @@ -271,22 +279,24 @@ export function ShortcutCommandBlock({ ) : ( - - - + onAppendBinding(item.id)} + > + + + } + /> {translate( 'auto.components.settings.ShortcutCommandBlock.035a822ef0', diff --git a/src/renderer/src/components/settings/ShortcutRecorderButton.tsx b/src/renderer/src/components/settings/ShortcutRecorderButton.tsx index 1961fbf50fc..8deda2bbc50 100644 --- a/src/renderer/src/components/settings/ShortcutRecorderButton.tsx +++ b/src/renderer/src/components/settings/ShortcutRecorderButton.tsx @@ -186,45 +186,47 @@ export function ShortcutRecorderButton({ return ( - - - + { + if (!recording) { + onStartRecording(actionId, bindingIndex) + } + }} + onKeyDown={handleRecordKeyDown} + onKeyUp={handleRecordKeyUp} + className={cn( + 'flex min-h-7 min-w-[5.5rem] max-w-[14rem] items-center justify-end gap-1.5 overflow-hidden rounded-md border px-2 py-1 text-xs outline-none transition-colors focus-visible:ring-[3px] focus-visible:ring-ring/50', + recording + ? 'border-ring bg-accent text-accent-foreground ring-[3px] ring-ring/30' + : 'border-transparent hover:border-border/70 hover:bg-background' + )} + > + {recording || binding === null ? ( + + {translate( + 'auto.components.settings.ShortcutRecorderButton.f5ed5dcbf6', + 'Press keys…' + )} + + ) : ( + + + + )} + + } + /> {tooltipLabel} diff --git a/src/renderer/src/components/settings/ShortcutRemoveButton.tsx b/src/renderer/src/components/settings/ShortcutRemoveButton.tsx index e669c74ad72..f26f0dbf26f 100644 --- a/src/renderer/src/components/settings/ShortcutRemoveButton.tsx +++ b/src/renderer/src/components/settings/ShortcutRemoveButton.tsx @@ -21,22 +21,24 @@ export function ShortcutRemoveButton({ }): React.JSX.Element { return ( - - - + onRemove(actionId, bindingIndex)} + > + + + } + /> {translate( 'auto.components.settings.ShortcutRemoveButton.2a9588b1c2', diff --git a/src/renderer/src/components/settings/SourceControlActionRecipeRow.tsx b/src/renderer/src/components/settings/SourceControlActionRecipeRow.tsx index cb0b4c8dc5a..4060a08fe7d 100644 --- a/src/renderer/src/components/settings/SourceControlActionRecipeRow.tsx +++ b/src/renderer/src/components/settings/SourceControlActionRecipeRow.tsx @@ -86,7 +86,11 @@ export function SourceControlActionRecipeRow({
setValue(event.currentTarget.value)} + onKeyDown={(event) => { + if (event.key === 'Enter' && value.trim()) { + void onSubmit(value.trim()) + } + }} + /> + ) : null} + + + + + +
+ ) +} + +function getActionCopy(action: SpoolFileAction | null): { + title: string + description: string + label: string +} { + switch (action?.kind) { + case 'new-file': + return { + title: translate('auto.components.spool.SpoolFileActionDialog.newFileTitle', 'New file'), + description: translate( + 'auto.components.spool.SpoolFileActionDialog.newFileDescription', + 'Create an empty file in the current remote folder.' + ), + label: translate('auto.components.spool.SpoolFileActionDialog.create', 'Create') + } + case 'new-directory': + return { + title: translate( + 'auto.components.spool.SpoolFileActionDialog.newDirectoryTitle', + 'New directory' + ), + description: translate( + 'auto.components.spool.SpoolFileActionDialog.newDirectoryDescription', + 'Create a directory in the current remote folder.' + ), + label: translate('auto.components.spool.SpoolFileActionDialog.create', 'Create') + } + case 'rename': + return { + title: translate('auto.components.spool.SpoolFileActionDialog.renameTitle', 'Rename item'), + description: translate( + 'auto.components.spool.SpoolFileActionDialog.renameDescription', + 'Enter a new name for {{value0}}.', + { value0: action.entry.name } + ), + label: translate('auto.components.spool.SpoolFileActionDialog.rename', 'Rename') + } + case 'delete': + return { + title: translate('auto.components.spool.SpoolFileActionDialog.deleteTitle', 'Delete item?'), + description: translate( + 'auto.components.spool.SpoolFileActionDialog.deleteDescription', + 'This permanently deletes {{value0}} from the owner’s worktree.', + { value0: action.entry.name } + ), + label: translate('auto.components.spool.SpoolFileActionDialog.delete', 'Delete') + } + case undefined: + return { title: '', description: '', label: '' } + } +} diff --git a/src/renderer/src/components/spool/SpoolFilePreview.tsx b/src/renderer/src/components/spool/SpoolFilePreview.tsx new file mode 100644 index 00000000000..cd00dac8d59 --- /dev/null +++ b/src/renderer/src/components/spool/SpoolFilePreview.tsx @@ -0,0 +1,304 @@ +import type React from 'react' +import { useState } from 'react' +import { ChevronLeft } from 'lucide-react' +import type { + SpoolFileDiffResult, + SpoolFileReadResult, + SpoolFileTreeEntry +} from '../../../../shared/spool/spool-operation-contract' +import { translate } from '@/i18n/i18n' +import { cn } from '@/lib/utils' +import { Button } from '@/components/ui/button' +import { SpoolFilePreviewToolbar, type SpoolFilePreviewMode } from './SpoolFilePreviewToolbar' + +export function SpoolFilePreview({ + canControl, + draft, + file, + fileEntry, + fileUnavailable, + loading, + saving, + supportsDiff, + diff, + diffLoading, + diffUnavailable, + onDelete, + onBack, + onDraftChange, + onLoadDiff, + onNextChunk, + onPreviousChunk, + onRefresh, + onRename, + onSave +}: { + canControl: boolean + draft: string + file: SpoolFileReadResult | null + fileEntry: SpoolFileTreeEntry | null + fileUnavailable: boolean + loading: boolean + saving: boolean + supportsDiff: boolean + diff: SpoolFileDiffResult | null + diffLoading: boolean + diffUnavailable: boolean + onDelete: () => void + onBack: () => void + onDraftChange: (value: string) => void + onLoadDiff: (staged: boolean) => void + onNextChunk: () => void + onPreviousChunk: () => void + onRefresh: () => void + onRename: () => void + onSave: () => void +}): React.JSX.Element { + const [mode, setMode] = useState('content') + + if (loading) { + return ( + + ) + } + if (fileUnavailable) { + return ( + + ) + } + if (!file || !fileEntry) { + return ( + + ) + } + + const dirty = file.encoding === 'utf8' && draft !== file.content + const completeFile = file.offset === 0 && file.bytesRead === file.totalBytes + const editable = canControl && file.encoding === 'utf8' && completeFile + const showDiff = supportsDiff && mode !== 'content' + const changeMode = (nextMode: SpoolFilePreviewMode): void => { + setMode(nextMode) + if (nextMode !== 'content') { + onLoadDiff(nextMode === 'staged-diff') + } + } + return ( +
+ + {showDiff ? ( + + ) : file.encoding === 'utf8' ? ( + + ) : ( + + )} +
+ ) +} + +function TextProjection({ + draft, + editable, + file, + onDraftChange +}: { + draft: string + editable: boolean + file: SpoolFileReadResult + onDraftChange: (value: string) => void +}): React.JSX.Element { + return ( +
+ {file.offset > 0 || file.truncated ? ( + + ) : null} +