From 8446d254403596d2ffefa5b75ca1d992afcca5cc Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Wed, 18 Mar 2026 18:01:23 -0700 Subject: [PATCH] state save + settings --- src/main/hooks.ts | 46 +- src/main/index.ts | 2 + src/main/ipc/repos.ts | 5 +- src/main/ipc/session.ts | 13 + src/main/ipc/worktrees.ts | 10 +- src/main/persistence.ts | 49 +- src/preload/index.d.ts | 11 +- src/preload/index.ts | 5 + src/renderer/src/App.tsx | 62 +- src/renderer/src/components/Settings.tsx | 800 ++++++++++++------ src/renderer/src/components/Terminal.tsx | 4 +- src/renderer/src/components/TerminalPane.tsx | 151 +++- .../src/components/sidebar/WorktreeCard.tsx | 2 +- src/renderer/src/store/slices/repos.ts | 7 +- src/renderer/src/store/slices/terminals.ts | 89 +- src/renderer/src/store/slices/worktrees.ts | 5 + src/shared/constants.ts | 30 +- src/shared/types.ts | 38 + 18 files changed, 1029 insertions(+), 300 deletions(-) create mode 100644 src/main/ipc/session.ts diff --git a/src/main/hooks.ts b/src/main/hooks.ts index 5f4b35530f6..554a062c049 100644 --- a/src/main/hooks.ts +++ b/src/main/hooks.ts @@ -1,9 +1,11 @@ import { readFileSync, existsSync } from 'fs' import { join } from 'path' import { exec } from 'child_process' -import type { OrcaHooks } from '../shared/types' +import { getDefaultRepoHookSettings } from '../shared/constants' +import type { OrcaHooks, Repo } from '../shared/types' const HOOK_TIMEOUT = 120_000 // 2 minutes +type HookName = keyof OrcaHooks['scripts'] /** * Parse a simple orca.yaml file. Handles only the `scripts:` block with @@ -39,7 +41,7 @@ function parseOrcaYaml(content: string): OrcaHooks | null { } // Content line (indented by 4+ spaces under a key) - if (currentKey && /^ /.test(line)) { + if (currentKey && line.startsWith(' ')) { currentValue += line.slice(4) + '\n' } } @@ -75,15 +77,45 @@ export function hasHooksFile(repoPath: string): boolean { return existsSync(join(repoPath, 'orca.yaml')) } +export function getEffectiveHooks(repo: Repo): OrcaHooks | null { + const defaults = getDefaultRepoHookSettings() + const yamlHooks = loadHooks(repo.path) + const repoSettings = { + ...defaults, + ...repo.hookSettings, + scripts: { + ...defaults.scripts, + ...repo.hookSettings?.scripts + } + } + + const hooks: OrcaHooks = { scripts: {} } + + for (const hookName of ['setup', 'archive'] as HookName[]) { + const yamlScript = yamlHooks?.scripts[hookName]?.trim() + const uiScript = repoSettings.scripts[hookName].trim() + + const autoScript = yamlScript || uiScript || undefined + const effectiveScript = repoSettings.mode === 'auto' ? autoScript : uiScript || undefined + + if (effectiveScript) { + hooks.scripts[hookName] = effectiveScript + } + } + + if (!hooks.scripts.setup && !hooks.scripts.archive) return null + return hooks +} + /** * Run a named hook script in the given working directory. */ export function runHook( hookName: 'setup' | 'archive', cwd: string, - repoPath: string + repo: Repo ): Promise<{ success: boolean; output: string }> { - const hooks = loadHooks(repoPath) + const hooks = getEffectiveHooks(repo) const script = hooks?.scripts[hookName] if (!script) { @@ -99,11 +131,11 @@ export function runHook( shell: '/bin/bash', env: { ...process.env, - ORCA_ROOT_PATH: repoPath, + ORCA_ROOT_PATH: repo.path, ORCA_WORKTREE_PATH: cwd, // Compat with conductor.json users - CONDUCTOR_ROOT_PATH: repoPath, - GHOSTX_ROOT_PATH: repoPath + CONDUCTOR_ROOT_PATH: repo.path, + GHOSTX_ROOT_PATH: repo.path } }, (error, stdout, stderr) => { diff --git a/src/main/index.ts b/src/main/index.ts index dbc6bff3f94..98a65ebf386 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -11,6 +11,7 @@ import { registerPtyHandlers, killAllPty } from './ipc/pty' import { registerGitHubHandlers } from './ipc/github' import { registerSettingsHandlers } from './ipc/settings' import { registerShellHandlers } from './ipc/shell' +import { registerSessionHandlers } from './ipc/session' let mainWindow: BrowserWindow | null = null let store: Store | null = null @@ -139,6 +140,7 @@ app.whenReady().then(() => { registerGitHubHandlers() registerSettingsHandlers(store) registerShellHandlers() + registerSessionHandlers(store) // macOS re-activate app.on('activate', function () { diff --git a/src/main/ipc/repos.ts b/src/main/ipc/repos.ts index b22888b1535..0545a6b2894 100644 --- a/src/main/ipc/repos.ts +++ b/src/main/ipc/repos.ts @@ -41,7 +41,10 @@ export function registerRepoHandlers(mainWindow: BrowserWindow, store: Store): v 'repos:update', ( _event, - args: { repoId: string; updates: Partial> } + args: { + repoId: string + updates: Partial> + } ) => { const updated = store.updateRepo(args.repoId, args.updates) if (updated) notifyReposChanged(mainWindow) diff --git a/src/main/ipc/session.ts b/src/main/ipc/session.ts new file mode 100644 index 00000000000..99304298701 --- /dev/null +++ b/src/main/ipc/session.ts @@ -0,0 +1,13 @@ +import { ipcMain } from 'electron' +import type { Store } from '../persistence' +import type { WorkspaceSessionState } from '../../shared/types' + +export function registerSessionHandlers(store: Store): void { + ipcMain.handle('session:get', () => { + return store.getWorkspaceSession() + }) + + ipcMain.handle('session:set', (_event, args: WorkspaceSessionState) => { + store.setWorkspaceSession(args) + }) +} diff --git a/src/main/ipc/worktrees.ts b/src/main/ipc/worktrees.ts index 605e5f52e7d..21622789445 100644 --- a/src/main/ipc/worktrees.ts +++ b/src/main/ipc/worktrees.ts @@ -4,7 +4,7 @@ import type { Store } from '../persistence' import type { Worktree, WorktreeMeta } from '../../shared/types' import { listWorktrees, addWorktree, removeWorktree } from '../git/worktree' import { getGitUsername, getDefaultBranch } from '../git/repo' -import { loadHooks, runHook, hasHooksFile } from '../hooks' +import { getEffectiveHooks, loadHooks, runHook, hasHooksFile } from '../hooks' export function registerWorktreeHandlers(mainWindow: BrowserWindow, store: Store): void { ipcMain.handle('worktrees:listAll', async () => { @@ -76,9 +76,9 @@ export function registerWorktreeHandlers(mainWindow: BrowserWindow, store: Store const worktree = mergeWorktree(repo.id, created, undefined) // Run setup hook asynchronously (don't block the UI) - const hooks = loadHooks(repo.path) + const hooks = getEffectiveHooks(repo) if (hooks?.scripts.setup) { - runHook('setup', worktreePath, repo.path).then((result) => { + runHook('setup', worktreePath, repo).then((result) => { if (!result.success) { console.error(`[hooks] setup hook failed for ${worktreePath}:`, result.output) } @@ -98,9 +98,9 @@ export function registerWorktreeHandlers(mainWindow: BrowserWindow, store: Store if (!repo) throw new Error(`Repo not found: ${repoId}`) // Run archive hook before removal - const hooks = loadHooks(repo.path) + const hooks = getEffectiveHooks(repo) if (hooks?.scripts.archive) { - const result = await runHook('archive', worktreePath, repo.path) + const result = await runHook('archive', worktreePath, repo) if (!result.success) { console.error(`[hooks] archive hook failed for ${worktreePath}:`, result.output) } diff --git a/src/main/persistence.ts b/src/main/persistence.ts index 834c0de5472..464f8f8e1c3 100644 --- a/src/main/persistence.ts +++ b/src/main/persistence.ts @@ -3,7 +3,11 @@ import { readFileSync, writeFileSync, mkdirSync, existsSync, renameSync } from ' import { join, dirname } from 'path' import { homedir } from 'os' import type { PersistedState, Repo, WorktreeMeta, GlobalSettings } from '../shared/types' -import { getDefaultPersistedState } from '../shared/constants' +import { + getDefaultPersistedState, + getDefaultRepoHookSettings, + getDefaultWorkspaceSession +} from '../shared/constants' const DATA_FILE = join(app.getPath('userData'), 'orca-data.json') @@ -26,7 +30,8 @@ export class Store { ...defaults, ...parsed, settings: { ...defaults.settings, ...parsed.settings }, - ui: { ...defaults.ui, ...parsed.ui } + ui: { ...defaults.ui, ...parsed.ui }, + workspaceSession: { ...defaults.workspaceSession, ...parsed.workspaceSession } } } } catch (err) { @@ -54,15 +59,17 @@ export class Store { // ── Repos ────────────────────────────────────────────────────────── getRepos(): Repo[] { - return this.state.repos + return this.state.repos.map((repo) => this.hydrateRepo(repo)) } getRepo(id: string): Repo | undefined { - return this.state.repos.find((r) => r.id === id) + const repo = this.state.repos.find((r) => r.id === id) + if (!repo) return undefined + return this.hydrateRepo(repo) } addRepo(repo: Repo): void { - this.state.repos.push(repo) + this.state.repos.push(this.hydrateRepo(repo)) this.scheduleSave() } @@ -78,12 +85,29 @@ export class Store { this.scheduleSave() } - updateRepo(id: string, updates: Partial>): Repo | null { + updateRepo( + id: string, + updates: Partial> + ): Repo | null { const repo = this.state.repos.find((r) => r.id === id) if (!repo) return null Object.assign(repo, updates) this.scheduleSave() - return repo + return this.hydrateRepo(repo) + } + + private hydrateRepo(repo: Repo): Repo { + return { + ...repo, + hookSettings: { + ...getDefaultRepoHookSettings(), + ...repo.hookSettings, + scripts: { + ...getDefaultRepoHookSettings().scripts, + ...repo.hookSettings?.scripts + } + } + } } // ── Worktree Meta ────────────────────────────────────────────────── @@ -143,6 +167,17 @@ export class Store { this.scheduleSave() } + // ── Workspace Session ───────────────────────────────────────────── + + getWorkspaceSession(): PersistedState['workspaceSession'] { + return this.state.workspaceSession ?? getDefaultWorkspaceSession() + } + + setWorkspaceSession(session: PersistedState['workspaceSession']): void { + this.state.workspaceSession = session + this.scheduleSave() + } + // ── Flush (for shutdown) ─────────────────────────────────────────── flush(): void { diff --git a/src/preload/index.d.ts b/src/preload/index.d.ts index 2fdeeb583b0..dad885d8900 100644 --- a/src/preload/index.d.ts +++ b/src/preload/index.d.ts @@ -6,7 +6,8 @@ import type { PRInfo, IssueInfo, GlobalSettings, - OrcaHooks + OrcaHooks, + WorkspaceSessionState } from '../../shared/types' interface ReposApi { @@ -15,7 +16,7 @@ interface ReposApi { remove: (args: { repoId: string }) => Promise update: (args: { repoId: string - updates: Partial> + updates: Partial> }) => Promise pickFolder: () => Promise onChanged: (callback: () => void) => () => void @@ -72,6 +73,11 @@ interface CacheApi { }) => Promise } +interface SessionApi { + get: () => Promise + set: (args: WorkspaceSessionState) => Promise +} + interface Api { repos: ReposApi worktrees: WorktreesApi @@ -81,6 +87,7 @@ interface Api { shell: ShellApi hooks: HooksApi cache: CacheApi + session: SessionApi } declare global { diff --git a/src/preload/index.ts b/src/preload/index.ts index bc4c7122054..2edd96776cc 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -108,6 +108,11 @@ const api = { cache: { getGitHub: () => ipcRenderer.invoke('cache:getGitHub'), setGitHub: (args: { cache: unknown }) => ipcRenderer.invoke('cache:setGitHub', args) + }, + + session: { + get: (): Promise => ipcRenderer.invoke('session:get'), + set: (args: unknown): Promise => ipcRenderer.invoke('session:set', args) } } diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 4689fb3f832..8342aee9bda 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -12,13 +12,18 @@ function App(): React.JSX.Element { const toggleSidebar = useAppStore((s) => s.toggleSidebar) const activeView = useAppStore((s) => s.activeView) const activeWorktreeId = useAppStore((s) => s.activeWorktreeId) + const activeRepoId = useAppStore((s) => s.activeRepoId) const tabsByWorktree = useAppStore((s) => s.tabsByWorktree) const activeTabId = useAppStore((s) => s.activeTabId) const expandedPaneByTabId = useAppStore((s) => s.expandedPaneByTabId) const canExpandPaneByTabId = useAppStore((s) => s.canExpandPaneByTabId) + const terminalLayoutsByTabId = useAppStore((s) => s.terminalLayoutsByTabId) + const workspaceSessionReady = useAppStore((s) => s.workspaceSessionReady) const fetchRepos = useAppStore((s) => s.fetchRepos) + const fetchAllWorktrees = useAppStore((s) => s.fetchAllWorktrees) const fetchSettings = useAppStore((s) => s.fetchSettings) const initGitHubCache = useAppStore((s) => s.initGitHubCache) + const hydrateWorkspaceSession = useAppStore((s) => s.hydrateWorkspaceSession) const openModal = useAppStore((s) => s.openModal) const repos = useAppStore((s) => s.repos) @@ -29,10 +34,59 @@ function App(): React.JSX.Element { // Fetch initial data + hydrate GitHub cache from disk useEffect(() => { - fetchRepos() - fetchSettings() - initGitHubCache() - }, [fetchRepos, fetchSettings, initGitHubCache]) + let cancelled = false + + void (async () => { + try { + await fetchRepos() + await fetchAllWorktrees() + const session = await window.api.session.get() + if (!cancelled) { + hydrateWorkspaceSession(session) + } + } catch (error) { + console.error('Failed to hydrate workspace session:', error) + if (!cancelled) { + hydrateWorkspaceSession({ + activeRepoId: null, + activeWorktreeId: null, + activeTabId: null, + tabsByWorktree: {}, + terminalLayoutsByTabId: {} + }) + } + } + void fetchSettings() + void initGitHubCache() + })() + + return () => { + cancelled = true + } + }, [fetchRepos, fetchAllWorktrees, fetchSettings, initGitHubCache, hydrateWorkspaceSession]) + + useEffect(() => { + if (!workspaceSessionReady) return + + const timer = window.setTimeout(() => { + void window.api.session.set({ + activeRepoId, + activeWorktreeId, + activeTabId, + tabsByWorktree, + terminalLayoutsByTabId + }) + }, 150) + + return () => window.clearTimeout(timer) + }, [ + workspaceSessionReady, + activeRepoId, + activeWorktreeId, + activeTabId, + tabsByWorktree, + terminalLayoutsByTabId + ]) // Apply theme to document useEffect(() => { diff --git a/src/renderer/src/components/Settings.tsx b/src/renderer/src/components/Settings.tsx index 62ff805cbeb..dee6db8e97c 100644 --- a/src/renderer/src/components/Settings.tsx +++ b/src/renderer/src/components/Settings.tsx @@ -1,6 +1,7 @@ import { useEffect, useState, useCallback } from 'react' +import type { OrcaHooks, Repo, RepoHookSettings } from '../../../shared/types' +import { REPO_COLORS, getDefaultRepoHookSettings } from '../../../shared/constants' import { useAppStore } from '../store' -import { REPO_COLORS } from '../../../shared/constants' import { ScrollArea } from './ui/scroll-area' import { Button } from './ui/button' import { Input } from './ui/input' @@ -8,6 +9,9 @@ import { Label } from './ui/label' import { Separator } from './ui/separator' import { ArrowLeft, FolderOpen, Minus, Plus, Trash2 } from 'lucide-react' +type HookName = keyof OrcaHooks['scripts'] +const DEFAULT_REPO_HOOK_SETTINGS = getDefaultRepoHookSettings() + function Settings(): React.JSX.Element { const settings = useAppStore((s) => s.settings) const updateSettings = useAppStore((s) => s.updateSettings) @@ -18,13 +22,16 @@ function Settings(): React.JSX.Element { const removeRepo = useAppStore((s) => s.removeRepo) const [confirmingRemove, setConfirmingRemove] = useState(null) - const [repoHooksMap, setRepoHooksMap] = useState>({}) + const [selectedPane, setSelectedPane] = useState<'general' | 'repo'>('general') + const [selectedRepoId, setSelectedRepoId] = useState(null) + const [repoHooksMap, setRepoHooksMap] = useState< + Record + >({}) useEffect(() => { fetchSettings() }, [fetchSettings]) - // Check which repos have orca.yaml hooks useEffect(() => { let stale = false const checkHooks = async () => { @@ -32,23 +39,41 @@ function Settings(): React.JSX.Element { repos.map(async (repo) => { try { const result = await window.api.hooks.check({ repoId: repo.id }) - return [repo.id, result.hasHooks] as const + return [repo.id, result] as const } catch { - return [repo.id, false] as const + return [repo.id, { hasHooks: false, hooks: null }] as const } }) ) + if (!stale) { setRepoHooksMap(Object.fromEntries(results)) } } - if (repos.length > 0) checkHooks() + + if (repos.length > 0) { + checkHooks() + } else { + setRepoHooksMap({}) + } + return () => { stale = true } }, [repos]) - // Apply theme immediately + useEffect(() => { + if (repos.length === 0) { + setSelectedRepoId(null) + setSelectedPane('general') + return + } + + if (!selectedRepoId || !repos.some((repo) => repo.id === selectedRepoId)) { + setSelectedRepoId(repos[0].id) + } + }, [repos, selectedRepoId]) + const applyTheme = useCallback((theme: 'system' | 'dark' | 'light') => { const root = document.documentElement if (theme === 'dark') { @@ -56,7 +81,6 @@ function Settings(): React.JSX.Element { } else if (theme === 'light') { root.classList.remove('dark') } else { - // system const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches if (prefersDark) { root.classList.add('dark') @@ -77,304 +101,552 @@ function Settings(): React.JSX.Element { if (confirmingRemove === repoId) { removeRepo(repoId) setConfirmingRemove(null) - } else { - setConfirmingRemove(repoId) + 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' || !selectedRepo + + 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...
) } return ( -
- {/* Header */} -
- -

Settings

-
+
+ - + +
+ {showGeneralPane ? ( +
+
+

General

+

+ Workspace, naming, appearance, and terminal defaults. +

+
- {/* ── Appearance ───────────────────────────────────── */} -
-

Appearance

+
+
+
+

Workspace

+

+ Configure where new worktrees are created. +

+
- {/* Theme */} -
- -
- {(['system', 'dark', 'light'] as const).map((option) => ( - - ))} +
+ +
+ 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' && ( + updateSettings({ branchPrefixCustom: e.target.value })} + placeholder="e.g. feature" + className="max-w-xs" + /> + )} +
+ + + +
+
+

Appearance

+

+ Choose how Orca looks in the app window. +

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

Terminal

+

+ Default terminal typography for new panes. +

+
+ +
+ +
+ + { + 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: e.target.value })} + placeholder="SF Mono" + className="max-w-xs" + /> +
+
- - - - - {/* ── Terminal ─────────────────────────────────────── */} -
-

Terminal

- - {/* Font Size */} -
- -
- - { - const val = parseInt(e.target.value, 10) - if (!isNaN(val) && val >= 10 && val <= 24) { - updateSettings({ terminalFontSize: val }) - } - }} - className="w-16 text-center tabular-nums" - /> - - px + ) : selectedRepo ? ( +
+
+
+ +

{selectedRepo.displayName}

+
+

{selectedRepo.path}

-
- {/* Font Family */} -
- - updateSettings({ terminalFontFamily: e.target.value })} - placeholder="SF Mono" - className="max-w-xs" - /> -
-
+
+
+
+
+
+

Identity

+

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

+
- + +
- {/* ── Repos ────────────────────────────────────────── */} -
-

Repositories

-

- Manage display names and badge colors for your repositories. -

+
+ + + updateRepo(selectedRepo.id, { displayName: e.target.value }) + } + className="h-9 text-sm" + /> +
- {repos.length === 0 ? ( -

No repositories added yet.

- ) : ( -
- {repos.map((repo) => ( -
- {/* Color picker */} -
- {REPO_COLORS.map((color) => ( +
+ +
+ {REPO_COLORS.map((color) => ( +
+
+
+ +
+
+

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) => ( ))}
- {/* Display name */} - updateRepo(repo.id, { displayName: e.target.value })} - className="flex-1 h-8 text-sm" - /> +
+ {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.

+ )} +
+
+
- {/* Hooks indicator */} - {repoHooksMap[repo.id] && ( - - hooks - - )} - - {/* Remove */} - +
+
+

Lifecycle Hooks

+

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

- ))} -
- )} - - {/* Bottom spacing */} -
+
+ {(['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.'} + +
+