prompt-language-shell 0.8.0 → 0.8.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.
package/dist/ui/Main.js CHANGED
@@ -1,13 +1,13 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useEffect, useState } from 'react';
3
3
  import { FeedbackType } from '../types/types.js';
4
- import { createAnthropicService, } from '../services/anthropic.js';
4
+ import { createAnthropicService } from '../services/anthropic.js';
5
5
  import { createCommandDefinition, createConfigDefinitionWithKeys, createFeedback, createMessage, createWelcomeDefinition, } from '../services/components.js';
6
6
  import { DebugLevel, getConfigurationRequiredMessage, getMissingConfigKeys, loadConfig, loadDebugSetting, saveConfig, saveDebugSetting, unflattenConfig, } from '../services/configuration.js';
7
7
  import { registerGlobalShortcut } from '../services/keyboard.js';
8
8
  import { initializeLogger, setDebugLevel } from '../services/logger.js';
9
9
  import { Workflow } from './Workflow.js';
10
- export const Main = ({ app, command }) => {
10
+ export const Main = ({ app, command, serviceFactory = createAnthropicService, }) => {
11
11
  const [service, setService] = useState(null);
12
12
  const [initialQueue, setInitialQueue] = useState(null);
13
13
  const [debug, setDebugLevelState] = useState(() => loadDebugSetting());
@@ -44,7 +44,7 @@ export const Main = ({ app, command }) => {
44
44
  // Config exists - create service immediately
45
45
  try {
46
46
  const config = loadConfig();
47
- const newService = createAnthropicService(config.anthropic);
47
+ const newService = serviceFactory(config.anthropic);
48
48
  setService(newService);
49
49
  }
50
50
  catch (error) {
@@ -56,7 +56,7 @@ export const Main = ({ app, command }) => {
56
56
  }
57
57
  }
58
58
  // If config is missing, service will be created after config completes
59
- }, [service]);
59
+ }, [service, serviceFactory]);
60
60
  // Initialize queue after service is ready
61
61
  useEffect(() => {
62
62
  // Only set initial queue once
@@ -75,7 +75,7 @@ export const Main = ({ app, command }) => {
75
75
  }
76
76
  // Load config and create service
77
77
  const newConfig = loadConfig();
78
- const newService = createAnthropicService(newConfig.anthropic);
78
+ const newService = serviceFactory(newConfig.anthropic);
79
79
  setService(newService);
80
80
  }
81
81
  catch (error) {
@@ -86,7 +86,7 @@ export const Main = ({ app, command }) => {
86
86
  throw new Error(errorMessage);
87
87
  }
88
88
  };
89
- const handleConfigAborted = (operation) => {
89
+ const handleConfigAborted = (_operation) => {
90
90
  // Config was cancelled
91
91
  };
92
92
  setInitialQueue([
package/dist/ui/Report.js CHANGED
@@ -1,14 +1,10 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from 'ink';
3
- import { Colors } from '../services/colors.js';
4
- function CapabilityItem({ name, description, isBuiltIn, isIndirect, isIncomplete, }) {
5
- const color = isIndirect
6
- ? Colors.Origin.Indirect
7
- : isBuiltIn
8
- ? Colors.Origin.BuiltIn
9
- : Colors.Origin.UserProvided;
3
+ import { Colors, getOriginColor } from '../services/colors.js';
4
+ function CapabilityItem({ name, description, origin, isIncomplete, }) {
5
+ const color = getOriginColor(origin);
10
6
  return (_jsxs(Box, { children: [_jsx(Text, { children: "- " }), _jsx(Text, { color: color, children: name }), _jsxs(Text, { children: [" - ", description] }), isIncomplete && _jsx(Text, { color: Colors.Status.Warning, children: " (incomplete)" })] }));
11
7
  }
12
8
  export function Report({ message, capabilities }) {
13
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginLeft: 1, children: _jsx(Text, { children: message }) }), _jsx(Box, { flexDirection: "column", marginLeft: 3, marginTop: 1, children: capabilities.map((capability, index) => (_jsx(CapabilityItem, { name: capability.name, description: capability.description, isBuiltIn: capability.isBuiltIn, isIndirect: capability.isIndirect, isIncomplete: capability.isIncomplete }, index))) })] }));
9
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginLeft: 1, children: _jsx(Text, { children: message }) }), _jsx(Box, { flexDirection: "column", marginLeft: 3, marginTop: 1, children: capabilities.map((capability, index) => (_jsx(CapabilityItem, { name: capability.name, description: capability.description, origin: capability.origin, isIncomplete: capability.isIncomplete }, index))) })] }));
14
10
  }
