378 lines
49 KiB
TypeScript
378 lines
49 KiB
TypeScript
|
|
import { c as _c } from "react/compiler-runtime";
|
||
|
|
import figures from 'figures';
|
||
|
|
import * as React from 'react';
|
||
|
|
import { useTerminalSize } from '../hooks/useTerminalSize.js';
|
||
|
|
import { stringWidth } from '../ink/stringWidth.js';
|
||
|
|
import { Box, Text } from '../ink.js';
|
||
|
|
import { useAppState } from '../state/AppState.js';
|
||
|
|
import { isInProcessTeammateTask } from '../tasks/InProcessTeammateTask/types.js';
|
||
|
|
import { AGENT_COLOR_TO_THEME_COLOR, type AgentColorName } from '../tools/AgentTool/agentColorManager.js';
|
||
|
|
import { isAgentSwarmsEnabled } from '../utils/agentSwarmsEnabled.js';
|
||
|
|
import { count } from '../utils/array.js';
|
||
|
|
import { summarizeRecentActivities } from '../utils/collapseReadSearch.js';
|
||
|
|
import { truncateToWidth } from '../utils/format.js';
|
||
|
|
import { isTodoV2Enabled, type Task } from '../utils/tasks.js';
|
||
|
|
import type { Theme } from '../utils/theme.js';
|
||
|
|
import ThemedText from './design-system/ThemedText.js';
|
||
|
|
type Props = {
|
||
|
|
tasks: Task[];
|
||
|
|
isStandalone?: boolean;
|
||
|
|
};
|
||
|
|
const RECENT_COMPLETED_TTL_MS = 30_000;
|
||
|
|
function byIdAsc(a: Task, b: Task): number {
|
||
|
|
const aNum = parseInt(a.id, 10);
|
||
|
|
const bNum = parseInt(b.id, 10);
|
||
|
|
if (!isNaN(aNum) && !isNaN(bNum)) {
|
||
|
|
return aNum - bNum;
|
||
|
|
}
|
||
|
|
return a.id.localeCompare(b.id);
|
||
|
|
}
|
||
|
|
export function TaskListV2({
|
||
|
|
tasks,
|
||
|
|
isStandalone = false
|
||
|
|
}: Props): React.ReactNode {
|
||
|
|
const teamContext = useAppState(s => s.teamContext);
|
||
|
|
const appStateTasks = useAppState(s_0 => s_0.tasks);
|
||
|
|
const [, forceUpdate] = React.useState(0);
|
||
|
|
const {
|
||
|
|
rows,
|
||
|
|
columns
|
||
|
|
} = useTerminalSize();
|
||
|
|
|
||
|
|
// Track when each task was last observed transitioning to completed
|
||
|
|
const completionTimestampsRef = React.useRef(new Map<string, number>());
|
||
|
|
const previousCompletedIdsRef = React.useRef<Set<string> | null>(null);
|
||
|
|
if (previousCompletedIdsRef.current === null) {
|
||
|
|
previousCompletedIdsRef.current = new Set(tasks.filter(t => t.status === 'completed').map(t_0 => t_0.id));
|
||
|
|
}
|
||
|
|
const maxDisplay = rows <= 10 ? 0 : Math.min(10, Math.max(3, rows - 14));
|
||
|
|
|
||
|
|
// Update completion timestamps: reset when a task transitions to completed
|
||
|
|
const currentCompletedIds = new Set(tasks.filter(t_1 => t_1.status === 'completed').map(t_2 => t_2.id));
|
||
|
|
const now = Date.now();
|
||
|
|
for (const id of currentCompletedIds) {
|
||
|
|
if (!previousCompletedIdsRef.current.has(id)) {
|
||
|
|
completionTimestampsRef.current.set(id, now);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
for (const id_0 of completionTimestampsRef.current.keys()) {
|
||
|
|
if (!currentCompletedIds.has(id_0)) {
|
||
|
|
completionTimestampsRef.current.delete(id_0);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
previousCompletedIdsRef.current = currentCompletedIds;
|
||
|
|
|
||
|
|
// Schedule re-render when the next recent completion expires.
|
||
|
|
// Depend on `tasks` so the timer is only reset when the task list changes,
|
||
|
|
// not on every render (which was causing unnecessary work).
|
||
|
|
React.useEffect(() => {
|
||
|
|
if (completionTimestampsRef.current.size === 0) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const currentNow = Date.now();
|
||
|
|
let earliestExpiry = Infinity;
|
||
|
|
for (const ts of completionTimestampsRef.current.values()) {
|
||
|
|
const expiry = ts + RECENT_COMPLETED_TTL_MS;
|
||
|
|
if (expiry > currentNow && expiry < earliestExpiry) {
|
||
|
|
earliestExpiry = expiry;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (earliestExpiry === Infinity) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const timer = setTimeout(forceUpdate_0 => forceUpdate_0((n: number) => n + 1), earliestExpiry - currentNow, forceUpdate);
|
||
|
|
return () => clearTimeout(timer);
|
||
|
|
}, [tasks]);
|
||
|
|
if (!isTodoV2Enabled()) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
if (tasks.length === 0) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Build a map of teammate name -> theme color
|
||
|
|
const teammateColors: Record<string, keyof Theme> = {};
|
||
|
|
if (isAgentSwarmsEnabled() && teamContext?.teammates) {
|
||
|
|
for (const teammate of Object.values(teamContext.teammates)) {
|
||
|
|
if (teammate.color) {
|
||
|
|
const themeColor = AGENT_COLOR_TO_THEME_COLOR[teammate.color as AgentColorName];
|
||
|
|
if (themeColor) {
|
||
|
|
teammateColors[teammate.name] = themeColor;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Build a map of teammate name -> current activity description
|
||
|
|
// Map both agentName ("researcher") and agentId ("researcher@team") so
|
||
|
|
// task owners match regardless of which format the model used.
|
||
|
|
// Rolls up consecutive search/read tool uses into a compact summary.
|
||
|
|
// Also track which teammates are still running (not shut down).
|
||
|
|
const teammateActivity: Record<string, string> = {};
|
||
|
|
const activeTeammates = new Set<string>();
|
||
|
|
if (isAgentSwarmsEnabled()) {
|
||
|
|
for (const bgTask of Object.values(appStateTasks)) {
|
||
|
|
if (isInProcessTeammateTask(bgTask) && bgTask.status === 'running') {
|
||
|
|
activeTeammates.add(bgTask.identity.agentName);
|
||
|
|
activeTeammates.add(bgTask.identity.agentId);
|
||
|
|
const activities = bgTask.progress?.recentActivities;
|
||
|
|
const desc = (activities && summarizeRecentActivities(activities)) ?? bgTask.progress?.lastActivity?.activityDescription;
|
||
|
|
if (desc) {
|
||
|
|
teammateActivity[bgTask.identity.agentName] = desc;
|
||
|
|
teammateActivity[bgTask.identity.agentId] = desc;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Get task counts for display
|
||
|
|
const completedCount = count(tasks, t_3 => t_3.status === 'completed');
|
||
|
|
const pendingCount = count(tasks, t_4 => t_4.status === 'pending');
|
||
|
|
const inProgressCount = tasks.length - completedCount - pendingCount;
|
||
|
|
// Unresolved tasks (open or in_progress) block dependent tasks
|
||
|
|
const unresolvedTaskIds = new Set(tasks.filter(t_5 => t_5.status !== 'completed').map(t_6 => t_6.id));
|
||
|
|
|
||
|
|
// Check if we need to truncate
|
||
|
|
const needsTruncation = tasks.length > maxDisplay;
|
||
|
|
let visibleTasks: Task[];
|
||
|
|
let hiddenTasks: Task[];
|
||
|
|
if (needsTruncation) {
|
||
|
|
// Prioritize: recently completed (within 30s), in-progress, pending, older completed
|
||
|
|
const recentCompleted: Task[] = [];
|
||
|
|
const olderCompleted: Task[] = [];
|
||
|
|
for (const task of tasks.filter(t_7 => t_7.status === 'completed')) {
|
||
|
|
const ts_0 = completionTimestampsRef.current.get(task.id);
|
||
|
|
if (ts_0 && now - ts_0 < RECENT_COMPLETED_TTL_MS) {
|
||
|
|
recentCompleted.push(task);
|
||
|
|
} else {
|
||
|
|
olderCompleted.push(task);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
recentCompleted.sort(byIdAsc);
|
||
|
|
olderCompleted.sort(byIdAsc);
|
||
|
|
const inProgress = tasks.filter(t_8 => t_8.status === 'in_progress').sort(byIdAsc);
|
||
|
|
const pending = tasks.filter(t_9 => t_9.status === 'pending').sort((a, b) => {
|
||
|
|
const aBlocked = a.blockedBy.some(id_1 => unresolvedTaskIds.has(id_1));
|
||
|
|
const bBlocked = b.blockedBy.some(id_2 => unresolvedTaskIds.has(id_2));
|
||
|
|
if (aBlocked !== bBlocked) {
|
||
|
|
return aBlocked ? 1 : -1;
|
||
|
|
}
|
||
|
|
return byIdAsc(a, b);
|
||
|
|
});
|
||
|
|
const prioritized = [...recentCompleted, ...inProgress, ...pending, ...olderCompleted];
|
||
|
|
visibleTasks = prioritized.slice(0, maxDisplay);
|
||
|
|
hiddenTasks = prioritized.slice(maxDisplay);
|
||
|
|
} else {
|
||
|
|
// No truncation needed — sort by ID for stable ordering
|
||
|
|
visibleTasks = [...tasks].sort(byIdAsc);
|
||
|
|
hiddenTasks = [];
|
||
|
|
}
|
||
|
|
let hiddenSummary = '';
|
||
|
|
if (hiddenTasks.length > 0) {
|
||
|
|
const parts: string[] = [];
|
||
|
|
const hiddenPending = count(hiddenTasks, t_10 => t_10.status === 'pending');
|
||
|
|
const hiddenInProgress = count(hiddenTasks, t_11 => t_11.status === 'in_progress');
|
||
|
|
const hiddenCompleted = count(hiddenTasks, t_12 => t_12.status === 'completed');
|
||
|
|
if (hiddenInProgress > 0) {
|
||
|
|
parts.push(`${hiddenInProgress} in progress`);
|
||
|
|
}
|
||
|
|
if (hiddenPending > 0) {
|
||
|
|
parts.push(`${hiddenPending} pending`);
|
||
|
|
}
|
||
|
|
if (hiddenCompleted > 0) {
|
||
|
|
parts.push(`${hiddenCompleted} completed`);
|
||
|
|
}
|
||
|
|
hiddenSummary = ` … +${parts.join(', ')}`;
|
||
|
|
}
|
||
|
|
const content = <>
|
||
|
|
{visibleTasks.map(task_0 => <TaskItem key={task_0.id} task={task_0} ownerColor={task_0.owner ? teammateColors[task_0.owner] : undefined} openBlockers={task_0.blockedBy.filter(id_3 => unresolvedTaskIds.has(id_3))} activity={task_0.owner ? teammateActivity[task_0.owner] : undefined} ownerActive={task_0.owner ? activeTeammates.has(task_0.owner) : false} columns={columns} />)}
|
||
|
|
{maxDisplay > 0 && hiddenSummary && <Text dimColor>{hiddenSummary}</Text>}
|
||
|
|
</>;
|
||
|
|
if (isStandalone) {
|
||
|
|
return <Box flexDirection="column" marginTop={1} marginLeft={2}>
|
||
|
|
<Box>
|
||
|
|
<Text dimColor>
|
||
|
|
<Text bold>{tasks.length}</Text>
|
||
|
|
{' tasks ('}
|
||
|
|
<Text bold>{completedCount}</Text>
|
||
|
|
{' done, '}
|
||
|
|
{inProgressCount > 0 && <>
|
||
|
|
<Text bold>{inProgressCount}</Text>
|
||
|
|
{' in progress, '}
|
||
|
|
</>}
|
||
|
|
<Text bold>{pendingCount}</Text>
|
||
|
|
{' open)'}
|
||
|
|
</Text>
|
||
|
|
</Box>
|
||
|
|
{content}
|
||
|
|
</Box>;
|
||
|
|
}
|
||
|
|
return <Box flexDirection="column">{content}</Box>;
|
||
|
|
}
|
||
|
|
type TaskItemProps = {
|
||
|
|
task: Task;
|
||
|
|
ownerColor?: keyof Theme;
|
||
|
|
openBlockers: string[];
|
||
|
|
activity?: string;
|
||
|
|
ownerActive: boolean;
|
||
|
|
columns: number;
|
||
|
|
};
|
||
|
|
function getTaskIcon(status: Task['status']): {
|
||
|
|
icon: string;
|
||
|
|
color: keyof Theme | undefined;
|
||
|
|
} {
|
||
|
|
switch (status) {
|
||
|
|
case 'completed':
|
||
|
|
return {
|
||
|
|
icon: figures.tick,
|
||
|
|
color: 'success'
|
||
|
|
};
|
||
|
|
case 'in_progress':
|
||
|
|
return {
|
||
|
|
icon: figures.squareSmallFilled,
|
||
|
|
color: 'claude'
|
||
|
|
};
|
||
|
|
case 'pending':
|
||
|
|
return {
|
||
|
|
icon: figures.squareSmall,
|
||
|
|
color: undefined
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}
|
||
|
|
function TaskItem(t0) {
|
||
|
|
const $ = _c(37);
|
||
|
|
const {
|
||
|
|
task,
|
||
|
|
ownerColor,
|
||
|
|
openBlockers,
|
||
|
|
activity,
|
||
|
|
ownerActive,
|
||
|
|
columns
|
||
|
|
} = t0;
|
||
|
|
const isCompleted = task.status === "completed";
|
||
|
|
const isInProgress = task.status === "in_progress";
|
||
|
|
const isBlocked = openBlockers.length > 0;
|
||
|
|
let t1;
|
||
|
|
if ($[0] !== task.status) {
|
||
|
|
t1 = getTaskIcon(task.status);
|
||
|
|
$[0] = task.status;
|
||
|
|
$[1] = t1;
|
||
|
|
} else {
|
||
|
|
t1 = $[1];
|
||
|
|
}
|
||
|
|
const {
|
||
|
|
icon,
|
||
|
|
color
|
||
|
|
} = t1;
|
||
|
|
const showActivity = isInProgress && !isBlocked && activity;
|
||
|
|
const showOwner = columns >= 60 && task.owner && ownerActive;
|
||
|
|
let t2;
|
||
|
|
if ($[2] !== showOwner || $[3] !== task.owner) {
|
||
|
|
t2 = showOwner ? stringWidth(` (@${task.owner})`) : 0;
|
||
|
|
$[2] = showOwner;
|
||
|
|
$[3] = task.owner;
|
||
|
|
$[4] = t2;
|
||
|
|
} else {
|
||
|
|
t2 = $[4];
|
||
|
|
}
|
||
|
|
const ownerWidth = t2;
|
||
|
|
const maxSubjectWidth = Math.max(15, columns - 15 - ownerWidth);
|
||
|
|
let t3;
|
||
|
|
if ($[5] !== maxSubjectWidth || $[6] !== task.subject) {
|
||
|
|
t3 = truncateToWidth(task.subject, maxSubjectWidth);
|
||
|
|
$[5] = maxSubjectWidth;
|
||
|
|
$[6] = task.subject;
|
||
|
|
$[7] = t3;
|
||
|
|
} else {
|
||
|
|
t3 = $[7];
|
||
|
|
}
|
||
|
|
const displaySubject = t3;
|
||
|
|
const maxActivityWidth = Math.max(15, columns - 15);
|
||
|
|
let t4;
|
||
|
|
if ($[8] !== activity || $[9] !== maxActivityWidth) {
|
||
|
|
t4 = activity ? truncateToWidth(activity, maxActivityWidth) : undefined;
|
||
|
|
$[8] = activity;
|
||
|
|
$[9] = maxActivityWidth;
|
||
|
|
$[10] = t4;
|
||
|
|
} else {
|
||
|
|
t4 = $[10];
|
||
|
|
}
|
||
|
|
const displayActivity = t4;
|
||
|
|
let t5;
|
||
|
|
if ($[11] !== color || $[12] !== icon) {
|
||
|
|
t5 = <Text color={color}>{icon} </Text>;
|
||
|
|
$[11] = color;
|
||
|
|
$[12] = icon;
|
||
|
|
$[13] = t5;
|
||
|
|
} else {
|
||
|
|
t5 = $[13];
|
||
|
|
}
|
||
|
|
const t6 = isCompleted || isBlocked;
|
||
|
|
let t7;
|
||
|
|
if ($[14] !== displaySubject || $[15] !== isCompleted || $[16] !== isInProgress || $[17] !== t6) {
|
||
|
|
t7 = <Text bold={isInProgress} strikethrough={isCompleted} dimColor={t6}>{displaySubject}</Text>;
|
||
|
|
$[14] = displaySubject;
|
||
|
|
$[15] = isCompleted;
|
||
|
|
$[16] = isInProgress;
|
||
|
|
$[17] = t6;
|
||
|
|
$[18] = t7;
|
||
|
|
} else {
|
||
|
|
t7 = $[18];
|
||
|
|
}
|
||
|
|
let t8;
|
||
|
|
if ($[19] !== ownerColor || $[20] !== showOwner || $[21] !== task.owner) {
|
||
|
|
t8 = showOwner && <Text dimColor={true}>{" ("}{ownerColor ? <ThemedText color={ownerColor}>@{task.owner}</ThemedText> : `@${task.owner}`}{")"}</Text>;
|
||
|
|
$[19] = ownerColor;
|
||
|
|
$[20] = showOwner;
|
||
|
|
$[21] = task.owner;
|
||
|
|
$[22] = t8;
|
||
|
|
} else {
|
||
|
|
t8 = $[22];
|
||
|
|
}
|
||
|
|
let t9;
|
||
|
|
if ($[23] !== isBlocked || $[24] !== openBlockers) {
|
||
|
|
t9 = isBlocked && <Text dimColor={true}>{" "}{figures.pointerSmall} blocked by{" "}{[...openBlockers].sort(_temp).map(_temp2).join(", ")}</Text>;
|
||
|
|
$[23] = isBlocked;
|
||
|
|
$[24] = openBlockers;
|
||
|
|
$[25] = t9;
|
||
|
|
} else {
|
||
|
|
t9 = $[25];
|
||
|
|
}
|
||
|
|
let t10;
|
||
|
|
if ($[26] !== t5 || $[27] !== t7 || $[28] !== t8 || $[29] !== t9) {
|
||
|
|
t10 = <Box>{t5}{t7}{t8}{t9}</Box>;
|
||
|
|
$[26] = t5;
|
||
|
|
$[27] = t7;
|
||
|
|
$[28] = t8;
|
||
|
|
$[29] = t9;
|
||
|
|
$[30] = t10;
|
||
|
|
} else {
|
||
|
|
t10 = $[30];
|
||
|
|
}
|
||
|
|
let t11;
|
||
|
|
if ($[31] !== displayActivity || $[32] !== showActivity) {
|
||
|
|
t11 = showActivity && displayActivity && <Box><Text dimColor={true}>{" "}{displayActivity}{figures.ellipsis}</Text></Box>;
|
||
|
|
$[31] = displayActivity;
|
||
|
|
$[32] = showActivity;
|
||
|
|
$[33] = t11;
|
||
|
|
} else {
|
||
|
|
t11 = $[33];
|
||
|
|
}
|
||
|
|
let t12;
|
||
|
|
if ($[34] !== t10 || $[35] !== t11) {
|
||
|
|
t12 = <Box flexDirection="column">{t10}{t11}</Box>;
|
||
|
|
$[34] = t10;
|
||
|
|
$[35] = t11;
|
||
|
|
$[36] = t12;
|
||
|
|
} else {
|
||
|
|
t12 = $[36];
|
||
|
|
}
|
||
|
|
return t12;
|
||
|
|
}
|
||
|
|
function _temp2(id) {
|
||
|
|
return `#${id}`;
|
||
|
|
}
|
||
|
|
function _temp(a, b) {
|
||
|
|
return parseInt(a, 10) - parseInt(b, 10);
|
||
|
|
}
|
||
|
|
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJmaWd1cmVzIiwiUmVhY3QiLCJ1c2VUZXJtaW5hbFNpemUiLCJzdHJpbmdXaWR0aCIsIkJveCIsIlRleHQiLCJ1c2VBcHBTdGF0ZSIsImlzSW5Qcm9jZXNzVGVhbW1hdGVUYXNrIiwiQUdFTlRfQ09MT1JfVE9fVEhFTUVfQ09MT1IiLCJBZ2VudENvbG9yTmFtZSIsImlzQWdlbnRTd2FybXNFbmFibGVkIiwiY291bnQiLCJzdW1tYXJpemVSZWNlbnRBY3Rpdml0aWVzIiwidHJ1bmNhdGVUb1dpZHRoIiwiaXNUb2RvVjJFbmFibGVkIiwiVGFzayIsIlRoZW1lIiwiVGhlbWVkVGV4dCIsIlByb3BzIiwidGFza3MiLCJpc1N0YW5kYWxvbmUiLCJSRUNFTlRfQ09NUExFVEVEX1RUTF9NUyIsImJ5SWRBc2MiLCJhIiwiYiIsImFOdW0iLCJwYXJzZUludCIsImlkIiwiYk51bSIsImlzTmFOIiwibG9jYWxlQ29tcGFyZSIsIlRhc2tMaXN0VjIiLCJSZWFjdE5vZGUiLCJ0ZWFtQ29udGV4dCIsInMiLCJhcHBTdGF0ZVRhc2tzIiwiZm9yY2VVcGRhdGUiLCJ1c2VTdGF0ZSIsInJvd3MiLCJjb2x1bW5zIiwiY29tcGxldGlvblRpbWVzdGFtcHNSZWYiLCJ1c2VSZWYiLCJNYXAiLCJwcmV2aW91c0NvbXBsZXRlZElkc1JlZiIsIlNldCIsImN1cnJlbnQiLCJmaWx0ZXIiLCJ0Iiwic3RhdHVzIiwibWFwIiwibWF4RGlzcGxheSIsIk1hdGgiLCJtaW4iLCJtYXgiLCJjdXJyZW50Q29tcGxldGVkSWRzIiwibm93IiwiRGF0ZSIsImhhcyIsInNldCIsImtleXMiLCJkZWxldGUiLCJ1c2VFZmZlY3QiLCJzaXplIiwiY3VycmVudE5vdyIsImVhcmxpZXN0RXhwaXJ5IiwiSW5maW5pdHkiLCJ0cyIsInZhbHVlcyIsImV4cGlyeSIsInRpbWVyIiwic2V0VGltZW91dCIsIm4iLCJjbGVhclRpbWVvdXQiLCJsZW5ndGgiLCJ0ZWFtbWF0ZUNvbG9ycyIsIlJlY29yZCIsInRlYW1tYXRlcyIsInRlYW1tYXRlIiwiT2JqZWN0IiwiY29sb3IiLCJ0aGVtZUNvbG9yIiwibmFtZSIsInRlYW1tYXRlQWN0aXZpdHkiLCJhY3RpdmVUZWFtbWF0ZXMiLCJiZ1Rhc2siLCJhZGQiLCJpZGVudGl0eSIsImFnZW50TmFtZSIsImFnZW50SWQiLCJhY3Rpdml0aWVzIiwicHJvZ3Jlc3MiLCJyZWNlbnRBY3Rpdml0aWVzIiwiZGVzYyIsImxhc3RBY3Rpdml0eSIsImFjdGl2aXR5RGVzY3JpcHRpb24iLCJjb21wbGV0ZWRDb3VudCIsInBlbmRpbmdDb3VudCIsImluUHJvZ3Jlc3NDb3VudCIsInVucmVzb2x2ZWRUYXNrSWRzIiwibmVlZHNUcnVuY2F0aW9uIiwidmlzaWJsZVRhc2tzIiwiaGlkZGVuVGFza3MiLCJyZWNlbnRDb21wbGV0ZWQiLCJvbGRlckNvbXBsZXRlZCIsInRhc2siLCJnZXQiLCJwdXNoIiwic29ydCIsImluUHJvZ3Jlc3MiLCJwZW5kaW5nIiwiYUJsb2NrZWQiLCJibG9ja2VkQnkiLCJzb21lIiwiYkJsb2NrZWQiLCJwcmlvcml0aXplZCIsInNsaWNlIiwiaGlkZGVuU3VtbWFyeSIsInBhcnRzIiwiaGlkZGVuUGVuZGluZyIsImhpZGRlbkluUHJvZ3Jlc3MiLCJoaWRkZW5Db21wbGV0ZWQiLCJqb2luIiwiY29udGVudCIsIm93bmVyIiwidW5kZWZpbmVkIiwiVGFza0l0ZW1Qcm9wcyIsIm93bmVyQ29sb3IiLCJvcGVuQmxvY2tlcnMiLCJhY3Rpdml0eSIsIm93bmVyQWN0aXZlIiwiZ2V0VGFza0ljb24iLCJpY29uIiwidGljayIsInNxdWFyZVNtYWxsRmlsbGVkIiwic3F1YXJlU21hbGwiLCJUYXNrSXRlbSIsInQwIiwiJCIsIl9jIiwiaXNDb21wbGV0ZWQiLCJpc0luUHJvZ3Jlc3MiLCJpc0Jsb2NrZWQiLCJ0MSIsInNob3dBY3Rpdml0eSIsInNob3dPd25lciIsInQyIiwib3duZXJXaWR0aCIsIm1heFN1YmplY3RXaWR0aCIsInQzIiwic3ViamVjdCIsImRpc3BsYXlTdWJqZWN0IiwibWF4QWN0aXZpdHlXaWR0aCIsInQ0IiwiZGlzcGxheUFjdGl2aXR5IiwidDUiLCJ0NiIsInQ3IiwidDgiLCJ0OSIsInBvaW50ZXJTbWFsbCIsIl90ZW1wIiwiX3RlbXAyIiwidDEwIiwidDExIiwiZWxsaXBzaXMiLCJ0MTIiXSwic291cmNlcyI6WyJUYXNrTGlzdFYyLnRzeCJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgZmlndXJlcyBmcm9tICdmaWd1cmVzJ1xuaW1wb3J0ICogYXMgUmVhY3QgZnJvbSAncmVhY3QnXG5pbXBvcnQgeyB1c2VUZXJtaW5hbFNpemUgfSBmcm9tICcuLi9ob29rcy91c2VUZXJtaW5hbFNpemUuanMnXG5pbXBvcnQgeyBzdHJpbmdXaWR0aCB9IGZyb20gJy4uL2luay9zdHJpbmdXaWR0aC5qcydcbmltcG9ydCB7IEJveCwgVGV4dCB9IGZyb20gJy4uL2luay5qcydcbmltcG9ydCB7IHVzZUFwcFN0YXRlIH0gZnJvbSAnLi4vc3RhdGUvQXBwU3RhdGUuanMnXG5pbXBvcnQgeyBpc0luUHJvY2Vzc1RlYW1tYXRlVGFzayB9IGZyb20gJy4uL3Rhc2tzL0luUHJvY2Vzc1RlYW1tYXRlVGFzay90eXBlcy5qcydcbmltcG9ydCB7XG4gIEFHRU5UX0NPTE9SX1RPX1RIRU1FX0NPTE9SLFxuICB0eXBlIEFnZW50Q29sb3JOYW1lLFxufSBmcm9tICcuLi90b29scy9BZ2VudFRvb2wvYWdlbnRDb2xvck1hbmFnZXIuanMnXG5pbXBvcnQgeyBpc0FnZW50U3dhcm1zRW5hYmxlZCB9IGZyb20gJy4uL3V0aWxzL2FnZW50U3dhcm1zRW5hYmxlZC5qcydcbmltcG9ydCB7IGNvdW50IH0gZnJvbSAnLi4vdXRpbHMvYXJyYXkuanMnXG5pbXBvcnQgeyBzdW1tYXJpemVSZWNlbnRBY3Rpdml0aWVzIH0gZnJvbSAnLi4vdXRpbHMvY29sbGFwc2VSZWFkU2VhcmNoLmpzJ1xuaW1wb3J0IHsgdHJ1bmNhdGVUb1dpZHRoIH0gZnJvbSAnLi4vdXRpbHMvZm9ybWF0LmpzJ1xuaW1wb3J0IHsgaXNUb2RvVjJFbmFibGVkLCB0eXBlIFRhc2sgfSBmcm9tICcuLi91dGlscy90YXNrcy5qcydcbmltcG9ydCB0eXBlIHsgVGhlbWUgfSBmcm9tICcuLi91dGlscy90aGVtZS5qcydcbmltcG9ydCBUaGVtZWRUZXh0IGZyb20gJy4vZGVzaWduLXN5c3RlbS9UaGVtZWRUZXh0LmpzJ1xuXG50eXBlIFByb3BzID0ge1xuICB0YXNrczogVGFza1tdXG4gIGlzU3RhbmRhbG9uZT86IGJvb2xlYW5cbn1cblxuY29uc3QgUkVDRU5UX0NPTVBMRVRFRF9UVExfTVMgPSAzMF8wMDBcblxuZnVuY3Rpb24gYnlJZEFzYyhhOiBUYXNrLCBiOiBUYXNrKTogbnVtYmVyIHtcbiAgY29uc3QgYU51bSA9IHB
|