prompt-language-shell 0.8.4 → 0.8.8

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 (41) 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 +36 -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 +2 -1
  14. package/dist/services/colors.js +22 -12
  15. package/dist/services/components.js +35 -11
  16. package/dist/services/config-labels.js +15 -15
  17. package/dist/services/logger.js +2 -1
  18. package/dist/services/messages.js +53 -1
  19. package/dist/services/refinement.js +11 -6
  20. package/dist/services/router.js +92 -52
  21. package/dist/skills/execute.md +79 -9
  22. package/dist/skills/schedule.md +121 -29
  23. package/dist/tools/execute.tool.js +4 -0
  24. package/dist/types/schemas.js +1 -0
  25. package/dist/ui/Answer.js +36 -15
  26. package/dist/ui/Command.js +43 -23
  27. package/dist/ui/Component.js +147 -33
  28. package/dist/ui/Config.js +73 -79
  29. package/dist/ui/Confirm.js +34 -21
  30. package/dist/ui/Execute.js +129 -329
  31. package/dist/ui/Feedback.js +2 -1
  32. package/dist/ui/Introspect.js +51 -24
  33. package/dist/ui/Label.js +4 -3
  34. package/dist/ui/List.js +3 -2
  35. package/dist/ui/Main.js +5 -1
  36. package/dist/ui/Refinement.js +8 -1
  37. package/dist/ui/Schedule.js +89 -61
  38. package/dist/ui/Validate.js +75 -77
  39. package/dist/ui/Workflow.js +47 -123
  40. package/package.json +1 -1
  41. package/dist/services/configuration.js +0 -409
@@ -1,31 +1,53 @@
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
+ import { ComponentStatus, } from '../types/components.js';
5
5
  import { Colors, getTextColor, Palette } from '../services/colors.js';
6
6
  import { useInput } from '../services/keyboard.js';
7
7
  import { UserQuery } from './UserQuery.js';
