Fix localized strings getting stuck on language switch (#5150)

* Fix localized strings getting stuck on language switch

Replace static module-level translation evaluations with property
getters and i18n hooks to ensure UI elements update dynamically when
the active language changes.

- Convert static configuration properties to dynamic getters.
- Call `useTranslation()` to trigger re-renders on language updates.
- Recreate Tiptap editor extensions when the active language changes
  to refresh frozen placeholder configurations.

* 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.

---------

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Jinjing
2026-06-11 19:45:50 -07:00
committed by GitHub
co-authored by Orca
parent 18ca691977
commit 868b024dbe
49 changed files with 3844 additions and 1772 deletions
@@ -1,26 +1,13 @@
import React, { useCallback, useEffect, useRef } from 'react'
import React, { useEffect, useMemo, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { EditorContent, useEditor } from '@tiptap/react'
import type { Editor } from '@tiptap/react'
import Placeholder from '@tiptap/extension-placeholder'
import {
Bold,
Code,
Heading1,
Heading2,
Italic,
Link as LinkIcon,
List,
ListOrdered,
ListTodo,
LoaderCircle,
Pilcrow,
Quote,
Strikethrough
} from 'lucide-react'
import { LoaderCircle } from 'lucide-react'
import { createRichMarkdownExtensions } from '@/components/editor/rich-markdown-extensions'
import { encodeRawMarkdownHtmlForRichEditor } from '@/components/editor/raw-markdown-html'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { LinearIssueMarkdownToolbar } from '@/components/LinearIssueMarkdownToolbar'
import { isScreenSubmitShortcut } from '@/lib/screen-submit-shortcut'
import { cn } from '@/lib/utils'
import { translate } from '@/i18n/i18n'
@@ -34,258 +21,17 @@ type LinearIssueMarkdownDescriptionEditorProps = {
submitShortcutLabel: string
}
type LinearIssueMarkdownToolbarButtonProps = {
active?: boolean
disabled?: boolean
label: string
onClick: () => void
children: React.ReactNode
}
const linearIssueMarkdownExtensions = [
...createRichMarkdownExtensions(),
Placeholder.configure({
placeholder: translate(
'auto.components.LinearIssueMarkdownDescriptionEditor.4f2fddc2b7',
'No description provided.'
)
})
]
function LinearIssueMarkdownToolbarButton({
active = false,
disabled = false,
label,
onClick,
children
}: LinearIssueMarkdownToolbarButtonProps): React.JSX.Element {
return (
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
aria-label={label}
disabled={disabled}
className={cn('linear-issue-markdown-toolbar-button', active && 'is-active')}
onMouseDown={(event) => event.preventDefault()}
onClick={onClick}
>
{children}
</button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={4}>
{label}
</TooltipContent>
</Tooltip>
)
}
function LinearIssueMarkdownToolbarSeparator(): React.JSX.Element {
return <div className="linear-issue-markdown-toolbar-separator" />
}
function applyLinearIssueLink(editor: Editor | null): void {
if (!editor) {
return
}
if (editor.isActive('link')) {
editor.chain().focus().unsetLink().run()
return
}
const previousHref = editor.getAttributes('link').href as string | undefined
const href = window.prompt(
translate('auto.components.LinearIssueMarkdownDescriptionEditor.5c16ec8f14', 'Link URL'),
previousHref ?? ''
)
if (href === null) {
editor.chain().focus().run()
return
}
const trimmed = href.trim()
if (!trimmed) {
editor.chain().focus().unsetLink().run()
return
}
editor.chain().focus().extendMarkRange('link').setLink({ href: trimmed }).run()
}
function LinearIssueMarkdownToolbar({
editor,
disabled
}: {
editor: Editor | null
disabled: boolean
}): React.JSX.Element {
const runCommand = useCallback(
(command: (editor: Editor) => void) => {
if (!editor || disabled) {
return
}
command(editor)
},
[disabled, editor]
)
return (
<div
className="linear-issue-markdown-toolbar"
aria-label={translate(
'auto.components.LinearIssueMarkdownDescriptionEditor.7c52151156',
'Issue description formatting'
)}
>
<LinearIssueMarkdownToolbarButton
label={translate(
'auto.components.LinearIssueMarkdownDescriptionEditor.68a41d5665',
'Body text'
)}
disabled={disabled}
onClick={() => runCommand((nextEditor) => nextEditor.chain().focus().setParagraph().run())}
>
<Pilcrow className="size-3.5" />
</LinearIssueMarkdownToolbarButton>
<LinearIssueMarkdownToolbarButton
label={translate(
'auto.components.LinearIssueMarkdownDescriptionEditor.e3f741d258',
'Heading 1'
)}
active={editor?.isActive('heading', { level: 1 }) ?? false}
disabled={disabled}
onClick={() =>
runCommand((nextEditor) => nextEditor.chain().focus().toggleHeading({ level: 1 }).run())
}
>
<Heading1 className="size-3.5" />
</LinearIssueMarkdownToolbarButton>
<LinearIssueMarkdownToolbarButton
label={translate(
'auto.components.LinearIssueMarkdownDescriptionEditor.dddaa7a0a6',
'Heading 2'
)}
active={editor?.isActive('heading', { level: 2 }) ?? false}
disabled={disabled}
onClick={() =>
runCommand((nextEditor) => nextEditor.chain().focus().toggleHeading({ level: 2 }).run())
}
>
<Heading2 className="size-3.5" />
</LinearIssueMarkdownToolbarButton>
<LinearIssueMarkdownToolbarSeparator />
<LinearIssueMarkdownToolbarButton
label={translate('auto.components.LinearIssueMarkdownDescriptionEditor.caa88f50d0', 'Bold')}
active={editor?.isActive('bold') ?? false}
disabled={disabled}
onClick={() => runCommand((nextEditor) => nextEditor.chain().focus().toggleBold().run())}
>
<Bold className="size-3.5" />
</LinearIssueMarkdownToolbarButton>
<LinearIssueMarkdownToolbarButton
label={translate(
'auto.components.LinearIssueMarkdownDescriptionEditor.5666b4493d',
'Italic'
)}
active={editor?.isActive('italic') ?? false}
disabled={disabled}
onClick={() => runCommand((nextEditor) => nextEditor.chain().focus().toggleItalic().run())}
>
<Italic className="size-3.5" />
</LinearIssueMarkdownToolbarButton>
<LinearIssueMarkdownToolbarButton
label={translate(
'auto.components.LinearIssueMarkdownDescriptionEditor.28fd951b83',
'Strike'
)}
active={editor?.isActive('strike') ?? false}
disabled={disabled}
onClick={() => runCommand((nextEditor) => nextEditor.chain().focus().toggleStrike().run())}
>
<Strikethrough className="size-3.5" />
</LinearIssueMarkdownToolbarButton>
<LinearIssueMarkdownToolbarButton
label={translate(
'auto.components.LinearIssueMarkdownDescriptionEditor.ad1869bd54',
'Inline code'
)}
active={editor?.isActive('code') ?? false}
disabled={disabled}
onClick={() => runCommand((nextEditor) => nextEditor.chain().focus().toggleCode().run())}
>
<Code className="size-3.5" />
</LinearIssueMarkdownToolbarButton>
<LinearIssueMarkdownToolbarSeparator />
<LinearIssueMarkdownToolbarButton
label={translate(
'auto.components.LinearIssueMarkdownDescriptionEditor.c82917e06e',
'Bullet list'
)}
active={editor?.isActive('bulletList') ?? false}
disabled={disabled}
onClick={() =>
runCommand((nextEditor) => nextEditor.chain().focus().toggleBulletList().run())
}
>
<List className="size-3.5" />
</LinearIssueMarkdownToolbarButton>
<LinearIssueMarkdownToolbarButton
label={translate(
'auto.components.LinearIssueMarkdownDescriptionEditor.d6b2f3d35b',
'Numbered list'
)}
active={editor?.isActive('orderedList') ?? false}
disabled={disabled}
onClick={() =>
runCommand((nextEditor) => nextEditor.chain().focus().toggleOrderedList().run())
}
>
<ListOrdered className="size-3.5" />
</LinearIssueMarkdownToolbarButton>
<LinearIssueMarkdownToolbarButton
label={translate(
'auto.components.LinearIssueMarkdownDescriptionEditor.e2a0267c8c',
'Checklist'
)}
active={editor?.isActive('taskList') ?? false}
disabled={disabled}
onClick={() =>
runCommand((nextEditor) => nextEditor.chain().focus().toggleTaskList().run())
}
>
<ListTodo className="size-3.5" />
</LinearIssueMarkdownToolbarButton>
<LinearIssueMarkdownToolbarSeparator />
<LinearIssueMarkdownToolbarButton
label={translate(
'auto.components.LinearIssueMarkdownDescriptionEditor.9eaf02ac01',
'Quote'
)}
active={editor?.isActive('blockquote') ?? false}
disabled={disabled}
onClick={() =>
runCommand((nextEditor) => nextEditor.chain().focus().toggleBlockquote().run())
}
>
<Quote className="size-3.5" />
</LinearIssueMarkdownToolbarButton>
<LinearIssueMarkdownToolbarButton
label={
editor?.isActive('link')
? translate(
'auto.components.LinearIssueMarkdownDescriptionEditor.340160f4e8',
'Remove link'
)
: translate('auto.components.LinearIssueMarkdownDescriptionEditor.632096eb1c', 'Link')
}
active={editor?.isActive('link') ?? false}
disabled={disabled}
onClick={() => runCommand(applyLinearIssueLink)}
>
<LinkIcon className="size-3.5" />
</LinearIssueMarkdownToolbarButton>
</div>
)
function createLinearIssueMarkdownExtensions() {
const extensions = createRichMarkdownExtensions()
return [
...extensions,
Placeholder.configure({
placeholder: translate(
'auto.components.LinearIssueMarkdownDescriptionEditor.4f2fddc2b7',
'No description provided.'
)
})
]
}
export function LinearIssueMarkdownDescriptionEditor({
@@ -296,46 +42,57 @@ export function LinearIssueMarkdownDescriptionEditor({
disabled,
submitShortcutLabel
}: LinearIssueMarkdownDescriptionEditorProps): React.JSX.Element {
const { i18n } = useTranslation()
const language = i18n.resolvedLanguage ?? i18n.language
const lastEditorMarkdownRef = useRef(value)
const editorRef = useRef<Editor | null>(null)
const linearIssueMarkdownExtensions = useMemo(() => {
// Why: Tiptap freezes extension options when the editor is created; the
// language value is the recreation key for translated extension options.
void language
return createLinearIssueMarkdownExtensions()
}, [language])
const editor = useEditor({
immediatelyRender: false,
extensions: linearIssueMarkdownExtensions,
content: encodeRawMarkdownHtmlForRichEditor(value),
contentType: 'markdown',
editable: !disabled,
editorProps: {
attributes: {
class: 'rich-markdown-editor',
spellcheck: 'true',
'aria-label': 'Issue description'
},
handleKeyDown: (_view, event) => {
if (!isScreenSubmitShortcut(event)) {
return false
const editor = useEditor(
{
immediatelyRender: false,
extensions: linearIssueMarkdownExtensions,
content: encodeRawMarkdownHtmlForRichEditor(value),
contentType: 'markdown',
editable: !disabled,
editorProps: {
attributes: {
class: 'rich-markdown-editor',
spellcheck: 'true',
'aria-label': 'Issue description'
},
handleKeyDown: (_view, event) => {
if (!isScreenSubmitShortcut(event)) {
return false
}
event.preventDefault()
editorRef.current?.commands.blur()
return true
}
event.preventDefault()
editorRef.current?.commands.blur()
return true
},
onFocus: () => {
window.api.ui.setMarkdownEditorFocused(true)
},
onBlur: ({ editor: nextEditor }) => {
window.api.ui.setMarkdownEditorFocused(false)
const nextValue = nextEditor.getMarkdown()
lastEditorMarkdownRef.current = nextValue
onChange(nextValue)
onSave(nextValue)
},
onUpdate: ({ editor: nextEditor }) => {
const nextValue = nextEditor.getMarkdown()
lastEditorMarkdownRef.current = nextValue
onChange(nextValue)
}
},
onFocus: () => {
window.api.ui.setMarkdownEditorFocused(true)
},
onBlur: ({ editor: nextEditor }) => {
window.api.ui.setMarkdownEditorFocused(false)
const nextValue = nextEditor.getMarkdown()
lastEditorMarkdownRef.current = nextValue
onChange(nextValue)
onSave(nextValue)
},
onUpdate: ({ editor: nextEditor }) => {
const nextValue = nextEditor.getMarkdown()
lastEditorMarkdownRef.current = nextValue
onChange(nextValue)
}
})
[language]
)
useEffect(() => {
editorRef.current = editor
@@ -0,0 +1,267 @@
import React, { useCallback } from 'react'
import type { Editor } from '@tiptap/react'
import { useTranslation } from 'react-i18next'
import {
Bold,
Code,
Heading1,
Heading2,
Italic,
Link as LinkIcon,
List,
ListOrdered,
ListTodo,
Pilcrow,
Quote,
Strikethrough
} from 'lucide-react'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { cn } from '@/lib/utils'
import { translate } from '@/i18n/i18n'
type LinearIssueMarkdownToolbarButtonProps = {
active?: boolean
disabled?: boolean
label: string
onClick: () => void
children: React.ReactNode
}
function LinearIssueMarkdownToolbarButton({
active = false,
disabled = false,
label,
onClick,
children
}: LinearIssueMarkdownToolbarButtonProps): React.JSX.Element {
return (
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
aria-label={label}
disabled={disabled}
className={cn('linear-issue-markdown-toolbar-button', active && 'is-active')}
onMouseDown={(event) => event.preventDefault()}
onClick={onClick}
>
{children}
</button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={4}>
{label}
</TooltipContent>
</Tooltip>
)
}
function LinearIssueMarkdownToolbarSeparator(): React.JSX.Element {
return <div className="linear-issue-markdown-toolbar-separator" />
}
function applyLinearIssueLink(editor: Editor | null): void {
if (!editor) {
return
}
if (editor.isActive('link')) {
editor.chain().focus().unsetLink().run()
return
}
const previousHref = editor.getAttributes('link').href as string | undefined
const href = window.prompt(
translate('auto.components.LinearIssueMarkdownDescriptionEditor.5c16ec8f14', 'Link URL'),
previousHref ?? ''
)
if (href === null) {
editor.chain().focus().run()
return
}
const trimmed = href.trim()
if (!trimmed) {
editor.chain().focus().unsetLink().run()
return
}
editor.chain().focus().extendMarkRange('link').setLink({ href: trimmed }).run()
}
export function LinearIssueMarkdownToolbar({
editor,
disabled
}: {
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) {
return
}
command(editor)
},
[disabled, editor]
)
return (
<div
className="linear-issue-markdown-toolbar"
aria-label={translate(
'auto.components.LinearIssueMarkdownDescriptionEditor.7c52151156',
'Issue description formatting'
)}
>
<LinearIssueMarkdownToolbarButton
label={translate(
'auto.components.LinearIssueMarkdownDescriptionEditor.68a41d5665',
'Body text'
)}
disabled={disabled}
onClick={() => runCommand((nextEditor) => nextEditor.chain().focus().setParagraph().run())}
>
<Pilcrow className="size-3.5" />
</LinearIssueMarkdownToolbarButton>
<LinearIssueMarkdownToolbarButton
label={translate(
'auto.components.LinearIssueMarkdownDescriptionEditor.e3f741d258',
'Heading 1'
)}
active={editor?.isActive('heading', { level: 1 }) ?? false}
disabled={disabled}
onClick={() =>
runCommand((nextEditor) => nextEditor.chain().focus().toggleHeading({ level: 1 }).run())
}
>
<Heading1 className="size-3.5" />
</LinearIssueMarkdownToolbarButton>
<LinearIssueMarkdownToolbarButton
label={translate(
'auto.components.LinearIssueMarkdownDescriptionEditor.dddaa7a0a6',
'Heading 2'
)}
active={editor?.isActive('heading', { level: 2 }) ?? false}
disabled={disabled}
onClick={() =>
runCommand((nextEditor) => nextEditor.chain().focus().toggleHeading({ level: 2 }).run())
}
>
<Heading2 className="size-3.5" />
</LinearIssueMarkdownToolbarButton>
<LinearIssueMarkdownToolbarSeparator />
<LinearIssueMarkdownToolbarButton
label={translate('auto.components.LinearIssueMarkdownDescriptionEditor.caa88f50d0', 'Bold')}
active={editor?.isActive('bold') ?? false}
disabled={disabled}
onClick={() => runCommand((nextEditor) => nextEditor.chain().focus().toggleBold().run())}
>
<Bold className="size-3.5" />
</LinearIssueMarkdownToolbarButton>
<LinearIssueMarkdownToolbarButton
label={translate(
'auto.components.LinearIssueMarkdownDescriptionEditor.5666b4493d',
'Italic'
)}
active={editor?.isActive('italic') ?? false}
disabled={disabled}
onClick={() => runCommand((nextEditor) => nextEditor.chain().focus().toggleItalic().run())}
>
<Italic className="size-3.5" />
</LinearIssueMarkdownToolbarButton>
<LinearIssueMarkdownToolbarButton
label={translate(
'auto.components.LinearIssueMarkdownDescriptionEditor.28fd951b83',
'Strike'
)}
active={editor?.isActive('strike') ?? false}
disabled={disabled}
onClick={() => runCommand((nextEditor) => nextEditor.chain().focus().toggleStrike().run())}
>
<Strikethrough className="size-3.5" />
</LinearIssueMarkdownToolbarButton>
<LinearIssueMarkdownToolbarButton
label={translate(
'auto.components.LinearIssueMarkdownDescriptionEditor.ad1869bd54',
'Inline code'
)}
active={editor?.isActive('code') ?? false}
disabled={disabled}
onClick={() => runCommand((nextEditor) => nextEditor.chain().focus().toggleCode().run())}
>
<Code className="size-3.5" />
</LinearIssueMarkdownToolbarButton>
<LinearIssueMarkdownToolbarSeparator />
<LinearIssueMarkdownToolbarButton
label={translate(
'auto.components.LinearIssueMarkdownDescriptionEditor.c82917e06e',
'Bullet list'
)}
active={editor?.isActive('bulletList') ?? false}
disabled={disabled}
onClick={() =>
runCommand((nextEditor) => nextEditor.chain().focus().toggleBulletList().run())
}
>
<List className="size-3.5" />
</LinearIssueMarkdownToolbarButton>
<LinearIssueMarkdownToolbarButton
label={translate(
'auto.components.LinearIssueMarkdownDescriptionEditor.d6b2f3d35b',
'Numbered list'
)}
active={editor?.isActive('orderedList') ?? false}
disabled={disabled}
onClick={() =>
runCommand((nextEditor) => nextEditor.chain().focus().toggleOrderedList().run())
}
>
<ListOrdered className="size-3.5" />
</LinearIssueMarkdownToolbarButton>
<LinearIssueMarkdownToolbarButton
label={translate(
'auto.components.LinearIssueMarkdownDescriptionEditor.e2a0267c8c',
'Checklist'
)}
active={editor?.isActive('taskList') ?? false}
disabled={disabled}
onClick={() =>
runCommand((nextEditor) => nextEditor.chain().focus().toggleTaskList().run())
}
>
<ListTodo className="size-3.5" />
</LinearIssueMarkdownToolbarButton>
<LinearIssueMarkdownToolbarSeparator />
<LinearIssueMarkdownToolbarButton
label={translate(
'auto.components.LinearIssueMarkdownDescriptionEditor.9eaf02ac01',
'Quote'
)}
active={editor?.isActive('blockquote') ?? false}
disabled={disabled}
onClick={() =>
runCommand((nextEditor) => nextEditor.chain().focus().toggleBlockquote().run())
}
>
<Quote className="size-3.5" />
</LinearIssueMarkdownToolbarButton>
<LinearIssueMarkdownToolbarButton
label={
editor?.isActive('link')
? translate(
'auto.components.LinearIssueMarkdownDescriptionEditor.340160f4e8',
'Remove link'
)
: translate('auto.components.LinearIssueMarkdownDescriptionEditor.632096eb1c', 'Link')
}
active={editor?.isActive('link') ?? false}
disabled={disabled}
onClick={() => runCommand(applyLinearIssueLink)}
>
<LinkIcon className="size-3.5" />
</LinearIssueMarkdownToolbarButton>
</div>
)
}
@@ -2,6 +2,7 @@
composer card markup together so the inline and modal variants share one UI
surface without splitting the controlled form into hard-to-follow fragments. */
import React from 'react'
import { useTranslation } from 'react-i18next'
import {
AlertTriangle,
Check,
@@ -99,15 +100,50 @@ type NewWorkspaceComposerCardProps = {
sparseControlsEnabled?: boolean
}
const SSH_STATUS_LABELS: 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',
error: translate('auto.components.NewWorkspaceComposerCard.a239038146', 'SSH connection error')
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 {
return SSH_STATUS_LABELS[status] ?? status
}
function SetupCommandPreview({
@@ -284,6 +320,9 @@ export default function NewWorkspaceComposerCard({
onSparseSelectPreset,
sparseControlsEnabled = true
}: NewWorkspaceComposerCardProps): React.JSX.Element {
// Why: this form uses the lightweight translate() helper directly; subscribe
// so an already-open create dialog repaints when the UI language changes.
useTranslation()
const { isFileDragOver, dragHandlers } = useComposerFileDragOver()
const openModal = useAppStore((s) => s.openModal)
const activeModal = useAppStore((s) => s.activeModal)
@@ -297,8 +336,8 @@ export default function NewWorkspaceComposerCard({
return repo?.displayName ?? repo?.path ?? 'This project'
}, [eligibleRepos, repoId])
const sshStatusLabel = selectedRepoSshStatus
? SSH_STATUS_LABELS[selectedRepoSshStatus]
: 'Not connected'
? getSshStatusLabel(selectedRepoSshStatus)
: translate('auto.components.NewWorkspaceComposerCard.notConnected', 'Not connected')
const connectButtonLabel =
selectedRepoSshStatus === 'disconnected' || selectedRepoSshStatus === null
? 'Connect'
@@ -188,12 +188,16 @@ type BrowserOverlayAnchor = {
const BROWSER_ANNOTATION_INTENT_OPTIONS = [
{
value: 'change',
label: translate('auto.components.browser.pane.BrowserPane.143204e423', 'Change'),
get label() {
return translate('auto.components.browser.pane.BrowserPane.143204e423', 'Change')
},
icon: PencilLine
},
{
value: 'question',
label: translate('auto.components.browser.pane.BrowserPane.b5ba6085de', 'Question'),
get label() {
return translate('auto.components.browser.pane.BrowserPane.b5ba6085de', 'Question')
},
icon: MessageCircleQuestionMark
}
] as const
@@ -1,4 +1,5 @@
import React from 'react'
import { useTranslation } from 'react-i18next'
import {
Code,
Eye,
@@ -27,29 +28,41 @@ type ViewModeMetadata = { label: string; icon: LucideIcon; title?: string }
const DEFAULT_VIEW_MODE_METADATA: Record<EditorToggleValue, ViewModeMetadata> = {
source: {
label: translate('auto.components.editor.EditorViewToggle.4d6ccb7ba6', 'Source'),
get label() {
return translate('auto.components.editor.EditorViewToggle.4d6ccb7ba6', 'Source')
},
icon: Code
},
rich: {
label: translate('auto.components.editor.EditorViewToggle.aff15f94f5', 'Rich Editor'),
get label() {
return translate('auto.components.editor.EditorViewToggle.aff15f94f5', 'Rich Editor')
},
icon: Pencil
},
preview: {
label: translate('auto.components.editor.EditorViewToggle.0d193dc03c', 'Preview'),
get label() {
return translate('auto.components.editor.EditorViewToggle.0d193dc03c', 'Preview')
},
icon: Eye
},
edit: {
label: translate('auto.components.editor.EditorViewToggle.ac3bb87913', 'Edit'),
get label() {
return translate('auto.components.editor.EditorViewToggle.ac3bb87913', 'Edit')
},
icon: FileText
},
changes: {
label: translate('auto.components.editor.EditorViewToggle.4837f3f578', 'Changes'),
get label() {
return translate('auto.components.editor.EditorViewToggle.4837f3f578', 'Changes')
},
icon: GitCompareArrows,
// Why: "Changes" collides with the Source Control sidebar's "Branch
// Changes" section, which diffs against the base ref. This toggle shows
// uncommitted changes (working tree vs HEAD), so disambiguate in the
// hover title without repeating the button label.
title: translate('auto.components.editor.EditorViewToggle.167f45888c', 'Uncommitted changes')
get title() {
return translate('auto.components.editor.EditorViewToggle.167f45888c', 'Uncommitted changes')
}
}
}
@@ -58,14 +71,18 @@ const DEFAULT_VIEW_MODE_METADATA: Record<EditorToggleValue, ViewModeMetadata> =
// which we don't offer, so callers can override the per-mode presentation.
export const CSV_VIEW_MODE_METADATA: Partial<Record<MarkdownViewMode, ViewModeMetadata>> = {
rich: {
label: translate('auto.components.editor.EditorViewToggle.e408aa9cd5', 'Table'),
get label() {
return translate('auto.components.editor.EditorViewToggle.e408aa9cd5', 'Table')
},
icon: TableIcon
}
}
export const NOTEBOOK_VIEW_MODE_METADATA: Partial<Record<MarkdownViewMode, ViewModeMetadata>> = {
rich: {
label: translate('auto.components.editor.EditorViewToggle.b3410cd5e0', 'Notebook'),
get label() {
return translate('auto.components.editor.EditorViewToggle.b3410cd5e0', 'Notebook')
},
icon: NotebookText
}
}
@@ -83,6 +100,9 @@ export default function EditorViewToggle({
onChange,
metadataOverride
}: EditorViewToggleProps): React.JSX.Element {
// Why: metadata labels are lightweight getters, so subscribe this compact
// control to repaint when the active language changes.
useTranslation()
return (
<TooltipProvider delayDuration={300}>
<ToggleGroup
@@ -1,4 +1,5 @@
import React, { useCallback, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { NodeViewContent, NodeViewWrapper } from '@tiptap/react'
import type { NodeViewProps } from '@tiptap/react'
import { Copy, Check } from 'lucide-react'
@@ -14,100 +15,148 @@ import { translate } from '@/i18n/i18n'
const LANGUAGES = [
{
value: '',
label: translate('auto.components.editor.RichMarkdownCodeBlock.13822cdfda', 'Plain text')
get label() {
return translate('auto.components.editor.RichMarkdownCodeBlock.13822cdfda', 'Plain text')
}
},
{
value: 'bash',
label: translate('auto.components.editor.RichMarkdownCodeBlock.4227cf50fe', 'Bash')
get label() {
return translate('auto.components.editor.RichMarkdownCodeBlock.4227cf50fe', 'Bash')
}
},
{ value: 'c', label: 'C' },
{
value: 'cpp',
label: translate('auto.components.editor.RichMarkdownCodeBlock.4daed43ae3', 'C++')
get label() {
return translate('auto.components.editor.RichMarkdownCodeBlock.4daed43ae3', 'C++')
}
},
{
value: 'css',
label: translate('auto.components.editor.RichMarkdownCodeBlock.026653f21f', 'CSS')
get label() {
return translate('auto.components.editor.RichMarkdownCodeBlock.026653f21f', 'CSS')
}
},
{
value: 'diff',
label: translate('auto.components.editor.RichMarkdownCodeBlock.bf6ee5caaa', 'Diff')
get label() {
return translate('auto.components.editor.RichMarkdownCodeBlock.bf6ee5caaa', 'Diff')
}
},
{
value: 'go',
label: translate('auto.components.editor.RichMarkdownCodeBlock.edfcc64182', 'Go')
get label() {
return translate('auto.components.editor.RichMarkdownCodeBlock.edfcc64182', 'Go')
}
},
{
value: 'graphql',
label: translate('auto.components.editor.RichMarkdownCodeBlock.706fd85738', 'GraphQL')
get label() {
return translate('auto.components.editor.RichMarkdownCodeBlock.706fd85738', 'GraphQL')
}
},
{
value: 'html',
label: translate('auto.components.editor.RichMarkdownCodeBlock.8c4a3fa02d', 'HTML')
get label() {
return translate('auto.components.editor.RichMarkdownCodeBlock.8c4a3fa02d', 'HTML')
}
},
{
value: 'java',
label: translate('auto.components.editor.RichMarkdownCodeBlock.36536ad539', 'Java')
get label() {
return translate('auto.components.editor.RichMarkdownCodeBlock.36536ad539', 'Java')
}
},
{
value: 'javascript',
label: translate('auto.components.editor.RichMarkdownCodeBlock.a209c57063', 'JavaScript')
get label() {
return translate('auto.components.editor.RichMarkdownCodeBlock.a209c57063', 'JavaScript')
}
},
{
value: 'json',
label: translate('auto.components.editor.RichMarkdownCodeBlock.78eba32de4', 'JSON')
get label() {
return translate('auto.components.editor.RichMarkdownCodeBlock.78eba32de4', 'JSON')
}
},
{
value: 'kotlin',
label: translate('auto.components.editor.RichMarkdownCodeBlock.bcb236e2d8', 'Kotlin')
get label() {
return translate('auto.components.editor.RichMarkdownCodeBlock.bcb236e2d8', 'Kotlin')
}
},
{
value: 'markdown',
label: translate('auto.components.editor.RichMarkdownCodeBlock.983b9576b4', 'Markdown')
get label() {
return translate('auto.components.editor.RichMarkdownCodeBlock.983b9576b4', 'Markdown')
}
},
{
value: 'mermaid',
label: translate('auto.components.editor.RichMarkdownCodeBlock.89d6cc14fb', 'Mermaid')
get label() {
return translate('auto.components.editor.RichMarkdownCodeBlock.89d6cc14fb', 'Mermaid')
}
},
{
value: 'python',
label: translate('auto.components.editor.RichMarkdownCodeBlock.2391f9cda9', 'Python')
get label() {
return translate('auto.components.editor.RichMarkdownCodeBlock.2391f9cda9', 'Python')
}
},
{
value: 'ruby',
label: translate('auto.components.editor.RichMarkdownCodeBlock.96182a2f64', 'Ruby')
get label() {
return translate('auto.components.editor.RichMarkdownCodeBlock.96182a2f64', 'Ruby')
}
},
{
value: 'rust',
label: translate('auto.components.editor.RichMarkdownCodeBlock.e72e6b03f4', 'Rust')
get label() {
return translate('auto.components.editor.RichMarkdownCodeBlock.e72e6b03f4', 'Rust')
}
},
{
value: 'scss',
label: translate('auto.components.editor.RichMarkdownCodeBlock.5af8251002', 'SCSS')
get label() {
return translate('auto.components.editor.RichMarkdownCodeBlock.5af8251002', 'SCSS')
}
},
{
value: 'shell',
label: translate('auto.components.editor.RichMarkdownCodeBlock.d01f55be57', 'Shell')
get label() {
return translate('auto.components.editor.RichMarkdownCodeBlock.d01f55be57', 'Shell')
}
},
{
value: 'sql',
label: translate('auto.components.editor.RichMarkdownCodeBlock.3009f722b9', 'SQL')
get label() {
return translate('auto.components.editor.RichMarkdownCodeBlock.3009f722b9', 'SQL')
}
},
{
value: 'swift',
label: translate('auto.components.editor.RichMarkdownCodeBlock.9e384d48dc', 'Swift')
get label() {
return translate('auto.components.editor.RichMarkdownCodeBlock.9e384d48dc', 'Swift')
}
},
{
value: 'typescript',
label: translate('auto.components.editor.RichMarkdownCodeBlock.88d777bc07', 'TypeScript')
get label() {
return translate('auto.components.editor.RichMarkdownCodeBlock.88d777bc07', 'TypeScript')
}
},
{
value: 'xml',
label: translate('auto.components.editor.RichMarkdownCodeBlock.5ef5605cb7', 'XML')
get label() {
return translate('auto.components.editor.RichMarkdownCodeBlock.5ef5605cb7', 'XML')
}
},
{
value: 'yaml',
label: translate('auto.components.editor.RichMarkdownCodeBlock.74eab1d9b2', 'YAML')
get label() {
return translate('auto.components.editor.RichMarkdownCodeBlock.74eab1d9b2', 'YAML')
}
}
]
@@ -115,6 +164,7 @@ export function RichMarkdownCodeBlock({
node,
updateAttributes
}: NodeViewProps): React.JSX.Element {
useTranslation()
const language = (node.attrs.language as string) || ''
const [copied, setCopied] = useState(false)
const copiedResetTimerRef = useRef<number | null>(null)
@@ -17,10 +17,12 @@ type UnsupportedMatch = {
const UNSUPPORTED_PATTERNS: UnsupportedMatch[] = [
{
reason: 'html-or-jsx',
message: translate(
'auto.components.editor.markdown.rich.mode.57128b73e1',
'Editable only in code mode because this file contains HTML, JSX, or MDX.'
),
get message() {
return translate(
'auto.components.editor.markdown.rich.mode.57128b73e1',
'Editable only in code mode because this file contains HTML, JSX, or MDX.'
)
},
// Why: the rich editor preserves common embedded markup via placeholder
// tokens before parsing, but any HTML shape that still fails round-trip
// must fall back instead of risking silent source corruption.
@@ -28,18 +30,22 @@ const UNSUPPORTED_PATTERNS: UnsupportedMatch[] = [
},
{
reason: 'reference-links',
message: translate(
'auto.components.editor.markdown.rich.mode.2fd2b44073',
'Editable only in code mode because this file contains reference-style links.'
),
get message() {
return translate(
'auto.components.editor.markdown.rich.mode.2fd2b44073',
'Editable only in code mode because this file contains reference-style links.'
)
},
pattern: /^\[[^\]]+\]:\s+\S+/m
},
{
reason: 'footnotes',
message: translate(
'auto.components.editor.markdown.rich.mode.7a8ce7c7da',
'Editable only in code mode because this file contains footnotes.'
),
get message() {
return translate(
'auto.components.editor.markdown.rich.mode.7a8ce7c7da',
'Editable only in code mode because this file contains footnotes.'
)
},
pattern: /^\[\^[^\]]+\]:\s+/m
}
]
@@ -0,0 +1,406 @@
import type {} from '@tiptap/extension-mathematics'
import {
ChevronRight,
Heading1,
Heading2,
Heading3,
ImageIcon,
List,
ListOrdered,
Quote,
Sigma,
Table2,
Workflow
} from 'lucide-react'
import { translate } from '@/i18n/i18n'
import {
icon,
insertCodeBlock,
insertTextWithSelection,
insertToggle,
textIcon,
type SlashCommand
} from './rich-markdown-slash-command-primitives'
export type {
SlashCommand,
SlashCommandGroup,
SlashCommandIcon,
SlashCommandId,
SlashMenuState
} from './rich-markdown-slash-command-primitives'
export const slashCommands: SlashCommand[] = [
{
id: 'heading-1',
get label() {
return translate(
'auto.components.editor.rich.markdown.slash.commands.e66e7f04c6',
'Heading 1'
)
},
aliases: ['h1', 'title'],
icon: icon(Heading1),
group: 'Headings',
get description() {
return translate(
'auto.components.editor.rich.markdown.slash.commands.570611864e',
'Large section heading.'
)
},
run: (editor) => {
// Use setHeading (not toggleHeading) so the slash command is idempotent —
// invoking "/h1" on an existing H1 should keep it as H1, not revert to paragraph.
editor.chain().focus().setHeading({ level: 1 }).run()
}
},
{
id: 'toggle-h1',
get label() {
return translate(
'auto.components.editor.rich.markdown.slash.commands.41482b15ce',
'Toggle Heading 1'
)
},
aliases: ['toggle-h1', 'toggle heading', 'details heading', 'collapse heading'],
icon: icon(ChevronRight),
group: 'Headings',
get description() {
return translate(
'auto.components.editor.rich.markdown.slash.commands.3294a2c0cc',
'Create a collapsible section with a large heading summary.'
)
},
run: (editor) => {
insertToggle(editor, 'heading-1')
}
},
{
id: 'heading-2',
get label() {
return translate(
'auto.components.editor.rich.markdown.slash.commands.c209a116b7',
'Heading 2'
)
},
aliases: ['h2'],
icon: icon(Heading2),
group: 'Headings',
get description() {
return translate(
'auto.components.editor.rich.markdown.slash.commands.45cf7ceb3f',
'Medium section heading.'
)
},
run: (editor) => {
// Use setHeading (not toggleHeading) so the slash command is idempotent —
// invoking "/h2" on an existing H2 should keep it as H2, not revert to paragraph.
editor.chain().focus().setHeading({ level: 2 }).run()
}
},
{
id: 'heading-3',
get label() {
return translate(
'auto.components.editor.rich.markdown.slash.commands.30566ee962',
'Heading 3'
)
},
aliases: ['h3'],
icon: icon(Heading3),
group: 'Headings',
get description() {
return translate(
'auto.components.editor.rich.markdown.slash.commands.4920740259',
'Small section heading.'
)
},
run: (editor) => {
// Use setHeading (not toggleHeading) so the slash command is idempotent —
// invoking "/h3" on an existing H3 should keep it as H3, not revert to paragraph.
editor.chain().focus().setHeading({ level: 3 }).run()
}
},
{
id: 'blockquote',
get label() {
return translate('auto.components.editor.rich.markdown.slash.commands.c4c775778b', 'Quote')
},
aliases: ['quote', 'blockquote'],
icon: icon(Quote),
group: 'Basic blocks',
get description() {
return translate(
'auto.components.editor.rich.markdown.slash.commands.6a3def14de',
'Insert a blockquote.'
)
},
run: (editor) => {
editor.chain().focus().toggleBlockquote().run()
}
},
{
id: 'ordered-list',
get label() {
return translate(
'auto.components.editor.rich.markdown.slash.commands.ed4cf0ebce',
'Numbered List'
)
},
aliases: ['ordered', 'ol', 'numbered'],
icon: icon(ListOrdered),
group: 'Basic blocks',
get description() {
return translate(
'auto.components.editor.rich.markdown.slash.commands.8e00aba296',
'Create an ordered list.'
)
},
run: (editor) => {
editor.chain().focus().toggleOrderedList().run()
}
},
{
id: 'bullet-list',
get label() {
return translate(
'auto.components.editor.rich.markdown.slash.commands.56ff3237e7',
'Bullet List'
)
},
aliases: ['bullet', 'ul', 'list'],
icon: icon(List),
group: 'Basic blocks',
get description() {
return translate(
'auto.components.editor.rich.markdown.slash.commands.c9b9e826b8',
'Create an unordered list.'
)
},
run: (editor) => {
editor.chain().focus().toggleBulletList().run()
}
},
{
id: 'task-list',
get label() {
return translate(
'auto.components.editor.rich.markdown.slash.commands.d0d2cdfbdb',
'Check List'
)
},
aliases: ['todo', 'task', 'checkbox'],
icon: icon(List),
group: 'Basic blocks',
get description() {
return translate(
'auto.components.editor.rich.markdown.slash.commands.d766f44867',
'Create a checklist.'
)
},
run: (editor) => {
editor.chain().focus().toggleTaskList().run()
}
},
{
id: 'text',
get label() {
return translate(
'auto.components.editor.rich.markdown.slash.commands.58abdb9d41',
'Paragraph'
)
},
aliases: ['paragraph', 'plain'],
icon: icon(List),
group: 'Basic blocks',
get description() {
return translate(
'auto.components.editor.rich.markdown.slash.commands.9a7fe896dc',
'Start a normal paragraph.'
)
},
run: (editor) => {
editor.chain().focus().setParagraph().run()
}
},
{
id: 'toggle-text',
get label() {
return translate(
'auto.components.editor.rich.markdown.slash.commands.f82c78a2ee',
'Toggle Text'
)
},
aliases: ['toggle', 'details', 'collapse', 'toggle-text'],
icon: icon(ChevronRight),
group: 'Basic blocks',
get description() {
return translate(
'auto.components.editor.rich.markdown.slash.commands.972ef9aeea',
'Create a collapsible text section.'
)
},
run: (editor) => {
insertToggle(editor)
}
},
{
id: 'code-block',
get label() {
return translate(
'auto.components.editor.rich.markdown.slash.commands.624b50cf25',
'Code Block'
)
},
aliases: ['code', 'snippet'],
icon: icon(List),
group: 'Basic blocks',
get description() {
return translate(
'auto.components.editor.rich.markdown.slash.commands.89e327e054',
'Insert a fenced code block.'
)
},
run: (editor) => {
editor.chain().focus().toggleCodeBlock().run()
}
},
{
id: 'divider',
get label() {
return translate('auto.components.editor.rich.markdown.slash.commands.ae8377cf6b', 'Divider')
},
aliases: ['divider', 'rule', 'hr'],
icon: icon(List),
group: 'Basic blocks',
get description() {
return translate(
'auto.components.editor.rich.markdown.slash.commands.fae45ef4d3',
'Insert a horizontal rule.'
)
},
run: (editor) => {
editor.chain().focus().setHorizontalRule().run()
}
},
{
id: 'table',
get label() {
return translate('auto.components.editor.rich.markdown.slash.commands.19ea597868', 'Table')
},
aliases: ['grid', 'columns', 'rows'],
icon: icon(Table2),
group: 'Advanced',
get description() {
return translate(
'auto.components.editor.rich.markdown.slash.commands.67faab829b',
'Insert a 3x3 markdown table.'
)
},
run: (editor) => {
editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run()
}
},
{
id: 'mermaid',
get label() {
return translate(
'auto.components.editor.rich.markdown.slash.commands.e516d3f6e3',
'Mermaid Diagram'
)
},
aliases: ['diagram', 'flowchart', 'chart', 'graph'],
icon: icon(Workflow),
group: 'Advanced',
get description() {
return translate(
'auto.components.editor.rich.markdown.slash.commands.0ed9a7b38c',
'Insert a Mermaid fenced block.'
)
},
run: (editor) => {
insertCodeBlock(editor, 'mermaid', 'graph TD\n A[Start] --> B[End]')
}
},
{
id: 'inline-math',
get label() {
return translate(
'auto.components.editor.rich.markdown.slash.commands.2bf5544faf',
'Inline Math'
)
},
aliases: ['math', 'latex', 'equation', 'formula'],
icon: icon(Sigma),
group: 'Advanced',
get description() {
return translate(
'auto.components.editor.rich.markdown.slash.commands.565907cf7a',
'Insert inline LaTeX math.'
)
},
run: (editor) => {
editor.commands.insertInlineMath({ latex: 'x' })
}
},
{
id: 'math-block',
get label() {
return translate(
'auto.components.editor.rich.markdown.slash.commands.6993a38ad1',
'Math Block'
)
},
aliases: ['display math', 'latex block', 'equation block'],
icon: icon(Sigma),
group: 'Advanced',
get description() {
return translate(
'auto.components.editor.rich.markdown.slash.commands.ae7d0f3f37',
'Insert display LaTeX math.'
)
},
run: (editor) => {
editor.commands.insertBlockMath({ latex: 'x' })
}
},
{
id: 'image',
get label() {
return translate('auto.components.editor.rich.markdown.slash.commands.572be8e524', 'Image')
},
aliases: ['image', 'img'],
icon: icon(ImageIcon),
group: 'Media',
get description() {
return translate(
'auto.components.editor.rich.markdown.slash.commands.3324eb391a',
'Insert an image from your computer.'
)
},
// Why: window.prompt() is not supported in Electron's renderer process,
// so image URL input is handled by an inline input bar in RichMarkdownEditor.
run: (editor) => {
editor.chain().focus().run()
}
},
{
id: 'emoji',
get label() {
return translate('auto.components.editor.rich.markdown.slash.commands.8a30cbaeca', 'Emoji')
},
aliases: ['smile', 'reaction', 'icon'],
icon: textIcon('🙂'),
group: 'Others',
get description() {
return translate(
'auto.components.editor.rich.markdown.slash.commands.07e1b32396',
'Insert a plain Unicode emoji.'
)
},
run: (editor) => {
insertTextWithSelection(editor, '🙂')
}
}
]
@@ -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,135 +1,15 @@
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,
Heading1,
Heading2,
Heading3,
ImageIcon,
List,
ListOrdered,
Quote,
Sigma,
Table2,
Workflow
} from 'lucide-react'
import { translate } from '@/i18n/i18n'
import type { SlashCommand, SlashMenuState } from './rich-markdown-slash-command-catalog'
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
}
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 { slashCommands } from './rich-markdown-slash-command-catalog'
export type {
SlashCommand,
SlashCommandGroup,
SlashCommandIcon,
SlashCommandId,
SlashMenuState
} from './rich-markdown-slash-command-catalog'
/**
* Executes a slash command by first deleting the typed slash text, then
@@ -157,215 +37,6 @@ export function runSlashCommand(
command.run(editor)
}
export const slashCommands: SlashCommand[] = [
{
id: 'heading-1',
label: translate("auto.components.editor.rich.markdown.slash.commands.e66e7f04c6", "Heading 1"),
aliases: ['h1', 'title'],
icon: icon(Heading1),
group: 'Headings',
description: translate("auto.components.editor.rich.markdown.slash.commands.570611864e", "Large section heading."),
run: (editor) => {
// Use setHeading (not toggleHeading) so the slash command is idempotent —
// invoking "/h1" on an existing H1 should keep it as H1, not revert to paragraph.
editor.chain().focus().setHeading({ level: 1 }).run()
}
},
{
id: 'toggle-h1',
label: translate("auto.components.editor.rich.markdown.slash.commands.41482b15ce", "Toggle Heading 1"),
aliases: ['toggle-h1', 'toggle heading', 'details heading', 'collapse heading'],
icon: icon(ChevronRight),
group: 'Headings',
description: translate("auto.components.editor.rich.markdown.slash.commands.3294a2c0cc", "Create a collapsible section with a large heading summary."),
run: (editor) => {
insertToggle(editor, 'heading-1')
}
},
{
id: 'heading-2',
label: translate("auto.components.editor.rich.markdown.slash.commands.c209a116b7", "Heading 2"),
aliases: ['h2'],
icon: icon(Heading2),
group: 'Headings',
description: translate("auto.components.editor.rich.markdown.slash.commands.45cf7ceb3f", "Medium section heading."),
run: (editor) => {
// Use setHeading (not toggleHeading) so the slash command is idempotent —
// invoking "/h2" on an existing H2 should keep it as H2, not revert to paragraph.
editor.chain().focus().setHeading({ level: 2 }).run()
}
},
{
id: 'heading-3',
label: translate("auto.components.editor.rich.markdown.slash.commands.30566ee962", "Heading 3"),
aliases: ['h3'],
icon: icon(Heading3),
group: 'Headings',
description: translate("auto.components.editor.rich.markdown.slash.commands.4920740259", "Small section heading."),
run: (editor) => {
// Use setHeading (not toggleHeading) so the slash command is idempotent —
// invoking "/h3" on an existing H3 should keep it as H3, not revert to paragraph.
editor.chain().focus().setHeading({ level: 3 }).run()
}
},
{
id: 'blockquote',
label: translate("auto.components.editor.rich.markdown.slash.commands.c4c775778b", "Quote"),
aliases: ['quote', 'blockquote'],
icon: icon(Quote),
group: 'Basic blocks',
description: translate("auto.components.editor.rich.markdown.slash.commands.6a3def14de", "Insert a blockquote."),
run: (editor) => {
editor.chain().focus().toggleBlockquote().run()
}
},
{
id: 'ordered-list',
label: translate("auto.components.editor.rich.markdown.slash.commands.ed4cf0ebce", "Numbered List"),
aliases: ['ordered', 'ol', 'numbered'],
icon: icon(ListOrdered),
group: 'Basic blocks',
description: translate("auto.components.editor.rich.markdown.slash.commands.8e00aba296", "Create an ordered list."),
run: (editor) => {
editor.chain().focus().toggleOrderedList().run()
}
},
{
id: 'bullet-list',
label: translate("auto.components.editor.rich.markdown.slash.commands.56ff3237e7", "Bullet List"),
aliases: ['bullet', 'ul', 'list'],
icon: icon(List),
group: 'Basic blocks',
description: translate("auto.components.editor.rich.markdown.slash.commands.c9b9e826b8", "Create an unordered list."),
run: (editor) => {
editor.chain().focus().toggleBulletList().run()
}
},
{
id: 'task-list',
label: translate("auto.components.editor.rich.markdown.slash.commands.d0d2cdfbdb", "Check List"),
aliases: ['todo', 'task', 'checkbox'],
icon: icon(List),
group: 'Basic blocks',
description: translate("auto.components.editor.rich.markdown.slash.commands.d766f44867", "Create a checklist."),
run: (editor) => {
editor.chain().focus().toggleTaskList().run()
}
},
{
id: 'text',
label: translate("auto.components.editor.rich.markdown.slash.commands.58abdb9d41", "Paragraph"),
aliases: ['paragraph', 'plain'],
icon: icon(List),
group: 'Basic blocks',
description: translate("auto.components.editor.rich.markdown.slash.commands.9a7fe896dc", "Start a normal paragraph."),
run: (editor) => {
editor.chain().focus().setParagraph().run()
}
},
{
id: 'toggle-text',
label: translate("auto.components.editor.rich.markdown.slash.commands.f82c78a2ee", "Toggle Text"),
aliases: ['toggle', 'details', 'collapse', 'toggle-text'],
icon: icon(ChevronRight),
group: 'Basic blocks',
description: translate("auto.components.editor.rich.markdown.slash.commands.972ef9aeea", "Create a collapsible text section."),
run: (editor) => {
insertToggle(editor)
}
},
{
id: 'code-block',
label: translate("auto.components.editor.rich.markdown.slash.commands.624b50cf25", "Code Block"),
aliases: ['code', 'snippet'],
icon: icon(List),
group: 'Basic blocks',
description: translate("auto.components.editor.rich.markdown.slash.commands.89e327e054", "Insert a fenced code block."),
run: (editor) => {
editor.chain().focus().toggleCodeBlock().run()
}
},
{
id: 'divider',
label: translate("auto.components.editor.rich.markdown.slash.commands.ae8377cf6b", "Divider"),
aliases: ['divider', 'rule', 'hr'],
icon: icon(List),
group: 'Basic blocks',
description: translate("auto.components.editor.rich.markdown.slash.commands.fae45ef4d3", "Insert a horizontal rule."),
run: (editor) => {
editor.chain().focus().setHorizontalRule().run()
}
},
{
id: 'table',
label: translate("auto.components.editor.rich.markdown.slash.commands.19ea597868", "Table"),
aliases: ['grid', 'columns', 'rows'],
icon: icon(Table2),
group: 'Advanced',
description: translate("auto.components.editor.rich.markdown.slash.commands.67faab829b", "Insert a 3x3 markdown table."),
run: (editor) => {
editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run()
}
},
{
id: 'mermaid',
label: translate("auto.components.editor.rich.markdown.slash.commands.e516d3f6e3", "Mermaid Diagram"),
aliases: ['diagram', 'flowchart', 'chart', 'graph'],
icon: icon(Workflow),
group: 'Advanced',
description: translate("auto.components.editor.rich.markdown.slash.commands.0ed9a7b38c", "Insert a Mermaid fenced block."),
run: (editor) => {
insertCodeBlock(editor, 'mermaid', 'graph TD\n A[Start] --> B[End]')
}
},
{
id: 'inline-math',
label: translate("auto.components.editor.rich.markdown.slash.commands.2bf5544faf", "Inline Math"),
aliases: ['math', 'latex', 'equation', 'formula'],
icon: icon(Sigma),
group: 'Advanced',
description: translate("auto.components.editor.rich.markdown.slash.commands.565907cf7a", "Insert inline LaTeX math."),
run: (editor) => {
editor.commands.insertInlineMath({ latex: 'x' })
}
},
{
id: 'math-block',
label: translate("auto.components.editor.rich.markdown.slash.commands.6993a38ad1", "Math Block"),
aliases: ['display math', 'latex block', 'equation block'],
icon: icon(Sigma),
group: 'Advanced',
description: translate("auto.components.editor.rich.markdown.slash.commands.ae7d0f3f37", "Insert display LaTeX math."),
run: (editor) => {
editor.commands.insertBlockMath({ latex: 'x' })
}
},
{
id: 'image',
label: translate("auto.components.editor.rich.markdown.slash.commands.572be8e524", "Image"),
aliases: ['image', 'img'],
icon: icon(ImageIcon),
group: 'Media',
description: translate("auto.components.editor.rich.markdown.slash.commands.3324eb391a", "Insert an image from your computer."),
// Why: window.prompt() is not supported in Electron's renderer process,
// so image URL input is handled by an inline input bar in RichMarkdownEditor.
run: (editor) => {
editor.chain().focus().run()
}
},
{
id: 'emoji',
label: translate("auto.components.editor.rich.markdown.slash.commands.8a30cbaeca", "Emoji"),
aliases: ['smile', 'reaction', 'icon'],
icon: textIcon('🙂'),
group: 'Others',
description: translate("auto.components.editor.rich.markdown.slash.commands.07e1b32396", "Insert a plain Unicode emoji."),
run: (editor) => {
insertTextWithSelection(editor, '🙂')
}
}
]
/**
* Inspects the editor selection to decide whether the slash-command menu
* should be open (and where to position it), or dismissed.
@@ -142,38 +142,50 @@ type AgentCapabilitySetupRow = {
const AGENT_CAPABILITY_SETUP_ROWS: readonly AgentCapabilitySetupRow[] = [
{
id: 'orchestration',
title: translate(
'auto.components.feature.wall.AgentCapabilitiesSetupAction.ac07f8887f',
'Agent Orchestration'
),
description: translate(
'auto.components.feature.wall.AgentCapabilitiesSetupAction.c61c91e642',
'Let agents coordinate through Orca to keep large, multi-step tasks moving to completion.'
),
get title() {
return translate(
'auto.components.feature.wall.AgentCapabilitiesSetupAction.ac07f8887f',
'Agent Orchestration'
)
},
get description() {
return translate(
'auto.components.feature.wall.AgentCapabilitiesSetupAction.c61c91e642',
'Let agents coordinate through Orca to keep large, multi-step tasks moving to completion.'
)
},
icon: <Workflow className="size-4" />
},
{
id: 'browserUse',
title: translate(
'auto.components.feature.wall.AgentCapabilitiesSetupAction.e638da007a',
'Agent Browser Use'
),
description: translate(
'auto.components.feature.wall.AgentCapabilitiesSetupAction.5e8fe5a72d',
"Give agents direct access to Orca's browser so they can test pages, capture screenshots, and act on what they see."
),
get title() {
return translate(
'auto.components.feature.wall.AgentCapabilitiesSetupAction.e638da007a',
'Agent Browser Use'
)
},
get description() {
return translate(
'auto.components.feature.wall.AgentCapabilitiesSetupAction.5e8fe5a72d',
"Give agents direct access to Orca's browser so they can test pages, capture screenshots, and act on what they see."
)
},
icon: <Globe2 className="size-4" />
},
{
id: 'computerUse',
title: translate(
'auto.components.feature.wall.AgentCapabilitiesSetupAction.362a07517d',
'Computer Use'
),
description: translate(
'auto.components.feature.wall.AgentCapabilitiesSetupAction.1b51644c2d',
'Let agents control the desktop, moving the cursor, clicking, and typing in any app.'
),
get title() {
return translate(
'auto.components.feature.wall.AgentCapabilitiesSetupAction.362a07517d',
'Computer Use'
)
},
get description() {
return translate(
'auto.components.feature.wall.AgentCapabilitiesSetupAction.1b51644c2d',
'Let agents control the desktop, moving the cursor, clicking, and typing in any app.'
)
},
icon: <MonitorCog className="size-4" />
}
]
@@ -127,17 +127,22 @@ const TERM_ENTRIES: readonly { entry: TermEntry; minPhase: Phase }[] = [
{
entry: {
kind: 'ok',
html: (
<>
{translate('auto.components.feature.wall.BrowserAnimatedVisual.4fa59ca545', '✓ Updated')}{' '}
<code className="text-emerald-600 dark:text-emerald-400">
get html() {
return (
<>
{translate(
'auto.components.feature.wall.BrowserAnimatedVisual.051c97d15a',
'.pp-card[data-card="starter"] .pp-cta'
)}
</code>
</>
)
'auto.components.feature.wall.BrowserAnimatedVisual.4fa59ca545',
'✓ Updated'
)}{' '}
<code className="text-emerald-600 dark:text-emerald-400">
{translate(
'auto.components.feature.wall.BrowserAnimatedVisual.051c97d15a',
'.pp-card[data-card="starter"] .pp-cta'
)}
</code>
</>
)
}
},
minPhase: 'updated'
},
@@ -156,14 +161,16 @@ const TERM_ENTRIES: readonly { entry: TermEntry; minPhase: Phase }[] = [
{
entry: {
kind: 'ok',
html: (
<>
{translate(
'auto.components.feature.wall.BrowserAnimatedVisual.eb88125c6f',
'✓ Verified — Try free still works.'
)}
</>
)
get html() {
return (
<>
{translate(
'auto.components.feature.wall.BrowserAnimatedVisual.eb88125c6f',
'✓ Verified — Try free still works.'
)}
</>
)
}
},
minPhase: 'verified'
}
@@ -7,48 +7,25 @@ import {
MailGlyph,
WorkingSpinner
} from './feature-tour-preview-glyphs'
import {
FEATURE_TOUR_ORCHESTRATION_CHILDREN,
FEATURE_TOUR_PREVIEW_COPY
} from './feature-tour-preview-copy'
import { FeatureTourWorkspaceCard } from './FeatureTourWorkspaceCard'
import { translate } from '@/i18n/i18n'
type FrameId = 1 | 2 | 3 | 4
export type FeatureTourPreviewFrameCopy = {
id: FrameId
title: string
caption: string
}
export const FEATURE_TOUR_PREVIEW_COPY: readonly FeatureTourPreviewFrameCopy[] = [
{
id: 1,
title: translate("auto.components.feature.wall.FeatureTourPreview.56a0271428", "Isolated workspaces"),
caption:
'Ship several things at once. Each workspace keeps its branch, terminal, and agent activity together.'
},
{
id: 2,
title: translate("auto.components.feature.wall.FeatureTourPreview.e44269e97d", "Agent orchestration"),
caption: 'Hand off a goal and walk away. A coordinator agent fans out and ships parallel PRs.'
},
{
id: 3,
title: translate("auto.components.feature.wall.FeatureTourPreview.ef737dcee1", "GitHub & Linear tasks"),
caption:
'Skip the tab-switching. Pick from your GitHub or Linear backlog and start a workspace in one click.'
},
{
id: 4,
title: translate("auto.components.feature.wall.FeatureTourPreview.1aa8a9a24a", "Splittable terminal"),
caption:
'Open any workspace to return to its terminal, then split panes for tests, logs, and agents.'
}
]
export { FEATURE_TOUR_PREVIEW_COPY } from './feature-tour-preview-copy'
export type { FeatureTourPreviewFrameCopy } from './feature-tour-preview-copy'
function WorkspaceFrame(): JSX.Element {
return (
<div className="absolute inset-0 flex flex-col gap-5 bg-card px-4 py-4">
<div className="text-[14.5px] font-semibold uppercase tracking-[0.07em] leading-none text-muted-foreground">
{translate("auto.components.feature.wall.FeatureTourPreview.56a0271428", "Isolated workspaces")}</div>
{translate(
'auto.components.feature.wall.FeatureTourPreview.56a0271428',
'Isolated workspaces'
)}
</div>
{/* Why: 3 cards in a row tells the "ship several at once" story by
composition; the wide preview aspect (~4.9:1) makes a vertical stack
read as wasted space. The grid auto-sizes (no flex-1) so the cards
@@ -57,7 +34,10 @@ function WorkspaceFrame(): JSX.Element {
<div className="grid grid-cols-3 gap-3 px-4">
<FeatureTourWorkspaceCard
status="working"
title={translate("auto.components.feature.wall.FeatureTourPreview.3c4adfd821", "fix login race condition")}
title={translate(
'auto.components.feature.wall.FeatureTourPreview.3c4adfd821',
'fix login race condition'
)}
agents={[
{ kind: 'claude', barWidth: '60%', state: 'working' },
{ kind: 'codex', barWidth: '52%', state: 'working' }
@@ -65,12 +45,18 @@ function WorkspaceFrame(): JSX.Element {
/>
<FeatureTourWorkspaceCard
status="done"
title={translate("auto.components.feature.wall.FeatureTourPreview.9c812e0d7c", "speed up CI pipeline")}
title={translate(
'auto.components.feature.wall.FeatureTourPreview.9c812e0d7c',
'speed up CI pipeline'
)}
agents={[{ kind: 'opencode-go', barWidth: '70%', state: 'done' }]}
/>
<FeatureTourWorkspaceCard
status="working"
title={translate("auto.components.feature.wall.FeatureTourPreview.e38112b289", "refactor billing webhook")}
title={translate(
'auto.components.feature.wall.FeatureTourPreview.e38112b289',
'refactor billing webhook'
)}
agents={[{ kind: 'claude', barWidth: '38%', state: 'working' }]}
/>
</div>
@@ -78,22 +64,6 @@ function WorkspaceFrame(): JSX.Element {
)
}
type OrchChildAgent = 'claude' | 'codex' | 'opencode-go'
const ORCH_CHILDREN: readonly {
key: 'top' | 'mid' | 'bot'
position: string
label: string
agent: OrchChildAgent
}[] = [
// Why: card vertical centers anchor to 18% / 50% / 82% — the same Y
// endpoints the dashed SVG paths terminate at — so the connectors land on
// each card's center regardless of card height.
{ key: 'top', position: 'top-[18%] -translate-y-1/2', label: translate("auto.components.feature.wall.FeatureTourPreview.b1f17bcc74", "PR 1/3"), agent: 'claude' },
{ key: 'mid', position: 'top-1/2 -translate-y-1/2', label: translate("auto.components.feature.wall.FeatureTourPreview.cfdfd4d6b4", "PR 2/3"), agent: 'codex' },
{ key: 'bot', position: 'top-[82%] -translate-y-1/2', label: translate("auto.components.feature.wall.FeatureTourPreview.ec4a73f5e6", "PR 3/3"), agent: 'opencode-go' }
]
function OrchestrationFrame(): JSX.Element {
// Why: a horizontal fan (root → 3 children L→R) reads naturally as
// "fans out and ships parallel PRs" at the wide aspect; the previous
@@ -104,7 +74,11 @@ function OrchestrationFrame(): JSX.Element {
return (
<div className="absolute inset-0 flex flex-col gap-5 bg-card px-4 py-4">
<div className="text-[14.5px] font-semibold uppercase tracking-[0.07em] leading-none text-muted-foreground">
{translate("auto.components.feature.wall.FeatureTourPreview.e44269e97d", "Agent orchestration")}</div>
{translate(
'auto.components.feature.wall.FeatureTourPreview.e44269e97d',
'Agent orchestration'
)}
</div>
<div className="relative w-full flex-1">
{/* Why: viewBox is percent-units (100×100, preserveAspectRatio="none")
so endpoints anchor to the same percentage anchors as the cards
@@ -158,20 +132,28 @@ function OrchestrationFrame(): JSX.Element {
<div className="flex items-center gap-2">
<WorkingSpinner />
<span className="truncate text-[15px] font-medium leading-none text-foreground">
{translate("auto.components.feature.wall.FeatureTourPreview.cebc7769cd", "redesign auth flow")}</span>
{translate(
'auto.components.feature.wall.FeatureTourPreview.cebc7769cd',
'redesign auth flow'
)}
</span>
</div>
<div className="mt-2 flex items-center gap-1.5 pl-3.5">
<WorkingSpinner size="xs" />
<ClaudeIcon size={13} />
<span className="truncate text-[12.5px] leading-none text-muted-foreground">
{translate("auto.components.feature.wall.FeatureTourPreview.5171768676", "orchestrating 3 agents")}</span>
{translate(
'auto.components.feature.wall.FeatureTourPreview.5171768676',
'orchestrating 3 agents'
)}
</span>
</div>
</div>
{/* Why: children mirror the parent's WorkspaceCard composition so the
fan reads as "coordinator workspace dispatches to 3 child
workspaces, each running its own agent." */}
{ORCH_CHILDREN.map(({ key, position, label, agent }) => (
{FEATURE_TOUR_ORCHESTRATION_CHILDREN.map(({ key, position, label, agent }) => (
<div
key={key}
className={cn(
@@ -188,9 +170,9 @@ function OrchestrationFrame(): JSX.Element {
</div>
<div className="mt-2 flex items-center gap-1.5 pl-3.5">
<WorkingSpinner size="xs" />
{agent === "claude" ? (
{agent === 'claude' ? (
<ClaudeIcon size={12} />
) : agent === "codex" ? (
) : agent === 'codex' ? (
<CodexInlineIcon />
) : (
<OpenCodeGoIcon size={12} />
@@ -200,7 +182,7 @@ function OrchestrationFrame(): JSX.Element {
</div>
))}
{ORCH_CHILDREN.map(({ key }) => (
{FEATURE_TOUR_ORCHESTRATION_CHILDREN.map(({ key }) => (
<div key={`bubble-${key}`} className={cn('feature-tour-orch-bubble', key)}>
<MailGlyph />
</div>
@@ -218,12 +200,17 @@ function TasksFrame(): JSX.Element {
return (
<div className="absolute inset-0 flex flex-col gap-5 bg-card px-4 py-4">
<div className="text-[14.5px] font-semibold uppercase tracking-[0.07em] leading-none text-muted-foreground">
{translate("auto.components.feature.wall.FeatureTourPreview.bee6b4088d", "GitHub & Linear tasks")}</div>
{translate(
'auto.components.feature.wall.FeatureTourPreview.bee6b4088d',
'GitHub & Linear tasks'
)}
</div>
<div className="relative grid flex-1 grid-cols-[minmax(0,1fr)_minmax(0,1fr)] items-center gap-4 px-4">
<div className="flex flex-col gap-2">
<div className="flex h-9 items-center gap-2.5 rounded-md border border-border bg-background px-3">
<span className="inline-flex h-5 items-center justify-center rounded-[3px] border border-border bg-muted px-1.5 font-mono text-[13px] leading-none text-muted-foreground">
{translate("auto.components.feature.wall.FeatureTourPreview.0688842445", "GH #1799")}</span>
{translate('auto.components.feature.wall.FeatureTourPreview.0688842445', 'GH #1799')}
</span>
{/* Why: surrounding rows show only the issue number + a skeleton
so the user's eye is drawn to the row that has real text — the
one the cursor clicks on. */}
@@ -231,12 +218,18 @@ function TasksFrame(): JSX.Element {
</div>
<div className="feature-tour-tasks-row relative flex h-9 items-center gap-2.5 rounded-md border border-border bg-background px-3">
<span className="inline-flex h-5 items-center justify-center rounded-[3px] border border-border bg-muted px-1.5 font-mono text-[13px] leading-none text-muted-foreground">
{translate("auto.components.feature.wall.FeatureTourPreview.fc0cc0b267", "GH #1842")}</span>
{translate('auto.components.feature.wall.FeatureTourPreview.fc0cc0b267', 'GH #1842')}
</span>
<span className="truncate text-[15px] font-medium leading-none text-foreground">
{translate("auto.components.feature.wall.FeatureTourPreview.c1f28c03b2", "Worktree picker truncates")}</span>
{translate(
'auto.components.feature.wall.FeatureTourPreview.c1f28c03b2',
'Worktree picker truncates'
)}
</span>
<span className="feature-tour-tasks-pill relative ml-auto flex h-6 items-center justify-center overflow-hidden rounded-full border border-emerald-500/30 bg-emerald-500/15">
<span className="feature-tour-tasks-pill-label flex items-center gap-1 whitespace-nowrap pl-3 pr-2.5 text-[13px] font-semibold leading-none tracking-[0.01em] text-primary-foreground">
{translate("auto.components.feature.wall.FeatureTourPreview.40bbd92ef4", "Start")}<svg
{translate('auto.components.feature.wall.FeatureTourPreview.40bbd92ef4', 'Start')}
<svg
width="11"
height="11"
viewBox="0 0 16 16"
@@ -262,7 +255,8 @@ function TasksFrame(): JSX.Element {
</div>
<div className="flex h-9 items-center gap-2.5 rounded-md border border-border bg-background px-3">
<span className="inline-flex h-5 items-center justify-center rounded-[3px] border border-border bg-muted px-1.5 font-mono text-[13px] leading-none text-muted-foreground">
{translate("auto.components.feature.wall.FeatureTourPreview.d54aefe09e", "LIN-329")}</span>
{translate('auto.components.feature.wall.FeatureTourPreview.d54aefe09e', 'LIN-329')}
</span>
<span className="h-2 w-[45%] rounded-full bg-foreground/12" />
</div>
</div>
@@ -271,7 +265,11 @@ function TasksFrame(): JSX.Element {
<div className="flex items-center gap-2.5">
<WorkingSpinner />
<span className="truncate text-[15.5px] font-medium leading-none text-foreground">
{translate("auto.components.feature.wall.FeatureTourPreview.3822d8d14b", "fix/worktree-picker-truncates")}</span>
{translate(
'auto.components.feature.wall.FeatureTourPreview.3822d8d14b',
'fix/worktree-picker-truncates'
)}
</span>
<span className="ml-auto inline-flex">
<ClaudeIcon size={13} />
</span>
@@ -281,7 +279,12 @@ function TasksFrame(): JSX.Element {
<ClaudeIcon size={12} />
<span className="h-2 w-[55%] rounded-full bg-foreground/15" />
</div>
<div className="text-[13.5px] leading-none text-muted-foreground">{translate("auto.components.feature.wall.FeatureTourPreview.2a7cfc82c8", "Linked to GH #1842")}</div>
<div className="text-[13.5px] leading-none text-muted-foreground">
{translate(
'auto.components.feature.wall.FeatureTourPreview.2a7cfc82c8',
'Linked to GH #1842'
)}
</div>
</div>
</div>
</div>
@@ -292,48 +295,73 @@ function TerminalFrame(): JSX.Element {
return (
<div className="absolute inset-0 flex flex-col gap-5 bg-card px-4 py-4">
<div className="text-[14.5px] font-semibold uppercase tracking-[0.07em] leading-none text-muted-foreground">
{translate("auto.components.feature.wall.FeatureTourPreview.1aa8a9a24a", "Splittable terminal")}</div>
{translate(
'auto.components.feature.wall.FeatureTourPreview.1aa8a9a24a',
'Splittable terminal'
)}
</div>
<div className="mx-4 flex min-h-0 flex-1 flex-col overflow-hidden rounded-md border border-border bg-background">
<div className="flex items-center gap-1.5 border-b border-border bg-muted/40 px-2 py-1">
<span className="size-1.5 rounded-full bg-foreground/15" />
<span className="size-1.5 rounded-full bg-foreground/15" />
<span className="size-1.5 rounded-full bg-foreground/15" />
<span className="ml-2 font-mono text-[13.5px] leading-none text-muted-foreground">
{translate("auto.components.feature.wall.FeatureTourPreview.04d54d50ec", "orca · zsh")}</span>
{translate('auto.components.feature.wall.FeatureTourPreview.04d54d50ec', 'orca · zsh')}
</span>
</div>
<div className="grid flex-1 grid-cols-2 divide-x divide-border font-mono text-[14.5px] leading-[1.4] text-foreground">
<div className="min-w-0 p-2">
<div className="flex items-center gap-1">
<span className="text-emerald-500">$</span>
<span className="feature-tour-terminal-line relative inline-block whitespace-nowrap text-foreground">
{translate("auto.components.feature.wall.FeatureTourPreview.6218a9014d", "pnpm playwright test")}</span>
{translate(
'auto.components.feature.wall.FeatureTourPreview.6218a9014d',
'pnpm playwright test'
)}
</span>
</div>
<div className="mt-1.5 flex flex-col gap-1">
<div
className="feature-tour-terminal-output truncate text-muted-foreground"
data-line="1"
>
{translate("auto.components.feature.wall.FeatureTourPreview.8279e9d95b", "Running 12 tests")}</div>
{translate(
'auto.components.feature.wall.FeatureTourPreview.8279e9d95b',
'Running 12 tests'
)}
</div>
<div
className="feature-tour-terminal-output flex min-w-0 items-center gap-1.5"
data-line="2"
>
<span className="font-bold text-emerald-600"></span>
<span className="truncate">{translate("auto.components.feature.wall.FeatureTourPreview.24fedd5a52", "login.spec.ts")}</span>
<span className="truncate">
{translate(
'auto.components.feature.wall.FeatureTourPreview.24fedd5a52',
'login.spec.ts'
)}
</span>
</div>
<div
className="feature-tour-terminal-output flex min-w-0 items-center gap-1.5"
data-line="3"
>
<span className="inline-block size-2 animate-spin rounded-full border-[1.5px] border-foreground/20 border-t-foreground" />
<span className="truncate">{translate("auto.components.feature.wall.FeatureTourPreview.6ed43cb0e0", "dashboard.spec.ts")}</span>
<span className="truncate">
{translate(
'auto.components.feature.wall.FeatureTourPreview.6ed43cb0e0',
'dashboard.spec.ts'
)}
</span>
</div>
</div>
</div>
<div className="min-w-0 p-2">
<div className="flex items-center gap-1">
<span className="text-emerald-500">$</span>
<span className="text-foreground">{translate("auto.components.feature.wall.FeatureTourPreview.771d8881c2", "claude")}</span>
<span className="text-foreground">
{translate('auto.components.feature.wall.FeatureTourPreview.771d8881c2', 'claude')}
</span>
</div>
<div className="mt-1.5 flex flex-col gap-1">
<div
@@ -341,21 +369,38 @@ function TerminalFrame(): JSX.Element {
data-line="1"
>
<ClaudeIcon size={12} />
<span className="truncate text-muted-foreground">{translate("auto.components.feature.wall.FeatureTourPreview.952d3ddd9a", "session started")}</span>
<span className="truncate text-muted-foreground">
{translate(
'auto.components.feature.wall.FeatureTourPreview.952d3ddd9a',
'session started'
)}
</span>
</div>
<div
className="feature-tour-terminal-output flex min-w-0 items-center gap-1"
data-line="2"
>
<span className="text-amber-600">{translate("auto.components.feature.wall.FeatureTourPreview.1170621527", ">")}</span>
<span className="truncate">{translate("auto.components.feature.wall.FeatureTourPreview.ef8b164dd1", "review src/auth")}</span>
<span className="text-amber-600">
{translate('auto.components.feature.wall.FeatureTourPreview.1170621527', '>')}
</span>
<span className="truncate">
{translate(
'auto.components.feature.wall.FeatureTourPreview.ef8b164dd1',
'review src/auth'
)}
</span>
</div>
<div
className="feature-tour-terminal-output flex min-w-0 items-center gap-1.5"
data-line="3"
>
<span className="inline-block size-2 animate-spin rounded-full border-[1.5px] border-amber-600/20 border-t-amber-600" />
<span className="truncate text-muted-foreground">{translate("auto.components.feature.wall.FeatureTourPreview.304ad0dfc1", "Thinking...")}</span>
<span className="truncate text-muted-foreground">
{translate(
'auto.components.feature.wall.FeatureTourPreview.304ad0dfc1',
'Thinking...'
)}
</span>
</div>
</div>
</div>
@@ -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'
@@ -13,10 +14,46 @@ const SIDEBAR_TABS: readonly {
icon: ComponentType<{ className?: string; size?: number }>
label: string
}[] = [
{ id: 'explorer', icon: Files, label: translate("auto.components.feature.wall.ReviewPRViewAnimatedVisual.6e3f5223c5", "Explorer") },
{ id: 'search', icon: Search, label: translate("auto.components.feature.wall.ReviewPRViewAnimatedVisual.8e715588e4", "Search") },
{ id: 'source-control', icon: GitBranch, label: translate("auto.components.feature.wall.ReviewPRViewAnimatedVisual.d7f80060ca", "Source Control") },
{ id: 'checks', icon: ListChecks, label: translate("auto.components.feature.wall.ReviewPRViewAnimatedVisual.ab2901bce6", "Checks") }
{
id: 'explorer',
icon: Files,
get label() {
return translate(
'auto.components.feature.wall.ReviewPRViewAnimatedVisual.6e3f5223c5',
'Explorer'
)
}
},
{
id: 'search',
icon: Search,
get label() {
return translate(
'auto.components.feature.wall.ReviewPRViewAnimatedVisual.8e715588e4',
'Search'
)
}
},
{
id: 'source-control',
icon: GitBranch,
get label() {
return translate(
'auto.components.feature.wall.ReviewPRViewAnimatedVisual.d7f80060ca',
'Source Control'
)
}
},
{
id: 'checks',
icon: ListChecks,
get label() {
return translate(
'auto.components.feature.wall.ReviewPRViewAnimatedVisual.ab2901bce6',
'Checks'
)
}
}
]
function SidebarTabs(props: { active: SidebarTabId; interactiveChecks?: boolean }): JSX.Element {
@@ -85,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.
@@ -106,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">
@@ -312,7 +137,12 @@ export function ReviewPRViewAnimatedVisual(props: { reducedMotion: boolean }): J
<div className="ravpr-sidebar is-visible" data-checks-sidebar-peek>
<SidebarTabs active="explorer" interactiveChecks />
<div className="ravpr-explorer">
<div className="ravpr-heading">{translate("auto.components.feature.wall.ReviewPRViewAnimatedVisual.6e3f5223c5", "Explorer")}</div>
<div className="ravpr-heading">
{translate(
'auto.components.feature.wall.ReviewPRViewAnimatedVisual.6e3f5223c5',
'Explorer'
)}
</div>
<div className="ravpr-file-list">
<ExplorerSkeletonRow active width={190} />
<ExplorerSkeletonRow width={158} />
@@ -327,38 +157,89 @@ export function ReviewPRViewAnimatedVisual(props: { reducedMotion: boolean }): J
<div className="ravpr-body">
<div className="ravpr-number-row">
<span className="ravpr-number">#2351</span>
<span className="ravpr-open">{translate("auto.components.feature.wall.ReviewPRViewAnimatedVisual.dfe313e0c9", "OPEN")}</span>
<span className="ravpr-open">
{translate(
'auto.components.feature.wall.ReviewPRViewAnimatedVisual.dfe313e0c9',
'OPEN'
)}
</span>
</div>
<div className="ravpr-title">
{translate(
'auto.components.feature.wall.ReviewPRViewAnimatedVisual.0aab7ab84a',
'Add local diagnostics error tracking'
)}
</div>
<div className="ravpr-title">{translate("auto.components.feature.wall.ReviewPRViewAnimatedVisual.0aab7ab84a", "Add local diagnostics error tracking")}</div>
<button className="ravpr-merge" data-merge-btn type="button">
<GitBranch className="size-3" />
{translate("auto.components.feature.wall.ReviewPRViewAnimatedVisual.2f37142229", "Squash and merge")}<ChevDownIcon />
{translate(
'auto.components.feature.wall.ReviewPRViewAnimatedVisual.2f37142229',
'Squash and merge'
)}
<ChevDownIcon />
</button>
<div className="ravpr-reveal" data-checks-block>
<div className="ravpr-section-row" data-check-summary>
<StatusCell />
<span className="ravpr-label" data-check-summary-label>
{translate("auto.components.feature.wall.ReviewPRViewAnimatedVisual.9a097cae12", "1 pending")}</span>
{translate(
'auto.components.feature.wall.ReviewPRViewAnimatedVisual.9a097cae12',
'1 pending'
)}
</span>
<span className="ravpr-meta" data-check-summary-meta>
{translate("auto.components.feature.wall.ReviewPRViewAnimatedVisual.d340c052fb", "verify")}</span>
{translate(
'auto.components.feature.wall.ReviewPRViewAnimatedVisual.d340c052fb',
'verify'
)}
</span>
</div>
<div className="ravpr-check-list">
<div className="ravpr-check-row" data-check-row="verify">
<StatusCell />
<span>{translate("auto.components.feature.wall.ReviewPRViewAnimatedVisual.d340c052fb", "verify")}</span>
<span>
{translate(
'auto.components.feature.wall.ReviewPRViewAnimatedVisual.d340c052fb',
'verify'
)}
</span>
<span className="ravpr-check-state" data-check-verify-state>
{translate("auto.components.feature.wall.ReviewPRViewAnimatedVisual.8ed213397c", "Running")}</span>
{translate(
'auto.components.feature.wall.ReviewPRViewAnimatedVisual.8ed213397c',
'Running'
)}
</span>
</div>
<div className="ravpr-check-row is-done">
<StatusCell />
<span>{translate("auto.components.feature.wall.ReviewPRViewAnimatedVisual.2ef0b97954", "typecheck")}</span>
<span className="ravpr-check-state">{translate("auto.components.feature.wall.ReviewPRViewAnimatedVisual.ca36f7b27c", "Passed")}</span>
<span>
{translate(
'auto.components.feature.wall.ReviewPRViewAnimatedVisual.2ef0b97954',
'typecheck'
)}
</span>
<span className="ravpr-check-state">
{translate(
'auto.components.feature.wall.ReviewPRViewAnimatedVisual.ca36f7b27c',
'Passed'
)}
</span>
</div>
<div className="ravpr-check-row is-done">
<StatusCell />
<span>{translate("auto.components.feature.wall.ReviewPRViewAnimatedVisual.25f6838e43", "lint")}</span>
<span className="ravpr-check-state">{translate("auto.components.feature.wall.ReviewPRViewAnimatedVisual.ca36f7b27c", "Passed")}</span>
<span>
{translate(
'auto.components.feature.wall.ReviewPRViewAnimatedVisual.25f6838e43',
'lint'
)}
</span>
<span className="ravpr-check-state">
{translate(
'auto.components.feature.wall.ReviewPRViewAnimatedVisual.ca36f7b27c',
'Passed'
)}
</span>
</div>
</div>
</div>
@@ -366,15 +247,43 @@ export function ReviewPRViewAnimatedVisual(props: { reducedMotion: boolean }): J
<div className="ravpr-reveal" data-comments-block>
<div className="ravpr-section-row">
<MessageSquare className="size-3.5" />
<span className="ravpr-label">{translate("auto.components.feature.wall.ReviewPRViewAnimatedVisual.7a8b896e11", "Comments")}</span>
<span className="ravpr-label">
{translate(
'auto.components.feature.wall.ReviewPRViewAnimatedVisual.7a8b896e11',
'Comments'
)}
</span>
<span className="ravpr-meta">
<span data-comments-count>0</span> {translate("auto.components.feature.wall.ReviewPRViewAnimatedVisual.fb1a856b6d", "open")}</span>
<span data-comments-count>0</span>{' '}
{translate(
'auto.components.feature.wall.ReviewPRViewAnimatedVisual.fb1a856b6d',
'open'
)}
</span>
</div>
<div className="ravpr-comment-list">
<CommentCard index={0} path="src/main/diagnostics.ts">
{translate("auto.components.feature.wall.ReviewPRViewAnimatedVisual.71828fba75", "Can we include the failing command in the diagnostic payload?")}</CommentCard>
{translate(
'auto.components.feature.wall.ReviewPRViewAnimatedVisual.71828fba75',
'Can we include the failing command in the diagnostic payload?'
)}
</CommentCard>
<CommentCard index={1} path="tests/diagnostics.test.ts">
{translate("auto.components.feature.wall.ReviewPRViewAnimatedVisual.6f4c2d7cb7", "Add a coverage case for")}<code>{translate("auto.components.feature.wall.ReviewPRViewAnimatedVisual.c2062da7ec", "stderr")}</code> {translate("auto.components.feature.wall.ReviewPRViewAnimatedVisual.7c2808ecff", "truncation before merge.")}</CommentCard>
{translate(
'auto.components.feature.wall.ReviewPRViewAnimatedVisual.6f4c2d7cb7',
'Add a coverage case for'
)}
<code>
{translate(
'auto.components.feature.wall.ReviewPRViewAnimatedVisual.c2062da7ec',
'stderr'
)}
</code>{' '}
{translate(
'auto.components.feature.wall.ReviewPRViewAnimatedVisual.7c2808ecff',
'truncation before merge.'
)}
</CommentCard>
</div>
</div>
</div>
@@ -15,10 +15,12 @@ type Issue = {
const ISSUES: readonly Issue[] = [
{
number: 1842,
title: translate(
'auto.components.feature.wall.TasksAnimatedVisual.b13375617e',
'Worktree picker truncates names'
)
get title() {
return translate(
'auto.components.feature.wall.TasksAnimatedVisual.b13375617e',
'Worktree picker truncates names'
)
}
}
]
@@ -0,0 +1,109 @@
import { translate } from '@/i18n/i18n'
type FrameId = 1 | 2 | 3 | 4
export type FeatureTourPreviewFrameCopy = {
id: FrameId
title: string
caption: string
}
export const FEATURE_TOUR_PREVIEW_COPY: readonly FeatureTourPreviewFrameCopy[] = [
{
id: 1,
get title() {
return translate(
'auto.components.feature.wall.FeatureTourPreview.56a0271428',
'Isolated workspaces'
)
},
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,
get title() {
return translate(
'auto.components.feature.wall.FeatureTourPreview.e44269e97d',
'Agent orchestration'
)
},
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,
get title() {
return translate(
'auto.components.feature.wall.FeatureTourPreview.ef737dcee1',
'GitHub & Linear tasks'
)
},
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,
get title() {
return translate(
'auto.components.feature.wall.FeatureTourPreview.1aa8a9a24a',
'Splittable terminal'
)
},
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.'
)
}
}
]
export type FeatureTourOrchestrationChildAgent = 'claude' | 'codex' | 'opencode-go'
export const FEATURE_TOUR_ORCHESTRATION_CHILDREN: readonly {
key: 'top' | 'mid' | 'bot'
position: string
label: string
agent: FeatureTourOrchestrationChildAgent
}[] = [
// Why: card vertical centers anchor to 18% / 50% / 82% — the same Y
// endpoints the dashed SVG paths terminate at — so the connectors land on
// each card's center regardless of card height.
{
key: 'top',
position: 'top-[18%] -translate-y-1/2',
get label() {
return translate('auto.components.feature.wall.FeatureTourPreview.b1f17bcc74', 'PR 1/3')
},
agent: 'claude'
},
{
key: 'mid',
position: 'top-1/2 -translate-y-1/2',
get label() {
return translate('auto.components.feature.wall.FeatureTourPreview.cfdfd4d6b4', 'PR 2/3')
},
agent: 'codex'
},
{
key: 'bot',
position: 'top-[82%] -translate-y-1/2',
get label() {
return translate('auto.components.feature.wall.FeatureTourPreview.ec4a73f5e6', 'PR 3/3')
},
agent: 'opencode-go'
}
]
@@ -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])
}
@@ -20,27 +20,30 @@ type BucketMeta = {
const BUCKETS: BucketMeta[] = [
{
key: 'core',
label: translate('auto.components.github.github.rate.limit.display.bb227706a6', 'REST'),
description: translate(
'auto.components.github.github.rate.limit.display.c392c749a6',
'REST API'
)
get label() {
return translate('auto.components.github.github.rate.limit.display.bb227706a6', 'REST')
},
get description() {
return translate('auto.components.github.github.rate.limit.display.c392c749a6', 'REST API')
}
},
{
key: 'search',
label: translate('auto.components.github.github.rate.limit.display.c377a4f06a', 'Search'),
description: translate(
'auto.components.github.github.rate.limit.display.1f2f28a4de',
'Search API'
)
get label() {
return translate('auto.components.github.github.rate.limit.display.c377a4f06a', 'Search')
},
get description() {
return translate('auto.components.github.github.rate.limit.display.1f2f28a4de', 'Search API')
}
},
{
key: 'graphql',
label: translate('auto.components.github.github.rate.limit.display.1daf0f22a9', 'GraphQL'),
description: translate(
'auto.components.github.github.rate.limit.display.01f7323e58',
'GraphQL API'
)
get label() {
return translate('auto.components.github.github.rate.limit.display.1daf0f22a9', 'GraphQL')
},
get description() {
return translate('auto.components.github.github.rate.limit.display.01f7323e58', 'GraphQL API')
}
}
]
@@ -6,18 +6,22 @@ export const PLATFORM_COPY: Record<
{ description: string; ctaLabel: string; url: string }
> = {
ios: {
description: translate(
'auto.components.mobile.mobile.platform.copy.432db52b73',
'Scan with your iPhone camera to open the App Store.'
),
get description() {
return translate(
'auto.components.mobile.mobile.platform.copy.432db52b73',
'Scan with your iPhone camera to open the App Store.'
)
},
ctaLabel: 'Open App Store',
url: 'https://apps.apple.com/app/orca-ide/id6766130217'
},
android: {
description: translate(
'auto.components.mobile.mobile.platform.copy.2a532d6fd7',
'Scan with your Android camera to download the latest APK from GitHub Releases.'
),
get description() {
return translate(
'auto.components.mobile.mobile.platform.copy.2a532d6fd7',
'Scan with your Android camera to download the latest APK from GitHub Releases.'
)
},
ctaLabel: 'Download APK',
url: 'https://github.com/stablyai/orca/releases/download/mobile-v0.0.12/app-release.apk'
}
@@ -11,13 +11,11 @@ import {
GitBranchPlus,
GitMerge,
GitPullRequest,
Github,
Gitlab,
LoaderCircle,
Search,
Sparkles,
X
} from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { useShallow } from 'zustand/react/shallow'
import { Command, CommandGroup, CommandItem, CommandList } from '@/components/ui/command'
import { Button } from '@/components/ui/button'
@@ -63,29 +61,11 @@ import type {
} from '../../../../shared/types'
import { resolveSmartWorkspaceCommandValue } from './smart-workspace-command-value'
import { translate } from '@/i18n/i18n'
// Why: GitLab MR list filter — Open / Merged / Closed / All — replaces
// GitHub's search-DSL on the GitLab tab per the agreed scope.
type MrStateFilter = 'opened' | 'merged' | 'closed' | 'all'
const MR_STATE_FILTERS: { id: MrStateFilter; label: string }[] = [
{
id: 'opened',
label: translate('auto.components.new.workspace.SmartWorkspaceNameField.622864b52a', 'Open')
},
{
id: 'merged',
label: translate('auto.components.new.workspace.SmartWorkspaceNameField.2319d87718', 'Merged')
},
{
id: 'closed',
label: translate('auto.components.new.workspace.SmartWorkspaceNameField.6fad211c66', 'Closed')
},
{
id: 'all',
label: translate('auto.components.new.workspace.SmartWorkspaceNameField.26824f60dd', 'All')
}
]
import {
getMrStateFilters,
getSmartWorkspaceNameModes,
type MrStateFilter
} from './smart-workspace-localized-options'
type RepoOption = ReturnType<typeof useAppStore.getState>['repos'][number]
@@ -120,47 +100,6 @@ export type SmartWorkspaceNameSelection = {
const SEARCH_DEBOUNCE_MS = 200
const RESULT_LIMIT = 12
const MODES: {
id: SmartNameMode
label: string
Icon: React.ComponentType<{ className?: string }>
}[] = [
{
id: 'smart',
label: translate('auto.components.new.workspace.SmartWorkspaceNameField.b3c60c2b7c', 'Smart'),
Icon: Sparkles
},
{
id: 'github',
label: translate('auto.components.new.workspace.SmartWorkspaceNameField.0a180280bd', 'GitHub'),
Icon: Github
},
{
id: 'linear',
label: translate('auto.components.new.workspace.SmartWorkspaceNameField.7a47af0565', 'Linear'),
Icon: ({ className }: { className?: string }) => (
<svg viewBox="0 0 24 24" aria-hidden className={className} fill="currentColor">
<path d="M2.886 4.18A11.982 11.982 0 0 1 11.99 0C18.624 0 24 5.376 24 12.009c0 3.64-1.62 6.903-4.18 9.105L2.887 4.18ZM1.817 5.626l16.556 16.556c-.524.33-1.075.62-1.65.866L.951 7.277c.247-.575.537-1.126.866-1.65ZM.322 9.163l14.515 14.515c-.71.172-1.443.282-2.195.322L0 11.358a12 12 0 0 1 .322-2.195Zm-.17 4.862 9.823 9.824a12.02 12.02 0 0 1-9.824-9.824Z" />
</svg>
)
},
{
id: 'gitlab',
label: translate('auto.components.new.workspace.SmartWorkspaceNameField.2cfc6be192', 'GitLab'),
Icon: Gitlab
},
{
id: 'branches',
label: translate('auto.components.new.workspace.SmartWorkspaceNameField.2e4c7c95fe', 'Branch'),
Icon: GitBranch
},
{
id: 'text',
label: translate('auto.components.new.workspace.SmartWorkspaceNameField.6f07a18604', 'Name'),
Icon: CaseSensitive
}
]
type RowEntry = SmartWorkspaceSourceRow
export default function SmartWorkspaceNameField({
@@ -182,6 +121,9 @@ export default function SmartWorkspaceNameField({
textOnly = false,
branchesEnabled = true
}: SmartWorkspaceNameFieldProps): React.JSX.Element {
// Why: tab/filter labels use the lightweight translate() helper; subscribing
// here makes them refresh even when language changes don't remount the field.
useTranslation()
const {
addRepo,
checkLinearConnection,
@@ -257,25 +199,22 @@ export default function SmartWorkspaceNameField({
)
const gitlabAvailable = availableTaskProviders.includes('gitlab')
const linearAvailable = availableTaskProviders.includes('linear')
const availableModes = useMemo(
() =>
MODES.filter((item) => {
if (textOnly) {
return item.id === 'text'
}
if (item.id === 'gitlab') {
return gitlabAvailable
}
if (item.id === 'linear') {
return linearAvailable
}
if (item.id === 'branches') {
return branchesEnabled
}
return true
}),
[branchesEnabled, gitlabAvailable, linearAvailable, textOnly]
)
const availableModes = getSmartWorkspaceNameModes().filter((item) => {
if (textOnly) {
return item.id === 'text'
}
if (item.id === 'gitlab') {
return gitlabAvailable
}
if (item.id === 'linear') {
return linearAvailable
}
if (item.id === 'branches') {
return branchesEnabled
}
return true
})
const mrStateFilters = getMrStateFilters()
const selectedSourceFocusKey = selectedSource
? `${selectedSource.kind}:${selectedSource.label}:${selectedSource.url ?? ''}`
@@ -1181,7 +1120,7 @@ export default function SmartWorkspaceNameField({
className="flex shrink-0 items-center gap-1 border-b border-border/40 px-2 py-1.5"
onMouseDown={(e) => e.preventDefault()}
>
{MR_STATE_FILTERS.map(({ id, label }) => (
{mrStateFilters.map(({ id, label }) => (
<Button
key={id}
type="button"
@@ -0,0 +1,61 @@
import { beforeEach, describe, expect, it } from 'vitest'
import { i18n } from '@/i18n/i18n'
import { getMrStateFilters, getSmartWorkspaceNameModes } from './smart-workspace-localized-options'
describe('smart-workspace-localized-options', () => {
beforeEach(async () => {
await i18n.changeLanguage('en')
})
it('refreshes create-workspace source tabs when the UI language changes', async () => {
expect(getSmartWorkspaceNameModes().map((mode) => mode.label)).toEqual([
'Smart',
'GitHub',
'Linear',
'GitLab',
'Branch',
'Name'
])
await i18n.changeLanguage('zh')
expect(getSmartWorkspaceNameModes().map((mode) => mode.label)).toEqual([
'聪明的',
'GitHub',
'Linear',
'GitLab',
'分支',
'姓名'
])
await i18n.changeLanguage('en')
expect(getSmartWorkspaceNameModes().map((mode) => mode.label)).toEqual([
'Smart',
'GitHub',
'Linear',
'GitLab',
'Branch',
'Name'
])
})
it('refreshes GitLab state filters when the UI language changes', async () => {
expect(getMrStateFilters().map((filter) => filter.label)).toEqual([
'Open',
'Merged',
'Closed',
'All'
])
await i18n.changeLanguage('zh')
expect(getMrStateFilters().map((filter) => filter.label)).toEqual([
'进行中',
'合并',
'已关闭',
'全部'
])
})
})
@@ -0,0 +1,89 @@
import type React from 'react'
import { CaseSensitive, GitBranch, Github, Gitlab, Sparkles } from 'lucide-react'
import { translate } from '@/i18n/i18n'
import type { SmartNameMode } from './smart-workspace-source-results'
export type MrStateFilter = 'opened' | 'merged' | 'closed' | 'all'
export type SmartWorkspaceNameModeOption = {
id: SmartNameMode
label: string
Icon: React.ComponentType<{ className?: string }>
}
function LinearModeIcon({ className }: { className?: string }): React.JSX.Element {
return (
<svg viewBox="0 0 24 24" aria-hidden className={className} fill="currentColor">
<path d="M2.886 4.18A11.982 11.982 0 0 1 11.99 0C18.624 0 24 5.376 24 12.009c0 3.64-1.62 6.903-4.18 9.105L2.887 4.18ZM1.817 5.626l16.556 16.556c-.524.33-1.075.62-1.65.866L.951 7.277c.247-.575.537-1.126.866-1.65ZM.322 9.163l14.515 14.515c-.71.172-1.443.282-2.195.322L0 11.358a12 12 0 0 1 .322-2.195Zm-.17 4.862 9.823 9.824a12.02 12.02 0 0 1-9.824-9.824Z" />
</svg>
)
}
export function getMrStateFilters(): { id: MrStateFilter; label: string }[] {
return [
{
id: 'opened',
label: translate('auto.components.new.workspace.SmartWorkspaceNameField.622864b52a', 'Open')
},
{
id: 'merged',
label: translate('auto.components.new.workspace.SmartWorkspaceNameField.2319d87718', 'Merged')
},
{
id: 'closed',
label: translate('auto.components.new.workspace.SmartWorkspaceNameField.6fad211c66', 'Closed')
},
{
id: 'all',
label: translate('auto.components.new.workspace.SmartWorkspaceNameField.26824f60dd', 'All')
}
]
}
export function getSmartWorkspaceNameModes(): SmartWorkspaceNameModeOption[] {
return [
{
id: 'smart',
label: translate('auto.components.new.workspace.SmartWorkspaceNameField.b3c60c2b7c', 'Smart'),
Icon: Sparkles
},
{
id: 'github',
label: translate(
'auto.components.new.workspace.SmartWorkspaceNameField.0a180280bd',
'GitHub'
),
Icon: Github
},
{
id: 'linear',
label: translate(
'auto.components.new.workspace.SmartWorkspaceNameField.7a47af0565',
'Linear'
),
Icon: LinearModeIcon
},
{
id: 'gitlab',
label: translate(
'auto.components.new.workspace.SmartWorkspaceNameField.2cfc6be192',
'GitLab'
),
Icon: Gitlab
},
{
id: 'branches',
label: translate(
'auto.components.new.workspace.SmartWorkspaceNameField.2e4c7c95fe',
'Branch'
),
Icon: GitBranch
},
{
id: 'text',
label: translate('auto.components.new.workspace.SmartWorkspaceNameField.6f07a18604', 'Name'),
Icon: CaseSensitive
}
]
}
@@ -23,37 +23,52 @@ type FeatureSetupRow = {
const FEATURE_SETUP_ROWS: readonly FeatureSetupRow[] = [
{
id: 'browserUse',
title: translate(
'auto.components.onboarding.FeatureSetupChecklist.ea85d9e628',
'Agent Browser Use'
),
description: translate(
'auto.components.onboarding.FeatureSetupChecklist.01426f3a23',
'Agents can navigate sites, inspect pages, and work through browser tasks.'
),
get title() {
return translate(
'auto.components.onboarding.FeatureSetupChecklist.ea85d9e628',
'Agent Browser Use'
)
},
get description() {
return translate(
'auto.components.onboarding.FeatureSetupChecklist.01426f3a23',
'Agents can navigate sites, inspect pages, and work through browser tasks.'
)
},
setupSummary: 'Enables browser use, prepares orca-cli, and leaves cookies for Settings.',
icon: <Globe2 className="size-4" />
},
{
id: 'computerUse',
title: translate('auto.components.onboarding.FeatureSetupChecklist.1ecfb490ac', 'Computer Use'),
description: translate(
'auto.components.onboarding.FeatureSetupChecklist.c5292c409d',
'Agents can inspect app windows and operate local apps when you ask.'
),
get title() {
return translate(
'auto.components.onboarding.FeatureSetupChecklist.1ecfb490ac',
'Computer Use'
)
},
get description() {
return translate(
'auto.components.onboarding.FeatureSetupChecklist.c5292c409d',
'Agents can inspect app windows and operate local apps when you ask.'
)
},
setupSummary: 'Registers the Orca CLI, opens permissions, and prepares the skill.',
icon: <MonitorCog className="size-4" />
},
{
id: 'orchestration',
title: translate(
'auto.components.onboarding.FeatureSetupChecklist.399cf885c0',
'Agent Orchestration'
),
description: translate(
'auto.components.onboarding.FeatureSetupChecklist.77f74946f5',
'Agents can message each other, take tasks, and coordinate handoffs.'
),
get title() {
return translate(
'auto.components.onboarding.FeatureSetupChecklist.399cf885c0',
'Agent Orchestration'
)
},
get description() {
return translate(
'auto.components.onboarding.FeatureSetupChecklist.77f74946f5',
'Agents can message each other, take tasks, and coordinate handoffs.'
)
},
setupSummary: 'Registers the Orca CLI, enables orchestration, and prepares the skill.',
icon: <Workflow className="size-4" />
}
@@ -17,41 +17,60 @@ import { translate } from '@/i18n/i18n'
const stepCopy = {
agent: {
title: translate(
'auto.components.onboarding.OnboardingFlow.198b148b3c',
'Pick your default agent'
),
subtitle: translate(
'auto.components.onboarding.OnboardingFlow.322fc50a18',
"Orca works with every CLI agent. Choose the one you'll reach for most. Switch any time."
)
get title() {
return translate(
'auto.components.onboarding.OnboardingFlow.198b148b3c',
'Pick your default agent'
)
},
get subtitle() {
return translate(
'auto.components.onboarding.OnboardingFlow.322fc50a18',
"Orca works with every CLI agent. Choose the one you'll reach for most. Switch any time."
)
}
},
theme: {
title: translate(
'auto.components.onboarding.OnboardingFlow.f396db9f20',
'Make it feel like home'
),
subtitle: translate(
'auto.components.onboarding.OnboardingFlow.04ae28d8ca',
'Pick the look you want to stare at for hours.'
)
get title() {
return translate(
'auto.components.onboarding.OnboardingFlow.f396db9f20',
'Make it feel like home'
)
},
get subtitle() {
return translate(
'auto.components.onboarding.OnboardingFlow.04ae28d8ca',
'Pick the look you want to stare at for hours.'
)
}
},
notifications: {
title: translate(
'auto.components.onboarding.OnboardingFlow.b054332836',
'Set up notifications'
),
subtitle: translate(
'auto.components.onboarding.OnboardingFlow.ff92d15436',
'Orca will notify you know when agents are done or need help.'
)
get title() {
return translate(
'auto.components.onboarding.OnboardingFlow.b054332836',
'Set up notifications'
)
},
get subtitle() {
return translate(
'auto.components.onboarding.OnboardingFlow.ff92d15436',
'Orca will notify you when agents are done or need help.'
)
}
},
integrations: {
title: translate('auto.components.onboarding.OnboardingFlow.ae3b00ca82', 'Set up GitHub tasks'),
subtitle: translate(
'auto.components.onboarding.OnboardingFlow.97c42cda00',
'Install the GitHub CLI to:'
)
get title() {
return translate(
'auto.components.onboarding.OnboardingFlow.ae3b00ca82',
'Set up GitHub tasks'
)
},
get subtitle() {
return translate(
'auto.components.onboarding.OnboardingFlow.97c42cda00',
'Install the GitHub CLI to:'
)
}
}
} as const
@@ -10,14 +10,18 @@ import {
import { translate } from '@/i18n/i18n'
export const ONBOARDING_SKIP_CONFIRMATION_COPY = {
title: translate(
'auto.components.onboarding.OnboardingSkipConfirmationDialog.e4726b2d50',
'Skip onboarding?'
),
description: translate(
'auto.components.onboarding.OnboardingSkipConfirmationDialog.9f47f345a4',
"It won't take long!"
),
get title() {
return translate(
'auto.components.onboarding.OnboardingSkipConfirmationDialog.e4726b2d50',
'Skip onboarding?'
)
},
get description() {
return translate(
'auto.components.onboarding.OnboardingSkipConfirmationDialog.9f47f345a4',
"It won't take long!"
)
},
skipLabel: 'Skip',
keepGoingLabel: 'No, keep going'
} as const
@@ -21,17 +21,23 @@ export type BundledPet = {
export const BUNDLED_PETS: readonly BundledPet[] = [
{
id: DEFAULT_PET_ID,
label: translate('auto.components.pet.pet.models.2528586aa7', 'Claudino'),
get label() {
return translate('auto.components.pet.pet.models.2528586aa7', 'Claudino')
},
url: claudeUrl
},
{
id: OPENCODE_PET_ID,
label: translate('auto.components.pet.pet.models.a84d5677ff', 'OpenCode'),
get label() {
return translate('auto.components.pet.pet.models.a84d5677ff', 'OpenCode')
},
url: opencodeUrl
},
{
id: GREMLIN_PET_ID,
label: translate('auto.components.pet.pet.models.7433516faf', 'Gremlin'),
get label() {
return translate('auto.components.pet.pet.models.7433516faf', 'Gremlin')
},
url: gremlinUrl
}
] as const
@@ -42,20 +42,28 @@ type PermissionDefinition = {
const PERMISSIONS: PermissionDefinition[] = [
{
id: 'accessibility',
label: translate('auto.components.settings.ComputerUsePane.6b5a2cd3a5', 'Accessibility'),
description: translate(
'auto.components.settings.ComputerUsePane.4d03dec2d0',
'Read app interface trees and perform requested actions.'
),
get label() {
return translate('auto.components.settings.ComputerUsePane.6b5a2cd3a5', 'Accessibility')
},
get description() {
return translate(
'auto.components.settings.ComputerUsePane.4d03dec2d0',
'Read app interface trees and perform requested actions.'
)
},
icon: <Accessibility className="size-4" />
},
{
id: 'screenshots',
label: translate('auto.components.settings.ComputerUsePane.07bbe4c4cb', 'Screenshots'),
description: translate(
'auto.components.settings.ComputerUsePane.0c9a33f468',
'Capture app windows so agents can inspect visual state.'
),
get label() {
return translate('auto.components.settings.ComputerUsePane.07bbe4c4cb', 'Screenshots')
},
get description() {
return translate(
'auto.components.settings.ComputerUsePane.0c9a33f468',
'Capture app windows so agents can inspect visual state.'
)
},
icon: <Camera className="size-4" />
}
]
@@ -34,103 +34,142 @@ type PermissionDefinition = {
const PERMISSIONS: PermissionDefinition[] = [
{
id: 'microphone',
label: translate('auto.components.settings.DeveloperPermissionsPane.16381e040a', 'Microphone'),
description: translate(
'auto.components.settings.DeveloperPermissionsPane.cc8151d9fa',
'Voice input, transcription, audio recording, sox, ffmpeg, and Whisper CLIs.'
),
get label() {
return translate('auto.components.settings.DeveloperPermissionsPane.16381e040a', 'Microphone')
},
get description() {
return translate(
'auto.components.settings.DeveloperPermissionsPane.cc8151d9fa',
'Voice input, transcription, audio recording, sox, ffmpeg, and Whisper CLIs.'
)
},
actionLabel: 'Request',
icon: <Mic className="size-4" />
},
{
id: 'camera',
label: translate('auto.components.settings.DeveloperPermissionsPane.e5b5f3d6b9', 'Camera'),
description: translate(
'auto.components.settings.DeveloperPermissionsPane.550cfa3750',
'Webcam capture and camera-driven local test apps.'
),
get label() {
return translate('auto.components.settings.DeveloperPermissionsPane.e5b5f3d6b9', 'Camera')
},
get description() {
return translate(
'auto.components.settings.DeveloperPermissionsPane.550cfa3750',
'Webcam capture and camera-driven local test apps.'
)
},
actionLabel: 'Request',
icon: <Camera className="size-4" />
},
{
id: 'screen',
label: translate(
'auto.components.settings.DeveloperPermissionsPane.f24f31a884',
'Screen Recording'
),
description: translate(
'auto.components.settings.DeveloperPermissionsPane.0639db5496',
'Screenshot, visual automation, and UI inspection tools.'
),
get label() {
return translate(
'auto.components.settings.DeveloperPermissionsPane.f24f31a884',
'Screen Recording'
)
},
get description() {
return translate(
'auto.components.settings.DeveloperPermissionsPane.0639db5496',
'Screenshot, visual automation, and UI inspection tools.'
)
},
actionLabel: 'Open Settings',
icon: <MonitorUp className="size-4" />
},
{
id: 'accessibility',
label: translate(
'auto.components.settings.DeveloperPermissionsPane.5b2f22ca2d',
'Accessibility'
),
description: translate(
'auto.components.settings.DeveloperPermissionsPane.9f35980756',
'Keystroke injection, window control, and UI automation tools.'
),
get label() {
return translate(
'auto.components.settings.DeveloperPermissionsPane.5b2f22ca2d',
'Accessibility'
)
},
get description() {
return translate(
'auto.components.settings.DeveloperPermissionsPane.9f35980756',
'Keystroke injection, window control, and UI automation tools.'
)
},
actionLabel: 'Request',
icon: <Accessibility className="size-4" />
},
{
id: 'full-disk-access',
label: translate(
'auto.components.settings.DeveloperPermissionsPane.c566bca278',
'Full Disk Access'
),
description: translate(
'auto.components.settings.DeveloperPermissionsPane.7ca17b62c8',
'Persistent access to protected folders from terminal sessions.'
),
get label() {
return translate(
'auto.components.settings.DeveloperPermissionsPane.c566bca278',
'Full Disk Access'
)
},
get description() {
return translate(
'auto.components.settings.DeveloperPermissionsPane.7ca17b62c8',
'Persistent access to protected folders from terminal sessions.'
)
},
actionLabel: 'Open Settings',
icon: <HardDrive className="size-4" />
},
{
id: 'automation',
label: translate('auto.components.settings.DeveloperPermissionsPane.e119f0d66b', 'Automation'),
description: translate(
'auto.components.settings.DeveloperPermissionsPane.4a73f5217a',
'Apple Events for scripts that control other local apps.'
),
get label() {
return translate('auto.components.settings.DeveloperPermissionsPane.e119f0d66b', 'Automation')
},
get description() {
return translate(
'auto.components.settings.DeveloperPermissionsPane.4a73f5217a',
'Apple Events for scripts that control other local apps.'
)
},
actionLabel: 'Trigger Prompt',
icon: <Workflow className="size-4" />
},
{
id: 'local-network',
label: translate(
'auto.components.settings.DeveloperPermissionsPane.e7bb06007c',
'Local Network'
),
description: translate(
'auto.components.settings.DeveloperPermissionsPane.f903bf20b5',
'Discovery and access for development servers on your network.'
),
get label() {
return translate(
'auto.components.settings.DeveloperPermissionsPane.e7bb06007c',
'Local Network'
)
},
get description() {
return translate(
'auto.components.settings.DeveloperPermissionsPane.f903bf20b5',
'Discovery and access for development servers on your network.'
)
},
actionLabel: 'Trigger Prompt',
icon: <Network className="size-4" />
},
{
id: 'usb',
label: translate('auto.components.settings.DeveloperPermissionsPane.bf51e4a542', 'USB Devices'),
description: translate(
'auto.components.settings.DeveloperPermissionsPane.dfbc12c8c8',
'Hardware debugging and device tools that talk to USB devices.'
),
get label() {
return translate(
'auto.components.settings.DeveloperPermissionsPane.bf51e4a542',
'USB Devices'
)
},
get description() {
return translate(
'auto.components.settings.DeveloperPermissionsPane.dfbc12c8c8',
'Hardware debugging and device tools that talk to USB devices.'
)
},
actionLabel: 'Open Settings',
icon: <Usb className="size-4" />
},
{
id: 'bluetooth',
label: translate('auto.components.settings.DeveloperPermissionsPane.b2210b1b4f', 'Bluetooth'),
description: translate(
'auto.components.settings.DeveloperPermissionsPane.4cfaa7e98a',
'Bluetooth device tools and local hardware experiments.'
),
get label() {
return translate('auto.components.settings.DeveloperPermissionsPane.b2210b1b4f', 'Bluetooth')
},
get description() {
return translate(
'auto.components.settings.DeveloperPermissionsPane.4cfaa7e98a',
'Bluetooth device tools and local hardware experiments.'
)
},
actionLabel: 'Open Settings',
icon: <Bluetooth className="size-4" />
}
@@ -19,31 +19,39 @@ type RepositorySourceControlAiHostedReviewDefaultsProps = {
const HOSTED_REVIEW_DEFAULT_ROWS: { key: HostedReviewDefaultKey; label: string }[] = [
{
key: 'draft',
label: translate(
'auto.components.settings.RepositorySourceControlAiHostedReviewDefaults.981eae7e14',
'Draft by default'
)
get label() {
return translate(
'auto.components.settings.RepositorySourceControlAiHostedReviewDefaults.981eae7e14',
'Draft by default'
)
}
},
{
key: 'useTemplate',
label: translate(
'auto.components.settings.RepositorySourceControlAiHostedReviewDefaults.d32b87e754',
'Use review template when available'
)
get label() {
return translate(
'auto.components.settings.RepositorySourceControlAiHostedReviewDefaults.d32b87e754',
'Use review template when available'
)
}
},
{
key: 'generateDetailsOnOpen',
label: translate(
'auto.components.settings.RepositorySourceControlAiHostedReviewDefaults.14f1eb99d0',
'Generate details when opening Create PR'
)
get label() {
return translate(
'auto.components.settings.RepositorySourceControlAiHostedReviewDefaults.14f1eb99d0',
'Generate details when opening Create PR'
)
}
},
{
key: 'openAfterCreate',
label: translate(
'auto.components.settings.RepositorySourceControlAiHostedReviewDefaults.629ed8a9d3',
'Open hosted review after creation'
)
get label() {
return translate(
'auto.components.settings.RepositorySourceControlAiHostedReviewDefaults.629ed8a9d3',
'Open hosted review after creation'
)
}
}
]
@@ -89,29 +89,51 @@ import { translate } from '@/i18n/i18n'
const SETTINGS_NAV_GROUPS = [
{
id: 'capabilities',
title: translate('auto.components.settings.Settings.23c6874fdf', 'AI Capabilities')
get title() {
return translate('auto.components.settings.Settings.23c6874fdf', 'AI Capabilities')
}
},
{
id: 'setup',
get title() {
return translate('auto.components.settings.Settings.9abb9be3bc', 'Set Up')
}
},
{ id: 'setup', title: translate('auto.components.settings.Settings.9abb9be3bc', 'Set Up') },
{
id: 'workflows',
title: translate('auto.components.settings.Settings.e1578cd4bc', 'Workflows')
get title() {
return translate('auto.components.settings.Settings.e1578cd4bc', 'Workflows')
}
},
{
id: 'interface',
title: translate('auto.components.settings.Settings.8bd117d669', 'Interface')
get title() {
return translate('auto.components.settings.Settings.8bd117d669', 'Interface')
}
},
{
id: 'remote',
title: translate('auto.components.settings.Settings.23931df7e8', 'Remote Access')
get title() {
return translate('auto.components.settings.Settings.23931df7e8', 'Remote Access')
}
},
{
id: 'security',
title: translate('auto.components.settings.Settings.084d8fac5b', 'Privacy & Security')
get title() {
return translate('auto.components.settings.Settings.084d8fac5b', 'Privacy & Security')
}
},
{
id: 'advanced',
get title() {
return translate('auto.components.settings.Settings.1c87f8d024', 'Advanced')
}
},
{ id: 'advanced', title: translate('auto.components.settings.Settings.1c87f8d024', 'Advanced') },
{
id: 'experimental',
title: translate('auto.components.settings.Settings.8b017f2506', 'Experimental')
get title() {
return translate('auto.components.settings.Settings.8b017f2506', 'Experimental')
}
}
] as const
@@ -29,36 +29,51 @@ type SourceControlAiActionRecipeDefaultsProps = {
}
const ACTION_RECIPES_SEARCH_ENTRY = {
title: translate(
'auto.components.settings.SourceControlAiActionRecipeDefaults.a79c567194',
'Action recipes'
),
description: translate(
'auto.components.settings.SourceControlAiActionRecipeDefaults.cf01d41bce',
'Agent, CLI arguments, and command template used by each Source Control AI button.'
),
keywords: [
translate('auto.components.settings.SourceControlAiActionRecipeDefaults.926d58e87f', 'agent'),
translate(
'auto.components.settings.SourceControlAiActionRecipeDefaults.db9bd75d10',
'arguments'
),
translate('auto.components.settings.SourceControlAiActionRecipeDefaults.2576299196', 'args'),
translate('auto.components.settings.SourceControlAiActionRecipeDefaults.673369fe0c', 'cli'),
translate('auto.components.settings.SourceControlAiActionRecipeDefaults.d74fdc776c', 'command'),
translate('auto.components.settings.SourceControlAiActionRecipeDefaults.eb7e8f3b39', 'model'),
translate(
'auto.components.settings.SourceControlAiActionRecipeDefaults.2037c78a6f',
'template'
),
translate('auto.components.settings.SourceControlAiActionRecipeDefaults.cb67b938c5', 'fix'),
translate('auto.components.settings.SourceControlAiActionRecipeDefaults.06a9dab64d', 'checks'),
translate('auto.components.settings.SourceControlAiActionRecipeDefaults.e5b24893ba', 'commit'),
translate(
'auto.components.settings.SourceControlAiActionRecipeDefaults.7ab1437a12',
'pull request'
get title() {
return translate(
'auto.components.settings.SourceControlAiActionRecipeDefaults.a79c567194',
'Action recipes'
)
]
},
get description() {
return translate(
'auto.components.settings.SourceControlAiActionRecipeDefaults.cf01d41bce',
'Agent, CLI arguments, and command template used by each Source Control AI button.'
)
},
get keywords() {
return [
translate('auto.components.settings.SourceControlAiActionRecipeDefaults.926d58e87f', 'agent'),
translate(
'auto.components.settings.SourceControlAiActionRecipeDefaults.db9bd75d10',
'arguments'
),
translate('auto.components.settings.SourceControlAiActionRecipeDefaults.2576299196', 'args'),
translate('auto.components.settings.SourceControlAiActionRecipeDefaults.673369fe0c', 'cli'),
translate(
'auto.components.settings.SourceControlAiActionRecipeDefaults.d74fdc776c',
'command'
),
translate('auto.components.settings.SourceControlAiActionRecipeDefaults.eb7e8f3b39', 'model'),
translate(
'auto.components.settings.SourceControlAiActionRecipeDefaults.2037c78a6f',
'template'
),
translate('auto.components.settings.SourceControlAiActionRecipeDefaults.cb67b938c5', 'fix'),
translate(
'auto.components.settings.SourceControlAiActionRecipeDefaults.06a9dab64d',
'checks'
),
translate(
'auto.components.settings.SourceControlAiActionRecipeDefaults.e5b24893ba',
'commit'
),
translate(
'auto.components.settings.SourceControlAiActionRecipeDefaults.7ab1437a12',
'pull request'
)
]
}
}
export function SourceControlAiActionRecipeDefaults({
@@ -127,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">
@@ -29,7 +29,9 @@ export const STATUS_LABELS: Record<SshConnectionStatus, string> = {
connected: 'Connected',
reconnecting: 'Reconnecting\u2026',
'reconnection-failed': 'Reconnection failed',
error: translate('auto.components.settings.SshTargetCard.18968ede9e', 'Error')
get error() {
return translate('auto.components.settings.SshTargetCard.18968ede9e', 'Error')
}
}
export function statusColor(status: SshConnectionStatus): string {
@@ -26,38 +26,54 @@ const TASK_PROVIDER_OPTIONS: readonly {
}[] = [
{
id: 'github',
label: translate('auto.components.settings.TasksPane.e14063e727', 'GitHub'),
description: translate(
'auto.components.settings.TasksPane.1db47236cd',
'Show GitHub in the Tasks source picker and sidebar shortcuts.'
),
get label() {
return translate('auto.components.settings.TasksPane.e14063e727', 'GitHub')
},
get description() {
return translate(
'auto.components.settings.TasksPane.1db47236cd',
'Show GitHub in the Tasks source picker and sidebar shortcuts.'
)
},
Icon: ({ className }) => <Github className={className} />
},
{
id: 'gitlab',
label: translate('auto.components.settings.TasksPane.7c5d7fdc20', 'GitLab'),
description: translate(
'auto.components.settings.TasksPane.dd67a1b6e1',
'Show GitLab in the Tasks source picker and sidebar shortcuts.'
),
get label() {
return translate('auto.components.settings.TasksPane.7c5d7fdc20', 'GitLab')
},
get description() {
return translate(
'auto.components.settings.TasksPane.dd67a1b6e1',
'Show GitLab in the Tasks source picker and sidebar shortcuts.'
)
},
Icon: ({ className }) => <Gitlab className={className} />
},
{
id: 'linear',
label: translate('auto.components.settings.TasksPane.09ae2d7c51', 'Linear'),
description: translate(
'auto.components.settings.TasksPane.e4170c9615',
'Show Linear in the Tasks source picker and sidebar shortcuts.'
),
get label() {
return translate('auto.components.settings.TasksPane.09ae2d7c51', 'Linear')
},
get description() {
return translate(
'auto.components.settings.TasksPane.e4170c9615',
'Show Linear in the Tasks source picker and sidebar shortcuts.'
)
},
Icon: ({ className }) => <LinearIcon className={className} />
},
{
id: 'jira',
label: translate('auto.components.settings.TasksPane.6b23a34f6d', 'Jira'),
description: translate(
'auto.components.settings.TasksPane.8e1305fcc6',
'Show Jira in the Tasks source picker and sidebar shortcuts.'
),
get label() {
return translate('auto.components.settings.TasksPane.6b23a34f6d', 'Jira')
},
get description() {
return translate(
'auto.components.settings.TasksPane.8e1305fcc6',
'Show Jira in the Tasks source picker and sidebar shortcuts.'
)
},
Icon: ({ className }) => <JiraIcon className={className} />
}
]
@@ -19,57 +19,339 @@ const COLOR_OVERRIDE_GROUPS: {
keys: { key: keyof TerminalColorOverrides; label: string; description: string }[]
}[] = [
{
label: translate("auto.components.settings.TerminalWindowSection.cf37ff69f6", "Base"),
get label() {
return translate('auto.components.settings.TerminalWindowSection.cf37ff69f6', 'Base')
},
keys: [
{ key: 'foreground', label: translate("auto.components.settings.TerminalWindowSection.79f6bfb76e", "Foreground"), description: translate("auto.components.settings.TerminalWindowSection.026a0b8013", "Main text color") },
{ key: 'background', label: translate("auto.components.settings.TerminalWindowSection.cc1b2ffeb2", "Background"), description: translate("auto.components.settings.TerminalWindowSection.da64e8f4c1", "Terminal background color") },
{ key: 'cursor', label: translate("auto.components.settings.TerminalWindowSection.c9e1fdf42f", "Cursor"), description: translate("auto.components.settings.TerminalWindowSection.cd0700762b", "Cursor color") },
{
key: 'foreground',
get label() {
return translate(
'auto.components.settings.TerminalWindowSection.79f6bfb76e',
'Foreground'
)
},
get description() {
return translate(
'auto.components.settings.TerminalWindowSection.026a0b8013',
'Main text color'
)
}
},
{
key: 'background',
get label() {
return translate(
'auto.components.settings.TerminalWindowSection.cc1b2ffeb2',
'Background'
)
},
get description() {
return translate(
'auto.components.settings.TerminalWindowSection.da64e8f4c1',
'Terminal background color'
)
}
},
{
key: 'cursor',
get label() {
return translate('auto.components.settings.TerminalWindowSection.c9e1fdf42f', 'Cursor')
},
get description() {
return translate(
'auto.components.settings.TerminalWindowSection.cd0700762b',
'Cursor color'
)
}
},
{
key: 'cursorAccent',
label: translate("auto.components.settings.TerminalWindowSection.a2d9f095a7", "Cursor Text"),
description: translate("auto.components.settings.TerminalWindowSection.7f4063076c", "Color of text under the cursor (block cursor)")
get label() {
return translate(
'auto.components.settings.TerminalWindowSection.a2d9f095a7',
'Cursor Text'
)
},
get description() {
return translate(
'auto.components.settings.TerminalWindowSection.7f4063076c',
'Color of text under the cursor (block cursor)'
)
}
},
{
key: 'selectionBackground',
label: translate("auto.components.settings.TerminalWindowSection.40c3cfd30a", "Selection Background"),
description: translate("auto.components.settings.TerminalWindowSection.74d8555f85", "Background color of selected text")
get label() {
return translate(
'auto.components.settings.TerminalWindowSection.40c3cfd30a',
'Selection Background'
)
},
get description() {
return translate(
'auto.components.settings.TerminalWindowSection.74d8555f85',
'Background color of selected text'
)
}
},
{
key: 'selectionForeground',
label: translate("auto.components.settings.TerminalWindowSection.8b450b5305", "Selection Foreground"),
description: translate("auto.components.settings.TerminalWindowSection.b2c0857c49", "Text color of selected text")
get label() {
return translate(
'auto.components.settings.TerminalWindowSection.8b450b5305',
'Selection Foreground'
)
},
get description() {
return translate(
'auto.components.settings.TerminalWindowSection.b2c0857c49',
'Text color of selected text'
)
}
},
{
key: 'bold',
label: translate("auto.components.settings.TerminalWindowSection.862e463f7f", "Bold Text"),
description: translate("auto.components.settings.TerminalWindowSection.fb8c6f1967", "Color for bold text. Falls back to the normal color if not set.")
get label() {
return translate('auto.components.settings.TerminalWindowSection.862e463f7f', 'Bold Text')
},
get description() {
return translate(
'auto.components.settings.TerminalWindowSection.fb8c6f1967',
'Color for bold text. Falls back to the normal color if not set.'
)
}
}
]
},
{
label: translate("auto.components.settings.TerminalWindowSection.68e9f07de0", "ANSI Normal"),
get label() {
return translate('auto.components.settings.TerminalWindowSection.68e9f07de0', 'ANSI Normal')
},
keys: [
{ key: 'black', label: translate("auto.components.settings.TerminalWindowSection.adfdee23cb", "Black"), description: translate("auto.components.settings.TerminalWindowSection.cf4437a2f7", "ANSI black color") },
{ key: 'red', label: translate("auto.components.settings.TerminalWindowSection.3a78f30b50", "Red"), description: translate("auto.components.settings.TerminalWindowSection.b41270f5ca", "ANSI red color") },
{ key: 'green', label: translate("auto.components.settings.TerminalWindowSection.8f2092b315", "Green"), description: translate("auto.components.settings.TerminalWindowSection.8a673d4206", "ANSI green color") },
{ key: 'yellow', label: translate("auto.components.settings.TerminalWindowSection.bb516de873", "Yellow"), description: translate("auto.components.settings.TerminalWindowSection.09c1c6b096", "ANSI yellow color") },
{ key: 'blue', label: translate("auto.components.settings.TerminalWindowSection.292a4c7316", "Blue"), description: translate("auto.components.settings.TerminalWindowSection.9635a71c51", "ANSI blue color") },
{ key: 'magenta', label: translate("auto.components.settings.TerminalWindowSection.d5e92fcd94", "Magenta"), description: translate("auto.components.settings.TerminalWindowSection.1705318506", "ANSI magenta color") },
{ key: 'cyan', label: translate("auto.components.settings.TerminalWindowSection.fb8bb4eb1f", "Cyan"), description: translate("auto.components.settings.TerminalWindowSection.bd4c759327", "ANSI cyan color") },
{ key: 'white', label: translate("auto.components.settings.TerminalWindowSection.0cb4459fb8", "White"), description: translate("auto.components.settings.TerminalWindowSection.28846b1ca6", "ANSI white color") }
{
key: 'black',
get label() {
return translate('auto.components.settings.TerminalWindowSection.adfdee23cb', 'Black')
},
get description() {
return translate(
'auto.components.settings.TerminalWindowSection.cf4437a2f7',
'ANSI black color'
)
}
},
{
key: 'red',
get label() {
return translate('auto.components.settings.TerminalWindowSection.3a78f30b50', 'Red')
},
get description() {
return translate(
'auto.components.settings.TerminalWindowSection.b41270f5ca',
'ANSI red color'
)
}
},
{
key: 'green',
get label() {
return translate('auto.components.settings.TerminalWindowSection.8f2092b315', 'Green')
},
get description() {
return translate(
'auto.components.settings.TerminalWindowSection.8a673d4206',
'ANSI green color'
)
}
},
{
key: 'yellow',
get label() {
return translate('auto.components.settings.TerminalWindowSection.bb516de873', 'Yellow')
},
get description() {
return translate(
'auto.components.settings.TerminalWindowSection.09c1c6b096',
'ANSI yellow color'
)
}
},
{
key: 'blue',
get label() {
return translate('auto.components.settings.TerminalWindowSection.292a4c7316', 'Blue')
},
get description() {
return translate(
'auto.components.settings.TerminalWindowSection.9635a71c51',
'ANSI blue color'
)
}
},
{
key: 'magenta',
get label() {
return translate('auto.components.settings.TerminalWindowSection.d5e92fcd94', 'Magenta')
},
get description() {
return translate(
'auto.components.settings.TerminalWindowSection.1705318506',
'ANSI magenta color'
)
}
},
{
key: 'cyan',
get label() {
return translate('auto.components.settings.TerminalWindowSection.fb8bb4eb1f', 'Cyan')
},
get description() {
return translate(
'auto.components.settings.TerminalWindowSection.bd4c759327',
'ANSI cyan color'
)
}
},
{
key: 'white',
get label() {
return translate('auto.components.settings.TerminalWindowSection.0cb4459fb8', 'White')
},
get description() {
return translate(
'auto.components.settings.TerminalWindowSection.28846b1ca6',
'ANSI white color'
)
}
}
]
},
{
label: translate("auto.components.settings.TerminalWindowSection.1be593d3e8", "ANSI Bright"),
get label() {
return translate('auto.components.settings.TerminalWindowSection.1be593d3e8', 'ANSI Bright')
},
keys: [
{ key: 'brightBlack', label: translate("auto.components.settings.TerminalWindowSection.260d69ce9a", "Bright Black"), description: translate("auto.components.settings.TerminalWindowSection.f30c492769", "ANSI bright black color") },
{ key: 'brightRed', label: translate("auto.components.settings.TerminalWindowSection.32b1b6acd7", "Bright Red"), description: translate("auto.components.settings.TerminalWindowSection.667de68863", "ANSI bright red color") },
{ key: 'brightGreen', label: translate("auto.components.settings.TerminalWindowSection.7dafd57730", "Bright Green"), description: translate("auto.components.settings.TerminalWindowSection.0ffb02f921", "ANSI bright green color") },
{ key: 'brightYellow', label: translate("auto.components.settings.TerminalWindowSection.936a326be3", "Bright Yellow"), description: translate("auto.components.settings.TerminalWindowSection.e2ef5f4ab7", "ANSI bright yellow color") },
{ key: 'brightBlue', label: translate("auto.components.settings.TerminalWindowSection.66820332fa", "Bright Blue"), description: translate("auto.components.settings.TerminalWindowSection.bef6c0f6bf", "ANSI bright blue color") },
{ key: 'brightMagenta', label: translate("auto.components.settings.TerminalWindowSection.e56e7d6ea0", "Bright Magenta"), description: translate("auto.components.settings.TerminalWindowSection.fe4d89ef85", "ANSI bright magenta color") },
{ key: 'brightCyan', label: translate("auto.components.settings.TerminalWindowSection.f94adc4113", "Bright Cyan"), description: translate("auto.components.settings.TerminalWindowSection.1601140f03", "ANSI bright cyan color") },
{ key: 'brightWhite', label: translate("auto.components.settings.TerminalWindowSection.16948119cb", "Bright White"), description: translate("auto.components.settings.TerminalWindowSection.42e01a6055", "ANSI bright white color") }
{
key: 'brightBlack',
get label() {
return translate(
'auto.components.settings.TerminalWindowSection.260d69ce9a',
'Bright Black'
)
},
get description() {
return translate(
'auto.components.settings.TerminalWindowSection.f30c492769',
'ANSI bright black color'
)
}
},
{
key: 'brightRed',
get label() {
return translate(
'auto.components.settings.TerminalWindowSection.32b1b6acd7',
'Bright Red'
)
},
get description() {
return translate(
'auto.components.settings.TerminalWindowSection.667de68863',
'ANSI bright red color'
)
}
},
{
key: 'brightGreen',
get label() {
return translate(
'auto.components.settings.TerminalWindowSection.7dafd57730',
'Bright Green'
)
},
get description() {
return translate(
'auto.components.settings.TerminalWindowSection.0ffb02f921',
'ANSI bright green color'
)
}
},
{
key: 'brightYellow',
get label() {
return translate(
'auto.components.settings.TerminalWindowSection.936a326be3',
'Bright Yellow'
)
},
get description() {
return translate(
'auto.components.settings.TerminalWindowSection.e2ef5f4ab7',
'ANSI bright yellow color'
)
}
},
{
key: 'brightBlue',
get label() {
return translate(
'auto.components.settings.TerminalWindowSection.66820332fa',
'Bright Blue'
)
},
get description() {
return translate(
'auto.components.settings.TerminalWindowSection.bef6c0f6bf',
'ANSI bright blue color'
)
}
},
{
key: 'brightMagenta',
get label() {
return translate(
'auto.components.settings.TerminalWindowSection.e56e7d6ea0',
'Bright Magenta'
)
},
get description() {
return translate(
'auto.components.settings.TerminalWindowSection.fe4d89ef85',
'ANSI bright magenta color'
)
}
},
{
key: 'brightCyan',
get label() {
return translate(
'auto.components.settings.TerminalWindowSection.f94adc4113',
'Bright Cyan'
)
},
get description() {
return translate(
'auto.components.settings.TerminalWindowSection.1601140f03',
'ANSI bright cyan color'
)
}
},
{
key: 'brightWhite',
get label() {
return translate(
'auto.components.settings.TerminalWindowSection.16948119cb',
'Bright White'
)
},
get description() {
return translate(
'auto.components.settings.TerminalWindowSection.42e01a6055',
'ANSI bright white color'
)
}
}
]
}
]
@@ -106,18 +388,37 @@ export function TerminalWindowSection({
return (
<section className="space-y-4">
<div className="space-y-1">
<h3 className="text-sm font-semibold">{translate("auto.components.settings.TerminalWindowSection.b96ba13ed1", "Window")}</h3>
<p className="text-xs text-muted-foreground">{translate("auto.components.settings.TerminalWindowSection.00eaa6b881", "Window appearance and background settings.")}</p>
<h3 className="text-sm font-semibold">
{translate('auto.components.settings.TerminalWindowSection.b96ba13ed1', 'Window')}
</h3>
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.settings.TerminalWindowSection.00eaa6b881',
'Window appearance and background settings.'
)}
</p>
</div>
<SearchableSetting
title={translate("auto.components.settings.TerminalWindowSection.ea7b1a158e", "Background Opacity")}
description={translate("auto.components.settings.TerminalWindowSection.03acb60aa0", "Controls the transparency of the terminal background.")}
title={translate(
'auto.components.settings.TerminalWindowSection.ea7b1a158e',
'Background Opacity'
)}
description={translate(
'auto.components.settings.TerminalWindowSection.03acb60aa0',
'Controls the transparency of the terminal background.'
)}
keywords={['opacity', 'transparency', 'background', 'alpha']}
>
<NumberField
label={translate("auto.components.settings.TerminalWindowSection.ea7b1a158e", "Background Opacity")}
description={translate("auto.components.settings.TerminalWindowSection.809f37738d", "Controls the transparency of the terminal background. 1 is fully opaque, 0 is fully transparent.")}
label={translate(
'auto.components.settings.TerminalWindowSection.ea7b1a158e',
'Background Opacity'
)}
description={translate(
'auto.components.settings.TerminalWindowSection.809f37738d',
'Controls the transparency of the terminal background. 1 is fully opaque, 0 is fully transparent.'
)}
value={settings.terminalBackgroundOpacity ?? 1}
defaultValue={1}
min={0}
@@ -131,16 +432,31 @@ export function TerminalWindowSection({
</SearchableSetting>
<SearchableSetting
title={translate("auto.components.settings.TerminalWindowSection.2b82242f43", "Window Blur")}
description={translate("auto.components.settings.TerminalWindowSection.97950bb087", "Apply background blur to the terminal window. Requires restart.")}
title={translate(
'auto.components.settings.TerminalWindowSection.2b82242f43',
'Window Blur'
)}
description={translate(
'auto.components.settings.TerminalWindowSection.97950bb087',
'Apply background blur to the terminal window. Requires restart.'
)}
keywords={['window', 'blur', 'background', 'transparency', 'vibrancy']}
className="space-y-3 py-2"
>
<div className="flex items-center justify-between gap-4">
<div className="space-y-0.5">
<Label>{translate("auto.components.settings.TerminalWindowSection.2b82242f43", "Window Blur")}</Label>
<Label>
{translate(
'auto.components.settings.TerminalWindowSection.2b82242f43',
'Window Blur'
)}
</Label>
<p className="text-xs text-muted-foreground">
{translate("auto.components.settings.TerminalWindowSection.97950bb087", "Apply background blur to the terminal window. Requires restart.")}</p>
{translate(
'auto.components.settings.TerminalWindowSection.97950bb087',
'Apply background blur to the terminal window. Requires restart.'
)}
</p>
</div>
<button
role="switch"
@@ -162,9 +478,17 @@ export function TerminalWindowSection({
<div className="flex items-center justify-between gap-3 rounded-md border border-yellow-500/50 bg-yellow-500/10 px-3 py-2.5">
<div className="min-w-0 flex-1 space-y-0.5">
<p className="text-sm font-medium text-yellow-700 dark:text-yellow-300">
{translate("auto.components.settings.TerminalWindowSection.c65bb9ce63", "Restart required")}</p>
{translate(
'auto.components.settings.TerminalWindowSection.c65bb9ce63',
'Restart required'
)}
</p>
<p className="text-xs text-muted-foreground">
{translate("auto.components.settings.TerminalWindowSection.53ce336e15", "Restart Orca to apply the window blur change.")}</p>
{translate(
'auto.components.settings.TerminalWindowSection.53ce336e15',
'Restart Orca to apply the window blur change.'
)}
</p>
</div>
<Button
size="sm"
@@ -174,20 +498,40 @@ export function TerminalWindowSection({
onClick={() => void handleRelaunch()}
>
<RotateCw className={`size-3 ${relaunchingBlur ? 'animate-spin' : ''}`} />
{relaunchingBlur ? translate("auto.components.settings.TerminalWindowSection.907131d741", "Restarting…") : translate("auto.components.settings.TerminalWindowSection.8abdab9f7c", "Restart now")}
{relaunchingBlur
? translate(
'auto.components.settings.TerminalWindowSection.907131d741',
'Restarting…'
)
: translate(
'auto.components.settings.TerminalWindowSection.8abdab9f7c',
'Restart now'
)}
</Button>
</div>
) : null}
</SearchableSetting>
<SearchableSetting
title={translate("auto.components.settings.TerminalWindowSection.36b8402015", "Horizontal Padding")}
description={translate("auto.components.settings.TerminalWindowSection.25e2f8e8e1", "Horizontal padding around the terminal grid in pixels.")}
title={translate(
'auto.components.settings.TerminalWindowSection.36b8402015',
'Horizontal Padding'
)}
description={translate(
'auto.components.settings.TerminalWindowSection.25e2f8e8e1',
'Horizontal padding around the terminal grid in pixels.'
)}
keywords={['padding', 'horizontal', 'spacing', 'margin']}
>
<NumberField
label={translate("auto.components.settings.TerminalWindowSection.36b8402015", "Horizontal Padding")}
description={translate("auto.components.settings.TerminalWindowSection.25e2f8e8e1", "Horizontal padding around the terminal grid in pixels.")}
label={translate(
'auto.components.settings.TerminalWindowSection.36b8402015',
'Horizontal Padding'
)}
description={translate(
'auto.components.settings.TerminalWindowSection.25e2f8e8e1',
'Horizontal padding around the terminal grid in pixels.'
)}
value={settings.terminalPaddingX ?? 4}
defaultValue={4}
min={0}
@@ -199,13 +543,25 @@ export function TerminalWindowSection({
</SearchableSetting>
<SearchableSetting
title={translate("auto.components.settings.TerminalWindowSection.1afcc1d973", "Vertical Padding")}
description={translate("auto.components.settings.TerminalWindowSection.1846f6ee6a", "Vertical padding around the terminal grid in pixels.")}
title={translate(
'auto.components.settings.TerminalWindowSection.1afcc1d973',
'Vertical Padding'
)}
description={translate(
'auto.components.settings.TerminalWindowSection.1846f6ee6a',
'Vertical padding around the terminal grid in pixels.'
)}
keywords={['padding', 'vertical', 'spacing', 'margin']}
>
<NumberField
label={translate("auto.components.settings.TerminalWindowSection.1afcc1d973", "Vertical Padding")}
description={translate("auto.components.settings.TerminalWindowSection.1846f6ee6a", "Vertical padding around the terminal grid in pixels.")}
label={translate(
'auto.components.settings.TerminalWindowSection.1afcc1d973',
'Vertical Padding'
)}
description={translate(
'auto.components.settings.TerminalWindowSection.1846f6ee6a',
'Vertical padding around the terminal grid in pixels.'
)}
value={settings.terminalPaddingY ?? 4}
defaultValue={4}
min={0}
@@ -217,15 +573,30 @@ export function TerminalWindowSection({
</SearchableSetting>
<SearchableSetting
title={translate("auto.components.settings.TerminalWindowSection.3530908ef9", "Hide Mouse While Typing")}
description={translate("auto.components.settings.TerminalWindowSection.1d1920dc8a", "Hide the mouse cursor when typing in the terminal.")}
title={translate(
'auto.components.settings.TerminalWindowSection.3530908ef9',
'Hide Mouse While Typing'
)}
description={translate(
'auto.components.settings.TerminalWindowSection.1d1920dc8a',
'Hide the mouse cursor when typing in the terminal.'
)}
keywords={['mouse', 'hide', 'typing', 'cursor']}
className="flex items-center justify-between gap-4 py-2"
>
<div className="space-y-0.5">
<Label>{translate("auto.components.settings.TerminalWindowSection.3530908ef9", "Hide Mouse While Typing")}</Label>
<Label>
{translate(
'auto.components.settings.TerminalWindowSection.3530908ef9',
'Hide Mouse While Typing'
)}
</Label>
<p className="text-xs text-muted-foreground">
{translate("auto.components.settings.TerminalWindowSection.1d1920dc8a", "Hide the mouse cursor when typing in the terminal.")}</p>
{translate(
'auto.components.settings.TerminalWindowSection.1d1920dc8a',
'Hide the mouse cursor when typing in the terminal.'
)}
</p>
</div>
<button
role="switch"
@@ -250,8 +621,14 @@ export function TerminalWindowSection({
</SearchableSetting>
<SearchableSetting
title={translate("auto.components.settings.TerminalWindowSection.63f8d9336e", "Color Overrides")}
description={translate("auto.components.settings.TerminalWindowSection.e86e09b5c7", "Override individual terminal colors.")}
title={translate(
'auto.components.settings.TerminalWindowSection.63f8d9336e',
'Color Overrides'
)}
description={translate(
'auto.components.settings.TerminalWindowSection.e86e09b5c7',
'Override individual terminal colors.'
)}
keywords={['color', 'override', 'ansi', 'palette', 'theme']}
className="space-y-3"
>
@@ -263,7 +640,11 @@ export function TerminalWindowSection({
<span className={`transition-transform ${colorOverridesExpanded ? 'rotate-90' : ''}`}>
</span>
{translate("auto.components.settings.TerminalWindowSection.63f8d9336e", "Color Overrides")}</button>
{translate(
'auto.components.settings.TerminalWindowSection.63f8d9336e',
'Color Overrides'
)}
</button>
<div
className={`grid overflow-hidden transition-all duration-300 ease-out ${
colorOverridesExpanded ? 'grid-rows-[1fr] opacity-100' : 'grid-rows-[0fr] opacity-0'
@@ -299,7 +680,11 @@ export function TerminalWindowSection({
size="sm"
onClick={() => updateSettings({ terminalColorOverrides: undefined })}
>
{translate("auto.components.settings.TerminalWindowSection.03c855d15f", "Reset all color overrides")}</Button>
{translate(
'auto.components.settings.TerminalWindowSection.03c855d15f',
'Reset all color overrides'
)}
</Button>
</div>
</div>
</div>
@@ -3,25 +3,33 @@ import { translate } from '@/i18n/i18n'
export const AUTO_RESTORE_FIT_OPTIONS: { value: string; label: string; ms: number | null }[] = [
{
value: 'indefinite',
label: translate(
'auto.components.settings.MobilePane.aa1263e881',
'Keep at phone size (default)'
),
get label() {
return translate(
'auto.components.settings.MobilePane.aa1263e881',
'Keep at phone size (default)'
)
},
ms: null
},
{
value: '60s',
label: translate('auto.components.settings.MobilePane.c474aa09d8', 'After 1 minute'),
get label() {
return translate('auto.components.settings.MobilePane.c474aa09d8', 'After 1 minute')
},
ms: 60_000
},
{
value: '5m',
label: translate('auto.components.settings.MobilePane.d4ba07d914', 'After 5 minutes'),
get label() {
return translate('auto.components.settings.MobilePane.d4ba07d914', 'After 5 minutes')
},
ms: 5 * 60_000
},
{
value: '30m',
label: translate('auto.components.settings.MobilePane.ff865419dc', 'After 30 minutes'),
get label() {
return translate('auto.components.settings.MobilePane.ff865419dc', 'After 30 minutes')
},
ms: 30 * 60_000
}
]
@@ -29,53 +29,195 @@ type SidebarWorkspaceOptionsMenuProps = {
}
const GROUP_BY_OPTIONS = [
{ id: 'none', label: translate("auto.components.sidebar.SidebarWorkspaceOptionsMenu.c2c7a45cda", "None") },
{ id: 'workspace-status', label: translate("auto.components.sidebar.SidebarWorkspaceOptionsMenu.e029a2d775", "Status") },
{ id: 'pr-status', label: translate("auto.components.sidebar.SidebarWorkspaceOptionsMenu.0f9b959b31", "PR") },
{ id: 'repo', label: translate("auto.components.sidebar.SidebarWorkspaceOptionsMenu.2170d553cf", "Project") }
{
id: 'none',
get label() {
return translate('auto.components.sidebar.SidebarWorkspaceOptionsMenu.c2c7a45cda', 'None')
}
},
{
id: 'workspace-status',
get label() {
return translate('auto.components.sidebar.SidebarWorkspaceOptionsMenu.e029a2d775', 'Status')
}
},
{
id: 'pr-status',
get label() {
return translate('auto.components.sidebar.SidebarWorkspaceOptionsMenu.0f9b959b31', 'PR')
}
},
{
id: 'repo',
get label() {
return translate('auto.components.sidebar.SidebarWorkspaceOptionsMenu.2170d553cf', 'Project')
}
}
] as const
const CARD_LAYOUT_OPTIONS = [
{ id: 'detailed', label: translate("auto.components.sidebar.SidebarWorkspaceOptionsMenu.cc17bd443b", "Detailed") },
{ id: 'compact', label: translate("auto.components.sidebar.SidebarWorkspaceOptionsMenu.25105b28cb", "Compact") }
{
id: 'detailed',
get label() {
return translate('auto.components.sidebar.SidebarWorkspaceOptionsMenu.cc17bd443b', 'Detailed')
}
},
{
id: 'compact',
get label() {
return translate('auto.components.sidebar.SidebarWorkspaceOptionsMenu.25105b28cb', 'Compact')
}
}
] as const
const PROPERTY_OPTIONS: { id: WorktreeCardProperty; label: string }[] = [
{ id: 'issue', label: translate("auto.components.sidebar.SidebarWorkspaceOptionsMenu.91dfc653e8", "GitHub ticket") },
{ id: 'linear-issue', label: translate("auto.components.sidebar.SidebarWorkspaceOptionsMenu.ca4d3c522e", "Linear issue") },
{ id: 'pr', label: translate("auto.components.sidebar.SidebarWorkspaceOptionsMenu.b8dcc6f321", "PR/MR link") },
{ id: 'comment', label: translate("auto.components.sidebar.SidebarWorkspaceOptionsMenu.26c71e536c", "Notes") },
{ id: 'ports', label: translate("auto.components.sidebar.SidebarWorkspaceOptionsMenu.b64d8bcca0", "Ports") },
{
id: 'issue',
get label() {
return translate(
'auto.components.sidebar.SidebarWorkspaceOptionsMenu.91dfc653e8',
'GitHub ticket'
)
}
},
{
id: 'linear-issue',
get label() {
return translate(
'auto.components.sidebar.SidebarWorkspaceOptionsMenu.ca4d3c522e',
'Linear issue'
)
}
},
{
id: 'pr',
get label() {
return translate(
'auto.components.sidebar.SidebarWorkspaceOptionsMenu.b8dcc6f321',
'PR/MR link'
)
}
},
{
id: 'comment',
get label() {
return translate('auto.components.sidebar.SidebarWorkspaceOptionsMenu.26c71e536c', 'Notes')
}
},
{
id: 'ports',
get label() {
return translate('auto.components.sidebar.SidebarWorkspaceOptionsMenu.b64d8bcca0', 'Ports')
}
},
// Why: toggles the inline "Agent activity" list rendered below each
// workspace card body (see WorktreeCard -> WorktreeCardAgents). Off hides
// the list; there is no alternate surface.
{ id: 'inline-agents', label: translate("auto.components.sidebar.SidebarWorkspaceOptionsMenu.d7084e8bc8", "Agent activity") }
{
id: 'inline-agents',
get label() {
return translate(
'auto.components.sidebar.SidebarWorkspaceOptionsMenu.d7084e8bc8',
'Agent activity'
)
}
}
]
const AGENT_ACTIVITY_DISPLAY_OPTIONS: { id: AgentActivityDisplayMode; label: string }[] = [
{ id: 'compact', label: translate("auto.components.sidebar.SidebarWorkspaceOptionsMenu.25105b28cb", "Compact") },
{ id: 'full', label: translate("auto.components.sidebar.SidebarWorkspaceOptionsMenu.2a81e07366", "Full list") }
{
id: 'compact',
get label() {
return translate('auto.components.sidebar.SidebarWorkspaceOptionsMenu.25105b28cb', 'Compact')
}
},
{
id: 'full',
get label() {
return translate(
'auto.components.sidebar.SidebarWorkspaceOptionsMenu.2a81e07366',
'Full list'
)
}
}
]
const SORT_OPTIONS = [
{ id: 'name', label: translate("auto.components.sidebar.SidebarWorkspaceOptionsMenu.3728165cdd", "Name"), description: null },
{
id: 'name',
get label() {
return translate('auto.components.sidebar.SidebarWorkspaceOptionsMenu.3728165cdd', 'Name')
},
description: null
},
{
id: 'smart',
label: translate("auto.components.sidebar.SidebarWorkspaceOptionsMenu.503462f2b4", "Agent Activity"),
description: translate("auto.components.sidebar.SidebarWorkspaceOptionsMenu.b759bb87ee", "Agents that need attention, then most recent activity.")
get label() {
return translate(
'auto.components.sidebar.SidebarWorkspaceOptionsMenu.503462f2b4',
'Agent Activity'
)
},
get description() {
return translate(
'auto.components.sidebar.SidebarWorkspaceOptionsMenu.b759bb87ee',
'Agents that need attention, then most recent activity.'
)
}
},
{
id: 'recent',
get label() {
return translate('auto.components.sidebar.SidebarWorkspaceOptionsMenu.b451c8b162', 'Recent')
},
description: null
},
{
id: 'repo',
get label() {
return translate('auto.components.sidebar.SidebarWorkspaceOptionsMenu.2170d553cf', 'Project')
},
description: null
},
{ id: 'recent', label: translate("auto.components.sidebar.SidebarWorkspaceOptionsMenu.b451c8b162", "Recent"), description: null },
{ id: 'repo', label: translate("auto.components.sidebar.SidebarWorkspaceOptionsMenu.2170d553cf", "Project"), description: null },
{
id: 'manual',
label: translate("auto.components.sidebar.SidebarWorkspaceOptionsMenu.7b316bdd51", "Manual"),
description: translate("auto.components.sidebar.SidebarWorkspaceOptionsMenu.7153d07485", "Drag workspaces to arrange them within each group.")
get label() {
return translate('auto.components.sidebar.SidebarWorkspaceOptionsMenu.7b316bdd51', 'Manual')
},
get description() {
return translate(
'auto.components.sidebar.SidebarWorkspaceOptionsMenu.7153d07485',
'Drag workspaces to arrange them within each group.'
)
}
}
] as const
const PROJECT_ORDER_OPTIONS = [
{ id: 'manual', label: translate("auto.components.sidebar.SidebarWorkspaceOptionsMenu.7b316bdd51", "Manual"), description: translate("auto.components.sidebar.SidebarWorkspaceOptionsMenu.6664282a7b", "Drag projects to arrange them") },
{ id: 'recent', label: translate("auto.components.sidebar.SidebarWorkspaceOptionsMenu.b451c8b162", "Recent"), description: translate("auto.components.sidebar.SidebarWorkspaceOptionsMenu.af9249c505", "Most recent workspace activity") }
{
id: 'manual',
get label() {
return translate('auto.components.sidebar.SidebarWorkspaceOptionsMenu.7b316bdd51', 'Manual')
},
get description() {
return translate(
'auto.components.sidebar.SidebarWorkspaceOptionsMenu.6664282a7b',
'Drag projects to arrange them'
)
}
},
{
id: 'recent',
get label() {
return translate('auto.components.sidebar.SidebarWorkspaceOptionsMenu.b451c8b162', 'Recent')
},
get description() {
return translate(
'auto.components.sidebar.SidebarWorkspaceOptionsMenu.af9249c505',
'Most recent workspace activity'
)
}
}
] as const
const SidebarWorkspaceOptionsMenu = React.memo(function SidebarWorkspaceOptionsMenu({
@@ -148,8 +290,15 @@ const SidebarWorkspaceOptionsMenu = React.memo(function SidebarWorkspaceOptionsM
className="relative text-muted-foreground"
aria-label={
hasAnyFilter
? translate("auto.components.sidebar.SidebarWorkspaceOptionsMenu.bc96dbd041", "Workspace options ({{value0}} active)", { value0: activeFilterLabel })
: translate("auto.components.sidebar.SidebarWorkspaceOptionsMenu.9919ae1082", "Workspace options")
? translate(
'auto.components.sidebar.SidebarWorkspaceOptionsMenu.bc96dbd041',
'Workspace options ({{value0}} active)',
{ value0: activeFilterLabel }
)
: translate(
'auto.components.sidebar.SidebarWorkspaceOptionsMenu.9919ae1082',
'Workspace options'
)
}
data-workspace-board-preserve-open={preserveWorkspaceBoardOpen ? '' : undefined}
>
@@ -168,7 +317,16 @@ const SidebarWorkspaceOptionsMenu = React.memo(function SidebarWorkspaceOptionsM
</DropdownMenuTrigger>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={6}>
{hasAnyFilter ? translate("auto.components.sidebar.SidebarWorkspaceOptionsMenu.bc96dbd041", "Workspace options ({{value0}})", { value0: activeFilterLabel }) : translate("auto.components.sidebar.SidebarWorkspaceOptionsMenu.9919ae1082", "Workspace options")}
{hasAnyFilter
? translate(
'auto.components.sidebar.SidebarWorkspaceOptionsMenu.bc96dbd041',
'Workspace options ({{value0}})',
{ value0: activeFilterLabel }
)
: translate(
'auto.components.sidebar.SidebarWorkspaceOptionsMenu.9919ae1082',
'Workspace options'
)}
</TooltipContent>
</Tooltip>
<DropdownMenuContent
@@ -178,7 +336,9 @@ const SidebarWorkspaceOptionsMenu = React.memo(function SidebarWorkspaceOptionsM
className="w-72 pb-2"
data-workspace-board-preserve-open={preserveWorkspaceBoardOpen ? '' : undefined}
>
<DropdownMenuLabel>{translate("auto.components.sidebar.SidebarWorkspaceOptionsMenu.dc0bb670bc", "Group by")}</DropdownMenuLabel>
<DropdownMenuLabel>
{translate('auto.components.sidebar.SidebarWorkspaceOptionsMenu.dc0bb670bc', 'Group by')}
</DropdownMenuLabel>
<div className="px-2 pt-0.5 pb-1">
<ToggleGroup
type="single"
@@ -208,7 +368,12 @@ const SidebarWorkspaceOptionsMenu = React.memo(function SidebarWorkspaceOptionsM
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<span className="flex flex-1 items-center justify-between">
<span>{translate("auto.components.sidebar.SidebarWorkspaceOptionsMenu.7bada3b1ab", "Sort by")}</span>
<span>
{translate(
'auto.components.sidebar.SidebarWorkspaceOptionsMenu.7bada3b1ab',
'Sort by'
)}
</span>
<span className="text-[11px] font-medium text-muted-foreground">{sortLabel}</span>
</span>
</DropdownMenuSubTrigger>
@@ -250,11 +415,16 @@ const SidebarWorkspaceOptionsMenu = React.memo(function SidebarWorkspaceOptionsM
{/* Why: project order only has a visible effect when grouping by
project; hide it in none/status/PR modes to avoid a dead control. */}
{groupBy === "repo" && (
{groupBy === 'repo' && (
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<span className="flex flex-1 items-center justify-between">
<span>{translate("auto.components.sidebar.SidebarWorkspaceOptionsMenu.09faabd875", "Project order")}</span>
<span>
{translate(
'auto.components.sidebar.SidebarWorkspaceOptionsMenu.09faabd875',
'Project order'
)}
</span>
<span className="text-[11px] font-medium text-muted-foreground">
{projectOrderLabel}
</span>
@@ -292,7 +462,12 @@ const SidebarWorkspaceOptionsMenu = React.memo(function SidebarWorkspaceOptionsM
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<span className="flex flex-1 items-center justify-between">
<span>{translate("auto.components.sidebar.SidebarWorkspaceOptionsMenu.320b675c9a", "Card layout")}</span>
<span>
{translate(
'auto.components.sidebar.SidebarWorkspaceOptionsMenu.320b675c9a',
'Card layout'
)}
</span>
<span className="text-[11px] font-medium text-muted-foreground">
{cardLayoutLabel}
</span>
@@ -326,9 +501,19 @@ const SidebarWorkspaceOptionsMenu = React.memo(function SidebarWorkspaceOptionsM
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<span className="flex flex-1 items-center justify-between">
<span>{translate("auto.components.sidebar.SidebarWorkspaceOptionsMenu.ba87080fb7", "Show properties")}</span>
{cardLayout === "compact" ? (
<span className="text-[11px] font-medium text-muted-foreground">{translate("auto.components.sidebar.SidebarWorkspaceOptionsMenu.3d4b9c4997", "Hover")}</span>
<span>
{translate(
'auto.components.sidebar.SidebarWorkspaceOptionsMenu.ba87080fb7',
'Show properties'
)}
</span>
{cardLayout === 'compact' ? (
<span className="text-[11px] font-medium text-muted-foreground">
{translate(
'auto.components.sidebar.SidebarWorkspaceOptionsMenu.3d4b9c4997',
'Hover'
)}
</span>
) : visiblePropertyCount > 0 ? (
<span className="text-[11px] font-medium text-muted-foreground">
{visiblePropertyCount}
@@ -352,7 +537,11 @@ const SidebarWorkspaceOptionsMenu = React.memo(function SidebarWorkspaceOptionsM
))}
<DropdownMenuSeparator />
<DropdownMenuLabel className="px-2 py-1 text-[11px] font-medium text-muted-foreground">
{translate("auto.components.sidebar.SidebarWorkspaceOptionsMenu.95c9754653", "Agent activity layout")}</DropdownMenuLabel>
{translate(
'auto.components.sidebar.SidebarWorkspaceOptionsMenu.95c9754653',
'Agent activity layout'
)}
</DropdownMenuLabel>
<DropdownMenuRadioGroup
value={agentActivityDisplayMode}
onValueChange={(value) =>
@@ -24,14 +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.',
error: translate(
'auto.components.sidebar.SshDisconnectedDialog.376bed88e5',
'The connection to the remote host encountered an error.'
),
'auth-failed': 'Authentication 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.'
)
},
get 'auth-failed'() {
return translate(
'auto.components.sidebar.SshDisconnectedDialog.authFailed',
'Authentication to the remote host failed.'
)
}
}
function isReconnectable(status: SshConnectionStatus): boolean {
@@ -76,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(() => {
@@ -128,22 +128,30 @@ export const PR_GROUP_META: Record<
}
> = {
done: {
label: translate('auto.components.sidebar.worktree.list.groups.5076efc3d2', 'Done'),
get label() {
return translate('auto.components.sidebar.worktree.list.groups.5076efc3d2', 'Done')
},
icon: ConductorDoneIcon,
tone: 'text-[#c7a594]'
},
'in-review': {
label: translate('auto.components.sidebar.worktree.list.groups.6798dc7c94', 'In review'),
get label() {
return translate('auto.components.sidebar.worktree.list.groups.6798dc7c94', 'In review')
},
icon: ConductorReviewIcon,
tone: 'text-[#16a34a]'
},
'in-progress': {
label: translate('auto.components.sidebar.worktree.list.groups.7c2f009786', 'In progress'),
get label() {
return translate('auto.components.sidebar.worktree.list.groups.7c2f009786', 'In progress')
},
icon: ConductorProgressIcon,
tone: 'text-[#d4a300]'
},
closed: {
label: translate('auto.components.sidebar.worktree.list.groups.682ed5d551', 'Closed'),
get label() {
return translate('auto.components.sidebar.worktree.list.groups.682ed5d551', 'Closed')
},
icon: CircleX,
tone: 'text-zinc-600 dark:text-zinc-300'
}
@@ -161,7 +169,9 @@ export function getProjectGroupHeaderKey(groupId: string | null): string {
export const PINNED_GROUP_KEY = 'pinned'
export const PINNED_GROUP_META = {
label: translate('auto.components.sidebar.worktree.list.groups.4aeefc5996', 'Pinned'),
get label() {
return translate('auto.components.sidebar.worktree.list.groups.4aeefc5996', 'Pinned')
},
tone: 'text-foreground',
icon: Pin
} as const
@@ -169,7 +179,9 @@ export const PINNED_GROUP_META = {
export const ALL_GROUP_KEY = 'all'
export const ALL_GROUP_META = {
label: translate('auto.components.sidebar.worktree.list.groups.0ed04075b8', 'All'),
get label() {
return translate('auto.components.sidebar.worktree.list.groups.0ed04075b8', 'All')
},
tone: 'text-foreground',
icon: List
} as const
@@ -0,0 +1,172 @@
import type {
ClaudeUsageBreakdownRow,
ClaudeUsageDailyPoint,
ClaudeUsageSessionRow,
ClaudeUsageSummary
} from '../../../../shared/claude-usage-types'
import { ClaudeUsageDailyChart } from './ClaudeUsageDailyChart'
import { translate } from '@/i18n/i18n'
type ClaudeUsageDetailsProps = {
daily: ClaudeUsageDailyPoint[]
formatTokens: (value: number) => string
modelBreakdown: ClaudeUsageBreakdownRow[]
projectBreakdown: ClaudeUsageBreakdownRow[]
recentSessions: ClaudeUsageSessionRow[]
summary: ClaudeUsageSummary | null | undefined
}
function formatSessionTime(timestamp: string): string {
const parsed = new Date(timestamp)
if (Number.isNaN(parsed.getTime())) {
return timestamp
}
return parsed.toLocaleString(undefined, {
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: '2-digit'
})
}
export function ClaudeUsageDetails({
daily,
formatTokens,
modelBreakdown,
projectBreakdown,
recentSessions,
summary
}: ClaudeUsageDetailsProps): React.JSX.Element {
return (
<>
<ClaudeUsageDailyChart daily={daily} />
<div className="grid gap-4 xl:grid-cols-2">
<ClaudeUsageBreakdownSection
formatTokens={formatTokens}
label={translate('auto.components.stats.ClaudeUsagePane.0f394c24e3', 'By model')}
rows={modelBreakdown}
topLabel={translate('auto.components.stats.ClaudeUsagePane.c3fdbc5474', 'Top model:')}
topValue={summary?.topModel}
/>
<ClaudeUsageBreakdownSection
formatTokens={formatTokens}
label={translate('auto.components.stats.ClaudeUsagePane.7dc9e5613b', 'By project')}
rows={projectBreakdown}
topLabel={translate('auto.components.stats.ClaudeUsagePane.f97435845c', 'Top project:')}
topValue={summary?.topProject}
/>
</div>
<section className="rounded-lg border border-border/60 bg-card/40 p-4">
<div className="mb-3">
<h4 className="text-sm font-semibold text-foreground">
{translate('auto.components.stats.ClaudeUsagePane.7e76c84153', 'Recent sessions')}
</h4>
<p className="text-xs text-muted-foreground">
{translate('auto.components.stats.ClaudeUsagePane.abfc4a4943', 'Cache reuse rate:')}{' '}
{summary?.cacheReuseRate !== null && summary?.cacheReuseRate !== undefined
? `${Math.round(summary.cacheReuseRate * 100)}%`
: translate('auto.components.stats.ClaudeUsagePane.7765a4c3e1', 'n/a')}
</p>
</div>
<div className="overflow-x-auto">
<table className="min-w-full text-sm">
<thead>
<tr className="border-b border-border/60 text-left text-xs text-muted-foreground">
<th className="px-2 py-2 font-medium">
{translate('auto.components.stats.ClaudeUsagePane.01476891c7', 'Last active')}
</th>
<th className="px-2 py-2 font-medium">
{translate('auto.components.stats.ClaudeUsagePane.c17bed0416', 'Project')}
</th>
<th className="px-2 py-2 font-medium">
{translate('auto.components.stats.ClaudeUsagePane.1afc25eb06', 'Model')}
</th>
<th className="px-2 py-2 font-medium">
{translate('auto.components.stats.ClaudeUsagePane.0f03975d59', 'Turns')}
</th>
<th className="px-2 py-2 font-medium">
{translate('auto.components.stats.ClaudeUsagePane.faf3444859', 'Input')}
</th>
<th className="px-2 py-2 font-medium">
{translate('auto.components.stats.ClaudeUsagePane.a8b7487ff7', 'Output')}
</th>
<th className="px-2 py-2 font-medium">
{translate('auto.components.stats.ClaudeUsagePane.21ea00bfa8', 'Cache')}
</th>
</tr>
</thead>
<tbody>
{recentSessions.map((row) => (
<tr key={row.sessionId} className="border-b border-border/40 last:border-b-0">
<td className="px-2 py-2 text-muted-foreground">
{formatSessionTime(row.lastActiveAt)}
</td>
<td className="px-2 py-2 text-foreground">{row.projectLabel}</td>
<td className="px-2 py-2 text-muted-foreground">
{row.model ??
translate('auto.components.stats.ClaudeUsagePane.cfe2282ffa', 'Unknown')}
</td>
<td className="px-2 py-2 text-muted-foreground">{row.turns}</td>
<td className="px-2 py-2 text-muted-foreground">
{formatTokens(row.inputTokens)}
</td>
<td className="px-2 py-2 text-muted-foreground">
{formatTokens(row.outputTokens)}
</td>
<td className="px-2 py-2 text-muted-foreground">
{formatTokens(row.cacheReadTokens + row.cacheWriteTokens)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
</>
)
}
function ClaudeUsageBreakdownSection({
formatTokens,
label,
rows,
topLabel,
topValue
}: {
formatTokens: (value: number) => string
label: string
rows: ClaudeUsageBreakdownRow[]
topLabel: string
topValue: string | null | undefined
}): React.JSX.Element {
return (
<section className="rounded-lg border border-border/60 bg-card/40 p-4">
<div className="mb-3">
<h4 className="text-sm font-semibold text-foreground">{label}</h4>
<p className="text-xs text-muted-foreground">
{topLabel}{' '}
{topValue ?? translate('auto.components.stats.ClaudeUsagePane.7765a4c3e1', 'n/a')}
</p>
</div>
<div className="space-y-3">
{rows.slice(0, 5).map((row) => (
<div key={row.key} className="space-y-1">
<div className="flex items-center justify-between gap-3 text-sm">
<span className="truncate text-foreground">{row.label}</span>
<span className="shrink-0 text-muted-foreground">
{formatTokens(row.inputTokens + row.outputTokens)}
</span>
</div>
<div className="text-xs text-muted-foreground">
{row.sessions}{' '}
{translate('auto.components.stats.ClaudeUsagePane.02a046792e', 'sessions •')}{' '}
{row.turns} {translate('auto.components.stats.ClaudeUsagePane.32176e1d44', 'turns')}
</div>
</div>
))}
</div>
</section>
)
}
@@ -23,9 +23,8 @@ import {
DropdownMenuTrigger
} from '../ui/dropdown-menu'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip'
import { ClaudeUsageDailyChart } from './ClaudeUsageDailyChart'
import { ClaudeUsageDetails } from './ClaudeUsageDetails'
import { ClaudeUsageLoadingState } from './ClaudeUsageLoadingState'
import { ClaudeUsageRecentSessionsTable } from './ClaudeUsageRecentSessionsTable'
import { ShareUsageButton } from './ShareUsageButton'
import { StatCard } from './StatCard'
import { formatCost, formatTokens, formatUpdatedAt } from './usage-formatters'
@@ -35,18 +34,30 @@ const RANGE_OPTIONS: ClaudeUsageRange[] = ['7d', '30d', '90d', 'all']
const SCOPE_OPTIONS: { value: ClaudeUsageScope; label: string }[] = [
{
value: 'orca',
label: translate('auto.components.stats.ClaudeUsagePane.4f8368c272', 'Orca worktrees only')
get label() {
return translate('auto.components.stats.ClaudeUsagePane.4f8368c272', 'Orca worktrees only')
}
},
{
value: 'all',
label: translate('auto.components.stats.ClaudeUsagePane.5ce4842c2c', 'All local Claude usage')
get label() {
return translate('auto.components.stats.ClaudeUsagePane.5ce4842c2c', 'All local Claude usage')
}
}
]
const RANGE_LABELS: Record<ClaudeUsageRange, string> = {
'7d': 'Last 7 days',
'30d': 'Last 30 days',
'90d': 'Last 90 days',
all: 'All time'
get '7d'() {
return translate('auto.components.stats.ClaudeUsagePane.rangeLast7Days', 'Last 7 days')
},
get '30d'() {
return translate('auto.components.stats.ClaudeUsagePane.rangeLast30Days', 'Last 30 days')
},
get '90d'() {
return translate('auto.components.stats.ClaudeUsagePane.rangeLast90Days', 'Last 90 days')
},
get all() {
return translate('auto.components.stats.ClaudeUsagePane.rangeAllTime', 'All time')
}
}
export function ClaudeUsagePane(): React.JSX.Element {
@@ -311,73 +322,14 @@ export function ClaudeUsagePane(): React.JSX.Element {
)}
</p>
<ClaudeUsageDailyChart daily={daily} />
<div className="grid gap-4 xl:grid-cols-2">
<section className="rounded-lg border border-border/60 bg-card/40 p-4">
<div className="mb-3">
<h4 className="text-sm font-semibold text-foreground">
{translate('auto.components.stats.ClaudeUsagePane.0f394c24e3', 'By model')}
</h4>
<p className="text-xs text-muted-foreground">
{translate('auto.components.stats.ClaudeUsagePane.c3fdbc5474', 'Top model:')}{' '}
{summary?.topModel ??
translate('auto.components.stats.ClaudeUsagePane.7765a4c3e1', 'n/a')}
</p>
</div>
<div className="space-y-3">
{modelBreakdown.slice(0, 5).map((row) => (
<div key={row.key} className="space-y-1">
<div className="flex items-center justify-between gap-3 text-sm">
<span className="truncate text-foreground">{row.label}</span>
<span className="shrink-0 text-muted-foreground">
{formatTokens(row.inputTokens + row.outputTokens)}
</span>
</div>
<div className="text-xs text-muted-foreground">
{row.sessions}{' '}
{translate('auto.components.stats.ClaudeUsagePane.02a046792e', 'sessions •')}{' '}
{row.turns}{' '}
{translate('auto.components.stats.ClaudeUsagePane.32176e1d44', 'turns')}
</div>
</div>
))}
</div>
</section>
<section className="rounded-lg border border-border/60 bg-card/40 p-4">
<div className="mb-3">
<h4 className="text-sm font-semibold text-foreground">
{translate('auto.components.stats.ClaudeUsagePane.7dc9e5613b', 'By project')}
</h4>
<p className="text-xs text-muted-foreground">
{translate('auto.components.stats.ClaudeUsagePane.f97435845c', 'Top project:')}{' '}
{summary?.topProject ??
translate('auto.components.stats.ClaudeUsagePane.7765a4c3e1', 'n/a')}
</p>
</div>
<div className="space-y-3">
{projectBreakdown.slice(0, 5).map((row) => (
<div key={row.key} className="space-y-1">
<div className="flex items-center justify-between gap-3 text-sm">
<span className="truncate text-foreground">{row.label}</span>
<span className="shrink-0 text-muted-foreground">
{formatTokens(row.inputTokens + row.outputTokens)}
</span>
</div>
<div className="text-xs text-muted-foreground">
{row.sessions}{' '}
{translate('auto.components.stats.ClaudeUsagePane.02a046792e', 'sessions •')}{' '}
{row.turns}{' '}
{translate('auto.components.stats.ClaudeUsagePane.32176e1d44', 'turns')}
</div>
</div>
))}
</div>
</section>
</div>
<ClaudeUsageRecentSessionsTable recentSessions={recentSessions} summary={summary} />
<ClaudeUsageDetails
daily={daily}
formatTokens={formatTokens}
modelBreakdown={modelBreakdown}
projectBreakdown={projectBreakdown}
recentSessions={recentSessions}
summary={summary}
/>
</>
)}
</div>
@@ -0,0 +1,183 @@
import type {
CodexUsageBreakdownRow,
CodexUsageDailyPoint,
CodexUsageSessionRow,
CodexUsageSummary
} from '../../../../shared/codex-usage-types'
import { CodexUsageDailyChart } from './CodexUsageDailyChart'
import { translate } from '@/i18n/i18n'
type CodexUsageDetailsProps = {
daily: CodexUsageDailyPoint[]
formatTokens: (value: number) => string
modelBreakdown: CodexUsageBreakdownRow[]
projectBreakdown: CodexUsageBreakdownRow[]
recentSessions: CodexUsageSessionRow[]
summary: CodexUsageSummary | null | undefined
}
function formatSessionTime(timestamp: string): string {
const parsed = new Date(timestamp)
if (Number.isNaN(parsed.getTime())) {
return timestamp
}
return parsed.toLocaleString(undefined, {
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: '2-digit'
})
}
export function CodexUsageDetails({
daily,
formatTokens,
modelBreakdown,
projectBreakdown,
recentSessions,
summary
}: CodexUsageDetailsProps): React.JSX.Element {
return (
<>
<CodexUsageDailyChart daily={daily} />
<div className="grid gap-4 xl:grid-cols-2">
<CodexUsageBreakdownSection
formatTokens={formatTokens}
label={translate('auto.components.stats.CodexUsagePane.5a0d1d69cd', 'By model')}
rows={modelBreakdown}
showInferredPricing={true}
topLabel={translate('auto.components.stats.CodexUsagePane.95d2d89285', 'Top model:')}
topValue={summary?.topModel}
/>
<CodexUsageBreakdownSection
formatTokens={formatTokens}
label={translate('auto.components.stats.CodexUsagePane.b98718aaab', 'By project')}
rows={projectBreakdown}
showInferredPricing={false}
topLabel={translate('auto.components.stats.CodexUsagePane.829ee743f2', 'Top project:')}
topValue={summary?.topProject}
/>
</div>
<section className="rounded-lg border border-border/60 bg-card/40 p-4">
<div className="mb-3">
<h4 className="text-sm font-semibold text-foreground">
{translate('auto.components.stats.CodexUsagePane.0cb0983c07', 'Recent sessions')}
</h4>
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.stats.CodexUsagePane.0bd8655475',
'Most recent local Codex sessions in this scope.'
)}
</p>
</div>
<div className="overflow-x-auto">
<table className="min-w-full text-sm">
<thead>
<tr className="border-b border-border/60 text-left text-xs text-muted-foreground">
<th className="px-2 py-2 font-medium">
{translate('auto.components.stats.CodexUsagePane.0c36b100be', 'Last active')}
</th>
<th className="px-2 py-2 font-medium">
{translate('auto.components.stats.CodexUsagePane.1a65900aea', 'Project')}
</th>
<th className="px-2 py-2 font-medium">
{translate('auto.components.stats.CodexUsagePane.c2478bcc3c', 'Model')}
</th>
<th className="px-2 py-2 font-medium">
{translate('auto.components.stats.CodexUsagePane.bd0822ca47', 'Events')}
</th>
<th className="px-2 py-2 font-medium">
{translate('auto.components.stats.CodexUsagePane.3acc582214', 'Input')}
</th>
<th className="px-2 py-2 font-medium">
{translate('auto.components.stats.CodexUsagePane.bbd20344b8', 'Output')}
</th>
<th className="px-2 py-2 font-medium">
{translate('auto.components.stats.CodexUsagePane.e0b988599d', 'Total')}
</th>
</tr>
</thead>
<tbody>
{recentSessions.map((row) => (
<tr key={row.sessionId} className="border-b border-border/40 last:border-b-0">
<td className="px-2 py-2 text-muted-foreground">
{formatSessionTime(row.lastActiveAt)}
</td>
<td className="px-2 py-2 text-foreground">{row.projectLabel}</td>
<td className="px-2 py-2 text-muted-foreground">
{row.model ??
translate('auto.components.stats.CodexUsagePane.bf6cf2d4dd', 'Unknown')}
{row.hasInferredPricing ? ' *' : ''}
</td>
<td className="px-2 py-2 text-muted-foreground">{row.events}</td>
<td className="px-2 py-2 text-muted-foreground">
{formatTokens(row.inputTokens)}
</td>
<td className="px-2 py-2 text-muted-foreground">
{formatTokens(row.outputTokens)}
</td>
<td className="px-2 py-2 text-muted-foreground">
{formatTokens(row.totalTokens)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
</>
)
}
function CodexUsageBreakdownSection({
formatTokens,
label,
rows,
showInferredPricing,
topLabel,
topValue
}: {
formatTokens: (value: number) => string
label: string
rows: CodexUsageBreakdownRow[]
showInferredPricing: boolean
topLabel: string
topValue: string | null | undefined
}): React.JSX.Element {
return (
<section className="rounded-lg border border-border/60 bg-card/40 p-4">
<div className="mb-3">
<h4 className="text-sm font-semibold text-foreground">{label}</h4>
<p className="text-xs text-muted-foreground">
{topLabel}{' '}
{topValue ?? translate('auto.components.stats.CodexUsagePane.ae255c3dba', 'n/a')}
</p>
</div>
<div className="space-y-3">
{rows.slice(0, 5).map((row) => (
<div key={row.key} className="space-y-1">
<div className="flex items-center justify-between gap-3 text-sm">
<span className="truncate text-foreground">{row.label}</span>
<span className="shrink-0 text-muted-foreground">
{formatTokens(row.totalTokens)}
</span>
</div>
<div className="text-xs text-muted-foreground">
{row.sessions}{' '}
{translate('auto.components.stats.CodexUsagePane.bf1bf2f674', 'sessions •')}{' '}
{row.events} {translate('auto.components.stats.CodexUsagePane.79a69522a5', 'events')}
{showInferredPricing && row.hasInferredPricing
? ` ${translate(
'auto.components.stats.CodexUsagePane.247c93ca92',
'• inferred pricing'
)}`
: ''}
</div>
</div>
))}
</div>
</section>
)
}
@@ -23,8 +23,7 @@ import {
} from '../ui/dropdown-menu'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip'
import { ClaudeUsageLoadingState } from './ClaudeUsageLoadingState'
import { CodexUsageDailyChart } from './CodexUsageDailyChart'
import { CodexUsageRecentSessionsTable } from './CodexUsageRecentSessionsTable'
import { CodexUsageDetails } from './CodexUsageDetails'
import { ShareUsageButton } from './ShareUsageButton'
import { StatCard } from './StatCard'
import { formatCost, formatTokens, formatUpdatedAt } from './usage-formatters'
@@ -34,18 +33,30 @@ const RANGE_OPTIONS: CodexUsageRange[] = ['7d', '30d', '90d', 'all']
const SCOPE_OPTIONS: { value: CodexUsageScope; label: string }[] = [
{
value: 'orca',
label: translate('auto.components.stats.CodexUsagePane.201766b754', 'Orca worktrees only')
get label() {
return translate('auto.components.stats.CodexUsagePane.201766b754', 'Orca worktrees only')
}
},
{
value: 'all',
label: translate('auto.components.stats.CodexUsagePane.4fe8820098', 'All local Codex usage')
get label() {
return translate('auto.components.stats.CodexUsagePane.4fe8820098', 'All local Codex usage')
}
}
]
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 {
@@ -292,76 +303,14 @@ export function CodexUsagePane(): React.JSX.Element {
)}
</p>
<CodexUsageDailyChart daily={daily} />
<div className="grid gap-4 xl:grid-cols-2">
<section className="rounded-lg border border-border/60 bg-card/40 p-4">
<div className="mb-3">
<h4 className="text-sm font-semibold text-foreground">
{translate('auto.components.stats.CodexUsagePane.5a0d1d69cd', 'By model')}
</h4>
<p className="text-xs text-muted-foreground">
{translate('auto.components.stats.CodexUsagePane.95d2d89285', 'Top model:')}{' '}
{summary?.topModel ??
translate('auto.components.stats.CodexUsagePane.ae255c3dba', 'n/a')}
</p>
</div>
<div className="space-y-3">
{modelBreakdown.slice(0, 5).map((row) => (
<div key={row.key} className="space-y-1">
<div className="flex items-center justify-between gap-3 text-sm">
<span className="truncate text-foreground">{row.label}</span>
<span className="shrink-0 text-muted-foreground">
{formatTokens(row.totalTokens)}
</span>
</div>
<div className="text-xs text-muted-foreground">
{row.sessions}{' '}
{translate('auto.components.stats.CodexUsagePane.bf1bf2f674', 'sessions •')}{' '}
{row.events}{' '}
{translate('auto.components.stats.CodexUsagePane.79a69522a5', 'events')}
{row.hasInferredPricing
? ` ${translate('auto.components.stats.CodexUsagePane.247c93ca92', '• inferred pricing')}`
: ''}
</div>
</div>
))}
</div>
</section>
<section className="rounded-lg border border-border/60 bg-card/40 p-4">
<div className="mb-3">
<h4 className="text-sm font-semibold text-foreground">
{translate('auto.components.stats.CodexUsagePane.b98718aaab', 'By project')}
</h4>
<p className="text-xs text-muted-foreground">
{translate('auto.components.stats.CodexUsagePane.829ee743f2', 'Top project:')}{' '}
{summary?.topProject ??
translate('auto.components.stats.CodexUsagePane.ae255c3dba', 'n/a')}
</p>
</div>
<div className="space-y-3">
{projectBreakdown.slice(0, 5).map((row) => (
<div key={row.key} className="space-y-1">
<div className="flex items-center justify-between gap-3 text-sm">
<span className="truncate text-foreground">{row.label}</span>
<span className="shrink-0 text-muted-foreground">
{formatTokens(row.totalTokens)}
</span>
</div>
<div className="text-xs text-muted-foreground">
{row.sessions}{' '}
{translate('auto.components.stats.CodexUsagePane.bf1bf2f674', 'sessions •')}{' '}
{row.events}{' '}
{translate('auto.components.stats.CodexUsagePane.79a69522a5', 'events')}
</div>
</div>
))}
</div>
</section>
</div>
<CodexUsageRecentSessionsTable recentSessions={recentSessions} />
<CodexUsageDetails
daily={daily}
formatTokens={formatTokens}
modelBreakdown={modelBreakdown}
projectBreakdown={projectBreakdown}
recentSessions={recentSessions}
summary={summary}
/>
</>
)}
</div>
@@ -0,0 +1,186 @@
import type {
OpenCodeUsageBreakdownRow,
OpenCodeUsageDailyPoint,
OpenCodeUsageSessionRow,
OpenCodeUsageSummary
} from '../../../../shared/opencode-usage-types'
import { CodexUsageDailyChart } from './CodexUsageDailyChart'
import { translate } from '@/i18n/i18n'
type OpenCodeUsageDetailsProps = {
daily: OpenCodeUsageDailyPoint[]
formatCost: (value: number | null) => string
formatTokens: (value: number) => string
modelBreakdown: OpenCodeUsageBreakdownRow[]
projectBreakdown: OpenCodeUsageBreakdownRow[]
recentSessions: OpenCodeUsageSessionRow[]
summary: OpenCodeUsageSummary | null | undefined
}
function formatSessionTime(timestamp: string): string {
const parsed = new Date(timestamp)
if (Number.isNaN(parsed.getTime())) {
return timestamp
}
return parsed.toLocaleString(undefined, {
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: '2-digit'
})
}
export function OpenCodeUsageDetails({
daily,
formatCost,
formatTokens,
modelBreakdown,
projectBreakdown,
recentSessions,
summary
}: OpenCodeUsageDetailsProps): React.JSX.Element {
return (
<>
<CodexUsageDailyChart daily={daily} />
<div className="grid gap-4 xl:grid-cols-2">
<OpenCodeUsageBreakdownSection
formatCost={formatCost}
formatTokens={formatTokens}
label={translate('auto.components.stats.OpenCodeUsagePane.040c044d39', 'By model')}
rows={modelBreakdown}
showCost={true}
topLabel={translate('auto.components.stats.OpenCodeUsagePane.a15206a63a', 'Top model:')}
topValue={summary?.topModel}
/>
<OpenCodeUsageBreakdownSection
formatCost={formatCost}
formatTokens={formatTokens}
label={translate('auto.components.stats.OpenCodeUsagePane.0f0a1684bb', 'By project')}
rows={projectBreakdown}
showCost={false}
topLabel={translate('auto.components.stats.OpenCodeUsagePane.048ffe4d65', 'Top project:')}
topValue={summary?.topProject}
/>
</div>
<section className="rounded-lg border border-border/60 bg-card/40 p-4">
<div className="mb-3">
<h4 className="text-sm font-semibold text-foreground">
{translate('auto.components.stats.OpenCodeUsagePane.4799177b1c', 'Recent sessions')}
</h4>
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.stats.OpenCodeUsagePane.81817a641a',
'Most recent local OpenCode sessions in this scope.'
)}
</p>
</div>
<div className="overflow-x-auto">
<table className="min-w-full text-sm">
<thead>
<tr className="border-b border-border/60 text-left text-xs text-muted-foreground">
<th className="px-2 py-2 font-medium">
{translate('auto.components.stats.OpenCodeUsagePane.d97bdf6e27', 'Last active')}
</th>
<th className="px-2 py-2 font-medium">
{translate('auto.components.stats.OpenCodeUsagePane.a4738de041', 'Project')}
</th>
<th className="px-2 py-2 font-medium">
{translate('auto.components.stats.OpenCodeUsagePane.08c78441b7', 'Model')}
</th>
<th className="px-2 py-2 font-medium">
{translate('auto.components.stats.OpenCodeUsagePane.d416f5cf92', 'Events')}
</th>
<th className="px-2 py-2 font-medium">
{translate('auto.components.stats.OpenCodeUsagePane.0f2f266c9d', 'Input')}
</th>
<th className="px-2 py-2 font-medium">
{translate('auto.components.stats.OpenCodeUsagePane.dfc4513657', 'Output')}
</th>
<th className="px-2 py-2 font-medium">
{translate('auto.components.stats.OpenCodeUsagePane.349f7c3f5c', 'Total')}
</th>
</tr>
</thead>
<tbody>
{recentSessions.map((row) => (
<tr key={row.sessionId} className="border-b border-border/40 last:border-b-0">
<td className="px-2 py-2 text-muted-foreground">
{formatSessionTime(row.lastActiveAt)}
</td>
<td className="px-2 py-2 text-foreground">{row.projectLabel}</td>
<td className="px-2 py-2 text-muted-foreground">
{row.model ??
translate('auto.components.stats.OpenCodeUsagePane.362231082f', 'Unknown')}
</td>
<td className="px-2 py-2 text-muted-foreground">{row.events}</td>
<td className="px-2 py-2 text-muted-foreground">
{formatTokens(row.inputTokens)}
</td>
<td className="px-2 py-2 text-muted-foreground">
{formatTokens(row.outputTokens)}
</td>
<td className="px-2 py-2 text-muted-foreground">
{formatTokens(row.totalTokens)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
</>
)
}
function OpenCodeUsageBreakdownSection({
formatCost,
formatTokens,
label,
rows,
showCost,
topLabel,
topValue
}: {
formatCost: (value: number | null) => string
formatTokens: (value: number) => string
label: string
rows: OpenCodeUsageBreakdownRow[]
showCost: boolean
topLabel: string
topValue: string | null | undefined
}): React.JSX.Element {
return (
<section className="rounded-lg border border-border/60 bg-card/40 p-4">
<div className="mb-3">
<h4 className="text-sm font-semibold text-foreground">{label}</h4>
<p className="text-xs text-muted-foreground">
{topLabel}{' '}
{topValue ?? translate('auto.components.stats.OpenCodeUsagePane.8095a63426', 'n/a')}
</p>
</div>
<div className="space-y-3">
{rows.slice(0, 5).map((row) => (
<div key={row.key} className="space-y-1">
<div className="flex items-center justify-between gap-3 text-sm">
<span className="truncate text-foreground">{row.label}</span>
<span className="shrink-0 text-muted-foreground">
{formatTokens(row.totalTokens)}
</span>
</div>
<div className="text-xs text-muted-foreground">
{row.sessions}{' '}
{translate('auto.components.stats.OpenCodeUsagePane.bc0cb89901', 'sessions •')}{' '}
{row.events}{' '}
{translate('auto.components.stats.OpenCodeUsagePane.1e5d410df0', 'events')}
{showCost && row.estimatedCostUsd !== null
? `${formatCost(row.estimatedCostUsd)}`
: ''}
</div>
</div>
))}
</div>
</section>
)
}
@@ -26,8 +26,7 @@ import {
} from '../ui/dropdown-menu'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip'
import { ClaudeUsageLoadingState } from './ClaudeUsageLoadingState'
import { CodexUsageDailyChart } from './CodexUsageDailyChart'
import { OpenCodeUsageRecentSessionsTable } from './OpenCodeUsageRecentSessionsTable'
import { OpenCodeUsageDetails } from './OpenCodeUsageDetails'
import { StatCard } from './StatCard'
import { formatCost, formatTokens, formatUpdatedAt } from './usage-formatters'
import { translate } from '@/i18n/i18n'
@@ -36,21 +35,33 @@ const RANGE_OPTIONS: OpenCodeUsageRange[] = ['7d', '30d', '90d', 'all']
const SCOPE_OPTIONS: { value: OpenCodeUsageScope; label: string }[] = [
{
value: 'orca',
label: translate('auto.components.stats.OpenCodeUsagePane.e04c58327c', 'Orca worktrees only')
get label() {
return translate('auto.components.stats.OpenCodeUsagePane.e04c58327c', 'Orca worktrees only')
}
},
{
value: 'all',
label: translate(
'auto.components.stats.OpenCodeUsagePane.144a6050e9',
'All local OpenCode usage'
)
get label() {
return translate(
'auto.components.stats.OpenCodeUsagePane.144a6050e9',
'All local OpenCode usage'
)
}
}
]
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 {
@@ -312,82 +323,15 @@ export function OpenCodeUsagePane(): React.JSX.Element {
)}
</p>
<CodexUsageDailyChart daily={daily} />
<div className="grid gap-4 xl:grid-cols-2">
<section className="rounded-lg border border-border/60 bg-card/40 p-4">
<div className="mb-3">
<h4 className="text-sm font-semibold text-foreground">
{translate('auto.components.stats.OpenCodeUsagePane.040c044d39', 'By model')}
</h4>
<p className="text-xs text-muted-foreground">
{translate('auto.components.stats.OpenCodeUsagePane.a15206a63a', 'Top model:')}{' '}
{summary?.topModel ??
translate('auto.components.stats.OpenCodeUsagePane.8095a63426', 'n/a')}
</p>
</div>
<div className="space-y-3">
{modelBreakdown.slice(0, 5).map((row) => (
<div key={row.key} className="space-y-1">
<div className="flex items-center justify-between gap-3 text-sm">
<span className="truncate text-foreground">{row.label}</span>
<span className="shrink-0 text-muted-foreground">
{formatTokens(row.totalTokens)}
</span>
</div>
<div className="text-xs text-muted-foreground">
{row.sessions}{' '}
{translate(
'auto.components.stats.OpenCodeUsagePane.bc0cb89901',
'sessions •'
)}{' '}
{row.events}{' '}
{translate('auto.components.stats.OpenCodeUsagePane.1e5d410df0', 'events')}
{row.estimatedCostUsd !== null
? `${formatCost(row.estimatedCostUsd)}`
: ''}
</div>
</div>
))}
</div>
</section>
<section className="rounded-lg border border-border/60 bg-card/40 p-4">
<div className="mb-3">
<h4 className="text-sm font-semibold text-foreground">
{translate('auto.components.stats.OpenCodeUsagePane.0f0a1684bb', 'By project')}
</h4>
<p className="text-xs text-muted-foreground">
{translate('auto.components.stats.OpenCodeUsagePane.048ffe4d65', 'Top project:')}{' '}
{summary?.topProject ??
translate('auto.components.stats.OpenCodeUsagePane.8095a63426', 'n/a')}
</p>
</div>
<div className="space-y-3">
{projectBreakdown.slice(0, 5).map((row) => (
<div key={row.key} className="space-y-1">
<div className="flex items-center justify-between gap-3 text-sm">
<span className="truncate text-foreground">{row.label}</span>
<span className="shrink-0 text-muted-foreground">
{formatTokens(row.totalTokens)}
</span>
</div>
<div className="text-xs text-muted-foreground">
{row.sessions}{' '}
{translate(
'auto.components.stats.OpenCodeUsagePane.bc0cb89901',
'sessions •'
)}{' '}
{row.events}{' '}
{translate('auto.components.stats.OpenCodeUsagePane.1e5d410df0', 'events')}
</div>
</div>
))}
</div>
</section>
</div>
<OpenCodeUsageRecentSessionsTable recentSessions={recentSessions} />
<OpenCodeUsageDetails
daily={daily}
formatCost={formatCost}
formatTokens={formatTokens}
modelBreakdown={modelBreakdown}
projectBreakdown={projectBreakdown}
recentSessions={recentSessions}
summary={summary}
/>
</>
)}
</div>
@@ -48,10 +48,30 @@ function formatTrackingSince(timestamp: number | null): string {
type UsageTab = 'overview' | 'claude' | 'codex' | 'opencode'
const USAGE_ANALYTICS_OPTIONS = [
{ id: 'overview', label: translate('auto.components.stats.StatsPane.b2cf4310ce', 'Overview') },
{ id: 'claude', label: translate('auto.components.stats.StatsPane.85457c02fe', 'Claude') },
{ id: 'codex', label: translate('auto.components.stats.StatsPane.7d26110cea', 'Codex') },
{ id: 'opencode', label: translate('auto.components.stats.StatsPane.1e696db2f6', 'OpenCode') }
{
id: 'overview',
get label() {
return translate('auto.components.stats.StatsPane.b2cf4310ce', 'Overview')
}
},
{
id: 'claude',
get label() {
return translate('auto.components.stats.StatsPane.85457c02fe', 'Claude')
}
},
{
id: 'codex',
get label() {
return translate('auto.components.stats.StatsPane.7d26110cea', 'Codex')
}
},
{
id: 'opencode',
get label() {
return translate('auto.components.stats.StatsPane.1e696db2f6', 'OpenCode')
}
}
] as const satisfies readonly { id: UsageTab; label: string }[]
function UsageAnalyticsOptionIcon({ tab }: { tab: UsageTab }): React.JSX.Element {
@@ -11,43 +11,63 @@ import { translate } from '@/i18n/i18n'
const TAB_COLORS = [
{
label: translate('auto.components.tab.bar.SortableTabContextMenu.20baa43c05', 'None'),
get label() {
return translate('auto.components.tab.bar.SortableTabContextMenu.20baa43c05', 'None')
},
value: null
},
{
label: translate('auto.components.tab.bar.SortableTabContextMenu.cb3eadefd2', 'Blue'),
get label() {
return translate('auto.components.tab.bar.SortableTabContextMenu.cb3eadefd2', 'Blue')
},
value: '#3b82f6'
},
{
label: translate('auto.components.tab.bar.SortableTabContextMenu.c2d8b0991f', 'Purple'),
get label() {
return translate('auto.components.tab.bar.SortableTabContextMenu.c2d8b0991f', 'Purple')
},
value: '#a855f7'
},
{
label: translate('auto.components.tab.bar.SortableTabContextMenu.03cf6dab1a', 'Pink'),
get label() {
return translate('auto.components.tab.bar.SortableTabContextMenu.03cf6dab1a', 'Pink')
},
value: '#ec4899'
},
{
label: translate('auto.components.tab.bar.SortableTabContextMenu.620aec6729', 'Red'),
get label() {
return translate('auto.components.tab.bar.SortableTabContextMenu.620aec6729', 'Red')
},
value: '#ef4444'
},
{
label: translate('auto.components.tab.bar.SortableTabContextMenu.a47629b3cf', 'Orange'),
get label() {
return translate('auto.components.tab.bar.SortableTabContextMenu.a47629b3cf', 'Orange')
},
value: '#f97316'
},
{
label: translate('auto.components.tab.bar.SortableTabContextMenu.69682e2ce4', 'Yellow'),
get label() {
return translate('auto.components.tab.bar.SortableTabContextMenu.69682e2ce4', 'Yellow')
},
value: '#eab308'
},
{
label: translate('auto.components.tab.bar.SortableTabContextMenu.be905e9b0a', 'Green'),
get label() {
return translate('auto.components.tab.bar.SortableTabContextMenu.be905e9b0a', 'Green')
},
value: '#22c55e'
},
{
label: translate('auto.components.tab.bar.SortableTabContextMenu.845576bed1', 'Teal'),
get label() {
return translate('auto.components.tab.bar.SortableTabContextMenu.845576bed1', 'Teal')
},
value: '#14b8a6'
},
{
label: translate('auto.components.tab.bar.SortableTabContextMenu.7703990447', 'Gray'),
get label() {
return translate('auto.components.tab.bar.SortableTabContextMenu.7703990447', 'Gray')
},
value: '#9ca3af'
}
] as const
@@ -0,0 +1,82 @@
import { readdirSync, readFileSync, statSync } from 'node:fs'
import { relative, resolve } from 'node:path'
import ts from 'typescript'
import { describe, expect, it } from 'vitest'
const RENDERER_ROOT = resolve('src/renderer/src')
const FUNCTION_KINDS = new Set<ts.SyntaxKind>([
ts.SyntaxKind.FunctionDeclaration,
ts.SyntaxKind.FunctionExpression,
ts.SyntaxKind.ArrowFunction,
ts.SyntaxKind.MethodDeclaration,
ts.SyntaxKind.Constructor,
ts.SyntaxKind.GetAccessor,
ts.SyntaxKind.SetAccessor
])
function collectSourceFiles(dir: string, files: string[] = []): string[] {
for (const name of readdirSync(dir)) {
const filePath = resolve(dir, name)
const stat = statSync(filePath)
if (stat.isDirectory()) {
collectSourceFiles(filePath, files)
} else if (
/\.(ts|tsx)$/.test(name) &&
!/\.test\.(ts|tsx)$/.test(name) &&
!filePath.includes('/i18n/locales/')
) {
files.push(filePath)
}
}
return files
}
function isInsideFunction(node: ts.Node): boolean {
let parent = node.parent
while (parent && parent.kind !== ts.SyntaxKind.SourceFile) {
if (FUNCTION_KINDS.has(parent.kind)) {
return true
}
parent = parent.parent
}
return false
}
describe('i18n import-time safety', () => {
it('does not evaluate translate() at module load time', () => {
const violations: string[] = []
for (const filePath of collectSourceFiles(RENDERER_ROOT)) {
const source = readFileSync(filePath, 'utf8')
if (!source.includes('translate(')) {
continue
}
const sourceFile = ts.createSourceFile(
filePath,
source,
ts.ScriptTarget.Latest,
true,
filePath.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS
)
function visit(node: ts.Node): void {
if (
ts.isCallExpression(node) &&
node.expression.getText(sourceFile) === 'translate' &&
!isInsideFunction(node)
) {
const { line, character } = sourceFile.getLineAndCharacterOfPosition(
node.getStart(sourceFile)
)
violations.push(`${relative(process.cwd(), filePath)}:${line + 1}:${character + 1}`)
}
ts.forEachChild(node, visit)
}
visit(sourceFile)
}
expect(violations).toEqual([])
})
})
+13 -5
View File
@@ -2,6 +2,7 @@ import './assets/main.css'
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { useTranslation } from 'react-i18next'
import App from './App'
import { RecoverableRenderErrorBoundary } from './components/error-boundaries/RecoverableRenderErrorBoundary'
import {
@@ -35,8 +36,9 @@ if (!rootElement) {
throw new Error('Renderer root element not found.')
}
createRoot(rootElement).render(
<StrictMode>
function RendererRoot(): React.JSX.Element {
useTranslation()
return (
<RecoverableRenderErrorBoundary
boundaryId="app.root"
surface="app-root"
@@ -46,10 +48,16 @@ createRoot(rootElement).render(
'The app shell could not finish rendering. Retry to remount it, or relaunch Orca if the error persists.'
)}
>
<I18nProvider>
<App />
</I18nProvider>
<App />
</RecoverableRenderErrorBoundary>
)
}
createRoot(rootElement).render(
<StrictMode>
<I18nProvider>
<RendererRoot />
</I18nProvider>
</StrictMode>
)
recordRendererCrashBreadcrumb('renderer_bootstrap_rendered')
+22 -14
View File
@@ -2,6 +2,7 @@ import '../assets/main.css'
import { lazy, Suspense, useMemo, useState } from 'react'
import ReactDOM from 'react-dom/client'
import { useTranslation } from 'react-i18next'
import WebConnect from './WebConnect'
import { RecoverableRenderErrorBoundary } from '../components/error-boundaries/RecoverableRenderErrorBoundary'
import {
@@ -46,23 +47,30 @@ function WebRoot(): React.JSX.Element {
installWebPreloadApi()
return (
<Suspense fallback={<div className="min-h-screen bg-background" />}>
<I18nProvider>
<App />
</I18nProvider>
<App />
</Suspense>
)
}
function WebRootBoundary(): React.JSX.Element {
useTranslation()
return (
<RecoverableRenderErrorBoundary
boundaryId="web.root"
surface="web-root"
title={translate('app.recoverableError.webTitle', 'Orca web hit a renderer error.')}
description={translate(
'app.recoverableError.webDescription',
'Retry the web client or reconnect to the paired runtime.'
)}
>
<WebRoot />
</RecoverableRenderErrorBoundary>
)
}
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<RecoverableRenderErrorBoundary
boundaryId="web.root"
surface="web-root"
title={translate('app.recoverableError.webTitle', 'Orca web hit a renderer error.')}
description={translate(
'app.recoverableError.webDescription',
'Retry the web client or reconnect to the paired runtime.'
)}
>
<WebRoot />
</RecoverableRenderErrorBoundary>
<I18nProvider>
<WebRootBoundary />
</I18nProvider>
)