askeroo
Version:
A modern CLI prompt library with flow control, history navigation, and conditional prompts
400 lines • 17.3 kB
JavaScript
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
import { useEffect, useState } from "react";
import { Box, Text, useInput } from "ink";
import { TaskWarning } from "./index.js";
import { taskStore, hasAnyTaskLists } from "./task-store.js";
// Track active task lists for tasks.add() functionality
let activeTaskLists = new Set();
let mostRecentTaskListId = null;
// Function to end a task list
function endTaskList(taskListId) {
activeTaskLists.delete(taskListId);
if (mostRecentTaskListId === taskListId) {
// Find the most recent active task list, or set to null if none
const activeLists = [...activeTaskLists];
mostRecentTaskListId =
activeLists.length > 0 ? activeLists[activeLists.length - 1] : null;
}
}
// Function to check if any tasks exist (initial tasks or dynamic tasks)
export function hasExistingTasks() {
return mostRecentTaskListId !== null || hasAnyTaskLists();
}
// Helper to update task state in the store
function setTaskState(taskListId, taskId, state) {
taskStore.update((s) => {
// Update allTaskStates
const all = new Map(s.allTaskStates.get(taskListId) || new Map());
all.set(taskId, state);
s.allTaskStates.set(taskListId, all);
// Update taskListStates for dynamic tasks
if (taskId.includes("_dynamic_")) {
const dyn = s.taskListStates.get(taskListId) || new Map();
dyn.set(taskId, state);
s.taskListStates.set(taskListId, dyn);
}
s.revision++;
});
}
// Function to add a task dynamically
export function addDynamicTask(task) {
if (!mostRecentTaskListId)
return Promise.resolve();
const taskListId = mostRecentTaskListId;
const taskId = `${taskListId}_dynamic_${Date.now()}_${Math.random()
.toString(36)
.substring(2, 11)}`;
return new Promise((resolve, reject) => {
const executor = async () => {
setTaskState(taskListId, taskId, { status: "running" });
try {
await task.action?.();
setTaskState(taskListId, taskId, { status: "success" });
resolve();
}
catch (error) {
const isWarning = error instanceof TaskWarning;
setTaskState(taskListId, taskId, {
status: isWarning ? "warning" : "error",
...(isWarning
? { warning: error.message }
: {
error: error instanceof Error
? error.message
: String(error),
}),
});
isWarning ? resolve() : reject(error);
}
};
// Add task to store
taskStore.update((s) => {
s.taskListDynamicTasks.set(taskListId, [
...(s.taskListDynamicTasks.get(taskListId) || []),
task,
]);
s.pendingTaskExecutors.set(taskId, executor);
s.revision++;
});
setTaskState(taskListId, taskId, { status: "idle" });
// Fallback: start if component doesn't
setTimeout(() => {
if (taskStore.get().pendingTaskExecutors.has(taskId)) {
taskStore.update((s) => {
s.pendingTaskExecutors.delete(taskId);
s.revision++;
});
executor();
}
}, 500);
});
}
// Function to wait for all pending tasks to complete
export async function waitForPendingTasks() {
return new Promise((resolve) => {
let timeout = null;
const isComplete = () => {
const s = taskStore.get();
for (const states of s.allTaskStates.values()) {
for (const state of states.values()) {
if (state.status === "idle" || state.status === "running")
return false;
}
}
return true;
};
const checkComplete = (unsubscribe) => {
if (timeout)
clearTimeout(timeout);
if (isComplete()) {
timeout = setTimeout(() => {
if (isComplete()) {
unsubscribe();
resolve();
}
}, 100);
}
};
const unsubscribe = taskStore.subscribe(() => checkComplete(unsubscribe));
checkComplete(unsubscribe); // Initial check
});
}
// Main component for the plugin
export const TasksDisplay = ({ node, options, events, }) => {
const [isExecuting, setIsExecuting] = useState(false);
const [spinnerFrame, setSpinnerFrame] = useState(0);
// Use a stable taskListId based on task content to enable state persistence
const [taskListId] = useState(() => {
// Create a deterministic ID based on task structure
const taskHash = JSON.stringify(options.tasks.map((t) => ({
label: t.label,
concurrent: t.concurrent,
})));
const hash = taskHash.split("").reduce((a, b) => {
a = (a << 5) - a + b.charCodeAt(0);
return a & a;
}, 0);
return `tasklist_${Math.abs(hash)}`;
});
// Subscribe to the task store - auto-updates on any store change
const store = taskStore.use();
// Extract data for this task list (no useMemo needed - store already handles caching)
const taskStates = store.allTaskStates.get(taskListId) || new Map();
const dynamicTasks = store.taskListDynamicTasks.get(taskListId) || [];
const pendingExecutors = store.pendingTaskExecutors;
// Register this task list as active
useEffect(() => {
activeTaskLists.add(taskListId);
mostRecentTaskListId = taskListId;
return () => {
endTaskList(taskListId);
};
}, [taskListId]);
// Start pending tasks after showing idle state
useEffect(() => {
const pending = [...pendingExecutors.keys()].filter((id) => id.startsWith(taskListId));
if (pending.length > 0) {
const tid = setTimeout(() => {
pending.forEach((taskId) => {
const executor = taskStore
.get()
.pendingTaskExecutors.get(taskId);
if (executor) {
taskStore.update((s) => {
s.pendingTaskExecutors.delete(taskId);
s.revision++;
});
executor();
}
});
}, 400);
return () => clearTimeout(tid);
}
}, [pendingExecutors, taskListId]);
// Animated spinner frames
const spinnerFrames = ["⠂", "-", "–", "—", "–", "-"];
// Block all input during task execution to prevent escape sequences from showing
useInput(() => { }, { isActive: isExecuting });
// Animate spinner only when tasks are running
useEffect(() => {
// Check if any tasks are currently running
const hasRunningTasks = [...taskStates.values()].some((state) => state.status === "running");
if (!hasRunningTasks) {
return; // Don't start animation if no tasks are running
}
const interval = setInterval(() => {
setSpinnerFrame((prev) => (prev + 1) % spinnerFrames.length);
}, 150);
return () => clearInterval(interval);
}, [taskStates]);
const getTaskId = (_task, index, parentId = "") => {
return `${taskListId}_${parentId}${index}`;
};
const getLabel = (task, status) => {
if (typeof task.label === "string")
return task.label;
const labelObj = task.label;
const fallback = labelObj.idle || "Task";
return ({
running: labelObj.running || fallback || "Running...",
success: labelObj.success || fallback || "Success",
error: labelObj.error || fallback || "Error",
warning: labelObj.success || fallback || "Success (with warnings)",
idle: fallback,
}[status] || fallback);
};
const getSymbol = (status) => {
return ({
idle: "□",
running: spinnerFrames[spinnerFrame],
success: "■",
warning: "▲",
error: "✗",
}[status] || "□");
};
const getColor = (status) => {
return ({
idle: "gray",
running: "blue",
success: "green",
warning: "yellow",
error: "red",
}[status] || "gray");
};
const updateTaskState = (taskId, newState) => {
taskStore.update((s) => {
const states = s.allTaskStates.get(taskListId) || new Map();
const current = states.get(taskId) || { status: "idle" };
const updated = new Map(states);
updated.set(taskId, { ...current, ...newState });
s.allTaskStates.set(taskListId, updated);
// Update dynamic states if needed
if (taskId.includes("_dynamic_")) {
const dynStates = s.taskListStates.get(taskListId) || new Map();
dynStates.set(taskId, { ...current, ...newState });
s.taskListStates.set(taskListId, dynStates);
}
s.revision++;
});
};
// Helper to execute subtasks (concurrent or sequential)
const executeSubtasks = async (tasks, taskId, concurrent) => {
if (concurrent) {
await Promise.all(tasks.map((subtask, index) => executeTask(subtask, getTaskId(subtask, index, `${taskId}.`))));
}
else {
for (let i = 0; i < tasks.length; i++) {
await executeTask(tasks[i], getTaskId(tasks[i], i, `${taskId}.`));
}
}
};
// Helper to handle task errors
const handleTaskError = (taskId, error, continueOnError) => {
if (error instanceof TaskWarning) {
updateTaskState(taskId, {
status: "warning",
warning: error.message,
});
}
else {
updateTaskState(taskId, {
status: "error",
error: error instanceof Error ? error.message : String(error),
});
if (!continueOnError)
throw error;
}
};
const executeTask = async (task, taskId) => {
updateTaskState(taskId, { status: "running" });
try {
const completeOn = task.completeOn || "children";
let actionCompleted = false;
let childrenCompleted = false;
const actionPromise = task.action
? task.action().then(() => {
actionCompleted = true;
})
: Promise.resolve().then(() => {
actionCompleted = true;
});
const createChildrenPromise = () => task.tasks && task.tasks.length > 0
? executeSubtasks(task.tasks, taskId, task.concurrent).then(() => {
childrenCompleted = true;
})
: Promise.resolve().then(() => {
childrenCompleted = true;
});
// Handle different completion modes
if (completeOn === "self") {
await actionPromise;
updateTaskState(taskId, { status: "success" });
// Run children in background (don't await)
if (task.tasks?.length)
createChildrenPromise();
}
else if (completeOn === "either") {
await Promise.race([actionPromise, createChildrenPromise()]);
updateTaskState(taskId, { status: "success" });
// Continue unfinished work in background
if (!actionCompleted || !childrenCompleted) {
Promise.allSettled([
actionPromise,
createChildrenPromise(),
]);
}
}
else {
// Default 'children' mode
await actionPromise;
if (task.tasks?.length)
await executeSubtasks(task.tasks, taskId, task.concurrent);
updateTaskState(taskId, { status: "success" });
}
}
catch (error) {
handleTaskError(taskId, error, task.continueOnError);
}
};
const executeAllTasks = async () => {
setIsExecuting(true);
try {
// Execute tasks based on concurrent setting (default: parallel)
if (options.concurrent === false) {
// Sequential execution
for (let i = 0; i < options.tasks.length; i++) {
const task = options.tasks[i];
await executeTask(task, getTaskId(task, i));
}
}
else {
// Parallel execution (default behavior)
await Promise.allSettled(options.tasks.map((task, i) => executeTask(task, getTaskId(task, i))));
}
}
finally {
setIsExecuting(false);
// Don't auto-submit here - let dynamic tasks be added
// Submission will happen when component detects all tasks complete
}
};
// Helper to render error/warning messages
const renderMessages = (state, marginLeft, isDimmed = false) => (_jsxs(_Fragment, { children: [state.warning && (_jsx(Box, { marginLeft: marginLeft, children: _jsx(Text, { color: "yellow", dimColor: isDimmed, children: state.warning }) })), state.error && (_jsx(Box, { marginLeft: marginLeft, children: _jsx(Text, { color: "red", dimColor: isDimmed, children: state.error }) }))] }));
const renderTask = (task, index, parentId = "", level = 0, parentDimmed = false) => {
const taskId = getTaskId(task, index, parentId);
const state = taskStates.get(taskId) || { status: "idle" };
const indent = " ".repeat(level);
const isDimmed = task.dimmed || parentDimmed;
// If task is not visible and doesn't have error/warning, skip rendering but still render children
if (task.visible === false &&
state.status !== "error" &&
state.status !== "warning") {
return task.tasks?.map((subtask, subIndex) => renderTask(subtask, subIndex, `${taskId}.`, level, isDimmed));
}
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: getColor(state.status), dimColor: isDimmed, children: [indent, getSymbol(state.status), " ", getLabel(task, state.status)] }), renderMessages(state, indent.length + 2, isDimmed), task.tasks?.map((subtask, subIndex) => renderTask(subtask, subIndex, `${taskId}.`, level + 1, isDimmed))] }, taskId));
};
const initializeTasksAsIdle = (tasks, parentId = "") => {
tasks.forEach((task, index) => {
const taskId = getTaskId(task, index, parentId);
updateTaskState(taskId, { status: "idle" });
if (task.tasks)
initializeTasksAsIdle(task.tasks, `${taskId}.`);
});
};
// Initialize all tasks as idle, then start execution after a brief delay
useEffect(() => {
if (node.state === "active" && !isExecuting) {
initializeTasksAsIdle(options.tasks);
setTimeout(executeAllTasks, 400);
}
}, [node.state]);
// Auto-submit when all tasks complete
useEffect(() => {
if (node.state !== "active" || isExecuting || !taskStates.size)
return;
const allDone = [...taskStates.values()].every((s) => ["success", "error", "warning"].includes(s.status));
if (allDone && events.onSubmit) {
events.onSubmit({ type: "auto" });
}
}, [taskStates, node.state, isExecuting, events.onSubmit]);
const renderDynamicTasks = () => {
const dynStates = store.taskListStates.get(taskListId) || new Map();
const taskIds = [...dynStates.keys()];
return dynamicTasks
.map((task, i) => {
const state = dynStates.get(taskIds[i]);
if (!state)
return null;
// Skip rendering if task is not visible and doesn't have error/warning
if (task.visible === false &&
state.status !== "error" &&
state.status !== "warning")
return null;
const isDimmed = task.dimmed || false;
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: getColor(state.status), dimColor: isDimmed, children: [getSymbol(state.status), " ", getLabel(task, state.status)] }), renderMessages(state, 2, isDimmed)] }, taskIds[i]));
})
.filter(Boolean);
};
return (_jsxs(Box, { flexDirection: "column", children: [options.tasks.map((task, index) => renderTask(task, index)), renderDynamicTasks()] }));
};
//# sourceMappingURL=Tasks.js.map