π File detail
screens/REPL.tsx
π― Use case
This file lives under βscreens/β, which covers full-screen React flows and primary UI routes. On the API surface it exposes Props, Screen, and REPL β mainly types, interfaces, or factory objects. Dependencies touch React UI, bun:bundle, subprocess spawning, and Node path helpers. It composes internal code from bootstrap, utils, ink, hooks, and components (relative imports).
Generated from folder role, exports, dependency roots, and inline comments β not hand-reviewed for every path.
π§ Inline summary
import { c as _c } from "react/compiler-runtime"; // biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered import { feature } from 'bun:bundle'; import { spawnSync } from 'child_process'; import { snapshotOutputTokensForTurn, getCurrentTurnTokenBudget, getTurnOutputTokens, getBudgetContinuationCount, getTotalInputTokens } from '../bootstrap/state.js';
π€ Exports (heuristic)
PropsScreenREPL
π External import roots
Package roots from from "β¦" (relative paths omitted).
reactbun:bundlechild_processpathosfiguresfssrccrypto@anthropic-ai
π₯οΈ Source preview
β οΈ Preview truncated to ~400KB. Open the file locally for the full content.
import { c as _c } from "react/compiler-runtime";
// biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered
import { feature } from 'bun:bundle';
import { spawnSync } from 'child_process';
import { snapshotOutputTokensForTurn, getCurrentTurnTokenBudget, getTurnOutputTokens, getBudgetContinuationCount, getTotalInputTokens } from '../bootstrap/state.js';
import { parseTokenBudget } from '../utils/tokenBudget.js';
import { count } from '../utils/array.js';
import { dirname, join } from 'path';
import { tmpdir } from 'os';
import figures from 'figures';
// eslint-disable-next-line custom-rules/prefer-use-keybindings -- / n N Esc [ v are bare letters in transcript modal context, same class as g/G/j/k in ScrollKeybindingHandler
import { useInput } from '../ink.js';
import { useSearchInput } from '../hooks/useSearchInput.js';
import { useTerminalSize } from '../hooks/useTerminalSize.js';
import { useSearchHighlight } from '../ink/hooks/use-search-highlight.js';
import type { JumpHandle } from '../components/VirtualMessageList.js';
import { renderMessagesToPlainText } from '../utils/exportRenderer.js';
import { openFileInExternalEditor } from '../utils/editor.js';
import { writeFile } from 'fs/promises';
import { Box, Text, useStdin, useTheme, useTerminalFocus, useTerminalTitle, useTabStatus } from '../ink.js';
import type { TabStatusKind } from '../ink/hooks/use-tab-status.js';
import { CostThresholdDialog } from '../components/CostThresholdDialog.js';
import { IdleReturnDialog } from '../components/IdleReturnDialog.js';
import * as React from 'react';
import { useEffect, useMemo, useRef, useState, useCallback, useDeferredValue, useLayoutEffect, type RefObject } from 'react';
import { useNotifications } from '../context/notifications.js';
import { sendNotification } from '../services/notifier.js';
import { startPreventSleep, stopPreventSleep } from '../services/preventSleep.js';
import { useTerminalNotification } from '../ink/useTerminalNotification.js';
import { hasCursorUpViewportYankBug } from '../ink/terminal.js';
import { createFileStateCacheWithSizeLimit, mergeFileStateCaches, READ_FILE_STATE_CACHE_SIZE } from '../utils/fileStateCache.js';
import { updateLastInteractionTime, getLastInteractionTime, getOriginalCwd, getProjectRoot, getSessionId, switchSession, setCostStateForRestore, getTurnHookDurationMs, getTurnHookCount, resetTurnHookDuration, getTurnToolDurationMs, getTurnToolCount, resetTurnToolDuration, getTurnClassifierDurationMs, getTurnClassifierCount, resetTurnClassifierDuration } from '../bootstrap/state.js';
import { asSessionId, asAgentId } from '../types/ids.js';
import { logForDebugging } from '../utils/debug.js';
import { QueryGuard } from '../utils/QueryGuard.js';
import { isEnvTruthy } from '../utils/envUtils.js';
import { formatTokens, truncateToWidth } from '../utils/format.js';
import { consumeEarlyInput } from '../utils/earlyInput.js';
import { setMemberActive } from '../utils/swarm/teamHelpers.js';
import { isSwarmWorker, generateSandboxRequestId, sendSandboxPermissionRequestViaMailbox, sendSandboxPermissionResponseViaMailbox } from '../utils/swarm/permissionSync.js';
import { registerSandboxPermissionCallback } from '../hooks/useSwarmPermissionPoller.js';
import { getTeamName, getAgentName } from '../utils/teammate.js';
import { WorkerPendingPermission } from '../components/permissions/WorkerPendingPermission.js';
import { injectUserMessageToTeammate, getAllInProcessTeammateTasks } from '../tasks/InProcessTeammateTask/InProcessTeammateTask.js';
import { isLocalAgentTask, queuePendingMessage, appendMessageToLocalAgent, type LocalAgentTaskState } from '../tasks/LocalAgentTask/LocalAgentTask.js';
import { registerLeaderToolUseConfirmQueue, unregisterLeaderToolUseConfirmQueue, registerLeaderSetToolPermissionContext, unregisterLeaderSetToolPermissionContext } from '../utils/swarm/leaderPermissionBridge.js';
import { endInteractionSpan } from '../utils/telemetry/sessionTracing.js';
import { useLogMessages } from '../hooks/useLogMessages.js';
import { useReplBridge } from '../hooks/useReplBridge.js';
import { type Command, type CommandResultDisplay, type ResumeEntrypoint, getCommandName, isCommandEnabled } from '../commands.js';
import type { PromptInputMode, QueuedCommand, VimMode } from '../types/textInputTypes.js';
import { MessageSelector, selectableUserMessagesFilter, messagesAfterAreOnlySynthetic } from '../components/MessageSelector.js';
import { useIdeLogging } from '../hooks/useIdeLogging.js';
import { PermissionRequest, type ToolUseConfirm } from '../components/permissions/PermissionRequest.js';
import { ElicitationDialog } from '../components/mcp/ElicitationDialog.js';
import { PromptDialog } from '../components/hooks/PromptDialog.js';
import type { PromptRequest, PromptResponse } from '../types/hooks.js';
import PromptInput from '../components/PromptInput/PromptInput.js';
import { PromptInputQueuedCommands } from '../components/PromptInput/PromptInputQueuedCommands.js';
import { useRemoteSession } from '../hooks/useRemoteSession.js';
import { useDirectConnect } from '../hooks/useDirectConnect.js';
import type { DirectConnectConfig } from '../server/directConnectManager.js';
import { useSSHSession } from '../hooks/useSSHSession.js';
import { useAssistantHistory } from '../hooks/useAssistantHistory.js';
import type { SSHSession } from '../ssh/createSSHSession.js';
import { SkillImprovementSurvey } from '../components/SkillImprovementSurvey.js';
import { useSkillImprovementSurvey } from '../hooks/useSkillImprovementSurvey.js';
import { useMoreRight } from '../moreright/useMoreRight.js';
import { SpinnerWithVerb, BriefIdleStatus, type SpinnerMode } from '../components/Spinner.js';
import { getSystemPrompt } from '../constants/prompts.js';
import { buildEffectiveSystemPrompt } from '../utils/systemPrompt.js';
import { getSystemContext, getUserContext } from '../context.js';
import { getMemoryFiles } from '../utils/claudemd.js';
import { startBackgroundHousekeeping } from '../utils/backgroundHousekeeping.js';
import { getTotalCost, saveCurrentSessionCosts, resetCostState, getStoredSessionCosts } from '../cost-tracker.js';
import { useCostSummary } from '../costHook.js';
import { useFpsMetrics } from '../context/fpsMetrics.js';
import { useAfterFirstRender } from '../hooks/useAfterFirstRender.js';
import { useDeferredHookMessages } from '../hooks/useDeferredHookMessages.js';
import { addToHistory, removeLastFromHistory, expandPastedTextRefs, parseReferences } from '../history.js';
import { prependModeCharacterToInput } from '../components/PromptInput/inputModes.js';
import { prependToShellHistoryCache } from '../utils/suggestions/shellHistoryCompletion.js';
import { useApiKeyVerification } from '../hooks/useApiKeyVerification.js';
import { GlobalKeybindingHandlers } from '../hooks/useGlobalKeybindings.js';
import { CommandKeybindingHandlers } from '../hooks/useCommandKeybindings.js';
import { KeybindingSetup } from '../keybindings/KeybindingProviderSetup.js';
import { useShortcutDisplay } from '../keybindings/useShortcutDisplay.js';
import { getShortcutDisplay } from '../keybindings/shortcutFormat.js';
import { CancelRequestHandler } from '../hooks/useCancelRequest.js';
import { useBackgroundTaskNavigation } from '../hooks/useBackgroundTaskNavigation.js';
import { useSwarmInitialization } from '../hooks/useSwarmInitialization.js';
import { useTeammateViewAutoExit } from '../hooks/useTeammateViewAutoExit.js';
import { errorMessage } from '../utils/errors.js';
import { isHumanTurn } from '../utils/messagePredicates.js';
import { logError } from '../utils/log.js';
// Dead code elimination: conditional imports
/* eslint-disable custom-rules/no-process-env-top-level, @typescript-eslint/no-require-imports */
const useVoiceIntegration: typeof import('../hooks/useVoiceIntegration.js').useVoiceIntegration = feature('VOICE_MODE') ? require('../hooks/useVoiceIntegration.js').useVoiceIntegration : () => ({
stripTrailing: () => 0,
handleKeyEvent: () => {},
resetAnchor: () => {}
});
const VoiceKeybindingHandler: typeof import('../hooks/useVoiceIntegration.js').VoiceKeybindingHandler = feature('VOICE_MODE') ? require('../hooks/useVoiceIntegration.js').VoiceKeybindingHandler : () => null;
// Frustration detection is ant-only (dogfooding). Conditional require so external
// builds eliminate the module entirely (including its two O(n) useMemos that run
// on every messages change, plus the GrowthBook fetch).
const useFrustrationDetection: typeof import('../components/FeedbackSurvey/useFrustrationDetection.js').useFrustrationDetection = "external" === 'ant' ? require('../components/FeedbackSurvey/useFrustrationDetection.js').useFrustrationDetection : () => ({
state: 'closed',
handleTranscriptSelect: () => {}
});
// Ant-only org warning. Conditional require so the org UUID list is
// eliminated from external builds (one UUID is on excluded-strings).
const useAntOrgWarningNotification: typeof import('../hooks/notifs/useAntOrgWarningNotification.js').useAntOrgWarningNotification = "external" === 'ant' ? require('../hooks/notifs/useAntOrgWarningNotification.js').useAntOrgWarningNotification : () => {};
// Dead code elimination: conditional import for coordinator mode
const getCoordinatorUserContext: (mcpClients: ReadonlyArray<{
name: string;
}>, scratchpadDir?: string) => {
[k: string]: string;
} = feature('COORDINATOR_MODE') ? require('../coordinator/coordinatorMode.js').getCoordinatorUserContext : () => ({});
/* eslint-enable custom-rules/no-process-env-top-level, @typescript-eslint/no-require-imports */
import useCanUseTool from '../hooks/useCanUseTool.js';
import type { ToolPermissionContext, Tool } from '../Tool.js';
import { applyPermissionUpdate, applyPermissionUpdates, persistPermissionUpdate } from '../utils/permissions/PermissionUpdate.js';
import { buildPermissionUpdates } from '../components/permissions/ExitPlanModePermissionRequest/ExitPlanModePermissionRequest.js';
import { stripDangerousPermissionsForAutoMode } from '../utils/permissions/permissionSetup.js';
import { getScratchpadDir, isScratchpadEnabled } from '../utils/permissions/filesystem.js';
import { WEB_FETCH_TOOL_NAME } from '../tools/WebFetchTool/prompt.js';
import { SLEEP_TOOL_NAME } from '../tools/SleepTool/prompt.js';
import { clearSpeculativeChecks } from '../tools/BashTool/bashPermissions.js';
import type { AutoUpdaterResult } from '../utils/autoUpdater.js';
import { getGlobalConfig, saveGlobalConfig, getGlobalConfigWriteCount } from '../utils/config.js';
import { hasConsoleBillingAccess } from '../utils/billing.js';
import { logEvent, type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS } from 'src/services/analytics/index.js';
import { getFeatureValue_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js';
import { textForResubmit, handleMessageFromStream, type StreamingToolUse, type StreamingThinking, isCompactBoundaryMessage, getMessagesAfterCompactBoundary, getContentText, createUserMessage, createAssistantMessage, createTurnDurationMessage, createAgentsKilledMessage, createApiMetricsMessage, createSystemMessage, createCommandInputMessage, formatCommandInputTags } from '../utils/messages.js';
import { generateSessionTitle } from '../utils/sessionTitle.js';
import { BASH_INPUT_TAG, COMMAND_MESSAGE_TAG, COMMAND_NAME_TAG, LOCAL_COMMAND_STDOUT_TAG } from '../constants/xml.js';
import { escapeXml } from '../utils/xml.js';
import type { ThinkingConfig } from '../utils/thinking.js';
import { gracefulShutdownSync } from '../utils/gracefulShutdown.js';
import { handlePromptSubmit, type PromptInputHelpers } from '../utils/handlePromptSubmit.js';
import { useQueueProcessor } from '../hooks/useQueueProcessor.js';
import { useMailboxBridge } from '../hooks/useMailboxBridge.js';
import { queryCheckpoint, logQueryProfileReport } from '../utils/queryProfiler.js';
import type { Message as MessageType, UserMessage, ProgressMessage, HookResultMessage, PartialCompactDirection } from '../types/message.js';
import { query } from '../query.js';
import { mergeClients, useMergedClients } from '../hooks/useMergedClients.js';
import { getQuerySourceForREPL } from '../utils/promptCategory.js';
import { useMergedTools } from '../hooks/useMergedTools.js';
import { mergeAndFilterTools } from '../utils/toolPool.js';
import { useMergedCommands } from '../hooks/useMergedCommands.js';
import { useSkillsChange } from '../hooks/useSkillsChange.js';
import { useManagePlugins } from '../hooks/useManagePlugins.js';
import { Messages } from '../components/Messages.js';
import { TaskListV2 } from '../components/TaskListV2.js';
import { TeammateViewHeader } from '../components/TeammateViewHeader.js';
import { useTasksV2WithCollapseEffect } from '../hooks/useTasksV2.js';
import { maybeMarkProjectOnboardingComplete } from '../projectOnboardingState.js';
import type { MCPServerConnection } from '../services/mcp/types.js';
import type { ScopedMcpServerConfig } from '../services/mcp/types.js';
import { randomUUID, type UUID } from 'crypto';
import { processSessionStartHooks } from '../utils/sessionStart.js';
import { executeSessionEndHooks, getSessionEndHookTimeoutMs } from '../utils/hooks.js';
import { type IDESelection, useIdeSelection } from '../hooks/useIdeSelection.js';
import { getTools, assembleToolPool } from '../tools.js';
import type { AgentDefinition } from '../tools/AgentTool/loadAgentsDir.js';
import { resolveAgentTools } from '../tools/AgentTool/agentToolUtils.js';
import { resumeAgentBackground } from '../tools/AgentTool/resumeAgent.js';
import { useMainLoopModel } from '../hooks/useMainLoopModel.js';
import { useAppState, useSetAppState, useAppStateStore } from '../state/AppState.js';
import type { ContentBlockParam, ImageBlockParam } from '@anthropic-ai/sdk/resources/messages.mjs';
import type { ProcessUserInputContext } from '../utils/processUserInput/processUserInput.js';
import type { PastedContent } from '../utils/config.js';
import { copyPlanForFork, copyPlanForResume, getPlanSlug, setPlanSlug } from '../utils/plans.js';
import { clearSessionMetadata, resetSessionFilePointer, adoptResumedSessionFile, removeTranscriptMessage, restoreSessionMetadata, getCurrentSessionTitle, isEphemeralToolProgress, isLoggableMessage, saveWorktreeState, getAgentTranscript } from '../utils/sessionStorage.js';
import { deserializeMessages } from '../utils/conversationRecovery.js';
import { extractReadFilesFromMessages, extractBashToolsFromMessages } from '../utils/queryHelpers.js';
import { resetMicrocompactState } from '../services/compact/microCompact.js';
import { runPostCompactCleanup } from '../services/compact/postCompactCleanup.js';
import { provisionContentReplacementState, reconstructContentReplacementState, type ContentReplacementRecord } from '../utils/toolResultStorage.js';
import { partialCompactConversation } from '../services/compact/compact.js';
import type { LogOption } from '../types/logs.js';
import type { AgentColorName } from '../tools/AgentTool/agentColorManager.js';
import { fileHistoryMakeSnapshot, type FileHistoryState, fileHistoryRewind, type FileHistorySnapshot, copyFileHistoryForResume, fileHistoryEnabled, fileHistoryHasAnyChanges } from '../utils/fileHistory.js';
import { type AttributionState, incrementPromptCount } from '../utils/commitAttribution.js';
import { recordAttributionSnapshot } from '../utils/sessionStorage.js';
import { computeStandaloneAgentContext, restoreAgentFromSession, restoreSessionStateFromLog, restoreWorktreeForResume, exitRestoredWorktree } from '../utils/sessionRestore.js';
import { isBgSession, updateSessionName, updateSessionActivity } from '../utils/concurrentSessions.js';
import { isInProcessTeammateTask, type InProcessTeammateTaskState } from '../tasks/InProcessTeammateTask/types.js';
import { restoreRemoteAgentTasks } from '../tasks/RemoteAgentTask/RemoteAgentTask.js';
import { useInboxPoller } from '../hooks/useInboxPoller.js';
// Dead code elimination: conditional import for loop mode
/* eslint-disable @typescript-eslint/no-require-imports */
const proactiveModule = feature('PROACTIVE') || feature('KAIROS') ? require('../proactive/index.js') : null;
const PROACTIVE_NO_OP_SUBSCRIBE = (_cb: () => void) => () => {};
const PROACTIVE_FALSE = () => false;
const SUGGEST_BG_PR_NOOP = (_p: string, _n: string): boolean => false;
const useProactive = feature('PROACTIVE') || feature('KAIROS') ? require('../proactive/useProactive.js').useProactive : null;
const useScheduledTasks = feature('AGENT_TRIGGERS') ? require('../hooks/useScheduledTasks.js').useScheduledTasks : null;
/* eslint-enable @typescript-eslint/no-require-imports */
import { isAgentSwarmsEnabled } from '../utils/agentSwarmsEnabled.js';
import { useTaskListWatcher } from '../hooks/useTaskListWatcher.js';
import type { SandboxAskCallback, NetworkHostPattern } from '../utils/sandbox/sandbox-adapter.js';
import { type IDEExtensionInstallationStatus, closeOpenDiffs, getConnectedIdeClient, type IdeType } from '../utils/ide.js';
import { useIDEIntegration } from '../hooks/useIDEIntegration.js';
import exit from '../commands/exit/index.js';
import { ExitFlow } from '../components/ExitFlow.js';
import { getCurrentWorktreeSession } from '../utils/worktree.js';
import { popAllEditable, enqueue, type SetAppState, getCommandQueue, getCommandQueueLength, removeByFilter } from '../utils/messageQueueManager.js';
import { useCommandQueue } from '../hooks/useCommandQueue.js';
import { SessionBackgroundHint } from '../components/SessionBackgroundHint.js';
import { startBackgroundSession } from '../tasks/LocalMainSessionTask.js';
import { useSessionBackgrounding } from '../hooks/useSessionBackgrounding.js';
import { diagnosticTracker } from '../services/diagnosticTracking.js';
import { handleSpeculationAccept, type ActiveSpeculationState } from '../services/PromptSuggestion/speculation.js';
import { IdeOnboardingDialog } from '../components/IdeOnboardingDialog.js';
import { EffortCallout, shouldShowEffortCallout } from '../components/EffortCallout.js';
import type { EffortValue } from '../utils/effort.js';
import { RemoteCallout } from '../components/RemoteCallout.js';
/* eslint-disable custom-rules/no-process-env-top-level, @typescript-eslint/no-require-imports */
const AntModelSwitchCallout = "external" === 'ant' ? require('../components/AntModelSwitchCallout.js').AntModelSwitchCallout : null;
const shouldShowAntModelSwitch = "external" === 'ant' ? require('../components/AntModelSwitchCallout.js').shouldShowModelSwitchCallout : (): boolean => false;
const UndercoverAutoCallout = "external" === 'ant' ? require('../components/UndercoverAutoCallout.js').UndercoverAutoCallout : null;
/* eslint-enable custom-rules/no-process-env-top-level, @typescript-eslint/no-require-imports */
import { activityManager } from '../utils/activityManager.js';
import { createAbortController } from '../utils/abortController.js';
import { MCPConnectionManager } from 'src/services/mcp/MCPConnectionManager.js';
import { useFeedbackSurvey } from 'src/components/FeedbackSurvey/useFeedbackSurvey.js';
import { useMemorySurvey } from 'src/components/FeedbackSurvey/useMemorySurvey.js';
import { usePostCompactSurvey } from 'src/components/FeedbackSurvey/usePostCompactSurvey.js';
import { FeedbackSurvey } from 'src/components/FeedbackSurvey/FeedbackSurvey.js';
import { useInstallMessages } from 'src/hooks/notifs/useInstallMessages.js';
import { useAwaySummary } from 'src/hooks/useAwaySummary.js';
import { useChromeExtensionNotification } from 'src/hooks/useChromeExtensionNotification.js';
import { useOfficialMarketplaceNotification } from 'src/hooks/useOfficialMarketplaceNotification.js';
import { usePromptsFromClaudeInChrome } from 'src/hooks/usePromptsFromClaudeInChrome.js';
import { getTipToShowOnSpinner, recordShownTip } from 'src/services/tips/tipScheduler.js';
import type { Theme } from 'src/utils/theme.js';
import { checkAndDisableBypassPermissionsIfNeeded, checkAndDisableAutoModeIfNeeded, useKickOffCheckAndDisableBypassPermissionsIfNeeded, useKickOffCheckAndDisableAutoModeIfNeeded } from 'src/utils/permissions/bypassPermissionsKillswitch.js';
import { SandboxManager } from 'src/utils/sandbox/sandbox-adapter.js';
import { SANDBOX_NETWORK_ACCESS_TOOL_NAME } from 'src/cli/structuredIO.js';
import { useFileHistorySnapshotInit } from 'src/hooks/useFileHistorySnapshotInit.js';
import { SandboxPermissionRequest } from 'src/components/permissions/SandboxPermissionRequest.js';
import { SandboxViolationExpandedView } from 'src/components/SandboxViolationExpandedView.js';
import { useSettingsErrors } from 'src/hooks/notifs/useSettingsErrors.js';
import { useMcpConnectivityStatus } from 'src/hooks/notifs/useMcpConnectivityStatus.js';
import { useAutoModeUnavailableNotification } from 'src/hooks/notifs/useAutoModeUnavailableNotification.js';
import { AUTO_MODE_DESCRIPTION } from 'src/components/AutoModeOptInDialog.js';
import { useLspInitializationNotification } from 'src/hooks/notifs/useLspInitializationNotification.js';
import { useLspPluginRecommendation } from 'src/hooks/useLspPluginRecommendation.js';
import { LspRecommendationMenu } from 'src/components/LspRecommendation/LspRecommendationMenu.js';
import { useClaudeCodeHintRecommendation } from 'src/hooks/useClaudeCodeHintRecommendation.js';
import { PluginHintMenu } from 'src/components/ClaudeCodeHint/PluginHintMenu.js';
import { DesktopUpsellStartup, shouldShowDesktopUpsellStartup } from 'src/components/DesktopUpsell/DesktopUpsellStartup.js';
import { usePluginInstallationStatus } from 'src/hooks/notifs/usePluginInstallationStatus.js';
import { usePluginAutoupdateNotification } from 'src/hooks/notifs/usePluginAutoupdateNotification.js';
import { performStartupChecks } from 'src/utils/plugins/performStartupChecks.js';
import { UserTextMessage } from 'src/components/messages/UserTextMessage.js';
import { AwsAuthStatusBox } from '../components/AwsAuthStatusBox.js';
import { useRateLimitWarningNotification } from 'src/hooks/notifs/useRateLimitWarningNotification.js';
import { useDeprecationWarningNotification } from 'src/hooks/notifs/useDeprecationWarningNotification.js';
import { useNpmDeprecationNotification } from 'src/hooks/notifs/useNpmDeprecationNotification.js';
import { useIDEStatusIndicator } from 'src/hooks/notifs/useIDEStatusIndicator.js';
import { useModelMigrationNotifications } from 'src/hooks/notifs/useModelMigrationNotifications.js';
import { useCanSwitchToExistingSubscription } from 'src/hooks/notifs/useCanSwitchToExistingSubscription.js';
import { useTeammateLifecycleNotification } from 'src/hooks/notifs/useTeammateShutdownNotification.js';
import { useFastModeNotification } from 'src/hooks/notifs/useFastModeNotification.js';
import { AutoRunIssueNotification, shouldAutoRunIssue, getAutoRunIssueReasonText, getAutoRunCommand, type AutoRunIssueReason } from '../utils/autoRunIssue.js';
import type { HookProgress } from '../types/hooks.js';
import { TungstenLiveMonitor } from '../tools/TungstenTool/TungstenLiveMonitor.js';
/* eslint-disable @typescript-eslint/no-require-imports */
const WebBrowserPanelModule = feature('WEB_BROWSER_TOOL') ? require('../tools/WebBrowserTool/WebBrowserPanel.js') as typeof import('../tools/WebBrowserTool/WebBrowserPanel.js') : null;
/* eslint-enable @typescript-eslint/no-require-imports */
import { IssueFlagBanner } from '../components/PromptInput/IssueFlagBanner.js';
import { useIssueFlagBanner } from '../hooks/useIssueFlagBanner.js';
import { CompanionSprite, CompanionFloatingBubble, MIN_COLS_FOR_FULL_SPRITE } from '../buddy/CompanionSprite.js';
import { DevBar } from '../components/DevBar.js';
// Session manager removed - using AppState now
import type { RemoteSessionConfig } from '../remote/RemoteSessionManager.js';
import { REMOTE_SAFE_COMMANDS } from '../commands.js';
import type { RemoteMessageContent } from '../utils/teleport/api.js';
import { FullscreenLayout, useUnseenDivider, computeUnseenDivider } from '../components/FullscreenLayout.js';
import { isFullscreenEnvEnabled, maybeGetTmuxMouseHint, isMouseTrackingEnabled } from '../utils/fullscreen.js';
import { AlternateScreen } from '../ink/components/AlternateScreen.js';
import { ScrollKeybindingHandler } from '../components/ScrollKeybindingHandler.js';
import { useMessageActions, MessageActionsKeybindings, MessageActionsBar, type MessageActionsState, type MessageActionsNav, type MessageActionCaps } from '../components/messageActions.js';
import { setClipboard } from '../ink/termio/osc.js';
import type { ScrollBoxHandle } from '../ink/components/ScrollBox.js';
import { createAttachmentMessage, getQueuedCommandAttachments } from '../utils/attachments.js';
// Stable empty array for hooks that accept MCPServerConnection[] β avoids
// creating a new [] literal on every render in remote mode, which would
// cause useEffect dependency changes and infinite re-render loops.
const EMPTY_MCP_CLIENTS: MCPServerConnection[] = [];
// Stable stub for useAssistantHistory's non-KAIROS branch β avoids a new
// function identity each render, which would break composedOnScroll's memo.
const HISTORY_STUB = {
maybeLoadOlder: (_: ScrollBoxHandle) => {}
};
// Window after a user-initiated scroll during which type-into-empty does NOT
// repin to bottom. Josh Rosen's workflow: Claude emits long output β scroll
// up to read the start β start typing β before this fix, snapped to bottom.
// https://anthropic.slack.com/archives/C07VBSHV7EV/p1773545449871739
const RECENT_SCROLL_REPIN_WINDOW_MS = 3000;
// Use LRU cache to prevent unbounded memory growth
// 100 files should be sufficient for most coding sessions while preventing
// memory issues when working across many files in large projects
function median(values: number[]): number {
const sorted = [...values].sort((a, b) => a - b);
const mid = Math.floor(sorted.length / 2);
return sorted.length % 2 === 0 ? Math.round((sorted[mid - 1]! + sorted[mid]!) / 2) : sorted[mid]!;
}
/**
* Small component to display transcript mode footer with dynamic keybinding.
* Must be rendered inside KeybindingSetup to access keybinding context.
*/
function TranscriptModeFooter(t0) {
const $ = _c(9);
const {
showAllInTranscript,
virtualScroll,
searchBadge,
suppressShowAll: t1,
status
} = t0;
const suppressShowAll = t1 === undefined ? false : t1;
const toggleShortcut = useShortcutDisplay("app:toggleTranscript", "Global", "ctrl+o");
const showAllShortcut = useShortcutDisplay("transcript:toggleShowAll", "Transcript", "ctrl+e");
const t2 = searchBadge ? " \xB7 n/N to navigate" : virtualScroll ? ` Β· ${figures.arrowUp}${figures.arrowDown} scroll Β· home/end top/bottom` : suppressShowAll ? "" : ` Β· ${showAllShortcut} to ${showAllInTranscript ? "collapse" : "show all"}`;
let t3;
if ($[0] !== t2 || $[1] !== toggleShortcut) {
t3 = <Text dimColor={true}>Showing detailed transcript Β· {toggleShortcut} to toggle{t2}</Text>;
$[0] = t2;
$[1] = toggleShortcut;
$[2] = t3;
} else {
t3 = $[2];
}
let t4;
if ($[3] !== searchBadge || $[4] !== status) {
t4 = status ? <><Box flexGrow={1} /><Text>{status} </Text></> : searchBadge ? <><Box flexGrow={1} /><Text dimColor={true}>{searchBadge.current}/{searchBadge.count}{" "}</Text></> : null;
$[3] = searchBadge;
$[4] = status;
$[5] = t4;
} else {
t4 = $[5];
}
let t5;
if ($[6] !== t3 || $[7] !== t4) {
t5 = <Box noSelect={true} alignItems="center" alignSelf="center" borderTopDimColor={true} borderBottom={false} borderLeft={false} borderRight={false} borderStyle="single" marginTop={1} paddingLeft={2} width="100%">{t3}{t4}</Box>;
$[6] = t3;
$[7] = t4;
$[8] = t5;
} else {
t5 = $[8];
}
return t5;
}
/** less-style / bar. 1-row, same border-top styling as TranscriptModeFooter
* so swapping them in the bottom slot doesn't shift ScrollBox height.
* useSearchInput handles readline editing; we report query changes and
* render the counter. Incremental β re-search + highlight per keystroke. */
function TranscriptSearchBar({
jumpRef,
count,
current,
onClose,
onCancel,
setHighlight,
initialQuery
}: {
jumpRef: RefObject<JumpHandle | null>;
count: number;
current: number;
/** Enter β commit. Query persists for n/N. */
onClose: (lastQuery: string) => void;
/** Esc/ctrl+c/ctrl+g β undo to pre-/ state. */
onCancel: () => void;
setHighlight: (query: string) => void;
// Seed with the previous query (less: / shows last pattern). Mount-fire
// of the effect re-scans with the same query β idempotent (same matches,
// nearest-ptr, same highlights). User can edit or clear.
initialQuery: string;
}): React.ReactNode {
const {
query,
cursorOffset
} = useSearchInput({
isActive: true,
initialQuery,
onExit: () => onClose(query),
onCancel
});
// Index warm-up runs before the query effect so it measures the real
// cost β otherwise setSearchQuery fills the cache first and warm
// reports ~0ms while the user felt the actual lag.
// First / in a transcript session pays the extractSearchText cost.
// Subsequent / return 0 immediately (indexWarmed ref in VML).
// Transcript is frozen at ctrl+o so the cache stays valid.
// Initial 'building' so warmDone is false on mount β the [query] effect
// waits for the warm effect's first resolve instead of racing it. With
// null initial, warmDone would be true on mount β [query] fires β
// setSearchQuery fills cache β warm reports ~0ms while the user felt
// the real lag.
const [indexStatus, setIndexStatus] = React.useState<'building' | {
ms: number;
} | null>('building');
React.useEffect(() => {
let alive = true;
const warm = jumpRef.current?.warmSearchIndex;
if (!warm) {
setIndexStatus(null); // VML not mounted yet β rare, skip indicator
return;
}
setIndexStatus('building');
warm().then(ms => {
if (!alive) return;
// <20ms = imperceptible. No point showing "indexed in 3ms".
if (ms < 20) {
setIndexStatus(null);
} else {
setIndexStatus({
ms
});
setTimeout(() => alive && setIndexStatus(null), 2000);
}
});
return () => {
alive = false;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); // mount-only: bar opens once per /
// Gate the query effect on warm completion. setHighlight stays instant
// (screen-space overlay, no indexing). setSearchQuery (the scan) waits.
const warmDone = indexStatus !== 'building';
useEffect(() => {
if (!warmDone) return;
jumpRef.current?.setSearchQuery(query);
setHighlight(query);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [query, warmDone]);
const off = cursorOffset;
const cursorChar = off < query.length ? query[off] : ' ';
return <Box borderTopDimColor borderBottom={false} borderLeft={false} borderRight={false} borderStyle="single" marginTop={1} paddingLeft={2} width="100%"
// applySearchHighlight scans the whole screen buffer. The query
// text rendered here IS on screen β /foo matches its own 'foo' in
// the bar. With no content matches that's the ONLY visible match β
// gets CURRENT β underlined. noSelect makes searchHighlight.ts:76
// skip these cells (same exclusion as gutters). You can't text-
// select the bar either; it's transient chrome, fine.
noSelect>
<Text>/</Text>
<Text>{query.slice(0, off)}</Text>
<Text inverse>{cursorChar}</Text>
{off < query.length && <Text>{query.slice(off + 1)}</Text>}
<Box flexGrow={1} />
{indexStatus === 'building' ? <Text dimColor>indexing⦠</Text> : indexStatus ? <Text dimColor>indexed in {indexStatus.ms}ms </Text> : count === 0 && query ? <Text color="error">no matches </Text> : count > 0 ?
// Engine-counted (indexOf on extractSearchText). May drift from
// render-count for ghost/phantom messages β badge is a rough
// location hint. scanElement gives exact per-message positions
// but counting ALL would cost ~1-3ms Γ matched-messages.
<Text dimColor>
{current}/{count}
{' '}
</Text> : null}
</Box>;
}
const TITLE_ANIMATION_FRAMES = ['β ', 'β '];
const TITLE_STATIC_PREFIX = 'β³';
const TITLE_ANIMATION_INTERVAL_MS = 960;
/**
* Sets the terminal tab title, with an animated prefix glyph while a query
* is running. Isolated from REPL so the 960ms animation tick re-renders only
* this leaf component (which returns null β pure side-effect) instead of the
* entire REPL tree. Before extraction, the tick was ~1 REPL render/sec for
* the duration of every turn, dragging PromptInput and friends along.
*/
function AnimatedTerminalTitle(t0) {
const $ = _c(6);
const {
isAnimating,
title,
disabled,
noPrefix
} = t0;
const terminalFocused = useTerminalFocus();
const [frame, setFrame] = useState(0);
let t1;
let t2;
if ($[0] !== disabled || $[1] !== isAnimating || $[2] !== noPrefix || $[3] !== terminalFocused) {
t1 = () => {
if (disabled || noPrefix || !isAnimating || !terminalFocused) {
return;
}
const interval = setInterval(_temp2, TITLE_ANIMATION_INTERVAL_MS, setFrame);
return () => clearInterval(interval);
};
t2 = [disabled, noPrefix, isAnimating, terminalFocused];
$[0] = disabled;
$[1] = isAnimating;
$[2] = noPrefix;
$[3] = terminalFocused;
$[4] = t1;
$[5] = t2;
} else {
t1 = $[4];
t2 = $[5];
}
useEffect(t1, t2);
const prefix = isAnimating ? TITLE_ANIMATION_FRAMES[frame] ?? TITLE_STATIC_PREFIX : TITLE_STATIC_PREFIX;
useTerminalTitle(disabled ? null : noPrefix ? title : `${prefix} ${title}`);
return null;
}
function _temp2(setFrame_0) {
return setFrame_0(_temp);
}
function _temp(f) {
return (f + 1) % TITLE_ANIMATION_FRAMES.length;
}
export type Props = {
commands: Command[];
debug: boolean;
initialTools: Tool[];
// Initial messages to populate the REPL with
initialMessages?: MessageType[];
// Deferred hook messages promise β REPL renders immediately and injects
// hook messages when they resolve. Awaited before the first API call.
pendingHookMessages?: Promise<HookResultMessage[]>;
initialFileHistorySnapshots?: FileHistorySnapshot[];
// Content-replacement records from a resumed session's transcript β used to
// reconstruct contentReplacementState so the same results are re-replaced
initialContentReplacements?: ContentReplacementRecord[];
// Initial agent context for session resume (name/color set via /rename or /color)
initialAgentName?: string;
initialAgentColor?: AgentColorName;
mcpClients?: MCPServerConnection[];
dynamicMcpConfig?: Record<string, ScopedMcpServerConfig>;
autoConnectIdeFlag?: boolean;
strictMcpConfig?: boolean;
systemPrompt?: string;
appendSystemPrompt?: string;
// Optional callback invoked before query execution
// Called after user message is added to conversation but before API call
// Return false to prevent query execution
onBeforeQuery?: (input: string, newMessages: MessageType[]) => Promise<boolean>;
// Optional callback when a turn completes (model finishes responding)
onTurnComplete?: (messages: MessageType[]) => void | Promise<void>;
// When true, disables REPL input (hides prompt and prevents message selector)
disabled?: boolean;
// Optional agent definition to use for the main thread
mainThreadAgentDefinition?: AgentDefinition;
// When true, disables all slash commands
disableSlashCommands?: boolean;
// Task list id: when set, enables tasks mode that watches a task list and auto-processes tasks.
taskListId?: string;
// Remote session config for --remote mode (uses CCR as execution engine)
remoteSessionConfig?: RemoteSessionConfig;
// Direct connect config for `claude connect` mode (connects to a claude server)
directConnectConfig?: DirectConnectConfig;
// SSH session for `claude ssh` mode (local REPL, remote tools over ssh)
sshSession?: SSHSession;
// Thinking configuration to use when thinking is enabled
thinkingConfig: ThinkingConfig;
};
export type Screen = 'prompt' | 'transcript';
export function REPL({
commands: initialCommands,
debug,
initialTools,
initialMessages,
pendingHookMessages,
initialFileHistorySnapshots,
initialContentReplacements,
initialAgentName,
initialAgentColor,
mcpClients: initialMcpClients,
dynamicMcpConfig: initialDynamicMcpConfig,
autoConnectIdeFlag,
strictMcpConfig = false,
systemPrompt: customSystemPrompt,
appendSystemPrompt,
onBeforeQuery,
onTurnComplete,
disabled = false,
mainThreadAgentDefinition: initialMainThreadAgentDefinition,
disableSlashCommands = false,
taskListId,
remoteSessionConfig,
directConnectConfig,
sshSession,
thinkingConfig
}: Props): React.ReactNode {
const isRemoteSession = !!remoteSessionConfig;
// Env-var gates hoisted to mount-time β isEnvTruthy does toLowerCase+trim+
// includes, and these were on the render path (hot during PageUp spam).
const titleDisabled = useMemo(() => isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_TERMINAL_TITLE), []);
const moreRightEnabled = useMemo(() => "external" === 'ant' && isEnvTruthy(process.env.CLAUDE_MORERIGHT), []);
const disableVirtualScroll = useMemo(() => isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_VIRTUAL_SCROLL), []);
const disableMessageActions = feature('MESSAGE_ACTIONS') ?
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
useMemo(() => isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_MESSAGE_ACTIONS), []) : false;
// Log REPL mount/unmount lifecycle
useEffect(() => {
logForDebugging(`[REPL:mount] REPL mounted, disabled=${disabled}`);
return () => logForDebugging(`[REPL:unmount] REPL unmounting`);
}, [disabled]);
// Agent definition is state so /resume can update it mid-session
const [mainThreadAgentDefinition, setMainThreadAgentDefinition] = useState(initialMainThreadAgentDefinition);
const toolPermissionContext = useAppState(s => s.toolPermissionContext);
const verbose = useAppState(s => s.verbose);
const mcp = useAppState(s => s.mcp);
const plugins = useAppState(s => s.plugins);
const agentDefinitions = useAppState(s => s.agentDefinitions);
const fileHistory = useAppState(s => s.fileHistory);
const initialMessage = useAppState(s => s.initialMessage);
const queuedCommands = useCommandQueue();
// feature() is a build-time constant β dead code elimination removes the hook
// call entirely in external builds, so this is safe despite looking conditional.
// These fields contain excluded strings that must not appear in external builds.
const spinnerTip = useAppState(s => s.spinnerTip);
const showExpandedTodos = useAppState(s => s.expandedView) === 'tasks';
const pendingWorkerRequest = useAppState(s => s.pendingWorkerRequest);
const pendingSandboxRequest = useAppState(s => s.pendingSandboxRequest);
const teamContext = useAppState(s => s.teamContext);
const tasks = useAppState(s => s.tasks);
const workerSandboxPermissions = useAppState(s => s.workerSandboxPermissions);
const elicitation = useAppState(s => s.elicitation);
const ultraplanPendingChoice = useAppState(s => s.ultraplanPendingChoice);
const ultraplanLaunchPending = useAppState(s => s.ultraplanLaunchPending);
const viewingAgentTaskId = useAppState(s => s.viewingAgentTaskId);
const setAppState = useSetAppState();
// Bootstrap: retained local_agent that hasn't loaded disk yet β read
// sidechain JSONL and UUID-merge with whatever stream has appended so far.
// Stream appends immediately on retain (no defer); bootstrap fills the
// prefix. Disk-write-before-yield means live is always a suffix of disk.
const viewedLocalAgent = viewingAgentTaskId ? tasks[viewingAgentTaskId] : undefined;
const needsBootstrap = isLocalAgentTask(viewedLocalAgent) && viewedLocalAgent.retain && !viewedLocalAgent.diskLoaded;
useEffect(() => {
if (!viewingAgentTaskId || !needsBootstrap) return;
const taskId = viewingAgentTaskId;
void getAgentTranscript(asAgentId(taskId)).then(result => {
setAppState(prev => {
const t = prev.tasks[taskId];
if (!isLocalAgentTask(t) || t.diskLoaded || !t.retain) return prev;
const live = t.messages ?? [];
const liveUuids = new Set(live.map(m => m.uuid));
const diskOnly = result ? result.messages.filter(m => !liveUuids.has(m.uuid)) : [];
return {
...prev,
tasks: {
...prev.tasks,
[taskId]: {
...t,
messages: [...diskOnly, ...live],
diskLoaded: true
}
}
};
});
});
}, [viewingAgentTaskId, needsBootstrap, setAppState]);
const store = useAppStateStore();
const terminal = useTerminalNotification();
const mainLoopModel = useMainLoopModel();
// Note: standaloneAgentContext is initialized in main.tsx (via initialState) or
// ResumeConversation.tsx (via setAppState before rendering REPL) to avoid
// useEffect-based state initialization on mount (per CLAUDE.md guidelines)
// Local state for commands (hot-reloadable when skill files change)
const [localCommands, setLocalCommands] = useState(initialCommands);
// Watch for skill file changes and reload all commands
useSkillsChange(isRemoteSession ? undefined : getProjectRoot(), setLocalCommands);
// Track proactive mode for tools dependency - SleepTool filters by proactive state
const proactiveActive = React.useSyncExternalStore(proactiveModule?.subscribeToProactiveChanges ?? PROACTIVE_NO_OP_SUBSCRIBE, proactiveModule?.isProactiveActive ?? PROACTIVE_FALSE);
// BriefTool.isEnabled() reads getUserMsgOptIn() from bootstrap state, which
// /brief flips mid-session alongside isBriefOnly. The memo below needs a
// React-visible dep to re-run getTools() when that happens; isBriefOnly is
// the AppState mirror that triggers the re-render. Without this, toggling
// /brief mid-session leaves the stale tool list (no SendUserMessage) and
// the model emits plain text the brief filter hides.
const isBriefOnly = useAppState(s => s.isBriefOnly);
const localTools = useMemo(() => getTools(toolPermissionContext), [toolPermissionContext, proactiveActive, isBriefOnly]);
useKickOffCheckAndDisableBypassPermissionsIfNeeded();
useKickOffCheckAndDisableAutoModeIfNeeded();
const [dynamicMcpConfig, setDynamicMcpConfig] = useState<Record<string, ScopedMcpServerConfig> | undefined>(initialDynamicMcpConfig);
const onChangeDynamicMcpConfig = useCallback((config: Record<string, ScopedMcpServerConfig>) => {
setDynamicMcpConfig(config);
}, [setDynamicMcpConfig]);
const [screen, setScreen] = useState<Screen>('prompt');
const [showAllInTranscript, setShowAllInTranscript] = useState(false);
// [ forces the dump-to-scrollback path inside transcript mode. Separate
// from CLAUDE_CODE_NO_FLICKER=0 (which is process-lifetime) β this is
// ephemeral, reset on transcript exit. Diagnostic escape hatch so
// terminal/tmux native cmd-F can search the full flat render.
const [dumpMode, setDumpMode] = useState(false);
// v-for-editor render progress. Inline in the footer β notifications
// render inside PromptInput which isn't mounted in transcript.
const [editorStatus, setEditorStatus] = useState('');
// Incremented on transcript exit. Async v-render captures this at start;
// each status write no-ops if stale (user left transcript mid-render β
// the stable setState would otherwise stamp a ghost toast into the next
// session). Also clears any pending 4s auto-clear.
const editorGenRef = useRef(0);
const editorTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
const editorRenderingRef = useRef(false);
const {
addNotification,
removeNotification
} = useNotifications();
// eslint-disable-next-line prefer-const
let trySuggestBgPRIntercept = SUGGEST_BG_PR_NOOP;
const mcpClients = useMergedClients(initialMcpClients, mcp.clients);
// IDE integration
const [ideSelection, setIDESelection] = useState<IDESelection | undefined>(undefined);
const [ideToInstallExtension, setIDEToInstallExtension] = useState<IdeType | null>(null);
const [ideInstallationStatus, setIDEInstallationStatus] = useState<IDEExtensionInstallationStatus | null>(null);
const [showIdeOnboarding, setShowIdeOnboarding] = useState(false);
// Dead code elimination: model switch callout state (ant-only)
const [showModelSwitchCallout, setShowModelSwitchCallout] = useState(() => {
if ("external" === 'ant') {
return shouldShowAntModelSwitch();
}
return false;
});
const [showEffortCallout, setShowEffortCallout] = useState(() => shouldShowEffortCallout(mainLoopModel));
const showRemoteCallout = useAppState(s => s.showRemoteCallout);
const [showDesktopUpsellStartup, setShowDesktopUpsellStartup] = useState(() => shouldShowDesktopUpsellStartup());
// notifications
useModelMigrationNotifications();
useCanSwitchToExistingSubscription();
useIDEStatusIndicator({
ideSelection,
mcpClients,
ideInstallationStatus
});
useMcpConnectivityStatus({
mcpClients
});
useAutoModeUnavailableNotification();
usePluginInstallationStatus();
usePluginAutoupdateNotification();
useSettingsErrors();
useRateLimitWarningNotification(mainLoopModel);
useFastModeNotification();
useDeprecationWarningNotification(mainLoopModel);
useNpmDeprecationNotification();
useAntOrgWarningNotification();
useInstallMessages();
useChromeExtensionNotification();
useOfficialMarketplaceNotification();
useLspInitializationNotification();
useTeammateLifecycleNotification();
const {
recommendation: lspRecommendation,
handleResponse: handleLspResponse
} = useLspPluginRecommendation();
const {
recommendation: hintRecommendation,
handleResponse: handleHintResponse
} = useClaudeCodeHintRecommendation();
// Memoize the combined initial tools array to prevent reference changes
const combinedInitialTools = useMemo(() => {
return [...localTools, ...initialTools];
}, [localTools, initialTools]);
// Initialize plugin management
useManagePlugins({
enabled: !isRemoteSession
});
const tasksV2 = useTasksV2WithCollapseEffect();
// Start background plugin installations
// SECURITY: This code is guaranteed to run ONLY after the "trust this folder" dialog
// has been confirmed by the user. The trust dialog is shown in cli.tsx (line ~387)
// before the REPL component is rendered. The dialog blocks execution until the user
// accepts, and only then is the REPL component mounted and this effect runs.
// This ensures that plugin installations from repository and user settings only
// happen after explicit user consent to trust the current working directory.
useEffect(() => {
if (isRemoteSession) return;
void performStartupChecks(setAppState);
}, [setAppState, isRemoteSession]);
// Allow Claude in Chrome MCP to send prompts through MCP notifications
// and sync permission mode changes to the Chrome extension
usePromptsFromClaudeInChrome(isRemoteSession ? EMPTY_MCP_CLIENTS : mcpClients, toolPermissionContext.mode);
// Initialize swarm features: teammate hooks and context
// Handles both fresh spawns and resumed teammate sessions
useSwarmInitialization(setAppState, initialMessages, {
enabled: !isRemoteSession
});
const mergedTools = useMergedTools(combinedInitialTools, mcp.tools, toolPermissionContext);
// Apply agent tool restrictions if mainThreadAgentDefinition is set
const {
tools,
allowedAgentTypes
} = useMemo(() => {
if (!mainThreadAgentDefinition) {
return {
tools: mergedTools,
allowedAgentTypes: undefined as string[] | undefined
};
}
const resolved = resolveAgentTools(mainThreadAgentDefinition, mergedTools, false, true);
return {
tools: resolved.resolvedTools,
allowedAgentTypes: resolved.allowedAgentTypes
};
}, [mainThreadAgentDefinition, mergedTools]);
// Merge commands from local state, plugins, and MCP
const commandsWithPlugins = useMergedCommands(localCommands, plugins.commands as Command[]);
const mergedCommands = useMergedCommands(commandsWithPlugins, mcp.commands as Command[]);
// Filter out all commands if disableSlashCommands is true
const commands = useMemo(() => disableSlashCommands ? [] : mergedCommands, [disableSlashCommands, mergedCommands]);
useIdeLogging(isRemoteSession ? EMPTY_MCP_CLIENTS : mcp.clients);
useIdeSelection(isRemoteSession ? EMPTY_MCP_CLIENTS : mcp.clients, setIDESelection);
const [streamMode, setStreamMode] = useState<SpinnerMode>('responding');
// Ref mirror so onSubmit can read the latest value without adding
// streamMode to its deps. streamMode flips between
// requesting/responding/tool-use ~10x per turn during streaming; having it
// in onSubmit's deps was recreating onSubmit on every flip, which
// cascaded into PromptInput prop churn and downstream useCallback/useMemo
// invalidation. The only consumers inside callbacks are debug logging and
// telemetry (handlePromptSubmit.ts), so a stale-by-one-render value is
// harmless β but ref mirrors sync on every render anyway so it's fresh.
const streamModeRef = useRef(streamMode);
streamModeRef.current = streamMode;
const [streamingToolUses, setStreamingToolUses] = useState<StreamingToolUse[]>([]);
const [streamingThinking, setStreamingThinking] = useState<StreamingThinking | null>(null);
// Auto-hide streaming thinking after 30 seconds of being completed
useEffect(() => {
if (streamingThinking && !streamingThinking.isStreaming && streamingThinking.streamingEndedAt) {
const elapsed = Date.now() - streamingThinking.streamingEndedAt;
const remaining = 30000 - elapsed;
if (remaining > 0) {
const timer = setTimeout(setStreamingThinking, remaining, null);
return () => clearTimeout(timer);
} else {
setStreamingThinking(null);
}
}
}, [streamingThinking]);
const [abortController, setAbortController] = useState<AbortController | null>(null);
// Ref that always points to the current abort controller, used by the
// REPL bridge to abort the active query when a remote interrupt arrives.
const abortControllerRef = useRef<AbortController | null>(null);
abortControllerRef.current = abortController;
// Ref for the bridge result callback β set after useReplBridge initializes,
// read in the onQuery finally block to notify mobile clients that a turn ended.
const sendBridgeResultRef = useRef<() => void>(() => {});
// Ref for the synchronous restore callback β set after restoreMessageSync is
// defined, read in the onQuery finally block for auto-restore on interrupt.
const restoreMessageSyncRef = useRef<(m: UserMessage) => void>(() => {});
// Ref to the fullscreen layout's scroll box for keyboard scrolling.
// Null when fullscreen mode is disabled (ref never attached).
const scrollRef = useRef<ScrollBoxHandle>(null);
// Separate ref for the modal slot's inner ScrollBox β passed through
// FullscreenLayout β ModalContext so Tabs can attach it to its own
// ScrollBox for tall content (e.g. /status's MCP-server list). NOT
// keyboard-driven β ScrollKeybindingHandler stays on the outer ref so
// PgUp/PgDn/wheel always scroll the transcript behind the modal.
// Plumbing kept for future modal-scroll wiring.
const modalScrollRef = useRef<ScrollBoxHandle>(null);
// Timestamp of the last user-initiated scroll (wheel, PgUp/PgDn, ctrl+u,
// End/Home, G, drag-to-scroll). Stamped in composedOnScroll β the single
// chokepoint ScrollKeybindingHandler calls for every user scroll action.
// Programmatic scrolls (repinScroll's scrollToBottom, sticky auto-follow)
// do NOT go through composedOnScroll, so they don't stamp this. Ref not
// state: no re-render on every wheel tick.
const lastUserScrollTsRef = useRef(0);
// Synchronous state machine for the query lifecycle. Replaces the
// error-prone dual-state pattern where isLoading (React state, async
// batched) and isQueryRunning (ref, sync) could desync. See QueryGuard.ts.
const queryGuard = React.useRef(new QueryGuard()).current;
// Subscribe to the guard β true during dispatching or running.
// This is the single source of truth for "is a local query in flight".
const isQueryActive = React.useSyncExternalStore(queryGuard.subscribe, queryGuard.getSnapshot);
// Separate loading flag for operations outside the local query guard:
// remote sessions (useRemoteSession / useDirectConnect) and foregrounded
// background tasks (useSessionBackgrounding). These don't route through
// onQuery / queryGuard, so they need their own spinner-visibility state.
// Initialize true if remote mode with initial prompt (CCR processing it).
const [isExternalLoading, setIsExternalLoadingRaw] = React.useState(remoteSessionConfig?.hasInitialPrompt ?? false);
// Derived: any loading source active. Read-only β no setter. Local query
// loading is driven by queryGuard (reserve/tryStart/end/cancelReservation),
// external loading by setIsExternalLoading.
const isLoading = isQueryActive || isExternalLoading;
// Elapsed time is computed by SpinnerWithVerb from these refs on each
// animation frame, avoiding a useInterval that re-renders the entire REPL.
const [userInputOnProcessing, setUserInputOnProcessingRaw] = React.useState<string | undefined>(undefined);
// messagesRef.current.length at the moment userInputOnProcessing was set.
// The placeholder hides once displayedMessages grows past this β i.e. the
// real user message has landed in the visible transcript.
const userInputBaselineRef = React.useRef(0);
// True while the submitted prompt is being processed but its user message
// hasn't reached setMessages yet. setMessages uses this to keep the
// baseline in sync when unrelated async messages (bridge status, hook
// results, scheduled tasks) land during that window.
const userMessagePendingRef = React.useRef(false);
// Wall-clock time tracking refs for accurate elapsed time calculation
const loadingStartTimeRef = React.useRef<number>(0);
const totalPausedMsRef = React.useRef(0);
const pauseStartTimeRef = React.useRef<number | null>(null);
const resetTimingRefs = React.useCallback(() => {
loadingStartTimeRef.current = Date.now();
totalPausedMsRef.current = 0;
pauseStartTimeRef.current = null;
}, []);
// Reset timing refs inline when isQueryActive transitions falseβtrue.
// queryGuard.reserve() (in executeUserInput) fires BEFORE processUserInput's
// first await, but the ref reset in onQuery's try block runs AFTER. During
// that gap, React renders the spinner with loadingStartTimeRef=0, computing
// elapsedTimeMs = Date.now() - 0 β 56 years. This inline reset runs on the
// first render where isQueryActive is observed true β the same render that
// first shows the spinner β so the ref is correct by the time the spinner
// reads it. See INC-4549.
const wasQueryActiveRef = React.useRef(false);
if (isQueryActive && !wasQueryActiveRef.current) {
resetTimingRefs();
}
wasQueryActiveRef.current = isQueryActive;
// Wrapper for setIsExternalLoading that resets timing refs on transition
// to true β SpinnerWithVerb reads these for elapsed time, so they must be
// reset for remote sessions / foregrounded tasks too (not just local
// queries, which reset them in onQuery). Without this, a remote-only
// session would show ~56 years elapsed (Date.now() - 0).
const setIsExternalLoading = React.useCallback((value: boolean) => {
setIsExternalLoadingRaw(value);
if (value) resetTimingRefs();
}, [resetTimingRefs]);
// Start time of the first turn that had swarm teammates running
// Used to compute total elapsed time (including teammate execution) for the deferred message
const swarmStartTimeRef = React.useRef<number | null>(null);
const swarmBudgetInfoRef = React.useRef<{
tokens: number;
limit: number;
nudges: number;
} | undefined>(undefined);
// Ref to track current focusedInputDialog for use in callbacks
// This avoids stale closures when checking dialog state in timer callbacks
const focusedInputDialogRef = React.useRef<ReturnType<typeof getFocusedInputDialog>>(undefined);
// How long after the last keystroke before deferred dialogs are shown
const PROMPT_SUPPRESSION_MS = 1500;
// True when user is actively typing β defers interrupt dialogs so keystrokes
// don't accidentally dismiss or answer a permission prompt the user hasn't read yet.
const [isPromptInputActive, setIsPromptInputActive] = React.useState(false);
const [autoUpdaterResult, setAutoUpdaterResult] = useState<AutoUpdaterResult | null>(null);
useEffect(() => {
if (autoUpdaterResult?.notifications) {
autoUpdaterResult.notifications.forEach(notification => {
addNotification({
key: 'auto-updater-notification',
text: notification,
priority: 'low'
});
});
}
}, [autoUpdaterResult, addNotification]);
// tmux + fullscreen + `mouse off`: one-time hint that wheel won't scroll.
// We no longer mutate tmux's session-scoped mouse option (it poisoned
// sibling panes); tmux users already know this tradeoff from vim/less.
useEffect(() => {
if (isFullscreenEnvEnabled()) {
void maybeGetTmuxMouseHint().then(hint => {
if (hint) {
addNotification({
key: 'tmux-mouse-hint',
text: hint,
priority: 'low'
});
}
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const [showUndercoverCallout, setShowUndercoverCallout] = useState(false);
useEffect(() => {
if ("external" === 'ant') {
void (async () => {
// Wait for repo classification to settle (memoized, no-op if primed).
const {
isInternalModelRepo
} = await import('../utils/commitAttribution.js');
await isInternalModelRepo();
const {
shouldShowUndercoverAutoNotice
} = await import('../utils/undercover.js');
if (shouldShowUndercoverAutoNotice()) {
setShowUndercoverCallout(true);
}
})();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const [toolJSX, setToolJSXInternal] = useState<{
jsx: React.ReactNode | null;
shouldHidePromptInput: boolean;
shouldContinueAnimation?: true;
showSpinner?: boolean;
isLocalJSXCommand?: boolean;
isImmediate?: boolean;
} | null>(null);
// Track local JSX commands separately so tools can't overwrite them.
// This enables "immediate" commands (like /btw) to persist while Claude is processing.
const localJSXCommandRef = useRef<{
jsx: React.ReactNode | null;
shouldHidePromptInput: boolean;
shouldContinueAnimation?: true;
showSpinner?: boolean;
isLocalJSXCommand: true;
} | null>(null);
// Wrapper for setToolJSX that preserves local JSX commands (like /btw).
// When a local JSX command is active, we ignore updates from tools
// unless they explicitly set clearLocalJSX: true (from onDone callbacks).
//
// TO ADD A NEW IMMEDIATE COMMAND:
// 1. Set `immediate: true` in the command definition
// 2. Set `isLocalJSXCommand: true` when calling setToolJSX in the command's JSX
// 3. In the onDone callback, use `setToolJSX({ jsx: null, shouldHidePromptInput: false, clearLocalJSX: true })`
// to explicitly clear the overlay when the user dismisses it
const setToolJSX = useCallback((args: {
jsx: React.ReactNode | null;
shouldHidePromptInput: boolean;
shouldContinueAnimation?: true;
showSpinner?: boolean;
isLocalJSXCommand?: boolean;
clearLocalJSX?: boolean;
} | null) => {
// If setting a local JSX command, store it in the ref
if (args?.isLocalJSXCommand) {
const {
clearLocalJSX: _,
...rest
} = args;
localJSXCommandRef.current = {
...rest,
isLocalJSXCommand: true
};
setToolJSXInternal(rest);
return;
}
// If there's an active local JSX command in the ref
if (localJSXCommandRef.current) {
// Allow clearing only if explicitly requested (from onDone callbacks)
if (args?.clearLocalJSX) {
localJSXCommandRef.current = null;
setToolJSXInternal(null);
return;
}
// Otherwise, keep the local JSX command visible - ignore tool updates
return;
}
// No active local JSX command, allow any update
if (args?.clearLocalJSX) {
setToolJSXInternal(null);
return;
}
setToolJSXInternal(args);
}, []);
const [toolUseConfirmQueue, setToolUseConfirmQueue] = useState<ToolUseConfirm[]>([]);
// Sticky footer JSX registered by permission request components (currently
// only ExitPlanModePermissionRequest). Renders in FullscreenLayout's `bottom`
// slot so response options stay visible while the user scrolls a long plan.
const [permissionStickyFooter, setPermissionStickyFooter] = useState<React.ReactNode | null>(null);
const [sandboxPermissionRequestQueue, setSandboxPermissionRequestQueue] = useState<Array<{
hostPattern: NetworkHostPattern;
resolvePromise: (allowConnection: boolean) => void;
}>>([]);
const [promptQueue, setPromptQueue] = useState<Array<{
request: PromptRequest;
title: string;
toolInputSummary?: string | null;
resolve: (response: PromptResponse) => void;
reject: (error: Error) => void;
}>>([]);
// Track bridge cleanup functions for sandbox permission requests so the
// local dialog handler can cancel the remote prompt when the local user
// responds first. Keyed by host to support concurrent same-host requests.
const sandboxBridgeCleanupRef = useRef<Map<string, Array<() => void>>>(new Map());
// -- Terminal title management
// Session title (set via /rename or restored on resume) wins over
// the agent name, which wins over the Haiku-extracted topic;
// all fall back to the product name.
const terminalTitleFromRename = useAppState(s => s.settings.terminalTitleFromRename) !== false;
const sessionTitle = terminalTitleFromRename ? getCurrentSessionTitle(getSessionId()) : undefined;
const [haikuTitle, setHaikuTitle] = useState<string>();
// Gates the one-shot Haiku call that generates the tab title. Seeded true
// on resume (initialMessages present) so we don't re-title a resumed
// session from mid-conversation context.
const haikuTitleAttemptedRef = useRef((initialMessages?.length ?? 0) > 0);
const agentTitle = mainThreadAgentDefinition?.agentType;
const terminalTitle = sessionTitle ?? agentTitle ?? haikuTitle ?? 'Claude Code';
const isWaitingForApproval = toolUseConfirmQueue.length > 0 || promptQueue.length > 0 || pendingWorkerRequest || pendingSandboxRequest;
// Local-jsx commands (like /plugin, /config) show user-facing dialogs that
// wait for input. Require jsx != null β if the flag is stuck true but jsx
// is null, treat as not-showing so TextInput focus and queue processor
// aren't deadlocked by a phantom overlay.
const isShowingLocalJSXCommand = toolJSX?.isLocalJSXCommand === true && toolJSX?.jsx != null;
const titleIsAnimating = isLoading && !isWaitingForApproval && !isShowingLocalJSXCommand;
// Title animation state lives in <AnimatedTerminalTitle> so the 960ms tick
// doesn't re-render REPL. titleDisabled/terminalTitle are still computed
// here because onQueryImpl reads them (background session description,
// haiku title extraction gate).
// Prevent macOS from sleeping while Claude is working
useEffect(() => {
if (isLoading && !isWaitingForApproval && !isShowingLocalJSXCommand) {
startPreventSleep();
return () => stopPreventSleep();
}
}, [isLoading, isWaitingForApproval, isShowingLocalJSXCommand]);
const sessionStatus: TabStatusKind = isWaitingForApproval || isShowingLocalJSXCommand ? 'waiting' : isLoading ? 'busy' : 'idle';
const waitingFor = sessionStatus !== 'waiting' ? undefined : toolUseConfirmQueue.length > 0 ? `approve ${toolUseConfirmQueue[0]!.tool.name}` : pendingWorkerRequest ? 'worker request' : pendingSandboxRequest ? 'sandbox request' : isShowingLocalJSXCommand ? 'dialog open' : 'input needed';
// Push status to the PID file for `claude ps`. Fire-and-forget; ps falls
// back to transcript-tail derivation when this is missing/stale.
useEffect(() => {
if (feature('BG_SESSIONS')) {
void updateSessionActivity({
status: sessionStatus,
waitingFor
});
}
}, [sessionStatus, waitingFor]);
// 3P default: off β OSC 21337 is ant-only while the spec stabilizes.
// Gated so we can roll back if the sidebar indicator conflicts with
// the title spinner in terminals that render both. When the flag is
// on, the user-facing config setting controls whether it's active.
const tabStatusGateEnabled = getFeatureValue_CACHED_MAY_BE_STALE('tengu_terminal_sidebar', false);
const showStatusInTerminalTab = tabStatusGateEnabled && (getGlobalConfig().showStatusInTerminalTab ?? false);
useTabStatus(titleDisabled || !showStatusInTerminalTab ? null : sessionStatus);
// Register the leader's setToolUseConfirmQueue for in-process teammates
useEffect(() => {
registerLeaderToolUseConfirmQueue(setToolUseConfirmQueue);
return () => unregisterLeaderToolUseConfirmQueue();
}, [setToolUseConfirmQueue]);
const [messages, rawSetMessages] = useState<MessageType[]>(initialMessages ?? []);
const messagesRef = useRef(messages);
// Stores the willowMode variant that was shown (or false if no hint shown).
// Captured at hint_shown time so hint_converted telemetry reports the same
// variant β the GrowthBook value shouldn't change mid-session, but reading
// it once guarantees consistency between the paired events.
const idleHintShownRef = useRef<string | false>(false);
// Wrap setMessages so messagesRef is always current the instant the
// call returns β not when React later processes the batch. Apply the
// updater eagerly against the ref, then hand React the computed value
// (not the function). rawSetMessages batching becomes last-write-wins,
// and the last write is correct because each call composes against the
// already-updated ref. This is the Zustand pattern: ref is source of
// truth, React state is the render projection. Without this, paths
// that queue functional updaters then synchronously read the ref
// (e.g. handleSpeculationAccept β onQuery) see stale data.
const setMessages = useCallback((action: React.SetStateAction<MessageType[]>) => {
const prev = messagesRef.current;
const next = typeof action === 'function' ? action(messagesRef.current) : action;
messagesRef.current = next;
if (next.length < userInputBaselineRef.current) {
// Shrank (compact/rewind/clear) β clamp so placeholderText's length
// check can't go stale.
userInputBaselineRef.current = 0;
} else if (next.length > prev.length && userMessagePendingRef.current) {
// Grew while the submitted user message hasn't landed yet. If the
// added messages don't include it (bridge status, hook results,
// scheduled tasks landing async during processUserInputBase), bump
// baseline so the placeholder stays visible. Once the user message
// lands, stop tracking β later additions (assistant stream) should
// not re-show the placeholder.
const delta = next.length - prev.length;
const added = prev.length === 0 || next[0] === prev[0] ? next.slice(-delta) : next.slice(0, delta);
if (added.some(isHumanTurn)) {
userMessagePendingRef.current = false;
} else {
userInputBaselineRef.current = next.length;
}
}
rawSetMessages(next);
}, []);
// Capture the baseline message count alongside the placeholder text so
// the render can hide it once displayedMessages grows past the baseline.
const setUserInputOnProcessing = useCallback((input: string | undefined) => {
if (input !== undefined) {
userInputBaselineRef.current = messagesRef.current.length;
userMessagePendingRef.current = true;
} else {
userMessagePendingRef.current = false;
}
setUserInputOnProcessingRaw(input);
}, []);
// Fullscreen: track the unseen-divider position. dividerIndex changes
// only ~twice/scroll-session (first scroll-away + repin). pillVisible
// and stickyPrompt now live in FullscreenLayout β they subscribe to
// ScrollBox directly so per-frame scroll never re-renders REPL.
const {
dividerIndex,
dividerYRef,
onScrollAway,
onRepin,
jumpToNew,
shiftDivider
} = useUnseenDivider(messages.length);
if (feature('AWAY_SUMMARY')) {
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
useAwaySummary(messages, setMessages, isLoading);
}
const [cursor, setCursor] = useState<MessageActionsState | null>(null);
const cursorNavRef = useRef<MessageActionsNav | null>(null);
// Memoized so Messages' React.memo holds.
const unseenDivider = useMemo(() => computeUnseenDivider(messages, dividerIndex),
// eslint-disable-next-line react-hooks/exhaustive-deps -- length change covers appends; useUnseenDivider's count-drop guard clears dividerIndex on replace/rewind
[dividerIndex, messages.length]);
// Re-pin scroll to bottom and clear the unseen-messages baseline. Called
// on any user-driven return-to-live action (submit, type-into-empty,
// overlay appear/dismiss).
const repinScroll = useCallback(() => {
scrollRef.current?.scrollToBottom();
onRepin();
setCursor(null);
}, [onRepin, setCursor]);
// Backstop for the submit-handler repin at onSubmit. If a buffered stdin
// event (wheel/drag) races between handler-fire and state-commit, the
// handler's scrollToBottom can be undone. This effect fires on the render
// where the user's message actually lands β tied to React's commit cycle,
// so it can't race with stdin. Keyed on lastMsg identity (not messages.length)
// so useAssistantHistory's prepends don't spuriously repin.
const lastMsg = messages.at(-1);
const lastMsgIsHuman = lastMsg != null && isHumanTurn(lastMsg);
useEffect(() => {
if (lastMsgIsHuman) {
repinScroll();
}
}, [lastMsgIsHuman, lastMsg, repinScroll]);
// Assistant-chat: lazy-load remote history on scroll-up. No-op unless
// KAIROS build + config.viewerOnly. feature() is build-time constant so
// the branch is dead-code-eliminated in non-KAIROS builds (same pattern
// as useUnseenDivider above).
const {
maybeLoadOlder
} = feature('KAIROS') ?
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
useAssistantHistory({
config: remoteSessionConfig,
setMessages,
scrollRef,
onPrepend: shiftDivider
}) : HISTORY_STUB;
// Compose useUnseenDivider's callbacks with the lazy-load trigger.
const composedOnScroll = useCallback((sticky: boolean, handle: ScrollBoxHandle) => {
lastUserScrollTsRef.current = Date.now();
if (sticky) {
onRepin();
} else {
onScrollAway(handle);
if (feature('KAIROS')) maybeLoadOlder(handle);
// Dismiss the companion bubble on scroll β it's absolute-positioned
// at bottom-right and covers transcript content. Scrolling = user is
// trying to read something under it.
if (feature('BUDDY')) {
setAppState(prev => prev.companionReaction === undefined ? prev : {
...prev,
companionReaction: undefined
});
}
}
}, [onRepin, onScrollAway, maybeLoadOlder, setAppState]);
// Deferred SessionStart hook messages β REPL renders immediately and
// hook messages are injected when they resolve. awaitPendingHooks()
// must be called before the first API call so the model sees hook context.
const awaitPendingHooks = useDeferredHookMessages(pendingHookMessages, setMessages);
// Deferred messages for the Messages component β renders at transition
// priority so the reconciler yields every 5ms, keeping input responsive
// while the expensive message processing pipeline runs.
const deferredMessages = useDeferredValue(messages);
const deferredBehind = messages.length - deferredMessages.length;
if (deferredBehind > 0) {
logForDebugging(`[useDeferredValue] Messages deferred by ${deferredBehind} (${deferredMessages.length}β${messages.length})`);
}
// Frozen state for transcript mode - stores lengths instead of cloning arrays for memory efficiency
const [frozenTranscriptState, setFrozenTranscriptState] = useState<{
messagesLength: number;
streamingToolUsesLength: number;
} | null>(null);
// Initialize input with any early input that was captured before REPL was ready.
// Using lazy initialization ensures cursor offset is set correctly in PromptInput.
const [inputValue, setInputValueRaw] = useState(() => consumeEarlyInput());
const inputValueRef = useRef(inputValue);
inputValueRef.current = inputValue;
const insertTextRef = useRef<{
insert: (text: string) => void;
setInputWithCursor: (value: string, cursor: number) => void;
cursorOffset: number;
} | null>(null);
// Wrap setInputValue to co-locate suppression state updates.
// Both setState calls happen in the same synchronous context so React
// batches them into a single render, eliminating the extra render that
// the previous useEffect β setState pattern caused.
const setInputValue = useCallback((value: string) => {
if (trySuggestBgPRIntercept(inputValueRef.current, value)) return;
// In fullscreen mode, typing into an empty prompt re-pins scroll to
// bottom. Only fires on emptyβnon-empty so scrolling up to reference
// something while composing a message doesn't yank the view back on
// every keystroke. Restores the pre-fullscreen muscle memory of
// typing to snap back to the end of the conversation.
// Skipped if the user scrolled within the last 3s β they're actively
// reading, not lost. lastUserScrollTsRef starts at 0 so the first-
// ever keypress (no scroll yet) always repins.
if (inputValueRef.current === '' && value !== '' && Date.now() - lastUserScrollTsRef.current >= RECENT_SCROLL_REPIN_WINDOW_MS) {
repinScroll();
}
// Sync ref immediately (like setMessages) so callers that read
// inputValueRef before React commits β e.g. the auto-restore finally
// block's `=== ''` guard β see the fresh value, not the stale render.
inputValueRef.current = value;
setInputValueRaw(value);
setIsPromptInputActive(value.trim().length > 0);
}, [setIsPromptInputActive, repinScroll, trySuggestBgPRIntercept]);
// Schedule a timeout to stop suppressing dialogs after the user stops typing.
// Only manages the timeout β the immediate activation is handled by setInputValue above.
useEffect(() => {
if (inputValue.trim().length === 0) return;
const timer = setTimeout(setIsPromptInputActive, PROMPT_SUPPRESSION_MS, false);
return () => clearTimeout(timer);
}, [inputValue]);
const [inputMode, setInputMode] = useState<PromptInputMode>('prompt');
const [stashedPrompt, setStashedPrompt] = useState<{
text: string;
cursorOffset: number;
pastedContents: Record<number, PastedContent>;
} | undefined>();
// Callback to filter commands based on CCR's available slash commands
const handleRemoteInit = useCallback((remoteSlashCommands: string[]) => {
const remoteCommandSet = new Set(remoteSlashCommands);
// Keep commands that CCR lists OR that are in the local-safe set
setLocalCommands(prev => prev.filter(cmd => remoteCommandSet.has(cmd.name) || REMOTE_SAFE_COMMANDS.has(cmd)));
}, [setLocalCommands]);
const [inProgressToolUseIDs, setInProgressToolUseIDs] = useState<Set<string>>(new Set());
const hasInterruptibleToolInProgressRef = useRef(false);
// Remote session hook - manages WebSocket connection and message handling for --remote mode
const remoteSession = useRemoteSession({
config: remoteSessionConfig,
setMessages,
setIsLoading: setIsExternalLoading,
onInit: handleRemoteInit,
setToolUseConfirmQueue,
tools: combinedInitialTools,
setStreamingToolUses,
setStreamMode,
setInProgressToolUseIDs
});
// Direct connect hook - manages WebSocket to a claude server for `claude connect` mode
const directConnect = useDirectConnect({
config: directConnectConfig,
setMessages,
setIsLoading: setIsExternalLoading,
setToolUseConfirmQueue,
tools: combinedInitialTools
});
// SSH session hook - manages ssh child process for `claude ssh` mode.
// Same callback shape as useDirectConnect; only the transport under the
// hood differs (ChildProcess stdin/stdout vs WebSocket).
const sshRemote = useSSHSession({
session: sshSession,
setMessages,
setIsLoading: setIsExternalLoading,
setToolUseConfirmQueue,
tools: combinedInitialTools
});
// Use whichever remote mode is active
const activeRemote = sshRemote.isRemoteMode ? sshRemote : directConnect.isRemoteMode ? directConnect : remoteSession;
const [pastedContents, setPastedContents] = useState<Record<number, PastedContent>>({});
const [submitCount, setSubmitCount] = useState(0);
// Ref instead of state to avoid triggering React re-renders on every
// streaming text_delta. The spinner reads this via its animation timer.
const responseLengthRef = useRef(0);
// API performance metrics ref for ant-only spinner display (TTFT/OTPS).
// Accumulates metrics from all API requests in a turn for P50 aggregation.
const apiMetricsRef = useRef<Array<{
ttftMs: number;
firstTokenTime: number;
lastTokenTime: number;
responseLengthBaseline: number;
// Tracks responseLengthRef at the time of the last content addition.
// Updated by both streaming deltas and subagent message content.
// lastTokenTime is also updated at the same time, so the OTPS
// denominator correctly includes subagent processing time.
endResponseLength: number;
}>>([]);
const setResponseLength = useCallback((f: (prev: number) => number) => {
const prev = responseLengthRef.current;
responseLengthRef.current = f(prev);
// When content is added (not a compaction reset), update the latest
// metrics entry so OTPS reflects all content generation activity.
// Updating lastTokenTime here ensures the denominator includes both
// streaming time AND subagent execution time, preventing inflation.
if (responseLengthRef.current > prev) {
const entries = apiMetricsRef.current;
if (entries.length > 0) {
const lastEntry = entries.at(-1)!;
lastEntry.lastTokenTime = Date.now();
lastEntry.endResponseLength = responseLengthRef.current;
}
}
}, []);
// Streaming text display: set state directly per delta (Ink's 16ms render
// throttle batches rapid updates). Cleared on message arrival (messages.ts)
// so displayedMessages switches from deferredMessages to messages atomically.
const [streamingText, setStreamingText] = useState<string | null>(null);
const reducedMotion = useAppState(s => s.settings.prefersReducedMotion) ?? false;
const showStreamingText = !reducedMotion && !hasCursorUpViewportYankBug();
const onStreamingText = useCallback((f: (current: string | null) => string | null) => {
if (!showStreamingText) return;
setStreamingText(f);
}, [showStreamingText]);
// Hide the in-progress source line so text streams line-by-line, not
// char-by-char. lastIndexOf returns -1 when no newline, giving '' β null.
// Guard on showStreamingText so toggling reducedMotion mid-stream
// immediately hides the streaming preview.
const visibleStreamingText = streamingText && showStreamingText ? streamingText.substring(0, streamingText.lastIndexOf('\n') + 1) || null : null;
const [lastQueryCompletionTime, setLastQueryCompletionTime] = useState(0);
const [spinnerMessage, setSpinnerMessage] = useState<string | null>(null);
const [spinnerColor, setSpinnerColor] = useState<keyof Theme | null>(null);
const [spinnerShimmerColor, setSpinnerShimmerColor] = useState<keyof Theme | null>(null);
const [isMessageSelectorVisible, setIsMessageSelectorVisible] = useState(false);
const [messageSelectorPreselect, setMessageSelectorPreselect] = useState<UserMessage | undefined>(undefined);
const [showCostDialog, setShowCostDialog] = useState(false);
const [conversationId, setConversationId] = useState(randomUUID());
// Idle-return dialog: shown when user submits after a long idle gap
const [idleReturnPending, setIdleReturnPending] = useState<{
input: string;
idleMinutes: number;
} | null>(null);
const skipIdleCheckRef = useRef(false);
const lastQueryCompletionTimeRef = useRef(lastQueryCompletionTime);
lastQueryCompletionTimeRef.current = lastQueryCompletionTime;
// Aggregate tool result budget: per-conversation decision tracking.
// When the GrowthBook flag is on, query.ts enforces the budget; when
// off (undefined), enforcement is skipped entirely. Stale entries after
// /clear, rewind, or compact are harmless (tool_use_ids are UUIDs, stale
// keys are never looked up). Memory is bounded by total replacement count
// Γ ~2KB preview over the REPL lifetime β negligible.
//
// Lazy init via useState initializer β useRef(expr) evaluates expr on every
// render (React ignores it after first, but the computation still runs).
// For large resumed sessions, reconstruction does O(messages Γ blocks)
// work; we only want that once.
const [contentReplacementStateRef] = useState(() => ({
current: provisionContentReplacementState(initialMessages, initialContentReplacements)
}));
const [haveShownCostDialog, setHaveShownCostDialog] = useState(getGlobalConfig().hasAcknowledgedCostThreshold);
const [vimMode, setVimMode] = useState<VimMode>('INSERT');
const [showBashesDialog, setShowBashesDialog] = useState<string | boolean>(false);
const [isSearchingHistory, setIsSearchingHistory] = useState(false);
const [isHelpOpen, setIsHelpOpen] = useState(false);
// showBashesDialog is REPL-level so it survives PromptInput unmounting.
// When ultraplan approval fires while the pill dialog is open, PromptInput
// unmounts (focusedInputDialog β 'ultraplan-choice') but this stays true;
// after accepting, PromptInput remounts into an empty "No tasks" dialog
// (the completed ultraplan task has been filtered out). Close it here.
useEffect(() => {
if (ultraplanPendingChoice && showBashesDialog) {
setShowBashesDialog(false);
}
}, [ultraplanPendingChoice, showBashesDialog]);
const isTerminalFocused = useTerminalFocus();
const terminalFocusRef = useRef(isTerminalFocused);
terminalFocusRef.current = isTerminalFocused;
const [theme] = useTheme();
// resetLoadingState runs twice per turn (onQueryImpl tail + onQuery finally).
// Without this guard, both calls pick a tip β two recordShownTip β two
// saveGlobalConfig writes back-to-back. Reset at submit in onSubmit.
const tipPickedThisTurnRef = React.useRef(false);
const pickNewSpinnerTip = useCallback(() => {
if (tipPickedThisTurnRef.current) return;
tipPickedThisTurnRef.current = true;
const newMessages = messagesRef.current.slice(bashToolsProcessedIdx.current);
for (const tool of extractBashToolsFromMessages(newMessages)) {
bashTools.current.add(tool);
}
bashToolsProcessedIdx.current = messagesRef.current.length;
void getTipToShowOnSpinner({
theme,
readFileState: readFileState.current,
bashTools: bashTools.current
}).then(async tip => {
if (tip) {
const content = await tip.content({
theme
});
setAppState(prev => ({
...prev,
spinnerTip: content
}));
recordShownTip(tip);
} else {
setAppState(prev => {
if (prev.spinnerTip === undefined) return prev;
return {
...prev,
spinnerTip: undefined
};
});
}
});
}, [setAppState, theme]);
// Resets UI loading state. Does NOT call onTurnComplete - that should be
// called explicitly only when a query turn actually completes.
const resetLoadingState = useCallback(() => {
// isLoading is now derived from queryGuard β no setter call needed.
// queryGuard.end() (onQuery finally) or cancelReservation() (executeUserInput
// finally) have already transitioned the guard to idle by the time this runs.
// External loading (remote/backgrounding) is reset separately by those hooks.
setIsExternalLoading(false);
setUserInputOnProcessing(undefined);
responseLengthRef.current = 0;
apiMetricsRef.current = [];
setStreamingText(null);
setStreamingToolUses([]);
setSpinnerMessage(null);
setSpinnerColor(null);
setSpinnerShimmerColor(null);
pickNewSpinnerTip();
endInteractionSpan();
// Speculative bash classifier checks are only valid for the current
// turn's commands β clear after each turn to avoid accumulating
// Promise chains for unconsumed checks (denied/aborted paths).
clearSpeculativeChecks();
}, [pickNewSpinnerTip]);
// Session backgrounding β hook is below, after getToolUseContext
const hasRunningTeammates = useMemo(() => getAllInProcessTeammateTasks(tasks).some(t => t.status === 'running'), [tasks]);
// Show deferred turn duration message once all swarm teammates finish
useEffect(() => {
if (!hasRunningTeammates && swarmStartTimeRef.current !== null) {
const totalMs = Date.now() - swarmStartTimeRef.current;
const deferredBudget = swarmBudgetInfoRef.current;
swarmStartTimeRef.current = null;
swarmBudgetInfoRef.current = undefined;
setMessages(prev => [...prev, createTurnDurationMessage(totalMs, deferredBudget,
// Count only what recordTranscript will persist β ephemeral
// progress ticks and non-ant attachments are filtered by
// isLoggableMessage and never reach disk. Using raw prev.length
// would make checkResumeConsistency report false delta<0 for
// every turn that ran a progress-emitting tool.
count(prev, isLoggableMessage))]);
}
}, [hasRunningTeammates, setMessages]);
// Show auto permissions warning when entering auto mode
// (either via Shift+Tab toggle or on startup). Debounced to avoid
// flashing when the user is cycling through modes quickly.
// Only shown 3 times total across sessions.
const safeYoloMessageShownRef = useRef(false);
useEffect(() => {
if (feature('TRANSCRIPT_CLASSIFIER')) {
if (toolPermissionContext.mode !== 'auto') {
safeYoloMessageShownRef.current = false;
return;
}
if (safeYoloMessageShownRef.current) return;
const config = getGlobalConfig();
const count = config.autoPermissionsNotificationCount ?? 0;
if (count >= 3) return;
const timer = setTimeout((ref, setMessages) => {
ref.current = true;
saveGlobalConfig(prev => {
const prevCount = prev.autoPermissionsNotificationCount ?? 0;
if (prevCount >= 3) return prev;
return {
...prev,
autoPermissionsNotificationCount: prevCount + 1
};
});
setMessages(prev => [...prev, createSystemMessage(AUTO_MODE_DESCRIPTION, 'warning')]);
}, 800, safeYoloMessageShownRef, setMessages);
return () => clearTimeout(timer);
}
}, [toolPermissionContext.mode, setMessages]);
// If worktree creation was slow and sparse-checkout isn't configured,
// nudge the user toward settings.worktree.sparsePaths.
const worktreeTipShownRef = useRef(false);
useEffect(() => {
if (worktreeTipShownRef.current) return;
const wt = getCurrentWorktreeSession();
if (!wt?.creationDurationMs || wt.usedSparsePaths) return;
if (wt.creationDurationMs < 15_000) return;
worktreeTipShownRef.current = true;
const secs = Math.round(wt.creationDurationMs / 1000);
setMessages(prev => [...prev, createSystemMessage(`Worktree creation took ${secs}s. For large repos, set \`worktree.sparsePaths\` in .claude/settings.json to check out only the directories you need β e.g. \`{"worktree": {"sparsePaths": ["src", "packages/foo"]}}\`.`, 'info')]);
}, [setMessages]);
// Hide spinner when the only in-progress tool is Sleep
const onlySleepToolActive = useMemo(() => {
const lastAssistant = messages.findLast(m => m.type === 'assistant');
if (lastAssistant?.type !== 'assistant') return false;
const inProgressToolUses = lastAssistant.message.content.filter(b => b.type === 'tool_use' && inProgressToolUseIDs.has(b.id));
return inProgressToolUses.length > 0 && inProgressToolUses.every(b => b.type === 'tool_use' && b.name === SLEEP_TOOL_NAME);
}, [messages, inProgressToolUseIDs]);
const {
onBeforeQuery: mrOnBeforeQuery,
onTurnComplete: mrOnTurnComplete,
render: mrRender
} = useMoreRight({
enabled: moreRightEnabled,
setMessages,
inputValue,
setInputValue,
setToolJSX
});
const showSpinner = (!toolJSX || toolJSX.showSpinner === true) && toolUseConfirmQueue.length === 0 && promptQueue.length === 0 && (
// Show spinner during input processing, API call, while teammates are running,
// or while pending task notifications are queued (prevents spinner bounce between consecutive notifications)
isLoading || userInputOnProcessing || hasRunningTeammates ||
// Keep spinner visible while task notifications are queued for processing.
// Without this, the spinner briefly disappears between consecutive notifications
// (e.g., multiple background agents completing in rapid succession) because
// isLoading goes false momentarily between processing each one.
getCommandQueueLength() > 0) &&
// Hide spinner when waiting for leader to approve permission request
!pendingWorkerRequest && !onlySleepToolActive && (
// Hide spinner when streaming text is visible (the text IS the feedback),
// but keep it when isBriefOnly suppresses the streaming text display
!visibleStreamingText || isBriefOnly);
// Check if any permission or ask question prompt is currently visible
// This is used to prevent the survey from opening while prompts are active
const hasActivePrompt = toolUseConfirmQueue.length > 0 || promptQueue.length > 0 || sandboxPermissionRequestQueue.length > 0 || elicitation.queue.length > 0 || workerSandboxPermissions.queue.length > 0;
const feedbackSurveyOriginal = useFeedbackSurvey(messages, isLoading, submitCount, 'session', hasActivePrompt);
const skillImprovementSurvey = useSkillImprovementSurvey(setMessages);
const showIssueFlagBanner = useIssueFlagBanner(messages, submitCount);
// Wrap feedback survey handler to trigger auto-run /issue
const feedbackSurvey = useMemo(() => ({
...feedbackSurveyOriginal,
handleSelect: (selected: 'dismissed' | 'bad' | 'fine' | 'good') => {
// Reset the ref when a new survey response comes in
didAutoRunIssueRef.current = false;
const showedTranscriptPrompt = feedbackSurveyOriginal.handleSelect(selected);
// Auto-run /issue for "bad" if transcript prompt wasn't shown
if (selected === 'bad' && !showedTranscriptPrompt && shouldAutoRunIssue('feedback_survey_bad')) {
setAutoRunIssueReason('feedback_survey_bad');
didAutoRunIssueRef.current = true;
}
}
}), [feedbackSurveyOriginal]);
// Post-compact survey: shown after compaction if feature gate is enabled
const postCompactSurvey = usePostCompactSurvey(messages, isLoading, hasActivePrompt, {
enabled: !isRemoteSession
});
// Memory survey: shown when the assistant mentions memory and a memory file
// was read this conversation
const memorySurvey = useMemorySurvey(messages, isLoading, hasActivePrompt, {
enabled: !isRemoteSession
});
// Frustration detection: show transcript sharing prompt after detecting frustrated messages
const frustrationDetection = useFrustrationDetection(messages, isLoading, hasActivePrompt, feedbackSurvey.state !== 'closed' || postCompactSurvey.state !== 'closed' || memorySurvey.state !== 'closed');
// Initialize IDE integration
useIDEIntegration({
autoConnectIdeFlag,
ideToInstallExtension,
setDynamicMcpConfig,
setShowIdeOnboarding,
setIDEInstallationState: setIDEInstallationStatus
});
useFileHistorySnapshotInit(initialFileHistorySnapshots, fileHistory, fileHistoryState => setAppState(prev => ({
...prev,
fileHistory: fileHistoryState
})));
const resume = useCallback(async (sessionId: UUID, log: LogOption, entrypoint: ResumeEntrypoint) => {
const resumeStart = performance.now();
try {
// Deserialize messages to properly clean up the conversation
// This filters unresolved tool uses and adds a synthetic assistant message if needed
const messages = deserializeMessages(log.messages);
// Match coordinator/normal mode to the resumed session
if (feature('COORDINATOR_MODE')) {
/* eslint-disable @typescript-eslint/no-require-imports */
const coordinatorModule = require('../coordinator/coordinatorMode.js') as typeof import('../coordinator/coordinatorMode.js');
/* eslint-enable @typescript-eslint/no-require-imports */
const warning = coordinatorModule.matchSessionMode(log.mode);
if (warning) {
// Re-derive agent definitions after mode switch so built-in agents
// reflect the new coordinator/normal mode
/* eslint-disable @typescript-eslint/no-require-imports */
const {
getAgentDefinitionsWithOverrides,
getActiveAgentsFromList
} = require('../tools/AgentTool/loadAgentsDir.js') as typeof import('../tools/AgentTool/loadAgentsDir.js');
/* eslint-enable @typescript-eslint/no-require-imports */
getAgentDefinitionsWithOverrides.cache.clear?.();
const freshAgentDefs = await getAgentDefinitionsWithOverrides(getOriginalCwd());
setAppState(prev => ({
...prev,
agentDefinitions: {
...freshAgentDefs,
allAgents: freshAgentDefs.allAgents,
activeAgents: getActiveAgentsFromList(freshAgentDefs.allAgents)
}
}));
messages.push(createSystemMessage(warning, 'warning'));
}
}
// Fire SessionEnd hooks for the current session before starting the
// resumed one, mirroring the /clear flow in conversation.ts.
const sessionEndTimeoutMs = getSessionEndHookTimeoutMs();
await executeSessionEndHooks('resume', {
getAppState: () => store.getState(),
setAppState,
signal: AbortSignal.timeout(sessionEndTimeoutMs),
timeoutMs: sessionEndTimeoutMs
});
// Process session start hooks for resume
const hookMessages = await processSessionStartHooks('resume', {
sessionId,
agentType: mainThreadAgentDefinition?.agentType,
model: mainLoopModel
});
// Append hook messages to the conversation
messages.push(...hookMessages);
// For forks, generate a new plan slug and copy the plan content so the
// original and forked sessions don't clobber each other's plan files.
// For regular resumes, reuse the original session's plan slug.
if (entrypoint === 'fork') {
void copyPlanForFork(log, asSessionId(sessionId));
} else {
void copyPlanForResume(log, asSessionId(sessionId));
}
// Restore file history and attribution state from the resumed conversation
restoreSessionStateFromLog(log, setAppState);
if (log.fileHistorySnapshots) {
void copyFileHistoryForResume(log);
}
// Restore agent setting from the resumed conversation
// Always reset to the new session's values (or clear if none),
// matching the standaloneAgentContext pattern below
const {
agentDefinition: restoredAgent
} = restoreAgentFromSession(log.agentSetting, initialMainThreadAgentDefinition, agentDefinitions);
setMainThreadAgentDefinition(restoredAgent);
setAppState(prev => ({
...prev,
agent: restoredAgent?.agentType
}));
// Restore standalone agent context from the resumed conversation
// Always reset to the new session's values (or clear if none)
setAppState(prev => ({
...prev,
standaloneAgentContext: computeStandaloneAgentContext(log.agentName, log.agentColor)
}));
void updateSessionName(log.agentName);
// Restore read file state from the message history
restoreReadFileState(messages, log.projectPath ?? getOriginalCwd());
// Clear any active loading state (no queryId since we're not in a query)
resetLoadingState();
setAbortController(null);
setConversationId(sessionId);
// Get target session's costs BEFORE saving current session
// (saveCurrentSessionCosts overwrites the config, so we need to read first)
const targetSessionCosts = getStoredSessionCosts(sessionId);
// Save current session's costs before switching to avoid losing accumulated costs
saveCurrentSessionCosts();
// Reset cost state for clean slate before restoring target session
resetCostState();
// Switch session (id + project dir atomically). fullPath may point to
// a different project (cross-worktree, /branch); null derives from
// current originalCwd.
switchSession(asSessionId(sessionId), log.fullPath ? dirname(log.fullPath) : null);
// Rename asciicast recording to match the resumed session ID
const {
renameRecordingForSession
} = await import('../utils/asciicast.js');
await renameRecordingForSession();
await resetSessionFilePointer();
// Clear then restore session metadata so it's re-appended on exit via
// reAppendSessionMetadata. clearSessionMetadata must be called first:
// restoreSessionMetadata only sets-if-truthy, so without the clear,
// a session without an agent name would inherit the previous session's
// cached name and write it to the wrong transcript on first message.
clearSessionMetadata();
restoreSessionMetadata(log);
// Resumed sessions shouldn't re-title from mid-conversation context
// (same reasoning as the useRef seed), and the previous session's
// Haiku title shouldn't carry over.
haikuTitleAttemptedRef.current = true;
setHaikuTitle(undefined);
// Exit any worktree a prior /resume entered, then cd into the one
// this session was in. Without the exit, resuming from worktree B
// to non-worktree C leaves cwd/currentWorktreeSession stale;
// resuming BβC where C is also a worktree fails entirely
// (getCurrentWorktreeSession guard blocks the switch).
//
// Skipped for /branch: forkLog doesn't carry worktreeSession, so
// this would kick the user out of a worktree they're still working
// in. Same fork skip as processResumedConversation for the adopt β
// fork materializes its own file via recordTranscript on REPL mount.
if (entrypoint !== 'fork') {
exitRestoredWorktree();
restoreWorktreeForResume(log.worktreeSession);
adoptResumedSessionFile();
void restoreRemoteAgentTasks({
abortController: new AbortController(),
getAppState: () => store.getState(),
setAppState
});
} else {
// Fork: same re-persist as /clear (conversation.ts). The clear
// above wiped currentSessionWorktree, forkLog doesn't carry it,
// and the process is still in the same worktree.
const ws = getCurrentWorktreeSession();
if (ws) saveWorktreeState(ws);
}
// Persist the current mode so future resumes know what mode this session was in
if (feature('COORDINATOR_MODE')) {
/* eslint-disable @typescript-eslint/no-require-imports */
const {
saveMode
} = require('../utils/sessionStorage.js');
const {
isCoordinatorMode
} = require('../coordinator/coordinatorMode.js') as typeof import('../coordinator/coordinatorMode.js');
/* eslint-enable @typescript-eslint/no-require-imports */
saveMode(isCoordinatorMode() ? 'coordinator' : 'normal');
}
// Restore target session's costs from the data we read earlier
if (targetSessionCosts) {
setCostStateForRestore(targetSessionCosts);
}
// Reconstruct replacement state for the resumed session. Runs after
// setSessionId so any NEW replacements post-resume write to the
// resumed session's tool-results dir. Gated on ref.current: the
// initial mount already read the feature flag, so we don't re-read
// it here (mid-session flag flips stay unobservable in both
// directions).
//
// Skipped for in-session /branch: the existing ref is already correct
// (branch preserves tool_use_ids), so there's no need to reconstruct.
// createFork() does write content-replacement entries to the forked
// JSONL with the fork's sessionId, so `claude -r {forkId}` also works.
if (contentReplacementStateRef.current && entrypoint !== 'fork') {
contentReplacementStateRef.current = reconstructContentReplacementState(messages, log.contentReplacements ?? []);
}
// Reset messages to the provided initial messages
// Use a callback to ensure we're not dependent on stale state
setMessages(() => messages);
// Clear any active tool JSX
setToolJSX(null);
// Clear input to ensure no residual state
setInputValue('');
logEvent('tengu_session_resumed', {
entrypoint: entrypoint as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
success: true,
resume_duration_ms: Math.round(performance.now() - resumeStart)
});
} catch (error) {
logEvent('tengu_session_resumed', {
entrypoint: entrypoint as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
success: false
});
throw error;
}
}, [resetLoadingState, setAppState]);
// Lazy init: useRef(createX()) would call createX on every render and
// discard the result. LRUCache construction inside FileStateCache is
// expensive (~170ms), so we use useState's lazy initializer to create
// it exactly once, then feed that stable reference into useRef.
const [initialReadFileState] = useState(() => createFileStateCacheWithSizeLimit(READ_FILE_STATE_CACHE_SIZE));
const readFileState = useRef(initialReadFileState);
const bashTools = useRef(new Set<string>());
const bashToolsProcessedIdx = useRef(0);
// Session-scoped skill discovery tracking (feeds was_discovered on
// tengu_skill_tool_invocation). Must persist across getToolUseContext
// rebuilds within a session: turn-0 discovery writes via processUserInput
// before onQuery builds its own context, and discovery on turn N must
// still attribute a SkillTool call on turn N+k. Cleared in clearConversation.
const discoveredSkillNamesRef = useRef(new Set<string>());
// Session-level dedup for nested_memory CLAUDE.md attachments.
// readFileState is a 100-entry LRU; once it evicts a CLAUDE.md path,
// the next discovery cycle re-injects it. Cleared in clearConversation.
const loadedNestedMemoryPathsRef = useRef(new Set<string>());
// Helper to restore read file state from messages (used for resume flows)
// This allows Claude to edit files that were read in previous sessions
const restoreReadFileState = useCallback((messages: MessageType[], cwd: string) => {
const extracted = extractReadFilesFromMessages(messages, cwd, READ_FILE_STATE_CACHE_SIZE);
readFileState.current = mergeFileStateCaches(readFileState.current, extracted);
for (const tool of extractBashToolsFromMessages(messages)) {
bashTools.current.add(tool);
}
}, []);
// Extract read file state from initialMessages on mount
// This handles CLI flag resume (--resume-session) and ResumeConversation screen
// where messages are passed as props rather than through the resume callback
useEffect(() => {
if (initialMessages && initialMessages.length > 0) {
restoreReadFileState(initialMessages, getOriginalCwd());
void restoreRemoteAgentTasks({
abortController: new AbortController(),
getAppState: () => store.getState(),
setAppState
});
}
// Only run on mount - initialMessages shouldn't change during component lifetime
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const {
status: apiKeyStatus,
reverify
} = useApiKeyVerification();
// Auto-run /issue state
const [autoRunIssueReason, setAutoRunIssueReason] = useState<AutoRunIssueReason | null>(null);
// Ref to track if autoRunIssue was triggered this survey cycle,
// so we can suppress the [1] follow-up prompt even after
// autoRunIssueReason is cleared.
const didAutoRunIssueRef = useRef(false);
// State for exit feedback flow
const [exitFlow, setExitFlow] = useState<React.ReactNode>(null);
const [isExiting, setIsExiting] = useState(false);
// Calculate if cost dialog should be shown
const showingCostDialog = !isLoading && showCostDialog;
// Determine which dialog should have focus (if any)
// Permission and interactive dialogs can show even when toolJSX is set,
// as long as shouldContinueAnimation is true. This prevents deadlocks when
// agents set background hints while waiting for user interaction.
function getFocusedInputDialog(): 'message-selector' | 'sandbox-permission' | 'tool-permission' | 'prompt' | 'worker-sandbox-permission' | 'elicitation' | 'cost' | 'idle-return' | 'init-onboarding' | 'ide-onboarding' | 'model-switch' | 'undercover-callout' | 'effort-callout' | 'remote-callout' | 'lsp-recommendation' | 'plugin-hint' | 'desktop-upsell' | 'ultraplan-choice' | 'ultraplan-launch' | undefined {
// Exit states always take precedence
if (isExiting || exitFlow) return undefined;
// High priority dialogs (always show regardless of typing)
if (isMessageSelectorVisible) return 'message-selector';
// Suppress interrupt dialogs while user is actively typing
if (isPromptInputActive) return undefined;
if (sandboxPermissionRequestQueue[0]) return 'sandbox-permission';
// Permission/interactive dialogs (show unless blocked by toolJSX)
const allowDialogsWithAnimation = !toolJSX || toolJSX.shouldContinueAnimation;
if (allowDialogsWithAnimation && toolUseConfirmQueue[0]) return 'tool-permission';
if (allowDialogsWithAnimation && promptQueue[0]) return 'prompt';
// Worker sandbox permission prompts (network access) from swarm workers
if (allowDialogsWithAnimation && workerSandboxPermissions.queue[0]) return 'worker-sandbox-permission';
if (allowDialogsWithAnimation && elicitation.queue[0]) return 'elicitation';
if (allowDialogsWithAnimation && showingCostDialog) return 'cost';
if (allowDialogsWithAnimation && idleReturnPending) return 'idle-return';
if (feature('ULTRAPLAN') && allowDialogsWithAnimation && !isLoading && ultraplanPendingChoice) return 'ultraplan-choice';
if (feature('ULTRAPLAN') && allowDialogsWithAnimation && !isLoading && ultraplanLaunchPending) return 'ultraplan-launch';
// Onboarding dialogs (special conditions)
if (allowDialogsWithAnimation && showIdeOnboarding) return 'ide-onboarding';
// Model switch callout (ant-only, eliminated from external builds)
if ("external" === 'ant' && allowDialogsWithAnimation && showModelSwitchCallout) return 'model-switch';
// Undercover auto-enable explainer (ant-only, eliminated from external builds)
if ("external" === 'ant' && allowDialogsWithAnimation && showUndercoverCallout) return 'undercover-callout';
// Effort callout (shown once for Opus 4.6 users when effort is enabled)
if (allowDialogsWithAnimation && showEffortCallout) return 'effort-callout';
// Remote callout (shown once before first bridge enable)
if (allowDialogsWithAnimation && showRemoteCallout) return 'remote-callout';
// LSP plugin recommendation (lowest priority - non-blocking suggestion)
if (allowDialogsWithAnimation && lspRecommendation) return 'lsp-recommendation';
// Plugin hint from CLI/SDK stderr (same priority band as LSP rec)
if (allowDialogsWithAnimation && hintRecommendation) return 'plugin-hint';
// Desktop app upsell (max 3 launches, lowest priority)
if (allowDialogsWithAnimation && showDesktopUpsellStartup) return 'desktop-upsell';
return undefined;
}
const focusedInputDialog = getFocusedInputDialog();
// True when permission prompts exist but are hidden because the user is typing
const hasSuppressedDialogs = isPromptInputActive && (sandboxPermissionRequestQueue[0] || toolUseConfirmQueue[0] || promptQueue[0] || workerSandboxPermissions.queue[0] || elicitation.queue[0] || showingCostDialog);
// Keep ref in sync so timer callbacks can read the current value
focusedInputDialogRef.current = focusedInputDialog;
// Immediately capture pause/resume when focusedInputDialog changes
// This ensures accurate timing even under high system load, rather than
// relying on the 100ms polling interval to detect state changes
useEffect(() => {
if (!isLoading) return;
const isPaused = focusedInputDialog === 'tool-permission';
const now = Date.now();
if (isPaused && pauseStartTimeRef.current === null) {
// Just entered pause state - record the exact moment
pauseStartTimeRef.current = now;
} else if (!isPaused && pauseStartTimeRef.current !== null) {
// Just exited pause state - accumulate paused time immediately
totalPausedMsRef.current += now - pauseStartTimeRef.current;
pauseStartTimeRef.current = null;
}
}, [focusedInputDialog, isLoading]);
// Re-pin scroll to bottom whenever the permission overlay appears or
// dismisses. Overlay now renders below messages inside the same
// ScrollBox (no remount), so we need an explicit scrollToBottom for:
// - appear: user may have been scrolled up (sticky broken) β the
// dialog is blocking and must be visible
// - dismiss: user may have scrolled up to read context during the
// overlay, and onScroll was suppressed so the pill state is stale
// useLayoutEffect so the re-pin commits before the Ink frame renders β
// no 1-frame flash of the wrong scroll position.
const prevDialogRef = useRef(focusedInputDialog);
useLayoutEffect(() => {
const was = prevDialogRef.current === 'tool-permission';
const now = focusedInputDialog === 'tool-permission';
if (was !== now) repinScroll();
prevDialogRef.current = focusedInputDialog;
}, [focusedInputDialog, repinScroll]);
function onCancel() {
if (focusedInputDialog === 'elicitation') {
// Elicitation dialog handles its own Escape, and closing it shouldn't affect any loading state.
return;
}
logForDebugging(`[onCancel] focusedInputDialog=${focusedInputDialog} streamMode=${streamMode}`);
// Pause proactive mode so the user gets control back.
// It will resume when they submit their next input (see onSubmit).
if (feature('PROACTIVE') || feature('KAIROS')) {
proactiveModule?.pauseProactive();
}
queryGuard.forceEnd();
skipIdleCheckRef.current = false;
// Preserve partially-streamed text so the user can read what was
// generated before pressing Esc. Pushed before resetLoadingState clears
// streamingText, and before query.ts yields the async interrupt marker,
// giving final order [user, partial-assistant, [Request interrupted by user]].
if (streamingText?.trim()) {
setMessages(prev => [...prev, createAssistantMessage({
content: streamingText
})]);
}
resetLoadingState();
// Clear any active token budget so the backstop doesn't fire on
// a stale budget if the query generator hasn't exited yet.
if (feature('TOKEN_BUDGET')) {
snapshotOutputTokensForTurn(null);
}
if (focusedInputDialog === 'tool-permission') {
// Tool use confirm handles the abort signal itself
toolUseConfirmQueue[0]?.onAbort();
setToolUseConfirmQueue([]);
} else if (focusedInputDialog === 'prompt') {
// Reject all pending prompts and clear the queue
for (const item of promptQueue) {
item.reject(new Error('Prompt cancelled by user'));
}
setPromptQueue([]);
abortController?.abort('user-cancel');
} else if (activeRemote.isRemoteMode) {
// Remote mode: send interrupt signal to CCR
activeRemote.cancelRequest();
} else {
abortController?.abort('user-cancel');
}
// Clear the controller so subsequent Escape presses don't see a stale
// aborted signal. Without this, canCancelRunningTask is false (signal
// defined but .aborted === true), so isActive becomes false if no other
// activating conditions hold β leaving the Escape keybinding inactive.
setAbortController(null);
// forceEnd() skips the finally path β fire directly (aborted=true).
void mrOnTurnComplete(messagesRef.current, true);
}
// Function to handle queued command when canceling a permission request
const handleQueuedCommandOnCancel = useCallback(() => {
const result = popAllEditable(inputValue, 0);
if (!result) return;
setInputValue(result.text);
setInputMode('prompt');
// 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;
});
}
}, [setInputValue, setInputMode, inputValue, setPastedContents]);
// CancelRequestHandler props - rendered inside KeybindingSetup
const cancelRequestProps = {
setToolUseConfirmQueue,
onCancel,
onAgentsKilled: () => setMessages(prev => [...prev, createAgentsKilledMessage()]),
isMessageSelectorVisible: isMessageSelectorVisible || !!showBashesDialog,
screen,
abortSignal: abortController?.signal,
popCommandFromQueue: handleQueuedCommandOnCancel,
vimMode,
isLocalJSXCommand: toolJSX?.isLocalJSXCommand,
isSearchingHistory,
isHelpOpen,
inputMode,
inputValue,
streamMode
};
useEffect(() => {
const totalCost = getTotalCost();
if (totalCost >= 5 /* $5 */ && !showCostDialog && !haveShownCostDialog) {
logEvent('tengu_cost_threshold_reached', {});
// Mark as shown even if the dialog won't render (no console billing
// access). Otherwise this effect re-fires on every message change for
// the rest of the session β 200k+ spurious events observed.
setHaveShownCostDialog(true);
if (hasConsoleBillingAccess()) {
setShowCostDialog(true);
}
}
}, [messages, showCostDialog, haveShownCostDialog]);
const sandboxAskCallback: SandboxAskCallback = useCallback(async (hostPattern: NetworkHostPattern) => {
// If running as a swarm worker, forward the request to the leader via mailbox
if (isAgentSwarmsEnabled() && isSwarmWorker()) {
const requestId = generateSandboxRequestId();
// Send the request to the leader via mailbox
const sent = await sendSandboxPermissionRequestViaMailbox(hostPattern.host, requestId);
return new Promise(resolveShouldAllowHost => {
if (!sent) {
// If we couldn't send via mailbox, fall back to local handling
setSandboxPermissionRequestQueue(prev => [...prev, {
hostPattern,
resolvePromise: resolveShouldAllowHost
}]);
return;
}
// Register the callback for when the leader responds
registerSandboxPermissionCallback({
requestId,
host: hostPattern.host,
resolve: resolveShouldAllowHost
});
// Update AppState to show pending indicator
setAppState(prev => ({
...prev,
pendingSandboxRequest: {
requestId,
host: hostPattern.host
}
}));
});
}
// Normal flow for non-workers: show local UI and optionally race
// against the REPL bridge (Remote Control) if connected.
return new Promise(resolveShouldAllowHost => {
let resolved = false;
function resolveOnce(allow: boolean): void {
if (resolved) return;
resolved = true;
resolveShouldAllowHost(allow);
}
// Queue the local sandbox permission dialog
setSandboxPermissionRequestQueue(prev => [...prev, {
hostPattern,
resolvePromise: resolveOnce
}]);
// When the REPL bridge is connected, also forward the sandbox
// permission request as a can_use_tool control_request so the
// remote user (e.g. on claude.ai) can approve it too.
if (feature('BRIDGE_MODE')) {
const bridgeCallbacks = store.getState().replBridgePermissionCallbacks;
if (bridgeCallbacks) {
const bridgeRequestId = randomUUID();
bridgeCallbacks.sendRequest(bridgeRequestId, SANDBOX_NETWORK_ACCESS_TOOL_NAME, {
host: hostPattern.host
}, randomUUID(), `Allow network connection to ${hostPattern.host}?`);
const unsubscribe = bridgeCallbacks.onResponse(bridgeRequestId, response => {
unsubscribe();
const allow = response.behavior === 'allow';
// Resolve ALL pending requests for the same host, not just
// this one β mirrors the local dialog handler pattern.
setSandboxPermissionRequestQueue(queue => {
queue.filter(item => item.hostPattern.host === hostPattern.host).forEach(item => item.resolvePromise(allow));
return queue.filter(item => item.hostPattern.host !== hostPattern.host);
});
// Clean up all sibling bridge subscriptions for this host
// (other concurrent same-host requests) before deleting.
const siblingCleanups = sandboxBridgeCleanupRef.current.get(hostPattern.host);
if (siblingCleanups) {
for (const fn of siblingCleanups) {
fn();
}
sandboxBridgeCleanupRef.current.delete(hostPattern.host);
}
});
// Register cleanup so the local dialog handler can cancel
// the remote prompt and unsubscribe when the local user
// responds first.
const cleanup = () => {
unsubscribe();
bridgeCallbacks.cancelRequest(bridgeRequestId);
};
const existing = sandboxBridgeCleanupRef.current.get(hostPattern.host) ?? [];
existing.push(cleanup);
sandboxBridgeCleanupRef.current.set(hostPattern.host, existing);
}
}
});
}, [setAppState, store]);
// #34044: if user explicitly set sandbox.enabled=true but deps are missing,
// isSandboxingEnabled() returns false silently. Surface the reason once at
// mount so users know their security config isn't being enforced. Full
// reason goes to debug log; notification points to /sandbox for details.
// addNotification is stable (useCallback) so the effect fires once.
useEffect(() => {
const reason = SandboxManager.getSandboxUnavailableReason();
if (!reason) return;
if (SandboxManager.isSandboxRequired()) {
process.stderr.write(`\nError: sandbox required but unavailable: ${reason}\n` + ` sandbox.failIfUnavailable is set β refusing to start without a working sandbox.\n\n`);
gracefulShutdownSync(1, 'other');
return;
}
logForDebugging(`sandbox disabled: ${reason}`, {
level: 'warn'
});
addNotification({
key: 'sandbox-unavailable',
jsx: <>
<Text color="warning">sandbox disabled</Text>
<Text dimColor> Β· /sandbox</Text>
</>,
priority: 'medium'
});
}, [addNotification]);
if (SandboxManager.isSandboxingEnabled()) {
// If sandboxing is enabled (setting.sandbox is defined, initialise the manager)
SandboxManager.initialize(sandboxAskCallback).catch(err => {
// Initialization/validation failed - display error and exit
process.stderr.write(`\nβ Sandbox Error: ${errorMessage(err)}\n`);
gracefulShutdownSync(1, 'other');
});
}
const setToolPermissionContext = useCallback((context: ToolPermissionContext, options?: {
preserveMode?: boolean;
}) => {
setAppState(prev => ({
...prev,
toolPermissionContext: {
...context,
// Preserve the coordinator's mode only when explicitly requested.
// Workers' getAppState() returns a transformed context with mode
// 'acceptEdits' that must not leak into the coordinator's actual
// state via permission-rule updates β those call sites pass
// { preserveMode: true }. User-initiated mode changes (e.g.,
// selecting "allow all edits") must NOT be overridden.
mode: options?.preserveMode ? prev.toolPermissionContext.mode : context.mode
}
}));
// When permission context changes, recheck all queued items
// This handles the case where approving item1 with "don't ask again"
// should auto-approve other queued items that now match the updated rules
setImmediate(setToolUseConfirmQueue => {
// Use setToolUseConfirmQueue callback to get current queue state
// instead of capturing it in the closure, to avoid stale closure issues
setToolUseConfirmQueue(currentQueue => {
currentQueue.forEach(item => {
void item.recheckPermission();
});
return currentQueue;
});
}, setToolUseConfirmQueue);
}, [setAppState, setToolUseConfirmQueue]);
// Register the leader's setToolPermissionContext for in-process teammates
useEffect(() => {
registerLeaderSetToolPermissionContext(setToolPermissionContext);
return () => unregisterLeaderSetToolPermissionContext();
}, [setToolPermissionContext]);
const canUseTool = useCanUseTool(setToolUseConfirmQueue, setToolPermissionContext);
const requestPrompt = useCallback((title: string, toolInputSummary?: string | null) => (request: PromptRequest): Promise<PromptResponse> => new Promise<PromptResponse>((resolve, reject) => {
setPromptQueue(prev => [...prev, {
request,
title,
toolInputSummary,
resolve,
reject
}]);
}), []);
const getToolUseContext = useCallback((messages: MessageType[], newMessages: MessageType[], abortController: AbortController, mainLoopModel: string): ProcessUserInputContext => {
// Read mutable values fresh from the store rather than closure-capturing
// useAppState() snapshots. Same values today (closure is refreshed by the
// render between turns); decouples freshness from React's render cycle for
// a future headless conversation loop. Same pattern refreshTools() uses.
const s = store.getState();
// Compute tools fresh from store.getState() rather than the closure-
// captured `tools`. useManageMCPConnections populates appState.mcp
// async as servers connect β the store may have newer MCP state than
// the closure captured at render time. Also doubles as refreshTools()
// for mid-query tool list updates.
const computeTools = () => {
const state = store.getState();
const assembled = assembleToolPool(state.toolPermissionContext, state.mcp.tools);
const merged = mergeAndFilterTools(combinedInitialTools, assembled, state.toolPermissionContext.mode);
if (!mainThreadAgentDefinition) return merged;
return resolveAgentTools(mainThreadAgentDefinition, merged, false, true).resolvedTools;
};
return {
abortController,
options: {
commands,
tools: computeTools(),
debug,
verbose: s.verbose,
mainLoopModel,
thinkingConfig: s.thinkingEnabled !== false ? thinkingConfig : {
type: 'disabled'
},
// Merge fresh from store rather than closing over useMergedClients'
// memoized output. initialMcpClients is a prop (session-constant).
mcpClients: mergeClients(initialMcpClients, s.mcp.clients),
mcpResources: s.mcp.resources,
ideInstallationStatus: ideInstallationStatus,
isNonInteractiveSession: false,
dynamicMcpConfig,
theme,
agentDefinitions: allowedAgentTypes ? {
...s.agentDefinitions,
allowedAgentTypes
} : s.agentDefinitions,
customSystemPrompt,
appendSystemPrompt,
refreshTools: computeTools
},
getAppState: () => store.getState(),
setAppState,
messages,
setMessages,
updateFileHistoryState(updater: (prev: FileHistoryState) => FileHistoryState) {
// Perf: skip the setState when the updater returns the same reference
// (e.g. fileHistoryTrackEdit returns `state` when the file is already
// tracked). Otherwise every no-op call would notify all store listeners.
setAppState(prev => {
const updated = updater(prev.fileHistory);
if (updated === prev.fileHistory) return prev;
return {
...prev,
fileHistory: updated
};
});
},
updateAttributionState(updater: (prev: AttributionState) => AttributionState) {
setAppState(prev => {
const updated = updater(prev.attribution);
if (updated === prev.attribution) return prev;
return {
...prev,
attribution: updated
};
});
},
openMessageSelector: () => {
if (!disabled) {
setIsMessageSelectorVisible(true);
}
},
onChangeAPIKey: reverify,
readFileState: readFileState.current,
setToolJSX,
addNotification,
appendSystemMessage: msg => setMessages(prev => [...prev, msg]),
sendOSNotification: opts => {
void sendNotification(opts, terminal);
},
onChangeDynamicMcpConfig,
onInstallIDEExtension: setIDEToInstallExtension,
nestedMemoryAttachmentTriggers: new Set<string>(),
loadedNestedMemoryPaths: loadedNestedMemoryPathsRef.current,
dynamicSkillDirTriggers: new Set<string>(),
discoveredSkillNames: discoveredSkillNamesRef.current,
setResponseLength,
pushApiMetricsEntry: "external" === 'ant' ? (ttftMs: number) => {
const now = Date.now();
const baseline = responseLengthRef.current;
apiMetricsRef.current.push({
ttftMs,
firstTokenTime: now,
lastTokenTime: now,
responseLengthBaseline: baseline,
endResponseLength: baseline
});
} : undefined,
setStreamMode,
onCompactProgress: event => {
switch (event.type) {
case 'hooks_start':
setSpinnerColor('claudeBlue_FOR_SYSTEM_SPINNER');
setSpinnerShimmerColor('claudeBlueShimmer_FOR_SYSTEM_SPINNER');
setSpinnerMessage(event.hookType === 'pre_compact' ? 'Running PreCompact hooks\u2026' : event.hookType === 'post_compact' ? 'Running PostCompact hooks\u2026' : 'Running SessionStart hooks\u2026');
break;
case 'compact_start':
setSpinnerMessage('Compacting conversation');
break;
case 'compact_end':
setSpinnerMessage(null);
setSpinnerColor(null);
setSpinnerShimmerColor(null);
break;
}
},
setInProgressToolUseIDs,
setHasInterruptibleToolInProgress: (v: boolean) => {
hasInterruptibleToolInProgressRef.current = v;
},
resume,
setConversationId,
requestPrompt: feature('HOOK_PROMPTS') ? requestPrompt : undefined,
contentReplacementState: contentReplacementStateRef.current
};
}, [commands, combinedInitialTools, mainThreadAgentDefinition, debug, initialMcpClients, ideInstallationStatus, dynamicMcpConfig, theme, allowedAgentTypes, store, setAppState, reverify, addNotification, setMessages, onChangeDynamicMcpConfig, resume, requestPrompt, disabled, customSystemPrompt, appendSystemPrompt, setConversationId]);
// Session backgrounding (Ctrl+B to background/foreground)
const handleBackgroundQuery = useCallback(() => {
// Stop the foreground query so the background one takes over
abortController?.abort('background');
// Aborting subagents may produce task-completed notifications.
// Clear task notifications so the queue processor doesn't immediately
// start a new foreground query; forward them to the background session.
const removedNotifications = removeByFilter(cmd => cmd.mode === 'task-notification');
void (async () => {
const toolUseContext = getToolUseContext(messagesRef.current, [], new AbortController(), mainLoopModel);
const [defaultSystemPrompt, userContext, systemContext] = await Promise.all([getSystemPrompt(toolUseContext.options.tools, mainLoopModel, Array.from(toolPermissionContext.additionalWorkingDirectories.keys()), toolUseContext.options.mcpClients), getUserContext(), getSystemContext()]);
const systemPrompt = buildEffectiveSystemPrompt({
mainThreadAgentDefinition,
toolUseContext,
customSystemPrompt,
defaultSystemPrompt,
appendSystemPrompt
});
toolUseContext.renderedSystemPrompt = systemPrompt;
const notificationAttachments = await getQueuedCommandAttachments(removedNotifications).catch(() => []);
const notificationMessages = notificationAttachments.map(createAttachmentMessage);
// Deduplicate: if the query loop already yielded a notification into
// messagesRef before we removed it from the queue, skip duplicates.
// We use prompt text for dedup because source_uuid is not set on
// task-notification QueuedCommands (enqueuePendingNotification callers
// don't pass uuid), so it would always be undefined.
const existingPrompts = new Set<string>();
for (const m of messagesRef.current) {
if (m.type === 'attachment' && m.attachment.type === 'queued_command' && m.attachment.commandMode === 'task-notification' && typeof m.attachment.prompt === 'string') {
existingPrompts.add(m.attachment.prompt);
}
}
const uniqueNotifications = notificationMessages.filter(m => m.attachment.type === 'queued_command' && (typeof m.attachment.prompt !== 'string' || !existingPrompts.has(m.attachment.prompt)));
startBackgroundSession({
messages: [...messagesRef.current, ...uniqueNotifications],
queryParams: {
systemPrompt,
userContext,
systemContext,
canUseTool,
toolUseContext,
querySource: getQuerySourceForREPL()
},
description: terminalTitle,
setAppState,
agentDefinition: mainThreadAgentDefinition
});
})();
}, [abortController, mainLoopModel, toolPermissionContext, mainThreadAgentDefinition, getToolUseContext, customSystemPrompt, appendSystemPrompt, canUseTool, setAppState]);
const {
handleBackgroundSession
} = useSessionBackgrounding({
setMessages,
setIsLoading: setIsExternalLoading,
resetLoadingState,
setAbortController,
onBackgroundQuery: handleBackgroundQuery
});
const onQueryEvent = useCallback((event: Parameters<typeof handleMessageFromStream>[0]) => {
handleMessageFromStream(event, newMessage => {
if (isCompactBoundaryMessage(newMessage)) {
// Fullscreen: keep pre-compact messages for scrollback. query.ts
// slices at the boundary for API calls, Messages.tsx skips the
// boundary filter in fullscreen, and useLogMessages treats this
// as an incremental append (first uuid unchanged). Cap at one
// compact-interval of scrollback β normalizeMessages/applyGrouping
// are O(n) per render, so drop everything before the previous
// boundary to keep n bounded across multi-day sessions.
if (isFullscreenEnvEnabled()) {
setMessages(old => [...getMessagesAfterCompactBoundary(old, {
includeSnipped: true
}), newMessage]);
} else {
setMessages(() => [newMessage]);
}
// Bump conversationId so Messages.tsx row keys change and
// stale memoized rows remount with post-compact content.
setConversationId(randomUUID());
// Compaction succeeded β clear the context-blocked flag so ticks resume
if (feature('PROACTIVE') || feature('KAIROS')) {
proactiveModule?.setContextBlocked(false);
}
} else if (newMessage.type === 'progress' && isEphemeralToolProgress(newMessage.data.type)) {
// Replace the previous ephemeral progress tick for the same tool
// call instead of appending. Sleep/Bash emit a tick per second and
// only the last one is rendered; appending blows up the messages
// array (13k+ observed) and the transcript (120MB of sleep_progress
// lines). useLogMessages tracks length, so same-length replacement
// also skips the transcript write.
// agent_progress / hook_progress / skill_progress are NOT ephemeral
// β each carries distinct state the UI needs (e.g. subagent tool
// history). Replacing those leaves the AgentTool UI stuck at
// "Initializingβ¦" because it renders the full progress trail.
setMessages(oldMessages => {
const last = oldMessages.at(-1);
if (last?.type === 'progress' && last.parentToolUseID === newMessage.parentToolUseID && last.data.type === newMessage.data.type) {
const copy = oldMessages.slice();
copy[copy.length - 1] = newMessage;
return copy;
}
return [...oldMessages, newMessage];
});
} else {
setMessages(oldMessages => [...oldMessages, newMessage]);
}
// Block ticks on API errors to prevent tick β error β tick
// runaway loops (e.g., auth failure, rate limit, blocking limit).
// Cleared on compact boundary (above) or successful response (below).
if (feature('PROACTIVE') || feature('KAIROS')) {
if (newMessage.type === 'assistant' && 'isApiErrorMessage' in newMessage && newMessage.isApiErrorMessage) {
proactiveModule?.setContextBlocked(true);
} else if (newMessage.type === 'assistant') {
proactiveModule?.setContextBlocked(false);
}
}
}, newContent => {
// setResponseLength handles updating both responseLengthRef (for
// spinner animation) and apiMetricsRef (endResponseLength/lastTokenTime
// for OTPS). No separate metrics update needed here.
setResponseLength(length => length + newContent.length);
}, setStreamMode, setStreamingToolUses, tombstonedMessage => {
setMessages(oldMessages => oldMessages.filter(m => m !== tombstonedMessage));
void removeTranscriptMessage(tombstonedMessage.uuid);
}, setStreamingThinking, metrics => {
const now = Date.now();
const baseline = responseLengthRef.current;
apiMetricsRef.current.push({
...metrics,
firstTokenTime: now,
lastTokenTime: now,
responseLengthBaseline: baseline,
endResponseLength: baseline
});
}, onStreamingText);
}, [setMessages, setResponseLength, setStreamMode, setStreamingToolUses, setStreamingThinking, onStreamingText]);
const onQueryImpl = useCallback(async (messagesIncludingNewMessages: MessageType[], newMessages: MessageType[], abortController: AbortController, shouldQuery: boolean, additionalAllowedTools: string[], mainLoopModelParam: string, effort?: EffortValue) => {
// Prepare IDE integration for new prompt. Read mcpClients fresh from
// store β useManageMCPConnections may have populated it since the
// render that captured this closure (same pattern as computeTools).
if (shouldQuery) {
const freshClients = mergeClients(initialMcpClients, store.getState().mcp.clients);
void diagnosticTracker.handleQueryStart(freshClients);
const ideClient = getConnectedIdeClient(freshClients);
if (ideClient) {
void closeOpenDiffs(ideClient);
}
}
// Mark onboarding as complete when any user message is sent to Claude
void maybeMarkProjectOnboardingComplete();
// Extract a session title from the first real user message. One-shot
// via ref (was tengu_birch_mist experiment: first-message-only to save
// Haiku calls). The ref replaces the old `messages.length <= 1` check,
// which was broken by SessionStart hook messages (prepended via
// useDeferredHookMessages) and attachment messages (appended by
// processTextPrompt) β both pushed length past 1 on turn one, so the
// title silently fell through to the "Claude Code" default.
if (!titleDisabled && !sessionTitle && !agentTitle && !haikuTitleAttemptedRef.current) {
const firstUserMessage = newMessages.find(m => m.type === 'user' && !m.isMeta);
const text = firstUserMessage?.type === 'user' ? getContentText(firstUserMessage.message.content) : null;
// Skip synthetic breadcrumbs β slash-command output, prompt-skill
// expansions (/commit β <command-message>), local-command headers
// (/help β <command-name>), and bash-mode (!cmd β <bash-input>).
// None of these are the user's topic; wait for real prose.
if (text && !text.startsWith(`<${LOCAL_COMMAND_STDOUT_TAG}>`) && !text.startsWith(`<${COMMAND_MESSAGE_TAG}>`) && !text.startsWith(`<${COMMAND_NAME_TAG}>`) && !text.startsWith(`<${BASH_INPUT_TAG}>`)) {
haikuTitleAttemptedRef.current = true;
void generateSessionTitle(text, new AbortController().signal).then(title => {
if (title) setHaikuTitle(title);else haikuTitleAttemptedRef.current = false;
}, () => {
haikuTitleAttemptedRef.current = false;
});
}
}
// Apply slash-command-scoped allowedTools (from skill frontmatter) to the
// store once per turn. This also covers the reset: the next non-skill turn
// passes [] and clears it. Must run before the !shouldQuery gate: forked
// commands (executeForkedSlashCommand) return shouldQuery=false, and
// createGetAppStateWithAllowedTools in forkedAgent.ts reads this field, so
// stale skill tools would otherwise leak into forked agent permissions.
// Previously this write was hidden inside getToolUseContext's getAppState
// (~85 calls/turn); hoisting it here makes getAppState a pure read and stops
// ephemeral contexts (permission dialog, BackgroundTasksDialog) from
// accidentally clearing it mid-turn.
store.setState(prev => {
const cur = prev.toolPermissionContext.alwaysAllowRules.command;
if (cur === additionalAllowedTools || cur?.length === additionalAllowedTools.length && cur.every((v, i) => v === additionalAllowedTools[i])) {
return prev;
}
return {
...prev,
toolPermissionContext: {
...prev.toolPermissionContext,
alwaysAllowRules: {
...prev.toolPermissionContext.alwaysAllowRules,
command: additionalAllowedTools
}
}
};
});
// The last message is an assistant message if the user input was a bash command,
// or if the user input was an invalid slash command.
if (!shouldQuery) {
// Manual /compact sets messages directly (shouldQuery=false) bypassing
// handleMessageFromStream. Clear context-blocked if a compact boundary
// is present so proactive ticks resume after compaction.
if (newMessages.some(isCompactBoundaryMessage)) {
// Bump conversationId so Messages.tsx row keys change and
// stale memoized rows remount with post-compact content.
setConversationId(randomUUID());
if (feature('PROACTIVE') || feature('KAIROS')) {
proactiveModule?.setContextBlocked(false);
}
}
resetLoadingState();
setAbortController(null);
return;
}
const toolUseContext = getToolUseContext(messagesIncludingNewMessages, newMessages, abortController, mainLoopModelParam);
// getToolUseContext reads tools/mcpClients fresh from store.getState()
// (via computeTools/mergeClients). Use those rather than the closure-
// captured `tools`/`mcpClients` β useManageMCPConnections may have
// flushed new MCP state between the render that captured this closure
// and now. Turn 1 via processInitialMessage is the main beneficiary.
const {
tools: freshTools,
mcpClients: freshMcpClients
} = toolUseContext.options;
// Scope the skill's effort override to this turn's context only β
// wrapping getAppState keeps the override out of the global store so
// background agents and UI subscribers (Spinner, LogoV2) never see it.
if (effort !== undefined) {
const previousGetAppState = toolUseContext.getAppState;
toolUseContext.getAppState = () => ({
...previousGetAppState(),
effortValue: effort
});
}
queryCheckpoint('query_context_loading_start');
const [,, defaultSystemPrompt, baseUserContext, systemContext] = await Promise.all([
// IMPORTANT: do this after setMessages() above, to avoid UI jank
checkAndDisableBypassPermissionsIfNeeded(toolPermissionContext, setAppState),
// Gated on TRANSCRIPT_CLASSIFIER so GrowthBook kill switch runs wherever auto mode is built in
feature('TRANSCRIPT_CLASSIFIER') ? checkAndDisableAutoModeIfNeeded(toolPermissionContext, setAppState, store.getState().fastMode) : undefined, getSystemPrompt(freshTools, mainLoopModelParam, Array.from(toolPermissionContext.additionalWorkingDirectories.keys()), freshMcpClients), getUserContext(), getSystemContext()]);
const userContext = {
...baseUserContext,
...getCoordinatorUserContext(freshMcpClients, isScratchpadEnabled() ? getScratchpadDir() : undefined),
...((feature('PROACTIVE') || feature('KAIROS')) && proactiveModule?.isProactiveActive() && !terminalFocusRef.current ? {
terminalFocus: 'The terminal is unfocused \u2014 the user is not actively watching.'
} : {})
};
queryCheckpoint('query_context_loading_end');
const systemPrompt = buildEffectiveSystemPrompt({
mainThreadAgentDefinition,
toolUseContext,
customSystemPrompt,
defaultSystemPrompt,
appendSystemPrompt
});
toolUseContext.renderedSystemPrompt = systemPrompt;
queryCheckpoint('query_query_start');
resetTurnHookDuration();
resetTurnToolDuration();
resetTurnClassifierDuration();
for await (const event of query({
messages: messagesIncludingNewMessages,
systemPrompt,
userContext,
systemContext,
canUseTool,
toolUseContext,
querySource: getQuerySourceForREPL()
})) {
onQueryEvent(event);
}
if (feature('BUDDY')) {
void fireCompanionObserver(messagesRef.current, reaction => setAppState(prev => prev.companionReaction === reaction ? prev : {
...prev,
companionReaction: reaction
}));
}
queryCheckpoint('query_end');
// Capture ant-only API metrics before resetLoadingState clears the ref.
// For multi-request turns (tool use loops), compute P50 across all requests.
if ("external" === 'ant' && apiMetricsRef.current.length > 0) {
const entries = apiMetricsRef.current;
const ttfts = entries.map(e => e.ttftMs);
// Compute per-request OTPS using only active streaming time and
// streaming-only content. endResponseLength tracks content added by
// streaming deltas only, excluding subagent/compaction inflation.
const otpsValues = entries.map(e => {
const delta = Math.round((e.endResponseLength - e.responseLengthBaseline) / 4);
const samplingMs = e.lastTokenTime - e.firstTokenTime;
return samplingMs > 0 ? Math.round(delta / (samplingMs / 1000)) : 0;
});
const isMultiRequest = entries.length > 1;
const hookMs = getTurnHookDurationMs();
const hookCount = getTurnHookCount();
const toolMs = getTurnToolDurationMs();
const toolCount = getTurnToolCount();
const classifierMs = getTurnClassifierDurationMs();
const classifierCount = getTurnClassifierCount();
const turnMs = Date.now() - loadingStartTimeRef.current;
setMessages(prev => [...prev, createApiMetricsMessage({
ttftMs: isMultiRequest ? median(ttfts) : ttfts[0]!,
otps: isMultiRequest ? median(otpsValues) : otpsValues[0]!,
isP50: isMultiRequest,
hookDurationMs: hookMs > 0 ? hookMs : undefined,
hookCount: hookCount > 0 ? hookCount : undefined,
turnDurationMs: turnMs > 0 ? turnMs : undefined,
toolDurationMs: toolMs > 0 ? toolMs : undefined,
toolCount: toolCount > 0 ? toolCount : undefined,
classifierDurationMs: classifierMs > 0 ? classifierMs : undefined,
classifierCount: classifierCount > 0 ? classifierCount : undefined,
configWriteCount: getGlobalConfigWriteCount()
})]);
}
resetLoadingState();
// Log query profiling report if enabled
logQueryProfileReport();
// Signal that a query turn has completed successfully
await onTurnComplete?.(messagesRef.current);
}, [initialMcpClients, resetLoadingState, getToolUseContext, toolPermissionContext, setAppState, customSystemPrompt, onTurnComplete, appendSystemPrompt, canUseTool, mainThreadAgentDefinition, onQueryEvent, sessionTitle, titleDisabled]);
const onQuery = useCallback(async (newMessages: MessageType[], abortController: AbortController, shouldQuery: boolean, additionalAllowedTools: string[], mainLoopModelParam: string, onBeforeQueryCallback?: (input: string, newMessages: MessageType[]) => Promise<boolean>, input?: string, effort?: EffortValue): Promise<void> => {
// If this is a teammate, mark them as active when starting a turn
if (isAgentSwarmsEnabled()) {
const teamName = getTeamName();
const agentName = getAgentName();
if (teamName && agentName) {
// Fire and forget - turn starts immediately, write happens in background
void setMemberActive(teamName, agentName, true);
}
}
// Concurrent guard via state machine. tryStart() atomically checks
// and transitions idleβrunning, returning the generation number.
// Returns null if already running β no separate check-then-set.
const thisGeneration = queryGuard.tryStart();
if (thisGeneration === null) {
logEvent('tengu_concurrent_onquery_detected', {});
// Extract and enqueue user message text, skipping meta messages
// (e.g. expanded skill content, tick prompts) that should not be
// replayed as user-visible text.
newMessages.filter((m): m is UserMessage => m.type === 'user' && !m.isMeta).map(_ => getContentText(_.message.content)).filter(_ => _ !== null).forEach((msg, i) => {
enqueue({
value: msg,
mode: 'prompt'
});
if (i === 0) {
logEvent('tengu_concurrent_onquery_enqueued', {});
}
});
return;
}
try {
// isLoading is derived from queryGuard β tryStart() above already
// transitioned dispatchingβrunning, so no setter call needed here.
resetTimingRefs();
setMessages(oldMessages => [...oldMessages, ...newMessages]);
responseLengthRef.current = 0;
if (feature('TOKEN_BUDGET')) {
const parsedBudget = input ? parseTokenBudget(input) : null;
snapshotOutputTokensForTurn(parsedBudget ?? getCurrentTurnTokenBudget());
}
apiMetricsRef.current = [];
setStreamingToolUses([]);
setStreamingText(null);
// messagesRef is updated synchronously by the setMessages wrapper
// above, so it already includes newMessages from the append at the
// top of this try block. No reconstruction needed, no waiting for
// React's scheduler (previously cost 20-56ms per prompt; the 56ms
// case was a GC pause caught during the await).
const latestMessages = messagesRef.current;
if (input) {
await mrOnBeforeQuery(input, latestMessages, newMessages.length);
}
// Pass full conversation history to callback
if (onBeforeQueryCallback && input) {
const shouldProceed = await onBeforeQueryCallback(input, latestMessages);
if (!shouldProceed) {
return;
}
}
await onQueryImpl(latestMessages, newMessages, abortController, shouldQuery, additionalAllowedTools, mainLoopModelParam, effort);
} finally {
// queryGuard.end() atomically checks generation and transitions
// runningβidle. Returns false if a newer query owns the guard
// (cancel+resubmit race where the stale finally fires as a microtask).
if (queryGuard.end(thisGeneration)) {
setLastQueryCompletionTime(Date.now());
skipIdleCheckRef.current = false;
// Always reset loading state in finally - this ensures cleanup even
// if onQueryImpl throws. onTurnComplete is called separately in
// onQueryImpl only on successful completion.
resetLoadingState();
await mrOnTurnComplete(messagesRef.current, abortController.signal.aborted);
// Notify bridge clients that the turn is complete so mobile apps
// can stop the spark animation and show post-turn UI.
sendBridgeResultRef.current();
// Auto-hide tungsten panel content at turn end (ant-only), but keep
// tungstenActiveSession set so the pill stays in the footer and the user
// can reopen the panel. Background tmux tasks (e.g. /hunter) run for
// minutes β wiping the session made the pill disappear entirely, forcing
// the user to re-invoke Tmux just to peek. Skip on abort so the panel
// stays open for inspection (matches the turn-duration guard below).
if ("external" === 'ant' && !abortController.signal.aborted) {
setAppState(prev => {
if (prev.tungstenActiveSession === undefined) return prev;
if (prev.tungstenPanelAutoHidden === true) return prev;
return {
...prev,
tungstenPanelAutoHidden: true
};
});
}
// Capture budget info before clearing (ant-only)
let budgetInfo: {
tokens: number;
limit: number;
nudges: number;
} | undefined;
if (feature('TOKEN_BUDGET')) {
if (getCurrentTurnTokenBudget() !== null && getCurrentTurnTokenBudget()! > 0 && !abortController.signal.aborted) {
budgetInfo = {
tokens: getTurnOutputTokens(),
limit: getCurrentTurnTokenBudget()!,
nudges: getBudgetContinuationCount()
};
}
snapshotOutputTokensForTurn(null);
}
// Add turn duration message for turns longer than 30s or with a budget
// Skip if user aborted or if in loop mode (too noisy between ticks)
// Defer if swarm teammates are still running (show when they finish)
const turnDurationMs = Date.now() - loadingStartTimeRef.current - totalPausedMsRef.current;
if ((turnDurationMs > 30000 || budgetInfo !== undefined) && !abortController.signal.aborted && !proactiveActive) {
const hasRunningSwarmAgents = getAllInProcessTeammateTasks(store.getState().tasks).some(t => t.status === 'running');
if (hasRunningSwarmAgents) {
// Only record start time on the first deferred turn
if (swarmStartTimeRef.current === null) {
swarmStartTimeRef.current = loadingStartTimeRef.current;
}
// Always update budget β later turns may carry the actual budget
if (budgetInfo) {
swarmBudgetInfoRef.current = budgetInfo;
}
} else {
setMessages(prev => [...prev, createTurnDurationMessage(turnDurationMs, budgetInfo, count(prev, isLoggableMessage))]);
}
}
// Clear the controller so CancelRequestHandler's canCancelRunningTask
// reads false at the idle prompt. Without this, the stale non-aborted
// controller makes ctrl+c fire onCancel() (aborting nothing) instead of
// propagating to the double-press exit flow.
setAbortController(null);
}
// Auto-restore: if the user interrupted before any meaningful response
// arrived, rewind the conversation and restore their prompt β same as
// opening the message selector and picking the last message.
// This runs OUTSIDE the queryGuard.end() check because onCancel calls
// forceEnd(), which bumps the generation so end() returns false above.
// Guards: reason === 'user-cancel' (onCancel/Esc; programmatic aborts
// use 'background'/'interrupt' and must not rewind β note abort() with
// no args sets reason to a DOMException, not undefined), !isActive (no
// newer query started β cancel+resubmit race), empty input (don't
// clobber text typed during loading), no queued commands (user queued
// B while A was loading β they've moved on, don't restore A; also
// avoids removeLastFromHistory removing B's entry instead of A's),
// not viewing a teammate (messagesRef is the main conversation β the
// old Up-arrow quick-restore had this guard, preserve it).
if (abortController.signal.reason === 'user-cancel' && !queryGuard.isActive && inputValueRef.current === '' && getCommandQueueLength() === 0 && !store.getState().viewingAgentTaskId) {
const msgs = messagesRef.current;
const lastUserMsg = msgs.findLast(selectableUserMessagesFilter);
if (lastUserMsg) {
const idx = msgs.lastIndexOf(lastUserMsg);
if (messagesAfterAreOnlySynthetic(msgs, idx)) {
// The submit is being undone β undo its history entry too,
// otherwise Up-arrow shows the restored text twice.
removeLastFromHistory();
restoreMessageSyncRef.current(lastUserMsg);
}
}
}
}
}, [onQueryImpl, setAppState, resetLoadingState, queryGuard, mrOnBeforeQuery, mrOnTurnComplete]);
// Handle initial message (from CLI args or plan mode exit with context clear)
// This effect runs when isLoading becomes false and there's a pending message
const initialMessageRef = useRef(false);
useEffect(() => {
const pending = initialMessage;
if (!pending || isLoading || initialMessageRef.current) return;
// Mark as processing to prevent re-entry
initialMessageRef.current = true;
async function processInitialMessage(initialMsg: NonNullable<typeof pending>) {
// Clear context if requested (plan mode exit)
if (initialMsg.clearContext) {
// Preserve the plan slug before clearing context, so the new session
// can access the same plan file after regenerateSessionId()
const oldPlanSlug = initialMsg.message.planContent ? getPlanSlug() : undefined;
const {
clearConversation
} = await import('../commands/clear/conversation.js');
await clearConversation({
setMessages,
readFileState: readFileState.current,
discoveredSkillNames: discoveredSkillNamesRef.current,
loadedNestedMemoryPaths: loadedNestedMemoryPathsRef.current,
getAppState: () => store.getState(),
setAppState,
setConversationId
});
haikuTitleAttemptedRef.current = false;
setHaikuTitle(undefined);
bashTools.current.clear();
bashToolsProcessedIdx.current = 0;
// Restore the plan slug for the new session so getPlan() finds the file
if (oldPlanSlug) {
setPlanSlug(getSessionId(), oldPlanSlug);
}
}
// Atomically: clear initial message, set permission mode and rules, and store plan for verification
const shouldStorePlanForVerification = initialMsg.message.planContent && "external" === 'ant' && isEnvTruthy(undefined);
setAppState(prev => {
// Build and apply permission updates (mode + allowedPrompts rules)
let updatedToolPermissionContext = initialMsg.mode ? applyPermissionUpdates(prev.toolPermissionContext, buildPermissionUpdates(initialMsg.mode, initialMsg.allowedPrompts)) : prev.toolPermissionContext;
// For auto, override the mode (buildPermissionUpdates maps
// it to 'default' via toExternalPermissionMode) and strip dangerous rules
if (feature('TRANSCRIPT_CLASSIFIER') && initialMsg.mode === 'auto') {
updatedToolPermissionContext = stripDangerousPermissionsForAutoMode({
...updatedToolPermissionContext,
mode: 'auto',
prePlanMode: undefined
});
}
return {
...prev,
initialMessage: null,
toolPermissionContext: updatedToolPermissionContext,
...(shouldStorePlanForVerification && {
pendingPlanVerification: {
plan: initialMsg.message.planContent!,
verificationStarted: false,
verificationCompleted: false
}
})
};
});
// Create file history snapshot for code rewind
if (fileHistoryEnabled()) {
void fileHistoryMakeSnapshot((updater: (prev: FileHistoryState) => FileHistoryState) => {
setAppState(prev => ({
...prev,
fileHistory: updater(prev.fileHistory)
}));
}, initialMsg.message.uuid);
}
// Ensure SessionStart hook context is available before the first API
// call. onSubmit calls this internally but the onQuery path below
// bypasses onSubmit β hoist here so both paths see hook messages.
await awaitPendingHooks();
// Route all initial prompts through onSubmit to ensure UserPromptSubmit hooks fire
// TODO: Simplify by always routing through onSubmit once it supports
// ContentBlockParam arrays (images) as input
const content = initialMsg.message.message.content;
// Route all string content through onSubmit to ensure hooks fire
// For complex content (images, etc.), fall back to direct onQuery
// Plan messages bypass onSubmit to preserve planContent metadata for rendering
if (typeof content === 'string' && !initialMsg.message.planContent) {
// Route through onSubmit for proper processing including UserPromptSubmit hooks
void onSubmit(content, {
setCursorOffset: () => {},
clearBuffer: () => {},
resetHistory: () => {}
});
} else {
// Plan messages or complex content (images, etc.) - send directly to model
// Plan messages use onQuery to preserve planContent metadata for rendering
// TODO: Once onSubmit supports ContentBlockParam arrays, remove this branch
const newAbortController = createAbortController();
setAbortController(newAbortController);
void onQuery([initialMsg.message], newAbortController, true,
// shouldQuery
[],
// additionalAllowedTools
mainLoopModel);
}
// Reset ref after a delay to allow new initial messages
setTimeout(ref => {
ref.current = false;
}, 100, initialMessageRef);
}
void processInitialMessage(pending);
}, [initialMessage, isLoading, setMessages, setAppState, onQuery, mainLoopModel, tools]);
const onSubmit = useCallback(async (input: string, helpers: PromptInputHelpers, speculationAccept?: {
state: ActiveSpeculationState;
speculationSessionTimeSavedMs: number;
setAppState: SetAppState;
}, options?: {
fromKeybinding?: boolean;
}) => {
// Re-pin scroll to bottom on submit so the user always sees the new
// exchange (matches OpenCode's auto-scroll behavior).
repinScroll();
// Resume loop mode if paused
if (feature('PROACTIVE') || feature('KAIROS')) {
proactiveModule?.resumeProactive();
}
// Handle immediate commands - these bypass the queue and execute right away
// even while Claude is processing. Commands opt-in via `immediate: true`.
// Commands triggered via keybindings are always treated as immediate.
if (!speculationAccept && input.trim().startsWith('/')) {
// Expand [Pasted text #N] refs so immediate commands (e.g. /btw) receive
// the pasted content, not the placeholder. The non-immediate path gets
// this expansion later in handlePromptSubmit.
const trimmedInput = expandPastedTextRefs(input, pastedContents).trim();
const spaceIndex = trimmedInput.indexOf(' ');
const commandName = spaceIndex === -1 ? trimmedInput.slice(1) : trimmedInput.slice(1, spaceIndex);
const commandArgs = spaceIndex === -1 ? '' : trimmedInput.slice(spaceIndex + 1).trim();
// Find matching command - treat as immediate if:
// 1. Command has `immediate: true`, OR
// 2. Command was triggered via keybinding (fromKeybinding option)
const matchingCommand = commands.find(cmd => isCommandEnabled(cmd) && (cmd.name === commandName || cmd.aliases?.includes(commandName) || getCommandName(cmd) === commandName));
if (matchingCommand?.name === 'clear' && idleHintShownRef.current) {
logEvent('tengu_idle_return_action', {
action: 'hint_converted' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
variant: idleHintShownRef.current as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
idleMinutes: Math.round((Date.now() - lastQueryCompletionTimeRef.current) / 60_000),
messageCount: messagesRef.current.length,
totalInputTokens: getTotalInputTokens()
});
idleHintShownRef.current = false;
}
const shouldTreatAsImmediate = queryGuard.isActive && (matchingCommand?.immediate || options?.fromKeybinding);
if (matchingCommand && shouldTreatAsImmediate && matchingCommand.type === 'local-jsx') {
// Only clear input if the submitted text matches what's in the prompt.
// When a command keybinding fires, input is "/<command>" but the actual
// input value is the user's existing text - don't clear it in that case.
if (input.trim() === inputValueRef.current.trim()) {
setInputValue('');
helpers.setCursorOffset(0);
helpers.clearBuffer();
setPastedContents({});
}
const pastedTextRefs = parseReferences(input).filter(r => pastedContents[r.id]?.type === 'text');
const pastedTextCount = pastedTextRefs.length;
const pastedTextBytes = pastedTextRefs.reduce((sum, r) => sum + (pastedContents[r.id]?.content.length ?? 0), 0);
logEvent('tengu_paste_text', {
pastedTextCount,
pastedTextBytes
});
logEvent('tengu_immediate_command_executed', {
commandName: matchingCommand.name as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
fromKeybinding: options?.fromKeybinding ?? false
});
// Execute the command directly
const executeImmediateCommand = async (): Promise<void> => {
let doneWasCalled = false;
const onDone = (result?: string, doneOptions?: {
display?: CommandResultDisplay;
metaMessages?: string[];
}): void => {
doneWasCalled = true;
setToolJSX({
jsx: null,
shouldHidePromptInput: false,
clearLocalJSX: true
});
const newMessages: MessageType[] = [];
if (result && doneOptions?.display !== 'skip') {
addNotification({
key: `immediate-${matchingCommand.name}`,
text: result,
priority: 'immediate'
});
// In fullscreen the command just showed as a centered modal
// pane β the notification above is enough feedback. Adding
// "β― /config" + "βΏ dismissed" to the transcript is clutter
// (those messages are type:system subtype:local_command β
// user-visible but NOT sent to the model, so skipping them
// doesn't change model context). Outside fullscreen the
// transcript entry stays so scrollback shows what ran.
if (!isFullscreenEnvEnabled()) {
newMessages.push(createCommandInputMessage(formatCommandInputTags(getCommandName(matchingCommand), commandArgs)), createCommandInputMessage(`<${LOCAL_COMMAND_STDOUT_TAG}>${escapeXml(result)}</${LOCAL_COMMAND_STDOUT_TAG}>`));
}
}
// Inject meta messages (model-visible, user-hidden) into the transcript
if (doneOptions?.metaMessages?.length) {
newMessages.push(...doneOptions.metaMessages.map(content => createUserMessage({
content,
isMeta: true
})));
}
if (newMessages.length) {
setMessages(prev => [...prev, ...newMessages]);
}
// Restore stashed prompt after local-jsx command completes.
// The normal stash restoration path (below) is skipped because
// local-jsx commands return early from onSubmit.
if (stashedPrompt !== undefined) {
setInputValue(stashedPrompt.text);
helpers.setCursorOffset(stashedPrompt.cursorOffset);
setPastedContents(stashedPrompt.pastedContents);
setStashedPrompt(undefined);
}
};
// Build context for the command (reuses existing getToolUseContext).
// Read messages via ref to keep onSubmit stable across message
// updates β matches the pattern at L2384/L2400/L2662 and avoids
// pinning stale REPL render scopes in downstream closures.
const context = getToolUseContext(messagesRef.current, [], createAbortController(), mainLoopModel);
const mod = await matchingCommand.load();
const jsx = await mod.call(onDone, context, commandArgs);
// Skip if onDone already fired β prevents stuck isLocalJSXCommand
// (see processSlashCommand.tsx local-jsx case for full mechanism).
if (jsx && !doneWasCalled) {
// shouldHidePromptInput: false keeps Notifications mounted
// so the onDone result isn't lost
setToolJSX({
jsx,
shouldHidePromptInput: false,
isLocalJSXCommand: true
});
}
};
void executeImmediateCommand();
return; // Always return early - don't add to history or queue
}
}
// Remote mode: skip empty input early before any state mutations
if (activeRemote.isRemoteMode && !input.trim()) {
return;
}
// Idle-return: prompt returning users to start fresh when the
// conversation is large and the cache is cold. tengu_willow_mode
// controls treatment: "dialog" (blocking), "hint" (notification), "off".
{
const willowMode = getFeatureValue_CACHED_MAY_BE_STALE('tengu_willow_mode', 'off');
const idleThresholdMin = Number(process.env.CLAUDE_CODE_IDLE_THRESHOLD_MINUTES ?? 75);
const tokenThreshold = Number(process.env.CLAUDE_CODE_IDLE_TOKEN_THRESHOLD ?? 100_000);
if (willowMode !== 'off' && !getGlobalConfig().idleReturnDismissed && !skipIdleCheckRef.current && !speculationAccept && !input.trim().startsWith('/') && lastQueryCompletionTimeRef.current > 0 && getTotalInputTokens() >= tokenThreshold) {
const idleMs = Date.now() - lastQueryCompletionTimeRef.current;
const idleMinutes = idleMs / 60_000;
if (idleMinutes >= idleThresholdMin && willowMode === 'dialog') {
setIdleReturnPending({
input,
idleMinutes
});
setInputValue('');
helpers.setCursorOffset(0);
helpers.clearBuffer();
return;
}
}
}
// Add to history for direct user submissions.
// Queued command processing (executeQueuedInput) doesn't call onSubmit,
// so notifications and already-queued user input won't be added to history here.
// Skip history for keybinding-triggered commands (user didn't type the command).
if (!options?.fromKeybinding) {
addToHistory({
display: speculationAccept ? input : prependModeCharacterToInput(input, inputMode),
pastedContents: speculationAccept ? {} : pastedContents
});
// Add the just-submitted command to the front of the ghost-text
// cache so it's suggested immediately (not after the 60s TTL).
if (inputMode === 'bash') {
prependToShellHistoryCache(input.trim());
}
}
// Restore stash if present, but NOT for slash commands or when loading.
// - Slash commands (especially interactive ones like /model, /context) hide
// the prompt and show a picker UI. Restoring the stash during a command would
// place the text in a hidden input, and the user would lose it by typing the
// next command. Instead, preserve the stash so it survives across command runs.
// - When loading, the submitted input will be queued and handlePromptSubmit
// will clear the input field (onInputChange('')), which would clobber the
// restored stash. Defer restoration to after handlePromptSubmit (below).
// Remote mode is exempt: it sends via WebSocket and returns early without
// calling handlePromptSubmit, so there's no clobbering risk β restore eagerly.
// In both deferred cases, the stash is restored after await handlePromptSubmit.
const isSlashCommand = !speculationAccept && input.trim().startsWith('/');
// Submit runs "now" (not queued) when not already loading, or when
// accepting speculation, or in remote mode (which sends via WS and
// returns early without calling handlePromptSubmit).
const submitsNow = !isLoading || speculationAccept || activeRemote.isRemoteMode;
if (stashedPrompt !== undefined && !isSlashCommand && submitsNow) {
setInputValue(stashedPrompt.text);
helpers.setCursorOffset(stashedPrompt.cursorOffset);
setPastedContents(stashedPrompt.pastedContents);
setStashedPrompt(undefined);
} else if (submitsNow) {
if (!options?.fromKeybinding) {
// Clear input when not loading or accepting speculation.
// Preserve input for keybinding-triggered commands.
setInputValue('');
helpers.setCursorOffset(0);
}
setPastedContents({});
}
if (submitsNow) {
setInputMode('prompt');
setIDESelection(undefined);
setSubmitCount(_ => _ + 1);
helpers.clearBuffer();
tipPickedThisTurnRef.current = false;
// Show the placeholder in the same React batch as setInputValue('').
// Skip for slash/bash (they have their own echo), speculation and remote
// mode (both setMessages directly with no gap to bridge).
if (!isSlashCommand && inputMode === 'prompt' && !speculationAccept && !activeRemote.isRemoteMode) {
setUserInputOnProcessing(input);
// showSpinner includes userInputOnProcessing, so the spinner appears
// on this render. Reset timing refs now (before queryGuard.reserve()
// would) so elapsed time doesn't read as Date.now() - 0. The
// isQueryActive transition above does the same reset β idempotent.
resetTimingRefs();
}
// Increment prompt count for attribution tracking and save snapshot
// The snapshot persists promptCount so it survives compaction
if (feature('COMMIT_ATTRIBUTION')) {
setAppState(prev => ({
...prev,
attribution: incrementPromptCount(prev.attribution, snapshot => {
void recordAttributionSnapshot(snapshot).catch(error => {
logForDebugging(`Attribution: Failed to save snapshot: ${error}`);
});
})
}));
}
}
// Handle speculation acceptance
if (speculationAccept) {
const {
queryRequired
} = await handleSpeculationAccept(speculationAccept.state, speculationAccept.speculationSessionTimeSavedMs, speculationAccept.setAppState, input, {
setMessages,
readFileState,
cwd: getOriginalCwd()
});
if (queryRequired) {
const newAbortController = createAbortController();
setAbortController(newAbortController);
void onQuery([], newAbortController, true, [], mainLoopModel);
}
return;
}
// Remote mode: send input via stream-json instead of local query.
// Permission requests from the remote are bridged into toolUseConfirmQueue
// and rendered using the standard PermissionRequest component.
//
// local-jsx slash commands (e.g. /agents, /config) render UI in THIS
// process β they have no remote equivalent. Let those fall through to
// handlePromptSubmit so they execute locally. Prompt commands and
// plain text go to the remote.
if (activeRemote.isRemoteMode && !(isSlashCommand && commands.find(c => {
const name = input.trim().slice(1).split(/\s/)[0];
return isCommandEnabled(c) && (c.name === name || c.aliases?.includes(name!) || getCommandName(c) === name);
})?.type === 'local-jsx')) {
// Build content blocks when there are pasted attachments (images)
const pastedValues = Object.values(pastedContents);
const imageContents = pastedValues.filter(c => c.type === 'image');
const imagePasteIds = imageContents.length > 0 ? imageContents.map(c => c.id) : undefined;
let messageContent: string | ContentBlockParam[] = input.trim();
let remoteContent: RemoteMessageContent = input.trim();
if (pastedValues.length > 0) {
const contentBlocks: ContentBlockParam[] = [];
const remoteBlocks: Array<{
type: string;
[key: string]: unknown;
}> = [];
const trimmedInput = input.trim();
if (trimmedInput) {
contentBlocks.push({
type: 'text',
text: trimmedInput
});
remoteBlocks.push({
type: 'text',
text: trimmedInput
});
}
for (const pasted of pastedValues) {
if (pasted.type === 'image') {
const source = {
type: 'base64' as const,
media_type: (pasted.mediaType ?? 'image/png') as 'image/jpeg' | 'image/png' | 'image/gif' | 'image/webp',
data: pasted.content
};
contentBlocks.push({
type: 'image',
source
});
remoteBlocks.push({
type: 'image',
source
});
} else {
contentBlocks.push({
type: 'text',
text: pasted.content
});
remoteBlocks.push({
type: 'text',
text: pasted.content
});
}
}
messageContent = contentBlocks;
remoteContent = remoteBlocks;
}
// Create and add user message to UI
// Note: empty input already handled by early return above
const userMessage = createUserMessage({
content: messageContent,
imagePasteIds
});
setMessages(prev => [...prev, userMessage]);
// Send to remote session
await activeRemote.sendMessage(remoteContent, {
uuid: userMessage.uuid
});
return;
}
// Ensure SessionStart hook context is available before the first API call.
await awaitPendingHooks();
await handlePromptSubmit({
input,
helpers,
queryGuard,
isExternalLoading,
mode: inputMode,
commands,
onInputChange: setInputValue,
setPastedContents,
setToolJSX,
getToolUseContext,
messages: messagesRef.current,
mainLoopModel,
pastedContents,
ideSelection,
setUserInputOnProcessing,
setAbortController,
abortController,
onQuery,
setAppState,
querySource: getQuerySourceForREPL(),
onBeforeQuery,
canUseTool,
addNotification,
setMessages,
// Read via ref so streamMode can be dropped from onSubmit deps β
// handlePromptSubmit only uses it for debug log + telemetry event.
streamMode: streamModeRef.current,
hasInterruptibleToolInProgress: hasInterruptibleToolInProgressRef.current
});
// Restore stash that was deferred above. Two cases:
// - Slash command: handlePromptSubmit awaited the full command execution
// (including interactive pickers). Restoring now places the stash back in
// the visible input.
// - Loading (queued): handlePromptSubmit enqueued + cleared input, then
// returned quickly. Restoring now places the stash back after the clear.
if ((isSlashCommand || isLoading) && stashedPrompt !== undefined) {
setInputValue(stashedPrompt.text);
helpers.setCursorOffset(stashedPrompt.cursorOffset);
setPastedContents(stashedPrompt.pastedContents);
setStashedPrompt(undefined);
}
}, [queryGuard,
// isLoading is read at the !isLoading checks above for input-clearing
// and submitCount gating. It's derived from isQueryActive || isExternalLoading,
// so including it here ensures the closure captures the fresh value.
isLoading, isExternalLoading, inputMode, commands, setInputValue, setInputMode, setPastedContents, setSubmitCount, setIDESelection, setToolJSX, getToolUseContext,
// messages is read via messagesRef.current inside the callback to
// keep onSubmit stable across message updates (see L2384/L2400/L2662).
// Without this, each setMessages call (~30Γ per turn) recreates
// onSubmit, pinning the REPL render scope (1776B) + that render's
// messages array in downstream closures (PromptInput, handleAutoRunIssue).
// Heap analysis showed ~9 REPL scopes and ~15 messages array versions
// accumulating after #20174/#20175, all traced to this dep.
mainLoopModel, pastedContents, ideSelection, setUserInputOnProcessing, setAbortController, addNotification, onQuery, stashedPrompt, setStashedPrompt, setAppState, onBeforeQuery, canUseTool, remoteSession, setMessages, awaitPendingHooks, repinScroll]);
// Callback for when user submits input while viewing a teammate's transcript
const onAgentSubmit = useCallback(async (input: string, task: InProcessTeammateTaskState | LocalAgentTaskState, helpers: PromptInputHelpers) => {
if (isLocalAgentTask(task)) {
appendMessageToLocalAgent(task.id, createUserMessage({
content: input
}), setAppState);
if (task.status === 'running') {
queuePendingMessage(task.id, input, setAppState);
} else {
void resumeAgentBackground({
agentId: task.id,
prompt: input,
toolUseContext: getToolUseContext(messagesRef.current, [], new AbortController(), mainLoopModel),
canUseTool
}).catch(err => {
logForDebugging(`resumeAgentBackground failed: ${errorMessage(err)}`);
addNotification({
key: `resume-agent-failed-${task.id}`,
jsx: <Text color="error">
Failed to resume agent: {errorMessage(err)}
</Text>,
priority: 'low'
});
});
}
} else {
injectUserMessageToTeammate(task.id, input, setAppState);
}
setInputValue('');
helpers.setCursorOffset(0);
helpers.clearBuffer();
}, [setAppState, setInputValue, getToolUseContext, canUseTool, mainLoopModel, addNotification]);
// Handlers for auto-run /issue or /good-claude (defined after onSubmit)
const handleAutoRunIssue = useCallback(() => {
const command = autoRunIssueReason ? getAutoRunCommand(autoRunIssueReason) : '/issue';
setAutoRunIssueReason(null); // Clear the state
onSubmit(command, {
setCursorOffset: () => {},
clearBuffer: () => {},
resetHistory: () => {}
}).catch(err => {
logForDebugging(`Auto-run ${command} failed: ${errorMessage(err)}`);
});
}, [onSubmit, autoRunIssueReason]);
const handleCancelAutoRunIssue = useCallback(() => {
setAutoRunIssueReason(null);
}, []);
// Handler for when user presses 1 on survey thanks screen to share details
const handleSurveyRequestFeedback = useCallback(() => {
const command = "external" === 'ant' ? '/issue' : '/feedback';
onSubmit(command, {
setCursorOffset: () => {},
clearBuffer: () => {},
resetHistory: () => {}
}).catch(err => {
logForDebugging(`Survey feedback request failed: ${err instanceof Error ? err.message : String(err)}`);
});
}, [onSubmit]);
// onSubmit is unstable (deps include `messages` which changes every turn).
// `handleOpenRateLimitOptions` is prop-drilled to every MessageRow, and each
// MessageRow fiber pins the closure (and transitively the entire REPL render
// scope, ~1.8KB) at mount time. Using a ref keeps this callback stable so
// old REPL scopes can be GC'd β saves ~35MB over a 1000-turn session.
const onSubmitRef = useRef(onSubmit);
onSubmitRef.current = onSubmit;
const handleOpenRateLimitOptions = useCallback(() => {
void onSubmitRef.current('/rate-limit-options', {
setCursorOffset: () => {},
clearBuffer: () => {},
resetHistory: () => {}
});
}, []);
const handleExit = useCallback(async () => {
setIsExiting(true);
// In bg sessions, always detach instead of kill β even when a worktree is
// active. Without this guard, the worktree branch below short-circuits into
// ExitFlow (which calls gracefulShutdown) before exit.tsx is ever loaded.
if (feature('BG_SESSIONS') && isBgSession()) {
spawnSync('tmux', ['detach-client'], {
stdio: 'ignore'
});
setIsExiting(false);
return;
}
const showWorktree = getCurrentWorktreeSession() !== null;
if (showWorktree) {
setExitFlow(<ExitFlow showWorktree onDone={() => {}} onCancel={() => {
setExitFlow(null);
setIsExiting(false);
}} />);
return;
}
const exitMod = await exit.load();
const exitFlowResult = await exitMod.call(() => {});
setExitFlow(exitFlowResult);
// If call() returned without killing the process (bg session detach),
// clear isExiting so the UI is usable on reattach. No-op on the normal
// path β gracefulShutdown's process.exit() means we never get here.
if (exitFlowResult === null) {
setIsExiting(false);
}
}, []);
const handleShowMessageSelector = useCallback(() => {
setIsMessageSelectorVisible(prev => !prev);
}, []);
// Rewind conversation state to just before `message`: slice messages,
// reset conversation ID, microcompact state, permission mode, prompt suggestion.
// Does NOT touch the prompt input. Index is computed from messagesRef (always
// fresh via the setMessages wrapper) so callers don't need to worry about
// stale closures.
const rewindConversationTo = useCallback((message: UserMessage) => {
const prev = messagesRef.current;
const messageIndex = prev.lastIndexOf(message);
if (messageIndex === -1) return;
logEvent('tengu_conversation_rewind', {
preRewindMessageCount: prev.length,
postRewindMessageCount: messageIndex,
messagesRemoved: prev.length - messageIndex,
rewindToMessageIndex: messageIndex
});
setMessages(prev.slice(0, messageIndex));
// Careful, this has to happen after setMessages
setConversationId(randomUUID());
// Reset cached microcompact state so stale pinned cache edits
// don't reference tool_use_ids from truncated messages
resetMicrocompactState();
if (feature('CONTEXT_COLLAPSE')) {
// Rewind truncates the REPL array. Commits whose archived span
// was past the rewind point can't be projected anymore
// (projectView silently skips them) but the staged queue and ID
// maps reference stale uuids. Simplest safe reset: drop
// everything. The ctx-agent will re-stage on the next
// threshold crossing.
/* eslint-disable @typescript-eslint/no-require-imports */
;
(require('../services/contextCollapse/index.js') as typeof import('../services/contextCollapse/index.js')).resetContextCollapse();
/* eslint-enable @typescript-eslint/no-require-imports */
}
// Restore state from the message we're rewinding to
setAppState(prev => ({
...prev,
// Restore permission mode from the message
toolPermissionContext: message.permissionMode && prev.toolPermissionContext.mode !== message.permissionMode ? {
...prev.toolPermissionContext,
mode: message.permissionMode
} : prev.toolPermissionContext,
// Clear stale prompt suggestion from previous conversation state
promptSuggestion: {
text: null,
promptId: null,
shownAt: 0,
acceptedAt: 0,
generationRequestId: null
}
}));
}, [setMessages, setAppState]);
// Synchronous rewind + input population. Used directly by auto-restore on
// interrupt (so React batches with the abort's setMessages β single render,
// no flicker). MessageSelector wraps this in setImmediate via handleRestoreMessage.
const restoreMessageSync = useCallback((message: UserMessage) => {
rewindConversationTo(message);
const r = textForResubmit(message);
if (r) {
setInputValue(r.text);
setInputMode(r.mode);
}
// Restore pasted images
if (Array.isArray(message.message.content) && message.message.content.some(block => block.type === 'image')) {
const imageBlocks: Array<ImageBlockParam> = message.message.content.filter(block => block.type === 'image');
if (imageBlocks.length > 0) {
const newPastedContents: Record<number, PastedContent> = {};
imageBlocks.forEach((block, index) => {
if (block.source.type === 'base64') {
const id = message.imagePasteIds?.[index] ?? index + 1;
newPastedContents[id] = {
id,
type: 'image',
content: block.source.data,
mediaType: block.source.media_type
};
}
});
setPastedContents(newPastedContents);
}
}
}, [rewindConversationTo, setInputValue]);
restoreMessageSyncRef.current = restoreMessageSync;
// MessageSelector path: defer via setImmediate so the "Interrupted" message
// renders to static output before rewind β otherwise it remains vestigial
// at the top of the screen.
const handleRestoreMessage = useCallback(async (message: UserMessage) => {
setImmediate((restore, message) => restore(message), restoreMessageSync, message);
}, [restoreMessageSync]);
// Not memoized β hook stores caps via ref, reads latest closure at dispatch.
// 24-char prefix: deriveUUID preserves first 24, renderable uuid prefix-matches raw source.
const findRawIndex = (uuid: string) => {
const prefix = uuid.slice(0, 24);
return messages.findIndex(m => m.uuid.slice(0, 24) === prefix);
};
const messageActionCaps: MessageActionCaps = {
copy: text =>
// setClipboard RETURNS OSC 52 β caller must stdout.write (tmux side-effects load-buffer, but that's tmux-only).
void setClipboard(text).then(raw => {
if (raw) process.stdout.write(raw);
addNotification({
// Same key as text-selection copy β repeated copies replace toast, don't queue.
key: 'selection-copied',
text: 'copied',
color: 'success',
priority: 'immediate',
timeoutMs: 2000
});
}),
edit: async msg => {
// Same skip-confirm check as /rewind: lossless β direct, else confirm dialog.
const rawIdx = findRawIndex(msg.uuid);
const raw = rawIdx >= 0 ? messages[rawIdx] : undefined;
if (!raw || !selectableUserMessagesFilter(raw)) return;
const noFileChanges = !(await fileHistoryHasAnyChanges(fileHistory, raw.uuid));
const onlySynthetic = messagesAfterAreOnlySynthetic(messages, rawIdx);
if (noFileChanges && onlySynthetic) {
// rewindConversationTo's setMessages races stream appends β cancel first (idempotent).
onCancel();
// handleRestoreMessage also restores pasted images.
void handleRestoreMessage(raw);
} else {
// Dialog path: onPreRestore (= onCancel) fires when user CONFIRMS, not on nevermind.
setMessageSelectorPreselect(raw);
setIsMessageSelectorVisible(true);
}
}
};
const {
enter: enterMessageActions,
handlers: messageActionHandlers
} = useMessageActions(cursor, setCursor, cursorNavRef, messageActionCaps);
async function onInit() {
// Always verify API key on startup, so we can show the user an error in the
// bottom right corner of the screen if the API key is invalid.
void reverify();
// Populate readFileState with CLAUDE.md files at startup
const memoryFiles = await getMemoryFiles();
if (memoryFiles.length > 0) {
const fileList = memoryFiles.map(f => ` [${f.type}] ${f.path} (${f.content.length} chars)${f.parent ? ` (included by ${f.parent})` : ''}`).join('\n');
logForDebugging(`Loaded ${memoryFiles.length} CLAUDE.md/rules files:\n${fileList}`);
} else {
logForDebugging('No CLAUDE.md/rules files found');
}
for (const file of memoryFiles) {
// When the injected content doesn't match disk (stripped HTML comments,
// stripped frontmatter, MEMORY.md truncation), cache the RAW disk bytes
// with isPartialView so Edit/Write require a real Read first while
// getChangedFiles + nested_memory dedup still work.
readFileState.current.set(file.path, {
content: file.contentDiffersFromDisk ? file.rawContent ?? file.content : file.content,
timestamp: Date.now(),
offset: undefined,
limit: undefined,
isPartialView: file.contentDiffersFromDisk
});
}
// Initial message handling is done via the initialMessage effect
}
// Register cost summary tracker
useCostSummary(useFpsMetrics());
// Record transcripts locally, for debugging and conversation recovery
// Don't record conversation if we only have initial messages; optimizes
// the case where user resumes a conversation then quites before doing
// anything else
useLogMessages(messages, messages.length === initialMessages?.length);
// REPL Bridge: replicate user/assistant messages to the bridge session
// for remote access via claude.ai. No-op in external builds or when not enabled.
const {
sendBridgeResult
} = useReplBridge(messages, setMessages, abortControllerRef, commands, mainLoopModel);
sendBridgeResultRef.current = sendBridgeResult;
useAfterFirstRender();
// Track prompt queue usage for analytics. Fire once per transition from
// empty to non-empty, not on every length change -- otherwise a render loop
// (concurrent onQuery thrashing, etc.) spams saveGlobalConfig, which hits
// ELOCKED under concurrent sessions and falls back to unlocked writes.
// That write storm is the primary trigger for ~/.claude.json corruption
// (GH #3117).
const hasCountedQueueUseRef = useRef(false);
useEffect(() => {
if (queuedCommands.length < 1) {
hasCountedQueueUseRef.current = false;
return;
}
if (hasCountedQueueUseRef.current) return;
hasCountedQueueUseRef.current = true;
saveGlobalConfig(current => ({
...current,
promptQueueUseCount: (current.promptQueueUseCount ?? 0) + 1
}));
}, [queuedCommands.length]);
// Process queued commands when query completes and queue has items
const executeQueuedInput = useCallback(async (queuedCommands: QueuedCommand[]) => {
await handlePromptSubmit({
helpers: {
setCursorOffset: () => {},
clearBuffer: () => {},
resetHistory: () => {}
},
queryGuard,
commands,
onInputChange: () => {},
setPastedContents: () => {},
setToolJSX,
getToolUseContext,
messages,
mainLoopModel,
ideSelection,
setUserInputOnProcessing,
setAbortController,
onQuery,
setAppState,
querySource: getQuerySourceForREPL(),
onBeforeQuery,
canUseTool,
addNotification,
setMessages,
queuedCommands
});
}, [queryGuard, commands, setToolJSX, getToolUseContext, messages, mainLoopModel, ideSelection, setUserInputOnProcessing, canUseTool, setAbortController, onQuery, addNotification, setAppState, onBeforeQuery]);
useQueueProcessor({
executeQueuedInput,
hasActiveLocalJsxUI: isShowingLocalJSXCommand,
queryGuard
});
// We'll use the global lastInteractionTime from state.ts
// Update last interaction time when input changes.
// Must be immediate because useEffect runs after the Ink render cycle flush.
useEffect(() => {
activityManager.recordUserActivity();
updateLastInteractionTime(true);
}, [inputValue, submitCount]);
useEffect(() => {
if (submitCount === 1) {
startBackgroundHousekeeping();
}
}, [submitCount]);
// Show notification when Claude is done responding and user is idle
useEffect(() => {
// Don't set up notification if Claude is busy
if (isLoading) return;
// Only enable notifications after the first new interaction in this session
if (submitCount === 0) return;
// No query has completed yet
if (lastQueryCompletionTime === 0) return;
// Set timeout to check idle state
const timer = setTimeout((lastQueryCompletionTime, isLoading, toolJSX, focusedInputDialogRef, terminal) => {
// Check if user has interacted since the response ended
const lastUserInteraction = getLastInteractionTime();
if (lastUserInteraction > lastQueryCompletionTime) {
// User has interacted since Claude finished - they're not idle, don't notify
return;
}
// User hasn't interacted since response ended, check other conditions
const idleTimeSinceResponse = Date.now() - lastQueryCompletionTime;
if (!isLoading && !toolJSX &&
// Use ref to get current dialog state, avoiding stale closure
focusedInputDialogRef.current === undefined && idleTimeSinceResponse >= getGlobalConfig().messageIdleNotifThresholdMs) {
void sendNotification({
message: 'Claude is waiting for your input',
notificationType: 'idle_prompt'
}, terminal);
}
}, getGlobalConfig().messageIdleNotifThresholdMs, lastQueryCompletionTime, isLoading, toolJSX, focusedInputDialogRef, terminal);
return () => clearTimeout(timer);
}, [isLoading, toolJSX, submitCount, lastQueryCompletionTime, terminal]);
// Idle-return hint: show notification when idle threshold is exceeded.
// Timer fires after the configured idle period; notification persists until
// dismissed or the user submits.
useEffect(() => {
if (lastQueryCompletionTime === 0) return;
if (isLoading) return;
const willowMode: string = getFeatureValue_CACHED_MAY_BE_STALE('tengu_willow_mode', 'off');
if (willowMode !== 'hint' && willowMode !== 'hint_v2') return;
if (getGlobalConfig().idleReturnDismissed) return;
const tokenThreshold = Number(process.env.CLAUDE_CODE_IDLE_TOKEN_THRESHOLD ?? 100_000);
if (getTotalInputTokens() < tokenThreshold) return;
const idleThresholdMs = Number(process.env.CLAUDE_CODE_IDLE_THRESHOLD_MINUTES ?? 75) * 60_000;
const elapsed = Date.now() - lastQueryCompletionTime;
const remaining = idleThresholdMs - elapsed;
const timer = setTimeout((lqct, addNotif, msgsRef, mode, hintRef) => {
if (msgsRef.current.length === 0) return;
const totalTokens = getTotalInputTokens();
const formattedTokens = formatTokens(totalTokens);
const idleMinutes = (Date.now() - lqct) / 60_000;
addNotif({
key: 'idle-return-hint',
jsx: mode === 'hint_v2' ? <>
<Text dimColor>new task? </Text>
<Text color="suggestion">/clear</Text>
<Text dimColor> to save </Text>
<Text color="suggestion">{formattedTokens} tokens</Text>
</> : <Text color="warning">
new task? /clear to save {formattedTokens} tokens
</Text>,
priority: 'medium',
// Persist until submit β the hint fires at T+75min idle, user may
// not return for hours. removeNotification in useEffect cleanup
// handles dismissal. 0x7FFFFFFF = setTimeout max (~24.8 days).
timeoutMs: 0x7fffffff
});
hintRef.current = mode;
logEvent('tengu_idle_return_action', {
action: 'hint_shown' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
variant: mode as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
idleMinutes: Math.round(idleMinutes),
messageCount: msgsRef.current.length,
totalInputTokens: totalTokens
});
}, Math.max(0, remaining), lastQueryCompletionTime, addNotification, messagesRef, willowMode, idleHintShownRef);
return () => {
clearTimeout(timer);
removeNotification('idle-return-hint');
idleHintShownRef.current = false;
};
}, [lastQueryCompletionTime, isLoading, addNotification, removeNotification]);
// Submits incoming prompts from teammate messages or tasks mode as new turns
// Returns true if submission succeeded, false if a query is already running
const handleIncomingPrompt = useCallback((content: string, options?: {
isMeta?: boolean;
}): boolean => {
if (queryGuard.isActive) return false;
// Defer to user-queued commands β user input always takes priority
// over system messages (teammate messages, task list items, etc.)
// Read from the module-level store at call time (not the render-time
// snapshot) to avoid a stale closure β this callback's deps don't
// include the queue.
if (getCommandQueue().some(cmd => cmd.mode === 'prompt' || cmd.mode === 'bash')) {
return false;
}
const newAbortController = createAbortController();
setAbortController(newAbortController);
// Create a user message with the formatted content (includes XML wrapper)
const userMessage = createUserMessage({
content,
isMeta: options?.isMeta ? true : undefined
});
void onQuery([userMessage], newAbortController, true, [], mainLoopModel);
return true;
}, [onQuery, mainLoopModel, store]);
// Voice input integration (VOICE_MODE builds only)
const voice = feature('VOICE_MODE') ?
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
useVoiceIntegration({
setInputValueRaw,
inputValueRef,
insertTextRef
}) : {
stripTrailing: () => 0,
handleKeyEvent: () => {},
resetAnchor: () => {},
interimRange: null
};
useInboxPoller({
enabled: isAgentSwarmsEnabled(),
isLoading,
focusedInputDialog,
onSubmitMessage: handleIncomingPrompt
});
useMailboxBridge({
isLoading,
onSubmitMessage: handleIncomingPrompt
});
// Scheduled tasks from .claude/scheduled_tasks.json (CronCreate/Delete/List)
if (feature('AGENT_TRIGGERS')) {
// Assistant mode bypasses the isLoading gate (the proactive tick β
// Sleep β tick loop would otherwise starve the scheduler).
// kairosEnabled is set once in initialState (main.tsx) and never mutated β no
// subscription needed. The tengu_kairos_cron runtime gate is checked inside
// useScheduledTasks's effect (not here) since wrapping a hook call in a dynamic
// condition would break rules-of-hooks.
const assistantMode = store.getState().kairosEnabled;
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
useScheduledTasks!({
isLoading,
assistantMode,
setMessages
});
}
// Note: Permission polling is now handled by useInboxPoller
// - Workers receive permission responses via mailbox messages
// - Leaders receive permission requests via mailbox messages
if ("external" === 'ant') {
// Tasks mode: watch for tasks and auto-process them
// eslint-disable-next-line react-hooks/rules-of-hooks
// biome-ignore lint/correctness/useHookAtTopLevel: conditional for dead code elimination in external builds
useTaskListWatcher({
taskListId,
isLoading,
onSubmitTask: handleIncomingPrompt
});
// Loop mode: auto-tick when enabled (via /job command)
// eslint-disable-next-line react-hooks/rules-of-hooks
// biome-ignore lint/correctness/useHookAtTopLevel: conditional for dead code elimination in external builds
useProactive?.({
// Suppress ticks while an initial message is pending β the initial
// message will be processed asynchronously and a premature tick would
// race with it, causing concurrent-query enqueue of expanded skill text.
isLoading: isLoading || initialMessage !== null,
queuedCommandsLength: queuedCommands.length,
hasActiveLocalJsxUI: isShowingLocalJSXCommand,
isInPlanMode: toolPermissionContext.mode === 'plan',
onSubmitTick: (prompt: string) => handleIncomingPrompt(prompt, {
isMeta: true
}),
onQueueTick: (prompt: string) => enqueue({
mode: 'prompt',
value: prompt,
isMeta: true
})
});
}
// Abort the current operation when a 'now' priority message arrives
// (e.g. from a chat UI client via UDS).
useEffect(() => {
if (queuedCommands.some(cmd => cmd.priority === 'now')) {
abortControllerRef.current?.abort('interrupt');
}
}, [queuedCommands]);
// Initial load
useEffect(() => {
void onInit();
// Cleanup on unmount
return () => {
void diagnosticTracker.shutdown();
};
// TODO: fix this
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// Listen for suspend/resume events
const {
internal_eventEmitter
} = useStdin();
const [remountKey, setRemountKey] = useState(0);
useEffect(() => {
const handleSuspend = () => {
// Print suspension instructions
process.stdout.write(`\nClaude Code has been suspended. Run \`fg\` to bring Claude Code back.\nNote: ctrl + z now suspends Claude Code, ctrl + _ undoes input.\n`);
};
const handleResume = () => {
// Force complete component tree replacement instead of terminal clear
// Ink now handles line count reset internally on SIGCONT
setRemountKey(prev => prev + 1);
};
internal_eventEmitter?.on('suspend', handleSuspend);
internal_eventEmitter?.on('resume', handleResume);
return () => {
internal_eventEmitter?.off('suspend', handleSuspend);
internal_eventEmitter?.off('resume', handleResume);
};
}, [internal_eventEmitter]);
// Derive stop hook spinner suffix from messages state
const stopHookSpinnerSuffix = useMemo(() => {
if (!isLoading) return null;
// Find stop hook progress messages
const progressMsgs = messages.filter((m): m is ProgressMessage<HookProgress> => m.type === 'progress' && m.data.type === 'hook_progress' && (m.data.hookEvent === 'Stop' || m.data.hookEvent === 'SubagentStop'));
if (progressMsgs.length === 0) return null;
// Get the most recent stop hook execution
const currentToolUseID = progressMsgs.at(-1)?.toolUseID;
if (!currentToolUseID) return null;
// Check if there's already a summary message for this execution (hooks completed)
const hasSummaryForCurrentExecution = messages.some(m => m.type === 'system' && m.subtype === 'stop_hook_summary' && m.toolUseID === currentToolUseID);
if (hasSummaryForCurrentExecution) return null;
const currentHooks = progressMsgs.filter(p => p.toolUseID === currentToolUseID);
const total = currentHooks.length;
// Count completed hooks
const completedCount = count(messages, m => {
if (m.type !== 'attachment') return false;
const attachment = m.attachment;
return 'hookEvent' in attachment && (attachment.hookEvent === 'Stop' || attachment.hookEvent === 'SubagentStop') && 'toolUseID' in attachment && attachment.toolUseID === currentToolUseID;
});
// Check if any hook has a custom status message
const customMessage = currentHooks.find(p => p.data.statusMessage)?.data.statusMessage;
if (customMessage) {
// Use custom message with progress counter if multiple hooks
return total === 1 ? `${customMessage}β¦` : `${customMessage}β¦ ${completedCount}/${total}`;
}
// Fall back to default behavior
const hookType = currentHooks[0]?.data.hookEvent === 'SubagentStop' ? 'subagent stop' : 'stop';
if ("external" === 'ant') {
const cmd = currentHooks[completedCount]?.data.command;
const label = cmd ? ` '${truncateToWidth(cmd, 40)}'` : '';
return total === 1 ? `running ${hookType} hook${label}` : `running ${hookType} hook${label}\u2026 ${completedCount}/${total}`;
}
return total === 1 ? `running ${hookType} hook` : `running stop hooks⦠${completedCount}/${total}`;
}, [messages, isLoading]);
// Callback to capture frozen state when entering transcript mode
const handleEnterTranscript = useCallback(() => {
setFrozenTranscriptState({
messagesLength: messages.length,
streamingToolUsesLength: streamingToolUses.length
});
}, [messages.length, streamingToolUses.length]);
// Callback to clear frozen state when exiting transcript mode
const handleExitTranscript = useCallback(() => {
setFrozenTranscriptState(null);
}, []);
// Props for GlobalKeybindingHandlers component (rendered inside KeybindingSetup)
const virtualScrollActive = isFullscreenEnvEnabled() && !disableVirtualScroll;
// Transcript search state. Hooks must be unconditional so they live here
// (not inside the `if (screen === 'transcript')` branch below); isActive
// gates the useInput. Query persists across bar open/close so n/N keep
// working after Enter dismisses the bar (less semantics).
const jumpRef = useRef<JumpHandle | null>(null);
const [searchOpen, setSearchOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const [searchCount, setSearchCount] = useState(0);
const [searchCurrent, setSearchCurrent] = useState(0);
const onSearchMatchesChange = useCallback((count: number, current: number) => {
setSearchCount(count);
setSearchCurrent(current);
}, []);
useInput((input, key, event) => {
if (key.ctrl || key.meta) return;
// No Esc handling here β less has no navigating mode. Search state
// (highlights, n/N) is just state. Esc/q/ctrl+c β transcript:exit
// (ungated). Highlights clear on exit via the screen-change effect.
if (input === '/') {
// Capture scrollTop NOW β typing is a preview, 0-matches snaps
// back here. Synchronous ref write, fires before the bar's
// mount-effect calls setSearchQuery.
jumpRef.current?.setAnchor();
setSearchOpen(true);
event.stopImmediatePropagation();
return;
}
// Held-key batching: tokenizer coalesces to 'nnn'. Same uniform-batch
// pattern as modalPagerAction in ScrollKeybindingHandler.tsx. Each
// repeat is a step (n isn't idempotent like g).
const c = input[0];
if ((c === 'n' || c === 'N') && input === c.repeat(input.length) && searchCount > 0) {
const fn = c === 'n' ? jumpRef.current?.nextMatch : jumpRef.current?.prevMatch;
if (fn) for (let i = 0; i < input.length; i++) fn();
event.stopImmediatePropagation();
}
},
// Search needs virtual scroll (jumpRef drives VirtualMessageList). [
// kills it, so !dumpMode β after [ there's nothing to jump in.
{
isActive: screen === 'transcript' && virtualScrollActive && !searchOpen && !dumpMode
});
const {
setQuery: setHighlight,
scanElement,
setPositions
} = useSearchHighlight();
// Resize β abort search. Positions are (msg, query, WIDTH)-keyed β
// cached positions are stale after a width change (new layout, new
// wrapping). Clearing searchQuery triggers VML's setSearchQuery('')
// which clears positionsCache + setPositions(null). Bar closes.
// User hits / again β fresh everything.
const transcriptCols = useTerminalSize().columns;
const prevColsRef = React.useRef(transcriptCols);
React.useEffect(() => {
if (prevColsRef.current !== transcriptCols) {
prevColsRef.current = transcriptCols;
if (searchQuery || searchOpen) {
setSearchOpen(false);
setSearchQuery('');
setSearchCount(0);
setSearchCurrent(0);
jumpRef.current?.disarmSearch();
setHighlight('');
}
}
}, [transcriptCols, searchQuery, searchOpen, setHighlight]);
// Transcript escape hatches. Bare letters in modal context (no prompt
// competing for input) β same class as g/G/j/k in ScrollKeybindingHandler.
useInput((input, key, event) => {
if (key.ctrl || key.meta) return;
if (input === 'q') {
// less: q quits the pager. ctrl+o toggles; q is the lineage exit.
handleExitTranscript();
event.stopImmediatePropagation();
return;
}
if (input === '[' && !dumpMode) {
// Force dump-to-scrollback. Also expand + uncap β no point dumping
// a subset. Terminal/tmux cmd-F can now find anything. Guard here
// (not in isActive) so v still works post-[ β dump-mode footer at
// ~4898 wires editorStatus, confirming v is meant to stay live.
setDumpMode(true);
setShowAllInTranscript(true);
event.stopImmediatePropagation();
} else if (input === 'v') {
// less-style: v opens the file in $VISUAL/$EDITOR. Render the full
// transcript (same path /export uses), write to tmp, hand off.
// openFileInExternalEditor handles alt-screen suspend/resume for
// terminal editors; GUI editors spawn detached.
event.stopImmediatePropagation();
// Drop double-taps: the render is async and a second press before it
// completes would run a second parallel render (double memory, two
// tempfiles, two editor spawns). editorGenRef only guards
// transcript-exit staleness, not same-session concurrency.
if (editorRenderingRef.current) return;
editorRenderingRef.current = true;
// Capture generation + make a staleness-aware setter. Each write
// checks gen (transcript exit bumps it β late writes from the
// async render go silent).
const gen = editorGenRef.current;
const setStatus = (s: string): void => {
if (gen !== editorGenRef.current) return;
clearTimeout(editorTimerRef.current);
setEditorStatus(s);
};
setStatus(`rendering ${deferredMessages.length} messagesβ¦`);
void (async () => {
try {
// Width = terminal minus vim's line-number gutter (4 digits +
// space + slack). Floor at 80. PassThrough has no .columns so
// without this Ink defaults to 80. Trailing-space strip: right-
// aligned timestamps still leave a flexbox spacer run at EOL.
// eslint-disable-next-line custom-rules/prefer-use-terminal-size -- one-shot at keypress time, not a reactive render dep
const w = Math.max(80, (process.stdout.columns ?? 80) - 6);
const raw = await renderMessagesToPlainText(deferredMessages, tools, w);
const text = raw.replace(/[ \t]+$/gm, '');
const path = join(tmpdir(), `cc-transcript-${Date.now()}.txt`);
await writeFile(path, text);
const opened = openFileInExternalEditor(path);
setStatus(opened ? `opening ${path}` : `wrote ${path} Β· no $VISUAL/$EDITOR set`);
} catch (e) {
setStatus(`render failed: ${e instanceof Error ? e.message : String(e)}`);
}
editorRenderingRef.current = false;
if (gen !== editorGenRef.current) return;
editorTimerRef.current = setTimeout(s => s(''), 4000, setEditorStatus);
})();
}
},
// !searchOpen: typing 'v' or '[' in the search bar is search input, not
// a command. No !dumpMode here β v should work after [ (the [ handler
// guards itself inline).
{
isActive: screen === 'transcript' && virtualScrollActive && !searchOpen
});
// Fresh `less` per transcript entry. Prevents stale highlights matching
// unrelated normal-mode text (overlay is alt-screen-global) and avoids
// surprise n/N on re-entry. Same exit resets [ dump mode β each ctrl+o
// entry is a fresh instance.
const inTranscript = screen === 'transcript' && virtualScrollActive;
useEffect(() => {
if (!inTranscript) {
setSearchQuery('');
setSearchCount(0);
setSearchCurrent(0);
setSearchOpen(false);
editorGenRef.current++;
clearTimeout(editorTimerRef.current);
setDumpMode(false);
setEditorStatus('');
}
}, [inTranscript]);
useEffect(() => {
setHighlight(inTranscript ? searchQuery : '');
// Clear the position-based CURRENT (yellow) overlay too. setHighlight
// only clears the scan-based inverse. Without this, the yellow box
// persists at its last screen coords after ctrl-c exits transcript.
if (!inTranscript) setPositions(null);
}, [inTranscript, searchQuery, setHighlight, setPositions]);
const globalKeybindingProps = {
screen,
setScreen,
showAllInTranscript,
setShowAllInTranscript,
messageCount: messages.length,
onEnterTranscript: handleEnterTranscript,
onExitTranscript: handleExitTranscript,
virtualScrollActive,
// Bar-open is a mode (owns keystrokes β j/k type, Esc cancels).
// Navigating (query set, bar closed) is NOT β Esc exits transcript,
// same as less q with highlights still visible. useSearchInput
// doesn't stopPropagation, so without this gate transcript:exit
// would fire on the same Esc that cancels the bar (child registers
// first, fires first, bubbles).
searchBarOpen: searchOpen
};
// Use frozen lengths to slice arrays, avoiding memory overhead of cloning
const transcriptMessages = frozenTranscriptState ? deferredMessages.slice(0, frozenTranscriptState.messagesLength) : deferredMessages;
const transcriptStreamingToolUses = frozenTranscriptState ? streamingToolUses.slice(0, frozenTranscriptState.streamingToolUsesLength) : streamingToolUses;
// Handle shift+down for teammate navigation and background task management.
// Guard onOpenBackgroundTasks when a local-jsx dialog (e.g. /mcp) is open β
// otherwise Shift+Down stacks BackgroundTasksDialog on top and deadlocks input.
useBackgroundTaskNavigation({
onOpenBackgroundTasks: isShowingLocalJSXCommand ? undefined : () => setShowBashesDialog(true)
});
// Auto-exit viewing mode when teammate completes or errors
useTeammateViewAutoExit();
if (screen === 'transcript') {
// Virtual scroll replaces the 30-message cap: everything is scrollable
// and memory is bounded by the viewport. Without it, wrapping transcript
// in a ScrollBox would mount all messages (~250 MB on long sessions β
// the exact problem), so the kill switch and non-fullscreen paths must
// fall through to the legacy render: no alt screen, dump to terminal
// scrollback, 30-cap + Ctrl+E. Reusing scrollRef is safe β normal-mode
// and transcript-mode are mutually exclusive (this early return), so
// only one ScrollBox is ever mounted at a time.
const transcriptScrollRef = isFullscreenEnvEnabled() && !disableVirtualScroll && !dumpMode ? scrollRef : undefined;
const transcriptMessagesElement = <Messages messages={transcriptMessages} tools={tools} commands={commands} verbose={true} toolJSX={null} toolUseConfirmQueue={[]} inProgressToolUseIDs={inProgressToolUseIDs} isMessageSelectorVisible={false} conversationId={conversationId} screen={screen} agentDefinitions={agentDefinitions} streamingToolUses={transcriptStreamingToolUses} showAllInTranscript={showAllInTranscript} onOpenRateLimitOptions={handleOpenRateLimitOptions} isLoading={isLoading} hidePastThinking={true} streamingThinking={streamingThinking} scrollRef={transcriptScrollRef} jumpRef={jumpRef} onSearchMatchesChange={onSearchMatchesChange} scanElement={scanElement} setPositions={setPositions} disableRenderCap={dumpMode} />;
const transcriptToolJSX = toolJSX && <Box flexDirection="column" width="100%">
{toolJSX.jsx}
</Box>;
const transcriptReturn = <KeybindingSetup>
<AnimatedTerminalTitle isAnimating={titleIsAnimating} title={terminalTitle} disabled={titleDisabled} noPrefix={showStatusInTerminalTab} />
<GlobalKeybindingHandlers {...globalKeybindingProps} />
{feature('VOICE_MODE') ? <VoiceKeybindingHandler voiceHandleKeyEvent={voice.handleKeyEvent} stripTrailing={voice.stripTrailing} resetAnchor={voice.resetAnchor} isActive={!toolJSX?.isLocalJSXCommand} /> : null}
<CommandKeybindingHandlers onSubmit={onSubmit} isActive={!toolJSX?.isLocalJSXCommand} />
{transcriptScrollRef ?
// ScrollKeybindingHandler must mount before CancelRequestHandler so
// ctrl+c-with-selection copies instead of cancelling the active task.
// Its raw useInput handler only stops propagation when a selection
// exists β without one, ctrl+c falls through to CancelRequestHandler.
<ScrollKeybindingHandler scrollRef={scrollRef}
// Yield wheel/ctrl+u/d to UltraplanChoiceDialog's own scroll
// handler while the modal is showing.
isActive={focusedInputDialog !== 'ultraplan-choice'}
// g/G/j/k/ctrl+u/ctrl+d would eat keystrokes the search bar
// wants. Off while searching.
isModal={!searchOpen}
// Manual scroll exits the search context β clear the yellow
// current-match marker. Positions are (msg, rowOffset)-keyed;
// j/k changes scrollTop so rowOffset is stale β wrong row
// gets yellow. Next n/N re-establishes via step()βjump().
onScroll={() => jumpRef.current?.disarmSearch()} /> : null}
<CancelRequestHandler {...cancelRequestProps} />
{transcriptScrollRef ? <FullscreenLayout scrollRef={scrollRef} scrollable={<>
{transcriptMessagesElement}
{transcriptToolJSX}
<SandboxViolationExpandedView />
</>} bottom={searchOpen ? <TranscriptSearchBar jumpRef={jumpRef}
// Seed was tried (c01578c8) β broke /hello muscle
// memory (cursor lands after 'foo', /hello β foohello).
// Cancel-restore handles the 'don't lose prior search'
// concern differently (onCancel re-applies searchQuery).
initialQuery="" count={searchCount} current={searchCurrent} onClose={q => {
// Enter β commit. 0-match guard: junk query shouldn't
// persist (badge hidden, n/N dead anyway).
setSearchQuery(searchCount > 0 ? q : '');
setSearchOpen(false);
// onCancel path: bar unmounts before its useEffect([query])
// can fire with ''. Without this, searchCount stays stale
// (n guard at :4956 passes) and VML's matches[] too
// (nextMatch walks the old array). Phantom nav, no
// highlight. onExit (Enter, q non-empty) still commits.
if (!q) {
setSearchCount(0);
setSearchCurrent(0);
jumpRef.current?.setSearchQuery('');
}
}} onCancel={() => {
// Esc/ctrl+c/ctrl+g β undo. Bar's effect last fired
// with whatever was typed. searchQuery (REPL state)
// is unchanged since / (onClose = commit, didn't run).
// Two VML calls: '' restores anchor (0-match else-
// branch), then searchQuery re-scans from anchor's
// nearest. Both synchronous β one React batch.
// setHighlight explicit: REPL's sync-effect dep is
// searchQuery (unchanged), wouldn't re-fire.
setSearchOpen(false);
jumpRef.current?.setSearchQuery('');
jumpRef.current?.setSearchQuery(searchQuery);
setHighlight(searchQuery);
}} setHighlight={setHighlight} /> : <TranscriptModeFooter showAllInTranscript={showAllInTranscript} virtualScroll={true} status={editorStatus || undefined} searchBadge={searchQuery && searchCount > 0 ? {
current: searchCurrent,
count: searchCount
} : undefined} />} /> : <>
{transcriptMessagesElement}
{transcriptToolJSX}
<SandboxViolationExpandedView />
<TranscriptModeFooter showAllInTranscript={showAllInTranscript} virtualScroll={false} suppressShowAll={dumpMode} status={editorStatus || undefined} />
</>}
</KeybindingSetup>;
// The virtual-scroll branch (FullscreenLayout above) needs
// <AlternateScreen>'s <Box height={rows}> constraint β without it,
// ScrollBox's flexGrow has no ceiling, viewport = content height,
// scrollTop pins at 0, and Ink's screen buffer sizes to the full
// spacer (200Γ5k+ rows on long sessions). Same root type + props as
// normal mode's wrap below so React reconciles and the alt buffer
// stays entered across toggle. The 30-cap dump branch stays
// unwrapped β it wants native terminal scrollback.
if (transcriptScrollRef) {
return <AlternateScreen mouseTracking={isMouseTrackingEnabled()}>
{transcriptReturn}
</AlternateScreen>;
}
return transcriptReturn;
}
// Get viewed agent task (inlined from selectors for explicit data flow).
// viewedAgentTask: teammate OR local_agent β drives the boolean checks
// below. viewedTeammateTask: teammate-only narrowed, for teammate-specific
// field access (inProgressToolUseIDs).
const viewedTask = viewingAgentTaskId ? tasks[viewingAgentTaskId] : undefined;
const viewedTeammateTask = viewedTask && isInProcessTeammateTask(viewedTask) ? viewedTask : undefined;
const viewedAgentTask = viewedTeammateTask ?? (viewedTask && isLocalAgentTask(viewedTask) ? viewedTask : undefined);
// Bypass useDeferredValue when streaming text is showing so Messages renders
// the final message in the same frame streaming text clears. Also bypass when
// not loading β deferredMessages only matters during streaming (keeps input
// responsive); after the turn ends, showing messages immediately prevents a
// jitter gap where the spinner is gone but the answer hasn't appeared yet.
// Only reducedMotion users keep the deferred path during loading.
const usesSyncMessages = showStreamingText || !isLoading;
// When viewing an agent, never fall through to leader β empty until
// bootstrap/stream fills. Closes the see-leader-type-agent footgun.
const displayedMessages = viewedAgentTask ? viewedAgentTask.messages ?? [] : usesSyncMessages ? messages : deferredMessages;
// Show the placeholder until the real user message appears in
// displayedMessages. userInputOnProcessing stays set for the whole turn
// (cleared in resetLoadingState); this length check hides it once
// displayedMessages grows past the baseline captured at submit time.
// Covers both gaps: before setMessages is called (processUserInput), and
// while deferredMessages lags behind messages. Suppressed when viewing an
// agent β displayedMessages is a different array there, and onAgentSubmit
// doesn't use the placeholder anyway.
const placeholderText = userInputOnProcessing && !viewedAgentTask && displayedMessages.length <= userInputBaselineRef.current ? userInputOnProcessing : undefined;
const toolPermissionOverlay = focusedInputDialog === 'tool-permission' ? <PermissionRequest key={toolUseConfirmQueue[0]?.toolUseID} onDone={() => setToolUseConfirmQueue(([_, ...tail]) => tail)} onReject={handleQueuedCommandOnCancel} toolUseConfirm={toolUseConfirmQueue[0]!} toolUseContext={getToolUseContext(messages, messages, abortController ?? createAbortController(), mainLoopModel)} verbose={verbose} workerBadge={toolUseConfirmQueue[0]?.workerBadge} setStickyFooter={isFullscreenEnvEnabled() ? setPermissionStickyFooter : undefined} /> : null;
// Narrow terminals: companion collapses to a one-liner that REPL stacks
// on its own row (above input in fullscreen, below in scrollback) instead
// of row-beside. Wide terminals keep the row layout with sprite on the right.
const companionNarrow = transcriptCols < MIN_COLS_FOR_FULL_SPRITE;
// Hide the sprite when PromptInput early-returns BackgroundTasksDialog.
// The sprite sits as a row sibling of PromptInput, so the dialog's Pane
// divider draws at useTerminalSize() width but only gets terminalWidth -
// spriteWidth β divider stops short and dialog text wraps early. Don't
// check footerSelection: pill FOCUS (arrow-down to tasks pill) must keep
// the sprite visible so arrow-right can navigate to it.
const companionVisible = !toolJSX?.shouldHidePromptInput && !focusedInputDialog && !showBashesDialog;
// In fullscreen, ALL local-jsx slash commands float in the modal slot β
// FullscreenLayout wraps them in an absolute-positioned bottom-anchored
// pane (β divider, ModalContext). Pane/Dialog inside detect the context
// and skip their own top-level frame. Non-fullscreen keeps the inline
// render paths below. Commands that used to route through bottom
// (immediate: /model, /mcp, /btw, ...) and scrollable (non-immediate:
// /config, /theme, /diff, ...) both go here now.
const toolJsxCentered = isFullscreenEnvEnabled() && toolJSX?.isLocalJSXCommand === true;
const centeredModal: React.ReactNode = toolJsxCentered ? toolJSX!.jsx : null;
// <AlternateScreen> at the root: everything below is inside its
// <Box height={rows}>. Handlers/contexts are zero-height so ScrollBox's
// flexGrow in FullscreenLayout resolves against this Box. The transcript
// early return above wraps its virtual-scroll branch the same way; only
// the 30-cap dump branch stays unwrapped for native terminal scrollback.
const mainReturn = <KeybindingSetup>
<AnimatedTerminalTitle isAnimating={titleIsAnimating} title={terminalTitle} disabled={titleDisabled} noPrefix={showStatusInTerminalTab} />
<GlobalKeybindingHandlers {...globalKeybindingProps} />
{feature('VOICE_MODE') ? <VoiceKeybindingHandler voiceHandleKeyEvent={voice.handleKeyEvent} stripTrailing={voice.stripTrailing} resetAnchor={voice.resetAnchor} isActive={!toolJSX?.isLocalJSXCommand} /> : null}
<CommandKeybindingHandlers onSubmit={onSubmit} isActive={!toolJSX?.isLocalJSXCommand} />
{/* ScrollKeybindingHandler must mount before CancelRequestHandler so
ctrl+c-with-selection copies instead of cancelling the active task.
Its raw useInput handler only stops propagation when a selection
exists β without one, ctrl+c falls through to CancelRequestHandler.
PgUp/PgDn/wheel always scroll the transcript behind the modal β
the modal's inner ScrollBox is not keyboard-driven. onScroll
stays suppressed while a modal is showing so scroll doesn't
stamp divider/pill state. */}
<ScrollKeybindingHandler scrollRef={scrollRef} isActive={isFullscreenEnvEnabled() && (centeredModal != null || !focusedInputDialog || focusedInputDialog === 'tool-permission')} onScroll={centeredModal || toolPermissionOverlay || viewedAgentTask ? undefined : composedOnScroll} />
{feature('MESSAGE_ACTIONS') && isFullscreenEnvEnabled() && !disableMessageActions ? <MessageActionsKeybindings handlers={messageActionHandlers} isActive={cursor !== null} /> : null}
<CancelRequestHandler {...cancelRequestProps} />
<MCPConnectionManager key={remountKey} dynamicMcpConfig={dynamicMcpConfig} isStrictMcpConfig={strictMcpConfig}>
<FullscreenLayout scrollRef={scrollRef} overlay={toolPermissionOverlay} bottomFloat={feature('BUDDY') && companionVisible && !companionNarrow ? <CompanionFloatingBubble /> : undefined} modal={centeredModal} modalScrollRef={modalScrollRef} dividerYRef={dividerYRef} hidePill={!!viewedAgentTask} hideSticky={!!viewedTeammateTask} newMessageCount={unseenDivider?.count ?? 0} onPillClick={() => {
setCursor(null);
jumpToNew(scrollRef.current);
}} scrollable={<>
<TeammateViewHeader />
<Messages messages={displayedMessages} tools={tools} commands={commands} verbose={verbose} toolJSX={toolJSX} toolUseConfirmQueue={toolUseConfirmQueue} inProgressToolUseIDs={viewedTeammateTask ? viewedTeammateTask.inProgressToolUseIDs ?? new Set() : inProgressToolUseIDs} isMessageSelectorVisible={isMessageSelectorVisible} conversationId={conversationId} screen={screen} streamingToolUses={streamingToolUses} showAllInTranscript={showAllInTranscript} agentDefinitions={agentDefinitions} onOpenRateLimitOptions={handleOpenRateLimitOptions} isLoading={isLoading} streamingText={isLoading && !viewedAgentTask ? visibleStreamingText : null} isBriefOnly={viewedAgentTask ? false : isBriefOnly} unseenDivider={viewedAgentTask ? undefined : unseenDivider} scrollRef={isFullscreenEnvEnabled() ? scrollRef : undefined} trackStickyPrompt={isFullscreenEnvEnabled() ? true : undefined} cursor={cursor} setCursor={setCursor} cursorNavRef={cursorNavRef} />
<AwsAuthStatusBox />
{/* Hide the processing placeholder while a modal is showing β
it would sit at the last visible transcript row right above
the β divider, showing "β― /config" as redundant clutter
(the modal IS the /config UI). Outside modals it stays so
the user sees their input echoed while Claude processes. */}
{!disabled && placeholderText && !centeredModal && <UserTextMessage param={{
text: placeholderText,
type: 'text'
}} addMargin={true} verbose={verbose} />}
{toolJSX && !(toolJSX.isLocalJSXCommand && toolJSX.isImmediate) && !toolJsxCentered && <Box flexDirection="column" width="100%">
{toolJSX.jsx}
</Box>}
{"external" === 'ant' && <TungstenLiveMonitor />}
{feature('WEB_BROWSER_TOOL') ? WebBrowserPanelModule && <WebBrowserPanelModule.WebBrowserPanel /> : null}
<Box flexGrow={1} />
{showSpinner && <SpinnerWithVerb mode={streamMode} spinnerTip={spinnerTip} responseLengthRef={responseLengthRef} apiMetricsRef={apiMetricsRef} overrideMessage={spinnerMessage} spinnerSuffix={stopHookSpinnerSuffix} verbose={verbose} loadingStartTimeRef={loadingStartTimeRef} totalPausedMsRef={totalPausedMsRef} pauseStartTimeRef={pauseStartTimeRef} overrideColor={spinnerColor} overrideShimmerColor={spinnerShimmerColor} hasActiveTools={inProgressToolUseIDs.size > 0} leaderIsIdle={!isLoading} />}
{!showSpinner && !isLoading && !userInputOnProcessing && !hasRunningTeammates && isBriefOnly && !viewedAgentTask && <BriefIdleStatus />}
{isFullscreenEnvEnabled() && <PromptInputQueuedCommands />}
</>} bottom={<Box flexDirection={feature('BUDDY') && companionNarrow ? 'column' : 'row'} width="100%" alignItems={feature('BUDDY') && companionNarrow ? undefined : 'flex-end'}>
{feature('BUDDY') && companionNarrow && isFullscreenEnvEnabled() && companionVisible ? <CompanionSprite /> : null}
<Box flexDirection="column" flexGrow={1}>
{permissionStickyFooter}
{/* Immediate local-jsx commands (/btw, /sandbox, /assistant,
/issue) render here, NOT inside scrollable. They stay mounted
while the main conversation streams behind them, so ScrollBox
relayouts on each new message would drag them around. bottom
is flexShrink={0} outside the ScrollBox β it never moves.
Non-immediate local-jsx (/diff, /status, /theme, ~40 others)
stays in scrollable: the main loop is paused so no jiggle,
and their tall content (DiffDetailView renders up to 400
lines with no internal scroll) needs the outer ScrollBox. */}
{toolJSX?.isLocalJSXCommand && toolJSX.isImmediate && !toolJsxCentered && <Box flexDirection="column" width="100%">
{toolJSX.jsx}
</Box>}
{!showSpinner && !toolJSX?.isLocalJSXCommand && showExpandedTodos && tasksV2 && tasksV2.length > 0 && <Box width="100%" flexDirection="column">
<TaskListV2 tasks={tasksV2} isStandalone={true} />
</Box>}
{focusedInputDialog === 'sandbox-permission' && <SandboxPermissionRequest key={sandboxPermissionRequestQueue[0]!.hostPattern.host} hostPattern={sandboxPermissionRequestQueue[0]!.hostPattern} onUserResponse={(response: {
allow: boolean;
persistToSettings: boolean;
}) => {
const {
allow,
persistToSettings
} = response;
const currentRequest = sandboxPermissionRequestQueue[0];
if (!currentRequest) return;
const approvedHost = currentRequest.hostPattern.host;
if (persistToSettings) {
const update = {
type: 'addRules' as const,
rules: [{
toolName: WEB_FETCH_TOOL_NAME,
ruleContent: `domain:${approvedHost}`
}],
behavior: (allow ? 'allow' : 'deny') as 'allow' | 'deny',
destination: 'localSettings' as const
};
setAppState(prev => ({
...prev,
toolPermissionContext: applyPermissionUpdate(prev.toolPermissionContext, update)
}));
persistPermissionUpdate(update);
// Immediately update sandbox in-memory config to prevent race conditions
// where pending requests slip through before settings change is detected
SandboxManager.refreshConfig();
}
// Resolve ALL pending requests for the same host (not just the first one)
// This handles the case where multiple parallel requests came in for the same domain
setSandboxPermissionRequestQueue(queue => {
queue.filter(item => item.hostPattern.host === approvedHost).forEach(item => item.resolvePromise(allow));
return queue.filter(item => item.hostPattern.host !== approvedHost);
});
// Clean up bridge subscriptions and cancel remote prompts
// for this host since the local user already responded.
const cleanups = sandboxBridgeCleanupRef.current.get(approvedHost);
if (cleanups) {
for (const fn of cleanups) {
fn();
}
sandboxBridgeCleanupRef.current.delete(approvedHost);
}
}} />}
{focusedInputDialog === 'prompt' && <PromptDialog key={promptQueue[0]!.request.prompt} title={promptQueue[0]!.title} toolInputSummary={promptQueue[0]!.toolInputSummary} request={promptQueue[0]!.request} onRespond={selectedKey => {
const item = promptQueue[0];
if (!item) return;
item.resolve({
prompt_response: item.request.prompt,
selected: selectedKey
});
setPromptQueue(([, ...tail]) => tail);
}} onAbort={() => {
const item = promptQueue[0];
if (!item) return;
item.reject(new Error('Prompt cancelled by user'));
setPromptQueue(([, ...tail]) => tail);
}} />}
{/* Show pending indicator on worker while waiting for leader approval */}
{pendingWorkerRequest && <WorkerPendingPermission toolName={pendingWorkerRequest.toolName} description={pendingWorkerRequest.description} />}
{/* Show pending indicator for sandbox permission on worker side */}
{pendingSandboxRequest && <WorkerPendingPermission toolName="Network Access" description={`Waiting for leader to approve network access to ${pendingSandboxRequest.host}`} />}
{/* Worker sandbox permission requests from swarm workers */}
{focusedInputDialog === 'worker-sandbox-permission' && <SandboxPermissionRequest key={workerSandboxPermissions.queue[0]!.requestId} hostPattern={{
host: workerSandboxPermissions.queue[0]!.host,
port: undefined
} as NetworkHostPattern} onUserResponse={(response: {
allow: boolean;
persistToSettings: boolean;
}) => {
const {
allow,
persistToSettings
} = response;
const currentRequest = workerSandboxPermissions.queue[0];
if (!currentRequest) return;
const approvedHost = currentRequest.host;
// Send response via mailbox to the worker
void sendSandboxPermissionResponseViaMailbox(currentRequest.workerName, currentRequest.requestId, approvedHost, allow, teamContext?.teamName);
if (persistToSettings && allow) {
const update = {
type: 'addRules' as const,
rules: [{
toolName: WEB_FETCH_TOOL_NAME,
ruleContent: `domain:${approvedHost}`
}],
behavior: 'allow' as const,
destination: 'localSettings' as const
};
setAppState(prev => ({
...prev,
toolPermissionContext: applyPermissionUpdate(prev.toolPermissionContext, update)
}));
persistPermissionUpdate(update);
SandboxManager.refreshConfig();
}
// Remove from queue
setAppState(prev => ({
...prev,
workerSandboxPermissions: {
...prev.workerSandboxPermissions,
queue: prev.workerSandboxPermissions.queue.slice(1)
}
}));
}} />}
{focusedInputDialog === 'elicitation' && <ElicitationDialog key={elicitation.queue[0]!.serverName + ':' + String(elicitation.queue[0]!.requestId)} event={elicitation.queue[0]!} onResponse={(action, content) => {
const currentRequest = elicitation.queue[0];
if (!currentRequest) return;
// Call respond callback to resolve Promise
currentRequest.respond({
action,
content
});
// For URL accept, keep in queue for phase 2
const isUrlAccept = currentRequest.params.mode === 'url' && action === 'accept';
if (!isUrlAccept) {
setAppState(prev => ({
...prev,
elicitation: {
queue: prev.elicitation.queue.slice(1)
}
}));
}
}} onWaitingDismiss={action => {
const currentRequest = elicitation.queue[0];
// Remove from queue
setAppState(prev => ({
...prev,
elicitation: {
queue: prev.elicitation.queue.slice(1)
}
}));
currentRequest?.onWaitingDismiss?.(action);
}} />}
{focusedInputDialog === 'cost' && <CostThresholdDialog onDone={() => {
setShowCostDialog(false);
setHaveShownCostDialog(true);
saveGlobalConfig(current => ({
...current,
hasAcknowledgedCostThreshold: true
}));
logEvent('tengu_cost_threshold_acknowledged', {});
}} />}
{focusedInputDialog === 'idle-return' && idleReturnPending && <IdleReturnDialog idleMinutes={idleReturnPending.idleMinutes} totalInputTokens={getTotalInputTokens()} onDone={async action => {
const pending = idleReturnPending;
setIdleReturnPending(null);
logEvent('tengu_idle_return_action', {
action: action as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
idleMinutes: Math.round(pending.idleMinutes),
messageCount: messagesRef.current.length,
totalInputTokens: getTotalInputTokens()
});
if (action === 'dismiss') {
setInputValue(pending.input);
return;
}
if (action === 'never') {
saveGlobalConfig(current => {
if (current.idleReturnDismissed) return current;
return {
...current,
idleReturnDismissed: true
};
});
}
if (action === 'clear') {
const {
clearConversation
} = await import('../commands/clear/conversation.js');
await clearConversation({
setMessages,
readFileState: readFileState.current,
discoveredSkillNames: discoveredSkillNamesRef.current,
loadedNestedMemoryPaths: loadedNestedMemoryPathsRef.current,
getAppState: () => store.getState(),
setAppState,
setConversationId
});
haikuTitleAttemptedRef.current = false;
setHaikuTitle(undefined);
bashTools.current.clear();
bashToolsProcessedIdx.current = 0;
}
skipIdleCheckRef.current = true;
void onSubmitRef.current(pending.input, {
setCursorOffset: () => {},
clearBuffer: () => {},
resetHistory: () => {}
});
}} />}
{focusedInputDialog === 'ide-onboarding' && <IdeOnboardingDialog onDone={() => setShowIdeOnboarding(false)} installationStatus={ideInstallationStatus} />}
{"external" === 'ant' && focusedInputDialog === 'model-switch' && AntModelSwitchCallout && <AntModelSwitchCallout onDone={(selection: string, modelAlias?: string) => {
setShowModelSwitchCallout(false);
if (selection === 'switch' && modelAlias) {
setAppState(prev => ({
...prev,
mainLoopModel: modelAlias,
mainLoopModelForSession: null
}));
}
}} />}
{"external" === 'ant' && focusedInputDialog === 'undercover-callout' && UndercoverAutoCallout && <UndercoverAutoCallout onDone={() => setShowUndercoverCallout(false)} />}
{focusedInputDialog === 'effort-callout' && <EffortCallout model={mainLoopModel} onDone={selection => {
setShowEffortCallout(false);
if (selection !== 'dismiss') {
setAppState(prev => ({
...prev,
effortValue: selection
}));
}
}} />}
{focusedInputDialog === 'remote-callout' && <RemoteCallout onDone={selection => {
setAppState(prev => {
if (!prev.showRemoteCallout) return prev;
return {
...prev,
showRemoteCallout: false,
...(selection === 'enable' && {
replBridgeEnabled: true,
replBridgeExplicit: true,
replBridgeOutboundOnly: false
})
};
});
}} />}
{exitFlow}
{focusedInputDialog === 'plugin-hint' && hintRecommendation && <PluginHintMenu pluginName={hintRecommendation.pluginName} pluginDescription={hintRecommendation.pluginDescription} marketplaceName={hintRecommendation.marketplaceName} sourceCommand={hintRecommendation.sourceCommand} onResponse={handleHintResponse} />}
{focusedInputDialog === 'lsp-recommendation' && lspRecommendation && <LspRecommendationMenu pluginName={lspRecommendation.pluginName} pluginDescription={lspRecommendation.pluginDescription} fileExtension={lspRecommendation.fileExtension} onResponse={handleLspResponse} />}
{focusedInputDialog === 'desktop-upsell' && <DesktopUpsellStartup onDone={() => setShowDesktopUpsellStartup(false)} />}
{feature('ULTRAPLAN') ? focusedInputDialog === 'ultraplan-choice' && ultraplanPendingChoice && <UltraplanChoiceDialog plan={ultraplanPendingChoice.plan} sessionId={ultraplanPendingChoice.sessionId} taskId={ultraplanPendingChoice.taskId} setMessages={setMessages} readFileState={readFileState.current} getAppState={() => store.getState()} setConversationId={setConversationId} /> : null}
{feature('ULTRAPLAN') ? focusedInputDialog === 'ultraplan-launch' && ultraplanLaunchPending && <UltraplanLaunchDialog onChoice={(choice, opts) => {
const blurb = ultraplanLaunchPending.blurb;
setAppState(prev => prev.ultraplanLaunchPending ? {
...prev,
ultraplanLaunchPending: undefined
} : prev);
if (choice === 'cancel') return;
// Command's onDone used display:'skip', so add the
// echo here β gives immediate feedback before the
// ~5s teleportToRemote resolves.
setMessages(prev => [...prev, createCommandInputMessage(formatCommandInputTags('ultraplan', blurb))]);
const appendStdout = (msg: string) => setMessages(prev => [...prev, createCommandInputMessage(`<${LOCAL_COMMAND_STDOUT_TAG}>${escapeXml(msg)}</${LOCAL_COMMAND_STDOUT_TAG}>`)]);
// Defer the second message if a query is mid-turn
// so it lands after the assistant reply, not
// between the user's prompt and the reply.
const appendWhenIdle = (msg: string) => {
if (!queryGuard.isActive) {
appendStdout(msg);
return;
}
const unsub = queryGuard.subscribe(() => {
if (queryGuard.isActive) return;
unsub();
// Skip if the user stopped ultraplan while we
// were waiting β avoids a stale "Monitoring
// <url>" message for a session that's gone.
if (!store.getState().ultraplanSessionUrl) return;
appendStdout(msg);
});
};
void launchUltraplan({
blurb,
getAppState: () => store.getState(),
setAppState,
signal: createAbortController().signal,
disconnectedBridge: opts?.disconnectedBridge,
onSessionReady: appendWhenIdle
}).then(appendStdout).catch(logError);
}} /> : null}
{mrRender()}
{!toolJSX?.shouldHidePromptInput && !focusedInputDialog && !isExiting && !disabled && !cursor && <>
{autoRunIssueReason && <AutoRunIssueNotification onRun={handleAutoRunIssue} onCancel={handleCancelAutoRunIssue} reason={getAutoRunIssueReasonText(autoRunIssueReason)} />}
{postCompactSurvey.state !== 'closed' ? <FeedbackSurvey state={postCompactSurvey.state} lastResponse={postCompactSurvey.lastResponse} handleSelect={postCompactSurvey.handleSelect} inputValue={inputValue} setInputValue={setInputValue} onRequestFeedback={handleSurveyRequestFeedback} /> : memorySurvey.state !== 'closed' ? <FeedbackSurvey state={memorySurvey.state} lastResponse={memorySurvey.lastResponse} handleSelect={memorySurvey.handleSelect} handleTranscriptSelect={memorySurvey.handleTranscriptSelect} inputValue={inputValue} setInputValue={setInputValue} onRequestFeedback={handleSurveyRequestFeedback} message="How well did Claude use its memory? (optional)" /> : <FeedbackSurvey state={feedbackSurvey.state} lastResponse={feedbackSurvey.lastResponse} handleSelect={feedbackSurvey.handleSelect} handleTranscriptSelect={feedbackSurvey.handleTranscriptSelect} inputValue={inputValue} setInputValue={setInputValue} onRequestFeedback={didAutoRunIssueRef.current ? undefined : handleSurveyRequestFeedback} />}
{/* Frustration-triggered transcript sharing prompt */}
{frustrationDetection.state !== 'closed' && <FeedbackSurvey state={frustrationDetection.state} lastResponse={null} handleSelect={() => {}} handleTranscriptSelect={frustrationDetection.handleTranscriptSelect} inputValue={inputValue} setInputValue={setInputValue} />}
{/* Skill improvement survey - appears when improvements detected (ant-only) */}
{"external" === 'ant' && skillImprovementSurvey.suggestion && <SkillImprovementSurvey isOpen={skillImprovementSurvey.isOpen} skillName={skillImprovementSurvey.suggestion.skillName} updates={skillImprovementSurvey.suggestion.updates} handleSelect={skillImprovementSurvey.handleSelect} inputValue={inputValue} setInputValue={setInputValue} />}
{showIssueFlagBanner && <IssueFlagBanner />}
{}
<PromptInput debug={debug} ideSelection={ideSelection} hasSuppressedDialogs={!!hasSuppressedDialogs} isLocalJSXCommandActive={isShowingLocalJSXCommand} getToolUseContext={getToolUseContext} toolPermissionContext={toolPermissionContext} setToolPermissionContext={setToolPermissionContext} apiKeyStatus={apiKeyStatus} commands={commands} agents={agentDefinitions.activeAgents} isLoading={isLoading} onExit={handleExit} verbose={verbose} messages={messages} onAutoUpdaterResult={setAutoUpdaterResult} autoUpdaterResult={autoUpdaterResult} input={inputValue} onInputChange={setInputValue} mode={inputMode} onModeChange={setInputMode} stashedPrompt={stashedPrompt} setStashedPrompt={setStashedPrompt} submitCount={submitCount} onShowMessageSelector={handleShowMessageSelector} onMessageActionsEnter={
// Works during isLoading β edit cancels first; uuid selection survives appends.
feature('MESSAGE_ACTIONS') && isFullscreenEnvEnabled() && !disableMessageActions ? enterMessageActions : undefined} mcpClients={mcpClients} pastedContents={pastedContents} setPastedContents={setPastedContents} vimMode={vimMode} setVimMode={setVimMode} showBashesDialog={showBashesDialog} setShowBashesDialog={setShowBashesDialog} onSubmit={onSubmit} onAgentSubmit={onAgentSubmit} isSearchingHistory={isSearchingHistory} setIsSearchingHistory={setIsSearchingHistory} helpOpen={isHelpOpen} setHelpOpen={setIsHelpOpen} insertTextRef={feature('VOICE_MODE') ? insertTextRef : undefined} voiceInterimRange={voice.interimRange} />
<SessionBackgroundHint onBackgroundSession={handleBackgroundSession} isLoading={isLoading} />
</>}
{cursor &&
// inputValue is REPL state; typed text survives the round-trip.
<MessageActionsBar cursor={cursor} />}
{focusedInputDialog === 'message-selector' && <MessageSelector messages={messages} preselectedMessage={messageSelectorPreselect} onPreRestore={onCancel} onRestoreCode={async (message: UserMessage) => {
await fileHistoryRewind((updater: (prev: FileHistoryState) => FileHistoryState) => {
setAppState(prev => ({
...prev,
fileHistory: updater(prev.fileHistory)
}));
}, message.uuid);
}} onSummarize={async (message: UserMessage, feedback?: string, direction: PartialCompactDirection = 'from') => {
// Project snipped messages so the compact model
// doesn't summarize content that was intentionally removed.
const compactMessages = getMessagesAfterCompactBoundary(messages);
const messageIndex = compactMessages.indexOf(message);
if (messageIndex === -1) {
// Selected a snipped or pre-compact message that the
// selector still shows (REPL keeps full history for
// scrollback). Surface why nothing happened instead
// of silently no-oping.
setMessages(prev => [...prev, createSystemMessage('That message is no longer in the active context (snipped or pre-compact). Choose a more recent message.', 'warning')]);
return;
}
const newAbortController = createAbortController();
const context = getToolUseContext(compactMessages, [], newAbortController, mainLoopModel);
const appState = context.getAppState();
const defaultSysPrompt = await getSystemPrompt(context.options.tools, context.options.mainLoopModel, Array.from(appState.toolPermissionContext.additionalWorkingDirectories.keys()), context.options.mcpClients);
const systemPrompt = buildEffectiveSystemPrompt({
mainThreadAgentDefinition: undefined,
toolUseContext: context,
customSystemPrompt: context.options.customSystemPrompt,
defaultSystemPrompt: defaultSysPrompt,
appendSystemPrompt: context.options.appendSystemPrompt
});
const [userContext, systemContext] = await Promise.all([getUserContext(), getSystemContext()]);
const result = await partialCompactConversation(compactMessages, messageIndex, context, {
systemPrompt,
userContext,
systemContext,
toolUseContext: context,
forkContextMessages: compactMessages
}, feedback, direction);
const kept = result.messagesToKeep ?? [];
const ordered = direction === 'up_to' ? [...result.summaryMessages, ...kept] : [...kept, ...result.summaryMessages];
const postCompact = [result.boundaryMarker, ...ordered, ...result.attachments, ...result.hookResults];
// Fullscreen 'from' keeps scrollback; 'up_to' must not
// (old[0] unchanged + grown array means incremental
// useLogMessages path, so boundary never persisted).
// Find by uuid since old is raw REPL history and snipped
// entries can shift the projected messageIndex.
if (isFullscreenEnvEnabled() && direction === 'from') {
setMessages(old => {
const rawIdx = old.findIndex(m => m.uuid === message.uuid);
return [...old.slice(0, rawIdx === -1 ? 0 : rawIdx), ...postCompact];
});
} else {
setMessages(postCompact);
}
// Partial compact bypasses handleMessageFromStream β clear
// the context-blocked flag so proactive ticks resume.
if (feature('PROACTIVE') || feature('KAIROS')) {
proactiveModule?.setContextBlocked(false);
}
setConversationId(randomUUID());
runPostCompactCleanup(context.options.querySource);
if (direction === 'from') {
const r = textForResubmit(message);
if (r) {
setInputValue(r.text);
setInputMode(r.mode);
}
}
// Show notification with ctrl+o hint
const historyShortcut = getShortcutDisplay('app:toggleTranscript', 'Global', 'ctrl+o');
addNotification({
key: 'summarize-ctrl-o-hint',
text: `Conversation summarized (${historyShortcut} for history)`,
priority: 'medium',
timeoutMs: 8000
});
}} onRestoreMessage={handleRestoreMessage} onClose={() => {
setIsMessageSelectorVisible(false);
setMessageSelectorPreselect(undefined);
}} />}
{"external" === 'ant' && <DevBar />}
</Box>
{feature('BUDDY') && !(companionNarrow && isFullscreenEnvEnabled()) && companionVisible ? <CompanionSprite /> : null}
</Box>} />
</MCPConnectionManager>
</KeybindingSetup>;
if (isFullscreenEnvEnabled()) {
return <AlternateScreen mouseTracking={isMouseTrackingEnabled()}>
{mainReturn}
</AlternateScreen>;
}
return mainReturn;
}
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJmZWF0dXJlIiwic3Bhd25TeW5jIiwic25hcHNob3RPdXRwdXRUb2tlbnNGb3JUdXJuIiwiZ2V0Q3VycmVudFR1cm5Ub2tlbkJ1ZGdldCIsImdldFR1cm5PdXRwdXRUb2tlbnMiLCJnZXRCdWRnZXRDb250aW51YXRpb25Db3VudCIsImdldFRvdGFsSW5wdXRUb2tlbnMiLCJwYXJzZVRva2VuQnVkZ2V0IiwiY291bnQiLCJkaXJuYW1lIiwiam9pbiIsInRtcGRpciIsImZpZ3VyZXMiLCJ1c2VJbnB1dCIsInVzZVNlYXJjaElucHV0IiwidXNlVGVybWluYWxTaXplIiwidXNlU2VhcmNoSGlnaGxpZ2h0IiwiSnVtcEhhbmRsZSIsInJlbmRlck1lc3NhZ2VzVG9QbGFpblRleHQiLCJvcGVuRmlsZUluRXh0ZXJuYWxFZGl0b3IiLCJ3cml0ZUZpbGUiLCJCb3giLCJUZXh0IiwidXNlU3RkaW4iLCJ1c2VUaGVtZSIsInVzZVRlcm1pbmFsRm9jdXMiLCJ1c2VUZXJtaW5hbFRpdGxlIiwidXNlVGFiU3RhdHVzIiwiVGFiU3RhdHVzS2luZCIsIkNvc3RUaHJlc2hvbGREaWFsb2ciLCJJZGxlUmV0dXJuRGlhbG9nIiwiUmVhY3QiLCJ1c2VFZmZlY3QiLCJ1c2VNZW1vIiwidXNlUmVmIiwidXNlU3RhdGUiLCJ1c2VDYWxsYmFjayIsInVzZURlZmVycmVkVmFsdWUiLCJ1c2VMYXlvdXRFZmZlY3QiLCJSZWZPYmplY3QiLCJ1c2VOb3RpZmljYXRpb25zIiwic2VuZE5vdGlmaWNhdGlvbiIsInN0YXJ0UHJldmVudFNsZWVwIiwic3RvcFByZXZlbnRTbGVlcCIsInVzZVRlcm1pbmFsTm90aWZpY2F0aW9uIiwiaGFzQ3Vyc29yVXBWaWV3cG9ydFlhbmtCdWciLCJjcmVhdGVGaWxlU3RhdGVDYWNoZVdpdGhTaXplTGltaXQiLCJtZXJnZUZpbGVTdGF0ZUNhY2hlcyIsIlJFQURfRklMRV9TVEFURV9DQUNIRV9TSVpFIiwidXBkYXRlTGFzdEludGVyYWN0aW9uVGltZSIsImdldExhc3RJbnRlcmFjdGlvblRpbWUiLCJnZXRPcmlnaW5hbEN3ZCIsImdldFByb2plY3RSb290IiwiZ2V0U2Vzc2lvbklkIiwic3dpdGNoU2Vzc2lvbiIsInNldENvc3RTdGF0ZUZvclJlc3RvcmUiLCJnZXRUdXJuSG9va0R1cmF0aW9uTXMiLCJnZXRUdXJuSG9va0NvdW50IiwicmVzZXRUdXJuSG9va0R1cmF0aW9uIiwiZ2V0VHVyblRvb2xEdXJhdGlvbk1zIiwiZ2V0VHVyblRvb2xDb3VudCIsInJlc2V0VHVyblRvb2xEdXJhdGlvbiIsImdldFR1cm5DbGFzc2lmaWVyRHVyYXRpb25NcyIsImdldFR1cm5DbGFzc2lmaWVyQ291bnQiLCJyZXNldFR1cm5DbGFzc2lmaWVyRHVyYXRpb24iLCJhc1Nlc3Npb25JZCIsImFzQWdlbnRJZCIsImxvZ0ZvckRlYnVnZ2luZyIsIlF1ZXJ5R3VhcmQiLCJpc0VudlRydXRoeSIsImZvcm1hdFRva2VucyIsInRydW5jYXRlVG9XaWR0aCIsImNvbnN1bWVFYXJseUlucHV0Iiwic2V0TWVtYmVyQWN0aXZlIiwiaXNTd2FybVdvcmtlciIsImdlbmVyYXRlU2FuZGJveFJlcXVlc3RJZCIsInNlbmRTYW5kYm94UGVybWlzc2lvblJlcXVlc3RWaWFNYWlsYm94Iiwic2VuZFNhbmRib3hQZXJtaXNzaW9uUmVzcG9uc2VWaWFNYWlsYm94IiwicmVnaXN0ZXJTYW5kYm94UGVybWlzc2lvbkNhbGxiYWNrIiwiZ2V0VGVhbU5hbWUiLCJnZXRBZ2VudE5hbWUiLCJXb3JrZXJQZW5kaW5nUGVybWlzc2lvbiIsImluamVjdFVzZXJNZXNzYWdlVG9UZWFtbWF0ZSIsImdldEFsbEluUHJvY2Vzc1RlYW1tYXRlVGFza3MiLCJpc0xvY2FsQWdlbnRUYXNrIiwicXVldWVQZW5kaW5nTWVzc2FnZSIsImFwcGVuZE1lc3NhZ2VUb0xvY2FsQWdlbnQiLCJMb2NhbEFnZW50VGFza1N0YXRlIiwicmVnaXN0ZXJMZWFkZXJUb29sVXNlQ29uZmlybVF1ZXVlIiwidW5yZWdpc3RlckxlYWRlclRvb2xVc2VDb25maXJtUXVldWUiLCJyZWdpc3RlckxlYWRlclNldFRvb2xQZXJtaXNzaW9uQ29udGV4dCIsInVucmVnaXN0ZXJMZWFkZXJTZXRUb29sUGVybWlzc2lvbkNvbnRleHQiLCJlbmRJbnRlcmFjdGlvblNwYW4iLCJ1c2VMb2dNZXNzYWdlcyIsInVzZVJlcGxCcmlkZ2UiLCJDb21tYW5kIiwiQ29tbWFuZFJlc3VsdERpc3BsYXkiLCJSZXN1bWVFbnRyeXBvaW50IiwiZ2V0Q29tbWFuZE5hbWUiLCJpc0NvbW1hbmRFbmFibGVkIiwiUHJvbXB0SW5wdXRNb2RlIiwiUXVldWVkQ29tbWFuZCIsIlZpbU1vZGUiLCJNZXNzYWdlU2VsZWN0b3IiLCJzZWxlY3RhYmxlVXNlck1lc3NhZ2VzRmlsdGVyIiwibWVzc2FnZXNBZnRlckFyZU9ubHlTeW50aGV0aWMiLCJ1c2VJZGVMb2dnaW5nIiwiUGVybWlzc2lvblJlcXVlc3QiLCJUb29sVXNlQ29uZmlybSIsIkVsaWNpdGF0aW9uRGlhbG9nIiwiUHJvbXB0RGlhbG9nIiwiUHJvbXB0UmVxdWVzdCIsIlByb21wdFJlc3BvbnNlIiwiUHJvbXB0SW5wdXQiLCJQcm9tcHRJbnB1dFF1ZXVlZENvbW1hbmRzIiwidXNlUmVtb3RlU2Vzc2lvbiIsInVzZURpcmVjdENvbm5lY3QiLCJEaXJlY3RDb25uZWN0Q29uZmlnIiwidXNlU1NIU2Vzc2lvbiIsInVzZUFzc2lzdGFudEhpc3RvcnkiLCJTU0hTZXNzaW9uIiwiU2tpbGxJbXByb3ZlbWVudFN1cnZleSIsInVzZVNraWxsSW1wcm92ZW1lbnRTdXJ2ZXkiLCJ1c2VNb3JlUmlnaHQiLCJTcGlubmVyV2l0aFZlcmIiLCJCcmllZklkbGVTdGF0dXMiLCJTcGlubmVyTW9kZSIsImdldFN5c3RlbVByb21wdCIsImJ1aWxkRWZmZWN0aXZlU3lzdGVtUHJvbXB0IiwiZ2V0U3lzdGVtQ29udGV4dCIsImdldFVzZXJDb250ZXh0IiwiZ2V0TWVtb3J5RmlsZXMiLCJzdGFydEJhY2tncm91bmRIb3VzZWtlZXBpbmciLCJnZXRUb3RhbENvc3QiLCJzYXZlQ3VycmVudFNlc3Npb25Db3N0cyIsInJlc2V0Q29zdFN0YXRlIiwiZ2V0U3RvcmVkU2Vzc2lvbkNvc3RzIiwidXNlQ29zdFN1bW1hcnkiLCJ1c2VGcHNNZXRyaWNzIiwidXNlQWZ0ZXJGaXJzdFJlbmRlciIsInVzZURlZmVycmVkSG9va01lc3NhZ2VzIiwiYWRkVG9IaXN0b3J5IiwicmVtb3ZlTGFzdEZyb21IaXN0b3J5IiwiZXhwYW5kUGFzdGVkVGV4dFJlZnMiLCJwYXJzZVJlZmVyZW5jZXMiLCJwcmVwZW5kTW9kZUNoYXJhY3RlclRvSW5wdXQiLCJwcmVwZW5kVG9TaGVsbEhpc3RvcnlDYWNoZSIsInVzZUFwaUtleVZlcmlmaWNhdGlvbiIsIkdsb2JhbEtleWJpbmRpbmdIYW5kbGVycyIsIkNvbW1hbmRLZXliaW5kaW5nSGFuZGxlcnMiLCJLZXliaW5kaW5nU2V0dXAiLCJ1c2VTaG9ydGN1dERpc3BsYXkiLCJnZXRTaG9ydGN1dERpc3BsYXkiLCJDYW5jZWxSZXF1ZXN0SGFuZGxlciIsInVzZUJhY2tncm91bmRUYXNrTmF2aWdhdGlvbiIsInVzZVN3YXJtSW5pdGlhbGl6YXRpb24iLCJ1c2VUZWFtbWF0ZVZpZXdBdXRvRXhpdCIsImVycm9yTWVzc2FnZSIsImlzSHVtYW5UdXJuIiwibG9nRXJyb3IiLCJ1c2VWb2ljZUludGVncmF0aW9uIiwicmVxdWlyZSIsInN0cmlwVHJhaWxpbmciLCJoYW5kbGVLZXlFdmVudCIsInJlc2V0QW5jaG9yIiwiVm9pY2VLZXliaW5kaW5nSGFuZGxlciIsInVzZUZydXN0cmF0aW9uRGV0ZWN0aW9uIiwic3RhdGUiLCJoYW5kbGVUcmFuc2NyaXB0U2VsZWN0IiwidXNlQW50T3JnV2FybmluZ05vdGlmaWNhdGlvbiIsImdldENvb3JkaW5hdG9yVXNlckNvbnRleHQiLCJtY3BDbGllbnRzIiwiUmVhZG9ubHlBcnJheSIsIm5hbWUiLCJzY3JhdGNocGFkRGlyIiwiayIsInVzZUNhblVzZVRvb2wiLCJUb29sUGVybWlzc2lvbkNvbnRleHQiLCJUb29sIiwiYXBwbHlQZXJtaXNzaW9uVXBkYXRlIiwiYXBwbHlQZXJtaXNzaW9uVXBkYXRlcyIsInBlcnNpc3RQZXJtaXNzaW9uVXBkYXRlIiwiYnVpbGRQZXJtaXNzaW9uVXBkYXRlcyIsInN0cmlwRGFuZ2Vyb3VzUGVybWlzc2lvbnNGb3JBdXRvTW9kZSIsImdldFNjcmF0Y2hwYWREaXIiLCJpc1NjcmF0Y2hwYWRFbmFibGVkIiwiV0VCX0ZFVENIX1RPT0xfTkFNRSIsIlNMRUVQX1RPT0xfTkFNRSIsImNsZWFyU3BlY3VsYXRpdmVDaGVja3MiLCJBdXRvVXBkYXRlclJlc3VsdCIsImdldEdsb2JhbENvbmZpZyIsInNhdmVHbG9iYWxDb25maWciLCJnZXRHbG9iYWxDb25maWdXcml0ZUNvdW50IiwiaGFzQ29uc29sZUJpbGxpbmdBY2Nlc3MiLCJsb2dFdmVudCIsIkFuYWx5dGljc01ldGFkYXRhX0lfVkVSSUZJRURfVEhJU19JU19OT1RfQ09ERV9PUl9GSUxFUEFUSFMiLCJnZXRGZWF0dXJlVmFsdWVfQ0FDSEVEX01BWV9CRV9TVEFMRSIsInRleHRGb3JSZXN1Ym1pdCIsImhhbmRsZU1lc3NhZ2VGcm9tU3RyZWFtIiwiU3RyZWFtaW5nVG9vbFVzZSIsIlN0cmVhbWluZ1RoaW5raW5nIiwiaXNDb21wYWN0Qm91bmRhcnlNZXNzYWdlIiwiZ2V0TWVzc2FnZXNBZnRlckNvbXBhY3RCb3VuZGFyeSIsImdldENvbnRlbnRUZXh0IiwiY3JlYXRlVXNlck1lc3NhZ2UiLCJjcmVhdGVBc3Npc3RhbnRNZXNzYWdlIiwiY3JlYXRlVHVybkR1cmF0aW9uTWVzc2FnZSIsImNyZWF0ZUFnZW50c0tpbGxlZE1lc3NhZ2UiLCJjcmVhdGVBcGlNZXRyaWNzTWVzc2FnZSIsImNyZWF0ZVN5c3RlbU1lc3NhZ2UiLCJjcmVhdGVDb21tYW5kSW5wdXRNZXNzYWdlIiwiZm9ybWF0Q29tbWFuZElucHV0VGFncyIsImdlbmVyYXRlU2Vzc2lvblRpdGxlIiwiQkFTSF9JTlBVVF9UQUciLCJDT01NQU5EX01FU1NBR0VfVEFHIiwiQ09NTUFORF9OQU1FX1RBRyIsIkxPQ0FMX0NPTU1BTkRfU1RET1VUX1RBRyIsImVzY2FwZVhtbCIsIlRoaW5raW5nQ29uZmlnIiwiZ3JhY2VmdWxTaHV0ZG93blN5bmMiLCJoYW5kbGVQcm9tcHRTdWJtaXQiLCJQcm9tcHRJbnB1dEhlbHBlcnMiLCJ1c2VRdWV1ZVByb2Nlc3NvciIsInVzZU1haWxib3hCcmlkZ2UiLCJxdWVyeUNoZWNrcG9pbnQiLCJsb2dRdWVyeVByb2ZpbGVSZXBvcnQiLCJNZXNzYWdlIiwiTWVzc2FnZVR5cGUiLCJVc2VyTWVzc2FnZSIsIlByb2dyZXNzTWVzc2FnZSIsIkhvb2tSZXN1bHRNZXNzYWdlIiwiUGFydGlhbENvbXBhY3REaXJlY3Rpb24iLCJxdWVyeSIsIm1lcmdlQ2xpZW50cyIsInVzZU1lcmdlZENsaWVudHMiLCJnZXRRdWVyeVNvdXJjZUZvclJFUEwiLCJ1c2VNZXJnZWRUb29scyIsIm1lcmdlQW5kRmlsdGVyVG9vbHMiLCJ1c2VNZXJnZWRDb21tYW5kcyIsInVzZVNraWxsc0NoYW5nZSIsInVzZU1hbmFnZVBsdWdpbnMiLCJNZXNzYWdlcyIsIlRhc2tMaXN0VjIiLCJUZWFtbWF0ZVZpZXdIZWFkZXIiLCJ1c2VUYXNrc1YyV2l0aENvbGxhcHNlRWZmZWN0IiwibWF5YmVNYXJrUHJvamVjdE9uYm9hcmRpbmdDb21wbGV0ZSIsIk1DUFNlcnZlckNvbm5lY3Rpb24iLCJTY29wZWRNY3BTZXJ2ZXJDb25maWciLCJyYW5kb21VVUlEIiwiVVVJRCIsInByb2Nlc3NTZXNzaW9uU3RhcnRIb29rcyIsImV4ZWN1dGVTZXNzaW9uRW5kSG9va3MiLCJnZXRTZXNzaW9uRW5kSG9va1RpbWVvdXRNcyIsIklERVNlbGVjdGlvbiIsInVzZUlkZVNlbGVjdGlvbiIsImdldFRvb2xzIiwiYXNzZW1ibGVUb29sUG9vbCIsIkFnZW50RGVmaW5pdGlvbiIsInJlc29sdmVBZ2VudFRvb2xzIiwicmVzdW1lQWdlbnRCYWNrZ3JvdW5kIiwidXNlTWFpbkxvb3BNb2RlbCIsInVzZUFwcFN0YXRlIiwidXNlU2V0QXBwU3RhdGUiLCJ1c2VBcHBTdGF0ZVN0b3JlIiwiQ29udGVudEJsb2NrUGFyYW0iLCJJbWFnZUJsb2NrUGFyYW0iLCJQcm9jZXNzVXNlcklucHV0Q29udGV4dCIsIlBhc3RlZENvbnRlbnQiLCJjb3B5UGxhbkZvckZvcmsiLCJjb3B5UGxhbkZvclJlc3VtZSIsImdldFBsYW5TbHVnIiwic2V0UGxhblNsdWciLCJjbGVhclNlc3Npb25NZXRhZGF0YSIsInJlc2V0U2Vzc2lvbkZpbGVQb2ludGVyIiwiYWRvcHRSZXN1bWVkU2Vzc2lvbkZpbGUiLCJyZW1vdmVUcmFuc2NyaXB0TWVzc2FnZSIsInJlc3RvcmVTZXNzaW9uTWV0YWRhdGEiLCJnZXRDdXJyZW50U2Vzc2lvblRpdGxlIiwiaXNFcGhlbWVyYWxUb29sUHJvZ3Jlc3MiLCJpc0xvZ2dhYmxlTWVzc2FnZSIsInNhdmVXb3JrdHJlZVN0YXRlIiwiZ2V0QWdlbnRUcmFuc2NyaXB0IiwiZGVzZXJpYWxpemVNZXNzYWdlcyIsImV4dHJhY3RSZWFkRmlsZXNGcm9tTWVzc2FnZXMiLCJleHRyYWN0QmFzaFRvb2xzRnJvbU1lc3NhZ2VzIiwicmVzZXRNaWNyb2NvbXBhY3RTdGF0ZSIsInJ1blBvc3RDb21wYWN0Q2xlYW51cCIsInByb3Zpc2lvbkNvbnRlbnRSZXBsYWNlbWVudFN0YXRlIiwicmVjb25zdHJ1Y3RDb250ZW50UmVwbGFjZW1lbnRTdGF0ZSIsIkNvbnRlbnRSZXBsYWNlbWVudFJlY29yZCIsInBhcnRpYWxDb21wYWN0Q29udmVyc2F0aW9uIiwiTG9nT3B0aW9uIiwiQWdlbnRDb2xvck5hbWUiLCJmaWxlSGlzdG9yeU1ha2VTbmFwc2hvdCIsIkZpbGVIaXN0b3J5U3RhdGUiLCJmaWxlSGlzdG9yeVJld2luZCIsIkZpbGVIaXN0b3J5U25hcHNob3QiLCJjb3B5RmlsZUhpc3RvcnlGb3JSZXN1bWUiLCJmaWxlSGlzdG9yeUVuYWJsZWQiLCJmaWxlSGlzdG9yeUhhc0FueUNoYW5nZXMiLCJBdHRyaWJ1dGlvblN0YXRlIiwiaW5jcmVtZW50UHJvbXB0Q291bnQiLCJyZWNvcmRBdHRyaWJ1dGlvblNuYXBzaG90IiwiY29tcHV0ZVN0YW5kYWxvbmVBZ2VudENvbnRleHQiLCJyZXN0b3JlQWdlbnRGcm9tU2Vzc2lvbiIsInJlc3RvcmVTZXNzaW9uU3RhdGVGcm9tTG9nIiwicmVzdG9yZVdvcmt0cmVlRm9yUmVzdW1lIiwiZXhpdFJlc3RvcmVkV29ya3RyZWUiLCJpc0JnU2Vzc2lvbiIsInVwZGF0ZVNlc3Npb25OYW1lIiwidXBkYXRlU2Vzc2lvbkFjdGl2aXR5IiwiaXNJblByb2Nlc3NUZWFtbWF0ZVRhc2siLCJJblByb2Nlc3NUZWFtbWF0ZVRhc2tTdGF0ZSIsInJlc3RvcmVSZW1vdGVBZ2VudFRhc2tzIiwidXNlSW5ib3hQb2xsZXIiLCJwcm9hY3RpdmVNb2R1bGUiLCJQUk9BQ1RJVkVfTk9fT1BfU1VCU0NSSUJFIiwiX2NiIiwiUFJPQUNUSVZFX0ZBTFNFIiwiU1VHR0VTVF9CR19QUl9OT09QIiwiX3AiLCJfbiIsInVzZVByb2FjdGl2ZSIsInVzZVNjaGVkdWxlZFRhc2tzIiwiaXNBZ2VudFN3YXJtc0VuYWJsZWQiLCJ1c2VUYXNrTGlzdFdhdGNoZXIiLCJTYW5kYm94QXNrQ2FsbGJhY2siLCJOZXR3b3JrSG9zdFBhdHRlcm4iLCJJREVFeHRlbnNpb25JbnN0YWxsYXRpb25TdGF0dXMiLCJjbG9zZU9wZW5EaWZmcyIsImdldENvbm5lY3RlZElkZUNsaWVudCIsIklkZVR5cGUiLCJ1c2VJREVJbnRlZ3JhdGlvbiIsImV4aXQiLCJFeGl0RmxvdyIsImdldEN1cnJlbnRXb3JrdHJlZVNlc3Npb24iLCJwb3BBbGxFZGl0YWJsZSIsImVucXVldWUiLCJTZXRBcHBTdGF0ZSIsImdldENvbW1hbmRRdWV1ZSIsImdldENvbW1hbmRRdWV1ZUxlbmd0aCIsInJlbW92ZUJ5RmlsdGVyIiwidXNlQ29tbWFuZFF1ZXVlIiwiU2Vzc2lvbkJhY2tncm91bmRIaW50Iiwic3RhcnRCYWNrZ3JvdW5kU2Vzc2lvbiIsInVzZVNlc3Npb25CYWNrZ3JvdW5kaW5nIiwiZGlhZ25vc3RpY1RyYWNrZXIiLCJoYW5kbGVTcGVjdWxhdGlvbkFjY2VwdCIsIkFjdGl2ZVNwZWN1bGF0aW9uU3RhdGUiLCJJZGVPbmJvYXJkaW5nRGlhbG9nIiwiRWZmb3J0Q2FsbG91dCIsInNob3VsZFNob3dFZmZvcnRDYWxsb3V0IiwiRWZmb3J0VmFsdWUiLCJSZW1vdGVDYWxsb3V0IiwiQW50TW9kZWxTd2l0Y2hDYWxsb3V0Iiwic2hvdWxkU2hvd0FudE1vZGVsU3dpdGNoIiwic2hvdWxkU2hvd01vZGVsU3dpdGNoQ2FsbG91dCIsIlVuZGVyY292ZXJBdXRvQ2FsbG91dCIsImFjdGl2aXR5TWFuYWdlciIsImNyZWF0ZUFib3J0Q29udHJvbGxlciIsIk1DUENvbm5lY3Rpb25NYW5hZ2VyIiwidXNlRmVlZGJhY2tTdXJ2ZXkiLCJ1c2VNZW1vcnlTdXJ2ZXkiLCJ1c2VQb3N0Q29tcGFjdFN1cnZleSIsIkZlZWRiYWNrU3VydmV5IiwidXNlSW5zdGFsbE1lc3NhZ2VzIiwidXNlQXdheVN1bW1hcnkiLCJ1c2VDaHJvbWVFeHRlbnNpb25Ob3RpZmljYXRpb24iLCJ1c2VPZmZpY2lhbE1hcmtldHBsYWNlTm90aWZpY2F0aW9uIiwidXNlUHJvbXB0c0Zyb21DbGF1ZGVJbkNocm9tZSIsImdldFRpcFRvU2hvd09uU3Bpbm5lciIsInJlY29yZFNob3duVGlwIiwiVGhlbWUiLCJjaGVja0FuZERpc2FibGVCeXBhc3NQZXJtaXNzaW9uc0lmTmVlZGVkIiwiY2hlY2tBbmREaXNhYmxlQXV0b01vZGVJZk5lZWRlZCIsInVzZUtpY2tPZmZDaGVja0FuZERpc2FibGVCeXBhc3NQZXJtaXNzaW9uc0lmTmVlZGVkIiwidXNlS2lja09mZkNoZWNrQW5kRGlzYWJsZUF1dG9Nb2RlSWZOZWVkZWQiLCJTYW5kYm94TWFuYWdlciIsIlNBTkRCT1hfTkVUV09SS19BQ0NFU1NfVE9PTF9OQU1FIiwidXNlRmlsZUhpc3RvcnlTbmFwc2hvdEluaXQiLCJTYW5kYm94UGVybWlzc2lvblJlcXVlc3QiLCJTYW5kYm94VmlvbGF0aW9uRXhwYW5kZWRWaWV3IiwidXNlU2V0dGluZ3NFcnJvcnMiLCJ1c2VNY3BDb25uZWN0aXZpdHlTdGF0dXMiLCJ1c2VBdXRvTW9kZVVuYXZhaWxhYmxlTm90aWZpY2F0aW9uIiwiQVVUT19NT0RFX0RFU0NSSVBUSU9OIiwidXNlTHNwSW5pdGlhbGl6YXRpb25Ob3RpZmljYXRpb24iLCJ1c2VMc3BQbHVnaW5SZWNvbW1lbmRhdGlvbiIsIkxzcFJlY29tbWVuZGF0aW9uTWVudSIsInVzZUNsYXVkZUNvZGVIaW50UmVjb21tZW5kYXRpb24iLCJQbHVnaW5IaW50TWVudSIsIkRlc2t0b3BVcHNlbGxTdGFydHVwIiwic2hvdWxkU2hvd0Rlc2t0b3BVcHNlbGxTdGFydHVwIiwidXNlUGx1Z2luSW5zdGFsbGF0aW9uU3RhdHVzIiwidXNlUGx1Z2luQXV0b3VwZGF0ZU5vdGlmaWNhdGlvbiIsInBlcmZvcm1TdGFydHVwQ2hlY2tzIiwiVXNlclRleHRNZXNzYWdlIiwiQXdzQXV0aFN0YXR1c0JveCIsInVzZVJhdGVMaW1pdFdhcm5pbmdOb3RpZmljYXRpb24iLCJ1c2VEZXByZWNhdGlvbldhcm5pbmdOb3RpZmljYXRpb24iLCJ1c2VOcG1EZXByZWNhdGlvbk5vdGlmaWNhdGlvbiIsInVzZUlERVN0YXR1c0luZGljYXRvciIsInVzZU1vZGVsTWlncmF0aW9uTm90aWZpY2F0aW9ucyIsInVzZUNhblN3aXRjaFRvRXhpc3RpbmdTdWJzY3JpcHRpb24iLCJ1c2VUZWFtbWF0ZUxpZmVjeWNsZU5vdGlmaWNhdGlvbiIsInVzZUZhc3RNb2RlTm90aWZpY2F0aW9uIiwiQXV0b1J1bklzc3VlTm90aWZpY2F0aW9uIiwic2hvdWxkQXV0b1J1bklzc3VlIiwiZ2V0QXV0b1J1bklzc3VlUmVhc29uVGV4dCIsImdldEF1dG9SdW5Db21tYW5kIiwiQXV0b1J1bklzc3VlUmVhc29uIiwiSG9va1Byb2dyZXNzIiwiVHVuZ3N0ZW5MaXZlTW9uaXRvciIsIldlYkJyb3dzZXJQYW5lbE1vZHVsZSIsIklzc3VlRmxhZ0Jhbm5lciIsInVzZUlzc3VlRmxhZ0Jhbm5lciIsIkNvbXBhbmlvblNwcml0ZSIsIkNvbXBhbmlvbkZsb2F0aW5nQnViYmxlIiwiTUlOX0NPTFNfRk9SX0ZVTExfU1BSSVRFIiwiRGV2QmFyIiwiUmVtb3RlU2Vzc2lvbkNvbmZpZyIsIlJFTU9URV9TQUZFX0NPTU1BTkRTIiwiUmVtb3RlTWVzc2FnZUNvbnRlbnQiLCJGdWxsc2NyZWVuTGF5b3V0IiwidXNlVW5zZWVuRGl2aWRlciIsImNvbXB1dGVVbnNlZW5EaXZpZGVyIiwiaXNGdWxsc2NyZWVuRW52RW5hYmxlZCIsIm1heWJlR2V0VG11eE1vdXNlSGludCIsImlzTW91c2VUcmFja2luZ0VuYWJsZWQiLCJBbHRlcm5hdGVTY3JlZW4iLCJTY3JvbGxLZXliaW5kaW5nSGFuZGxlciIsInVzZU1lc3NhZ2VBY3Rpb25zIiwiTWVzc2FnZUFjdGlvbnNLZXliaW5kaW5ncyIsIk1lc3NhZ2VBY3Rpb25zQmFyIiwiTWVzc2FnZUFjdGlvbnNTdGF0ZSIsIk1lc3NhZ2VBY3Rpb25zTmF2IiwiTWVzc2FnZUFjdGlvbkNhcHMiLCJzZXRDbGlwYm9hcmQiLCJTY3JvbGxCb3hIYW5kbGUiLCJjcmVhdGVBdHRhY2htZW50TWVzc2FnZSIsImdldFF1ZXVlZENvbW1hbmRBdHRhY2htZW50cyIsIkVNUFRZX01DUF9DTElFTlRTIiwiSElTVE9SWV9TVFVCIiwibWF5YmVMb2FkT2xkZXIiLCJfIiwiUkVDRU5UX1NDUk9MTF9SRVBJTl9XSU5ET1dfTVMiLCJtZWRpYW4iLCJ2YWx1ZXMiLCJzb3J0ZWQiLCJzb3J0IiwiYSIsImIiLCJtaWQiLCJNYXRoIiwiZmxvb3IiLCJsZW5ndGgiLCJyb3VuZCIsIlRyYW5zY3JpcHRNb2RlRm9vdGVyIiwidDAiLCIkIiwiX2MiLCJzaG93QWxsSW5UcmFuc2NyaXB0IiwidmlydHVhbFNjcm9sbCIsInNlYXJjaEJhZGdlIiwic3VwcHJlc3NTaG93QWxsIiwidDEiLCJzdGF0dXMiLCJ1bmRlZmluZWQiLCJ0b2dnbGVTaG9ydGN1dCIsInNob3dBbGxTaG9ydGN1dCIsInQyIiwiYXJyb3dVcCIsImFycm93RG93biIsInQzIiwidDQiLCJjdXJyZW50IiwidDUiLCJUcmFuc2NyaXB0U2VhcmNoQmFyIiwianVtcFJlZiIsIm9uQ2xvc2UiLCJvbkNhbmNlbCIsInNldEhpZ2hsaWdodCIsImluaXRpYWxRdWVyeSIsImxhc3RRdWVyeSIsIlJlYWN0Tm9kZSIsImN1cnNvck9mZnNldCIsImlzQWN0aXZlIiwib25FeGl0IiwiaW5kZXhTdGF0dXMiLCJzZXRJbmRleFN0YXR1cyIsIm1zIiwiYWxpdmUiLCJ3YXJtIiwid2FybVNlYXJjaEluZGV4IiwidGhlbiIsInNldFRpbWVvdXQiLCJ3YXJtRG9uZSIsInNldFNlYXJjaFF1ZXJ5Iiwib2ZmIiwiY3Vyc29yQ2hhciIsInNsaWNlIiwiVElUTEVfQU5JTUFUSU9OX0ZSQU1FUyIsIlRJVExFX1NUQVRJQ19QUkVGSVgiLCJUSVRMRV9BTklNQVRJT05fSU5URVJWQUxfTVMiLCJBbmltYXRlZFRlcm1pbmFsVGl0bGUiLCJpc0FuaW1hdGluZyIsInRpdGxlIiwiZGlzYWJsZWQiLCJub1ByZWZpeCIsInRlcm1pbmFsRm9jdXNlZCIsImZyYW1lIiwic2V0RnJhbWUiLCJpbnRlcnZhbCIsInNldEludGVydmFsIiwiX3RlbXAyIiwiY2xlYXJJbnRlcnZhbCIsInByZWZpeCIsInNldEZyYW1lXzAiLCJfdGVtcCIsImYiLCJQcm9wcyIsImNvbW1hbmRzIiwiZGVidWciLCJpbml0aWFsVG9vbHMiLCJpbml0aWFsTWVzc2FnZXMiLCJwZW5kaW5nSG9va01lc3NhZ2VzIiwiUHJvbWlzZSIsImluaXRpYWxGaWxlSGlzdG9yeVNuYXBzaG90cyIsImluaXRpYWxDb250ZW50UmVwbGFjZW1lbnRzIiwiaW5pdGlhbEFnZW50TmFtZSIsImluaXRpYWxBZ2VudENvbG9yIiwiZHluYW1pY01jcENvbmZpZyIsIlJlY29yZCIsImF1dG9Db25uZWN0SWRlRmxhZyIsInN0cmljdE1jcENvbmZpZyIsInN5c3RlbVByb21wdCIsImFwcGVuZFN5c3RlbVByb21wdCIsIm9uQmVmb3JlUXVlcnkiLCJpbnB1dCIsIm5ld01lc3NhZ2VzIiwib25UdXJuQ29tcGxldGUiLCJtZXNzYWdlcyIsIm1haW5UaHJlYWRBZ2VudERlZmluaXRpb24iLCJkaXNhYmxlU2xhc2hDb21tYW5kcyIsInRhc2tMaXN0SWQiLCJyZW1vdGVTZXNzaW9uQ29uZmlnIiwiZGlyZWN0Q29ubmVjdENvbmZpZyIsInNzaFNlc3Npb24iLCJ0aGlua2luZ0NvbmZpZyIsIlNjcmVlbiIsIlJFUEwiLCJpbml0aWFsQ29tbWFuZHMiLCJpbml0aWFsTWNwQ2xpZW50cyIsImluaXRpYWxEeW5hbWljTWNwQ29uZmlnIiwiY3VzdG9tU3lzdGVtUHJvbXB0IiwiaW5pdGlhbE1haW5UaHJlYWRBZ2VudERlZmluaXRpb24iLCJpc1JlbW90ZVNlc3Npb24iLCJ0aXRsZURpc2FibGVkIiwicHJvY2VzcyIsImVudiIsIkNMQVVERV9DT0RFX0RJU0FCTEVfVEVSTUlOQUxfVElUTEUiLCJtb3JlUmlnaHRFbmFibGVkIiwiQ0xBVURFX01PUkVSSUdIVCIsImRpc2FibGVWaXJ0dWFsU2Nyb2xsIiwiQ0xBVURFX0NPREVfRElTQUJMRV9WSVJUVUFMX1NDUk9MTCIsImRpc2FibGVNZXNzYWdlQWN0aW9ucyIsIkNMQVVERV9DT0RFX0RJU0FCTEVfTUVTU0FHRV9BQ1RJT05TIiwic2V0TWFpblRocmVhZEFnZW50RGVmaW5pdGlvbiIsInRvb2xQZXJtaXNzaW9uQ29udGV4dCIsInMiLCJ2ZXJib3NlIiwibWNwIiwicGx1Z2lucyIsImFnZW50RGVmaW5pdGlvbnMiLCJmaWxlSGlzdG9yeSIsImluaXRpYWxNZXNzYWdlIiwicXVldWVkQ29tbWFuZHMiLCJzcGlubmVyVGlwIiwic2hvd0V4cGFuZGVkVG9kb3MiLCJleHBhbmRlZFZpZXciLCJwZW5kaW5nV29ya2VyUmVxdWVzdCIsInBlbmRpbmdTYW5kYm94UmVxdWVzdCIsInRlYW1Db250ZXh0IiwidGFza3MiLCJ3b3JrZXJTYW5kYm94UGVybWlzc2lvbnMiLCJlbGljaXRhdGlvbiIsInVsdHJhcGxhblBlbmRpbmdDaG9pY2UiLCJ1bHRyYXBsYW5MYXVuY2hQZW5kaW5nIiwidmlld2luZ0FnZW50VGFza0lkIiwic2V0QXBwU3RhdGUiLCJ2aWV3ZWRMb2NhbEFnZW50IiwibmVlZHNCb290c3RyYXAiLCJyZXRhaW4iLCJkaXNrTG9hZGVkIiwidGFza0lkIiwicmVzdWx0IiwicHJldiIsInQiLCJsaXZlIiwibGl2ZVV1aWRzIiwiU2V0IiwibWFwIiwibSIsInV1aWQiLCJkaXNrT25seSIsImZpbHRlciIsImhhcyIsInN0b3JlIiwidGVybWluYWwiLCJtYWluTG9vcE1vZGVsIiwibG9jYWxDb21tYW5kcyIsInNldExvY2FsQ29tbWFuZHMiLCJwcm9hY3RpdmVBY3RpdmUiLCJ1c2VTeW5jRXh0ZXJuYWxTdG9yZSIsInN1YnNjcmliZVRvUHJvYWN0aXZlQ2hhbmdlcyIsImlzUHJvYWN0aXZlQWN0aXZlIiwiaXNCcmllZk9ubHkiLCJsb2NhbFRvb2xzIiwic2V0RHluYW1pY01jcENvbmZpZyIsIm9uQ2hhbmdlRHluYW1pY01jcENvbmZpZyIsImNvbmZpZyIsInNjcmVlbiIsInNldFNjcmVlbiIsInNldFNob3dBbGxJblRyYW5zY3JpcHQiLCJkdW1wTW9kZSIsInNldER1bXBNb2RlIiwiZWRpdG9yU3RhdHVzIiwic2V0RWRpdG9yU3RhdHVzIiwiZWRpdG9yR2VuUmVmIiwiZWRpdG9yVGltZXJSZWYiLCJSZXR1cm5UeXBlIiwiZWRpdG9yUmVuZGVyaW5nUmVmIiwiYWRkTm90aWZpY2F0aW9uIiwicmVtb3ZlTm90aWZpY2F0aW9uIiwidHJ5U3VnZ2VzdEJnUFJJbnRlcmNlcHQiLCJjbGllbnRzIiwiaWRlU2VsZWN0aW9uIiwic2V0SURFU2VsZWN0aW9uIiwiaWRlVG9JbnN0YWxsRXh0ZW5zaW9uIiwic2V0SURFVG9JbnN0YWxsRXh0ZW5zaW9uIiwiaWRlSW5zdGFsbGF0aW9uU3RhdHVzIiwic2V0SURFSW5zdGFsbGF0aW9uU3RhdHVzIiwic2hvd0lkZU9uYm9hcmRpbmciLCJzZXRTaG93SWRlT25ib2FyZGluZyIsInNob3dNb2RlbFN3aXRjaENhbGxvdXQiLCJzZXRTaG93TW9kZWxTd2l0Y2hDYWxsb3V0Iiwic2hvd0VmZm9ydENhbGxvdXQiLCJzZXRTaG93RWZmb3J0Q2FsbG91dCIsInNob3dSZW1vdGVDYWxsb3V0Iiwic2hvd0Rlc2t0b3BVcHNlbGxTdGFydHVwIiwic2V0U2hvd0Rlc2t0b3BVcHNlbGxTdGFydHVwIiwicmVjb21tZW5kYXRpb24iLCJsc3BSZWNvbW1lbmRhdGlvbiIsImhhbmRsZVJlc3BvbnNlIiwiaGFuZGxlTHNwUmVzcG9uc2UiLCJoaW50UmVjb21tZW5kYXRpb24iLCJoYW5kbGVIaW50UmVzcG9uc2UiLCJjb21iaW5lZEluaXRpYWxUb29scyIsImVuYWJsZWQiLCJ0YXNrc1YyIiwibW9kZSIsIm1lcmdlZFRvb2xzIiwidG9vbHMiLCJhbGxvd2VkQWdlbnRUeXBlcyIsInJlc29sdmVkIiwicmVzb2x2ZWRUb29scyIsImNvbW1hbmRzV2l0aFBsdWdpbnMiLCJtZXJnZWRDb21tYW5kcyIsInN0cmVhbU1vZGUiLCJzZXRTdHJlYW1Nb2RlIiwic3RyZWFtTW9kZVJlZiIsInN0cmVhbWluZ1Rvb2xVc2VzIiwic2V0U3RyZWFtaW5nVG9vbFVzZXMiLCJzdHJlYW1pbmdUaGlua2luZyIsInNldFN0cmVhbWluZ1RoaW5raW5nIiwiaXNTdHJlYW1pbmciLCJzdHJlYW1pbmdFbmRlZEF0IiwiZWxhcHNlZCIsIkRhdGUiLCJub3ciLCJyZW1haW5pbmciLCJ0aW1lciIsImNsZWFyVGltZW91dCIsImFib3J0Q29udHJvbGxlciIsInNldEFib3J0Q29udHJvbGxlciIsIkFib3J0Q29udHJvbGxlciIsImFib3J0Q29udHJvbGxlclJlZiIsInNlbmRCcmlkZ2VSZXN1bHRSZWYiLCJyZXN0b3JlTWVzc2FnZVN5bmNSZWYiLCJzY3JvbGxSZWYiLCJtb2RhbFNjcm9sbFJlZiIsImxhc3RVc2VyU2Nyb2xsVHNSZWYiLCJxdWVyeUd1YXJkIiwiaXNRdWVyeUFjdGl2ZSIsInN1YnNjcmliZSIsImdldFNuYXBzaG90IiwiaXNFeHRlcm5hbExvYWRpbmciLCJzZXRJc0V4dGVybmFsTG9hZGluZ1JhdyIsImhhc0luaXRpYWxQcm9tcHQiLCJpc0xvYWRpbmciLCJ1c2VySW5wdXRPblByb2Nlc3NpbmciLCJzZXRVc2VySW5wdXRPblByb2Nlc3NpbmdSYXciLCJ1c2VySW5wdXRCYXNlbGluZVJlZiIsInVzZXJNZXNzYWdlUGVuZGluZ1JlZiIsImxvYWRpbmdTdGFydFRpbWVSZWYiLCJ0b3RhbFBhdXNlZE1zUmVmIiwicGF1c2VTdGFydFRpbWVSZWYiLCJyZXNldFRpbWluZ1JlZnMiLCJ3YXNRdWVyeUFjdGl2ZVJlZiIsInNldElzRXh0ZXJuYWxMb2FkaW5nIiwidmFsdWUiLCJzd2FybVN0YXJ0VGltZVJlZiIsInN3YXJtQnVkZ2V0SW5mb1JlZiIsInRva2VucyIsImxpbWl0IiwibnVkZ2VzIiwiZm9jdXNlZElucHV0RGlhbG9nUmVmIiwiZ2V0Rm9jdXNlZElucHV0RGlhbG9nIiwiUFJPTVBUX1NVUFBSRVNTSU9OX01TIiwiaXNQcm9tcHRJbnB1dEFjdGl2ZSIsInNldElzUHJvbXB0SW5wdXRBY3RpdmUiLCJhdXRvVXBkYXRlclJlc3VsdCIsInNldEF1dG9VcGRhdGVyUmVzdWx0Iiwibm90aWZpY2F0aW9ucyIsImZvckVhY2giLCJub3RpZmljYXRpb24iLCJrZXkiLCJ0ZXh0IiwicHJpb3JpdHkiLCJoaW50Iiwic2hvd1VuZGVyY292ZXJDYWxsb3V0Iiwic2V0U2hvd1VuZGVyY292ZXJDYWxsb3V0IiwiaXNJbnRlcm5hbE1vZGVsUmVwbyIsInNob3VsZFNob3dVbmRlcmNvdmVyQXV0b05vdGljZSIsInRvb2xKU1giLCJzZXRUb29sSlNYSW50ZXJuYWwiLCJqc3giLCJzaG91bGRIaWRlUHJvbXB0SW5wdXQiLCJzaG91bGRDb250aW51ZUFuaW1hdGlvbiIsInNob3dTcGlubmVyIiwiaXNMb2NhbEpTWENvbW1hbmQiLCJpc0ltbWVkaWF0ZSIsImxvY2FsSlNYQ29tbWFuZFJlZiIsInNldFRvb2xKU1giLCJhcmdzIiwiY2xlYXJMb2NhbEpTWCIsInJlc3QiLCJ0b29sVXNlQ29uZmlybVF1ZXVlIiwic2V0VG9vbFVzZUNvbmZpcm1RdWV1ZSIsInBlcm1pc3Npb25TdGlja3lGb290ZXIiLCJzZXRQZXJtaXNzaW9uU3RpY2t5Rm9vdGVyIiwic2FuZGJveFBlcm1pc3Npb25SZXF1ZXN0UXVldWUiLCJzZXRTYW5kYm94UGVybWlzc2lvblJlcXVlc3RRdWV1ZSIsIkFycmF5IiwiaG9zdFBhdHRlcm4iLCJyZXNvbHZlUHJvbWlzZSIsImFsbG93Q29ubmVjdGlvbiIsInByb21wdFF1ZXVlIiwic2V0UHJvbXB0UXVldWUiLCJyZXF1ZXN0IiwidG9vbElucHV0U3VtbWFyeSIsInJlc29sdmUiLCJyZXNwb25zZSIsInJlamVjdCIsImVycm9yIiwiRXJyb3IiLCJzYW5kYm94QnJpZGdlQ2xlYW51cFJlZiIsIk1hcCIsInRlcm1pbmFsVGl0bGVGcm9tUmVuYW1lIiwic2V0dGluZ3MiLCJzZXNzaW9uVGl0bGUiLCJoYWlrdVRpdGxlIiwic2V0SGFpa3VUaXRsZSIsImhhaWt1VGl0bGVBdHRlbXB0ZWRSZWYiLCJhZ2VudFRpdGxlIiwiYWdlbnRUeXBlIiwidGVybWluYWxUaXRsZSIsImlzV2FpdGluZ0ZvckFwcHJvdmFsIiwiaXNTaG93aW5nTG9jYWxKU1hDb21tYW5kIiwidGl0bGVJc0FuaW1hdGluZyIsInNlc3Npb25TdGF0dXMiLCJ3YWl0aW5nRm9yIiwidG9vbCIsInRhYlN0YXR1c0dhdGVFbmFibGVkIiwic2hvd1N0YXR1c0luVGVybWluYWxUYWIiLCJyYXdTZXRNZXNzYWdlcyIsIm1lc3NhZ2VzUmVmIiwiaWRsZUhpbnRTaG93blJlZiIsInNldE1lc3NhZ2VzIiwiYWN0aW9uIiwiU2V0U3RhdGVBY3Rpb24iLCJuZXh0IiwiZGVsdGEiLCJhZGRlZCIsInNvbWUiLCJzZXRVc2VySW5wdXRPblByb2Nlc3NpbmciLCJkaXZpZGVySW5kZXgiLCJkaXZpZGVyWVJlZiIsIm9uU2Nyb2xsQXdheSIsIm9uUmVwaW4iLCJqdW1wVG9OZXciLCJzaGlmdERpdmlkZXIiLCJjdXJzb3IiLCJzZXRDdXJzb3IiLCJjdXJzb3JOYXZSZWYiLCJ1bnNlZW5EaXZpZGVyIiwicmVwaW5TY3JvbGwiLCJzY3JvbGxUb0JvdHRvbSIsImxhc3RNc2ciLCJhdCIsImxhc3RNc2dJc0h1bWFuIiwib25QcmVwZW5kIiwiY29tcG9zZWRPblNjcm9sbCIsInN0aWNreSIsImhhbmRsZSIsImNvbXBhbmlvblJlYWN0aW9uIiwiYXdhaXRQZW5kaW5nSG9va3MiLCJkZWZlcnJlZE1lc3NhZ2VzIiwiZGVmZXJyZWRCZWhpbmQiLCJmcm96ZW5UcmFuc2NyaXB0U3RhdGUiLCJzZXRGcm96ZW5UcmFuc2NyaXB0U3RhdGUiLCJtZXNzYWdlc0xlbmd0aCIsInN0cmVhbWluZ1Rvb2xVc2VzTGVuZ3RoIiwiaW5wdXRWYWx1ZSIsInNldElucHV0VmFsdWVSYXciLCJpbnB1dFZhbHVlUmVmIiwiaW5zZXJ0VGV4dFJlZiIsImluc2VydCIsInNldElucHV0V2l0aEN1cnNvciIsInNldElucHV0VmFsdWUiLCJ0cmltIiwiaW5wdXRNb2RlIiwic2V0SW5wdXRNb2RlIiwic3Rhc2hlZFByb21wdCIsInNldFN0YXNoZWRQcm9tcHQiLCJwYXN0ZWRDb250ZW50cyIsImhhbmRsZVJlbW90ZUluaXQiLCJyZW1vdGVTbGFzaENvbW1hbmRzIiwicmVtb3RlQ29tbWFuZFNldCIsImNtZCIsImluUHJvZ3Jlc3NUb29sVXNlSURzIiwic2V0SW5Qcm9ncmVzc1Rvb2xVc2VJRHMiLCJoYXNJbnRlcnJ1cHRpYmxlVG9vbEluUHJvZ3Jlc3NSZWYiLCJyZW1vdGVTZXNzaW9uIiwic2V0SXNMb2FkaW5nIiwib25Jbml0IiwiZGlyZWN0Q29ubmVjdCIsInNzaFJlbW90ZSIsInNlc3Npb24iLCJhY3RpdmVSZW1vdGUiLCJpc1JlbW90ZU1vZGUiLCJzZXRQYXN0ZWRDb250ZW50cyIsInN1Ym1pdENvdW50Iiwic2V0U3VibWl0Q291bnQiLCJyZXNwb25zZUxlbmd0aFJlZiIsImFwaU1ldHJpY3NSZWYiLCJ0dGZ0TXMiLCJmaXJzdFRva2VuVGltZSIsImxhc3RUb2tlblRpbWUiLCJyZXNwb25zZUxlbmd0aEJhc2VsaW5lIiwiZW5kUmVzcG9uc2VMZW5ndGgiLCJzZXRSZXNwb25zZUxlbmd0aCIsImVudHJpZXMiLCJsYXN0RW50cnkiLCJzdHJlYW1pbmdUZXh0Iiwic2V0U3RyZWFtaW5nVGV4dCIsInJlZHVjZWRNb3Rpb24iLCJwcmVmZXJzUmVkdWNlZE1vdGlvbiIsInNob3dTdHJlYW1pbmdUZXh0Iiwib25TdHJlYW1pbmdUZXh0IiwidmlzaWJsZVN0cmVhbWluZ1RleHQiLCJzdWJzdHJpbmciLCJsYXN0SW5kZXhPZiIsImxhc3RRdWVyeUNvbXBsZXRpb25UaW1lIiwic2V0TGFzdFF1ZXJ5Q29tcGxldGlvblRpbWUiLCJzcGlubmVyTWVzc2FnZSIsInNldFNwaW5uZXJNZXNzYWdlIiwic3Bpbm5lckNvbG9yIiwic2V0U3Bpbm5lckNvbG9yIiwic3Bpbm5lclNoaW1tZXJDb2xvciIsInNldFNwaW5uZXJTaGltbWVyQ29sb3IiLCJpc01lc3NhZ2VTZWxlY3RvclZpc2libGUiLCJzZXRJc01lc3NhZ2VTZWxlY3RvclZpc2libGUiLCJtZXNzYWdlU2VsZWN0b3JQcmVzZWxlY3QiLCJzZXRNZXNzYWdlU2VsZWN0b3JQcmVzZWxlY3QiLCJzaG93Q29zdERpYWxvZyIsInNldFNob3dDb3N0RGlhbG9nIiwiY29udmVyc2F0aW9uSWQiLCJzZXRDb252ZXJzYXRpb25JZCIsImlkbGVSZXR1cm5QZW5kaW5nIiwic2V0SWRsZVJldHVyblBlbmRpbmciLCJpZGxlTWludXRlcyIsInNraXBJZGxlQ2hlY2tSZWYiLCJsYXN0UXVlcnlDb21wbGV0aW9uVGltZVJlZiIsImNvbnRlbnRSZXBsYWNlbWVudFN0YXRlUmVmIiwiaGF2ZVNob3duQ29zdERpYWxvZyIsInNldEhhdmVTaG93bkNvc3REaWFsb2ciLCJoYXNBY2tub3dsZWRnZWRDb3N0VGhyZXNob2xkIiwidmltTW9kZSIsInNldFZpbU1vZGUiLCJzaG93QmFzaGVzRGlhbG9nIiwic2V0U2hvd0Jhc2hlc0RpYWxvZyIsImlzU2VhcmNoaW5nSGlzdG9yeSIsInNldElzU2VhcmNoaW5nSGlzdG9yeSIsImlzSGVscE9wZW4iLCJzZXRJc0hlbHBPcGVuIiwiaXNUZXJtaW5hbEZvY3VzZWQiLCJ0ZXJtaW5hbEZvY3VzUmVmIiwidGhlbWUiLCJ0aXBQaWNrZWRUaGlzVHVyblJlZiIsInBpY2tOZXdTcGlubmVyVGlwIiwiYmFzaFRvb2xzUHJvY2Vzc2VkSWR4IiwiYmFzaFRvb2xzIiwiYWRkIiwicmVhZEZpbGVTdGF0ZSIsInRpcCIsImNvbnRlbnQiLCJyZXNldExvYWRpbmdTdGF0ZSIsImhhc1J1bm5pbmdUZWFtbWF0ZXMiLCJ0b3RhbE1zIiwiZGVmZXJyZWRCdWRnZXQiLCJzYWZlWW9sb01lc3NhZ2VTaG93blJlZiIsImF1dG9QZXJtaXNzaW9uc05vdGlmaWNhdGlvbkNvdW50IiwicmVmIiwicHJldkNvdW50Iiwid29ya3RyZWVUaXBTaG93blJlZiIsInd0IiwiY3JlYXRpb25EdXJhdGlvbk1zIiwidXNlZFNwYXJzZVBhdGhzIiwic2VjcyIsIm9ubHlTbGVlcFRvb2xBY3RpdmUiLCJsYXN0QXNzaXN0YW50IiwiZmluZExhc3QiLCJ0eXBlIiwiaW5Qcm9ncmVzc1Rvb2xVc2VzIiwibWVzc2FnZSIsImlkIiwiZXZlcnkiLCJtck9uQmVmb3JlUXVlcnkiLCJtck9uVHVybkNvbXBsZXRlIiwicmVuZGVyIiwibXJSZW5kZXIiLCJoYXNBY3RpdmVQcm9tcHQiLCJxdWV1ZSIsImZlZWRiYWNrU3VydmV5T3JpZ2luYWwiLCJza2lsbEltcHJvdmVtZW50U3VydmV5Iiwic2hvd0lzc3VlRmxhZ0Jhbm5lciIsImZlZWRiYWNrU3VydmV5IiwiaGFuZGxlU2VsZWN0Iiwic2VsZWN0ZWQiLCJkaWRBdXRvUnVuSXNzdWVSZWYiLCJzaG93ZWRUcmFuc2NyaXB0UHJvbXB0Iiwic2V0QXV0b1J1bklzc3VlUmVhc29uIiwicG9zdENvbXBhY3RTdXJ2ZXkiLCJtZW1vcnlTdXJ2ZXkiLCJmcnVzdHJhdGlvbkRldGVjdGlvbiIsInNldElERUluc3RhbGxhdGlvblN0YXRlIiwiZmlsZUhpc3RvcnlTdGF0ZSIsInJlc3VtZSIsInNlc3Npb25JZCIsImxvZyIsImVudHJ5cG9pbnQiLCJyZXN1bWVTdGFydCIsInBlcmZvcm1hbmNlIiwiY29vcmRpbmF0b3JNb2R1bGUiLCJ3YXJuaW5nIiwibWF0Y2hTZXNzaW9uTW9kZSIsImdldEFnZW50RGVmaW5pdGlvbnNXaXRoT3ZlcnJpZGVzIiwiZ2V0QWN0aXZlQWdlbnRzRnJvbUxpc3QiLCJjYWNoZSIsImNsZWFyIiwiZnJlc2hBZ2VudERlZnMiLCJhbGxBZ2VudHMiLCJhY3RpdmVBZ2VudHMiLCJwdXNoIiwic2Vzc2lvbkVuZFRpbWVvdXRNcyIsImdldEFwcFN0YXRlIiwiZ2V0U3RhdGUiLCJzaWduYWwiLCJBYm9ydFNpZ25hbCIsInRpbWVvdXQiLCJ0aW1lb3V0TXMiLCJob29rTWVzc2FnZXMiLCJtb2RlbCIsImZpbGVIaXN0b3J5U25hcHNob3RzIiwiYWdlbnREZWZpbml0aW9uIiwicmVzdG9yZWRBZ2VudCIsImFnZW50U2V0dGluZyIsImFnZW50Iiwic3RhbmRhbG9uZUFnZW50Q29udGV4dCIsImFnZW50TmFtZSIsImFnZW50Q29sb3IiLCJyZXN0b3JlUmVhZEZpbGVTdGF0ZSIsInByb2plY3RQYXRoIiwidGFyZ2V0U2Vzc2lvbkNvc3RzIiwiZnVsbFBhdGgiLCJyZW5hbWVSZWNvcmRpbmdGb3JTZXNzaW9uIiwid29ya3RyZWVTZXNzaW9uIiwid3MiLCJzYXZlTW9kZSIsImlzQ29vcmRpbmF0b3JNb2RlIiwiY29udGVudFJlcGxhY2VtZW50cyIsInN1Y2Nlc3MiLCJyZXN1bWVfZHVyYXRpb25fbXMiLCJpbml0aWFsUmVhZEZpbGVTdGF0ZSIsImRpc2NvdmVyZWRTa2lsbE5hbWVzUmVmIiwibG9hZGVkTmVzdGVkTWVtb3J5UGF0aHNSZWYiLCJjd2QiLCJleHRyYWN0ZWQiLCJhcGlLZXlTdGF0dXMiLCJyZXZlcmlmeSIsImF1dG9SdW5Jc3N1ZVJlYXNvbiIsImV4aXRGbG93Iiwic2V0RXhpdEZsb3ciLCJpc0V4aXRpbmciLCJzZXRJc0V4aXRpbmciLCJzaG93aW5nQ29zdERpYWxvZyIsImFsbG93RGlhbG9nc1dpdGhBbmltYXRpb24iLCJmb2N1c2VkSW5wdXREaWFsb2ciLCJoYXNTdXBwcmVzc2VkRGlhbG9ncyIsImlzUGF1c2VkIiwicHJldkRpYWxvZ1JlZiIsIndhcyIsInBhdXNlUHJvYWN0aXZlIiwiZm9yY2VFbmQiLCJvbkFib3J0IiwiaXRlbSIsImFib3J0IiwiY2FuY2VsUmVxdWVzdCIsImhhbmRsZVF1ZXVlZENvbW1hbmRPbkNhbmNlbCIsImltYWdlcyIsIm5ld0NvbnRlbnRzIiwiaW1hZ2UiLCJjYW5jZWxSZXF1ZXN0UHJvcHMiLCJvbkFnZW50c0tpbGxlZCIsImFib3J0U2lnbmFsIiwicG9wQ29tbWFuZEZyb21RdWV1ZSIsInRvdGFsQ29zdCIsInNhbmRib3hBc2tDYWxsYmFjayIsInJlcXVlc3RJZCIsInNlbnQiLCJob3N0IiwicmVzb2x2ZVNob3VsZEFsbG93SG9zdCIsInJlc29sdmVPbmNlIiwiYWxsb3ciLCJicmlkZ2VDYWxsYmFja3MiLCJyZXBsQnJpZGdlUGVybWlzc2lvbkNhbGxiYWNrcyIsImJyaWRnZVJlcXVlc3RJZCIsInNlbmRSZXF1ZXN0IiwidW5zdWJzY3JpYmUiLCJvblJlc3BvbnNlIiwiYmVoYXZpb3IiLCJzaWJsaW5nQ2xlYW51cHMiLCJnZXQiLCJmbiIsImRlbGV0ZSIsImNsZWFudXAiLCJleGlzdGluZyIsInNldCIsInJlYXNvbiIsImdldFNhbmRib3hVbmF2YWlsYWJsZVJlYXNvbiIsImlzU2FuZGJveFJlcXVpcmVkIiwic3RkZXJyIiwid3JpdGUiLCJsZXZlbCIsImlzU2FuZGJveGluZ0VuYWJsZWQiLCJpbml0aWFsaXplIiwiY2F0Y2giLCJlcnIiLCJzZXRUb29sUGVybWlzc2lvbkNvbnRleHQiLCJjb250ZXh0Iiwib3B0aW9ucyIsInByZXNlcnZlTW9kZSIsInNldEltbWVkaWF0ZSIsImN1cnJlbnRRdWV1ZSIsInJlY2hlY2tQZXJtaXNzaW9uIiwiY2FuVXNlVG9vbCIsInJlcXVlc3RQcm9tcHQiLCJnZXRUb29sVXNlQ29udGV4dCIsImNvbXB1dGVUb29scyIsImFzc2VtYmxlZCIsIm1lcmdlZCIsInRoaW5raW5nRW5hYmxlZCIsIm1jcFJlc291cmNlcyIsInJlc291cmNlcyIsImlzTm9uSW50ZXJhY3RpdmVTZXNzaW9uIiwicmVmcmVzaFRvb2xzIiwidXBkYXRlRmlsZUhpc3RvcnlTdGF0ZSIsInVwZGF0ZXIiLCJ1cGRhdGVkIiwidXBkYXRlQXR0cmlidXRpb25TdGF0ZSIsImF0dHJpYnV0aW9uIiwib3Blbk1lc3NhZ2VTZWxlY3RvciIsIm9uQ2hhbmdlQVBJS2V5IiwiYXBwZW5kU3lzdGVtTWVzc2FnZSIsIm1zZyIsInNlbmRPU05vdGlmaWNhdGlvbiIsIm9wdHMiLCJvbkluc3RhbGxJREVFeHRlbnNpb24iLCJuZXN0ZWRNZW1vcnlBdHRhY2htZW50VHJpZ2dlcnMiLCJsb2FkZWROZXN0ZWRNZW1vcnlQYXRocyIsImR5bmFtaWNTa2lsbERpclRyaWdnZXJzIiwiZGlzY292ZXJlZFNraWxsTmFtZXMiLCJwdXNoQXBpTWV0cmljc0VudHJ5IiwiYmFzZWxpbmUiLCJvbkNvbXBhY3RQcm9ncmVzcyIsImV2ZW50IiwiaG9va1R5cGUiLCJzZXRIYXNJbnRlcnJ1cHRpYmxlVG9vbEluUHJvZ3Jlc3MiLCJ2IiwiY29udGVudFJlcGxhY2VtZW50U3RhdGUiLCJoYW5kbGVCYWNrZ3JvdW5kUXVlcnkiLCJyZW1vdmVkTm90aWZpY2F0aW9ucyIsInRvb2xVc2VDb250ZXh0IiwiZGVmYXVsdFN5c3RlbVByb21wdCIsInVzZXJDb250ZXh0Iiwic3lzdGVtQ29udGV4dCIsImFsbCIsImZyb20iLCJhZGRpdGlvbmFsV29ya2luZ0RpcmVjdG9yaWVzIiwia2V5cyIsInJlbmRlcmVkU3lzdGVtUHJvbXB0Iiwibm90aWZpY2F0aW9uQXR0YWNobWVudHMiLCJub3RpZmljYXRpb25NZXNzYWdlcyIsImV4aXN0aW5nUHJvbXB0cyIsImF0dGFjaG1lbnQiLCJjb21tYW5kTW9kZSIsInByb21wdCIsInVuaXF1ZU5vdGlmaWNhdGlvbnMiLCJxdWVyeVBhcmFtcyIsInF1ZXJ5U291cmNlIiwiZGVzY3JpcHRpb24iLCJoYW5kbGVCYWNrZ3JvdW5kU2Vzc2lvbiIsIm9uQmFja2dyb3VuZFF1ZXJ5Iiwib25RdWVyeUV2ZW50IiwiUGFyYW1ldGVycyIsIm5ld01lc3NhZ2UiLCJvbGQiLCJpbmNsdWRlU25pcHBlZCIsInNldENvbnRleHRCbG9ja2VkIiwiZGF0YSIsIm9sZE1lc3NhZ2VzIiwibGFzdCIsInBhcmVudFRvb2xVc2VJRCIsImNvcHkiLCJpc0FwaUVycm9yTWVzc2FnZSIsIm5ld0NvbnRlbnQiLCJ0b21ic3RvbmVkTWVzc2FnZSIsIm1ldHJpY3MiLCJvblF1ZXJ5SW1wbCIsIm1lc3NhZ2VzSW5jbHVkaW5nTmV3TWVzc2FnZXMiLCJzaG91bGRRdWVyeSIsImFkZGl0aW9uYWxBbGxvd2VkVG9vbHMiLCJtYWluTG9vcE1vZGVsUGFyYW0iLCJlZmZvcnQiLCJmcmVzaENsaWVudHMiLCJoYW5kbGVRdWVyeVN0YXJ0IiwiaWRlQ2xpZW50IiwiZmlyc3RVc2VyTWVzc2FnZSIsImZpbmQiLCJpc01ldGEiLCJzdGFydHNXaXRoIiwic2V0U3RhdGUiLCJjdXIiLCJhbHdheXNBbGxvd1J1bGVzIiwiY29tbWFuZCIsImkiLCJmcmVzaFRvb2xzIiwiZnJlc2hNY3BDbGllbnRzIiwicHJldmlvdXNHZXRBcHBTdGF0ZSIsImVmZm9ydFZhbHVlIiwiYmFzZVVzZXJDb250ZXh0IiwiZmFzdE1vZGUiLCJ0ZXJtaW5hbEZvY3VzIiwiZmlyZUNvbXBhbmlvbk9ic2VydmVyIiwicmVhY3Rpb24iLCJ0dGZ0cyIsImUiLCJvdHBzVmFsdWVzIiwic2FtcGxpbmdNcyIsImlzTXVsdGlSZXF1ZXN0IiwiaG9va01zIiwiaG9va0NvdW50IiwidG9vbE1zIiwidG9vbENvdW50IiwiY2xhc3NpZmllck1zIiwiY2xhc3NpZmllckNvdW50IiwidHVybk1zIiwib3RwcyIsImlzUDUwIiwiaG9va0R1cmF0aW9uTXMiLCJ0dXJuRHVyYXRpb25NcyIsInRvb2xEdXJhdGlvbk1zIiwiY2xhc3NpZmllckR1cmF0aW9uTXMiLCJjb25maWdXcml0ZUNvdW50Iiwib25RdWVyeSIsIm9uQmVmb3JlUXVlcnlDYWxsYmFjayIsInRlYW1OYW1lIiwidGhpc0dlbmVyYXRpb24iLCJ0cnlTdGFydCIsInBhcnNlZEJ1ZGdldCIsImxhdGVzdE1lc3NhZ2VzIiwic2hvdWxkUHJvY2VlZCIsImVuZCIsImFib3J0ZWQiLCJ0dW5nc3RlbkFjdGl2ZVNlc3Npb24iLCJ0dW5nc3RlblBhbmVsQXV0b0hpZGRlbiIsImJ1ZGdldEluZm8iLCJoYXNSdW5uaW5nU3dhcm1BZ2VudHMiLCJtc2dzIiwibGFzdFVzZXJNc2ciLCJpZHgiLCJpbml0aWFsTWVzc2FnZVJlZiIsInBlbmRpbmciLCJwcm9jZXNzSW5pdGlhbE1lc3NhZ2UiLCJpbml0aWFsTXNnIiwiTm9uTnVsbGFibGUiLCJjbGVhckNvbnRleHQiLCJvbGRQbGFuU2x1ZyIsInBsYW5Db250ZW50IiwiY2xlYXJDb252ZXJzYXRpb24iLCJzaG91bGRTdG9yZVBsYW5Gb3JWZXJpZmljYXRpb24iLCJ1cGRhdGVkVG9vbFBlcm1pc3Npb25Db250ZXh0IiwiYWxsb3dlZFByb21wdHMiLCJwcmVQbGFuTW9kZSIsInBlbmRpbmdQbGFuVmVyaWZpY2F0aW9uIiwicGxhbiIsInZlcmlmaWNhdGlvblN0YXJ0ZWQiLCJ2ZXJpZmljYXRpb25Db21wbGV0ZWQiLCJvblN1Ym1pdCIsInNldEN1cnNvck9mZnNldCIsImNsZWFyQnVmZmVyIiwicmVzZXRIaXN0b3J5IiwibmV3QWJvcnRDb250cm9sbGVyIiwiaGVscGVycyIsInNwZWN1bGF0aW9uQWNjZXB0Iiwic3BlY3VsYXRpb25TZXNzaW9uVGltZVNhdmVkTXMiLCJmcm9tS2V5YmluZGluZyIsInJlc3VtZVByb2FjdGl2ZSIsInRyaW1tZWRJbnB1dCIsInNwYWNlSW5kZXgiLCJpbmRleE9mIiwiY29tbWFuZE5hbWUiLCJjb21tYW5kQXJncyIsIm1hdGNoaW5nQ29tbWFuZCIsImFsaWFzZXMiLCJpbmNsdWRlcyIsInZhcmlhbnQiLCJtZXNzYWdlQ291bnQiLCJ0b3RhbElucHV0VG9rZW5zIiwic2hvdWxkVHJlYXRBc0ltbWVkaWF0ZSIsImltbWVkaWF0ZSIsInBhc3RlZFRleHRSZWZzIiwiciIsInBhc3RlZFRleHRDb3VudCIsInBhc3RlZFRleHRCeXRlcyIsInJlZHVjZSIsInN1bSIsImV4ZWN1dGVJbW1lZGlhdGVDb21tYW5kIiwiZG9uZVdhc0NhbGxlZCIsIm9uRG9uZSIsImRvbmVPcHRpb25zIiwiZGlzcGxheSIsIm1ldGFNZXNzYWdlcyIsIm1vZCIsImxvYWQiLCJjYWxsIiwid2lsbG93TW9kZSIsImlkbGVUaHJlc2hvbGRNaW4iLCJOdW1iZXIiLCJDTEFVREVfQ09ERV9JRExFX1RIUkVTSE9MRF9NSU5VVEVTIiwidG9rZW5UaHJlc2hvbGQiLCJDTEFVREVfQ09ERV9JRExFX1RPS0VOX1RIUkVTSE9MRCIsImlkbGVSZXR1cm5EaXNtaXNzZWQiLCJpZGxlTXMiLCJpc1NsYXNoQ29tbWFuZCIsInN1Ym1pdHNOb3ciLCJzbmFwc2hvdCIsInF1ZXJ5UmVxdWlyZWQiLCJjIiwic3BsaXQiLCJwYXN0ZWRWYWx1ZXMiLCJPYmplY3QiLCJpbWFnZUNvbnRlbnRzIiwiaW1hZ2VQYXN0ZUlkcyIsIm1lc3NhZ2VDb250ZW50IiwicmVtb3RlQ29udGVudCIsImNvbnRlbnRCbG9ja3MiLCJyZW1vdGVCbG9ja3MiLCJwYXN0ZWQiLCJzb3VyY2UiLCJjb25zdCIsIm1lZGlhX3R5cGUiLCJtZWRpYVR5cGUiLCJ1c2VyTWVzc2FnZSIsInNlbmRNZXNzYWdlIiwib25JbnB1dENoYW5nZSIsImhhc0ludGVycnVwdGlibGVUb29sSW5Qcm9ncmVzcyIsIm9uQWdlbnRTdWJtaXQiLCJ0YXNrIiwiYWdlbnRJZCIsImhhbmRsZUF1dG9SdW5Jc3N1ZSIsImhhbmRsZUNhbmNlbEF1dG9SdW5Jc3N1ZSIsImhhbmRsZVN1cnZleVJlcXVlc3RGZWVkYmFjayIsIlN0cmluZyIsIm9uU3VibWl0UmVmIiwiaGFuZGxlT3BlblJhdGVMaW1pdE9wdGlvbnMiLCJoYW5kbGVFeGl0Iiwic3RkaW8iLCJzaG93V29ya3RyZWUiLCJleGl0TW9kIiwiZXhpdEZsb3dSZXN1bHQiLCJoYW5kbGVTaG93TWVzc2FnZVNlbGVjdG9yIiwicmV3aW5kQ29udmVyc2F0aW9uVG8iLCJtZXNzYWdlSW5kZXgiLCJwcmVSZXdpbmRNZXNzYWdlQ291bnQiLCJwb3N0UmV3aW5kTWVzc2FnZUNvdW50IiwibWVzc2FnZXNSZW1vdmVkIiwicmV3aW5kVG9NZXNzYWdlSW5kZXgiLCJyZXNldENvbnRleHRDb2xsYXBzZSIsInBlcm1pc3Npb25Nb2RlIiwicHJvbXB0U3VnZ2VzdGlvbiIsInByb21wdElkIiwic2hvd25BdCIsImFjY2VwdGVkQXQiLCJnZW5lcmF0aW9uUmVxdWVzdElkIiwicmVzdG9yZU1lc3NhZ2VTeW5jIiwiaXNBcnJheSIsImJsb2NrIiwiaW1hZ2VCbG9ja3MiLCJuZXdQYXN0ZWRDb250ZW50cyIsImluZGV4IiwiaGFuZGxlUmVzdG9yZU1lc3NhZ2UiLCJyZXN0b3JlIiwiZmluZFJhd0luZGV4IiwiZmluZEluZGV4IiwibWVzc2FnZUFjdGlvbkNhcHMiLCJyYXciLCJzdGRvdXQiLCJjb2xvciIsImVkaXQiLCJyYXdJZHgiLCJub0ZpbGVDaGFuZ2VzIiwib25seVN5bnRoZXRpYyIsImVudGVyIiwiZW50ZXJNZXNzYWdlQWN0aW9ucyIsImhhbmRsZXJzIiwibWVzc2FnZUFjdGlvbkhhbmRsZXJzIiwibWVtb3J5RmlsZXMiLCJmaWxlTGlzdCIsInBhdGgiLCJwYXJlbnQiLCJmaWxlIiwiY29udGVudERpZmZlcnNGcm9tRGlzayIsInJhd0NvbnRlbnQiLCJ0aW1lc3RhbXAiLCJvZmZzZXQiLCJpc1BhcnRpYWxWaWV3Iiwic2VuZEJyaWRnZVJlc3VsdCIsImhhc0NvdW50ZWRRdWV1ZVVzZVJlZiIsInByb21wdFF1ZXVlVXNlQ291bnQiLCJleGVjdXRlUXVldWVkSW5wdXQiLCJoYXNBY3RpdmVMb2NhbEpzeFVJIiwicmVjb3JkVXNlckFjdGl2aXR5IiwibGFzdFVzZXJJbnRlcmFjdGlvbiIsImlkbGVUaW1lU2luY2VSZXNwb25zZSIsIm1lc3NhZ2VJZGxlTm90aWZUaHJlc2hvbGRNcyIsIm5vdGlmaWNhdGlvblR5cGUiLCJpZGxlVGhyZXNob2xkTXMiLCJscWN0IiwiYWRkTm90aWYiLCJtc2dzUmVmIiwiaGludFJlZiIsInRvdGFsVG9rZW5zIiwiZm9ybWF0dGVkVG9rZW5zIiwibWF4IiwiaGFuZGxlSW5jb21pbmdQcm9tcHQiLCJ2b2ljZSIsImludGVyaW1SYW5nZSIsIm9uU3VibWl0TWVzc2FnZSIsImFzc2lzdGFudE1vZGUiLCJrYWlyb3NFbmFibGVkIiwib25TdWJtaXRUYXNrIiwicXVldWVkQ29tbWFuZHNMZW5ndGgiLCJpc0luUGxhbk1vZGUiLCJvblN1Ym1pdFRpY2siLCJvblF1ZXVlVGljayIsInNodXRkb3duIiwiaW50ZXJuYWxfZXZlbnRFbWl0dGVyIiwicmVtb3VudEtleSIsInNldFJlbW91bnRLZXkiLCJoYW5kbGVTdXNwZW5kIiwiaGFuZGxlUmVzdW1lIiwib24iLCJzdG9wSG9va1NwaW5uZXJTdWZmaXgiLCJwcm9ncmVzc01zZ3MiLCJob29rRXZlbnQiLCJjdXJyZW50VG9vbFVzZUlEIiwidG9vbFVzZUlEIiwiaGFzU3VtbWFyeUZvckN1cnJlbnRFeGVjdXRpb24iLCJzdWJ0eXBlIiwiY3VycmVudEhvb2tzIiwicCIsInRvdGFsIiwiY29tcGxldGVkQ291bnQiLCJjdXN0b21NZXNzYWdlIiwic3RhdHVzTWVzc2FnZSIsImxhYmVsIiwiaGFuZGxlRW50ZXJUcmFuc2NyaXB0IiwiaGFuZGxlRXhpdFRyYW5zY3JpcHQiLCJ2aXJ0dWFsU2Nyb2xsQWN0aXZlIiwic2VhcmNoT3BlbiIsInNldFNlYXJjaE9wZW4iLCJzZWFyY2hRdWVyeSIsInNlYXJjaENvdW50Iiwic2V0U2VhcmNoQ291bnQiLCJzZWFyY2hDdXJyZW50Iiwic2V0U2VhcmNoQ3VycmVudCIsIm9uU2VhcmNoTWF0Y2hlc0NoYW5nZSIsImN0cmwiLCJtZXRhIiwic2V0QW5jaG9yIiwic3RvcEltbWVkaWF0ZVByb3BhZ2F0aW9uIiwicmVwZWF0IiwibmV4dE1hdGNoIiwicHJldk1hdGNoIiwic2V0UXVlcnkiLCJzY2FuRWxlbWVudCIsInNldFBvc2l0aW9ucyIsInRyYW5zY3JpcHRDb2xzIiwiY29sdW1ucyIsInByZXZDb2xzUmVmIiwiZGlzYXJtU2VhcmNoIiwiZ2VuIiwic2V0U3RhdHVzIiwidyIsInJlcGxhY2UiLCJvcGVuZWQiLCJpblRyYW5zY3JpcHQiLCJnbG9iYWxLZXliaW5kaW5nUHJvcHMiLCJvbkVudGVyVHJhbnNjcmlwdCIsIm9uRXhpdFRyYW5zY3JpcHQiLCJzZWFyY2hCYXJPcGVuIiwidHJhbnNjcmlwdE1lc3NhZ2VzIiwidHJhbnNjcmlwdFN0cmVhbWluZ1Rvb2xVc2VzIiwib25PcGVuQmFja2dyb3VuZFRhc2tzIiwidHJhbnNjcmlwdFNjcm9sbFJlZiIsInRyYW5zY3JpcHRNZXNzYWdlc0VsZW1lbnQiLCJ0cmFuc2NyaXB0VG9vbEpTWCIsInRyYW5zY3JpcHRSZXR1cm4iLCJxIiwidmlld2VkVGFzayIsInZpZXdlZFRlYW1tYXRlVGFzayIsInZpZXdlZEFnZW50VGFzayIsInVzZXNTeW5jTWVzc2FnZXMiLCJkaXNwbGF5ZWRNZXNzYWdlcyIsInBsYWNlaG9sZGVyVGV4dCIsInRvb2xQZXJtaXNzaW9uT3ZlcmxheSIsInRhaWwiLCJ3b3JrZXJCYWRnZSIsImNvbXBhbmlvbk5hcnJvdyIsImNvbXBhbmlvblZpc2libGUiLCJ0b29sSnN4Q2VudGVyZWQiLCJjZW50ZXJlZE1vZGFsIiwibWFpblJldHVybiIsInNpemUiLCJwZXJzaXN0VG9TZXR0aW5ncyIsImN1cnJlbnRSZXF1ZXN0IiwiYXBwcm92ZWRIb3N0IiwidXBkYXRlIiwicnVsZXMiLCJ0b29sTmFtZSIsInJ1bGVDb250ZW50IiwiZGVzdGluYXRpb24iLCJyZWZyZXNoQ29uZmlnIiwiY2xlYW51cHMiLCJzZWxlY3RlZEtleSIsInByb21wdF9yZXNwb25zZSIsInBvcnQiLCJ3b3JrZXJOYW1lIiwic2VydmVyTmFtZSIsInJlc3BvbmQiLCJpc1VybEFjY2VwdCIsInBhcmFtcyIsIm9uV2FpdGluZ0Rpc21pc3MiLCJzZWxlY3Rpb24iLCJtb2RlbEFsaWFzIiwibWFpbkxvb3BNb2RlbEZvclNlc3Npb24iLCJyZXBsQnJpZGdlRW5hYmxlZCIsInJlcGxCcmlkZ2VFeHBsaWNpdCIsInJlcGxCcmlkZ2VPdXRib3VuZE9ubHkiLCJwbHVnaW5OYW1lIiwicGx1Z2luRGVzY3JpcHRpb24iLCJtYXJrZXRwbGFjZU5hbWUiLCJzb3VyY2VDb21tYW5kIiwiZmlsZUV4dGVuc2lvbiIsImNob2ljZSIsImJsdXJiIiwiYXBwZW5kU3Rkb3V0IiwiYXBwZW5kV2hlbklkbGUiLCJ1bnN1YiIsInVsdHJhcGxhblNlc3Npb25VcmwiLCJsYXVuY2hVbHRyYXBsYW4iLCJkaXNjb25uZWN0ZWRCcmlkZ2UiLCJvblNlc3Npb25SZWFkeSIsImxhc3RSZXNwb25zZSIsInN1Z2dlc3Rpb24iLCJpc09wZW4iLCJza2lsbE5hbWUiLCJ1cGRhdGVzIiwiZmVlZGJhY2siLCJkaXJlY3Rpb24iLCJjb21wYWN0TWVzc2FnZXMiLCJhcHBTdGF0ZSIsImRlZmF1bHRTeXNQcm9tcHQiLCJmb3JrQ29udGV4dE1lc3NhZ2VzIiwia2VwdCIsIm1lc3NhZ2VzVG9LZWVwIiwib3JkZXJlZCIsInN1bW1hcnlNZXNzYWdlcyIsInBvc3RDb21wYWN0IiwiYm91bmRhcnlNYXJrZXIiLCJhdHRhY2htZW50cyIsImhvb2tSZXN1bHRzIiwiaGlzdG9yeVNob3J0Y3V0Il0sInNvdXJjZXMiOlsiUkVQTC50c3giXSwic291cmNlc0NvbnRlbnQiOlsiLy8gYmlvbWUtaWdub3JlLWFsbCBhc3Npc3Qvc291cmNlL29yZ2FuaXplSW1wb3J0czogQU5ULU9OTFkgaW1wb3J0IG1hcmtlcnMgbXVzdCBub3QgYmUgcmVvcmRlcmVkXG5pbXBvcnQgeyBmZWF0dXJlIH0gZnJvbSAnYnVuOmJ1bmRsZSdcbmltcG9ydCB7IHNwYXduU3luYyB9IGZyb20gJ2NoaWxkX3Byb2Nlc3MnXG5pbXBvcnQge1xuICBzbmFwc2hvdE91dHB1dFRva2Vuc0ZvclR1cm4sXG4gIGdldEN1cnJlbnRUdXJuVG9rZW5CdWRnZXQsXG4gIGdldFR1cm5PdXRwdXRUb2tlbnMsXG4gIGdldEJ1ZGdldENvbnRpbnVhdGlvbkNvdW50LFxuICBnZXRUb3RhbElucHV0VG9rZW5zLFxufSBmcm9tICcuLi9ib290c3RyYXAvc3RhdGUuanMnXG5pbXBvcnQgeyBwYXJzZVRva2VuQnVkZ2V0IH0gZnJvbSAnLi4vdXRpbHMvdG9rZW5CdWRnZXQuanMnXG5pbXBvcnQgeyBjb3VudCB9IGZyb20gJy4uL3V0aWxzL2FycmF5LmpzJ1xuaW1wb3J0IHsgZGlybmFtZSwgam9pbiB9IGZyb20gJ3BhdGgnXG5pbXBvcnQgeyB0bXBkaXIgfSBmcm9tICdvcydcbmltcG9ydCBmaWd1cmVzIGZyb20gJ2ZpZ3VyZXMnXG4vLyBlc2xpbnQtZGlzYWJsZS1uZXh0LWxpbmUgY3VzdG9tLXJ1bGVzL3ByZWZlci11c2Uta2V5YmluZGluZ3MgLS0gLyBuIE4gRXNjIFsgdiBhcmUgYmFyZSBsZXR0ZXJzIGluIHRyYW5zY3JpcHQgbW9kYWwgY29udGV4dCwgc2FtZSBjbGFzcyBhcyBnL0cvai9rIGluIFNjcm9sbEtleWJpbmRpbmdIYW5kbGVyXG5pbXBvcnQgeyB1c2VJbnB1dCB9IGZyb20gJy4uL2luay5qcydcbmltcG9ydCB7IHVzZVNlYXJjaElucHV0IH0gZnJvbSAnLi4vaG9va3MvdXNlU2VhcmNoSW5wdXQuanMnXG5pbXBvcnQgeyB1c2VUZXJtaW5hbFNpemUgfSBmcm9tICcuLi9ob29rcy91c2VUZXJtaW5hbFNpemUuanMnXG5pbXBvcnQgeyB1c2VTZWFyY2hIaWdobGlnaHQgfSBmcm9tICcuLi9pbmsvaG9va3MvdXNlLXNlYXJjaC1oaWdobGlnaHQuanMnXG5pbXBvcnQgdHlwZSB7IEp1bXBIYW5kbGUgfSBmcm9tICcuLi9jb21wb25lbnRzL1ZpcnR1YWxNZXNzYWdlTGlzdC5qcydcbmltcG9ydCB7IHJlbmRlck1lc3NhZ2VzVG9QbGFpblRleHQgfSBmcm9tICcuLi91dGlscy9leHBvcnRSZW5kZXJlci5qcydcbmltcG9ydCB7IG9wZW5GaWxlSW5FeHRlcm5hbEVkaXRvciB9IGZyb20gJy4uL3V0aWxzL2VkaXRvci5qcydcbmltcG9ydCB7IHdyaXRlRmlsZSB9IGZyb20gJ2ZzL3Byb21pc2VzJ1xuaW1wb3J0IHtcbiAgQm94LFxuICBUZXh0LFxuICB1c2VTdGRpbixcbiAgdXNlVGhlbWUsXG4gIHVzZVRlcm1pbmFsRm9jdXMsXG4gIHVzZVRlcm1pbmFsVGl0bGUsXG4gIHVzZVRhYlN0YXR1cyxcbn0gZnJvbSAnLi4vaW5rLmpzJ1xuaW1wb3J0IHR5cGUgeyBUYWJTdGF0dXNLaW5kIH0gZnJvbSAnLi4vaW5rL2hvb2tzL3VzZS10YWItc3RhdHVzLmpzJ1xuaW1wb3J0IHsgQ29zdFRocmVzaG9sZERpYWxvZyB9IGZyb20gJy4uL2NvbXBvbmVudHMvQ29zdFRocmVzaG9sZERpYWxvZy5qcydcbmltcG9ydCB7IElkbGVSZXR1cm5EaWFsb2cgfSBmcm9tICcuLi9jb21wb25lbnRzL0lkbGVSZXR1cm5EaWFsb2cuanMnXG5pbXBvcnQgKiBhcyBSZWFjdCBmcm9tICdyZWFjdCdcbmltcG9ydCB7XG4gIHVzZUVmZmVjdCxcbiAgdXNlTWVtbyxcbiAgdXNlUmVmLFxuICB1c2VTdGF0ZSxcbiAgdXNlQ2FsbGJhY2ssXG4gIHVzZURlZmVycmVkVmFsdWUsXG4gIHVzZUxheW91dEVmZmVjdCxcbiAgdHlwZSBSZWZPYmplY3QsXG59IGZyb20gJ3JlYWN0J1xuaW1wb3J0IHsgdXNlTm90aWZpY2F0aW9ucyB9IGZyb20gJy4uL2NvbnRleHQvbm90aWZpY2F0aW9ucy5qcydcbmltcG9ydCB7IHNlbmROb3RpZmljYXRpb24gfSBmcm9tICcuLi9zZXJ2aWNlcy9ub3RpZmllci5qcydcbmltcG9ydCB7XG4gIHN0YXJ0UHJldmVudFNsZWVwLFxuICBzdG9wUHJldmVudFNsZWVwLFxufSBmcm9tICcuLi9zZXJ2aWNlcy9wcmV2ZW50U2xlZXAuanMnXG5pbXBvcnQgeyB1c2VUZXJtaW5hbE5vdGlmaWNhdGlvbiB9IGZyb20gJy4uL2luay91c2VUZXJtaW5hbE5vdGlmaWNhdGlvbi5qcydcbmltcG9ydCB7IGhhc0N1cnNvclVwVmlld3BvcnRZYW5rQnVnIH0gZnJvbSAnLi4vaW5rL3Rlcm1pbmFsLmpzJ1xuaW1wb3J0IHtcbiAgY3JlYXRlRmlsZVN0YXRlQ2FjaGVXaXRoU2l6ZUxpbWl0LFxuICBtZXJnZUZpbGVTdGF0ZUNhY2hlcyxcbiAgUkVBRF9GSUxFX1NUQVRFX0NBQ0hFX1NJWkUsXG59IGZyb20gJy4uL3V0aWxzL2ZpbGVTdGF0ZUNhY2hlLmpzJ1xuaW1wb3J0IHtcbiAgdXBkYXRlTGFzdEludGVyYWN0aW9uVGltZSxcbiAgZ2V0TGFzdEludGVyYWN0aW9uVGltZSxcbiAgZ2V0T3JpZ2luYWxDd2QsXG4gIGdldFByb2plY3RSb290LFxuICBnZXRTZXNzaW9uSWQsXG4gIHN3aXRjaFNlc3Npb24sXG4gIHNldENvc3RTdGF0ZUZvclJlc3RvcmUsXG4gIGdldFR1cm5Ib29rRHVyYXRpb25NcyxcbiAgZ2V0VHVybkhvb2tDb3VudCxcbiAgcmVzZXRUdXJuSG9va0R1cmF0aW9uLFxuICBnZXRUdXJuVG9vbER1cmF0aW9uTXMsXG4gIGdldFR1cm5Ub29sQ291bnQsXG4gIHJlc2V0VHVyblRvb2xEdXJhdGlvbixcbiAgZ2V0VHVybkNsYXNzaWZpZXJEdXJhdGlvbk1zLFxuICBnZXRUdXJuQ2xhc3NpZmllckNvdW50LFxuICByZXNldFR1cm5DbGFzc2lmaWVyRHVyYXRpb24sXG59IGZyb20gJy4uL2Jvb3RzdHJhcC9zdGF0ZS5qcydcbmltcG9ydCB7IGFzU2Vzc2lvbklkLCBhc0FnZW50SWQgfSBmcm9tICcuLi90eXBlcy9pZHMuanMnXG5pbXBvcnQgeyBsb2dGb3JEZWJ1Z2dpbmcgfSBmcm9tICcuLi91dGlscy9kZWJ1Zy5qcydcbmltcG9ydCB7IFF1ZXJ5R3VhcmQgfSBmcm9tICcuLi91dGlscy9RdWVyeUd1YXJkLmpzJ1xuaW1wb3J0IHsgaXNFbnZUcnV0aHkgfSBmcm9tICcuLi91dGlscy9lbnZVdGlscy5qcydcbmltcG9ydCB7IGZvcm1hdFRva2VucywgdHJ1bmNhdGVUb1dpZHRoIH0gZnJvbSAnLi4vdXRpbHMvZm9ybWF0LmpzJ1xuaW1wb3J0IHsgY29uc3VtZUVhcmx5SW5wdXQgfSBmcm9tICcuLi91dGlscy9lYXJseUlucHV0LmpzJ1xuXG5pbXBvcnQgeyBzZXRNZW1iZXJBY3RpdmUgfSBmcm9tICcuLi91dGlscy9zd2FybS90ZWFtSGVscGVycy5qcydcbmltcG9ydCB7XG4gIGlzU3dhcm1Xb3JrZXIsXG4gIGdlbmVyYXRlU2FuZGJveFJlcXVlc3RJZCxcbiAgc2VuZFNhbmRib3hQZXJtaXNzaW9uUmVxdWVzdFZpYU1haWxib3gsXG4gIHNlbmRTYW5kYm94UGVybWlzc2lvblJlc3BvbnNlVmlhTWFpbGJveCxcbn0gZnJvbSAnLi4vdXRpbHMvc3dhcm0vcGVybWlzc2lvblN5bmMuanMnXG5pbXBvcnQgeyByZWdpc3RlclNhbmRib3hQZXJtaXNzaW9uQ2FsbGJhY2sgfSBmcm9tICcuLi9ob29rcy91c2VTd2FybVBlcm1pc3Npb25Qb2xsZXIuanMnXG5pbXBvcnQgeyBnZXRUZWFtTmFtZSwgZ2V0QWdlbnROYW1lIH0gZnJvbSAnLi4vdXRpbHMvdGVhbW1hdGUuanMnXG5pbXBvcnQgeyBXb3JrZXJQZW5kaW5nUGVybWlzc2lvbiB9IGZyb20gJy4uL2NvbXBvbmVudHMvcGVybWlzc2lvbnMvV29ya2VyUGVuZGluZ1Blcm1pc3Npb24uanMnXG5pbXBvcnQge1xuICBpbmplY3RVc2VyTWVzc2FnZVRvVGVhbW1hdGUsXG4gIGdldEFsbEluUHJvY2Vzc1RlYW1tYXRlVGFza3MsXG59IGZyb20gJy4uL3Rhc2tzL0luUHJvY2Vzc1RlYW1tYXRlVGFzay9JblByb2Nlc3NUZWFtbWF0ZVRhc2suanMnXG5pbXBvcnQge1xuICBpc0xvY2FsQWdlbnRUYXNrLFxuICBxdWV1ZVBlbmRpbmdNZXNzYWdlLFxuICBhcHBlbmRNZXNzYWdlVG9Mb2NhbEFnZW50LFxuICB0eXBlIExvY2FsQWdlbnRUYXNrU3RhdGUsXG59IGZyb20gJy4uL3Rhc2tzL0xvY2FsQWdlbnRUYXNrL0xvY2FsQWdlbnRUYXNrLmpzJ1xuaW1wb3J0IHtcbiAgcmVnaXN0ZXJMZWFkZXJUb29sVXNlQ29uZmlybVF1ZXVlLFxuICB1bnJlZ2lzdGVyTGVhZGVyVG9vbFVzZUNvbmZpcm1RdWV1ZSxcbiAgcmVnaXN0ZXJMZWFkZXJTZXRUb29sUGVybWlzc2lvbkNvbnRleHQsXG4gIHVucmVnaXN0ZXJMZWFkZXJTZXRUb29sUGVybWlzc2lvbkNvbnRleHQsXG59IGZyb20gJy4uL3V0aWxzL3N3YXJtL2xlYWRlclBlcm1pc3Npb25CcmlkZ2UuanMnXG5pbXBvcnQgeyBlbmRJbnRlcmFjdGlvblNwYW4gfSBmcm9tICcuLi91dGlscy90ZWxlbWV0cnkvc2Vzc2lvblRyYWNpbmcuanMnXG5pbXBvcnQgeyB1c2VMb2dNZXNzYWdlcyB9IGZyb20gJy4uL2hvb2tzL3VzZUxvZ01lc3NhZ2VzLmpzJ1xuaW1wb3J0IHsgdXNlUmVwbEJyaWRnZSB9IGZyb20gJy4uL2hvb2tzL3VzZVJlcGxCcmlkZ2UuanMnXG5pbXBvcnQge1xuICB0eXBlIENvbW1hbmQsXG4gIHR5cGUgQ29tbWFuZFJlc3VsdERpc3BsYXksXG4gIHR5cGUgUmVzdW1lRW50cnlwb2ludCxcbiAgZ2V0Q29tbWFuZE5hbWUsXG4gIGlzQ29tbWFuZEVuYWJsZWQsXG59IGZyb20gJy4uL2NvbW1hbmRzLmpzJ1xuaW1wb3J0IHR5cGUge1xuICBQcm9tcHRJbnB1dE1vZGUsXG4gIFF1ZXVlZENvbW1hbmQsXG4gIFZpbU1vZGUsXG59IGZyb20gJy4uL3R5cGVzL3RleHRJbnB1dFR5cGVzLmpzJ1xuaW1wb3J0IHtcbiAgTWVzc2FnZVNlbGVjdG9yLFxuICBzZWxlY3RhYmxlVXNlck1lc3NhZ2VzRmlsdGVyLFxuICBtZXNzYWdlc0FmdGVyQXJlT25seVN5bnRoZXRpYyxcbn0gZnJvbSAnLi4vY29tcG9uZW50cy9NZXNzYWdlU2VsZWN0b3IuanMnXG5pbXBvcnQgeyB1c2VJZGVMb2dnaW5nIH0gZnJvbSAnLi4vaG9va3MvdXNlSWRlTG9nZ2luZy5qcydcbmltcG9ydCB7XG4gIFBlcm1pc3Npb25SZXF1ZXN0LFxuICB0eXBlIFRvb2xVc2VDb25maXJtLFxufSBmcm9tICcuLi9jb21wb25lbnRzL3Blcm1pc3Npb25zL1Blcm1pc3Npb25SZXF1ZXN0LmpzJ1xuaW1wb3J0IHsgRWxpY2l0YXRpb25EaWFsb2cgfSBmcm9tICcuLi9jb21wb25lbnRzL21jcC9FbGljaXRhdGlvbkRpYWxvZy5qcydcbmltcG9ydCB7IFByb21wdERpYWxvZyB9IGZyb20gJy4uL2NvbXBvbmVudHMvaG9va3MvUHJvbXB0RGlhbG9nLmpzJ1xuaW1wb3J0IHR5cGUgeyBQcm9tcHRSZXF1ZXN0LCBQcm9tcHRSZXNwb25zZSB9IGZyb20gJy4uL3R5cGVzL2hvb2tzLmpzJ1xuaW1wb3J0IFByb21wdElucHV0IGZyb20gJy4uL2NvbXBvbmVudHMvUHJvbXB0SW5wdXQvUHJvbXB0SW5wdXQuanMnXG5pbXBvcnQgeyBQcm9tcHRJbnB1dFF1ZXVlZENvbW1hbmRzIH0gZnJvbSAnLi4vY29tcG9uZW50cy9Qcm9tcHRJbnB1dC9Qcm9tcHRJbnB1dFF1ZXVlZENvbW1hbmRzLmpzJ1xuaW1wb3J0IHsgdXNlUmVtb3RlU2Vzc2lvbiB9IGZyb20gJy4uL2hvb2tzL3VzZVJlbW90ZVNlc3Npb24uanMnXG5pbXBvcnQgeyB1c2VEaXJlY3RDb25uZWN0IH0gZnJvbSAnLi4vaG9va3MvdXNlRGlyZWN0Q29ubmVjdC5qcydcbmltcG9ydCB0eXBlIHsgRGlyZWN0Q29ubmVjdENvbmZpZyB9IGZyb20gJy4uL3NlcnZlci9kaXJlY3RDb25uZWN0TWFuYWdlci5qcydcbmltcG9ydCB7IHVzZVNTSFNlc3Npb24gfSBmcm9tICcuLi9ob29rcy91c2VTU0hTZXNzaW9uLmpzJ1xuaW1wb3J0IHsgdXNlQXNzaXN0YW50SGlzdG9yeSB9IGZyb20gJy4uL2hvb2tzL3VzZUFzc2lzdGFudEhpc3RvcnkuanMnXG5pbXBvcnQgdHlwZSB7IFNTSFNlc3Npb24gfSBmcm9tICcuLi9zc2gvY3JlYXRlU1NIU2Vzc2lvbi5qcydcbmltcG9ydCB7IFNraWxsSW1wcm92ZW1lbnRTdXJ2ZXkgfSBmcm9tICcuLi9jb21wb25lbnRzL1NraWxsSW1wcm92ZW1lbnRTdXJ2ZXkuanMnXG5pbXBvcnQgeyB1c2VTa2lsbEltcHJvdmVtZW50U3VydmV5IH0gZnJvbSAnLi4vaG9va3MvdXNlU2tpbGxJbXByb3ZlbWVudFN1cnZleS5qcydcbmltcG9ydCB7IHVzZU1vcmVSaWdodCB9IGZyb20gJy4uL21vcmVyaWdodC91c2VNb3JlUmlnaHQuanMnXG5pbXBvcnQge1xuICBTcGlubmVyV2l0aFZlcmIsXG4gIEJyaWVmSWRsZVN0YXR1cyxcbiAgdHlwZSBTcGlubmVyTW9kZSxcbn0gZnJvbSAnLi4vY29tcG9uZW50cy9TcGlubmVyLmpzJ1xuaW1wb3J0IHsgZ2V0U3lzdGVtUHJvbXB0IH0gZnJvbSAnLi4vY29uc3RhbnRzL3Byb21wdHMuanMnXG5pbXBvcnQgeyBidWlsZEVmZmVjdGl2ZVN5c3RlbVByb21wdCB9IGZyb20gJy4uL3V0aWxzL3N5c3RlbVByb21wdC5qcydcbmltcG9ydCB7IGdldFN5c3RlbUNvbnRleHQsIGdldFVzZXJDb250ZXh0IH0gZnJvbSAnLi4vY29udGV4dC5qcydcbmltcG9ydCB7IGdldE1lbW9yeUZpbGVzIH0gZnJvbSAnLi4vdXRpbHMvY2xhdWRlbWQuanMnXG5pbXBvcnQgeyBzdGFydEJhY2tncm91bmRIb3VzZWtlZXBpbmcgfSBmcm9tICcuLi91dGlscy9iYWNrZ3JvdW5kSG91c2VrZWVwaW5nLmpzJ1xuaW1wb3J0IHtcbiAgZ2V0VG90YWxDb3N0LFxuICBzYXZlQ3VycmVudFNlc3Npb25Db3N0cyxcbiAgcmVzZXRDb3N0U3RhdGUsXG4gIGdldFN0b3JlZFNlc3Npb25Db3N0cyxcbn0gZnJvbSAnLi4vY29zdC10cmFja2VyLmpzJ1xuaW1wb3J0IHsgdXNlQ29zdFN1bW1hcnkgfSBmcm9tICcuLi9jb3N0SG9vay5qcydcbmltcG9ydCB7IHVzZUZwc01ldHJpY3MgfSBmcm9tICcuLi9jb250ZXh0L2Zwc01ldHJpY3MuanMnXG5pbXBvcnQgeyB1c2VBZnRlckZpcnN0UmVuZGVyIH0gZnJvbSAnLi4vaG9va3MvdXNlQWZ0ZXJGaXJzdFJlbmRlci5qcydcbmltcG9ydCB7IHVzZURlZmVycmVkSG9va01lc3NhZ2VzIH0gZnJvbSAnLi4vaG9va3MvdXNlRGVmZXJyZWRIb29rTWVzc2FnZXMuanMnXG5pbXBvcnQge1xuICBhZGRUb0hpc3RvcnksXG4gIHJlbW92ZUxhc3RGcm9tSGlzdG9yeSxcbiAgZXhwYW5kUGFzdGVkVGV4dFJlZnMsXG4gIHBhcnNlUmVmZXJlbmNlcyxcbn0gZnJvbSAnLi4vaGlzdG9yeS5qcydcbmltcG9ydCB7IHByZXBlbmRNb2RlQ2hhcmFjdGVyVG9JbnB1dCB9IGZyb20gJy4uL2NvbXBvbmVudHMvUHJvbXB0SW5wdXQvaW5wdXRNb2Rlcy5qcydcbmltcG9ydCB7IHByZXBlbmRUb1NoZWxsSGlzdG9yeUNhY2hlIH0gZnJvbSAnLi4vdXRpbHMvc3VnZ2VzdGlvbnMvc2hlbGxIaXN0b3J5Q29tcGxldGlvbi5qcydcbmltcG9ydCB7IHVzZUFwaUtleVZlcmlmaWNhdGlvbiB9IGZyb20gJy4uL2hvb2tzL3VzZUFwaUtleVZlcmlmaWNhdGlvbi5qcydcbmltcG9ydCB7IEdsb2JhbEtleWJpbmRpbmdIYW5kbGVycyB9IGZyb20gJy4uL2hvb2tzL3VzZUdsb2JhbEtleWJpbmRpbmdzLmpzJ1xuaW1wb3J0IHsgQ29tbWFuZEtleWJpbmRpbmdIYW5kbGVycyB9IGZyb20gJy4uL2hvb2tzL3VzZUNvbW1hbmRLZXliaW5kaW5ncy5qcydcbmltcG9ydCB7IEtleWJpbmRpbmdTZXR1cCB9IGZyb20gJy4uL2tleWJpbmRpbmdzL0tleWJpbmRpbmdQcm92aWRlclNldHVwLmpzJ1xuaW1wb3J0IHsgdXNlU2hvcnRjdXREaXNwbGF5IH0gZnJvbSAnLi4va2V5YmluZGluZ3MvdXNlU2hvcnRjdXREaXNwbGF5LmpzJ1xuaW1wb3J0IHsgZ2V0U2hvcnRjdXREaXNwbGF5IH0gZnJvbSAnLi4va2V5YmluZGluZ3Mvc2hvcnRjdXRGb3JtYXQuanMnXG5pbXBvcnQgeyBDYW5jZWxSZXF1ZXN0SGFuZGxlciB9IGZyb20gJy4uL2hvb2tzL3VzZUNhbmNlbFJlcXVlc3QuanMnXG5pbXBvcnQgeyB1c2VCYWNrZ3JvdW5kVGFza05hdmlnYXRpb24gfSBmcm9tICcuLi9ob29rcy91c2VCYWNrZ3JvdW5kVGFza05hdmlnYXRpb24uanMnXG5pbXBvcnQgeyB1c2VTd2FybUluaXRpYWxpemF0aW9uIH0gZnJvbSAnLi4vaG9va3MvdXNlU3dhcm1Jbml0aWFsaXphdGlvbi5qcydcbmltcG9ydCB7IHVzZVRlYW1tYXRlVmlld0F1dG9FeGl0IH0gZnJvbSAnLi4vaG9va3MvdXNlVGVhbW1hdGVWaWV3QXV0b0V4aXQuanMnXG5pbXBvcnQgeyBlcnJvck1lc3NhZ2UgfSBmcm9tICcuLi91dGlscy9lcnJvcnMuanMnXG5pbXBvcnQgeyBpc0h1bWFuVHVybiB9IGZyb20gJy4uL3V0aWxzL21lc3NhZ2VQcmVkaWNhdGVzLmpzJ1xuaW1wb3J0IHsgbG9nRXJyb3IgfSBmcm9tICcuLi91dGlscy9sb2cuanMnXG4vLyBEZWFkIGNvZGUgZWxpbWluYXRpb246IGNvbmRpdGlvbmFsIGltcG9ydHNcbi8qIGVzbGludC1kaXNhYmxlIGN1c3RvbS1ydWxlcy9uby1wcm9jZXNzLWVudi10b3AtbGV2ZWwsIEB0eXBlc2NyaXB0LWVzbGludC9uby1yZXF1aXJlLWltcG9ydHMgKi9cbmNvbnN0IHVzZVZvaWNlSW50ZWdyYXRpb246IHR5cGVvZiBpbXBvcnQoJy4uL2hvb2tzL3VzZVZvaWNlSW50ZWdyYXRpb24uanMnKS51c2VWb2ljZUludGVncmF0aW9uID1cbiAgZmVhdHVyZSgnVk9JQ0VfTU9ERScpXG4gICAgPyByZXF1aXJlKCcuLi9ob29rcy91c2VWb2ljZUludGVncmF0aW9uLmpzJykudXNlVm9pY2VJbnRlZ3JhdGlvblxuICAgIDogKCkgPT4gKHtcbiAgICAgICAgc3RyaXBUcmFpbGluZzogKCkgPT4gMCxcbiAgICAgICAgaGFuZGxlS2V5RXZlbnQ6ICgpID0+IHt9LFxuICAgICAgICByZXNldEFuY2hvcjogKCkgPT4ge30sXG4gICAgICB9KVxuY29uc3QgVm9pY2VLZXliaW5kaW5nSGFuZGxlcjogdHlwZW9mIGltcG9ydCgnLi4vaG9va3MvdXNlVm9pY2VJbnRlZ3JhdGlvbi5qcycpLlZvaWNlS2V5YmluZGluZ0hhbmRsZXIgPVxuICBmZWF0dXJlKCdWT0lDRV9NT0RFJylcbiAgICA/IHJlcXVpcmUoJy4uL2hvb2tzL3VzZVZvaWNlSW50ZWdyYXRpb24uanMnKS5Wb2ljZUtleWJpbmRpbmdIYW5kbGVyXG4gICAgOiAoKSA9PiBudWxsXG4vLyBGcnVzdHJhdGlvbiBkZXRlY3Rpb24gaXMgYW50LW9ubHkgKGRvZ2Zvb2RpbmcpLiBDb25kaXRpb25hbCByZXF1aXJlIHNvIGV4dGVybmFsXG4vLyBidWlsZHMgZWxpbWluYXRlIHRoZSBtb2R1bGUgZW50aXJlbHkgKGluY2x1ZGluZyBpdHMgdHdvIE8obikgdXNlTWVtb3MgdGhhdCBydW5cbi8vIG9uIGV2ZXJ5IG1lc3NhZ2VzIGNoYW5nZSwgcGx1cyB0aGUgR3Jvd3RoQm9vayBmZXRjaCkuXG5jb25zdCB1c2VGcnVzdHJhdGlvbkRldGVjdGlvbjogdHlwZW9mIGltcG9ydCgnLi4vY29tcG9uZW50cy9GZWVkYmFja1N1cnZleS91c2VGcnVzdHJhdGlvbkRldGVjdGlvbi5qcycpLnVzZUZydXN0cmF0aW9uRGV0ZWN0aW9uID1cbiAgXCJleHRlcm5hbFwiID09PSAnYW50J1xuICAgID8gcmVxdWlyZSgnLi4vY29tcG9uZW50cy9GZWVkYmFja1N1cnZleS91c2VGcnVzdHJhdGlvbkRldGVjdGlvbi5qcycpXG4gICAgICAgIC51c2VGcnVzdHJhdGlvbkRldGVjdGlvblxuICAgIDogKCkgPT4gKHsgc3RhdGU6ICdjbG9zZWQnLCBoYW5kbGVUcmFuc2NyaXB0U2VsZWN0OiAoKSA9PiB7fSB9KVxuLy8gQW50LW9ubHkgb3JnIHdhcm5pbmcuIENvbmRpdGlvbmFsIHJlcXVpcmUgc28gdGhlIG9yZyBVVUlEIGxpc3QgaXNcbi8vIGVsaW1pbmF0ZWQgZnJvbSBleHRlcm5hbCBidWlsZHMgKG9uZSBVVUlEIGlzIG9uIGV4Y2x1ZGVkLXN0cmluZ3MpLlxuY29uc3QgdXNlQW50T3JnV2FybmluZ05vdGlmaWNhdGlvbjogdHlwZW9mIGltcG9ydCgnLi4vaG9va3Mvbm90aWZzL3VzZUFudE9yZ1dhcm5pbmdOb3RpZmljYXRpb24uanMnKS51c2VBbnRPcmdXYXJuaW5nTm90aWZpY2F0aW9uID1cbiAgXCJleHRlcm5hbFwiID09PSAnYW50J1xuICAgID8gcmVxdWlyZSgnLi4vaG9va3Mvbm90aWZzL3VzZUFudE9yZ1dhcm5pbmdOb3RpZmljYXRpb24uanMnKVxuICAgICAgICAudXNlQW50T3JnV2FybmluZ05vdGlmaWNhdGlvblxuICAgIDogKCkgPT4ge31cbi8vIERlYWQgY29kZSBlbGltaW5hdGlvbjogY29uZGl0aW9uYWwgaW1wb3J0IGZvciBjb29yZGluYXRvciBtb2RlXG5jb25zdCBnZXRDb29yZGluYXRvclVzZXJDb250ZXh0OiAoXG4gIG1jcENsaWVudHM6IFJlYWRvbmx5QXJyYXk8eyBuYW1lOiBzdHJpbmcgfT4sXG4gIHNjcmF0Y2hwYWREaXI/OiBzdHJpbmcsXG4pID0+IHsgW2s6IHN0cmluZ106IHN0cmluZyB9ID0gZmVhdHVyZSgnQ09PUkRJTkFUT1JfTU9ERScpXG4gID8gcmVxdWlyZSgnLi4vY29vcmRpbmF0b3IvY29vcmRpbmF0b3JNb2RlLmpzJykuZ2V0Q29vcmRpbmF0b3JVc2VyQ29udGV4dFxuICA6ICgpID0+ICh7fSlcbi8qIGVzbGludC1lbmFibGUgY3VzdG9tLXJ1bGVzL25vLXByb2Nlc3MtZW52LXRvcC1sZXZlbCwgQHR5cGVzY3JpcHQtZXNsaW50L25vLXJlcXVpcmUtaW1wb3J0cyAqL1xuaW1wb3J0IHVzZUNhblVzZVRvb2wgZnJvbSAnLi4vaG9va3MvdXNlQ2FuVXNlVG9vbC5qcydcbmltcG9ydCB0eXBlIHsgVG9vbFBlcm1pc3Npb25Db250ZXh0LCBUb29sIH0gZnJvbSAnLi4vVG9vbC5qcydcbmltcG9ydCB7XG4gIGFwcGx5UGVybWlzc2lvblVwZGF0ZSxcbiAgYXBwbHlQZXJtaXNzaW9uVXBkYXRlcyxcbiAgcGVyc2lzdFBlcm1pc3Npb25VcGRhdGUsXG59IGZyb20gJy4uL3V0aWxzL3Blcm1pc3Npb25zL1Blcm1pc3Npb25VcGRhdGUuanMnXG5pbXBvcnQgeyBidWlsZFBlcm1pc3Npb25VcGRhdGVzIH0gZnJvbSAnLi4vY29tcG9uZW50cy9wZXJtaXNzaW9ucy9FeGl0UGxhbk1vZGVQZXJtaXNzaW9uUmVxdWVzdC9FeGl0UGxhbk1vZGVQZXJtaXNzaW9uUmVxdWVzdC5qcydcbmltcG9ydCB7IHN0cmlwRGFuZ2Vyb3VzUGVybWlzc2lvbnNGb3JBdXRvTW9kZSB9IGZyb20gJy4uL3V0aWxzL3Blcm1pc3Npb25zL3Blcm1pc3Npb25TZXR1cC5qcydcbmltcG9ydCB7XG4gIGdldFNjcmF0Y2hwYWREaXIsXG4gIGlzU2NyYXRjaHBhZEVuYWJsZWQsXG59IGZyb20gJy4uL3V0aWxzL3Blcm1pc3Npb25zL2ZpbGVzeXN0ZW0uanMnXG5pbXBvcnQgeyBXRUJfRkVUQ0hfVE9PTF9OQU1FIH0gZnJvbSAnLi4vdG9vbHMvV2ViRmV0Y2hUb29sL3Byb21wdC5qcydcbmltcG9ydCB7IFNMRUVQX1RPT0xfTkFNRSB9IGZyb20gJy4uL3Rvb2xzL1NsZWVwVG9vbC9wcm9tcHQuanMnXG5pbXBvcnQgeyBjbGVhclNwZWN1bGF0aXZlQ2hlY2tzIH0gZnJvbSAnLi4vdG9vbHMvQmFzaFRvb2wvYmFzaFBlcm1pc3Npb25zLmpzJ1xuaW1wb3J0IHR5cGUgeyBBdXRvVXBkYXRlclJlc3VsdCB9IGZyb20gJy4uL3V0aWxzL2F1dG9VcGRhdGVyLmpzJ1xuaW1wb3J0IHtcbiAgZ2V0R2xvYmFsQ29uZmlnLFxuICBzYXZlR2xvYmFsQ29uZmlnLFxuICBnZXRHbG9iYWxDb25maWdXcml0ZUNvdW50LFxufSBmcm9tICcuLi91dGlscy9jb25maWcuanMnXG5pbXBvcnQgeyBoYXNDb25zb2xlQmlsbGluZ0FjY2VzcyB9IGZyb20gJy4uL3V0aWxzL2JpbGxpbmcuanMnXG5pbXBvcnQge1xuICBsb2dFdmVudCxcbiAgdHlwZSBBbmFseXRpY3NNZXRhZGF0YV9JX1ZFUklGSUVEX1RISVNfSVNfTk9UX0NPREVfT1JfRklMRVBBVEhTLFxufSBmcm9tICdzcmMvc2VydmljZXMvYW5hbHl0aWNzL2luZGV4LmpzJ1xuaW1wb3J0IHsgZ2V0RmVhdHVyZVZhbHVlX0NBQ0hFRF9NQVlfQkVfU1RBTEUgfSBmcm9tICdzcmMvc2VydmljZXMvYW5hbHl0aWNzL2dyb3d0aGJvb2suanMnXG5pbXBvcnQge1xuICB0ZXh0Rm9yUmVzdWJtaXQsXG4gIGhhbmRsZU1lc3NhZ2VGcm9tU3RyZWFtLFxuICB0eXBlIFN0cmVhbWluZ1Rvb2xVc2UsXG4gIHR5cGUgU3RyZWFtaW5nVGhpbmtpbmcsXG4gIGlzQ29tcGFjdEJvdW5kYXJ5TWVzc2FnZSxcbiAgZ2V0TWVzc2FnZXNBZnRlckNvbXBhY3RCb3VuZGFyeSxcbiAgZ2V0Q29udGVudFRleHQsXG4gIGNyZWF0ZVVzZXJNZXNzYWdlLFxuICBjcmVhdGVBc3Npc3RhbnRNZXNzYWdlLFxuICBjcmVhdGVUdXJuRHVyYXRpb25NZXNzYWdlLFxuICBjcmVhdGVBZ2VudHNLaWxsZWRNZXNzYWdlLFxuICBjcmVhdGVBcGlNZXRyaWNzTWVzc2FnZSxcbiAgY3JlYXRlU3lzdGVtTWVzc2FnZSxcbiAgY3JlYXRlQ29tbWFuZElucHV0TWVzc2FnZSxcbiAgZm9ybWF0Q29tbWFuZElucHV0VGFncyxcbn0gZnJvbSAnLi4vdXRpbHMvbWVzc2FnZXMuanMnXG5pbXBvcnQgeyBnZW5lcmF0ZVNlc3Npb25UaXRsZSB9IGZyb20gJy4uL3V0aWxzL3Nlc3Npb25UaXRsZS5qcydcbmltcG9ydCB7XG4gIEJBU0hfSU5QVVRfVEFHLFxuICBDT01NQU5EX01FU1NBR0VfVEFHLFxuICBDT01NQU5EX05BTUVfVEFHLFxuICBMT0NBTF9DT01NQU5EX1NURE9VVF9UQUcsXG59IGZyb20gJy4uL2NvbnN0YW50cy94bWwuanMnXG5pbXBvcnQgeyBlc2NhcGVYbWwgfSBmcm9tICcuLi91dGlscy94bWwuanMnXG5pbXBvcnQgdHlwZSB7IFRoaW5raW5nQ29uZmlnIH0gZnJvbSAnLi4vdXRpbHMvdGhpbmtpbmcuanMnXG5pbXBvcnQgeyBncmFjZWZ1bFNodXRkb3duU3luYyB9IGZyb20gJy4uL3V0aWxzL2dyYWNlZnVsU2h1dGRvd24uanMnXG5pbXBvcnQge1xuICBoYW5kbGVQcm9tcHRTdWJtaXQsXG4gIHR5cGUgUHJvbXB0SW5wdXRIZWxwZXJzLFxufSBmcm9tICcuLi91dGlscy9oYW5kbGVQcm9tcHRTdWJtaXQuanMnXG5pbXBvcnQgeyB1c2VRdWV1ZVByb2Nlc3NvciB9IGZyb20gJy4uL2hvb2tzL3VzZVF1ZXVlUHJvY2Vzc29yLmpzJ1xuaW1wb3J0IHsgdXNlTWFpbGJveEJyaWRnZSB9IGZyb20gJy4uL2hvb2tzL3VzZU1haWxib3hCcmlkZ2UuanMnXG5pbXBvcnQge1xuICBxdWVyeUNoZWNrcG9pbnQsXG4gIGxvZ1F1ZXJ5UHJvZmlsZVJlcG9ydCxcbn0gZnJvbSAnLi4vdXRpbHMvcXVlcnlQcm9maWxlci5qcydcbmltcG9ydCB0eXBlIHtcbiAgTWVzc2FnZSBhcyBNZXNzYWdlVHlwZSxcbiAgVXNlck1lc3NhZ2UsXG4gIFByb2dyZXNzTWVzc2FnZSxcbiAgSG9va1Jlc3VsdE1lc3NhZ2UsXG4gIFBhcnRpYWxDb21wYWN0RGlyZWN0aW9uLFxufSBmcm9tICcuLi90eXBlcy9tZXNzYWdlLmpzJ1xuaW1wb3J0IHsgcXVlcnkgfSBmcm9tICcuLi9xdWVyeS5qcydcbmltcG9ydCB7IG1lcmdlQ2xpZW50cywgdXNlTWVyZ2VkQ2xpZW50cyB9IGZyb20gJy4uL2hvb2tzL3VzZU1lcmdlZENsaWVudHMuanMnXG5pbXBvcnQgeyBnZXRRdWVyeVNvdXJjZUZvclJFUEwgfSBmcm9tICcuLi91dGlscy9wcm9tcHRDYXRlZ29yeS5qcydcbmltcG9ydCB7IHVzZU1lcmdlZFRvb2xzIH0gZnJvbSAnLi4vaG9va3MvdXNlTWVyZ2VkVG9vbHMuanMnXG5pbXBvcnQgeyBtZXJnZUFuZEZpbHRlclRvb2xzIH0gZnJvbSAnLi4vdXRpbHMvdG9vbFBvb2wuanMnXG5pbXBvcnQgeyB1c2VNZXJnZWRDb21tYW5kcyB9IGZyb20gJy4uL2hvb2tzL3VzZU1lcmdlZENvbW1hbmRzLmpzJ1xuaW1wb3J0IHsgdXNlU2tpbGxzQ2hhbmdlIH0gZnJvbSAnLi4vaG9va3MvdXNlU2tpbGxzQ2hhbmdlLmpzJ1xuaW1wb3J0IHsgdXNlTWFuYWdlUGx1Z2lucyB9IGZyb20gJy4uL2hvb2tzL3VzZU1hbmFnZVBsdWdpbnMuanMnXG5pbXBvcnQgeyBNZXNzYWdlcyB9IGZyb20gJy4uL2NvbXBvbmVudHMvTWVzc2FnZXMuanMnXG5pbXBvcnQgeyBUYXNrTGlzdFYyIH0gZnJvbSAnLi4vY29tcG9uZW50cy9UYXNrTGlzdFYyLmpzJ1xuaW1wb3J0IHsgVGVhbW1hdGVWaWV3SGVhZGVyIH0gZnJvbSAnLi4vY29tcG9uZW50cy9UZWFtbWF0ZVZpZXdIZWFkZXIuanMnXG5pbXBvcnQgeyB1c2VUYXNrc1YyV2l0aENvbGxhcHNlRWZmZWN0IH0gZnJvbSAnLi4vaG9va3MvdXNlVGFza3NWMi5qcydcbmltcG9ydCB7IG1heWJlTWFya1Byb2plY3RPbmJvYXJkaW5nQ29tcGxldGUgfSBmcm9tICcuLi9wcm9qZWN0T25ib2FyZGluZ1N0YXRlLmpzJ1xuaW1wb3J0IHR5cGUgeyBNQ1BTZXJ2ZXJDb25uZWN0aW9uIH0gZnJvbSAnLi4vc2VydmljZXMvbWNwL3R5cGVzLmpzJ1xuaW1wb3J0IHR5cGUgeyBTY29wZWRNY3BTZXJ2ZXJDb25maWcgfSBmcm9tICcuLi9zZXJ2aWNlcy9tY3AvdHlwZXMuanMnXG5pbXBvcnQgeyByYW5kb21VVUlELCB0eXBlIFVVSUQgfSBmcm9tICdjcnlwdG8nXG5pbXBvcnQgeyBwcm9jZXNzU2Vzc2lvblN0YXJ0SG9va3MgfSBmcm9tICcuLi91dGlscy9zZXNzaW9uU3RhcnQuanMnXG5pbXBvcnQge1xuICBleGVjdXRlU2Vzc2lvbkVuZEhvb2tzLFxuICBnZXRTZXNzaW9uRW5kSG9va1RpbWVvdXRNcyxcbn0gZnJvbSAnLi4vdXRpbHMvaG9va3MuanMnXG5pbXBvcnQgeyB0eXBlIElERVNlbGVjdGlvbiwgdXNlSWRlU2VsZWN0aW9uIH0gZnJvbSAnLi4vaG9va3MvdXNlSWRlU2VsZWN0aW9uLmpzJ1xuaW1wb3J0IHsgZ2V0VG9vbHMsIGFzc2VtYmxlVG9vbFBvb2wgfSBmcm9tICcuLi90b29scy5qcydcbmltcG9ydCB0eXBlIHsgQWdlbnREZWZpbml0aW9uIH0gZnJvbSAnLi4vdG9vbHMvQWdlbnRUb29sL2xvYWRBZ2VudHNEaXIuanMnXG5pbXBvcnQgeyByZXNvbHZlQWdlbnRUb29scyB9IGZyb20gJy4uL3Rvb2xzL0FnZW50VG9vbC9hZ2VudFRvb2xVdGlscy5qcydcbmltcG9ydCB7IHJlc3VtZUFnZW50QmFja2dyb3VuZCB9IGZyb20gJy4uL3Rvb2xzL0FnZW50VG9vbC9yZXN1bWVBZ2VudC5qcydcbmltcG9ydCB7IHVzZU1haW5Mb29wTW9kZWwgfSBmcm9tICcuLi9ob29rcy91c2VNYWluTG9vcE1vZGVsLmpzJ1xuaW1wb3J0IHtcbiAgdXNlQXBwU3RhdGUsXG4gIHVzZVNldEFwcFN0YXRlLFxuICB1c2VBcHBTdGF0ZVN0b3JlLFxufSBmcm9tICcuLi9zdGF0ZS9BcHBTdGF0ZS5qcydcbmltcG9ydCB0eXBlIHtcbiAgQ29udGVudEJsb2NrUGFyYW0sXG4gIEltYWdlQmxvY2tQYXJhbSxcbn0gZnJvbSAnQGFudGhyb3BpYy1haS9zZGsvcmVzb3VyY2VzL21lc3NhZ2VzLm1qcydcbmltcG9ydCB0eXBlIHsgUHJvY2Vzc1VzZXJJbnB1dENvbnRleHQgfSBmcm9tICcuLi91dGlscy9wcm9jZXNzVXNlcklucHV0L3Byb2Nlc3NVc2VySW5wdXQuanMnXG5pbXBvcnQgdHlwZSB7IFBhc3RlZENvbnRlbnQgfSBmcm9tICcuLi91dGlscy9jb25maWcuanMnXG5pbXBvcnQge1xuICBjb3B5UGxhbkZvckZvcmssXG4gIGNvcHlQbGFuRm9yUmVzdW1lLFxuICBnZXRQbGFuU2x1ZyxcbiAgc2V0UGxhblNsdWcsXG59IGZyb20gJy4uL3V0aWxzL3BsYW5zLmpzJ1xuaW1wb3J0IHtcbiAgY2xlYXJTZXNzaW9uTWV0YWRhdGEsXG4gIHJlc2V0U2Vzc2lvbkZpbGVQb2ludGVyLFxuICBhZG9wdFJlc3VtZWRTZXNzaW9uRmlsZSxcbiAgcmVtb3ZlVHJhbnNjcmlwdE1lc3NhZ2UsXG4gIHJlc3RvcmVTZXNzaW9uTWV0YWRhdGEsXG4gIGdldEN1cnJlbnRTZXNzaW9uVGl0bGUsXG4gIGlzRXBoZW1lcmFsVG9vbFByb2dyZXNzLFxuICBpc0xvZ2dhYmxlTWVzc2FnZSxcbiAgc2F2ZVdvcmt0cmVlU3RhdGUsXG4gIGdldEFnZW50VHJhbnNjcmlwdCxcbn0gZnJvbSAnLi4vdXRpbHMvc2Vzc2lvblN0b3JhZ2UuanMnXG5pbXBvcnQgeyBkZXNlcmlhbGl6ZU1lc3NhZ2VzIH0gZnJvbSAnLi4vdXRpbHMvY29udmVyc2F0aW9uUmVjb3ZlcnkuanMnXG5pbXBvcnQge1xuICBleHRyYWN0UmVhZEZpbGVzRnJvbU1lc3NhZ2VzLFxuICBleHRyYWN0QmFzaFRvb2xzRnJvbU1lc3NhZ2VzLFxufSBmcm9tICcuLi91dGlscy9xdWVyeUhlbHBlcnMuanMnXG5pbXBvcnQgeyByZXNldE1pY3JvY29tcGFjdFN0YXRlIH0gZnJvbSAnLi4vc2VydmljZXMvY29tcGFjdC9taWNyb0NvbXBhY3QuanMnXG5pbXBvcnQgeyBydW5Qb3N0Q29tcGFjdENsZWFudXAgfSBmcm9tICcuLi9zZXJ2aWNlcy9jb21wYWN0L3Bvc3RDb21wYWN0Q2xlYW51cC5qcydcbmltcG9ydCB7XG4gIHByb3Zpc2lvbkNvbnRlbnRSZXBsYWNlbWVudFN0YXRlLFxuICByZWNvbnN0cnVjdENvbnRlbnRSZXBsYWNlbWVudFN0YXRlLFxuICB0eXBlIENvbnRlbnRSZXBsYWNlbWVudFJlY29yZCxcbn0gZnJvbSAnLi4vdXRpbHMvdG9vbFJlc3VsdFN0b3JhZ2UuanMnXG5pbXBvcnQgeyBwYXJ0aWFsQ29tcGFjdENvbnZlcnNhdGlvbiB9IGZyb20gJy4uL3NlcnZpY2VzL2NvbXBhY3QvY29tcGFjdC5qcydcbmltcG9ydCB0eXBlIHsgTG9nT3B0aW9uIH0gZnJvbSAnLi4vdHlwZXMvbG9ncy5qcydcbmltcG9ydCB0eXBlIHsgQWdlbnRDb2xvck5hbWUgfSBmcm9tICcuLi90b29scy9BZ2VudFRvb2wvYWdlbnRDb2xvck1hbmFnZXIuanMnXG5pbXBvcnQge1xuICBmaWxlSGlzdG9yeU1ha2VTbmFwc2hvdCxcbiAgdHlwZSBGaWxlSGlzdG9yeVN0YXRlLFxuICBmaWxlSGlzdG9yeVJld2luZCxcbiAgdHlwZSBGaWxlSGlzdG9yeVNuYXBzaG90LFxuICBjb3B5RmlsZUhpc3RvcnlGb3JSZXN1bWUsXG4gIGZpbGVIaXN0b3J5RW5hYmxlZCxcbiAgZmlsZUhpc3RvcnlIYXNBbnlDaGFuZ2VzLFxufSBmcm9tICcuLi91dGlscy9maWxlSGlzdG9yeS5qcydcbmltcG9ydCB7XG4gIHR5cGUgQXR0cmlidXRpb25TdGF0ZSxcbiAgaW5jcmVtZW50UHJvbXB0Q291bnQsXG59IGZyb20gJy4uL3V0aWxzL2NvbW1pdEF0dHJpYnV0aW9uLmpzJ1xuaW1wb3J0IHsgcmVjb3JkQXR0cmlidXRpb25TbmFwc2hvdCB9IGZyb20gJy4uL3V0aWxzL3Nlc3Npb25TdG9yYWdlLmpzJ1xuaW1wb3J0IHtcbiAgY29tcHV0ZVN0YW5kYWxvbmVBZ2VudENvbnRleHQsXG4gIHJlc3RvcmVBZ2VudEZyb21TZXNzaW9uLFxuICByZXN0b3JlU2Vzc2lvblN0YXRlRnJvbUxvZyxcbiAgcmVzdG9yZVdvcmt0cmVlRm9yUmVzdW1lLFxuICBleGl0UmVzdG9yZWRXb3JrdHJlZSxcbn0gZnJvbSAnLi4vdXRpbHMvc2Vzc2lvblJlc3RvcmUuanMnXG5pbXBvcnQge1xuICBpc0JnU2Vzc2lvbixcbiAgdXBkYXRlU2Vzc2lvbk5hbWUsXG4gIHVwZGF0ZVNlc3Npb25BY3Rpdml0eSxcbn0gZnJvbSAnLi4vdXRpbHMvY29uY3VycmVudFNlc3Npb25zLmpzJ1xuaW1wb3J0IHtcbiAgaXNJblByb2Nlc3NUZWFtbWF0ZVRhc2ssXG4gIHR5cGUgSW5Qcm9jZXNzVGVhbW1hdGVUYXNrU3RhdGUsXG59IGZyb20gJy4uL3Rhc2tzL0luUHJvY2Vzc1RlYW1tYXRlVGFzay90eXBlcy5qcydcbmltcG9ydCB7IHJlc3RvcmVSZW1vdGVBZ2VudFRhc2tzIH0gZnJvbSAnLi4vdGFza3MvUmVtb3RlQWdlbnRUYXNrL1JlbW90ZUFnZW50VGFzay5qcydcbmltcG9ydCB7IHVzZUluYm94UG9sbGVyIH0gZnJvbSAnLi4vaG9va3MvdXNlSW5ib3hQb2xsZXIuanMnXG4vLyBEZWFkIGNvZGUgZWxpbWluYXRpb246IGNvbmRpdGlvbmFsIGltcG9ydCBmb3IgbG9vcCBtb2RlXG4vKiBlc2xpbnQtZGlzYWJsZSBAdHlwZXNjcmlwdC1lc2xpbnQvbm8tcmVxdWlyZS1pbXBvcnRzICovXG5jb25zdCBwcm9hY3RpdmVNb2R1bGUgPVxuICBmZWF0dXJlKCdQUk9BQ1RJVkUnKSB8fCBmZWF0dXJlKCdLQUlST1MnKVxuICAgID8gcmVxdWlyZSgnLi4vcHJvYWN0aXZlL2luZGV4LmpzJylcbiAgICA6IG51bGxcbmNvbnN0IFBST0FDVElWRV9OT19PUF9TVUJTQ1JJQkUgPSAoX2NiOiAoKSA9PiB2b2lkKSA9PiAoKSA9PiB7fVxuY29uc3QgUFJPQUNUSVZFX0ZBTFNFID0gKCkgPT4gZmFsc2VcbmNvbnN0IFNVR0dFU1RfQkdfUFJfTk9PUCA9IChfcDogc3RyaW5nLCBfbjogc3RyaW5nKTogYm9vbGVhbiA9PiBmYWxzZVxuY29uc3QgdXNlUHJvYWN0aXZlID1cbiAgZmVhdHVyZSgnUFJPQUNUSVZFJykgfHwgZmVhdHVyZSgnS0FJUk9TJylcbiAgICA/IHJlcXVpcmUoJy4uL3Byb2FjdGl2ZS91c2VQcm9hY3RpdmUuanMnKS51c2VQcm9hY3RpdmVcbiAgICA6IG51bGxcbmNvbnN0IHVzZVNjaGVkdWxlZFRhc2tzID0gZmVhdHVyZSgnQUdFTlRfVFJJR0dFUlMnKVxuICA/IHJlcXVpcmUoJy4uL2hvb2tzL3VzZVNjaGVkdWxlZFRhc2tzLmpzJykudXNlU2NoZWR1bGVkVGFza3NcbiAgOiBudWxsXG4vKiBlc2xpbnQtZW5hYmxlIEB0eXBlc2NyaXB0LWVzbGludC9uby1yZXF1aXJlLWltcG9ydHMgKi9cbmltcG9ydCB7IGlzQWdlbnRTd2FybXNFbmFibGVkIH0gZnJvbSAnLi4vdXRpbHMvYWdlbnRTd2FybXNFbmFibGVkLmpzJ1xuaW1wb3J0IHsgdXNlVGFza0xpc3RXYXRjaGVyIH0gZnJvbSAnLi4vaG9va3MvdXNlVGFza0xpc3RXYXRjaGVyLmpzJ1xuaW1wb3J0IHR5cGUge1xuICBTYW5kYm94QXNrQ2FsbGJhY2ssXG4gIE5ldHdvcmtIb3N0UGF0dGVybixcbn0gZnJvbSAnLi4vdXRpbHMvc2FuZGJveC9zYW5kYm94LWFkYXB0ZXIuanMnXG5cbmltcG9ydCB7XG4gIHR5cGUgSURFRXh0ZW5zaW9uSW5zdGFsbGF0aW9uU3RhdHVzLFxuICBjbG9zZU9wZW5EaWZmcyxcbiAgZ2V0Q29ubmVjdGVkSWRlQ2xpZW50LFxuICB0eXBlIElkZVR5cGUsXG59IGZyb20gJy4uL3V0aWxzL2lkZS5qcydcbmltcG9ydCB7IHVzZUlERUludGVncmF0aW9uIH0gZnJvbSAnLi4vaG9va3MvdXNlSURFSW50ZWdyYXRpb24uanMnXG5pbXBvcnQgZXhpdCBmcm9tICcuLi9jb21tYW5kcy9leGl0L2luZGV4LmpzJ1xuaW1wb3J0IHsgRXhpdEZsb3cgfSBmcm9tICcuLi9jb21wb25lbnRzL0V4aXRGbG93LmpzJ1xuaW1wb3J0IHsgZ2V0Q3VycmVudFdvcmt0cmVlU2Vzc2lvbiB9IGZyb20gJy4uL3V0aWxzL3dvcmt0cmVlLmpzJ1xuaW1wb3J0IHtcbiAgcG9wQWxsRWRpdGFibGUsXG4gIGVucXVldWUsXG4gIHR5cGUgU2V0QXBwU3RhdGUsXG4gIGdldENvbW1hbmRRdWV1ZSxcbiAgZ2V0Q29tbWFuZFF1ZXVlTGVuZ3RoLFxuICByZW1vdmVCeUZpbHRlcixcbn0gZnJvbSAnLi4vdXRpbHMvbWVzc2FnZVF1ZXVlTWFuYWdlci5qcydcbmltcG9ydCB7IHVzZUNvbW1hbmRRdWV1ZSB9IGZyb20gJy4uL2hvb2tzL3VzZUNvbW1hbmRRdWV1ZS5qcydcbmltcG9ydCB7IFNlc3Npb25CYWNrZ3JvdW5kSGludCB9IGZyb20gJy4uL2NvbXBvbmVudHMvU2Vzc2lvbkJhY2tncm91bmRIaW50LmpzJ1xuaW1wb3J0IHsgc3RhcnRCYWNrZ3JvdW5kU2Vzc2lvbiB9IGZyb20gJy4uL3Rhc2tzL0xvY2FsTWFpblNlc3Npb25UYXNrLmpzJ1xuaW1wb3J0IHsgdXNlU2Vzc2lvbkJhY2tncm91bmRpbmcgfSBmcm9tICcuLi9ob29rcy91c2VTZXNzaW9uQmFja2dyb3VuZGluZy5qcydcbmltcG9ydCB7IGRpYWdub3N0aWNUcmFja2VyIH0gZnJvbSAnLi4vc2VydmljZXMvZGlhZ25vc3RpY1RyYWNraW5nLmpzJ1xuaW1wb3J0IHtcbiAgaGFuZGxlU3BlY3VsYXRpb25BY2NlcHQsXG4gIHR5cGUgQWN0aXZlU3BlY3VsYXRpb25TdGF0ZSxcbn0gZnJvbSAnLi4vc2VydmljZXMvUHJvbXB0U3VnZ2VzdGlvbi9zcGVjdWxhdGlvbi5qcydcbmltcG9ydCB7IElkZU9uYm9hcmRpbmdEaWFsb2cgfSBmcm9tICcuLi9jb21wb25lbnRzL0lkZU9uYm9hcmRpbmdEaWFsb2cuanMnXG5pbXBvcnQge1xuICBFZmZvcnRDYWxsb3V0LFxuICBzaG91bGRTaG93RWZmb3J0Q2FsbG91dCxcbn0gZnJvbSAnLi4vY29tcG9uZW50cy9FZmZvcnRDYWxsb3V0LmpzJ1xuaW1wb3J0IHR5cGUgeyBFZmZvcnRWYWx1ZSB9IGZyb20gJy4uL3V0aWxzL2VmZm9ydC5qcydcbmltcG9ydCB7IFJlbW90ZUNhbGxvdXQgfSBmcm9tICcuLi9jb21wb25lbnRzL1JlbW90ZUNhbGxvdXQuanMnXG4vKiBlc2xpbnQtZGlzYWJsZSBjdXN0b20tcnVsZXMvbm8tcHJvY2Vzcy1lbnYtdG9wLWxldmVsLCBAdHlwZXNjcmlwdC1lc2xpbnQvbm8tcmVxdWlyZS1pbXBvcnRzICovXG5jb25zdCBBbnRNb2RlbFN3aXRjaENhbGxvdXQgPVxuICBcImV4dGVybmFsXCIgPT09ICdhbnQnXG4gICAgPyByZXF1aXJlKCcuLi9jb21wb25lbnRzL0FudE1vZGVsU3dpdGNoQ2FsbG91dC5qcycpLkFudE1vZGVsU3dpdGNoQ2FsbG91dFxuICAgIDogbnVsbFxuY29uc3Qgc2hvdWxkU2hvd0FudE1vZGVsU3dpdGNoID1cbiAgXCJleHRlcm5hbFwiID09PSAnYW50J1xuICAgID8gcmVxdWlyZSgnLi4vY29tcG9uZW50cy9BbnRNb2RlbFN3aXRjaENhbGxvdXQuanMnKVxuICAgICAgICAuc2hvdWxkU2hvd01vZGVsU3dpdGNoQ2FsbG91dFxuICAgIDogKCk6IGJvb2xlYW4gPT4gZmFsc2VcbmNvbnN0IFVuZGVyY292ZXJBdXRvQ2FsbG91dCA9XG4gIFwiZXh0ZXJuYWxcIiA9PT0gJ2FudCdcbiAgICA/IHJlcXVpcmUoJy4uL2NvbXBvbmVudHMvVW5kZXJjb3ZlckF1dG9DYWxsb3V0LmpzJykuVW5kZXJjb3ZlckF1dG9DYWxsb3V0XG4gICAgOiBudWxsXG4vKiBlc2xpbnQtZW5hYmxlIGN1c3RvbS1ydWxlcy9uby1wcm9jZXNzLWVudi10b3AtbGV2ZWwsIEB0eXBlc2NyaXB0LWVzbGludC9uby1yZXF1aXJlLWltcG9ydHMgKi9cbmltcG9ydCB7IGFjdGl2aXR5TWFuYWdlciB9IGZyb20gJy4uL3V0aWxzL2FjdGl2aXR5TWFuYWdlci5qcydcbmltcG9ydCB7IGNyZWF0ZUFib3J0Q29udHJvbGxlciB9IGZyb20gJy4uL3V0aWxzL2Fib3J0Q29udHJvbGxlci5qcydcbmltcG9ydCB7IE1DUENvbm5lY3Rpb25NYW5hZ2VyIH0gZnJvbSAnc3JjL3NlcnZpY2VzL21jcC9NQ1BDb25uZWN0aW9uTWFuYWdlci5qcydcbmltcG9ydCB7IHVzZUZlZWRiYWNrU3VydmV5IH0gZnJvbSAnc3JjL2NvbXBvbmVudHMvRmVlZGJhY2tTdXJ2ZXkvdXNlRmVlZGJhY2tTdXJ2ZXkuanMnXG5pbXBvcnQgeyB1c2VNZW1vcnlTdXJ2ZXkgfSBmcm9tICdzcmMvY29tcG9uZW50cy9GZWVkYmFja1N1cnZleS91c2VNZW1vcnlTdXJ2ZXkuanMnXG5pbXBvcnQgeyB1c2VQb3N0Q29tcGFjdFN1cnZleSB9IGZyb20gJ3NyYy9jb21wb25lbnRzL0ZlZWRiYWNrU3VydmV5L3VzZVBvc3RDb21wYWN0U3VydmV5LmpzJ1xuaW1wb3J0IHsgRmVlZGJhY2tTdXJ2ZXkgfSBmcm9tICdzcmMvY29tcG9uZW50cy9GZWVkYmFja1N1cnZleS9GZWVkYmFja1N1cnZleS5qcydcbmltcG9ydCB7IHVzZUluc3RhbGxNZXNzYWdlcyB9IGZyb20gJ3NyYy9ob29rcy9ub3RpZnMvdXNlSW5zdGFsbE1lc3NhZ2VzLmpzJ1xuaW1wb3J0IHsgdXNlQXdheVN1bW1hcnkgfSBmcm9tICdzcmMvaG9va3MvdXNlQXdheVN1bW1hcnkuanMnXG5pbXBvcnQgeyB1c2VDaHJvbWVFeHRlbnNpb25Ob3RpZmljYXRpb24gfSBmcm9tICdzcmMvaG9va3MvdXNlQ2hyb21lRXh0ZW5zaW9uTm90aWZpY2F0aW9uLmpzJ1xuaW1wb3J0IHsgdXNlT2ZmaWNpYWxNYXJrZXRwbGFjZU5vdGlmaWNhdGlvbiB9IGZyb20gJ3NyYy9ob29rcy91c2VPZmZpY2lhbE1hcmtldHBsYWNlTm90aWZpY2F0aW9uLmpzJ1xuaW1wb3J0IHsgdXNlUHJvbXB0c0Zyb21DbGF1ZGVJbkNocm9tZSB9IGZyb20gJ3NyYy9ob29rcy91c2VQcm9tcHRzRnJvbUNsYXVkZUluQ2hyb21lLmpzJ1xuaW1wb3J0IHtcbiAgZ2V0VGlwVG9TaG93T25TcGlubmVyLFxuICByZWNvcmRTaG93blRpcCxcbn0gZnJvbSAnc3JjL3NlcnZpY2VzL3RpcHMvdGlwU2NoZWR1bGVyLmpzJ1xuaW1wb3J0IHR5cGUgeyBUaGVtZSB9IGZyb20gJ3NyYy91dGlscy90aGVtZS5qcydcbmltcG9ydCB7XG4gIGNoZWNrQW5kRGlzYWJsZUJ5cGFzc1Blcm1pc3Npb25zSWZOZWVkZWQsXG4gIGNoZWNrQW5kRGlzYWJsZUF1dG9Nb2RlSWZOZWVkZWQsXG4gIHVzZUtpY2tPZmZDaGVja0FuZERpc2FibGVCeXBhc3NQZXJtaXNzaW9uc0lmTmVlZGVkLFxuICB1c2VLaWNrT2ZmQ2hlY2tBbmREaXNhYmxlQXV0b01vZGVJZk5lZWRlZCxcbn0gZnJvbSAnc3JjL3V0aWxzL3Blcm1pc3Npb25zL2J5cGFzc1Blcm1pc3Npb25zS2lsbHN3aXRjaC5qcydcbmltcG9ydCB7IFNhbmRib3hNYW5hZ2VyIH0gZnJvbSAnc3JjL3V0aWxzL3NhbmRib3gvc2FuZGJveC1hZGFwdGVyLmpzJ1xuaW1wb3J0IHsgU0FOREJPWF9ORVRXT1JLX0FDQ0VTU19UT09MX05BTUUgfSBmcm9tICdzcmMvY2xpL3N0cnVjdHVyZWRJTy5qcydcbmltcG9ydCB7IHVzZUZpbGVIaXN0b3J5U25hcHNob3RJbml0IH0gZnJvbSAnc3JjL2hvb2tzL3VzZUZpbGVIaXN0b3J5U25hcHNob3RJbml0LmpzJ1xuaW1wb3J0IHsgU2FuZGJveFBlcm1pc3Npb25SZXF1ZXN0IH0gZnJvbSAnc3JjL2NvbXBvbmVudHMvcGVybWlzc2lvbnMvU2FuZGJveFBlcm1pc3Npb25SZXF1ZXN0LmpzJ1xuaW1wb3J0IHsgU2FuZGJveFZpb2xhdGlvbkV4cGFuZGVkVmlldyB9IGZyb20gJ3NyYy9jb21wb25lbnRzL1NhbmRib3hWaW9sYXRpb25FeHBhbmRlZFZpZXcuanMnXG5pbXBvcnQgeyB1c2VTZXR0aW5nc0Vycm9ycyB9IGZyb20gJ3NyYy9ob29rcy9ub3RpZnMvdXNlU2V0dGluZ3NFcnJvcnMuanMnXG5pbXBvcnQgeyB1c2VNY3BDb25uZWN0aXZpdHlTdGF0dXMgfSBmcm9tICdzcmMvaG9va3Mvbm90aWZzL3VzZU1jcENvbm5lY3Rpdml0eVN0YXR1cy5qcydcbmltcG9ydCB7IHVzZUF1dG9Nb2RlVW5hdmFpbGFibGVOb3RpZmljYXRpb24gfSBmcm9tICdzcmMvaG9va3Mvbm90aWZzL3VzZUF1dG9Nb2RlVW5hdmFpbGFibGVOb3RpZmljYXRpb24uanMnXG5pbXBvcnQgeyBBVVRPX01PREVfREVTQ1JJUFRJT04gfSBmcm9tICdzcmMvY29tcG9uZW50cy9BdXRvTW9kZU9wdEluRGlhbG9nLmpzJ1xuaW1wb3J0IHsgdXNlTHNwSW5pdGlhbGl6YXRpb25Ob3RpZmljYXRpb24gfSBmcm9tICdzcmMvaG9va3Mvbm90aWZzL3VzZUxzcEluaXRpYWxpemF0aW9uTm90aWZpY2F0aW9uLmpzJ1xuaW1wb3J0IHsgdXNlTHNwUGx1Z2luUmVjb21tZW5kYXRpb24gfSBmcm9tICdzcmMvaG9va3MvdXNlTHNwUGx1Z2luUmVjb21tZW5kYXRpb24uanMnXG5pbXBvcnQgeyBMc3BSZWNvbW1lbmRhdGlvbk1lbnUgfSBmcm9tICdzcmMvY29tcG9uZW50cy9Mc3BSZWNvbW1lbmRhdGlvbi9Mc3BSZWNvbW1lbmRhdGlvbk1lbnUuanMnXG5pbXBvcnQgeyB1c2VDbGF1ZGVDb2RlSGludFJlY29tbWVuZGF0aW9uIH0gZnJvbSAnc3JjL2hvb2tzL3VzZUNsYXVkZUNvZGVIaW50UmVjb21tZW5kYXRpb24uanMnXG5pbXBvcnQgeyBQbHVnaW5IaW50TWVudSB9IGZyb20gJ3NyYy9jb21wb25lbnRzL0NsYXVkZUNvZGVIaW50L1BsdWdpbkhpbnRNZW51LmpzJ1xuaW1wb3J0IHtcbiAgRGVza3RvcFVwc2VsbFN0YXJ0dXAsXG4gIHNob3VsZFNob3dEZXNrdG9wVXBzZWxsU3RhcnR1cCxcbn0gZnJvbSAnc3JjL2NvbXBvbmVudHMvRGVza3RvcFVwc2VsbC9EZXNrdG9wVXBzZWxsU3RhcnR1cC5qcydcbmltcG9ydCB7IHVzZVBsdWdpbkluc3RhbGxhdGlvblN0YXR1cyB9IGZyb20gJ3NyYy9ob29rcy9ub3RpZnMvdXNlUGx1Z2luSW5zdGFsbGF0aW9uU3RhdHVzLmpzJ1xuaW1wb3J0IHsgdXNlUGx1Z2luQXV0b3VwZGF0ZU5vdGlmaWNhdGlvbiB9IGZyb20gJ3NyYy9ob29rcy9ub3RpZnMvdXNlUGx1Z2luQXV0b3VwZGF0ZU5vdGlmaWNhdGlvbi5qcydcbmltcG9ydCB7IHBlcmZvcm1TdGFydHVwQ2hlY2tzIH0gZnJvbSAnc3JjL3V0aWxzL3BsdWdpbnMvcGVyZm9ybVN0YXJ0dXBDaGVja3MuanMnXG5pbXBvcnQgeyBVc2VyVGV4dE1lc3NhZ2UgfSBmcm9tICdzcmMvY29tcG9uZW50cy9tZXNzYWdlcy9Vc2VyVGV4dE1lc3NhZ2UuanMnXG5pbXBvcnQgeyBBd3NBdXRoU3RhdHVzQm94IH0gZnJvbSAnLi4vY29tcG9uZW50cy9Bd3NBdXRoU3RhdHVzQm94LmpzJ1xuaW1wb3J0IHsgdXNlUmF0ZUxpbWl0V2FybmluZ05vdGlmaWNhdGlvbiB9IGZyb20gJ3NyYy9ob29rcy9ub3RpZnMvdXNlUmF0ZUxpbWl0V2FybmluZ05vdGlmaWNhdGlvbi5qcydcbmltcG9ydCB7IHVzZURlcHJlY2F0aW9uV2FybmluZ05vdGlmaWNhdGlvbiB9IGZyb20gJ3NyYy9ob29rcy9ub3RpZnMvdXNlRGVwcmVjYXRpb25XYXJuaW5nTm90aWZpY2F0aW9uLmpzJ1xuaW1wb3J0IHsgdXNlTnBtRGVwcmVjYXRpb25Ob3RpZmljYXRpb24gfSBmcm9tICdzcmMvaG9va3Mvbm90aWZzL3VzZU5wbURlcHJlY2F0aW9uTm90aWZpY2F0aW9uLmpzJ1xuaW1wb3J0IHsgdXNlSURFU3RhdHVzSW5kaWNhdG9yIH0gZnJvbSAnc3JjL2hvb2tzL25vdGlmcy91c2VJREVTdGF0dXNJbmRpY2F0b3IuanMnXG5pbXBvcnQgeyB1c2VNb2RlbE1pZ3JhdGlvbk5vdGlmaWNhdGlvbnMgfSBmcm9tICdzcmMvaG9va3Mvbm90aWZzL3VzZU1vZGVsTWlncmF0aW9uTm90aWZpY2F0aW9ucy5qcydcbmltcG9ydCB7IHVzZUNhblN3aXRjaFRvRXhpc3RpbmdTdWJzY3JpcHRpb24gfSBmcm9tICdzcmMvaG9va3Mvbm90aWZzL3VzZUNhblN3aXRjaFRvRXhpc3RpbmdTdWJzY3JpcHRpb24uanMnXG5pbXBvcnQgeyB1c2VUZWFtbWF0ZUxpZmVjeWNsZU5vdGlmaWNhdGlvbiB9IGZyb20gJ3NyYy9ob29rcy9ub3RpZnMvdXNlVGVhbW1hdGVTaHV0ZG93bk5vdGlmaWNhdGlvbi5qcydcbmltcG9ydCB7IHVzZUZhc3RNb2RlTm90aWZpY2F0aW9uIH0gZnJvbSAnc3JjL2hvb2tzL25vdGlmcy91c2VGYXN0TW9kZU5vdGlmaWNhdGlvbi5qcydcbmltcG9ydCB7XG4gIEF1dG9SdW5Jc3N1ZU5vdGlmaWNhdGlvbixcbiAgc2hvdWxkQXV0b1J1bklzc3VlLFxuICBnZXRBdXRvUnVuSXNzdWVSZWFzb25UZXh0LFxuICBnZXRBdXRvUnVuQ29tbWFuZCxcbiAgdHlwZSBBdXRvUnVuSXNzdWVSZWFzb24sXG59IGZyb20gJy4uL3V0aWxzL2F1dG9SdW5Jc3N1ZS5qcydcbmltcG9ydCB0eXBlIHsgSG9va1Byb2dyZXNzIH0gZnJvbSAnLi4vdHlwZXMvaG9va3MuanMnXG5pbXBvcnQgeyBUdW5nc3RlbkxpdmVNb25pdG9yIH0gZnJvbSAnLi4vdG9vbHMvVHVuZ3N0ZW5Ub29sL1R1bmdzdGVuTGl2ZU1vbml0b3IuanMnXG4vKiBlc2xpbnQtZGlzYWJsZSBAdHlwZXNjcmlwdC1lc2xpbnQvbm8tcmVxdWlyZS1pbXBvcnRzICovXG5jb25zdCBXZWJCcm93c2VyUGFuZWxNb2R1bGUgPSBmZWF0dXJlKCdXRUJfQlJPV1NFUl9UT09MJylcbiAgPyAocmVxdWlyZSgnLi4vdG9vbHMvV2ViQnJvd3NlclRvb2wvV2ViQnJvd3NlclBhbmVsLmpzJykgYXMgdHlwZW9mIGltcG9ydCgnLi4vdG9vbHMvV2ViQnJvd3NlclRvb2wvV2ViQnJvd3NlclBhbmVsLmpzJykpXG4gIDogbnVsbFxuLyogZXNsaW50LWVuYWJsZSBAdHlwZXNjcmlwdC1lc2xpbnQvbm8tcmVxdWlyZS1pbXBvcnRzICovXG5pbXBvcnQgeyBJc3N1ZUZsYWdCYW5uZXIgfSBmcm9tICcuLi9jb21wb25lbnRzL1Byb21wdElucHV0L0lzc3VlRmxhZ0Jhbm5lci5qcydcbmltcG9ydCB7IHVzZUlzc3VlRmxhZ0Jhbm5lciB9IGZyb20gJy4uL2hvb2tzL3VzZUlzc3VlRmxhZ0Jhbm5lci5qcydcbmltcG9ydCB7XG4gIENvbXBhbmlvblNwcml0ZSxcbiAgQ29tcGFuaW9uRmxvYXRpbmdCdWJibGUsXG4gIE1JTl9DT0xTX0ZPUl9GVUxMX1NQUklURSxcbn0gZnJvbSAnLi4vYnVkZHkvQ29tcGFuaW9uU3ByaXRlLmpzJ1xuaW1wb3J0IHsgRGV2QmFyIH0gZnJvbSAnLi4vY29tcG9uZW50cy9EZXZCYXIuanMnXG4vLyBTZXNzaW9uIG1hbmFnZXIgcmVtb3ZlZCAtIHVzaW5nIEFwcFN0YXRlIG5vd1xuaW1wb3J0IHR5cGUgeyBSZW1vdGVTZXNzaW9uQ29uZmlnIH0gZnJvbSAnLi4vcmVtb3RlL1JlbW90ZVNlc3Npb25NYW5hZ2VyLmpzJ1xuaW1wb3J0IHsgUkVNT1RFX1NBRkVfQ09NTUFORFMgfSBmcm9tICcuLi9jb21tYW5kcy5qcydcbmltcG9ydCB0eXBlIHsgUmVtb3RlTWVzc2FnZUNvbnRlbnQgfSBmcm9tICcuLi91dGlscy90ZWxlcG9ydC9hcGkuanMnXG5pbXBvcnQge1xuICBGdWxsc2NyZWVuTGF5b3V0LFxuICB1c2VVbnNlZW5EaXZpZGVyLFxuICBjb21wdXRlVW5zZWVuRGl2aWRlcixcbn0gZnJvbSAnLi4vY29tcG9uZW50cy9GdWxsc2NyZWVuTGF5b3V0LmpzJ1xuaW1wb3J0IHtcbiAgaXNGdWxsc2NyZWVuRW52RW5hYmxlZCxcbiAgbWF5YmVHZXRUbXV4TW91c2VIaW50LFxuICBpc01vdXNlVHJhY2tpbmdFbmFibGVkLFxufSBmcm9tICcuLi91dGlscy9mdWxsc2NyZWVuLmpzJ1xuaW1wb3J0IHsgQWx0ZXJuYXRlU2NyZWVuIH0gZnJvbSAnLi4vaW5rL2NvbXBvbmVudHMvQWx0ZXJuYXRlU2NyZWVuLmpzJ1xuaW1wb3J0IHsgU2Nyb2xsS2V5YmluZGluZ0hhbmRsZXIgfSBmcm9tICcuLi9jb21wb25lbnRzL1Njcm9sbEtleWJpbmRpbmdIYW5kbGVyLmpzJ1xuaW1wb3J0IHtcbiAgdXNlTWVzc2FnZUFjdGlvbnMsXG4gIE1lc3NhZ2VBY3Rpb25zS2V5YmluZGluZ3MsXG4gIE1lc3NhZ2VBY3Rpb25zQmFyLFxuICB0eXBlIE1lc3NhZ2VBY3Rpb25zU3RhdGUsXG4gIHR5cGUgTWVzc2FnZUFjdGlvbnNOYXYsXG4gIHR5cGUgTWVzc2FnZUFjdGlvbkNhcHMsXG59IGZyb20gJy4uL2NvbXBvbmVudHMvbWVzc2FnZUFjdGlvbnMuanMnXG5pbXBvcnQgeyBzZXRDbGlwYm9hcmQgfSBmcm9tICcuLi9pbmsvdGVybWlvL29zYy5qcydcbmltcG9ydCB0eXBlIHsgU2Nyb2xsQm94SGFuZGxlIH0gZnJvbSAnLi4vaW5rL2NvbXBvbmVudHMvU2Nyb2xsQm94LmpzJ1xuaW1wb3J0IHtcbiAgY3JlYXRlQXR0YWNobWVudE1lc3NhZ2UsXG4gIGdldFF1ZXVlZENvbW1hbmRBdHRhY2htZW50cyxcbn0gZnJvbSAnLi4vdXRpbHMvYXR0YWNobWVudHMuanMnXG5cbi8vIFN0YWJsZSBlbXB0eSBhcnJheSBmb3IgaG9va3MgdGhhdCBhY2NlcHQgTUNQU2VydmVyQ29ubmVjdGlvbltdIOKAlCBhdm9pZHNcbi8vIGNyZWF0aW5nIGEgbmV3IFtdIGxpdGVyYWwgb24gZXZlcnkgcmVuZGVyIGluIHJlbW90ZSBtb2RlLCB3aGljaCB3b3VsZFxuLy8gY2F1c2UgdXNlRWZmZWN0IGRlcGVuZGVuY3kgY2hhbmdlcyBhbmQgaW5maW5pdGUgcmUtcmVuZGVyIGxvb3BzLlxuY29uc3QgRU1QVFlfTUNQX0NMSUVOVFM6IE1DUFNlcnZlckNvbm5lY3Rpb25bXSA9IFtdXG5cbi8vIFN0YWJsZSBzdHViIGZvciB1c2VBc3Npc3RhbnRIaXN0b3J5J3Mgbm9uLUtBSVJPUyBicmFuY2gg4oCUIGF2b2lkcyBhIG5ld1xuLy8gZnVuY3Rpb24gaWRlbnRpdHkgZWFjaCByZW5kZXIsIHdoaWNoIHdvdWxkIGJyZWFrIGNvbXBvc2VkT25TY3JvbGwncyBtZW1vLlxuY29uc3QgSElTVE9SWV9TVFVCID0geyBtYXliZUxvYWRPbGRlcjogKF86IFNjcm9sbEJveEhhbmRsZSkgPT4ge30gfVxuLy8gV2luZG93IGFmdGVyIGEgdXNlci1pbml0aWF0ZWQgc2Nyb2xsIGR1cmluZyB3aGljaCB0eXBlLWludG8tZW1wdHkgZG9lcyBOT1Rcbi8vIHJlcGluIHRvIGJvdHRvbS4gSm9zaCBSb3NlbidzIHdvcmtmbG93OiBDbGF1ZGUgZW1pdHMgbG9uZyBvdXRwdXQg4oaSIHNjcm9sbFxuLy8gdXAgdG8gcmVhZCB0aGUgc3RhcnQg4oaSIHN0YXJ0IHR5cGluZyDihpIgYmVmb3JlIHRoaXMgZml4LCBzbmFwcGVkIHRvIGJvdHRvbS5cbi8vIGh0dHBzOi8vYW50aHJvcGljLnNsYWNrLmNvbS9hcmNoaXZlcy9DMDdWQlNIVjdFVi9wMTc3MzU0NTQ0OTg3MTczOVxuY29uc3QgUkVDRU5UX1NDUk9MTF9SRVBJTl9XSU5ET1dfTVMgPSAzMDAwXG5cbi8vIFVzZSBMUlUgY2FjaGUgdG8gcHJldmVudCB1bmJvdW5kZWQgbWVtb3J5IGdyb3d0aFxuLy8gMTAwIGZpbGVzIHNob3VsZCBiZSBzdWZmaWNpZW50IGZvciBtb3N0IGNvZGluZyBzZXNzaW9ucyB3aGlsZSBwcmV2ZW50aW5nXG4vLyBtZW1vcnkgaXNzdWVzIHdoZW4gd29ya2luZyBhY3Jvc3MgbWFueSBmaWxlcyBpbiBsYXJnZSBwcm9qZWN0c1xuXG5mdW5jdGlvbiBtZWRpYW4odmFsdWVzOiBudW1iZXJbXSk6IG51bWJlciB7XG4gIGNvbnN0IHNvcnRlZCA9IFsuLi52YWx1ZXNdLnNvcnQoKGEsIGIpID0+IGEgLSBiKVxuICBjb25zdCBtaWQgPSBNYXRoLmZsb29yKHNvcnRlZC5sZW5ndGggLyAyKVxuICByZXR1cm4gc29ydGVkLmxlbmd0aCAlIDIgPT09IDBcbiAgICA/IE1hdGgucm91bmQoKHNvcnRlZFttaWQgLSAxXSEgKyBzb3J0ZWRbbWlkXSEpIC8gMilcbiAgICA6IHNvcnRlZFttaWRdIVxufVxuXG4vKipcbiAqIFNtYWxsIGNvbXBvbmVudCB0byBkaXNwbGF5IHRyYW5zY3JpcHQgbW9kZSBmb290ZXIgd2l0aCBkeW5hbWljIGtleWJpbmRpbmcuXG4gKiBNdXN0IGJlIHJlbmRlcmVkIGluc2lkZSBLZXliaW5kaW5nU2V0dXAgdG8gYWNjZXNzIGtleWJpbmRpbmcgY29udGV4dC5cbiAqL1xuZnVuY3Rpb24gVHJhbnNjcmlwdE1vZGVGb290ZXIoe1xuICBzaG93QWxsSW5UcmFuc2NyaXB0LFxuICB2aXJ0dWFsU2Nyb2xsLFxuICBzZWFyY2hCYWRnZSxcbiAgc3VwcHJlc3NTaG93QWxsID0gZmFsc2UsXG4gIHN0YXR1cyxcbn06IHtcbiAgc2hvd0FsbEluVHJhbnNjcmlwdDogYm9vbGVhblxuICB2aXJ0dWFsU2Nyb2xsOiBib29sZWFuXG4gIC8qKiBNaW5pbWFwIHdoaWxlIG5hdmlnYXRpbmcgYSBjbG9zZWQtYmFyIHNlYXJjaC4gU2hvd3Mgbi9OIGhpbnRzICtcbiAgICogIHJpZ2h0LWFsaWduZWQgY291bnQgaW5zdGVhZCBvZiBzY3JvbGwgaGludHMuICovXG4gIHNlYXJjaEJhZGdlPzogeyBjdXJyZW50OiBudW1iZXI7IGNvdW50OiBudW1iZXIgfVxuICAvKiogSGlkZSB0aGUgY3RybCtlIGhpbnQuIFRoZSBbIGR1bXAgcGF0aCBzaGFyZXMgdGhpcyBmb290ZXIgd2l0aFxuICAgKiAgZW52LW9wdGVkIGR1bXAgKENMQVVERV9DT0RFX05PX0ZMSUNLRVI9MCAvIERJU0FCTEVfVklSVFVBTF9TQ1JPTEw9MSksXG4gICAqICBidXQgY3RybCtlIG9ubHkgd29ya3MgaW4gdGhlIGVudiBjYXNlIOKAlCB1c2VHbG9iYWxLZXliaW5kaW5ncy50c3hcbiAgICogIGdhdGVzIG9uICF2aXJ0dWFsU2Nyb2xsQWN0aXZlIHdoaWNoIGlzIGVudi1kZXJpdmVkLCBkb2Vzbid0IGtub3dcbiAgICogIFsgaGFwcGVuZWQuICovXG4gIHN1cHByZXNzU2hvd0FsbD86IGJvb2xlYW5cbiAgLyoqIFRyYW5zaWVudCBzdGF0dXMgKHYtZm9yLWVkaXRvciBwcm9ncmVzcykuIE5vdGlmaWNhdGlvbnMgcmVuZGVyIGluc2lkZVxuICAgKiAgUHJvbXB0SW5wdXQgd2hpY2ggaXNuJ3QgbW91bnRlZCBpbiB0cmFuc2NyaXB0IOKAlCBhZGROb3RpZmljYXRpb24gcXVldWVzXG4gICAqICBidXQgbm90aGluZyBkcmF3cyBpdC4gKi9cbiAgc3RhdHVzPzogc3RyaW5nXG59KTogUmVhY3QuUmVhY3ROb2RlIHtcbiAgY29uc3QgdG9nZ2xlU2hvcnRjdXQgPSB1c2VTaG9ydGN1dERpc3BsYXkoXG4gICAgJ2FwcDp0b2dnbGVUcmFuc2NyaXB0JyxcbiAgICAnR2xvYmFsJyxcbiAgICAnY3RybCtvJyxcbiAgKVxuICBjb25zdCBzaG93QWxsU2hvcnRjdXQgPSB1c2VTaG9ydGN1dERpc3BsYXkoXG4gICAgJ3RyYW5zY3JpcHQ6dG9nZ2xlU2hvd0FsbCcsXG4gICAgJ1RyYW5zY3JpcHQnLFxuICAgICdjdHJsK2UnLFxuICApXG4gIHJldHVybiAoXG4gICAgPEJveFxuICAgICAgbm9TZWxlY3RcbiAgICAgIGFsaWduSXRlbXM9XCJjZW50ZXJcIlxuICAgICAgYWxpZ25TZWxmPVwiY2VudGVyXCJcbiAgICAgIGJvcmRlclRvcERpbUNvbG9yXG4gICAgICBib3JkZXJCb3R0b209e2ZhbHNlfVxuICAgICAgYm9yZGVyTGVmdD17ZmFsc2V9XG4gICAgICBib3JkZXJSaWdodD17ZmFsc2V9XG4gICAgICBib3JkZXJTdHlsZT1cInNpbmdsZVwiXG4gICAgICBtYXJnaW5Ub3A9ezF9XG4gICAgICBwYWRkaW5nTGVmdD17Mn1cbiAgICAgIHdpZHRoPVwiMTAwJVwiXG4gICAgPlxuICAgICAgPFRleHQgZGltQ29sb3I+XG4gICAgICAgIFNob3dpbmcgZGV0YWlsZWQgdHJhbnNjcmlwdCDCtyB7dG9nZ2xlU2hvcnRjdXR9IHRvIHRvZ2dsZVxuICAgICAgICB7c2VhcmNoQmFkZ2VcbiAgICAgICAgICA/ICcgwrcgbi9OIHRvIG5hdmlnYXRlJ1xuICAgICAgICAgIDogdmlydHVhbFNjcm9sbFxuICAgICAgICAgICAgPyBgIMK3ICR7ZmlndXJlcy5hcnJvd1VwfSR7ZmlndXJlcy5hcnJvd0Rvd259IHNjcm9sbCDCtyBob21lL2VuZCB0b3AvYm90dG9tYFxuICAgICAgICAgICAgOiBzdXBwcmVzc1Nob3dBbGxcbiAgICAgICAgICAgICAgPyAnJ1xuICAgICAgICAgICAgICA6IGAgwrcgJHtzaG93QWxsU2hvcnRjdXR9IHRvICR7c2hvd0FsbEluVHJhbnNjcmlwdCA/ICdjb2xsYXBzZScgOiAnc2hvdyBhbGwnfWB9XG4gICAgICA8L1RleHQ+XG4gICAgICB7c3RhdHVzID8gKFxuICAgICAgICAvLyB2LWZvci1lZGl0b3IgcmVuZGVyIHByb2dyZXNzIOKAlCB0cmFuc2llbnQsIHByZWVtcHRzIHRoZSBzZWFyY2hcbiAgICAgICAgLy8gYmFkZ2Ugc2luY2UgdGhlIHVzZXIganVzdCBwcmVzc2VkIHYgYW5kIHdhbnRzIHRvIHNlZSB3aGF0J3NcbiAgICAgICAgLy8gaGFwcGVuaW5nLiBDbGVhcnMgYWZ0ZXIgNHMuXG4gICAgICAgIDw+XG4gICAgICAgICAgPEJveCBmbGV4R3Jvdz17MX0gLz5cbiAgICAgICAgICA8VGV4dD57c3RhdHVzfSA8L1RleHQ+XG4gICAgICAgIDwvPlxuICAgICAgKSA6IHNlYXJjaEJhZGdlID8gKFxuICAgICAgICAvLyBFbmdpbmUtY291bnRlZCDigJQgY2xvc2UgZW5vdWdoIGZvciBhIHJvdWdoIGxvY2F0aW9uIGhpbnQuIE1heVxuICAgICAgICAvLyBkcmlmdCBmcm9tIHJlbmRlci1jb3VudCBmb3IgZ2hvc3QvcGhhbnRvbSBtZXNzYWdlcy5cbiAgICAgICAgPD5cbiAgICAgICAgICA8Qm94IGZsZXhHcm93PXsxfSAvPlxuICAgICAgICAgIDxUZXh0IGRpbUNvbG9yPlxuICAgICAgICAgICAge3NlYXJjaEJhZGdlLmN1cnJlbnR9L3tzZWFyY2hCYWRnZS5jb3VudH1cbiAgICAgICAgICAgIHsnICAnfVxuICAgICAgICAgIDwvVGV4dD5cbiAgICAgICAgPC8+XG4gICAgICApIDogbnVsbH1cbiAgICA8L0JveD5cbiAgKVxufVxuXG4vKiogbGVzcy1zdHlsZSAvIGJhci4gMS1yb3csIHNhbWUgYm9yZGVyLXRvcCBzdHlsaW5nIGFzIFRyYW5zY3JpcHRNb2RlRm9vdGVyXG4gKiAgc28gc3dhcHBpbmcgdGhlbSBpbiB0aGUgYm90dG9tIHNsb3QgZG9lc24ndCBzaGlmdCBTY3JvbGxCb3ggaGVpZ2h0LlxuICogIHVzZVNlYXJjaElucHV0IGhhbmRsZXMgcmVhZGxpbmUgZWRpdGluZzsgd2UgcmVwb3J0IHF1ZXJ5IGNoYW5nZXMgYW5kXG4gKiAgcmVuZGVyIHRoZSBjb3VudGVyLiBJbmNyZW1lbnRhbCDigJQgcmUtc2VhcmNoICsgaGlnaGxpZ2h0IHBlciBrZXlzdHJva2UuICovXG5mdW5jdGlvbiBUcmFuc2NyaXB0U2VhcmNoQmFyKHtcbiAganVtcFJlZixcbiAgY291bnQsXG4gIGN1cnJlbnQsXG4gIG9uQ2xvc2UsXG4gIG9uQ2FuY2VsLFxuICBzZXRIaWdobGlnaHQsXG4gIGluaXRpYWxRdWVyeSxcbn06IHtcbiAganVtcFJlZjogUmVmT2JqZWN0PEp1bXBIYW5kbGUgfCBudWxsPlxuICBjb3VudDogbnVtYmVyXG4gIGN1cnJlbnQ6IG51bWJlclxuICAvKiogRW50ZXIg4oCUIGNvbW1pdC4gUXVlcnkgcGVyc2lzdHMgZm9yIG4vTi4gKi9cbiAgb25DbG9zZTogKGxhc3RRdWVyeTogc3RyaW5nKSA9PiB2b2lkXG4gIC8qKiBFc2MvY3RybCtjL2N0cmwrZyDigJQgdW5kbyB0byBwcmUtLyBzdGF0ZS4gKi9cbiAgb25DYW5jZWw6ICgpID0+IHZvaWRcbiAgc2V0SGlnaGxpZ2h0OiAocXVlcnk6IHN0cmluZykgPT4gdm9pZFxuICAvLyBTZWVkIHdpdGggdGhlIHByZXZpb3VzIHF1ZXJ5IChsZXNzOiAvIHNob3dzIGxhc3QgcGF0dGVybikuIE1vdW50LWZpcmVcbiAgLy8gb2YgdGhlIGVmZmVjdCByZS1zY2FucyB3aXRoIHRoZSBzYW1lIHF1ZXJ5IOKAlCBpZGVtcG90ZW50IChzYW1lIG1hdGNoZXMsXG4gIC8vIG5lYXJlc3QtcHRyLCBzYW1lIGhpZ2hsaWdodHMpLiBVc2VyIGNhbiBlZGl0IG9yIGNsZWFyLlxuICBpbml0aWFsUXVlcnk6IHN0cmluZ1xufSk6IFJlYWN0LlJlYWN0Tm9kZSB7XG4gIGNvbnN0IHsgcXVlcnksIGN1cnNvck9mZnNldCB9ID0gdXNlU2VhcmNoSW5wdXQoe1xuICAgIGlzQWN0aXZlOiB0cnVlLFxuICAgIGluaXRpYWxRdWVyeSxcbiAgICBvbkV4aXQ6ICgpID0+IG9uQ2xvc2UocXVlcnkpLFxuICAgIG9uQ2FuY2VsLFxuICB9KVxuICAvLyBJbmRleCB3YXJtLXVwIHJ1bnMgYmVmb3JlIHRoZSBxdWVyeSBlZmZlY3Qgc28gaXQgbWVhc3VyZXMgdGhlIHJlYWxcbiAgLy8gY29zdCDigJQgb3RoZXJ3aXNlIHNldFNlYXJjaFF1ZXJ5IGZpbGxzIHRoZSBjYWNoZSBmaXJzdCBhbmQgd2FybVxuICAvLyByZXBvcnRzIH4wbXMgd2hpbGUgdGhlIHVzZXIgZmVsdCB0aGUgYWN0dWFsIGxhZy5cbiAgLy8gRmlyc3QgLyBpbiBhIHRyYW5zY3JpcHQgc2Vzc2lvbiBwYXlzIHRoZSBleHRyYWN0U2VhcmNoVGV4dCBjb3N0LlxuICAvLyBTdWJzZXF1ZW50IC8gcmV0dXJuIDAgaW1tZWRpYXRlbHkgKGluZGV4V2FybWVkIHJlZiBpbiBWTUwpLlxuICAvLyBUcmFuc2NyaXB0IGlzIGZyb3plbiBhdCBjdHJsK28gc28gdGhlIGNhY2hlIHN0YXlzIHZhbGlkLlxuICAvLyBJbml0aWFsICdidWlsZGluZycgc28gd2FybURvbmUgaXMgZmFsc2Ugb24gbW91bnQg4oCUIHRoZSBbcXVlcnldIGVmZmVjdFxuICAvLyB3YWl0cyBmb3IgdGhlIHdhcm0gZWZmZWN0J3MgZmlyc3QgcmVzb2x2ZSBpbnN0ZWFkIG9mIHJhY2luZyBpdC4gV2l0aFxuICAvLyBudWxsIGluaXRpYWwsIHdhcm1Eb25lIHdvdWxkIGJlIHRydWUgb24gbW91bnQg4oaSIFtxdWVyeV0gZmlyZXMg4oaSXG4gIC8vIHNldFNlYXJjaFF1ZXJ5IGZpbGxzIGNhY2hlIOKGkiB3YXJtIHJlcG9ydHMgfjBtcyB3aGlsZSB0aGUgdXNlciBmZWx0XG4gIC8vIHRoZSByZWFsIGxhZy5cbiAgY29uc3QgW2luZGV4U3RhdHVzLCBzZXRJbmRleFN0YXR1c10gPSBSZWFjdC51c2VTdGF0ZTxcbiAgICAnYnVpbGRpbmcnIHwgeyBtczogbnVtYmVyIH0gfCBudWxsXG4gID4oJ2J1aWxkaW5nJylcbiAgUmVhY3QudXNlRWZmZWN0KCgpID0+IHtcbiAgICBsZXQgYWxpdmUgPSB0cnVlXG4gICAgY29uc3Qgd2FybSA9IGp1bXBSZWYuY3VycmVudD8ud2FybVNlYXJjaEluZGV4XG4gICAgaWYgKCF3YXJtKSB7XG4gICAgICBzZXRJbmRleFN0YXR1cyhudWxsKSAvLyBWTUwgbm90IG1vdW50ZWQgeWV0IOKAlCByYXJlLCBza2lwIGluZGljYXRvclxuICAgICAgcmV0dXJuXG4gICAgfVxuICAgIHNldEluZGV4U3RhdHVzKCdidWlsZGluZycpXG4gICAgd2FybSgpLnRoZW4obXMgPT4ge1xuICAgICAgaWYgKCFhbGl2ZSkgcmV0dXJuXG4gICAgICAvLyA8MjBtcyA9IGltcGVyY2VwdGlibGUuIE5vIHBvaW50IHNob3dpbmcgXCJpbmRleGVkIGluIDNtc1wiLlxuICAgICAgaWYgKG1zIDwgMjApIHtcbiAgICAgICAgc2V0SW5kZXhTdGF0dXMobnVsbClcbiAgICAgIH0gZWxzZSB7XG4gICAgICAgIHNldEluZGV4U3RhdHVzKHsgbXMgfSlcbiAgICAgICAgc2V0VGltZW91dCgoKSA9PiBhbGl2ZSAmJiBzZXRJbmRleFN0YXR1cyhudWxsKSwgMjAwMClcbiAgICAgIH1cbiAgICB9KVxuICAgIHJldHVybiAoKSA9PiB7XG4gICAgICBhbGl2ZSA9IGZhbHNlXG4gICAgfVxuICAgIC8vIGVzbGludC1kaXNhYmxlLW5leHQtbGluZSByZWFjdC1ob29rcy9leGhhdXN0aXZlLWRlcHNcbiAgfSwgW10pIC8vIG1vdW50LW9ubHk6IGJhciBvcGVucyBvbmNlIHBlciAvXG4gIC8vIEdhdGUgdGhlIHF1ZXJ5IGVmZmVjdCBvbiB3YXJtIGNvbXBsZXRpb24uIHNldEhpZ2hsaWdodCBzdGF5cyBpbnN0YW50XG4gIC8vIChzY3JlZW4tc3BhY2Ugb3ZlcmxheSwgbm8gaW5kZXhpbmcpLiBzZXRTZWFyY2hRdWVyeSAodGhlIHNjYW4pIHdhaXRzLlxuICBjb25zdCB3YXJtRG9uZSA9IGluZGV4U3RhdHVzICE9PSAnYnVpbGRpbmcnXG4gIHVzZUVmZmVjdCgoKSA9PiB7XG4gICAgaWYgKCF3YXJtRG9uZSkgcmV0dXJuXG4gICAganVtcFJlZi5jdXJyZW50Py5zZXRTZWFyY2hRdWVyeShxdWVyeSlcbiAgICBzZXRIaWdobGlnaHQocXVlcnkpXG4gICAgLy8gZXNsaW50LWRpc2FibGUtbmV4dC1saW5lIHJlYWN0LWhvb2tzL2V4aGF1c3RpdmUtZGVwc1xuICB9LCBbcXVlcnksIHdhcm1Eb25lXSlcbiAgY29uc3Qgb2ZmID0gY3Vyc29yT2Zmc2V0XG4gIGNvbnN0IGN1cnNvckNoYXIgPSBvZmYgPCBxdWVyeS5sZW5ndGggPyBxdWVyeVtvZmZdIDogJyAnXG4gIHJldHVybiAoXG4gICAgPEJveFxuICAgICAgYm9yZGVyVG9wRGltQ29sb3JcbiAgICAgIGJvcmRlckJvdHRvbT17ZmFsc2V9XG4gICAgICBib3JkZXJMZWZ0PXtmYWxzZX1cbiAgICAgIGJvcmRlclJpZ2h0PXtmYWxzZX1cbiAgICAgIGJvcmRlclN0eWxlPVwic2luZ2xlXCJcbiAgICAgIG1hcmdpblRvcD17MX1cbiAgICAgIHBhZGRpbmdMZWZ0PXsyfVxuICAgICAgd2lkdGg9XCIxMDAlXCJcbiAgICAgIC8vIGFwcGx5U2VhcmNoSGlnaGxpZ2h0IHNjYW5zIHRoZSB3aG9sZSBzY3JlZW4gYnVmZmVyLiBUaGUgcXVlcnlcbiAgICAgIC8vIHRleHQgcmVuZGVyZWQgaGVyZSBJUyBvbiBzY3JlZW4g4oCUIC9mb28gbWF0Y2hlcyBpdHMgb3duICdmb28nIGluXG4gICAgICAvLyB0aGUgYmFyLiBXaXRoIG5vIGNvbnRlbnQgbWF0Y2hlcyB0aGF0J3MgdGhlIE9OTFkgdmlzaWJsZSBtYXRjaCDihpJcbiAgICAgIC8vIGdldHMgQ1VSUkVOVCDihpIgdW5kZXJsaW5lZC4gbm9TZWxlY3QgbWFrZXMgc2VhcmNoSGlnaGxpZ2h0LnRzOjc2XG4gICAgICAvLyBza2lwIHRoZXNlIGNlbGxzIChzYW1lIGV4Y2x1c2lvbiBhcyBndXR0ZXJzKS4gWW91IGNhbid0IHRleHQtXG4gICAgICAvLyBzZWxlY3QgdGhlIGJhciBlaXRoZXI7IGl0J3MgdHJhbnNpZW50IGNocm9tZSwgZmluZS5cbiAgICAgIG5vU2VsZWN0XG4gICAgPlxuICAgICAgPFRleHQ+LzwvVGV4dD5cbiAgICAgIDxUZXh0PntxdWVyeS5zbGljZSgwLCBvZmYpfTwvVGV4dD5cbiAgICAgIDxUZXh0IGludmVyc2U+e2N1cnNvckNoYXJ9PC9UZXh0PlxuICAgICAge29mZiA8IHF1ZXJ5Lmxlbmd0aCAmJiA8VGV4dD57cXVlcnkuc2xpY2Uob2ZmICsgMSl9PC9UZXh0Pn1cbiAgICAgIDxCb3ggZmxleEdyb3c9ezF9IC8+XG4gICAgICB7aW5kZXhTdGF0dXMgPT09ICdidWlsZGluZycgPyAoXG4gICAgICAgIDxUZXh0IGRpbUNvbG9yPmluZGV4aW5n4oCmIDwvVGV4dD5cbiAgICAgICkgOiBpbmRleFN0YXR1cyA/IChcbiAgICAgICAgPFRleHQgZGltQ29sb3I+aW5kZXhlZCBpbiB7aW5kZXhTdGF0dXMubXN9bXMgPC9UZXh0PlxuICAgICAgKSA6IGNvdW50ID09PSAwICYmIHF1ZXJ5ID8gKFxuICAgICAgICA8VGV4dCBjb2xvcj1cImVycm9yXCI+bm8gbWF0Y2hlcyA8L1RleHQ+XG4gICAgICApIDogY291bnQgPiAwID8gKFxuICAgICAgICAvLyBFbmdpbmUtY291bnRlZCAoaW5kZXhPZiBvbiBleHRyYWN0U2VhcmNoVGV4dCkuIE1heSBkcmlmdCBmcm9tXG4gICAgICAgIC8vIHJlbmRlci1jb3VudCBmb3IgZ2hvc3QvcGhhbnRvbSBtZXNzYWdlcyDigJQgYmFkZ2UgaXMgYSByb3VnaFxuICAgICAgICAvLyBsb2NhdGlvbiBoaW50LiBzY2FuRWxlbWVudCBnaXZlcyBleGFjdCBwZXItbWVzc2FnZSBwb3NpdGlvbnNcbiAgICAgICAgLy8gYnV0IGNvdW50aW5nIEFMTCB3b3VsZCBjb3N0IH4xLTNtcyDDlyBtYXRjaGVkLW1lc3NhZ2VzLlxuICAgICAgICA8VGV4dCBkaW1Db2xvcj5cbiAgICAgICAgICB7Y3VycmVudH0ve2NvdW50fVxuICAgICAgICAgIHsnICAnfVxuICAgICAgICA8L1RleHQ+XG4gICAgICApIDogbnVsbH1cbiAgICA8L0JveD5cbiAgKVxufVxuXG5jb25zdCBUSVRMRV9BTklNQVRJT05fRlJBTUVTID0gWyfioIInLCAn4qCQJ11cbmNvbnN0IFRJVExFX1NUQVRJQ19QUkVGSVggPSAn4pyzJ1xuY29uc3QgVElUTEVfQU5JTUFUSU9OX0lOVEVSVkFMX01TID0gOTYwXG5cbi8qKlxuICogU2V0cyB0aGUgdGVybWluYWwgdGFiIHRpdGxlLCB3aXRoIGFuIGFuaW1hdGVkIHByZWZpeCBnbHlwaCB3aGlsZSBhIHF1ZXJ5XG4gKiBpcyBydW5uaW5nLiBJc29sYXRlZCBmcm9tIFJFUEwgc28gdGhlIDk2MG1zIGFuaW1hdGlvbiB0aWNrIHJlLXJlbmRlcnMgb25seVxuICogdGhpcyBsZWFmIGNvbXBvbmVudCAod2hpY2ggcmV0dXJucyBudWxsIOKAlCBwdXJlIHNpZGUtZWZmZWN0KSBpbnN0ZWFkIG9mIHRoZVxuICogZW50aXJlIFJFUEwgdHJlZS4gQmVmb3JlIGV4dHJhY3Rpb24sIHRoZSB0aWNrIHdhcyB+MSBSRVBMIHJlbmRlci9zZWMgZm9yXG4gKiB0aGUgZHVyYXRpb24gb2YgZXZlcnkgdHVybiwgZHJhZ2dpbmcgUHJvbXB0SW5wdXQgYW5kIGZyaWVuZHMgYWxvbmcuXG4gKi9cbmZ1bmN0aW9uIEFuaW1hdGVkVGVybWluYWxUaXRsZSh7XG4gIGlzQW5pbWF0aW5nLFxuICB0aXRsZSxcbiAgZGlzYWJsZWQsXG4gIG5vUHJlZml4LFxufToge1xuICBpc0FuaW1hdGluZzogYm9vbGVhblxuICB0aXRsZTogc3RyaW5nXG4gIGRpc2FibGVkOiBib29sZWFuXG4gIG5vUHJlZml4OiBib29sZWFuXG59KTogbnVsbCB7XG4gIGNvbnN0IHRlcm1pbmFsRm9jdXNlZCA9IHVzZVRlcm1pbmFsRm9jdXMoKVxuICBjb25zdCBbZnJhbWUsIHNldEZyYW1lXSA9IHVzZVN0YXRlKDApXG4gIHVzZUVmZmVjdCgoKSA9PiB7XG4gICAgaWYgKGRpc2FibGVkIHx8IG5vUHJlZml4IHx8ICFpc0FuaW1hdGluZyB8fCAhdGVybWluYWxGb2N1c2VkKSByZXR1cm5cbiAgICBjb25zdCBpbnRlcnZhbCA9IHNldEludGVydmFsKFxuICAgICAgc2V0RnJhbWUgPT4gc2V0RnJhbWUoZiA9PiAoZiArIDEpICUgVElUTEVfQU5JTUFUSU9OX0ZSQU1FUy5sZW5ndGgpLFxuICAgICAgVElUTEVfQU5JTUFUSU9OX0lOVEVSVkFMX01TLFxuICAgICAgc2V0RnJhbWUsXG4gICAgKVxuICAgIHJldHVybiAoKSA9PiBjbGVhckludGVydmFsKGludGVydmFsKVxuICB9LCBbZGlzYWJsZWQsIG5vUHJlZml4LCBpc0FuaW1hdGluZywgdGVybWluYWxGb2N1c2VkXSlcbiAgY29uc3QgcHJlZml4ID0gaXNBbmltYXRpbmdcbiAgICA/IChUSVRMRV9BTklNQVRJT05fRlJBTUVTW2ZyYW1lXSA/PyBUSVRMRV9TVEFUSUNfUFJFRklYKVxuICAgIDogVElUTEVfU1RBVElDX1BSRUZJWFxuICB1c2VUZXJtaW5hbFRpdGxlKGRpc2FibGVkID8gbnVsbCA6IG5vUHJlZml4ID8gdGl0bGUgOiBgJHtwcmVmaXh9ICR7dGl0bGV9YClcbiAgcmV0dXJuIG51bGxcbn1cblxuZXhwb3J0IHR5cGUgUHJvcHMgPSB7XG4gIGNvbW1hbmRzOiBDb21tYW5kW11cbiAgZGVidWc6IGJvb2xlYW5cbiAgaW5pdGlhbFRvb2xzOiBUb29sW11cbiAgLy8gSW5pdGlhbCBtZXNzYWdlcyB0byBwb3B1bGF0ZSB0aGUgUkVQTCB3aXRoXG4gIGluaXRpYWxNZXNzYWdlcz86IE1lc3NhZ2VUeXBlW11cbiAgLy8gRGVmZXJyZWQgaG9vayBtZXNzYWdlcyBwcm9taXNlIOKAlCBSRVBMIHJlbmRlcnMgaW1tZWRpYXRlbHkgYW5kIGluamVjdHNcbiAgLy8gaG9vayBtZXNzYWdlcyB3aGVuIHRoZXkgcmVzb2x2ZS4gQXdhaXRlZCBiZWZvcmUgdGhlIGZpcnN0IEFQSSBjYWxsLlxuICBwZW5kaW5nSG9va01lc3NhZ2VzPzogUHJvbWlzZTxIb29rUmVzdWx0TWVzc2FnZVtdPlxuICBpbml0aWFsRmlsZUhpc3RvcnlTbmFwc2hvdHM/OiBGaWxlSGlzdG9yeVNuYXBzaG90W11cbiAgLy8gQ29udGVudC1yZXBsYWNlbWVudCByZWNvcmRzIGZyb20gYSByZXN1bWVkIHNlc3Npb24ncyB0cmFuc2NyaXB0IOKAlCB1c2VkIHRvXG4gIC8vIHJlY29uc3RydWN0IGNvbnRlbnRSZXBsYWNlbWVudFN0YXRlIHNvIHRoZSBzYW1lIHJlc3VsdHMgYXJlIHJlLXJlcGxhY2VkXG4gIGluaXRpYWxDb250ZW50UmVwbGFjZW1lbnRzPzogQ29udGVudFJlcGxhY2VtZW50UmVjb3JkW11cbiAgLy8gSW5pdGlhbCBhZ2VudCBjb250ZXh0IGZvciBzZXNzaW9uIHJlc3VtZSAobmFtZS9jb2xvciBzZXQgdmlhIC9yZW5hbWUgb3IgL2NvbG9yKVxuICBpbml0aWFsQWdlbnROYW1lPzogc3RyaW5nXG4gIGluaXRpYWxBZ2VudENvbG9yPzogQWdlbnRDb2xvck5hbWVcbiAgbWNwQ2xpZW50cz86IE1DUFNlcnZlckNvbm5lY3Rpb25bXVxuICBkeW5hbWljTWNwQ29uZmlnPzogUmVjb3JkPHN0cmluZywgU2NvcGVkTWNwU2VydmVyQ29uZmlnPlxuICBhdXRvQ29ubmVjdElkZUZsYWc/OiBib29sZWFuXG4gIHN0cmljdE1jcENvbmZpZz86IGJvb2xlYW5cbiAgc3lzdGVtUHJvbXB0Pzogc3RyaW5nXG4gIGFwcGVuZFN5c3RlbVByb21wdD86IHN0cmluZ1xuICAvLyBPcHRpb25hbCBjYWxsYmFjayBpbnZva2VkIGJlZm9yZSBxdWVyeSBleGVjdXRpb25cbiAgLy8gQ2FsbGVkIGFmdGVyIHVzZXIgbWVzc2FnZSBpcyBhZGRlZCB0byBjb252ZXJzYXRpb24gYnV0IGJlZm9yZSBBUEkgY2FsbFxuICAvLyBSZXR1cm4gZmFsc2UgdG8gcHJldmVudCBxdWVyeSBleGVjdXRpb25cbiAgb25CZWZvcmVRdWVyeT86IChcbiAgICBpbnB1dDogc3RyaW5nLFxuICAgIG5ld01lc3NhZ2VzOiBNZXNzYWdlVHlwZVtdLFxuICApID0+IFByb21pc2U8Ym9vbGVhbj5cbiAgLy8gT3B0aW9uYWwgY2FsbGJhY2sgd2hlbiBhIHR1cm4gY29tcGxldGVzIChtb2RlbCBmaW5pc2hlcyByZXNwb25kaW5nKVxuICBvblR1cm5Db21wbGV0ZT86IChtZXNzYWdlczogTWVzc2FnZVR5cGVbXSkgPT4gdm9pZCB8IFByb21pc2U8dm9pZD5cbiAgLy8gV2hlbiB0cnVlLCBkaXNhYmxlcyBSRVBMIGlucHV0IChoaWRlcyBwcm9tcHQgYW5kIHByZXZlbnRzIG1lc3NhZ2Ugc2VsZWN0b3IpXG4gIGRpc2FibGVkPzogYm9vbGVhblxuICAvLyBPcHRpb25hbCBhZ2VudCBkZWZpbml0aW9uIHRvIHVzZSBmb3IgdGhlIG1haW4gdGhyZWFkXG4gIG1haW5UaHJlYWRBZ2VudERlZmluaXRpb24/OiBBZ2VudERlZmluaXRpb25cbiAgLy8gV2hlbiB0cnVlLCBkaXNhYmxlcyBhbGwgc2xhc2ggY29tbWFuZHNcbiAgZGlzYWJsZVNsYXNoQ29tbWFuZHM/OiBib29sZWFuXG4gIC8vIFRhc2sgbGlzdCBpZDogd2hlbiBzZXQsIGVuYWJsZXMgdGFza3MgbW9kZSB0aGF0IHdhdGNoZXMgYSB0YXNrIGxpc3QgYW5kIGF1dG8tcHJvY2Vzc2VzIHRhc2tzLlxuICB0YXNrTGlzdElkPzogc3RyaW5nXG4gIC8vIFJlbW90ZSBzZXNzaW9uIGNvbmZpZyBmb3IgLS1yZW1vdGUgbW9kZSAodXNlcyBDQ1IgYXMgZXhlY3V0aW9uIGVuZ2luZSlcbiAgcmVtb3RlU2Vzc2lvbkNvbmZpZz86IFJlbW90ZVNlc3Npb25Db25maWdcbiAgLy8gRGlyZWN0IGNvbm5lY3QgY29uZmlnIGZvciBgY2xhdWRlIGNvbm5lY3RgIG1vZGUgKGNvbm5lY3RzIHRvIGEgY2xhdWRlIHNlcnZlcilcbiAgZGlyZWN0Q29ubmVjdENvbmZpZz86IERpcmVjdENvbm5lY3RDb25maWdcbiAgLy8gU1NIIHNlc3Npb24gZm9yIGBjbGF1ZGUgc3NoYCBtb2RlIChsb2NhbCBSRVBMLCByZW1vdGUgdG9vbHMgb3ZlciBzc2gpXG4gIHNzaFNlc3Npb24/OiBTU0hTZXNzaW9uXG4gIC8vIFRoaW5raW5nIGNvbmZpZ3VyYXRpb24gdG8gdXNlIHdoZW4gdGhpbmtpbmcgaXMgZW5hYmxlZFxuICB0aGlua2luZ0NvbmZpZzogVGhpbmtpbmdDb25maWdcbn1cblxuZXhwb3J0IHR5cGUgU2NyZWVuID0gJ3Byb21wdCcgfCAndHJhbnNjcmlwdCdcblxuZXhwb3J0IGZ1bmN0aW9uIFJFUEwoe1xuICBjb21tYW5kczogaW5pdGlhbENvbW1hbmRzLFxuICBkZWJ1ZyxcbiAgaW5pdGlhbFRvb2xzLFxuICBpbml0aWFsTWVzc2FnZXMsXG4gIHBlbmRpbmdIb29rTWVzc2FnZXMsXG4gIGluaXRpYWxGaWxlSGlzdG9yeVNuYXBzaG90cyxcbiAgaW5pdGlhbENvbnRlbnRSZXBsYWNlbWVudHMsXG4gIGluaXRpYWxBZ2VudE5hbWUsXG4gIGluaXRpYWxBZ2VudENvbG9yLFxuICBtY3BDbGllbnRzOiBpbml0aWFsTWNwQ2xpZW50cyxcbiAgZHluYW1pY01jcENvbmZpZzogaW5pdGlhbER5bmFtaWNNY3BDb25maWcsXG4gIGF1dG9Db25uZWN0SWRlRmxhZyxcbiAgc3RyaWN0TWNwQ29uZmlnID0gZmFsc2UsXG4gIHN5c3RlbVByb21wdDogY3VzdG9tU3lzdGVtUHJvbXB0LFxuICBhcHBlbmRTeXN0ZW1Qcm9tcHQsXG4gIG9uQmVmb3JlUXVlcnksXG4gIG9uVHVybkNvbXBsZXRlLFxuICBkaXNhYmxlZCA9IGZhbHNlLFxuICBtYWluVGhyZWFkQWdlbnREZWZpbml0aW9uOiBpbml0aWFsTWFpblRocmVhZEFnZW50RGVmaW5pdGlvbixcbiAgZGlzYWJsZVNsYXNoQ29tbWFuZHMgPSBmYWxzZSxcbiAgdGFza0xpc3RJZCxcbiAgcmVtb3RlU2Vzc2lvbkNvbmZpZyxcbiAgZGlyZWN0Q29ubmVjdENvbmZpZyxcbiAgc3NoU2Vzc2lvbixcbiAgdGhpbmtpbmdDb25maWcsXG59OiBQcm9wcyk6IFJlYWN0LlJlYWN0Tm9kZSB7XG4gIGNvbnN0IGlzUmVtb3RlU2Vzc2lvbiA9ICEhcmVtb3RlU2Vzc2lvbkNvbmZpZ1xuXG4gIC8vIEVudi12YXIgZ2F0ZXMgaG9pc3RlZCB0byBtb3VudC10aW1lIOKAlCBpc0VudlRydXRoeSBkb2VzIHRvTG93ZXJDYXNlK3RyaW0rXG4gIC8vIGluY2x1ZGVzLCBhbmQgdGhlc2Ugd2VyZSBvbiB0aGUgcmVuZGVyIHBhdGggKGhvdCBkdXJpbmcgUGFnZVVwIHNwYW0pLlxuICBjb25zdCB0aXRsZURpc2FibGVkID0gdXNlTWVtbyhcbiAgICAoKSA9PiBpc0VudlRydXRoeShwcm9jZXNzLmVudi5DTEFVREVfQ09ERV9ESVNBQkxFX1RFUk1JTkFMX1RJVExFKSxcbiAgICBbXSxcbiAgKVxuICBjb25zdCBtb3JlUmlnaHRFbmFibGVkID0gdXNlTWVtbyhcbiAgICAoKSA9PlxuICAgICAgXCJleHRlcm5hbFwiID09PSAnYW50JyAmJlxuICAgICAgaXNFbnZUcnV0aHkocHJvY2Vzcy5lbnYuQ0xBVURFX01PUkVSSUdIVCksXG4gICAgW10sXG4gIClcbiAgY29uc3QgZGlzYWJsZVZpcnR1YWxTY3JvbGwgPSB1c2VNZW1vKFxuICAgICgpID0+IGlzRW52VHJ1dGh5KHByb2Nlc3MuZW52LkNMQVVERV9DT0RFX0RJU0FCTEVfVklSVFVBTF9TQ1JPTEwpLFxuICAgIFtdLFxuICApXG4gIGNvbnN0IGRpc2FibGVNZXNzYWdlQWN0aW9ucyA9IGZlYXR1cmUoJ01FU1NBR0VfQUNUSU9OUycpXG4gICAgPyAvLyBiaW9tZS1pZ25vcmUgbGludC9jb3JyZWN0bmVzcy91c2VIb29rQXRUb3BMZXZlbDogZmVhdHVyZSgpIGlzIGEgY29tcGlsZS10aW1lIGNvbnN0YW50XG4gICAgICB1c2VNZW1vKFxuICAgICAgICAoKSA9PiBpc0VudlRydXRoeShwcm9jZXNzLmVudi5DTEFVREVfQ09ERV9ESVNBQkxFX01FU1NBR0VfQUNUSU9OUyksXG4gICAgICAgIFtdLFxuICAgICAgKVxuICAgIDogZmFsc2VcblxuICAvLyBMb2cgUkVQTCBtb3VudC91bm1vdW50IGxpZmVjeWNsZVxuICB1c2VFZmZlY3QoKCkgPT4ge1xuICAgIGxvZ0ZvckRlYnVnZ2luZyhgW1JFUEw6bW91bnRdIFJFUEwgbW91bnRlZCwgZGlzYWJsZWQ9JHtkaXNhYmxlZH1gKVxuICAgIHJldHVybiAoKSA9PiBsb2dGb3JEZWJ1Z2dpbmcoYFtSRVBMOnVubW91bnRdIFJFUEwgdW5tb3VudGluZ2ApXG4gIH0sIFtkaXNhYmxlZF0pXG5cbiAgLy8gQWdlbnQgZGVmaW5pdGlvbiBpcyBzdGF0ZSBzbyAvcmVzdW1lIGNhbiB1cGRhdGUgaXQgbWlkLXNlc3Npb25cbiAgY29uc3QgW21haW5UaHJlYWRBZ2VudERlZmluaXRpb24sIHNldE1haW5UaHJlYWRBZ2VudERlZmluaXRpb25dID0gdXNlU3RhdGUoXG4gICAgaW5pdGlhbE1haW5UaHJlYWRBZ2VudERlZmluaXRpb24sXG4gIClcblxuICBjb25zdCB0b29sUGVybWlzc2lvbkNvbnRleHQgPSB1c2VBcHBTdGF0ZShzID0+IHMudG9vbFBlcm1pc3Npb25Db250ZXh0KVxuICBjb25zdCB2ZXJib3NlID0gdXNlQXBwU3RhdGUocyA9PiBzLnZlcmJvc2UpXG4gIGNvbnN0IG1jcCA9IHVzZUFwcFN0YXRlKHMgPT4gcy5tY3ApXG4gIGNvbnN0IHBsdWdpbnMgPSB1c2VBcHBTdGF0ZShzID0+IHMucGx1Z2lucylcbiAgY29uc3QgYWdlbnREZWZpbml0aW9ucyA9IHVzZUFwcFN0YXRlKHMgPT4gcy5hZ2VudERlZmluaXRpb25zKVxuICBjb25zdCBmaWxlSGlzdG9yeSA9IHVzZUFwcFN0YXRlKHMgPT4gcy5maWxlSGlzdG9yeSlcbiAgY29uc3QgaW5pdGlhbE1lc3NhZ2UgPSB1c2VBcHBTdGF0ZShzID0+IHMuaW5pdGlhbE1lc3NhZ2UpXG4gIGNvbnN0IHF1ZXVlZENvbW1hbmRzID0gdXNlQ29tbWFuZFF1ZXVlKClcbiAgLy8gZmVhdHVyZSgpIGlzIGEgYnVpbGQtdGltZSBjb25zdGFudCDigJQgZGVhZCBjb2RlIGVsaW1pbmF0aW9uIHJlbW92ZXMgdGhlIGhvb2tcbiAgLy8gY2FsbCBlbnRpcmVseSBpbiBleHRlcm5hbCBidWlsZHMsIHNvIHRoaXMgaXMgc2FmZSBkZXNwaXRlIGxvb2tpbmcgY29uZGl0aW9uYWwuXG4gIC8vIFRoZXNlIGZpZWxkcyBjb250YWluIGV4Y2x1ZGVkIHN0cmluZ3MgdGhhdCBtdXN0IG5vdCBhcHBlYXIgaW4gZXh0ZXJuYWwgYnVpbGRzLlxuICBjb25zdCBzcGlubmVyVGlwID0gdXNlQXBwU3RhdGUocyA9PiBzLnNwaW5uZXJUaXApXG4gIGNvbnN0IHNob3dFeHBhbmRlZFRvZG9zID0gdXNlQXBwU3RhdGUocyA9PiBzLmV4cGFuZGVkVmlldykgPT09ICd0YXNrcydcbiAgY29uc3QgcGVuZGluZ1dvcmtlclJlcXVlc3QgPSB1c2VBcHBTdGF0ZShzID0+IHMucGVuZGluZ1dvcmtlclJlcXVlc3QpXG4gIGNvbnN0IHBlbmRpbmdTYW5kYm94UmVxdWVzdCA9IHVzZUFwcFN0YXRlKHMgPT4gcy5wZW5kaW5nU2FuZGJveFJlcXVlc3QpXG4gIGNvbnN0IHRlYW1Db250ZXh0ID0gdXNlQXBwU3RhdGUocyA9PiBzLnRlYW1Db250ZXh0KVxuICBjb25zdCB0YXNrcyA9IHVzZUFwcFN0YXRlKHMgPT4gcy50YXNrcylcbiAgY29uc3Qgd29ya2VyU2FuZGJveFBlcm1pc3Npb25zID0gdXNlQXBwU3RhdGUocyA9PiBzLndvcmtlclNhbmRib3hQZXJtaXNzaW9ucylcbiAgY29uc3QgZWxpY2l0YXRpb24gPSB1c2VBcHBTdGF0ZShzID0+IHMuZWxpY2l0YXRpb24pXG4gIGNvbnN0IHVsdHJhcGxhblBlbmRpbmdDaG9pY2UgPSB1c2VBcHBTdGF0ZShzID0+IHMudWx0cmFwbGFuUGVuZGluZ0Nob2ljZSlcbiAgY29uc3QgdWx0cmFwbGFuTGF1bmNoUGVuZGluZyA9IHVzZUFwcFN0YXRlKHMgPT4gcy51bHRyYXBsYW5MYXVuY2hQZW5kaW5nKVxuICBjb25zdCB2aWV3aW5nQWdlbnRUYXNrSWQgPSB1c2VBcHBTdGF0ZShzID0+IHMudmlld2luZ0FnZW50VGFza0lkKVxuICBjb25zdCBzZXRBcHBTdGF0ZSA9IHVzZVNldEFwcFN0YXRlKClcblxuICAvLyBCb290c3RyYXA6IHJldGFpbmVkIGxvY2FsX2FnZW50IHRoYXQgaGFzbid0IGxvYWRlZCBkaXNrIHlldCDihpIgcmVhZFxuICAvLyBzaWRlY2hhaW4gSlNPTkwgYW5kIFVVSUQtbWVyZ2Ugd2l0aCB3aGF0ZXZlciBzdHJlYW0gaGFzIGFwcGVuZGVkIHNvIGZhci5cbiAgLy8gU3RyZWFtIGFwcGVuZHMgaW1tZWRpYXRlbHkgb24gcmV0YWluIChubyBkZWZlcik7IGJvb3RzdHJhcCBmaWxscyB0aGVcbiAgLy8gcHJlZml4LiBEaXNrLXdyaXRlLWJlZm9yZS15aWVsZCBtZWFucyBsaXZlIGlzIGFsd2F5cyBhIHN1ZmZpeCBvZiBkaXNrLlxuICBjb25zdCB2aWV3ZWRMb2NhbEFnZW50ID0gdmlld2luZ0FnZW50VGFza0lkXG4gICAgPyB0YXNrc1t2aWV3aW5nQWdlbnRUYXNrSWRdXG4gICAgOiB1bmRlZmluZWRcbiAgY29uc3QgbmVlZHNCb290c3RyYXAgPVxuICAgIGlzTG9jYWxBZ2VudFRhc2sodmlld2VkTG9jYWxBZ2VudCkgJiZcbiAgICB2aWV3ZWRMb2NhbEFnZW50LnJldGFpbiAmJlxuICAgICF2aWV3ZWRMb2NhbEFnZW50LmRpc2tMb2FkZWRcbiAgdXNlRWZmZWN0KCgpID0+IHtcbiAgICBpZiAoIXZpZXdpbmdBZ2VudFRhc2tJZCB8fCAhbmVlZHNCb290c3RyYXApIHJldHVyblxuICAgIGNvbnN0IHRhc2tJZCA9IHZpZXdpbmdBZ2VudFRhc2tJZFxuICAgIHZvaWQgZ2V0QWdlbnRUcmFuc2NyaXB0KGFzQWdlbnRJZCh0YXNrSWQpKS50aGVuKHJlc3VsdCA9PiB7XG4gICAgICBzZXRBcHBTdGF0ZShwcmV2ID0+IHtcbiAgICAgICAgY29uc3QgdCA9IHByZXYudGFza3NbdGFza0lkXVxuICAgICAgICBpZiAoIWlzTG9jYWxBZ2VudFRhc2sodCkgfHwgdC5kaXNrTG9hZGVkIHx8ICF0LnJldGFpbikgcmV0dXJuIHByZXZcbiAgICAgICAgY29uc3QgbGl2ZSA9IHQubWVzc2FnZXMgPz8gW11cbiAgICAgICAgY29uc3QgbGl2ZVV1aWRzID0gbmV3IFNldChsaXZlLm1hcChtID0+IG0udXVpZCkpXG4gICAgICAgIGNvbnN0IGRpc2tPbmx5ID0gcmVzdWx0XG4gICAgICAgICAgPyByZXN1bHQubWVzc2FnZXMuZmlsdGVyKG0gPT4gIWxpdmVVdWlkcy5oYXMobS51dWlkKSlcbiAgICAgICAgICA6IFtdXG4gICAgICAgIHJldHVybiB7XG4gICAgICAgICAgLi4ucHJldixcbiAgICAgICAgICB0YXNrczoge1xuICAgICAgICAgICAgLi4ucHJldi50YXNrcyxcbiAgICAgICAgICAgIFt0YXNrSWRdOiB7XG4gICAgICAgICAgICAgIC4uLnQsXG4gICAgICAgICAgICAgIG1lc3NhZ2VzOiBbLi4uZGlza09ubHksIC4uLmxpdmVdLFxuICAgICAgICAgICAgICBkaXNrTG9hZGVkOiB0cnVlLFxuICAgICAgICAgICAgfSxcbiAgICAgICAgICB9LFxuICAgICAgICB9XG4gICAgICB9KVxuICAgIH0pXG4gIH0sIFt2aWV3aW5nQWdlbnRUYXNrSWQsIG5lZWRzQm9vdHN0cmFwLCBzZXRBcHBTdGF0ZV0pXG5cbiAgY29uc3Qgc3RvcmUgPSB1c2VBcHBTdGF0ZVN0b3JlKClcbiAgY29uc3QgdGVybWluYWwgPSB1c2VUZXJtaW5hbE5vdGlmaWNhdGlvbigpXG4gIGNvbnN0IG1haW5Mb29wTW9kZWwgPSB1c2VNYWluTG9vcE1vZGVsKClcblxuICAvLyBOb3RlOiBzdGFuZGFsb25lQWdlbnRDb250ZXh0IGlzIGluaXRpYWxpemVkIGluIG1haW4udHN4ICh2aWEgaW5pdGlhbFN0YXRlKSBvclxuICAvLyBSZXN1bWVDb252ZXJzYXRpb24udHN4ICh2aWEgc2V0QXBwU3RhdGUgYmVmb3JlIHJlbmRlcmluZyBSRVBMKSB0byBhdm9pZFxuICAvLyB1c2VFZmZlY3QtYmFzZWQgc3RhdGUgaW5pdGlhbGl6YXRpb24gb24gbW91bnQgKHBlciBDTEFVREUubWQgZ3VpZGVsaW5lcylcblxuICAvLyBMb2NhbCBzdGF0ZSBmb3IgY29tbWFuZHMgKGhvdC1yZWxvYWRhYmxlIHdoZW4gc2tpbGwgZmlsZXMgY2hhbmdlKVxuICBjb25zdCBbbG9jYWxDb21tYW5kcywgc2V0TG9jYWxDb21tYW5kc10gPSB1c2VTdGF0ZShpbml0aWFsQ29tbWFuZHMpXG5cbiAgLy8gV2F0Y2ggZm9yIHNraWxsIGZpbGUgY2hhbmdlcyBhbmQgcmVsb2FkIGFsbCBjb21tYW5kc1xuICB1c2VTa2lsbHNDaGFuZ2UoXG4gICAgaXNSZW1vdGVTZXNzaW9uID8gdW5kZWZpbmVkIDogZ2V0UHJvamVjdFJvb3QoKSxcbiAgICBzZXRMb2NhbENvbW1hbmRzLFxuICApXG5cbiAgLy8gVHJhY2sgcHJvYWN0aXZlIG1vZGUgZm9yIHRvb2xzIGRlcGVuZGVuY3kgLSBTbGVlcFRvb2wgZmlsdGVycyBieSBwcm9hY3RpdmUgc3RhdGVcbiAgY29uc3QgcHJvYWN0aXZlQWN0aXZlID0gUmVhY3QudXNlU3luY0V4dGVybmFsU3RvcmUoXG4gICAgcHJvYWN0aXZlTW9kdWxlPy5zdWJzY3JpYmVUb1Byb2FjdGl2ZUNoYW5nZXMgPz8gUFJPQUNUSVZFX05PX09QX1NVQlNDUklCRSxcbiAgICBwcm9hY3RpdmVNb2R1bGU/LmlzUHJvYWN0aXZlQWN0aXZlID8/IFBST0FDVElWRV9GQUxTRSxcbiAgKVxuXG4gIC8vIEJyaWVmVG9vbC5pc0VuYWJsZWQoKSByZWFkcyBnZXRVc2VyTXNnT3B0SW4oKSBmcm9tIGJvb3RzdHJhcCBzdGF0ZSwgd2hpY2hcbiAgLy8gL2JyaWVmIGZsaXBzIG1pZC1zZXNzaW9uIGFsb25nc2lkZSBpc0JyaWVmT25seS4gVGhlIG1lbW8gYmVsb3cgbmVlZHMgYVxuICAvLyBSZWFjdC12aXNpYmxlIGRlcCB0byByZS1ydW4gZ2V0VG9vbHMoKSB3aGVuIHRoYXQgaGFwcGVuczsgaXNCcmllZk9ubHkgaXNcbiAgLy8gdGhlIEFwcFN0YXRlIG1pcnJvciB0aGF0IHRyaWdnZXJzIHRoZSByZS1yZW5kZXIuIFdpdGhvdXQgdGhpcywgdG9nZ2xpbmdcbiAgLy8gL2JyaWVmIG1pZC1zZXNzaW9uIGxlYXZlcyB0aGUgc3RhbGUgdG9vbCBsaXN0IChubyBTZW5kVXNlck1lc3NhZ2UpIGFuZFxuICAvLyB0aGUgbW9kZWwgZW1pdHMgcGxhaW4gdGV4dCB0aGUgYnJpZWYgZmlsdGVyIGhpZGVzLlxuICBjb25zdCBpc0JyaWVmT25seSA9IHVzZUFwcFN0YXRlKHMgPT4gcy5pc0JyaWVmT25seSlcblxuICBjb25zdCBsb2NhbFRvb2xzID0gdXNlTWVtbyhcbiAgICAoKSA9PiBnZXRUb29scyh0b29sUGVybWlzc2lvbkNvbnRleHQpLFxuICAgIFt0b29sUGVybWlzc2lvbkNvbnRleHQsIHByb2FjdGl2ZUFjdGl2ZSwgaXNCcmllZk9ubHldLFxuICApXG5cbiAgdXNlS2lja09mZkNoZWNrQW5kRGlzYWJsZUJ5cGFzc1Blcm1pc3Npb25zSWZOZWVkZWQoKVxuICB1c2VLaWNrT2ZmQ2hlY2tBbmREaXNhYmxlQXV0b01vZGVJZk5lZWRlZCgpXG5cbiAgY29uc3QgW2R5bmFtaWNNY3BDb25maWcsIHNldER5bmFtaWNNY3BDb25maWddID0gdXNlU3RhdGU8XG4gICAgUmVjb3JkPHN0cmluZywgU2NvcGVkTWNwU2VydmVyQ29uZmlnPiB8IHVuZGVmaW5lZFxuICA+KGluaXRpYWxEeW5hbWljTWNwQ29uZmlnKVxuXG4gIGNvbnN0IG9uQ2hhbmdlRHluYW1pY01jcENvbmZpZyA9IHVzZUNhbGxiYWNrKFxuICAgIChjb25maWc6IFJlY29yZDxzdHJpbmcsIFNjb3BlZE1jcFNlcnZlckNvbmZpZz4pID0+IHtcbiAgICAgIHNldER5bmFtaWNNY3BDb25maWcoY29uZmlnKVxuICAgIH0sXG4gICAgW3NldER5bmFtaWNNY3BDb25maWddLFxuICApXG5cbiAgY29uc3QgW3NjcmVlbiwgc2V0U2NyZWVuXSA9IHVzZVN0YXRlPFNjcmVlbj4oJ3Byb21wdCcpXG4gIGNvbnN0IFtzaG93QWxsSW5UcmFuc2NyaXB0LCBzZXRTaG93QWxsSW5UcmFuc2NyaXB0XSA9IHVzZVN0YXRlKGZhbHNlKVxuICAvLyBbIGZvcmNlcyB0aGUgZHVtcC10by1zY3JvbGxiYWNrIHBhdGggaW5zaWRlIHRyYW5zY3JpcHQgbW9kZS4gU2VwYXJhdGVcbiAgLy8gZnJvbSBDTEFVREVfQ09ERV9OT19GTElDS0VSPTAgKHdoaWNoIGlzIHByb2Nlc3MtbGlmZXRpbWUpIOKAlCB0aGlzIGlzXG4gIC8vIGVwaGVtZXJhbCwgcmVzZXQgb24gdHJhbnNjcmlwdCBleGl0LiBEaWFnbm9zdGljIGVzY2FwZSBoYXRjaCBzb1xuICAvLyB0ZXJtaW5hbC90bXV4IG5hdGl2ZSBjbWQtRiBjYW4gc2VhcmNoIHRoZSBmdWxsIGZsYXQgcmVuZGVyLlxuICBjb25zdCBbZHVtcE1vZGUsIHNldER1bXBNb2RlXSA9IHVzZVN0YXRlKGZhbHNlKVxuICAvLyB2LWZvci1lZGl0b3IgcmVuZGVyIHByb2dyZXNzLiBJbmxpbmUgaW4gdGhlIGZvb3RlciDigJQgbm90aWZpY2F0aW9uc1xuICAvLyByZW5kZXIgaW5zaWRlIFByb21wdElucHV0IHdoaWNoIGlzbid0IG1vdW50ZWQgaW4gdHJhbnNjcmlwdC5cbiAgY29uc3QgW2VkaXRvclN0YXR1cywgc2V0RWRpdG9yU3RhdHVzXSA9IHVzZVN0YXRlKCcnKVxuICAvLyBJbmNyZW1lbnRlZCBvbiB0cmFuc2NyaXB0IGV4aXQuIEFzeW5jIHYtcmVuZGVyIGNhcHR1cmVzIHRoaXMgYXQgc3RhcnQ7XG4gIC8vIGVhY2ggc3RhdHVzIHdyaXRlIG5vLW9wcyBpZiBzdGFsZSAodXNlciBsZWZ0IHRyYW5zY3JpcHQgbWlkLXJlbmRlciDigJRcbiAgLy8gdGhlIHN0YWJsZSBzZXRTdGF0ZSB3b3VsZCBvdGhlcndpc2Ugc3RhbXAgYSBnaG9zdCB0b2FzdCBpbnRvIHRoZSBuZXh0XG4gIC8vIHNlc3Npb24pLiBBbHNvIGNsZWFycyBhbnkgcGVuZGluZyA0cyBhdXRvLWNsZWFyLlxuICBjb25zdCBlZGl0b3JHZW5SZWYgPSB1c2VSZWYoMClcbiAgY29uc3QgZWRpdG9yVGltZXJSZWYgPSB1c2VSZWY8UmV0dXJuVHlwZTx0eXBlb2Ygc2V0VGltZW91dD4gfCB1bmRlZmluZWQ+KFxuICAgIHVuZGVmaW5lZCxcbiAgKVxuICBjb25zdCBlZGl0b3JSZW5kZXJpbmdSZWYgPSB1c2VSZWYoZmFsc2UpXG4gIGNvbnN0IHsgYWRkTm90aWZpY2F0aW9uLCByZW1vdmVOb3RpZmljYXRpb24gfSA9IHVzZU5vdGlmaWNhdGlvbnMoKVxuXG4gIC8vIGVzbGludC1kaXNhYmxlLW5leHQtbGluZSBwcmVmZXItY29uc3RcbiAgbGV0IHRyeVN1Z2dlc3RCZ1BSSW50ZXJjZXB0ID0gU1VHR0VTVF9CR19QUl9OT09QXG5cbiAgY29uc3QgbWNwQ2xpZW50cyA9IHVzZU1lcmdlZENsaWVudHMoaW5pdGlhbE1jcENsaWVudHMsIG1jcC5jbGllbnRzKVxuXG4gIC8vIElERSBpbnRlZ3JhdGlvblxuICBjb25zdCBbaWRlU2VsZWN0aW9uLCBzZXRJREVTZWxlY3Rpb25dID0gdXNlU3RhdGU8SURFU2VsZWN0aW9uIHwgdW5kZWZpbmVkPihcbiAgICB1bmRlZmluZWQsXG4gIClcbiAgY29uc3QgW2lkZVRvSW5zdGFsbEV4dGVuc2lvbiwgc2V0SURFVG9JbnN0YWxsRXh0ZW5zaW9uXSA9XG4gICAgdXNlU3RhdGU8SWRlVHlwZSB8IG51bGw+KG51bGwpXG4gIGNvbnN0IFtpZGVJbnN0YWxsYXRpb25TdGF0dXMsIHNldElERUluc3RhbGxhdGlvblN0YXR1c10gPVxuICAgIHVzZVN0YXRlPElERUV4dGVuc2lvbkluc3RhbGxhdGlvblN0YXR1cyB8IG51bGw+KG51bGwpXG4gIGNvbnN0IFtzaG93SWRlT25ib2FyZGluZywgc2V0U2hvd0lkZU9uYm9hcmRpbmddID0gdXNlU3RhdGUoZmFsc2UpXG4gIC8vIERlYWQgY29kZSBlbGltaW5hdGlvbjogbW9kZWwgc3dpdGNoIGNhbGxvdXQgc3RhdGUgKGFudC1vbmx5KVxuICBjb25zdCBbc2hvd01vZGVsU3dpdGNoQ2FsbG91dCwgc2V0U2hvd01vZGVsU3dpdGNoQ2FsbG91dF0gPSB1c2VTdGF0ZSgoKSA9PiB7XG4gICAgaWYgKFwiZXh0ZXJuYWxcIiA9PT0gJ2FudCcpIHtcbiAgICAgIHJldHVybiBzaG91bGRTaG93QW50TW9kZWxTd2l0Y2goKVxuICAgIH1cbiAgICByZXR1cm4gZmFsc2VcbiAgfSlcbiAgY29uc3QgW3Nob3dFZmZvcnRDYWxsb3V0LCBzZXRTaG93RWZmb3J0Q2FsbG91dF0gPSB1c2VTdGF0ZSgoKSA9PlxuICAgIHNob3VsZFNob3dFZmZvcnRDYWxsb3V0KG1haW5Mb29wTW9kZWwpLFxuICApXG4gIGNvbnN0IHNob3dSZW1vdGVDYWxsb3V0ID0gdXNlQXBwU3RhdGUocyA9PiBzLnNob3dSZW1vdGVDYWxsb3V0KVxuICBjb25zdCBbc2hvd0Rlc2t0b3BVcHNlbGxTdGFydHVwLCBzZXRTaG93RGVza3RvcFVwc2VsbFN0YXJ0dXBdID0gdXNlU3RhdGUoKCkgPT5cbiAgICBzaG91bGRTaG93RGVza3RvcFVwc2VsbFN0YXJ0dXAoKSxcbiAgKVxuICAvLyBub3RpZmljYXRpb25zXG4gIHVzZU1vZGVsTWlncmF0aW9uTm90aWZpY2F0aW9ucygpXG4gIHVzZUNhblN3aXRjaFRvRXhpc3RpbmdTdWJzY3JpcHRpb24oKVxuICB1c2VJREVTdGF0dXNJbmRpY2F0b3IoeyBpZGVTZWxlY3Rpb24sIG1jcENsaWVudHMsIGlkZUluc3RhbGxhdGlvblN0YXR1cyB9KVxuICB1c2VNY3BDb25uZWN0aXZpdHlTdGF0dXMoeyBtY3BDbGllbnRzIH0pXG4gIHVzZUF1dG9Nb2RlVW5hdmFpbGFibGVOb3RpZmljYXRpb24oKVxuICB1c2VQbHVnaW5JbnN0YWxsYXRpb25TdGF0dXMoKVxuICB1c2VQbHVnaW5BdXRvdXBkYXRlTm90aWZpY2F0aW9uKClcbiAgdXNlU2V0dGluZ3NFcnJvcnMoKVxuICB1c2VSYXRlTGltaXRXYXJuaW5nTm90aWZpY2F0aW9uKG1haW5Mb29wTW9kZWwpXG4gIHVzZUZhc3RNb2RlTm90aWZpY2F0aW9uKClcbiAgdXNlRGVwcmVjYXRpb25XYXJuaW5nTm90aWZpY2F0aW9uKG1haW5Mb29wTW9kZWwpXG4gIHVzZU5wbURlcHJlY2F0aW9uTm90aWZpY2F0aW9uKClcbiAgdXNlQW50T3JnV2FybmluZ05vdGlmaWNhdGlvbigpXG4gIHVzZUluc3RhbGxNZXNzYWdlcygpXG4gIHVzZUNocm9tZUV4dGVuc2lvbk5vdGlmaWNhdGlvbigpXG4gIHVzZU9mZmljaWFsTWFya2V0cGxhY2VOb3RpZmljYXRpb24oKVxuICB1c2VMc3BJbml0aWFsaXphdGlvbk5vdGlmaWNhdGlvbigpXG4gIHVzZVRlYW1tYXRlTGlmZWN5Y2xlTm90aWZpY2F0aW9uKClcbiAgY29uc3Qge1xuICAgIHJlY29tbWVuZGF0aW9uOiBsc3BSZWNvbW1lbmRhdGlvbixcbiAgICBoYW5kbGVSZXNwb25zZTogaGFuZGxlTHNwUmVzcG9uc2UsXG4gIH0gPSB1c2VMc3BQbHVnaW5SZWNvbW1lbmRhdGlvbigpXG4gIGNvbnN0IHtcbiAgICByZWNvbW1lbmRhdGlvbjogaGludFJlY29tbWVuZGF0aW9uLFxuICAgIGhhbmRsZVJlc3BvbnNlOiBoYW5kbGVIaW50UmVzcG9uc2UsXG4gIH0gPSB1c2VDbGF1ZGVDb2RlSGludFJlY29tbWVuZGF0aW9uKClcblxuICAvLyBNZW1vaXplIHRoZSBjb21iaW5lZCBpbml0aWFsIHRvb2xzIGFycmF5IHRvIHByZXZlbnQgcmVmZXJlbmNlIGNoYW5nZXNcbiAgY29uc3QgY29tYmluZWRJbml0aWFsVG9vbHMgPSB1c2VNZW1vKCgpID0+IHtcbiAgICByZXR1cm4gWy4uLmxvY2FsVG9vbHMsIC4uLmluaXRpYWxUb29sc11cbiAgfSwgW2xvY2FsVG9vbHMsIGluaXRpYWxUb29sc10pXG5cbiAgLy8gSW5pdGlhbGl6ZSBwbHVnaW4gbWFuYWdlbWVudFxuICB1c2VNYW5hZ2VQbHVnaW5zKHsgZW5hYmxlZDogIWlzUmVtb3RlU2Vzc2lvbiB9KVxuXG4gIGNvbnN0IHRhc2tzVjIgPSB1c2VUYXNrc1YyV2l0aENvbGxhcHNlRWZmZWN0KClcblxuICAvLyBTdGFydCBiYWNrZ3JvdW5kIHBsdWdpbiBpbnN0YWxsYXRpb25zXG5cbiAgLy8gU0VDVVJJVFk6IFRoaXMgY29kZSBpcyBndWFyYW50ZWVkIHRvIHJ1biBPTkxZIGFmdGVyIHRoZSBcInRydXN0IHRoaXMgZm9sZGVyXCIgZGlhbG9nXG4gIC8vIGhhcyBiZWVuIGNvbmZpcm1lZCBieSB0aGUgdXNlci4gVGhlIHRydXN0IGRpYWxvZyBpcyBzaG93biBpbiBjbGkudHN4IChsaW5lIH4zODcpXG4gIC8vIGJlZm9yZSB0aGUgUkVQTCBjb21wb25lbnQgaXMgcmVuZGVyZWQuIFRoZSBkaWFsb2cgYmxvY2tzIGV4ZWN1dGlvbiB1bnRpbCB0aGUgdXNlclxuICAvLyBhY2NlcHRzLCBhbmQgb25seSB0aGVuIGlzIHRoZSBSRVBMIGNvbXBvbmVudCBtb3VudGVkIGFuZCB0aGlzIGVmZmVjdCBydW5zLlxuICAvLyBUaGlzIGVuc3VyZXMgdGhhdCBwbHVnaW4gaW5zdGFsbGF0aW9ucyBmcm9tIHJlcG9zaXRvcnkgYW5kIHVzZXIgc2V0dGluZ3Mgb25seVxuICAvLyBoYXBwZW4gYWZ0ZXIgZXhwbGljaXQgdXNlciBjb25zZW50IHRvIHRydXN0IHRoZSBjdXJyZW50IHdvcmtpbmcgZGlyZWN0b3J5LlxuICB1c2VFZmZlY3QoKCkgPT4ge1xuICAgIGlmIChpc1JlbW90ZVNlc3Npb24pIHJldHVyblxuICAgIHZvaWQgcGVyZm9ybVN0YXJ0dXBDaGVja3Moc2V0QXBwU3RhdGUpXG4gIH0sIFtzZXRBcHBTdGF0ZSwgaXNSZW1vdGVTZXNzaW9uXSlcblxuICAvLyBBbGxvdyBDbGF1ZGUgaW4gQ2hyb21lIE1DUCB0byBzZW5kIHByb21wdHMgdGhyb3VnaCBNQ1Agbm90aWZpY2F0aW9uc1xuICAvLyBhbmQgc3luYyBwZXJtaXNzaW9uIG1vZGUgY2hhbmdlcyB0byB0aGUgQ2hyb21lIGV4dGVuc2lvblxuICB1c2VQcm9tcHRzRnJvbUNsYXVkZUluQ2hyb21lKFxuICAgIGlzUmVtb3RlU2Vzc2lvbiA/IEVNUFRZX01DUF9DTElFTlRTIDogbWNwQ2xpZW50cyxcbiAgICB0b29sUGVybWlzc2lvbkNvbnRleHQubW9kZSxcbiAgKVxuXG4gIC8vIEluaXRpYWxpemUgc3dhcm0gZmVhdHVyZXM6IHRlYW1tYXRlIGhvb2tzIGFuZCBjb250ZXh0XG4gIC8vIEhhbmRsZXMgYm90aCBmcmVzaCBzcGF3bnMgYW5kIHJlc3VtZWQgdGVhbW1hdGUgc2Vzc2lvbnNcbiAgdXNlU3dhcm1Jbml0aWFsaXphdGlvbihzZXRBcHBTdGF0ZSwgaW5pdGlhbE1lc3NhZ2VzLCB7XG4gICAgZW5hYmxlZDogIWlzUmVtb3RlU2Vzc2lvbixcbiAgfSlcblxuICBjb25zdCBtZXJnZWRUb29scyA9IHVzZU1lcmdlZFRvb2xzKFxuICAgIGNvbWJpbmVkSW5pdGlhbFRvb2xzLFxuICAgIG1jcC50b29scyxcbiAgICB0b29sUGVybWlzc2lvbkNvbnRleHQsXG4gIClcblxuICAvLyBBcHBseSBhZ2VudCB0b29sIHJlc3RyaWN0aW9ucyBpZiBtYWluVGhyZWFkQWdlbnREZWZpbml0aW9uIGlzIHNldFxuICBjb25zdCB7IHRvb2xzLCBhbGxvd2VkQWdlbnRUeXBlcyB9ID0gdXNlTWVtbygoKSA9PiB7XG4gICAgaWYgKCFtYWluVGhyZWFkQWdlbnREZWZpbml0aW9uKSB7XG4gICAgICByZXR1cm4ge1xuICAgICAgICB0b29sczogbWVyZ2VkVG9vbHMsXG4gICAgICAgIGFsbG93ZWRBZ2VudFR5cGVzOiB1bmRlZmluZWQgYXMgc3RyaW5nW10gfCB1bmRlZmluZWQsXG4gICAgICB9XG4gICAgfVxuICAgIGNvbnN0IHJlc29sdmVkID0gcmVzb2x2ZUFnZW50VG9vbHMoXG4gICAgICBtYWluVGhyZWFkQWdlbnREZWZpbml0aW9uLFxuICAgICAgbWVyZ2VkVG9vbHMsXG4gICAgICBmYWxzZSxcbiAgICAgIHRydWUsXG4gICAgKVxuICAgIHJldHVybiB7XG4gICAgICB0b29sczogcmVzb2x2ZWQucmVzb2x2ZWRUb29scyxcbiAgICAgIGFsbG93ZWRBZ2VudFR5cGVzOiByZXNvbHZlZC5hbGxvd2VkQWdlbnRUeXBlcyxcbiAgICB9XG4gIH0sIFttYWluVGhyZWFkQWdlbnREZWZpbml0aW9uLCBtZXJnZWRUb29sc10pXG5cbiAgLy8gTWVyZ2UgY29tbWFuZHMgZnJvbSBsb2NhbCBzdGF0ZSwgcGx1Z2lucywgYW5kIE1DUFxuICBjb25zdCBjb21tYW5kc1dpdGhQbHVnaW5zID0gdXNlTWVyZ2VkQ29tbWFuZHMoXG4gICAgbG9jYWxDb21tYW5kcyxcbiAgICBwbHVnaW5zLmNvbW1hbmRzIGFzIENvbW1hbmRbXSxcbiAgKVxuICBjb25zdCBtZXJnZWRDb21tYW5kcyA9IHVzZU1lcmdlZENvbW1hbmRzKFxuICAgIGNvbW1hbmRzV2l0aFBsdWdpbnMsXG4gICAgbWNwLmNvbW1hbmRzIGFzIENvbW1hbmRbXSxcbiAgKVxuICAvLyBGaWx0ZXIgb3V0IGFsbCBjb21tYW5kcyBpZiBkaXNhYmxlU2xhc2hDb21tYW5kcyBpcyB0cnVlXG4gIGNvbnN0IGNvbW1hbmRzID0gdXNlTWVtbyhcbiAgICAoKSA9PiAoZGlzYWJsZVNsYXNoQ29tbWFuZHMgPyBbXSA6IG1lcmdlZENvbW1hbmRzKSxcbiAgICBbZGlzYWJsZVNsYXNoQ29tbWFuZHMsIG1lcmdlZENvbW1hbmRzXSxcbiAgKVxuXG4gIHVzZUlkZUxvZ2dpbmcoaXNSZW1vdGVTZXNzaW9uID8gRU1QVFlfTUNQX0NMSUVOVFMgOiBtY3AuY2xpZW50cylcbiAgdXNlSWRlU2VsZWN0aW9uKFxuICAgIGlzUmVtb3RlU2Vzc2lvbiA/IEVNUFRZX01DUF9DTElFTlRTIDogbWNwLmNsaWVudHMsXG4gICAgc2V0SURFU2VsZWN0aW9uLFxuICApXG5cbiAgY29uc3QgW3N0cmVhbU1vZGUsIHNldFN0cmVhbU1vZGVdID0gdXNlU3RhdGU8U3Bpbm5lck1vZGU+KCdyZXNwb25kaW5nJylcbiAgLy8gUmVmIG1pcnJvciBzbyBvblN1Ym1pdCBjYW4gcmVhZCB0aGUgbGF0ZXN0IHZhbHVlIHdpdGhvdXQgYWRkaW5nXG4gIC8vIHN0cmVhbU1vZGUgdG8gaXRzIGRlcHMuIHN0cmVhbU1vZGUgZmxpcHMgYmV0d2VlblxuICAvLyByZXF1ZXN0aW5nL3Jlc3BvbmRpbmcvdG9vbC11c2UgfjEweCBwZXIgdHVybiBkdXJpbmcgc3RyZWFtaW5nOyBoYXZpbmcgaXRcbiAgLy8gaW4gb25TdWJtaXQncyBkZXBzIHdhcyByZWNyZWF0aW5nIG9uU3VibWl0IG9uIGV2ZXJ5IGZsaXAsIHdoaWNoXG4gIC8vIGNhc2NhZGVkIGludG8gUHJvbXB0SW5wdXQgcHJvcCBjaHVybiBhbmQgZG93bnN0cmVhbSB1c2VDYWxsYmFjay91c2VNZW1vXG4gIC8vIGludmFsaWRhdGlvbi4gVGhlIG9ubHkgY29uc3VtZXJzIGluc2lkZSBjYWxsYmFja3MgYXJlIGRlYnVnIGxvZ2dpbmcgYW5kXG4gIC8vIHRlbGVtZXRyeSAoaGFuZGxlUHJvbXB0U3VibWl0LnRzKSwgc28gYSBzdGFsZS1ieS1vbmUtcmVuZGVyIHZhbHVlIGlzXG4gIC8vIGhhcm1sZXNzIOKAlCBidXQgcmVmIG1pcnJvcnMgc3luYyBvbiBldmVyeSByZW5kZXIgYW55d2F5IHNvIGl0J3MgZnJlc2guXG4gIGNvbnN0IHN0cmVhbU1vZGVSZWYgPSB1c2VSZWYoc3RyZWFtTW9kZSlcbiAgc3RyZWFtTW9kZVJlZi5jdXJyZW50ID0gc3RyZWFtTW9kZVxuICBjb25zdCBbc3RyZWFtaW5nVG9vbFVzZXMsIHNldFN0cmVhbWluZ1Rvb2xVc2VzXSA9IHVzZVN0YXRlPFxuICAgIFN0cmVhbWluZ1Rvb2xVc2VbXVxuICA+KFtdKVxuICBjb25zdCBbc3RyZWFtaW5nVGhpbmtpbmcsIHNldFN0cmVhbWluZ1RoaW5raW5nXSA9XG4gICAgdXNlU3RhdGU8U3RyZWFtaW5nVGhpbmtpbmcgfCBudWxsPihudWxsKVxuXG4gIC8vIEF1dG8taGlkZSBzdHJlYW1pbmcgdGhpbmtpbmcgYWZ0ZXIgMzAgc2Vjb25kcyBvZiBiZWluZyBjb21wbGV0ZWRcbiAgdXNlRWZmZWN0KCgpID0+IHtcbiAgICBpZiAoXG4gICAgICBzdHJlYW1pbmdUaGlua2luZyAmJlxuICAgICAgIXN0cmVhbWluZ1RoaW5raW5nLmlzU3RyZWFtaW5nICYmXG4gICAgICBzdHJlYW1pbmdUaGlua2luZy5zdHJlYW1pbmdFbmRlZEF0XG4gICAgKSB7XG4gICAgICBjb25zdCBlbGFwc2VkID0gRGF0ZS5ub3coKSAtIHN0cmVhbWluZ1RoaW5raW5nLnN0cmVhbWluZ0VuZGVkQXRcbiAgICAgIGNvbnN0IHJlbWFpbmluZyA9IDMwMDAwIC0gZWxhcHNlZFxuICAgICAgaWYgKHJlbWFpbmluZyA+IDApIHtcbiAgICAgICAgY29uc3QgdGltZXIgPSBzZXRUaW1lb3V0KHNldFN0cmVhbWluZ1RoaW5raW5nLCByZW1haW5pbmcsIG51bGwpXG4gICAgICAgIHJldHVybiAoKSA9PiBjbGVhclRpbWVvdXQodGltZXIpXG4gICAgICB9IGVsc2Uge1xuICAgICAgICBzZXRTdHJlYW1pbmdUaGlua2luZyhudWxsKVxuICAgICAgfVxuICAgIH1cbiAgfSwgW3N0cmVhbWluZ1RoaW5raW5nXSlcblxuICBjb25zdCBbYWJvcnRDb250cm9sbGVyLCBzZXRBYm9ydENvbnRyb2xsZXJdID1cbiAgICB1c2VTdGF0ZTxBYm9ydENvbnRyb2xsZXIgfCBudWxsPihudWxsKVxuICAvLyBSZWYgdGhhdCBhbHdheXMgcG9pbnRzIHRvIHRoZSBjdXJyZW50IGFib3J0IGNvbnRyb2xsZXIsIHVzZWQgYnkgdGhlXG4gIC8vIFJFUEwgYnJpZGdlIHRvIGFib3J0IHRoZSBhY3RpdmUgcXVlcnkgd2hlbiBhIHJlbW90ZSBpbnRlcnJ1cHQgYXJyaXZlcy5cbiAgY29uc3QgYWJvcnRDb250cm9sbGVyUmVmID0gdXNlUmVmPEFib3J0Q29udHJvbGxlciB8IG51bGw+KG51bGwpXG4gIGFib3J0Q29udHJvbGxlclJlZi5jdXJyZW50ID0gYWJvcnRDb250cm9sbGVyXG5cbiAgLy8gUmVmIGZvciB0aGUgYnJpZGdlIHJlc3VsdCBjYWxsYmFjayDigJQgc2V0IGFmdGVyIHVzZVJlcGxCcmlkZ2UgaW5pdGlhbGl6ZXMsXG4gIC8vIHJlYWQgaW4gdGhlIG9uUXVlcnkgZmluYWxseSBibG9jayB0byBub3RpZnkgbW9iaWxlIGNsaWVudHMgdGhhdCBhIHR1cm4gZW5kZWQuXG4gIGNvbnN0IHNlbmRCcmlkZ2VSZXN1bHRSZWYgPSB1c2VSZWY8KCkgPT4gdm9pZD4oKCkgPT4ge30pXG5cbiAgLy8gUmVmIGZvciB0aGUgc3luY2hyb25vdXMgcmVzdG9yZSBjYWxsYmFjayDigJQgc2V0IGFmdGVyIHJlc3RvcmVNZXNzYWdlU3luYyBpc1xuICAvLyBkZWZpbmVkLCByZWFkIGluIHRoZSBvblF1ZXJ5IGZpbmFsbHkgYmxvY2sgZm9yIGF1dG8tcmVzdG9yZSBvbiBpbnRlcnJ1cHQuXG4gIGNvbnN0IHJlc3RvcmVNZXNzYWdlU3luY1JlZiA9IHVzZVJlZjwobTogVXNlck1lc3NhZ2UpID0+IHZvaWQ+KCgpID0+IHt9KVxuXG4gIC8vIFJlZiB0byB0aGUgZnVsbHNjcmVlbiBsYXlvdXQncyBzY3JvbGwgYm94IGZvciBrZXlib2FyZCBzY3JvbGxpbmcuXG4gIC8vIE51bGwgd2hlbiBmdWxsc2NyZWVuIG1vZGUgaXMgZGlzYWJsZWQgKHJlZiBuZXZlciBhdHRhY2hlZCkuXG4gIGNvbnN0IHNjcm9sbFJlZiA9IHVzZVJlZjxTY3JvbGxCb3hIYW5kbGU+KG51bGwpXG4gIC8vIFNlcGFyYXRlIHJlZiBmb3IgdGhlIG1vZGFsIHNsb3QncyBpbm5lciBTY3JvbGxCb3gg4oCUIHBhc3NlZCB0aHJvdWdoXG4gIC8vIEZ1bGxzY3JlZW5MYXlvdXQg4oaSIE1vZGFsQ29udGV4dCBzbyBUYWJzIGNhbiBhdHRhY2ggaXQgdG8gaXRzIG93blxuICAvLyBTY3JvbGxCb3ggZm9yIHRhbGwgY29udGVudCAoZS5nLiAvc3RhdHVzJ3MgTUNQLXNlcnZlciBsaXN0KS4gTk9UXG4gIC8vIGtleWJvYXJkLWRyaXZlbiDigJQgU2Nyb2xsS2V5YmluZGluZ0hhbmRsZXIgc3RheXMgb24gdGhlIG91dGVyIHJlZiBzb1xuICAvLyBQZ1VwL1BnRG4vd2hlZWwgYWx3YXlzIHNjcm9sbCB0aGUgdHJhbnNjcmlwdCBiZWhpbmQgdGhlIG1vZGFsLlxuICAvLyBQbHVtYmluZyBrZXB0IGZvciBmdXR1cmUgbW9kYWwtc2Nyb2xsIHdpcmluZy5cbiAgY29uc3QgbW9kYWxTY3JvbGxSZWYgPSB1c2VSZWY8U2Nyb2xsQm94SGFuZGxlPihudWxsKVxuICAvLyBUaW1lc3RhbXAgb2YgdGhlIGxhc3QgdXNlci1pbml0aWF0ZWQgc2Nyb2xsICh3aGVlbCwgUGdVcC9QZ0RuLCBjdHJsK3UsXG4gIC8vIEVuZC9Ib21lLCBHLCBkcmFnLXRvLXNjcm9sbCkuIFN0YW1wZWQgaW4gY29tcG9zZWRPblNjcm9sbCDigJQgdGhlIHNpbmdsZVxuICAvLyBjaG9rZXBvaW50IFNjcm9sbEtleWJpbmRpbmdIYW5kbGVyIGNhbGxzIGZvciBldmVyeSB1c2VyIHNjcm9sbCBhY3Rpb24uXG4gIC8vIFByb2dyYW1tYXRpYyBzY3JvbGxzIChyZXBpblNjcm9sbCdzIHNjcm9sbFRvQm90dG9tLCBzdGlja3kgYXV0by1mb2xsb3cpXG4gIC8vIGRvIE5PVCBnbyB0aHJvdWdoIGNvbXBvc2VkT25TY3JvbGwsIHNvIHRoZXkgZG9uJ3Qgc3RhbXAgdGhpcy4gUmVmIG5vdFxuICAvLyBzdGF0ZTogbm8gcmUtcmVuZGVyIG9uIGV2ZXJ5IHdoZWVsIHRpY2suXG4gIGNvbnN0IGxhc3RVc2VyU2Nyb2xsVHNSZWYgPSB1c2VSZWYoMClcblxuICAvLyBTeW5jaHJvbm91cyBzdGF0ZSBtYWNoaW5lIGZvciB0aGUgcXVlcnkgbGlmZWN5Y2xlLiBSZXBsYWNlcyB0aGVcbiAgLy8gZXJyb3ItcHJvbmUgZHVhbC1zdGF0ZSBwYXR0ZXJuIHdoZXJlIGlzTG9hZGluZyAoUmVhY3Qgc3RhdGUsIGFzeW5jXG4gIC8vIGJhdGNoZWQpIGFuZCBpc1F1ZXJ5UnVubmluZyAocmVmLCBzeW5jKSBjb3VsZCBkZXN5bmMuIFNlZSBRdWVyeUd1YXJkLnRzLlxuICBjb25zdCBxdWVyeUd1YXJkID0gUmVhY3QudXNlUmVmKG5ldyBRdWVyeUd1YXJkKCkpLmN1cnJlbnRcblxuICAvLyBTdWJzY3JpYmUgdG8gdGhlIGd1YXJkIOKAlCB0cnVlIGR1cmluZyBkaXNwYXRjaGluZyBvciBydW5uaW5nLlxuICAvLyBUaGlzIGlzIHRoZSBzaW5nbGUgc291cmNlIG9mIHRydXRoIGZvciBcImlzIGEgbG9jYWwgcXVlcnkgaW4gZmxpZ2h0XCIuXG4gIGNvbnN0IGlzUXVlcnlBY3RpdmUgPSBSZWFjdC51c2VTeW5jRXh0ZXJuYWxTdG9yZShcbiAgICBxdWVyeUd1YXJkLnN1YnNjcmliZSxcbiAgICBxdWVyeUd1YXJkLmdldFNuYXBzaG90LFxuICApXG5cbiAgLy8gU2VwYXJhdGUgbG9hZGluZyBmbGFnIGZvciBvcGVyYXRpb25zIG91dHNpZGUgdGhlIGxvY2FsIHF1ZXJ5IGd1YXJkOlxuICAvLyByZW1vdGUgc2Vzc2lvbnMgKHVzZVJlbW90ZVNlc3Npb24gLyB1c2VEaXJlY3RDb25uZWN0KSBhbmQgZm9yZWdyb3VuZGVkXG4gIC8vIGJhY2tncm91bmQgdGFza3MgKHVzZVNlc3Npb25CYWNrZ3JvdW5kaW5nKS4gVGhlc2UgZG9uJ3Qgcm91dGUgdGhyb3VnaFxuICAvLyBvblF1ZXJ5IC8gcXVlcnlHdWFyZCwgc28gdGhleSBuZWVkIHRoZWlyIG93biBzcGlubmVyLXZpc2liaWxpdHkgc3RhdGUuXG4gIC8vIEluaXRpYWxpemUgdHJ1ZSBpZiByZW1vdGUgbW9kZSB3aXRoIGluaXRpYWwgcHJvbXB0IChDQ1IgcHJvY2Vzc2luZyBpdCkuXG4gIGNvbnN0IFtpc0V4dGVybmFsTG9hZGluZywgc2V0SXNFeHRlcm5hbExvYWRpbmdSYXddID0gUmVhY3QudXNlU3RhdGUoXG4gICAgcmVtb3RlU2Vzc2lvbkNvbmZpZz8uaGFzSW5pdGlhbFByb21wdCA/PyBmYWxzZSxcbiAgKVxuXG4gIC8vIERlcml2ZWQ6IGFueSBsb2FkaW5nIHNvdXJjZSBhY3RpdmUuIFJlYWQtb25seSDigJQgbm8gc2V0dGVyLiBMb2NhbCBxdWVyeVxuICAvLyBsb2FkaW5nIGlzIGRyaXZlbiBieSBxdWVyeUd1YXJkIChyZXNlcnZlL3RyeVN0YXJ0L2VuZC9jYW5jZWxSZXNlcnZhdGlvbiksXG4gIC8vIGV4dGVybmFsIGxvYWRpbmcgYnkgc2V0SXNFeHRlcm5hbExvYWRpbmcuXG4gIGNvbnN0IGlzTG9hZGluZyA9IGlzUXVlcnlBY3RpdmUgfHwgaXNFeHRlcm5hbExvYWRpbmdcblxuICAvLyBFbGFwc2VkIHRpbWUgaXMgY29tcHV0ZWQgYnkgU3Bpbm5lcldpdGhWZXJiIGZyb20gdGhlc2UgcmVmcyBvbiBlYWNoXG4gIC8vIGFuaW1hdGlvbiBmcmFtZSwgYXZvaWRpbmcgYSB1c2VJbnRlcnZhbCB0aGF0IHJlLXJlbmRlcnMgdGhlIGVudGlyZSBSRVBMLlxuICBjb25zdCBbdXNlcklucHV0T25Qcm9jZXNzaW5nLCBzZXRVc2VySW5wdXRPblByb2Nlc3NpbmdSYXddID0gUmVhY3QudXNlU3RhdGU8XG4gICAgc3RyaW5nIHwgdW5kZWZpbmVkXG4gID4odW5kZWZpbmVkKVxuICAvLyBtZXNzYWdlc1JlZi5jdXJyZW50Lmxlbmd0aCBhdCB0aGUgbW9tZW50IHVzZXJJbnB1dE9uUHJvY2Vzc2luZyB3YXMgc2V0LlxuICAvLyBUaGUgcGxhY2Vob2xkZXIgaGlkZXMgb25jZSBkaXNwbGF5ZWRNZXNzYWdlcyBncm93cyBwYXN0IHRoaXMg4oCUIGkuZS4gdGhlXG4gIC8vIHJlYWwgdXNlciBtZXNzYWdlIGhhcyBsYW5kZWQgaW4gdGhlIHZpc2libGUgdHJhbnNjcmlwdC5cbiAgY29uc3QgdXNlcklucHV0QmFzZWxpbmVSZWYgPSBSZWFjdC51c2VSZWYoMClcbiAgLy8gVHJ1ZSB3aGlsZSB0aGUgc3VibWl0dGVkIHByb21wdCBpcyBiZWluZyBwcm9jZXNzZWQgYnV0IGl0cyB1c2VyIG1lc3NhZ2VcbiAgLy8gaGFzbid0IHJlYWNoZWQgc2V0TWVzc2FnZXMgeWV0LiBzZXRNZXNzYWdlcyB1c2VzIHRoaXMgdG8ga2VlcCB0aGVcbiAgLy8gYmFzZWxpbmUgaW4gc3luYyB3aGVuIHVucmVsYXRlZCBhc3luYyBtZXNzYWdlcyAoYnJpZGdlIHN0YXR1cywgaG9va1xuICAvLyByZXN1bHRzLCBzY2hlZHVsZWQgdGFza3MpIGxhbmQgZHVyaW5nIHRoYXQgd2luZG93LlxuICBjb25zdCB1c2VyTWVzc2FnZVBlbmRpbmdSZWYgPSBSZWFjdC51c2VSZWYoZmFsc2UpXG5cbiAgLy8gV2FsbC1jbG9jayB0aW1lIHRyYWNraW5nIHJlZnMgZm9yIGFjY3VyYXRlIGVsYXBzZWQgdGltZSBjYWxjdWxhdGlvblxuICBjb25zdCBsb2FkaW5nU3RhcnRUaW1lUmVmID0gUmVhY3QudXNlUmVmPG51bWJlcj4oMClcbiAgY29uc3QgdG90YWxQYXVzZWRNc1JlZiA9IFJlYWN0LnVzZVJlZigwKVxuICBjb25zdCBwYXVzZVN0YXJ0VGltZVJlZiA9IFJlYWN0LnVzZVJlZjxudW1iZXIgfCBudWxsPihudWxsKVxuICBjb25zdCByZXNldFRpbWluZ1JlZnMgPSBSZWFjdC51c2VDYWxsYmFjaygoKSA9PiB7XG4gICAgbG9hZGluZ1N0YXJ0VGltZVJlZi5jdXJyZW50ID0gRGF0ZS5ub3coKVxuICAgIHRvdGFsUGF1c2VkTXNSZWYuY3VycmVudCA9IDBcbiAgICBwYXVzZVN0YXJ0VGltZVJlZi5jdXJyZW50ID0gbnVsbFxuICB9LCBbXSlcblxuICAvLyBSZXNldCB0aW1pbmcgcmVmcyBpbmxpbmUgd2hlbiBpc1F1ZXJ5QWN0aXZlIHRyYW5zaXRpb25zIGZhbHNl4oaSdHJ1ZS5cbiAgLy8gcXVlcnlHdWFyZC5yZXNlcnZlKCkgKGluIGV4ZWN1dGVVc2VySW5wdXQpIGZpcmVzIEJFRk9SRSBwcm9jZXNzVXNlcklucHV0J3NcbiAgLy8gZmlyc3QgYXdhaXQsIGJ1dCB0aGUgcmVmIHJlc2V0IGluIG9uUXVlcnkncyB0cnkgYmxvY2sgcnVucyBBRlRFUi4gRHVyaW5nXG4gIC8vIHRoYXQgZ2FwLCBSZWFjdCByZW5kZXJzIHRoZSBzcGlubmVyIHdpdGggbG9hZGluZ1N0YXJ0VGltZVJlZj0wLCBjb21wdXRpbmdcbiAgLy8gZWxhcHNlZFRpbWVNcyA9IERhdGUubm93KCkgLSAwIOKJiCA1NiB5ZWFycy4gVGhpcyBpbmxpbmUgcmVzZXQgcnVucyBvbiB0aGVcbiAgLy8gZmlyc3QgcmVuZGVyIHdoZXJlIGlzUXVlcnlBY3RpdmUgaXMgb2JzZXJ2ZWQgdHJ1ZSDigJQgdGhlIHNhbWUgcmVuZGVyIHRoYXRcbiAgLy8gZmlyc3Qgc2hvd3MgdGhlIHNwaW5uZXIg4oCUIHNvIHRoZSByZWYgaXMgY29ycmVjdCBieSB0aGUgdGltZSB0aGUgc3Bpbm5lclxuICAvLyByZWFkcyBpdC4gU2VlIElOQy00NTQ5LlxuICBjb25zdCB3YXNRdWVyeUFjdGl2ZVJlZiA9IFJlYWN0LnVzZVJlZihmYWxzZSlcbiAgaWYgKGlzUXVlcnlBY3RpdmUgJiYgIXdhc1F1ZXJ5QWN0aXZlUmVmLmN1cnJlbnQpIHtcbiAgICByZXNldFRpbWluZ1JlZnMoKVxuICB9XG4gIHdhc1F1ZXJ5QWN0aXZlUmVmLmN1cnJlbnQgPSBpc1F1ZXJ5QWN0aXZlXG5cbiAgLy8gV3JhcHBlciBmb3Igc2V0SXNFeHRlcm5hbExvYWRpbmcgdGhhdCByZXNldHMgdGltaW5nIHJlZnMgb24gdHJhbnNpdGlvblxuICAvLyB0byB0cnVlIOKAlCBTcGlubmVyV2l0aFZlcmIgcmVhZHMgdGhlc2UgZm9yIGVsYXBzZWQgdGltZSwgc28gdGhleSBtdXN0IGJlXG4gIC8vIHJlc2V0IGZvciByZW1vdGUgc2Vzc2lvbnMgLyBmb3JlZ3JvdW5kZWQgdGFza3MgdG9vIChub3QganVzdCBsb2NhbFxuICAvLyBxdWVyaWVzLCB3aGljaCByZXNldCB0aGVtIGluIG9uUXVlcnkpLiBXaXRob3V0IHRoaXMsIGEgcmVtb3RlLW9ubHlcbiAgLy8gc2Vzc2lvbiB3b3VsZCBzaG93IH41NiB5ZWFycyBlbGFwc2VkIChEYXRlLm5vdygpIC0gMCkuXG4gIGNvbnN0IHNldElzRXh0ZXJuYWxMb2FkaW5nID0gUmVhY3QudXNlQ2FsbGJhY2soXG4gICAgKHZhbHVlOiBib29sZWFuKSA9PiB7XG4gICAgICBzZXRJc0V4dGVybmFsTG9hZGluZ1Jhdyh2YWx1ZSlcbiAgICAgIGlmICh2YWx1ZSkgcmVzZXRUaW1pbmdSZWZzKClcbiAgICB9LFxuICAgIFtyZXNldFRpbWluZ1JlZnNdLFxuICApXG5cbiAgLy8gU3RhcnQgdGltZSBvZiB0aGUgZmlyc3QgdHVybiB0aGF0IGhhZCBzd2FybSB0ZWFtbWF0ZXMgcnVubmluZ1xuICAvLyBVc2VkIHRvIGNvbXB1dGUgdG90YWwgZWxhcHNlZCB0aW1lIChpbmNsdWRpbmcgdGVhbW1hdGUgZXhlY3V0aW9uKSBmb3IgdGhlIGRlZmVycmVkIG1lc3NhZ2VcbiAgY29uc3Qgc3dhcm1TdGFydFRpbWVSZWYgPSBSZWFjdC51c2VSZWY8bnVtYmVyIHwgbnVsbD4obnVsbClcbiAgY29uc3Qgc3dhcm1CdWRnZXRJbmZvUmVmID0gUmVhY3QudXNlUmVmPFxuICAgIHsgdG9rZW5zOiBudW1iZXI7IGxpbWl0OiBudW1iZXI7IG51ZGdlczogbnVtYmVyIH0gfCB1bmRlZmluZWRcbiAgPih1bmRlZmluZWQpXG5cbiAgLy8gUmVmIHRvIHRyYWNrIGN1cnJlbnQgZm9jdXNlZElucHV0RGlhbG9nIGZvciB1c2UgaW4gY2FsbGJhY2tzXG4gIC8vIFRoaXMgYXZvaWRzIHN0YWxlIGNsb3N1cmVzIHdoZW4gY2hlY2tpbmcgZGlhbG9nIHN0YXRlIGluIHRpbWVyIGNhbGxiYWNrc1xuICBjb25zdCBmb2N1c2VkSW5wdXREaWFsb2dSZWYgPVxuICAgIFJlYWN0LnVzZVJlZjxSZXR1cm5UeXBlPHR5cGVvZiBnZXRGb2N1c2VkSW5wdXREaWFsb2c+Pih1bmRlZmluZWQpXG5cbiAgLy8gSG93IGxvbmcgYWZ0ZXIgdGhlIGxhc3Qga2V5c3Ryb2tlIGJlZm9yZSBkZWZlcnJlZCBkaWFsb2dzIGFyZSBzaG93blxuICBjb25zdCBQUk9NUFRfU1VQUFJFU1NJT05fTVMgPSAxNTAwXG4gIC8vIFRydWUgd2hlbiB1c2VyIGlzIGFjdGl2ZWx5IHR5cGluZyDigJQgZGVmZXJzIGludGVycnVwdCBkaWFsb2dzIHNvIGtleXN0cm9rZXNcbiAgLy8gZG9uJ3QgYWNjaWRlbnRhbGx5IGRpc21pc3Mgb3IgYW5zd2VyIGEgcGVybWlzc2lvbiBwcm9tcHQgdGhlIHVzZXIgaGFzbid0IHJlYWQgeWV0LlxuICBjb25zdCBbaXNQcm9tcHRJbnB1dEFjdGl2ZSwgc2V0SXNQcm9tcHRJbnB1dEFjdGl2ZV0gPSBSZWFjdC51c2VTdGF0ZShmYWxzZSlcblxuICBjb25zdCBbYXV0b1VwZGF0ZXJSZXN1bHQsIHNldEF1dG9VcGRhdGVyUmVzdWx0XSA9XG4gICAgdXNlU3RhdGU8QXV0b1VwZGF0ZXJSZXN1bHQgfCBudWxsPihudWxsKVxuXG4gIHVzZUVmZmVjdCgoKSA9PiB7XG4gICAgaWYgKGF1dG9VcGRhdGVyUmVzdWx0Py5ub3RpZmljYXRpb25zKSB7XG4gICAgICBhdXRvVXBkYXRlclJlc3VsdC5ub3RpZmljYXRpb25zLmZvckVhY2gobm90aWZpY2F0aW9uID0+IHtcbiAgICAgICAgYWRkTm90aWZpY2F0aW9uKHtcbiAgICAgICAgICBrZXk6ICdhdXRvLXVwZGF0ZXItbm90aWZpY2F0aW9uJyxcbiAgICAgICAgICB0ZXh0OiBub3RpZmljYXRpb24sXG4gICAgICAgICAgcHJpb3JpdHk6ICdsb3cnLFxuICAgICAgICB9KVxuICAgICAgfSlcbiAgICB9XG4gIH0sIFthdXRvVXBkYXRlclJlc3VsdCwgYWRkTm90aWZpY2F0aW9uXSlcblxuICAvLyB0bXV4ICsgZnVsbHNjcmVlbiArIGBtb3VzZSBvZmZgOiBvbmUtdGltZSBoaW50IHRoYXQgd2hlZWwgd29uJ3Qgc2Nyb2xsLlxuICAvLyBXZSBubyBsb25nZXIgbXV0YXRlIHRtdXgncyBzZXNzaW9uLXNjb3BlZCBtb3VzZSBvcHRpb24gKGl0IHBvaXNvbmVkXG4gIC8vIHNpYmxpbmcgcGFuZXMpOyB0bXV4IHVzZXJzIGFscmVhZHkga25vdyB0aGlzIHRyYWRlb2ZmIGZyb20gdmltL2xlc3MuXG4gIHVzZUVmZmVjdCgoKSA9PiB7XG4gICAgaWYgKGlzRnVsbHNjcmVlbkVudkVuYWJsZWQoKSkge1xuICAgICAgdm9pZCBtYXliZUdldFRtdXhNb3VzZUhpbnQoKS50aGVuKGhpbnQgPT4ge1xuICAgICAgICBpZiAoaGludCkge1xuICAgICAgICAgIGFkZE5vdGlmaWNhdGlvbih7XG4gICAgICAgICAgICBrZXk6ICd0bXV4LW1vdXNlLWhpbnQnLFxuICAgICAgICAgICAgdGV4dDogaGludCxcbiAgICAgICAgICAgIHByaW9yaXR5OiAnbG93JyxcbiAgICAgICAgICB9KVxuICAgICAgICB9XG4gICAgICB9KVxuICAgIH1cbiAgICAvLyBlc2xpbnQtZGlzYWJsZS1uZXh0LWxpbmUgcmVhY3QtaG9va3MvZXhoYXVzdGl2ZS1kZXBzXG4gIH0sIFtdKVxuXG4gIGNvbnN0IFtzaG93VW5kZXJjb3ZlckNhbGxvdXQsIHNldFNob3dVbmRlcmNvdmVyQ2FsbG91dF0gPSB1c2VTdGF0ZShmYWxzZSlcbiAgdXNlRWZmZWN0KCgpID0+IHtcbiAgICBpZiAoXCJleHRlcm5hbFwiID09PSAnYW50Jykge1xuICAgICAgdm9pZCAoYXN5bmMgKCkgPT4ge1xuICAgICAgICAvLyBXYWl0IGZvciByZXBvIGNsYXNzaWZpY2F0aW9uIHRvIHNldHRsZSAobWVtb2l6ZWQsIG5vLW9wIGlmIHByaW1lZCkuXG4gICAgICAgIGNvbnN0IHsgaXNJbnRlcm5hbE1vZGVsUmVwbyB9ID0gYXdhaXQgaW1wb3J0KFxuICAgICAgICAgICcuLi91dGlscy9jb21taXRBdHRyaWJ1dGlvbi5qcydcbiAgICAgICAgKVxuICAgICAgICBhd2FpdCBpc0ludGVybmFsTW9kZWxSZXBvKClcbiAgICAgICAgY29uc3QgeyBzaG91bGRTaG93VW5kZXJjb3ZlckF1dG9Ob3RpY2UgfSA9IGF3YWl0IGltcG9ydChcbiAgICAgICAgICAnLi4vdXRpbHMvdW5kZXJjb3Zlci5qcydcbiAgICAgICAgKVxuICAgICAgICBpZiAoc2hvdWxkU2hvd1VuZGVyY292ZXJBdXRvTm90aWNlKCkpIHtcbiAgICAgICAgICBzZXRTaG93VW5kZXJjb3ZlckNhbGxvdXQodHJ1ZSlcbiAgICAgICAgfVxuICAgICAgfSkoKVxuICAgIH1cbiAgICAvLyBlc2xpbnQtZGlzYWJsZS1uZXh0LWxpbmUgcmVhY3QtaG9va3MvZXhoYXVzdGl2ZS1kZXBzXG4gIH0sIFtdKVxuXG4gIGNvbnN0IFt0b29sSlNYLCBzZXRUb29sSlNYSW50ZXJuYWxdID0gdXNlU3RhdGU8e1xuICAgIGpzeDogUmVhY3QuUmVhY3ROb2RlIHwgbnVsbFxuICAgIHNob3VsZEhpZGVQcm9tcHRJbnB1dDogYm9vbGVhblxuICAgIHNob3VsZENvbnRpbnVlQW5pbWF0aW9uPzogdHJ1ZVxuICAgIHNob3dTcGlubmVyPzogYm9vbGVhblxuICAgIGlzTG9jYWxKU1hDb21tYW5kPzogYm9vbGVhblxuICAgIGlzSW1tZWRpYXRlPzogYm9vbGVhblxuICB9IHwgbnVsbD4obnVsbClcblxuICAvLyBUcmFjayBsb2NhbCBKU1ggY29tbWFuZHMgc2VwYXJhdGVseSBzbyB0b29scyBjYW4ndCBvdmVyd3JpdGUgdGhlbS5cbiAgLy8gVGhpcyBlbmFibGVzIFwiaW1tZWRpYXRlXCIgY29tbWFuZHMgKGxpa2UgL2J0dykgdG8gcGVyc2lzdCB3aGlsZSBDbGF1ZGUgaXMgcHJvY2Vzc2luZy5cbiAgY29uc3QgbG9jYWxKU1hDb21tYW5kUmVmID0gdXNlUmVmPHtcbiAgICBqc3g6IFJlYWN0LlJlYWN0Tm9kZSB8IG51bGxcbiAgICBzaG91bGRIaWRlUHJvbXB0SW5wdXQ6IGJvb2xlYW5cbiAgICBzaG91bGRDb250aW51ZUFuaW1hdGlvbj86IHRydWVcbiAgICBzaG93U3Bpbm5lcj86IGJvb2xlYW5cbiAgICBpc0xvY2FsSlNYQ29tbWFuZDogdHJ1ZVxuICB9IHwgbnVsbD4obnVsbClcblxuICAvLyBXcmFwcGVyIGZvciBzZXRUb29sSlNYIHRoYXQgcHJlc2VydmVzIGxvY2FsIEpTWCBjb21tYW5kcyAobGlrZSAvYnR3KS5cbiAgLy8gV2hlbiBhIGxvY2FsIEpTWCBjb21tYW5kIGlzIGFjdGl2ZSwgd2UgaWdub3JlIHVwZGF0ZXMgZnJvbSB0b29sc1xuICAvLyB1bmxlc3MgdGhleSBleHBsaWNpdGx5IHNldCBjbGVhckxvY2FsSlNYOiB0cnVlIChmcm9tIG9uRG9uZSBjYWxsYmFja3MpLlxuICAvL1xuICAvLyBUTyBBREQgQSBORVcgSU1NRURJQVRFIENPTU1BTkQ6XG4gIC8vIDEuIFNldCBgaW1tZWRpYXRlOiB0cnVlYCBpbiB0aGUgY29tbWFuZCBkZWZpbml0aW9uXG4gIC8vIDIuIFNldCBgaXNMb2NhbEpTWENvbW1hbmQ6IHRydWVgIHdoZW4gY2FsbGluZyBzZXRUb29sSlNYIGluIHRoZSBjb21tYW5kJ3MgSlNYXG4gIC8vIDMuIEluIHRoZSBvbkRvbmUgY2FsbGJhY2ssIHVzZSBgc2V0VG9vbEpTWCh7IGpzeDogbnVsbCwgc2hvdWxkSGlkZVByb21wdElucHV0OiBmYWxzZSwgY2xlYXJMb2NhbEpTWDogdHJ1ZSB9KWBcbiAgLy8gICAgdG8gZXhwbGljaXRseSBjbGVhciB0aGUgb3ZlcmxheSB3aGVuIHRoZSB1c2VyIGRpc21pc3NlcyBpdFxuICBjb25zdCBzZXRUb29sSlNYID0gdXNlQ2FsbGJhY2soXG4gICAgKFxuICAgICAgYXJnczoge1xuICAgICAgICBqc3g6IFJlYWN0LlJlYWN0Tm9kZSB8IG51bGxcbiAgICAgICAgc2hvdWxkSGlkZVByb21wdElucHV0OiBib29sZWFuXG4gICAgICAgIHNob3VsZENvbnRpbnVlQW5pbWF0aW9uPzogdHJ1ZVxuICAgICAgICBzaG93U3Bpbm5lcj86IGJvb2xlYW5cbiAgICAgICAgaXNMb2NhbEpTWENvbW1hbmQ/OiBib29sZWFuXG4gICAgICAgIGNsZWFyTG9jYWxKU1g/OiBib29sZWFuXG4gICAgICB9IHwgbnVsbCxcbiAgICApID0+IHtcbiAgICAgIC8vIElmIHNldHRpbmcgYSBsb2NhbCBKU1ggY29tbWFuZCwgc3RvcmUgaXQgaW4gdGhlIHJlZlxuICAgICAgaWYgKGFyZ3M/LmlzTG9jYWxKU1hDb21tYW5kKSB7XG4gICAgICAgIGNvbnN0IHsgY2xlYXJMb2NhbEpTWDogXywgLi4ucmVzdCB9ID0gYXJnc1xuICAgICAgICBsb2NhbEpTWENvbW1hbmRSZWYuY3VycmVudCA9IHsgLi4ucmVzdCwgaXNMb2NhbEpTWENvbW1hbmQ6IHRydWUgfVxuICAgICAgICBzZXRUb29sSlNYSW50ZXJuYWwocmVzdClcbiAgICAgICAgcmV0dXJuXG4gICAgICB9XG5cbiAgICAgIC8vIElmIHRoZXJlJ3MgYW4gYWN0aXZlIGxvY2FsIEpTWCBjb21tYW5kIGluIHRoZSByZWZcbiAgICAgIGlmIChsb2NhbEpTWENvbW1hbmRSZWYuY3VycmVudCkge1xuICAgICAgICAvLyBBbGxvdyBjbGVhcmluZyBvbmx5IGlmIGV4cGxpY2l0bHkgcmVxdWVzdGVkIChmcm9tIG9uRG9uZSBjYWxsYmFja3MpXG4gICAgICAgIGlmIChhcmdzPy5jbGVhckxvY2FsSlNYKSB7XG4gICAgICAgICAgbG9jYWxKU1hDb21tYW5kUmVmLmN1cnJlbnQgPSBudWxsXG4gICAgICAgICAgc2V0VG9vbEpTWEludGVybmFsKG51bGwpXG4gICAgICAgICAgcmV0dXJuXG4gICAgICAgIH1cbiAgICAgICAgLy8gT3RoZXJ3aXNlLCBrZWVwIHRoZSBsb2NhbCBKU1ggY29tbWFuZCB2aXNpYmxlIC0gaWdub3JlIHRvb2wgdXBkYXRlc1xuICAgICAgICByZXR1cm5cbiAgICAgIH1cblxuICAgICAgLy8gTm8gYWN0aXZlIGxvY2FsIEpTWCBjb21tYW5kLCBhbGxvdyBhbnkgdXBkYXRlXG4gICAgICBpZiAoYXJncz8uY2xlYXJMb2NhbEpTWCkge1xuICAgICAgICBzZXRUb29sSlNYSW50ZXJuYWwobnVsbClcbiAgICAgICAgcmV0dXJuXG4gICAgICB9XG4gICAgICBzZXRUb29sSlNYSW50ZXJuYWwoYXJncylcbiAgICB9LFxuICAgIFtdLFxuICApXG4gIGNvbnN0IFt0b29sVXNlQ29uZmlybVF1ZXVlLCBzZXRUb29sVXNlQ29uZmlybVF1ZXVlXSA9IHVzZVN0YXRlPFxuICAgIFRvb2xVc2VDb25maXJtW11cbiAgPihbXSlcbiAgLy8gU3RpY2t5IGZvb3RlciBKU1ggcmVnaXN0ZXJlZCBieSBwZXJtaXNzaW9uIHJlcXVlc3QgY29tcG9uZW50cyAoY3VycmVudGx5XG4gIC8vIG9ubHkgRXhpdFBsYW5Nb2RlUGVybWlzc2lvblJlcXVlc3QpLiBSZW5kZXJzIGluIEZ1bGxzY3JlZW5MYXlvdXQncyBgYm90dG9tYFxuICAvLyBzbG90IHNvIHJlc3BvbnNlIG9wdGlvbnMgc3RheSB2aXNpYmxlIHdoaWxlIHRoZSB1c2VyIHNjcm9sbHMgYSBsb25nIHBsYW4uXG4gIGNvbnN0IFtwZXJtaXNzaW9uU3RpY2t5Rm9vdGVyLCBzZXRQZXJtaXNzaW9uU3RpY2t5Rm9vdGVyXSA9XG4gICAgdXNlU3RhdGU8UmVhY3QuUmVhY3ROb2RlIHwgbnVsbD4obnVsbClcbiAgY29uc3QgW3NhbmRib3hQZXJtaXNzaW9uUmVxdWVzdFF1ZXVlLCBzZXRTYW5kYm94UGVybWlzc2lvblJlcXVlc3RRdWV1ZV0gPVxuICAgIHVzZVN0YXRlPFxuICAgICAgQXJyYXk8e1xuICAgICAgICBob3N0UGF0dGVybjogTmV0d29ya0hvc3RQYXR0ZXJuXG4gICAgICAgIHJlc29sdmVQcm9taXNlOiAoYWxsb3dDb25uZWN0aW9uOiBib29sZWFuKSA9PiB2b2lkXG4gICAgICB9PlxuICAgID4oW10pXG4gIGNvbnN0IFtwcm9tcHRRdWV1ZSwgc2V0UHJvbXB0UXVldWVdID0gdXNlU3RhdGU8XG4gICAgQXJyYXk8e1xuICAgICAgcmVxdWVzdDogUHJvbXB0UmVxdWVzdFxuICAgICAgdGl0bGU6IHN0cmluZ1xuICAgICAgdG9vbElucHV0U3VtbWFyeT86IHN0cmluZyB8IG51bGxcbiAgICAgIHJlc29sdmU6IChyZXNwb25zZTogUHJvbXB0UmVzcG9uc2UpID0+IHZvaWRcbiAgICAgIHJlamVjdDogKGVycm9yOiBFcnJvcikgPT4gdm9pZFxuICAgIH0+XG4gID4oW10pXG5cbiAgLy8gVHJhY2sgYnJpZGdlIGNsZWFudXAgZnVuY3Rpb25zIGZvciBzYW5kYm94IHBlcm1pc3Npb24gcmVxdWVzdHMgc28gdGhlXG4gIC8vIGxvY2FsIGRpYWxvZyBoYW5kbGVyIGNhbiBjYW5jZWwgdGhlIHJlbW90ZSBwcm9tcHQgd2hlbiB0aGUgbG9jYWwgdXNlclxuICAvLyByZXNwb25kcyBmaXJzdC4gS2V5ZWQgYnkgaG9zdCB0byBzdXBwb3J0IGNvbmN1cnJlbnQgc2FtZS1ob3N0IHJlcXVlc3RzLlxuICBjb25zdCBzYW5kYm94QnJpZGdlQ2xlYW51cFJlZiA9IHVzZVJlZjxNYXA8c3RyaW5nLCBBcnJheTwoKSA9PiB2b2lkPj4+KFxuICAgIG5ldyBNYXAoKSxcbiAgKVxuXG4gIC8vIC0tIFRlcm1pbmFsIHRpdGxlIG1hbmFnZW1lbnRcbiAgLy8gU2Vzc2lvbiB0aXRsZSAoc2V0IHZpYSAvcmVuYW1lIG9yIHJlc3RvcmVkIG9uIHJlc3VtZSkgd2lucyBvdmVyXG4gIC8vIHRoZSBhZ2VudCBuYW1lLCB3aGljaCB3aW5zIG92ZXIgdGhlIEhhaWt1LWV4dHJhY3RlZCB0b3BpYztcbiAgLy8gYWxsIGZhbGwgYmFjayB0byB0aGUgcHJvZHVjdCBuYW1lLlxuICBjb25zdCB0ZXJtaW5hbFRpdGxlRnJvbVJlbmFtZSA9XG4gICAgdXNlQXBwU3RhdGUocyA9PiBzLnNldHRpbmdzLnRlcm1pbmFsVGl0bGVGcm9tUmVuYW1lKSAhPT0gZmFsc2VcbiAgY29uc3Qgc2Vzc2lvblRpdGxlID0gdGVybWluYWxUaXRsZUZyb21SZW5hbWVcbiAgICA/IGdldEN1cnJlbnRTZXNzaW9uVGl0bGUoZ2V0U2Vzc2lvbklkKCkpXG4gICAgOiB1bmRlZmluZWRcbiAgY29uc3QgW2hhaWt1VGl0bGUsIHNldEhhaWt1VGl0bGVdID0gdXNlU3RhdGU8c3RyaW5nPigpXG4gIC8vIEdhdGVzIHRoZSBvbmUtc2hvdCBIYWlrdSBjYWxsIHRoYXQgZ2VuZXJhdGVzIHRoZSB0YWIgdGl0bGUuIFNlZWRlZCB0cnVlXG4gIC8vIG9uIHJlc3VtZSAoaW5pdGlhbE1lc3NhZ2VzIHByZXNlbnQpIHNvIHdlIGRvbid0IHJlLXRpdGxlIGEgcmVzdW1lZFxuICAvLyBzZXNzaW9uIGZyb20gbWlkLWNvbnZlcnNhdGlvbiBjb250ZXh0LlxuICBjb25zdCBoYWlrdVRpdGxlQXR0ZW1wdGVkUmVmID0gdXNlUmVmKChpbml0aWFsTWVzc2FnZXM/Lmxlbmd0aCA/PyAwKSA+IDApXG4gIGNvbnN0IGFnZW50VGl0bGUgPSBtYWluVGhyZWFkQWdlbnREZWZpbml0aW9uPy5hZ2VudFR5cGVcbiAgY29uc3QgdGVybWluYWxUaXRsZSA9XG4gICAgc2Vzc2lvblRpdGxlID8/IGFnZW50VGl0bGUgPz8gaGFpa3VUaXRsZSA/PyAnQ2xhdWRlIENvZGUnXG4gIGNvbnN0IGlzV2FpdGluZ0ZvckFwcHJvdmFsID1cbiAgICB0b29sVXNlQ29uZmlybVF1ZXVlLmxlbmd0aCA+IDAgfHxcbiAgICBwcm9tcHRRdWV1ZS5sZW5ndGggPiAwIHx8XG4gICAgcGVuZGluZ1dvcmtlclJlcXVlc3QgfHxcbiAgICBwZW5kaW5nU2FuZGJveFJlcXVlc3RcbiAgLy8gTG9jYWwtanN4IGNvbW1hbmRzIChsaWtlIC9wbHVnaW4sIC9jb25maWcpIHNob3cgdXNlci1mYWNpbmcgZGlhbG9ncyB0aGF0XG4gIC8vIHdhaXQgZm9yIGlucHV0LiBSZXF1aXJlIGpzeCAhPSBudWxsIOKAlCBpZiB0aGUgZmxhZyBpcyBzdHVjayB0cnVlIGJ1dCBqc3hcbiAgLy8gaXMgbnVsbCwgdHJlYXQgYXMgbm90LXNob3dpbmcgc28gVGV4dElucHV0IGZvY3VzIGFuZCBxdWV1ZSBwcm9jZXNzb3JcbiAgLy8gYXJlbid0IGRlYWRsb2NrZWQgYnkgYSBwaGFudG9tIG92ZXJsYXkuXG4gIGNvbnN0IGlzU2hvd2luZ0xvY2FsSlNYQ29tbWFuZCA9XG4gICAgdG9vbEpTWD8uaXNMb2NhbEpTWENvbW1hbmQgPT09IHRydWUgJiYgdG9vbEpTWD8uanN4ICE9IG51bGxcbiAgY29uc3QgdGl0bGVJc0FuaW1hdGluZyA9XG4gICAgaXNMb2FkaW5nICYmICFpc1dhaXRpbmdGb3JBcHByb3ZhbCAmJiAhaXNTaG93aW5nTG9jYWxKU1hDb21tYW5kXG4gIC8vIFRpdGxlIGFuaW1hdGlvbiBzdGF0ZSBsaXZlcyBpbiA8QW5pbWF0ZWRUZXJtaW5hbFRpdGxlPiBzbyB0aGUgOTYwbXMgdGlja1xuICAvLyBkb2Vzbid0IHJlLXJlbmRlciBSRVBMLiB0aXRsZURpc2FibGVkL3Rlcm1pbmFsVGl0bGUgYXJlIHN0aWxsIGNvbXB1dGVkXG4gIC8vIGhlcmUgYmVjYXVzZSBvblF1ZXJ5SW1wbCByZWFkcyB0aGVtIChiYWNrZ3JvdW5kIHNlc3Npb24gZGVzY3JpcHRpb24sXG4gIC8vIGhhaWt1IHRpdGxlIGV4dHJhY3Rpb24gZ2F0ZSkuXG5cbiAgLy8gUHJldmVudCBtYWNPUyBmcm9tIHNsZWVwaW5nIHdoaWxlIENsYXVkZSBpcyB3b3JraW5nXG4gIHVzZUVmZmVjdCgoKSA9PiB7XG4gICAgaWYgKGlzTG9hZGluZyAmJiAhaXNXYWl0aW5nRm9yQXBwcm92YWwgJiYgIWlzU2hvd2luZ0xvY2FsSlNYQ29tbWFuZCkge1xuICAgICAgc3RhcnRQcmV2ZW50U2xlZXAoKVxuICAgICAgcmV0dXJuICgpID0+IHN0b3BQcmV2ZW50U2xlZXAoKVxuICAgIH1cbiAgfSwgW2lzTG9hZGluZywgaXNXYWl0aW5nRm9yQXBwcm92YWwsIGlzU2hvd2luZ0xvY2FsSlNYQ29tbWFuZF0pXG5cbiAgY29uc3Qgc2Vzc2lvblN0YXR1czogVGFiU3RhdHVzS2luZCA9XG4gICAgaXNXYWl0aW5nRm9yQXBwcm92YWwgfHwgaXNTaG93aW5nTG9jYWxKU1hDb21tYW5kXG4gICAgICA/ICd3YWl0aW5nJ1xuICAgICAgOiBpc0xvYWRpbmdcbiAgICAgICAgPyAnYnVzeSdcbiAgICAgICAgOiAnaWRsZSdcblxuICBjb25zdCB3YWl0aW5nRm9yID1cbiAgICBzZXNzaW9uU3RhdHVzICE9PSAnd2FpdGluZydcbiAgICAgID8gdW5kZWZpbmVkXG4gICAgICA6IHRvb2xVc2VDb25maXJtUXVldWUubGVuZ3RoID4gMFxuICAgICAgICA/IGBhcHByb3ZlICR7dG9vbFVzZUNvbmZpcm1RdWV1ZVswXSEudG9vbC5uYW1lfWBcbiAgICAgICAgOiBwZW5kaW5nV29ya2VyUmVxdWVzdFxuICAgICAgICAgID8gJ3dvcmtlciByZXF1ZXN0J1xuICAgICAgICAgIDogcGVuZGluZ1NhbmRib3hSZXF1ZXN0XG4gICAgICAgICAgICA/ICdzYW5kYm94IHJlcXVlc3QnXG4gICAgICAgICAgICA6IGlzU2hvd2luZ0xvY2FsSlNYQ29tbWFuZFxuICAgICAgICAgICAgICA/ICdkaWFsb2cgb3BlbidcbiAgICAgICAgICAgICAgOiAnaW5wdXQgbmVlZGVkJ1xuXG4gIC8vIFB1c2ggc3RhdHVzIHRvIHRoZSBQSUQgZmlsZSBmb3IgYGNsYXVkZSBwc2AuIEZpcmUtYW5kLWZvcmdldDsgcHMgZmFsbHNcbiAgLy8gYmFjayB0byB0cmFuc2NyaXB0LXRhaWwgZGVyaXZhdGlvbiB3aGVuIHRoaXMgaXMgbWlzc2luZy9zdGFsZS5cbiAgdXNlRWZmZWN0KCgpID0+IHtcbiAgICBpZiAoZmVhdHVyZSgnQkdfU0VTU0lPTlMnKSkge1xuICAgICAgdm9pZCB1cGRhdGVTZXNzaW9uQWN0aXZpdHkoeyBzdGF0dXM6IHNlc3Npb25TdGF0dXMsIHdhaXRpbmdGb3IgfSlcbiAgICB9XG4gIH0sIFtzZXNzaW9uU3RhdHVzLCB3YWl0aW5nRm9yXSlcblxuICAvLyAzUCBkZWZhdWx0OiBvZmYg4oCUIE9TQyAyMTMzNyBpcyBhbnQtb25seSB3aGlsZSB0aGUgc3BlYyBzdGFiaWxpemVzLlxuICAvLyBHYXRlZCBzbyB3ZSBjYW4gcm9sbCBiYWNrIGlmIHRoZSBzaWRlYmFyIGluZGljYXRvciBjb25mbGljdHMgd2l0aFxuICAvLyB0aGUgdGl0bGUgc3Bpbm5lciBpbiB0ZXJtaW5hbHMgdGhhdCByZW5kZXIgYm90aC4gV2hlbiB0aGUgZmxhZyBpc1xuICAvLyBvbiwgdGhlIHVzZXItZmFjaW5nIGNvbmZpZyBzZXR0aW5nIGNvbnRyb2xzIHdoZXRoZXIgaXQncyBhY3RpdmUuXG4gIGNvbnN0IHRhYlN0YXR1c0dhdGVFbmFibGVkID0gZ2V0RmVhdHVyZVZhbHVlX0NBQ0hFRF9NQVlfQkVfU1RBTEUoXG4gICAgJ3Rlbmd1X3Rlcm1pbmFsX3NpZGViYXInLFxuICAgIGZhbHNlLFxuICApXG4gIGNvbnN0IHNob3dTdGF0dXNJblRlcm1pbmFsVGFiID1cbiAgICB0YWJTdGF0dXNHYXRlRW5hYmxlZCAmJiAoZ2V0R2xvYmFsQ29uZmlnKCkuc2hvd1N0YXR1c0luVGVybWluYWxUYWIgPz8gZmFsc2UpXG4gIHVzZVRhYlN0YXR1cyh0aXRsZURpc2FibGVkIHx8ICFzaG93U3RhdHVzSW5UZXJtaW5hbFRhYiA/IG51bGwgOiBzZXNzaW9uU3RhdHVzKVxuXG4gIC8vIFJlZ2lzdGVyIHRoZSBsZWFkZXIncyBzZXRUb29sVXNlQ29uZmlybVF1ZXVlIGZvciBpbi1wcm9jZXNzIHRlYW1tYXRlc1xuICB1c2VFZmZlY3QoKCkgPT4ge1xuICAgIHJlZ2lzdGVyTGVhZGVyVG9vbFVzZUNvbmZpcm1RdWV1ZShzZXRUb29sVXNlQ29uZmlybVF1ZXVlKVxuICAgIHJldHVybiAoKSA9PiB1bnJlZ2lzdGVyTGVhZGVyVG9vbFVzZUNvbmZpcm1RdWV1ZSgpXG4gIH0sIFtzZXRUb29sVXNlQ29uZmlybVF1ZXVlXSlcblxuICBjb25zdCBbbWVzc2FnZXMsIHJhd1NldE1lc3NhZ2VzXSA9IHVzZVN0YXRlPE1lc3NhZ2VUeXBlW10+KFxuICAgIGluaXRpYWxNZXNzYWdlcyA/PyBbXSxcbiAgKVxuICBjb25zdCBtZXNzYWdlc1JlZiA9IHVzZVJlZihtZXNzYWdlcylcbiAgLy8gU3RvcmVzIHRoZSB3aWxsb3dNb2RlIHZhcmlhbnQgdGhhdCB3YXMgc2hvd24gKG9yIGZhbHNlIGlmIG5vIGhpbnQgc2hvd24pLlxuICAvLyBDYXB0dXJlZCBhdCBoaW50X3Nob3duIHRpbWUgc28gaGludF9jb252ZXJ0ZWQgdGVsZW1ldHJ5IHJlcG9ydHMgdGhlIHNhbWVcbiAgLy8gdmFyaWFudCDigJQgdGhlIEdyb3d0aEJvb2sgdmFsdWUgc2hvdWxkbid0IGNoYW5nZSBtaWQtc2Vzc2lvbiwgYnV0IHJlYWRpbmdcbiAgLy8gaXQgb25jZSBndWFyYW50ZWVzIGNvbnNpc3RlbmN5IGJldHdlZW4gdGhlIHBhaXJlZCBldmVudHMuXG4gIGNvbnN0IGlkbGVIaW50U2hvd25SZWYgPSB1c2VSZWY8c3RyaW5nIHwgZmFsc2U+KGZhbHNlKVxuICAvLyBXcmFwIHNldE1lc3NhZ2VzIHNvIG1lc3NhZ2VzUmVmIGlzIGFsd2F5cyBjdXJyZW50IHRoZSBpbnN0YW50IHRoZVxuICAvLyBjYWxsIHJldHVybnMg4oCUIG5vdCB3aGVuIFJlYWN0IGxhdGVyIHByb2Nlc3NlcyB0aGUgYmF0Y2guICBBcHBseSB0aGVcbiAgLy8gdXBkYXRlciBlYWdlcmx5IGFnYWluc3QgdGhlIHJlZiwgdGhlbiBoYW5kIFJlYWN0IHRoZSBjb21wdXRlZCB2YWx1ZVxuICAvLyAobm90IHRoZSBmdW5jdGlvbikuICByYXdTZXRNZXNzYWdlcyBiYXRjaGluZyBiZWNvbWVzIGxhc3Qtd3JpdGUtd2lucyxcbiAgLy8gYW5kIHRoZSBsYXN0IHdyaXRlIGlzIGNvcnJlY3QgYmVjYXVzZSBlYWNoIGNhbGwgY29tcG9zZXMgYWdhaW5zdCB0aGVcbiAgLy8gYWxyZWFkeS11cGRhdGVkIHJlZi4gIFRoaXMgaXMgdGhlIFp1c3RhbmQgcGF0dGVybjogcmVmIGlzIHNvdXJjZSBvZlxuICAvLyB0cnV0aCwgUmVhY3Qgc3RhdGUgaXMgdGhlIHJlbmRlciBwcm9qZWN0aW9uLiAgV2l0aG91dCB0aGlzLCBwYXRoc1xuICAvLyB0aGF0IHF1ZXVlIGZ1bmN0aW9uYWwgdXBkYXRlcnMgdGhlbiBzeW5jaHJvbm91c2x5IHJlYWQgdGhlIHJlZlxuICAvLyAoZS5nLiBoYW5kbGVTcGVjdWxhdGlvbkFjY2VwdCDihpIgb25RdWVyeSkgc2VlIHN0YWxlIGRhdGEuXG4gIGNvbnN0IHNldE1lc3NhZ2VzID0gdXNlQ2FsbGJhY2soXG4gICAgKGFjdGlvbjogUmVhY3QuU2V0U3RhdGVBY3Rpb248TWVzc2FnZVR5cGVbXT4pID0+IHtcbiAgICAgIGNvbnN0IHByZXYgPSBtZXNzYWdlc1JlZi5jdXJyZW50XG4gICAgICBjb25zdCBuZXh0ID1cbiAgICAgICAgdHlwZW9mIGFjdGlvbiA9PT0gJ2Z1bmN0aW9uJyA/IGFjdGlvbihtZXNzYWdlc1JlZi5jdXJyZW50KSA6IGFjdGlvblxuICAgICAgbWVzc2FnZXNSZWYuY3VycmVudCA9IG5leHRcbiAgICAgIGlmIChuZXh0Lmxlbmd0aCA8IHVzZXJJbnB1dEJhc2VsaW5lUmVmLmN1cnJlbnQpIHtcbiAgICAgICAgLy8gU2hyYW5rIChjb21wYWN0L3Jld2luZC9jbGVhcikg4oCUIGNsYW1wIHNvIHBsYWNlaG9sZGVyVGV4dCdzIGxlbmd0aFxuICAgICAgICAvLyBjaGVjayBjYW4ndCBnbyBzdGFsZS5cbiAgICAgICAgdXNlcklucHV0QmFzZWxpbmVSZWYuY3VycmVudCA9IDBcbiAgICAgIH0gZWxzZSBpZiAobmV4dC5sZW5ndGggPiBwcmV2Lmxlbmd0aCAmJiB1c2VyTWVzc2FnZVBlbmRpbmdSZWYuY3VycmVudCkge1xuICAgICAgICAvLyBHcmV3IHdoaWxlIHRoZSBzdWJtaXR0ZWQgdXNlciBtZXNzYWdlIGhhc24ndCBsYW5kZWQgeWV0LiBJZiB0aGVcbiAgICAgICAgLy8gYWRkZWQgbWVzc2FnZXMgZG9uJ3QgaW5jbHVkZSBpdCAoYnJpZGdlIHN0YXR1cywgaG9vayByZXN1bHRzLFxuICAgICAgICAvLyBzY2hlZHVsZWQgdGFza3MgbGFuZGluZyBhc3luYyBkdXJpbmcgcHJvY2Vzc1VzZXJJbnB1dEJhc2UpLCBidW1wXG4gICAgICAgIC8vIGJhc2VsaW5lIHNvIHRoZSBwbGFjZWhvbGRlciBzdGF5cyB2aXNpYmxlLiBPbmNlIHRoZSB1c2VyIG1lc3NhZ2VcbiAgICAgICAgLy8gbGFuZHMsIHN0b3AgdHJhY2tpbmcg4oCUIGxhdGVyIGFkZGl0aW9ucyAoYXNzaXN0YW50IHN0cmVhbSkgc2hvdWxkXG4gICAgICAgIC8vIG5vdCByZS1zaG93IHRoZSBwbGFjZWhvbGRlci5cbiAgICAgICAgY29uc3QgZGVsdGEgPSBuZXh0Lmxlbmd0aCAtIHByZXYubGVuZ3RoXG4gICAgICAgIGNvbnN0IGFkZGVkID1cbiAgICAgICAgICBwcmV2Lmxlbmd0aCA9PT0gMCB8fCBuZXh0WzBdID09PSBwcmV2WzBdXG4gICAgICAgICAgICA/IG5leHQuc2xpY2UoLWRlbHRhKVxuICAgICAgICAgICAgOiBuZXh0LnNsaWNlKDAsIGRlbHRhKVxuICAgICAgICBpZiAoYWRkZWQuc29tZShpc0h1bWFuVHVybikpIHtcbiAgICAgICAgICB1c2VyTWVzc2FnZVBlbmRpbmdSZWYuY3VycmVudCA9IGZhbHNlXG4gICAgICAgIH0gZWxzZSB7XG4gICAgICAgICAgdXNlcklucHV0QmFzZWxpbmVSZWYuY3VycmVudCA9IG5leHQubGVuZ3RoXG4gICAgICAgIH1cbiAgICAgIH1cbiAgICAgIHJhd1NldE1lc3NhZ2VzKG5leHQpXG4gICAgfSxcbiAgICBbXSxcbiAgKVxuICAvLyBDYXB0dXJlIHRoZSBiYXNlbGluZSBtZXNzYWdlIGNvdW50IGFsb25nc2lkZSB0aGUgcGxhY2Vob2xkZXIgdGV4dCBzb1xuICAvLyB0aGUgcmVuZGVyIGNhbiBoaWRlIGl0IG9uY2UgZGlzcGxheWVkTWVzc2FnZXMgZ3Jvd3MgcGFzdCB0aGUgYmFzZWxpbmUuXG4gIGNvbnN0IHNldFVzZXJJbnB1dE9uUHJvY2Vzc2luZyA9IHVzZUNhbGxiYWNrKChpbnB1dDogc3RyaW5nIHwgdW5kZWZpbmVkKSA9PiB7XG4gICAgaWYgKGlucHV0ICE9PSB1bmRlZmluZWQpIHtcbiAgICAgIHVzZXJJbnB1dEJhc2VsaW5lUmVmLmN1cnJlbnQgPSBtZXNzYWdlc1JlZi5jdXJyZW50Lmxlbmd0aFxuICAgICAgdXNlck1lc3NhZ2VQZW5kaW5nUmVmLmN1cnJlbnQgPSB0cnVlXG4gICAgfSBlbHNlIHtcbiAgICAgIHVzZXJNZXNzYWdlUGVuZGluZ1JlZi5jdXJyZW50ID0gZmFsc2VcbiAgICB9XG4gICAgc2V0VXNlcklucHV0T25Qcm9jZXNzaW5nUmF3KGlucHV0KVxuICB9LCBbXSlcbiAgLy8gRnVsbHNjcmVlbjogdHJhY2sgdGhlIHVuc2Vlbi1kaXZpZGVyIHBvc2l0aW9uLiBkaXZpZGVySW5kZXggY2hhbmdlc1xuICAvLyBvbmx5IH50d2ljZS9zY3JvbGwtc2Vzc2lvbiAoZmlyc3Qgc2Nyb2xsLWF3YXkgKyByZXBpbikuIHBpbGxWaXNpYmxlXG4gIC8vIGFuZCBzdGlja3lQcm9tcHQgbm93IGxpdmUgaW4gRnVsbHNjcmVlbkxheW91dCDigJQgdGhleSBzdWJzY3JpYmUgdG9cbiAgLy8gU2Nyb2xsQm94IGRpcmVjdGx5IHNvIHBlci1mcmFtZSBzY3JvbGwgbmV2ZXIgcmUtcmVuZGVycyBSRVBMLlxuICBjb25zdCB7XG4gICAgZGl2aWRlckluZGV4LFxuICAgIGRpdmlkZXJZUmVmLFxuICAgIG9uU2Nyb2xsQXdheSxcbiAgICBvblJlcGluLFxuICAgIGp1bXBUb05ldyxcbiAgICBzaGlmdERpdmlkZXIsXG4gIH0gPSB1c2VVbnNlZW5EaXZpZGVyKG1lc3NhZ2VzLmxlbmd0aClcbiAgaWYgKGZlYXR1cmUoJ0FXQVlfU1VNTUFSWScpKSB7XG4gICAgLy8gYmlvbWUtaWdub3JlIGxpbnQvY29ycmVjdG5lc3MvdXNlSG9va0F0VG9wTGV2ZWw6IGZlYXR1cmUoKSBpcyBhIGNvbXBpbGUtdGltZSBjb25zdGFudFxuICAgIHVzZUF3YXlTdW1tYXJ5KG1lc3NhZ2VzLCBzZXRNZXNzYWdlcywgaXNMb2FkaW5nKVxuICB9XG4gIGNvbnN0IFtjdXJzb3IsIHNldEN1cnNvcl0gPSB1c2VTdGF0ZTxNZXNzYWdlQWN0aW9uc1N0YXRlIHwgbnVsbD4obnVsbClcbiAgY29uc3QgY3Vyc29yTmF2UmVmID0gdXNlUmVmPE1lc3NhZ2VBY3Rpb25zTmF2IHwgbnVsbD4obnVsbClcbiAgLy8gTWVtb2l6ZWQgc28gTWVzc2FnZXMnIFJlYWN0Lm1lbW8gaG9sZHMuXG4gIGNvbnN0IHVuc2VlbkRpdmlkZXIgPSB1c2VNZW1vKFxuICAgICgpID0+IGNvbXB1dGVVbnNlZW5EaXZpZGVyKG1lc3NhZ2VzLCBkaXZpZGVySW5kZXgpLFxuICAgIC8vIGVzbGludC1kaXNhYmxlLW5leHQtbGluZSByZWFjdC1ob29rcy9leGhhdXN0aXZlLWRlcHMgLS0gbGVuZ3RoIGNoYW5nZSBjb3ZlcnMgYXBwZW5kczsgdXNlVW5zZWVuRGl2aWRlcidzIGNvdW50LWRyb3AgZ3VhcmQgY2xlYXJzIGRpdmlkZXJJbmRleCBvbiByZXBsYWNlL3Jld2luZFxuICAgIFtkaXZpZGVySW5kZXgsIG1lc3NhZ2VzLmxlbmd0aF0sXG4gIClcbiAgLy8gUmUtcGluIHNjcm9sbCB0byBib3R0b20gYW5kIGNsZWFyIHRoZSB1bnNlZW4tbWVzc2FnZXMgYmFzZWxpbmUuIENhbGxlZFxuICAvLyBvbiBhbnkgdXNlci1kcml2ZW4gcmV0dXJuLXRvLWxpdmUgYWN0aW9uIChzdWJtaXQsIHR5cGUtaW50by1lbXB0eSxcbiAgLy8gb3ZlcmxheSBhcHBlYXIvZGlzbWlzcykuXG4gIGNvbnN0IHJlcGluU2Nyb2xsID0gdXNlQ2FsbGJhY2soKCkgPT4ge1xuICAgIHNjcm9sbFJlZi5jdXJyZW50Py5zY3JvbGxUb0JvdHRvbSgpXG4gICAgb25SZXBpbigpXG4gICAgc2V0Q3Vyc29yKG51bGwpXG4gIH0sIFtvblJlcGluLCBzZXRDdXJzb3JdKVxuICAvLyBCYWNrc3RvcCBmb3IgdGhlIHN1Ym1pdC1oYW5kbGVyIHJlcGluIGF0IG9uU3VibWl0LiBJZiBhIGJ1ZmZlcmVkIHN0ZGluXG4gIC8vIGV2ZW50ICh3aGVlbC9kcmFnKSByYWNlcyBiZXR3ZWVuIGhhbmRsZXItZmlyZSBhbmQgc3RhdGUtY29tbWl0LCB0aGVcbiAgLy8gaGFuZGxlcidzIHNjcm9sbFRvQm90dG9tIGNhbiBiZSB1bmRvbmUuIFRoaXMgZWZmZWN0IGZpcmVzIG9uIHRoZSByZW5kZXJcbiAgLy8gd2hlcmUgdGhlIHVzZXIncyBtZXNzYWdlIGFjdHVhbGx5IGxhbmRzIOKAlCB0aWVkIHRvIFJlYWN0J3MgY29tbWl0IGN5Y2xlLFxuICAvLyBzbyBpdCBjYW4ndCByYWNlIHdpdGggc3RkaW4uIEtleWVkIG9uIGxhc3RNc2cgaWRlbnRpdHkgKG5vdCBtZXNzYWdlcy5sZW5ndGgpXG4gIC8vIHNvIHVzZUFzc2lzdGFudEhpc3RvcnkncyBwcmVwZW5kcyBkb24ndCBzcHVyaW91c2x5IHJlcGluLlxuICBjb25zdCBsYXN0TXNnID0gbWVzc2FnZXMuYXQoLTEpXG4gIGNvbnN0IGxhc3RNc2dJc0h1bWFuID0gbGFzdE1zZyAhPSBudWxsICYmIGlzSHVtYW5UdXJuKGxhc3RNc2cpXG4gIHVzZUVmZmVjdCgoKSA9PiB7XG4gICAgaWYgKGxhc3RNc2dJc0h1bWFuKSB7XG4gICAgICByZXBpblNjcm9sbCgpXG4gICAgfVxuICB9LCBbbGFzdE1zZ0lzSHVtYW4sIGxhc3RNc2csIHJlcGluU2Nyb2xsXSlcbiAgLy8gQXNzaXN0YW50LWNoYXQ6IGxhenktbG9hZCByZW1vdGUgaGlzdG9yeSBvbiBzY3JvbGwtdXAuIE5vLW9wIHVubGVzc1xuICAvLyBLQUlST1MgYnVpbGQgKyBjb25maWcudmlld2VyT25seS4gZmVhdHVyZSgpIGlzIGJ1aWxkLXRpbWUgY29uc3RhbnQgc29cbiAgLy8gdGhlIGJyYW5jaCBpcyBkZWFkLWNvZGUtZWxpbWluYXRlZCBpbiBub24tS0FJUk9TIGJ1aWxkcyAoc2FtZSBwYXR0ZXJuXG4gIC8vIGFzIHVzZVVuc2VlbkRpdmlkZXIgYWJvdmUpLlxuICBjb25zdCB7IG1heWJlTG9hZE9sZGVyIH0gPSBmZWF0dXJlKCdLQUlST1MnKVxuICAgID8gLy8gYmlvbWUtaWdub3JlIGxpbnQvY29ycmVjdG5lc3MvdXNlSG9va0F0VG9wTGV2ZWw6IGZlYXR1cmUoKSBpcyBhIGNvbXBpbGUtdGltZSBjb25zdGFudFxuICAgICAgdXNlQXNzaXN0YW50SGlzdG9yeSh7XG4gICAgICAgIGNvbmZpZzogcmVtb3RlU2Vzc2lvbkNvbmZpZyxcbiAgICAgICAgc2V0TWVzc2FnZXMsXG4gICAgICAgIHNjcm9sbFJlZixcbiAgICAgICAgb25QcmVwZW5kOiBzaGlmdERpdmlkZXIsXG4gICAgICB9KVxuICAgIDogSElTVE9SWV9TVFVCXG4gIC8vIENvbXBvc2UgdXNlVW5zZWVuRGl2aWRlcidzIGNhbGxiYWNrcyB3aXRoIHRoZSBsYXp5LWxvYWQgdHJpZ2dlci5cbiAgY29uc3QgY29tcG9zZWRPblNjcm9sbCA9IHVzZUNhbGxiYWNrKFxuICAgIChzdGlja3k6IGJvb2xlYW4sIGhhbmRsZTogU2Nyb2xsQm94SGFuZGxlKSA9PiB7XG4gICAgICBsYXN0VXNlclNjcm9sbFRzUmVmLmN1cnJlbnQgPSBEYXRlLm5vdygpXG4gICAgICBpZiAoc3RpY2t5KSB7XG4gICAgICAgIG9uUmVwaW4oKVxuICAgICAgfSBlbHNlIHtcbiAgICAgICAgb25TY3JvbGxBd2F5KGhhbmRsZSlcbiAgICAgICAgaWYgKGZlYXR1cmUoJ0tBSVJPUycpKSBtYXliZUxvYWRPbGRlcihoYW5kbGUpXG4gICAgICAgIC8vIERpc21pc3MgdGhlIGNvbXBhbmlvbiBidWJibGUgb24gc2Nyb2xsIOKAlCBpdCdzIGFic29sdXRlLXBvc2l0aW9uZWRcbiAgICAgICAgLy8gYXQgYm90dG9tLXJpZ2h0IGFuZCBjb3ZlcnMgdHJhbnNjcmlwdCBjb250ZW50LiBTY3JvbGxpbmcgPSB1c2VyIGlzXG4gICAgICAgIC8vIHRyeWluZyB0byByZWFkIHNvbWV0aGluZyB1bmRlciBpdC5cbiAgICAgICAgaWYgKGZlYXR1cmUoJ0JVRERZJykpIHtcbiAgICAgICAgICBzZXRBcHBTdGF0ZShwcmV2ID0+XG4gICAgICAgICAgICBwcmV2LmNvbXBhbmlvblJlYWN0aW9uID09PSB1bmRlZmluZWRcbiAgICAgICAgICAgICAgPyBwcmV2XG4gICAgICAgICAgICAgIDogeyAuLi5wcmV2LCBjb21wYW5pb25SZWFjdGlvbjogdW5kZWZpbmVkIH0sXG4gICAgICAgICAgKVxuICAgICAgICB9XG4gICAgICB9XG4gICAgfSxcbiAgICBbb25SZXBpbiwgb25TY3JvbGxBd2F5LCBtYXliZUxvYWRPbGRlciwgc2V0QXBwU3RhdGVdLFxuICApXG4gIC8vIERlZmVycmVkIFNlc3Npb25TdGFydCBob29rIG1lc3NhZ2VzIOKAlCBSRVBMIHJlbmRlcnMgaW1tZWRpYXRlbHkgYW5kXG4gIC8vIGhvb2sgbWVzc2FnZXMgYXJlIGluamVjdGVkIHdoZW4gdGhleSByZXNvbHZlLiBhd2FpdFBlbmRpbmdIb29rcygpXG4gIC8vIG11c3QgYmUgY2FsbGVkIGJlZm9yZSB0aGUgZmlyc3QgQVBJIGNhbGwgc28gdGhlIG1vZGVsIHNlZXMgaG9vayBjb250ZXh0LlxuICBjb25zdCBhd2FpdFBlbmRpbmdIb29rcyA9IHVzZURlZmVycmVkSG9va01lc3NhZ2VzKFxuICAgIHBlbmRpbmdIb29rTWVzc2FnZXMsXG4gICAgc2V0TWVzc2FnZXMsXG4gIClcblxuICAvLyBEZWZlcnJlZCBtZXNzYWdlcyBmb3IgdGhlIE1lc3NhZ2VzIGNvbXBvbmVudCDigJQgcmVuZGVycyBhdCB0cmFuc2l0aW9uXG4gIC8vIHByaW9yaXR5IHNvIHRoZSByZWNvbmNpbGVyIHlpZWxkcyBldmVyeSA1bXMsIGtlZXBpbmcgaW5wdXQgcmVzcG9uc2l2ZVxuICAvLyB3aGlsZSB0aGUgZXhwZW5zaXZlIG1lc3NhZ2UgcHJvY2Vzc2luZyBwaXBlbGluZSBydW5zLlxuICBjb25zdCBkZWZlcnJlZE1lc3NhZ2VzID0gdXNlRGVmZXJyZWRWYWx1ZShtZXNzYWdlcylcbiAgY29uc3QgZGVmZXJyZWRCZWhpbmQgPSBtZXNzYWdlcy5sZW5ndGggLSBkZWZlcnJlZE1lc3NhZ2VzLmxlbmd0aFxuICBpZiAoZGVmZXJyZWRCZWhpbmQgPiAwKSB7XG4gICAgbG9nRm9yRGVidWdnaW5nKFxuICAgICAgYFt1c2VEZWZlcnJlZFZhbHVlXSBNZXNzYWdlcyBkZWZlcnJlZCBieSAke2RlZmVycmVkQmVoaW5kfSAoJHtkZWZlcnJlZE1lc3NhZ2VzLmxlbmd0aH3ihpIke21lc3NhZ2VzLmxlbmd0aH0pYCxcbiAgICApXG4gIH1cblxuICAvLyBGcm96ZW4gc3RhdGUgZm9yIHRyYW5zY3JpcHQgbW9kZSAtIHN0b3JlcyBsZW5ndGhzIGluc3RlYWQgb2YgY2xvbmluZyBhcnJheXMgZm9yIG1lbW9yeSBlZmZpY2llbmN5XG4gIGNvbnN0IFtmcm96ZW5UcmFuc2NyaXB0U3RhdGUsIHNldEZyb3plblRyYW5zY3JpcHRTdGF0ZV0gPSB1c2VTdGF0ZTx7XG4gICAgbWVzc2FnZXNMZW5ndGg6IG51bWJlclxuICAgIHN0cmVhbWluZ1Rvb2xVc2VzTGVuZ3RoOiBudW1iZXJcbiAgfSB8IG51bGw+KG51bGwpXG4gIC8vIEluaXRpYWxpemUgaW5wdXQgd2l0aCBhbnkgZWFybHkgaW5wdXQgdGhhdCB3YXMgY2FwdHVyZWQgYmVmb3JlIFJFUEwgd2FzIHJlYWR5LlxuICAvLyBVc2luZyBsYXp5IGluaXRpYWxpemF0aW9uIGVuc3VyZXMgY3Vyc29yIG9mZnNldCBpcyBzZXQgY29ycmVjdGx5IGluIFByb21wdElucHV0LlxuICBjb25zdCBbaW5wdXRWYWx1ZSwgc2V0SW5wdXRWYWx1ZVJhd10gPSB1c2VTdGF0ZSgoKSA9PiBjb25zdW1lRWFybHlJbnB1dCgpKVxuICBjb25zdCBpbnB1dFZhbHVlUmVmID0gdXNlUmVmKGlucHV0VmFsdWUpXG4gIGlucHV0VmFsdWVSZWYuY3VycmVudCA9IGlucHV0VmFsdWVcbiAgY29uc3QgaW5zZXJ0VGV4dFJlZiA9IHVzZVJlZjx7XG4gICAgaW5zZXJ0OiAodGV4dDogc3RyaW5nKSA9PiB2b2lkXG4gICAgc2V0SW5wdXRXaXRoQ3Vyc29yOiAodmFsdWU6IHN0cmluZywgY3Vyc29yOiBudW1iZXIpID0+IHZvaWRcbiAgICBjdXJzb3JPZmZzZXQ6IG51bWJlclxuICB9IHwgbnVsbD4obnVsbClcblxuICAvLyBXcmFwIHNldElucHV0VmFsdWUgdG8gY28tbG9jYXRlIHN1cHByZXNzaW9uIHN0YXRlIHVwZGF0ZXMuXG4gIC8vIEJvdGggc2V0U3RhdGUgY2FsbHMgaGFwcGVuIGluIHRoZSBzYW1lIHN5bmNocm9ub3VzIGNvbnRleHQgc28gUmVhY3RcbiAgLy8gYmF0Y2hlcyB0aGVtIGludG8gYSBzaW5nbGUgcmVuZGVyLCBlbGltaW5hdGluZyB0aGUgZXh0cmEgcmVuZGVyIHRoYXRcbiAgLy8gdGhlIHByZXZpb3VzIHVzZUVmZmVjdCDihpIgc2V0U3RhdGUgcGF0dGVybiBjYXVzZWQuXG4gIGNvbnN0IHNldElucHV0VmFsdWUgPSB1c2VDYWxsYmFjayhcbiAgICAodmFsdWU6IHN0cmluZykgPT4ge1xuICAgICAgaWYgKHRyeVN1Z2dlc3RCZ1BSSW50ZXJjZXB0KGlucHV0VmFsdWVSZWYuY3VycmVudCwgdmFsdWUpKSByZXR1cm5cbiAgICAgIC8vIEluIGZ1bGxzY3JlZW4gbW9kZSwgdHlwaW5nIGludG8gYW4gZW1wdHkgcHJvbXB0IHJlLXBpbnMgc2Nyb2xsIHRvXG4gICAgICAvLyBib3R0b20uIE9ubHkgZmlyZXMgb24gZW1wdHnihpJub24tZW1wdHkgc28gc2Nyb2xsaW5nIHVwIHRvIHJlZmVyZW5jZVxuICAgICAgLy8gc29tZXRoaW5nIHdoaWxlIGNvbXBvc2luZyBhIG1lc3NhZ2UgZG9lc24ndCB5YW5rIHRoZSB2aWV3IGJhY2sgb25cbiAgICAgIC8vIGV2ZXJ5IGtleXN0cm9rZS4gUmVzdG9yZXMgdGhlIHByZS1mdWxsc2NyZWVuIG11c2NsZSBtZW1vcnkgb2ZcbiAgICAgIC8vIHR5cGluZyB0byBzbmFwIGJhY2sgdG8gdGhlIGVuZCBvZiB0aGUgY29udmVyc2F0aW9uLlxuICAgICAgLy8gU2tpcHBlZCBpZiB0aGUgdXNlciBzY3JvbGxlZCB3aXRoaW4gdGhlIGxhc3QgM3Mg4oCUIHRoZXkncmUgYWN0aXZlbHlcbiAgICAgIC8vIHJlYWRpbmcsIG5vdCBsb3N0LiBsYXN0VXNlclNjcm9sbFRzUmVmIHN0YXJ0cyBhdCAwIHNvIHRoZSBmaXJzdC1cbiAgICAgIC8vIGV2ZXIga2V5cHJlc3MgKG5vIHNjcm9sbCB5ZXQpIGFsd2F5cyByZXBpbnMuXG4gICAgICBpZiAoXG4gICAgICAgIGlucHV0VmFsdWVSZWYuY3VycmVudCA9PT0gJycgJiZcbiAgICAgICAgdmFsdWUgIT09ICcnICYmXG4gICAgICAgIERhdGUubm93KCkgLSBsYXN0VXNlclNjcm9sbFRzUmVmLmN1cnJlbnQgPj1cbiAgICAgICAgICBSRUNFTlRfU0NST0xMX1JFUElOX1dJTkRPV19NU1xuICAgICAgKSB7XG4gICAgICAgIHJlcGluU2Nyb2xsKClcbiAgICAgIH1cbiAgICAgIC8vIFN5bmMgcmVmIGltbWVkaWF0ZWx5IChsaWtlIHNldE1lc3NhZ2VzKSBzbyBjYWxsZXJzIHRoYXQgcmVhZFxuICAgICAgLy8gaW5wdXRWYWx1ZVJlZiBiZWZvcmUgUmVhY3QgY29tbWl0cyDigJQgZS5nLiB0aGUgYXV0by1yZXN0b3JlIGZpbmFsbHlcbiAgICAgIC8vIGJsb2NrJ3MgYD09PSAnJ2AgZ3VhcmQg4oCUIHNlZSB0aGUgZnJlc2ggdmFsdWUsIG5vdCB0aGUgc3RhbGUgcmVuZGVyLlxuICAgICAgaW5wdXRWYWx1ZVJlZi5jdXJyZW50ID0gdmFsdWVcbiAgICAgIHNldElucHV0VmFsdWVSYXcodmFsdWUpXG4gICAgICBzZXRJc1Byb21wdElucHV0QWN0aXZlKHZhbHVlLnRyaW0oKS5sZW5ndGggPiAwKVxuICAgIH0sXG4gICAgW3NldElzUHJvbXB0SW5wdXRBY3RpdmUsIHJlcGluU2Nyb2xsLCB0cnlTdWdnZXN0QmdQUkludGVyY2VwdF0sXG4gIClcblxuICAvLyBTY2hlZHVsZSBhIHRpbWVvdXQgdG8gc3RvcCBzdXBwcmVzc2luZyBkaWFsb2dzIGFmdGVyIHRoZSB1c2VyIHN0b3BzIHR5cGluZy5cbiAgLy8gT25seSBtYW5hZ2VzIHRoZSB0aW1lb3V0IOKAlCB0aGUgaW1tZWRpYXRlIGFjdGl2YXRpb24gaXMgaGFuZGxlZCBieSBzZXRJbnB1dFZhbHVlIGFib3ZlLlxuICB1c2VFZmZlY3QoKCkgPT4ge1xuICAgIGlmIChpbnB1dFZhbHVlLnRyaW0oKS5sZW5ndGggPT09IDApIHJldHVyblxuICAgIGNvbnN0IHRpbWVyID0gc2V0VGltZW91dChcbiAgICAgIHNldElzUHJvbXB0SW5wdXRBY3RpdmUsXG4gICAgICBQUk9NUFRfU1VQUFJFU1NJT05fTVMsXG4gICAgICBmYWxzZSxcbiAgICApXG4gICAgcmV0dXJuICgpID0+IGNsZWFyVGltZW91dCh0aW1lcilcbiAgfSwgW2lucHV0VmFsdWVdKVxuXG4gIGNvbnN0IFtpbnB1dE1vZGUsIHNldElucHV0TW9kZV0gPSB1c2VTdGF0ZTxQcm9tcHRJbnB1dE1vZGU+KCdwcm9tcHQnKVxuICBjb25zdCBbc3Rhc2hlZFByb21wdCwgc2V0U3Rhc2hlZFByb21wdF0gPSB1c2VTdGF0ZTxcbiAgICB8IHtcbiAgICAgICAgdGV4dDogc3RyaW5nXG4gICAgICAgIGN1cnNvck9mZnNldDogbnVtYmVyXG4gICAgICAgIHBhc3RlZENvbnRlbnRzOiBSZWNvcmQ8bnVtYmVyLCBQYXN0ZWRDb250ZW50PlxuICAgICAgfVxuICAgIHwgdW5kZWZpbmVkXG4gID4oKV