prompt-language-shell 0.5.2 → 0.6.2

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.
Files changed (40) hide show
  1. package/dist/config/ANSWER.md +4 -0
  2. package/dist/config/CONFIG.md +4 -2
  3. package/dist/config/PLAN.md +23 -0
  4. package/dist/config/VALIDATE.md +12 -11
  5. package/dist/services/anthropic.js +49 -4
  6. package/dist/services/components.js +56 -66
  7. package/dist/services/configuration.js +169 -13
  8. package/dist/services/messages.js +22 -0
  9. package/dist/services/queue.js +2 -2
  10. package/dist/services/refinement.js +37 -0
  11. package/dist/services/task-router.js +141 -0
  12. package/dist/types/types.js +0 -1
  13. package/dist/ui/Answer.js +18 -27
  14. package/dist/ui/Command.js +44 -27
  15. package/dist/ui/Component.js +23 -50
  16. package/dist/ui/Config.js +77 -55
  17. package/dist/ui/Confirm.js +17 -11
  18. package/dist/ui/Execute.js +66 -45
  19. package/dist/ui/Feedback.js +1 -1
  20. package/dist/ui/Introspect.js +26 -23
  21. package/dist/ui/Main.js +71 -100
  22. package/dist/ui/Message.js +1 -1
  23. package/dist/ui/Plan.js +54 -32
  24. package/dist/ui/Refinement.js +6 -7
  25. package/dist/ui/Report.js +1 -1
  26. package/dist/ui/UserQuery.js +6 -0
  27. package/dist/ui/Validate.js +49 -19
  28. package/dist/ui/Welcome.js +1 -1
  29. package/dist/ui/Workflow.js +132 -0
  30. package/package.json +1 -1
  31. package/dist/handlers/answer.js +0 -21
  32. package/dist/handlers/command.js +0 -34
  33. package/dist/handlers/config.js +0 -88
  34. package/dist/handlers/execute.js +0 -46
  35. package/dist/handlers/execution.js +0 -140
  36. package/dist/handlers/introspect.js +0 -21
  37. package/dist/handlers/plan.js +0 -79
  38. package/dist/types/handlers.js +0 -1
  39. package/dist/ui/AnswerDisplay.js +0 -8
  40. package/dist/ui/Column.js +0 -7
@@ -1,8 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import React from 'react';
2
+ import { memo } from 'react';
3
3
  import { ComponentName } from '../types/types.js';
4
4
  import { Answer } from './Answer.js';
5
- import { AnswerDisplay } from './AnswerDisplay.js';
6
5
  import { Command } from './Command.js';
7
6
  import { Confirm } from './Confirm.js';
8
7
  import { Config } from './Config.js';
@@ -15,62 +14,36 @@ import { Refinement } from './Refinement.js';
15
14
  import { Report } from './Report.js';
16
15
  import { Validate } from './Validate.js';
17
16
  import { Welcome } from './Welcome.js';