8
- export function Confirm({ message, state, status, stateHandlers, onConfirmed, onCancelled, }) {
8
+ export const ConfirmView = ({ message, state, status }) => {
9
9
  const isActive = status === ComponentStatus.Active;
10
- const [selectedIndex, setSelectedIndex] = useState(state?.selectedIndex ?? 0); // 0 = Yes, 1 = No
10
+ const { selectedIndex } = state;
11
+ const options = [
12
+ { label: 'yes', value: 'yes', color: Palette.BrightGreen },
13
+ { label: 'no', value: 'no', color: Colors.Status.Error },
14
+ ];
15
+ // Timeline rendering (Done status)
16
+ if (status === ComponentStatus.Done) {
17
+ 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] })] }));
18
+ }
19
+ // Active/Pending rendering
20
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, marginLeft: 1, children: _jsx(Text, { color: getTextColor(isActive), children: message }) }), _jsxs(Box, { marginLeft: 1, children: [_jsx(Text, { color: Colors.Action.Select, children: ">" }), _jsx(Text, { children: " " }), _jsx(Box, { children: options.map((option, index) => {
21
+ const isSelected = index === selectedIndex;
22
+ return (_jsx(Box, { marginRight: 2, children: _jsx(Text, { color: isSelected ? option.color : undefined, dimColor: !isSelected, children: option.label }) }, option.value));
23
+ }) })] })] }));
24
+ };
25
+ /**
26
+ * Confirm controller: Manages yes/no selection
27
+ */
28
+ export function Confirm({ message, status, requestHandlers, onConfirmed, onCancelled, }) {
29
+ const isActive = status === ComponentStatus.Active;
30
+ const [selectedIndex, setSelectedIndex] = useState(0); // 0 = Yes, 1 = No
11
31
  useInput((input, key) => {
12
32
  if (!isActive)
13
33
  return;
14
34
  if (key.escape) {
15
35
  // Escape: highlight "No" and cancel
16
- setSelectedIndex(1);
17
- stateHandlers?.updateState({ selectedIndex: 1 });
36
+ const finalState = { selectedIndex: 1, confirmed: false };
37
+ requestHandlers.onCompleted(finalState);
18
38
  onCancelled();
19
39
  }
20
40
  else if (key.tab) {
21
41
  // Toggle between Yes (0) and No (1)
22
- const newIndex = selectedIndex === 0 ? 1 : 0;
23
- setSelectedIndex(newIndex);
24
- stateHandlers?.updateState({ selectedIndex: newIndex });
42
+ setSelectedIndex((prev) => (prev === 0 ? 1 : 0));
25
43
  }
26
44
  else if (key.return) {
27
45
  // Confirm selection
28
- stateHandlers?.updateState({ selectedIndex, confirmed: true });
46
+ const finalState = {
47
+ selectedIndex,
48
+ confirmed: true,
49
+ };
50
+ requestHandlers.onCompleted(finalState);
29
51
  if (selectedIndex === 0) {
30
52
  onConfirmed();
31
53
  }
@@ -34,16 +56,7 @@ export function Confirm({ message, state, status, stateHandlers, onConfirmed, on
34
56
  }
35
57
  }
36
58
  }, { isActive });
37
- const options = [
38
- { label: 'yes', value: 'yes', color: Palette.BrightGreen },
39
- { label: 'no', value: 'no', color: Colors.Status.Error },
40
- ];
41
- if (!isActive) {
42
- // When done, show both the message and user's choice in timeline
43
- 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] })] }));
44
- }
45
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, marginLeft: 1, children: _jsx(Text, { color: getTextColor(isActive), children: message }) }), _jsxs(Box, { marginLeft: 1, children: [_jsx(Text, { color: Colors.Action.Select, children: ">" }), _jsx(Text, { children: " " }), _jsx(Box, { children: options.map((option, index) => {
46
- const isSelected = index === selectedIndex;
47
- return (_jsx(Box, { marginRight: 2, children: _jsx(Text, { color: isSelected ? option.color : undefined, dimColor: !isSelected, children: option.label }) }, option.value));
48
- }) })] })] }));
59
+ // Controller always renders View, passing current state
60
+ const state = { selectedIndex, confirmed: false };
61
+ return _jsx(ConfirmView, { message: message, state: state, status: status });
49
62
  }
@@ -2,182 +2,51 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useCallback, useEffect, useReducer } from 'react';
3
3
  import { Box, Text } from 'ink';
4
4
  import { ComponentStatus, } from '../types/components.js';
5
- import { Colors, getTextColor } from '../services/colors.js';
5
+ import { getTextColor } from '../services/colors.js';
6
6
  import { useInput } from '../services/keyboard.js';
7
- import { loadUserConfig } from '../services/loader.js';
8
- import { formatErrorMessage } from '../services/messages.js';
9
- import { replacePlaceholders } from '../services/resolver.js';
7
+ import { formatErrorMessage, getExecutionErrorMessage, } from '../services/messages.js';
10
8
  import { ExecutionStatus } from '../services/shell.js';
11
9
  import { ensureMinimumTime } from '../services/timing.js';
12
- import { formatDuration } from '../services/utils.js';
10
+ import { buildAbortedState, handleTaskCompletion, handleTaskFailure, } from '../execution/handlers.js';
11
+ import { processTasks } from '../execution/processing.js';
12
+ import { executeReducer, initialState } from '../execution/reducer.js';
13
+ import { ExecuteActionType } from '../execution/types.js';
14
+ import { createMessage, markAsDone } from '../services/components.js';
15
+ import { Message } from './Message.js';
13
16
  import { Spinner } from './Spinner.js';
14
17
  import { Task } from './Task.js';
15
18
  const MINIMUM_PROCESSING_TIME = 400;
