prompt-language-shell 0.6.0 → 0.6.4

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.
@@ -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
@@ -90,8 +91,13 @@ export function Config({ steps, state, isActive = true, debug, handlers, onFinis
90
91
  });
91
92
  const [inputValue, setInputValue] = useState('');
92
93
  const [selectedIndex, setSelectedIndex] = useState(() => {
93
- const firstStep = steps[0];
94
- return firstStep?.type === StepType.Selection ? firstStep.defaultIndex : 0;
94
+ // Initialize selectedIndex based on current step's defaultIndex
95
+ if (isActive &&
96
+ step < steps.length &&
97
+ steps[step].type === StepType.Selection) {
98
+ return steps[step].defaultIndex;
99
+ }
100
+ return 0;
95
101
  });
96
102
  const normalizeValue = (value) => {
97
103
  if (value === null || value === undefined) {
@@ -99,10 +105,12 @@ export function Config({ steps, state, isActive = true, debug, handlers, onFinis
99
105
  }
100
106
  return value.replace(/\n/g, '').trim();
101
107
  };
102
- useInput((input, key) => {
103
- if (key.escape && isActive && step < steps.length) {
108
+ useInput((_, key) => {
109
+ if (!isActive || step >= steps.length)
110
+ return;
111
+ const currentStepConfig = steps[step];
112
+ if (key.escape) {
104
113
  // Save current value before aborting
105
- const currentStepConfig = steps[step];
106
114
  if (currentStepConfig) {
107
115
  const configKey = currentStepConfig.path || currentStepConfig.key;
108
116
  let currentValue = '';
@@ -111,10 +119,7 @@ export function Config({ steps, state, isActive = true, debug, handlers, onFinis
111
119
  currentValue = inputValue || values[configKey] || '';
112
120
  break;
113
121
  case StepType.Selection:
114
- currentValue =
115
- currentStepConfig.options[selectedIndex]?.value ||
116
- values[configKey] ||
117
- '';
122
+ currentValue = values[configKey] || '';
118
123
  break;
119
124
  default: {
120
125
  const exhaustiveCheck = currentStepConfig;
@@ -130,24 +135,13 @@ export function Config({ steps, state, isActive = true, debug, handlers, onFinis
130
135
  }
131
136
  return;
132
137
  }
133
- const currentStep = steps[step];
134
- if (isActive && step < steps.length && currentStep) {
135
- switch (currentStep.type) {
136
- case StepType.Selection:
137
- if (key.tab) {
138
- setSelectedIndex((prev) => (prev + 1) % currentStep.options.length);
139
- }
140
- else if (key.return) {
141
- handleSubmit(currentStep.options[selectedIndex].value);
142
- }
143
- break;
144
- case StepType.Text:
145
- // Text input handled by TextInput component
146
- break;
147
- default: {
148
- const exhaustiveCheck = currentStep;
149
- throw new Error(`Unsupported step type: ${exhaustiveCheck}`);
150
- }
138
+ // Handle selection step navigation
139
+ if (currentStepConfig.type === StepType.Selection) {
140
+ if (key.tab) {
141
+ setSelectedIndex((prev) => (prev + 1) % currentStepConfig.options.length);
142
+ }
143
+ else if (key.return) {
144
+ handleSubmit(currentStepConfig.options[selectedIndex].value);
151
145
  }
152
146
  }
153
147
  });
@@ -188,35 +182,38 @@ export function Config({ steps, state, isActive = true, debug, handlers, onFinis
188
182
  setInputValue('');
189
183
  if (step === steps.length - 1) {
190
184
  // Last step completed
185
+ // IMPORTANT: Update state BEFORE calling onFinished
186
+ // onFinished may call handlers.completeActive(), so state must be saved first
187
+ const stateUpdate = {
188
+ values: newValues,
189
+ completedStep: steps.length,
190
+ };
191
+ handlers?.updateState(stateUpdate);
192
+ // Now call onFinished - this may trigger completeActive()
191
193
  if (onFinished) {
192
194
  onFinished(newValues);
193
195
  }
194
- // Save state before completing
195
- handlers?.updateState({
196
- values: newValues,
197
- completedStep: steps.length,
198
- });
199
- // Signal Workflow that config is complete
200
- handlers?.onComplete();
201
196
  setStep(steps.length);
202
197
  }
203
198
  else {
204
199
  // Save state after each step
205
- handlers?.updateState({
200
+ const stateUpdate = {
206
201
  values: newValues,
207
202
  completedStep: step + 1,
208
- });
209
- setStep(step + 1);
210
- // Reset selection index for next step
211
- const nextStep = steps[step + 1];
212
- if (nextStep?.type === StepType.Selection) {
213
- setSelectedIndex(nextStep.defaultIndex);
203
+ };
204
+ handlers?.updateState(stateUpdate);
205
+ const nextStep = step + 1;
206
+ setStep(nextStep);
207
+ // Reset selectedIndex for next step
208
+ if (nextStep < steps.length &&
209
+ steps[nextStep].type === StepType.Selection) {
210
+ setSelectedIndex(steps[nextStep].defaultIndex);
214
211
  }
215
212
  }
216
213
  };
217
214
  const renderStepInput = (stepConfig, isCurrentStep) => {
218
215
  const configKey = stepConfig.path || stepConfig.key;
219
- // Use state values if not active (in timeline), otherwise use local values
216
+ // Use state values when inactive, local values when active
220
217
  const displayValue = !isActive && state?.values ? state.values[configKey] : values[configKey];
221
218
  switch (stepConfig.type) {
222
219
  case StepType.Text:
@@ -226,10 +223,11 @@ export function Config({ steps, state, isActive = true, debug, handlers, onFinis
226
223
  return (_jsx(Text, { dimColor: true, wrap: "truncate-end", children: displayValue || '' }));
227
224
  case StepType.Selection: {
228
225
  if (!isCurrentStep) {
229
- const selectedOption = stepConfig.options.find((opt) => opt.value === displayValue);
230
- return _jsx(Text, { dimColor: true, children: selectedOption?.label || '' });
226
+ // Find the option that matches the saved/current value
227
+ const option = stepConfig.options.find((opt) => opt.value === displayValue);
228
+ return _jsx(Text, { dimColor: true, children: option?.label || '' });
231
229
  }
232
- return (_jsx(SelectionStep, { options: stepConfig.options, selectedIndex: selectedIndex, isCurrentStep: isCurrentStep }));
230
+ return (_jsx(SelectionStep, { options: stepConfig.options, selectedIndex: selectedIndex, isCurrentStep: true }));
233
231
  }
234
232
  default: {
235
233
  const exhaustiveCheck = stepConfig;
@@ -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);
@@ -157,7 +159,7 @@ export function Execute({ tasks, state, isActive = true, service, handlers, }) {
157
159
  commandStatuses,
158
160
  error,
159
161
  });
160
- handlers?.onComplete();
162
+ handlers?.completeActive();
161
163
  }, [isExecuting, commandStatuses, outputs, handlers, message, error]);
162
164
  useEffect(() => {
163
165
  if (!isActive) {
@@ -197,7 +199,7 @@ export function Execute({ tasks, state, isActive = true, service, handlers, }) {
197
199
  message: result.message,
198
200
  commandStatuses: [],
199
201
  });
200
- handlers?.onComplete();
202
+ handlers?.completeActive();
201
203
  return;
202
204
  }
203
205
  // Resolve placeholders in command strings before execution
@@ -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';
@@ -42,7 +43,8 @@ function parseCapabilityFromTask(task) {
42
43
  isIndirect,
43
44
  };
44
45
  }
45
- export function Introspect({ tasks, state, isActive = true, service, children, debug = false, handlers, }) {
46
+ export function Introspect({ tasks, state, status, service, children, debug = false, handlers, }) {
47
+ const isActive = status === ComponentStatus.Active;
46
48
  // isActive passed as prop
47
49
  const [error, setError] = useState(null);
48
50
  useInput((input, key) => {
@@ -86,7 +88,7 @@ export function Introspect({ tasks, state, isActive = true, service, children, d
86
88
  // Add Report component to queue
87
89
  handlers?.addToQueue(createReportDefinition(result.message, capabilities));
88
90
  // Signal completion
89
- handlers?.onComplete();
91
+ handlers?.completeActive();
90
92
  }
91
93
  }
92
94
  catch (err) {
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);
75
+ // Complete the selection phase - it goes to timeline
76
+ // Callback will create a new Plan showing refined tasks (pending) + Confirm (active)
77
+ handlers?.completeActive();
73
78
  onSelectionConfirmed(concreteTasks);
74
- // Signal Plan completion after adding Confirm to queue
75
- handlers?.onComplete();
76
79
  }
77
80
  }, [
78
81
  isActive,
@@ -143,14 +146,14 @@ 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
148
- handlers?.onComplete();
149
+ // Complete the selection phase - it goes to timeline
150
+ // Callback will create a new Plan showing refined tasks (pending) + Confirm (active)
151
+ handlers?.completeActive();
149
152
  onSelectionConfirmed(refinedTasks);
150
153
  }
151
154
  else {
152
155
  // No selection callback, just complete normally
153
- handlers?.onComplete();
156
+ handlers?.completeActive();
154
157
  }
155
158
  }
156
159
  }
@@ -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');
@@ -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
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,26 +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
- // Function to move active component to timeline
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)
14
26
  const moveActiveToTimeline = useCallback(() => {
15
- setActive((curr) => {
16
- if (!curr)
17
- return null;
18
- const doneComponent = markAsDone(curr);
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);
19
33
  setTimeline((prev) => [...prev, doneComponent]);
20
- return null;
34
+ return { active: null, pending };
21
35
  });
22
36
  }, []);
23
37
  // Global handlers for all stateful components
24
38
  const handlers = useMemo(() => ({
25
- onComplete: () => {
26
- moveActiveToTimeline();
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]);
27
84
  },
28
85
  onAborted: (operation) => {
29
86
  moveActiveToTimeline();
30
- // Add feedback to queue and exit
87
+ // Add feedback to queue
31
88
  const message = getCancellationMessage(operation);
32
89
  setQueue((queue) => [
33
90
  ...queue,
@@ -36,73 +93,82 @@ export const Workflow = ({ initialQueue, debug }) => {
36
93
  },
37
94
  onError: (error) => {
38
95
  moveActiveToTimeline();
39
- // Add feedback to queue and exit with error code
96
+ // Add feedback to queue
40
97
  setQueue((queue) => [
41
98
  ...queue,
42
99
  createFeedback(FeedbackType.Failed, error),
43
100
  ]);
44
101
  },
45
- addToQueue: (...items) => {
46
- setQueue((queue) => [...queue, ...items]);
47
- },
48
- addToTimeline: (...items) => {
49
- setTimeline((prev) => [...prev, ...items]);
50
- },
51
- completeActive: () => {
52
- moveActiveToTimeline();
53
- },
54
- updateState: (newState) => {
55
- setActive((curr) => {
56
- if (!curr || !('state' in curr))
57
- return curr;
58
- const stateful = curr;
59
- return {
60
- ...stateful,
61
- state: {
62
- ...stateful.state,
63
- ...newState,
64
- },
65
- };
66
- });
67
- },
68
- }), [moveActiveToTimeline]);
102
+ }), [moveActiveToPending, moveActiveToTimeline]);
69
103
  // Global Esc handler removed - components handle their own Esc individually
70
104
  // Move next item from queue to active
71
105
  useEffect(() => {
72
- if (queue.length > 0 && active === null) {
73
- 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) {
74
115
  setQueue(rest);
75
- setActive(first);
116
+ setCurrent({ active: activeComponent, pending });
117
+ return;
118
+ }
119
+ // Other components - move pending to timeline first, then activate
120
+ if (pending) {
121
+ const donePending = markAsDone(pending);
122
+ setTimeline((prev) => [...prev, donePending]);
76
123
  }
77
- }, [queue, active]);
124
+ setQueue(rest);
125
+ setCurrent({ active: activeComponent, pending: null });
126
+ }, [queue, current]);
78
127
  // Process active component - stateless components auto-move to timeline
79
128
  useEffect(() => {
129
+ const { active, pending } = current;
80
130
  if (!active)
81
131
  return;
82
132
  if (isStateless(active)) {
133
+ // Stateless components move directly to timeline
83
134
  const doneComponent = markAsDone(active);
84
135
  setTimeline((prev) => [...prev, doneComponent]);
85
- setActive(null);
136
+ setCurrent({ active: null, pending });
86
137
  }
87
- // Stateful components stay in active until handlers move them to timeline
88
- }, [active]);
89
- // 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
90
141
  useEffect(() => {
91
- if (active === null && queue.length === 0 && timeline.length > 0) {
92
- // Check if last item in timeline is a failed feedback
93
- const lastItem = timeline[timeline.length - 1];
94
- const isFailed = lastItem.name === ComponentName.Feedback &&
95
- lastItem.props.type === FeedbackType.Failed;
96
- 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;
97
153
  }
98
- }, [active, queue, timeline]);
99
- // 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
100
165
  const activeComponent = useMemo(() => {
166
+ const { active } = current;
101
167
  if (!active)
102
168
  return null;
103
- // For stateless components, render as-is with isActive=true
169
+ // For stateless components, render as-is
104
170
  if (isStateless(active)) {
105
- return (_jsx(Component, { def: active, isActive: true, debug: debug }, active.id));
171
+ return _jsx(Component, { def: active, debug: debug }, active.id);
106
172
  }
107
173
  // For stateful components, inject global handlers
108
174
  const statefulActive = active;
@@ -113,7 +179,14 @@ export const Workflow = ({ initialQueue, debug }) => {
113
179
  handlers,
114
180
  },
115
181
  };
116
- return (_jsx(Component, { def: wrappedDef, isActive: true, debug: debug }, active.id));
117
- }, [active, debug, handlers]);
118
- 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 })] }));
119
192
  };