mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 16:02:32 +00:00
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:
@@ -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" />
|
||||
}
|
||||
|
||||
+24
-16
@@ -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([])
|
||||
})
|
||||
})
|
||||
@@ -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')
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user