import {
	type Component,
	createEffect,
	createMemo,
	createSignal,
	lazy,
	on,
	onCleanup,
	onMount,
	Show,
	Suspense,
	untrack,
} from "solid-js";
import { KnowledgeHistoryOverlay } from "./components/KnowledgeHistory/KnowledgeHistoryOverlay";
import { PanelOrchestrator } from "./components/PanelOrchestrator";
import { PromptDrawer } from "./components/PromptDrawer";
import { PromptOverlay } from "./components/PromptOverlay";
import type { SettingsContext } from "./components/SettingsPanel";
import { Sidebar } from "./components/Sidebar";
import { StatusBar } from "./components/StatusBar";
import { TabBar } from "./components/TabBar";
import { TerminalArea } from "./components/TerminalArea";
import { Toolbar } from "./components/Toolbar";
import { editorTabsStore } from "./stores/editorTabs";

const SettingsPanel = lazy(() => import("./components/SettingsPanel").then((m) => ({ default: m.SettingsPanel })));

import releaseNotes from "./assets/release-notes.json";
import { ConfirmDialog } from "./components/ConfirmDialog";
import { ContextMenu, type ContextMenuItem, createContextMenu } from "./components/ContextMenu";
import { CreateBranchDialog } from "./components/CreateBranchDialog";
import { CreateWorktreeDialog } from "./components/CreateWorktreeDialog";
import { GeneratorsModal } from "./components/GeneratorsModal";
import {
	type CleanupStep,
	PostMergeCleanupDialog,
	type StepId,
	type StepStatus,
} from "./components/PostMergeCleanupDialog/PostMergeCleanupDialog";
import { ProcessManagerModal } from "./components/ProcessManagerModal/ProcessManagerModal";
import { PromptDialog } from "./components/PromptDialog";
import { RenameBranchDialog } from "./components/RenameBranchDialog";
import { RunCommandDialog } from "./components/RunCommandDialog";
import { TaskQueuePanel } from "./components/TaskQueuePanel";
import { getCompletionSuppression } from "./components/Terminal/completionDecision";
import { UpdateProgressDialog } from "./components/UpdateProgressDialog";
import { WhatsNewDialog } from "./components/WhatsNewDialog/WhatsNewDialog";
import { executeCleanup } from "./hooks/usePostMergeCleanup";

const HelpPanel = lazy(() => import("./components/HelpPanel").then((m) => ({ default: m.HelpPanel })));

import { BranchSwitcher } from "./components/BranchSwitcher/BranchSwitcher";
import { CommandPalette } from "./components/CommandPalette";

const ActivityDashboard = lazy(() =>
	import("./components/ActivityDashboard").then((m) => ({ default: m.ActivityDashboard })),
);

const TunnelsPanel = lazy(() => import("./components/TunnelsPanel").then((m) => ({ default: m.TunnelsPanel })));

import { getVersion } from "@tauri-apps/api/app";
import { WebviewWindow } from "@tauri-apps/api/webviewWindow";
import { getCurrentWindow } from "@tauri-apps/api/window";
import { open as openDialog, save as saveDialog } from "@tauri-apps/plugin-dialog";
import { getActionEntries } from "./actions/actionRegistry";
import { AGENTS, type AgentType } from "./agents";
import { AIChatPanel } from "./components/AIChatPanel/AIChatPanel";
import { registerAiChatContextActions } from "./components/AIChatPanel/contextMenuActions";
import { DictationToast } from "./components/DictationToast/DictationToast";
import { ErrorLogPanel } from "./components/ErrorLogPanel";
import { McpPopup } from "./components/McpPopup/McpPopup";
import { MobileViewBanner } from "./components/MobileViewBanner";
import qd from "./components/QuitDialog/QuitDialog.module.css";
import { ToastContainer } from "./components/ToastContainer/ToastContainer";
import { getAgentIconSvg } from "./components/ui/AgentIcon";
import { type WorktreeActions, WorktreeManager } from "./components/WorktreeManager";
import { initDeepLinkHandler } from "./deep-link-handler";
import { useAgentDetection } from "./hooks/useAgentDetection";
import { useAgentPolling } from "./hooks/useAgentPolling";
import { initApp } from "./hooks/useAppInit";
import { useAutoDeleteBranch } from "./hooks/useAutoDeleteBranch";
import { startAutoFetch } from "./hooks/useAutoFetch";
import { useCiHeal } from "./hooks/useCiHeal";
import { useConfirmDialog } from "./hooks/useConfirmDialog";
import { useDictation } from "./hooks/useDictation";
import { useFocusRestore } from "./hooks/useFocusRestore";
import { useFocusTracker } from "./hooks/useFocusTracker";
import { useGitOperations } from "./hooks/useGitOperations";
import { useKeyboardRedirect } from "./hooks/useKeyboardRedirect";
import { useKeyboardShortcuts } from "./hooks/useKeyboardShortcuts";
import { createLongPressHandlerFromHotkey } from "./hooks/useLongPressHotkey";
import { usePty } from "./hooks/usePty";
import { useQuickSwitcher } from "./hooks/useQuickSwitcher";
import { markTccAlertShown, tccDeniedPaths, useRepository } from "./hooks/useRepository";
import { useSmartPrompts } from "./hooks/useSmartPrompts";
import { useSplitPanes } from "./hooks/useSplitPanes";
import { useTerminalLifecycle } from "./hooks/useTerminalLifecycle";
import { useWorktreeSwitchPrompt } from "./hooks/useWorktreeSwitchPrompt";
import { invoke, listen } from "./invoke";
import { setLastMenuActionTime } from "./menuDedup";
import { activityPanelAdapter } from "./panelAdapters/activity";
import { fileBrowserPanelAdapter } from "./panelAdapters/fileBrowser";
import { gitPanelAdapter } from "./panelAdapters/git";
import { markdownPanelAdapter } from "./panelAdapters/markdown";
import { notesPanelAdapter } from "./panelAdapters/notes";
import { outlinePanelAdapter } from "./panelAdapters/outline";
import { detachPanel, panelRegistry, registerPanel, renderPanelMode, togglePanel } from "./panelRouter";
import { applyPlatformClass, getModifierSymbol, isQuickSwitcherActive, isQuickSwitcherRelease } from "./platform";
import { initPlugins } from "./plugins";
import { pluginRegistry } from "./plugins/pluginRegistry";
import { activityDashboardStore } from "./stores/activityDashboard";
import { activityStore } from "./stores/activityStore";
import { agentConfigsStore } from "./stores/agentConfigs";
import { aiTriageStore } from "./stores/aiTriageStore";
import { appLogger } from "./stores/appLogger";
import { branchSwitcherStore } from "./stores/branchSwitcher";
import { commandPaletteStore } from "./stores/commandPalette";
import { contextMenuActionsStore } from "./stores/contextMenuActionsStore";
import { conversationStore } from "./stores/conversationStore";
import { dictationStore } from "./stores/dictation";
import { diffTabsStore } from "./stores/diffTabs";
import { errorLogStore } from "./stores/errorLog";
import { githubStore } from "./stores/github";
import { globalWorkspaceStore } from "./stores/globalWorkspace";
import { keybindingsStore } from "./stores/keybindings";
import { mcpPopupStore } from "./stores/mcpPopup";
import { mdTabsStore } from "./stores/mdTabs";
import { notesStore } from "./stores/notes";
import { notificationsStore } from "./stores/notifications";
import { paneLayoutStore } from "./stores/paneLayout";
import { pluginStore } from "./stores/pluginStore";
import { prNotificationsStore } from "./stores/prNotifications";
import { promptLibraryStore } from "./stores/promptLibrary";
import { providerRegistryStore } from "./stores/providerRegistry";
import { registryStore } from "./stores/registryStore";
import { repoDefaultsStore } from "./stores/repoDefaults";
import { repoSettingsStore } from "./stores/repoSettings";
import { repositoriesStore } from "./stores/repositories";
import { paneLayoutKey } from "./stores/savedPaneLayouts";
import { settingsStore } from "./stores/settings";
import { tasksStore } from "./stores/tasks";
import { terminalsStore } from "./stores/terminals";
import { toastsStore } from "./stores/toasts";
import { tunnelPanelStore } from "./stores/tunnelPanel";
import { uiStore } from "./stores/ui";
import { updaterStore } from "./stores/updater";
import { userActivityStore } from "./stores/userActivity";
import { handleWatcherFire, type WatcherFirePayload, watcherFireDeps } from "./stores/watcherFire";
import { worktreeManagerStore } from "./stores/worktreeManager";
import { applyAppTheme, applyFontFamily, themesLoaded } from "./themes";
import { isTauri } from "./transport";
import { buildAgentLaunchCommand } from "./utils/agentSession";
import { openFileAction } from "./utils/filePreview";
import { keyFor } from "./utils/hotkey";
import { navigateToTerminal } from "./utils/navigateToTerminal";
import { nextWaitingTerminal } from "./utils/nextWaitingTerminal";
import { handleOpenUrl } from "./utils/openUrl";
import { createPanelSyncProvider, type PanelAction } from "./utils/panelSync";
import { initPaneTabAssignment } from "./utils/paneTabAssign";
import { pathBasename, pathStartsWith, pathStripPrefix } from "./utils/pathUtils";
import { getShellFamily, sendCommand } from "./utils/sendCommand";
import { switchToTerminalBySession } from "./utils/switchToTerminalBySession";

const getDefaultFontSize = () => settingsStore.state.defaultFontSize;
const getMaxTabNameLength = () => settingsStore.state.maxTabNameLength;

/** Detect secondary window mode via URL query param */
const isSecondaryWindow = () => new URLSearchParams(window.location.search).get("mode") === "secondary";

registerPanel({
	id: "ai-chat",
	title: "AI Chat",
	defaultSize: { width: 500, height: 700 },
	toggle: () => uiStore.toggleAiChatPanel(),
	detachParams: () => ({ chatId: conversationStore.chatId() }),
	Component: (props: { params: URLSearchParams }) => {
		const chatId = props.params.get("chatId");
		if (chatId) conversationStore.setChatId(chatId);
		return <AIChatPanel visible={true} onClose={() => window.close()} />;
	},
});

registerPanel(activityPanelAdapter);
registerPanel(gitPanelAdapter);
registerPanel(fileBrowserPanelAdapter);
registerPanel(markdownPanelAdapter);
registerPanel(notesPanelAdapter);
registerPanel(outlinePanelAdapter);

