πŸ“„ File detail

components/PromptInput/PromptInput.tsx

🧩 .tsxπŸ“ 2,339 linesπŸ’Ύ 355,032 bytesπŸ“ text
← Back to all files

🎯 Use case

This file lives under β€œcomponents/”, which covers shared React UI pieces. It primarily provides a default export (component, class, or entry function). Dependencies touch bun:bundle, terminal styling, Node path helpers, and React UI. It composes internal code from buddy, commands, context, history, and hooks (relative imports).

Generated from folder role, exports, dependency roots, and inline comments β€” not hand-reviewed for every path.

🧠 Inline summary

import { feature } from 'bun:bundle'; import chalk from 'chalk'; import * as path from 'path'; import * as React from 'react'; import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';

πŸ“€ Exports (heuristic)

  • default

πŸ“š External import roots

Package roots from from "…" (relative paths omitted).

  • bun:bundle
  • chalk
  • path
  • react
  • src
  • strip-ansi

πŸ–₯️ Source preview

⚠️ Syntax highlighting applies to the first ~150k characters only (performance); the raw preview above may be longer.

import { feature } from 'bun:bundle';
import chalk from 'chalk';
import * as path from 'path';
import * as React from 'react';
import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
import { useNotifications } from 'src/context/notifications.js';
import { useCommandQueue } from 'src/hooks/useCommandQueue.js';
import { type IDEAtMentioned, useIdeAtMentioned } from 'src/hooks/useIdeAtMentioned.js';
import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js';
import { type AppState, useAppState, useAppStateStore, useSetAppState } from 'src/state/AppState.js';
import type { FooterItem } from 'src/state/AppStateStore.js';
import { getCwd } from 'src/utils/cwd.js';
import { isQueuedCommandEditable, popAllEditable } from 'src/utils/messageQueueManager.js';
import stripAnsi from 'strip-ansi';
import { companionReservedColumns } from '../../buddy/CompanionSprite.js';
import { findBuddyTriggerPositions, useBuddyNotification } from '../../buddy/useBuddyNotification.js';
import { FastModePicker } from '../../commands/fast/fast.js';
import { isUltrareviewEnabled } from '../../commands/review/ultrareviewEnabled.js';
import { getNativeCSIuTerminalDisplayName } from '../../commands/terminalSetup/terminalSetup.js';
import { type Command, hasCommand } from '../../commands.js';
import { useIsModalOverlayActive } from '../../context/overlayContext.js';
import { useSetPromptOverlayDialog } from '../../context/promptOverlayContext.js';
import { formatImageRef, formatPastedTextRef, getPastedTextRefNumLines, parseReferences } from '../../history.js';
import type { VerificationStatus } from '../../hooks/useApiKeyVerification.js';
import { type HistoryMode, useArrowKeyHistory } from '../../hooks/useArrowKeyHistory.js';
import { useDoublePress } from '../../hooks/useDoublePress.js';
import { useHistorySearch } from '../../hooks/useHistorySearch.js';
import type { IDESelection } from '../../hooks/useIdeSelection.js';
import { useInputBuffer } from '../../hooks/useInputBuffer.js';
import { useMainLoopModel } from '../../hooks/useMainLoopModel.js';
import { usePromptSuggestion } from '../../hooks/usePromptSuggestion.js';
import { useTerminalSize } from '../../hooks/useTerminalSize.js';
import { useTypeahead } from '../../hooks/useTypeahead.js';
import type { BorderTextOptions } from '../../ink/render-border.js';
import { stringWidth } from '../../ink/stringWidth.js';
import { Box, type ClickEvent, type Key, Text, useInput } from '../../ink.js';
import { useOptionalKeybindingContext } from '../../keybindings/KeybindingContext.js';
import { getShortcutDisplay } from '../../keybindings/shortcutFormat.js';
import { useKeybinding, useKeybindings } from '../../keybindings/useKeybinding.js';
import type { MCPServerConnection } from '../../services/mcp/types.js';
import { abortPromptSuggestion, logSuggestionSuppressed } from '../../services/PromptSuggestion/promptSuggestion.js';
import { type ActiveSpeculationState, abortSpeculation } from '../../services/PromptSuggestion/speculation.js';
import { getActiveAgentForInput, getViewedTeammateTask } from '../../state/selectors.js';
import { enterTeammateView, exitTeammateView, stopOrDismissAgent } from '../../state/teammateViewHelpers.js';
import type { ToolPermissionContext } from '../../Tool.js';
import { getRunningTeammatesSorted } from '../../tasks/InProcessTeammateTask/InProcessTeammateTask.js';
import type { InProcessTeammateTaskState } from '../../tasks/InProcessTeammateTask/types.js';
import { isPanelAgentTask, type LocalAgentTaskState } from '../../tasks/LocalAgentTask/LocalAgentTask.js';
import { isBackgroundTask } from '../../tasks/types.js';
import { AGENT_COLOR_TO_THEME_COLOR, AGENT_COLORS, type AgentColorName } from '../../tools/AgentTool/agentColorManager.js';
import type { AgentDefinition } from '../../tools/AgentTool/loadAgentsDir.js';
import type { Message } from '../../types/message.js';
import type { PermissionMode } from '../../types/permissions.js';
import type { BaseTextInputProps, PromptInputMode, VimMode } from '../../types/textInputTypes.js';
import { isAgentSwarmsEnabled } from '../../utils/agentSwarmsEnabled.js';
import { count } from '../../utils/array.js';
import type { AutoUpdaterResult } from '../../utils/autoUpdater.js';
import { Cursor } from '../../utils/Cursor.js';
import { getGlobalConfig, type PastedContent, saveGlobalConfig } from '../../utils/config.js';
import { logForDebugging } from '../../utils/debug.js';
import { parseDirectMemberMessage, sendDirectMemberMessage } from '../../utils/directMemberMessage.js';
import type { EffortLevel } from '../../utils/effort.js';
import { env } from '../../utils/env.js';
import { errorMessage } from '../../utils/errors.js';
import { isBilledAsExtraUsage } from '../../utils/extraUsage.js';
import { getFastModeUnavailableReason, isFastModeAvailable, isFastModeCooldown, isFastModeEnabled, isFastModeSupportedByModel } from '../../utils/fastMode.js';
import { isFullscreenEnvEnabled } from '../../utils/fullscreen.js';
import type { PromptInputHelpers } from '../../utils/handlePromptSubmit.js';
import { getImageFromClipboard, PASTE_THRESHOLD } from '../../utils/imagePaste.js';
import type { ImageDimensions } from '../../utils/imageResizer.js';
import { cacheImagePath, storeImage } from '../../utils/imageStore.js';
import { isMacosOptionChar, MACOS_OPTION_SPECIAL_CHARS } from '../../utils/keyboardShortcuts.js';
import { logError } from '../../utils/log.js';
import { isOpus1mMergeEnabled, modelDisplayString } from '../../utils/model/model.js';
import { setAutoModeActive } from '../../utils/permissions/autoModeState.js';
import { cyclePermissionMode, getNextPermissionMode } from '../../utils/permissions/getNextPermissionMode.js';
import { transitionPermissionMode } from '../../utils/permissions/permissionSetup.js';
import { getPlatform } from '../../utils/platform.js';
import type { ProcessUserInputContext } from '../../utils/processUserInput/processUserInput.js';
import { editPromptInEditor } from '../../utils/promptEditor.js';
import { hasAutoModeOptIn } from '../../utils/settings/settings.js';
import { findBtwTriggerPositions } from '../../utils/sideQuestion.js';
import { findSlashCommandPositions } from '../../utils/suggestions/commandSuggestions.js';
import { findSlackChannelPositions, getKnownChannelsVersion, hasSlackMcpServer, subscribeKnownChannels } from '../../utils/suggestions/slackChannelSuggestions.js';
import { isInProcessEnabled } from '../../utils/swarm/backends/registry.js';
import { syncTeammateMode } from '../../utils/swarm/teamHelpers.js';
import type { TeamSummary } from '../../utils/teamDiscovery.js';
import { getTeammateColor } from '../../utils/teammate.js';
import { isInProcessTeammate } from '../../utils/teammateContext.js';
import { writeToMailbox } from '../../utils/teammateMailbox.js';
import type { TextHighlight } from '../../utils/textHighlighting.js';
import type { Theme } from '../../utils/theme.js';
import { findThinkingTriggerPositions, getRainbowColor, isUltrathinkEnabled } from '../../utils/thinking.js';
import { findTokenBudgetPositions } from '../../utils/tokenBudget.js';
import { findUltraplanTriggerPositions, findUltrareviewTriggerPositions } from '../../utils/ultraplan/keyword.js';
import { AutoModeOptInDialog } from '../AutoModeOptInDialog.js';
import { BridgeDialog } from '../BridgeDialog.js';
import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js';
import { getVisibleAgentTasks, useCoordinatorTaskCount } from '../CoordinatorAgentStatus.js';
import { getEffortNotificationText } from '../EffortIndicator.js';
import { getFastIconString } from '../FastIcon.js';
import { GlobalSearchDialog } from '../GlobalSearchDialog.js';
import { HistorySearchDialog } from '../HistorySearchDialog.js';
import { ModelPicker } from '../ModelPicker.js';
import { QuickOpenDialog } from '../QuickOpenDialog.js';
import TextInput from '../TextInput.js';
import { ThinkingToggle } from '../ThinkingToggle.js';
import { BackgroundTasksDialog } from '../tasks/BackgroundTasksDialog.js';
import { shouldHideTasksFooter } from '../tasks/taskStatusUtils.js';
import { TeamsDialog } from '../teams/TeamsDialog.js';
import VimTextInput from '../VimTextInput.js';
import { getModeFromInput, getValueFromInput } from './inputModes.js';
import { FOOTER_TEMPORARY_STATUS_TIMEOUT, Notifications } from './Notifications.js';
import PromptInputFooter from './PromptInputFooter.js';
import type { SuggestionItem } from './PromptInputFooterSuggestions.js';
import { PromptInputModeIndicator } from './PromptInputModeIndicator.js';
import { PromptInputQueuedCommands } from './PromptInputQueuedCommands.js';
import { PromptInputStashNotice } from './PromptInputStashNotice.js';
import { useMaybeTruncateInput } from './useMaybeTruncateInput.js';
import { usePromptInputPlaceholder } from './usePromptInputPlaceholder.js';
import { useShowFastIconHint } from './useShowFastIconHint.js';
import { useSwarmBanner } from './useSwarmBanner.js';
import { isNonSpacePrintable, isVimModeEnabled } from './utils.js';
type Props = {
  debug: boolean;
  ideSelection: IDESelection | undefined;
  toolPermissionContext: ToolPermissionContext;
  setToolPermissionContext: (ctx: ToolPermissionContext) => void;
  apiKeyStatus: VerificationStatus;
  commands: Command[];
  agents: AgentDefinition[];
  isLoading: boolean;
  verbose: boolean;
  messages: Message[];
  onAutoUpdaterResult: (result: AutoUpdaterResult) => void;
  autoUpdaterResult: AutoUpdaterResult | null;
  input: string;
  onInputChange: (value: string) => void;
  mode: PromptInputMode;
  onModeChange: (mode: PromptInputMode) => void;
  stashedPrompt: {
    text: string;
    cursorOffset: number;
    pastedContents: Record<number, PastedContent>;
  } | undefined;
  setStashedPrompt: (value: {
    text: string;
    cursorOffset: number;
    pastedContents: Record<number, PastedContent>;
  } | undefined) => void;
  submitCount: number;
  onShowMessageSelector: () => void;
  /** Fullscreen message actions: shift+↑ enters cursor. */
  onMessageActionsEnter?: () => void;
  mcpClients: MCPServerConnection[];
  pastedContents: Record<number, PastedContent>;
  setPastedContents: React.Dispatch<React.SetStateAction<Record<number, PastedContent>>>;
  vimMode: VimMode;
  setVimMode: (mode: VimMode) => void;
  showBashesDialog: string | boolean;
  setShowBashesDialog: (show: string | boolean) => void;
  onExit: () => void;
  getToolUseContext: (messages: Message[], newMessages: Message[], abortController: AbortController, mainLoopModel: string) => ProcessUserInputContext;
  onSubmit: (input: string, helpers: PromptInputHelpers, speculationAccept?: {
    state: ActiveSpeculationState;
    speculationSessionTimeSavedMs: number;
    setAppState: (f: (prev: AppState) => AppState) => void;
  }, options?: {
    fromKeybinding?: boolean;
  }) => Promise<void>;
  onAgentSubmit?: (input: string, task: InProcessTeammateTaskState | LocalAgentTaskState, helpers: PromptInputHelpers) => Promise<void>;
  isSearchingHistory: boolean;
  setIsSearchingHistory: (isSearching: boolean) => void;
  onDismissSideQuestion?: () => void;
  isSideQuestionVisible?: boolean;
  helpOpen: boolean;
  setHelpOpen: React.Dispatch<React.SetStateAction<boolean>>;
  hasSuppressedDialogs?: boolean;
  isLocalJSXCommandActive?: boolean;
  insertTextRef?: React.MutableRefObject<{
    insert: (text: string) => void;
    setInputWithCursor: (value: string, cursor: number) => void;
    cursorOffset: number;
  } | null>;
  voiceInterimRange?: {
    start: number;
    end: number;
  } | null;
};

