mirror of
http://10.0.2.1:3031/sauer/claude-code.git
synced 2026-06-30 23:36:57 +10:00
328 lines
52 KiB
TypeScript
328 lines
52 KiB
TypeScript
|
|
import figures from 'figures';
|
||
|
|
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||
|
|
import { useTerminalSize } from '../../../hooks/useTerminalSize.js';
|
||
|
|
import type { KeyboardEvent } from '../../../ink/events/keyboard-event.js';
|
||
|
|
import { Box, Text } from '../../../ink.js';
|
||
|
|
import { useKeybinding, useKeybindings } from '../../../keybindings/useKeybinding.js';
|
||
|
|
import { useAppState } from '../../../state/AppState.js';
|
||
|
|
import type { Question } from '../../../tools/AskUserQuestionTool/AskUserQuestionTool.js';
|
||
|
|
import { getExternalEditor } from '../../../utils/editor.js';
|
||
|
|
import { toIDEDisplayName } from '../../../utils/ide.js';
|
||
|
|
import { editPromptInEditor } from '../../../utils/promptEditor.js';
|
||
|
|
import { Divider } from '../../design-system/Divider.js';
|
||
|
|
import TextInput from '../../TextInput.js';
|
||
|
|
import { PermissionRequestTitle } from '../PermissionRequestTitle.js';
|
||
|
|
import { PreviewBox } from './PreviewBox.js';
|
||
|
|
import { QuestionNavigationBar } from './QuestionNavigationBar.js';
|
||
|
|
import type { QuestionState } from './use-multiple-choice-state.js';
|
||
|
|
type Props = {
|
||
|
|
question: Question;
|
||
|
|
questions: Question[];
|
||
|
|
currentQuestionIndex: number;
|
||
|
|
answers: Record<string, string>;
|
||
|
|
questionStates: Record<string, QuestionState>;
|
||
|
|
hideSubmitTab?: boolean;
|
||
|
|
minContentHeight?: number;
|
||
|
|
minContentWidth?: number;
|
||
|
|
onUpdateQuestionState: (questionText: string, updates: Partial<QuestionState>, isMultiSelect: boolean) => void;
|
||
|
|
onAnswer: (questionText: string, label: string | string[], textInput?: string, shouldAdvance?: boolean) => void;
|
||
|
|
onTextInputFocus: (isInInput: boolean) => void;
|
||
|
|
onCancel: () => void;
|
||
|
|
onTabPrev?: () => void;
|
||
|
|
onTabNext?: () => void;
|
||
|
|
onRespondToClaude: () => void;
|
||
|
|
onFinishPlanInterview: () => void;
|
||
|
|
};
|
||
|
|
|
||
|
|
/**
|
||
|
|
* A side-by-side question view for questions with preview content.
|
||
|
|
* Displays a vertical option list on the left with a preview panel on the right.
|
||
|
|
*/
|
||
|
|
export function PreviewQuestionView({
|
||
|
|
question,
|
||
|
|
questions,
|
||
|
|
currentQuestionIndex,
|
||
|
|
answers,
|
||
|
|
questionStates,
|
||
|
|
hideSubmitTab = false,
|
||
|
|
minContentHeight,
|
||
|
|
minContentWidth,
|
||
|
|
onUpdateQuestionState,
|
||
|
|
onAnswer,
|
||
|
|
onTextInputFocus,
|
||
|
|
onCancel,
|
||
|
|
onTabPrev,
|
||
|
|
onTabNext,
|
||
|
|
onRespondToClaude,
|
||
|
|
onFinishPlanInterview
|
||
|
|
}: Props): React.ReactNode {
|
||
|
|
const isInPlanMode = useAppState(s => s.toolPermissionContext.mode) === 'plan';
|
||
|
|
const [isFooterFocused, setIsFooterFocused] = useState(false);
|
||
|
|
const [footerIndex, setFooterIndex] = useState(0);
|
||
|
|
const [isInNotesInput, setIsInNotesInput] = useState(false);
|
||
|
|
const [cursorOffset, setCursorOffset] = useState(0);
|
||
|
|
const editor = getExternalEditor();
|
||
|
|
const editorName = editor ? toIDEDisplayName(editor) : null;
|
||
|
|
const questionText = question.question;
|
||
|
|
const questionState = questionStates[questionText];
|
||
|
|
|
||
|
|
// Only real options — no "Other" for preview questions
|
||
|
|
const allOptions = question.options;
|
||
|
|
|
||
|
|
// Track which option is focused (for preview display)
|
||
|
|
const [focusedIndex, setFocusedIndex] = useState(0);
|
||
|
|
|
||
|
|
// Reset focusedIndex when navigating to a different question
|
||
|
|
const prevQuestionText = useRef(questionText);
|
||
|
|
if (prevQuestionText.current !== questionText) {
|
||
|
|
prevQuestionText.current = questionText;
|
||
|
|
const selected = questionState?.selectedValue as string | undefined;
|
||
|
|
const idx = selected ? allOptions.findIndex(opt => opt.label === selected) : -1;
|
||
|
|
setFocusedIndex(idx >= 0 ? idx : 0);
|
||
|
|
}
|
||
|
|
const focusedOption = allOptions[focusedIndex];
|
||
|
|
const selectedValue = questionState?.selectedValue as string | undefined;
|
||
|
|
const notesValue = questionState?.textInputValue || '';
|
||
|
|
const handleSelectOption = useCallback((index: number) => {
|
||
|
|
const option = allOptions[index];
|
||
|
|
if (!option) return;
|
||
|
|
setFocusedIndex(index);
|
||
|
|
onUpdateQuestionState(questionText, {
|
||
|
|
selectedValue: option.label
|
||
|
|
}, false);
|
||
|
|
onAnswer(questionText, option.label);
|
||
|
|
}, [allOptions, questionText, onUpdateQuestionState, onAnswer]);
|
||
|
|
const handleNavigate = useCallback((direction: 'up' | 'down' | number) => {
|
||
|
|
if (isInNotesInput) return;
|
||
|
|
let newIndex: number;
|
||
|
|
if (typeof direction === 'number') {
|
||
|
|
newIndex = direction;
|
||
|
|
} else if (direction === 'up') {
|
||
|
|
newIndex = focusedIndex > 0 ? focusedIndex - 1 : focusedIndex;
|
||
|
|
} else {
|
||
|
|
newIndex = focusedIndex < allOptions.length - 1 ? focusedIndex + 1 : focusedIndex;
|
||
|
|
}
|
||
|
|
if (newIndex >= 0 && newIndex < allOptions.length) {
|
||
|
|
setFocusedIndex(newIndex);
|
||
|
|
}
|
||
|
|
}, [focusedIndex, allOptions.length, isInNotesInput]);
|
||
|
|
|
||
|
|
// Handle ctrl+g to open external editor for notes
|
||
|
|
useKeybinding('chat:externalEditor', async () => {
|
||
|
|
const currentValue = questionState?.textInputValue || '';
|
||
|
|
const result = await editPromptInEditor(currentValue);
|
||
|
|
if (result.content !== null && result.content !== currentValue) {
|
||
|
|
onUpdateQuestionState(questionText, {
|
||
|
|
textInputValue: result.content
|
||
|
|
}, false);
|
||
|
|
}
|
||
|
|
}, {
|
||
|
|
context: 'Chat',
|
||
|
|
isActive: isInNotesInput && !!editor
|
||
|
|
});
|
||
|
|
|
||
|
|
// Handle left/right arrow and tab for question navigation.
|
||
|
|
// This must be in the child component (not just the parent) because child useInput
|
||
|
|
// handlers register first on the event emitter and fire before parent handlers.
|
||
|
|
// Without this, the parent's useKeybindings may not fire reliably depending on
|
||
|
|
// listener ordering in the event emitter.
|
||
|
|
useKeybindings({
|
||
|
|
'tabs:previous': () => onTabPrev?.(),
|
||
|
|
'tabs:next': () => onTabNext?.()
|
||
|
|
}, {
|
||
|
|
context: 'Tabs',
|
||
|
|
isActive: !isInNotesInput && !isFooterFocused
|
||
|
|
});
|
||
|
|
|
||
|
|
// Re-submit the answer (plain label) when exiting notes input.
|
||
|
|
// Notes are stored in questionStates and collected at submit time via annotations.
|
||
|
|
const handleNotesExit = useCallback(() => {
|
||
|
|
setIsInNotesInput(false);
|
||
|
|
onTextInputFocus(false);
|
||
|
|
if (selectedValue) {
|
||
|
|
onAnswer(questionText, selectedValue);
|
||
|
|
}
|
||
|
|
}, [selectedValue, questionText, onAnswer, onTextInputFocus]);
|
||
|
|
const handleDownFromPreview = useCallback(() => {
|
||
|
|
setIsFooterFocused(true);
|
||
|
|
}, []);
|
||
|
|
const handleUpFromFooter = useCallback(() => {
|
||
|
|
setIsFooterFocused(false);
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
// Handle keyboard input for option/footer/notes navigation.
|
||
|
|
// Always active — the handler routes internally based on isFooterFocused/isInNotesInput.
|
||
|
|
const handleKeyDown = useCallback((e: KeyboardEvent) => {
|
||
|
|
if (isFooterFocused) {
|
||
|
|
if (e.key === 'up' || e.ctrl && e.key === 'p') {
|
||
|
|
e.preventDefault();
|
||
|
|
if (footerIndex === 0) {
|
||
|
|
handleUpFromFooter();
|
||
|
|
} else {
|
||
|
|
setFooterIndex(0);
|
||
|
|
}
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (e.key === 'down' || e.ctrl && e.key === 'n') {
|
||
|
|
e.preventDefault();
|
||
|
|
if (isInPlanMode && footerIndex === 0) {
|
||
|
|
setFooterIndex(1);
|
||
|
|
}
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (e.key === 'return') {
|
||
|
|
e.preventDefault();
|
||
|
|
if (footerIndex === 0) {
|
||
|
|
onRespondToClaude();
|
||
|
|
} else {
|
||
|
|
onFinishPlanInterview();
|
||
|
|
}
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (e.key === 'escape') {
|
||
|
|
e.preventDefault();
|
||
|
|
onCancel();
|
||
|
|
}
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (isInNotesInput) {
|
||
|
|
// In notes input mode, handle escape to exit back to option navigation
|
||
|
|
if (e.key === 'escape') {
|
||
|
|
e.preventDefault();
|
||
|
|
handleNotesExit();
|
||
|
|
}
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Handle option navigation (vertical)
|
||
|
|
if (e.key === 'up' || e.ctrl && e.key === 'p') {
|
||
|
|
e.preventDefault();
|
||
|
|
if (focusedIndex > 0) {
|
||
|
|
handleNavigate('up');
|
||
|
|
}
|
||
|
|
} else if (e.key === 'down' || e.ctrl && e.key === 'n') {
|
||
|
|
e.preventDefault();
|
||
|
|
if (focusedIndex === allOptions.length - 1) {
|
||
|
|
// At bottom of options, go to footer
|
||
|
|
handleDownFromPreview();
|
||
|
|
} else {
|
||
|
|
handleNavigate('down');
|
||
|
|
}
|
||
|
|
} else if (e.key === 'return') {
|
||
|
|
e.preventDefault();
|
||
|
|
handleSelectOption(focusedIndex);
|
||
|
|
} else if (e.key === 'n' && !e.ctrl && !e.meta) {
|
||
|
|
// Press 'n' to focus the notes input
|
||
|
|
e.preventDefault();
|
||
|
|
setIsInNotesInput(true);
|
||
|
|
onTextInputFocus(true);
|
||
|
|
} else if (e.key === 'escape') {
|
||
|
|
e.preventDefault();
|
||
|
|
onCancel();
|
||
|
|
} else if (e.key.length === 1 && e.key >= '1' && e.key <= '9') {
|
||
|
|
e.preventDefault();
|
||
|
|
const idx_0 = parseInt(e.key, 10) - 1;
|
||
|
|
if (idx_0 < allOptions.length) {
|
||
|
|
handleNavigate(idx_0);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}, [isFooterFocused, footerIndex, isInPlanMode, isInNotesInput, focusedIndex, allOptions.length, handleUpFromFooter, handleDownFromPreview, handleNavigate, handleSelectOption, handleNotesExit, onRespondToClaude, onFinishPlanInterview, onCancel, onTextInputFocus]);
|
||
|
|
const previewContent = focusedOption?.preview || null;
|
||
|
|
|
||
|
|
// The right panel's available width is terminal minus the left panel and gap.
|
||
|
|
const LEFT_PANEL_WIDTH = 30;
|
||
|
|
const GAP = 4;
|
||
|
|
const {
|
||
|
|
columns
|
||
|
|
} = useTerminalSize();
|
||
|
|
const previewMaxWidth = columns - LEFT_PANEL_WIDTH - GAP;
|
||
|
|
|
||
|
|
// Lines used within the content area that aren't preview content:
|
||
|
|
// 1: marginTop on side-by-side box
|
||
|
|
// 2: PreviewBox borders (top + bottom)
|
||
|
|
// 2: notes section (marginTop=1 + text)
|
||
|
|
// 2: footer section (marginTop=1 + divider)
|
||
|
|
// 1: "Chat about this" line
|
||
|
|
// 1: plan mode line (may or may not show)
|
||
|
|
// 2: help text (marginTop=1 + text)
|
||
|
|
const PREVIEW_OVERHEAD = 11;
|
||
|
|
|
||
|
|
// Compute the max lines available for preview content from the parent's
|
||
|
|
// height budget to prevent terminal overflow. We do NOT pad shorter options
|
||
|
|
// to match the tallest — the outer box's minHeight handles cross-question
|
||
|
|
// layout consistency, and within-question shifts are acceptable.
|
||
|
|
const previewMaxLines = useMemo(() => {
|
||
|
|
return minContentHeight ? Math.max(1, minContentHeight - PREVIEW_OVERHEAD) : undefined;
|
||
|
|
}, [minContentHeight]);
|
||
|
|
return <Box flexDirection="column" marginTop={1} tabIndex={0} autoFocus onKeyDown={handleKeyDown}>
|
||
|
|
<Divider color="inactive" />
|
||
|
|
<Box flexDirection="column" paddingTop={0}>
|
||
|
|
<QuestionNavigationBar questions={questions} currentQuestionIndex={currentQuestionIndex} answers={answers} hideSubmitTab={hideSubmitTab} />
|
||
|
|
<PermissionRequestTitle title={question.question} color={'text'} />
|
||
|
|
|
||
|
|
<Box flexDirection="column" minHeight={minContentHeight}>
|
||
|
|
{/* Side-by-side layout: options on left, preview on right */}
|
||
|
|
<Box marginTop={1} flexDirection="row" gap={4}>
|
||
|
|
{/* Left panel: vertical option list */}
|
||
|
|
<Box flexDirection="column" width={30}>
|
||
|
|
{allOptions.map((option_0, index_0) => {
|
||
|
|
const isFocused = focusedIndex === index_0;
|
||
|
|
const isSelected = selectedValue === option_0.label;
|
||
|
|
return <Box key={option_0.label} flexDirection="row">
|
||
|
|
{isFocused ? <Text color="suggestion">{figures.pointer}</Text> : <Text> </Text>}
|
||
|
|
<Text dimColor> {index_0 + 1}.</Text>
|
||
|
|
<Text color={isSelected ? 'success' : isFocused ? 'suggestion' : undefined} bold={isFocused}>
|
||
|
|
{' '}
|
||
|
|
{option_0.label}
|
||
|
|
</Text>
|
||
|
|
{isSelected && <Text color="success"> {figures.tick}</Text>}
|
||
|
|
</Box>;
|
||
|
|
})}
|
||
|
|
</Box>
|
||
|
|
|
||
|
|
{/* Right panel: preview + notes */}
|
||
|
|
<Box flexDirection="column" flexGrow={1}>
|
||
|
|
<PreviewBox content={previewContent || 'No preview available'} maxLines={previewMaxLines} minWidth={minContentWidth} maxWidth={previewMaxWidth} />
|
||
|
|
<Box marginTop={1} flexDirection="row" gap={1}>
|
||
|
|
<Text color="suggestion">Notes:</Text>
|
||
|
|
{isInNotesInput ? <TextInput value={notesValue} placeholder="Add notes on this design…" onChange={value => {
|
||
|
|
onUpdateQuestionState(questionText, {
|
||
|
|
textInputValue: value
|
||
|
|
}, false);
|
||
|
|
}} onSubmit={handleNotesExit} onExit={handleNotesExit} focus={true} showCursor={true} columns={60} cursorOffset={cursorOffset} onChangeCursorOffset={setCursorOffset} /> : <Text dimColor italic>
|
||
|
|
{notesValue || 'press n to add notes'}
|
||
|
|
</Text>}
|
||
|
|
</Box>
|
||
|
|
</Box>
|
||
|
|
</Box>
|
||
|
|
|
||
|
|
{/* Footer section */}
|
||
|
|
<Box flexDirection="column" marginTop={1}>
|
||
|
|
<Divider color="inactive" />
|
||
|
|
<Box flexDirection="row" gap={1}>
|
||
|
|
{isFooterFocused && footerIndex === 0 ? <Text color="suggestion">{figures.pointer}</Text> : <Text> </Text>}
|
||
|
|
<Text color={isFooterFocused && footerIndex === 0 ? 'suggestion' : undefined}>
|
||
|
|
Chat about this
|
||
|
|
</Text>
|
||
|
|
</Box>
|
||
|
|
{isInPlanMode && <Box flexDirection="row" gap={1}>
|
||
|
|
{isFooterFocused && footerIndex === 1 ? <Text color="suggestion">{figures.pointer}</Text> : <Text> </Text>}
|
||
|
|
<Text color={isFooterFocused && footerIndex === 1 ? 'suggestion' : undefined}>
|
||
|
|
Skip interview and plan immediately
|
||
|
|
</Text>
|
||
|
|
</Box>}
|
||
|
|
</Box>
|
||
|
|
<Box marginTop={1}>
|
||
|
|
<Text color="inactive" dimColor>
|
||
|
|
Enter to select · {figures.arrowUp}/{figures.arrowDown} to
|
||
|
|
navigate · n to add notes
|
||
|
|
{questions.length > 1 && <> · Tab to switch questions</>}
|
||
|
|
{isInNotesInput && editorName && <> · ctrl+g to edit in {editorName}</>}{' '}
|
||
|
|
· Esc to cancel
|
||
|
|
</Text>
|
||
|
|
</Box>
|
||
|
|
</Box>
|
||
|
|
</Box>
|
||
|
|
</Box>;
|
||
|
|
}
|
||
|
|
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJmaWd1cmVzIiwiUmVhY3QiLCJ1c2VDYWxsYmFjayIsInVzZU1lbW8iLCJ1c2VSZWYiLCJ1c2VTdGF0ZSIsInVzZVRlcm1pbmFsU2l6ZSIsIktleWJvYXJkRXZlbnQiLCJCb3giLCJUZXh0IiwidXNlS2V5YmluZGluZyIsInVzZUtleWJpbmRpbmdzIiwidXNlQXBwU3RhdGUiLCJRdWVzdGlvbiIsImdldEV4dGVybmFsRWRpdG9yIiwidG9JREVEaXNwbGF5TmFtZSIsImVkaXRQcm9tcHRJbkVkaXRvciIsIkRpdmlkZXIiLCJUZXh0SW5wdXQiLCJQZXJtaXNzaW9uUmVxdWVzdFRpdGxlIiwiUHJldmlld0JveCIsIlF1ZXN0aW9uTmF2aWdhdGlvbkJhciIsIlF1ZXN0aW9uU3RhdGUiLCJQcm9wcyIsInF1ZXN0aW9uIiwicXVlc3Rpb25zIiwiY3VycmVudFF1ZXN0aW9uSW5kZXgiLCJhbnN3ZXJzIiwiUmVjb3JkIiwicXVlc3Rpb25TdGF0ZXMiLCJoaWRlU3VibWl0VGFiIiwibWluQ29udGVudEhlaWdodCIsIm1pbkNvbnRlbnRXaWR0aCIsIm9uVXBkYXRlUXVlc3Rpb25TdGF0ZSIsInF1ZXN0aW9uVGV4dCIsInVwZGF0ZXMiLCJQYXJ0aWFsIiwiaXNNdWx0aVNlbGVjdCIsIm9uQW5zd2VyIiwibGFiZWwiLCJ0ZXh0SW5wdXQiLCJzaG91bGRBZHZhbmNlIiwib25UZXh0SW5wdXRGb2N1cyIsImlzSW5JbnB1dCIsIm9uQ2FuY2VsIiwib25UYWJQcmV2Iiwib25UYWJOZXh0Iiwib25SZXNwb25kVG9DbGF1ZGUiLCJvbkZpbmlzaFBsYW5JbnRlcnZpZXciLCJQcmV2aWV3UXVlc3Rpb25WaWV3IiwiUmVhY3ROb2RlIiwiaXNJblBsYW5Nb2RlIiwicyIsInRvb2xQZXJtaXNzaW9uQ29udGV4dCIsIm1vZGUiLCJpc0Zvb3RlckZvY3VzZWQiLCJzZXRJc0Zvb3RlckZvY3VzZWQiLCJmb290ZXJJbmRleCIsInNldEZvb3RlckluZGV4IiwiaXNJbk5vdGVzSW5wdXQiLCJzZXRJc0luTm90ZXNJbnB1dCIsImN1cnNvck9mZnNldCIsInNldEN1cnNvck9mZnNldCIsImVkaXRvciIsImVkaXRvck5hbWUiLCJxdWVzdGlvblN0YXRlIiwiYWxsT3B0aW9ucyIsIm9wdGlvbnMiLCJmb2N1c2VkSW5kZXgiLCJzZXRGb2N1c2VkSW5kZXgiLCJwcmV2UXVlc3Rpb25UZXh0IiwiY3VycmVudCIsInNlbGVjdGVkIiwic2VsZWN0ZWRWYWx1ZSIsImlkeCIsImZpbmRJbmRleCIsIm9wdCIsImZvY3VzZWRPcHRpb24iLCJub3Rlc1ZhbHVlIiwidGV4dElucHV0VmFsdWUiLCJoYW5kbGVTZWxlY3RPcHRpb24iLCJpbmRleCIsIm9wdGlvbiIsImhhbmRsZU5hdmlnYXRlIiwiZGlyZWN0aW9uIiwibmV3SW5kZXgiLCJsZW5ndGgiLCJjdXJyZW50VmFsdWUiLCJyZXN1bHQiLCJjb250ZW50IiwiY29udGV4dCIsImlzQWN0aXZlIiwidGFiczpwcmV2aW91cyIsInRhYnM6bmV4dCIsImhhbmRsZU5vdGVzRXhpdCIsImhhbmRsZURvd25Gcm9tUHJldmlldyIsImhhbmRsZVVwRnJvbUZvb3RlciIsImhhbmRsZUtleURvd24iLCJlIiwia2V5IiwiY3RybCIsInByZXZlbnREZWZhdWx0IiwibWV0YSIsInBhcnNlSW50IiwicHJldmlld0NvbnRlbnQiLCJwcmV2aWV3IiwiTEVGVF9QQU5FTF9XSURUSCIsIkdBUCIsImNvbHVtbnMiLCJwcmV2aWV3TWF4V2lkdGgiLCJQUkVWSUVXX09WRVJIRUFEIiwicHJldmlld01heExpbmVzIiwiTWF0aCIsIm1heCIsInVuZGVmaW5lZCIsIm1hcCIsImlzRm9jdXNlZCIsImlzU2VsZWN0ZWQiLCJwb2ludGVyIiwidGljayIsInZhbHVlIiwiYXJyb3dVcCIsImFycm93RG93biJdLCJzb3VyY2VzIjpbIlByZXZpZXdRdWVzdGlvblZpZXcudHN4Il0sInNvdXJjZXNDb250ZW50IjpbImltcG9ydCBmaWd1cmVzIGZyb20gJ2ZpZ3VyZXMnXG5pbXBvcnQgUmVhY3QsIHsgdXNlQ2FsbGJhY2ssIHVzZU1lbW8sIHVzZVJlZiwgdXNlU3RhdGUgfSBmcm9tICdyZWFjdCdcbmltcG9ydCB7IHVzZVRlcm1pbmFsU2l6ZSB9IGZyb20gJy4uLy4uLy4uL2hvb2tzL3VzZVRlcm1pbmFsU2l6ZS5qcydcbmltcG9ydCB0eXBlIHsgS2V5Ym9hcmRFdmVudCB9IGZyb20gJy4uLy4uLy4uL2luay9ldmVudHMva2V5Ym9hcmQtZXZlbnQuanMnXG5pbXBvcnQgeyBCb3gsIFRleHQgfSBmcm9tICcuLi8uLi8uLi9pbmsuanMnXG5pbXBvcnQge1xuICB1c2VLZXliaW5kaW5nLFxuICB1c2VLZXliaW5kaW5ncyxcbn0gZnJvbSAnLi4vLi4vLi4va2V5YmluZGluZ3MvdXNlS2V5YmluZGluZy5qcydcbmltcG9ydCB7IHVzZUFwcFN0YXRlIH0gZnJvbSAnLi4vLi4vLi4vc3RhdGUvQXBwU3RhdGUuanMnXG5pbXBvcnQgdHlwZSB7IFF1ZXN0aW9uIH0gZnJvbSAnLi4vLi4vLi4vdG9vbHMvQXNrVXNlclF1ZXN0aW9uVG9vbC9Bc2tVc2VyUXVlc3Rpb25Ub29sLmpzJ1xuaW1wb3J0IHsgZ2V0RXh0ZXJuYWxFZGl0b3IgfSBmcm9tICcuLi8uLi8uLi91dGlscy9lZGl0b3IuanMnXG5pbXBvcnQgeyB0b0lERURpc3BsYXlOYW1lIH0gZnJvbSAnLi4vLi4vLi4vdXRpbHMvaWRlLmpzJ1xuaW1wb3J0IHsgZWRpdFByb21wdEluRWRpdG9yIH0gZnJvbSAnLi4vLi4vLi4vdXRpbHMvcHJvbXB0RWRpdG9yLmpzJ1xuaW1wb3J0IHsgRGl2aWRlciB9IGZyb20gJy4uLy4uL2Rlc2lnbi1zeXN0ZW0vRGl2aWRlci5qcydcbmltcG9ydCBUZXh0SW5wdXQgZnJvbSAnLi4vLi4vVGV4dElucHV0LmpzJ1xuaW1wb3J0IHsgUGVybWlzc2lvblJlcXVlc3RUaXRsZSB9IGZyb20gJy4uL1Blcm1pc3Npb25SZXF1ZXN0VGl0bGUuanMnXG5pbXBvcnQgeyBQcmV2aWV3Qm94IH0gZnJvbSAnLi9QcmV2aWV3Qm94LmpzJ1xuaW1wb3J0IHsgUXVlc3Rpb25OYXZpZ2F0aW9uQmFyIH0gZnJvbSAnLi9RdWVzdGlvbk5hdmlnYXRpb25CYXIuanMnXG5pbXBvcnQgdHlwZSB7IFF1ZXN0aW9uU3RhdGUgfSBmcm9tICcuL3VzZS1tdWx0aXBsZS1jaG9pY2Utc3RhdGUuanMnXG5cbnR5cGUgUHJvcHMgPSB7XG4gIHF1ZXN0aW9uOiBRdWVzdGlvblxuICBxdWVzdGlvbnM6IFF1ZXN0aW9uW11cbiAgY3VycmVudFF1ZXN0aW9uSW5kZXg6IG51bWJlclxuICBhbnN3ZXJzOiBSZWNvcmQ8c3RyaW5nLCBzdHJpbmc+XG4gIHF1ZXN0aW9uU3RhdGVzOiBSZWNvcmQ8c3RyaW5nLCBRdWVzdGlvblN0YXRlPlxuICBoaWRlU3VibWl0VGF
|