diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index a1a90068bf6..583030cd91e 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -8,7 +8,7 @@ import { useIpcEvents } from './hooks/useIpcEvents' import Sidebar from './components/Sidebar' import Terminal from './components/Terminal' import Landing from './components/Landing' -import Settings from './components/Settings' +import Settings from './components/settings/Settings' import RightSidebar from './components/right-sidebar' function App(): React.JSX.Element { @@ -100,7 +100,9 @@ function App(): React.JSX.Element { ]) useEffect(() => { - if (!workspaceSessionReady) return + if (!workspaceSessionReady) { + return + } const timer = window.setTimeout(() => { void window.api.session.set({ @@ -123,7 +125,9 @@ function App(): React.JSX.Element { ]) useEffect(() => { - if (!persistedUIReady) return + if (!persistedUIReady) { + return + } const timer = window.setTimeout(() => { void window.api.ui.set({ @@ -139,7 +143,9 @@ function App(): React.JSX.Element { // Apply theme to document useEffect(() => { - if (!settings) return + if (!settings) { + return + } const applyTheme = (dark: boolean): void => { document.documentElement.classList.toggle('dark', dark) @@ -147,10 +153,10 @@ function App(): React.JSX.Element { if (settings.theme === 'dark') { applyTheme(true) - return + return undefined } else if (settings.theme === 'light') { applyTheme(false) - return + return undefined } else { // system const mq = window.matchMedia('(prefers-color-scheme: dark)') @@ -178,7 +184,9 @@ function App(): React.JSX.Element { const showSidebar = activeView !== 'settings' const handleToggleExpand = (): void => { - if (!effectiveActiveTabId) return + if (!effectiveActiveTabId) { + return + } window.dispatchEvent( new CustomEvent(TOGGLE_TERMINAL_PANE_EXPAND_EVENT, { detail: { tabId: effectiveActiveTabId } @@ -188,12 +196,18 @@ function App(): React.JSX.Element { useEffect(() => { const onKeyDown = (e: KeyboardEvent): void => { - if (e.repeat) return - if (!e.metaKey) return + if (e.repeat) { + return + } + if (!e.metaKey) { + return + } // Cmd+N — create worktree if (!e.ctrlKey && !e.altKey && !e.shiftKey && e.key.toLowerCase() === 'n') { - if (repos.length === 0) return + if (repos.length === 0) { + return + } e.preventDefault() openModal('create-worktree') return @@ -212,7 +226,6 @@ function App(): React.JSX.Element { e.preventDefault() setRightSidebarTab('source-control') setRightSidebarOpen(true) - return } } diff --git a/src/renderer/src/components/Settings.tsx b/src/renderer/src/components/Settings.tsx deleted file mode 100644 index b406b9e47c2..00000000000 --- a/src/renderer/src/components/Settings.tsx +++ /dev/null @@ -1,1720 +0,0 @@ -import { useEffect, useState, useCallback, useMemo, useRef } from 'react' -import type { OrcaHooks, Repo, RepoHookSettings } from '../../../shared/types' -import { REPO_COLORS, getDefaultRepoHookSettings } from '../../../shared/constants' -import { useAppStore } from '../store' -import { ScrollArea } from './ui/scroll-area' -import { Button } from './ui/button' -import { Input } from './ui/input' -import { Label } from './ui/label' -import { Separator } from './ui/separator' -import { ToggleGroup, ToggleGroupItem } from './ui/toggle-group' -import { TerminalThemePreview } from './settings/TerminalThemePreview' -import { applyUIZoom } from '@/lib/ui-zoom' -import { - BUILTIN_TERMINAL_THEME_NAMES, - clampNumber, - getSystemPrefersDark, - normalizeColor, - resolvePaneStyleOptions, - resolveEffectiveTerminalAppearance -} from '@/lib/terminal-theme' -import { - ArrowLeft, - Check, - ChevronsUpDown, - CircleX, - Download, - FolderOpen, - Loader2, - Minus, - Palette, - Plus, - RefreshCw, - SlidersHorizontal, - RotateCcw, - SquareTerminal, - Trash2 -} from 'lucide-react' - -type HookName = keyof OrcaHooks['scripts'] -const DEFAULT_REPO_HOOK_SETTINGS = getDefaultRepoHookSettings() -const MAX_THEME_RESULTS = 80 -const MAX_FONT_RESULTS = 12 -const SCROLLBACK_PRESETS_MB = [10, 25, 50, 100, 250] as const -const ZOOM_STEP = 0.5 -const ZOOM_MIN = -3 -const ZOOM_MAX = 5 - -function zoomLevelToPercent(level: number): number { - return Math.round(100 * Math.pow(1.2, level)) -} - -function UIZoomControl(): React.JSX.Element { - const [zoomLevel, setZoomLevel] = useState(() => window.api.ui.getZoomLevel()) - - useEffect(() => { - return window.api.ui.onTerminalZoom(() => { - setZoomLevel(window.api.ui.getZoomLevel()) - }) - }, []) - - const applyZoom = useCallback((level: number) => { - const clamped = Math.max(ZOOM_MIN, Math.min(ZOOM_MAX, level)) - applyUIZoom(clamped) - setZoomLevel(clamped) - window.api.ui.set({ uiZoomLevel: clamped }) - }, []) - - const percent = zoomLevelToPercent(zoomLevel) - - return ( -
- - {percent}% - - -
- ) -} - -function getFallbackTerminalFonts(): string[] { - const nav = - typeof navigator !== 'undefined' - ? (navigator as Navigator & { userAgentData?: { platform?: string } }) - : null - const platform = nav ? (nav.userAgentData?.platform ?? nav.platform ?? '') : '' - const normalizedPlatform = platform.toLowerCase() - - if (normalizedPlatform.includes('mac')) { - return ['SF Mono', 'Menlo', 'Monaco', 'JetBrains Mono', 'Fira Code'] - } - - if (normalizedPlatform.includes('win')) { - return ['Cascadia Mono', 'Consolas', 'Lucida Console', 'JetBrains Mono', 'Fira Code'] - } - - return [ - 'JetBrains Mono', - 'Fira Code', - 'DejaVu Sans Mono', - 'Liberation Mono', - 'Ubuntu Mono', - 'Noto Sans Mono' - ] -} - -type ThemePickerProps = { - label: string - description: string - selectedTheme: string - query: string - onQueryChange: (value: string) => void - onSelectTheme: (theme: string) => void -} - -type ColorFieldProps = { - label: string - description: string - value: string - fallback: string - onChange: (value: string) => void -} - -function ThemePicker({ - label, - description, - selectedTheme, - query, - onQueryChange, - onSelectTheme -}: ThemePickerProps): React.JSX.Element { - const normalizedQuery = query.trim().toLowerCase() - const filteredThemes = BUILTIN_TERMINAL_THEME_NAMES.filter((theme) => - theme.toLowerCase().includes(normalizedQuery) - ).slice(0, MAX_THEME_RESULTS) - - return ( -
-
- -

{description}

-
- onQueryChange(e.target.value)} - placeholder="Search builtin themes" - /> -
-
- Selected: {selectedTheme} - - Showing {filteredThemes.length} - {normalizedQuery - ? ` matching "${query.trim()}"` - : ` of ${BUILTIN_TERMINAL_THEME_NAMES.length}`} - -
- -
- {filteredThemes.map((theme) => ( - - ))} - {filteredThemes.length === 0 ? ( -
No themes found.
- ) : null} -
-
-
-
- ) -} - -function ColorField({ - label, - description, - value, - fallback, - onChange -}: ColorFieldProps): React.JSX.Element { - const normalized = normalizeColor(value, fallback) - - return ( -
-
- -

{description}

-
-
- onChange(e.target.value)} - className="h-9 w-12 rounded-md border border-input bg-transparent p-1" - /> - onChange(e.target.value)} - placeholder={fallback} - className="max-w-xs font-mono text-xs" - /> -
-
- ) -} - -type NumberFieldProps = { - label: string - description: string - value: number - defaultValue?: number - min: number - max: number - step?: number - onChange: (value: number) => void - suffix?: string -} - -type FontAutocompleteProps = { - value: string - suggestions: string[] - onChange: (value: string) => void -} - -function NumberField({ - label, - description, - value, - defaultValue, - min, - max, - step = 1, - onChange, - suffix -}: NumberFieldProps): React.JSX.Element { - return ( -
-
- -

{description}

-
-
- { - const next = Number(e.target.value) - if (!Number.isFinite(next)) return - onChange(next) - }} - className="number-input-clean w-28 tabular-nums" - /> - {suffix ? {suffix} : null} -
-

- Current: {value} - {defaultValue !== undefined ? ` · Default: ${defaultValue}` : ''} -

-
- ) -} - -function FontAutocomplete({ - value, - suggestions, - onChange -}: FontAutocompleteProps): React.JSX.Element { - const [query, setQuery] = useState(value) - const [prevValue, setPrevValue] = useState(value) - const [open, setOpen] = useState(false) - const rootRef = useRef(null) - - if (value !== prevValue) { - setPrevValue(value) - setQuery(value) - } - - useEffect(() => { - if (!open) return - - const handlePointerDown = (event: MouseEvent): void => { - if (!rootRef.current?.contains(event.target as Node)) { - setOpen(false) - } - } - - document.addEventListener('mousedown', handlePointerDown) - return () => document.removeEventListener('mousedown', handlePointerDown) - }, [open]) - - const normalizedQuery = query.trim().toLowerCase() - const filteredSuggestions = useMemo(() => { - const startsWith = suggestions.filter((font) => font.toLowerCase().startsWith(normalizedQuery)) - const includes = suggestions.filter( - (font) => - !font.toLowerCase().startsWith(normalizedQuery) && - font.toLowerCase().includes(normalizedQuery) - ) - const ordered = normalizedQuery ? [...startsWith, ...includes] : suggestions - return ordered.slice(0, MAX_FONT_RESULTS) - }, [suggestions, normalizedQuery]) - - const commitValue = (nextValue: string): void => { - setQuery(nextValue) - onChange(nextValue) - setOpen(false) - } - - return ( -
-
- { - const next = e.target.value - setQuery(next) - onChange(next) - setOpen(true) - }} - onFocus={() => setOpen(true)} - placeholder="SF Mono" - className="pr-18" - /> -
- {query ? ( - - ) : null} - -
-
- - {open ? ( -
- -
- {filteredSuggestions.length > 0 ? ( - filteredSuggestions.map((font) => ( - - )) - ) : ( -
No matching fonts.
- )} -
-
-
- ) : null} -
- ) -} - -function Settings(): React.JSX.Element { - const settings = useAppStore((s) => s.settings) - const updateSettings = useAppStore((s) => s.updateSettings) - const fetchSettings = useAppStore((s) => s.fetchSettings) - const setActiveView = useAppStore((s) => s.setActiveView) - const repos = useAppStore((s) => s.repos) - const updateRepo = useAppStore((s) => s.updateRepo) - const removeRepo = useAppStore((s) => s.removeRepo) - - const [confirmingRemove, setConfirmingRemove] = useState(null) - const [selectedPane, setSelectedPane] = useState<'general' | 'appearance' | 'terminal' | 'repo'>( - 'general' - ) - const [selectedRepoId, setSelectedRepoId] = useState(null) - const [repoHooksMap, setRepoHooksMap] = useState< - Record - >({}) - const [defaultBaseRef, setDefaultBaseRef] = useState('origin/main') - const [baseRefQuery, setBaseRefQuery] = useState('') - const [baseRefResults, setBaseRefResults] = useState([]) - const [isSearchingBaseRefs, setIsSearchingBaseRefs] = useState(false) - const [themeSearchDark, setThemeSearchDark] = useState('') - const [themeSearchLight, setThemeSearchLight] = useState('') - const [systemPrefersDark, setSystemPrefersDark] = useState(getSystemPrefersDark()) - const [scrollbackMode, setScrollbackMode] = useState<'preset' | 'custom'>('preset') - const [prevSettings, setPrevSettings] = useState(settings) - const [terminalFontSuggestions, setTerminalFontSuggestions] = useState( - getFallbackTerminalFonts() - ) - const terminalFontsLoadedRef = useRef(false) - const updateStatus = useAppStore((s) => s.updateStatus) - - useEffect(() => { - fetchSettings() - }, [fetchSettings]) - - useEffect(() => { - const media = window.matchMedia('(prefers-color-scheme: dark)') - const handleChange = (event: MediaQueryListEvent): void => { - setSystemPrefersDark(event.matches) - } - setSystemPrefersDark(media.matches) - media.addEventListener('change', handleChange) - return () => media.removeEventListener('change', handleChange) - }, []) - - useEffect(() => { - if (selectedPane !== 'terminal' || terminalFontsLoadedRef.current) return - - let stale = false - - const loadFontSuggestions = async (): Promise => { - try { - const fonts = await window.api.settings.listFonts() - if (stale || fonts.length === 0) return - terminalFontsLoadedRef.current = true - setTerminalFontSuggestions((prev) => Array.from(new Set([...fonts, ...prev])).slice(0, 320)) - } catch { - // Fall back to curated cross-platform suggestions. - } - } - - void loadFontSuggestions() - - return () => { - stale = true - } - }, [selectedPane]) - - if (settings !== prevSettings) { - setPrevSettings(settings) - if (settings) { - const scrollbackMb = Math.max(1, Math.round(settings.terminalScrollbackBytes / 1_000_000)) - setScrollbackMode( - SCROLLBACK_PRESETS_MB.includes(scrollbackMb as (typeof SCROLLBACK_PRESETS_MB)[number]) - ? 'preset' - : 'custom' - ) - } - } - - useEffect(() => { - let stale = false - const checkHooks = async () => { - const results = await Promise.all( - repos.map(async (repo) => { - try { - const result = await window.api.hooks.check({ repoId: repo.id }) - return [repo.id, result] as const - } catch { - return [repo.id, { hasHooks: false, hooks: null }] as const - } - }) - ) - - if (!stale) { - setRepoHooksMap(Object.fromEntries(results)) - } - } - - if (repos.length > 0) { - checkHooks() - } else { - setRepoHooksMap({}) - } - - return () => { - stale = true - } - }, [repos]) - - useEffect(() => { - let stale = false - - const loadDefaultBaseRef = async (repoId: string) => { - try { - const result = await window.api.repos.getBaseRefDefault({ repoId }) - if (stale) return - setDefaultBaseRef(result) - } catch { - if (stale) return - setDefaultBaseRef('origin/main') - } - } - - if (!selectedRepoId) { - setDefaultBaseRef('origin/main') - setBaseRefQuery('') - setBaseRefResults([]) - } else { - setBaseRefQuery('') - setBaseRefResults([]) - void loadDefaultBaseRef(selectedRepoId) - } - - return () => { - stale = true - } - }, [selectedRepoId]) - - useEffect(() => { - if (!selectedRepoId) return - - const trimmedQuery = baseRefQuery.trim() - if (trimmedQuery.length < 2) { - setBaseRefResults([]) - setIsSearchingBaseRefs(false) - return - } - - let stale = false - setIsSearchingBaseRefs(true) - - const timer = window.setTimeout(() => { - void window.api.repos - .searchBaseRefs({ - repoId: selectedRepoId, - query: trimmedQuery, - limit: 20 - }) - .then((results) => { - if (!stale) { - setBaseRefResults(results) - } - }) - .catch(() => { - if (!stale) { - setBaseRefResults([]) - } - }) - .finally(() => { - if (!stale) { - setIsSearchingBaseRefs(false) - } - }) - }, 200) - - return () => { - stale = true - window.clearTimeout(timer) - } - }, [selectedRepoId, baseRefQuery]) - - // Validate selectedRepoId against current repos (adjusting state during render) - if (repos.length === 0) { - if (selectedRepoId !== null) { - setSelectedRepoId(null) - if (selectedPane === 'repo') setSelectedPane('general') - } - } else if (!selectedRepoId || !repos.some((repo) => repo.id === selectedRepoId)) { - setSelectedRepoId(repos[0].id) - } - - const applyTheme = useCallback((theme: 'system' | 'dark' | 'light') => { - const root = document.documentElement - if (theme === 'dark') { - root.classList.add('dark') - } else if (theme === 'light') { - root.classList.remove('dark') - } else { - const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches - if (prefersDark) { - root.classList.add('dark') - } else { - root.classList.remove('dark') - } - } - }, []) - - const handleBrowseWorkspace = async () => { - const path = await window.api.repos.pickFolder() - if (path) { - updateSettings({ workspaceDir: path }) - } - } - - const handleRemoveRepo = (repoId: string) => { - if (confirmingRemove === repoId) { - removeRepo(repoId) - setConfirmingRemove(null) - return - } - - setConfirmingRemove(repoId) - } - - const selectedRepo = repos.find((repo) => repo.id === selectedRepoId) ?? null - const selectedYamlHooks = selectedRepo ? (repoHooksMap[selectedRepo.id]?.hooks ?? null) : null - const showGeneralPane = selectedPane === 'general' - const showAppearancePane = selectedPane === 'appearance' - const showTerminalPane = selectedPane === 'terminal' - const showRepoPane = selectedPane === 'repo' && !!selectedRepo - const displayedGitUsername = (selectedRepo ?? repos[0])?.gitUsername ?? '' - const effectiveBaseRef = selectedRepo?.worktreeBaseRef ?? defaultBaseRef - - const updateSelectedRepoHookSettings = ( - repo: Repo, - updates: Omit, 'scripts'> & { - scripts?: Partial - } - ) => { - const nextSettings: RepoHookSettings = { - ...DEFAULT_REPO_HOOK_SETTINGS, - ...repo.hookSettings, - ...updates, - scripts: { - ...DEFAULT_REPO_HOOK_SETTINGS.scripts, - ...repo.hookSettings?.scripts, - ...updates.scripts - } - } - - updateRepo(repo.id, { - hookSettings: nextSettings - }) - } - - if (!settings) { - return ( -
- Loading settings... -
- ) - } - - const darkPreviewAppearance = resolveEffectiveTerminalAppearance( - { - ...settings, - theme: 'dark' - }, - systemPrefersDark - ) - const lightPreviewAppearance = resolveEffectiveTerminalAppearance( - { - ...settings, - theme: 'light' - }, - systemPrefersDark - ) - const paneStyleOptions = resolvePaneStyleOptions(settings) - const scrollbackMb = Math.max(1, Math.round(settings.terminalScrollbackBytes / 1_000_000)) - const scrollbackPresetSelection = SCROLLBACK_PRESETS_MB.includes( - scrollbackMb as (typeof SCROLLBACK_PRESETS_MB)[number] - ) - ? `${scrollbackMb}` - : 'custom' - const scrollbackToggleValue = scrollbackMode === 'custom' ? 'custom' : scrollbackPresetSelection - const contentClassName = 'w-full max-w-5xl px-8' - const pageHeader = showGeneralPane ? ( -
-

General

-

Workspace, naming, and updates.

-
- ) : showAppearancePane ? ( -
-

Appearance

-

Theme and UI scaling.

-
- ) : showTerminalPane ? ( -
-

Terminal

-

- Terminal appearance, previews, and defaults for new panes. -

-
- ) : selectedRepo ? ( -
-
- -

{selectedRepo.displayName}

-
-

{selectedRepo.path}

-
- ) : ( -
-

Repository Settings

-

Select a repository to edit its settings.

-
- ) - - return ( -
- - -
-
-
{pageHeader}
-
- - -
- {showGeneralPane ? ( -
-
-
-

Workspace

-

- Configure where new worktrees are created. -

-
- -
- -
- updateSettings({ workspaceDir: e.target.value })} - className="flex-1 font-mono text-xs" - /> - -
-

- Root directory where worktree folders are created. -

-
- -
-
- -

- Create worktrees inside a repo-named subfolder. -

-
- -
-
- - - -
-
-

Branch Naming

-

- Prefix added to branch names when creating worktrees. -

-
- -
- {(['git-username', 'custom', 'none'] as const).map((option) => ( - - ))} -
- {(settings.branchPrefix === 'custom' || - settings.branchPrefix === 'git-username') && ( - updateSettings({ branchPrefixCustom: e.target.value })} - placeholder={ - settings.branchPrefix === 'git-username' - ? 'No git username configured' - : 'e.g. feature' - } - className="max-w-xs" - readOnly={settings.branchPrefix === 'git-username'} - /> - )} -
- - - -
-
-

Updates

-

Check for new versions of Orca.

-
- -
- - - {updateStatus.state === 'downloaded' ? ( - - ) : null} -
- -

- {updateStatus.state === 'idle' && - 'Updates are checked automatically on launch.'} - {updateStatus.state === 'checking' && 'Checking for updates...'} - {updateStatus.state === 'available' && - `Version ${updateStatus.version} is available. Downloading...`} - {updateStatus.state === 'not-available' && 'You\u2019re on the latest version.'} - {updateStatus.state === 'downloading' && - `Downloading update... ${updateStatus.percent}%`} - {updateStatus.state === 'downloaded' && - `Version ${updateStatus.version} is ready to install.`} - {updateStatus.state === 'error' && `Update error: ${updateStatus.message}`} -

-
-
- ) : showAppearancePane ? ( -
-
-
-

Theme

-

- Choose how Orca looks in the app window. -

-
- -
- {(['system', 'dark', 'light'] as const).map((option) => ( - - ))} -
-
- - - -
-
-

UI Zoom

-

- Scale the entire application interface. Use{' '} - ⌘+ /{' '} - ⌘- when not in a - terminal pane. -

-
- - -
-
- ) : showTerminalPane ? ( -
-
-
-

Typography

-

- Default terminal typography for new panes and live updates. -

-
- -
- -
- - { - const value = parseInt(e.target.value, 10) - if (!Number.isNaN(value) && value >= 10 && value <= 24) { - updateSettings({ terminalFontSize: value }) - } - }} - className="w-16 text-center tabular-nums" - /> - - px -
-
- -
- - updateSettings({ terminalFontFamily: value })} - /> -
-
- - - -
-
-

Cursor

-

- Default cursor appearance for Orca terminal panes. -

-
- -
-
- -
- {(['bar', 'block', 'underline'] as const).map((option) => ( - - ))} -
-
- -
-
- -

- Uses the blinking variant of the selected cursor shape. -

-
- -
-
-
- - - -
-
-

Pane Styling

-

- Control inactive pane dimming, divider thickness, and transition timing. -

-
- -
- - updateSettings({ - terminalInactivePaneOpacity: clampNumber(value, 0, 1) - }) - } - /> - - updateSettings({ - terminalDividerThicknessPx: clampNumber(value, 1, 32) - }) - } - /> -
-
- - - -
-
- updateSettings({ terminalThemeDark: theme })} - /> - - updateSettings({ terminalDividerColorDark: value })} - /> -
- - -
- - - -
-
-
- -

- When disabled, light mode reuses the dark terminal theme. -

-
- -
- -
-
-
-
- updateSettings({ terminalThemeLight: theme })} - /> - - - updateSettings({ terminalDividerColorLight: value }) - } - /> -
- - -
-
-
-
- - - -
-
-

Advanced

-

- Scrollback is bounded for stability. This setting applies to new terminal - panes. -

-
- -
- - { - if (!value) return - if (value === 'custom') { - setScrollbackMode('custom') - return - } - - setScrollbackMode('preset') - updateSettings({ - terminalScrollbackBytes: Number(value) * 1_000_000 - }) - }} - variant="outline" - size="sm" - className="h-8 flex-wrap" - > - {SCROLLBACK_PRESETS_MB.map((preset) => ( - - {preset} MB - - ))} - - Custom - - - - {scrollbackMode === 'custom' ? ( - - updateSettings({ - terminalScrollbackBytes: clampNumber(value, 1, 256) * 1_000_000 - }) - } - /> - ) : null} -
-
-
- ) : selectedRepo ? ( -
-
-
-
-

Identity

-

- Repo-specific display details for the sidebar and tabs. -

-
- - -
- -
- - - updateRepo(selectedRepo.id, { - displayName: e.target.value - }) - } - className="h-9 text-sm" - /> -
- -
- -
- {REPO_COLORS.map((color) => ( -
-
- -
- -
-
-
-
- {effectiveBaseRef} -
-

- {selectedRepo.worktreeBaseRef - ? 'Pinned for this repo' - : `Following primary branch (${defaultBaseRef})`} -

-
- -
- -
- setBaseRefQuery(e.target.value)} - placeholder="Search branches by name..." - className="max-w-md" - /> -

Type at least 2 characters.

-
- - {isSearchingBaseRefs ? ( -

Searching branches...

- ) : null} - - {!isSearchingBaseRefs && baseRefQuery.trim().length >= 2 ? ( - baseRefResults.length > 0 ? ( - -
- {baseRefResults.map((ref) => ( - - ))} -
-
- ) : ( -

- No matching branches found. -

- ) - ) : null} -
-

- New worktrees default to the repo primary branch unless you pin a different - base here. -

-
-
- - - -
-
-

Hook Source

-

- Auto prefers `orca.yaml` when present, then falls back to the UI script. - Override ignores YAML and only uses the UI script. -

-
- -
- {(['auto', 'override'] as const).map((mode) => ( - - ))} -
- -
- {selectedYamlHooks ? ( -
-

- YAML hooks detected in `orca.yaml` -

-
- {(['setup', 'archive'] as HookName[]).map((hookName) => - selectedYamlHooks.scripts[hookName] ? ( - - {hookName} - - ) : null - )} -
-
- ) : ( -

No YAML hooks detected for this repo.

- )} -
-
- - - -
-
-

Lifecycle Hooks

-

- Write scripts directly in the UI. Each repo stores its own setup and archive - hook script. -

-
- -
- {(['setup', 'archive'] as HookName[]).map((hookName) => ( - - updateSelectedRepoHookSettings(selectedRepo, { - scripts: hookName === 'setup' ? { setup: script } : { archive: script } - }) - } - /> - ))} -
-
-
- ) : ( -
- Select a repository to edit its settings. -
- )} -
-
-
-
- ) -} - -function HookEditor({ - hookName, - repo, - yamlHooks, - onScriptChange -}: { - hookName: HookName - repo: Repo - yamlHooks: OrcaHooks | null - onScriptChange: (script: string) => void -}): React.JSX.Element { - const uiScript = repo.hookSettings?.scripts[hookName] ?? '' - const yamlScript = yamlHooks?.scripts[hookName] - const effectiveSource = - repo.hookSettings?.mode === 'auto' && yamlScript ? 'yaml' : uiScript.trim() ? 'ui' : 'none' - - return ( -
-
-
-
{hookName}
-

- {hookName === 'setup' - ? 'Runs after a worktree is created.' - : 'Runs before a worktree is archived.'} -

-
- - - {effectiveSource === 'yaml' - ? 'Honoring YAML' - : effectiveSource === 'ui' - ? 'Using UI' - : 'Inactive'} - -
- - {yamlScript && ( -
-
- - Read-only from `orca.yaml` -
-
-            {yamlScript}
-          
-
- )} - -
-
- - - {repo.hookSettings?.mode === 'auto' && yamlScript - ? 'Stored as fallback until you switch to override.' - : 'Editable script stored with this repo.'} - -
-