const App: Component = () => {
	// Detached panel mode: full-viewport single panel.
	// Must be checked FIRST — before any createEffect/onMount registrations —
	// otherwise panel windows run initApp() (re-adopts PTY sessions → "Terminal 1"
	// flashing), sync providers, keybindings, and every other main-window effect.
	const panelEl = renderPanelMode();
	if (panelEl) return panelEl;

	const [statusInfo, _setStatusInfoRaw] = createSignal("Ready");
	let statusInfoTimer: ReturnType<typeof setTimeout> | null = null;
	const setStatusInfo = (text: string) => {
		if (statusInfoTimer) clearTimeout(statusInfoTimer);
		_setStatusInfoRaw(text);
		if (text !== "Ready" && text !== "") {
			statusInfoTimer = setTimeout(() => _setStatusInfoRaw("Ready"), 30_000);
		}
	};
	// Production: used by CanvasTerminal for status feedback
	(window as unknown as Record<string, unknown>).__tuic_setStatusInfo = setStatusInfo;
	if (import.meta.env.DEV) {
		(window as unknown as Record<string, unknown>).__debug = {
			// Stores
			terminals: terminalsStore,
			repositories: repositoriesStore,
			settings: settingsStore,
			github: githubStore,
			prompts: promptLibraryStore,
			agentConfigs: agentConfigsStore,
			plugins: pluginStore,
			ui: uiStore,
			notifications: notificationsStore,
			activity: activityStore,
			activityDashboard: activityDashboardStore,
			repoSettings: repoSettingsStore,
			repoDefaults: repoDefaultsStore,
			diffTabs: diffTabsStore,
			mdTabs: mdTabsStore,
			editorTabs: editorTabsStore,
			notes: notesStore,
			keybindings: keybindingsStore,
			prNotifications: prNotificationsStore,
			updater: updaterStore,
			tasks: tasksStore,
			showWhatsNew: (v: string) => setWhatsNewVersion(v),
			dictation: dictationStore,
			userActivity: userActivityStore,
			contextMenuActions: contextMenuActionsStore,
			worktreeManager: worktreeManagerStore,
			errorLog: errorLogStore,
			logger: appLogger,
			// Tauri bridge
			invoke,
			listen,
			isTauri,
		};
	}
	const [settingsPanelVisible, setSettingsPanelVisible] = createSignal(false);
	const [settingsInitialTab, setSettingsInitialTab] = createSignal<string | undefined>(undefined);
	const [settingsContext, setSettingsContext] = createSignal<SettingsContext>({ kind: "global" });

	const openSettings = (tab?: string) => {
		setSettingsContext({ kind: "global" });
		setSettingsInitialTab(tab);
		setSettingsPanelVisible(true);
	};
	const [taskQueueVisible, setTaskQueueVisible] = createSignal(false);

	// Help panel state (Story 053)
	const [helpPanelVisible, setHelpPanelVisible] = createSignal(false);

	// Quit confirmation state (Story 057)
	const [quitDialogVisible, setQuitDialogVisible] = createSignal(false);

	// Quick switcher state
	const [quickSwitcherVisible, setQuickSwitcherVisible] = createSignal(false);

	// Rename branch dialog state
	const [renameBranchDialogVisible, setRenameBranchDialogVisible] = createSignal(false);
	// Create branch dialog state
	const [createBranchDialogVisible, setCreateBranchDialogVisible] = createSignal(false);

	// Run command dialog state
	const [runCommandDialogVisible, setRunCommandDialogVisible] = createSignal(false);

	// Background git operations tracking (for button loading states)
	const [runningGitOps, setRunningGitOps] = createSignal<Set<string>>(new Set());

	// Terminal rename prompt state
	const [termRenamePromptVisible, setTermRenamePromptVisible] = createSignal(false);
	const [termRenameDefault, setTermRenameDefault] = createSignal("");
	const [repoPathPromptVisible, setRepoPathPromptVisible] = createSignal(false);
	let repoPathPromptResolve: ((value: string | null) => void) | null = null;

	/** Show an in-app text-input dialog for repo path (browser mode only) */
	const promptRepoPath = (): Promise<string | null> =>
		new Promise((resolve) => {
			repoPathPromptResolve = resolve;
			setRepoPathPromptVisible(true);
		});

	// Arbitrary path prompt — powers "Open Path…" to route files to the viewer/editor
	// and folders into the file browser.
	const [openPathPromptVisible, setOpenPathPromptVisible] = createSignal(false);
	let openPathPromptResolve: ((value: string | null) => void) | null = null;
	const promptOpenPath = (): Promise<string | null> =>
		new Promise((resolve) => {
			openPathPromptResolve = resolve;
			setOpenPathPromptVisible(true);
		});

	// Context menu state
	const contextMenu = createContextMenu();

	const pty = usePty();
	const repo = useRepository();
	const dialogs = useConfirmDialog();

	const [showProcessManager, setShowProcessManager] = createSignal(false);
	const [showGenerators, setShowGenerators] = createSignal(false);
	const [whatsNewVersion, setWhatsNewVersion] = createSignal<string | null>(null);
	const whatsNewEntry = () => {
		const v = whatsNewVersion();
		if (!v) return null;
		return (
			(releaseNotes as Record<string, { highlights: string[]; contributions?: { text: string; author: string }[] }>)[
				v
			] ?? null
		);
	};

	// Drop on folder: when source is a directory, we pause to ask for confirmation.
	const [pendingFolderDrop, setPendingFolderDrop] = createSignal<
		import("./hooks/useFileDrop").FolderDropRequest | null
	>(null);
	onMount(() => {
		import("./hooks/useFileDrop").then(({ setFolderDropConfirmHandler }) => {
			setFolderDropConfirmHandler((req) => setPendingFolderDrop(req));
		});
	});

	// Redirect keyboard input from sidebar to terminal
	useKeyboardRedirect(true);

	// Centralized focus restore: when any dialog/overlay closes (DOM removed),
	// focus returns to the active terminal automatically.
	useFocusRestore();

	// Remember last-focused element per-repo + globally, and restore it on
	// repo switch / window focus regain.
	useFocusTracker();

	const terminalLifecycle = useTerminalLifecycle({
		pty,
		dialogs,
		setStatusInfo,
		getDefaultFontSize,
	});

	const gitOps = useGitOperations({
		repo,
		pty,
		dialogs: {
			...dialogs,
			promptRepoPath,
			confirmOrphanCleanup: dialogs.confirmOrphanCleanup,
			confirmRemoveLockedWorktree: dialogs.confirmRemoveLockedWorktree,
		},
		closeTerminal: terminalLifecycle.closeTerminal,
		createNewTerminal: terminalLifecycle.createNewTerminal,
		setStatusInfo,
		getDefaultFontSize,
		getMaxTabNameLength,
		getPromptOnCreate: (repoPath: string) => {
			const effective = repoSettingsStore.getEffective(repoPath);
			return effective?.promptOnCreate ?? repoDefaultsStore.state.promptOnCreate;
		},
	});

	// ── Post-merge worktree cleanup dialog state ──
	const [worktreeCleanupExecuting, setWorktreeCleanupExecuting] = createSignal(false);
	const [worktreeCleanupStepStatuses, setWorktreeCleanupStepStatuses] = createSignal<
		Partial<Record<StepId, StepStatus>>
	>({});
	const [worktreeCleanupStepErrors, setWorktreeCleanupStepErrors] = createSignal<Partial<Record<StepId, string>>>({});
	const [worktreeCleanupStepNotes, setWorktreeCleanupStepNotes] = createSignal<Partial<Record<StepId, string>>>({});
	const [worktreeCleanupAction, setWorktreeCleanupAction] = createSignal<"archive" | "delete">("archive");

	const handleWorktreeCleanupExecute = async (steps: CleanupStep[], options?: { unstash?: boolean }) => {
		const ctx = gitOps.mergePendingCtx();
		if (!ctx) return;
		setWorktreeCleanupExecuting(true);
		setWorktreeCleanupStepStatuses({});
		setWorktreeCleanupStepErrors({});
		setWorktreeCleanupStepNotes({});
		await executeCleanup({
			repoPath: ctx.repoPath,
			branchName: ctx.branchName,
			baseBranch: ctx.baseBranch,
			steps: steps.map((s) => ({ id: s.id, checked: s.checked })),
			worktreeAction: worktreeCleanupAction(),
			unstash: options?.unstash,
			onStepStart: (id) => setWorktreeCleanupStepStatuses((prev) => ({ ...prev, [id]: "running" as StepStatus })),
			onStepDone: (id, result, error) => {
				setWorktreeCleanupStepStatuses((prev) => ({ ...prev, [id]: result as StepStatus }));
				if (error) setWorktreeCleanupStepErrors((prev) => ({ ...prev, [id]: error }));
			},
			onStepNote: (id, note) => setWorktreeCleanupStepNotes((prev) => ({ ...prev, [id]: note })),
			closeTerminalsForBranch: gitOps.closeTerminalsForBranch,
		});
		// Brief delay so user sees final statuses
		setTimeout(() => {
			setWorktreeCleanupExecuting(false);
			gitOps.dismissMergePending();
		}, 600);
	};

	const handleWorktreeCleanupSkip = () => {
		gitOps.dismissMergePending();
	};

	const dictation = useDictation({
		pty,
		dictation: dictationStore,
		setStatusInfo,
		openSettings,
	});

	const quickSwitcher = useQuickSwitcher({
		handleBranchSelect: gitOps.handleBranchSelect,
	});

	const splitPanes = useSplitPanes();

	// Register pane tab auto-assignment hook (must be after store imports)
	initPaneTabAssignment();

	// Poll active terminal for foreground agent detection
	useAgentPolling();

	// Agent detection for context menu
	const agentDetection = useAgentDetection();
	const smartPrompts = useSmartPrompts();

	// Show a one-time dialog when macOS TCC denies access to repo directories
	createEffect(() => {
		const paths = tccDeniedPaths();
		if (paths.length === 0) return;
		markTccAlertShown();
		const repos = paths.map((p) => pathBasename(p) || p).join(", ");
		void dialogs.confirm({
			title: "Permission denied",
			message: `macOS blocked access to: ${repos}\n\nRepositories inside ~/Documents, ~/Desktop, or ~/Downloads require Full Disk Access.\n\nTo fix: System Settings → Privacy & Security → Full Disk Access → add TUICommander.\n\nAlternatively, move your repositories to a non-protected folder (e.g. ~/Repositories).`,
			okLabel: "Got it",
			cancelLabel: "Dismiss",
			kind: "error",
		});
	});

	// Auto-delete local branches when their PR is merged/closed
	useAutoDeleteBranch({ confirm: (opts) => dialogs.confirm(opts) });

	// Offer to switch to newly created worktrees (from MCP) + activity notification
	useWorktreeSwitchPrompt({
		confirm: (opts) => dialogs.confirm(opts),
		handleBranchSelect: gitOps.handleBranchSelect,
	});

	// Register built-in activity sections for git and worktree notifications
	activityStore.registerSection({ id: "terminals", label: "TERMINALS", priority: 10, canDismissAll: true });
	activityStore.registerSection({ id: "git-ops", label: "GIT", priority: 30, canDismissAll: true });
	activityStore.registerSection({ id: "worktrees", label: "WORKTREES", priority: 40, canDismissAll: true });

	// Auto-heal CI failures by injecting logs into agent terminals
	useCiHeal();

	// Register git-branches smart prompts as branch context menu actions.
	// Reactive: re-registers when prompts are enabled/disabled.
	createEffect(() => {
		const disposables: Array<{ dispose(): void }> = [];
		for (const prompt of promptLibraryStore.getSmartByPlacement("git-branches")) {
			const p = prompt;
			disposables.push(
				contextMenuActionsStore.registerContextAction("smart-prompts", {
					id: `smart:${p.id}`,
					label: p.name,
					target: "branch",
					action: (ctx) => {
						smartPrompts
							.executeSmartPrompt(p, ctx.branchName ? { branch_name: ctx.branchName } : undefined)
							.catch((err) => appLogger.error("prompts", "Smart prompt execution failed", err));
					},
				}),
			);
		}
		onCleanup(() => disposables.forEach((d) => d.dispose()));
	});

	// Register terminal-context smart prompts as terminal context menu actions.
	// Reactive: re-registers when prompts are enabled/disabled.
	createEffect(() => {
		const disposables: Array<{ dispose(): void }> = [];
		for (const prompt of promptLibraryStore.getSmartByPlacement("terminal-context")) {
			const p = prompt;
			disposables.push(
				contextMenuActionsStore.registerContextAction("smart-prompts", {
					id: `smart:${p.id}`,
					label: p.name,
					target: "terminal",
					action: () => {
						smartPrompts
							.executeSmartPrompt(p)
							.catch((err) => appLogger.error("prompts", "Smart prompt execution failed", err));
					},
					disabled: () => !smartPrompts.canExecute(p).ok,
				}),
			);
		}
		onCleanup(() => disposables.forEach((d) => d.dispose()));
	});

	// Register AI Chat context menu actions (reactive — tracks feature flag)
	let aiChatDisposables: Array<{ dispose(): void }> = [];
	createEffect(() => {
		aiChatDisposables.forEach((d) => d.dispose());
		aiChatDisposables = [];
		if (settingsStore.isAiChatEnabled()) {
			aiChatDisposables = registerAiChatContextActions();
			void conversationStore.initFromDisk();
		} else if (uiStore.state.aiChatPanelVisible) {
			uiStore.setAiChatPanelVisible(false);
		}
	});
	onCleanup(() => aiChatDisposables.forEach((d) => d.dispose()));

	// Stop GitHub polling on component teardown — registered at body level so
	// SolidJS can track it synchronously (onCleanup inside async onMount is unreliable).
	onCleanup(() => githubStore.stopPolling());

	// Generic panel window lifecycle: clear detached state when any panel window closes.
	{
		let unlisten: (() => void) | undefined;
		listen<string>("panel-window-closed", (event) => {
			const panelId = event.payload;
			uiStore.clearDetached(panelId);
			panelRegistry[panelId]?.toggle?.();
		}).then((fn) => {
			unlisten = fn;
		});
		onCleanup(() => unlisten?.());
	}

	// Route actions from detached panel windows to the correct adapter.
	{
		let unlisten: (() => void) | undefined;
		listen<PanelAction>("panel-action", (event) => {
			const { panelId, action, data } = event.payload;
			if (action === "reattach") {
				uiStore.clearDetached(panelId);
				panelRegistry[panelId]?.toggle?.();
			} else {
				panelRegistry[panelId]?.handleAction?.(action, data);
			}
		}).then((fn) => {
			unlisten = fn;
		});
		onCleanup(() => unlisten?.());
	}

	// AI suggestion triggers: show toast with Investigate button.
	{
		let unlisten: (() => void) | undefined;
		listen<{ session_id: string; trigger_reason: string; proposed_goal: string }>("ai-suggestion", (event) => {
			const { session_id, trigger_reason, proposed_goal } = event.payload;
			toastsStore.add(trigger_reason, "", "warn", false, {
				label: "Investigate",
				onClick: () => {
					// Reveal the panel and point the chat context at the failed session
					// before starting — otherwise startAgent runs on the active terminal's
					// conversation (the wrong one) and nothing visible happens.
					if (!settingsStore.isAiChatEnabled()) {
						toastsStore.add("Enable AI Chat in Settings to investigate", "", "warn", false);
						return;
					}
					uiStore.setAiChatPanelVisible(true);
					switchToTerminalBySession(session_id);
					conversationStore.startAgent(session_id, proposed_goal);
				},
			});
		}).then((fn) => {
			unlisten = fn;
		});
		onCleanup(() => unlisten?.());
	}

	// Backend watcher fires: resolve the referenced smart prompt (or instructions)
	// and run it in the target session. PR fires go through the assisted gate.
	{
		let unlisten: (() => void) | undefined;
		const deps = watcherFireDeps((p) => smartPrompts.executeSmartPrompt(p));
		listen<WatcherFirePayload>("watcher-fire", (event) => {
			void handleWatcherFire(event.payload, deps);
		}).then((fn) => {
			unlisten = fn;
		});
		onCleanup(() => unlisten?.());
	}

	// Start sync providers for detached projection panels.
	// untrack: serialize() reads stores — those must NOT become effect deps,
	// otherwise every store mutation recreates the provider.
	for (const adapter of Object.values(panelRegistry)) {
		if (!adapter.serialize || !adapter.syncIntervalMs) continue;
		const a = adapter;
		createEffect(() => {
			if (!uiStore.isDetached(a.id)) return;
			const provider = untrack(() => {
				const p = createPanelSyncProvider(a.id, a.serialize!, a.syncIntervalMs!);
				p.start();
				return p;
			});
			onCleanup(() => provider.stop());
		});
	}

	// Notification sounds are now played natively via Rust (rodio) —
	// no Web Audio warmup needed.

	onMount(async () => {
		await initApp({
			pty,
			setQuitDialogVisible,
			setStatusInfo,
			setCurrentRepoPath: gitOps.setCurrentRepoPath,
			setCurrentBranch: gitOps.setCurrentBranch,
			handleBranchSelect: gitOps.handleBranchSelect,
			refreshAllBranchStats: gitOps.refreshAllBranchStats,
			handleWorktreeCreateFailed: gitOps.handleWorktreeCreateFailed,
			getDefaultFontSize,
			stores: {
				hydrate: async () => {
					const results = await Promise.allSettled([
						repositoriesStore.hydrate(),
						uiStore.hydrate(),
						settingsStore.hydrate(),
						notificationsStore.hydrate(),
						repoSettingsStore.hydrate(),
						repoDefaultsStore.hydrate(),
						promptLibraryStore.hydrate(),
						notesStore.hydrate(),
						activityStore.hydrate(),
						keybindingsStore.hydrate(),
						agentConfigsStore.hydrate(),
						providerRegistryStore.hydrate(),
						agentDetection.detectAll(),
					]);
					const failures = results.filter((r) => r.status === "rejected");
					if (failures.length > 0) {
						appLogger.error("store", `${failures.length} store(s) failed to hydrate`, failures);
						throw new Error(`${failures.length} store(s) failed`);
					}
				},
				startPolling: githubStore.startPolling,
				stopPolling: githubStore.stopPolling,
				startAutoFetch,
				startPrNotificationTimer: prNotificationsStore.startFocusTimer,
				loadFontFromConfig: settingsStore.loadFontFromConfig,
				refreshDictationConfig: () =>
					dictationStore.refreshConfig().then(() => {
						if (dictationStore.state.enabled) {
							dictationStore.refreshStatus();
						}
					}),
				startUserActivityListening: userActivityStore.startListening,
			},
			applyPlatformClass,
			onCloseRequested: (handler) => {
				if (!isTauri()) return;
				getCurrentWindow().onCloseRequested(async (event) => handler(event));
			},
		}).catch((err) => {
			appLogger.error("app", "Fatal initialization error", err);
			setStatusInfo("Error: App failed to initialize — check error log");
			document.getElementById("splash")?.remove();
		});

		// Reopen detached panel windows that were persisted before shutdown.
		if (isTauri()) {
			const panels = { ...uiStore.state.detachedPanels };
			for (const [panelId, _label] of Object.entries(panels)) {
				const adapter = panelRegistry[panelId];
				if (!adapter) {
					uiStore.clearDetached(panelId);
					continue;
				}
				invoke("open_panel_window", {
					panelId,
					title: adapter.title,
					params: {},
					width: adapter.defaultSize.width,
					height: adapter.defaultSize.height,
				}).catch((err) => {
					appLogger.warn("app", `Failed to restore detached panel: ${panelId}`, { error: String(err) });
					uiStore.clearDetached(panelId);
				});
			}
		}

		// Check for updates after hydration (non-blocking)
		if (settingsStore.state.autoUpdateEnabled) {
			updaterStore.checkForUpdate().catch((err) => appLogger.debug("app", "Updater auto-check failed", err));
		}
		if (settingsStore.state.autoUpdatePluginsEnabled) {
			registryStore.fetch().catch((err) => appLogger.debug("app", "Plugin registry fetch failed", err));
		}

		// "What's New" dialog — shown once after a stable version update
		if (isTauri()) {
			Promise.all([getVersion(), invoke<string | null>("get_last_seen_version")])
				.then(([currentVersion, lastSeen]) => {
					const baseVersion = currentVersion.replace(/[-+].*$/, "");
					if (!lastSeen) {
						invoke("set_last_seen_version", { version: baseVersion }).catch(() => {});
						return;
					}
					if (lastSeen !== baseVersion && !currentVersion.includes("nightly")) {
						setWhatsNewVersion(baseVersion);
					}
				})
				.catch((err) => appLogger.debug("app", "Version check for What's New failed", err));
		}

		// First-run CLI install prompt (one-time)
		if (isTauri()) {
			invoke<{ installed: boolean; prompt_dismissed: boolean }>("get_cli_status")
				.then(async (status) => {
					if (!status.installed && !status.prompt_dismissed) {
						const confirmed = await dialogs.confirm({
							title: "Install tuic CLI?",
							message:
								"The tuic command lets you control TUICommander from the terminal:\n\n" +
								"• tuic open file.rs:42 — open files with line numbers\n" +
								"• tuic ls / new / send — manage terminal sessions\n" +
								"• tuic agent spawn claude — spawn AI agents\n" +
								"• Works as a tmux replacement (tuic alias)\n\n" +
								"Install to /usr/local/bin/tuic? (You can always install later from Settings.)",
							kind: "info",
						});
						if (confirmed) {
							invoke("install_cli").catch((err) => appLogger.error("app", "CLI install failed", err));
						}
						invoke("dismiss_cli_prompt").catch(() => {});
					}
				})
				.catch((e) => appLogger.debug("app", "get_cli_status failed, skipping CLI prompt", { error: String(e) }));
		}

		// Register tuic:// deep link handler
		initDeepLinkHandler({
			openSettings,
			confirm: (title, message) => dialogs.confirm({ title, message, kind: "warning" }),
			onInstallError: (msg) => appLogger.error("plugin", msg),
		});
	});

	// Apply the active theme to the entire app chrome (sidebar, tabs, toolbar, etc.)
	// Read theme BEFORE the guard so SolidJS always tracks it as a dependency.
	// The explicit call in initApp handles the initial apply after loading.
	createEffect(() => {
		const theme = settingsStore.state.theme;
		if (themesLoaded()) applyAppTheme(theme);
	});

	// Sync --font-mono CSS variable when font selection changes
	createEffect(() => applyFontFamily(settingsStore.state.font));

	// Enforce mutual exclusivity between tab stores.
	// When a non-terminal tab becomes active (e.g. from mdTabsStore.add()),
	// deactivate the terminal so its pane hides and xterm releases focus.
	// Using `on()` with `defer: true` so each effect only fires on its own store's change,
	// preventing the setActive(null) calls from triggering cascading re-runs.
	createEffect(
		on(
			() => mdTabsStore.state.activeId,
			(id) => {
				if (id) {
					terminalsStore.setActive(null);
					diffTabsStore.setActive(null);
					editorTabsStore.setActive(null);
				}
			},
			{ defer: true },
		),
	);
	createEffect(
		on(
			() => diffTabsStore.state.activeId,
			(id) => {
				if (id) {
					terminalsStore.setActive(null);
					mdTabsStore.setActive(null);
					editorTabsStore.setActive(null);
				}
			},
			{ defer: true },
		),
	);
	createEffect(
		on(
			() => editorTabsStore.state.activeId,
			(id) => {
				if (id) {
					terminalsStore.setActive(null);
					diffTabsStore.setActive(null);
					mdTabsStore.setActive(null);
				}
			},
			{ defer: true },
		),
	);
	createEffect(
		on(
			() => terminalsStore.state.activeId,
			(id) => {
				if (id) {
					diffTabsStore.setActive(null);
					mdTabsStore.setActive(null);
					editorTabsStore.setActive(null);
				}
			},
			{ defer: true },
		),
	);

	// Wire terminal tab switches → conversationStore per-terminal context
	createEffect(
		on(
			() => terminalsStore.state.activeId,
			(id) => {
				if (!id) return;
				const term = terminalsStore.get(id);
				const key = term?.tuicSession ?? id;
				conversationStore.setActiveTerminal(key);
				void conversationStore.initFromDisk(term?.tuicSession ?? undefined);
			},
			{ defer: true },
		),
	);

	// Dismiss terminal completion activity item when user views the terminal
	createEffect(
		on(
			() => terminalsStore.state.activeId,
			(id) => {
				if (id) activityStore.dismissItem(`terminal-done-${id}`);
			},
			{ defer: true },
		),
	);

	// Wire terminal close → conversationStore cleanup
	{
		const dispose = terminalsStore.onRemove((id) => {
			const term = terminalsStore.get(id);
			const key = term?.tuicSession ?? id;
			void conversationStore.onTerminalClose(key);
		});
		onCleanup(dispose);
	}

	// Notify plugins when the active repository changes so globally-pinned
	// plugin panels (e.g. Wiz Kanban) can reload their content.
	createEffect(
		on(
			() => repositoriesStore.state.activeRepoPath,
			(repoPath) => {
				pluginRegistry.notifyStateChange({
					type: "repo-changed",
					sessionId: null,
					terminalId: "",
					detail: repoPath ?? undefined,
				});
			},
			{ defer: true },
		),
	);

	// Persist the currently-active terminal to its owning branch on every change,
	// so returning to a branch (via sidebar click or repo switch) restores the
	// tab the user last focused — not the first terminal in branch.terminals.
	createEffect(
		on(
			() => terminalsStore.state.activeId,
			(id) => {
				if (!id) return;
				const repoPath = repositoriesStore.getRepoPathForTerminal(id);
				if (!repoPath) return;
				const repo = repositoriesStore.state.repositories[repoPath];
				if (!repo) return;
				for (const [branchName, branch] of Object.entries(repo.branches)) {
					if (branch.terminals.includes(id)) {
						if (branch.lastActiveTerminal !== id) {
							repositoriesStore.setBranch(repoPath, branchName, { lastActiveTerminal: id });
						}
						break;
					}
				}
			},
			{ defer: true },
		),
	);

	// Prevent system sleep while any terminal is busy (debounced — Story 258/405)
	let lastSleepBlocked: boolean | null = null;
	createEffect(() => {
		if (!isTauri()) return;
		const enabled = settingsStore.state.preventSleepWhenBusy;
		const anyBusy = terminalsStore.isAnyBusy();
		const shouldBlock = enabled && anyBusy;

		if (shouldBlock === lastSleepBlocked) return;
		lastSleepBlocked = shouldBlock;

		if (shouldBlock) {
			invoke("block_sleep").catch((err) => appLogger.warn("app", "Failed to block sleep", err));
		} else {
			invoke("unblock_sleep").catch((err) => appLogger.warn("app", "Failed to unblock sleep", err));
		}
	});

	// Completion notification: play sound when a terminal was busy for >=5s then goes idle.
	// Deferred when the agent has active sub-tasks or is an agent process (sub-agents may still be running).
	const BUSY_COMPLETION_THRESHOLD_MS = 5000;
	const DEFERRED_COMPLETION_MS = 10_000;
	const deferredCompletionTimers = new Map<string, ReturnType<typeof setTimeout>>();

	const unsubBusyToIdle = terminalsStore.onBusyToIdle((id, durationMs) => {
		if (durationMs < BUSY_COMPLETION_THRESHOLD_MS) return;
		if (terminalsStore.state.activeId === id) return;

		const fireCompletion = () => {
			deferredCompletionTimers.delete(id);
			// Re-check: terminal may have been removed during deferral.
			const terminal = terminalsStore.get(id);
			if (!terminal) return;

			const reason = getCompletionSuppression({
				isActiveTerminal: terminalsStore.state.activeId === id,
				isDebouncedBusy: !!terminalsStore.state.debouncedBusy[id],
				activeSubTasks: terminal.activeSubTasks,
				awaitingInput: terminal.awaitingInput,
				durationMs,
				thresholdMs: BUSY_COMPLETION_THRESHOLD_MS,
			});
			if (reason) {
				appLogger.debug("terminal", `[Notify] ${id} completion SUPPRESSED — ${reason}`);
				return;
			}
			appLogger.info("terminal", `[Notify] ${id} completion — busy for ${Math.round(durationMs / 1000)}s then idle`);
			terminalsStore.update(id, { activity: true, unseen: true });
			notificationsStore.playCompletion(id);
			const agentLabel = terminal.agentType ? terminal.agentType[0].toUpperCase() + terminal.agentType.slice(1) : null;
			const repoPath = repositoriesStore.getRepoPathForTerminal(id);
			const repoName = repoPath ? pathBasename(repoPath) : null;
			const durationStr = `ran for ${Math.round(durationMs / 1000)}s`;
			const subtitleParts: string[] = [];
			if (repoName) subtitleParts.push(repoName);
			subtitleParts.push(durationStr);
			if (terminal.agentIntent) subtitleParts.push(terminal.agentIntent);

			const defaultIcon =
				'<svg viewBox="0 0 16 16" width="14" height="14" fill="currentColor"><path d="M0 2.75C0 1.784.784 1 1.75 1h12.5c.966 0 1.75.784 1.75 1.75v10.5A1.75 1.75 0 0 1 14.25 15H1.75A1.75 1.75 0 0 1 0 13.25Zm1.75-.25a.25.25 0 0 0-.25.25v10.5c0 .138.112.25.25.25h12.5a.25.25 0 0 0 .25-.25V2.75a.25.25 0 0 0-.25-.25ZM7.25 8a.749.749 0 0 1-.22.53l-2.25 2.25a.749.749 0 1 1-1.06-1.06L5.44 8 3.72 6.28a.749.749 0 1 1 1.06-1.06l2.25 2.25c.141.14.22.331.22.53Zm1.5 1.5h3a.75.75 0 0 1 0 1.5h-3a.75.75 0 0 1 0-1.5Z"/></svg>';

			activityStore.addItem({
				id: `terminal-done-${id}`,
				pluginId: "core",
				sectionId: "terminals",
				title: `${agentLabel ?? terminal.name} finished`,
				subtitle: subtitleParts.join(" · "),
				icon: (terminal.agentType && getAgentIconSvg(terminal.agentType, 14)) || defaultIcon,
				repoPath: repoPath ?? undefined,
				dismissible: true,
				onClick: () => navigateToTerminal(id),
			});
		};

		const t = terminalsStore.get(id);
		// Suppress immediately if agent has known active sub-tasks.
		if (t && t.activeSubTasks > 0) {
			appLogger.debug("terminal", `[Notify] ${id} completion SUPPRESSED — ${t.activeSubTasks} active sub-tasks`);
			return;
		}
		if (t?.agentType) {
			// Agent process: defer — if the terminal stays idle for 10s, it's truly done.
			clearTimeout(deferredCompletionTimers.get(id));
			deferredCompletionTimers.set(id, setTimeout(fireCompletion, DEFERRED_COMPLETION_MS));
		} else {
			// Non-agent: short defer to let question detection settle awaitingInput
			// before checking suppression (avoids completion+question double-sound).
			clearTimeout(deferredCompletionTimers.get(id));
			deferredCompletionTimers.set(id, setTimeout(fireCompletion, 800));
		}
	});
	const unsubIdleToBusy = terminalsStore.onIdleToBusy((id) => {
		const timer = deferredCompletionTimers.get(id);
		if (timer) {
			clearTimeout(timer);
			deferredCompletionTimers.delete(id);
			appLogger.debug("terminal", `[Notify] ${id} deferred completion CANCELLED — terminal went busy`);
		}
	});

	onCleanup(() => {
		unsubBusyToIdle();
		unsubIdleToBusy();
		for (const timer of deferredCompletionTimers.values()) clearTimeout(timer);
		deferredCompletionTimers.clear();
	});

	// Auto-trigger AI diff triage when an agent terminal goes idle after meaningful work.
	const TRIAGE_BUSY_THRESHOLD_MS = 5000;
	const unsubTriageOnIdle = terminalsStore.onBusyToIdle((id, durationMs) => {
		if (!settingsStore.isAiTriageEnabled()) return;
		if (durationMs < TRIAGE_BUSY_THRESHOLD_MS) return;
		const t = terminalsStore.get(id);
		if (!t?.agentType) return;
		const repoPath = repositoriesStore.getRepoPathForTerminal(id);
		if (!repoPath) return;
		aiTriageStore.runTriage(repoPath);
	});
	onCleanup(unsubTriageOnIdle);

	// Initialize plugin system
	onMount(() => {
		initPlugins().catch((err) =>
			appLogger.error("plugin", "Plugin initialization failed", err instanceof Error ? { stack: err.stack } : err),
		);
	});

	// Clear dock badge when window gains focus
	onMount(() => {
		const handleFocus = () => notificationsStore.clearBadge();
		window.addEventListener("focus", handleFocus);
		onCleanup(() => window.removeEventListener("focus", handleFocus));
	});

	// Force quit - close all sessions and exit (Story 057)
	const forceQuit = async () => {
		setQuitDialogVisible(false);

		// Destroy window after a short timeout regardless of PTY cleanup
		const destroyTimer = isTauri() ? setTimeout(() => getCurrentWindow().destroy(), 500) : null;

		try {
			const sessionIds = terminalsStore
				.getIds()
				.map((id) => terminalsStore.get(id)?.sessionId)
				.filter((sid): sid is string => sid != null);
			await Promise.all(
				sessionIds.map((sid) =>
					pty.close(sid).catch((err) => appLogger.warn("app", `Failed to close PTY ${sid} on quit`, err)),
				),
			);
		} catch (e) {
			appLogger.warn("app", "forceQuit: unexpected error", { error: String(e) });
		}

		if (destroyTimer !== null) clearTimeout(destroyTimer);
		if (isTauri()) getCurrentWindow().destroy();
	};

	// Build agent submenu items for the context menu
	/** Launch an agent in the active terminal */
	const launchAgentInActiveTerminal = async (agentType: AgentType, cmd: string) => {
		const active = terminalsStore.getActive();
		if (!active?.ref || !active.sessionId) return;
		// Claude: no --session-id injection — session is discovered from disk after launch.
		// Other agents: inject tab UUID as session ID when supported.
		const agentSessionId = agentType === "claude" ? null : (active.tuicSession ?? null);
		const finalCmd = buildAgentLaunchCommand(cmd, agentSessionId, agentType);
		// Shell family matters here: we're still at the user's shell prompt
		// (agent not running yet) so Ctrl-U must match the shell, not the agent.
		const shellFamily = await getShellFamily(active.sessionId);
		await sendCommand(
			(data) => invoke("write_pty", { sessionId: active.sessionId, data }),
			finalCmd,
			null,
			shellFamily,
		);
		terminalsStore.update(active.id, {
			name: AGENTS[agentType].name,
			nameIsCustom: true,
			agentLaunchCommand: cmd,
		});
	};

	const buildAgentMenuItems = (): ContextMenuItem[] => {
		const available = agentDetection.getAvailable().filter((a) => a.type !== "git" && a.type !== "api");
		if (available.length === 0) return [];

		return available.map((agent) => {
			const agentConfig = AGENTS[agent.type];
			const runConfigs = agentConfigsStore.getRunConfigs(agent.type);

			// Multiple run configs: submenu with each config
			if (runConfigs.length > 1) {
				return {
					label: agentConfig.name,
					action: () => {},
					children: runConfigs.map((rc) => ({
						label: rc.name + (rc.is_default ? " (Default)" : ""),
						action: () => launchAgentInActiveTerminal(agent.type, [rc.command, ...rc.args].join(" ")),
					})),
				};
			}

			// 0-1 run configs: flat item, click launches directly
			const cmd =
				runConfigs.length === 1 ? [runConfigs[0].command, ...runConfigs[0].args].join(" ") : agentConfig.binary;
			return {
				label: agentConfig.name,
				action: () => launchAgentInActiveTerminal(agent.type, cmd),
			};
		});
	};

	// Build agent menu items for the sidebar branch context menu.
	// Creates a new terminal on the branch and writes the agent launch command.
	// Returns items ready to splice into the context menu:
	// - 0 enabled agents: []
	// - 1 enabled agent: [{ label: "Add <AgentName>", action }]
	// - N enabled agents: [{ label: "Add Agent", children: [...] }]
	const buildSidebarAgentMenuItems = (repoPath: string, branchName: string): ContextMenuItem[] => {
		const enabled = agentDetection
			.getAvailable()
			.filter((a) => a.type !== "git" && a.type !== "api" && settingsStore.isAgentEnabled(a.type));
		if (enabled.length === 0) return [];

		const buildAgentEntry = (agent: (typeof enabled)[0]) => {
			const agentConfig = AGENTS[agent.type];
			const runConfigs = agentConfigsStore.getRunConfigs(agent.type);

			const launchAgent = async (cmd: string) => {
				const termId = await gitOps.handleAddTerminalToBranch(repoPath, branchName);
				if (termId) {
					// Claude: no --session-id injection — session is discovered from disk after launch.
					// Other agents: inject tab UUID as session ID when supported.
					const term = terminalsStore.get(termId);
					const agentSessionId = agent.type === "claude" ? null : (term?.tuicSession ?? null);
					const finalCmd = buildAgentLaunchCommand(cmd, agentSessionId, agent.type);
					terminalsStore.update(termId, {
						name: agentConfig.name,
						nameIsCustom: true,
						pendingInitCommand: finalCmd,
						agentType: agent.type,
						agentLaunchCommand: cmd,
					});
				}
			};

			const children: ContextMenuItem[] =
				runConfigs.length > 0
					? runConfigs.map((rc) => ({
							label: rc.name + (rc.is_default ? " (Default)" : ""),
							action: () => launchAgent([rc.command, ...rc.args].join(" ")),
						}))
					: [
							{
								label: "(Default)",
								action: () => launchAgent(agentConfig.binary),
							},
						];

			return { agentConfig, children };
		};

		if (enabled.length === 1) {
			const { agentConfig, children } = buildAgentEntry(enabled[0]);
			// Single agent: flat item "Add <Name>" (with sub-configs if multiple)
			if (children.length === 1) {
				return [{ label: `Add ${agentConfig.name}`, action: children[0].action }];
			}
			return [{ label: `Add ${agentConfig.name}`, action: () => {}, children }];
		}

		// Multiple agents: "Add Agent" submenu
		const agentItems = enabled.map((agent) => {
			const { agentConfig, children } = buildAgentEntry(agent);
			if (children.length === 1) {
				return { label: agentConfig.name, action: children[0].action };
			}
			return { label: agentConfig.name, action: () => {}, children };
		});

		return [{ label: "Add Agent", action: () => {}, children: agentItems }];
	};

	// Context menu items
	/** Disable split when no active terminal, or active group is at max tree depth */
	const splitDisabled = () => {
		if (!paneLayoutStore.isSplit()) return !terminalsStore.state.activeId;
		const activeGroupId = paneLayoutStore.state.activeGroupId;
		return !activeGroupId || !paneLayoutStore.canSplit(activeGroupId);
	};

	/** Check if the active terminal has a running agent (disables agent submenu) */
	const activeTerminalBusy = (): boolean => {
		const activeId = terminalsStore.state.activeId;
		if (!activeId) return true;
		const term = terminalsStore.get(activeId);
		return !!term?.agentType;
	};

	const getContextMenuItems = (): ContextMenuItem[] => [
		...(agentDetection.getAvailable().length > 0
			? [
					{
						label: "Agents",
						action: () => {},
						disabled: activeTerminalBusy(),
						children: buildAgentMenuItems(),
					},
				]
			: []),
		{
			label: "Copy",
			shortcut: `${getModifierSymbol()}C`,
			action: terminalLifecycle.copyFromTerminal,
			separator: agentDetection.getAvailable().length > 0,
		},
		{
			label: "Copy Block Output",
			action: async () => {
				const activeId = terminalsStore.state.activeId;
				if (!activeId) return;
				const term = terminalsStore.get(activeId);
				if (!term) return;
				const blocks = term.commandBlocks;
				const lastBlock = blocks[blocks.length - 1];
				if (!lastBlock || lastBlock.executionLine == null || lastBlock.endLine == null) return;
				const ref = term.ref;
				if (!ref) return;
				const lines = await ref.getBufferLines(lastBlock.executionLine + 1, lastBlock.endLine);
				const text = lines.join("\n").trimEnd();
				if (text) navigator.clipboard.writeText(text);
			},
			disabled: (() => {
				const activeId = terminalsStore.state.activeId;
				if (!activeId) return true;
				const term = terminalsStore.get(activeId);
				return !term?.commandBlocks?.length;
			})(),
		},
		{ label: "Paste", shortcut: `${getModifierSymbol()}V`, action: terminalLifecycle.pasteToTerminal },
		{
			label: "Split Right",
			shortcut: keyFor("split-vertical"),
			action: () => splitPanes.handleSplit("vertical"),
			disabled: splitDisabled(),
		},
		{ label: "Split Left", action: () => splitPanes.handleSplit("vertical"), disabled: splitDisabled() },
		{
			label: "Split Down",
			shortcut: keyFor("split-horizontal"),
			action: () => splitPanes.handleSplit("horizontal"),
			disabled: splitDisabled(),
		},
		{
			label: "Split Up",
			action: () => splitPanes.handleSplit("horizontal"),
			disabled: splitDisabled(),
			separator: true,
		},
		{ label: "Clear", shortcut: keyFor("clear-terminal"), action: terminalLifecycle.clearTerminal },
		{
			label: "Reset Terminal",
			action: () => {
				const activeId = terminalsStore.state.activeId;
				if (activeId) terminalsStore.get(activeId)?.ref?.write("\x1bc");
			},
		},
		{
			label: "Change Title\u2026",
			action: () => {
				const activeId = terminalsStore.state.activeId;
				if (!activeId) return;
				const current = terminalsStore.get(activeId)?.name || "";
				setTermRenameDefault(current);
				setTermRenamePromptVisible(true);
			},
			separator: true,
		},
		...(() => {
			const legacyActions = contextMenuActionsStore.getActions();
			const pluginContextActions = contextMenuActionsStore.getContextActions("terminal", {
				excludePluginId: "smart-prompts",
			});
			if (legacyActions.length === 0 && pluginContextActions.length === 0) return [];
			const activeId = terminalsStore.state.activeId;
			const sessionId = activeId ? (terminalsStore.get(activeId)?.sessionId ?? null) : null;
			const repoPath = repositoriesStore.state.activeRepoPath ?? null;
			const legacyCtx = { sessionId, repoPath };
			const ctxMenuCtx = {
				target: "terminal" as const,
				sessionId: sessionId ?? undefined,
				repoPath: repoPath ?? undefined,
			};
			return [
				{
					label: "Actions",
					action: () => {},
					children: [
						...legacyActions.map((a) => ({
							label: a.label,
							action: () => a.action(legacyCtx),
							disabled: a.disabled?.(legacyCtx) ?? false,
						})),
						...pluginContextActions.map((a) => ({
							label: a.label,
							action: () => a.action(ctxMenuCtx),
							disabled: a.disabled?.(ctxMenuCtx) ?? false,
						})),
					],
					separator: true,
				},
			];
		})(),
		...(() => {
			const smartPromptActions = contextMenuActionsStore.getContextActions("terminal", { pluginId: "smart-prompts" });
			if (smartPromptActions.length === 0) return [];
			const activeId = terminalsStore.state.activeId;
			const sessionId = activeId ? (terminalsStore.get(activeId)?.sessionId ?? undefined) : undefined;
			const repoPath = repositoriesStore.state.activeRepoPath ?? undefined;
			const ctx = { target: "terminal" as const, sessionId, repoPath };
			return [
				{
					label: "Prompts",
					action: () => {},
					children: smartPromptActions.map((a) => ({
						label: a.label,
						action: () => a.action(ctx),
						disabled: a.disabled?.(ctx) ?? false,
					})),
					separator: true,
				},
			];
		})(),
		{
			label: "Close Terminal",
			shortcut: keyFor("close-terminal"),
			action: () => {
				const activeId = terminalsStore.state.activeId;
				if (activeId) terminalLifecycle.closeTerminal(activeId);
			},
			separator: true,
		},
	];

	/**
	 * Reveal an arbitrary folder in the FileBrowser panel. Sets an ephemeral
	 * external root so browsing works outside the active repo — git badges and
	 * gitignore integration degrade gracefully when the folder isn't a repo.
	 */
	const revealFolderInBrowser = (absolutePath: string) => {
		uiStore.setFileBrowserExternalRoot(absolutePath);
		uiStore.setFileBrowserPanelVisible(true);
	};

	/** Open a file path from terminal output — .md/.mdx in MD viewer, others in internal editor */
	const handleOpenFilePath = (absolutePath: string, _line?: number, _col?: number) => {
		const repoPath = repositoriesStore.state.activeRepoPath;
		if (!repoPath) return;
		const fsRoot = gitOps.activeWorktreePath() || repoPath;

		// Convert to relative path when inside the effective root (worktree or repo), keep absolute otherwise
		const filePath = pathStartsWith(absolutePath, fsRoot) ? pathStripPrefix(absolutePath, fsRoot)! : absolutePath;

		openFileAction(filePath, repoPath, fsRoot, undefined, (tabId) => {
			terminalLifecycle.handleTerminalSelect(tabId);
		});
	};

	// Listen for file-open events from macOS file associations
	createEffect(() => {
		let unlisten: (() => void) | undefined;
		listen<string[]>("file-open", (event) => {
			for (const absolutePath of event.payload) {
				const repoPath = repositoriesStore.state.activeRepoPath ?? "";
				const fsRoot = gitOps.activeWorktreePath() || repoPath;
				const filePath =
					fsRoot && pathStartsWith(absolutePath, fsRoot) ? pathStripPrefix(absolutePath, fsRoot)! : absolutePath;
				const effectiveRepo = filePath === absolutePath ? "" : repoPath;
				const effectiveRoot = filePath === absolutePath ? "" : fsRoot;

				openFileAction(filePath, effectiveRepo, effectiveRoot || undefined);
			}
		})
			.then((fn) => {
				unlisten = fn;
			})
			.catch((err) => appLogger.error("app", "Failed to listen for file-open events", err));

		onCleanup(() => unlisten?.());
	});

	/** Patterns in stderr that indicate the command needs interactive terminal input */
	const NEEDS_TERMINAL_PATTERNS = [
		/terminal prompts disabled/i, // GIT_TERMINAL_PROMPT=0 rejection
		/could not read Username/i, // HTTP credential prompt blocked
		/could not read Password/i,
		/Permission denied.*publickey/i, // SSH auth failed (askpass cancelled/missing)
		/Host key verification failed/i,
		/Please make sure you have the correct access rights/i,
	];

	/** Fall back to running a git command in the active terminal */
	const fallbackToTerminal = (repoPath: string, args: string[]) => {
		const active = terminalsStore.getActive();
		if (active?.ref) {
			const cmd = `cd ${JSON.stringify(repoPath)} && git ${args.join(" ")}`;
			active.ref.write(`${cmd}\r`);
			setStatusInfo(`git ${args[0]} requires auth — running in terminal`);
		}
	};

	/** Run a git command in the background via Rust, with task tracking and notifications.
	 *  Falls back to terminal if the command needs interactive authentication. */
	const handleBackgroundGit = async (repoPath: string, op: string, args: string[]) => {
		// Prevent duplicate runs of the same op
		if (runningGitOps().has(op)) return;
		setRunningGitOps((prev) => new Set([...prev, op]));

		const taskId = tasksStore.create({
			name: `git ${op}`,
			description: `Running git ${args.join(" ")} in ${repoPath}`,
			agentType: "git",
		});
		tasksStore.start(taskId, `git-${op}-${Date.now()}`);
		setStatusInfo(`Running git ${op}...`);

		try {
			const result = await invoke("run_git_command", { path: repoPath, args });
			const { success, stdout, stderr } = result as {
				success: boolean;
				stdout: string;
				stderr: string;
				exit_code: number;
			};

			if (success) {
				tasksStore.complete(taskId, 0);
				repositoriesStore.bumpRevision(repoPath);
				// Show meaningful output: prefer stderr (git progress goes there), fall back to stdout
				const output = (stderr.trim() || stdout.trim()).split("\n").pop()?.trim();
				const summary = output ? `git ${op}: ${output}` : `git ${op} completed`;
				setStatusInfo(summary);
				appLogger.info("app", `[Notify] completion — git ${op} succeeded`);
				notificationsStore.playCompletion();
				activityStore.addItem({
					id: `git-${op}-${Date.now()}`,
					pluginId: "core",
					sectionId: "git-ops",
					title: `git ${op}`,
					subtitle: output || "completed",
					icon: '<svg viewBox="0 0 16 16" width="14" height="14" fill="currentColor"><path d="M8 0a8 8 0 1 0 0 16A8 8 0 0 0 8 0zm3.78 5.97l-4.5 4.5a.75.75 0 0 1-1.06 0l-2-2a.75.75 0 1 1 1.06-1.06L6.75 8.88l3.97-3.97a.75.75 0 1 1 1.06 1.06z"/></svg>',
					repoPath,
					dismissible: true,
				});
			} else if (NEEDS_TERMINAL_PATTERNS.some((p) => p.test(stderr))) {
				// Auth or interactive prompt needed — cancel background task, run in terminal
				tasksStore.cancel(taskId);
				fallbackToTerminal(repoPath, args);
			} else {
				const errMsg = stderr.trim() || `git ${op} failed`;
				tasksStore.fail(taskId, errMsg);
				setStatusInfo(`git ${op} failed: ${errMsg}`);
				appLogger.info("app", `[Notify] error — git ${op} failed: ${errMsg}`);
				notificationsStore.playError();
				activityStore.addItem({
					id: `git-${op}-${Date.now()}`,
					pluginId: "core",
					sectionId: "git-ops",
					title: `git ${op} failed`,
					subtitle: errMsg,
					icon: '<svg viewBox="0 0 16 16" width="14" height="14" fill="#f85149"><path d="M8 0a8 8 0 1 0 0 16A8 8 0 0 0 8 0zm3.36 10.3a.75.75 0 0 1-1.06 1.06L8 9.06l-2.3 2.3a.75.75 0 0 1-1.06-1.06L6.94 8 4.64 5.7a.75.75 0 0 1 1.06-1.06L8 6.94l2.3-2.3a.75.75 0 0 1 1.06 1.06L9.06 8l2.3 2.3z"/></svg>',
					repoPath,
					dismissible: true,
				});
			}
		} catch (err) {
			tasksStore.fail(taskId, String(err));
			setStatusInfo(`git ${op} error: ${err}`);
			appLogger.info("app", `[Notify] error — git ${op} exception: ${err}`);
			notificationsStore.playError();
		} finally {
			setRunningGitOps((prev) => {
				const next = new Set(prev);
				next.delete(op);
				return next;
			});
		}
	};

	/** Detach a terminal tab to a floating OS window */
	const handleDetachTab = async (tabId: string) => {
		if (!isTauri()) return;
		const term = terminalsStore.get(tabId);
		if (!term?.sessionId) return;

		const windowLabel = `floating-${tabId}`;
		const url = `index.html#/floating?sessionId=${encodeURIComponent(term.sessionId)}&tabId=${encodeURIComponent(tabId)}&name=${encodeURIComponent(term.name)}`;

		const floatingWin = new WebviewWindow(windowLabel, {
			url,
			title: term.name || "Terminal",
			width: 800,
			height: 600,
			center: true,
			decorations: true,
		});

		floatingWin.once("tauri://destroyed", () => {
			if (terminalsStore.isDetached(tabId)) {
				reattachFallback(tabId);
				setTimeout(() => {
					const ref = terminalsStore.get(tabId)?.ref;
					if (ref) {
						ref.refresh();
						ref.fit();
					}
				}, 150);
			}
		});

		terminalsStore.detach(tabId, windowLabel);

		// Switch to next available tab
		const ids = terminalLifecycle.terminalIds().filter((id) => !terminalsStore.isDetached(id));
		if (ids.length > 0) {
			terminalLifecycle.handleTerminalSelect(ids[0]);
		}
	};

	const reattachFallback = (tabId: string) => {
		terminalsStore.reattach(tabId);
		terminalLifecycle.handleTerminalSelect(tabId);
	};

	/** Focus a detached terminal's floating window */
	const handleFocusDetachedTab = async (tabId: string) => {
		if (!isTauri()) return;
		const windowLabel = terminalsStore.state.detachedWindows[tabId];
		if (!windowLabel) return;
		try {
			const win = await WebviewWindow.getByLabel(windowLabel);
			if (win) {
				await win.unminimize();
				await win.setFocus();
			} else {
				appLogger.info("terminal", `Floating window ${windowLabel} gone, reattaching ${tabId}`);
				reattachFallback(tabId);
			}
		} catch (err) {
			appLogger.warn("terminal", `Error focusing floating window ${windowLabel}, reattaching`, err);
			reattachFallback(tabId);
		}
	};

	/** Reattach a detached terminal by closing its floating window */
	const handleReattachTab = async (tabId: string) => {
		if (!isTauri()) return;
		const windowLabel = terminalsStore.state.detachedWindows[tabId];
		if (!windowLabel) return;
		try {
			const win = await WebviewWindow.getByLabel(windowLabel);
			await win?.close();
		} catch (err) {
			appLogger.warn("terminal", `Failed to close floating window ${windowLabel}`, err);
		}
		reattachFallback(tabId);
	};

	// Listen for reattach events from floating windows
	createEffect(() => {
		if (!isTauri()) return;
		let unlisten: (() => void) | undefined;

		listen<{ tabId: string; sessionId: string }>("reattach-terminal", (event) => {
			const { tabId } = event.payload;
			if (!terminalsStore.isDetached(tabId)) return;
			reattachFallback(tabId);
			setStatusInfo("Tab reattached");
			setTimeout(() => {
				const ref = terminalsStore.get(tabId)?.ref;
				if (ref) {
					ref.refresh();
					ref.fit();
				}
			}, 150);
		})
			.then((fn) => {
				unlisten = fn;
			})
			.catch((err) => appLogger.error("terminal", "Failed to listen for reattach events", err));

		onCleanup(() => unlisten?.());
	});

	// Quick Switcher: visible while modifier combo is held, hides on release
	// macOS: Cmd+Ctrl, Windows/Linux: Ctrl+Alt
	// Also dismiss on window blur/visibility change — keyup events are missed
	// when the window loses focus while modifier keys are held.
	createEffect(() => {
		const trackKeydown = (e: KeyboardEvent) => {
			if (isQuickSwitcherActive(e)) {
				setQuickSwitcherVisible(true);
			}
		};

		const trackKeyup = (e: KeyboardEvent) => {
			if (isQuickSwitcherRelease(e)) {
				setQuickSwitcherVisible(false);
			}
		};

		const dismiss = () => setQuickSwitcherVisible(false);

		document.addEventListener("keydown", trackKeydown);
		document.addEventListener("keyup", trackKeyup);
		window.addEventListener("blur", dismiss);
		document.addEventListener("visibilitychange", dismiss);
		onCleanup(() => {
			document.removeEventListener("keydown", trackKeydown);
			document.removeEventListener("keyup", trackKeyup);
			window.removeEventListener("blur", dismiss);
			document.removeEventListener("visibilitychange", dismiss);
		});
	});

	// Shared shortcut handlers used by both keyboard shortcuts and command palette.
	// Zoom is context-aware: when a markdown tab is active, zoom scales its content;
	// otherwise it scales the active terminal (same dispatch pattern as `findInTerminal`).
	const shortcutHandlers = {
		zoomIn: () => {
			if (mdTabsStore.state.activeId) mdTabsStore.zoomIn();
			else terminalLifecycle.zoomIn();
		},
		zoomOut: () => {
			if (mdTabsStore.state.activeId) mdTabsStore.zoomOut();
			else terminalLifecycle.zoomOut();
		},
		zoomReset: () => {
			if (mdTabsStore.state.activeId) mdTabsStore.zoomReset();
			else terminalLifecycle.zoomReset();
		},
		zoomInAll: terminalLifecycle.zoomInAll,
		zoomOutAll: terminalLifecycle.zoomOutAll,
		zoomResetAll: terminalLifecycle.zoomResetAll,
		// Route Cmd+T / command palette through handleNewTab (the + button's path) so
		// the new terminal is registered in the active branch.terminals and the tab
		// appears immediately — not only after a worktree switch re-syncs orphans (#81).
		// handleNewTab falls back to createNewTerminal for the no-repo case.
		createNewTerminal: gitOps.handleNewTab,
		closeTerminal: terminalLifecycle.closeTerminal,
		reopenClosedTab: terminalLifecycle.reopenClosedTab,
		navigateTab: terminalLifecycle.navigateTab,
		focusLastTerminal: () => {
			const prevId = terminalsStore.getPreviousActiveId();
			if (prevId) navigateToTerminal(prevId);
		},
		jumpWaitingTerminal: () => {
			// Cycle through terminals awaiting input (agent asked a question / hit an
			// error), across all repos/branches, in tab order. Repeat presses advance
			// to the next one; does nothing when none are waiting.
			const waiting = terminalsStore
				.getIds()
				.filter((id) => terminalsStore.get(id)?.awaitingInput != null && !terminalsStore.isDetached(id));
			const next = nextWaitingTerminal(waiting, terminalsStore.state.activeId);
			if (next) navigateToTerminal(next);
		},
		clearTerminal: terminalLifecycle.clearTerminal,
		refreshTerminal: terminalLifecycle.refreshTerminal,
		clearScrollback: terminalLifecycle.clearScrollback,
		scrollToTop: terminalLifecycle.scrollToTop,
		scrollToBottom: terminalLifecycle.scrollToBottom,
		scrollPageUp: terminalLifecycle.scrollPageUp,
		scrollPageDown: terminalLifecycle.scrollPageDown,
		toggleZoomPane: splitPanes.toggleZoomPane,
		toggleFocusMode: uiStore.toggleFocusMode,
		closeActivePane: splitPanes.closeActivePane,
		terminalIds: terminalLifecycle.terminalIds,
		handleTerminalSelect: terminalLifecycle.handleTerminalSelect,
		handleSplit: splitPanes.handleSplit,
		handleRunCommand: (forceDialog: boolean) =>
			gitOps.handleRunCommand(forceDialog, () => setRunCommandDialogVisible(true)),
		switchToBranchByIndex: quickSwitcher.switchToBranchByIndex,
		isQuickSwitcherOpen: quickSwitcherVisible,
		toggleMarkdownPanel: uiStore.toggleMarkdownPanel,
		toggleSidebar: uiStore.toggleSidebar,
		togglePromptLibrary: promptLibraryStore.toggleDrawer,
		toggleSettings: () => setSettingsPanelVisible((v) => !v),
		toggleTaskQueue: () => setTaskQueueVisible((v) => !v),
		toggleGitOpsPanel: () => togglePanel("git"),
		toggleHelpPanel: () => setHelpPanelVisible((v) => !v),
		toggleNotesPanel: uiStore.toggleNotesPanel,
		toggleFileBrowserPanel: uiStore.toggleFileBrowserPanel,
		requestFileBrowserContentSearch: uiStore.requestFileBrowserContentSearch,
		toggleOutlinePanel: uiStore.toggleOutlinePanel,
		findInTerminal: () => {
			// Context-aware: open search in whichever tab type is active
			const diffActiveId = diffTabsStore.state.activeId;
			if (diffActiveId) {
				const handle = diffTabsStore.getHandle<{ openSearch: () => void }>(diffActiveId);
				handle?.openSearch();
				return;
			}
			const mdActiveId = mdTabsStore.state.activeId;
			if (mdActiveId) {
				const handle = mdTabsStore.getHandle<{ openSearch: () => void }>(mdActiveId);
				handle?.openSearch();
				return;
			}
			const active = terminalsStore.getActive();
			active?.ref?.openSearch();
		},
		toggleCommandPalette: () => commandPaletteStore.toggle(),
		toggleActivityDashboard: () => togglePanel("activity"),
		toggleWorktreeManager: () => worktreeManagerStore.toggle(),
		toggleBranchSwitcher: () => branchSwitcherStore.toggle(),
		toggleErrorLog: () => errorLogStore.toggle(),
		toggleBranchesTab: () => (uiStore.isDetached("git") ? togglePanel("git") : uiStore.toggleGitPanelOnTab("branches")),
		toggleAiChatPanel: () => togglePanel("ai-chat"),
		toggleMcpPopup: () => mcpPopupStore.toggle(),
		toggleGlobalWorkspace: () => {
			if (!globalWorkspaceStore.hasPromoted()) return;
			const repoPath = repositoriesStore.state.activeRepoPath;
			const repo = repoPath ? repositoriesStore.state.repositories[repoPath] : null;
			const key = repoPath && repo?.activeBranch ? paneLayoutKey(repoPath, repo.activeBranch) : undefined;
			if (globalWorkspaceStore.isActive()) {
				globalWorkspaceStore.deactivate(key);
			} else {
				globalWorkspaceStore.activate(key);
			}
		},
		toggleDiffScroll: () => {
			// Open a diff tab in scroll mode for the active repo
			const repoPath = repositoriesStore.state.activeRepoPath;
			if (!repoPath) return;
			uiStore.setDiffViewMode("scroll");
			diffTabsStore.add(repoPath, "", "M");
		},
		openFile: () => {
			const defaultPath = gitOps.activeWorktreePath() || repositoriesStore.state.activeRepoPath || undefined;
			(async () => {
				try {
					const selected = await openDialog({ multiple: false, directory: false, defaultPath });
					if (typeof selected === "string") handleOpenFilePath(selected);
				} catch (err) {
					appLogger.error("app", "Open file dialog failed", err);
				}
			})();
		},
		openFolder: () => {
			const defaultPath = gitOps.activeWorktreePath() || repositoriesStore.state.activeRepoPath || undefined;
			(async () => {
				try {
					const selected = await openDialog({ multiple: false, directory: true, defaultPath });
					if (typeof selected === "string") revealFolderInBrowser(selected);
				} catch (err) {
					appLogger.error("app", "Open folder dialog failed", err);
				}
			})();
		},
		openPath: () => {
			(async () => {
				try {
					const typed = await promptOpenPath();
					if (!typed) return;
					const stat = await invoke<{ exists: boolean; is_dir: boolean }>("stat_path", { path: typed });
					if (!stat.exists) {
						appLogger.warn("app", "Open Path: path does not exist", { path: typed });
						return;
					}
					if (stat.is_dir) revealFolderInBrowser(typed);
					else handleOpenFilePath(typed);
				} catch (err) {
					appLogger.error("app", "Open path failed", err);
				}
			})();
		},
		openSecondaryWindow: () => {
			invoke("open_secondary_window", {}).catch((err) =>
				appLogger.error("app", "Failed to open secondary window", err),
			);
		},
		toggleCommandOverview: () => {
			mdTabsStore.addCommandOverview();
		},
		openAiTriage: () => {
			if (settingsStore.isAiTriageEnabled()) uiStore.toggleAiTriagePanel();
		},
		toggleComposePanel: () => {
			const active = terminalsStore.getActive();
			active?.ref?.toggleCompose();
		},
		detachActivityDashboard: () => {
			if (!isTauri()) return;
			detachPanel("activity").catch((e) =>
				appLogger.error("app", "Failed to detach Activity Dashboard", { error: String(e) }),
			);
		},
		toggleProcessManager: () => setShowProcessManager((v) => !v),
		toggleGenerators: () => setShowGenerators((v) => !v),
		blockPrev: () => {
			const term = terminalsStore.getActive();
			if (!term?.ref || term.commandBlocks.length === 0) return;
			const sessionId = term.ref.getSessionId();
			if (!sessionId) return;
			invoke<[number, number, number]>("terminal_scroll_info", { sessionId })
				.then(([offset, total]) => {
					const viewTop = total - offset;
					const blocks = term.commandBlocks;
					for (let i = blocks.length - 1; i >= 0; i--) {
						if (blocks[i].promptLine < viewTop - 1) {
							term.ref!.scrollToLine(blocks[i].promptLine);
							return;
						}
					}
				})
				.catch(() => {});
		},
		blockNext: () => {
			const term = terminalsStore.getActive();
			if (!term?.ref || term.commandBlocks.length === 0) return;
			const sessionId = term.ref.getSessionId();
			if (!sessionId) return;
			invoke<[number, number, number]>("terminal_scroll_info", { sessionId })
				.then(([offset, total]) => {
					const viewTop = total - offset;
					const blocks = term.commandBlocks;
					for (const block of blocks) {
						if (block.promptLine > viewTop + 1) {
							term.ref!.scrollToLine(block.promptLine);
							return;
						}
					}
					term.ref!.scrollToBottom();
				})
				.catch(() => {});
		},
		blockFoldToggle: () => {
			const term = terminalsStore.getActive();
			if (!term?.ref || term.commandBlocks.length === 0) return;
			const sessionId = term.ref.getSessionId();
			if (!sessionId) return;
			invoke<[number, number, number]>("terminal_scroll_info", { sessionId })
				.then(([offset, total, screenRows]) => {
					const viewCenter = total - offset + Math.floor(screenRows / 2);
					const blocks = term.commandBlocks;
					let nearest = blocks[0];
					let bestDist = Math.abs(nearest.promptLine - viewCenter);
					for (let i = 1; i < blocks.length; i++) {
						const dist = Math.abs(blocks[i].promptLine - viewCenter);
						if (dist < bestDist) {
							nearest = blocks[i];
							bestDist = dist;
						}
					}
					terminalsStore.toggleBlockFold(term.id, nearest.promptLine);
				})
				.catch(() => {});
		},
		blockSearchToggle: () => {
			const active = terminalsStore.getActive();
			active?.ref?.openSearch();
		},
		newFile: () => {
			const defaultPath = gitOps.activeWorktreePath() || repositoriesStore.state.activeRepoPath || undefined;
			(async () => {
				try {
					// Prompt for name+location upfront. The file is created empty and routed
					// through handleOpenFilePath so existing extension-based dispatch applies.
					const target = await saveDialog({ title: "New File", defaultPath });
					if (typeof target !== "string") return;
					await invoke("write_external_file", { path: target, content: "" });
					handleOpenFilePath(target);
				} catch (err) {
					appLogger.error("app", "New file creation failed", err);
				}
			})();
		},
	};

	// Worktree manager action callbacks
	const worktreeActions: WorktreeActions = {
		onOpenTerminal: (repoPath, branchName) => {
			void gitOps.handleAddTerminalToBranch(repoPath, branchName);
		},
		onDelete: (repoPath, branchName) => {
			void gitOps.handleRemoveBranch(repoPath, branchName);
		},
		onMergeAndArchive: (repoPath, branchName) => {
			const repoState = repositoriesStore.get(repoPath);
			const mainBranch = repoState ? Object.values(repoState.branches).find((b) => b.isMain)?.name : undefined;
			if (!mainBranch) {
				setStatusInfo("Cannot merge: no main branch found");
				return;
			}
			const effective = repoSettingsStore.getEffective(repoPath);
			const afterMerge = effective?.afterMerge ?? "archive";
			void gitOps.handleMergeAndArchive(repoPath, branchName, mainBranch, afterMerge);
		},
	};

	// Action entries for the command palette: static registry + dynamic entries
	const actionEntries = createMemo(() => {
		const entries = getActionEntries(shortcutHandlers);

		// Dynamic: one entry per active (non-parked) repo for quick switching
		const repos = Object.values(repositoriesStore.state.repositories);
		for (const repo of repos) {
			if (repo.parked) continue;
			entries.push({
				id: `switch-repo:${repo.path}`,
				label: repo.displayName,
				category: "Repository",
				keybinding: "",
				execute: () => {
					repositoriesStore.setActive(repo.path);
					const branch = repo.activeBranch || Object.keys(repo.branches)[0];
					if (branch) gitOps.handleBranchSelect(repo.path, branch);
				},
			});
		}

		// Dynamic: one entry per parked repo for unparking
		for (const repo of repos) {
			if (!repo.parked) continue;
			entries.push({
				id: `unpark-repo:${repo.path}`,
				label: `Unpark: ${repo.displayName}`,
				category: "Repository",
				keybinding: "",
				execute: () => {
					repositoriesStore.setPark(repo.path, false);
					repositoriesStore.setActive(repo.path);
					const branch = repo.activeBranch || Object.keys(repo.branches)[0];
					if (branch) gitOps.handleBranchSelect(repo.path, branch);
				},
			});
		}

		// Dynamic: park/unpark groups
		for (const group of Object.values(repositoriesStore.state.groups)) {
			if (group.repoOrder.length === 0) continue;
			const allParked = repositoriesStore.isGroupFullyParked(group.id);
			entries.push({
				id: allParked ? `unpark-group:${group.id}` : `park-group:${group.id}`,
				label: allParked ? `Unpark Group: ${group.name}` : `Park Group: ${group.name}`,
				category: "Repository",
				keybinding: "",
				execute: () => repositoriesStore.setParkGroup(group.id, !allParked),
			});
		}

		// Static extra actions (no keybinding)
		entries.push({
			id: "add-repository",
			label: "Add Repository",
			category: "Repository",
			keybinding: "",
			execute: () => gitOps.handleAddRepo(),
		});
		entries.push({
			id: "check-for-updates",
			label: "Check for Updates",
			category: "Application",
			keybinding: "",
			execute: () => updaterStore.checkForUpdate().catch((err) => appLogger.warn("app", "Updater check failed", err)),
		});
		entries.push({
			id: "reset-panel-sizes",
			label: "Reset Panel Sizes",
			category: "Application",
			keybinding: "",
			execute: () => splitPanes.resetLayout(),
		});

		// Search mode shortcuts — open palette with pre-filled prefix
		entries.push({
			id: "search-terminals",
			label: "Search Terminals",
			category: "Search",
			keybinding: "",
			execute: () => commandPaletteStore.openWithQuery("~ "),
		});
		entries.push({
			id: "search-files",
			label: "Search Files",
			category: "Search",
			keybinding: "",
			execute: () => commandPaletteStore.openWithQuery("! "),
		});
		entries.push({
			id: "search-file-contents",
			label: "Search in File Contents",
			category: "Search",
			keybinding: "",
			execute: () => commandPaletteStore.openWithQuery("? "),
		});

		// Dynamic: one entry per non-built-in plugin for enable/disable toggle
		for (const plugin of pluginStore.state.plugins) {
			if (plugin.builtIn) continue;
			const name = plugin.manifest?.name ?? plugin.id;
			entries.push({
				id: `toggle-plugin:${plugin.id}`,
				label: `${plugin.enabled ? "Disable" : "Enable"} plugin: ${name}`,
				category: "Plugins",
				keybinding: "",
				execute: () => {
					const fresh = pluginStore.getPlugin(plugin.id);
					if (!fresh) return;
					pluginStore
						.setEnabled(plugin.id, !fresh.enabled)
						.catch((err) => appLogger.error("plugin", `Failed to toggle plugin ${name}`, err));
				},
			});
		}

		// Dynamic: worktree move targets for the active terminal
		const activeTermId = terminalsStore.state.activeId;
		if (activeTermId) {
			for (const wt of gitOps.getWorktreeTargets(activeTermId)) {
				entries.push({
					id: `move-to-worktree:${wt.path}`,
					label: `Move to worktree: ${wt.branchName}`,
					category: "Terminal",
					keybinding: "",
					execute: () => gitOps.moveTerminalToWorktree(activeTermId, wt.path),
				});
			}
		}

		// Dynamic: smart prompts with command-palette placement
		for (const prompt of promptLibraryStore.getSmartByPlacement("command-palette")) {
			const p = prompt; // capture for closure
			entries.push({
				id: `smart:${p.id}`,
				label: `Smart: ${p.name}`,
				category: "Smart Prompts",
				keybinding: p.shortcut ?? "",
				execute: () => {
					smartPrompts
						.executeSmartPrompt(p)
						.catch((err) => appLogger.error("prompts", "Smart prompt execution failed", err));
				},
			});
		}

		// Dynamic: plugin-registered terminal actions (context menu + multi-target)
		for (const action of contextMenuActionsStore.getActions()) {
			entries.push({
				id: `plugin-action:${action.id}`,
				label: action.label,
				category: "Plugins",
				keybinding: "",
				execute: () => {
					const activeId = terminalsStore.state.activeId;
					const terminal = activeId ? terminalsStore.get(activeId) : null;
					action.action({ sessionId: terminal?.sessionId ?? null, repoPath: null });
				},
			});
		}

		return entries;
	});

	// Keyboard shortcuts
	createEffect(() => {
		const cleanup = useKeyboardShortcuts(shortcutHandlers);
		onCleanup(cleanup);
	});

	// Native menu bar events — dispatches to the same handlers as keyboard shortcuts
	createEffect(() => {
		let unlisten: (() => void) | undefined;

		listen<string>("menu-action", (event) => {
			setLastMenuActionTime(Date.now());
			const action = event.payload;

			switch (action) {
				// File
				case "new-tab":
					// Same as Cmd+T / + button: register in active branch so the tab
					// shows immediately (#81).
					gitOps.handleNewTab();
					break;
				case "close-tab": {
					if (paneLayoutStore.isSplit()) {
						splitPanes.closeActivePane();
					} else {
						const activeId = terminalsStore.state.activeId;
						if (activeId) terminalLifecycle.closeTerminal(activeId);
					}
					break;
				}
				case "reopen-closed-tab":
					terminalLifecycle.reopenClosedTab();
					break;
				case "new-file":
					shortcutHandlers.newFile();
					break;
				case "open-file":
					shortcutHandlers.openFile();
					break;
				case "open-folder":
					shortcutHandlers.openFolder();
					break;
				case "open-path":
					shortcutHandlers.openPath();
					break;
				case "settings":
					setSettingsPanelVisible((v) => !v);
					break;
				case "quit-app": {
					if (settingsStore.state.confirmBeforeQuit) {
						const activeTerminals = terminalsStore.getIds().filter((id) => terminalsStore.get(id)?.sessionId);
						if (activeTerminals.length > 0) {
							setQuitDialogVisible(true);
							break;
						}
					}
					void forceQuit();
					break;
				}

				// Edit
				case "copy":
					terminalLifecycle.copyFromTerminal();
					break;
				// No "paste" case: the Edit > Paste menu item is the native PredefinedMenuItem,
				// which pastes via NSPasteboard into the focused keyInputRef without a JS
				// clipboard read (avoids the macOS Sequoia paste-permission popup).
				case "clear-terminal":
					terminalLifecycle.clearTerminal();
					break;
				case "clear-scrollback":
					terminalLifecycle.clearScrollback();
					break;
				case "refresh-terminal":
					terminalLifecycle.refreshTerminal();
					break;
				case "find-in-terminal":
					shortcutHandlers.findInTerminal();
					break;

				// View
				case "toggle-sidebar":
					uiStore.toggleSidebar();
					break;
				case "split-right":
					splitPanes.handleSplit("vertical");
					break;
				case "split-down":
					splitPanes.handleSplit("horizontal");
					break;
				case "zoom-in":
					shortcutHandlers.zoomIn();
					break;
				case "zoom-out":
					shortcutHandlers.zoomOut();
					break;
				case "zoom-reset":
					shortcutHandlers.zoomReset();
					break;
				case "zoom-in-all":
					terminalLifecycle.zoomInAll();
					break;
				case "zoom-out-all":
					terminalLifecycle.zoomOutAll();
					break;
				case "zoom-reset-all":
					terminalLifecycle.zoomResetAll();
					break;
				case "diff-panel":
					togglePanel("git");
					break;
				case "markdown-panel":
					uiStore.toggleMarkdownPanel();
					break;
				case "notes-panel":
					uiStore.toggleNotesPanel();
					break;
				case "file-browser":
					uiStore.toggleFileBrowserPanel();
					break;
				case "outline-panel":
					uiStore.toggleOutlinePanel();
					break;
				case "ai-chat":
					if (settingsStore.isAiChatEnabled()) shortcutHandlers.toggleAiChatPanel();
					break;
				case "compose-panel":
					shortcutHandlers.toggleComposePanel();
					break;
				case "zoom-pane":
					splitPanes.toggleZoomPane();
					break;
				case "focus-mode":
					uiStore.toggleFocusMode();
					break;
				case "global-workspace":
					shortcutHandlers.toggleGlobalWorkspace();
					break;

				// Go
				case "next-tab":
					terminalLifecycle.navigateTab("next");
					break;
				case "prev-tab":
					terminalLifecycle.navigateTab("prev");
					break;

				case "block-prev":
					shortcutHandlers.blockPrev();
					break;
				case "block-next":
					shortcutHandlers.blockNext();
					break;
				case "block-fold-toggle":
					shortcutHandlers.blockFoldToggle();
					break;
				case "block-search-toggle":
					shortcutHandlers.blockSearchToggle();
					break;

				// Tools
				case "prompt-library":
					promptLibraryStore.toggleDrawer();
					break;
				case "run-command":
					gitOps.handleRunCommand(false, () => setRunCommandDialogVisible(true));
					break;
				case "edit-run-command":
					gitOps.handleRunCommand(true, () => setRunCommandDialogVisible(true));
					break;
				case "git-operations":
					togglePanel("git");
					break;
				case "diff-scroll": {
					const repoPath = repositoriesStore.state.activeRepoPath;
					if (repoPath) {
						uiStore.setDiffViewMode("scroll");
						diffTabsStore.add(repoPath, "", "M");
					}
					break;
				}
				case "branches":
					uiStore.isDetached("git") ? togglePanel("git") : uiStore.toggleGitPanelOnTab("branches");
					break;
				case "worktree-manager":
					shortcutHandlers.toggleWorktreeManager();
					break;
				case "quick-branch-switch":
					shortcutHandlers.toggleBranchSwitcher();
					break;
				case "task-queue":
					setTaskQueueVisible((v) => !v);
					break;
				case "content-search":
					commandPaletteStore.open();
					commandPaletteStore.setQuery("?");
					break;
				case "tunnels":
					tunnelPanelStore.toggle();
					break;
				case "process-manager":
					setShowProcessManager((v) => !v);
					break;

				// Help
				case "help-panel":
					setHelpPanelVisible((v) => !v);
					break;
				case "command-palette":
					commandPaletteStore.toggle();
					break;
				case "activity-dashboard":
					shortcutHandlers.toggleActivityDashboard();
					break;
				case "error-log":
					errorLogStore.toggle();
					break;
				case "mcp-popup":
					mcpPopupStore.toggle();
					break;
				case "check-for-updates":
					updaterStore.checkForUpdate().catch((err) => appLogger.warn("app", "Updater manual check failed", err));
					break;
				case "online-guide":
					handleOpenUrl("https://tuicommander.com/docs/");
					break;
				case "changelog":
					handleOpenUrl("https://github.com/sstraus/tuicommander/blob/main/CHANGELOG.md");
					break;
				case "about":
					setHelpPanelVisible(true);
					break;

				default: {
					// switch-tab-1 through switch-tab-9
					const tabMatch = action.match(/^switch-tab-(\d)$/);
					if (tabMatch) {
						const index = parseInt(tabMatch[1], 10) - 1;
						const ids = terminalLifecycle.terminalIds();
						if (index < ids.length) terminalLifecycle.handleTerminalSelect(ids[index]);
					}
					break;
				}
			}
		})
			.then((fn) => {
				unlisten = fn;
			})
			.catch((err) => appLogger.error("app", "Failed to register menu-action listener", err));

		onCleanup(() => unlisten?.());
	});

	// Ctrl+Tab / Ctrl+Shift+Tab — intercepted at Cocoa NSEvent level on macOS
	// because WKWebView swallows these before JS keydown fires.
	createEffect(() => {
		let unlisten: (() => void) | undefined;
		listen<string>("ctrl-tab", (event) => {
			const dir = event.payload;
			if (dir !== "prev" && dir !== "next") return;
			terminalLifecycle.navigateTab(dir);
		})
			.then((fn) => {
				unlisten = fn;
			})
			.catch((err) => appLogger.error("app", "Failed to register ctrl-tab listener", err));
		onCleanup(() => unlisten?.());
	});

	// Push-to-talk hotkey handler.
	// For Fn/Globe key: listens for Tauri events from the native macOS monitor.
	// For other keys: uses DOM keydown/keyup events.
	// Pauses while capturingHotkey is true so the settings UI can capture a new key.
	createEffect(() => {
		const hotkey = dictationStore.state.hotkey;
		const capturing = dictationStore.state.capturingHotkey;
		const longPressMs = dictationStore.state.longPressMs;
		if (!dictationStore.state.enabled || !hotkey || capturing) return;

		const handler = createLongPressHandlerFromHotkey(hotkey, longPressMs, {
			onStart: () => dictation.handleDictationStart(),
			onStop: () => dictation.handleDictationStop(),
		});
		if (!handler) return;

		let cleanupListeners: () => void;

		if (hotkey === "Fn") {
			// Fn/Globe key: native monitor emits Tauri events (macOS only)
			let cancelled = false;
			let unDown: (() => void) | undefined;
			let unUp: (() => void) | undefined;
			if (isTauri()) {
				listen("fn-key-down", () => {
					handler.handleEvent({ eventType: "KeyPress", key: "Fn" });
				})
					.then((fn) => {
						cancelled ? fn() : (unDown = fn);
					})
					.catch((err) => appLogger.error("dictation", "Failed to listen for fn-key-down", err));
				listen("fn-key-up", () => {
					handler.handleEvent({ eventType: "KeyRelease", key: "Fn" });
				})
					.then((fn) => {
						cancelled ? fn() : (unUp = fn);
					})
					.catch((err) => appLogger.error("dictation", "Failed to listen for fn-key-up", err));
			}
			cleanupListeners = () => {
				cancelled = true;
				unDown?.();
				unUp?.();
			};
		} else {
			// Regular keys: DOM events. event.code naming matches our format.
			const hasModifiers = hotkey.includes("+");
			const onKeyDown = (e: KeyboardEvent) => {
				const consumed = handler.handleEvent({ eventType: "KeyPress", key: e.code });
				if (consumed && (hasModifiers || e.repeat)) {
					e.preventDefault();
				}
			};
			const onKeyUp = (e: KeyboardEvent) => {
				handler.handleEvent({ eventType: "KeyRelease", key: e.code });
			};
			window.addEventListener("keydown", onKeyDown);
			window.addEventListener("keyup", onKeyUp);
			cleanupListeners = () => {
				window.removeEventListener("keydown", onKeyDown);
				window.removeEventListener("keyup", onKeyUp);
			};
		}

		onCleanup(() => {
			handler.cleanup();
			cleanupListeners();
		});
	});

	// Secondary window: minimal pane-only layout
	if (isSecondaryWindow()) {
		return (
			<div id="app" class="sidebar-hidden secondary-window">
				<div id="app-body">
					<main id="main">
						<TerminalArea
							onTerminalFocus={terminalLifecycle.handleTerminalFocus}
							onCloseTab={terminalLifecycle.closeTerminal}
							onOpenFilePath={handleOpenFilePath}
							onContextMenu={contextMenu.open}
							onCwdChange={gitOps.handleTerminalCwdChange}
							onNewTerminal={(groupId) => {
								paneLayoutStore.setActiveGroup(groupId);
								gitOps.handleNewTab();
							}}
						/>
					</main>
				</div>
			</div>
		);
	}

	return (
		<div
			id="app"
			classList={{
				"sidebar-hidden": !uiStore.state.sidebarVisible,
				"focus-mode": uiStore.state.focusMode,
			}}
		>
			<MobileViewBanner />
			{/* Toolbar - drag region spanning full width */}
			<Toolbar
				repoPath={gitOps.currentRepoPath()}
				runCommand={gitOps.activeRunCommand()}
				onBranchClick={() => {
					const activeRepo = repositoriesStore.getActive();
					if (activeRepo?.activeBranch) {
						gitOps.handleOpenRenameBranchDialog(activeRepo.path, activeRepo.activeBranch);
						setRenameBranchDialogVisible(true);
					}
				}}
				onRun={(shiftKey) => gitOps.handleRunCommand(shiftKey, () => setRunCommandDialogVisible(true))}
				onReviewPr={gitOps.handleReviewPr}
				onOpenSettings={() => setSettingsPanelVisible(true)}
				onShowWhatsNew={(v) => setWhatsNewVersion(v)}
			/>

			{/* Body: sidebar + main content side by side */}
			<div id="app-body">
				{/* Sidebar - always mounted, hidden via CSS */}
				<Sidebar
					quickSwitcherActive={quickSwitcherVisible()}
					onBranchSelect={gitOps.handleBranchSelect}
					onAddTerminal={gitOps.handleAddTerminalToBranch}
					onRemoveBranch={gitOps.handleRemoveBranch}
					onRenameBranch={(repoPath, branchName) => {
						gitOps.handleOpenRenameBranchDialog(repoPath, branchName);
						setRenameBranchDialogVisible(true);
					}}
					onCreateBranch={(repoPath, fromBranch) => {
						gitOps.handleOpenCreateBranchDialog(repoPath, fromBranch);
						setCreateBranchDialogVisible(true);
					}}
					onAddWorktree={gitOps.handleAddWorktree}
					onCreateWorktreeFromBranch={gitOps.handleCreateWorktreeFromBranch}
					onMergeAndArchive={(repoPath, branchName) => {
						const repoState = repositoriesStore.get(repoPath);
						const mainBranch = repoState ? Object.values(repoState.branches).find((b) => b.isMain)?.name : undefined;
						if (!mainBranch) {
							setStatusInfo("Cannot merge: no main branch found");
							return;
						}
						const effective = repoSettingsStore.getEffective(repoPath);
						const afterMerge = effective?.afterMerge ?? "archive";
						gitOps.handleMergeAndArchive(repoPath, branchName, mainBranch, afterMerge);
					}}
					creatingWorktreeRepos={gitOps.creatingWorktreeRepos()}
					removingBranches={gitOps.removingBranches()}
					onAddRepo={gitOps.handleAddRepo}
					onAddRemoteRepo={gitOps.handleAddRemoteRepo}
					onRepoSettings={(repoPath) =>
						gitOps.handleRepoSettings(repoPath, (ctx) => {
							setSettingsContext(ctx);
							setSettingsInitialTab(undefined);
							setSettingsPanelVisible(true);
						})
					}
					onRemoveRepo={gitOps.handleRemoveRepo}
					onOpenSettings={() => openSettings()}
					onOpenHelp={() => setHelpPanelVisible(true)}
					buildAgentMenuItems={buildSidebarAgentMenuItems}
					onRefreshBranchStats={gitOps.refreshAllBranchStats}
					onCheckoutRemoteBranch={gitOps.handleCheckoutRemoteBranch}
					onSwitchBranch={gitOps.handleSwitchBranch}
					switchBranchLists={gitOps.switchBranchLists()}
					currentBranches={gitOps.currentBranches()}
					onBackgroundGit={handleBackgroundGit}
					runningGitOps={runningGitOps()}
					onReviewPr={gitOps.handleReviewPr}
				/>

				{/* Main content */}
				<main id="main">
					{/* Tab bar */}
					<div id="tab-bar">
						<TabBar
							quickSwitcherActive={quickSwitcherVisible()}
							onTabSelect={terminalLifecycle.handleTerminalSelect}
							onTabClose={terminalLifecycle.closeTerminal}
							onCloseOthers={terminalLifecycle.closeOtherTabs}
							onCloseToRight={terminalLifecycle.closeTabsToRight}
							onNewTab={gitOps.handleNewTab}
							onSplitVertical={() => splitPanes.handleSplit("vertical")}
							onSplitHorizontal={() => splitPanes.handleSplit("horizontal")}
							onReorder={(from, to) => {
								const activeRepo = repositoriesStore.getActive();
								if (activeRepo?.activeBranch) {
									repositoriesStore.reorderTerminals(activeRepo.path, activeRepo.activeBranch, from, to);
								}
							}}
							onDetachTab={handleDetachTab}
							onReattachTab={handleReattachTab}
							onFocusDetachedTab={handleFocusDetachedTab}
							getWorktreeTargets={gitOps.getWorktreeTargets}
							onMoveToWorktree={gitOps.moveTerminalToWorktree}
						/>
					</div>

					{/* Terminal container - render ALL terminals so they never unmount (preserves PTY sessions) */}
					<TerminalArea
						onTerminalFocus={terminalLifecycle.handleTerminalFocus}
						onCloseTab={terminalLifecycle.closeTerminal}
						onOpenFilePath={handleOpenFilePath}
						onContextMenu={contextMenu.open}
						onCwdChange={gitOps.handleTerminalCwdChange}
						onNewTerminal={(groupId) => {
							paneLayoutStore.setActiveGroup(groupId);
							gitOps.handleNewTab();
						}}
					>
						{/* Side panels (right panes inside #terminal-container) */}
						<PanelOrchestrator
							repoPath={gitOps.currentRepoPath() || null}
							fsRoot={gitOps.activeWorktreePath() || null}
							onFileOpen={(fsRoot, filePath, line) => {
								const repoPath = gitOps.currentRepoPath() || fsRoot;
								openFileAction(filePath, repoPath, fsRoot || undefined, line, (tabId) => {
									terminalLifecycle.handleTerminalSelect(tabId);
								});
							}}
						/>
					</TerminalArea>

					{/* Status bar */}
					<StatusBar
						fontSize={terminalLifecycle.activeFontSize()}
						defaultFontSize={getDefaultFontSize()}
						statusInfo={statusInfo()}
						onToggleDiff={() => togglePanel("git")}
						onToggleMarkdown={() => uiStore.toggleMarkdownPanel()}
						onToggleNotes={() => uiStore.toggleNotesPanel()}
						onToggleFileBrowser={() => uiStore.toggleFileBrowserPanel()}
						onToggleAiChat={() => togglePanel("ai-chat")}
						onToggleErrorLog={() => errorLogStore.toggle()}
						onDictationStart={dictation.handleDictationStart}
						onDictationStop={dictation.handleDictationStop}
						currentRepoPath={
							globalWorkspaceStore.isActive()
								? terminalsStore.state.activeId
									? (repositoriesStore.getRepoPathForTerminal(terminalsStore.state.activeId) ?? undefined)
									: undefined
								: gitOps.currentRepoPath()
						}
						cwd={terminalsStore.getActive()?.cwd || gitOps.activeWorktreePath()}
						repoRoot={gitOps.activeWorktreePath()}
						onBranchRenamed={(oldName, newName) => {
							const repoPath = gitOps.currentRepoPath();
							if (repoPath) {
								repositoriesStore.renameBranch(repoPath, oldName, newName);
							}
							if (gitOps.currentBranch() === oldName) {
								gitOps.setCurrentBranch(newName);
							}
							setStatusInfo(`Renamed branch ${oldName} to ${newName}`);
						}}
						onReviewPr={gitOps.handleReviewPr}
					/>
				</main>
			</div>

			{/* Prompt overlay */}
			<PromptOverlay />

			{/* AI knowledge history overlay */}
			<KnowledgeHistoryOverlay />

			{/* Dictation streaming toast — shows partial transcription */}
			<DictationToast />
			<ToastContainer />

			{/* Prompt library drawer */}
			<PromptDrawer />

			{/* Command palette (Tauri only — many actions are Tauri-specific) */}
			<Show when={isTauri()}>
				<CommandPalette actions={actionEntries()} />
			</Show>

			{/* Quick branch switcher */}
			<BranchSwitcher
				activeRepoPath={repositoriesStore.state.activeRepoPath ?? undefined}
				onSelect={(repoPath, branchName) => {
					// If the branch exists in the store (has a worktree), just switch UI view.
					// Otherwise it's a regular branch needing a real git checkout.
					const branch = repositoriesStore.get(repoPath)?.branches[branchName];
					if (branch) {
						gitOps.handleBranchSelect(repoPath, branchName);
					} else {
						gitOps.handleSwitchBranch(repoPath, branchName);
					}
				}}
				onCheckoutRemote={gitOps.handleCheckoutRemoteBranch}
			/>

			{/* Activity dashboard — unmount when closed to release memos/subscriptions */}
			<Show when={!uiStore.isDetached("activity") && activityDashboardStore.state.isOpen}>
				<Suspense>
					<ActivityDashboard onSelect={terminalLifecycle.handleTerminalSelect} />
				</Suspense>
			</Show>

			{/* SSH Tunnels panel (experimental) */}
			<Show when={settingsStore.state.experimentalFeaturesEnabled}>
				<Suspense>
					<TunnelsPanel />
				</Suspense>
			</Show>

			{/* Worktree manager */}
			<WorktreeManager actions={worktreeActions} />

			{/* MCP servers popup (per-repo) */}
			<McpPopup onOpenSettings={openSettings} />

			{/* Error log panel */}
			<ErrorLogPanel />

			{/* Settings panel */}
			<Suspense>
				<SettingsPanel
					visible={settingsPanelVisible()}
					onClose={() => setSettingsPanelVisible(false)}
					initialTab={settingsInitialTab()}
					context={settingsContext()}
				/>
			</Suspense>

			{/* Task queue panel */}
			<TaskQueuePanel visible={taskQueueVisible()} onClose={() => setTaskQueueVisible(false)} />

			{/* Context menu */}
			<ContextMenu
				items={getContextMenuItems()}
				x={contextMenu.position().x}
				y={contextMenu.position().y}
				visible={contextMenu.visible()}
				onClose={contextMenu.close}
			/>

			{/* Rename branch dialog */}
			<RenameBranchDialog
				visible={renameBranchDialogVisible()}
				currentName={gitOps.branchToRename()?.branchName || ""}
				onClose={() => {
					setRenameBranchDialogVisible(false);
					gitOps.setBranchToRename(null);
				}}
				onRename={gitOps.handleRenameBranch}
			/>

			{/* Create branch dialog */}
			<CreateBranchDialog
				visible={createBranchDialogVisible()}
				startPoint={gitOps.branchToCreate()?.startPoint}
				onClose={() => {
					setCreateBranchDialogVisible(false);
					gitOps.setBranchToCreate(null);
				}}
				onCreate={gitOps.handleCreateBranch}
			/>

			{/* Create worktree dialog */}
			<CreateWorktreeDialog
				visible={gitOps.worktreeDialogState() !== null}
				suggestedName={gitOps.worktreeDialogState()?.suggestedName ?? ""}
				existingBranches={gitOps.worktreeDialogState()?.existingBranches ?? []}
				worktreeBranches={gitOps.worktreeDialogState()?.worktreeBranches ?? []}
				worktreesDir={gitOps.worktreeDialogState()?.worktreesDir ?? ""}
				baseRefs={gitOps.worktreeDialogState()?.baseRefs}
				onGenerateName={gitOps.generateWorktreeName}
				onClose={() => gitOps.setWorktreeDialogState(null)}
				onCreate={gitOps.confirmCreateWorktree}
			/>

			{/* Run command dialog */}
			<RunCommandDialog
				visible={runCommandDialogVisible()}
				savedCommand={gitOps.activeRunCommand() || ""}
				onClose={() => setRunCommandDialogVisible(false)}
				onSaveAndRun={(command) => {
					setRunCommandDialogVisible(false);
					gitOps.executeRunCommand(command);
				}}
			/>

			{/* Terminal rename prompt */}
			<PromptDialog
				visible={termRenamePromptVisible()}
				title="Terminal Title"
				placeholder="Enter title"
				defaultValue={termRenameDefault()}
				confirmLabel="Rename"
				onClose={() => setTermRenamePromptVisible(false)}
				onConfirm={(newName) => {
					const activeId = terminalsStore.state.activeId;
					if (activeId && newName !== termRenameDefault()) {
						terminalsStore.update(activeId, { name: newName, nameIsCustom: true });
					}
				}}
			/>

			{/* Open arbitrary path (file or folder) */}
			<PromptDialog
				visible={openPathPromptVisible()}
				title="Open Path"
				placeholder="Absolute path to file or folder"
				confirmLabel="Open"
				onClose={() => {
					setOpenPathPromptVisible(false);
					openPathPromptResolve?.(null);
					openPathPromptResolve = null;
				}}
				onConfirm={(path) => {
					setOpenPathPromptVisible(false);
					openPathPromptResolve?.(path);
					openPathPromptResolve = null;
				}}
			/>

			{/* Add repository: path prompt (browser mode only) */}
			<PromptDialog
				visible={repoPathPromptVisible()}
				title="Add Repository"
				placeholder="Enter absolute path to repository"
				confirmLabel="Add"
				onClose={() => {
					setRepoPathPromptVisible(false);
					repoPathPromptResolve?.(null);
					repoPathPromptResolve = null;
				}}
				onConfirm={(path) => {
					setRepoPathPromptVisible(false);
					repoPathPromptResolve?.(path);
					repoPathPromptResolve = null;
				}}
			/>

			{/* In-app confirm dialog (replaces native OS dialogs) */}
			<ConfirmDialog
				visible={dialogs.dialogState() !== null}
				title={dialogs.dialogState()?.title ?? ""}
				message={dialogs.dialogState()?.message ?? ""}
				confirmLabel={dialogs.dialogState()?.confirmLabel}
				cancelLabel={dialogs.dialogState()?.cancelLabel}
				kind={dialogs.dialogState()?.kind}
				autoCancelMs={dialogs.dialogState()?.autoCancelMs}
				onClose={dialogs.handleClose}
				onConfirm={dialogs.handleConfirm}
			/>

			{/* Folder-drop recursive transfer confirmation */}
			<ConfirmDialog
				visible={pendingFolderDrop() !== null}
				title={pendingFolderDrop()?.mode === "copy" ? "Copy folder(s)?" : "Move folder(s)?"}
				message={(() => {
					const req = pendingFolderDrop();
					if (!req) return "";
					const count = req.paths.length;
					const verb = req.mode === "copy" ? "copy" : "move";
					const items = count === 1 ? "1 item" : `${count} items`;
					return `About to recursively ${verb} ${items} into ${req.destDir}. Existing files with the same name will be skipped.`;
				})()}
				confirmLabel={pendingFolderDrop()?.mode === "copy" ? "Copy" : "Move"}
				onClose={() => setPendingFolderDrop(null)}
				onConfirm={async () => {
					const req = pendingFolderDrop();
					setPendingFolderDrop(null);
					if (!req) return;
					const { confirmFolderDrop } = await import("./hooks/useFileDrop");
					await confirmFolderDrop(req);
				}}
			/>

			{/* Process Manager modal */}
			<Show when={showProcessManager()}>
				<ProcessManagerModal onClose={() => setShowProcessManager(false)} />
			</Show>

			{/* Generators modal */}
			<Show when={showGenerators()}>
				<GeneratorsModal onClose={() => setShowGenerators(false)} />
			</Show>

			{/* What's New dialog — shown once after stable version update */}
			<WhatsNewDialog
				visible={whatsNewVersion() !== null && (whatsNewEntry()?.highlights.length ?? 0) > 0}
				version={whatsNewVersion() ?? ""}
				highlights={whatsNewEntry()?.highlights ?? []}
				contributions={whatsNewEntry()?.contributions ?? []}
				onClose={() => {
					const v = whatsNewVersion();
					if (v) invoke("set_last_seen_version", { version: v }).catch(() => {});
					setWhatsNewVersion(null);
				}}
			/>

			{/* Update download progress dialog */}
			<UpdateProgressDialog />

			{/* Post-merge worktree cleanup — shown when afterMerge=ask */}
			<Show when={gitOps.mergePendingCtx() !== null}>
				{(() => {
					const ctx = gitOps.mergePendingCtx()!;
					const repoState = repositoriesStore.get(ctx.repoPath);
					const activeBranch = repoState?.activeBranch ?? "";
					const isOnBaseBranch = activeBranch === ctx.baseBranch;
					const branchState = repoState?.branches[ctx.branchName];
					const isDefaultBranch = branchState?.isMain ?? false;
					const hasTerminals = (branchState?.terminals.length ?? 0) > 0;
					return (
						<PostMergeCleanupDialog
							branchName={ctx.branchName}
							baseBranch={ctx.baseBranch}
							repoPath={ctx.repoPath}
							isOnBaseBranch={isOnBaseBranch}
							isDefaultBranch={isDefaultBranch}
							hasTerminals={hasTerminals}
							hasDirtyFiles={ctx.hasDirtyFiles}
							worktreeAction={worktreeCleanupAction()}
							onWorktreeActionChange={setWorktreeCleanupAction}
							executing={worktreeCleanupExecuting()}
							stepStatuses={worktreeCleanupStepStatuses()}
							stepErrors={worktreeCleanupStepErrors()}
							stepNotes={worktreeCleanupStepNotes()}
							onExecute={handleWorktreeCleanupExecute}
							onSkip={handleWorktreeCleanupSkip}
						/>
					);
				})()}
			</Show>

			{/* Help panel (Story 053) */}
			<Suspense>
				<HelpPanel visible={helpPanelVisible()} onClose={() => setHelpPanelVisible(false)} />
			</Suspense>

			{/* Quit confirmation dialog (Story 057) */}
			<Show when={quitDialogVisible()}>
				<div class={qd.overlay} onClick={() => setQuitDialogVisible(false)}>
					<div class={qd.dialog} onClick={(e) => e.stopPropagation()}>
						<h3>Quit TUICommander?</h3>
						<p>
							You have {terminalsStore.getIds().filter((id) => terminalsStore.get(id)?.sessionId).length} active
							terminal session(s). Quitting will close all sessions.
						</p>
						<div class={qd.actions}>
							<button class={qd.cancel} onClick={() => setQuitDialogVisible(false)}>
								Cancel
							</button>
							<button class={qd.quit} onClick={forceQuit}>
								Quit
							</button>
						</div>
					</div>
				</div>
			</Show>
		</div>
	);
};

export default App;
