feat(onboarding): theme step with color mode + optional theme carousel (#38021)

* feat(onboarding): studio theme picker with plugin themes and Match Terminal palettes

Replace the three System/Light/Dark tiles with a live look picker: big
preview, bundled GitHub/VS Code app themes, curated terminal palettes that
recolor Orca via Match Terminal, and per-slot dark/light looks under System.
Picks repaint the app live and never change the mode; skip restores the full
arrival look.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(onboarding): simple theme tiles with optional look carousel

Restore the original System/Dark/Light tiles and move theme looks into an
optional carousel beneath them. Add a theme-plugin directory link that opens
the system browser at onorca.dev/plugins?kind=themes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(onboarding): label color mode and theme sections on theme step

Add Color mode / Theme section headings and replace the step subtitle with
guidance on what to pick.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(onboarding): preserve arrival look across Ghostty import

* fix(onboarding): follow moved terminal theme modules

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Neil
2026-11-03 22:20:13 -08:00
committed by GitHub
co-authored by Copilot
parent 8a4dd3a7bb
commit e0714aebca
28 changed files with 1501 additions and 121 deletions
+1
View File
@@ -19,6 +19,7 @@
1 src/renderer/src/components/jira-connect-credential-guide.tsx
2 src/renderer/src/components/linear-api-key-dialog.tsx
1 src/renderer/src/components/native-chat/NativeChatMarkdownImage.tsx
1 src/renderer/src/components/onboarding/ThemeLookCarousel.tsx
1 src/renderer/src/components/right-sidebar/ssh-forwarded-ports-section.tsx
1 src/renderer/src/components/right-sidebar/ssh-ports-panel.tsx
1 src/renderer/src/components/settings/accounts/codex/CodexLoginLinkNotice.tsx
@@ -42,8 +42,8 @@ const stepCopy = {
},
get subtitle() {
return translate(
'auto.components.onboarding.OnboardingFlow.04ae28d8ca',
'Pick the look you want to stare at for hours.'
'components.onboarding.theme.subtitle',
'Choose light or dark, then optionally a theme. Change either any time in Settings.'
)
}
},
@@ -331,6 +331,8 @@ export default function OnboardingFlow({
onThemeChange={flow.setTheme}
settings={flow.settings}
updateSettings={flow.updateSettings}
lookBaseline={flow.themeLookBaseline}
onLookBaselineChange={flow.setThemeLookBaseline}
/>
)}
{currentStep.id === 'notifications' && (
@@ -0,0 +1,61 @@
// @vitest-environment happy-dom
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { getDefaultSettings } from '../../../../shared/constants'
import { buildThemeLooks } from './onboarding-theme-looks'
import { THEME_PLUGINS_URL, ThemeLookCarousel } from './ThemeLookCarousel'
const openUrl = vi.fn()
const looks = buildThemeLooks({
base: 'dark',
pluginThemes: [],
pluginTerminalThemes: [],
baseline: getDefaultSettings('/tmp'),
labels: { orca: 'Orca', community: 'Terminal palette', builtIn: () => 'Orca Dark' }
})
beforeEach(() => {
openUrl.mockReset().mockResolvedValue(undefined)
Object.defineProperty(window, 'api', { configurable: true, value: { shell: { openUrl } } })
})
afterEach(cleanup)
function renderCarousel(onPick = vi.fn()): void {
render(
<ThemeLookCarousel
looks={looks}
selectedId={looks[0].id}
onPick={onPick}
ariaLabel="Dark looks"
header={null}
/>
)
}
describe('ThemeLookCarousel', () => {
it('opens the themes plugin directory in the system browser', () => {
renderCarousel()
fireEvent.click(screen.getByRole('button', { name: /More themes/ }))
expect(openUrl).toHaveBeenCalledWith(THEME_PLUGINS_URL)
expect(new URL(THEME_PLUGINS_URL).searchParams.get('kind')).toBe('themes')
})
it('moves focus with arrow keys and applies the focused look on click', () => {
const onPick = vi.fn()
renderCarousel(onPick)
const cards = [
...screen
.getByRole('group', { name: 'Dark looks' })
.querySelectorAll<HTMLElement>('[data-theme-look]')
]
cards[0].focus()
fireEvent.keyDown(cards[0], { key: 'ArrowRight' })
expect(document.activeElement).toBe(cards[1])
fireEvent.keyDown(cards[1], { key: 'End' })
const last = cards.at(-1)!
expect(document.activeElement).toBe(last)
fireEvent.click(last)
expect(onPick).toHaveBeenCalledWith(looks.at(-1))
})
})
@@ -0,0 +1,175 @@
import type React from 'react'
import { useEffect, useRef, useState } from 'react'
import { Check, ChevronLeft, ChevronRight, ExternalLink, Puzzle } from 'lucide-react'
import { cn } from '@/lib/utils'
import { translate } from '@/i18n/i18n'
import { Button } from '@/components/ui/button'
import type { ThemeLook } from './onboarding-theme-looks'
import { ThemeLookCardPreview } from './ThemeLookPreview'
export const THEME_PLUGINS_URL = 'https://onorca.dev/plugins?kind=themes'
export function openThemePluginDirectory(): void {
// Why the system browser: onboarding has no workspace browser to host the directory.
void window.api.shell.openUrl(THEME_PLUGINS_URL)
}
type ScrollEdges = { start: boolean; end: boolean }
function readEdges(el: HTMLElement): ScrollEdges {
return {
start: el.scrollLeft <= 1,
end: el.scrollLeft + el.clientWidth >= el.scrollWidth - 1
}
}
/** Optional looks for one mode in a snap carousel; click or Enter applies, the app repaints live. */
export function ThemeLookCarousel({
looks,
selectedId,
onPick,
ariaLabel,
header
}: {
looks: ThemeLook[]
selectedId: string | null
onPick: (look: ThemeLook) => void
ariaLabel: string
header: React.ReactNode
}): React.JSX.Element {
const trackRef = useRef<HTMLDivElement>(null)
const [edges, setEdges] = useState<ScrollEdges>({ start: true, end: false })
const tabStopId = looks.some((look) => look.id === selectedId) ? selectedId : looks[0]?.id
// Why: bring the applied look into view when the mode or slot swaps the card set.
useEffect(() => {
const el = trackRef.current
if (!el) {
return
}
const card = selectedId
? el.querySelector<HTMLElement>(`[data-theme-look="${CSS.escape(selectedId)}"]`)
: null
el.scrollLeft = card
? Math.max(0, card.offsetLeft - (el.clientWidth - card.clientWidth) / 2)
: 0
setEdges(readEdges(el))
// eslint-disable-next-line react-hooks/exhaustive-deps -- only on card-set swaps, not every pick
}, [looks])
const page = (direction: 1 | -1): void => {
const el = trackRef.current
el?.scrollBy({ left: direction * el.clientWidth * 0.8, behavior: 'smooth' })
}
const onKeyDown = (event: React.KeyboardEvent<HTMLDivElement>): void => {
const moves: Record<string, (index: number) => number> = {
ArrowRight: (index) => Math.min(index + 1, looks.length - 1),
ArrowLeft: (index) => Math.max(index - 1, 0),
Home: () => 0,
End: () => looks.length - 1
}
const move = moves[event.key]
const targetId = event.target instanceof HTMLElement ? event.target.dataset.themeLook : null
const current = looks.findIndex((look) => look.id === targetId)
if (!move || current === -1) {
return
}
event.preventDefault()
const next = looks[move(current)]
event.currentTarget
.querySelector<HTMLElement>(`[data-theme-look="${CSS.escape(next.id)}"]`)
?.focus()
}
return (
<section className="space-y-2.5">
<div className="flex items-center gap-3">
<div className="min-w-0 flex-1">{header}</div>
<Button
variant="ghost"
size="icon-sm"
disabled={edges.start}
aria-label={translate('components.onboarding.theme.looks.previous', 'Previous looks')}
onClick={() => page(-1)}
>
<ChevronLeft />
</Button>
<Button
variant="ghost"
size="icon-sm"
disabled={edges.end}
aria-label={translate('components.onboarding.theme.looks.next', 'More looks')}
onClick={() => page(1)}
>
<ChevronRight />
</Button>
</div>
<div
ref={trackRef}
role="group"
aria-label={ariaLabel}
onKeyDown={onKeyDown}
onScroll={(event) => setEdges(readEdges(event.currentTarget))}
className={cn(
'scrollbar-none relative -mx-1 flex snap-x snap-mandatory scroll-px-1 gap-2.5 overflow-x-auto px-1 py-1',
!edges.start &&
!edges.end &&
'[mask-image:linear-gradient(to_right,transparent,black_3rem,black_calc(100%-3rem),transparent)]',
edges.start &&
!edges.end &&
'[mask-image:linear-gradient(to_right,black_calc(100%-3rem),transparent)]',
!edges.start &&
edges.end &&
'[mask-image:linear-gradient(to_right,transparent,black_3rem)]'
)}
>
{looks.map((look) => {
const selected = look.id === selectedId
return (
<button
key={look.id}
type="button"
aria-pressed={selected}
tabIndex={look.id === tabStopId ? 0 : -1}
data-theme-look={look.id}
title={`${look.label} · ${look.source}`}
className={cn(
'w-40 shrink-0 snap-start rounded-lg border p-1.5 text-left outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring motion-reduce:transition-none',
selected ? 'border-foreground/60 bg-accent' : 'border-border hover:bg-accent/60'
)}
onClick={() => onPick(look)}
>
<div className="relative h-[4.5rem] overflow-hidden rounded-md border border-border">
<ThemeLookCardPreview palette={look.palette} />
{selected && (
<div className="absolute right-1 top-1 grid size-4 place-items-center rounded-full bg-foreground text-background shadow-sm">
<Check className="size-2.5" strokeWidth={3} />
</div>
)}
</div>
<div className="truncate px-0.5 pt-1.5 text-xs font-medium text-foreground">
{look.label}
</div>
<div className="truncate px-0.5 text-2xs text-muted-foreground">{look.source}</div>
</button>
)
})}
<button
type="button"
className="flex w-40 shrink-0 snap-start flex-col items-center justify-center gap-1.5 rounded-lg border border-dashed border-border p-1.5 text-center text-muted-foreground transition-colors hover:bg-accent/60 hover:text-foreground"
onClick={openThemePluginDirectory}
>
<Puzzle className="size-4" />
<span className="text-xs font-medium">
{translate('components.onboarding.theme.looks.moreThemes', 'More themes')}
</span>
<span className="flex items-center gap-1 text-2xs">
{translate('components.onboarding.theme.looks.browsePlugins', 'Browse plugins')}
<ExternalLink className="size-2.5" />
</span>
</button>
</div>
</section>
)
}
@@ -0,0 +1,84 @@
import type React from 'react'
import { useMemo } from 'react'
import { translate } from '@/i18n/i18n'
import { useUiLanguageRevision } from '@/i18n/use-ui-language-revision'
import { usePluginTerminalThemes, usePluginThemes } from '@/store/plugin-themes'
import type { GlobalSettings } from '../../../../shared/global-settings-types'
import { SettingsSegmentedControl } from '../settings/form-controls/SettingsFormControls'
import { buildThemeLooks, type ThemeLook, type ThemeLookBase } from './onboarding-theme-looks'
import { selectedThemeLookId } from './onboarding-theme-look-settings'
export type ThemeLooksByBase = Record<ThemeLookBase, ThemeLook[]>
/** The looks for both modes and which one each mode's slot holds now. */
export function useThemeLooks(
settings: GlobalSettings | null,
baseline: GlobalSettings | null
): {
looks: ThemeLooksByBase
selected: Record<ThemeLookBase, string | null>
pluginTerminalThemes: ReturnType<typeof usePluginTerminalThemes>
} {
const pluginThemes = usePluginThemes()
const pluginTerminalThemes = usePluginTerminalThemes()
const languageRevision = useUiLanguageRevision()
const looks = useMemo<ThemeLooksByBase>(() => {
if (!baseline) {
return { dark: [], light: [] }
}
const labels = {
orca: translate('components.onboarding.theme.looks.sourceBuiltIn', 'Built in'),
community: translate('components.onboarding.theme.looks.sourceCommunity', 'Terminal palette'),
builtIn: (base: ThemeLookBase) =>
base === 'dark'
? translate('components.onboarding.theme.looks.orcaDark', 'Orca Dark')
: translate('components.onboarding.theme.looks.orcaLight', 'Orca Light')
}
const build = (base: ThemeLookBase): ThemeLook[] =>
buildThemeLooks({ base, pluginThemes, pluginTerminalThemes, baseline, labels })
return { dark: build('dark'), light: build('light') }
// eslint-disable-next-line react-hooks/exhaustive-deps -- languageRevision re-reads translated labels
}, [baseline, pluginThemes, pluginTerminalThemes, languageRevision])
const selected = {
dark: settings ? selectedThemeLookId(settings, 'dark', looks.dark) : null,
light: settings ? selectedThemeLookId(settings, 'light', looks.light) : null
}
return { looks, selected, pluginTerminalThemes }
}
/** Under System: which of the two persisted looks the list edits; marks the one showing now. */
export function ThemeSlotControl({
value,
osBase,
onChange
}: {
value: ThemeLookBase
osBase: ThemeLookBase
onChange: (base: ThemeLookBase) => void
}): React.JSX.Element {
const label = (base: ThemeLookBase): React.ReactNode => (
<span className="flex items-center gap-1.5 whitespace-nowrap">
{base === 'dark'
? translate('components.onboarding.theme.looks.darkLook', 'Dark look')
: translate('components.onboarding.theme.looks.lightLook', 'Light look')}
{base === osBase && (
<span className="text-2xs font-normal text-muted-foreground">
{translate('components.onboarding.theme.looks.activeNow', 'now')}
</span>
)}
</span>
)
return (
<SettingsSegmentedControl
ariaLabel={translate('components.onboarding.theme.looks.slotLabel', 'Look to edit')}
value={value}
onChange={onChange}
equalWidth
size="sm"
options={[
{ value: 'dark', label: label('dark') },
{ value: 'light', label: label('light') }
]}
/>
)
}
@@ -0,0 +1,80 @@
import type React from 'react'
import type { ThemeLookPalette } from './onboarding-theme-looks'
// Why inline colors throughout: each preview paints another theme's palette, not the live tokens.
/** A tiny Orca window in one look: sidebar, tab strip, and an agent transcript as color bars. */
export function ThemeLookCardPreview({
palette
}: {
palette: ThemeLookPalette
}): React.JSX.Element {
const { terminal: t } = palette
const line = (parts: [string, string][]): React.JSX.Element => (
<div className="flex h-[3px] gap-[3px]">
{parts.map(([color, width], index) => (
<span key={index} className="rounded-full" style={{ background: color, width }} />
))}
</div>
)
return (
<div aria-hidden="true" className="flex size-full" style={{ background: palette.background }}>
<div
className="flex w-[28%] shrink-0 flex-col gap-[5px] px-1.5 py-2"
style={{ background: palette.sidebar, borderRight: `1px solid ${palette.border}` }}
>
{[t.green, t.yellow, palette.mutedForeground].map((dot, index) => (
<div key={index} className="flex items-center gap-1">
<span className="size-[4px] shrink-0 rounded-full" style={{ background: dot }} />
<span
className="h-[3px] rounded-full"
style={{
background: index === 0 ? palette.foreground : palette.mutedForeground,
width: `${70 - index * 15}%`,
opacity: index === 0 ? 0.85 : 0.5
}}
/>
</div>
))}
</div>
<div className="flex min-w-0 flex-1 flex-col">
<div
className="flex h-3 shrink-0 items-end gap-1 px-1.5"
style={{ borderBottom: `1px solid ${palette.border}` }}
>
<span className="h-[2px] w-5 rounded-t" style={{ background: palette.primary }} />
<span
className="mb-[3px] h-[2px] w-4 rounded-full"
style={{ background: palette.border }}
/>
</div>
<div
className="flex flex-1 flex-col gap-[5px] px-2 py-2"
style={{ background: t.background }}
>
{line([
[t.magenta, '8%'],
[t.foreground, '55%']
])}
{line([
[t.blue, '8%'],
[t.muted, '30%'],
[t.cyan, '28%']
])}
{line([
[t.green, '42%'],
[t.green, '18%']
])}
{line([
[t.red, '36%'],
[t.red, '14%']
])}
{line([
[t.yellow, '8%'],
[t.foreground, '32%']
])}
</div>
</div>
</div>
)
}
@@ -0,0 +1,76 @@
import type React from 'react'
import { Check, Monitor, Moon, Sun } from 'lucide-react'
import { cn } from '@/lib/utils'
import { translate } from '@/i18n/i18n'
import type { GlobalSettings } from '../../../../shared/global-settings-types'
import { ChromePreview } from './theme-chrome-preview'
/** The step's primary choice: System, Dark, or Light. */
export function ThemeModeTiles({
value,
onChange
}: {
value: GlobalSettings['theme']
onChange: (theme: GlobalSettings['theme']) => void
}): React.JSX.Element {
const themes: {
id: GlobalSettings['theme']
label: string
hint: string
icon: typeof Monitor
}[] = [
{
id: 'system',
label: translate('auto.components.onboarding.ThemeStep.827ea7b4a2', 'System'),
hint: translate('components.onboarding.theme.hints.system', 'Match OS'),
icon: Monitor
},
{
id: 'dark',
label: translate('auto.components.onboarding.ThemeStep.fa7b673ea9', 'Dark'),
hint: translate('components.onboarding.theme.hints.dark', 'Easy on the eyes'),
icon: Moon
},
{
id: 'light',
label: translate('auto.components.onboarding.ThemeStep.ad192706e6', 'Light'),
hint: translate('components.onboarding.theme.hints.light', 'Bright & crisp'),
icon: Sun
}
]
return (
<div className="grid grid-cols-3 gap-3">
{themes.map(({ id, label, hint, icon: Icon }) => {
const selected = value === id
return (
<button
key={id}
className={cn(
'group overflow-hidden rounded-xl border p-3 text-left transition-colors',
selected ? 'border-foreground/60 bg-accent' : 'border-border hover:bg-accent/60'
)}
aria-pressed={selected}
onClick={() => onChange(id)}
>
<div className="relative mb-3 h-24 overflow-hidden rounded-lg border border-border">
<ChromePreview variant={id} />
{selected && (
<div className="absolute right-1.5 top-1.5 grid size-5 place-items-center rounded-full bg-foreground text-background shadow-sm">
<Check className="size-3" strokeWidth={3} />
</div>
)}
</div>
<div className="flex items-baseline justify-between gap-2">
<div className="flex items-center gap-1.5 text-sm font-medium text-foreground">
<Icon className="size-3.5 text-muted-foreground" />
{label}
</div>
<div className="text-[11px] text-muted-foreground">{hint}</div>
</div>
</button>
)
})}
</div>
)
}
@@ -0,0 +1,57 @@
import { useState } from 'react'
import { defineFixture } from '../../component-fixtures/component-fixture'
import { getDefaultSettings } from '../../../../shared/constants'
import type { GlobalSettings } from '../../../../shared/global-settings-types'
import { ThemeStep } from './ThemeStep'
import { seedPluginThemes } from './onboarding-theme-fixture-themes'
function ThemeStepHarness({ initialTheme }: { initialTheme: GlobalSettings['theme'] }) {
const [settings, setSettings] = useState<GlobalSettings>(() => ({
...getDefaultSettings('/Users/preview'),
theme: initialTheme
}))
const [theme, setTheme] = useState(initialTheme)
const [baseline, setBaseline] = useState<GlobalSettings | null>(settings)
return (
<div className="p-6">
<ThemeStep
theme={theme}
onThemeChange={setTheme}
settings={settings}
updateSettings={async (updates) => setSettings((current) => ({ ...current, ...updates }))}
lookBaseline={baseline}
onLookBaselineChange={setBaseline}
/>
</div>
)
}
export const darkMode = defineFixture({
width: 1036,
setup: seedPluginThemes,
render: () => <ThemeStepHarness initialTheme="dark" />
})
export const systemNarrow = defineFixture({
width: 760,
setup: seedPluginThemes,
render: () => <ThemeStepHarness initialTheme="system" />
})
const pickLook = (id: string) => (frame: HTMLElement) => {
frame.querySelector<HTMLElement>(`[data-theme-look="${id}"]`)?.click()
}
export const pickedPalette = defineFixture({
width: 1036,
setup: seedPluginThemes,
interact: pickLook('terminal:Dracula'),
render: () => <ThemeStepHarness initialTheme="dark" />
})
export const pickedPluginLight = defineFixture({
width: 1036,
setup: seedPluginThemes,
interact: pickLook('plugin:stablyai.orca-github-vscode-theme/github-light-default'),
render: () => <ThemeStepHarness initialTheme="light" />
})
@@ -1,26 +1,33 @@
import { useEffect, useState } from 'react'
import { Check, Monitor, Moon, Settings2, Sun } from 'lucide-react'
import { ExternalLink, Puzzle, Settings2 } from 'lucide-react'
import { Trans } from 'react-i18next'
import { toast } from 'sonner'
import { cn } from '@/lib/utils'
import { track } from '@/lib/telemetry'
import { useMountedRef } from '@/hooks/useMountedRef'
import { GhosttyDiscoveryRow } from './GhosttyDiscoveryRow'
import type { GlobalSettings } from '../../../../shared/global-settings-types'
import type { DiscoveryStatusEmitted } from '../../../../shared/onboarding-state-types'
import { i18n, translate } from '@/i18n/i18n'
import { ChromePreview } from './theme-chrome-preview'
import { ThemeSlotControl, useThemeLooks } from './ThemeLookGallery'
import { ThemeLookCarousel, openThemePluginDirectory } from './ThemeLookCarousel'
import { ThemeModeTiles } from './ThemeModeTiles'
import type { ThemeLook, ThemeLookBase } from './onboarding-theme-looks'
import { themeLookSettingsUpdate } from './onboarding-theme-look-settings'
import {
terminalConfigImportSettingsUpdate,
type TerminalConfigImportPreview
} from '../../../../shared/terminal/appearance/terminal-config-import-preview'
import { getSystemPrefersDark } from '@/lib/terminal-theme'
import { useSystemPrefersDark } from '@/components/terminal-pane/use-system-prefers-dark'
type ThemeStepProps = {
theme: GlobalSettings['theme']
onThemeChange: (theme: GlobalSettings['theme']) => void
settings: GlobalSettings | null
updateSettings: (updates: Partial<GlobalSettings>) => Promise<void>
/** Settings as the step found them; each pick replaces only its own mode's slot from here. */
lookBaseline: GlobalSettings | null
onLookBaselineChange: (settings: GlobalSettings) => void
}
export function applyOnboardingThemeSelection(
@@ -62,7 +69,14 @@ function fieldGroupCountBucket(count: number): '0' | '1-3' | '4-7' | '8+' {
return '8+'
}
export function ThemeStep({ theme, onThemeChange, settings, updateSettings }: ThemeStepProps) {
export function ThemeStep({
theme,
onThemeChange,
settings,
updateSettings,
lookBaseline,
onLookBaselineChange
}: ThemeStepProps) {
const [importing, setImporting] = useState(false)
const [discovery, setDiscovery] = useState<DiscoveryState>({ status: 'idle' })
const mountedRef = useMountedRef()
@@ -133,9 +147,16 @@ export function ThemeStep({ theme, onThemeChange, settings, updateSettings }: Th
track('onboarding_ghostty_import_failed', { reason: 'empty_diff' })
return
}
await updateSettings(
terminalConfigImportSettingsUpdate(settings, resolved, getSystemPrefersDark())
const imported = terminalConfigImportSettingsUpdate(
settings,
resolved,
getSystemPrefersDark()
)
await updateSettings(imported)
// Why: later look picks start from the imported terminal, not the one it replaced.
if (mountedRef.current) {
onLookBaselineChange({ ...(lookBaseline ?? settings), ...imported })
}
// Sync the controller’s local theme so Continue cannot overwrite the imported theme.
if (resolved.diff.theme && mountedRef.current) {
onThemeChange(resolved.diff.theme)
@@ -168,65 +189,78 @@ export function ThemeStep({ theme, onThemeChange, settings, updateSettings }: Th
}
}
const themes: {
id: GlobalSettings['theme']
label: string
hint: string
icon: typeof Monitor
}[] = [
{
id: 'system',
label: translate('auto.components.onboarding.ThemeStep.827ea7b4a2', 'System'),
hint: translate('components.onboarding.theme.hints.system', 'Match OS'),
icon: Monitor
},
{
id: 'dark',
label: translate('auto.components.onboarding.ThemeStep.fa7b673ea9', 'Dark'),
hint: translate('components.onboarding.theme.hints.dark', 'Easy on the eyes'),
icon: Moon
},
{
id: 'light',
label: translate('auto.components.onboarding.ThemeStep.ad192706e6', 'Light'),
hint: translate('components.onboarding.theme.hints.light', 'Bright & crisp'),
icon: Sun
const systemPrefersDark = useSystemPrefersDark()
const osBase: ThemeLookBase = systemPrefersDark ? 'dark' : 'light'
const [systemSlot, setSystemSlot] = useState<ThemeLookBase | null>(null)
const { looks, selected, pluginTerminalThemes } = useThemeLooks(settings, lookBaseline)
// Why a slot picker under System: both looks persist, and only the OS chooses which one shows.
const base: ThemeLookBase = theme === 'system' ? (systemSlot ?? osBase) : theme
const pickLook = (look: ThemeLook): void => {
if (settings && lookBaseline) {
void updateSettings(
themeLookSettingsUpdate(look, settings, lookBaseline, pluginTerminalThemes)
)
}
]
}
return (
<div className="space-y-5">
<div className="grid grid-cols-3 gap-3">
{themes.map(({ id, label, hint, icon: Icon }) => {
const selected = theme === id
return (
<button
key={id}
className={cn(
'group overflow-hidden rounded-xl border p-3 text-left transition-colors',
selected ? 'border-foreground/60 bg-accent' : 'border-border hover:bg-accent/60'
)}
onClick={() => applyOnboardingThemeSelection(id, onThemeChange, updateSettings)}
>
<div className="relative mb-3 h-24 overflow-hidden rounded-lg border border-border">
<ChromePreview variant={id} />
{selected && (
<div className="absolute right-1.5 top-1.5 grid size-5 place-items-center rounded-full bg-foreground text-background shadow-sm">
<Check className="size-3" strokeWidth={3} />
</div>
)}
<section className="space-y-2.5">
<ThemeSectionHeading
title={translate('components.onboarding.theme.looks.colorMode', 'Color mode')}
caption={translate(
'components.onboarding.theme.looks.colorModeCaption',
'Light, dark, or follow your OS.'
)}
/>
<ThemeModeTiles
value={theme}
onChange={(id) => {
setSystemSlot(null)
applyOnboardingThemeSelection(id, onThemeChange, updateSettings)
}}
/>
</section>
<ThemeLookCarousel
looks={looks[base]}
selectedId={selected[base]}
onPick={pickLook}
ariaLabel={
base === 'dark'
? translate('components.onboarding.theme.looks.darkLooks', 'Dark looks')
: translate('components.onboarding.theme.looks.lightLooks', 'Light looks')
}
header={
<div className="flex flex-wrap items-center gap-x-3 gap-y-1.5">
<ThemeSectionHeading
title={translate('components.onboarding.theme.looks.themes', 'Theme')}
caption={
theme === 'system' && base !== osBase
? base === 'dark'
? translate(
'components.onboarding.theme.looks.showsWhenDark',
'Shows when your system is dark'
)
: translate(
'components.onboarding.theme.looks.showsWhenLight',
'Shows when your system is light'
)
: translate(
'components.onboarding.theme.looks.themesCaption',
'Optional. Go further with app themes and terminal palettes.'
)
}
/>
{theme === 'system' && (
<div className="w-60 shrink-0">
<ThemeSlotControl value={base} osBase={osBase} onChange={setSystemSlot} />
</div>
<div className="flex items-baseline justify-between gap-2">
<div className="flex items-center gap-1.5 text-sm font-medium text-foreground">
<Icon className="size-3.5 text-muted-foreground" />
{label}
</div>
<div className="text-[11px] text-muted-foreground">{hint}</div>
</div>
</button>
)
})}
</div>
)}
</div>
}
/>
<GhosttyDiscoveryRow
discovery={discovery}
@@ -235,18 +269,40 @@ export function ThemeStep({ theme, onThemeChange, settings, updateSettings }: Th
onImport={importGhostty}
/>
<div className="flex items-center gap-2 px-1 text-[12px] text-muted-foreground">
<Settings2 className="size-3.5" />
<span>
<Trans
i18n={i18n}
defaults={translate(
'auto.components.onboarding.ThemeStep.moreTerminalOptions',
'More terminal options, including font, cursor, and palette, in <settings>Settings → Terminal</settings>'
<div className="space-y-1.5 px-1 text-[12px] text-muted-foreground">
<div className="flex items-center gap-2">
<Puzzle className="size-3.5" />
<span>
{translate(
'components.onboarding.theme.looks.pluginsHint',
'More themes are available as plugins.'
)}
components={{ settings: <span className="font-medium text-foreground" /> }}
/>
</span>
</span>
<button
type="button"
className="inline-flex items-center gap-1 font-medium text-foreground underline-offset-4 hover:underline"
onClick={openThemePluginDirectory}
>
{translate(
'components.onboarding.theme.looks.browseThemePlugins',
'Browse theme plugins'
)}
<ExternalLink className="size-3" />
</button>
</div>
<div className="flex items-center gap-2">
<Settings2 className="size-3.5" />
<span>
<Trans
i18n={i18n}
defaults={translate(
'auto.components.onboarding.ThemeStep.moreTerminalOptions',
'More terminal options, including font, cursor, and palette, in <settings>Settings → Terminal</settings>'
)}
components={{ settings: <span className="font-medium text-foreground" /> }}
/>
</span>
</div>
</div>
</div>
)
@@ -294,3 +350,18 @@ function humanFields(diff: Partial<GlobalSettings>): string[] {
]
return groups.filter(({ keys }) => keys.some((k) => k in diff)).map(({ label }) => label)
}
function ThemeSectionHeading({
title,
caption
}: {
title: string
caption: string
}): React.JSX.Element {
return (
<h3 className="text-sm font-medium text-foreground">
{title}
<span className="ml-2 text-xs font-normal text-muted-foreground">{caption}</span>
</h3>
)
}
@@ -10,6 +10,7 @@ import type { GlobalSettings } from '../../../../shared/global-settings-types'
import type { OnboardingState } from '../../../../shared/onboarding-state-types'
import type { TuiAgent } from '../../../../shared/agent-launch/catalog/tui-agent'
import { STEPS } from './use-onboarding-flow-types'
import type { ThemeLookSettings } from './onboarding-theme-look-settings'
type TaskSourcesSnapshotProps = EventProps<'onboarding_task_sources_snapshot'>
type TaskSourcesGithubStatus = TaskSourcesSnapshotProps['github_status']
@@ -123,7 +124,8 @@ export function remapOpenOnboardingLastCompletedStep({
type SkippedOnboardingPreferenceOptions = {
currentStepId: OnboardingStepId
themeBeforePreview: GlobalSettings['theme'] | null
/** Look settings as the theme step found them; null before it was reached. */
lookBeforePreview: ThemeLookSettings | null
settingsTheme: GlobalSettings['theme'] | undefined
selectedAgent: TuiAgent | null
setTheme: (theme: GlobalSettings['theme']) => void
@@ -134,7 +136,7 @@ type SkippedOnboardingPreferenceOptions = {
export async function prepareSkippedOnboardingPreferences({
currentStepId,
themeBeforePreview,
lookBeforePreview,
settingsTheme,
selectedAgent,
setTheme,
@@ -145,11 +147,11 @@ export async function prepareSkippedOnboardingPreferences({
try {
// Why: theme tiles save immediately for a stable preview, but skip must not keep this step's choice.
if (currentStepId === 'theme') {
const themeToRestore = themeBeforePreview ?? settingsTheme
const themeToRestore = lookBeforePreview?.theme ?? settingsTheme
if (themeToRestore) {
setTheme(themeToRestore)
applyTheme(themeToRestore)
await updateSettings({ theme: themeToRestore })
await updateSettings({ ...lookBeforePreview, theme: themeToRestore })
}
}
// Why: skipping bypasses step persistence, so save the visible agent choice before closing.
@@ -0,0 +1,55 @@
import { usePluginTerminalThemeStore, usePluginThemeStore } from '@/store/plugin-themes'
import type { PluginThemeRegistration } from '../../../../shared/plugins/artifacts/plugin-theme-artifact'
import type { PluginTerminalThemeRegistration } from '../../../../shared/plugins/artifacts/plugin-terminal-theme-artifact'
// Why real artifacts: the gallery should be judged against the themes Orca actually bundles.
const appThemeFiles = import.meta.glob<{ base: 'dark' | 'light'; tokens: Record<string, string> }>(
'../../../../../resources/plugins/launch/stablyai.orca-{github-vscode-theme,vscode-default-themes}/themes/*.json',
{ eager: true, import: 'default' }
)
const terminalThemeFiles = import.meta.glob<PluginTerminalThemeRegistration['terminal']>(
'../../../../../resources/plugins/launch/stablyai.orca-github-vscode-theme/terminal/*.json',
{ eager: true, import: 'terminal' }
)
const fileId = (path: string): string =>
path
.split('/')
.pop()!
.replace(/\.json$/, '')
const pluginKey = (path: string): string => path.match(/launch\/([^/]+)\//)![1]
const manifests = import.meta.glob<{ contributes: { themes: { id: string; title: string }[] } }>(
'../../../../../resources/plugins/launch/stablyai.orca-{github-vscode-theme,vscode-default-themes}/orca-plugin.json',
{ eager: true, import: 'default' }
)
const titles = new Map(
Object.values(manifests).flatMap((manifest) =>
manifest.contributes.themes.map((theme) => [theme.id, theme.title] as const)
)
)
const titleCase = (id: string): string => titles.get(id) ?? id
const appThemes: PluginThemeRegistration[] = Object.entries(appThemeFiles).map(([path, file]) => ({
id: `plugin:${pluginKey(path)}/${fileId(path)}`,
base: file.base,
tokens: file.tokens,
revision: '1',
pluginKey: pluginKey(path),
pluginName: pluginKey(path).includes('github') ? 'GitHub Themes' : 'VS Code Themes',
label: titleCase(fileId(path))
}))
const terminalThemes: PluginTerminalThemeRegistration[] = Object.entries(terminalThemeFiles).map(
([path, terminal]) => ({
id: `plugin:${pluginKey(path)}/${fileId(path)}`,
pluginKey: pluginKey(path),
pluginName: 'GitHub Themes',
label: titleCase(fileId(path)),
mode: fileId(path).includes('light') ? 'light' : 'dark',
terminal
})
)
export function seedPluginThemes(): void {
usePluginThemeStore.setState({ entries: appThemes, loaded: true })
usePluginTerminalThemeStore.setState({ entries: terminalThemes, loaded: true })
}
@@ -0,0 +1,86 @@
import { describe, expect, it } from 'vitest'
import { getDefaultSettings } from '../../../../shared/constants'
import { MATCH_TERMINAL_APP_THEME } from '../../../../shared/plugin-theme-settings'
import type { GlobalSettings } from '../../../../shared/global-settings-types'
import type { PluginThemeRegistration } from '../../../../shared/plugins/artifacts/plugin-theme-artifact'
import type { PluginTerminalThemeRegistration } from '../../../../shared/plugins/artifacts/plugin-terminal-theme-artifact'
import { buildThemeLooks, type ThemeLook, type ThemeLookBase } from './onboarding-theme-looks'
import { selectedThemeLookId, themeLookSettingsUpdate } from './onboarding-theme-look-settings'
const labels = { orca: 'Orca', community: 'Terminal palette', builtIn: (b: ThemeLookBase) => b }
const appTheme: PluginThemeRegistration = {
id: 'plugin:gh/github-dark-default',
base: 'dark',
tokens: { '--background': '#0d1117', '--foreground': '#e6edf3' },
revision: '1',
pluginKey: 'gh',
pluginName: 'GitHub Themes',
label: 'GitHub Dark Default'
}
const terminalTheme: PluginTerminalThemeRegistration = {
id: 'plugin:gh/github-dark-default',
pluginKey: 'gh',
pluginName: 'GitHub Themes',
label: 'GitHub Dark Default',
mode: 'dark',
terminal: { background: '#010409', foreground: '#e6edf3', red: '#ff7b72' }
}
function setup(overrides: Partial<GlobalSettings> = {}) {
const settings: GlobalSettings = { ...getDefaultSettings('/home/test'), ...overrides }
const looks = (base: ThemeLookBase): ThemeLook[] =>
buildThemeLooks({
base,
pluginThemes: [appTheme],
pluginTerminalThemes: [terminalTheme],
baseline: settings,
labels
})
const look = (base: ThemeLookBase, id: string): ThemeLook =>
looks(base).find((candidate) => candidate.id === id)!
const pick = (current: GlobalSettings, chosen: ThemeLook): GlobalSettings => ({
...current,
...themeLookSettingsUpdate(chosen, current, settings, [terminalTheme])
})
return { settings, looks, look, pick }
}
describe('themeLookSettingsUpdate', () => {
it('paints a terminal palette app-wide and drops overrides that would mask it', () => {
const { settings, look, looks, pick } = setup({
terminalColorOverrides: { background: '#123456' }
})
const next = pick(settings, look('dark', 'terminal:Dracula'))
expect(next.pluginAppThemeDark).toBe(MATCH_TERMINAL_APP_THEME)
expect(next.terminalThemeDark).toBe('Dracula')
expect(next.terminalColorOverrides).toBeUndefined()
expect(selectedThemeLookId(next, 'dark', looks('dark'))).toBe('terminal:Dracula')
})
it('replaces an earlier pick in the same mode with the baseline before applying', () => {
const { settings, look, looks, pick } = setup()
const afterPalette = pick(settings, look('dark', 'terminal:Dracula'))
const next = pick(afterPalette, look('dark', appTheme.id))
expect(next.pluginAppThemeDark).toBe(appTheme.id)
// The plugin's own terminal palette replaces Dracula rather than layering on it.
expect(next.terminalThemeDark).not.toBe('Dracula')
expect(selectedThemeLookId(next, 'dark', looks('dark'))).toBe(appTheme.id)
const reset = pick(next, look('dark', 'built-in:dark'))
expect(reset.pluginAppThemeDark).toBeNull()
expect(reset.terminalThemeDark).toBe(settings.terminalThemeDark)
})
it('leaves the other mode untouched and keeps light palettes off the dark terminal', () => {
const { settings, look, pick } = setup({ terminalUseSeparateLightTheme: false })
const dark = pick(settings, look('dark', 'terminal:Dracula'))
const next = pick(dark, look('light', 'terminal:Catppuccin Latte'))
expect(next.pluginAppThemeDark).toBe(MATCH_TERMINAL_APP_THEME)
expect(next.terminalThemeDark).toBe('Dracula')
expect(next.terminalThemeLight).toBe('Catppuccin Latte')
expect(next.terminalUseSeparateLightTheme).toBe(true)
})
})
@@ -0,0 +1,118 @@
import type { GlobalSettings } from '../../../../shared/global-settings-types'
import type { PluginTerminalThemeRegistration } from '../../../../shared/plugins/artifacts/plugin-terminal-theme-artifact'
import {
MATCH_TERMINAL_APP_THEME,
pluginAppThemeSlotUpdate
} from '../../../../shared/plugin-theme-settings'
import {
hasTerminalColorOverrides,
TERMINAL_COLOR_OVERRIDES_SETTING
} from '../../../../shared/terminal-themes/terminal-color-overrides'
import { withTerminalPluginPalettes } from '@/lib/terminal-plugin-palettes'
import { terminalThemeUpdate } from '../plugins/state/plugin-theme-apply'
import { builtInThemeLookId, type ThemeLook, type ThemeLookBase } from './onboarding-theme-looks'
/** Settings a look writes; the rest of GlobalSettings is never touched by a pick. */
export type ThemeLookSettings = Pick<
GlobalSettings,
| 'theme'
| 'pluginAppThemeDark'
| 'pluginAppThemeLight'
| 'terminalThemeDark'
| 'terminalThemeLight'
| 'terminalUseSeparateLightTheme'
| 'terminalColorOverrides'
| 'terminalColorOverridesLight'
| 'terminalPluginPalettes'
>
function slotTerminalTheme(settings: ThemeLookSettings, base: ThemeLookBase): string {
return base === 'dark' || !settings.terminalUseSeparateLightTheme
? settings.terminalThemeDark
: settings.terminalThemeLight
}
/** The look a mode's slot holds now, or null when it holds something the step doesn't offer. */
export function selectedThemeLookId(
settings: ThemeLookSettings,
base: ThemeLookBase,
looks: readonly ThemeLook[]
): string | null {
const app = base === 'dark' ? settings.pluginAppThemeDark : settings.pluginAppThemeLight
if (!app) {
return builtInThemeLookId(base)
}
if (app === MATCH_TERMINAL_APP_THEME) {
const name = slotTerminalTheme(settings, base)
return (
looks.find((look) => look.kind === 'terminal' && look.terminalThemeName === name)?.id ?? null
)
}
return looks.find((look) => look.kind === 'plugin' && look.id === app)?.id ?? null
}
/** One mode's look settings as they were, so a new pick replaces the last one cleanly. */
function slotBaseline(baseline: ThemeLookSettings, base: ThemeLookBase): Partial<GlobalSettings> {
return base === 'dark'
? {
pluginAppThemeDark: baseline.pluginAppThemeDark ?? null,
terminalThemeDark: baseline.terminalThemeDark,
terminalColorOverrides: baseline.terminalColorOverrides
}
: {
pluginAppThemeLight: baseline.pluginAppThemeLight ?? null,
terminalThemeLight: baseline.terminalThemeLight,
terminalUseSeparateLightTheme: baseline.terminalUseSeparateLightTheme,
terminalColorOverridesLight: baseline.terminalColorOverridesLight
}
}
/** The settings write for picking a look: that mode's slot only, starting from the baseline. */
export function themeLookSettingsUpdate(
look: ThemeLook,
settings: GlobalSettings,
baseline: ThemeLookSettings,
pluginTerminalThemes: readonly PluginTerminalThemeRegistration[]
): Partial<GlobalSettings> {
const dark = look.base === 'dark'
let next: Partial<GlobalSettings> = slotBaseline(baseline, look.base)
if (look.kind === 'built-in') {
next = { ...next, ...pluginAppThemeSlotUpdate(look.base, null) }
} else if (look.kind === 'plugin') {
next = { ...next, ...pluginAppThemeSlotUpdate(look.base, look.theme.id) }
if (look.terminal) {
next = {
...next,
...terminalThemeUpdate({ ...settings, ...next }, look.terminal, dark, pluginTerminalThemes)
}
}
} else {
const merged = { ...settings, ...next }
next = {
...next,
...pluginAppThemeSlotUpdate(look.base, MATCH_TERMINAL_APP_THEME),
...(dark
? { terminalThemeDark: look.terminalThemeName }
: { terminalThemeLight: look.terminalThemeName, terminalUseSeparateLightTheme: true }),
// A picked palette must replace its slot's overrides or they mask its colors.
...(hasTerminalColorOverrides(merged, look.base)
? { [TERMINAL_COLOR_OVERRIDES_SETTING[look.base]]: undefined }
: {})
}
}
return withTerminalPluginPalettes({ ...settings, ...next }, next, pluginTerminalThemes)
}
export function captureThemeLookSettings(settings: GlobalSettings): ThemeLookSettings {
return {
theme: settings.theme,
pluginAppThemeDark: settings.pluginAppThemeDark ?? null,
pluginAppThemeLight: settings.pluginAppThemeLight ?? null,
terminalThemeDark: settings.terminalThemeDark,
terminalThemeLight: settings.terminalThemeLight,
terminalUseSeparateLightTheme: settings.terminalUseSeparateLightTheme,
terminalColorOverrides: settings.terminalColorOverrides,
terminalColorOverridesLight: settings.terminalColorOverridesLight,
terminalPluginPalettes: settings.terminalPluginPalettes ?? []
}
}
@@ -0,0 +1,234 @@
import type { ITheme } from '@xterm/xterm'
import type { GlobalSettings } from '../../../../shared/global-settings-types'
import {
completePluginAppThemeTokens,
type PluginThemeRegistration
} from '../../../../shared/plugins/artifacts/plugin-theme-artifact'
import type { PluginTerminalThemeRegistration } from '../../../../shared/plugins/artifacts/plugin-terminal-theme-artifact'
import { resolveTerminalPalette } from '../../../../shared/terminal-themes/terminal-theme-selection'
import { TERMINAL_THEME_CATALOG } from '../../../../shared/terminal-themes/terminal-theme-catalog'
export type ThemeLookBase = 'light' | 'dark'
export type ThemeLookPalette = {
background: string
sidebar: string
foreground: string
mutedForeground: string
border: string
accent: string
primary: string
terminal: {
background: string
foreground: string
muted: string
red: string
green: string
yellow: string
blue: string
magenta: string
cyan: string
}
}
export type ThemeLook = {
id: string
label: string
/** Who made it, shown as the card's caption. */
source: string
base: ThemeLookBase
palette: ThemeLookPalette
} & (
| { kind: 'built-in' }
| {
kind: 'plugin'
theme: PluginThemeRegistration
terminal: PluginTerminalThemeRegistration | null
}
| { kind: 'terminal'; terminalThemeName: string }
)
// Popular community palettes Orca ships; Match Terminal turns each into a whole-app theme.
const TERMINAL_LOOKS: Record<ThemeLookBase, readonly string[]> = {
dark: [
'Catppuccin Mocha',
'Tokyo Night',
'Dracula',
'Rose Pine',
'Gruvbox Dark',
'One Dark',
'Everforest Dark',
'Night Owl',
'Solarized Dark'
],
light: [
'Catppuccin Latte',
'Rose Pine Dawn',
'Tokyo Night Light',
'Gruvbox Light',
'One Light',
'Everforest Light',
'Solarized Light'
]
}
// Why literal colors: previews show main.css's built-in tokens while the other mode is painted.
const BUILT_IN_CHROME: Record<ThemeLookBase, Omit<ThemeLookPalette, 'terminal'>> = {
dark: {
background: '#0a0a0a',
sidebar: '#171717',
foreground: '#f0f0f0',
mutedForeground: '#acacac',
border: 'rgb(255 255 255 / 0.07)',
accent: '#2a2a2a',
primary: '#e5e5e5'
},
light: {
background: '#ffffff',
sidebar: '#fafafa',
foreground: '#0a0a0a',
mutedForeground: '#696969',
border: '#e5e5e5',
accent: '#f0f0f0',
primary: '#171717'
}
}
export function builtInThemeLookId(base: ThemeLookBase): string {
return `built-in:${base}`
}
function terminalLookId(name: string): string {
return `terminal:${name}`
}
function mixInto(color: string, background: string, percent: number): string {
return `color-mix(in srgb, ${color} ${percent}%, ${background})`
}
function terminalPalette(theme: ITheme | null | undefined): ThemeLookPalette['terminal'] | null {
if (!theme?.background || !theme.foreground) {
return null
}
const { background, foreground } = theme
return {
background,
foreground,
muted: theme.brightBlack ?? mixInto(foreground, background, 55),
red: theme.red ?? foreground,
green: theme.green ?? foreground,
yellow: theme.yellow ?? foreground,
blue: theme.blue ?? foreground,
magenta: theme.magenta ?? foreground,
cyan: theme.cyan ?? foreground
}
}
/** The terminal a slot shows today, so built-in looks preview the user's own terminal colors. */
function currentTerminal(
settings: GlobalSettings,
base: ThemeLookBase
): ThemeLookPalette['terminal'] {
const fallback =
TERMINAL_THEME_CATALOG[base === 'dark' ? 'Ghostty Default Style Dark' : 'Builtin Tango Light']
return (
terminalPalette(resolveTerminalPalette({ ...settings, theme: base }, base === 'dark').theme) ??
terminalPalette(fallback)!
)
}
function pluginTerminalFor(
theme: PluginThemeRegistration,
terminals: readonly PluginTerminalThemeRegistration[]
): PluginTerminalThemeRegistration | null {
const own = terminals.filter(
(terminal) =>
terminal.pluginKey === theme.pluginKey &&
(terminal.mode === theme.base || terminal.mode === 'unknown')
)
return (
own.find((terminal) => terminal.label === theme.label) ?? (own.length === 1 ? own[0] : null)
)
}
function pluginLook(
theme: PluginThemeRegistration,
terminals: readonly PluginTerminalThemeRegistration[],
baseline: GlobalSettings
): ThemeLook {
const tokens = completePluginAppThemeTokens(theme.tokens)
const terminal = pluginTerminalFor(theme, terminals)
return {
kind: 'plugin',
id: theme.id,
label: theme.label,
source: theme.pluginName,
base: theme.base,
theme,
terminal,
palette: {
background: tokens['--background'],
sidebar: tokens['--worktree-sidebar'] ?? tokens['--sidebar'] ?? tokens['--background'],
foreground: tokens['--foreground'],
mutedForeground: tokens['--muted-foreground'] ?? tokens['--foreground'],
border: tokens['--border'] ?? mixInto(tokens['--foreground'], tokens['--background'], 16),
accent: tokens['--accent'] ?? mixInto(tokens['--foreground'], tokens['--background'], 12),
primary: tokens['--primary'] ?? tokens['--foreground'],
terminal: terminalPalette(terminal?.terminal) ?? currentTerminal(baseline, theme.base)
}
}
}
function terminalLook(name: string, base: ThemeLookBase, source: string): ThemeLook | null {
const terminal = terminalPalette(TERMINAL_THEME_CATALOG[name])
if (!terminal) {
return null
}
const { background, foreground } = terminal
return {
kind: 'terminal',
id: terminalLookId(name),
label: name,
source,
base,
terminalThemeName: name,
palette: {
background,
// Why equal: Match Terminal paints the sidebar with the terminal background.
sidebar: background,
foreground,
mutedForeground: mixInto(foreground, background, 70),
border: mixInto(foreground, background, 10),
accent: mixInto(foreground, background, 12),
primary: terminal.blue,
terminal
}
}
}
/** Every look the theme step offers for one mode: Orca's own first, then installed plugin
* themes, then popular terminal palettes painted through Match Terminal. */
export function buildThemeLooks(options: {
base: ThemeLookBase
pluginThemes: readonly PluginThemeRegistration[]
pluginTerminalThemes: readonly PluginTerminalThemeRegistration[]
baseline: GlobalSettings
labels: { orca: string; builtIn: (base: ThemeLookBase) => string; community: string }
}): ThemeLook[] {
const { base, pluginThemes, pluginTerminalThemes, baseline, labels } = options
const builtIn: ThemeLook = {
kind: 'built-in',
id: builtInThemeLookId(base),
label: labels.builtIn(base),
source: labels.orca,
base,
palette: { ...BUILT_IN_CHROME[base], terminal: currentTerminal(baseline, base) }
}
const plugins = pluginThemes
.filter((theme) => theme.base === base)
.map((theme) => pluginLook(theme, pluginTerminalThemes, baseline))
const terminals = TERMINAL_LOOKS[base].flatMap(
(name) => terminalLook(name, base, labels.community) ?? []
)
return [builtIn, ...plugins, ...terminals]
}
@@ -10,6 +10,7 @@ import type { TuiAgent } from '../../../../shared/agent-launch/catalog/tui-agent
import { buildWindowsTerminalSnapshotPayload } from './windows-terminal-onboarding-telemetry'
import { persistStep, type PersistCurrentStepResult } from './use-onboarding-flow-persistence'
import { STEPS, type StepNumber } from './use-onboarding-flow-types'
import { captureThemeLookSettings } from './onboarding-theme-look-settings'
import {
prepareSkippedOnboardingPreferences,
resolveStepIndex,
@@ -44,7 +45,7 @@ type OnboardingFlowActionsArgs = {
stepIndex: number
setStepIndex: Dispatch<SetStateAction<number>>
selectedAgent: TuiAgent | null
themeStepEntryThemeRef: { current: GlobalSettings['theme'] | null }
themeLookBaseline: GlobalSettings | null
setTheme: Dispatch<SetStateAction<GlobalSettings['theme']>>
updateSettings: (updates: Partial<GlobalSettings>) => Promise<void> | void
skipOptions: OnboardingStepSkipOptions
@@ -66,7 +67,7 @@ export function useOnboardingFlowActions({
stepIndex,
setStepIndex,
selectedAgent,
themeStepEntryThemeRef,
themeLookBaseline,
setTheme,
updateSettings,
skipOptions
@@ -181,7 +182,7 @@ export function useOnboardingFlowActions({
const durationMs = consumeStepDurationMs()
const preferencesSaved = await prepareSkippedOnboardingPreferences({
currentStepId: currentStep.id,
themeBeforePreview: themeStepEntryThemeRef.current,
lookBeforePreview: themeLookBaseline ? captureThemeLookSettings(themeLookBaseline) : null,
settingsTheme: settings?.theme,
selectedAgent,
setTheme,
@@ -242,7 +243,7 @@ export function useOnboardingFlowActions({
setBusyLabel,
setError,
setTheme,
themeStepEntryThemeRef
themeLookBaseline
])
const dismissOnboarding = useCallback(
@@ -24,7 +24,17 @@ describe('prepareSkippedOnboardingPreferences', () => {
await expect(
prepareSkippedOnboardingPreferences({
currentStepId: 'theme',
themeBeforePreview: 'dark',
lookBeforePreview: {
theme: 'dark',
pluginAppThemeDark: null,
pluginAppThemeLight: null,
terminalThemeDark: 'Ghostty Default Style Dark',
terminalThemeLight: 'Builtin Tango Light',
terminalUseSeparateLightTheme: true,
terminalColorOverrides: undefined,
terminalColorOverridesLight: undefined,
terminalPluginPalettes: []
},
settingsTheme: 'light',
selectedAgent: null,
setTheme,
@@ -36,7 +46,14 @@ describe('prepareSkippedOnboardingPreferences', () => {
expect(setTheme).toHaveBeenCalledWith('dark')
expect(applyTheme).toHaveBeenCalledWith('dark')
expect(updateSettings).toHaveBeenCalledWith({ theme: 'dark' })
// Restores the app and terminal slots a picked look replaced, not just the mode.
expect(updateSettings).toHaveBeenCalledWith(
expect.objectContaining({
theme: 'dark',
pluginAppThemeDark: null,
terminalThemeDark: 'Ghostty Default Style Dark'
})
)
expect(setError).toHaveBeenCalledWith('settings IPC failed')
expect(toast.error).toHaveBeenCalledWith('Could not save progress', {
description: 'settings IPC failed'
@@ -52,7 +69,7 @@ describe('prepareSkippedOnboardingPreferences', () => {
await expect(
prepareSkippedOnboardingPreferences({
currentStepId: 'agent',
themeBeforePreview: null,
lookBeforePreview: null,
settingsTheme: 'light',
selectedAgent: 'codex',
setTheme,
@@ -161,20 +161,14 @@ export function useOnboardingFlow(
useEffect(() => {
paintedThemeRef.current = paintedTheme
}, [paintedTheme])
const themeStepEntryThemeRef = useRef<GlobalSettings['theme'] | null>(null)
const themeStepEntryCapturedRef = useRef(false)
useEffect(() => {
if (currentStep.id !== 'theme') {
themeStepEntryCapturedRef.current = false
return
}
if (!settings || themeStepEntryCapturedRef.current) {
return
}
// Why: capture entry theme so "Skip to project setup" keeps the preference the user arrived with.
themeStepEntryCapturedRef.current = true
themeStepEntryThemeRef.current = settings.theme
}, [currentStep.id, settings])
// Why per entry: "Skip to project setup" keeps the look the user arrived with, and each pick
// replaces only its own mode's slot from here.
const [themeLookBaseline, setThemeLookBaseline] = useState<GlobalSettings | null>(null)
if (currentStep.id !== 'theme' && themeLookBaseline) {
setThemeLookBaseline(null)
} else if (currentStep.id === 'theme' && settings && !themeLookBaseline) {
setThemeLookBaseline(settings)
}
const previewTheme = resolvePaintedTheme(settings, theme)
useEffect(() => {
@@ -286,7 +280,7 @@ export function useOnboardingFlow(
stepIndex,
setStepIndex,
selectedAgent,
themeStepEntryThemeRef,
themeLookBaseline,
setTheme,
updateSettings,
skipOptions
@@ -305,6 +299,8 @@ export function useOnboardingFlow(
setYoloPermissions: setYoloPermissionsInteractive,
theme,
setTheme: setThemeInteractive,
themeLookBaseline,
setThemeLookBaseline,
busyLabel,
detectedSet,
isDetectingAgents,
+26 -2
View File
@@ -15062,7 +15062,6 @@
"ae3b00ca82": "GitHub-Aufgaben einrichten",
"ff92d15436": "Orca zeigt Benachrichtigungen an, wenn Agenten fertig sind oder Hilfe benötigen.",
"b054332836": "Benachrichtigungen einrichten",
"04ae28d8ca": "Ein Erscheinungsbild auswählen, das auch nach Stunden noch gefällt.",
"f396db9f20": "Vertraute Umgebung einrichten",
"322fc50a18": "Orca funktioniert mit jedem CLI-Agenten. Den bevorzugten Agenten auswählen. Ein Wechsel ist jederzeit möglich.",
"198b148b3c": "Standardagenten auswählen",
@@ -19078,7 +19077,32 @@
"system": "An Betriebssystem anpassen",
"dark": "Angenehm für die Augen",
"light": "Hell & klar"
}
},
"looks": {
"activeNow": "jetzt",
"browsePlugins": "Plugins durchsuchen",
"browseThemePlugins": "Design-Plugins durchsuchen",
"colorMode": "Farbmodus",
"colorModeCaption": "Hell, dunkel oder wie dein System.",
"darkLook": "Dunkler Look",
"darkLooks": "Dunkle Looks",
"lightLook": "Heller Look",
"lightLooks": "Helle Looks",
"moreThemes": "Weitere Designs",
"next": "Weitere Looks",
"orcaDark": "Orca Dunkel",
"orcaLight": "Orca Hell",
"pluginsHint": "Weitere Designs gibt es als Plugins.",
"previous": "Vorherige Looks",
"showsWhenDark": "Wird angezeigt, wenn dein System dunkel ist",
"showsWhenLight": "Wird angezeigt, wenn dein System hell ist",
"slotLabel": "Zu bearbeitender Look",
"sourceBuiltIn": "Integriert",
"sourceCommunity": "Terminal-Palette",
"themes": "Design",
"themesCaption": "Optional. Geh weiter mit App-Designs und Terminal-Paletten."
},
"subtitle": "Wähle hell oder dunkel und optional ein Design. Beides kannst du jederzeit in den Einstellungen ändern."
},
"integrations": {
"capabilities": {
+26 -2
View File
@@ -15301,7 +15301,6 @@
"ae3b00ca82": "Set up GitHub tasks",
"ff92d15436": "Orca will notify you when agents are done or need help.",
"b054332836": "Set up notifications",
"04ae28d8ca": "Pick the look you want to stare at for hours.",
"f396db9f20": "Make it feel like home",
"322fc50a18": "Orca works with every CLI agent. Choose the one you'll reach for most. Switch any time.",
"198b148b3c": "Pick your default agent",
@@ -19376,7 +19375,32 @@
"system": "Match OS",
"dark": "Easy on the eyes",
"light": "Bright & crisp"
}
},
"looks": {
"activeNow": "now",
"browsePlugins": "Browse plugins",
"browseThemePlugins": "Browse theme plugins",
"colorMode": "Color mode",
"colorModeCaption": "Light, dark, or follow your OS.",
"darkLook": "Dark look",
"darkLooks": "Dark looks",
"lightLook": "Light look",
"lightLooks": "Light looks",
"moreThemes": "More themes",
"next": "More looks",
"orcaDark": "Orca Dark",
"orcaLight": "Orca Light",
"pluginsHint": "More themes are available as plugins.",
"previous": "Previous looks",
"showsWhenDark": "Shows when your system is dark",
"showsWhenLight": "Shows when your system is light",
"slotLabel": "Look to edit",
"sourceBuiltIn": "Built in",
"sourceCommunity": "Terminal palette",
"themes": "Theme",
"themesCaption": "Optional. Go further with app themes and terminal palettes."
},
"subtitle": "Choose light or dark, then optionally a theme. Change either any time in Settings."
},
"integrations": {
"capabilities": {
+26 -2
View File
@@ -14777,7 +14777,6 @@
"ae3b00ca82": "Configurar tareas de GitHub",
"ff92d15436": "Orca te notificará cuando los agentes terminen o necesiten ayuda.",
"b054332836": "Configurar notificaciones",
"04ae28d8ca": "Elige la apariencia que quieras ver durante horas.",
"f396db9f20": "Haz que te sientas como en casa",
"322fc50a18": "Orca funciona con cualquier agente CLI. Elige el que más usarás. Puedes cambiarlo en cualquier momento.",
"198b148b3c": "Elija su agente predeterminado",
@@ -20341,7 +20340,32 @@
"system": "Igual que el SO",
"dark": "Suave a la vista",
"light": "Brillante y nítido"
}
},
"looks": {
"activeNow": "ahora",
"browsePlugins": "Explorar plugins",
"browseThemePlugins": "Explorar plugins de temas",
"colorMode": "Modo de color",
"colorModeCaption": "Claro, oscuro o según tu sistema.",
"darkLook": "Estilo oscuro",
"darkLooks": "Estilos oscuros",
"lightLook": "Estilo claro",
"lightLooks": "Estilos claros",
"moreThemes": "Más temas",
"next": "Más estilos",
"orcaDark": "Orca oscuro",
"orcaLight": "Orca claro",
"pluginsHint": "Hay más temas disponibles como plugins.",
"previous": "Estilos anteriores",
"showsWhenDark": "Se muestra cuando tu sistema está en modo oscuro",
"showsWhenLight": "Se muestra cuando tu sistema está en modo claro",
"slotLabel": "Estilo a editar",
"sourceBuiltIn": "Integrado",
"sourceCommunity": "Paleta de terminal",
"themes": "Tema",
"themesCaption": "Opcional. Ve más allá con temas de la app y paletas de terminal."
},
"subtitle": "Elige claro u oscuro y, si quieres, un tema. Puedes cambiar ambos cuando quieras en Ajustes."
},
"integrations": {
"capabilities": {
+26 -2
View File
@@ -15001,7 +15001,6 @@
"ae3b00ca82": "Configurer les tâches GitHub",
"ff92d15436": "Orca vous préviendra quand les agents auront terminé ou auront besoin d'aide.",
"b054332836": "Configurer les notifications",
"04ae28d8ca": "Choisissez le look que vous allez contempler pendant des heures.",
"f396db9f20": "Donnez-lui vos couleurs",
"322fc50a18": "Orca fonctionne avec tous les agents CLI. Choisissez celui que vous solliciterez le plus. Changez quand vous voulez.",
"198b148b3c": "Choisissez votre agent par défaut",
@@ -20510,7 +20509,32 @@
"system": "Faire correspondre le système d'exploitation",
"dark": "Agréable pour les yeux",
"light": "Lumineux et net"
}
},
"looks": {
"activeNow": "actuel",
"browsePlugins": "Parcourir les plugins",
"browseThemePlugins": "Parcourir les plugins de thèmes",
"colorMode": "Mode de couleur",
"colorModeCaption": "Clair, sombre ou selon votre système.",
"darkLook": "Style sombre",
"darkLooks": "Styles sombres",
"lightLook": "Style clair",
"lightLooks": "Styles clairs",
"moreThemes": "Plus de thèmes",
"next": "Plus de styles",
"orcaDark": "Orca sombre",
"orcaLight": "Orca clair",
"pluginsHint": "D’autres thèmes sont disponibles sous forme de plugins.",
"previous": "Styles précédents",
"showsWhenDark": "Affiché quand votre système est en mode sombre",
"showsWhenLight": "Affiché quand votre système est en mode clair",
"slotLabel": "Style à modifier",
"sourceBuiltIn": "Intégré",
"sourceCommunity": "Palette de terminal",
"themes": "Thème",
"themesCaption": "Facultatif. Allez plus loin avec des thèmes d’app et des palettes de terminal."
},
"subtitle": "Choisissez clair ou sombre, puis un thème si vous le souhaitez. Modifiable à tout moment dans les Réglages."
},
"integrations": {
"capabilities": {
+26 -2
View File
@@ -14402,7 +14402,6 @@
"ae3b00ca82": "GitHub タスクをセットアップする",
"ff92d15436": "Orca は、Agent の作業が完了したとき、またはサポートが必要になったときに通知します。",
"b054332836": "通知を設定する",
"04ae28d8ca": "何時間も眺めていたくなるテーマを選んでください。",
"f396db9f20": "まるで家にいるような気分にさせてくれる",
"322fc50a18": "Orca はすべての CLI Agent で動作します。最もよく使うものを選択してください。いつでも切り替えられます。",
"198b148b3c": "デフォルトの Agent を選択",
@@ -18420,7 +18419,32 @@
"system": "OS に合わせる",
"dark": "目にやさしい",
"light": "明るく鮮明"
}
},
"looks": {
"activeNow": "現在",
"browsePlugins": "プラグインを見る",
"browseThemePlugins": "テーマプラグインを見る",
"colorMode": "カラーモード",
"colorModeCaption": "ライト、ダーク、または OS に合わせる。",
"darkLook": "ダークルック",
"darkLooks": "ダークルック",
"lightLook": "ライトルック",
"lightLooks": "ライトルック",
"moreThemes": "その他のテーマ",
"next": "その他のルック",
"orcaDark": "Orca ダーク",
"orcaLight": "Orca ライト",
"pluginsHint": "その他のテーマはプラグインとして入手できます。",
"previous": "前のルック",
"showsWhenDark": "システムがダークのときに表示",
"showsWhenLight": "システムがライトのときに表示",
"slotLabel": "編集するルック",
"sourceBuiltIn": "組み込み",
"sourceCommunity": "ターミナルパレット",
"themes": "テーマ",
"themesCaption": "任意。アプリテーマやターミナルパレットでさらにカスタマイズできます。"
},
"subtitle": "ライトかダークを選び、必要ならテーマも選びましょう。どちらも設定からいつでも変更できます。"
},
"integrations": {
"capabilities": {
+26 -2
View File
@@ -14402,7 +14402,6 @@
"ae3b00ca82": "GitHub 작업 설정",
"ff92d15436": "agents 작업이 완료되거나 도움이 필요할 때 Orca가 알려 드립니다.",
"b054332836": "알림 설정",
"04ae28d8ca": "몇 시간 내내 보고 싶은 테마를 선택하세요.",
"f396db9f20": "집처럼 느껴지도록 하세요",
"322fc50a18": "Orca는 모든 CLI agent와 함께 작동합니다. 가장 자주 사용할 agent를 선택하세요. 언제든지 전환할 수 있습니다.",
"198b148b3c": "기본 agent를 선택하세요",
@@ -18420,7 +18419,32 @@
"system": "OS 설정에 맞춤",
"dark": "눈이 편안한 화면",
"light": "밝고 선명한 화면"
}
},
"looks": {
"activeNow": "현재",
"browsePlugins": "플러그인 둘러보기",
"browseThemePlugins": "테마 플러그인 둘러보기",
"colorMode": "색상 모드",
"colorModeCaption": "라이트, 다크 또는 OS 설정 따르기.",
"darkLook": "다크 룩",
"darkLooks": "다크 룩",
"lightLook": "라이트 룩",
"lightLooks": "라이트 룩",
"moreThemes": "더 많은 테마",
"next": "더 많은 룩",
"orcaDark": "Orca 다크",
"orcaLight": "Orca 라이트",
"pluginsHint": "더 많은 테마를 플러그인으로 사용할 수 있습니다.",
"previous": "이전 룩",
"showsWhenDark": "시스템이 다크 모드일 때 표시",
"showsWhenLight": "시스템이 라이트 모드일 때 표시",
"slotLabel": "편집할 룩",
"sourceBuiltIn": "기본 제공",
"sourceCommunity": "터미널 팔레트",
"themes": "테마",
"themesCaption": "선택 사항. 앱 테마와 터미널 팔레트로 더 꾸며 보세요."
},
"subtitle": "라이트 또는 다크를 고르고, 원하면 테마도 선택하세요. 둘 다 설정에서 언제든 바꿀 수 있습니다."
},
"integrations": {
"capabilities": {
+26 -2
View File
@@ -2879,7 +2879,6 @@
"ae3b00ca82": "Configurar tarefas do GitHub",
"ff92d15436": "O Orca vai avisá-lo quando os agentes terminarem ou precisarem de ajuda.",
"b054332836": "Configurar notificações",
"04ae28d8ca": "Escolha a aparência que você vai encarar por horas.",
"f396db9f20": "Deixe com a sua cara",
"322fc50a18": "O Orca funciona com todo agente de CLI. Escolha o que você vai usar mais. Troque quando quiser.",
"198b148b3c": "Escolha seu agente padrão",
@@ -19992,7 +19991,32 @@
"system": "Seguir o sistema",
"dark": "Suave para os olhos",
"light": "Claro e nítido"
}
},
"looks": {
"activeNow": "agora",
"browsePlugins": "Explorar plugins",
"browseThemePlugins": "Explorar plugins de temas",
"colorMode": "Modo de cor",
"colorModeCaption": "Claro, escuro ou igual ao sistema.",
"darkLook": "Visual escuro",
"darkLooks": "Visuais escuros",
"lightLook": "Visual claro",
"lightLooks": "Visuais claros",
"moreThemes": "Mais temas",
"next": "Mais visuais",
"orcaDark": "Orca escuro",
"orcaLight": "Orca claro",
"pluginsHint": "Há mais temas disponíveis como plugins.",
"previous": "Visuais anteriores",
"showsWhenDark": "Aparece quando o sistema está no modo escuro",
"showsWhenLight": "Aparece quando o sistema está no modo claro",
"slotLabel": "Visual a editar",
"sourceBuiltIn": "Integrado",
"sourceCommunity": "Paleta do terminal",
"themes": "Tema",
"themesCaption": "Opcional. Vá além com temas do app e paletas do terminal."
},
"subtitle": "Escolha claro ou escuro e, se quiser, um tema. Altere ambos quando quiser em Configurações."
},
"integrations": {
"capabilities": {
+26 -2
View File
@@ -15531,7 +15531,6 @@
"97c42cda00": "Установите GitHub CLI, чтобы:",
"ae3b00ca82": "Настройте задачи GitHub",
"b054332836": "Настройте уведомления",
"04ae28d8ca": "Выберите оформление, с которым приятно работать часами.",
"f396db9f20": "Устройтесь поудобнее",
"322fc50a18": "Orca работает с любым CLI-агентом. Выберите того, которым пользуетесь чаще всего. Выбор можно изменить в любой момент.",
"198b148b3c": "Выберите агента по умолчанию",
@@ -19652,7 +19651,32 @@
"system": "Как в ОС",
"dark": "Комфортная для глаз",
"light": "Светлая и чёткая"
}
},
"looks": {
"activeNow": "сейчас",
"browsePlugins": "Обзор плагинов",
"browseThemePlugins": "Обзор плагинов тем",
"colorMode": "Цветовой режим",
"colorModeCaption": "Светлый, тёмный или как в системе.",
"darkLook": "Тёмный стиль",
"darkLooks": "Тёмные стили",
"lightLook": "Светлый стиль",
"lightLooks": "Светлые стили",
"moreThemes": "Ещё темы",
"next": "Ещё стили",
"orcaDark": "Orca Тёмная",
"orcaLight": "Orca Светлая",
"pluginsHint": "Больше тем доступно в виде плагинов.",
"previous": "Предыдущие стили",
"showsWhenDark": "Показывается, когда система в тёмном режиме",
"showsWhenLight": "Показывается, когда система в светлом режиме",
"slotLabel": "Изменяемый стиль",
"sourceBuiltIn": "Встроенная",
"sourceCommunity": "Палитра терминала",
"themes": "Тема",
"themesCaption": "Необязательно. Темы приложения и палитры терминала для большей настройки."
},
"subtitle": "Выберите светлый или тёмный режим и при желании тему. И то и другое можно изменить в настройках."
},
"integrations": {
"capabilities": {
+26 -2
View File
@@ -15048,7 +15048,6 @@
"ae3b00ca82": "GitHub görevlerini ayarla",
"ff92d15436": "Orca, ajanlar işlerini bitirdiğinde veya yardıma ihtiyaç duyduğunda size bildirim gönderecek.",
"b054332836": "Bildirimleri ayarla",
"04ae28d8ca": "Saatlerce bakacağınız görünümü seçin.",
"f396db9f20": "Kendi evinizdeymiş gibi hissettirin",
"322fc50a18": "Orca her CLI ajanıyla çalışır. En sık kullanacağınızı seçin. İstediğiniz zaman değiştirin.",
"198b148b3c": "Varsayılan ajanınızı seçin",
@@ -19064,7 +19063,32 @@
"system": "İşletim sistemiyle eşleştir",
"dark": "Gözü yormaz",
"light": "Parlak ve net"
}
},
"looks": {
"activeNow": "şimdi",
"browsePlugins": "Eklentilere göz at",
"browseThemePlugins": "Tema eklentilerine göz at",
"colorMode": "Renk modu",
"colorModeCaption": "Açık, koyu veya sisteminize uygun.",
"darkLook": "Koyu görünüm",
"darkLooks": "Koyu görünümler",
"lightLook": "Açık görünüm",
"lightLooks": "Açık görünümler",
"moreThemes": "Daha fazla tema",
"next": "Daha fazla görünüm",
"orcaDark": "Orca Koyu",
"orcaLight": "Orca Açık",
"pluginsHint": "Daha fazla tema eklenti olarak sunulur.",
"previous": "Önceki görünümler",
"showsWhenDark": "Sisteminiz koyu olduğunda gösterilir",
"showsWhenLight": "Sisteminiz açık olduğunda gösterilir",
"slotLabel": "Düzenlenecek görünüm",
"sourceBuiltIn": "Yerleşik",
"sourceCommunity": "Terminal paleti",
"themes": "Tema",
"themesCaption": "İsteğe bağlı. Uygulama temaları ve terminal paletleriyle daha ileri gidin."
},
"subtitle": "Açık veya koyu seçin, isterseniz bir tema da seçin. İkisini de istediğiniz zaman Ayarlar’dan değiştirebilirsiniz."
},
"integrations": {
"capabilities": {
+26 -2
View File
@@ -14850,7 +14850,6 @@
"ae3b00ca82": "設定 GitHub 任務",
"ff92d15436": "Orca 會在代理程式完成工作或需要協助時發出通知。",
"b054332836": "設定通知",
"04ae28d8ca": "選擇你想盯著看幾個小時的主題。",
"f396db9f20": "讓這裡有家的感覺",
"322fc50a18": "Orca 支援所有 CLI 代理程式。選擇你最常用的一個,隨時可切換。",
"198b148b3c": "選擇預設代理程式",
@@ -18819,7 +18818,32 @@
"system": "與作業系統一致",
"dark": "柔和護眼",
"light": "明亮清晰"
}
},
"looks": {
"activeNow": "目前",
"browsePlugins": "瀏覽外掛程式",
"browseThemePlugins": "瀏覽主題外掛程式",
"colorMode": "色彩模式",
"colorModeCaption": "淺色、深色或跟隨系統。",
"darkLook": "深色外觀",
"darkLooks": "深色外觀",
"lightLook": "淺色外觀",
"lightLooks": "淺色外觀",
"moreThemes": "更多主題",
"next": "更多外觀",
"orcaDark": "Orca 深色",
"orcaLight": "Orca 淺色",
"pluginsHint": "更多主題以外掛程式形式提供。",
"previous": "上一組外觀",
"showsWhenDark": "系統為深色時顯示",
"showsWhenLight": "系統為淺色時顯示",
"slotLabel": "要編輯的外觀",
"sourceBuiltIn": "內建",
"sourceCommunity": "終端機調色盤",
"themes": "主題",
"themesCaption": "選用。使用 App 主題和終端機調色盤進一步自訂。"
},
"subtitle": "選擇淺色或深色,也可以再選一個主題。兩者都能隨時在設定中變更。"
},
"integrations": {
"capabilities": {
+26 -2
View File
@@ -14402,7 +14402,6 @@
"ae3b00ca82": "设置 GitHub 任务",
"ff92d15436": "Orca 会在智能体完成工作或需要帮助时通知你。",
"b054332836": "设置通知",
"04ae28d8ca": "选择你想盯着看几个小时的主题。",
"f396db9f20": "让这里有家的感觉",
"322fc50a18": "Orca 支持所有 CLI 智能体。选择你最常用的一个,随时可切换。",
"198b148b3c": "选择默认智能体",
@@ -19700,7 +19699,32 @@
"system": "匹配操作系统",
"dark": "轻松护眼",
"light": "明亮、清脆"
}
},
"looks": {
"activeNow": "当前",
"browsePlugins": "浏览插件",
"browseThemePlugins": "浏览主题插件",
"colorMode": "颜色模式",
"colorModeCaption": "浅色、深色或跟随系统。",
"darkLook": "深色外观",
"darkLooks": "深色外观",
"lightLook": "浅色外观",
"lightLooks": "浅色外观",
"moreThemes": "更多主题",
"next": "更多外观",
"orcaDark": "Orca 深色",
"orcaLight": "Orca 浅色",
"pluginsHint": "更多主题以插件形式提供。",
"previous": "上一组外观",
"showsWhenDark": "系统为深色时显示",
"showsWhenLight": "系统为浅色时显示",
"slotLabel": "要编辑的外观",
"sourceBuiltIn": "内置",
"sourceCommunity": "终端调色板",
"themes": "主题",
"themesCaption": "可选。使用应用主题和终端调色板进一步自定义。"
},
"subtitle": "选择浅色或深色,也可以再选一个主题。两者都可以随时在设置中更改。"
},
"integrations": {
"capabilities": {