19
+ export const ExecuteView = ({ state, status, onTaskComplete, onTaskAbort, onTaskError, }) => {
20
+ const isActive = status === ComponentStatus.Active;
21
+ const { error, taskInfos, message, completed, completionMessage } = state;
22
+ const hasProcessed = taskInfos.length > 0;
23
+ // Derive loading state from current conditions
24
+ const isLoading = isActive && taskInfos.length === 0 && !error && !hasProcessed;
25
+ const isExecuting = completed < taskInfos.length;
26
+ // Return null only when loading completes with no commands
27
+ if (!isActive && taskInfos.length === 0 && !error) {
28
+ return null;
29
+ }
30
+ // Show completed steps when not active
31
+ const showTasks = !isActive && taskInfos.length > 0;
32
+ 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 || showTasks) && (_jsxs(Box, { flexDirection: "column", marginLeft: 1, children: [message && (_jsxs(Box, { marginBottom: 1, gap: 1, children: [_jsx(Text, { color: getTextColor(isActive), children: message }), isExecuting && _jsx(Spinner, {})] })), taskInfos.map((taskInfo, index) => (_jsx(Box, { marginBottom: index < taskInfos.length - 1 ? 1 : 0, children: _jsx(Task, { label: taskInfo.label, command: taskInfo.command, isActive: isActive && index === completed, index: index, initialStatus: taskInfo.status, initialElapsed: taskInfo.elapsed, onComplete: onTaskComplete, onAbort: onTaskAbort, onError: onTaskError }) }, index)))] })), completionMessage && !isActive && (_jsx(Box, { marginTop: 1, marginLeft: 1, children: _jsx(Text, { color: getTextColor(false), children: completionMessage }) })), error && _jsx(Message, { text: error, status: status })] }));
33
+ };
16
34
  /**
17
- * Validates that all placeholders in a command have been resolved.
18
- * Throws an error if unresolved placeholders are found.
35
+ * Execute controller: Runs tasks sequentially
19
36
  */
