prompt-language-shell 0.6.2 → 0.6.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1,8 @@
1
- export {};
1
+ // Component lifecycle status
2
+ export var ComponentStatus;
3
+ (function (ComponentStatus) {
4
+ ComponentStatus["Awaiting"] = "awaiting";
5
+ ComponentStatus["Active"] = "active";
6
+ ComponentStatus["Pending"] = "pending";
7
+ ComponentStatus["Done"] = "done";
8
+ })(ComponentStatus || (ComponentStatus = {}));
package/dist/ui/Answer.js CHANGED
@@ -1,13 +1,15 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useEffect, useState } from 'react';
3
3
  import { Box, Text } from 'ink';
4
+ import { ComponentStatus } from '../types/components.js';
4
5
  import { Colors, getTextColor } from '../services/colors.js';
5
6
  import { useInput } from '../services/keyboard.js';
6
7
  import { formatErrorMessage } from '../services/messages.js';
7
8
  import { withMinimumTime } from '../services/timing.js';
8
9
  import { Spinner } from './Spinner.js';
9
10
  const MINIMUM_PROCESSING_TIME = 400;
10
- export function Answer({ question, state, isActive = true, service, handlers, }) {
11
+ export function Answer({ question, state, status, service, handlers, }) {
12
+ const isActive = status === ComponentStatus.Active;
11
13
  const [error, setError] = useState(null);
12
14
  const [answer, setAnswer] = useState(state?.answer ?? null);
13
15
  useInput((input, key) => {
@@ -1,6 +1,7 @@
1
1
  import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useEffect, useState } from 'react';
3
3
  import { Box, Text } from 'ink';
4
+ import { ComponentStatus, } from '../types/components.js';
4
5
  import { TaskType } from '../types/types.js';
5
6
  import { Colors } from '../services/colors.js';
6
7
  import { createPlanDefinition } from '../services/components.js';
@@ -12,7 +13,8 @@ import { ensureMinimumTime } from '../services/timing.js';
12
13
  import { Spinner } from './Spinner.js';
13
14
  import { UserQuery } from './UserQuery.js';
14
15
  const MIN_PROCESSING_TIME = 400; // purely for visual effect
15
- export function Command({ command, state, isActive = true, service, handlers, onAborted, }) {
16
+ export function Command({ command, state, status, service, handlers, onAborted, }) {
17
+ const isActive = status === ComponentStatus.Active;
16
18
  const [error, setError] = useState(state?.error ?? null);
17
19
  useInput((_, key) => {
18
20
  if (key.escape && isActive) {
@@ -66,9 +68,9 @@ export function Command({ command, state, isActive = true, service, handlers, on
66
68
  handlers?.addToQueue(planDefinition);
67
69
  }
68
70
  else {
69
- // No DEFINE tasks: Pass Plan to be added atomically with Command
70
- routeTasksWithConfirm(result.tasks, result.message, svc, command, handlers, false, planDefinition // Pass Plan for atomic update
71
- );
71
+ // No DEFINE tasks: Complete Command, then route to Confirm flow
72
+ handlers?.completeActive();
73
+ routeTasksWithConfirm(result.tasks, result.message, svc, command, handlers, false);
72
74
  }
73
75
  }
74
76
  }
@@ -14,36 +14,33 @@ import { Refinement } from './Refinement.js';
14
14
  import { Report } from './Report.js';
15
15
  import { Validate } from './Validate.js';
16
16
  import { Welcome } from './Welcome.js';
17
- export const Component = memo(function Component({ def, isActive, debug, }) {
18
- // For stateless components, always inactive
19
- const isStatelessComponent = !('state' in def);
20
- const componentIsActive = isStatelessComponent ? false : isActive;
17
+ export const Component = memo(function Component({ def, debug, }) {
21
18
  switch (def.name) {
22
19
  case ComponentName.Welcome:
23
- return _jsx(Welcome, { ...def.props });
20
+ return _jsx(Welcome, { ...def.props, status: def.status });
24
21
  case ComponentName.Config:
25
- return (_jsx(Config, { ...def.props, state: def.state, isActive: componentIsActive, debug: debug }));
22
+ return (_jsx(Config, { ...def.props, state: def.state, status: def.status, debug: debug }));
26
23
  case ComponentName.Command:
27
- return _jsx(Command, { ...def.props, state: def.state, isActive: isActive });
24
+ return _jsx(Command, { ...def.props, state: def.state, status: def.status });
28
25
  case ComponentName.Plan:
29
- return (_jsx(Plan, { ...def.props, state: def.state, isActive: componentIsActive, debug: debug }));
26
+ return (_jsx(Plan, { ...def.props, state: def.state, status: def.status, debug: debug }));
30
27
  case ComponentName.Feedback:
31
- return _jsx(Feedback, { ...def.props });
28
+ return _jsx(Feedback, { ...def.props, status: def.status });
32
29
  case ComponentName.Message:
33
- return _jsx(Message, { ...def.props });
30
+ return _jsx(Message, { ...def.props, status: def.status });
34
31
  case ComponentName.Refinement:
35
- return (_jsx(Refinement, { ...def.props, state: def.state, isActive: isActive }));
32
+ return (_jsx(Refinement, { ...def.props, state: def.state, status: def.status }));
36
33
  case ComponentName.Confirm:
37
- return _jsx(Confirm, { ...def.props, state: def.state, isActive: isActive });
34
+ return _jsx(Confirm, { ...def.props, state: def.state, status: def.status });
38
35
  case ComponentName.Introspect:
39
- return (_jsx(Introspect, { ...def.props, state: def.state, isActive: componentIsActive, debug: debug }));
36
+ return (_jsx(Introspect, { ...def.props, state: def.state, status: def.status, debug: debug }));
40
37
  case ComponentName.Report:
41
- return _jsx(Report, { ...def.props });
38
+ return _jsx(Report, { ...def.props, status: def.status });
42
39
  case ComponentName.Answer:
43
- return _jsx(Answer, { ...def.props, state: def.state, isActive: isActive });
40
+ return _jsx(Answer, { ...def.props, state: def.state, status: def.status });
44
41
  case ComponentName.Execute:
45
- return _jsx(Execute, { ...def.props, state: def.state, isActive: isActive });
42
+ return _jsx(Execute, { ...def.props, state: def.state, status: def.status });
46
43
  case ComponentName.Validate:
47
- return (_jsx(Validate, { ...def.props, state: def.state, isActive: isActive, debug: debug }));
44
+ return (_jsx(Validate, { ...def.props, state: def.state, status: def.status, debug: debug }));
48
45
  }
49
46
  });
package/dist/ui/Config.js CHANGED
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState } from 'react';
3
3
  import { Box, Text, useFocus } from 'ink';
4
4
  import TextInput from 'ink-text-input';
5
+ import { ComponentStatus } from '../types/components.js';
5
6
  import { Colors } from '../services/colors.js';
6
7
  import { useInput } from '../services/keyboard.js';
7
8
  export var StepType;
@@ -57,8 +58,8 @@ function SelectionStep({ options, selectedIndex, isCurrentStep, }) {
57
58
  return (_jsx(Box, { marginRight: 2, children: _jsx(Text, { dimColor: !isSelected || !isCurrentStep, bold: isSelected, children: option.label }) }, option.value));
58
59
  }) }));
59
60
  }
60
- export function Config({ steps, state, isActive = true, debug, handlers, onFinished, onAborted, }) {
61
- // isActive passed as prop
61
+ export function Config({ steps, state, status, debug, handlers, onFinished, onAborted, }) {
62
+ const isActive = status === ComponentStatus.Active;
62
63
  const [step, setStep] = useState(!isActive ? (state?.completedStep ?? steps.length) : 0);
63
64
  const [values, setValues] = useState(() => {
64
65
  // If not active and we have saved state values, use those
@@ -1,11 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState } from 'react';
3
3
  import { Box, Text } from 'ink';
4
+ import { ComponentStatus, } from '../types/components.js';
4
5
  import { Colors, Palette } from '../services/colors.js';
5
6
  import { useInput } from '../services/keyboard.js';
6
7
  import { UserQuery } from './UserQuery.js';
7
- export function Confirm({ message, state, isActive = true, handlers, onConfirmed, onCancelled, }) {
8
- // isActive passed as prop
8
+ export function Confirm({ message, state, status, handlers, onConfirmed, onCancelled, }) {
9
+ const isActive = status === ComponentStatus.Active;
9
10
  const [selectedIndex, setSelectedIndex] = useState(state?.selectedIndex ?? 0); // 0 = Yes, 1 = No
10
11
  useInput((input, key) => {
11
12
  if (!isActive)
@@ -18,11 +19,9 @@ export function Confirm({ message, state, isActive = true, handlers, onConfirmed
18
19
  }
19
20
  else if (key.tab) {
20
21
  // Toggle between Yes (0) and No (1)
21
- setSelectedIndex((prev) => {
22
- const newIndex = prev === 0 ? 1 : 0;
23
- handlers?.updateState({ selectedIndex: newIndex });
24
- return newIndex;
25
- });
22
+ const newIndex = selectedIndex === 0 ? 1 : 0;
23
+ setSelectedIndex(newIndex);
24
+ handlers?.updateState({ selectedIndex: newIndex });
26
25
  }
27
26
  else if (key.return) {
28
27
  // Confirm selection
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useState } from 'react';
3
3
  import { Box, Text } from 'ink';
4
+ import { ComponentStatus } from '../types/components.js';
4
5
  import { Colors, getTextColor, Palette } from '../services/colors.js';
5
6
  import { useInput } from '../services/keyboard.js';
6
7
  import { formatErrorMessage } from '../services/messages.js';
@@ -85,7 +86,8 @@ function CommandStatusDisplay({ item, elapsed }) {
85
86
  const elapsedTime = getElapsedTime();
86
87
  return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { paddingLeft: 2, children: [_jsx(Text, { color: colors.icon, children: STATUS_ICONS[item.status] }), _jsx(Text, { color: colors.description, children: item.label || item.command.description }), elapsedTime !== undefined && (_jsxs(Text, { color: Palette.DarkGray, children: [" (", formatDuration(elapsedTime), ")"] }))] }), _jsxs(Box, { paddingLeft: 5, children: [_jsx(Text, { color: colors.symbol, children: "\u221F " }), _jsx(Text, { color: colors.command, children: item.command.command }), item.status === ExecutionStatus.Running && (_jsxs(Text, { children: [' ', _jsx(Spinner, {})] }))] })] }));
87
88
  }
88
- export function Execute({ tasks, state, isActive = true, service, handlers, }) {
89
+ export function Execute({ tasks, state, status, service, handlers, }) {
90
+ const isActive = status === ComponentStatus.Active;
89
91
  // isActive passed as prop
90
92
  const [error, setError] = useState(state?.error ?? null);
91
93
  const [isExecuting, setIsExecuting] = useState(false);
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useState } from 'react';
3
3
  import { Box, Text } from 'ink';
4
+ import { ComponentStatus, } from '../types/components.js';
4
5
  import { Colors, getTextColor } from '../services/colors.js';
5
6
  import { createReportDefinition } from '../services/components.js';
6
7
  import { useInput } from '../services/keyboard.js';
@@ -23,26 +24,35 @@ function parseCapabilityFromTask(task) {
23
24
  const colonIndex = task.action.indexOf(':');
24
25
  if (colonIndex === -1) {
25
26
  const upperName = task.action.toUpperCase();
27
+ // Check for status markers
28
+ const isIncomplete = task.action.includes('(INCOMPLETE)');
29
+ const cleanName = task.action.replace(/\s*\(INCOMPLETE\)\s*/gi, '').trim();
26
30
  return {
27
- name: task.action,
31
+ name: cleanName,
28
32
  description: '',
29
33
  isBuiltIn: BUILT_IN_CAPABILITIES.has(upperName),
30
34
  isIndirect: INDIRECT_CAPABILITIES.has(upperName),
35
+ isIncomplete,
31
36
  };
32
37
  }
33
38
  const name = task.action.substring(0, colonIndex).trim();
34
39
  const description = task.action.substring(colonIndex + 1).trim();
35
- const upperName = name.toUpperCase();
40
+ // Check for status markers
41
+ const isIncomplete = name.includes('(INCOMPLETE)');
42
+ const cleanName = name.replace(/\s*\(INCOMPLETE\)\s*/gi, '').trim();
43
+ const upperName = cleanName.toUpperCase();
36
44
  const isBuiltIn = BUILT_IN_CAPABILITIES.has(upperName);
37
45
  const isIndirect = INDIRECT_CAPABILITIES.has(upperName);
38
46
  return {
39
- name,
47
+ name: cleanName,
40
48
  description,
41
49
  isBuiltIn,
42
50
  isIndirect,
51
+ isIncomplete,
43
52
  };
44
53
  }
45
- export function Introspect({ tasks, state, isActive = true, service, children, debug = false, handlers, }) {
54
+ export function Introspect({ tasks, state, status, service, children, debug = false, handlers, }) {
55
+ const isActive = status === ComponentStatus.Active;
46
56
  // isActive passed as prop
47
57
  const [error, setError] = useState(null);
48
58
  useInput((input, key) => {
package/dist/ui/Plan.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useState } from 'react';
3
3
  import { Box } from 'ink';
4
+ import { ComponentStatus } from '../types/components.js';
4
5
  import { TaskType } from '../types/types.js';
5
6
  import { getTaskColors } from '../services/colors.js';
6
7
  import { useInput } from '../services/keyboard.js';
@@ -49,7 +50,8 @@ function taskToListItem(task, highlightedChildIndex = null, isDefineTaskWithoutS
49
50
  }
50
51
  return item;
51
52
  }
52
- export function Plan({ message, tasks, state, isActive = true, debug = false, handlers, onSelectionConfirmed, }) {
53
+ export function Plan({ message, tasks, state, status, debug = false, handlers, onSelectionConfirmed, }) {
54
+ const isActive = status === ComponentStatus.Active;
53
55
  // isActive passed as prop
54
56
  const [highlightedIndex, setHighlightedIndex] = useState(state?.highlightedIndex ?? null);
55
57
  const [currentDefineGroupIndex, setCurrentDefineGroupIndex] = useState(state?.currentDefineGroupIndex ?? 0);
@@ -70,9 +72,10 @@ export function Plan({ message, tasks, state, isActive = true, debug = false, ha
70
72
  if (isActive && defineTaskIndices.length === 0 && onSelectionConfirmed) {
71
73
  // No selection needed - all tasks are concrete
72
74
  const concreteTasks = tasks.filter((task) => task.type !== TaskType.Ignore && task.type !== TaskType.Discard);
73
- onSelectionConfirmed(concreteTasks);
74
- // Signal Plan completion after adding Confirm to queue
75
+ // Complete the selection phase - it goes to timeline
76
+ // Callback will create a new Plan showing refined tasks (pending) + Confirm (active)
75
77
  handlers?.completeActive();
78
+ onSelectionConfirmed(concreteTasks);
76
79
  }
77
80
  }, [
78
81
  isActive,
@@ -143,8 +146,8 @@ export function Plan({ message, tasks, state, isActive = true, debug = false, ha
143
146
  }
144
147
  });
145
148
  if (onSelectionConfirmed) {
146
- // Callback will handle the entire flow (Refinement, refined Plan, Confirm)
147
- // So we need to complete the Plan first
149
+ // Complete the selection phase - it goes to timeline
150
+ // Callback will create a new Plan showing refined tasks (pending) + Confirm (active)
148
151
  handlers?.completeActive();
149
152
  onSelectionConfirmed(refinedTasks);
150
153
  }
@@ -1,9 +1,11 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box } from 'ink';
3
+ import { ComponentStatus } from '../types/components.js';
3
4
  import { useInput } from '../services/keyboard.js';
4
5
  import { Message } from './Message.js';
5
6
  import { Spinner } from './Spinner.js';
6
- export const Refinement = ({ text, isActive = true, onAborted, }) => {
7
+ export const Refinement = ({ text, status, onAborted }) => {
8
+ const isActive = status === ComponentStatus.Active;
7
9
  useInput((_, key) => {
8
10
  if (key.escape && isActive) {
9
11
  onAborted('plan refinement');
package/dist/ui/Report.js CHANGED
@@ -1,14 +1,14 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from 'ink';
3
3
  import { Colors } from '../services/colors.js';
4
- function CapabilityItem({ name, description, isBuiltIn, isIndirect, }) {
4
+ function CapabilityItem({ name, description, isBuiltIn, isIndirect, isIncomplete, }) {
5
5
  const color = isIndirect
6
6
  ? Colors.Origin.Indirect
7
7
  : isBuiltIn
8
8
  ? Colors.Origin.BuiltIn
9
9
  : Colors.Origin.UserProvided;
10
- return (_jsxs(Box, { children: [_jsx(Text, { children: "- " }), _jsx(Text, { color: color, children: name }), _jsxs(Text, { children: [" - ", description] })] }));
10
+ return (_jsxs(Box, { children: [_jsx(Text, { children: "- " }), _jsx(Text, { color: color, children: name }), _jsxs(Text, { children: [" - ", description] }), isIncomplete && _jsx(Text, { color: Colors.Status.Warning, children: " (incomplete)" })] }));
11
11
  }
12
12
  export function Report({ message, capabilities }) {
13
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginLeft: 1, children: _jsx(Text, { children: message }) }), _jsx(Box, { flexDirection: "column", marginLeft: 3, marginTop: 1, children: capabilities.map((capability, index) => (_jsx(CapabilityItem, { name: capability.name, description: capability.description, isBuiltIn: capability.isBuiltIn, isIndirect: capability.isIndirect }, index))) })] }));
13
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginLeft: 1, children: _jsx(Text, { children: message }) }), _jsx(Box, { flexDirection: "column", marginLeft: 3, marginTop: 1, children: capabilities.map((capability, index) => (_jsx(CapabilityItem, { name: capability.name, description: capability.description, isBuiltIn: capability.isBuiltIn, isIndirect: capability.isIndirect, isIncomplete: capability.isIncomplete }, index))) })] }));
14
14
  }
@@ -1,6 +1,7 @@
1
1
  import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useEffect, useState } from 'react';
3
3
  import { Box, Text } from 'ink';
4
+ import { ComponentStatus } from '../types/components.js';
4
5
  import { TaskType } from '../types/types.js';
5
6
  import { Colors, getTextColor } from '../services/colors.js';
6
7
  import { useInput } from '../services/keyboard.js';
@@ -10,8 +11,8 @@ import { saveConfig, unflattenConfig } from '../services/configuration.js';
10
11
  import { Config, StepType } from './Config.js';
11
12
  import { Spinner } from './Spinner.js';
12
13
  const MIN_PROCESSING_TIME = 1000;
13
- export function Validate({ missingConfig, userRequest, state, isActive = true, service, children, debug, onError, onComplete, onAborted, handlers, }) {
14
- // isActive passed as prop
14
+ export function Validate({ missingConfig, userRequest, state, status, service, children, debug, onError, onComplete, onAborted, handlers, }) {
15
+ const isActive = status === ComponentStatus.Active;
15
16
  const [error, setError] = useState(null);
16
17
  const [completionMessage, setCompletionMessage] = useState(null);
17
18
  const [configRequirements, setConfigRequirements] = useState(null);
@@ -132,7 +133,7 @@ export function Validate({ missingConfig, userRequest, state, isActive = true, s
132
133
  const handleConfigAborted = (operation) => {
133
134
  onAborted(operation);
134
135
  };
135
- return (_jsxs(Box, { alignSelf: "flex-start", flexDirection: "column", children: [isActive && !completionMessage && !error && (_jsxs(Box, { marginLeft: 1, children: [_jsxs(Text, { color: getTextColor(isActive), children: ["Validating configuration requirements.", ' '] }), _jsx(Spinner, {})] })), completionMessage && (_jsx(Box, { marginLeft: 1, children: _jsx(Text, { color: getTextColor(isActive), children: completionMessage }) })), error && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: Colors.Status.Error, children: ["Error: ", error] }) })), configSteps && !error && (_jsx(Box, { marginTop: 1, children: _jsx(Config, { steps: configSteps, isActive: isActive, debug: debug, onFinished: handleConfigFinished, onAborted: handleConfigAborted, handlers: handlers }) })), children] }));
136
+ return (_jsxs(Box, { alignSelf: "flex-start", flexDirection: "column", children: [isActive && !completionMessage && !error && (_jsxs(Box, { marginLeft: 1, children: [_jsxs(Text, { color: getTextColor(isActive), children: ["Validating configuration requirements.", ' '] }), _jsx(Spinner, {})] })), completionMessage && (_jsx(Box, { marginLeft: 1, children: _jsx(Text, { color: getTextColor(isActive), children: completionMessage }) })), error && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: Colors.Status.Error, children: ["Error: ", error] }) })), configSteps && !error && (_jsx(Box, { marginTop: 1, children: _jsx(Config, { steps: configSteps, status: status, debug: debug, onFinished: handleConfigFinished, onAborted: handleConfigAborted, handlers: handlers }) })), children] }));
136
137
  }
