askeroo
Version:
A modern CLI prompt library with flow control, history navigation, and conditional prompts
324 lines • 18.5 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { Text, Box } from "ink";
import { globalRegistry } from "../core/registry.js";
import { PluginWrapper } from "./PluginWrapper.js";
function HintText({ children }) {
return _jsx(Text, { dimColor: true, children: children });
}
export function RecursiveGroupContainer({ item, treeManager, onSubmit, onBack, onComplete, onHintChange, hintText, showOnlyActiveAndCompleted = false, isFrozen = false, hintsByPromptId, }) {
// Calculate indentation based on depth
// Depth 0 = root, depth 1 = root children (0 indent), depth 2 = first nesting level (3 spaces), etc.
const baseIndent = Math.max(0, (item.depth - 1) * 3);
// For group nodes, render through the plugin system
if (item.type === "group") {
// Check if group plugin exists
const groupPluginExists = globalRegistry.getComponent("group");
if (!groupPluginExists) {
return null;
}
// Don't render anything for empty groups without a label
if ((!item.children || item.children.length === 0) && !item.label) {
return null;
}
// Empty group with label only
if (!item.children || item.children.length === 0) {
return (_jsx(PluginWrapper, { pluginType: "group", ...item.properties, label: item.label, flow: item.flow, depth: item.depth, state: item.active
? "active"
: item.completed
? "completed"
: "disabled" }, `group-${item.id}-empty`));
}
// Note: Removed auto-completion logic as it was triggering too early
// Groups are completed in the runtime when their body function finishes
// Filter children based on visibility rules
const visibleChildren = item.children.filter((child) => {
if (showOnlyActiveAndCompleted) {
// Only show active, completed, or visited children
return child.active || child.completed || child.visited;
}
// For static groups, show all discovered children BUT only if the group itself should be visible
// The group visibility should be controlled at a higher level (when the group is added to the tree)
if (item.flow === "static") {
return true; // Show all children in static groups (group-level visibility is controlled elsewhere)
}
// For root level (no flow), show first pending field
if (!item.flow && item.id === "root") {
// Show completed, active, visited (with deferred completion), and first pending
if (child.completed || child.active || child.visited) {
return true;
}
// Show first pending field
const siblings = item.children;
const childIndex = siblings.indexOf(child);
const allPreviousCompleted = siblings
.slice(0, childIndex)
.every((prev) => prev.completed);
return allPreviousCompleted;
}
// Handle different flow types separately
if (item.flow === "progressive") {
// Progressive flow: show completed, active, visited (with deferred completion), and the next pending field
if (child.completed || child.active || child.visited) {
return true;
}
// Show the next pending field after all completed ones
const siblings = item.children;
const childIndex = siblings.indexOf(child);
const allPreviousCompleted = siblings
.slice(0, childIndex)
.every((prev) => prev.completed);
return allPreviousCompleted;
}
if (item.flow === "phased") {
// Phased flow: show only the active field during execution
// The last completed field will be shown only after group completion
// For groups inside phased groups, also hide completed groups
if (child.type === "group" &&
child.completed &&
!child.active) {
return false;
}
return child.active;
}
return false;
});
// Determine group state BEFORE checking visibleChildren
const groupState = item.completed && !item.active
? "completed"
: item.active
? "active"
: "disabled";
// Handle completed groups differently based on flow type
// This logic must come BEFORE the visibleChildren.length === 0 check
const childrenToRender = item.completed && !item.active
? item.flow === "phased"
? // Phased: show only the last prompt, unless this group is inside another phased group
item.parent?.flow === "phased"
? [] // Hide all if nested inside another phased group
: (() => {
// Find the last completed field from all children (not just visibleChildren)
const allCompletedFields = item.children.filter((c) => c.type === "field" &&
c.completed &&
!c.hideOnCompletion);
const lastCompletedField = allCompletedFields[allCompletedFields.length - 1];
// Return the last completed field directly, don't rely on visibleChildren
// because visibleChildren was filtered for active state during execution
return lastCompletedField
? [lastCompletedField]
: [];
})()
: visibleChildren.filter((child) =>
// Progressive: show completed fields and completed groups
child.completed &&
(child.type === "group" ||
(child.type === "field" &&
!child.hideOnCompletion)))
: visibleChildren;
// Handle case where active/pending groups have no visible children
if (!item.completed && visibleChildren.length === 0) {
// If group has a label, show it even without visible children
if (item.label) {
return (_jsx(PluginWrapper, { pluginType: "group", ...item.properties, label: item.label, flow: item.flow, depth: item.depth, state: item.active ? "active" : "disabled" }, `group-${item.id}-no-visible`));
}
return null;
}
// If no children to render and no label, return null
if (childrenToRender.length === 0 && !item.label) {
return null;
}
// Render children recursively
// Pass down hints map and let each child determine its own hint
const renderedChildren = childrenToRender.map((child, index) => {
// Determine hint text for this specific child
const childHintText = child.active
? hintsByPromptId?.get(child.id) ||
(child.id === item.id ? hintText : null)
: null;
return (_jsx(RecursiveGroupContainer, { item: child, treeManager: treeManager, onSubmit: isFrozen || child.frozen ? undefined : onSubmit, onBack: isFrozen || child.frozen ? undefined : onBack, onComplete: onComplete, onHintChange: isFrozen || child.frozen ? undefined : onHintChange, hintText: childHintText, showOnlyActiveAndCompleted: showOnlyActiveAndCompleted ||
(item.completed && !item.active), isFrozen: isFrozen || child.frozen, hintsByPromptId: hintsByPromptId }, `${item.id}-child-${child.id}-${index}`));
});
// For root node, check if there's a custom root container
// If so, use it instead of the group plugin (to allow ask component to control layout)
const isRootNode = item.id === "root" && item.depth === 0;
const CustomRootContainer = isRootNode
? globalThis.__customRootContainer
: null;
// If custom root container exists for root node, use it directly
if (CustomRootContainer && isRootNode) {
// Separate regular children from cancel prompts
const regularChildren = [];
const cancelChildren = [];
// Split rendered children based on whether they are cancel prompts
renderedChildren.forEach((renderedChild, index) => {
const correspondingChild = childrenToRender[index];
if (correspondingChild?.isCancelPrompt) {
cancelChildren.push(renderedChild);
}
else {
regularChildren.push(renderedChild);
}
});
return (_jsx(CustomRootContainer, { onCancelNodes: cancelChildren, children: regularChildren }));
}
// Otherwise, render group through plugin system
// Hint text is displayed by the active field itself, not at group level
return (_jsx(PluginWrapper, { pluginType: "group", ...item.properties, label: item.label, flow: item.flow, depth: item.depth, state: groupState, children: renderedChildren }, `group-${item.id}-${groupState}`));
}
// For field nodes, render the appropriate component
if (item.type === "field" && item.fieldType) {
const pluginExists = globalRegistry.getComponent(item.fieldType);
if (!pluginExists) {
return null;
}
const isActive = item.active;
const isCompleted = item.completed && !isActive;
const isVisited = item.visited;
// Skip rendering if field should be hidden after submit
if (isCompleted && item.hideOnCompletion) {
return null;
}
// For showOnlyActiveAndCompleted mode, only show active, completed, or visited fields
// Visited-but-not-completed nodes are those with deferred completion (like streams)
if (showOnlyActiveAndCompleted &&
!isActive &&
!isCompleted &&
!isVisited) {
return null;
}
// Determine hint text for this specific field node
const fieldHintText = item.active
? hintsByPromptId?.get(item.id) || hintText
: null;
// Get initial value
const getInitialValue = () => {
if (item.value !== undefined) {
return item.value;
}
// Check if there's an initialValue in properties
if (item.properties.initialValue !== undefined) {
return item.properties.initialValue;
}
// Type-specific defaults
if (item.fieldType === "multi")
return [];
if (item.fieldType === "confirm")
return false;
return "";
};
// Determine position in group for navigation
const parent = item.parent;
const siblingIndex = parent ? parent.children.indexOf(item) : 0;
const isFirstInGroup = siblingIndex === 0;
const isLastInGroup = parent
? siblingIndex === parent.children.length - 1
: true;
// Check if this is the first user-interactive prompt in the root flow
// Cannot go back on the first user-interactive prompt (auto-submit prompts don't count)
const isFirstInteractivePrompt = () => {
// Helper to check if a node requires user interaction (not auto-submit)
const requiresUserInteraction = (node) => {
if (node.type === "group")
return false; // Groups don't require user interaction
if (!node.fieldType)
return false;
return !globalRegistry.shouldAutoSubmit(node.fieldType);
};
// Helper to check if a group contains any user-interactive prompts
const groupHasInteractive = (group) => {
return group.children.some((child) => requiresUserInteraction(child) ||
(child.type === "group" && groupHasInteractive(child)));
};
// Case 1: Direct child of root
if (parent?.id === "root") {
// Check if there are any user-interactive prompts before this one
const siblingsBefore = parent.children.slice(0, siblingIndex);
const hasInteractiveBefore = siblingsBefore.some((sibling) => requiresUserInteraction(sibling) ||
(sibling.type === "group" &&
groupHasInteractive(sibling)));
return !hasInteractiveBefore;
}
// Case 2: Inside a group at root level
if (parent?.type === "group" && parent.parent?.id === "root") {
const groupIndex = parent.parent.children.indexOf(parent);
// Check siblings before this one in the same group
const siblingsBefore = parent.children.slice(0, siblingIndex);
if (siblingsBefore.some(requiresUserInteraction)) {
return false;
}
// Check if there are user-interactive prompts in previous root-level items
const rootSiblingsBefore = parent.parent.children.slice(0, groupIndex);
const hasInteractiveBefore = rootSiblingsBefore.some((sibling) => requiresUserInteraction(sibling) ||
(sibling.type === "group" &&
groupHasInteractive(sibling)));
return !hasInteractiveBefore;
}
// Case 3: Nested groups - can always go back
return false;
};
const isFirstRootPrompt = isFirstInteractivePrompt();
// Determine flow type
const flowType = parent?.flow || "progressive";
// CompletedFields and other display-only plugins should not be indented
const shouldIndent = item.fieldType !== "completedFields";
// Determine plugin state
const pluginState = isCompleted
? "completed"
: !isActive
? "disabled"
: "active";
// Compute effective allowBack based on:
// 1. Node's explicit allowBack setting
// 2. First root prompt check
// 3. Previous node's submission type
const computeEffectiveAllowBack = () => {
// If explicitly set to false, respect it
if (item.allowBack === false)
return false;
// First interactive prompt can't go back
if (isFirstRootPrompt)
return false;
// Check if back navigation should be allowed based on previous node's submission type
const canGoBackBasedOnHistory = treeManager.canGoBackBasedOnSubmissionType();
// If previous node was auto-submitted (and doesn't explicitly allow back), prevent navigation
if (!canGoBackBasedOnHistory)
return false;
// Default: allow back (node.allowBack is either true or undefined)
return true;
};
const effectiveAllowBack = computeEffectiveAllowBack();
// Special handling for completedFields plugin to avoid Box wrapper when empty
if (item.fieldType === "completedFields") {
// Extract user's onSubmit callback from properties
const { onSubmit: userOnSubmit, ...otherProperties } = item.properties;
return (_jsx(PluginWrapper, { pluginType: item.fieldType, promptId: item.id, ...otherProperties, userOnSubmit: userOnSubmit, message: item.properties.message || item.label || "", initialValue: getInitialValue(), state: pluginState, completedValue: isCompleted ? item.value : undefined, onSubmit: isActive && !isFrozen && !item.frozen
? onSubmit
: () => { }, onBack: isActive && !isFrozen && !item.frozen
? onBack
: undefined, onComplete: onComplete, allowBack: effectiveAllowBack, flow: flowType, isFirstInGroup: isFirstInGroup, isLastInGroup: isLastInGroup, isFirstRootPrompt: isFirstRootPrompt, enableArrowNavigation: parent?.enableArrowNavigation, ...(isActive &&
!isFrozen &&
!item.frozen &&
onHintChange && { onHintChange }) }, `plugin-${item.id}-${item.depth}-${pluginState}`));
}
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(PluginWrapper, { pluginType: item.fieldType, promptId: item.id, ...(() => {
// Extract user's onSubmit callback from properties
const { onSubmit: userOnSubmit, ...otherProperties } = item.properties;
return {
...otherProperties, // Spread all plugin properties except onSubmit
userOnSubmit, // Pass user's onSubmit callback separately
};
})(), message: item.properties.message || item.label || "", initialValue: getInitialValue(), state: pluginState, completedValue: isCompleted ? item.value : undefined, onSubmit: isActive && !isFrozen && !item.frozen
? onSubmit
: () => { }, onBack: isActive && !isFrozen && !item.frozen
? onBack
: undefined, onComplete: onComplete, allowBack: effectiveAllowBack, flow: flowType, isFirstInGroup: isFirstInGroup, isLastInGroup: isLastInGroup, isFirstRootPrompt: isFirstRootPrompt, enableArrowNavigation: parent?.enableArrowNavigation, ...(isActive &&
!isFrozen &&
!item.frozen &&
onHintChange && { onHintChange }) }, `plugin-${item.id}-${item.depth}-${pluginState}`), isActive &&
fieldHintText &&
item.fieldType !== "note" &&
!globalRegistry.shouldAutoSubmit(item.fieldType || "") && (_jsx(HintText, { children: fieldHintText }))] }));
}
// Fallback for unknown types
return null;
}
//# sourceMappingURL=RecursiveGroupContainer.js.map