20
- function validatePlaceholderResolution(command, original) {
21
- const unresolvedPattern = /\{[^}]+\}/g;
22
- const matches = command.match(unresolvedPattern);
23
- if (matches && matches.length > 0) {
24
- throw new Error(`Unresolved placeholders in command: ${matches.join(', ')}\nCommand: ${original}`);
25
- }
26
- }
27
- function executeReducer(state, action) {
28
- switch (action.type) {
29
- case 'PROCESSING_COMPLETE':
30
- return {
31
- ...state,
32
- message: action.payload.message,
33
- hasProcessed: true,
34
- };
35
- case 'COMMANDS_READY':
36
- return {
37
- ...state,
38
- message: action.payload.message,
39
- summary: action.payload.summary,
40
- taskInfos: action.payload.taskInfos,
41
- completed: 0,
42
- };
43
- case 'PROCESSING_ERROR':
44
- return {
45
- ...state,
46
- error: action.payload.error,
47
- hasProcessed: true,
48
- };
49
- case 'TASK_COMPLETE': {
50
- const updatedTimes = [
51
- ...state.taskExecutionTimes,
52
- action.payload.elapsed,
53
- ];
54
- const updatedTaskInfos = state.taskInfos.map((task, i) => i === action.payload.index
55
- ? {
56
- ...task,
57
- status: ExecutionStatus.Success,
58
- elapsed: action.payload.elapsed,
59
- }
60
- : task);
61
- return {
62
- ...state,
63
- taskInfos: updatedTaskInfos,
64
- taskExecutionTimes: updatedTimes,
65
- completed: action.payload.index + 1,
66
- };
67
- }
68
- case 'ALL_TASKS_COMPLETE': {
69
- const updatedTimes = [
70
- ...state.taskExecutionTimes,
71
- action.payload.elapsed,
72
- ];
73
- const updatedTaskInfos = state.taskInfos.map((task, i) => i === action.payload.index
74
- ? {
75
- ...task,
76
- status: ExecutionStatus.Success,
77
- elapsed: action.payload.elapsed,
78
- }
79
- : task);
80
- const totalElapsed = updatedTimes.reduce((sum, time) => sum + time, 0);
81
- const completion = `${action.payload.summaryText} in ${formatDuration(totalElapsed)}.`;
82
- return {
83
- ...state,
84
- taskInfos: updatedTaskInfos,
85
- taskExecutionTimes: updatedTimes,
86
- completed: action.payload.index + 1,
87
- completionMessage: completion,
88
- };
89
- }
90
- case 'TASK_ERROR_CRITICAL': {
91
- const updatedTaskInfos = state.taskInfos.map((task, i) => i === action.payload.index
92
- ? { ...task, status: ExecutionStatus.Failed, elapsed: 0 }
93
- : task);
94
- return {
95
- ...state,
96
- taskInfos: updatedTaskInfos,
97
- error: action.payload.error,
98
- };
99
- }
100
- case 'TASK_ERROR_CONTINUE': {
101
- const updatedTimes = [
102
- ...state.taskExecutionTimes,
103
- action.payload.elapsed,
104
- ];
105
- const updatedTaskInfos = state.taskInfos.map((task, i) => i === action.payload.index
106
- ? {
107
- ...task,
108
- status: ExecutionStatus.Failed,
109
- elapsed: action.payload.elapsed,
110
- }
111
- : task);
112
- return {
113
- ...state,
114
- taskInfos: updatedTaskInfos,
115
- taskExecutionTimes: updatedTimes,
116
- completed: action.payload.index + 1,
117
- };
118
- }
119
- case 'LAST_TASK_ERROR': {
120
- const updatedTimes = [
121
- ...state.taskExecutionTimes,
122
- action.payload.elapsed,
123
- ];
124
- const updatedTaskInfos = state.taskInfos.map((task, i) => i === action.payload.index
125
- ? {
126
- ...task,
127
- status: ExecutionStatus.Failed,
128
- elapsed: action.payload.elapsed,
129
- }
130
- : task);
131
- const totalElapsed = updatedTimes.reduce((sum, time) => sum + time, 0);
132
- const completion = `${action.payload.summaryText} in ${formatDuration(totalElapsed)}.`;
133
- return {
134
- ...state,
135
- taskInfos: updatedTaskInfos,
136
- taskExecutionTimes: updatedTimes,
137
- completed: action.payload.index + 1,
138
- completionMessage: completion,
139
- };
140
- }
141
- case 'CANCEL_EXECUTION': {
142
- const updatedTaskInfos = state.taskInfos.map((task, taskIndex) => {
143
- if (taskIndex < action.payload.completed) {
144
- return { ...task, status: ExecutionStatus.Success };
145
- }
146
- else if (taskIndex === action.payload.completed) {
147
- return { ...task, status: ExecutionStatus.Aborted };
148
- }
149
- else {
150
- return { ...task, status: ExecutionStatus.Cancelled };
151
- }
152
- });
153
- return {
154
- ...state,
155
- taskInfos: updatedTaskInfos,
156
- };
157
- }
158
- default:
159
- return state;
160
- }
161
- }
162
- export function Execute({ tasks, state, status, service, stateHandlers, lifecycleHandlers, errorHandlers, workflowHandlers, }) {
37
+ export function Execute({ tasks, status, service, requestHandlers, lifecycleHandlers, workflowHandlers, }) {
163
38
  const isActive = status === ComponentStatus.Active;
164
- const [localState, dispatch] = useReducer(executeReducer, {
165
- error: state?.error ?? null,
166
- taskInfos: state?.taskInfos ?? [],
167
- message: state?.message ?? '',
168
- completed: state?.completed ?? 0,
169
- hasProcessed: false,
170
- taskExecutionTimes: state?.taskExecutionTimes ?? [],
171
- completionMessage: state?.completionMessage ?? null,
172
- summary: state?.summary ?? '',
173
- });
39
+ const [localState, dispatch] = useReducer(executeReducer, initialState);
174
40
  const { error, taskInfos, message, completed, hasProcessed, taskExecutionTimes, completionMessage, summary, } = localState;
175
41
  // Derive loading state from current conditions
176
42
  const isLoading = isActive && taskInfos.length === 0 && !error && !hasProcessed;
177
43
  const isExecuting = completed < taskInfos.length;
178
44
  // Handle cancel with useCallback to ensure we capture latest state
179
45
  const handleCancel = useCallback(() => {
180
- dispatch({ type: 'CANCEL_EXECUTION', payload: { completed } });
46
+ dispatch({
47
+ type: ExecuteActionType.CancelExecution,
48
+ payload: { completed },
49
+ });
181
50
  // Get updated task infos after cancel
182
51
  const updatedTaskInfos = taskInfos.map((task, taskIndex) => {
183
52
  if (taskIndex < completed) {
@@ -190,7 +59,8 @@ export function Execute({ tasks, state, status, service, stateHandlers, lifecycl
190
59
  return { ...task, status: ExecutionStatus.Cancelled };
191
60
  }
192
61
  });
193
- stateHandlers?.updateState({
62
+ // Expose final state
63
+ const finalState = {
194
64
  message,
195
65
  summary,
196
66
  taskInfos: updatedTaskInfos,
@@ -198,16 +68,16 @@ export function Execute({ tasks, state, status, service, stateHandlers, lifecycl
198
68
  taskExecutionTimes,
199
69
  completionMessage: null,
200
70
  error: null,
201
- });
202
- errorHandlers?.onAborted('execution');
71
+ };
72
+ requestHandlers.onCompleted(finalState);
73
+ requestHandlers.onAborted('execution');
203
74
  }, [
204
75
  message,
205
76
  summary,
206
77
  taskInfos,
207
78
  completed,
208
79
  taskExecutionTimes,
209
- stateHandlers,
210
- errorHandlers,
80
+ requestHandlers,
211
81
  ]);
212
82
  useInput((_, key) => {
213
83
  if (key.escape && (isLoading || isExecuting) && isActive) {
@@ -223,33 +93,40 @@ export function Execute({ tasks, state, status, service, stateHandlers, lifecycl
223
93
  async function process(svc) {
224
94
  const startTime = Date.now();
225
95
  try {
226
- // Load user config for placeholder resolution
227
- const userConfig = loadUserConfig();
228
- // Format tasks for the execute tool and resolve placeholders
229
- const taskDescriptions = tasks
230
- .map((task) => {
231
- const resolvedAction = replacePlaceholders(task.action, userConfig);
232
- const params = task.params
233
- ? ` (params: ${JSON.stringify(task.params)})`
234
- : '';
235
- return `- ${resolvedAction}${params}`;
236
- })
237
- .join('\n');
238
- // Call execute tool to get commands
239
- const result = await svc.processWithTool(taskDescriptions, 'execute');
96
+ const result = await processTasks(tasks, svc);
240
97
  await ensureMinimumTime(startTime, MINIMUM_PROCESSING_TIME);
241
98
  if (!mounted)
242
99
  return;
243
100
  // Add debug components to timeline if present
244
101
  if (result.debug?.length) {
245
- workflowHandlers?.addToTimeline(...result.debug);
102
+ workflowHandlers.addToTimeline(...result.debug);
246
103
  }
247
- if (!result.commands || result.commands.length === 0) {
104
+ if (result.commands.length === 0) {
105
+ // Check if this is an error response (has error field)
106
+ if (result.error) {
107
+ // Add error message to timeline
108
+ const errorMessage = getExecutionErrorMessage(result.error);
109
+ workflowHandlers.addToTimeline(markAsDone(createMessage(errorMessage)));
110
+ // Complete without error in state (message already in timeline)
111
+ const finalState = {
112
+ message: result.message,
113
+ summary: '',
114
+ taskInfos: [],
115
+ completed: 0,
116
+ taskExecutionTimes: [],
117
+ completionMessage: null,
118
+ error: null,
119
+ };
120
+ requestHandlers.onCompleted(finalState);
121
+ lifecycleHandlers.completeActive();
122
+ return;
123
+ }
124
+ // No commands and no error - just complete
248
125
  dispatch({
249
- type: 'PROCESSING_COMPLETE',
126
+ type: ExecuteActionType.ProcessingComplete,
250
127
  payload: { message: result.message },
251
128
  });
252
- stateHandlers?.updateState({
129
+ const finalState = {
253
130
  message: result.message,
254
131
  summary: '',
255
132
  taskInfos: [],
@@ -257,51 +134,45 @@ export function Execute({ tasks, state, status, service, stateHandlers, lifecycl
257
134
  taskExecutionTimes: [],
258
135
  completionMessage: null,
259
136
  error: null,
260
- });
261
- lifecycleHandlers?.completeActive();
137
+ };
138
+ requestHandlers.onCompleted(finalState);
139
+ lifecycleHandlers.completeActive();
262
140
  return;
263
141
  }
264
- // Resolve placeholders in command strings
265
- const resolvedCommands = result.commands.map((cmd) => {
266
- const resolved = replacePlaceholders(cmd.command, userConfig);
267
- validatePlaceholderResolution(resolved, cmd.command);
268
- return { ...cmd, command: resolved };
269
- });
270
- // Set message, summary, and create task infos
271
- const newMessage = result.message;
272
- const newSummary = result.summary || '';
273
- const infos = resolvedCommands.map((cmd, index) => ({
142
+ // Create task infos from commands
143
+ const infos = result.commands.map((cmd, index) => ({
274
144
  label: tasks[index]?.action,
275
145
  command: cmd,
276
146
  }));
277
147
  dispatch({
278
- type: 'COMMANDS_READY',
148
+ type: ExecuteActionType.CommandsReady,
279
149
  payload: {
280
- message: newMessage,
281
- summary: newSummary,
150
+ message: result.message,
151
+ summary: result.summary,
282
152
  taskInfos: infos,
283
153
  },
284
154
  });
285
155
  // Update state after AI processing
286
- stateHandlers?.updateState({
287
- message: newMessage,
288
- summary: newSummary,
156
+ const finalState = {
157
+ message: result.message,
158
+ summary: result.summary,
289
159
  taskInfos: infos,
290
160
  completed: 0,
291
161
  taskExecutionTimes: [],
292
162
  completionMessage: null,
293
163
  error: null,
294
- });
164
+ };
165
+ requestHandlers.onCompleted(finalState);
295
166
  }
296
167
  catch (err) {
297
168
  await ensureMinimumTime(startTime, MINIMUM_PROCESSING_TIME);
298
169
  if (mounted) {
299
170
  const errorMessage = formatErrorMessage(err);
300
171
  dispatch({
301
- type: 'PROCESSING_ERROR',
172
+ type: ExecuteActionType.ProcessingError,
302
173
  payload: { error: errorMessage },
303
174
  });
304
- stateHandlers?.updateState({
175
+ const finalState = {
305
176
  message: '',
306
177
  summary: '',
307
178
  taskInfos: [],
@@ -309,8 +180,9 @@ export function Execute({ tasks, state, status, service, stateHandlers, lifecycl
309
180
  taskExecutionTimes: [],
310
181
  completionMessage: null,
311
182
  error: errorMessage,
312
- });
313
- errorHandlers?.onError(errorMessage);
183
+ };
184
+ requestHandlers.onCompleted(finalState);
185
+ requestHandlers.onError(errorMessage);
314
186
  }
315
187
  }
316
188
  }
@@ -322,150 +194,78 @@ export function Execute({ tasks, state, status, service, stateHandlers, lifecycl
322
194
  tasks,
323
195
  isActive,
324
196
  service,
325
- stateHandlers,
197
+ requestHandlers,
326
198
  lifecycleHandlers,
327
199
  workflowHandlers,
328
- errorHandlers,
329
200
  taskInfos.length,
330
201
  hasProcessed,
331
202
  ]);
332
203
  // Handle task completion - move to next task
333
204
  const handleTaskComplete = useCallback((index, _output, elapsed) => {
334
- if (index < taskInfos.length - 1) {
335
- // More tasks to execute
336
- dispatch({ type: 'TASK_COMPLETE', payload: { index, elapsed } });
337
- const updatedTimes = [...taskExecutionTimes, elapsed];
338
- const updatedTaskInfos = taskInfos.map((task, i) => i === index
339
- ? { ...task, status: ExecutionStatus.Success, elapsed }
340
- : task);
341
- stateHandlers?.updateState({
342
- message,
343
- summary,
344
- taskInfos: updatedTaskInfos,
345
- completed: index + 1,
346
- taskExecutionTimes: updatedTimes,
347
- completionMessage: null,
348
- error: null,
349
- });
350
- }
351
- else {
352
- // All tasks complete
353
- const summaryText = summary.trim() || 'Execution completed';
354
- dispatch({
355
- type: 'ALL_TASKS_COMPLETE',
356
- payload: { index, elapsed, summaryText },
357
- });
358
- const updatedTimes = [...taskExecutionTimes, elapsed];
359
- const updatedTaskInfos = taskInfos.map((task, i) => i === index
360
- ? { ...task, status: ExecutionStatus.Success, elapsed }
361
- : task);
362
- const totalElapsed = updatedTimes.reduce((sum, time) => sum + time, 0);
363
- const completion = `${summaryText} in ${formatDuration(totalElapsed)}.`;
364
- stateHandlers?.updateState({
365
- message,
366
- summary,
367
- taskInfos: updatedTaskInfos,
368
- completed: index + 1,
369
- taskExecutionTimes: updatedTimes,
370
- completionMessage: completion,
371
- error: null,
372
- });
373
- lifecycleHandlers?.completeActive();
205
+ const result = handleTaskCompletion(index, elapsed, {
206
+ taskInfos,
207
+ message,
208
+ summary,
209
+ taskExecutionTimes,
210
+ });
211
+ dispatch(result.action);
212
+ requestHandlers.onCompleted(result.finalState);
213
+ if (result.shouldComplete) {
214
+ lifecycleHandlers.completeActive();
374
215
  }
375
216
  }, [
376
217
  taskInfos,
377
218
  message,
378
- lifecycleHandlers,
379
- taskExecutionTimes,
380
219
  summary,
381
- stateHandlers,
220
+ taskExecutionTimes,
221
+ requestHandlers,
222
+ lifecycleHandlers,
382
223
  ]);
383
224
  const handleTaskError = useCallback((index, error, elapsed) => {
384
- const task = taskInfos[index];
385
- const isCritical = task.command.critical !== false; // Default to true
386
- const updatedTaskInfos = taskInfos.map((task, i) => i === index
387
- ? { ...task, status: ExecutionStatus.Failed, elapsed }
388
- : task);
389
- if (isCritical) {
390
- // Critical failure - stop execution
391
- dispatch({ type: 'TASK_ERROR_CRITICAL', payload: { index, error } });
392
- stateHandlers?.updateState({
393
- message,
394
- summary,
395
- taskInfos: updatedTaskInfos,
396
- completed: index + 1,
397
- taskExecutionTimes,
398
- completionMessage: null,
399
- error,
400
- });
401
- errorHandlers?.onError(error);
225
+ const result = handleTaskFailure(index, error, elapsed, {
226
+ taskInfos,
227
+ message,
228
+ summary,
229
+ taskExecutionTimes,
230
+ });
231
+ dispatch(result.action);
232
+ requestHandlers.onCompleted(result.finalState);
233
+ if (result.shouldReportError) {
234
+ requestHandlers.onError(error);
402
235
  }
403
- else {
404
- // Non-critical failure - continue to next task
405
- const updatedTimes = [...taskExecutionTimes, elapsed];
406
- if (index < taskInfos.length - 1) {
407
- dispatch({
408
- type: 'TASK_ERROR_CONTINUE',
409
- payload: { index, elapsed },
410
- });
411
- stateHandlers?.updateState({
412
- message,
413
- summary,
414
- taskInfos: updatedTaskInfos,
415
- completed: index + 1,
416
- taskExecutionTimes: updatedTimes,
417
- completionMessage: null,
418
- error: null,
419
- });
420
- }
421
- else {
422
- // Last task, complete execution
423
- const summaryText = summary.trim() || 'Execution completed';
424
- dispatch({
425
- type: 'LAST_TASK_ERROR',
426
- payload: { index, elapsed, summaryText },
427
- });
428
- const totalElapsed = updatedTimes.reduce((sum, time) => sum + time, 0);
429
- const completion = `${summaryText} in ${formatDuration(totalElapsed)}.`;
430
- stateHandlers?.updateState({
431
- message,
432
- summary,
433
- taskInfos: updatedTaskInfos,
434
- completed: index + 1,
435
- taskExecutionTimes: updatedTimes,
436
- completionMessage: completion,
437
- error: null,
438
- });
439
- lifecycleHandlers?.completeActive();
440
- }
236
+ if (result.shouldComplete) {
237
+ lifecycleHandlers.completeActive();
441
238
  }
442
239
  }, [
443
240
  taskInfos,
444
241
  message,
445
- stateHandlers,
446
- lifecycleHandlers,
447
- errorHandlers,
448
- taskExecutionTimes,
449
242
  summary,
243
+ taskExecutionTimes,
244
+ requestHandlers,
245
+ lifecycleHandlers,
450
246
  ]);
451
247
  const handleTaskAbort = useCallback((_index) => {
452
248
  // Task was aborted - execution already stopped by Escape handler
453
249
  // Just update state, don't call onAborted (already called at Execute level)
454
- stateHandlers?.updateState({
455
- message,
456
- summary,
457
- taskInfos,
458
- completed,
459
- taskExecutionTimes,
460
- completionMessage: null,
461
- error: null,
462
- });
463
- }, [taskInfos, message, summary, completed, taskExecutionTimes, stateHandlers]);
464
- // Return null only when loading completes with no commands
465
- if (!isActive && taskInfos.length === 0 && !error) {
466
- return null;
467
- }
468
- // Show completed steps when not active
469
- const showTasks = !isActive && taskInfos.length > 0;
470
- 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 || showTasks) && (_jsxs(Box, { flexDirection: "column", marginLeft: 1, children: [message && (_jsxs(Box, { marginBottom: 1, gap: 1, children: [_jsx(Text, { color: getTextColor(isActive), children: message }), isExecuting && _jsx(Spinner, {})] })), taskInfos.map((taskInfo, index) => (_jsx(Box, { marginBottom: index < taskInfos.length - 1 ? 1 : 0, children: _jsx(Task, { label: taskInfo.label, command: taskInfo.command, isActive: isActive && index === completed, index: index, initialStatus: taskInfo.status, initialElapsed: taskInfo.elapsed, onComplete: handleTaskComplete, onAbort: handleTaskAbort, onError: handleTaskError }) }, index)))] })), completionMessage && !isActive && (_jsx(Box, { marginTop: 1, marginLeft: 1, children: _jsx(Text, { color: getTextColor(false), children: completionMessage }) })), error && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: Colors.Status.Error, children: ["Error: ", error] }) }))] }));
250
+ const finalState = buildAbortedState(taskInfos, message, summary, completed, taskExecutionTimes);
251
+ requestHandlers.onCompleted(finalState);
252
+ }, [
253
+ taskInfos,
254
+ message,
255
+ summary,
256
+ completed,
257
+ taskExecutionTimes,
258
+ requestHandlers,
259
+ ]);
260
+ // Controller always renders View with current state
261
+ const viewState = {
262
+ error,
263
+ taskInfos,
264
+ message,
265
+ summary,
266
+ completed,
267
+ taskExecutionTimes,
268
+ completionMessage,
269
+ };
270
+ return (_jsx(ExecuteView, { tasks: tasks, state: viewState, status: status, onTaskComplete: handleTaskComplete, onTaskAbort: handleTaskAbort, onTaskError: handleTaskError }));
471
271
  }