prompt-language-shell 0.8.8 → 0.9.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.
- package/README.md +0 -1
- package/dist/configuration/io.js +22 -1
- package/dist/{services/config-labels.js → configuration/labels.js} +1 -1
- package/dist/configuration/schema.js +2 -2
- package/dist/configuration/steps.js +171 -0
- package/dist/configuration/transformation.js +17 -0
- package/dist/configuration/types.js +3 -4
- package/dist/execution/handlers.js +20 -35
- package/dist/execution/hooks.js +291 -0
- package/dist/execution/processing.js +15 -2
- package/dist/execution/reducer.js +30 -48
- package/dist/execution/runner.js +81 -0
- package/dist/execution/types.js +1 -0
- package/dist/execution/utils.js +28 -0
- package/dist/services/components.js +109 -395
- package/dist/services/filesystem.js +21 -1
- package/dist/services/logger.js +3 -3
- package/dist/services/messages.js +10 -16
- package/dist/services/process.js +7 -2
- package/dist/services/refinement.js +5 -2
- package/dist/services/router.js +120 -67
- package/dist/services/shell.js +179 -10
- package/dist/services/skills.js +2 -1
- package/dist/skills/answer.md +14 -12
- package/dist/skills/execute.md +98 -39
- package/dist/skills/introspect.md +9 -9
- package/dist/skills/schedule.md +0 -6
- package/dist/types/errors.js +47 -0
- package/dist/types/result.js +40 -0
- package/dist/ui/Command.js +11 -7
- package/dist/ui/Component.js +6 -3
- package/dist/ui/Config.js +9 -3
- package/dist/ui/Execute.js +249 -163
- package/dist/ui/Introspect.js +13 -14
- package/dist/ui/List.js +2 -2
- package/dist/ui/Main.js +14 -7
- package/dist/ui/Output.js +54 -0
- package/dist/ui/Schedule.js +3 -1
- package/dist/ui/Subtask.js +6 -3
- package/dist/ui/Task.js +10 -85
- package/dist/ui/Validate.js +26 -21
- package/dist/ui/Workflow.js +21 -4
- package/package.json +1 -1
- package/dist/parser.js +0 -13
- package/dist/services/config-utils.js +0 -20
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState, } from 'react';
|
|
2
|
+
import { ComponentStatus, } from '../types/components.js';
|
|
3
|
+
import { FeedbackType } from '../types/types.js';
|
|
4
|
+
import { createFeedback, createMessage } from '../services/components.js';
|
|
5
|
+
import { formatErrorMessage, getExecutionErrorMessage, } from '../services/messages.js';
|
|
6
|
+
import { ExecutionStatus } from '../services/shell.js';
|
|
7
|
+
import { ensureMinimumTime } from '../services/timing.js';
|
|
8
|
+
import { handleTaskCompletion, handleTaskFailure } from './handlers.js';
|
|
9
|
+
import { processTasks } from './processing.js';
|
|
10
|
+
import { executeTask } from './runner.js';
|
|
11
|
+
import { ExecuteActionType } from './types.js';
|
|
12
|
+
import { getCurrentTaskIndex } from './utils.js';
|
|
13
|
+
const ELAPSED_UPDATE_INTERVAL = 1000;
|
|
14
|
+
/**
|
|
15
|
+
* Track elapsed time from a start timestamp.
|
|
16
|
+
* Returns 0 when not active or no start time.
|
|
17
|
+
*/
|
|
18
|
+
export function useElapsedTimer(startTime, isActive) {
|
|
19
|
+
const [elapsed, setElapsed] = useState(0);
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (!startTime || !isActive)
|
|
22
|
+
return;
|
|
23
|
+
const interval = setInterval(() => {
|
|
24
|
+
setElapsed(Date.now() - startTime);
|
|
25
|
+
}, ELAPSED_UPDATE_INTERVAL);
|
|
26
|
+
return () => {
|
|
27
|
+
clearInterval(interval);
|
|
28
|
+
};
|
|
29
|
+
}, [startTime, isActive]);
|
|
30
|
+
return elapsed;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Manage live output and timing for the currently executing task.
|
|
34
|
+
* Groups related state for tracking a running task's output.
|
|
35
|
+
*/
|
|
36
|
+
export function useLiveTaskOutput() {
|
|
37
|
+
const [output, setOutput] = useState({
|
|
38
|
+
stdout: '',
|
|
39
|
+
stderr: '',
|
|
40
|
+
error: '',
|
|
41
|
+
});
|
|
42
|
+
const [startTime, setStartTime] = useState(null);
|
|
43
|
+
const start = useCallback(() => {
|
|
44
|
+
setOutput({ stdout: '', stderr: '', error: '' });
|
|
45
|
+
setStartTime(Date.now());
|
|
46
|
+
}, []);
|
|
47
|
+
const stop = useCallback(() => {
|
|
48
|
+
setStartTime(null);
|
|
49
|
+
}, []);
|
|
50
|
+
return {
|
|
51
|
+
output,
|
|
52
|
+
startTime,
|
|
53
|
+
setOutput,
|
|
54
|
+
start,
|
|
55
|
+
stop,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Handle execution cancellation with a ref-based flag.
|
|
60
|
+
* The ref is needed because callbacks check the current cancellation state.
|
|
61
|
+
*/
|
|
62
|
+
export function useCancellation() {
|
|
63
|
+
const cancelledRef = useRef(false);
|
|
64
|
+
const cancel = useCallback(() => {
|
|
65
|
+
cancelledRef.current = true;
|
|
66
|
+
}, []);
|
|
67
|
+
const reset = useCallback(() => {
|
|
68
|
+
cancelledRef.current = false;
|
|
69
|
+
}, []);
|
|
70
|
+
return {
|
|
71
|
+
cancelledRef,
|
|
72
|
+
cancel,
|
|
73
|
+
reset,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
const MINIMUM_PROCESSING_TIME = 400;
|
|
77
|
+
/**
|
|
78
|
+
* Helper to create ExecuteState with defaults
|
|
79
|
+
*/
|
|
80
|
+
function createExecuteState(overrides = {}) {
|
|
81
|
+
return {
|
|
82
|
+
message: '',
|
|
83
|
+
summary: '',
|
|
84
|
+
tasks: [],
|
|
85
|
+
completionMessage: null,
|
|
86
|
+
error: null,
|
|
87
|
+
...overrides,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Process input tasks through AI to generate executable commands.
|
|
92
|
+
* Handles the initial phase of task execution.
|
|
93
|
+
*/
|
|
94
|
+
export function useTaskProcessor(config) {
|
|
95
|
+
const { inputTasks, service, isActive, hasProcessed, tasksCount, dispatch, requestHandlers, lifecycleHandlers, workflowHandlers, } = config;
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
if (!isActive || tasksCount > 0 || hasProcessed) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
let mounted = true;
|
|
101
|
+
async function process(svc) {
|
|
102
|
+
const startTime = Date.now();
|
|
103
|
+
try {
|
|
104
|
+
const result = await processTasks(inputTasks, svc);
|
|
105
|
+
await ensureMinimumTime(startTime, MINIMUM_PROCESSING_TIME);
|
|
106
|
+
if (!mounted)
|
|
107
|
+
return;
|
|
108
|
+
// Add debug components to timeline if present
|
|
109
|
+
if (result.debug?.length) {
|
|
110
|
+
workflowHandlers.addToTimeline(...result.debug);
|
|
111
|
+
}
|
|
112
|
+
if (result.commands.length === 0) {
|
|
113
|
+
if (result.error) {
|
|
114
|
+
const errorMessage = getExecutionErrorMessage(result.error);
|
|
115
|
+
workflowHandlers.addToTimeline(createMessage({ text: errorMessage }, ComponentStatus.Done));
|
|
116
|
+
requestHandlers.onCompleted(createExecuteState({ message: result.message }));
|
|
117
|
+
lifecycleHandlers.completeActive();
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
dispatch({
|
|
121
|
+
type: ExecuteActionType.ProcessingComplete,
|
|
122
|
+
payload: { message: result.message },
|
|
123
|
+
});
|
|
124
|
+
requestHandlers.onCompleted(createExecuteState({ message: result.message }));
|
|
125
|
+
lifecycleHandlers.completeActive();
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
// Create task infos from commands
|
|
129
|
+
const taskInfos = result.commands.map((cmd, index) => ({
|
|
130
|
+
label: inputTasks[index]?.action ?? cmd.description,
|
|
131
|
+
command: cmd,
|
|
132
|
+
status: ExecutionStatus.Pending,
|
|
133
|
+
elapsed: 0,
|
|
134
|
+
}));
|
|
135
|
+
dispatch({
|
|
136
|
+
type: ExecuteActionType.CommandsReady,
|
|
137
|
+
payload: {
|
|
138
|
+
message: result.message,
|
|
139
|
+
summary: result.summary,
|
|
140
|
+
tasks: taskInfos,
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
requestHandlers.onCompleted(createExecuteState({
|
|
144
|
+
message: result.message,
|
|
145
|
+
summary: result.summary,
|
|
146
|
+
tasks: taskInfos,
|
|
147
|
+
}));
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
await ensureMinimumTime(startTime, MINIMUM_PROCESSING_TIME);
|
|
151
|
+
if (mounted) {
|
|
152
|
+
const errorMessage = formatErrorMessage(err);
|
|
153
|
+
dispatch({
|
|
154
|
+
type: ExecuteActionType.ProcessingError,
|
|
155
|
+
payload: { error: errorMessage },
|
|
156
|
+
});
|
|
157
|
+
requestHandlers.onCompleted(createExecuteState({ error: errorMessage }));
|
|
158
|
+
requestHandlers.onError(errorMessage);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
void process(service);
|
|
163
|
+
return () => {
|
|
164
|
+
mounted = false;
|
|
165
|
+
};
|
|
166
|
+
}, [
|
|
167
|
+
inputTasks,
|
|
168
|
+
isActive,
|
|
169
|
+
service,
|
|
170
|
+
requestHandlers,
|
|
171
|
+
lifecycleHandlers,
|
|
172
|
+
workflowHandlers,
|
|
173
|
+
tasksCount,
|
|
174
|
+
hasProcessed,
|
|
175
|
+
dispatch,
|
|
176
|
+
]);
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Execute tasks sequentially, managing state and handling completion/errors.
|
|
180
|
+
*/
|
|
181
|
+
export function useTaskExecutor(config) {
|
|
182
|
+
const { isActive, tasks, message, summary, error, workdir, setWorkdir, cancelledRef, liveOutput, dispatch, requestHandlers, lifecycleHandlers, workflowHandlers, } = config;
|
|
183
|
+
const currentTaskIndex = getCurrentTaskIndex(tasks);
|
|
184
|
+
useEffect(() => {
|
|
185
|
+
if (!isActive ||
|
|
186
|
+
tasks.length === 0 ||
|
|
187
|
+
currentTaskIndex >= tasks.length ||
|
|
188
|
+
error) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const currentTask = tasks[currentTaskIndex];
|
|
192
|
+
if (currentTask.status !== ExecutionStatus.Pending) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
cancelledRef.current = false;
|
|
196
|
+
// Mark task as started (running)
|
|
197
|
+
dispatch({
|
|
198
|
+
type: ExecuteActionType.TaskStarted,
|
|
199
|
+
payload: { index: currentTaskIndex },
|
|
200
|
+
});
|
|
201
|
+
// Reset live state for new task
|
|
202
|
+
liveOutput.start();
|
|
203
|
+
// Merge workdir into command
|
|
204
|
+
const command = workdir
|
|
205
|
+
? { ...currentTask.command, workdir }
|
|
206
|
+
: currentTask.command;
|
|
207
|
+
void executeTask(command, currentTaskIndex, {
|
|
208
|
+
onOutputChange: (output) => {
|
|
209
|
+
if (!cancelledRef.current) {
|
|
210
|
+
liveOutput.setOutput(output);
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
onComplete: (elapsed, output) => {
|
|
214
|
+
if (cancelledRef.current)
|
|
215
|
+
return;
|
|
216
|
+
liveOutput.stop();
|
|
217
|
+
// Track working directory
|
|
218
|
+
if (output.workdir) {
|
|
219
|
+
setWorkdir(output.workdir);
|
|
220
|
+
}
|
|
221
|
+
const tasksWithOutput = tasks.map((task, i) => i === currentTaskIndex
|
|
222
|
+
? {
|
|
223
|
+
...task,
|
|
224
|
+
stdout: output.stdout,
|
|
225
|
+
stderr: output.stderr,
|
|
226
|
+
error: output.error,
|
|
227
|
+
}
|
|
228
|
+
: task);
|
|
229
|
+
const result = handleTaskCompletion(currentTaskIndex, elapsed, {
|
|
230
|
+
tasks: tasksWithOutput,
|
|
231
|
+
message,
|
|
232
|
+
summary,
|
|
233
|
+
});
|
|
234
|
+
dispatch(result.action);
|
|
235
|
+
requestHandlers.onCompleted(result.finalState);
|
|
236
|
+
if (result.shouldComplete) {
|
|
237
|
+
lifecycleHandlers.completeActive();
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
onError: (errorMsg, elapsed, output) => {
|
|
241
|
+
if (cancelledRef.current)
|
|
242
|
+
return;
|
|
243
|
+
liveOutput.stop();
|
|
244
|
+
// Track working directory
|
|
245
|
+
if (output.workdir) {
|
|
246
|
+
setWorkdir(output.workdir);
|
|
247
|
+
}
|
|
248
|
+
const tasksWithOutput = tasks.map((task, i) => i === currentTaskIndex
|
|
249
|
+
? {
|
|
250
|
+
...task,
|
|
251
|
+
stdout: output.stdout,
|
|
252
|
+
stderr: output.stderr,
|
|
253
|
+
error: output.error,
|
|
254
|
+
}
|
|
255
|
+
: task);
|
|
256
|
+
const result = handleTaskFailure(currentTaskIndex, errorMsg, elapsed, {
|
|
257
|
+
tasks: tasksWithOutput,
|
|
258
|
+
message,
|
|
259
|
+
summary,
|
|
260
|
+
});
|
|
261
|
+
dispatch(result.action);
|
|
262
|
+
requestHandlers.onCompleted(result.finalState);
|
|
263
|
+
if (result.action.type === ExecuteActionType.TaskErrorCritical) {
|
|
264
|
+
const criticalErrorMessage = getExecutionErrorMessage(errorMsg);
|
|
265
|
+
workflowHandlers.addToQueue(createFeedback({
|
|
266
|
+
type: FeedbackType.Failed,
|
|
267
|
+
message: criticalErrorMessage,
|
|
268
|
+
}));
|
|
269
|
+
}
|
|
270
|
+
if (result.shouldComplete) {
|
|
271
|
+
lifecycleHandlers.completeActive();
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
}, [
|
|
276
|
+
isActive,
|
|
277
|
+
tasks,
|
|
278
|
+
currentTaskIndex,
|
|
279
|
+
message,
|
|
280
|
+
summary,
|
|
281
|
+
error,
|
|
282
|
+
workdir,
|
|
283
|
+
setWorkdir,
|
|
284
|
+
cancelledRef,
|
|
285
|
+
liveOutput,
|
|
286
|
+
dispatch,
|
|
287
|
+
requestHandlers,
|
|
288
|
+
lifecycleHandlers,
|
|
289
|
+
workflowHandlers,
|
|
290
|
+
]);
|
|
291
|
+
}
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { loadUserConfig } from '../services/loader.js';
|
|
2
2
|
import { replacePlaceholders } from '../services/resolver.js';
|
|
3
3
|
import { validatePlaceholderResolution } from './validation.js';
|
|
4
|
+
/**
|
|
5
|
+
* Fix escaped quotes in commands
|
|
6
|
+
* JSON parsing removes backslashes before quotes in patterns like key="value"
|
|
7
|
+
* This restores them: key="value" -> key=\"value\"
|
|
8
|
+
*/
|
|
9
|
+
export function fixEscapedQuotes(command) {
|
|
10
|
+
// Replace ="value" with =\"value\"
|
|
11
|
+
return command.replace(/="([^"]*)"/g, '=\\"$1\\"');
|
|
12
|
+
}
|
|
4
13
|
/**
|
|
5
14
|
* Processes tasks through the AI service to generate executable commands.
|
|
6
15
|
* Resolves placeholders in task descriptions and validates the results.
|
|
@@ -9,7 +18,7 @@ export async function processTasks(tasks, service) {
|
|
|
9
18
|
// Load user config for placeholder resolution
|
|
10
19
|
const userConfig = loadUserConfig();
|
|
11
20
|
// Format tasks for the execute tool and resolve placeholders
|
|
12
|
-
const
|
|
21
|
+
const taskList = tasks
|
|
13
22
|
.map((task) => {
|
|
14
23
|
const resolvedAction = replacePlaceholders(task.action, userConfig);
|
|
15
24
|
const params = task.params
|
|
@@ -18,11 +27,15 @@ export async function processTasks(tasks, service) {
|
|
|
18
27
|
return `- ${resolvedAction}${params}`;
|
|
19
28
|
})
|
|
20
29
|
.join('\n');
|
|
30
|
+
// Build message with confirmed schedule header
|
|
31
|
+
const taskDescriptions = `Confirmed schedule (${tasks.length} tasks):\n${taskList}`;
|
|
21
32
|
// Call execute tool to get commands
|
|
22
33
|
const result = await service.processWithTool(taskDescriptions, 'execute');
|
|
23
34
|
// Resolve placeholders in command strings
|
|
24
35
|
const resolvedCommands = (result.commands || []).map((cmd) => {
|
|
25
|
-
|
|
36
|
+
// Fix escaped quotes lost in JSON parsing
|
|
37
|
+
const fixed = fixEscapedQuotes(cmd.command);
|
|
38
|
+
const resolved = replacePlaceholders(fixed, userConfig);
|
|
26
39
|
validatePlaceholderResolution(resolved);
|
|
27
40
|
return { ...cmd, command: resolved };
|
|
28
41
|
});
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { ExecutionStatus } from '../services/shell.js';
|
|
2
2
|
import { formatDuration } from '../services/utils.js';
|
|
3
3
|
import { ExecuteActionType, } from './types.js';
|
|
4
|
+
import { getTotalElapsed } from './utils.js';
|
|
4
5
|
export const initialState = {
|
|
5
6
|
error: null,
|
|
6
|
-
|
|
7
|
+
tasks: [],
|
|
7
8
|
message: '',
|
|
8
|
-
completed: 0,
|
|
9
9
|
hasProcessed: false,
|
|
10
|
-
taskExecutionTimes: [],
|
|
11
10
|
completionMessage: null,
|
|
12
11
|
summary: '',
|
|
13
12
|
};
|
|
@@ -24,8 +23,7 @@ export function executeReducer(state, action) {
|
|
|
24
23
|
...state,
|
|
25
24
|
message: action.payload.message,
|
|
26
25
|
summary: action.payload.summary,
|
|
27
|
-
|
|
28
|
-
completed: 0,
|
|
26
|
+
tasks: action.payload.tasks,
|
|
29
27
|
};
|
|
30
28
|
case ExecuteActionType.ProcessingError:
|
|
31
29
|
return {
|
|
@@ -33,12 +31,17 @@ export function executeReducer(state, action) {
|
|
|
33
31
|
error: action.payload.error,
|
|
34
32
|
hasProcessed: true,
|
|
35
33
|
};
|
|
34
|
+
case ExecuteActionType.TaskStarted: {
|
|
35
|
+
const updatedTasks = state.tasks.map((task, i) => i === action.payload.index
|
|
36
|
+
? { ...task, status: ExecutionStatus.Running }
|
|
37
|
+
: task);
|
|
38
|
+
return {
|
|
39
|
+
...state,
|
|
40
|
+
tasks: updatedTasks,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
36
43
|
case ExecuteActionType.TaskComplete: {
|
|
37
|
-
const
|
|
38
|
-
...state.taskExecutionTimes,
|
|
39
|
-
action.payload.elapsed,
|
|
40
|
-
];
|
|
41
|
-
const updatedTaskInfos = state.taskInfos.map((task, i) => i === action.payload.index
|
|
44
|
+
const updatedTaskInfos = state.tasks.map((task, i) => i === action.payload.index
|
|
42
45
|
? {
|
|
43
46
|
...task,
|
|
44
47
|
status: ExecutionStatus.Success,
|
|
@@ -47,49 +50,37 @@ export function executeReducer(state, action) {
|
|
|
47
50
|
: task);
|
|
48
51
|
return {
|
|
49
52
|
...state,
|
|
50
|
-
|
|
51
|
-
taskExecutionTimes: updatedTimes,
|
|
52
|
-
completed: action.payload.index + 1,
|
|
53
|
+
tasks: updatedTaskInfos,
|
|
53
54
|
};
|
|
54
55
|
}
|
|
55
56
|
case ExecuteActionType.AllTasksComplete: {
|
|
56
|
-
const
|
|
57
|
-
...state.taskExecutionTimes,
|
|
58
|
-
action.payload.elapsed,
|
|
59
|
-
];
|
|
60
|
-
const updatedTaskInfos = state.taskInfos.map((task, i) => i === action.payload.index
|
|
57
|
+
const updatedTaskInfos = state.tasks.map((task, i) => i === action.payload.index
|
|
61
58
|
? {
|
|
62
59
|
...task,
|
|
63
60
|
status: ExecutionStatus.Success,
|
|
64
61
|
elapsed: action.payload.elapsed,
|
|
65
62
|
}
|
|
66
63
|
: task);
|
|
67
|
-
const totalElapsed =
|
|
64
|
+
const totalElapsed = getTotalElapsed(updatedTaskInfos);
|
|
68
65
|
const completion = `${action.payload.summaryText} in ${formatDuration(totalElapsed)}.`;
|
|
69
66
|
return {
|
|
70
67
|
...state,
|
|
71
|
-
|
|
72
|
-
taskExecutionTimes: updatedTimes,
|
|
73
|
-
completed: action.payload.index + 1,
|
|
68
|
+
tasks: updatedTaskInfos,
|
|
74
69
|
completionMessage: completion,
|
|
75
70
|
};
|
|
76
71
|
}
|
|
77
72
|
case ExecuteActionType.TaskErrorCritical: {
|
|
78
|
-
const updatedTaskInfos = state.
|
|
73
|
+
const updatedTaskInfos = state.tasks.map((task, i) => i === action.payload.index
|
|
79
74
|
? { ...task, status: ExecutionStatus.Failed, elapsed: 0 }
|
|
80
75
|
: task);
|
|
81
76
|
return {
|
|
82
77
|
...state,
|
|
83
|
-
|
|
78
|
+
tasks: updatedTaskInfos,
|
|
84
79
|
error: action.payload.error,
|
|
85
80
|
};
|
|
86
81
|
}
|
|
87
82
|
case ExecuteActionType.TaskErrorContinue: {
|
|
88
|
-
const
|
|
89
|
-
...state.taskExecutionTimes,
|
|
90
|
-
action.payload.elapsed,
|
|
91
|
-
];
|
|
92
|
-
const updatedTaskInfos = state.taskInfos.map((task, i) => i === action.payload.index
|
|
83
|
+
const updatedTaskInfos = state.tasks.map((task, i) => i === action.payload.index
|
|
93
84
|
? {
|
|
94
85
|
...task,
|
|
95
86
|
status: ExecutionStatus.Failed,
|
|
@@ -98,48 +89,39 @@ export function executeReducer(state, action) {
|
|
|
98
89
|
: task);
|
|
99
90
|
return {
|
|
100
91
|
...state,
|
|
101
|
-
|
|
102
|
-
taskExecutionTimes: updatedTimes,
|
|
103
|
-
completed: action.payload.index + 1,
|
|
92
|
+
tasks: updatedTaskInfos,
|
|
104
93
|
};
|
|
105
94
|
}
|
|
106
95
|
case ExecuteActionType.LastTaskError: {
|
|
107
|
-
const
|
|
108
|
-
...state.taskExecutionTimes,
|
|
109
|
-
action.payload.elapsed,
|
|
110
|
-
];
|
|
111
|
-
const updatedTaskInfos = state.taskInfos.map((task, i) => i === action.payload.index
|
|
96
|
+
const updatedTaskInfos = state.tasks.map((task, i) => i === action.payload.index
|
|
112
97
|
? {
|
|
113
98
|
...task,
|
|
114
99
|
status: ExecutionStatus.Failed,
|
|
115
100
|
elapsed: action.payload.elapsed,
|
|
116
101
|
}
|
|
117
102
|
: task);
|
|
118
|
-
const totalElapsed =
|
|
103
|
+
const totalElapsed = getTotalElapsed(updatedTaskInfos);
|
|
119
104
|
const completion = `${action.payload.summaryText} in ${formatDuration(totalElapsed)}.`;
|
|
120
105
|
return {
|
|
121
106
|
...state,
|
|
122
|
-
|
|
123
|
-
taskExecutionTimes: updatedTimes,
|
|
124
|
-
completed: action.payload.index + 1,
|
|
107
|
+
tasks: updatedTaskInfos,
|
|
125
108
|
completionMessage: completion,
|
|
126
109
|
};
|
|
127
110
|
}
|
|
128
111
|
case ExecuteActionType.CancelExecution: {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
else if (taskIndex === action.payload.completed) {
|
|
112
|
+
// Mark running task as aborted, pending tasks as cancelled
|
|
113
|
+
const updatedTaskInfos = state.tasks.map((task) => {
|
|
114
|
+
if (task.status === ExecutionStatus.Running) {
|
|
134
115
|
return { ...task, status: ExecutionStatus.Aborted };
|
|
135
116
|
}
|
|
136
|
-
else {
|
|
117
|
+
else if (task.status === ExecutionStatus.Pending) {
|
|
137
118
|
return { ...task, status: ExecutionStatus.Cancelled };
|
|
138
119
|
}
|
|
120
|
+
return task;
|
|
139
121
|
});
|
|
140
122
|
return {
|
|
141
123
|
...state,
|
|
142
|
-
|
|
124
|
+
tasks: updatedTaskInfos,
|
|
143
125
|
};
|
|
144
126
|
}
|
|
145
127
|
default:
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { ExecutionResult, ExecutionStatus, executeCommand, setOutputCallback, } from '../services/shell.js';
|
|
2
|
+
import { calculateElapsed } from '../services/utils.js';
|
|
3
|
+
/**
|
|
4
|
+
* Execute a single task and track its progress.
|
|
5
|
+
* All execution logic is contained here, outside of React components.
|
|
6
|
+
*/
|
|
7
|
+
export async function executeTask(command, index, callbacks) {
|
|
8
|
+
const startTime = Date.now();
|
|
9
|
+
let stdout = '';
|
|
10
|
+
let stderr = '';
|
|
11
|
+
let error = '';
|
|
12
|
+
let workdir;
|
|
13
|
+
// Helper to create current output snapshot
|
|
14
|
+
const createOutput = () => ({
|
|
15
|
+
stdout,
|
|
16
|
+
stderr,
|
|
17
|
+
error,
|
|
18
|
+
workdir,
|
|
19
|
+
});
|
|
20
|
+
// Set up output streaming callback
|
|
21
|
+
setOutputCallback((data, stream) => {
|
|
22
|
+
if (stream === 'stdout') {
|
|
23
|
+
stdout += data;
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
stderr += data;
|
|
27
|
+
}
|
|
28
|
+
callbacks.onOutputChange?.(createOutput());
|
|
29
|
+
});
|
|
30
|
+
callbacks.onStart?.();
|
|
31
|
+
try {
|
|
32
|
+
const result = await executeCommand(command, undefined, index);
|
|
33
|
+
// Clear callback
|
|
34
|
+
setOutputCallback(undefined);
|
|
35
|
+
const elapsed = calculateElapsed(startTime);
|
|
36
|
+
// Update final output from result
|
|
37
|
+
stdout = result.output;
|
|
38
|
+
stderr = result.errors;
|
|
39
|
+
workdir = result.workdir;
|
|
40
|
+
if (result.result === ExecutionResult.Success) {
|
|
41
|
+
const output = createOutput();
|
|
42
|
+
callbacks.onComplete?.(elapsed, output);
|
|
43
|
+
return {
|
|
44
|
+
status: ExecutionStatus.Success,
|
|
45
|
+
elapsed,
|
|
46
|
+
output,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
const errorMsg = result.errors || result.error || 'Command failed';
|
|
51
|
+
error = errorMsg;
|
|
52
|
+
const output = createOutput();
|
|
53
|
+
callbacks.onError?.(errorMsg, elapsed, output);
|
|
54
|
+
return {
|
|
55
|
+
status: ExecutionStatus.Failed,
|
|
56
|
+
elapsed,
|
|
57
|
+
output,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
// Clear callback
|
|
63
|
+
setOutputCallback(undefined);
|
|
64
|
+
const elapsed = calculateElapsed(startTime);
|
|
65
|
+
const errorMsg = err instanceof Error ? err.message : 'Unknown error';
|
|
66
|
+
error = errorMsg;
|
|
67
|
+
const output = createOutput();
|
|
68
|
+
callbacks.onError?.(errorMsg, elapsed, output);
|
|
69
|
+
return {
|
|
70
|
+
status: ExecutionStatus.Failed,
|
|
71
|
+
elapsed,
|
|
72
|
+
output,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Create an empty task output
|
|
78
|
+
*/
|
|
79
|
+
export function createEmptyOutput() {
|
|
80
|
+
return { stdout: '', stderr: '', error: '' };
|
|
81
|
+
}
|
package/dist/execution/types.js
CHANGED
|
@@ -3,6 +3,7 @@ export var ExecuteActionType;
|
|
|
3
3
|
ExecuteActionType["ProcessingComplete"] = "PROCESSING_COMPLETE";
|
|
4
4
|
ExecuteActionType["CommandsReady"] = "COMMANDS_READY";
|
|
5
5
|
ExecuteActionType["ProcessingError"] = "PROCESSING_ERROR";
|
|
6
|
+
ExecuteActionType["TaskStarted"] = "TASK_STARTED";
|
|
6
7
|
ExecuteActionType["TaskComplete"] = "TASK_COMPLETE";
|
|
7
8
|
ExecuteActionType["AllTasksComplete"] = "ALL_TASKS_COMPLETE";
|
|
8
9
|
ExecuteActionType["TaskErrorCritical"] = "TASK_ERROR_CRITICAL";
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { ExecutionStatus } from '../services/shell.js';
|
|
2
|
+
/**
|
|
3
|
+
* Calculate total elapsed time from task infos
|
|
4
|
+
*/
|
|
5
|
+
export function getTotalElapsed(tasks) {
|
|
6
|
+
return tasks.reduce((sum, task) => sum + task.elapsed, 0);
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Calculate the number of finished tasks (success, failed, or aborted)
|
|
10
|
+
*/
|
|
11
|
+
export function getCompletedCount(tasks) {
|
|
12
|
+
return tasks.filter((task) => task.status === ExecutionStatus.Success ||
|
|
13
|
+
task.status === ExecutionStatus.Failed ||
|
|
14
|
+
task.status === ExecutionStatus.Aborted).length;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Get the index of the current task to execute.
|
|
18
|
+
* Returns the index of the first Running or Pending task, or tasks.length if all done.
|
|
19
|
+
*/
|
|
20
|
+
export function getCurrentTaskIndex(tasks) {
|
|
21
|
+
const runningIndex = tasks.findIndex((t) => t.status === ExecutionStatus.Running);
|
|
22
|
+
if (runningIndex !== -1)
|
|
23
|
+
return runningIndex;
|
|
24
|
+
const pendingIndex = tasks.findIndex((t) => t.status === ExecutionStatus.Pending);
|
|
25
|
+
if (pendingIndex !== -1)
|
|
26
|
+
return pendingIndex;
|
|
27
|
+
return tasks.length;
|
|
28
|
+
}
|