// Bottom slot has maxHeight="50%"; reserve lines for footer, border, status.
const PROMPT_FOOTER_LINES = 5;
const MIN_INPUT_VIEWPORT_LINES = 3;
function PromptInput({
  debug,
  ideSelection,
  toolPermissionContext,
  setToolPermissionContext,
  apiKeyStatus,
  commands,
  agents,
  isLoading,
  verbose,
  messages,
  onAutoUpdaterResult,
  autoUpdaterResult,
  input,
  onInputChange,
  mode,
  onModeChange,
  stashedPrompt,
  setStashedPrompt,
  submitCount,
  onShowMessageSelector,
  onMessageActionsEnter,
  mcpClients,
  pastedContents,
  setPastedContents,
  vimMode,
  setVimMode,
  showBashesDialog,
  setShowBashesDialog,
  onExit,
  getToolUseContext,
  onSubmit: onSubmitProp,
  onAgentSubmit,
  isSearchingHistory,
  setIsSearchingHistory,
  onDismissSideQuestion,
  isSideQuestionVisible,
  helpOpen,
  setHelpOpen,
  hasSuppressedDialogs,
  isLocalJSXCommandActive = false,
  insertTextRef,
  voiceInterimRange
}: Props): React.ReactNode {
  const mainLoopModel = useMainLoopModel();
  // A local-jsx command (e.g., /mcp while agent is running) renders a full-
  // screen dialog on top of PromptInput via the immediate-command path with
  // shouldHidePromptInput: false. Those dialogs don't register in the overlay
  // system, so treat them as a modal overlay here to stop navigation keys from
  // leaking into TextInput/footer handlers and stacking a second dialog.
  const isModalOverlayActive = useIsModalOverlayActive() || isLocalJSXCommandActive;
  const [isAutoUpdating, setIsAutoUpdating] = useState(false);
  const [exitMessage, setExitMessage] = useState<{
    show: boolean;
    key?: string;
  }>({
    show: false
  });
  const [cursorOffset, setCursorOffset] = useState<number>(input.length);
  // Track the last input value set via internal handlers so we can detect
  // external input changes (e.g. speech-to-text injection) and move cursor to end.
  const lastInternalInputRef = React.useRef(input);
  if (input !== lastInternalInputRef.current) {
    // Input changed externally (not through any internal handler) β€” move cursor to end
    setCursorOffset(input.length);
    lastInternalInputRef.current = input;
  }
  // Wrap onInputChange to track internal changes before they trigger re-render
  const trackAndSetInput = React.useCallback((value: string) => {
    lastInternalInputRef.current = value;
    onInputChange(value);
  }, [onInputChange]);
  // Expose an insertText function so callers (e.g. STT) can splice text at the
  // current cursor position instead of replacing the entire input.
  if (insertTextRef) {
    insertTextRef.current = {
      cursorOffset,
      insert: (text: string) => {
        const needsSpace = cursorOffset === input.length && input.length > 0 && !/\s$/.test(input);
        const insertText = needsSpace ? ' ' + text : text;
        const newValue = input.slice(0, cursorOffset) + insertText + input.slice(cursorOffset);
        lastInternalInputRef.current = newValue;
        onInputChange(newValue);
        setCursorOffset(cursorOffset + insertText.length);
      },
      setInputWithCursor: (value: string, cursor: number) => {
        lastInternalInputRef.current = value;
        onInputChange(value);
        setCursorOffset(cursor);
      }
    };
  }
  const store = useAppStateStore();
  const setAppState = useSetAppState();
  const tasks = useAppState(s => s.tasks);
  const replBridgeConnected = useAppState(s => s.replBridgeConnected);
  const replBridgeExplicit = useAppState(s => s.replBridgeExplicit);
  const replBridgeReconnecting = useAppState(s => s.replBridgeReconnecting);
  // Must match BridgeStatusIndicator's render condition (PromptInputFooter.tsx) β€”
  // the pill returns null for implicit-and-not-reconnecting, so nav must too,
  // otherwise bridge becomes an invisible selection stop.
  const bridgeFooterVisible = replBridgeConnected && (replBridgeExplicit || replBridgeReconnecting);
  // Tmux pill (ant-only) β€” visible when there's an active tungsten session
  const hasTungstenSession = useAppState(s => "external" === 'ant' && s.tungstenActiveSession !== undefined);
  const tmuxFooterVisible = "external" === 'ant' && hasTungstenSession;
  // WebBrowser pill β€” visible when a browser is open
  const bagelFooterVisible = useAppState(s => false);
  const teamContext = useAppState(s => s.teamContext);
  const queuedCommands = useCommandQueue();
  const promptSuggestionState = useAppState(s => s.promptSuggestion);
  const speculation = useAppState(s => s.speculation);
  const speculationSessionTimeSavedMs = useAppState(s => s.speculationSessionTimeSavedMs);
  const viewingAgentTaskId = useAppState(s => s.viewingAgentTaskId);
  const viewSelectionMode = useAppState(s => s.viewSelectionMode);
  const showSpinnerTree = useAppState(s => s.expandedView) === 'teammates';
  const {
    companion: _companion,
    companionMuted
  } = feature('BUDDY') ? getGlobalConfig() : {
    companion: undefined,
    companionMuted: undefined
  };
  const companionFooterVisible = !!_companion && !companionMuted;
  // Brief mode: BriefSpinner/BriefIdleStatus own the 2-row footprint above
  // the input. Dropping marginTop here lets the spinner sit flush against
  // the input bar. viewingAgentTaskId mirrors the gate on both (Spinner.tsx,
  // REPL.tsx) β€” teammate view falls back to SpinnerWithVerbInner which has
  // its own marginTop, so the gap stays even without ours.
  const briefOwnsGap = feature('KAIROS') || feature('KAIROS_BRIEF') ?
  // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
  useAppState(s => s.isBriefOnly) && !viewingAgentTaskId : false;
  const mainLoopModel_ = useAppState(s => s.mainLoopModel);
  const mainLoopModelForSession = useAppState(s => s.mainLoopModelForSession);
  const thinkingEnabled = useAppState(s => s.thinkingEnabled);
  const isFastMode = useAppState(s => isFastModeEnabled() ? s.fastMode : false);
  const effortValue = useAppState(s => s.effortValue);
  const viewedTeammate = getViewedTeammateTask(store.getState());
  const viewingAgentName = viewedTeammate?.identity.agentName;
  // identity.color is typed as `string | undefined` (not AgentColorName) because
  // teammate identity comes from file-based config. Validate before casting to
  // ensure we only use valid color names (falls back to cyan if invalid).
  const viewingAgentColor = viewedTeammate?.identity.color && AGENT_COLORS.includes(viewedTeammate.identity.color as AgentColorName) ? viewedTeammate.identity.color as AgentColorName : undefined;
  // In-process teammates sorted alphabetically for footer team selector
  const inProcessTeammates = useMemo(() => getRunningTeammatesSorted(tasks), [tasks]);

  // Team mode: all background tasks are in-process teammates
  const isTeammateMode = inProcessTeammates.length > 0 || viewedTeammate !== undefined;

  // When viewing a teammate, show their permission mode in the footer instead of the leader's
  const effectiveToolPermissionContext = useMemo((): ToolPermissionContext => {
    if (viewedTeammate) {
      return {
        ...toolPermissionContext,
        mode: viewedTeammate.permissionMode
      };
    }
    return toolPermissionContext;
  }, [viewedTeammate, toolPermissionContext]);
  const {
    historyQuery,
    setHistoryQuery,
    historyMatch,
    historyFailedMatch
  } = useHistorySearch(entry => {
    setPastedContents(entry.pastedContents);
    void onSubmit(entry.display);
  }, input, trackAndSetInput, setCursorOffset, cursorOffset, onModeChange, mode, isSearchingHistory, setIsSearchingHistory, setPastedContents, pastedContents);
  // Counter for paste IDs (shared between images and text).
  // Compute initial value once from existing messages (for --continue/--resume).
  // useRef(fn()) evaluates fn() on every render and discards the result after
  // mount β€” getInitialPasteId walks all messages + regex-scans text blocks,
  // so guard with a lazy-init pattern to run it exactly once.
  const nextPasteIdRef = useRef(-1);
  if (nextPasteIdRef.current === -1) {
    nextPasteIdRef.current = getInitialPasteId(messages);
  }
  // Armed by onImagePaste; if the very next keystroke is a non-space
  // printable, inputFilter prepends a space before it. Any other input
  // (arrow, escape, backspace, paste, space) disarms without inserting.
  const pendingSpaceAfterPillRef = useRef(false);
  const [showTeamsDialog, setShowTeamsDialog] = useState(false);
  const [showBridgeDialog, setShowBridgeDialog] = useState(false);
  const [teammateFooterIndex, setTeammateFooterIndex] = useState(0);
  // -1 sentinel: tasks pill is selected but no specific agent row is selected yet.
  // First ↓ selects the pill, second ↓ moves to row 0. Prevents double-select
  // of pill + row when both bg tasks (pill) and forked agents (rows) are visible.
  const coordinatorTaskIndex = useAppState(s => s.coordinatorTaskIndex);
  const setCoordinatorTaskIndex = useCallback((v: number | ((prev: number) => number)) => setAppState(prev => {
    const next = typeof v === 'function' ? v(prev.coordinatorTaskIndex) : v;
    if (next === prev.coordinatorTaskIndex) return prev;
    return {
      ...prev,
      coordinatorTaskIndex: next
    };
  }), [setAppState]);
  const coordinatorTaskCount = useCoordinatorTaskCount();
  // The pill (BackgroundTaskStatus) only renders when non-local_agent bg tasks
  // exist. When only local_agent tasks are running (coordinator/fork mode), the
  // pill is absent, so the -1 sentinel would leave nothing visually selected.
  // In that case, skip -1 and treat 0 as the minimum selectable index.
  const hasBgTaskPill = useMemo(() => Object.values(tasks).some(t => isBackgroundTask(t) && !("external" === 'ant' && isPanelAgentTask(t))), [tasks]);
  const minCoordinatorIndex = hasBgTaskPill ? -1 : 0;
  // Clamp index when tasks complete and the list shrinks beneath the cursor
  useEffect(() => {
    if (coordinatorTaskIndex >= coordinatorTaskCount) {
      setCoordinatorTaskIndex(Math.max(minCoordinatorIndex, coordinatorTaskCount - 1));
    } else if (coordinatorTaskIndex < minCoordinatorIndex) {
      setCoordinatorTaskIndex(minCoordinatorIndex);
    }
  }, [coordinatorTaskCount, coordinatorTaskIndex, minCoordinatorIndex]);
  const [isPasting, setIsPasting] = useState(false);
  const [isExternalEditorActive, setIsExternalEditorActive] = useState(false);
  const [showModelPicker, setShowModelPicker] = useState(false);
  const [showQuickOpen, setShowQuickOpen] = useState(false);
  const [showGlobalSearch, setShowGlobalSearch] = useState(false);
  const [showHistoryPicker, setShowHistoryPicker] = useState(false);
  const [showFastModePicker, setShowFastModePicker] = useState(false);
  const [showThinkingToggle, setShowThinkingToggle] = useState(false);
  const [showAutoModeOptIn, setShowAutoModeOptIn] = useState(false);
  const [previousModeBeforeAuto, setPreviousModeBeforeAuto] = useState<PermissionMode | null>(null);
  const autoModeOptInTimeoutRef = useRef<NodeJS.Timeout | null>(null);

  // Check if cursor is on the first line of input
  const isCursorOnFirstLine = useMemo(() => {
    const firstNewlineIndex = input.indexOf('\n');
    if (firstNewlineIndex === -1) {
      return true; // No newlines, cursor is always on first line
    }
    return cursorOffset <= firstNewlineIndex;
  }, [input, cursorOffset]);
  const isCursorOnLastLine = useMemo(() => {
    const lastNewlineIndex = input.lastIndexOf('\n');
    if (lastNewlineIndex === -1) {
      return true; // No newlines, cursor is always on last line
    }
    return cursorOffset > lastNewlineIndex;
  }, [input, cursorOffset]);

  // Derive team info from teamContext (no filesystem I/O needed)
  // A session can only lead one team at a time
  const cachedTeams: TeamSummary[] = useMemo(() => {
    if (!isAgentSwarmsEnabled()) return [];
    // In-process mode uses Shift+Down/Up navigation instead of footer menu
    if (isInProcessEnabled()) return [];
    if (!teamContext) {
      return [];
    }
    const teammateCount = count(Object.values(teamContext.teammates), t => t.name !== 'team-lead');
    return [{
      name: teamContext.teamName,
      memberCount: teammateCount,
      runningCount: 0,
      idleCount: 0
    }];
  }, [teamContext]);

  // ─── Footer pill navigation ─────────────────────────────────────────────
  // Which pills render below the input box. Order here IS the nav order
  // (down/right = forward, up/left = back). Selection lives in AppState so
  // pills rendered outside PromptInput (CompanionSprite) can read focus.
  const runningTaskCount = useMemo(() => count(Object.values(tasks), t => t.status === 'running'), [tasks]);
  // Panel shows retained-completed agents too (getVisibleAgentTasks), so the
  // pill must stay navigable whenever the panel has rows β€” not just when
  // something is running.
  const tasksFooterVisible = (runningTaskCount > 0 || "external" === 'ant' && coordinatorTaskCount > 0) && !shouldHideTasksFooter(tasks, showSpinnerTree);
  const teamsFooterVisible = cachedTeams.length > 0;
  const footerItems = useMemo(() => [tasksFooterVisible && 'tasks', tmuxFooterVisible && 'tmux', bagelFooterVisible && 'bagel', teamsFooterVisible && 'teams', bridgeFooterVisible && 'bridge', companionFooterVisible && 'companion'].filter(Boolean) as FooterItem[], [tasksFooterVisible, tmuxFooterVisible, bagelFooterVisible, teamsFooterVisible, bridgeFooterVisible, companionFooterVisible]);

  // Effective selection: null if the selected pill stopped rendering (bridge
  // disconnected, task finished). The derivation makes the UI correct
  // immediately; the useEffect below clears the raw state so it doesn't
  // resurrect when the same pill reappears (new task starts β†’ focus stolen).
  const rawFooterSelection = useAppState(s => s.footerSelection);
  const footerItemSelected = rawFooterSelection && footerItems.includes(rawFooterSelection) ? rawFooterSelection : null;
  useEffect(() => {
    if (rawFooterSelection && !footerItemSelected) {
      setAppState(prev => prev.footerSelection === null ? prev : {
        ...prev,
        footerSelection: null
      });
    }
  }, [rawFooterSelection, footerItemSelected, setAppState]);
  const tasksSelected = footerItemSelected === 'tasks';
  const tmuxSelected = footerItemSelected === 'tmux';
  const bagelSelected = footerItemSelected === 'bagel';
  const teamsSelected = footerItemSelected === 'teams';
  const bridgeSelected = footerItemSelected === 'bridge';
  function selectFooterItem(item: FooterItem | null): void {
    setAppState(prev => prev.footerSelection === item ? prev : {
      ...prev,
      footerSelection: item
    });
    if (item === 'tasks') {
      setTeammateFooterIndex(0);
      setCoordinatorTaskIndex(minCoordinatorIndex);
    }
  }

  // delta: +1 = down/right, -1 = up/left. Returns true if nav happened
  // (including deselecting at the start), false if at a boundary.
  function navigateFooter(delta: 1 | -1, exitAtStart = false): boolean {
    const idx = footerItemSelected ? footerItems.indexOf(footerItemSelected) : -1;
    const next = footerItems[idx + delta];
    if (next) {
      selectFooterItem(next);
      return true;
    }
    if (delta < 0 && exitAtStart) {
      selectFooterItem(null);
      return true;
    }
    return false;
  }

  // Prompt suggestion hook - reads suggestions generated by forked agent in query loop
  const {
    suggestion: promptSuggestion,
    markAccepted,
    logOutcomeAtSubmission,
    markShown
  } = usePromptSuggestion({
    inputValue: input,
    isAssistantResponding: isLoading
  });
  const displayedValue = useMemo(() => isSearchingHistory && historyMatch ? getValueFromInput(typeof historyMatch === 'string' ? historyMatch : historyMatch.display) : input, [isSearchingHistory, historyMatch, input]);
  const thinkTriggers = useMemo(() => findThinkingTriggerPositions(displayedValue), [displayedValue]);
  const ultraplanSessionUrl = useAppState(s => s.ultraplanSessionUrl);
  const ultraplanLaunching = useAppState(s => s.ultraplanLaunching);
  const ultraplanTriggers = useMemo(() => feature('ULTRAPLAN') && !ultraplanSessionUrl && !ultraplanLaunching ? findUltraplanTriggerPositions(displayedValue) : [], [displayedValue, ultraplanSessionUrl, ultraplanLaunching]);
  const ultrareviewTriggers = useMemo(() => isUltrareviewEnabled() ? findUltrareviewTriggerPositions(displayedValue) : [], [displayedValue]);
  const btwTriggers = useMemo(() => findBtwTriggerPositions(displayedValue), [displayedValue]);
  const buddyTriggers = useMemo(() => findBuddyTriggerPositions(displayedValue), [displayedValue]);
  const slashCommandTriggers = useMemo(() => {
    const positions = findSlashCommandPositions(displayedValue);
    // Only highlight valid commands
    return positions.filter(pos => {
      const commandName = displayedValue.slice(pos.start + 1, pos.end); // +1 to skip "/"
      return hasCommand(commandName, commands);
    });
  }, [displayedValue, commands]);
  const tokenBudgetTriggers = useMemo(() => feature('TOKEN_BUDGET') ? findTokenBudgetPositions(displayedValue) : [], [displayedValue]);
  const knownChannelsVersion = useSyncExternalStore(subscribeKnownChannels, getKnownChannelsVersion);
  const slackChannelTriggers = useMemo(() => hasSlackMcpServer(store.getState().mcp.clients) ? findSlackChannelPositions(displayedValue) : [],
  // eslint-disable-next-line react-hooks/exhaustive-deps -- store is a stable ref
  [displayedValue, knownChannelsVersion]);

  // Find @name mentions and highlight with team member's color
  const memberMentionHighlights = useMemo((): Array<{
    start: number;
    end: number;
    themeColor: keyof Theme;
  }> => {
    if (!isAgentSwarmsEnabled()) return [];
    if (!teamContext?.teammates) return [];
    const highlights: Array<{
      start: number;
      end: number;
      themeColor: keyof Theme;
    }> = [];
    const members = teamContext.teammates;
    if (!members) return highlights;

    // Find all @name patterns in the input
    const regex = /(^|\s)@([\w-]+)/g;
    const memberValues = Object.values(members);
    let match;
    while ((match = regex.exec(displayedValue)) !== null) {
      const leadingSpace = match[1] ?? '';
      const nameStart = match.index + leadingSpace.length;
      const fullMatch = match[0].trimStart();
      const name = match[2];

      // Check if this name matches a team member
      const member = memberValues.find(t => t.name === name);
      if (member?.color) {
        const themeColor = AGENT_COLOR_TO_THEME_COLOR[member.color as AgentColorName];
        if (themeColor) {
          highlights.push({
            start: nameStart,
            end: nameStart + fullMatch.length,
            themeColor
          });
        }
      }
    }
    return highlights;
  }, [displayedValue, teamContext]);
  const imageRefPositions = useMemo(() => parseReferences(displayedValue).filter(r => r.match.startsWith('[Image')).map(r => ({
    start: r.index,
    end: r.index + r.match.length
  })), [displayedValue]);

  // chip.start is the "selected" state: the inverted chip IS the cursor.
  // chip.end stays a normal position so you can park the cursor right after
  // `]` like any other character.
  const cursorAtImageChip = imageRefPositions.some(r => r.start === cursorOffset);

  // up/down movement or a fullscreen click can land the cursor strictly
  // inside a chip; snap to the nearer boundary so it's never editable
  // char-by-char.
  useEffect(() => {
    const inside = imageRefPositions.find(r => cursorOffset > r.start && cursorOffset < r.end);
    if (inside) {
      const mid = (inside.start + inside.end) / 2;
      setCursorOffset(cursorOffset < mid ? inside.start : inside.end);
    }
  }, [cursorOffset, imageRefPositions, setCursorOffset]);
  const combinedHighlights = useMemo((): TextHighlight[] => {
    const highlights: TextHighlight[] = [];

    // Invert the [Image #N] chip when the cursor is at chip.start (the
    // "selected" state) so backspace-to-delete is visually obvious.
    for (const ref of imageRefPositions) {
      if (cursorOffset === ref.start) {
        highlights.push({
          start: ref.start,
          end: ref.end,
          color: undefined,
          inverse: true,
          priority: 8
        });
      }
    }
    if (isSearchingHistory && historyMatch && !historyFailedMatch) {
      highlights.push({
        start: cursorOffset,
        end: cursorOffset + historyQuery.length,
        color: 'warning',
        priority: 20
      });
    }

    // Add "btw" highlighting (solid yellow)
    for (const trigger of btwTriggers) {
      highlights.push({
        start: trigger.start,
        end: trigger.end,
        color: 'warning',
        priority: 15
      });
    }

    // Add /command highlighting (blue)
    for (const trigger of slashCommandTriggers) {
      highlights.push({
        start: trigger.start,
        end: trigger.end,
        color: 'suggestion',
        priority: 5
      });
    }

    // Add token budget highlighting (blue)
    for (const trigger of tokenBudgetTriggers) {
      highlights.push({
        start: trigger.start,
        end: trigger.end,
        color: 'suggestion',
        priority: 5
      });
    }
    for (const trigger of slackChannelTriggers) {
      highlights.push({
        start: trigger.start,
        end: trigger.end,
        color: 'suggestion',
        priority: 5
      });
    }

    // Add @name highlighting with team member's color
    for (const mention of memberMentionHighlights) {
      highlights.push({
        start: mention.start,
        end: mention.end,
        color: mention.themeColor,
        priority: 5
      });
    }

    // Dim interim voice dictation text
    if (voiceInterimRange) {
      highlights.push({
        start: voiceInterimRange.start,
        end: voiceInterimRange.end,
        color: undefined,
        dimColor: true,
        priority: 1
      });
    }

    // Rainbow highlighting for ultrathink keyword (per-character cycling colors)
    if (isUltrathinkEnabled()) {
      for (const trigger of thinkTriggers) {
        for (let i = trigger.start; i < trigger.end; i++) {
          highlights.push({
            start: i,
            end: i + 1,
            color: getRainbowColor(i - trigger.start),
            shimmerColor: getRainbowColor(i - trigger.start, true),
            priority: 10
          });
        }
      }
    }

    // Same rainbow treatment for the ultraplan keyword
    if (feature('ULTRAPLAN')) {
      for (const trigger of ultraplanTriggers) {
        for (let i = trigger.start; i < trigger.end; i++) {
          highlights.push({
            start: i,
            end: i + 1,
            color: getRainbowColor(i - trigger.start),
            shimmerColor: getRainbowColor(i - trigger.start, true),
            priority: 10
          });
        }
      }
    }

    // Same rainbow treatment for the ultrareview keyword
    for (const trigger of ultrareviewTriggers) {
      for (let i = trigger.start; i < trigger.end; i++) {
        highlights.push({
          start: i,
          end: i + 1,
          color: getRainbowColor(i - trigger.start),
          shimmerColor: getRainbowColor(i - trigger.start, true),
          priority: 10
        });
      }
    }

    // Rainbow for /buddy
    for (const trigger of buddyTriggers) {
      for (let i = trigger.start; i < trigger.end; i++) {
        highlights.push({
          start: i,
          end: i + 1,
          color: getRainbowColor(i - trigger.start),
          shimmerColor: getRainbowColor(i - trigger.start, true),
          priority: 10
        });
      }
    }
    return highlights;
  }, [isSearchingHistory, historyQuery, historyMatch, historyFailedMatch, cursorOffset, btwTriggers, imageRefPositions, memberMentionHighlights, slashCommandTriggers, tokenBudgetTriggers, slackChannelTriggers, displayedValue, voiceInterimRange, thinkTriggers, ultraplanTriggers, ultrareviewTriggers, buddyTriggers]);
  const {
    addNotification,
    removeNotification
  } = useNotifications();

  // Show ultrathink notification
  useEffect(() => {
    if (thinkTriggers.length && isUltrathinkEnabled()) {
      addNotification({
        key: 'ultrathink-active',
        text: 'Effort set to high for this turn',
        priority: 'immediate',
        timeoutMs: 5000
      });
    } else {
      removeNotification('ultrathink-active');
    }
  }, [addNotification, removeNotification, thinkTriggers.length]);
  useEffect(() => {
    if (feature('ULTRAPLAN') && ultraplanTriggers.length) {
      addNotification({
        key: 'ultraplan-active',
        text: 'This prompt will launch an ultraplan session in Claude Code on the web',
        priority: 'immediate',
        timeoutMs: 5000
      });
    } else {
      removeNotification('ultraplan-active');
    }
  }, [addNotification, removeNotification, ultraplanTriggers.length]);
  useEffect(() => {
    if (isUltrareviewEnabled() && ultrareviewTriggers.length) {
      addNotification({
        key: 'ultrareview-active',
        text: 'Run /ultrareview after Claude finishes to review these changes in the cloud',
        priority: 'immediate',
        timeoutMs: 5000
      });
    }
  }, [addNotification, ultrareviewTriggers.length]);

  // Track input length for stash hint
  const prevInputLengthRef = useRef(input.length);
  const peakInputLengthRef = useRef(input.length);

  // Dismiss stash hint when user makes any input change
  const dismissStashHint = useCallback(() => {
    removeNotification('stash-hint');
  }, [removeNotification]);

  // Show stash hint when user gradually clears substantial input
  useEffect(() => {
    const prevLength = prevInputLengthRef.current;
    const peakLength = peakInputLengthRef.current;
    const currentLength = input.length;
    prevInputLengthRef.current = currentLength;

    // Update peak when input grows
    if (currentLength > peakLength) {
      peakInputLengthRef.current = currentLength;
      return;
    }

    // Reset state when input is empty
    if (currentLength === 0) {
      peakInputLengthRef.current = 0;
      return;
    }

    // Detect gradual clear: peak was high, current is low, but this wasn't a single big jump
    // (rapid clears like esc-esc go from 20+ to 0 in one step)
    const clearedSubstantialInput = peakLength >= 20 && currentLength <= 5;
    const wasRapidClear = prevLength >= 20 && currentLength <= 5;
    if (clearedSubstantialInput && !wasRapidClear) {
      const config = getGlobalConfig();
      if (!config.hasUsedStash) {
        addNotification({
          key: 'stash-hint',
          jsx: <Text dimColor>
              Tip:{' '}
              <ConfigurableShortcutHint action="chat:stash" context="Chat" fallback="ctrl+s" description="stash" />
            </Text>,
          priority: 'immediate',
          timeoutMs: FOOTER_TEMPORARY_STATUS_TIMEOUT
        });
      }
      peakInputLengthRef.current = currentLength;
    }
  }, [input.length, addNotification]);

  // Initialize input buffer for undo functionality
  const {
    pushToBuffer,
    undo,
    canUndo,
    clearBuffer
  } = useInputBuffer({
    maxBufferSize: 50,
    debounceMs: 1000
  });
  useMaybeTruncateInput({
    input,
    pastedContents,
    onInputChange: trackAndSetInput,
    setCursorOffset,
    setPastedContents
  });
  const defaultPlaceholder = usePromptInputPlaceholder({
    input,
    submitCount,
    viewingAgentName
  });
  const onChange = useCallback((value: string) => {
    if (value === '?') {
      logEvent('tengu_help_toggled', {});
      setHelpOpen(v => !v);
      return;
    }
    setHelpOpen(false);

    // Dismiss stash hint when user makes any input change
    dismissStashHint();

    // Cancel any pending prompt suggestion and speculation when user types
    abortPromptSuggestion();
    abortSpeculation(setAppState);

    // Check if this is a single character insertion at the start
    const isSingleCharInsertion = value.length === input.length + 1;
    const insertedAtStart = cursorOffset === 0;
    const mode = getModeFromInput(value);
    if (insertedAtStart && mode !== 'prompt') {
      if (isSingleCharInsertion) {
        onModeChange(mode);
        return;
      }
      // Multi-char insertion into empty input (e.g. tab-accepting "! gcloud auth login")
      if (input.length === 0) {
        onModeChange(mode);
        const valueWithoutMode = getValueFromInput(value).replaceAll('\t', '    ');
        pushToBuffer(input, cursorOffset, pastedContents);
        trackAndSetInput(valueWithoutMode);
        setCursorOffset(valueWithoutMode.length);
        return;
      }
    }
    const processedValue = value.replaceAll('\t', '    ');

    // Push current state to buffer before making changes
    if (input !== processedValue) {
      pushToBuffer(input, cursorOffset, pastedContents);
    }

    // Deselect footer items when user types
    setAppState(prev => prev.footerSelection === null ? prev : {
      ...prev,
      footerSelection: null
    });
    trackAndSetInput(processedValue);
  }, [trackAndSetInput, onModeChange, input, cursorOffset, pushToBuffer, pastedContents, dismissStashHint, setAppState]);
  const {
    resetHistory,
    onHistoryUp,
    onHistoryDown,
    dismissSearchHint,
    historyIndex
  } = useArrowKeyHistory((value: string, historyMode: HistoryMode, pastedContents: Record<number, PastedContent>) => {
    onChange(value);
    onModeChange(historyMode);
    setPastedContents(pastedContents);
  }, input, pastedContents, setCursorOffset, mode);

  // Dismiss search hint when user starts searching
  useEffect(() => {
    if (isSearchingHistory) {
      dismissSearchHint();
    }
  }, [isSearchingHistory, dismissSearchHint]);

  // Only use history navigation when there are 0 or 1 slash command suggestions.
  // Footer nav is NOT here β€” when a pill is selected, TextInput focus=false so
  // these never fire. The Footer keybinding context handles ↑/↓ instead.
  function handleHistoryUp() {
    if (suggestions.length > 1) {
      return;
    }

    // Only navigate history when cursor is on the first line.
    // In multiline inputs, up arrow should move the cursor (handled by TextInput)
    // and only trigger history when at the top of the input.
    if (!isCursorOnFirstLine) {
      return;
    }

    // If there's an editable queued command, move it to the input for editing when UP is pressed
    const hasEditableCommand = queuedCommands.some(isQueuedCommandEditable);
    if (hasEditableCommand) {
      void popAllCommandsFromQueue();
      return;
    }
    onHistoryUp();
  }
  function handleHistoryDown() {
    if (suggestions.length > 1) {
      return;
    }

    // Only navigate history/footer when cursor is on the last line.
    // In multiline inputs, down arrow should move the cursor (handled by TextInput)
    // and only trigger navigation when at the bottom of the input.
    if (!isCursorOnLastLine) {
      return;
    }

    // At bottom of history β†’ enter footer at first visible pill
    if (onHistoryDown() && footerItems.length > 0) {
      const first = footerItems[0]!;
      selectFooterItem(first);
      if (first === 'tasks' && !getGlobalConfig().hasSeenTasksHint) {
        saveGlobalConfig(c => c.hasSeenTasksHint ? c : {
          ...c,
          hasSeenTasksHint: true
        });
      }
    }
  }

  // Create a suggestions state directly - we'll sync it with useTypeahead later
  const [suggestionsState, setSuggestionsStateRaw] = useState<{
    suggestions: SuggestionItem[];
    selectedSuggestion: number;
    commandArgumentHint?: string;
  }>({
    suggestions: [],
    selectedSuggestion: -1,
    commandArgumentHint: undefined
  });

  // Setter for suggestions state
  const setSuggestionsState = useCallback((updater: typeof suggestionsState | ((prev: typeof suggestionsState) => typeof suggestionsState)) => {
    setSuggestionsStateRaw(prev => typeof updater === 'function' ? updater(prev) : updater);
  }, []);
  const onSubmit = useCallback(async (inputParam: string, isSubmittingSlashCommand = false) => {
    inputParam = inputParam.trimEnd();

    // Don't submit if a footer indicator is being opened. Read fresh from
    // store β€” footer:openSelected calls selectFooterItem(null) then onSubmit
    // in the same tick, and the closure value hasn't updated yet. Apply the
    // same "still visible?" derivation as footerItemSelected so a stale
    // selection (pill disappeared) doesn't swallow Enter.
    const state = store.getState();
    if (state.footerSelection && footerItems.includes(state.footerSelection)) {
      return;
    }

    // Enter in selection modes confirms selection (useBackgroundTaskNavigation).
    // BaseTextInput's useInput registers before that hook (child effects fire first),
    // so without this guard Enter would double-fire and auto-submit the suggestion.
    if (state.viewSelectionMode === 'selecting-agent') {
      return;
    }

    // Check for images early - we need this for suggestion logic below
    const hasImages = Object.values(pastedContents).some(c => c.type === 'image');

    // If input is empty OR matches the suggestion, submit it
    // But if there are images attached, don't auto-accept the suggestion -
    // the user wants to submit just the image(s).
    // Only in leader view β€” promptSuggestion is leader-context, not teammate.
    const suggestionText = promptSuggestionState.text;
    const inputMatchesSuggestion = inputParam.trim() === '' || inputParam === suggestionText;
    if (inputMatchesSuggestion && suggestionText && !hasImages && !state.viewingAgentTaskId) {
      // If speculation is active, inject messages immediately as they stream
      if (speculation.status === 'active') {
        markAccepted();
        // skipReset: resetSuggestion would abort the speculation before we accept it
        logOutcomeAtSubmission(suggestionText, {
          skipReset: true
        });
        void onSubmitProp(suggestionText, {
          setCursorOffset,
          clearBuffer,
          resetHistory
        }, {
          state: speculation,
          speculationSessionTimeSavedMs: speculationSessionTimeSavedMs,
          setAppState
        });
        return; // Skip normal query - speculation handled it
      }

      // Regular suggestion acceptance (requires shownAt > 0)
      if (promptSuggestionState.shownAt > 0) {
        markAccepted();
        inputParam = suggestionText;
      }
    }

    // Handle @name direct message
    if (isAgentSwarmsEnabled()) {
      const directMessage = parseDirectMemberMessage(inputParam);
      if (directMessage) {
        const result = await sendDirectMemberMessage(directMessage.recipientName, directMessage.message, teamContext, writeToMailbox);
        if (result.success) {
          addNotification({
            key: 'direct-message-sent',
            text: `Sent to @${result.recipientName}`,
            priority: 'immediate',
            timeoutMs: 3000
          });
          trackAndSetInput('');
          setCursorOffset(0);
          clearBuffer();
          resetHistory();
          return;
        } else if (result.error === 'no_team_context') {
          // No team context - fall through to normal prompt submission
        } else {
          // Unknown recipient - fall through to normal prompt submission
          // This allows e.g. "@utils explain this code" to be sent as a prompt
        }
      }
    }

    // Allow submission if there are images attached, even without text
    if (inputParam.trim() === '' && !hasImages) {
      return;
    }

    // PromptInput UX: Check if suggestions dropdown is showing
    // For directory suggestions, allow submission (Tab is used for completion)
    const hasDirectorySuggestions = suggestionsState.suggestions.length > 0 && suggestionsState.suggestions.every(s => s.description === 'directory');
    if (suggestionsState.suggestions.length > 0 && !isSubmittingSlashCommand && !hasDirectorySuggestions) {
      logForDebugging(`[onSubmit] early return: suggestions showing (count=${suggestionsState.suggestions.length})`);
      return; // Don't submit, user needs to clear suggestions first
    }

    // Log suggestion outcome if one exists
    if (promptSuggestionState.text && promptSuggestionState.shownAt > 0) {
      logOutcomeAtSubmission(inputParam);
    }

    // Clear stash hint notification on submit
    removeNotification('stash-hint');

    // Route input to viewed agent (in-process teammate or named local_agent).
    const activeAgent = getActiveAgentForInput(store.getState());
    if (activeAgent.type !== 'leader' && onAgentSubmit) {
      logEvent('tengu_transcript_input_to_teammate', {});
      await onAgentSubmit(inputParam, activeAgent.task, {
        setCursorOffset,
        clearBuffer,
        resetHistory
      });
      return;
    }

    // Normal leader submission
    await onSubmitProp(inputParam, {
      setCursorOffset,
      clearBuffer,
      resetHistory
    });
  }, [promptSuggestionState, speculation, speculationSessionTimeSavedMs, teamContext, store, footerItems, suggestionsState.suggestions, onSubmitProp, onAgentSubmit, clearBuffer, resetHistory, logOutcomeAtSubmission, setAppState, markAccepted, pastedContents, removeNotification]);
  const {
    suggestions,
    selectedSuggestion,
    commandArgumentHint,
    inlineGhostText,
    maxColumnWidth
  } = useTypeahead({
    commands,
    onInputChange: trackAndSetInput,
    onSubmit,
    setCursorOffset,
    input,
    cursorOffset,
    mode,
    agents,
    setSuggestionsState,
    suggestionsState,
    suppressSuggestions: isSearchingHistory || historyIndex > 0,
    markAccepted,
    onModeChange
  });

  // Track if prompt suggestion should be shown (computed later with terminal width).
  // Hidden in teammate view β€” suggestion is leader-context only.
  const showPromptSuggestion = mode === 'prompt' && suggestions.length === 0 && promptSuggestion && !viewingAgentTaskId;
  if (showPromptSuggestion) {
    markShown();
  }

  // If suggestion was generated but can't be shown due to timing, log suppression.
  // Exclude teammate view: markShown() is gated above, so shownAt stays 0 there β€”
  // but that's not a timing failure, the suggestion is valid when returning to leader.
  if (promptSuggestionState.text && !promptSuggestion && promptSuggestionState.shownAt === 0 && !viewingAgentTaskId) {
    logSuggestionSuppressed('timing', promptSuggestionState.text);
    setAppState(prev => ({
      ...prev,
      promptSuggestion: {
        text: null,
        promptId: null,
        shownAt: 0,
        acceptedAt: 0,
        generationRequestId: null
      }
    }));
  }
  function onImagePaste(image: string, mediaType?: string, filename?: string, dimensions?: ImageDimensions, sourcePath?: string) {
    logEvent('tengu_paste_image', {});
    onModeChange('prompt');
    const pasteId = nextPasteIdRef.current++;
    const newContent: PastedContent = {
      id: pasteId,
      type: 'image',
      content: image,
      mediaType: mediaType || 'image/png',
      // default to PNG if not provided
      filename: filename || 'Pasted image',
      dimensions,
      sourcePath
    };

    // Cache path immediately (fast) so links work on render
    cacheImagePath(newContent);

    // Store image to disk in background
    void storeImage(newContent);

    // Update UI
    setPastedContents(prev => ({
      ...prev,
      [pasteId]: newContent
    }));
    // Multi-image paste calls onImagePaste in a loop. If the ref is already
    // armed, the previous pill's lazy space fires now (before this pill)
    // rather than being lost.
    const prefix = pendingSpaceAfterPillRef.current ? ' ' : '';
    insertTextAtCursor(prefix + formatImageRef(pasteId));
    pendingSpaceAfterPillRef.current = true;
  }

  // Prune images whose [Image #N] placeholder is no longer in the input text.
  // Covers pill backspace, Ctrl+U, char-by-char deletion β€” any edit that drops
  // the ref. onImagePaste batches setPastedContents + insertTextAtCursor in the
  // same event, so this effect sees the placeholder already present.
  useEffect(() => {
    const referencedIds = new Set(parseReferences(input).map(r => r.id));
    setPastedContents(prev => {
      const orphaned = Object.values(prev).filter(c => c.type === 'image' && !referencedIds.has(c.id));
      if (orphaned.length === 0) return prev;
      const next = {
        ...prev
      };
      for (const img of orphaned) delete next[img.id];
      return next;
    });
  }, [input, setPastedContents]);
  function onTextPaste(rawText: string) {
    pendingSpaceAfterPillRef.current = false;
    // Clean up pasted text - strip ANSI escape codes and normalize line endings and tabs
    let text = stripAnsi(rawText).replace(/\r/g, '\n').replaceAll('\t', '    ');

    // Match typed/auto-suggest: `!cmd` pasted into empty input enters bash mode.
    if (input.length === 0) {
      const pastedMode = getModeFromInput(text);
      if (pastedMode !== 'prompt') {
        onModeChange(pastedMode);
        text = getValueFromInput(text);
      }
    }
    const numLines = getPastedTextRefNumLines(text);
    // Limit the number of lines to show in the input
    // If the overall layout is too high then Ink will repaint
    // the entire terminal.
    // The actual required height is dependent on the content, this
    // is just an estimate.
    const maxLines = Math.min(rows - 10, 2);

    // Use special handling for long pasted text (>PASTE_THRESHOLD chars)
    // or if it exceeds the number of lines we want to show
    if (text.length > PASTE_THRESHOLD || numLines > maxLines) {
      const pasteId = nextPasteIdRef.current++;
      const newContent: PastedContent = {
        id: pasteId,
        type: 'text',
        content: text
      };
      setPastedContents(prev => ({
        ...prev,
        [pasteId]: newContent
      }));
      insertTextAtCursor(formatPastedTextRef(pasteId, numLines));
    } else {
      // For shorter pastes, just insert the text normally
      insertTextAtCursor(text);
    }
  }
  const lazySpaceInputFilter = useCallback((input: string, key: Key): string => {
    if (!pendingSpaceAfterPillRef.current) return input;
    pendingSpaceAfterPillRef.current = false;
    if (isNonSpacePrintable(input, key)) return ' ' + input;
    return input;
  }, []);
  function insertTextAtCursor(text: string) {
    // Push current state to buffer before inserting
    pushToBuffer(input, cursorOffset, pastedContents);
    const newInput = input.slice(0, cursorOffset) + text + input.slice(cursorOffset);
    trackAndSetInput(newInput);
    setCursorOffset(cursorOffset + text.length);
  }
  const doublePressEscFromEmpty = useDoublePress(() => {}, () => onShowMessageSelector());

  // Function to get the queued command for editing. Returns true if commands were popped.
  const popAllCommandsFromQueue = useCallback((): boolean => {
    const result = popAllEditable(input, cursorOffset);
    if (!result) {
      return false;
    }
    trackAndSetInput(result.text);
    onModeChange('prompt'); // Always prompt mode for queued commands
    setCursorOffset(result.cursorOffset);

    // Restore images from queued commands to pastedContents
    if (result.images.length > 0) {
      setPastedContents(prev => {
        const newContents = {
          ...prev
        };
        for (const image of result.images) {
          newContents[image.id] = image;
        }
        return newContents;
      });
    }
    return true;
  }, [trackAndSetInput, onModeChange, input, cursorOffset, setPastedContents]);

  // Insert the at-mentioned reference (the file and, optionally, a line range) when
  // we receive an at-mentioned notification the IDE.
  const onIdeAtMentioned = function (atMentioned: IDEAtMentioned) {
    logEvent('tengu_ext_at_mentioned', {});
    let atMentionedText: string;
    const relativePath = path.relative(getCwd(), atMentioned.filePath);
    if (atMentioned.lineStart && atMentioned.lineEnd) {
      atMentionedText = atMentioned.lineStart === atMentioned.lineEnd ? `@${relativePath}#L${atMentioned.lineStart} ` : `@${relativePath}#L${atMentioned.lineStart}-${atMentioned.lineEnd} `;
    } else {
      atMentionedText = `@${relativePath} `;
    }
    const cursorChar = input[cursorOffset - 1] ?? ' ';
    if (!/\s/.test(cursorChar)) {
      atMentionedText = ` ${atMentionedText}`;
    }
    insertTextAtCursor(atMentionedText);
  };
  useIdeAtMentioned(mcpClients, onIdeAtMentioned);

  // Handler for chat:undo - undo last edit
  const handleUndo = useCallback(() => {
    if (canUndo) {
      const previousState = undo();
      if (previousState) {
        trackAndSetInput(previousState.text);
        setCursorOffset(previousState.cursorOffset);
        setPastedContents(previousState.pastedContents);
      }
    }
  }, [canUndo, undo, trackAndSetInput, setPastedContents]);

  // Handler for chat:newline - insert a newline at the cursor position
  const handleNewline = useCallback(() => {
    pushToBuffer(input, cursorOffset, pastedContents);
    const newInput = input.slice(0, cursorOffset) + '\n' + input.slice(cursorOffset);
    trackAndSetInput(newInput);
    setCursorOffset(cursorOffset + 1);
  }, [input, cursorOffset, trackAndSetInput, setCursorOffset, pushToBuffer, pastedContents]);

  // Handler for chat:externalEditor - edit in $EDITOR
  const handleExternalEditor = useCallback(async () => {
    logEvent('tengu_external_editor_used', {});
    setIsExternalEditorActive(true);
    try {
      // Pass pastedContents to expand collapsed text references
      const result = await editPromptInEditor(input, pastedContents);
      if (result.error) {
        addNotification({
          key: 'external-editor-error',
          text: result.error,
          color: 'warning',
          priority: 'high'
        });
      }
      if (result.content !== null && result.content !== input) {
        // Push current state to buffer before making changes
        pushToBuffer(input, cursorOffset, pastedContents);
        trackAndSetInput(result.content);
        setCursorOffset(result.content.length);
      }
    } catch (err) {
      if (err instanceof Error) {
        logError(err);
      }
      addNotification({
        key: 'external-editor-error',
        text: `External editor failed: ${errorMessage(err)}`,
        color: 'warning',
        priority: 'high'
      });
    } finally {
      setIsExternalEditorActive(false);
    }
  }, [input, cursorOffset, pastedContents, pushToBuffer, trackAndSetInput, addNotification]);

  // Handler for chat:stash - stash/unstash prompt
  const handleStash = useCallback(() => {
    if (input.trim() === '' && stashedPrompt !== undefined) {
      // Pop stash when input is empty
      trackAndSetInput(stashedPrompt.text);
      setCursorOffset(stashedPrompt.cursorOffset);
      setPastedContents(stashedPrompt.pastedContents);
      setStashedPrompt(undefined);
    } else if (input.trim() !== '') {
      // Push to stash (save text, cursor position, and pasted contents)
      setStashedPrompt({
        text: input,
        cursorOffset,
        pastedContents
      });
      trackAndSetInput('');
      setCursorOffset(0);
      setPastedContents({});
      // Track usage for /discover and stop showing hint
      saveGlobalConfig(c => {
        if (c.hasUsedStash) return c;
        return {
          ...c,
          hasUsedStash: true
        };
      });
    }
  }, [input, cursorOffset, stashedPrompt, trackAndSetInput, setStashedPrompt, pastedContents, setPastedContents]);

  // Handler for chat:modelPicker - toggle model picker
  const handleModelPicker = useCallback(() => {
    setShowModelPicker(prev => !prev);
    if (helpOpen) {
      setHelpOpen(false);
    }
  }, [helpOpen]);

  // Handler for chat:fastMode - toggle fast mode picker
  const handleFastModePicker = useCallback(() => {
    setShowFastModePicker(prev => !prev);
    if (helpOpen) {
      setHelpOpen(false);
    }
  }, [helpOpen]);

  // Handler for chat:thinkingToggle - toggle thinking mode
  const handleThinkingToggle = useCallback(() => {
    setShowThinkingToggle(prev => !prev);
    if (helpOpen) {
      setHelpOpen(false);
    }
  }, [helpOpen]);

  // Handler for chat:cycleMode - cycle through permission modes
  const handleCycleMode = useCallback(() => {
    // When viewing a teammate, cycle their mode instead of the leader's
    if (isAgentSwarmsEnabled() && viewedTeammate && viewingAgentTaskId) {
      const teammateContext: ToolPermissionContext = {
        ...toolPermissionContext,
        mode: viewedTeammate.permissionMode
      };
      // Pass undefined for teamContext (unused but kept for API compatibility)
      const nextMode = getNextPermissionMode(teammateContext, undefined);
      logEvent('tengu_mode_cycle', {
        to: nextMode as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
      });
      const teammateTaskId = viewingAgentTaskId;
      setAppState(prev => {
        const task = prev.tasks[teammateTaskId];
        if (!task || task.type !== 'in_process_teammate') {
          return prev;
        }
        if (task.permissionMode === nextMode) {
          return prev;
        }
        return {
          ...prev,
          tasks: {
            ...prev.tasks,
            [teammateTaskId]: {
              ...task,
              permissionMode: nextMode
            }
          }
        };
      });
      if (helpOpen) {
        setHelpOpen(false);
      }
      return;
    }

    // Compute the next mode without triggering side effects first
    logForDebugging(`[auto-mode] handleCycleMode: currentMode=${toolPermissionContext.mode} isAutoModeAvailable=${toolPermissionContext.isAutoModeAvailable} showAutoModeOptIn=${showAutoModeOptIn} timeoutPending=${!!autoModeOptInTimeoutRef.current}`);
    const nextMode = getNextPermissionMode(toolPermissionContext, teamContext);

    // Check if user is entering auto mode for the first time. Gated on the
    // persistent settings flag (hasAutoModeOptIn) rather than the broader
    // hasAutoModeOptInAnySource so that --enable-auto-mode users still see
    // the warning dialog once β€” the CLI flag should grant carousel access,
    // not bypass the safety text.
    let isEnteringAutoModeFirstTime = false;
    if (feature('TRANSCRIPT_CLASSIFIER')) {
      isEnteringAutoModeFirstTime = nextMode === 'auto' && toolPermissionContext.mode !== 'auto' && !hasAutoModeOptIn() && !viewingAgentTaskId; // Only show for primary agent, not subagents
    }
    if (feature('TRANSCRIPT_CLASSIFIER')) {
      if (isEnteringAutoModeFirstTime) {
        // Store previous mode so we can revert if user declines
        setPreviousModeBeforeAuto(toolPermissionContext.mode);

        // Only update the UI mode label β€” do NOT call transitionPermissionMode
        // or cyclePermissionMode yet; we haven't confirmed with the user.
        setAppState(prev => ({
          ...prev,
          toolPermissionContext: {
            ...prev.toolPermissionContext,
            mode: 'auto'
          }
        }));
        setToolPermissionContext({
          ...toolPermissionContext,
          mode: 'auto'
        });

        // Show opt-in dialog after 400ms debounce
        if (autoModeOptInTimeoutRef.current) {
          clearTimeout(autoModeOptInTimeoutRef.current);
        }
        autoModeOptInTimeoutRef.current = setTimeout((setShowAutoModeOptIn, autoModeOptInTimeoutRef) => {
          setShowAutoModeOptIn(true);
          autoModeOptInTimeoutRef.current = null;
        }, 400, setShowAutoModeOptIn, autoModeOptInTimeoutRef);
        if (helpOpen) {
          setHelpOpen(false);
        }
        return;
      }
    }

    // Dismiss auto mode opt-in dialog if showing or pending (user is cycling away).
    // Do NOT revert to previousModeBeforeAuto here β€” shift+tab means "advance the
    // carousel", not "decline". Reverting causes a ping-pong loop: auto reverts to
    // the prior mode, whose next mode is auto again, forever.
    // The dialog's own decline button (handleAutoModeOptInDecline) handles revert.
    if (feature('TRANSCRIPT_CLASSIFIER')) {
      if (showAutoModeOptIn || autoModeOptInTimeoutRef.current) {
        if (showAutoModeOptIn) {
          logEvent('tengu_auto_mode_opt_in_dialog_decline', {});
        }
        setShowAutoModeOptIn(false);
        if (autoModeOptInTimeoutRef.current) {
          clearTimeout(autoModeOptInTimeoutRef.current);
          autoModeOptInTimeoutRef.current = null;
        }
        setPreviousModeBeforeAuto(null);
        // Fall through β€” mode is 'auto', cyclePermissionMode below goes to 'default'.
      }
    }

    // Now that we know this is NOT the first-time auto mode path,
    // call cyclePermissionMode to apply side effects (e.g. strip
    // dangerous permissions, activate classifier)
    const {
      context: preparedContext
    } = cyclePermissionMode(toolPermissionContext, teamContext);
    logEvent('tengu_mode_cycle', {
      to: nextMode as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
    });

    // Track when user enters plan mode
    if (nextMode === 'plan') {
      saveGlobalConfig(current => ({
        ...current,
        lastPlanModeUse: Date.now()
      }));
    }

    // Set the mode via setAppState directly because setToolPermissionContext
    // intentionally preserves the existing mode (to prevent coordinator mode
    // corruption from workers). Then call setToolPermissionContext to trigger
    // recheck of queued permission prompts.
    setAppState(prev => ({
      ...prev,
      toolPermissionContext: {
        ...preparedContext,
        mode: nextMode
      }
    }));
    setToolPermissionContext({
      ...preparedContext,
      mode: nextMode
    });

    // If this is a teammate, update config.json so team lead sees the change
    syncTeammateMode(nextMode, teamContext?.teamName);

    // Close help tips if they're open when mode is cycled
    if (helpOpen) {
      setHelpOpen(false);
    }
  }, [toolPermissionContext, teamContext, viewingAgentTaskId, viewedTeammate, setAppState, setToolPermissionContext, helpOpen, showAutoModeOptIn]);

  // Handler for auto mode opt-in dialog acceptance
  const handleAutoModeOptInAccept = useCallback(() => {
    if (feature('TRANSCRIPT_CLASSIFIER')) {
      setShowAutoModeOptIn(false);
      setPreviousModeBeforeAuto(null);

      // Now that the user accepted, apply the full transition: activate the
      // auto mode backend (classifier, beta headers) and strip dangerous
      // permissions (e.g. Bash(*) always-allow rules).
      const strippedContext = transitionPermissionMode(previousModeBeforeAuto ?? toolPermissionContext.mode, 'auto', toolPermissionContext);
      setAppState(prev => ({
        ...prev,
        toolPermissionContext: {
          ...strippedContext,
          mode: 'auto'
        }
      }));
      setToolPermissionContext({
        ...strippedContext,
        mode: 'auto'
      });

      // Close help tips if they're open when auto mode is enabled
      if (helpOpen) {
        setHelpOpen(false);
      }
    }
  }, [helpOpen, setHelpOpen, previousModeBeforeAuto, toolPermissionContext, setAppState, setToolPermissionContext]);

  // Handler for auto mode opt-in dialog decline
  const handleAutoModeOptInDecline = useCallback(() => {
    if (feature('TRANSCRIPT_CLASSIFIER')) {
      logForDebugging(`[auto-mode] handleAutoModeOptInDecline: reverting to ${previousModeBeforeAuto}, setting isAutoModeAvailable=false`);
      setShowAutoModeOptIn(false);
      if (autoModeOptInTimeoutRef.current) {
        clearTimeout(autoModeOptInTimeoutRef.current);
        autoModeOptInTimeoutRef.current = null;
      }

      // Revert to previous mode and remove auto from the carousel
      // for the rest of this session
      if (previousModeBeforeAuto) {
        setAutoModeActive(false);
        setAppState(prev => ({
          ...prev,
          toolPermissionContext: {
            ...prev.toolPermissionContext,
            mode: previousModeBeforeAuto,
            isAutoModeAvailable: false
          }
        }));
        setToolPermissionContext({
          ...toolPermissionContext,
          mode: previousModeBeforeAuto,
          isAutoModeAvailable: false
        });
        setPreviousModeBeforeAuto(null);
      }
    }
  }, [previousModeBeforeAuto, toolPermissionContext, setAppState, setToolPermissionContext]);

  // Handler for chat:imagePaste - paste image from clipboard
  const handleImagePaste = useCallback(() => {
    void getImageFromClipboard().then(imageData => {
      if (imageData) {
        onImagePaste(imageData.base64, imageData.mediaType);
      } else {
        const shortcutDisplay = getShortcutDisplay('chat:imagePaste', 'Chat', 'ctrl+v');
        const message = env.isSSH() ? "No image found in clipboard. You're SSH'd; try scp?" : `No image found in clipboard. Use ${shortcutDisplay} to paste images.`;
        addNotification({
          key: 'no-image-in-clipboard',
          text: message,
          priority: 'immediate',
          timeoutMs: 1000
        });
      }
    });
  }, [addNotification, onImagePaste]);

  // Register chat:submit handler directly in the handler registry (not via
  // useKeybindings) so that only the ChordInterceptor can invoke it for chord
  // completions (e.g., "ctrl+e s"). The default Enter binding for submit is
  // handled by TextInput directly (via onSubmit prop) and useTypeahead (for
  // autocomplete acceptance). Using useKeybindings would cause
  // stopImmediatePropagation on Enter, blocking autocomplete from seeing the key.
  const keybindingContext = useOptionalKeybindingContext();
  useEffect(() => {
    if (!keybindingContext || isModalOverlayActive) return;
    return keybindingContext.registerHandler({
      action: 'chat:submit',
      context: 'Chat',
      handler: () => {
        void onSubmit(input);
      }
    });
  }, [keybindingContext, isModalOverlayActive, onSubmit, input]);

  // Chat context keybindings for editing shortcuts
  // Note: history:previous/history:next are NOT handled here. They are passed as
  // onHistoryUp/onHistoryDown props to TextInput, so that useTextInput's
  // upOrHistoryUp/downOrHistoryDown can try cursor movement first and only
  // fall through to history when the cursor can't move further.
  const chatHandlers = useMemo(() => ({
    'chat:undo': handleUndo,
    'chat:newline': handleNewline,
    'chat:externalEditor': handleExternalEditor,
    'chat:stash': handleStash,
    'chat:modelPicker': handleModelPicker,
    'chat:thinkingToggle': handleThinkingToggle,
    'chat:cycleMode': handleCycleMode,
    'chat:imagePaste': handleImagePaste
  }), [handleUndo, handleNewline, handleExternalEditor, handleStash, handleModelPicker, handleThinkingToggle, handleCycleMode, handleImagePaste]);
  useKeybindings(chatHandlers, {
    context: 'Chat',
    isActive: !isModalOverlayActive
  });

  // Shift+↑ enters message-actions cursor. Separate isActive so ctrl+r search
  // doesn't leave stale isSearchingHistory on cursor-exit remount.
  useKeybinding('chat:messageActions', () => onMessageActionsEnter?.(), {
    context: 'Chat',
    isActive: !isModalOverlayActive && !isSearchingHistory
  });

  // Fast mode keybinding is only active when fast mode is enabled and available
  useKeybinding('chat:fastMode', handleFastModePicker, {
    context: 'Chat',
    isActive: !isModalOverlayActive && isFastModeEnabled() && isFastModeAvailable()
  });

  // Handle help:dismiss keybinding (ESC closes help menu)
  // This is registered separately from Chat context so it has priority over
  // CancelRequestHandler when help menu is open
  useKeybinding('help:dismiss', () => {
    setHelpOpen(false);
  }, {
    context: 'Help',
    isActive: helpOpen
  });

  // Quick Open / Global Search. Hook calls are unconditional (Rules of Hooks);
  // the handler body is feature()-gated so the setState calls and component
  // references get tree-shaken in external builds.
  const quickSearchActive = feature('QUICK_SEARCH') ? !isModalOverlayActive : false;
  useKeybinding('app:quickOpen', () => {
    if (feature('QUICK_SEARCH')) {
      setShowQuickOpen(true);
      setHelpOpen(false);
    }
  }, {
    context: 'Global',
    isActive: quickSearchActive
  });
  useKeybinding('app:globalSearch', () => {
    if (feature('QUICK_SEARCH')) {
      setShowGlobalSearch(true);
      setHelpOpen(false);
    }
  }, {
    context: 'Global',
    isActive: quickSearchActive
  });
  useKeybinding('history:search', () => {
    if (feature('HISTORY_PICKER')) {
      setShowHistoryPicker(true);
      setHelpOpen(false);
    }
  }, {
    context: 'Global',
    isActive: feature('HISTORY_PICKER') ? !isModalOverlayActive : false
  });

  // Handle Ctrl+C to abort speculation when idle (not loading)
  // CancelRequestHandler only handles Ctrl+C during active tasks
  useKeybinding('app:interrupt', () => {
    abortSpeculation(setAppState);
  }, {
    context: 'Global',
    isActive: !isLoading && speculation.status === 'active'
  });

  // Footer indicator navigation keybindings. ↑/↓ live here (not in
  // handleHistoryUp/Down) because TextInput focus=false when a pill is
  // selected β€” its useInput is inactive, so this is the only path.
  useKeybindings({
    'footer:up': () => {
      // ↑ scrolls within the coordinator task list before leaving the pill
      if (tasksSelected && "external" === 'ant' && coordinatorTaskCount > 0 && coordinatorTaskIndex > minCoordinatorIndex) {
        setCoordinatorTaskIndex(prev => prev - 1);
        return;
      }
      navigateFooter(-1, true);
    },
    'footer:down': () => {
      // ↓ scrolls within the coordinator task list, never leaves the pill
      if (tasksSelected && "external" === 'ant' && coordinatorTaskCount > 0) {
        if (coordinatorTaskIndex < coordinatorTaskCount - 1) {
          setCoordinatorTaskIndex(prev => prev + 1);
        }
        return;
      }
      if (tasksSelected && !isTeammateMode) {
        setShowBashesDialog(true);
        selectFooterItem(null);
        return;
      }
      navigateFooter(1);
    },
    'footer:next': () => {
      // Teammate mode: ←/β†’ cycles within the team member list
      if (tasksSelected && isTeammateMode) {
        const totalAgents = 1 + inProcessTeammates.length;
        setTeammateFooterIndex(prev => (prev + 1) % totalAgents);
        return;
      }
      navigateFooter(1);
    },
    'footer:previous': () => {
      if (tasksSelected && isTeammateMode) {
        const totalAgents = 1 + inProcessTeammates.length;
        setTeammateFooterIndex(prev => (prev - 1 + totalAgents) % totalAgents);
        return;
      }
      navigateFooter(-1);
    },
    'footer:openSelected': () => {
      if (viewSelectionMode === 'selecting-agent') {
        return;
      }
      switch (footerItemSelected) {
        case 'companion':
          if (feature('BUDDY')) {
            selectFooterItem(null);
            void onSubmit('/buddy');
          }
          break;
        case 'tasks':
          if (isTeammateMode) {
            // Enter switches to the selected agent's view
            if (teammateFooterIndex === 0) {
              exitTeammateView(setAppState);
            } else {
              const teammate = inProcessTeammates[teammateFooterIndex - 1];
              if (teammate) enterTeammateView(teammate.id, setAppState);
            }
          } else if (coordinatorTaskIndex === 0 && coordinatorTaskCount > 0) {
            exitTeammateView(setAppState);
          } else {
            const selectedTaskId = getVisibleAgentTasks(tasks)[coordinatorTaskIndex - 1]?.id;
            if (selectedTaskId) {
              enterTeammateView(selectedTaskId, setAppState);
            } else {
              setShowBashesDialog(true);
              selectFooterItem(null);
            }
          }
          break;
        case 'tmux':
          if ("external" === 'ant') {
            setAppState(prev => prev.tungstenPanelAutoHidden ? {
              ...prev,
              tungstenPanelAutoHidden: false
            } : {
              ...prev,
              tungstenPanelVisible: !(prev.tungstenPanelVisible ?? true)
            });
          }
          break;
        case 'bagel':
          break;
        case 'teams':
          setShowTeamsDialog(true);
          selectFooterItem(null);
          break;
        case 'bridge':
          setShowBridgeDialog(true);
          selectFooterItem(null);
          break;
      }
    },
    'footer:clearSelection': () => {
      selectFooterItem(null);
    },
    'footer:close': () => {
      if (tasksSelected && coordinatorTaskIndex >= 1) {
        const task = getVisibleAgentTasks(tasks)[coordinatorTaskIndex - 1];
        if (!task) return false;
        // When the selected row IS the viewed agent, 'x' types into the
        // steering input. Any other row β€” dismiss it.
        if (viewSelectionMode === 'viewing-agent' && task.id === viewingAgentTaskId) {
          onChange(input.slice(0, cursorOffset) + 'x' + input.slice(cursorOffset));
          setCursorOffset(cursorOffset + 1);
          return;
        }
        stopOrDismissAgent(task.id, setAppState);
        if (task.status !== 'running') {
          setCoordinatorTaskIndex(i => Math.max(minCoordinatorIndex, i - 1));
        }
        return;
      }
      // Not handled β€” let 'x' fall through to type-to-exit
      return false;
    }
  }, {
    context: 'Footer',
    isActive: !!footerItemSelected && !isModalOverlayActive
  });
  useInput((char, key) => {
    // Skip all input handling when a full-screen dialog is open. These dialogs
    // render via early return, but hooks run unconditionally β€” so without this
    // guard, Escape inside a dialog leaks to the double-press message-selector.
    if (showTeamsDialog || showQuickOpen || showGlobalSearch || showHistoryPicker) {
      return;
    }

    // Detect failed Alt shortcuts on macOS (Option key produces special characters)
    if (getPlatform() === 'macos' && isMacosOptionChar(char)) {
      const shortcut = MACOS_OPTION_SPECIAL_CHARS[char];
      const terminalName = getNativeCSIuTerminalDisplayName();
      const jsx = terminalName ? <Text dimColor>
          To enable {shortcut}, set <Text bold>Option as Meta</Text> in{' '}
          {terminalName} preferences (⌘,)
        </Text> : <Text dimColor>To enable {shortcut}, run /terminal-setup</Text>;
      addNotification({
        key: 'option-meta-hint',
        jsx,
        priority: 'immediate',
        timeoutMs: 5000
      });
      // Don't return - let the character be typed so user sees the issue
    }

    // Footer navigation is handled via useKeybindings above (Footer context)

    // NOTE: ctrl+_, ctrl+g, ctrl+s are handled via Chat context keybindings above

    // Type-to-exit footer: printable chars while a pill is selected refocus
    // the input and type the char. Nav keys are captured by useKeybindings
    // above, so anything reaching here is genuinely not a footer action.
    // onChange clears footerSelection, so no explicit deselect.
    if (footerItemSelected && char && !key.ctrl && !key.meta && !key.escape && !key.return) {
      onChange(input.slice(0, cursorOffset) + char + input.slice(cursorOffset));
      setCursorOffset(cursorOffset + char.length);
      return;
    }

    // Exit special modes when backspace/escape/delete/ctrl+u is pressed at cursor position 0
    if (cursorOffset === 0 && (key.escape || key.backspace || key.delete || key.ctrl && char === 'u')) {
      onModeChange('prompt');
      setHelpOpen(false);
    }

    // Exit help mode when backspace is pressed and input is empty
    if (helpOpen && input === '' && (key.backspace || key.delete)) {
      setHelpOpen(false);
    }

    // esc is a little overloaded:
    // - when we're loading a response, it's used to cancel the request
    // - otherwise, it's used to show the message selector
    // - when double pressed, it's used to clear the input
    // - when input is empty, pop from command queue

    // Handle ESC key press
    if (key.escape) {
      // Abort active speculation
      if (speculation.status === 'active') {
        abortSpeculation(setAppState);
        return;
      }

      // Dismiss side question response if visible
      if (isSideQuestionVisible && onDismissSideQuestion) {
        onDismissSideQuestion();
        return;
      }

      // Close help menu if open
      if (helpOpen) {
        setHelpOpen(false);
        return;
      }

      // Footer selection clearing is now handled via Footer context keybindings
      // (footer:clearSelection action bound to escape)
      // If a footer item is selected, let the Footer keybinding handle it
      if (footerItemSelected) {
        return;
      }

      // If there's an editable queued command, move it to the input for editing when ESC is pressed
      const hasEditableCommand = queuedCommands.some(isQueuedCommandEditable);
      if (hasEditableCommand) {
        void popAllCommandsFromQueue();
        return;
      }
      if (messages.length > 0 && !input && !isLoading) {
        doublePressEscFromEmpty();
      }
    }
    if (key.return && helpOpen) {
      setHelpOpen(false);
    }
  });
  const swarmBanner = useSwarmBanner();
  const fastModeCooldown = isFastModeEnabled() ? isFastModeCooldown() : false;
  const showFastIcon = isFastModeEnabled() ? isFastMode && (isFastModeAvailable() || fastModeCooldown) : false;
  const showFastIconHint = useShowFastIconHint(showFastIcon ?? false);

  // Show effort notification on startup and when effort changes.
  // Suppressed in brief/assistant mode β€” the value reflects the local
  // client's effort, not the connected agent's.
  const effortNotificationText = briefOwnsGap ? undefined : getEffortNotificationText(effortValue, mainLoopModel);
  useEffect(() => {
    if (!effortNotificationText) {
      removeNotification('effort-level');
      return;
    }
    addNotification({
      key: 'effort-level',
      text: effortNotificationText,
      priority: 'high',
      timeoutMs: 12_000
    });
  }, [effortNotificationText, addNotification, removeNotification]);
  useBuddyNotification();
  const companionSpeaking = feature('BUDDY') ?
  // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
  useAppState(s => s.companionReaction !== undefined) : false;
  const {
    columns,
    rows
  } = useTerminalSize();
  const textInputColumns = columns - 3 - companionReservedColumns(columns, companionSpeaking);

  // POC: click-to-position-cursor. Mouse tracking is only enabled inside
  // <AlternateScreen>, so this is dormant in the normal main-screen REPL.
  // localCol/localRow are relative to the onClick Box's top-left; the Box
  // tightly wraps the text input so they map directly to (column, line)
  // in the Cursor wrap model. MeasuredText.getOffsetFromPosition handles
  // wide chars, wrapped lines, and clamps past-end clicks to line end.
  const maxVisibleLines = isFullscreenEnvEnabled() ? Math.max(MIN_INPUT_VIEWPORT_LINES, Math.floor(rows / 2) - PROMPT_FOOTER_LINES) : undefined;
  const handleInputClick = useCallback((e: ClickEvent) => {
    // During history search the displayed text is historyMatch, not
    // input, and showCursor is false anyway β€” skip rather than
    // compute an offset against the wrong string.
    if (!input || isSearchingHistory) return;
    const c = Cursor.fromText(input, textInputColumns, cursorOffset);
    const viewportStart = c.getViewportStartLine(maxVisibleLines);
    const offset = c.measuredText.getOffsetFromPosition({
      line: e.localRow + viewportStart,
      column: e.localCol
    });
    setCursorOffset(offset);
  }, [input, textInputColumns, isSearchingHistory, cursorOffset, maxVisibleLines]);
  const handleOpenTasksDialog = useCallback((taskId?: string) => setShowBashesDialog(taskId ?? true), [setShowBashesDialog]);
  const placeholder = showPromptSuggestion && promptSuggestion ? promptSuggestion : defaultPlaceholder;

  // Calculate if input has multiple lines
  const isInputWrapped = useMemo(() => input.includes('\n'), [input]);

  // Memoized callbacks for model picker to prevent re-renders when unrelated
  // state (like notifications) changes. This prevents the inline model picker
  // from visually "jumping" when notifications arrive.
  const handleModelSelect = useCallback((model: string | null, _effort: EffortLevel | undefined) => {
    let wasFastModeDisabled = false;
    setAppState(prev => {
      wasFastModeDisabled = isFastModeEnabled() && !isFastModeSupportedByModel(model) && !!prev.fastMode;
      return {
        ...prev,
        mainLoopModel: model,
        mainLoopModelForSession: null,
        // Turn off fast mode if switching to a model that doesn't support it
        ...(wasFastModeDisabled && {
          fastMode: false
        })
      };
    });
    setShowModelPicker(false);
    const effectiveFastMode = (isFastMode ?? false) && !wasFastModeDisabled;
    let message = `Model set to ${modelDisplayString(model)}`;
    if (isBilledAsExtraUsage(model, effectiveFastMode, isOpus1mMergeEnabled())) {
      message += ' Β· Billed as extra usage';
    }
    if (wasFastModeDisabled) {
      message += ' Β· Fast mode OFF';
    }
    addNotification({
      key: 'model-switched',
      jsx: <Text>{message}</Text>,
      priority: 'immediate',
      timeoutMs: 3000
    });
    logEvent('tengu_model_picker_hotkey', {
      model: model as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
    });
  }, [setAppState, addNotification, isFastMode]);
  const handleModelCancel = useCallback(() => {
    setShowModelPicker(false);
  }, []);

  // Memoize the model picker element to prevent unnecessary re-renders
  // when AppState changes for unrelated reasons (e.g., notifications arriving)
  const modelPickerElement = useMemo(() => {
    if (!showModelPicker) return null;
    return <Box flexDirection="column" marginTop={1}>
        <ModelPicker initial={mainLoopModel_} sessionModel={mainLoopModelForSession} onSelect={handleModelSelect} onCancel={handleModelCancel} isStandaloneCommand showFastModeNotice={isFastModeEnabled() && isFastMode && isFastModeSupportedByModel(mainLoopModel_) && isFastModeAvailable()} />
      </Box>;
  }, [showModelPicker, mainLoopModel_, mainLoopModelForSession, handleModelSelect, handleModelCancel]);
  const handleFastModeSelect = useCallback((result?: string) => {
    setShowFastModePicker(false);
    if (result) {
      addNotification({
        key: 'fast-mode-toggled',
        jsx: <Text>{result}</Text>,
        priority: 'immediate',
        timeoutMs: 3000
      });
    }
  }, [addNotification]);

  // Memoize the fast mode picker element
  const fastModePickerElement = useMemo(() => {
    if (!showFastModePicker) return null;
    return <Box flexDirection="column" marginTop={1}>
        <FastModePicker onDone={handleFastModeSelect} unavailableReason={getFastModeUnavailableReason()} />
      </Box>;
  }, [showFastModePicker, handleFastModeSelect]);

  // Memoized callbacks for thinking toggle
  const handleThinkingSelect = useCallback((enabled: boolean) => {
    setAppState(prev => ({
      ...prev,
      thinkingEnabled: enabled
    }));
    setShowThinkingToggle(false);
    logEvent('tengu_thinking_toggled_hotkey', {
      enabled
    });
    addNotification({
      key: 'thinking-toggled-hotkey',
      jsx: <Text color={enabled ? 'suggestion' : undefined} dimColor={!enabled}>
            Thinking {enabled ? 'on' : 'off'}
          </Text>,
      priority: 'immediate',
      timeoutMs: 3000
    });
  }, [setAppState, addNotification]);
  const handleThinkingCancel = useCallback(() => {
    setShowThinkingToggle(false);
  }, []);

  // Memoize the thinking toggle element
  const thinkingToggleElement = useMemo(() => {
    if (!showThinkingToggle) return null;
    return <Box flexDirection="column" marginTop={1}>
        <ThinkingToggle currentValue={thinkingEnabled ?? true} onSelect={handleThinkingSelect} onCancel={handleThinkingCancel} isMidConversation={messages.some(m => m.type === 'assistant')} />
      </Box>;
  }, [showThinkingToggle, thinkingEnabled, handleThinkingSelect, handleThinkingCancel, messages.length]);

  // Portal dialog to DialogOverlay in fullscreen so it escapes the bottom
  // slot's overflowY:hidden clip (same pattern as SuggestionsOverlay).
  // Must be called before early returns below to satisfy rules-of-hooks.
  // Memoized so the portal useEffect doesn't churn on every PromptInput render.
  const autoModeOptInDialog = useMemo(() => feature('TRANSCRIPT_CLASSIFIER') && showAutoModeOptIn ? <AutoModeOptInDialog onAccept={handleAutoModeOptInAccept} onDecline={handleAutoModeOptInDecline} /> : null, [showAutoModeOptIn, handleAutoModeOptInAccept, handleAutoModeOptInDecline]);
  useSetPromptOverlayDialog(isFullscreenEnvEnabled() ? autoModeOptInDialog : null);
  if (showBashesDialog) {
    return <BackgroundTasksDialog onDone={() => setShowBashesDialog(false)} toolUseContext={getToolUseContext(messages, [], new AbortController(), mainLoopModel)} initialDetailTaskId={typeof showBashesDialog === 'string' ? showBashesDialog : undefined} />;
  }
  if (isAgentSwarmsEnabled() && showTeamsDialog) {
    return <TeamsDialog initialTeams={cachedTeams} onDone={() => {
      setShowTeamsDialog(false);
    }} />;
  }
  if (feature('QUICK_SEARCH')) {
    const insertWithSpacing = (text: string) => {
      const cursorChar = input[cursorOffset - 1] ?? ' ';
      insertTextAtCursor(/\s/.test(cursorChar) ? text : ` ${text}`);
    };
    if (showQuickOpen) {
      return <QuickOpenDialog onDone={() => setShowQuickOpen(false)} onInsert={insertWithSpacing} />;
    }
    if (showGlobalSearch) {
      return <GlobalSearchDialog onDone={() => setShowGlobalSearch(false)} onInsert={insertWithSpacing} />;
    }
  }
  if (feature('HISTORY_PICKER') && showHistoryPicker) {
    return <HistorySearchDialog initialQuery={input} onSelect={entry => {
      const entryMode = getModeFromInput(entry.display);
      const value = getValueFromInput(entry.display);
      onModeChange(entryMode);
      trackAndSetInput(value);
      setPastedContents(entry.pastedContents);
      setCursorOffset(value.length);
      setShowHistoryPicker(false);
    }} onCancel={() => setShowHistoryPicker(false)} />;
  }

  // Show loop mode menu when requested (ant-only, eliminated from external builds)
  if (modelPickerElement) {
    return modelPickerElement;
  }
  if (fastModePickerElement) {
    return fastModePickerElement;
  }
  if (thinkingToggleElement) {
    return thinkingToggleElement;
  }
  if (showBridgeDialog) {
    return <BridgeDialog onDone={() => {
      setShowBridgeDialog(false);
      selectFooterItem(null);
    }} />;
  }
  const baseProps: BaseTextInputProps = {
    multiline: true,
    onSubmit,
    onChange,
    value: historyMatch ? getValueFromInput(typeof historyMatch === 'string' ? historyMatch : historyMatch.display) : input,
    // History navigation is handled via TextInput props (onHistoryUp/onHistoryDown),
    // NOT via useKeybindings. This allows useTextInput's upOrHistoryUp/downOrHistoryDown
    // to try cursor movement first and only fall through to history navigation when the
    // cursor can't move further (important for wrapped text and multi-line input).
    onHistoryUp: handleHistoryUp,
    onHistoryDown: handleHistoryDown,
    onHistoryReset: resetHistory,
    placeholder,
    onExit,
    onExitMessage: (show, key) => setExitMessage({
      show,
      key
    }),
    onImagePaste,
    columns: textInputColumns,
    maxVisibleLines,
    disableCursorMovementForUpDownKeys: suggestions.length > 0 || !!footerItemSelected,
    disableEscapeDoublePress: suggestions.length > 0,
    cursorOffset,
    onChangeCursorOffset: setCursorOffset,
    onPaste: onTextPaste,
    onIsPastingChange: setIsPasting,
    focus: !isSearchingHistory && !isModalOverlayActive && !footerItemSelected,
    showCursor: !footerItemSelected && !isSearchingHistory && !cursorAtImageChip,
    argumentHint: commandArgumentHint,
    onUndo: canUndo ? () => {
      const previousState = undo();
      if (previousState) {
        trackAndSetInput(previousState.text);
        setCursorOffset(previousState.cursorOffset);
        setPastedContents(previousState.pastedContents);
      }
    } : undefined,
    highlights: combinedHighlights,
    inlineGhostText,
    inputFilter: lazySpaceInputFilter
  };
  const getBorderColor = (): keyof Theme => {
    const modeColors: Record<string, keyof Theme> = {
      bash: 'bashBorder'
    };

    // Mode colors take priority, then teammate color, then default
    if (modeColors[mode]) {
      return modeColors[mode];
    }

    // In-process teammates run headless - don't apply teammate colors to leader UI
    if (isInProcessTeammate()) {
      return 'promptBorder';
    }

    // Check for teammate color from environment
    const teammateColorName = getTeammateColor();
    if (teammateColorName && AGENT_COLORS.includes(teammateColorName as AgentColorName)) {
      return AGENT_COLOR_TO_THEME_COLOR[teammateColorName as AgentColorName];
    }
    return 'promptBorder';
  };
  if (isExternalEditorActive) {
    return <Box flexDirection="row" alignItems="center" justifyContent="center" borderColor={getBorderColor()} borderStyle="round" borderLeft={false} borderRight={false} borderBottom width="100%">
        <Text dimColor italic>
          Save and close editor to continue...
        </Text>
      </Box>;
  }
  const textInputElement = isVimModeEnabled() ? <VimTextInput {...baseProps} initialMode={vimMode} onModeChange={setVimMode} /> : <TextInput {...baseProps} />;
  return <Box flexDirection="column" marginTop={briefOwnsGap ? 0 : 1}>
      {!isFullscreenEnvEnabled() && <PromptInputQueuedCommands />}
      {hasSuppressedDialogs && <Box marginTop={1} marginLeft={2}>
          <Text dimColor>Waiting for permission…</Text>
        </Box>}
      <PromptInputStashNotice hasStash={stashedPrompt !== undefined} />
      {swarmBanner ? <>
          <Text color={swarmBanner.bgColor}>
            {swarmBanner.text ? <>
                {'─'.repeat(Math.max(0, columns - stringWidth(swarmBanner.text) - 4))}
                <Text backgroundColor={swarmBanner.bgColor} color="inverseText">
                  {' '}
                  {swarmBanner.text}{' '}
                </Text>
                {'──'}
              </> : '─'.repeat(columns)}
          </Text>
          <Box flexDirection="row" width="100%">
            <PromptInputModeIndicator mode={mode} isLoading={isLoading} viewingAgentName={viewingAgentName} viewingAgentColor={viewingAgentColor} />
            <Box flexGrow={1} flexShrink={1} onClick={handleInputClick}>
              {textInputElement}
            </Box>
          </Box>
          <Text color={swarmBanner.bgColor}>{'─'.repeat(columns)}</Text>
        </> : <Box flexDirection="row" alignItems="flex-start" justifyContent="flex-start" borderColor={getBorderColor()} borderStyle="round" borderLeft={false} borderRight={false} borderBottom width="100%" borderText={buildBorderText(showFastIcon ?? false, showFastIconHint, fastModeCooldown)}>
          <PromptInputModeIndicator mode={mode} isLoading={isLoading} viewingAgentName={viewingAgentName} viewingAgentColor={viewingAgentColor} />
          <Box flexGrow={1} flexShrink={1} onClick={handleInputClick}>
            {textInputElement}
          </Box>
        </Box>}
      <PromptInputFooter apiKeyStatus={apiKeyStatus} debug={debug} exitMessage={exitMessage} vimMode={isVimModeEnabled() ? vimMode : undefined} mode={mode} autoUpdaterResult={autoUpdaterResult} isAutoUpdating={isAutoUpdating} verbose={verbose} onAutoUpdaterResult={onAutoUpdaterResult} onChangeIsUpdating={setIsAutoUpdating} suggestions={suggestions} selectedSuggestion={selectedSuggestion} maxColumnWidth={maxColumnWidth} toolPermissionContext={effectiveToolPermissionContext} helpOpen={helpOpen} suppressHint={input.length > 0} isLoading={isLoading} tasksSelected={tasksSelected} teamsSelected={teamsSelected} bridgeSelected={bridgeSelected} tmuxSelected={tmuxSelected} teammateFooterIndex={teammateFooterIndex} ideSelection={ideSelection} mcpClients={mcpClients} isPasting={isPasting} isInputWrapped={isInputWrapped} messages={messages} isSearching={isSearchingHistory} historyQuery={historyQuery} setHistoryQuery={setHistoryQuery} historyFailedMatch={historyFailedMatch} onOpenTasksDialog={isFullscreenEnvEnabled() ? handleOpenTasksDialog : undefined} />
      {isFullscreenEnvEnabled() ? null : autoModeOptInDialog}
      {isFullscreenEnvEnabled() ?
    // position=absolute takes zero layout height so the spinner
    // doesn't shift when a notification appears/disappears. Yoga
    // anchors absolute children at the parent's content-box origin;
    // marginTop=-1 pulls it into the marginTop=1 gap row above the
    // prompt border. In brief mode there is no such gap (briefOwnsGap
    // strips our marginTop) and BriefSpinner sits flush against the
    // border β€” marginTop=-2 skips over the spinner content into
    // BriefSpinner's own marginTop=1 blank row. height=1 +
    // overflow=hidden clips multi-line notifications to a single row.
    // flex-end anchors the bottom line so the visible row is always
    // the most recent. Suppressed while the slash overlay or
    // auto-mode opt-in dialog is up by height=0 (NOT unmount) β€” this
    // Box renders later in tree order so it would paint over their
    // bottom row. Keeping Notifications mounted prevents AutoUpdater's
    // initial-check effect from re-firing on every slash-completion
    // toggle (PR#22413).
    <Box position="absolute" marginTop={briefOwnsGap ? -2 : -1} height={suggestions.length === 0 && !showAutoModeOptIn ? 1 : 0} width="100%" paddingLeft={2} paddingRight={1} flexDirection="column" justifyContent="flex-end" overflow="hidden">
          <Notifications apiKeyStatus={apiKeyStatus} autoUpdaterResult={autoUpdaterResult} debug={debug} isAutoUpdating={isAutoUpdating} verbose={verbose} messages={messages} onAutoUpdaterResult={onAutoUpdaterResult} onChangeIsUpdating={setIsAutoUpdating} ideSelection={ideSelection} mcpClients={mcpClients} isInputWrapped={isInputWrapped} />
        </Box> : null}
    </Box>;
}