18
- export const Component = React.memo(function Component({ def, debug, }) {
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;
19
21
  switch (def.name) {
20
22
  case ComponentName.Welcome:
21
23
  return _jsx(Welcome, { ...def.props });
22
- case ComponentName.Config: {
23
- const props = def.props;
24
- const state = def.state;
25
- return _jsx(Config, { ...props, state: state, debug: debug });
26
- }
27
- case ComponentName.Command: {
28
- const props = def.props;
29
- const state = def.state;
30
- return _jsx(Command, { ...props, state: state });
31
- }
32
- case ComponentName.Plan: {
33
- const props = def.props;
34
- const state = def.state;
35
- return _jsx(Plan, { ...props, state: state, debug: debug });
36
- }
24
+ case ComponentName.Config:
25
+ return (_jsx(Config, { ...def.props, state: def.state, isActive: componentIsActive, debug: debug }));
26
+ case ComponentName.Command:
27
+ return _jsx(Command, { ...def.props, state: def.state, isActive: isActive });
28
+ case ComponentName.Plan:
29
+ return (_jsx(Plan, { ...def.props, state: def.state, isActive: componentIsActive, debug: debug }));
37
30
  case ComponentName.Feedback:
38
31
  return _jsx(Feedback, { ...def.props });
39
32
  case ComponentName.Message:
40
33
  return _jsx(Message, { ...def.props });
41
- case ComponentName.Refinement: {
42
- const props = def.props;
43
- const state = def.state;
44
- return _jsx(Refinement, { ...props, state: state });
45
- }
46
- case ComponentName.Confirm: {
47
- const props = def.props;
48
- const state = def.state;
49
- return _jsx(Confirm, { ...props, state: state });
50
- }
51
- case ComponentName.Introspect: {
52
- const props = def.props;
53
- const state = def.state;
54
- return _jsx(Introspect, { ...props, state: state, debug: debug });
55
- }
34
+ case ComponentName.Refinement:
35
+ return (_jsx(Refinement, { ...def.props, state: def.state, isActive: isActive }));
36
+ case ComponentName.Confirm:
37
+ return _jsx(Confirm, { ...def.props, state: def.state, isActive: isActive });
38
+ case ComponentName.Introspect:
39
+ return (_jsx(Introspect, { ...def.props, state: def.state, isActive: componentIsActive, debug: debug }));
56
40
  case ComponentName.Report:
57
41
  return _jsx(Report, { ...def.props });
58
- case ComponentName.Answer: {
59
- const props = def.props;
60
- const state = def.state;
61
- return _jsx(Answer, { ...props, state: state });
62
- }
63
- case ComponentName.AnswerDisplay:
64
- return _jsx(AnswerDisplay, { ...def.props });
65
- case ComponentName.Execute: {
66
- const props = def.props;
67
- const state = def.state;
68
- return _jsx(Execute, { ...props, state: state });
69
- }
70
- case ComponentName.Validate: {
71
- const props = def.props;
72
- const state = def.state;
73
- return _jsx(Validate, { ...props, state: state });
74
- }
42
+ case ComponentName.Answer:
43
+ return _jsx(Answer, { ...def.props, state: def.state, isActive: isActive });
44
+ case ComponentName.Execute:
45
+ return _jsx(Execute, { ...def.props, state: def.state, isActive: isActive });
46
+ case ComponentName.Validate:
47
+ return (_jsx(Validate, { ...def.props, state: def.state, isActive: isActive, debug: debug }));
75
48
  }
76
49
  });
package/dist/ui/Config.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import React from 'react';
2
+ import { useState } from 'react';
3
3
  import { Box, Text, useFocus } from 'ink';
4
4
  import TextInput from 'ink-text-input';
5
5
  import { Colors } from '../services/colors.js';
@@ -10,8 +10,8 @@ export var StepType;
10
10
  StepType["Selection"] = "selection";
11
11
  })(StepType || (StepType = {}));
12
12
  function TextStep({ value, placeholder, validate, onChange, onSubmit, }) {
13
- const [inputValue, setInputValue] = React.useState(value);
14
- const [validationFailed, setValidationFailed] = React.useState(false);
13
+ const [inputValue, setInputValue] = useState(value);
14
+ const [validationFailed, setValidationFailed] = useState(false);
15
15
  const { isFocused } = useFocus({ autoFocus: true });
16
16
  const handleChange = (newValue) => {
17
17
  setInputValue(newValue);
@@ -57,20 +57,27 @@ function SelectionStep({ options, selectedIndex, isCurrentStep, }) {
57
57
  return (_jsx(Box, { marginRight: 2, children: _jsx(Text, { dimColor: !isSelected || !isCurrentStep, bold: isSelected, children: option.label }) }, option.value));
58
58
  }) }));
59
59
  }