@@ -37,7 +37,7 @@ function taskToListItem(task, highlightedChildIndex = null, isDefineTaskWithoutS
37
37
  const planColors = getTaskColors(TaskType.Schedule, isCurrent);
38
38
  return {
39
39
  description: {
40
- text: String(option),
40
+ text: option,
41
41
  color: colors.description,
42
42
  highlightedColor: planColors.description,
43
43
  },
@@ -71,7 +71,7 @@ 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 function Schedule({ message, tasks, state, status, debug = DebugLevel.None, stateHandlers, lifecycleHandlers, errorHandlers, onSelectionConfirmed, }) {
75
75
  const isActive = status === ComponentStatus.Active;
76
76
  // isActive passed as prop
77
77
  const [highlightedIndex, setHighlightedIndex] = useState(state?.highlightedIndex ?? null);
@@ -95,15 +95,15 @@ export function Schedule({ message, tasks, state, status, debug = DebugLevel.Non
95
95
  const concreteTasks = tasks.filter((task) => task.type !== TaskType.Ignore && task.type !== TaskType.Discard);
96
96
  // Complete the selection phase - it goes to timeline
97
97
  // Callback will create a new Plan showing refined tasks (pending) + Confirm (active)
98
- handlers?.completeActive();
99
- onSelectionConfirmed(concreteTasks);
98
+ lifecycleHandlers?.completeActive();
99
+ void onSelectionConfirmed(concreteTasks);
100
100
  }
101
101
  }, [
102
102
  isActive,
103
103
  defineTaskIndices.length,
104
104
  tasks,
105
105
  onSelectionConfirmed,
106
- handlers,
106
+ lifecycleHandlers,
107
107
  ]);
108
108
  useInput((input, key) => {
109
109
  // Don't handle input if not active or no define task
@@ -111,7 +111,7 @@ export function Schedule({ message, tasks, state, status, debug = DebugLevel.Non
111
111
  return;
112
112
  }
113
113
  if (key.escape) {
114
- handlers?.onAborted('task selection');
114
+ errorHandlers?.onAborted('task selection');
115
115
  return;
116
116
  }
117
117
  if (key.downArrow) {
@@ -153,7 +153,7 @@ export function Schedule({ message, tasks, state, status, debug = DebugLevel.Non
153
153
  // This is a Define task - only include the selected option
154
154
  const options = task.params.options;
155
155
  const selectedIndex = newCompletedSelections[defineGroupIndex];
156
- const selectedOption = String(options[selectedIndex]);
156
+ const selectedOption = options[selectedIndex];
157
157
  // Use Execute as default - LLM will properly classify during refinement
158
158
  refinedTasks.push({
159
159
  action: selectedOption,
@@ -170,12 +170,12 @@ export function Schedule({ message, tasks, state, status, debug = DebugLevel.Non
170
170
  if (onSelectionConfirmed) {
171
171
  // Complete the selection phase - it goes to timeline
172
172
  // Callback will create a new Plan showing refined tasks (pending) + Confirm (active)
173
- handlers?.completeActive();
174
- onSelectionConfirmed(refinedTasks);
173
+ lifecycleHandlers?.completeActive();
174
+ void onSelectionConfirmed(refinedTasks);
175
175
  }
176
176
  else {
177
177
  // No selection callback, just complete normally
178
- handlers?.completeActive();
178
+ lifecycleHandlers?.completeActive();
179
179
  }
180
180
  }
181
181
  }
@@ -183,7 +183,7 @@ export function Schedule({ message, tasks, state, status, debug = DebugLevel.Non
183
183
  // Sync state back to component definition
184
184
  // This ensures timeline can render historical state and tests can validate behavior
185
185
  useEffect(() => {
186
- handlers?.updateState({
186
+ stateHandlers?.updateState({
187
187
  highlightedIndex,
188
188
  currentDefineGroupIndex,
189
189
  completedSelections,
@@ -192,7 +192,7 @@ export function Schedule({ message, tasks, state, status, debug = DebugLevel.Non
192
192
  highlightedIndex,
193
193
  currentDefineGroupIndex,
194
194
  completedSelections,
195
- handlers,
195
+ stateHandlers,
196
196
  ]);
197
197
  const listItems = tasks.map((task, idx) => {
198
198
  // Find which define group this task belongs to (if any)
@@ -17,7 +17,9 @@ export function Spinner() {
17
17
  return next !== prev ? next : prev;
18
18
  });
19
19
  }, INTERVAL);
20
- return () => clearInterval(timer);
20
+ return () => {
21
+ clearInterval(timer);
22
+ };
21
23
  }, []);
22
24
  return _jsx(Text, { color: Palette.Cyan, children: FRAMES[frame] });
23
25
  }
@@ -4,7 +4,7 @@ import { getStatusColors, Palette, STATUS_ICONS } from '../services/colors.js';
4
4
  import { ExecutionStatus } from '../services/shell.js';
5
5
  import { formatDuration } from '../services/utils.js';
6
6
  import { Spinner } from './Spinner.js';
7
- export function Subtask({ label, command, status, isActive, startTime, endTime, elapsed, }) {
7
+ export function Subtask({ label, command, status, isActive: _isActive, startTime, endTime, elapsed, }) {
8
8
  const colors = getStatusColors(status);
9
9
  const isCancelled = status === ExecutionStatus.Cancelled;
10
10
  const isAborted = status === ExecutionStatus.Aborted;
package/dist/ui/Task.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useEffect, useState } from 'react';
3
- import { ExecutionStatus, executeCommand, } from '../services/shell.js';
3
+ import { ExecutionResult, ExecutionStatus, executeCommand, } from '../services/shell.js';
4
4
  import { calculateElapsed } from '../services/utils.js';
5
5
  import { Subtask } from './Subtask.js';
6
6
  export function Task({ label, command, isActive, index, initialStatus, initialElapsed, onComplete, onAbort, onError, }) {
@@ -19,7 +19,9 @@ export function Task({ label, command, isActive, index, initialStatus, initialEl
19
19
  return next !== prev ? next : prev;
20
20
  });
21
21
  }, 1000);
22
- return () => clearInterval(interval);
22
+ return () => {
23
+ clearInterval(interval);
24
+ };
23
25
  }, [status, startTime]);
24
26
  // Execute command when becoming active
25
27
  useEffect(() => {
@@ -43,10 +45,10 @@ export function Task({ label, command, isActive, index, initialStatus, initialEl
43
45
  setEndTime(end);
44
46
  const taskDuration = calculateElapsed(start);
45
47
  setElapsed(taskDuration);
46
- setStatus(output.result === 'success'
48
+ setStatus(output.result === ExecutionResult.Success
47
49
  ? ExecutionStatus.Success
48
50
  : ExecutionStatus.Failed);
49
- if (output.result === 'success') {
51
+ if (output.result === ExecutionResult.Success) {
50
52
  onComplete?.(index, output, taskDuration);
51
53
  }
52
54
  else {
@@ -64,11 +66,10 @@ export function Task({ label, command, isActive, index, initialStatus, initialEl
64
66
  onError?.(index, err instanceof Error ? err.message : 'Unknown error', errorDuration);
65
67
  }
66
68
  }
67
- execute();
69
+ void execute();
68
70
  return () => {
69
71
  mounted = false;
70
72
  };
71
- // eslint-disable-next-line react-hooks/exhaustive-deps
72
73
  }, [isActive]);
73
74
  // Handle abort when task becomes inactive while running
74
75
  useEffect(() => {
@@ -4,35 +4,30 @@ import { Box, Text } from 'ink';
4
4
  import { ComponentStatus } from '../types/components.js';
5
5
  import { TaskType } from '../types/types.js';
6
6
  import { Colors, getTextColor } from '../services/colors.js';
7
- import { addDebugToTimeline, createConfigStepsFromSchema, } from '../services/components.js';
7
+ import { createConfigStepsFromSchema } from '../services/components.js';
8
8
  import { DebugLevel, saveConfig, unflattenConfig, } from '../services/configuration.js';
9
+ import { saveConfigLabels } from '../services/config-labels.js';
9
10
  import { useInput } from '../services/keyboard.js';
10
11
  import { formatErrorMessage } from '../services/messages.js';
11
12
  import { ensureMinimumTime } from '../services/timing.js';
12
13
  import { Config } from './Config.js';
13
14
  import { Spinner } from './Spinner.js';
14
15
  const MIN_PROCESSING_TIME = 1000;
15
- export function Validate({ missingConfig, userRequest, state, status, service, children, debug = DebugLevel.None, onError, onComplete, onAborted, handlers, }) {
16
+ export function Validate({ missingConfig, userRequest, state, status, service, children, debug = DebugLevel.None, onError, onComplete, onAborted, stateHandlers, lifecycleHandlers, workflowHandlers, }) {
16
17
  const isActive = status === ComponentStatus.Active;
17
18
  const [error, setError] = useState(state?.error ?? null);
18
19
  const [completionMessage, setCompletionMessage] = useState(state?.completionMessage ?? null);
19
20
  const [configRequirements, setConfigRequirements] = useState(state?.configRequirements ?? null);
20
- const [showConfig, setShowConfig] = useState(false);
21
21
  useInput((_, key) => {
22
- if (key.escape && isActive && !showConfig) {
22
+ if (key.escape && isActive) {
23
23
  onAborted('validation');
24
24
  }
25
- }, { isActive: isActive && !showConfig });
25
+ }, { isActive });
26
26
  useEffect(() => {
27
27
  // Skip processing if not active
28
28
  if (!isActive) {
29
29
  return;
30
30
  }
31
- // Skip processing if no service available
32
- if (!service) {
33
- setError('No service available');
34
- return;
35
- }
36
31
  let mounted = true;
37
32
  async function process(svc) {
38
33
  const startTime = Date.now();
@@ -44,7 +39,9 @@ export function Validate({ missingConfig, userRequest, state, status, service, c
44
39
  await ensureMinimumTime(startTime, MIN_PROCESSING_TIME);
45
40
  if (mounted) {
46
41
  // Add debug components to timeline if present
47
- addDebugToTimeline(result.debug, handlers);
42
+ if (result.debug?.length) {
43
+ workflowHandlers?.addToTimeline(...result.debug);
44
+ }
48
45
  // Extract CONFIG tasks with descriptions from result
49
46
  const configTasks = result.tasks.filter((task) => task.type === TaskType.Config);
50
47
  // Build ConfigRequirements with descriptions
@@ -73,7 +70,7 @@ export function Validate({ missingConfig, userRequest, state, status, service, c
73
70
  setCompletionMessage(message);
74
71
  setConfigRequirements(withDescriptions);
75
72
  // Save state after validation completes
76
- handlers?.updateState({
73
+ stateHandlers?.updateState({
77
74
  completionMessage: message,
78
75
  configRequirements: withDescriptions,
79
76
  validated: true,
@@ -87,19 +84,17 @@ export function Validate({ missingConfig, userRequest, state, status, service, c
87
84
  const errorMessage = formatErrorMessage(err);
88
85
  setError(errorMessage);
89
86
  // Save error state
90
- handlers?.updateState({
87
+ stateHandlers?.updateState({
91
88
  error: errorMessage,
92
89
  completionMessage: null,
93
90
  configRequirements: null,
94
91
  validated: false,
95
92
  });
96
- if (onError) {
97
- onError(errorMessage);
98
- }
93
+ onError(errorMessage);
99
94
  }
100
95
  }
101
96
  }
102
- process(service);
97
+ void process(service);
103
98
  return () => {
104
99
  mounted = false;
105
100
  };
@@ -133,22 +128,34 @@ export function Validate({ missingConfig, userRequest, state, status, service, c
133
128
  const handleConfigFinished = (config) => {
134
129
  // Convert flat dotted keys to nested structure grouped by section
135
130
  const configBySection = unflattenConfig(config);
131
+ // Extract and save labels to cache
132
+ if (configRequirements) {
133
+ const labels = {};
134
+ for (const req of configRequirements) {
135
+ if (req.description) {
136
+ labels[req.path] = req.description;
137
+ }
138
+ }
139
+ saveConfigLabels(labels);
140
+ }
136
141
  // Save each section
137
142
  for (const [section, sectionConfig] of Object.entries(configBySection)) {
138
143
  saveConfig(section, sectionConfig);
139
144
  }
140
145
  // Mark validation component as complete before invoking callback
141
146
  // This allows the workflow to proceed to execution
142
- handlers?.completeActive();
147
+ lifecycleHandlers?.completeActive();
143
148
  // Invoke callback which will queue the Execute component
144
- onComplete?.(configRequirements);
149
+ if (configRequirements) {
150
+ onComplete(configRequirements);
151
+ }
145
152
  };
146
153
  const handleConfigAborted = (operation) => {
147
154
  // Mark validation component as complete when aborted
148
- handlers?.completeActive();
155
+ lifecycleHandlers?.completeActive();
149
156
  onAborted(operation);
150
157
  };
151
- 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] }));
158
+ 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 }) })), 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] }));
152
159
  }
153
160
  /**
154
161
  * Build prompt for VALIDATE tool
@@ -5,6 +5,7 @@ import { ComponentStatus, } from '../types/components.js';
5
5
  import { ComponentName, FeedbackType } from '../types/types.js';
6
6
  import { createFeedback, isStateless, markAsDone, } from '../services/components.js';
7
7
  import { DebugLevel } from '../services/configuration.js';
8
+ import { getWarnings } from '../services/logger.js';
8
9
  import { getCancellationMessage } from '../services/messages.js';
9
10
  import { exitApp } from '../services/process.js';
10
11
  import { Component } from './Component.js';
@@ -35,11 +36,8 @@ export const Workflow = ({ initialQueue, debug }) => {
35
36
  return { active: null, pending };
36
37
  });
37
38
  }, []);
38
- // Global handlers for all stateful components
39
- const handlers = useMemo(() => ({
40
- addToQueue: (...items) => {
41
- setQueue((queue) => [...queue, ...items]);
42
- },
39
+ // Focused handler instances - segregated by responsibility
40
+ const stateHandlers = useMemo(() => ({
43
41
  updateState: (newState) => {
44
42
  setCurrent((curr) => {
45
43
  const { active, pending } = curr;
@@ -56,12 +54,41 @@ export const Workflow = ({ initialQueue, debug }) => {
56
54
  return { active: updated, pending };
57
55
  });
58
56
  },
57
+ }), []);
58
+ const lifecycleHandlers = useMemo(() => ({
59
59
  completeActive: (...items) => {
60
60
  moveActiveToPending();
61
61
  if (items.length > 0) {
62
62
  setQueue((queue) => [...items, ...queue]);
63
63
  }
64
64
  },
65
+ }), [moveActiveToPending]);
66
+ const queueHandlers = useMemo(() => ({
67
+ addToQueue: (...items) => {
68
+ setQueue((queue) => [...queue, ...items]);
69
+ },
70
+ }), []);
71
+ const errorHandlers = useMemo(() => ({
72
+ onAborted: (operation) => {
73
+ moveActiveToTimeline();
74
+ // Add feedback to queue
75
+ const message = getCancellationMessage(operation);
76
+ setQueue((queue) => [
77
+ ...queue,
78
+ createFeedback(FeedbackType.Aborted, message),
79
+ ]);
80
+ },
81
+ onError: (error) => {
82
+ moveActiveToTimeline();
83
+ // Add feedback to queue
84
+ setQueue((queue) => [
85
+ ...queue,
86
+ createFeedback(FeedbackType.Failed, error),
87
+ ]);
88
+ },
89
+ }), [moveActiveToTimeline]);
90
+ // Workflow handlers - used for timeline/queue management
91
+ const workflowHandlers = useMemo(() => ({
65
92
  completeActiveAndPending: (...items) => {
66
93
  setCurrent((curr) => {
67
94
  const { active, pending } = curr;
@@ -83,24 +110,7 @@ export const Workflow = ({ initialQueue, debug }) => {
83
110
  addToTimeline: (...items) => {
84
111
  setTimeline((prev) => [...prev, ...items]);
85
112
  },
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]);
113
+ }), []);
104
114
  // Global Esc handler removed - components handle their own Esc individually
105
115
  // Move next item from queue to active
106
116
  useEffect(() => {
@@ -138,6 +148,14 @@ export const Workflow = ({ initialQueue, debug }) => {
138
148
  }
139
149
  // Stateful components stay in active until handlers move them to pending
140
150
  }, [current]);
151
+ // Check for accumulated warnings and add them to timeline
152
+ useEffect(() => {
153
+ const warningMessages = getWarnings();
154
+ if (warningMessages.length > 0) {
155
+ const warningComponents = warningMessages.map((msg) => markAsDone(createFeedback(FeedbackType.Warning, msg)));
156
+ setTimeline((prev) => [...prev, ...warningComponents]);
157
+ }
158
+ }, [timeline, current]);
141
159
  // Move final pending to timeline and exit when all done
142
160
  useEffect(() => {
143
161
  const { active, pending } = current;
@@ -171,23 +189,77 @@ export const Workflow = ({ initialQueue, debug }) => {
171
189
  if (isStateless(active)) {
172
190
  return _jsx(Component, { def: active, debug: debug }, active.id);
173
191
  }
174
- // For stateful components, inject global handlers
192
+ // For stateful components, inject focused handlers
175
193
  const statefulActive = active;
176
194
  const wrappedDef = {
177
195
  ...statefulActive,
178
196
  props: {
179
197
  ...statefulActive.props,
180
- handlers,
198
+ stateHandlers,
199
+ lifecycleHandlers,
200
+ queueHandlers,
201
+ errorHandlers,
202
+ workflowHandlers,
181
203
  },
182
204
  };
183
205
  return _jsx(Component, { def: wrappedDef, debug: debug }, active.id);
184
- }, [current, debug, handlers]);
206
+ }, [
207
+ current,
208
+ debug,
209
+ stateHandlers,
210
+ lifecycleHandlers,
211
+ queueHandlers,
212
+ errorHandlers,
213
+ workflowHandlers,
214
+ ]);
185
215
  const pendingComponent = useMemo(() => {
186
216
  const { pending } = current;
187
217
  if (!pending)
188
218
  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 })] }));
219
+ // For stateless components, render as-is
220
+ if (isStateless(pending)) {
221
+ return _jsx(Component, { def: pending, debug: debug }, pending.id);
222
+ }
223
+ // For stateful components, inject focused handlers (they may have useEffect hooks)
224
+ const statefulPending = pending;
225
+ const wrappedDef = {
226
+ ...statefulPending,
227
+ props: {
228
+ ...statefulPending.props,
229
+ stateHandlers,
230
+ lifecycleHandlers,
231
+ queueHandlers,
232
+ errorHandlers,
233
+ workflowHandlers,
234
+ },
235
+ };
236
+ return _jsx(Component, { def: wrappedDef, debug: debug }, pending.id);
237
+ }, [
238
+ current,
239
+ debug,
240
+ stateHandlers,
241
+ lifecycleHandlers,
242
+ queueHandlers,
243
+ errorHandlers,
244
+ workflowHandlers,
245
+ ]);
246
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: timeline, children: (item) => {
247
+ // For stateful timeline components, inject handlers (useEffect hooks may still run)
248
+ let def = item;
249
+ if (!isStateless(item)) {
250
+ const statefulItem = item;
251
+ def = {
252
+ ...statefulItem,
253
+ props: {
254
+ ...statefulItem.props,
255
+ stateHandlers,
256
+ lifecycleHandlers,
257
+ queueHandlers,
258
+ errorHandlers,
259
+ workflowHandlers,
260
+ },
261
+ };
262
+ }
263
+ return (_jsx(Box, { marginTop: 1, children: _jsx(Component, { def: def, debug: DebugLevel.None }) }, item.id));
264
+ } }, "timeline"), pendingComponent && _jsx(Box, { marginTop: 1, children: pendingComponent }), activeComponent && _jsx(Box, { marginTop: 1, children: activeComponent })] }));
193
265
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prompt-language-shell",
3
- "version": "0.8.0",
3
+ "version": "0.8.4",
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",