/**
 * Compute the initial paste ID by finding the max ID used in existing messages.
 * This handles --continue/--resume scenarios where we need to avoid ID collisions.
 */
function getInitialPasteId(messages: Message[]): number {
  let maxId = 0;
  for (const message of messages) {
    if (message.type === 'user') {
      // Check image paste IDs
      if (message.imagePasteIds) {
        for (const id of message.imagePasteIds) {
          if (id > maxId) maxId = id;
        }
      }
      // Check text paste references in message content
      if (Array.isArray(message.message.content)) {
        for (const block of message.message.content) {
          if (block.type === 'text') {
            const refs = parseReferences(block.text);
            for (const ref of refs) {
              if (ref.id > maxId) maxId = ref.id;
            }
          }
        }
      }
    }
  }
  return maxId + 1;
}
function buildBorderText(showFastIcon: boolean, showFastIconHint: boolean, fastModeCooldown: boolean): BorderTextOptions | undefined {
  if (!showFastIcon) return undefined;
  const fastSeg = showFastIconHint ? `${getFastIconString(true, fastModeCooldown)} ${chalk.dim('/fast')}` : getFastIconString(true, fastModeCooldown);
  return {
    content: ` ${fastSeg} `,
    position: 'top',
    align: 'end',
    offset: 0
  };
}
export default React.memo(PromptInput);
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJmZWF0dXJlIiwiY2hhbGsiLCJwYXRoIiwiUmVhY3QiLCJ1c2VDYWxsYmFjayIsInVzZUVmZmVjdCIsInVzZU1lbW8iLCJ1c2VSZWYiLCJ1c2VTdGF0ZSIsInVzZVN5bmNFeHRlcm5hbFN0b3JlIiwidXNlTm90aWZpY2F0aW9ucyIsInVzZUNvbW1hbmRRdWV1ZSIsIklERUF0TWVudGlvbmVkIiwidXNlSWRlQXRNZW50aW9uZWQiLCJBbmFseXRpY3NNZXRhZGF0YV9JX1ZFUklGSUVEX1RISVNfSVNfTk9UX0NPREVfT1JfRklMRVBBVEhTIiwibG9nRXZlbnQiLCJBcHBTdGF0ZSIsInVzZUFwcFN0YXRlIiwidXNlQXBwU3RhdGVTdG9yZSIsInVzZVNldEFwcFN0YXRlIiwiRm9vdGVySXRlbSIsImdldEN3ZCIsImlzUXVldWVkQ29tbWFuZEVkaXRhYmxlIiwicG9wQWxsRWRpdGFibGUiLCJzdHJpcEFuc2kiLCJjb21wYW5pb25SZXNlcnZlZENvbHVtbnMiLCJmaW5kQnVkZHlUcmlnZ2VyUG9zaXRpb25zIiwidXNlQnVkZHlOb3RpZmljYXRpb24iLCJGYXN0TW9kZVBpY2tlciIsImlzVWx0cmFyZXZpZXdFbmFibGVkIiwiZ2V0TmF0aXZlQ1NJdVRlcm1pbmFsRGlzcGxheU5hbWUiLCJDb21tYW5kIiwiaGFzQ29tbWFuZCIsInVzZUlzTW9kYWxPdmVybGF5QWN0aXZlIiwidXNlU2V0UHJvbXB0T3ZlcmxheURpYWxvZyIsImZvcm1hdEltYWdlUmVmIiwiZm9ybWF0UGFzdGVkVGV4dFJlZiIsImdldFBhc3RlZFRleHRSZWZOdW1MaW5lcyIsInBhcnNlUmVmZXJlbmNlcyIsIlZlcmlmaWNhdGlvblN0YXR1cyIsIkhpc3RvcnlNb2RlIiwidXNlQXJyb3dLZXlIaXN0b3J5IiwidXNlRG91YmxlUHJlc3MiLCJ1c2VIaXN0b3J5U2VhcmNoIiwiSURFU2VsZWN0aW9uIiwidXNlSW5wdXRCdWZmZXIiLCJ1c2VNYWluTG9vcE1vZGVsIiwidXNlUHJvbXB0U3VnZ2VzdGlvbiIsInVzZVRlcm1pbmFsU2l6ZSIsInVzZVR5cGVhaGVhZCIsIkJvcmRlclRleHRPcHRpb25zIiwic3RyaW5nV2lkdGgiLCJCb3giLCJDbGlja0V2ZW50IiwiS2V5IiwiVGV4dCIsInVzZUlucHV0IiwidXNlT3B0aW9uYWxLZXliaW5kaW5nQ29udGV4dCIsImdldFNob3J0Y3V0RGlzcGxheSIsInVzZUtleWJpbmRpbmciLCJ1c2VLZXliaW5kaW5ncyIsIk1DUFNlcnZlckNvbm5lY3Rpb24iLCJhYm9ydFByb21wdFN1Z2dlc3Rpb24iLCJsb2dTdWdnZXN0aW9uU3VwcHJlc3NlZCIsIkFjdGl2ZVNwZWN1bGF0aW9uU3RhdGUiLCJhYm9ydFNwZWN1bGF0aW9uIiwiZ2V0QWN0aXZlQWdlbnRGb3JJbnB1dCIsImdldFZpZXdlZFRlYW1tYXRlVGFzayIsImVudGVyVGVhbW1hdGVWaWV3IiwiZXhpdFRlYW1tYXRlVmlldyIsInN0b3BPckRpc21pc3NBZ2VudCIsIlRvb2xQZXJtaXNzaW9uQ29udGV4dCIsImdldFJ1bm5pbmdUZWFtbWF0ZXNTb3J0ZWQiLCJJblByb2Nlc3NUZWFtbWF0ZVRhc2tTdGF0ZSIsImlzUGFuZWxBZ2VudFRhc2siLCJMb2NhbEFnZW50VGFza1N0YXRlIiwiaXNCYWNrZ3JvdW5kVGFzayIsIkFHRU5UX0NPTE9SX1RPX1RIRU1FX0NPTE9SIiwiQUdFTlRfQ09MT1JTIiwiQWdlbnRDb2xvck5hbWUiLCJBZ2VudERlZmluaXRpb24iLCJNZXNzYWdlIiwiUGVybWlzc2lvbk1vZGUiLCJCYXNlVGV4dElucHV0UHJvcHMiLCJQcm9tcHRJbnB1dE1vZGUiLCJWaW1Nb2RlIiwiaXNBZ2VudFN3YXJtc0VuYWJsZWQiLCJjb3VudCIsIkF1dG9VcGRhdGVyUmVzdWx0IiwiQ3Vyc29yIiwiZ2V0R2xvYmFsQ29uZmlnIiwiUGFzdGVkQ29udGVudCIsInNhdmVHbG9iYWxDb25maWciLCJsb2dGb3JEZWJ1Z2dpbmciLCJwYXJzZURpcmVjdE1lbWJlck1lc3NhZ2UiLCJzZW5kRGlyZWN0TWVtYmVyTWVzc2FnZSIsIkVmZm9ydExldmVsIiwiZW52IiwiZXJyb3JNZXNzYWdlIiwiaXNCaWxsZWRBc0V4dHJhVXNhZ2UiLCJnZXRGYXN0TW9kZVVuYXZhaWxhYmxlUmVhc29uIiwiaXNGYXN0TW9kZUF2YWlsYWJsZSIsImlzRmFzdE1vZGVDb29sZG93biIsImlzRmFzdE1vZGVFbmFibGVkIiwiaXNGYXN0TW9kZVN1cHBvcnRlZEJ5TW9kZWwiLCJpc0Z1bGxzY3JlZW5FbnZFbmFibGVkIiwiUHJvbXB0SW5wdXRIZWxwZXJzIiwiZ2V0SW1hZ2VGcm9tQ2xpcGJvYXJkIiwiUEFTVEVfVEhSRVNIT0xEIiwiSW1hZ2VEaW1lbnNpb25zIiwiY2FjaGVJbWFnZVBhdGgiLCJzdG9yZUltYWdlIiwiaXNNYWNvc09wdGlvbkNoYXIiLCJNQUNPU19PUFRJT05fU1BFQ0lBTF9DSEFSUyIsImxvZ0Vycm9yIiwiaXNPcHVzMW1NZXJnZUVuYWJsZWQiLCJtb2RlbERpc3BsYXlTdHJpbmciLCJzZXRBdXRvTW9kZUFjdGl2ZSIsImN5Y2xlUGVybWlzc2lvbk1vZGUiLCJnZXROZXh0UGVybWlzc2lvbk1vZGUiLCJ0cmFuc2l0aW9uUGVybWlzc2lvbk1vZGUiLCJnZXRQbGF0Zm9ybSIsIlByb2Nlc3NVc2VySW5wdXRDb250ZXh0IiwiZWRpdFByb21wdEluRWRpdG9yIiwiaGFzQXV0b01vZGVPcHRJbiIsImZpbmRCdHdUcmlnZ2VyUG9zaXRpb25zIiwiZmluZFNsYXNoQ29tbWFuZFBvc2l0aW9ucyIsImZpbmRTbGFja0NoYW5uZWxQb3NpdGlvbnMiLCJnZXRLbm93bkNoYW5uZWxzVmVyc2lvbiIsImhhc1NsYWNrTWNwU2VydmVyIiwic3Vic2NyaWJlS25vd25DaGFubmVscyIsImlzSW5Qcm9jZXNzRW5hYmxlZCIsInN5bmNUZWFtbWF0ZU1vZGUiLCJUZWFtU3VtbWFyeSIsImdldFRlYW1tYXRlQ29sb3IiLCJpc0luUHJvY2Vzc1RlYW1tYXRlIiwid3JpdGVUb01haWxib3giLCJUZXh0SGlnaGxpZ2h0IiwiVGhlbWUiLCJmaW5kVGhpbmtpbmdUcmlnZ2VyUG9zaXRpb25zIiwiZ2V0UmFpbmJvd0NvbG9yIiwiaXNVbHRyYXRoaW5rRW5hYmxlZCIsImZpbmRUb2tlbkJ1ZGdldFBvc2l0aW9ucyIsImZpbmRVbHRyYXBsYW5UcmlnZ2VyUG9zaXRpb25zIiwiZmluZFVsdHJhcmV2aWV3VHJpZ2dlclBvc2l0aW9ucyIsIkF1dG9Nb2RlT3B0SW5EaWFsb2ciLCJCcmlkZ2VEaWFsb2ciLCJDb25maWd1cmFibGVTaG9ydGN1dEhpbnQiLCJnZXRWaXNpYmxlQWdlbnRUYXNrcyIsInVzZUNvb3JkaW5hdG9yVGFza0NvdW50IiwiZ2V0RWZmb3J0Tm90aWZpY2F0aW9uVGV4dCIsImdldEZhc3RJY29uU3RyaW5nIiwiR2xvYmFsU2VhcmNoRGlhbG9nIiwiSGlzdG9yeVNlYXJjaERpYWxvZyIsIk1vZGVsUGlja2VyIiwiUXVpY2tPcGVuRGlhbG9nIiwiVGV4dElucHV0IiwiVGhpbmtpbmdUb2dnbGUiLCJCYWNrZ3JvdW5kVGFza3NEaWFsb2ciLCJzaG91bGRIaWRlVGFza3NGb290ZXIiLCJUZWFtc0RpYWxvZyIsIlZpbVRleHRJbnB1dCIsImdldE1vZGVGcm9tSW5wdXQiLCJnZXRWYWx1ZUZyb21JbnB1dCIsIkZPT1RFUl9URU1QT1JBUllfU1RBVFVTX1RJTUVPVVQiLCJOb3RpZmljYXRpb25zIiwiUHJvbXB0SW5wdXRGb290ZXIiLCJTdWdnZXN0aW9uSXRlbSIsIlByb21wdElucHV0TW9kZUluZGljYXRvciIsIlByb21wdElucHV0UXVldWVkQ29tbWFuZHMiLCJQcm9tcHRJbnB1dFN0YXNoTm90aWNlIiwidXNlTWF5YmVUcnVuY2F0ZUlucHV0IiwidXNlUHJvbXB0SW5wdXRQbGFjZWhvbGRlciIsInVzZVNob3dGYXN0SWNvbkhpbnQiLCJ1c2VTd2FybUJhbm5lciIsImlzTm9uU3BhY2VQcmludGFibGUiLCJpc1ZpbU1vZGVFbmFibGVkIiwiUHJvcHMiLCJkZWJ1ZyIsImlkZVNlbGVjdGlvbiIsInRvb2xQZXJtaXNzaW9uQ29udGV4dCIsInNldFRvb2xQZXJtaXNzaW9uQ29udGV4dCIsImN0eCIsImFwaUtleVN0YXR1cyIsImNvbW1hbmRzIiwiYWdlbnRzIiwiaXNMb2FkaW5nIiwidmVyYm9zZSIsIm1lc3NhZ2VzIiwib25BdXRvVXBkYXRlclJlc3VsdCIsInJlc3VsdCIsImF1dG9VcGRhdGVyUmVzdWx0IiwiaW5wdXQiLCJvbklucHV0Q2hhbmdlIiwidmFsdWUiLCJtb2RlIiwib25Nb2RlQ2hhbmdlIiwic3Rhc2hlZFByb21wdCIsInRleHQiLCJjdXJzb3JPZmZzZXQiLCJwYXN0ZWRDb250ZW50cyIsIlJlY29yZCIsInNldFN0YXNoZWRQcm9tcHQiLCJzdWJtaXRDb3VudCIsIm9uU2hvd01lc3NhZ2VTZWxlY3RvciIsIm9uTWVzc2FnZUFjdGlvbnNFbnRlciIsIm1jcENsaWVudHMiLCJzZXRQYXN0ZWRDb250ZW50cyIsIkRpc3BhdGNoIiwiU2V0U3RhdGVBY3Rpb24iLCJ2aW1Nb2RlIiwic2V0VmltTW9kZSIsInNob3dCYXNoZXNEaWFsb2ciLCJzZXRTaG93QmFzaGVzRGlhbG9nIiwic2hvdyIsIm9uRXhpdCIsImdldFRvb2xVc2VDb250ZXh0IiwibmV3TWVzc2FnZXMiLCJhYm9ydENvbnRyb2xsZXIiLCJBYm9ydENvbnRyb2xsZXIiLCJtYWluTG9vcE1vZGVsIiwib25TdWJtaXQiLCJoZWxwZXJzIiwic3BlY3VsYXRpb25BY2NlcHQiLCJzdGF0ZSIsInNwZWN1bGF0aW9uU2Vzc2lvblRpbWVTYXZlZE1zIiwic2V0QXBwU3RhdGUiLCJmIiwicHJldiIsIm9wdGlvbnMiLCJmcm9tS2V5YmluZGluZyIsIlByb21pc2UiLCJvbkFnZW50U3VibWl0IiwidGFzayIsImlzU2VhcmNoaW5nSGlzdG9yeSIsInNldElzU2VhcmNoaW5nSGlzdG9yeSIsImlzU2VhcmNoaW5nIiwib25EaXNtaXNzU2lkZVF1ZXN0aW9uIiwiaXNTaWRlUXVlc3Rpb25WaXNpYmxlIiwiaGVscE9wZW4iLCJzZXRIZWxwT3BlbiIsImhhc1N1cHByZXNzZWREaWFsb2dzIiwiaXNMb2NhbEpTWENvbW1hbmRBY3RpdmUiLCJpbnNlcnRUZXh0UmVmIiwiTXV0YWJsZVJlZk9iamVjdCIsImluc2VydCIsInNldElucHV0V2l0aEN1cnNvciIsImN1cnNvciIsInZvaWNlSW50ZXJpbVJhbmdlIiwic3RhcnQiLCJlbmQiLCJQUk9NUFRfRk9PVEVSX0xJTkVTIiwiTUlOX0lOUFVUX1ZJRVdQT1JUX0xJTkVTIiwiUHJvbXB0SW5wdXQiLCJvblN1Ym1pdFByb3AiLCJSZWFjdE5vZGUiLCJpc01vZGFsT3ZlcmxheUFjdGl2ZSIsImlzQXV0b1VwZGF0aW5nIiwic2V0SXNBdXRvVXBkYXRpbmciLCJleGl0TWVzc2FnZSIsInNldEV4aXRNZXNzYWdlIiwia2V5Iiwic2V0Q3Vyc29yT2Zmc2V0IiwibGVuZ3RoIiwibGFzdEludGVybmFsSW5wdXRSZWYiLCJjdXJyZW50IiwidHJhY2tBbmRTZXRJbnB1dCIsIm5lZWRzU3BhY2UiLCJ0ZXN0IiwiaW5zZXJ0VGV4dCIsIm5ld1ZhbHVlIiwic2xpY2UiLCJzdG9yZSIsInRhc2tzIiwicyIsInJlcGxCcmlkZ2VDb25uZWN0ZWQiLCJyZXBsQnJpZGdlRXhwbGljaXQiLCJyZXBsQnJpZGdlUmVjb25uZWN0aW5nIiwiYnJpZGdlRm9vdGVyVmlzaWJsZSIsImhhc1R1bmdzdGVuU2Vzc2lvbiIsInR1bmdzdGVuQWN0aXZlU2Vzc2lvbiIsInVuZGVmaW5lZCIsInRtdXhGb290ZXJWaXNpYmxlIiwiYmFnZWxGb290ZXJWaXNpYmxlIiwidGVhbUNvbnRleHQiLCJxdWV1ZWRDb21tYW5kcyIsInByb21wdFN1Z2dlc3Rpb25TdGF0ZSIsInByb21wdFN1Z2dlc3Rpb24iLCJzcGVjdWxhdGlvbiIsInZpZXdpbmdBZ2VudFRhc2tJZCIsInZpZXdTZWxlY3Rpb25Nb2RlIiwic2hvd1NwaW5uZXJUcmVlIiwiZXhwYW5kZWRWaWV3IiwiY29tcGFuaW9uIiwiX2NvbXBhbmlvbiIsImNvbXBhbmlvbk11dGVkIiwiY29tcGFuaW9uRm9vdGVyVmlzaWJsZSIsImJyaWVmT3duc0dhcCIsImlzQnJpZWZPbmx5IiwibWFpbkxvb3BNb2RlbF8iLCJtYWluTG9vcE1vZGVsRm9yU2Vzc2lvbiIsInRoaW5raW5nRW5hYmxlZCIsImlzRmFzdE1vZGUiLCJmYXN0TW9kZSIsImVmZm9ydFZhbHVlIiwidmlld2VkVGVhbW1hdGUiLCJnZXRTdGF0ZSIsInZpZXdpbmdBZ2VudE5hbWUiLCJpZGVudGl0eSIsImFnZW50TmFtZSIsInZpZXdpbmdBZ2VudENvbG9yIiwiY29sb3IiLCJpbmNsdWRlcyIsImluUHJvY2Vzc1RlYW1tYXRlcyIsImlzVGVhbW1hdGVNb2RlIiwiZWZmZWN0aXZlVG9vbFBlcm1pc3Npb25Db250ZXh0IiwicGVybWlzc2lvbk1vZGUiLCJoaXN0b3J5UXVlcnkiLCJzZXRIaXN0b3J5UXVlcnkiLCJoaXN0b3J5TWF0Y2giLCJoaXN0b3J5RmFpbGVkTWF0Y2giLCJlbnRyeSIsImRpc3BsYXkiLCJuZXh0UGFzdGVJZFJlZiIsImdldEluaXRpYWxQYXN0ZUlkIiwicGVuZGluZ1NwYWNlQWZ0ZXJQaWxsUmVmIiwic2hvd1RlYW1zRGlhbG9nIiwic2V0U2hvd1RlYW1zRGlhbG9nIiwic2hvd0JyaWRnZURpYWxvZyIsInNldFNob3dCcmlkZ2VEaWFsb2ciLCJ0ZWFtbWF0ZUZvb3RlckluZGV4Iiwic2V0VGVhbW1hdGVGb290ZXJJbmRleCIsImNvb3JkaW5hdG9yVGFza0luZGV4Iiwic2V0Q29vcmRpbmF0b3JUYXNrSW5kZXgiLCJ2IiwibmV4dCIsImNvb3JkaW5hdG9yVGFza0NvdW50IiwiaGFzQmdUYXNrUGlsbCIsIk9iamVjdCIsInZhbHVlcyIsInNvbWUiLCJ0IiwibWluQ29vcmRpbmF0b3JJbmRleCIsIk1hdGgiLCJtYXgiLCJpc1Bhc3RpbmciLCJzZXRJc1Bhc3RpbmciLCJpc0V4dGVybmFsRWRpdG9yQWN0aXZlIiwic2V0SXNFeHRlcm5hbEVkaXRvckFjdGl2ZSIsInNob3dNb2RlbFBpY2tlciIsInNldFNob3dNb2RlbFBpY2tlciIsInNob3dRdWlja09wZW4iLCJzZXRTaG93UXVpY2tPcGVuIiwic2hvd0dsb2JhbFNlYXJjaCIsInNldFNob3dHbG9iYWxTZWFyY2giLCJzaG93SGlzdG9yeVBpY2tlciIsInNldFNob3dIaXN0b3J5UGlja2VyIiwic2hvd0Zhc3RNb2RlUGlja2VyIiwic2V0U2hvd0Zhc3RNb2RlUGlja2VyIiwic2hvd1RoaW5raW5nVG9nZ2xlIiwic2V0U2hvd1RoaW5raW5nVG9nZ2xlIiwic2hvd0F1dG9Nb2RlT3B0SW4iLCJzZXRTaG93QXV0b01vZGVPcHRJbiIsInByZXZpb3VzTW9kZUJlZm9yZUF1dG8iLCJzZXRQcmV2aW91c01vZGVCZWZvcmVBdXRvIiwiYXV0b01vZGVPcHRJblRpbWVvdXRSZWYiLCJOb2RlSlMiLCJUaW1lb3V0IiwiaXNDdXJzb3JPbkZpcnN0TGluZSIsImZpcnN0TmV3bGluZUluZGV4IiwiaW5kZXhPZiIsImlzQ3Vyc29yT25MYXN0TGluZSIsImxhc3ROZXdsaW5lSW5kZXgiLCJsYXN0SW5kZXhPZiIsImNhY2hlZFRlYW1zIiwidGVhbW1hdGVDb3VudCIsInRlYW1tYXRlcyIsIm5hbWUiLCJ0ZWFtTmFtZSIsIm1lbWJlckNvdW50IiwicnVubmluZ0NvdW50IiwiaWRsZUNvdW50IiwicnVubmluZ1Rhc2tDb3VudCIsInN0YXR1cyIsInRhc2tzRm9vdGVyVmlzaWJsZSIsInRlYW1zRm9vdGVyVmlzaWJsZSIsImZvb3Rlckl0ZW1zIiwiZmlsdGVyIiwiQm9vbGVhbiIsInJhd0Zvb3RlclNlbGVjdGlvbiIsImZvb3RlclNlbGVjdGlvbiIsImZvb3Rlckl0ZW1TZWxlY3RlZCIsInRhc2tzU2VsZWN0ZWQiLCJ0bXV4U2VsZWN0ZWQiLCJiYWdlbFNlbGVjdGVkIiwidGVhbXNTZWxlY3RlZCIsImJyaWRnZVNlbGVjdGVkIiwic2VsZWN0Rm9vdGVySXRlbSIsIml0ZW0iLCJuYXZpZ2F0ZUZvb3RlciIsImRlbHRhIiwiZXhpdEF0U3RhcnQiLCJpZHgiLCJzdWdnZXN0aW9uIiwibWFya0FjY2VwdGVkIiwibG9nT3V0Y29tZUF0U3VibWlzc2lvbiIsIm1hcmtTaG93biIsImlucHV0VmFsdWUiLCJpc0Fzc2lzdGFudFJlc3BvbmRpbmciLCJkaXNwbGF5ZWRWYWx1ZSIsInRoaW5rVHJpZ2dlcnMiLCJ1bHRyYXBsYW5TZXNzaW9uVXJsIiwidWx0cmFwbGFuTGF1bmNoaW5nIiwidWx0cmFwbGFuVHJpZ2dlcnMiLCJ1bHRyYXJldmlld1RyaWdnZXJzIiwiYnR3VHJpZ2dlcnMiLCJidWRkeVRyaWdnZXJzIiwic2xhc2hDb21tYW5kVHJpZ2dlcnMiLCJwb3NpdGlvbnMiLCJwb3MiLCJjb21tYW5kTmFtZSIsInRva2VuQnVkZ2V0VHJpZ2dlcnMiLCJrbm93bkNoYW5uZWxzVmVyc2lvbiIsInNsYWNrQ2hhbm5lbFRyaWdnZXJzIiwibWNwIiwiY2xpZW50cyIsIm1lbWJlck1lbnRpb25IaWdobGlnaHRzIiwiQXJyYXkiLCJ0aGVtZUNvbG9yIiwiaGlnaGxpZ2h0cyIsIm1lbWJlcnMiLCJyZWdleCIsIm1lbWJlclZhbHVlcyIsIm1hdGNoIiwiZXhlYyIsImxlYWRpbmdTcGFjZSIsIm5hbWVTdGFydCIsImluZGV4IiwiZnVsbE1hdGNoIiwidHJpbVN0YXJ0IiwibWVtYmVyIiwiZmluZCIsInB1c2giLCJpbWFnZVJlZlBvc2l0aW9ucyIsInIiLCJzdGFydHNXaXRoIiwibWFwIiwiY3Vyc29yQXRJbWFnZUNoaXAiLCJpbnNpZGUiLCJtaWQiLCJjb21iaW5lZEhpZ2hsaWdodHMiLCJyZWYiLCJpbnZlcnNlIiwicHJpb3JpdHkiLCJ0cmlnZ2VyIiwibWVudGlvbiIsImRpbUNvbG9yIiwiaSIsInNoaW1tZXJDb2xvciIsImFkZE5vdGlmaWNhdGlvbiIsInJlbW92ZU5vdGlmaWNhdGlvbiIsInRpbWVvdXRNcyIsInByZXZJbnB1dExlbmd0aFJlZiIsInBlYWtJbnB1dExlbmd0aFJlZiIsImRpc21pc3NTdGFzaEhpbnQiLCJwcmV2TGVuZ3RoIiwicGVha0xlbmd0aCIsImN1cnJlbnRMZW5ndGgiLCJjbGVhcmVkU3Vic3RhbnRpYWxJbnB1dCIsIndhc1JhcGlkQ2xlYXIiLCJjb25maWciLCJoYXNVc2VkU3Rhc2giLCJqc3giLCJwdXNoVG9CdWZmZXIiLCJ1bmRvIiwiY2FuVW5kbyIsImNsZWFyQnVmZmVyIiwibWF4QnVmZmVyU2l6ZSIsImRlYm91bmNlTXMiLCJkZWZhdWx0UGxhY2Vob2xkZXIiLCJvbkNoYW5nZSIsImlzU2luZ2xlQ2hhckluc2VydGlvbiIsImluc2VydGVkQXRTdGFydCIsInZhbHVlV2l0aG91dE1vZGUiLCJyZXBsYWNlQWxsIiwicHJvY2Vzc2VkVmFsdWUiLCJyZXNldEhpc3RvcnkiLCJvbkhpc3RvcnlVcCIsIm9uSGlzdG9yeURvd24iLCJkaXNtaXNzU2VhcmNoSGludCIsImhpc3RvcnlJbmRleCIsImhpc3RvcnlNb2RlIiwiaGFuZGxlSGlzdG9yeVVwIiwic3VnZ2VzdGlvbnMiLCJoYXNFZGl0YWJsZUNvbW1hbmQiLCJwb3BBbGxDb21tYW5kc0Zyb21RdWV1ZSIsImhhbmRsZUhpc3RvcnlEb3duIiwiZmlyc3QiLCJoYXNTZWVuVGFza3NIaW50IiwiYyIsInN1Z2dlc3Rpb25zU3RhdGUiLCJzZXRTdWdnZXN0aW9uc1N0YXRlUmF3Iiwic2VsZWN0ZWRTdWdnZXN0aW9uIiwiY29tbWFuZEFyZ3VtZW50SGludCIsInNldFN1Z2dlc3Rpb25zU3RhdGUiLCJ1cGRhdGVyIiwiaW5wdXRQYXJhbSIsImlzU3VibWl0dGluZ1NsYXNoQ29tbWFuZCIsInRyaW1FbmQiLCJoYXNJbWFnZXMiLCJ0eXBlIiwic3VnZ2VzdGlvblRleHQiLCJpbnB1dE1hdGNoZXNTdWdnZXN0aW9uIiwidHJpbSIsInNraXBSZXNldCIsInNob3duQXQiLCJkaXJlY3RNZXNzYWdlIiwicmVjaXBpZW50TmFtZSIsIm1lc3NhZ2UiLCJzdWNjZXNzIiwiZXJyb3IiLCJoYXNEaXJlY3RvcnlTdWdnZXN0aW9ucyIsImV2ZXJ5IiwiZGVzY3JpcHRpb24iLCJhY3RpdmVBZ2VudCIsImlubGluZUdob3N0VGV4dCIsIm1heENvbHVtbldpZHRoIiwic3VwcHJlc3NTdWdnZXN0aW9ucyIsInNob3dQcm9tcHRTdWdnZXN0aW9uIiwicHJvbXB0SWQiLCJhY2NlcHRlZEF0IiwiZ2VuZXJhdGlvblJlcXVlc3RJZCIsIm9uSW1hZ2VQYXN0ZSIsImltYWdlIiwibWVkaWFUeXBlIiwiZmlsZW5hbWUiLCJkaW1lbnNpb25zIiwic291cmNlUGF0aCIsInBhc3RlSWQiLCJuZXdDb250ZW50IiwiaWQiLCJjb250ZW50IiwicHJlZml4IiwiaW5zZXJ0VGV4dEF0Q3Vyc29yIiwicmVmZXJlbmNlZElkcyIsIlNldCIsIm9ycGhhbmVkIiwiaGFzIiwiaW1nIiwib25UZXh0UGFzdGUiLCJyYXdUZXh0IiwicmVwbGFjZSIsInBhc3RlZE1vZGUiLCJudW1MaW5lcyIsIm1heExpbmVzIiwibWluIiwicm93cyIsImxhenlTcGFjZUlucHV0RmlsdGVyIiwibmV3SW5wdXQiLCJkb3VibGVQcmVzc0VzY0Zyb21FbXB0eSIsImltYWdlcyIsIm5ld0NvbnRlbnRzIiwib25JZGVBdE1lbnRpb25lZCIsImF0TWVudGlvbmVkIiwiYXRNZW50aW9uZWRUZXh0IiwicmVsYXRpdmVQYXRoIiwicmVsYXRpdmUiLCJmaWxlUGF0aCIsImxpbmVTdGFydCIsImxpbmVFbmQiLCJjdXJzb3JDaGFyIiwiaGFuZGxlVW5kbyIsInByZXZpb3VzU3RhdGUiLCJoYW5kbGVOZXdsaW5lIiwiaGFuZGxlRXh0ZXJuYWxFZGl0b3IiLCJlcnIiLCJFcnJvciIsImhhbmRsZVN0YXNoIiwiaGFuZGxlTW9kZWxQaWNrZXIiLCJoYW5kbGVGYXN0TW9kZVBpY2tlciIsImhhbmRsZVRoaW5raW5nVG9nZ2xlIiwiaGFuZGxlQ3ljbGVNb2RlIiwidGVhbW1hdGVDb250ZXh0IiwibmV4dE1vZGUiLCJ0byIsInRlYW1tYXRlVGFza0lkIiwiaXNBdXRvTW9kZUF2YWlsYWJsZSIsImlzRW50ZXJpbmdBdXRvTW9kZUZpcnN0VGltZSIsImNsZWFyVGltZW91dCIsInNldFRpbWVvdXQiLCJjb250ZXh0IiwicHJlcGFyZWRDb250ZXh0IiwibGFzdFBsYW5Nb2RlVXNlIiwiRGF0ZSIsIm5vdyIsImhhbmRsZUF1dG9Nb2RlT3B0SW5BY2NlcHQiLCJzdHJpcHBlZENvbnRleHQiLCJoYW5kbGVBdXRvTW9kZU9wdEluRGVjbGluZSIsImhhbmRsZUltYWdlUGFzdGUiLCJ0aGVuIiwiaW1hZ2VEYXRhIiwiYmFzZTY0Iiwic2hvcnRjdXREaXNwbGF5IiwiaXNTU0giLCJrZXliaW5kaW5nQ29udGV4dCIsInJlZ2lzdGVySGFuZGxlciIsImFjdGlvbiIsImhhbmRsZXIiLCJjaGF0SGFuZGxlcnMiLCJpc0FjdGl2ZSIsInF1aWNrU2VhcmNoQWN0aXZlIiwiZm9vdGVyOnVwIiwiZm9vdGVyOmRvd24iLCJmb290ZXI6bmV4dCIsInRvdGFsQWdlbnRzIiwiZm9vdGVyOnByZXZpb3VzIiwiZm9vdGVyOm9wZW5TZWxlY3RlZCIsInRlYW1tYXRlIiwic2VsZWN0ZWRUYXNrSWQiLCJ0dW5nc3RlblBhbmVsQXV0b0hpZGRlbiIsInR1bmdzdGVuUGFuZWxWaXNpYmxlIiwiZm9vdGVyOmNsZWFyU2VsZWN0aW9uIiwiZm9vdGVyOmNsb3NlIiwiY2hhciIsInNob3J0Y3V0IiwidGVybWluYWxOYW1lIiwiY3RybCIsIm1ldGEiLCJlc2NhcGUiLCJyZXR1cm4iLCJiYWNrc3BhY2UiLCJkZWxldGUiLCJzd2FybUJhbm5lciIsImZhc3RNb2RlQ29vbGRvd24iLCJzaG93RmFzdEljb24iLCJzaG93RmFzdEljb25IaW50IiwiZWZmb3J0Tm90aWZpY2F0aW9uVGV4dCIsImNvbXBhbmlvblNwZWFraW5nIiwiY29tcGFuaW9uUmVhY3Rpb24iLCJjb2x1bW5zIiwidGV4dElucHV0Q29sdW1ucyIsIm1heFZpc2libGVMaW5lcyIsImZsb29yIiwiaGFuZGxlSW5wdXRDbGljayIsImUiLCJmcm9tVGV4dCIsInZpZXdwb3J0U3RhcnQiLCJnZXRWaWV3cG9ydFN0YXJ0TGluZSIsIm9mZnNldCIsIm1lYXN1cmVkVGV4dCIsImdldE9mZnNldEZyb21Qb3NpdGlvbiIsImxpbmUiLCJsb2NhbFJvdyIsImNvbHVtbiIsImxvY2FsQ29sIiwiaGFuZGxlT3BlblRhc2tzRGlhbG9nIiwidGFza0lkIiwicGxhY2Vob2xkZXIiLCJpc0lucHV0V3JhcHBlZCIsImhhbmRsZU1vZGVsU2VsZWN0IiwibW9kZWwiLCJfZWZmb3J0Iiwid2FzRmFzdE1vZGVEaXNhYmxlZCIsImVmZmVjdGl2ZUZhc3RNb2RlIiwiaGFuZGxlTW9kZWxDYW5jZWwiLCJtb2RlbFBpY2tlckVsZW1lbnQiLCJoYW5kbGVGYXN0TW9kZVNlbGVjdCIsImZhc3RNb2RlUGlja2VyRWxlbWVudCIsImhhbmRsZVRoaW5raW5nU2VsZWN0IiwiZW5hYmxlZCIsImhhbmRsZVRoaW5raW5nQ2FuY2VsIiwidGhpbmtpbmdUb2dnbGVFbGVtZW50IiwibSIsImF1dG9Nb2RlT3B0SW5EaWFsb2ciLCJpbnNlcnRXaXRoU3BhY2luZyIsImVudHJ5TW9kZSIsImJhc2VQcm9wcyIsIm11bHRpbGluZSIsIm9uSGlzdG9yeVJlc2V0Iiwib25FeGl0TWVzc2FnZSIsImRpc2FibGVDdXJzb3JNb3ZlbWVudEZvclVwRG93bktleXMiLCJkaXNhYmxlRXNjYXBlRG91YmxlUHJlc3MiLCJvbkNoYW5nZUN1cnNvck9mZnNldCIsIm9uUGFzdGUiLCJvbklzUGFzdGluZ0NoYW5nZSIsImZvY3VzIiwic2hvd0N1cnNvciIsImFyZ3VtZW50SGludCIsIm9uVW5kbyIsImlucHV0RmlsdGVyIiwiZ2V0Qm9yZGVyQ29sb3IiLCJtb2RlQ29sb3JzIiwiYmFzaCIsInRlYW1tYXRlQ29sb3JOYW1lIiwidGV4dElucHV0RWxlbWVudCIsImJnQ29sb3IiLCJyZXBlYXQiLCJidWlsZEJvcmRlclRleHQiLCJtYXhJZCIsImltYWdlUGFzdGVJZHMiLCJpc0FycmF5IiwiYmxvY2siLCJyZWZzIiwiZmFzdFNlZyIsImRpbSIsInBvc2l0aW9uIiwiYWxpZ24iLCJtZW1vIl0sInNvdXJjZXMiOlsiUHJvbXB0SW5wdXQudHN4Il0sInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7IGZlYXR1cmUgfSBmcm9tICdidW46YnVuZGxlJ1xuaW1wb3J0IGNoYWxrIGZyb20gJ2NoYWxrJ1xuaW1wb3J0ICogYXMgcGF0aCBmcm9tICdwYXRoJ1xuaW1wb3J0ICogYXMgUmVhY3QgZnJvbSAncmVhY3QnXG5pbXBvcnQge1xuICB1c2VDYWxsYmFjayxcbiAgdXNlRWZmZWN0LFxuICB1c2VNZW1vLFxuICB1c2VSZWYsXG4gIHVzZVN0YXRlLFxuICB1c2VTeW5jRXh0ZXJuYWxTdG9yZSxcbn0gZnJvbSAncmVhY3QnXG5pbXBvcnQgeyB1c2VOb3RpZmljYXRpb25zIH0gZnJvbSAnc3JjL2NvbnRleHQvbm90aWZpY2F0aW9ucy5qcydcbmltcG9ydCB7IHVzZUNvbW1hbmRRdWV1ZSB9IGZyb20gJ3NyYy9ob29rcy91c2VDb21tYW5kUXVldWUuanMnXG5pbXBvcnQge1xuICB0eXBlIElERUF0TWVudGlvbmVkLFxuICB1c2VJZGVBdE1lbnRpb25lZCxcbn0gZnJvbSAnc3JjL2hvb2tzL3VzZUlkZUF0TWVudGlvbmVkLmpzJ1xuaW1wb3J0IHtcbiAgdHlwZSBBbmFseXRpY3NNZXRhZGF0YV9JX1ZFUklGSUVEX1RISVNfSVNfTk9UX0NPREVfT1JfRklMRVBBVEhTLFxuICBsb2dFdmVudCxcbn0gZnJvbSAnc3JjL3NlcnZpY2VzL2FuYWx5dGljcy9pbmRleC5qcydcbmltcG9ydCB7XG4gIHR5cGUgQXBwU3RhdGUsXG4gIHVzZUFwcFN0YXRlLFxuICB1c2VBcHBTdGF0ZVN0b3JlLFxuICB1c2VTZXRBcHBTdGF0ZSxcbn0gZnJvbSAnc3JjL3N0YXRlL0FwcFN0YXRlLmpzJ1xuaW1wb3J0IHR5cGUgeyBGb290ZXJJdGVtIH0gZnJvbSAnc3JjL3N0YXRlL0FwcFN0YXRlU3RvcmUuanMnXG5pbXBvcnQgeyBnZXRDd2QgfSBmcm9tICdzcmMvdXRpbHMvY3dkLmpzJ1xuaW1wb3J0IHtcbiAgaXNRdWV1ZWRDb21tYW5kRWRpdGFibGUsXG4gIHBvcEFsbEVkaXRhYmxlLFxufSBmcm9tICdzcmMvdXRpbHMvbWVzc2FnZVF1ZXVlTWFuYWdlci5qcydcbmltcG9ydCBzdHJpcEFuc2kgZnJvbSAnc3RyaXAtYW5zaSdcbmltcG9ydCB7IGNvbXBhbmlvblJlc2VydmVkQ29sdW1ucyB9IGZyb20gJy4uLy4uL2J1ZGR5L0NvbXBhbmlvblNwcml0ZS5qcydcbmltcG9ydCB7XG4gIGZpbmRCdWRkeVRyaWdnZXJQb3NpdGlvbnMsXG4gIHVzZUJ1ZGR5Tm90aWZpY2F0aW9uLFxufSBmcm9tICcuLi8uLi9idWRkeS91c2VCdWRkeU5vdGlmaWNhdGlvbi5qcydcbmltcG9ydCB7IEZhc3RNb2RlUGlja2VyIH0gZnJvbSAnLi4vLi4vY29tbWFuZHMvZmFzdC9mYXN0LmpzJ1xuaW1wb3J0IHsgaXNVbHRyYXJldmlld0VuYWJsZWQgfSBmcm9tICcuLi8uLi9jb21tYW5kcy9yZXZpZXcvdWx0cmFyZXZpZXdFbmFibGVkLmpzJ1xuaW1wb3J0IHsgZ2V0TmF0aXZlQ1NJdVRlcm1pbmFsRGlzcGxheU5hbWUgfSBmcm9tICcuLi8uLi9jb21tYW5kcy90ZXJtaW5hbFNldHVwL3Rlcm1pbmFsU2V0dXAuanMnXG5pbXBvcnQgeyB0eXBlIENvbW1hbmQsIGhhc0NvbW1hbmQgfSBmcm9tICcuLi8uLi9jb21tYW5kcy5qcydcbmltcG9ydCB7IHVzZUlzTW9kYWxPdmVybGF5QWN0aXZlIH0gZnJvbSAnLi4vLi4vY29udGV4dC9vdmVybGF5Q29udGV4dC5qcydcbmltcG9ydCB7IHVzZVNldFByb21wdE92ZXJsYXlEaWFsb2cgfSBmcm9tICcuLi8uLi9jb250ZXh0L3Byb21wdE92ZXJsYXlDb250ZXh0LmpzJ1xuaW1wb3J0IHtcbiAgZm9ybWF0SW1hZ2VSZWYsXG4gIGZvcm1hdFBhc3RlZFRleHRSZWYsXG4gIGdldFBhc3RlZFRleHRSZWZOdW1MaW5lcyxcbiAgcGFyc2VSZWZlcmVuY2VzLFxufSBmcm9tICcuLi8uLi9oaXN0b3J5LmpzJ1xuaW1wb3J0IHR5cGUgeyBWZXJpZmljYXRpb25TdGF0dXMgfSBmcm9tICcuLi8uLi9ob29rcy91c2VBcGlLZXlWZXJpZmljYXRpb24uanMnXG5pbXBvcnQge1xuICB0eXBlIEhpc3RvcnlNb2RlLFxuICB1c2VBcnJvd0tleUhpc3RvcnksXG59IGZyb20gJy4uLy4uL2hvb2tzL3VzZUFycm93S2V5SGlzdG9yeS5qcydcbmltcG9ydCB7IHVzZURvdWJsZVByZXNzIH0gZnJvbSAnLi4vLi4vaG9va3MvdXNlRG91YmxlUHJlc3MuanMnXG5pbXBvcnQgeyB1c2VIaXN0b3J5U2VhcmNoIH0gZnJvbSAnLi4vLi4vaG9va3MvdXNlSGlzdG9yeVNlYXJjaC5qcydcbmltcG9ydCB0eXBlIHsgSURFU2VsZWN0aW9uIH0gZnJvbSAnLi4vLi4vaG9va3MvdXNlSWRlU2VsZWN0aW9uLmpzJ1xuaW1wb3J0IHsgdXNlSW5wdXRCdWZmZXIgfSBmcm9tICcuLi8uLi9ob29rcy91c2VJbnB1dEJ1ZmZlci5qcydcbmltcG9ydCB7IHVzZU1haW5Mb29wTW9kZWwgfSBmcm9tICcuLi8uLi9ob29rcy91c2VNYWluTG9vcE1vZGVsLmpzJ1xuaW1wb3J0IHsgdXNlUHJvbXB0U3VnZ2VzdGlvbiB9IGZyb20gJy4uLy4uL2hvb2tzL3VzZVByb21wdFN1Z2dlc3Rpb24uanMnXG5pbXBvcnQgeyB1c2VUZXJtaW5hbFNpemUgfSBmcm9tICcuLi8uLi9ob29rcy91c2VUZXJtaW5hbFNpemUuanMnXG5pbXBvcnQgeyB1c2VUeXBlYWhlYWQgfSBmcm9tICcuLi8uLi9ob29rcy91c2VUeXBlYWhlYWQuanMnXG5pbXBvcnQgdHlwZSB7IEJvcmRlclRleHRPcHRpb25zIH0gZnJvbSAnLi4vLi4vaW5rL3JlbmRlci1ib3JkZXIuanMnXG5pbXBvcnQgeyBzdHJpbmdXaWR0aCB9IGZyb20gJy4uLy4uL2luay9zdHJpbmdXaWR0aC5qcydcbmltcG9ydCB7IEJveCwgdHlwZSBDbGlja0V2ZW50LCB0eXBlIEtleSwgVGV4dCwgdXNlSW5wdXQgfSBmcm9tICcuLi8uLi9pbmsuanMnXG5pbXBvcnQgeyB1c2VPcHRpb25hbEtleWJpbmRpbmdDb250ZXh0IH0gZnJvbSAnLi4vLi4va2V5YmluZGluZ3MvS2V5YmluZGluZ0NvbnRleHQuanMnXG5pbXBvcnQgeyBnZXRTaG9ydGN1dERpc3BsYXkgfSBmcm9tICcuLi8uLi9rZXliaW5kaW5ncy9zaG9ydGN1dEZvcm1hdC5qcydcbmltcG9ydCB7XG4gIHVzZUtleWJpbmRpbmcsXG4gIHVzZUtleWJpbmRpbmdzLFxufSBmcm9tICcuLi8uLi9rZXliaW5kaW5ncy91c2VLZXliaW5kaW5nLmpzJ1xuaW1wb3J0IHR5cGUgeyBNQ1BTZXJ2ZXJDb25uZWN0aW9uIH0gZnJvbSAnLi4vLi4vc2VydmljZXMvbWNwL3R5cGVzLmpzJ1xuaW1wb3J0IHtcbiAgYWJvcnRQcm9tcHRTdWdnZXN0aW9uLFxuICBsb2dTdWdnZXN0aW9uU3VwcHJlc3NlZCxcbn0gZnJvbSAnLi4vLi4vc2VydmljZXMvUHJvbXB0U3VnZ2VzdGlvbi9wcm9tcHRTdWdnZXN0aW9uLmpzJ1xuaW1wb3J0IHtcbiAgdHlwZSBBY3RpdmVTcGVjdWxhdGlvblN0YXRlLFxuICBhYm9ydFNwZWN1bGF0aW9uLFxufSBmcm9tICcuLi8uLi9zZXJ2aWNlcy9Qcm9tcHRTdWdnZXN0aW9uL3NwZWN1bGF0aW9uLmpzJ1xuaW1wb3J0IHtcbiAgZ2V0QWN0aXZlQWdlbnRGb3JJbnB1dCxcbiAgZ2V0Vmlld2VkVGVhbW1hdGVUYXNrLFxufSBmcm9tICcuLi8uLi9zdGF0ZS9zZWxlY3RvcnMuanMnXG5pbXBvcnQge1xuICBlbnRlclRlYW1tYXRlVmlldyxcbiAgZXhpdFRlYW1tYXRlVmlldyxcbiAgc3RvcE9yRGlzbWlzc0FnZW50LFxufSBmcm9tICcuLi8uLi9zdGF0ZS90ZWFtbWF0ZVZpZXdIZWxwZXJzLmpzJ1xuaW1wb3J0IHR5cGUgeyBUb29sUGVybWlzc2lvbkNvbnRleHQgfSBmcm9tICcuLi8uLi9Ub29sLmpzJ1xuaW1wb3J0IHsgZ2V0UnVubmluZ1RlYW1tYXRlc1NvcnRlZCB9IGZyb20gJy4uLy4uL3Rhc2tzL0luUHJvY2Vzc1RlYW1tYXRlVGFzay9JblByb2Nlc3NUZWFtbWF0ZVRhc2suanMnXG5pbXBvcnQgdHlwZSB7IEluUHJvY2Vzc1RlYW1tYXRlVGFza1N0YXRlIH0gZnJvbSAnLi4vLi4vdGFza3MvSW5Qcm9jZXNzVGVhbW1hdGVUYXNrL3R5cGVzLmpzJ1xuaW1wb3J0IHtcbiAgaXNQYW5lbEFnZW50VGFzayxcbiAgdHlwZSBMb2NhbEFnZW50VGFza1N0YXRlLFxufSBmcm9tICcuLi8uLi90YXNrcy9Mb2NhbEFnZW50VGFzay9Mb2NhbEFnZW50VGFzay5qcydcbmltcG9ydCB7IGlzQmFja2dyb3VuZFRhc2sgfSBmcm9tICcuLi8uLi90YXNrcy90eXBlcy5qcydcbmltcG9ydCB7XG4gIEFHRU5UX0NPTE9SX1RPX1RIRU1FX0NPTE9SLFxuICBBR0VOVF9DT0xPUlMsXG4gIHR5cGUgQWdlbnRDb2xvck5hbWUsXG59IGZyb20gJy4uLy4uL3Rvb2xzL0FnZW50VG9vbC9hZ2VudENvbG9yTWFuYWdlci5qcydcbmltcG9ydCB0eXBlIHsgQWdlbnREZWZpbml0aW9uIH0gZnJvbSAnLi4vLi4vdG9vbHMvQWdlbnRUb29sL2xvYWRBZ2VudHNEaXIuanMnXG5pbXBvcnQgdHlwZSB7IE1lc3NhZ2UgfSBmcm9tICcuLi8uLi90eXBlcy9tZXNzYWdlLmpzJ1xuaW1wb3J0IHR5cGUgeyBQZXJtaXNzaW9uTW9kZSB9IGZyb20gJy4uLy4uL3R5cGVzL3Blcm1pc3Npb25zLmpzJ1xuaW1wb3J0IHR5cGUge1xuICBCYXNlVGV4dElucHV0UHJvcHMsXG4gIFByb21wdElucHV0TW9kZSxcbiAgVmltTW9kZSxcbn0gZnJvbSAnLi4vLi4vdHlwZXMvdGV4dElucHV0VHlwZXMuanMnXG5pbXBvcnQgeyBpc0FnZW50U3dhcm1zRW5hYmxlZCB9IGZyb20gJy4uLy4uL3V0aWxzL2FnZW50U3dhcm1zRW5hYmxlZC5qcydcbmltcG9ydCB7IGNvdW50IH0gZnJvbSAnLi4vLi4vdXRpbHMvYXJyYXkuanMnXG5pbXBvcnQgdHlwZSB7IEF1dG9VcGRhdGVyUmVzdWx0IH0gZnJvbSAnLi4vLi4vdXRpbHMvYXV0b1VwZGF0ZXIuanMnXG5pbXBvcnQgeyBDdXJzb3IgfSBmcm9tICcuLi8uLi91dGlscy9DdXJzb3IuanMnXG5pbXBvcnQge1xuICBnZXRHbG9iYWxDb25maWcsXG4gIHR5cGUgUGFzdGVkQ29udGVudCxcbiAgc2F2ZUdsb2JhbENvbmZpZyxcbn0gZnJvbSAnLi4vLi4vdXRpbHMvY29uZmlnLmpzJ1xuaW1wb3J0IHsgbG9nRm9yRGVidWdnaW5nIH0gZnJvbSAnLi4vLi4vdXRpbHMvZGVidWcuanMnXG5pbXBvcnQge1xuICBwYXJzZURpcmVjdE1lbWJlck1lc3NhZ2UsXG4gIHNlbmREaXJlY3RNZW1iZXJNZXNzYWdlLFxufSBmcm9tICcuLi8uLi91dGlscy9kaXJlY3RNZW1iZXJNZXNzYWdlLmpzJ1xuaW1wb3J0IHR5cGUgeyBFZmZvcnRMZXZlbCB9IGZyb20gJy4uLy4uL3V0aWxzL2VmZm9ydC5qcydcbmltcG9ydCB7IGVudiB9IGZyb20gJy4uLy4uL3V0aWxzL2Vudi5qcydcbmltcG9ydCB7IGVycm9yTWVzc2FnZSB9IGZyb20gJy4uLy4uL3V0aWxzL2Vycm9ycy5qcydcbmltcG9ydCB7IGlzQmlsbGVkQXNFeHRyYVVzYWdlIH0gZnJvbSAnLi4vLi4vdXRpbHMvZXh0cmFVc2FnZS5qcydcbmltcG9ydCB7XG4gIGdldEZhc3RNb2RlVW5hdmFpbGFibGVSZWFzb24sXG4gIGlzRmFzdE1vZGVBdmFpbGFibGUsXG4gIGlzRmFzdE1vZGVDb29sZG93bixcbiAgaXNGYXN0TW9kZUVuYWJsZWQsXG4gIGlzRmFzdE1vZGVTdXBwb3J0ZWRCeU1vZGVsLFxufSBmcm9tICcuLi8uLi91dGlscy9mYXN0TW9kZS5qcydcbmltcG9ydCB7IGlzRnVsbHNjcmVlbkVudkVuYWJsZWQgfSBmcm9tICcuLi8uLi91dGlscy9mdWxsc2NyZWVuLmpzJ1xuaW1wb3J0IHR5cGUgeyBQcm9tcHRJbnB1dEhlbHBlcnMgfSBmcm9tICcuLi8uLi91dGlscy9oYW5kbGVQcm9tcHRTdWJtaXQuanMnXG5pbXBvcnQge1xuICBnZXRJbWFnZUZyb21DbGlwYm9hcmQsXG4gIFBBU1RFX1RIUkVTSE9MRCxcbn0gZnJvbSAnLi4vLi4vdXRpbHMvaW1hZ2VQYXN0ZS5qcydcbmltcG9ydCB0eXBlIHsgSW1hZ2VEaW1lbnNpb25zIH0gZnJvbSAnLi4vLi4vdXRpbHMvaW1hZ2VSZXNpemVyLmpzJ1xuaW1wb3J0IHsgY2FjaGVJbWFnZVBhdGgsIHN0b3JlSW1hZ2UgfSBmcm9tICcuLi8uLi91dGlscy9pbWFnZVN0b3JlLmpzJ1xuaW1wb3J0IHtcbiAgaXNNYWNvc09wdGlvbkNoYXIsXG4gIE1BQ09TX09QVElPTl9TUEVDSUFMX0NIQVJTLFxufSBmcm9tICcuLi8uLi91dGlscy9rZXlib2FyZFNob3J0Y3V0cy5qcydcbmltcG9ydCB7IGxvZ0Vycm9yIH0gZnJvbSAnLi4vLi4vdXRpbHMvbG9nLmpzJ1xuaW1wb3J0IHtcbiAgaXNPcHVzMW1NZXJnZUVuYWJsZWQsXG4gIG1vZGVsRGlzcGxheVN0cmluZyxcbn0gZnJvbSAnLi4vLi4vdXRpbHMvbW9kZWwvbW9kZWwuanMnXG5pbXBvcnQgeyBzZXRBdXRvTW9kZUFjdGl2ZSB9IGZyb20gJy4uLy4uL3V0aWxzL3Blcm1pc3Npb25zL2F1dG9Nb2RlU3RhdGUuanMnXG5pbXBvcnQge1xuICBjeWNsZVBlcm1pc3Npb25Nb2RlLFxuICBnZXROZXh0UGVybWlzc2lvbk1vZGUsXG59IGZyb20gJy4uLy4uL3V0aWxzL3Blcm1pc3Npb25zL2dldE5leHRQZXJtaXNzaW9uTW9kZS5qcydcbmltcG9ydCB7IHRyYW5zaXRpb25QZXJtaXNzaW9uTW9kZSB9IGZyb20gJy4uLy4uL3V0aWxzL3Blcm1pc3Npb25zL3Blcm1pc3Npb25TZXR1cC5qcydcbmltcG9ydCB7IGdldFBsYXRmb3JtIH0gZnJvbSAnLi4vLi4vdXRpbHMvcGxhdGZvcm0uanMnXG5pbXBvcnQgdHlwZSB7IFByb2Nlc3NVc2VySW5wdXRDb250ZXh0IH0gZnJvbSAnLi4vLi4vdXRpbHMvcHJvY2Vzc1VzZXJJbnB1dC9wcm9jZXNzVXNlcklucHV0LmpzJ1xuaW1wb3J0IHsgZWRpdFByb21wdEluRWRpdG9yIH0gZnJvbSAnLi4vLi4vdXRpbHMvcHJvbXB0RWRpdG9yLmpzJ1xuaW1wb3J0IHsgaGFzQXV0b01vZGVPcHRJbiB9IGZyb20gJy4uLy4uL3V0aWxzL3NldHRpbmdzL3NldHRpbmdzLmpzJ1xuaW1wb3J0IHsgZmluZEJ0d1RyaWdnZXJQb3NpdGlvbnMgfSBmcm9tICcuLi8uLi91dGlscy9zaWRlUXVlc3Rpb24uanMnXG5pbXBvcnQgeyBmaW5kU2xhc2hDb21tYW5kUG9zaXRpb25zIH0gZnJvbSAnLi4vLi4vdXRpbHMvc3VnZ2VzdGlvbnMvY29tbWFuZFN1Z2dlc3Rpb25zLmpzJ1xuaW1wb3J0IHtcbiAgZmluZFNsYWNrQ2hhbm5lbFBvc2l0aW9ucyxcbiAgZ2V0S25vd25DaGFubmVsc1ZlcnNpb24sXG4gIGhhc1NsYWNrTWNwU2VydmVyLFxuICBzdWJzY3JpYmVLbm93bkNoYW5uZWxzLFxufSBmcm9tICcuLi8uLi91dGlscy9zdWdnZXN0aW9ucy9zbGFja0NoYW5uZWxTdWdnZXN0aW9ucy5qcydcbmltcG9ydCB7IGlzSW5Qcm9jZXNzRW5hYmxlZCB9IGZyb20gJy4uLy4uL3V0aWxzL3N3YXJtL2JhY2tlbmRzL3JlZ2lzdHJ5LmpzJ1xuaW1wb3J0IHsgc3luY1RlYW1tYXRlTW9kZSB9IGZyb20gJy4uLy4uL3V0aWxzL3N3YXJtL3RlYW1IZWxwZXJzLmpzJ1xuaW1wb3J0IHR5cGUgeyBUZWFtU3VtbWFyeSB9IGZyb20gJy4uLy4uL3V0aWxzL3RlYW1EaXNjb3ZlcnkuanMnXG5pbXBvcnQgeyBnZXRUZWFtbWF0ZUNvbG9yIH0gZnJvbSAnLi4vLi4vdXRpbHMvdGVhbW1hdGUuanMnXG5pbXBvcnQgeyBpc0luUHJvY2Vzc1RlYW1tYXRlIH0gZnJvbSAnLi4vLi4vdXRpbHMvdGVhbW1hdGVDb250ZXh0LmpzJ1xuaW1wb3J0IHsgd3JpdGVUb01haWxib3ggfSBmcm9tICcuLi8uLi91dGlscy90ZWFtbWF0ZU1haWxib3guanMnXG5pbXBvcnQgdHlwZSB7IFRleHRIaWdobGlnaHQgfSBmcm9tICcuLi8uLi91dGlscy90ZXh0SGlnaGxpZ2h0aW5nLmpzJ1xuaW1wb3J0IHR5cGUgeyBUaGVtZSB9IGZyb20gJy4uLy4uL3V0aWxzL3RoZW1lLmpzJ1xuaW1wb3J0IHtcbiAgZmluZFRoaW5raW5nVHJpZ2dlclBvc2l0aW9ucyxcbiAgZ2V0UmFpbmJvd0NvbG9yLFxuICBpc1VsdHJhdGhpbmtFbmFibGVkLFxufSBmcm9tICcuLi8uLi91dGlscy90aGlua2luZy5qcydcbmltcG9ydCB7IGZpbmRUb2tlbkJ1ZGdldFBvc2l0aW9ucyB9IGZyb20gJy4uLy4uL3V0aWxzL3Rva2VuQnVkZ2V0LmpzJ1xuaW1wb3J0IHtcbiAgZmluZFVsdHJhcGxhblRyaWdnZXJQb3NpdGlvbnMsXG4gIGZpbmRVbHRyYXJldmlld1RyaWdnZXJQb3NpdGlvbnMsXG59IGZyb20gJy4uLy4uL3V0aWxzL3VsdHJhcGxhbi9rZXl3b3JkLmpzJ1xuaW1wb3J0IHsgQXV0b01vZGVPcHRJbkRpYWxvZyB9IGZyb20gJy4uL0F1dG9Nb2RlT3B0SW5EaWFsb2cuanMnXG5pbXBvcnQgeyBCcmlkZ2VEaWFsb2cgfSBmcm9tICcuLi9CcmlkZ2VEaWFsb2cuanMnXG5pbXBvcnQgeyBDb25maWd1cmFibGVTaG9ydGN1dEhpbnQgfSBmcm9tICcuLi9Db25maWd1cmFibGVTaG9ydGN1dEhpbnQuanMnXG5pbXBvcnQge1xuICBnZXRWaXNpYmxlQWdlbnRUYXNrcyxcbiAgdXNlQ29vcmRpbmF0b3JUYXNrQ291bnQsXG59IGZyb20gJy4uL0Nvb3JkaW5hdG9yQWdlbnRTdGF0dXMuanMnXG5pbXBvcnQgeyBnZXRFZmZvcnROb3RpZmljYXRpb25UZXh0IH0gZnJvbSAnLi4vRWZmb3J0SW5kaWNhdG9yLmpzJ1xuaW1wb3J0IHsgZ2V0RmFzdEljb25TdHJpbmcgfSBmcm9tICcuLi9GYXN0SWNvbi5qcydcbmltcG9ydCB7IEdsb2JhbFNlYXJjaERpYWxvZyB9IGZyb20gJy4uL0dsb2JhbFNlYXJjaERpYWxvZy5qcydcbmltcG9ydCB7IEhpc3RvcnlTZWFyY2hEaWFsb2cgfSBmcm9tICcuLi9IaXN0b3J5U2VhcmNoRGlhbG9nLmpzJ1xuaW1wb3J0IHsgTW9kZWxQaWNrZXIgfSBmcm9tICcuLi9Nb2RlbFBpY2tlci5qcydcbmltcG9ydCB7IFF1aWNrT3BlbkRpYWxvZyB9IGZyb20gJy4uL1F1aWNrT3BlbkRpYWxvZy5qcydcbmltcG9ydCBUZXh0SW5wdXQgZnJvbSAnLi4vVGV4dElucHV0LmpzJ1xuaW1wb3J0IHsgVGhpbmtpbmdUb2dnbGUgfSBmcm9tICcuLi9UaGlua2luZ1RvZ2dsZS5qcydcbmltcG9ydCB7IEJhY2tncm91bmRUYXNrc0RpYWxvZyB9IGZyb20gJy4uL3Rhc2tzL0JhY2tncm91bmRUYXNrc0RpYWxvZy5qcydcbmltcG9ydCB7IHNob3VsZEhpZGVUYXNrc0Zvb3RlciB9IGZyb20gJy4uL3Rhc2tzL3Rhc2tTdGF0dXNVdGlscy5qcydcbmltcG9ydCB7IFRlYW1zRGlhbG9nIH0gZnJvbSAnLi4vdGVhbXMvVGVhbXNEaWFsb2cuanMnXG5pbXBvcnQgVmltVGV4dElucHV0IGZyb20gJy4uL1ZpbVRleHRJbnB1dC5qcydcbmltcG9ydCB7IGdldE1vZGVGcm9tSW5wdXQsIGdldFZhbHVlRnJvbUlucHV0IH0gZnJvbSAnLi9pbnB1dE1vZGVzLmpzJ1xuaW1wb3J0IHtcbiAgRk9PVEVSX1RFTVBPUkFSWV9TVEFUVVNfVElNRU9VVCxcbiAgTm90aWZpY2F0aW9ucyxcbn0gZnJvbSAnLi9Ob3RpZmljYXRpb25zLmpzJ1xuaW1wb3J0IFByb21wdElucHV0Rm9vdGVyIGZyb20gJy4vUHJvbXB0SW5wdXRGb290ZXIuanMnXG5pbXBvcnQgdHlwZSB7IFN1Z2dlc3Rpb25JdGVtIH0gZnJvbSAnLi9Qcm9tcHRJbnB1dEZvb3RlclN1Z2dlc3Rpb25zLmpzJ1xuaW1wb3J0IHsgUHJvbXB0SW5wdXRNb2RlSW5kaWNhdG9yIH0gZnJvbSAnLi9Qcm9tcHRJbnB1dE1vZGVJbmRpY2F0b3IuanMnXG5pbXBvcnQgeyBQcm9tcHRJbnB1dFF1ZXVlZENvbW1hbmRzIH0gZnJvbSAnLi9Qcm9tcHRJbnB1dFF1ZXVlZENvbW1hbmRzLmpzJ1xuaW1wb3J0IHsgUHJvbXB0SW5wdXRTdGFzaE5vdGljZSB9IGZyb20gJy4vUHJvbXB0SW5wdXRTdGFzaE5vdGljZS5qcydcbmltcG9ydCB7IHVzZU1heWJlVHJ1bmNhdGVJbnB1dCB9IGZyb20gJy4vdXNlTWF5YmVUcnVuY2F0ZUlucHV0LmpzJ1xuaW1wb3J0IHsgdXNlUHJvbXB0SW5wdXRQbGFjZWhvbGRlciB9IGZyb20gJy4vdXNlUHJvbXB0SW5wdXRQbGFjZWhvbGRlci5qcydcbmltcG9ydCB7IHVzZVNob3dGYXN0SWNvbkhpbnQgfSBmcm9tICcuL3VzZVNob3dGYXN0SWNvbkhpbnQuanMnXG5pbXBvcnQgeyB1c2VTd2FybUJhbm5lciB9IGZyb20gJy4vdXNlU3dhcm1CYW5uZXIuanMnXG5pbXBvcnQgeyBpc05vblNwYWNlUHJpbnRhYmxlLCBpc1ZpbU1vZGVFbmFibGVkIH0gZnJvbSAnLi91dGlscy5qcydcblxudHlwZSBQcm9wcyA9IHtcbiAgZGVidWc6IGJvb2xlYW5cbiAgaWRlU2VsZWN0aW9uOiBJREVTZWxlY3Rpb24gfCB1bmRlZmluZWRcbiAgdG9vbFBlcm1pc3Npb25Db250ZXh0OiBUb29sUGVybWlzc2lvbkNvbnRleHRcbiAgc2V0VG9vbFBlcm1pc3Npb25Db250ZXh0OiAoY3R4OiBUb29sUGVybWlzc2lvbkNvbnRleHQpID0+IHZvaWRcbiAgYXBpS2V5U3RhdHVzOiBWZXJpZmljYXRpb25TdGF0dXNcbiAgY29tbWFuZHM6IENvbW1hbmRbXVxuICBhZ2VudHM6IEFnZW50RGVmaW5pdGlvbltdXG4gIGlzTG9hZGluZzogYm9vbGVhblxuICB2ZXJib3NlOiBib29sZWFuXG4gIG1lc3NhZ2VzOiBNZXNzYWdlW11cbiAgb25BdXRvVXBkYXRlclJlc3VsdDogKHJlc3VsdDogQXV0b1VwZGF0ZXJSZXN1bHQpID0+IHZvaWRcbiAgYXV0b1VwZGF0ZXJSZXN1bHQ6IEF1dG9VcGRhdGVyUmVzdWx0IHwgbnVsbFxuICBpbnB1dDogc3RyaW5nXG4gIG9uSW5wdXRDaGFuZ2U6ICh2YWx1ZTogc3RyaW5nKSA9PiB2b2lkXG4gIG1vZGU6IFByb21wdElucHV0TW9kZVxuICBvbk1vZGVDaGFuZ2U6IChtb2RlOiBQcm9tcHRJbnB1dE1vZGUpID0+IHZvaWRcbiAgc3Rhc2hlZFByb21wdDpcbiAgICB8IHtcbiAgICAgICAgdGV4dDogc3RyaW5nXG4gICAgICAgIGN1cnNvck9mZnNldDogbnVtYmVyXG4gICAgICAgIHBhc3RlZENvbnRlbnRzOiBSZWNvcmQ8bnVtYmVyLCBQYXN0ZWRDb250ZW50PlxuICAgICAgfVxuICAgIHwgdW5kZWZpbmVkXG4gIHNldFN0YXNoZWRQcm9tcHQ6IChcbiAgICB2YWx1ZTpcbiAgICAgIHwge1xuICAgICAgICAgIHRleHQ6IHN0cmluZ1xuICAgICAgICAgIGN1cnNvck9mZnNldDogbnVtYmVyXG4gICAgICAgICAgcGFzdGVkQ29udGVudHM6IFJlY29yZDxudW1iZXIsIFBhc3RlZENvbnRlbnQ+XG4gICAgICAgIH1cbiAgICAgIHwgdW5kZWZpbmVkLFxuICApID0+IHZvaWRcbiAgc3VibWl0Q291bnQ6IG51bWJlclxuICBvblNob3dNZXNzYWdlU2VsZWN0b3I6ICgpID0+IHZvaWRcbiAgLyoqIEZ1bGxzY3JlZW4gbWVzc2FnZSBhY3Rpb25zOiBzaGlmdCvihpEgZW50ZXJzIGN1cnNvci4gKi9cbiAgb25NZXNzYWdlQWN0aW9uc0VudGVyPzogKCkgPT4gdm9pZFxuICBtY3BDbGllbnRzOiBNQ1BTZXJ2ZXJDb25uZWN0aW9uW11cbiAgcGFzdGVkQ29udGVudHM6IFJlY29yZDxudW1iZXIsIFBhc3RlZENvbnRlbnQ+XG4gIHNldFBhc3RlZENvbnRlbnRzOiBSZWFjdC5EaXNwYXRjaDxcbiAgICBSZWFjdC5TZXRTdGF0ZUFjdGlvbjxSZWNvcmQ8bnVtYmVyLCBQYXN0ZWRDb250ZW50Pj5cbiAgPlxuICB2aW1Nb2RlOiBWaW1Nb2RlXG4gIHNldFZpbU1vZGU6IChtb2RlOiBWaW1Nb2RlKSA9PiB2b2lkXG4gIHNob3dCYXNoZXNEaWFsb2c6IHN0cmluZyB8IGJvb2xlYW5cbiAgc2V0U2hvd0Jhc2hlc0RpYWxvZzogKHNob3c6IHN0cmluZyB8IGJvb2xlYW4pID0+IHZvaWRcbiAgb25FeGl0OiAoKSA9PiB2b2lkXG4gIGdldFRvb2xVc2VDb250ZXh0OiAoXG4gICAgbWVzc2FnZXM6IE1lc3NhZ2VbXSxcbiAgICBuZXdNZXNzYWdlczogTWVzc2FnZVtdLFxuICAgIGFib3J0Q29udHJvbGxlcjogQWJvcnRDb250cm9sbGVyLFxuICAgIG1haW5Mb29wTW9kZWw6IHN0cmluZyxcbiAgKSA9PiBQcm9jZXNzVXNlcklucHV0Q29udGV4dFxuICBvblN1Ym1pdDogKFxuICAgIGlucHV0OiBzdHJpbmcsXG4gICAgaGVscGVyczogUHJvbXB0SW5wdXRIZWxwZXJzLFxuICAgIHNwZWN1bGF0aW9uQWNjZXB0Pzoge1xuICAgICAgc3RhdGU6IEFjdGl2ZVNwZWN1bGF0aW9uU3RhdGVcbiAgICAgIHNwZWN1bGF0aW9uU2Vzc2lvblRpbWVTYXZlZE1zOiBudW1iZXJcbiAgICAgIHNldEFwcFN0YXRlOiAoZjogKHByZXY6IEFwcFN0YXRlKSA9PiBBcHBTdGF0ZSkgPT4gdm9pZFxuICAgIH0sXG4gICAgb3B0aW9ucz86IHsgZnJvbUtleWJpbmRpbmc/OiBib29sZWFuIH0sXG4gICkgPT4gUHJvbWlzZTx2b2lkPlxuICBvbkFnZW50U3VibWl0PzogKFxuICAgIGlucHV0OiBzdHJpbmcsXG4gICAgdGFzazogSW5Qcm9jZXNzVGVhbW1hdGVUYXNrU3RhdGUgfCBMb2NhbEFnZW50VGFza1N0YXRlLFxuICAgIGhlbHBlcnM6IFByb21wdElucHV0SGVscGVycyxcbiAgKSA9PiBQcm9taXNlPHZvaWQ+XG4gIGlzU2VhcmNoaW5nSGlzdG9yeTogYm9vbGVhblxuICBzZXRJc1NlYXJjaGluZ0hpc3Rvcnk6IChpc1NlYXJjaGluZzogYm9vbGVhbikgPT4gdm9pZFxuICBvbkRpc21pc3NTaWRlUXVlc3Rpb24/OiAoKSA9PiB2b2lkXG4gIGlzU2lkZVF1ZXN0aW9uVmlzaWJsZT86IGJvb2xlYW5cbiAgaGVscE9wZW46IGJvb2xlYW5cbiAgc2V0SGVscE9wZW46IFJlYWN0LkRpc3BhdGNoPFJlYWN0LlNldFN0YXRlQWN0aW9uPGJvb2xlYW4+PlxuICBoYXNTdXBwcmVzc2VkRGlhbG9ncz86IGJvb2xlYW5cbiAgaXNMb2NhbEpTWENvbW1hbmRBY3RpdmU/OiBib29sZWFuXG4gIGluc2VydFRleHRSZWY/OiBSZWFjdC5NdXRhYmxlUmVmT2JqZWN0PHtcbiAgICBpbnNlcnQ6ICh0ZXh0OiBzdHJpbmcpID0+IHZvaWRcbiAgICBzZXRJbnB1dFdpdGhDdXJzb3I6ICh2YWx1ZTogc3RyaW5nLCBjdXJzb3I6IG51bWJlcikgPT4gdm9pZFxuICAgIGN1cnNvck9mZnNldDogbnVtYmVyXG4gIH0gfCBudWxsPlxuICB2b2ljZUludGVyaW1SYW5nZT86IHsgc3RhcnQ6IG51bWJlcjsgZW5kOiBudW1iZXIgfSB8IG51bGxcbn1cblxuLy8gQm90dG9tIHNsb3QgaGFzIG1heEhlaWdodD1cIjUwJVwiOyByZXNlcnZlIGxpbmVzIGZvciBmb290ZXIsIGJvcmRlciwgc3RhdHVzLlxuY29uc3QgUFJPTVBUX0ZPT1RFUl9MSU5FUyA9IDVcbmNvbnN0IE1JTl9JTlBVVF9WSUVXUE9SVF9MSU5FUyA9IDNcblxuZnVuY3Rpb24gUHJvbXB0SW5wdXQoe1xuICBkZWJ1ZyxcbiAgaWRlU2VsZWN0aW9uLFxuICB0b29sUGVybWlzc2lvbkNvbnRleHQsXG4gIHNldFRvb2xQZXJtaXNzaW9uQ29udGV4dCxcbiAgYXBpS2V5U3RhdHVzLFxuICBjb21tYW5kcyxcbiAgYWdlbnRzLFxuICBpc0xvYWRpbmcsXG4gIHZlcmJvc2UsXG4gIG1lc3NhZ2VzLFxuICBvbkF1dG9VcGRhdGVyUmVzdWx0LFxuICBhdXRvVXBkYXRlclJlc3VsdCxcbiAgaW5wdXQsXG4gIG9uSW5wdXRDaGFuZ2UsXG4gIG1vZGUsXG4gIG9uTW9kZUNoYW5nZSxcbiAgc3Rhc2hlZFByb21wdCxcbiAgc2V0U3Rhc2hlZFByb21wdCxcbiAgc3VibWl0Q291bnQsXG4gIG9uU2hvd01lc3NhZ2VTZWxlY3RvcixcbiAgb25NZXNzYWdlQWN0aW9uc0VudGVyLFxuICBtY3BDbGllbnRzLFxuICBwYXN0ZWRDb250ZW50cyxcbiAgc2V0UGFzdGVkQ29udGVudHMsXG4gIHZpbU1vZGUsXG4gIHNldFZpbU1vZGUsXG4gIHNob3dCYXNoZXNEaWFsb2csXG4gIHNldFNob3dCYXNoZXNEaWFsb2csXG4gIG9uRXhpdCxcbiAgZ2V0VG9vbFVzZUNvbnRleHQsXG4gIG9uU3VibWl0OiBvblN1Ym1pdFByb3AsXG4gIG9uQWdlbnRTdWJtaXQsXG4gIGlzU2VhcmNoaW5nSGlzdG9yeSxcbiAgc2V0SXNTZWFyY2hpbmdIaXN0b3J5LFxuICBvbkRpc21pc3NTaWRlUXVlc3Rpb24sXG4gIGlzU2lkZVF1ZXN0aW9uVmlzaWJsZSxcbiAgaGVscE9wZW4sXG4gIHNldEhlbHBPcGVuLFxuICBoYXNTdXBwcmVzc2VkRGlhbG9ncyxcbiAgaXNMb2NhbEpTWENvbW1hbmRBY3RpdmUgPSBmYWxzZSxcbiAgaW5zZXJ0VGV4dFJlZixcbiAgdm9pY2VJbnRlcmltUmFuZ2UsXG59OiBQcm9wcyk6IFJlYWN0LlJlYWN0Tm9kZSB7XG4gIGNvbnN0IG1haW5Mb29wTW9kZWwgPSB1c2VNYWluTG9vcE1vZGVsKClcbiAgLy8gQSBsb2NhbC1qc3ggY29tbWFuZCAoZS5nLiwgL21jcCB3aGlsZSBhZ2VudCBpcyBydW5uaW5nKSByZW5kZXJzIGEgZnVsbC1cbiAgLy8gc2NyZWVuIGRpYWxvZyBvbiB0b3Agb2YgUHJvbXB0SW5wdXQgdmlhIHRoZSBpbW1lZGlhdGUtY29tbWFuZCBwYXRoIHdpdGhcbiAgLy8gc2hvdWxkSGlkZVByb21wdElucHV0OiBmYWxzZS4gVGhvc2UgZGlhbG9ncyBkb24ndCByZWdpc3RlciBpbiB0aGUgb3ZlcmxheVxuICAvLyBzeXN0ZW0sIHNvIHRyZWF0IHRoZW0gYXMgYSBtb2RhbCBvdmVybGF5IGhlcmUgdG8gc3RvcCBuYXZpZ2F0aW9uIGtleXMgZnJvbVxuICAvLyBsZWFraW5nIGludG8gVGV4dElucHV0L2Zvb3RlciBoYW5kbGVycyBhbmQgc3RhY2tpbmcgYSBzZWNvbmQgZGlhbG9nLlxuICBjb25zdCBpc01vZGFsT3ZlcmxheUFjdGl2ZSA9XG4gICAgdXNlSXNNb2RhbE92ZXJsYXlBY3RpdmUoKSB8fCBpc0xvY2FsSlNYQ29tbWFuZEFjdGl2ZVxuICBjb25zdCBbaXNBdXRvVXBkYXRpbmcsIHNldElzQXV0b1VwZGF0aW5nXSA9IHVzZVN0YXRlKGZhbHNlKVxuICBjb25zdCBbZXhpdE1lc3NhZ2UsIHNldEV4aXRNZXNzYWdlXSA9IHVzZVN0YXRlPHtcbiAgICBzaG93OiBib29sZWFuXG4gICAga2V5Pzogc3RyaW5nXG4gIH0+KHsgc2hvdzogZmFsc2UgfSlcbiAgY29uc3QgW2N1cnNvck9mZnNldCwgc2V0Q3Vyc29yT2Zmc2V0XSA9IHVzZVN0YXRlPG51bWJlcj4oaW5wdXQubGVuZ3RoKVxuICAvLyBUcmFjayB0aGUgbGFzdCBpbnB1dCB2YWx1ZSBzZXQgdmlhIGludGVybmFsIGhhbmRsZXJzIHNvIHdlIGNhbiBkZXRlY3RcbiAgLy8gZXh0ZXJuYWwgaW5wdXQgY2hhbmdlcyAoZS5nLiBzcGVlY2gtdG8tdGV4dCBpbmplY3Rpb24pIGFuZCBtb3ZlIGN1cnNvciB0byBlbmQuXG4gIGNvbnN0IGxhc3RJbnRlcm5hbElucHV0UmVmID0gUmVhY3QudXNlUmVmKGlucHV0KVxuICBpZiAoaW5wdXQgIT09IGxhc3RJbnRlcm5hbElucHV0UmVmLmN1cnJlbnQpIHtcbiAgICAvLyBJbnB1dCBjaGFuZ2VkIGV4dGVybmFsbHkgKG5vdCB0aHJvdWdoIGFueSBpbnRlcm5hbCBoYW5kbGVyKSDigJQgbW92ZSBjdXJzb3IgdG8gZW5kXG4gICAgc2V0Q3Vyc29yT2Zmc2V0KGlucHV0Lmxlbmd0aClcbiAgICBsYXN0SW50ZXJuYWxJbnB1dFJlZi5jdXJyZW50ID0gaW5wdXRcbiAgfVxuICAvLyBXcmFwIG9uSW5wdXRDaGFuZ2UgdG8gdHJhY2sgaW50ZXJuYWwgY2hhbmdlcyBiZWZvcmUgdGhleSB0cmlnZ2VyIHJlLXJlbmRlclxuICBjb25zdCB0cmFja0FuZFNldElucHV0ID0gUmVhY3QudXNlQ2FsbGJhY2soXG4gICAgKHZhbHVlOiBzdHJpbmcpID0+IHtcbiAgICAgIGxhc3RJbnRlcm5hbElucHV0UmVmLmN1cnJlbnQgPSB2YWx1ZVxuICAgICAgb25JbnB1dENoYW5nZSh2YWx1ZSlcbiAgICB9LFxuICAgIFtvbklucHV0Q2hhbmdlXSxcbiAgKVxuICAvLyBFeHBvc2UgYW4gaW5zZXJ0VGV4dCBmdW5jdGlvbiBzbyBjYWxsZXJzIChlLmcuIFNUVCkgY2FuIHNwbGljZSB0ZXh0IGF0IHRoZVxuICAvLyBjdXJyZW50IGN1cnNvciBwb3NpdGlvbiBpbnN0ZWFkIG9mIHJlcGxhY2luZyB0aGUgZW50aXJlIGlucHV0LlxuICBpZiAoaW5zZXJ0VGV4dFJlZikge1xuICAgIGluc2VydFRleHRSZWYuY3VycmVudCA9IHtcbiAgICAgIGN1cnNvck9mZnNldCxcbiAgICAgIGluc2VydDogKHRleHQ6IHN0cmluZykgPT4ge1xuICAgICAgICBjb25zdCBuZWVkc1NwYWNlID1cbiAgICAgICAgICBjdXJzb3JPZmZzZXQgPT09IGlucHV0Lmxlbmd0aCAmJlxuICAgICAgICAgIGlucHV0Lmxlbmd0aCA+IDAgJiZcbiAgICAgICAgICAhL1xccyQvLnRlc3QoaW5wdXQpXG4gICAgICAgIGNvbnN0IGluc2VydFRleHQgPSBuZWVkc1NwYWNlID8gJyAnICsgdGV4dCA6IHRleHRcbiAgICAgICAgY29uc3QgbmV3VmFsdWUgPVxuICAgICAgICAgIGlucHV0LnNsaWNlKDAsIGN1cnNvck9mZnNldCkgKyBpbnNlcnRUZXh0ICsgaW5wdXQuc2xpY2UoY3Vyc29yT2Zmc2V0KVxuICAgICAgICBsYXN0SW50ZXJuYWxJbnB1dFJlZi5jdXJyZW50ID0gbmV3VmFsdWVcbiAgICAgICAgb25JbnB1dENoYW5nZShuZXdWYWx1ZSlcbiAgICAgICAgc2V0Q3Vyc29yT2Zmc2V0KGN1cnNvck9mZnNldCArIGluc2VydFRleHQubGVuZ3RoKVxuICAgICAgfSxcbiAgICAgIHNldElucHV0V2l0aEN1cnNvcjogKHZhbHVlOiBzdHJpbmcsIGN1cnNvcjogbnVtYmVyKSA9PiB7XG4gICAgICAgIGxhc3RJbnRlcm5hbElucHV0UmVmLmN1cnJlbnQgPSB2YWx1ZVxuICAgICAgICBvbklucHV0Q2hhbmdlKHZhbHVlKVxuICAgICAgICBzZXRDdXJzb3JPZmZzZXQoY3Vyc29yKVxuICAgICAgfSxcbiAgICB9XG4gIH1cbiAgY29uc3Qgc3RvcmUgPSB1c2VBcHBTdGF0ZVN0b3JlKClcbiAgY29uc3Qgc2V0QXBwU3RhdGUgPSB1c2VTZXRBcHBTdGF0ZSgpXG4gIGNvbnN0IHRhc2tzID0gdXNlQXBwU3RhdGUocyA9PiBzLnRhc2tzKVxuICBjb25zdCByZXBsQnJpZGdlQ29ubmVjdGVkID0gdXNlQXBwU3RhdGUocyA9PiBzLnJlcGxCcmlkZ2VDb25uZWN0ZWQpXG4gIGNvbnN0IHJlcGxCcmlkZ2VFeHBsaWNpdCA9IHVzZUFwcFN0YXRlKHMgPT4gcy5yZXBsQnJpZGdlRXhwbGljaXQpXG4gIGNvbnN0IHJlcGxCcmlkZ2VSZWNvbm5lY3RpbmcgPSB1c2VBcHBTdGF0ZShzID0+IHMucmVwbEJyaWRnZVJlY29ubmVjdGluZylcbiAgLy8gTXVzdCBtYXRjaCBCcmlkZ2VTdGF0dXNJbmRpY2F0b3IncyByZW5kZXIgY29uZGl0aW9uIChQcm9tcHRJbnB1dEZvb3Rlci50c3gpIOKAlFxuICAvLyB0aGUgcGlsbCByZXR1cm5zIG51bGwgZm9yIGltcGxpY2l0LWFuZC1ub3QtcmVjb25uZWN0aW5nLCBzbyBuYXYgbXVzdCB0b28sXG4gIC8vIG90aGVyd2lzZSBicmlkZ2UgYmVjb21lcyBhbiBpbnZpc2libGUgc2VsZWN0aW9uIHN0b3AuXG4gIGNvbnN0IGJyaWRnZUZvb3RlclZpc2libGUgPVxuICAgIHJlcGxCcmlkZ2VDb25uZWN0ZWQgJiYgKHJlcGxCcmlkZ2VFeHBsaWNpdCB8fCByZXBsQnJpZGdlUmVjb25uZWN0aW5nKVxuICAvLyBUbXV4IHBpbGwgKGFudC1vbmx5KSDigJQgdmlzaWJsZSB3aGVuIHRoZXJlJ3MgYW4gYWN0aXZlIHR1bmdzdGVuIHNlc3Npb25cbiAgY29uc3QgaGFzVHVuZ3N0ZW5TZXNzaW9uID0gdXNlQXBwU3RhdGUoXG4gICAgcyA9PlxuICAgICAgXCJleHRlcm5hbFwiID09PSAnYW50JyAmJiBzLnR1bmdzdGVuQWN0aXZlU2Vzc2lvbiAhPT0gdW5kZWZpbmVkLFxuICApXG4gIGNvbnN0IHRtdXhGb290ZXJWaXNpYmxlID1cbiAgICBcImV4dGVybmFsXCIgPT09ICdhbnQnICYmIGhhc1R1bmdzdGVuU2Vzc2lvblxuICAvLyBXZWJCcm93c2VyIHBpbGwg4oCUIHZpc2libGUgd2hlbiBhIGJyb3dzZXIgaXMgb3BlblxuICBjb25zdCBiYWdlbEZvb3RlclZpc2libGUgPSB1c2VBcHBTdGF0ZShzID0+XG4gICAgICAgIGZhbHNlLFxuICApXG4gIGNvbnN0IHRlYW1Db250ZXh0ID0gdXNlQXBwU3RhdGUocyA9PiBzLnRlYW1Db250ZXh0KVxuICBjb25zdCBxdWV1ZWRDb21tYW5kcyA9IHVzZUNvbW1hbmRRdWV1ZSgpXG4gIGNvbnN0IHByb21wdFN1Z2dlc3Rpb25TdGF0ZSA9IHVzZUFwcFN0YXRlKHMgPT4gcy5wcm9tcHRTdWdnZXN0aW9uKVxuICBjb25zdCBzcGVjdWxhdGlvbiA9IHVzZUFwcFN0YXRlKHMgPT4gcy5zcGVjdWxhdGlvbilcbiAgY29uc3Qgc3BlY3VsYXRpb25TZXNzaW9uVGltZVNhdmVkTXMgPSB1c2VBcHBTdGF0ZShcbiAgICBzID0+IHMuc3BlY3VsYXRpb25TZXNzaW9uVGltZVNhdmVkTXMsXG4gIClcbiAgY29uc3Qgdmlld2luZ0FnZW50VGFza0lkID0gdXNlQXBwU3RhdGUocyA9PiBzLnZpZXdpbmdBZ2VudFRhc2tJZClcbiAgY29uc3Qgdmlld1NlbGVjdGlvbk1vZGUgPSB1c2VBcHBTdGF0ZShzID0+IHMudmlld1NlbGVjdGlvbk1vZGUpXG4gIGNvbnN0IHNob3dTcGlubmVyVHJlZSA9IHVzZUFwcFN0YXRlKHMgPT4gcy5leHBhbmRlZFZpZXcpID09PSAndGVhbW1hdGVzJ1xuICBjb25zdCB7IGNvbXBhbmlvbjogX2NvbXBhbmlvbiwgY29tcGFuaW9uTXV0ZWQgfSA9IGZlYXR1cmUoJ0JVRERZJylcbiAgICA/IGdldEdsb2JhbENvbmZpZygpXG4gICAgOiB7IGNvbXBhbmlvbjogdW5kZWZpbmVkLCBjb21wYW5pb25NdXRlZDogdW5kZWZpbmVkIH1cbiAgY29uc3QgY29tcGFuaW9uRm9vdGVyVmlzaWJsZSA9ICEhX2NvbXBhbmlvbiAmJiAhY29tcGFuaW9uTXV0ZWRcbiAgLy8gQnJpZWYgbW9kZTogQnJpZWZTcGlubmVyL0JyaWVmSWRsZVN0YXR1cyBvd24gdGhlIDItcm93IGZvb3RwcmludCBhYm92ZVxuICAvLyB0aGUgaW5wdXQuIERyb3BwaW5nIG1hcmdpblRvcCBoZXJlIGxldHMgdGhlIHNwaW5uZXIgc2l0IGZsdXNoIGFnYWluc3RcbiAgLy8gdGhlIGlucHV0IGJhci4gdmlld2luZ0FnZW50VGFza0lkIG1pcnJvcnMgdGhlIGdhdGUgb24gYm90aCAoU3Bpbm5lci50c3gsXG4gIC8vIFJFUEwudHN4KSDigJQgdGVhbW1hdGUgdmlldyBmYWxscyBiYWNrIHRvIFNwaW5uZXJXaXRoVmVyYklubmVyIHdoaWNoIGhhc1xuICAvLyBpdHMgb3duIG1hcmdpblRvcCwgc28gdGhlIGdhcCBzdGF5cyBldmVuIHdpdGhvdXQgb3Vycy5cbiAgY29uc3QgYnJpZWZPd25zR2FwID1cbiAgICBmZWF0dXJlKCdLQUlST1MnKSB8fCBmZWF0dXJlKCdLQUlST1NfQlJJRUYnKVxuICAgICAgPyAvLyBiaW9tZS1pZ25vcmUgbGludC9jb3JyZWN0bmVzcy91c2VIb29rQXRUb3BMZXZlbDogZmVhdHVyZSgpIGlzIGEgY29tcGlsZS10aW1lIGNvbnN0YW50XG4gICAgICAgIHVzZUFwcFN0YXRlKHMgPT4gcy5pc0JyaWVmT25seSkgJiYgIXZpZXdpbmdBZ2VudFRhc2tJZFxuICAgICAgOiBmYWxzZVxuICBjb25zdCBtYWluTG9vcE1vZGVsXyA9IHVzZUFwcFN0YXRlKHMgPT4gcy5tYWluTG9vcE1vZGVsKVxuICBjb25zdCBtYWluTG9vcE1vZGVsRm9yU2Vzc2lvbiA9IHVzZUFwcFN0YXRlKHMgPT4gcy5tYWluTG9vcE1vZGVsRm9yU2Vzc2lvbilcbiAgY29uc3QgdGhpbmtpbmdFbmFibGVkID0gdXNlQXBwU3RhdGUocyA9PiBzLnRoaW5raW5nRW5hYmxlZClcbiAgY29uc3QgaXNGYXN0TW9kZSA9IHVzZUFwcFN0YXRlKHMgPT5cbiAgICBpc0Zhc3RNb2RlRW5hYmxlZCgpID8gcy5mYXN0TW9kZSA6IGZhbHNlLFxuICApXG4gIGNvbnN0IGVmZm9ydFZhbHVlID0gdXNlQXBwU3RhdGUocyA9PiBzLmVmZm9ydFZhbHVlKVxuICBjb25zdCB2aWV3ZWRUZWFtbWF0ZSA9IGdldFZpZXdlZFRlYW1tYXRlVGFzayhzdG9yZS5nZXRTdGF0ZSgpKVxuICBjb25zdCB2aWV3aW5nQWdlbnROYW1lID0gdmlld2VkVGVhbW1hdGU/LmlkZW50aXR5LmFnZW50TmFtZVxuICAvLyBpZGVudGl0eS5jb2xvciBpcyB0eXBlZCBhcyBgc3RyaW5nIHwgdW5kZWZpbmVkYCAobm90IEFnZW50Q29sb3JOYW1lKSBiZWNhdXNlXG4gIC8vIHRlYW1tYXRlIGlkZW50aXR5IGNvbWVzIGZyb20gZmlsZS1iYXNlZCBjb25maWcuIFZhbGlkYXRlIGJlZm9yZSBjYXN0aW5nIHRvXG4gIC8vIGVuc3VyZSB3ZSBvbmx5IHVzZSB2YWxpZCBjb2xvciBuYW1lcyAoZmFsbHMgYmFjayB0byBjeWFuIGlmIGludmFsaWQpLlxuICBjb25zdCB2aWV3aW5nQWdlbnRDb2xvciA9XG4gICAgdmlld2VkVGVhbW1hdGU/LmlkZW50aXR5LmNvbG9yICYmXG4gICAgQUdFTlRfQ09MT1JTLmluY2x1ZGVzKHZpZXdlZFRlYW1tYXRlLmlkZW50aXR5LmNvbG9yIGFzIEFnZW50Q29sb3JOYW1lKVxuICAgICAgPyAodmlld2VkVGVhbW1hdGUuaWRlbnRpdHkuY29sb3IgYXMgQWdlbnRDb2xvck5hbWUpXG4gICAgICA6IHVuZGVmaW5lZFxuICAvLyBJbi1wcm9jZXNzIHRlYW1tYXRlcyBzb3J0ZWQgYWxwaGFiZXRpY2FsbHkgZm9yIGZvb3RlciB0ZWFtIHNlbGVjdG9yXG4gIGNvbnN0IGluUHJvY2Vzc1RlYW1tYXRlcyA9IHVzZU1lbW8oXG4gICAgKCkgPT4gZ2V0UnVubmluZ1RlYW1tYXRlc1NvcnRlZCh0YXNrcyksXG4gICAgW3Rhc2tzXSxcbiAgKVxuXG4gIC8vIFRlYW0gbW9kZTogYWxsIGJhY2tncm91bmQgdGFza3MgYXJlIGluLXByb2Nlc3MgdGVhbW1hdGVzXG4gIGNvbnN0IGlzVGVhbW1hdGVNb2RlID1cbiAgICBpblByb2Nlc3NUZWFtbWF0ZXMubGVuZ3RoID4gMCB8fCB2aWV3ZWRUZWFtbWF0ZSAhPT0gdW5kZWZpbmVkXG5cbiAgLy8gV2hlbiB2aWV3aW5nIGEgdGVhbW1hdGUsIHNob3cgdGhlaXIgcGVybWlzc2lvbiBtb2RlIGluIHRoZSBmb290ZXIgaW5zdGVhZCBvZiB0aGUgbGVhZGVyJ3NcbiAgY29uc3QgZWZmZWN0aXZlVG9vbFBlcm1pc3Npb25Db250ZXh0ID0gdXNlTWVtbygoKTogVG9vbFBlcm1pc3Npb25Db250ZXh0ID0+IHtcbiAgICBpZiAodmlld2VkVGVhbW1hdGUpIHtcbiAgICAgIHJldHVybiB7XG4gICAgICAgIC4uLnRvb2xQZXJtaXNzaW9uQ29udGV4dCxcbiAgICAgICAgbW9kZTogdmlld2VkVGVhbW1hdGUucGVybWlzc2lvbk1vZGUsXG4gICAgICB9XG4gICAgfVxuICAgIHJldHVybiB0b29sUGVybWlzc2lvbkNvbnRleHRcbiAgfSwgW3ZpZXdlZFRlYW1tYXRlLCB0b29sUGVybWlzc2lvbkNvbnRleHRdKVxuICBjb25zdCB7IGhpc3RvcnlRdWVyeSwgc2V0SGlzdG9yeVF1ZXJ5LCBoaXN0b3J5TWF0Y2gsIGhpc3RvcnlGYWlsZWRNYXRjaCB9ID1cbiAgICB1c2VIaXN0b3J5U2VhcmNoKFxuICAgICAgZW50cnkgPT4ge1xuICAgICAgICBzZXRQYXN0ZWRDb250ZW50cyhlbnRyeS5wYXN0ZWRDb250ZW50cylcbiAgICAgICAgdm9pZCBvblN1Ym1pdChlbnRyeS5kaXNwbGF5KVxuICAgICAgfSxcbiAgICAgIGlucHV0LFxuICAgICAgdHJhY2tBbmRTZXRJbnB1dCxcbiAgICAgIHNldEN1cnNvck9mZnNldCxcbiAgICAgIGN1cnNvck9mZnNldCxcbiAgICAgIG9uTW9kZUNoYW5nZSxcbiAgICAgIG1vZGUsXG4gICAgICBpc1NlYXJjaGluZ0hpc3RvcnksXG4gICAgICBzZXRJc1NlYXJjaGluZ0hpc3RvcnksXG4gICAgICBzZXRQYXN0ZWRDb250ZW50cyxcbiAgICAgIHBhc3RlZENvbnRlbnRzLFxuICAgIClcbiAgLy8gQ291bnRlciBmb3IgcGFzdGUgSURzIChzaGFyZWQgYmV0d2VlbiBpbWFnZXMgYW5kIHRleHQpLlxuICAvLyBDb21wdXRlIGluaXRpYWwgdmFsdWUgb25jZSBmcm9tIGV4aXN0aW5nIG1lc3NhZ2VzIChmb3IgLS1jb250aW51ZS8tLXJlc3VtZSkuXG4gIC8vIHVzZVJlZihmbigpKSBldmFsdWF0ZXMgZm4oKSBvbiBldmVyeSByZW5kZXIgYW5kIGRpc2NhcmRzIHRoZSByZXN1bHQgYWZ0ZXJcbiAgLy8gbW91bnQg4oCUIGdldEluaXRpYWxQYXN0ZUlkIHdhbGtzIGFsbCBtZXNzYWdlcyArIHJlZ2V4LXNjYW5zIHRleHQgYmxvY2tzLFxuICAvLyBzbyBndWFyZCB3aXRoIGEgbGF6eS1pbml0IHBhdHRlcm4gdG8gcnVuIGl0IGV4YWN0bHkgb25jZS5cbiAgY29uc3QgbmV4dFBhc3RlSWRSZWYgPSB1c2VSZWYoLTEpXG4gIGlmIChuZXh0UGFzdGVJZFJlZi5jdXJyZW50ID09PSAtMSkge1xuICAgIG5leHRQYXN0ZUlkUmVmLmN1cnJlbnQgPSBnZXRJbml0aWFsUGFzdGVJZChtZXNzYWdlcylcbiAgfVxuICAvLyBBcm1lZCBieSBvbkltYWdlUGFzdGU7IGlmIHRoZSB2ZXJ5IG5leHQga2V5c3Ryb2tlIGlzIGEgbm9uLXNwYWNlXG4gIC8vIHByaW50YWJsZSwgaW5wdXRGaWx0ZXIgcHJlcGVuZHMgYSBzcGFjZSBiZWZvcmUgaXQuIEFueSBvdGhlciBpbnB1dFxuICAvLyAoYXJyb3csIGVzY2FwZSwgYmFja3NwYWNlLCBwYXN0ZSwgc3BhY2UpIGRpc2FybXMgd2l0aG91dCBpbnNlcnRpbmcuXG4gIGNvbnN0IHBlbmRpbmdTcGFjZUFmdGVyUGlsbFJlZiA9IHVzZVJlZihmYWxzZSlcblxuICBjb25zdCBbc2hvd1RlYW1zRGlhbG9nLCBzZXRTaG93VGVhbXNEaWFsb2ddID0gdXNlU3RhdGUoZmFsc2UpXG4gIGNvbnN0IFtzaG93QnJpZGdlRGlhbG9nLCBzZXRTaG93QnJpZGdlRGlhbG9nXSA9IHVzZVN0YXRlKGZhbHNlKVxuICBjb25zdCBbdGVhbW1hdGVGb290ZXJJbmRleCwgc2V0VGVhbW1hdGVGb290ZXJJbmRleF0gPSB1c2VTdGF0ZSgwKVxuICAvLyAtMSBzZW50aW5lbDogdGFza3MgcGlsbCBpcyBzZWxlY3RlZCBidXQgbm8gc3BlY2lmaWMgYWdlbnQgcm93IGlzIHNlbGVjdGVkIHlldC5cbiAgLy8gRmlyc3Qg4oaTIHNlbGVjdHMgdGhlIHBpbGwsIHNlY29uZCDihpMgbW92ZXMgdG8gcm93IDAuIFByZXZlbnRzIGRvdWJsZS1zZWxlY3RcbiAgLy8gb2YgcGlsbCArIHJvdyB3aGVuIGJvdGggYmcgdGFza3MgKHBpbGwpIGFuZCBmb3JrZWQgYWdlbnRzIChyb3dzKSBhcmUgdmlzaWJsZS5cbiAgY29uc3QgY29vcmRpbmF0b3JUYXNrSW5kZXggPSB1c2VBcHBTdGF0ZShzID0+IHMuY29vcmRpbmF0b3JUYXNrSW5kZXgpXG4gIGNvbnN0IHNldENvb3JkaW5hdG9yVGFza0luZGV4ID0gdXNlQ2FsbGJhY2soXG4gICAgKHY6IG51bWJlciB8ICgocHJldjogbnVtYmVyKSA9PiBudW1iZXIpKSA9PlxuICAgICAgc2V0QXBwU3RhdGUocHJldiA9PiB7XG4gICAgICAgIGNvbnN0IG5leHQgPSB0eXBlb2YgdiA9PT0gJ2Z1bmN0aW9uJyA/IHYocHJldi5jb29yZGluYXRvclRhc2tJbmRleCkgOiB2XG4gICAgICAgIGlmIChuZXh0ID09PSBwcmV2LmNvb3JkaW5hdG9yVGFza0luZGV4KSByZXR1cm4gcHJldlxuICAgICAgICByZXR1cm4geyAuLi5wcmV2LCBjb29yZGluYXRvclRhc2tJbmRleDogbmV4dCB9XG4gICAgICB9KSxcbiAgICBbc2V0QXBwU3RhdGVdLFxuICApXG4gIGNvbnN0IGNvb3JkaW5hdG9yVGFza0NvdW50ID0gdXNlQ29vcmRpbmF0b3JUYXNrQ291bnQoKVxuICAvLyBUaGUgcGlsbCAoQmFja2dyb3VuZFRhc2tTdGF0dXMpIG9ubHkgcmVuZGVycyB3aGVuIG5vbi1sb2NhbF9hZ2VudCBiZyB0YXNrc1xuICAvLyBleGlzdC4gV2hlbiBvbmx5IGxvY2FsX2FnZW50IHRhc2tzIGFyZSBydW5uaW5nIChjb29yZGluYXRvci9mb3JrIG1vZGUpLCB0aGVcbiAgLy8gcGlsbCBpcyBhYnNlbnQsIHNvIHRoZSAtMSBzZW50aW5lbCB3b3VsZCBsZWF2ZSBub3RoaW5nIHZpc3VhbGx5IHNlbGVjdGVkLlxuICAvLyBJbiB0aGF0IGNhc2UsIHNraXAgLTEgYW5kIHRyZWF0IDAgYXMgdGhlIG1pbmltdW0gc2VsZWN0YWJsZSBpbmRleC5cbiAgY29uc3QgaGFzQmdUYXNrUGlsbCA9IHVzZU1lbW8oXG4gICAgKCkgPT5cbiAgICAgIE9iamVjdC52YWx1ZXModGFza3MpLnNvbWUoXG4gICAgICAgIHQgPT5cbiAgICAgICAgICBpc0JhY2tncm91bmRUYXNrKHQpICYmXG4gICAgICAgICAgIShcImV4dGVybmFsXCIgPT09ICdhbnQnICYmIGlzUGFuZWxBZ2VudFRhc2sodCkpLFxuICAgICAgKSxcbiAgICBbdGFza3NdLFxuICApXG4gIGNvbnN0IG1pbkNvb3JkaW5hdG9ySW5kZXggPSBoYXNCZ1Rhc2tQaWxsID8gLTEgOiAwXG4gIC8vIENsYW1wIGluZGV4IHdoZW4gdGFza3MgY29tcGxldGUgYW5kIHRoZSBsaXN0IHNocmlua3MgYmVuZWF0aCB0aGUgY3Vyc29yXG4gIHVzZUVmZmVjdCgoKSA9PiB7XG4gICAgaWYgKGNvb3JkaW5hdG9yVGFza0luZGV4ID49IGNvb3JkaW5hdG9yVGFza0NvdW50KSB7XG4gICAgICBzZXRDb29yZGluYXRvclRhc2tJbmRleChcbiAgICAgICAgTWF0aC5tYXgobWluQ29vcmRpbmF0b3JJbmRleCwgY29vcmRpbmF0b3JUYXNrQ291bnQgLSAxKSxcbiAgICAgIClcbiAgICB9IGVsc2UgaWYgKGNvb3JkaW5hdG9yVGFza0luZGV4IDwgbWluQ29vcmRpbmF0b3JJbmRleCkge1xuICAgICAgc2V0Q29vcmRpbmF0b3JUYXNrSW5kZXgobWluQ29vcmRpbmF0b3JJbmRleClcbiAgICB9XG4gIH0sIFtjb29yZGluYXRvclRhc2tDb3VudCwgY29vcmRpbmF0b3JUYXNrSW5kZXgsIG1pbkNvb3JkaW5hdG9ySW5kZXhdKVxuICBjb25zdCBbaXNQYXN0aW5nLCBzZXRJc1Bhc3RpbmddID0gdXNlU3RhdGUoZmFsc2UpXG4gIGNvbnN0IFtpc0V4dGVybmFsRWRpdG9yQWN0aXZlLCBzZXRJc0V4dGVybmFsRWRpdG9yQWN0aXZlXSA9IHVzZVN0YXRlKGZhbHNlKVxuICBjb25zdCBbc2hvd01vZGVsUGlja2VyLCBzZXRTaG93TW9kZWxQaWNrZXJdID0gdXNlU3RhdGUoZmFsc2UpXG4gIGNvbnN0IFtzaG93UXVpY2tPcGVuLCBzZXRTaG93UXVpY2tPcGVuXSA9IHVzZVN0YXRlKGZhbHNlKVxuICBjb25zdCBbc2hvd0dsb2JhbFNlYXJjaCwgc2V0U2hvd0dsb2JhbFNlYXJjaF0gPSB1c2VTdGF0ZShmYWxzZSlcbiAgY29uc3QgW3Nob3dIaXN0b3J5UGlja2VyLCBzZXRTaG93SGlzdG9yeVBpY2tlcl0gPSB1c2VTdGF0ZShmYWxzZSlcbiAgY29uc3QgW3Nob3dGYXN0TW9kZVBpY2tlciwgc2V0U2hvd0Zhc3RNb2RlUGlja2VyXSA9IHVzZVN0YXRlKGZhbHNlKVxuICBjb25zdCBbc2hvd1RoaW5raW5nVG9nZ2xlLCBzZXRTaG93VGhpbmtpbmdUb2dnbGVdID0gdXNlU3RhdGUoZmFsc2UpXG4gIGNvbnN0IFtzaG93QXV0b01vZGVPcHRJbiwgc2V0U2hvd0F1dG9Nb2RlT3B0SW5dID0gdXNlU3RhdGUoZmFsc2UpXG4gIGNvbnN0IFtwcmV2aW91c01vZGVCZWZvcmVBdXRvLCBzZXRQcmV2aW91c01vZGVCZWZvcmVBdXRvXSA9XG4gICAgdXNlU3RhdGU8UGVybWlzc2lvbk1vZGUgfCBudWxsPihudWxsKVxuICBjb25zdCBhdXRvTW9kZU9wdEluVGltZW91dFJlZiA9IHVzZVJlZjxOb2RlSlMuVGltZW91dCB8IG51bGw+KG51bGwpXG5cbiAgLy8gQ2hlY2sgaWYgY3Vyc29yIGlzIG9uIHRoZSBmaXJzdCBsaW5lIG9mIGlucHV0XG4gIGNvbnN0IGlzQ3Vyc29yT25GaXJzdExpbmUgPSB1c2VNZW1vKCgpID0+IHtcbiAgICBjb25zdCBmaXJzdE5ld2xpbmVJbmRleCA9IGlucHV0LmluZGV4T2YoJ1xcbicpXG4gICAgaWYgKGZpcnN0TmV3bGluZUluZGV4ID09PSAtMSkge1xuICAgICAgcmV0dXJuIHRydWUgLy8gTm8gbmV3bGluZXMsIGN1cnNvciBpcyBhbHdheXMgb24gZmlyc3QgbGluZVxuICAgIH1cbiAgICByZXR1cm4gY3Vyc29yT2Zmc2V0IDw9IGZpcnN0TmV3bGluZUluZGV4XG4gIH0sIFtpbnB1dCwgY3Vyc29yT2Zmc2V0XSlcblxuICBjb25zdCBpc0N1cnNvck9uTGFzdExpbmUgPSB1c2VNZW1vKCgpID0+IHtcbiAgICBjb25zdCBsYXN0TmV3bGluZUluZGV4ID0gaW5wdXQubGFzdEluZGV4T2YoJ1xcbicpXG4gICAgaWYgKGxhc3ROZXdsaW5lSW5kZXggPT09IC0xKSB7XG4gICAgICByZXR1cm4gdHJ1ZSAvLyBObyBuZXdsaW5lcywgY3Vyc29yIGlzIGFsd2F5cyBvbiBsYXN0IGxpbmVcbiAgICB9XG4gICAgcmV0dXJuIGN1cnNvck9mZnNldCA+IGxhc3ROZXdsaW5lSW5kZXhcbiAgfSwgW2lucHV0LCBjdXJzb3JPZmZzZXRdKVxuXG4gIC8vIERlcml2ZSB0ZWFtIGluZm8gZnJvbSB0ZWFtQ29udGV4dCAobm8gZmlsZXN5c3RlbSBJL08gbmVlZGVkKVxuICAvLyBBIHNlc3Npb24gY2FuIG9ubHkgbGVhZCBvbmUgdGVhbSBhdCBhIHRpbWVcbiAgY29uc3QgY2FjaGVkVGVhbXM6IFRlYW1TdW1tYXJ5W10gPSB1c2VNZW1vKCgpID0+IHtcbiAgICBpZiAoIWlzQWdlbnRTd2FybXNFbmFibGVkKCkpIHJldHVybiBbXVxuICAgIC8vIEluLXByb2Nlc3MgbW9kZSB1c2VzIFNoaWZ0K0Rvd24vVXAgbmF2aWdhdGlvbiBpbnN0ZWFkIG9mIGZvb3RlciBtZW51XG4gICAgaWYgKGlzSW5Qcm9jZXNzRW5hYmxlZCgpKSByZXR1cm4gW11cbiAgICBpZiAoIXRlYW1Db250ZXh0KSB7XG4gICAgICByZXR1cm4gW11cbiAgICB9XG4gICAgY29uc3QgdGVhbW1hdGVDb3VudCA9IGNvdW50KFxuICAgICAgT2JqZWN0LnZhbHVlcyh0ZWFtQ29udGV4dC50ZWFtbWF0ZXMpLFxuICAgICAgdCA9PiB0Lm5hbWUgIT09ICd0ZWFtLWxlYWQnLFxuICAgIClcbiAgICByZXR1cm4gW1xuICAgICAge1xuICAgICAgICBuYW1lOiB0ZWFtQ29udGV4dC50ZWFtTmFtZSxcbiAgICAgICAgbWVtYmVyQ291bnQ6IHRlYW1tYXRlQ291bnQsXG4gICAgICAgIHJ1bm5pbmdDb3VudDogMCxcbiAgICAgICAgaWRsZUNvdW50OiAwLFxuICAgICAgfSxcbiAgICBdXG4gIH0sIFt0ZWFtQ29udGV4dF0pXG5cbiAgLy8g4pSA4pSA4pSAIEZvb3RlciBwaWxsIG5hdmlnYXRpb24g4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSAXG4gIC8vIFdoaWNoIHBpbGxzIHJlbmRlciBiZWxvdyB0aGUgaW5wdXQgYm94LiBPcmRlciBoZXJlIElTIHRoZSBuYXYgb3JkZXJcbiAgLy8gKGRvd24vcmlnaHQgPSBmb3J3YXJkLCB1cC9sZWZ0ID0gYmFjaykuIFNlbGVjdGlvbiBsaXZlcyBpbiBBcHBTdGF0ZSBzb1xuICAvLyBwaWxscyByZW5kZXJlZCBvdXRzaWRlIFByb21wdElucHV0IChDb21wYW5pb25TcHJpdGUpIGNhbiByZWFkIGZvY3VzLlxuICBjb25zdCBydW5uaW5nVGFza0NvdW50ID0gdXNlTWVtbyhcbiAgICAoKSA9PiBjb3VudChPYmplY3QudmFsdWVzKHRhc2tzKSwgdCA9PiB0LnN0YXR1cyA9PT0gJ3J1bm5pbmcnKSxcbiAgICBbdGFza3NdLFxuICApXG4gIC8vIFBhbmVsIHNob3dzIHJldGFpbmVkLWNvbXBsZXRlZCBhZ2VudHMgdG9vIChnZXRWaXNpYmxlQWdlbnRUYXNrcyksIHNvIHRoZVxuICAvLyBwaWxsIG11c3Qgc3RheSBuYXZpZ2FibGUgd2hlbmV2ZXIgdGhlIHBhbmVsIGhhcyByb3dzIOKAlCBub3QganVzdCB3aGVuXG4gIC8vIHNvbWV0aGluZyBpcyBydW5uaW5nLlxuICBjb25zdCB0YXNrc0Zvb3RlclZpc2libGUgPVxuICAgIChydW5uaW5nVGFza0NvdW50ID4gMCB8fFxuICAgICAgKFwiZXh0ZXJuYWxcIiA9PT0gJ2FudCcgJiYgY29vcmRpbmF0b3JUYXNrQ291bnQgPiAwKSkgJiZcbiAgICAhc2hvdWxkSGlkZVRhc2tzRm9vdGVyKHRhc2tzLCBzaG93U3Bpbm5lclRyZWUpXG4gIGNvbnN0IHRlYW1zRm9vdGVyVmlzaWJsZSA9IGNhY2hlZFRlYW1zLmxlbmd0aCA+IDBcblxuICBjb25zdCBmb290ZXJJdGVtcyA9IHVzZU1lbW8oXG4gICAgKCkgPT5cbiAgICAgIFtcbiAgICAgICAgdGFza3NGb290ZXJWaXNpYmxlICYmICd0YXNrcycsXG4gICAgICAgIHRtdXhGb290ZXJWaXNpYmxlICYmICd0bXV4JyxcbiAgICAgICAgYmFnZWxGb290ZXJWaXNpYmxlICYmICdiYWdlbCcsXG4gICAgICAgIHRlYW1zRm9vdGVyVmlzaWJsZSAmJiAndGVhbXMnLFxuICAgICAgICBicmlkZ2VGb290ZXJWaXNpYmxlICYmICdicmlkZ2UnLFxuICAgICAgICBjb21wYW5pb25Gb290ZXJWaXNpYmxlICYmICdjb21wYW5pb24nLFxuICAgICAgXS5maWx0ZXIoQm9vbGVhbikgYXMgRm9vdGVySXRlbVtdLFxuICAgIFtcbiAgICAgIHRhc2