prompt-language-shell 0.8.2 → 0.8.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.
Files changed (45) hide show
  1. package/dist/configuration/io.js +85 -0
  2. package/dist/configuration/messages.js +30 -0
  3. package/dist/configuration/schema.js +167 -0
  4. package/dist/configuration/transformation.js +55 -0
  5. package/dist/configuration/types.js +30 -0
  6. package/dist/configuration/validation.js +52 -0
  7. package/dist/execution/handlers.js +135 -0
  8. package/dist/execution/processing.js +35 -0
  9. package/dist/execution/reducer.js +148 -0
  10. package/dist/execution/types.js +12 -0
  11. package/dist/execution/validation.js +12 -0
  12. package/dist/index.js +1 -1
  13. package/dist/services/anthropic.js +43 -56
  14. package/dist/services/colors.js +2 -1
  15. package/dist/services/components.js +40 -24
  16. package/dist/services/config-labels.js +15 -15
  17. package/dist/services/filesystem.js +114 -0
  18. package/dist/services/loader.js +8 -5
  19. package/dist/services/logger.js +26 -1
  20. package/dist/services/messages.js +32 -1
  21. package/dist/services/parser.js +3 -1
  22. package/dist/services/refinement.js +10 -10
  23. package/dist/services/router.js +43 -27
  24. package/dist/services/skills.js +12 -11
  25. package/dist/services/validator.js +4 -3
  26. package/dist/types/guards.js +4 -6
  27. package/dist/types/handlers.js +1 -0
  28. package/dist/types/schemas.js +103 -0
  29. package/dist/types/types.js +1 -0
  30. package/dist/ui/Answer.js +38 -16
  31. package/dist/ui/Command.js +48 -22
  32. package/dist/ui/Component.js +147 -33
  33. package/dist/ui/Config.js +69 -78
  34. package/dist/ui/Confirm.js +34 -21
  35. package/dist/ui/Execute.js +151 -178
  36. package/dist/ui/Feedback.js +1 -0
  37. package/dist/ui/Introspect.js +54 -25
  38. package/dist/ui/Label.js +1 -1
  39. package/dist/ui/Main.js +10 -6
  40. package/dist/ui/Refinement.js +8 -1
  41. package/dist/ui/Schedule.js +76 -53
  42. package/dist/ui/Validate.js +77 -77
  43. package/dist/ui/Workflow.js +60 -61
  44. package/package.json +3 -2
  45. package/dist/services/configuration.js +0 -409
@@ -1,10 +1,10 @@
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
+ import { ComponentStatus, } from '../types/components.js';
5
5
  import { TaskType } from '../types/types.js';
6
6
  import { getTaskColors, getTaskTypeLabel } from '../services/colors.js';
7
- import { DebugLevel } from '../services/configuration.js';
7
+ import { DebugLevel } from '../configuration/types.js';
8
8
  import { useInput } from '../services/keyboard.js';
9
9
  import { Label } from './Label.js';
10
10
  import { List } from './List.js';
@@ -71,12 +71,54 @@ function taskToListItem(task, highlightedChildIndex = null, isDefineTaskWithoutS
71
71
  }
72
72
  return item;
73
73
  }
