Evaluate localized strings dynamically in static UI structures

- Use ES6 getters with `translate()` in static object maps (such as SSH
  connection status, usage ranges, and feature tour copy) to evaluate
  strings on demand when the language changes.
- Add the `useTranslation()` hook to `LinearIssueMarkdownToolbar` so it
  subscribes directly to translation updates.
- Extract markdown slash command primitives to a separate file to keep
  catalog configuration clean.
This commit is contained in:
Jinjing
2026-06-11 19:30:21 -07:00
parent 45c365e975
commit ffe4e4920c
12 changed files with 529 additions and 377 deletions
@@ -1,5 +1,6 @@
import React, { useCallback } from 'react'
import type { Editor } from '@tiptap/react'
import { useTranslation } from 'react-i18next'
import {
Bold,
Code,
@@ -94,6 +95,8 @@ export function LinearIssueMarkdownToolbar({
editor: Editor | null
disabled: boolean
}): React.JSX.Element {
// Why: this toolbar can outlive editor recreation, so subscribe directly to language changes.
useTranslation()
const runCommand = useCallback(
(command: (editor: Editor) => void) => {
if (!editor || disabled) {
@@ -100,21 +100,50 @@ type NewWorkspaceComposerCardProps = {
sparseControlsEnabled?: boolean
}
const SSH_STATUS_LABEL_FALLBACKS: Partial<Record<SshConnectionStatus, string>> = {
disconnected: 'SSH not connected',
connecting: 'Connecting SSH...',
'auth-failed': 'SSH authentication failed',
'deploying-relay': 'Preparing SSH connection...',
connected: 'Connected',
reconnecting: 'Reconnecting SSH...',
'reconnection-failed': 'SSH reconnection failed'
const SSH_STATUS_LABELS: Partial<Record<SshConnectionStatus, string>> = {
get disconnected() {
return translate(
'auto.components.NewWorkspaceComposerCard.sshNotConnected',
'SSH not connected'
)
},
get connecting() {
return translate('auto.components.NewWorkspaceComposerCard.connectingSsh', 'Connecting SSH...')
},
get 'auth-failed'() {
return translate(
'auto.components.NewWorkspaceComposerCard.sshAuthenticationFailed',
'SSH authentication failed'
)
},
get 'deploying-relay'() {
return translate(
'auto.components.NewWorkspaceComposerCard.preparingSshConnection',
'Preparing SSH connection...'
)
},
get connected() {
return translate('auto.components.NewWorkspaceComposerCard.connected', 'Connected')
},
get reconnecting() {
return translate(
'auto.components.NewWorkspaceComposerCard.reconnectingSsh',
'Reconnecting SSH...'
)
},
get 'reconnection-failed'() {
return translate(
'auto.components.NewWorkspaceComposerCard.sshReconnectionFailed',
'SSH reconnection failed'
)
},
get error() {
return translate('auto.components.NewWorkspaceComposerCard.a239038146', 'SSH connection error')
}
}
function getSshStatusLabel(status: SshConnectionStatus): string {
if (status === 'error') {
return translate('auto.components.NewWorkspaceComposerCard.a239038146', 'SSH connection error')
}
return SSH_STATUS_LABEL_FALLBACKS[status] ?? status
return SSH_STATUS_LABELS[status] ?? status
}
function SetupCommandPreview({
@@ -308,7 +337,7 @@ export default function NewWorkspaceComposerCard({
}, [eligibleRepos, repoId])
const sshStatusLabel = selectedRepoSshStatus
? getSshStatusLabel(selectedRepoSshStatus)
: 'Not connected'
: translate('auto.components.NewWorkspaceComposerCard.notConnected', 'Not connected')
const connectButtonLabel =
selectedRepoSshStatus === 'disconnected' || selectedRepoSshStatus === null
? 'Connect'
@@ -1,6 +1,3 @@
import type React from 'react'
import type { Editor } from '@tiptap/react'
import { TextSelection } from '@tiptap/pm/state'
import type {} from '@tiptap/extension-mathematics'
import {
ChevronRight,
@@ -17,119 +14,22 @@ import {
} from 'lucide-react'
import { translate } from '@/i18n/i18n'
export type SlashMenuState = {
query: string
from: number
to: number
left: number
top: number
}
import {
icon,
insertCodeBlock,
insertTextWithSelection,
insertToggle,
textIcon,
type SlashCommand
} from './rich-markdown-slash-command-primitives'
export type SlashCommandId =
| 'text'
| 'toggle-text'
| 'heading-1'
| 'toggle-h1'
| 'heading-2'
| 'heading-3'
| 'task-list'
| 'bullet-list'
| 'ordered-list'
| 'blockquote'
| 'code-block'
| 'divider'
| 'image'
| 'table'
| 'mermaid'
| 'inline-math'
| 'math-block'
| 'emoji'
export type SlashCommandIcon =
| { kind: 'component'; component: React.ComponentType<{ className?: string }> }
| { kind: 'text'; value: string }
export type SlashCommandGroup = 'Headings' | 'Basic blocks' | 'Advanced' | 'Media' | 'Others'
export type SlashCommand = {
id: SlashCommandId
label: string
aliases: string[]
icon: SlashCommandIcon
group: SlashCommandGroup
description: string
run: (editor: Editor) => void
}
function icon(component: React.ComponentType<{ className?: string }>): SlashCommandIcon {
return { kind: 'component', component }
}
function textIcon(value: string): SlashCommandIcon {
return { kind: 'text', value }
}
function insertTextWithSelection(
editor: Editor,
text: string,
selectionStartOffset?: number,
selectionEndOffset = selectionStartOffset
): void {
editor.commands.command(({ state, dispatch }) => {
const from = state.selection.from
const tr = state.tr.insertText(text, from, state.selection.to)
if (selectionStartOffset !== undefined) {
const selectionFrom = from + selectionStartOffset
const selectionTo = from + (selectionEndOffset ?? selectionStartOffset)
tr.setSelection(TextSelection.create(tr.doc, selectionFrom, selectionTo))
}
dispatch?.(tr.scrollIntoView())
return true
})
}
function insertCodeBlock(editor: Editor, language: string, text: string): void {
editor.commands.command(({ state, dispatch }) => {
const codeBlockType = state.schema.nodes.codeBlock
if (!codeBlockType) {
return false
}
const node = codeBlockType.create({ language }, text ? state.schema.text(text) : undefined)
const tr = state.tr.replaceSelectionWith(node).scrollIntoView()
const cursor = tr.selection.from + 1
tr.setSelection(TextSelection.create(tr.doc, cursor, cursor))
dispatch?.(tr)
return true
})
}
function insertToggle(editor: Editor, variant?: 'heading-1'): void {
const insertAt = editor.state.selection.from
editor
.chain()
.focus()
.insertContentAt(insertAt, {
type: 'details',
attrs: {
open: true,
...(variant ? { variant } : {})
},
content: [
{
type: 'detailsSummary'
},
{
type: 'detailsContent',
content: [{ type: 'paragraph' }]
}
]
})
.setTextSelection(insertAt + 1)
.run()
}
export type {
SlashCommand,
SlashCommandGroup,
SlashCommandIcon,
SlashCommandId,
SlashMenuState
} from './rich-markdown-slash-command-primitives'
export const slashCommands: SlashCommand[] = [
{
@@ -0,0 +1,117 @@
import type React from 'react'
import type { Editor } from '@tiptap/react'
import { TextSelection } from '@tiptap/pm/state'
export type SlashMenuState = {
query: string
from: number
to: number
left: number
top: number
}
export type SlashCommandId =
| 'text'
| 'toggle-text'
| 'heading-1'
| 'toggle-h1'
| 'heading-2'
| 'heading-3'
| 'task-list'
| 'bullet-list'
| 'ordered-list'
| 'blockquote'
| 'code-block'
| 'divider'
| 'image'
| 'table'
| 'mermaid'
| 'inline-math'
| 'math-block'
| 'emoji'
export type SlashCommandIcon =
| { kind: 'component'; component: React.ComponentType<{ className?: string }> }
| { kind: 'text'; value: string }
export type SlashCommandGroup = 'Headings' | 'Basic blocks' | 'Advanced' | 'Media' | 'Others'
export type SlashCommand = {
id: SlashCommandId
label: string
aliases: string[]
icon: SlashCommandIcon
group: SlashCommandGroup
description: string
run: (editor: Editor) => void
}
export function icon(component: React.ComponentType<{ className?: string }>): SlashCommandIcon {
return { kind: 'component', component }
}
export function textIcon(value: string): SlashCommandIcon {
return { kind: 'text', value }
}
export function insertTextWithSelection(
editor: Editor,
text: string,
selectionStartOffset?: number,
selectionEndOffset = selectionStartOffset
): void {
editor.commands.command(({ state, dispatch }) => {
const from = state.selection.from
const tr = state.tr.insertText(text, from, state.selection.to)
if (selectionStartOffset !== undefined) {
const selectionFrom = from + selectionStartOffset
const selectionTo = from + (selectionEndOffset ?? selectionStartOffset)
tr.setSelection(TextSelection.create(tr.doc, selectionFrom, selectionTo))
}
dispatch?.(tr.scrollIntoView())
return true
})
}
export function insertCodeBlock(editor: Editor, language: string, text: string): void {
editor.commands.command(({ state, dispatch }) => {
const codeBlockType = state.schema.nodes.codeBlock
if (!codeBlockType) {
return false
}
const node = codeBlockType.create({ language }, text ? state.schema.text(text) : undefined)
const tr = state.tr.replaceSelectionWith(node).scrollIntoView()
const cursor = tr.selection.from + 1
tr.setSelection(TextSelection.create(tr.doc, cursor, cursor))
dispatch?.(tr)
return true
})
}
export function insertToggle(editor: Editor, variant?: 'heading-1'): void {
const insertAt = editor.state.selection.from
editor
.chain()
.focus()
.insertContentAt(insertAt, {
type: 'details',
attrs: {
open: true,
...(variant ? { variant } : {})
},
content: [
{
type: 'detailsSummary'
},
{
type: 'detailsContent',
content: [{ type: 'paragraph' }]
}
]
})
.setTextSelection(insertAt + 1)
.run()
}
@@ -1,9 +1,10 @@
import { useEffect, useRef } from 'react'
import { useRef } from 'react'
import type { ComponentType, JSX, ReactNode } from 'react'
import { Files, GitBranch, ListChecks, MessageSquare, Search } from 'lucide-react'
import { useShortcutLabel } from '@/hooks/useShortcutLabel'
import { ReviewPRViewVisualStyles } from './review-animated-visual-pr-view-styles'
import { CheckTinyIcon, ChevDownIcon, CursorIcon } from './review-animated-visual-shared'
import { useReviewPrViewAnimation } from './review-pr-view-animation'
import { translate } from '@/i18n/i18n'
type SidebarTabId = 'explorer' | 'search' | 'source-control' | 'checks'
@@ -121,20 +122,6 @@ function CommentCard(props: { index: number; path: string; children: ReactNode }
)
}
function moveCursor(
root: HTMLElement,
cursor: HTMLElement,
anchor: HTMLElement,
ox = 0,
oy = 0
): void {
const rootRect = root.getBoundingClientRect()
const anchorRect = anchor.getBoundingClientRect()
cursor.style.transform = `translate(${anchorRect.left - rootRect.left + ox}px, ${
anchorRect.top - rootRect.top + oy
}px)`
}
// Why: the Review PR visual follows the approved HTML mock beat-for-beat. The
// real app keeps Explorer / Checks in one right-sidebar surface, so the
// animation selects Checks before the PR status content appears.
@@ -142,205 +129,7 @@ export function ReviewPRViewAnimatedVisual(props: { reducedMotion: boolean }): J
const { reducedMotion } = props
const rootRef = useRef<HTMLDivElement | null>(null)
useEffect(() => {
const root = rootRef.current
if (!root) {
return
}
const sidebarPeek = root.querySelector<HTMLDivElement>('[data-checks-sidebar-peek]')
const prCard = root.querySelector<HTMLDivElement>('[data-pr-view-card]')
const cursor = root.querySelector<HTMLDivElement>('[data-cursor]')
const explorerTab = root.querySelector<HTMLSpanElement>('[data-explorer-tab]')
const checksTab = root.querySelector<HTMLSpanElement>('[data-checks-tab]')
const checksTooltip = root.querySelector<HTMLSpanElement>('[data-checks-tooltip]')
const checksBlock = root.querySelector<HTMLDivElement>('[data-checks-block]')
const commentsBlock = root.querySelector<HTMLDivElement>('[data-comments-block]')
const comments = Array.from(root.querySelectorAll<HTMLDivElement>('[data-comment-card]'))
const commentsCount = root.querySelector<HTMLSpanElement>('[data-comments-count]')
const checkSummary = root.querySelector<HTMLDivElement>('[data-check-summary]')
const checkSummaryLabel = root.querySelector<HTMLSpanElement>('[data-check-summary-label]')
const checkSummaryMeta = root.querySelector<HTMLSpanElement>('[data-check-summary-meta]')
const verifyRow = root.querySelector<HTMLDivElement>('[data-check-row="verify"]')
const verifyState = root.querySelector<HTMLSpanElement>('[data-check-verify-state]')
const mergeBtn = root.querySelector<HTMLButtonElement>('[data-merge-btn]')
if (
!sidebarPeek ||
!prCard ||
!cursor ||
!explorerTab ||
!checksTab ||
!checksTooltip ||
!checksBlock ||
!commentsBlock ||
!commentsCount ||
!checkSummary ||
!checkSummaryLabel ||
!checkSummaryMeta ||
!verifyRow ||
!verifyState ||
!mergeBtn
) {
return
}
const rootEl: HTMLDivElement = root
const sidebarPeekEl: HTMLDivElement = sidebarPeek
const prCardEl: HTMLDivElement = prCard
const cursorEl: HTMLDivElement = cursor
const explorerTabEl: HTMLSpanElement = explorerTab
const checksTabEl: HTMLSpanElement = checksTab
const checksTooltipEl: HTMLSpanElement = checksTooltip
const checksBlockEl: HTMLDivElement = checksBlock
const commentsBlockEl: HTMLDivElement = commentsBlock
const commentsCountEl: HTMLSpanElement = commentsCount
const checkSummaryEl: HTMLDivElement = checkSummary
const checkSummaryLabelEl: HTMLSpanElement = checkSummaryLabel
const checkSummaryMetaEl: HTMLSpanElement = checkSummaryMeta
const verifyRowEl: HTMLDivElement = verifyRow
const verifyStateEl: HTMLSpanElement = verifyState
const mergeBtnEl: HTMLButtonElement = mergeBtn
let cancelled = false
const timers: number[] = []
const wait = (ms: number): Promise<void> =>
new Promise((resolve) => {
const id = window.setTimeout(() => resolve(), ms)
timers.push(id)
})
function resetState(): void {
sidebarPeekEl.classList.add('is-visible')
sidebarPeekEl.classList.remove('is-hiding')
prCardEl.classList.remove('is-visible')
explorerTabEl.classList.add('is-active')
checksTabEl.classList.remove('is-active', 'is-hovered')
checksTooltipEl.classList.remove('is-visible')
cursorEl.classList.remove('is-visible', 'is-clicking')
cursorEl.style.transition = 'none'
cursorEl.style.transform = 'translate(-30px, 220px)'
void cursorEl.offsetWidth
cursorEl.style.transition = ''
checksBlockEl.classList.remove('is-visible')
commentsBlockEl.classList.remove('is-visible')
comments.forEach((el) => el.classList.remove('is-visible'))
commentsCountEl.textContent = '0'
checkSummaryEl.classList.remove('is-done')
checkSummaryLabelEl.textContent = '1 pending'
checkSummaryMetaEl.textContent = 'verify'
verifyRowEl.classList.remove('is-done')
verifyStateEl.textContent = 'Running'
mergeBtnEl.classList.remove('is-ready')
}
function showFinalState(): void {
resetState()
sidebarPeekEl.classList.add('is-hiding')
prCardEl.classList.add('is-visible')
checksBlockEl.classList.add('is-visible')
commentsBlockEl.classList.add('is-visible')
comments.forEach((el) => el.classList.add('is-visible'))
commentsCountEl.textContent = String(comments.length)
checkSummaryEl.classList.add('is-done')
checkSummaryLabelEl.textContent = 'Checks passed'
checkSummaryMetaEl.textContent = '3 checks'
verifyRowEl.classList.add('is-done')
verifyStateEl.textContent = 'Passed'
mergeBtnEl.classList.add('is-ready')
cursorEl.classList.remove('is-visible')
}
if (reducedMotion) {
showFinalState()
return
}
async function loop(): Promise<void> {
while (!cancelled) {
resetState()
await wait(420)
if (cancelled) {
return
}
cursorEl.classList.add('is-visible')
moveCursor(rootEl, cursorEl, checksTabEl, 5, 6)
checksTabEl.classList.add('is-hovered')
await wait(260)
if (cancelled) {
return
}
checksTooltipEl.classList.add('is-visible')
await wait(1300)
if (cancelled) {
return
}
cursorEl.classList.add('is-clicking')
await wait(220)
if (cancelled) {
return
}
cursorEl.classList.remove('is-clicking')
checksTooltipEl.classList.remove('is-visible')
checksTabEl.classList.remove('is-hovered')
explorerTabEl.classList.remove('is-active')
checksTabEl.classList.add('is-active')
await wait(420)
if (cancelled) {
return
}
sidebarPeekEl.classList.add('is-hiding')
prCardEl.classList.add('is-visible')
cursorEl.classList.remove('is-visible')
await wait(560)
if (cancelled) {
return
}
checksBlockEl.classList.add('is-visible')
await wait(1050)
if (cancelled) {
return
}
verifyRowEl.classList.add('is-done')
verifyStateEl.textContent = 'Passed'
checkSummaryEl.classList.add('is-done')
checkSummaryLabelEl.textContent = 'Checks passed'
checkSummaryMetaEl.textContent = '3 checks'
mergeBtnEl.classList.add('is-ready')
await wait(560)
if (cancelled) {
return
}
commentsBlockEl.classList.add('is-visible')
await wait(260)
if (cancelled) {
return
}
for (let i = 0; i < comments.length; i++) {
comments[i]?.classList.add('is-visible')
commentsCountEl.textContent = String(i + 1)
await wait(520)
if (cancelled) {
return
}
}
await wait(2900)
}
}
void loop()
return () => {
cancelled = true
timers.forEach((timer) => window.clearTimeout(timer))
}
}, [reducedMotion])
useReviewPrViewAnimation(rootRef, reducedMotion)
return (
<div ref={rootRef} className="ravpr-stage" data-page="pr-view">
@@ -17,8 +17,12 @@ export const FEATURE_TOUR_PREVIEW_COPY: readonly FeatureTourPreviewFrameCopy[] =
'Isolated workspaces'
)
},
caption:
'Ship several things at once. Each workspace keeps its branch, terminal, and agent activity together.'
get caption() {
return translate(
'auto.components.feature.wall.FeatureTourPreview.47f16ecf34',
'Ship several things at once. Each workspace keeps its branch, terminal, and agent activity together.'
)
}
},
{
id: 2,
@@ -28,7 +32,12 @@ export const FEATURE_TOUR_PREVIEW_COPY: readonly FeatureTourPreviewFrameCopy[] =
'Agent orchestration'
)
},
caption: 'Hand off a goal and walk away. A coordinator agent fans out and ships parallel PRs.'
get caption() {
return translate(
'auto.components.feature.wall.FeatureTourPreview.70aa182266',
'Hand off a goal and walk away. A coordinator agent fans out and ships parallel PRs.'
)
}
},
{
id: 3,
@@ -38,8 +47,12 @@ export const FEATURE_TOUR_PREVIEW_COPY: readonly FeatureTourPreviewFrameCopy[] =
'GitHub & Linear tasks'
)
},
caption:
'Skip the tab-switching. Pick from your GitHub or Linear backlog and start a workspace in one click.'
get caption() {
return translate(
'auto.components.feature.wall.FeatureTourPreview.f10c14dd9d',
'Skip the tab-switching. Pick from your GitHub or Linear backlog and start a workspace in one click.'
)
}
},
{
id: 4,
@@ -49,8 +62,12 @@ export const FEATURE_TOUR_PREVIEW_COPY: readonly FeatureTourPreviewFrameCopy[] =
'Splittable terminal'
)
},
caption:
'Open any workspace to return to its terminal, then split panes for tests, logs, and agents.'
get caption() {
return translate(
'auto.components.feature.wall.FeatureTourPreview.5d6ee181b6',
'Open any workspace to return to its terminal, then split panes for tests, logs, and agents.'
)
}
}
]
@@ -0,0 +1,261 @@
import { useEffect } from 'react'
import type { RefObject } from 'react'
import { translate } from '@/i18n/i18n'
function getReviewPrAnimatedStatusCopy(): {
pendingLabel: string
verifyLabel: string
runningLabel: string
checksPassedLabel: string
checksCountLabel: string
passedLabel: string
} {
return {
pendingLabel: translate(
'auto.components.feature.wall.ReviewPRViewAnimatedVisual.9a097cae12',
'1 pending'
),
verifyLabel: translate(
'auto.components.feature.wall.ReviewPRViewAnimatedVisual.d340c052fb',
'verify'
),
runningLabel: translate(
'auto.components.feature.wall.ReviewPRViewAnimatedVisual.8ed213397c',
'Running'
),
checksPassedLabel: translate(
'auto.components.feature.wall.ReviewPRViewAnimatedVisual.a6c8b9e32f',
'Checks passed'
),
checksCountLabel: translate(
'auto.components.feature.wall.ReviewPRViewAnimatedVisual.f4d5e1a7b2',
'3 checks'
),
passedLabel: translate(
'auto.components.feature.wall.ReviewPRViewAnimatedVisual.ca36f7b27c',
'Passed'
)
}
}
function moveCursor(
root: HTMLElement,
cursor: HTMLElement,
anchor: HTMLElement,
ox = 0,
oy = 0
): void {
const rootRect = root.getBoundingClientRect()
const anchorRect = anchor.getBoundingClientRect()
cursor.style.transform = `translate(${anchorRect.left - rootRect.left + ox}px, ${
anchorRect.top - rootRect.top + oy
}px)`
}
export function useReviewPrViewAnimation(
rootRef: RefObject<HTMLDivElement | null>,
reducedMotion: boolean
): void {
useEffect(() => {
const root = rootRef.current
if (!root) {
return
}
const sidebarPeek = root.querySelector<HTMLDivElement>('[data-checks-sidebar-peek]')
const prCard = root.querySelector<HTMLDivElement>('[data-pr-view-card]')
const cursor = root.querySelector<HTMLDivElement>('[data-cursor]')
const explorerTab = root.querySelector<HTMLSpanElement>('[data-explorer-tab]')
const checksTab = root.querySelector<HTMLSpanElement>('[data-checks-tab]')
const checksTooltip = root.querySelector<HTMLSpanElement>('[data-checks-tooltip]')
const checksBlock = root.querySelector<HTMLDivElement>('[data-checks-block]')
const commentsBlock = root.querySelector<HTMLDivElement>('[data-comments-block]')
const comments = Array.from(root.querySelectorAll<HTMLDivElement>('[data-comment-card]'))
const commentsCount = root.querySelector<HTMLSpanElement>('[data-comments-count]')
const checkSummary = root.querySelector<HTMLDivElement>('[data-check-summary]')
const checkSummaryLabel = root.querySelector<HTMLSpanElement>('[data-check-summary-label]')
const checkSummaryMeta = root.querySelector<HTMLSpanElement>('[data-check-summary-meta]')
const verifyRow = root.querySelector<HTMLDivElement>('[data-check-row="verify"]')
const verifyState = root.querySelector<HTMLSpanElement>('[data-check-verify-state]')
const mergeBtn = root.querySelector<HTMLButtonElement>('[data-merge-btn]')
if (
!sidebarPeek ||
!prCard ||
!cursor ||
!explorerTab ||
!checksTab ||
!checksTooltip ||
!checksBlock ||
!commentsBlock ||
!commentsCount ||
!checkSummary ||
!checkSummaryLabel ||
!checkSummaryMeta ||
!verifyRow ||
!verifyState ||
!mergeBtn
) {
return
}
const rootEl: HTMLDivElement = root
const sidebarPeekEl: HTMLDivElement = sidebarPeek
const prCardEl: HTMLDivElement = prCard
const cursorEl: HTMLDivElement = cursor
const explorerTabEl: HTMLSpanElement = explorerTab
const checksTabEl: HTMLSpanElement = checksTab
const checksTooltipEl: HTMLSpanElement = checksTooltip
const checksBlockEl: HTMLDivElement = checksBlock
const commentsBlockEl: HTMLDivElement = commentsBlock
const commentsCountEl: HTMLSpanElement = commentsCount
const checkSummaryEl: HTMLDivElement = checkSummary
const checkSummaryLabelEl: HTMLSpanElement = checkSummaryLabel
const checkSummaryMetaEl: HTMLSpanElement = checkSummaryMeta
const verifyRowEl: HTMLDivElement = verifyRow
const verifyStateEl: HTMLSpanElement = verifyState
const mergeBtnEl: HTMLButtonElement = mergeBtn
let cancelled = false
const timers: number[] = []
const wait = (ms: number): Promise<void> =>
new Promise((resolve) => {
const id = window.setTimeout(() => resolve(), ms)
timers.push(id)
})
function resetState(): void {
sidebarPeekEl.classList.add('is-visible')
sidebarPeekEl.classList.remove('is-hiding')
prCardEl.classList.remove('is-visible')
explorerTabEl.classList.add('is-active')
checksTabEl.classList.remove('is-active', 'is-hovered')
checksTooltipEl.classList.remove('is-visible')
cursorEl.classList.remove('is-visible', 'is-clicking')
cursorEl.style.transition = 'none'
cursorEl.style.transform = 'translate(-30px, 220px)'
void cursorEl.offsetWidth
cursorEl.style.transition = ''
checksBlockEl.classList.remove('is-visible')
commentsBlockEl.classList.remove('is-visible')
comments.forEach((el) => el.classList.remove('is-visible'))
commentsCountEl.textContent = '0'
checkSummaryEl.classList.remove('is-done')
const copy = getReviewPrAnimatedStatusCopy()
checkSummaryLabelEl.textContent = copy.pendingLabel
checkSummaryMetaEl.textContent = copy.verifyLabel
verifyRowEl.classList.remove('is-done')
verifyStateEl.textContent = copy.runningLabel
mergeBtnEl.classList.remove('is-ready')
}
function showFinalState(): void {
resetState()
sidebarPeekEl.classList.add('is-hiding')
prCardEl.classList.add('is-visible')
checksBlockEl.classList.add('is-visible')
commentsBlockEl.classList.add('is-visible')
comments.forEach((el) => el.classList.add('is-visible'))
commentsCountEl.textContent = String(comments.length)
checkSummaryEl.classList.add('is-done')
const copy = getReviewPrAnimatedStatusCopy()
checkSummaryLabelEl.textContent = copy.checksPassedLabel
checkSummaryMetaEl.textContent = copy.checksCountLabel
verifyRowEl.classList.add('is-done')
verifyStateEl.textContent = copy.passedLabel
mergeBtnEl.classList.add('is-ready')
cursorEl.classList.remove('is-visible')
}
if (reducedMotion) {
showFinalState()
return
}
async function loop(): Promise<void> {
while (!cancelled) {
resetState()
await wait(420)
if (cancelled) {
return
}
cursorEl.classList.add('is-visible')
moveCursor(rootEl, cursorEl, checksTabEl, 5, 6)
checksTabEl.classList.add('is-hovered')
await wait(260)
if (cancelled) {
return
}
checksTooltipEl.classList.add('is-visible')
await wait(1300)
if (cancelled) {
return
}
cursorEl.classList.add('is-clicking')
await wait(220)
if (cancelled) {
return
}
cursorEl.classList.remove('is-clicking')
checksTooltipEl.classList.remove('is-visible')
checksTabEl.classList.remove('is-hovered')
explorerTabEl.classList.remove('is-active')
checksTabEl.classList.add('is-active')
await wait(420)
if (cancelled) {
return
}
sidebarPeekEl.classList.add('is-hiding')
prCardEl.classList.add('is-visible')
cursorEl.classList.remove('is-visible')
await wait(560)
if (cancelled) {
return
}
checksBlockEl.classList.add('is-visible')
await wait(1050)
if (cancelled) {
return
}
verifyRowEl.classList.add('is-done')
const copy = getReviewPrAnimatedStatusCopy()
verifyStateEl.textContent = copy.passedLabel
checkSummaryEl.classList.add('is-done')
checkSummaryLabelEl.textContent = copy.checksPassedLabel
checkSummaryMetaEl.textContent = copy.checksCountLabel
mergeBtnEl.classList.add('is-ready')
await wait(560)
if (cancelled) {
return
}
commentsBlockEl.classList.add('is-visible')
await wait(260)
if (cancelled) {
return
}
for (let i = 0; i < comments.length; i++) {
comments[i]?.classList.add('is-visible')
commentsCountEl.textContent = String(i + 1)
await wait(520)
if (cancelled) {
return
}
}
await wait(2900)
}
}
void loop()
return () => {
cancelled = true
timers.forEach((timer) => window.clearTimeout(timer))
}
}, [reducedMotion, rootRef])
}
@@ -54,7 +54,7 @@ const stepCopy = {
get subtitle() {
return translate(
'auto.components.onboarding.OnboardingFlow.ff92d15436',
'Orca will notify you know when agents are done or need help.'
'Orca will notify you when agents are done or need help.'
)
}
},
@@ -142,19 +142,7 @@ export function SourceControlAiActionRecipeDefaults({
<SearchableSetting
title={ACTION_RECIPES_SEARCH_ENTRY.title}
description={ACTION_RECIPES_SEARCH_ENTRY.description}
keywords={[
'agent',
'arguments',
'args',
'cli',
'command',
'model',
'template',
'fix',
'checks',
'commit',
'pull request'
]}
keywords={ACTION_RECIPES_SEARCH_ENTRY.keywords}
className="space-y-3 px-1 py-2"
>
<div className="space-y-0.5">
@@ -24,16 +24,36 @@ type SshDisconnectedDialogProps = {
}
const STATUS_MESSAGES: Partial<Record<SshConnectionStatus, string>> = {
disconnected: 'This remote repository is not connected.',
reconnecting: 'Reconnecting to the remote host...',
'reconnection-failed': 'Reconnection to the remote host failed.',
get disconnected() {
return translate(
'auto.components.sidebar.SshDisconnectedDialog.disconnected',
'This remote repository is not connected.'
)
},
get reconnecting() {
return translate(
'auto.components.sidebar.SshDisconnectedDialog.reconnecting',
'Reconnecting to the remote host...'
)
},
get 'reconnection-failed'() {
return translate(
'auto.components.sidebar.SshDisconnectedDialog.reconnectionFailed',
'Reconnection to the remote host failed.'
)
},
get error() {
return translate(
'auto.components.sidebar.SshDisconnectedDialog.376bed88e5',
'The connection to the remote host encountered an error.'
)
},
'auth-failed': 'Authentication to the remote host failed.'
get 'auth-failed'() {
return translate(
'auto.components.sidebar.SshDisconnectedDialog.authFailed',
'Authentication to the remote host failed.'
)
}
}
function isReconnectable(status: SshConnectionStatus): boolean {
@@ -78,9 +98,21 @@ export function SshDisconnectedDialog({
status === 'connecting' ||
status === 'deploying-relay' ||
status === 'reconnecting'
const reconnectingMessage =
STATUS_MESSAGES.reconnecting ??
translate(
'auto.components.sidebar.SshDisconnectedDialog.reconnecting',
'Reconnecting to the remote host...'
)
const disconnectedMessage =
STATUS_MESSAGES.disconnected ??
translate(
'auto.components.sidebar.SshDisconnectedDialog.disconnected',
'This remote repository is not connected.'
)
const message = isConnecting
? 'Reconnecting to the remote host...'
: (STATUS_MESSAGES[status] ?? 'This remote repository is not connected.')
? reconnectingMessage
: (STATUS_MESSAGES[status] ?? disconnectedMessage)
const showReconnect = isReconnectable(status)
useEffect(() => {
@@ -45,10 +45,18 @@ const SCOPE_OPTIONS: { value: CodexUsageScope; label: string }[] = [
}
]
const RANGE_LABELS: Record<CodexUsageRange, string> = {
'7d': 'Last 7 days',
'30d': 'Last 30 days',
'90d': 'Last 90 days',
all: 'All time'
get '7d'() {
return translate('auto.components.stats.CodexUsagePane.rangeLast7Days', 'Last 7 days')
},
get '30d'() {
return translate('auto.components.stats.CodexUsagePane.rangeLast30Days', 'Last 30 days')
},
get '90d'() {
return translate('auto.components.stats.CodexUsagePane.rangeLast90Days', 'Last 90 days')
},
get all() {
return translate('auto.components.stats.CodexUsagePane.rangeAllTime', 'All time')
}
}
export function CodexUsagePane(): React.JSX.Element {
@@ -50,10 +50,18 @@ const SCOPE_OPTIONS: { value: OpenCodeUsageScope; label: string }[] = [
}
]
const RANGE_LABELS: Record<OpenCodeUsageRange, string> = {
'7d': 'Last 7 days',
'30d': 'Last 30 days',
'90d': 'Last 90 days',
all: 'All time'
get '7d'() {
return translate('auto.components.stats.OpenCodeUsagePane.rangeLast7Days', 'Last 7 days')
},
get '30d'() {
return translate('auto.components.stats.OpenCodeUsagePane.rangeLast30Days', 'Last 30 days')
},
get '90d'() {
return translate('auto.components.stats.OpenCodeUsagePane.rangeLast90Days', 'Last 90 days')
},
get all() {
return translate('auto.components.stats.OpenCodeUsagePane.rangeAllTime', 'All time')
}
}
export function OpenCodeUsagePane(): React.JSX.Element {