60
- export function Config({ steps, state, debug, onFinished, onAborted }) {
61
- const done = state?.done ?? false;
62
- const [step, setStep] = React.useState(done ? steps.length : 0);
63
- const [values, setValues] = React.useState(() => {
60
+ export function Config({ steps, state, isActive = true, debug, handlers, onFinished, onAborted, }) {
61
+ // isActive passed as prop
62
+ const [step, setStep] = useState(!isActive ? (state?.completedStep ?? steps.length) : 0);
63
+ const [values, setValues] = useState(() => {
64
+ // If not active and we have saved state values, use those
65
+ if (!isActive && state?.values) {
66
+ return state.values;
67
+ }
68
+ // Otherwise initialize from step defaults
64
69
  const initial = {};
65
70
  steps.forEach((stepConfig) => {
71
+ // Use full path if available, otherwise use key
72
+ const configKey = stepConfig.path || stepConfig.key;
66
73
  switch (stepConfig.type) {
67
74
  case StepType.Text:
68
75
  if (stepConfig.value !== null) {
69
- initial[stepConfig.key] = stepConfig.value;
76
+ initial[configKey] = stepConfig.value;
70
77
  }
71
78
  break;
72
79
  case StepType.Selection:
73
- initial[stepConfig.key] =
80
+ initial[configKey] =
74
81
  stepConfig.options[stepConfig.defaultIndex].value;
75
82
  break;
76
83
  default: {
@@ -81,10 +88,15 @@ export function Config({ steps, state, debug, onFinished, onAborted }) {
81
88
  });
82
89
  return initial;
83
90
  });
84
- const [inputValue, setInputValue] = React.useState('');
85
- const [selectedIndex, setSelectedIndex] = React.useState(() => {
86
- const firstStep = steps[0];
87
- return firstStep?.type === StepType.Selection ? firstStep.defaultIndex : 0;
91
+ const [inputValue, setInputValue] = useState('');
92
+ const [selectedIndex, setSelectedIndex] = useState(() => {
93
+ // Initialize selectedIndex based on current step's defaultIndex
94
+ if (isActive &&
95
+ step < steps.length &&
96
+ steps[step].type === StepType.Selection) {
97
+ return steps[step].defaultIndex;
98
+ }
99
+ return 0;
88
100
  });
89
101
  const normalizeValue = (value) => {
90
102
  if (value === null || value === undefined) {
@@ -92,21 +104,21 @@ export function Config({ steps, state, debug, onFinished, onAborted }) {
92
104
  }
93
105
  return value.replace(/\n/g, '').trim();
94
106
  };
95
- useInput((input, key) => {
96
- if (key.escape && !done && step < steps.length) {
107
+ useInput((_, key) => {
108
+ if (!isActive || step >= steps.length)
109
+ return;
110
+ const currentStepConfig = steps[step];
111
+ if (key.escape) {
97
112
  // Save current value before aborting
98
- const currentStepConfig = steps[step];
99
113
  if (currentStepConfig) {
114
+ const configKey = currentStepConfig.path || currentStepConfig.key;
100
115
  let currentValue = '';
101
116
  switch (currentStepConfig.type) {
102
117
  case StepType.Text:
103
- currentValue = inputValue || values[currentStepConfig.key] || '';
118
+ currentValue = inputValue || values[configKey] || '';
104
119
  break;
105
120
  case StepType.Selection:
106
- currentValue =
107
- currentStepConfig.options[selectedIndex]?.value ||
108
- values[currentStepConfig.key] ||
109
- '';
121
+ currentValue = values[configKey] || '';
110
122
  break;
111
123
  default: {
112
124
  const exhaustiveCheck = currentStepConfig;
@@ -114,32 +126,21 @@ export function Config({ steps, state, debug, onFinished, onAborted }) {
114
126
  }
115
127
  }
116
128
  if (currentValue) {
117
- setValues({ ...values, [currentStepConfig.key]: currentValue });
129
+ setValues({ ...values, [configKey]: currentValue });
118
130
  }
119
131
  }
120
132
  if (onAborted) {
121
- onAborted();
133
+ onAborted('configuration');
122
134
  }
123
135
  return;
124
136
  }
125
- const currentStep = steps[step];
126
- if (!done && step < steps.length && currentStep) {
127
- switch (currentStep.type) {
128
- case StepType.Selection:
129
- if (key.tab) {
130
- setSelectedIndex((prev) => (prev + 1) % currentStep.options.length);
131
- }
132
- else if (key.return) {
133
- handleSubmit(currentStep.options[selectedIndex].value);
134
- }
135
- break;
136
- case StepType.Text:
137
- // Text input handled by TextInput component
138
- break;
139
- default: {
140
- const exhaustiveCheck = currentStep;
141
- throw new Error(`Unsupported step type: ${exhaustiveCheck}`);
142
- }
137
+ // Handle selection step navigation
138
+ if (currentStepConfig.type === StepType.Selection) {
139
+ if (key.tab) {
140
+ setSelectedIndex((prev) => (prev + 1) % currentStepConfig.options.length);
141
+ }
142
+ else if (key.return) {
143
+ handleSubmit(currentStepConfig.options[selectedIndex].value);
143
144
  }
144
145
  }
145
146
  });
@@ -173,38 +174,59 @@ export function Config({ steps, state, debug, onFinished, onAborted }) {
173
174
  if (!finalValue) {
174
175
  return;
175
176
  }
176
- const newValues = { ...values, [currentStepConfig.key]: finalValue };
177
+ // Use full path if available, otherwise use key
178
+ const configKey = currentStepConfig.path || currentStepConfig.key;
179
+ const newValues = { ...values, [configKey]: finalValue };
177
180
  setValues(newValues);
178
181
  setInputValue('');
179
182
  if (step === steps.length - 1) {
180
183
  // Last step completed
184
+ // IMPORTANT: Update state BEFORE calling onFinished
185
+ // onFinished may call handlers.completeActive(), so state must be saved first
186
+ const stateUpdate = {
187
+ values: newValues,
188
+ completedStep: steps.length,
189
+ };
190
+ handlers?.updateState(stateUpdate);
191
+ // Now call onFinished - this may trigger completeActive()
181
192
  if (onFinished) {
182
193
  onFinished(newValues);
183
194
  }
184
195
  setStep(steps.length);
185
196
  }
186
197
  else {
187
- setStep(step + 1);
188
- // Reset selection index for next step
189
- const nextStep = steps[step + 1];
190
- if (nextStep?.type === StepType.Selection) {
191
- setSelectedIndex(nextStep.defaultIndex);
198
+ // Save state after each step
199
+ const stateUpdate = {
200
+ values: newValues,
201
+ completedStep: step + 1,
202
+ };
203
+ handlers?.updateState(stateUpdate);
204
+ const nextStep = step + 1;
205
+ setStep(nextStep);
206
+ // Reset selectedIndex for next step
207
+ if (nextStep < steps.length &&
208
+ steps[nextStep].type === StepType.Selection) {
209
+ setSelectedIndex(steps[nextStep].defaultIndex);
192
210
  }
193
211
  }
194
212
  };
195
213
  const renderStepInput = (stepConfig, isCurrentStep) => {
214
+ const configKey = stepConfig.path || stepConfig.key;
215
+ // Use state values when inactive, local values when active
216
+ const displayValue = !isActive && state?.values ? state.values[configKey] : values[configKey];
196
217
  switch (stepConfig.type) {
197
218
  case StepType.Text:
198
219
  if (isCurrentStep) {
199
220
  return (_jsx(TextStep, { value: inputValue, placeholder: stepConfig.value || undefined, validate: stepConfig.validate, onChange: setInputValue, onSubmit: handleSubmit }));
200
221
  }
201
- return _jsx(Text, { dimColor: true, children: values[stepConfig.key] || '' });
222
+ return (_jsx(Text, { dimColor: true, wrap: "truncate-end", children: displayValue || '' }));
202
223
  case StepType.Selection: {
203
224
  if (!isCurrentStep) {
204
- const selectedOption = stepConfig.options.find((opt) => opt.value === values[stepConfig.key]);
205
- return _jsx(Text, { dimColor: true, children: selectedOption?.label || '' });
225
+ // Find the option that matches the saved/current value
226
+ const option = stepConfig.options.find((opt) => opt.value === displayValue);
227
+ return _jsx(Text, { dimColor: true, children: option?.label || '' });
206
228
  }
207
- return (_jsx(SelectionStep, { options: stepConfig.options, selectedIndex: selectedIndex, isCurrentStep: isCurrentStep }));
229
+ return (_jsx(SelectionStep, { options: stepConfig.options, selectedIndex: selectedIndex, isCurrentStep: true }));
208
230
  }
209
231
  default: {
210
232
  const exhaustiveCheck = stepConfig;
@@ -212,14 +234,14 @@ export function Config({ steps, state, debug, onFinished, onAborted }) {
212
234
  }
213
235
  }
214
236
  };
215
- return (_jsx(Box, { flexDirection: "column", children: steps.map((stepConfig, index) => {
216
- const isCurrentStep = index === step && !done;
237
+ return (_jsx(Box, { flexDirection: "column", marginLeft: 1, children: steps.map((stepConfig, index) => {
238
+ const isCurrentStep = index === step && isActive;
217
239
  const isCompleted = index < step;
218
- const wasAborted = index === step && done;
240
+ const wasAborted = index === step && !isActive;
219
241
  const shouldShow = isCompleted || isCurrentStep || wasAborted;
220
242
  if (!shouldShow) {
221
243
  return null;
222
244
  }
223
- return (_jsxs(Box, { flexDirection: "column", marginTop: index === 0 ? 0 : 1, children: [_jsxs(Box, { children: [_jsx(Text, { children: stepConfig.description }), _jsx(Text, { children: ": " }), debug && stepConfig.path && (_jsxs(Text, { color: Colors.Type.Define, children: ['{', stepConfig.path, '}'] }))] }), _jsxs(Box, { children: [_jsx(Text, { children: " " }), _jsx(Text, { color: Colors.Action.Select, dimColor: !isCurrentStep, children: ">" }), _jsx(Text, { children: " " }), renderStepInput(stepConfig, isCurrentStep)] })] }, stepConfig.key));
245
+ return (_jsxs(Box, { flexDirection: "column", marginTop: index === 0 ? 0 : 1, children: [_jsxs(Box, { children: [_jsx(Text, { children: stepConfig.description }), _jsx(Text, { children: ": " }), debug && stepConfig.path && (_jsxs(Text, { color: Colors.Type.Define, children: ['{', stepConfig.path, '}'] }))] }), _jsxs(Box, { children: [_jsx(Text, { children: " " }), _jsx(Text, { color: Colors.Action.Select, dimColor: !isCurrentStep, children: ">" }), _jsx(Text, { children: " " }), renderStepInput(stepConfig, isCurrentStep)] })] }, stepConfig.path || stepConfig.key));
224
246
  }) }));
225
247
  }
@@ -1,26 +1,32 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import React from 'react';
2
+ import { useState } from 'react';
3
3
  import { Box, Text } from 'ink';
4
4
  import { Colors, Palette } from '../services/colors.js';
5
5
  import { useInput } from '../services/keyboard.js';
6
- export function Confirm({ message, state, onConfirmed, onCancelled, }) {
7
- const done = state?.done ?? false;
8
- const isCurrent = done === false;
9
- const [selectedIndex, setSelectedIndex] = React.useState(0); // 0 = Yes, 1 = No
6
+ import { UserQuery } from './UserQuery.js';
7
+ export function Confirm({ message, state, isActive = true, handlers, onConfirmed, onCancelled, }) {
8
+ // isActive passed as prop
9
+ const [selectedIndex, setSelectedIndex] = useState(state?.selectedIndex ?? 0); // 0 = Yes, 1 = No
10
10
  useInput((input, key) => {
11
- if (done)
11
+ if (!isActive)
12
12
  return;
13
13
  if (key.escape) {
14
14
  // Escape: highlight "No" and cancel
15
15
  setSelectedIndex(1);
16
+ handlers?.updateState({ selectedIndex: 1 });
16
17
  onCancelled?.();
17
18
  }
18
19
  else if (key.tab) {
19
20
  // Toggle between Yes (0) and No (1)
20
- setSelectedIndex((prev) => (prev === 0 ? 1 : 0));
21
+ setSelectedIndex((prev) => {
22
+ const newIndex = prev === 0 ? 1 : 0;
23
+ handlers?.updateState({ selectedIndex: newIndex });
24
+ return newIndex;
25
+ });
21
26
  }
22
27
  else if (key.return) {
23
28
  // Confirm selection
29
+ handlers?.updateState({ selectedIndex, confirmed: true });
24
30
  if (selectedIndex === 0) {
25
31
  onConfirmed?.();
26
32
  }
@@ -28,16 +34,16 @@ export function Confirm({ message, state, onConfirmed, onCancelled, }) {
28
34
  onCancelled?.();
29
35
  }
30
36
  }
31
- }, { isActive: !done });
37
+ }, { isActive });
32
38
  const options = [
33
39
  { label: 'yes', value: 'yes', color: Palette.BrightGreen },
34
40
  { label: 'no', value: 'no', color: Colors.Status.Error },
35
41
  ];
36
- if (done) {
42
+ if (!isActive) {
37
43
  // When done, show both the message and user's choice in timeline
38
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: undefined, children: message }) }), _jsx(Box, { paddingX: 1, marginX: -1, alignSelf: "flex-start", backgroundColor: Colors.Background.UserQuery, children: _jsxs(Text, { color: Colors.Text.UserQuery, children: ["> ", options[selectedIndex].label] }) })] }));
44
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, marginLeft: 1, children: _jsx(Text, { color: undefined, children: message }) }), _jsxs(UserQuery, { children: ["> ", options[selectedIndex].label] })] }));
39
45
  }
40
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: isCurrent ? Colors.Text.Active : Colors.Text.Inactive, children: message }) }), _jsxs(Box, { children: [_jsx(Text, { color: Colors.Action.Select, children: ">" }), _jsx(Text, { children: " " }), _jsx(Box, { children: options.map((option, index) => {
46
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, marginLeft: 1, children: _jsx(Text, { color: isActive ? Colors.Text.Active : Colors.Text.Inactive, children: message }) }), _jsxs(Box, { marginLeft: 1, children: [_jsx(Text, { color: Colors.Action.Select, children: ">" }), _jsx(Text, { children: " " }), _jsx(Box, { children: options.map((option, index) => {
41
47
  const isSelected = index === selectedIndex;
42
48
  return (_jsx(Box, { marginRight: 2, children: _jsx(Text, { color: isSelected ? option.color : undefined, dimColor: !isSelected, children: option.label }) }, option.value));
43
49
  }) })] })] }));
@@ -85,41 +85,53 @@ function CommandStatusDisplay({ item, elapsed }) {
85
85
  const elapsedTime = getElapsedTime();
86
86
  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
87
  }
88
- export function Execute({ tasks, state, service, onError, onComplete, onAborted, }) {
89
- const done = state?.done ?? false;
90
- const isCurrent = done === false;
91
- const [error, setError] = useState(null);
92
- const [isLoading, setIsLoading] = useState(state?.isLoading ?? !done);
88
+ export function Execute({ tasks, state, isActive = true, service, handlers, }) {
89
+ // isActive passed as prop
90
+ const [error, setError] = useState(state?.error ?? null);
93
91
  const [isExecuting, setIsExecuting] = useState(false);
94
- const [commandStatuses, setCommandStatuses] = useState([]);
95
- const [message, setMessage] = useState('');
92
+ const [commandStatuses, setCommandStatuses] = useState(state?.commandStatuses ?? []);
93
+ const [message, setMessage] = useState(state?.message ?? '');
96
94
  const [currentElapsed, setCurrentElapsed] = useState(0);
97
95
  const [runningIndex, setRunningIndex] = useState(null);
98
96
  const [outputs, setOutputs] = useState([]);
99
- useInput((input, key) => {
100
- if (key.escape && (isLoading || isExecuting) && !done) {
101
- setIsLoading(false);
97
+ const [hasProcessed, setHasProcessed] = useState(false);
98
+ // Derive loading state from current conditions
99
+ const isLoading = isActive &&
100
+ commandStatuses.length === 0 &&
101
+ !error &&
102
+ !isExecuting &&
103
+ !hasProcessed;
104
+ useInput((_, key) => {
105
+ if (key.escape && (isLoading || isExecuting) && isActive) {
102
106
  setIsExecuting(false);
103
107
  setRunningIndex(null);
104
108
  // Mark any running command as aborted when cancelled
105
109
  const now = Date.now();
106
- setCommandStatuses((prev) => prev.map((item) => {
107
- if (item.status === ExecutionStatus.Running) {
108
- const elapsed = item.startTime
109
- ? Math.floor((now - item.startTime) / 1000) * 1000
110
- : undefined;
111
- return {
112
- ...item,
113
- status: ExecutionStatus.Aborted,
114
- endTime: now,
115
- elapsed,
116
- };
117
- }
118
- return item;
119
- }));
120
- onAborted(calculateTotalElapsed(commandStatuses));
110
+ setCommandStatuses((prev) => {
111
+ const updated = prev.map((item) => {
112
+ if (item.status === ExecutionStatus.Running) {
113
+ const elapsed = item.startTime
114
+ ? Math.floor((now - item.startTime) / 1000) * 1000
115
+ : undefined;
116
+ return {
117
+ ...item,
118
+ status: ExecutionStatus.Aborted,
119
+ endTime: now,
120
+ elapsed,
121
+ };
122
+ }
123
+ return item;
124
+ });
125
+ // Save state after updating
126
+ handlers?.updateState({
127
+ commandStatuses: updated,
128
+ message,
129
+ });
130
+ return updated;
131
+ });
132
+ handlers?.onAborted('execution');
121
133
  }
122
- }, { isActive: (isLoading || isExecuting) && !done });
134
+ }, { isActive: (isLoading || isExecuting) && isActive });
123
135
  // Update elapsed time for running command
124
136
  useEffect(() => {
125
137
  if (runningIndex === null)
@@ -139,15 +151,20 @@ export function Execute({ tasks, state, service, onError, onComplete, onAborted,
139
151
  useEffect(() => {
140
152
  if (isExecuting || commandStatuses.length === 0 || !outputs.length)
141
153
  return;
142
- onComplete?.(outputs, calculateTotalElapsed(commandStatuses));
143
- }, [isExecuting, commandStatuses, outputs, onComplete]);
154
+ // Save state before completing
155
+ handlers?.updateState({
156
+ message,
157
+ commandStatuses,
158
+ error,
159
+ });
160
+ handlers?.completeActive();
161
+ }, [isExecuting, commandStatuses, outputs, handlers, message, error]);
144
162
  useEffect(() => {
145
- if (done) {
163
+ if (!isActive) {
146
164
  return;
147
165
  }
148
166
  if (!service) {
149
167
  setError('No service available');
150
- setIsLoading(false);
151
168
  return;
152
169
  }
153
170
  let mounted = true;
@@ -173,9 +190,14 @@ export function Execute({ tasks, state, service, onError, onComplete, onAborted,
173
190
  if (!mounted)
174
191
  return;
175
192
  if (!result.commands || result.commands.length === 0) {
176
- setIsLoading(false);
177
193
  setOutputs([]);
178
- onComplete?.([], 0);
194
+ setHasProcessed(true);
195
+ // Save state before completing
196
+ handlers?.updateState({
197
+ message: result.message,
198
+ commandStatuses: [],
199
+ });
200
+ handlers?.completeActive();
179
201
  return;
180
202
  }
181
203
  // Resolve placeholders in command strings before execution
@@ -190,7 +212,6 @@ export function Execute({ tasks, state, service, onError, onComplete, onAborted,
190
212
  status: ExecutionStatus.Pending,
191
213
  label: tasks[index]?.action,
192
214
  })));
193
- setIsLoading(false);
194
215
  setIsExecuting(true);
195
216
  // Execute commands sequentially
196
217
  const outputs = await executeCommands(resolvedCommands, (progress) => {
@@ -236,14 +257,14 @@ export function Execute({ tasks, state, service, onError, onComplete, onAborted,
236
257
  await ensureMinimumTime(startTime, MINIMUM_PROCESSING_TIME);
237
258
  if (mounted) {
238
259
  const errorMessage = formatErrorMessage(err);
239
- setIsLoading(false);
240
260
  setIsExecuting(false);
241
- if (onError) {
242
- onError(errorMessage);
243
- }
244
- else {
245
- setError(errorMessage);
246
- }
261
+ setError(errorMessage);
262
+ setHasProcessed(true);
263
+ // Save error state
264
+ handlers?.updateState({
265
+ error: errorMessage,
266
+ });
267
+ handlers?.onError(errorMessage);
247
268
  }
248
269
  }
249
270
  }
@@ -251,12 +272,12 @@ export function Execute({ tasks, state, service, onError, onComplete, onAborted,
251
272
  return () => {
252
273
  mounted = false;
253
274
  };
254
- }, [tasks, done, service, onComplete, onError]);
275
+ }, [tasks, isActive, service, handlers]);
255
276
  // Return null only when loading completes with no commands
256
- if (done && commandStatuses.length === 0 && !error) {
277
+ if (!isActive && commandStatuses.length === 0 && !error) {
257
278
  return null;
258
279
  }
259
- // Show completed steps when done
260
- const showCompletedSteps = done && commandStatuses.length > 0;
261
- return (_jsxs(Box, { alignSelf: "flex-start", flexDirection: "column", children: [isLoading && (_jsxs(Box, { children: [_jsx(Text, { color: getTextColor(isCurrent), children: "Preparing commands. " }), _jsx(Spinner, {})] })), (isExecuting || showCompletedSteps) && (_jsxs(Box, { flexDirection: "column", children: [message && (_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { color: getTextColor(isCurrent), children: message }), isExecuting && (_jsxs(Text, { children: [' ', _jsx(Spinner, {})] }))] })), commandStatuses.map((item, index) => (_jsx(Box, { marginBottom: index < commandStatuses.length - 1 ? 1 : 0, children: _jsx(CommandStatusDisplay, { item: item, elapsed: index === runningIndex ? currentElapsed : undefined }) }, index)))] })), error && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: Colors.Status.Error, children: ["Error: ", error] }) }))] }));
280
+ // Show completed steps when not active
281
+ const showCompletedSteps = !isActive && commandStatuses.length > 0;
282
+ return (_jsxs(Box, { alignSelf: "flex-start", flexDirection: "column", children: [isLoading && (_jsxs(Box, { marginLeft: 1, children: [_jsx(Text, { color: getTextColor(isActive), children: "Preparing commands. " }), _jsx(Spinner, {})] })), (isExecuting || showCompletedSteps) && (_jsxs(Box, { flexDirection: "column", marginLeft: 1, children: [message && (_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { color: getTextColor(isActive), children: message }), isExecuting && (_jsxs(Text, { children: [' ', _jsx(Spinner, {})] }))] })), commandStatuses.map((item, index) => (_jsx(Box, { marginBottom: index < commandStatuses.length - 1 ? 1 : 0, children: _jsx(CommandStatusDisplay, { item: item, elapsed: index === runningIndex ? currentElapsed : undefined }) }, index)))] })), error && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: Colors.Status.Error, children: ["Error: ", error] }) }))] }));
262
283
  }
@@ -13,5 +13,5 @@ function getSymbol(type) {
13
13
  export function Feedback({ type, message }) {
14
14
  const color = getFeedbackColor(type, false);
15
15
  const symbol = getSymbol(type);
16
- return (_jsx(Box, { children: _jsxs(Text, { color: color, children: [symbol, " ", message] }) }));
16
+ return (_jsx(Box, { marginLeft: 1, children: _jsxs(Text, { color: color, children: [symbol, " ", message] }) }));
17
17
  }