74
- export function Schedule({ message, tasks, state, status, debug = DebugLevel.None, handlers, onSelectionConfirmed, }) {
74
+ export const ScheduleView = ({ message, tasks, state, status, debug = DebugLevel.None, }) => {
75
75
  const isActive = status === ComponentStatus.Active;
76
- // isActive passed as prop
77
- const [highlightedIndex, setHighlightedIndex] = useState(state?.highlightedIndex ?? null);
78
- const [currentDefineGroupIndex, setCurrentDefineGroupIndex] = useState(state?.currentDefineGroupIndex ?? 0);
79
- const [completedSelections, setCompletedSelections] = useState(state?.completedSelections ?? []);
76
+ const { highlightedIndex, currentDefineGroupIndex, completedSelections } = state;
77
+ // Find all Define tasks
78
+ const defineTaskIndices = tasks
79
+ .map((t, idx) => (t.type === TaskType.Define ? idx : -1))
80
+ .filter((idx) => idx !== -1);
81
+ // Get the current active define task
82
+ const currentDefineTaskIndex = defineTaskIndices[currentDefineGroupIndex] ?? -1;
83
+ const listItems = tasks.map((task, idx) => {
84
+ // Find which define group this task belongs to (if any)
85
+ const defineGroupIndex = defineTaskIndices.indexOf(idx);
86
+ const isDefineTask = defineGroupIndex !== -1;
87
+ // Determine child selection state
88
+ let childIndex = null;
89
+ if (isDefineTask) {
90
+ if (defineGroupIndex < currentDefineGroupIndex) {
91
+ // Previously completed group - show the selection
92
+ childIndex = completedSelections[defineGroupIndex] ?? null;
93
+ }
94
+ else if (defineGroupIndex === currentDefineGroupIndex) {
95
+ // Current active group - show live navigation unless not active
96
+ if (!isActive) {
97
+ // If not active, show the completed selection for this group too
98
+ childIndex = completedSelections[defineGroupIndex] ?? null;
99
+ }
100
+ else {
101
+ childIndex = null;
102
+ }
103
+ }
104
+ }
105
+ // Show arrow on current active define task when no child is highlighted and is active
106
+ const isDefineWithoutSelection = isDefineTask &&
107
+ defineGroupIndex === currentDefineGroupIndex &&
108
+ highlightedIndex === null &&
109
+ isActive;
110
+ return taskToListItem(task, childIndex, isDefineWithoutSelection, isActive, debug);
111
+ });
112
+ return (_jsxs(Box, { flexDirection: "column", children: [message && (_jsx(Box, { marginBottom: 1, marginLeft: 1, children: _jsx(Label, { description: message, taskType: TaskType.Schedule, showType: debug !== DebugLevel.None, isCurrent: isActive, debug: debug }) })), _jsx(Box, { marginLeft: 1, children: _jsx(List, { items: listItems, highlightedIndex: currentDefineTaskIndex >= 0 ? highlightedIndex : null, highlightedParentIndex: currentDefineTaskIndex, showType: debug !== DebugLevel.None }) })] }));
113
+ };
114
+ /**
115
+ * Schedule controller: Manages task selection and navigation
116
+ */
117
+ export function Schedule({ message, tasks, status, debug = DebugLevel.None, requestHandlers, lifecycleHandlers, onSelectionConfirmed, }) {
118
+ const isActive = status === ComponentStatus.Active;
119
+ const [highlightedIndex, setHighlightedIndex] = useState(null);
120
+ const [currentDefineGroupIndex, setCurrentDefineGroupIndex] = useState(0);
121
+ const [completedSelections, setCompletedSelections] = useState([]);
80
122
  // Find all Define tasks
81
123
  const defineTaskIndices = tasks
82
124
  .map((t, idx) => (t.type === TaskType.Define ? idx : -1))
@@ -93,9 +135,16 @@ export function Schedule({ message, tasks, state, status, debug = DebugLevel.Non
93
135
  if (isActive && defineTaskIndices.length === 0 && onSelectionConfirmed) {
94
136
  // No selection needed - all tasks are concrete
95
137
  const concreteTasks = tasks.filter((task) => task.type !== TaskType.Ignore && task.type !== TaskType.Discard);
138
+ // Expose final state
139
+ const finalState = {
140
+ highlightedIndex,
141
+ currentDefineGroupIndex,
142
+ completedSelections,
143
+ };
144
+ requestHandlers.onCompleted(finalState);
96
145
  // Complete the selection phase - it goes to timeline
97
146
  // Callback will create a new Plan showing refined tasks (pending) + Confirm (active)
98
- handlers?.completeActive();
147
+ lifecycleHandlers.completeActive();
99
148
  void onSelectionConfirmed(concreteTasks);
100
149
  }
101
150
  }, [
@@ -103,7 +152,11 @@ export function Schedule({ message, tasks, state, status, debug = DebugLevel.Non
103
152
  defineTaskIndices.length,
104
153
  tasks,
105
154
  onSelectionConfirmed,
106
- handlers,
155
+ lifecycleHandlers,
156
+ highlightedIndex,
157
+ currentDefineGroupIndex,
158
+ completedSelections,
159
+ requestHandlers,
107
160
  ]);
108
161
  useInput((input, key) => {
109
162
  // Don't handle input if not active or no define task
@@ -111,7 +164,7 @@ export function Schedule({ message, tasks, state, status, debug = DebugLevel.Non
111
164
  return;
112
165
  }
113
166
  if (key.escape) {
114
- handlers?.onAborted('task selection');
167
+ requestHandlers.onAborted('task selection');
115
168
  return;
116
169
  }
117
170
  if (key.downArrow) {
@@ -167,61 +220,31 @@ export function Schedule({ message, tasks, state, status, debug = DebugLevel.Non
167
220
  refinedTasks.push(task);
168
221
  }
169
222
  });
223
+ // Expose final state
224
+ const finalState = {
225
+ highlightedIndex: null,
226
+ currentDefineGroupIndex,
227
+ completedSelections: newCompletedSelections,
228
+ };
229
+ requestHandlers.onCompleted(finalState);
170
230
  if (onSelectionConfirmed) {
171
231
  // Complete the selection phase - it goes to timeline
172
232
  // Callback will create a new Plan showing refined tasks (pending) + Confirm (active)
173
- handlers?.completeActive();
233
+ lifecycleHandlers.completeActive();
174
234
  void onSelectionConfirmed(refinedTasks);
175
235
  }
176
236
  else {
177
237
  // No selection callback, just complete normally
178
- handlers?.completeActive();
238
+ lifecycleHandlers.completeActive();
179
239
  }
180
240
  }
181
241
  }
182
242
  }, { isActive: isActive && defineTask !== null });
183
- // Sync state back to component definition
184
- // This ensures timeline can render historical state and tests can validate behavior
185
- useEffect(() => {
186
- handlers?.updateState({
187
- highlightedIndex,
188
- currentDefineGroupIndex,
189
- completedSelections,
190
- });
191
- }, [
243
+ // Controller always renders View, passing current state
244
+ const state = {
192
245
  highlightedIndex,
193
246
  currentDefineGroupIndex,
194
247
  completedSelections,
195
- handlers,
196
- ]);
197
- const listItems = tasks.map((task, idx) => {
198
- // Find which define group this task belongs to (if any)
199
- const defineGroupIndex = defineTaskIndices.indexOf(idx);
200
- const isDefineTask = defineGroupIndex !== -1;
201
- // Determine child selection state
202
- let childIndex = null;
203
- if (isDefineTask) {
204
- if (defineGroupIndex < currentDefineGroupIndex) {
205
- // Previously completed group - show the selection
206
- childIndex = completedSelections[defineGroupIndex] ?? null;
207
- }
208
- else if (defineGroupIndex === currentDefineGroupIndex) {
209
- // Current active group - show live navigation unless not active
210
- if (!isActive) {
211
- // If not active, show the completed selection for this group too
212
- childIndex = completedSelections[defineGroupIndex] ?? null;
213
- }
214
- else {
215
- childIndex = null;
216
- }
217
- }
218
- }
219
- // Show arrow on current active define task when no child is highlighted and is active
220
- const isDefineWithoutSelection = isDefineTask &&
221
- defineGroupIndex === currentDefineGroupIndex &&
222
- highlightedIndex === null &&
223
- isActive;
224
- return taskToListItem(task, childIndex, isDefineWithoutSelection, isActive, debug);
225
- });
226
- return (_jsxs(Box, { flexDirection: "column", children: [message && (_jsx(Box, { marginBottom: 1, marginLeft: 1, children: _jsx(Label, { description: message, taskType: TaskType.Schedule, showType: debug !== DebugLevel.None, isCurrent: isActive, debug: debug }) })), _jsx(Box, { marginLeft: 1, children: _jsx(List, { items: listItems, highlightedIndex: currentDefineTaskIndex >= 0 ? highlightedIndex : null, highlightedParentIndex: currentDefineTaskIndex, showType: debug !== DebugLevel.None }) })] }));
248
+ };
249
+ return (_jsx(ScheduleView, { message: message, tasks: tasks, state: state, status: status, debug: debug }));
227
250
  }
@@ -1,23 +1,35 @@
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
+ import { ComponentStatus, } from '../types/components.js';
5
5
  import { TaskType } from '../types/types.js';
6
+ import { saveConfig } from '../configuration/io.js';
7
+ import { unflattenConfig } from '../configuration/transformation.js';
6
8
  import { Colors, getTextColor } from '../services/colors.js';
7
- import { addDebugToTimeline, createConfigStepsFromSchema, } from '../services/components.js';
8
- import { DebugLevel, saveConfig, unflattenConfig, } from '../services/configuration.js';
9
+ import { createConfigDefinitionWithKeys, createMessage, } from '../services/components.js';
9
10
  import { saveConfigLabels } from '../services/config-labels.js';
10
11
  import { useInput } from '../services/keyboard.js';
11
- import { formatErrorMessage } from '../services/messages.js';
12
+ import { formatErrorMessage, getUnresolvedPlaceholdersMessage, } from '../services/messages.js';
12
13
  import { ensureMinimumTime } from '../services/timing.js';
13
- import { Config } from './Config.js';
14
14
  import { Spinner } from './Spinner.js';
15
15
  const MIN_PROCESSING_TIME = 1000;
16
- export function Validate({ missingConfig, userRequest, state, status, service, children, debug = DebugLevel.None, onError, onComplete, onAborted, handlers, }) {
16
+ export const ValidateView = ({ state, status }) => {
17
17
  const isActive = status === ComponentStatus.Active;
18
- const [error, setError] = useState(state?.error ?? null);
19
- const [completionMessage, setCompletionMessage] = useState(state?.completionMessage ?? null);
20
- const [configRequirements, setConfigRequirements] = useState(state?.configRequirements ?? null);
18
+ const { error, completionMessage } = state;
19
+ // Don't render when not active and nothing to show
20
+ if (!isActive && !completionMessage && !error) {
21
+ return null;
22
+ }
23
+ 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] }) }))] }));
24
+ };
25
+ /**
26
+ * Validate controller: Validates missing config
27
+ */
28
+ export function Validate({ missingConfig, userRequest, status, service, onError, requestHandlers, onValidationComplete, onAborted, lifecycleHandlers, workflowHandlers, }) {
29
+ const isActive = status === ComponentStatus.Active;
30
+ const [error, setError] = useState(null);
31
+ const [completionMessage, setCompletionMessage] = useState(null);
32
+ const [configRequirements, setConfigRequirements] = useState([]);
21
33
  useInput((_, key) => {
22
34
  if (key.escape && isActive) {
23
35
  onAborted('validation');
@@ -39,7 +51,9 @@ export function Validate({ missingConfig, userRequest, state, status, service, c
39
51
  await ensureMinimumTime(startTime, MIN_PROCESSING_TIME);
40
52
  if (mounted) {
41
53
  // Add debug components to timeline if present
42
- addDebugToTimeline(result.debug, handlers);
54
+ if (result.debug?.length) {
55
+ workflowHandlers.addToTimeline(...result.debug);
56
+ }
43
57
  // Extract CONFIG tasks with descriptions from result
44
58
  const configTasks = result.tasks.filter((task) => task.type === TaskType.Config);
45
59
  // Build ConfigRequirements with descriptions
@@ -55,25 +69,50 @@ export function Validate({ missingConfig, userRequest, state, status, service, c
55
69
  };
56
70
  });
57
71
  // Build completion message showing which config properties are needed
58
- const count = withDescriptions.length;
59
- const propertyWord = count === 1 ? 'property' : 'properties';
60
- // Shuffle between different message variations
61
- const messages = [
62
- `Additional configuration ${propertyWord} required.`,
63
- `Configuration ${propertyWord} needed.`,
64
- `Missing configuration ${propertyWord} detected.`,
65
- `Setup requires configuration ${propertyWord}.`,
66
- ];
67
- const message = messages[Math.floor(Math.random() * messages.length)];
72
+ const message = getUnresolvedPlaceholdersMessage(withDescriptions.length);
68
73
  setCompletionMessage(message);
69
74
  setConfigRequirements(withDescriptions);
70
- // Save state after validation completes
71
- handlers?.updateState({
75
+ // Add validation message to timeline before Config component
76
+ workflowHandlers.addToTimeline(createMessage(message));
77
+ // Create Config component and add to queue
78
+ const keys = withDescriptions.map((req) => req.path);
79
+ const configDef = createConfigDefinitionWithKeys(keys, (config) => {
80
+ // Convert flat dotted keys to nested structure grouped by section
81
+ const configBySection = unflattenConfig(config);
82
+ // Extract and save labels to cache
83
+ const labels = {};
84
+ for (const req of withDescriptions) {
85
+ if (req.description) {
86
+ labels[req.path] = req.description;
87
+ }
88
+ }
89
+ saveConfigLabels(labels);
90
+ // Save each section
91
+ for (const [section, sectionConfig] of Object.entries(configBySection)) {
92
+ saveConfig(section, sectionConfig);
93
+ }
94
+ // After config is saved, invoke callback to add Execute component to queue
95
+ onValidationComplete(withDescriptions);
96
+ }, (operation) => {
97
+ onAborted(operation);
98
+ });
99
+ // Override descriptions with LLM-generated ones
100
+ if ('props' in configDef && 'steps' in configDef.props) {
101
+ configDef.props.steps = configDef.props.steps.map((step, index) => ({
102
+ ...step,
103
+ description: withDescriptions[index].description ||
104
+ withDescriptions[index].path,
105
+ }));
106
+ }
107
+ workflowHandlers.addToQueue(configDef);
108
+ lifecycleHandlers.completeActive();
109
+ const finalState = {
110
+ error: null,
72
111
  completionMessage: message,
73
112
  configRequirements: withDescriptions,
74
113
  validated: true,
75
- error: null,
76
- });
114
+ };
115
+ requestHandlers.onCompleted(finalState);
77
116
  }
78
117
  }
79
118
  catch (err) {
@@ -81,13 +120,13 @@ export function Validate({ missingConfig, userRequest, state, status, service, c
81
120
  if (mounted) {
82
121
  const errorMessage = formatErrorMessage(err);
83
122
  setError(errorMessage);
84
- // Save error state
85
- handlers?.updateState({
123
+ const finalState = {
86
124
  error: errorMessage,
87
125
  completionMessage: null,
88
- configRequirements: null,
126
+ configRequirements: [],
89
127
  validated: false,
90
- });
128
+ };
129
+ requestHandlers.onCompleted(finalState);
91
130
  onError(errorMessage);
92
131
  }
93
132
  }
@@ -101,59 +140,20 @@ export function Validate({ missingConfig, userRequest, state, status, service, c
101
140
  userRequest,
102
141
  isActive,
103
142
  service,
104
- onComplete,
143
+ requestHandlers,
105
144
  onError,
106
145
  onAborted,
146
+ onValidationComplete,
147
+ lifecycleHandlers,
148
+ workflowHandlers,
107
149
  ]);
108
- // Don't render when not active and nothing to show
109
- if (!isActive && !completionMessage && !error && !children) {
110
- return null;
111
- }
112
- // Create ConfigSteps from requirements using createConfigStepsFromSchema
113
- // to load current values from config file, then override descriptions
114
- const configSteps = configRequirements
115
- ? (() => {
116
- const keys = configRequirements.map((req) => req.path);
117
- const steps = createConfigStepsFromSchema(keys);
118
- // Override descriptions with LLM-generated ones
119
- return steps.map((step, index) => ({
120
- ...step,
121
- description: configRequirements[index].description ||
122
- configRequirements[index].path,
123
- }));
124
- })()
125
- : null;
126
- const handleConfigFinished = (config) => {
127
- // Convert flat dotted keys to nested structure grouped by section
128
- const configBySection = unflattenConfig(config);
129
- // Extract and save labels to cache
130
- if (configRequirements) {
131
- const labels = {};
132
- for (const req of configRequirements) {
133
- if (req.description) {
134
- labels[req.path] = req.description;
135
- }
136
- }
137
- saveConfigLabels(labels);
138
- }
139
- // Save each section
140
- for (const [section, sectionConfig] of Object.entries(configBySection)) {
141
- saveConfig(section, sectionConfig);
142
- }
143
- // Mark validation component as complete before invoking callback
144
- // This allows the workflow to proceed to execution
145
- handlers?.completeActive();
146
- // Invoke callback which will queue the Execute component
147
- if (configRequirements) {
148
- onComplete(configRequirements);
149
- }
150
- };
151
- const handleConfigAborted = (operation) => {
152
- // Mark validation component as complete when aborted
153
- handlers?.completeActive();
154
- onAborted(operation);
150
+ const state = {
151
+ error,
152
+ completionMessage,
153
+ configRequirements,
154
+ validated: error === null && completionMessage !== null,
155
155
  };
156
- 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 && configSteps.length > 0 && !error && (_jsx(Box, { marginTop: 1, children: _jsx(Config, { steps: configSteps, status: status, debug: debug, onFinished: handleConfigFinished, onAborted: handleConfigAborted, handlers: handlers }) })), configSteps && configSteps.length === 0 && !error && (_jsx(Box, { marginTop: 1, marginLeft: 1, children: _jsx(Text, { color: Colors.Status.Error, children: "Error: No configuration steps generated. Please try again." }) })), children] }));
156
+ return _jsx(ValidateView, { state: state, status: status });
157
157
  }
158
158
  /**
159
159
  * Build prompt for VALIDATE tool
@@ -3,11 +3,11 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
3
3
  import { Box, Static } from 'ink';
4
4
  import { ComponentStatus, } from '../types/components.js';
5
5
  import { ComponentName, FeedbackType } from '../types/types.js';
6
- import { createFeedback, isStateless, markAsDone, } from '../services/components.js';
7
- import { DebugLevel } from '../services/configuration.js';
6
+ import { createFeedback, isSimple, markAsDone, } from '../services/components.js';
7
+ import { getWarnings } from '../services/logger.js';
8
8
  import { getCancellationMessage } from '../services/messages.js';
9
9
  import { exitApp } from '../services/process.js';
10
- import { Component } from './Component.js';
10
+ import { SimpleComponent, ControllerComponent, TimelineComponent, } from './Component.js';
11
11
  export const Workflow = ({ initialQueue, debug }) => {
12
12
  const [timeline, setTimeline] = useState([]);
13
13
  const [current, setCurrent] = useState({ active: null, pending: null });
@@ -35,27 +35,43 @@ export const Workflow = ({ initialQueue, debug }) => {
35
35
  return { active: null, pending };
36
36
  });
37
37
  }, []);
38
- // Global handlers for all stateful components
39
- const handlers = useMemo(() => ({
40
- addToQueue: (...items) => {
41
- setQueue((queue) => [...queue, ...items]);
38
+ // Request handlers - manages errors, aborts, and completions
39
+ const requestHandlers = useMemo(() => ({
40
+ onError: (error) => {
41
+ moveActiveToTimeline();
42
+ // Add feedback to queue
43
+ setQueue((queue) => [
44
+ ...queue,
45
+ createFeedback(FeedbackType.Failed, error),
46
+ ]);
42
47
  },
43
- updateState: (newState) => {
48
+ onAborted: (operation) => {
49
+ moveActiveToTimeline();
50
+ // Add feedback to queue
51
+ const message = getCancellationMessage(operation);
52
+ setQueue((queue) => [
53
+ ...queue,
54
+ createFeedback(FeedbackType.Aborted, message),
55
+ ]);
56
+ },
57
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
58
+ onCompleted: (finalState) => {
44
59
  setCurrent((curr) => {
45
60
  const { active, pending } = curr;
46
61
  if (!active || !('state' in active))
47
62
  return curr;
48
- const stateful = active;
63
+ // Save final state to definition
64
+ const managed = active;
49
65
  const updated = {
50
- ...stateful,
51
- state: {
52
- ...stateful.state,
53
- ...newState,
54
- },
66
+ ...managed,
67
+ state: finalState,
55
68
  };
56
69
  return { active: updated, pending };
57
70
  });
58
71
  },
72
+ }), [moveActiveToTimeline]);
73
+ // Lifecycle handlers - for components with active/pending states
74
+ const lifecycleHandlers = useMemo(() => ({
59
75
  completeActive: (...items) => {
60
76
  moveActiveToPending();
61
77
  if (items.length > 0) {
@@ -80,27 +96,16 @@ export const Workflow = ({ initialQueue, debug }) => {
80
96
  setQueue((queue) => [...items, ...queue]);
81
97
  }
82
98
  },
99
+ }), [moveActiveToPending]);
100
+ // Workflow handlers - manages queue and timeline
101
+ const workflowHandlers = useMemo(() => ({
102
+ addToQueue: (...items) => {
103
+ setQueue((queue) => [...queue, ...items]);
104
+ },
83
105
  addToTimeline: (...items) => {
84
106
  setTimeline((prev) => [...prev, ...items]);
85
107
  },
86
- onAborted: (operation) => {
87
- moveActiveToTimeline();
88
- // Add feedback to queue
89
- const message = getCancellationMessage(operation);
90
- setQueue((queue) => [
91
- ...queue,
92
- createFeedback(FeedbackType.Aborted, message),
93
- ]);
94
- },
95
- onError: (error) => {
96
- moveActiveToTimeline();
97
- // Add feedback to queue
98
- setQueue((queue) => [
99
- ...queue,
100
- createFeedback(FeedbackType.Failed, error),
101
- ]);
102
- },
103
- }), [moveActiveToPending, moveActiveToTimeline]);
108
+ }), []);
104
109
  // Global Esc handler removed - components handle their own Esc individually
105
110
  // Move next item from queue to active
106
111
  useEffect(() => {
@@ -130,14 +135,22 @@ export const Workflow = ({ initialQueue, debug }) => {
130
135
  const { active, pending } = current;
131
136
  if (!active)
132
137
  return;
133
- if (isStateless(active)) {
134
- // Stateless components move directly to timeline
138
+ if (isSimple(active)) {
139
+ // Simple components move directly to timeline
135
140
  const doneComponent = markAsDone(active);
136
141
  setTimeline((prev) => [...prev, doneComponent]);
137
142
  setCurrent({ active: null, pending });
138
143
  }
139
- // Stateful components stay in active until handlers move them to pending
144
+ // Managed components stay in active until handlers move them to pending
140
145
  }, [current]);
146
+ // Check for accumulated warnings and add them to timeline
147
+ useEffect(() => {
148
+ const warningMessages = getWarnings();
149
+ if (warningMessages.length > 0) {
150
+ const warningComponents = warningMessages.map((msg) => markAsDone(createFeedback(FeedbackType.Warning, msg)));
151
+ setTimeline((prev) => [...prev, ...warningComponents]);
152
+ }
153
+ }, [timeline, current]);
141
154
  // Move final pending to timeline and exit when all done
142
155
  useEffect(() => {
143
156
  const { active, pending } = current;
@@ -162,32 +175,18 @@ export const Workflow = ({ initialQueue, debug }) => {
162
175
  lastItem.props.type === FeedbackType.Failed;
163
176
  exitApp(isFailed ? 1 : 0);
164
177
  }, [current, queue, timeline]);
165
- // Render active and pending components
166
- const activeComponent = useMemo(() => {
167
- const { active } = current;
168
- if (!active)
178
+ // Render component with handlers (used for both active and pending)
179
+ const renderComponent = useCallback((def, status) => {
180
+ if (!def)
169
181
  return null;
170
- // For stateless components, render as-is
171
- if (isStateless(active)) {
172
- return _jsx(Component, { def: active, debug: debug }, active.id);
182
+ // For simple components, render as-is
183
+ if (isSimple(def)) {
184
+ return _jsx(SimpleComponent, { def: def }, def.id);
173
185
  }
174
- // For stateful components, inject global handlers
175
- const statefulActive = active;
176
- const wrappedDef = {
177
- ...statefulActive,
178
- props: {
179
- ...statefulActive.props,
180
- handlers,
181
- },
182
- };
183
- return _jsx(Component, { def: wrappedDef, debug: debug }, active.id);
184
- }, [current, debug, handlers]);
185
- const pendingComponent = useMemo(() => {
186
- const { pending } = current;
187
- if (!pending)
188
- return null;
189
- // Pending components don't receive input
190
- return _jsx(Component, { def: pending, debug: debug }, pending.id);
191
- }, [current, debug]);
192
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: timeline, children: (item) => (_jsx(Box, { marginTop: 1, children: _jsx(Component, { def: item, debug: DebugLevel.None }) }, item.id)) }, "timeline"), pendingComponent && _jsx(Box, { marginTop: 1, children: pendingComponent }), activeComponent && _jsx(Box, { marginTop: 1, children: activeComponent })] }));
186
+ // For managed components, inject handlers via ControllerComponent
187
+ return (_jsx(ControllerComponent, { def: { ...def, status }, debug: debug, requestHandlers: requestHandlers, lifecycleHandlers: lifecycleHandlers, workflowHandlers: workflowHandlers }, def.id));
188
+ }, [debug, requestHandlers, lifecycleHandlers, workflowHandlers]);
189
+ const activeComponent = useMemo(() => renderComponent(current.active, ComponentStatus.Active), [current.active, renderComponent]);
190
+ const pendingComponent = useMemo(() => renderComponent(current.pending, ComponentStatus.Pending), [current.pending, renderComponent]);
191
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: timeline, children: (item) => (_jsx(Box, { marginTop: 1, children: _jsx(TimelineComponent, { def: item }) }, item.id)) }, "timeline"), pendingComponent && _jsx(Box, { marginTop: 1, children: pendingComponent }), activeComponent && _jsx(Box, { marginTop: 1, children: activeComponent })] }));
193
192
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prompt-language-shell",
3
- "version": "0.8.2",
3
+ "version": "0.8.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",
@@ -51,7 +51,8 @@
51
51
  "ink": "^6.6.0",
52
52
  "ink-text-input": "^6.0.0",
53
53
  "react": "^19.2.3",
54
- "yaml": "^2.8.2"
54
+ "yaml": "^2.8.2",
55
+ "zod": "^4.2.1"
55
56
  },
56
57
  "devDependencies": {
57
58
  "@types/node": "^25.0.3",