137
138
  /**
138
139
  * Build prompt for VALIDATE tool
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
+ import { useCallback, useEffect, useMemo, useState } from 'react';
3
3
  import { Box, Static } from 'ink';
4
+ import { ComponentStatus, } from '../types/components.js';
4
5
  import { ComponentName, FeedbackType } from '../types/types.js';
5
6
  import { createFeedback, isStateless, markAsDone, } from '../services/components.js';
6
7
  import { exitApp } from '../services/process.js';
@@ -8,36 +9,82 @@ import { getCancellationMessage } from '../services/messages.js';
8
9
  import { Component } from './Component.js';
9
10
  export const Workflow = ({ initialQueue, debug }) => {
10
11
  const [timeline, setTimeline] = useState([]);
11
- const [active, setActive] = useState(null);
12
+ const [current, setCurrent] = useState({ active: null, pending: null });
12
13
  const [queue, setQueue] = useState(initialQueue);
13
- // Ref to track active component for synchronous access
14
- const activeRef = useRef(null);
15
- // Keep ref in sync with active state
16
- useEffect(() => {
17
- activeRef.current = active;
18
- }, [active]);
19
- // Function to move active component to timeline with optional additional items
20
- const moveActiveToTimeline = useCallback((...items) => {
21
- const curr = activeRef.current;
22
- if (!curr) {
23
- // No active component, just add items if provided
24
- if (items.length > 0) {
25
- setTimeline((prev) => [...prev, ...items]);
26
- }
27
- return;
28
- }
29
- const doneComponent = markAsDone(curr);
30
- // Atomic update: add active component and any additional items
31
- setTimeline((prev) => items.length > 0
32
- ? [...prev, doneComponent, ...items]
33
- : [...prev, doneComponent]);
34
- setActive(null);
14
+ // Function to move active to pending (component just completed)
15
+ const moveActiveToPending = useCallback(() => {
16
+ setCurrent((curr) => {
17
+ const { active } = curr;
18
+ if (!active)
19
+ return curr;
20
+ // Move active to pending without marking as done
21
+ const pendingComponent = { ...active, status: ComponentStatus.Pending };
22
+ return { active: null, pending: pendingComponent };
23
+ });
24
+ }, []);
25
+ // Function to move active directly to timeline (error/abort)
26
+ const moveActiveToTimeline = useCallback(() => {
27
+ setCurrent((curr) => {
28
+ const { active, pending } = curr;
29
+ if (!active)
30
+ return curr;
31
+ // Mark as done and add to timeline
32
+ const doneComponent = markAsDone(active);
33
+ setTimeline((prev) => [...prev, doneComponent]);
34
+ return { active: null, pending };
35
+ });
35
36
  }, []);
36
37
  // Global handlers for all stateful components
37
38
  const handlers = useMemo(() => ({
39
+ addToQueue: (...items) => {
40
+ setQueue((queue) => [...queue, ...items]);
41
+ },
42
+ updateState: (newState) => {
43
+ setCurrent((curr) => {
44
+ const { active, pending } = curr;
45
+ if (!active || !('state' in active))
46
+ return curr;
47
+ const stateful = active;
48
+ const updated = {
49
+ ...stateful,
50
+ state: {
51
+ ...stateful.state,
52
+ ...newState,
53
+ },
54
+ };
55
+ return { active: updated, pending };
56
+ });
57
+ },
58
+ completeActive: (...items) => {
59
+ moveActiveToPending();
60
+ if (items.length > 0) {
61
+ setQueue((queue) => [...items, ...queue]);
62
+ }
63
+ },
64
+ completeActiveAndPending: (...items) => {
65
+ setCurrent((curr) => {
66
+ const { active, pending } = curr;
67
+ // Move both to timeline - pending first (Plan), then active (Confirm)
68
+ if (pending) {
69
+ const donePending = markAsDone(pending);
70
+ setTimeline((prev) => [...prev, donePending]);
71
+ }
72
+ if (active) {
73
+ const doneActive = markAsDone(active);
74
+ setTimeline((prev) => [...prev, doneActive]);
75
+ }
76
+ return { active: null, pending: null };
77
+ });
78
+ if (items.length > 0) {
79
+ setQueue((queue) => [...items, ...queue]);
80
+ }
81
+ },
82
+ addToTimeline: (...items) => {
83
+ setTimeline((prev) => [...prev, ...items]);
84
+ },
38
85
  onAborted: (operation) => {
39
86
  moveActiveToTimeline();
40
- // Add feedback to queue and exit
87
+ // Add feedback to queue
41
88
  const message = getCancellationMessage(operation);
42
89
  setQueue((queue) => [
43
90
  ...queue,
@@ -46,76 +93,82 @@ export const Workflow = ({ initialQueue, debug }) => {
46
93
  },
47
94
  onError: (error) => {
48
95
  moveActiveToTimeline();
49
- // Add feedback to queue and exit with error code
96
+ // Add feedback to queue
50
97
  setQueue((queue) => [
51
98
  ...queue,
52
99
  createFeedback(FeedbackType.Failed, error),
53
100
  ]);
54
101
  },
55
- addToQueue: (...items) => {
56
- setQueue((queue) => [...queue, ...items]);
57
- },
58
- addToTimeline: (...items) => {
59
- setTimeline((prev) => [...prev, ...items]);
60
- },
61
- completeActive: (...items) => {
62
- moveActiveToTimeline(...items);
63
- },
64
- updateState: (newState) => {
65
- setActive((curr) => {
66
- if (!curr || !('state' in curr))
67
- return curr;
68
- const stateful = curr;
69
- const updated = {
70
- ...stateful,
71
- state: {
72
- ...stateful.state,
73
- ...newState,
74
- },
75
- };
76
- // Update ref synchronously so moveActiveToTimeline sees the latest state
77
- activeRef.current = updated;
78
- return updated;
79
- });
80
- },
81
- }), [moveActiveToTimeline]);
102
+ }), [moveActiveToPending, moveActiveToTimeline]);
82
103
  // Global Esc handler removed - components handle their own Esc individually
83
104
  // Move next item from queue to active
84
105
  useEffect(() => {
85
- if (queue.length > 0 && active === null) {
86
- const [first, ...rest] = queue;
106
+ const { active, pending } = current;
107
+ // Early return: not ready to activate next
108
+ if (queue.length === 0 || active !== null) {
109
+ return;
110
+ }
111
+ const [first, ...rest] = queue;
112
+ const activeComponent = { ...first, status: ComponentStatus.Active };
113
+ // Confirm - keep pending visible (Plan showing what will execute)
114
+ if (first.name === ComponentName.Confirm) {
87
115
  setQueue(rest);
88
- setActive(first);
116
+ setCurrent({ active: activeComponent, pending });
117
+ return;
89
118
  }
90
- }, [queue, active]);
119
+ // Other components - move pending to timeline first, then activate
120
+ if (pending) {
121
+ const donePending = markAsDone(pending);
122
+ setTimeline((prev) => [...prev, donePending]);
123
+ }
124
+ setQueue(rest);
125
+ setCurrent({ active: activeComponent, pending: null });
126
+ }, [queue, current]);
91
127
  // Process active component - stateless components auto-move to timeline
92
128
  useEffect(() => {
129
+ const { active, pending } = current;
93
130
  if (!active)
94
131
  return;
95
132
  if (isStateless(active)) {
133
+ // Stateless components move directly to timeline
96
134
  const doneComponent = markAsDone(active);
97
135
  setTimeline((prev) => [...prev, doneComponent]);
98
- setActive(null);
136
+ setCurrent({ active: null, pending });
99
137
  }
100
- // Stateful components stay in active until handlers move them to timeline
101
- }, [active]);
102
- // Exit when all done
138
+ // Stateful components stay in active until handlers move them to pending
139
+ }, [current]);
140
+ // Move final pending to timeline and exit when all done
103
141
  useEffect(() => {
104
- if (active === null && queue.length === 0 && timeline.length > 0) {
105
- // Check if last item in timeline is a failed feedback
106
- const lastItem = timeline[timeline.length - 1];
107
- const isFailed = lastItem.name === ComponentName.Feedback &&
108
- lastItem.props.type === FeedbackType.Failed;
109
- exitApp(isFailed ? 1 : 0);
142
+ const { active, pending } = current;
143
+ // Early return: not ready to finish
144
+ if (active !== null || queue.length > 0) {
145
+ return;
146
+ }
147
+ // Handle pending component
148
+ if (pending) {
149
+ const donePending = markAsDone(pending);
150
+ setTimeline((prev) => [...prev, donePending]);
151
+ setCurrent({ active: null, pending: null });
152
+ return;
110
153
  }
111
- }, [active, queue, timeline]);
112
- // Inject global handlers into active component
154
+ // Early return: nothing to exit with
155
+ if (timeline.length === 0) {
156
+ return;
157
+ }
158
+ // Everything is done, exit
159
+ const lastItem = timeline[timeline.length - 1];
160
+ const isFailed = lastItem.name === ComponentName.Feedback &&
161
+ lastItem.props.type === FeedbackType.Failed;
162
+ exitApp(isFailed ? 1 : 0);
163
+ }, [current, queue, timeline]);
164
+ // Render active and pending components
113
165
  const activeComponent = useMemo(() => {
166
+ const { active } = current;
114
167
  if (!active)
115
168
  return null;
116
- // For stateless components, render as-is with isActive=true
169
+ // For stateless components, render as-is
117
170
  if (isStateless(active)) {
118
- return (_jsx(Component, { def: active, isActive: true, debug: debug }, active.id));
171
+ return _jsx(Component, { def: active, debug: debug }, active.id);
119
172
  }
120
173
  // For stateful components, inject global handlers
121
174
  const statefulActive = active;
@@ -126,7 +179,14 @@ export const Workflow = ({ initialQueue, debug }) => {
126
179
  handlers,
127
180
  },
128
181
  };
129
- return (_jsx(Component, { def: wrappedDef, isActive: true, debug: debug }, active.id));
130
- }, [active, debug, handlers]);
131
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: timeline, children: (item) => (_jsx(Box, { marginTop: 1, children: _jsx(Component, { def: item, isActive: false, debug: debug }) }, item.id)) }, "timeline"), _jsx(Box, { marginTop: 1, children: activeComponent })] }));
182
+ return _jsx(Component, { def: wrappedDef, debug: debug }, active.id);
183
+ }, [current, debug, handlers]);
184
+ const pendingComponent = useMemo(() => {
185
+ const { pending } = current;
186
+ if (!pending)
187
+ return null;
188
+ // Pending components don't receive input
189
+ return _jsx(Component, { def: pending, debug: debug }, pending.id);
190
+ }, [current, debug]);
191
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: timeline, children: (item) => (_jsx(Box, { marginTop: 1, children: _jsx(Component, { def: item, debug: false }) }, item.id)) }, "timeline"), pendingComponent && _jsx(Box, { marginTop: 1, children: pendingComponent }), activeComponent && _jsx(Box, { marginTop: 1, children: activeComponent })] }));
132
192
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prompt-language-shell",
3
- "version": "0.6.2",
3
+ "version": "0.6.6",
4
4
  "description": "Your personal command-line concierge. Ask politely, and it gets things done.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",