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.
Files changed (45) hide show
  1. package/README.md +0 -1
  2. package/dist/configuration/io.js +22 -1
  3. package/dist/{services/config-labels.js → configuration/labels.js} +1 -1
  4. package/dist/configuration/schema.js +2 -2
  5. package/dist/configuration/steps.js +171 -0
  6. package/dist/configuration/transformation.js +17 -0
  7. package/dist/configuration/types.js +3 -4
  8. package/dist/execution/handlers.js +20 -35
  9. package/dist/execution/hooks.js +291 -0
  10. package/dist/execution/processing.js +15 -2
  11. package/dist/execution/reducer.js +30 -48
  12. package/dist/execution/runner.js +81 -0
  13. package/dist/execution/types.js +1 -0
  14. package/dist/execution/utils.js +28 -0
  15. package/dist/services/components.js +109 -395
  16. package/dist/services/filesystem.js +21 -1
  17. package/dist/services/logger.js +3 -3
  18. package/dist/services/messages.js +10 -16
  19. package/dist/services/process.js +7 -2
  20. package/dist/services/refinement.js +5 -2
  21. package/dist/services/router.js +120 -67
  22. package/dist/services/shell.js +179 -10
  23. package/dist/services/skills.js +2 -1
  24. package/dist/skills/answer.md +14 -12
  25. package/dist/skills/execute.md +98 -39
  26. package/dist/skills/introspect.md +9 -9
  27. package/dist/skills/schedule.md +0 -6
  28. package/dist/types/errors.js +47 -0
  29. package/dist/types/result.js +40 -0
  30. package/dist/ui/Command.js +11 -7
  31. package/dist/ui/Component.js +6 -3
  32. package/dist/ui/Config.js +9 -3
  33. package/dist/ui/Execute.js +249 -163
  34. package/dist/ui/Introspect.js +13 -14
  35. package/dist/ui/List.js +2 -2
  36. package/dist/ui/Main.js +14 -7
  37. package/dist/ui/Output.js +54 -0
  38. package/dist/ui/Schedule.js +3 -1
  39. package/dist/ui/Subtask.js +6 -3
  40. package/dist/ui/Task.js +10 -85
  41. package/dist/ui/Validate.js +26 -21
  42. package/dist/ui/Workflow.js +21 -4
  43. package/package.json +1 -1
  44. package/dist/parser.js +0 -13
  45. 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 taskDescriptions = tasks
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
- const resolved = replacePlaceholders(cmd.command, userConfig);
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
- taskInfos: [],
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
- taskInfos: action.payload.taskInfos,
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 updatedTimes = [
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
- taskInfos: updatedTaskInfos,
51
- taskExecutionTimes: updatedTimes,
52
- completed: action.payload.index + 1,
53
+ tasks: updatedTaskInfos,
53
54
  };
54
55
  }
55
56
  case ExecuteActionType.AllTasksComplete: {
56
- const updatedTimes = [
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 = updatedTimes.reduce((sum, time) => sum + time, 0);
64
+ const totalElapsed = getTotalElapsed(updatedTaskInfos);
68
65
  const completion = `${action.payload.summaryText} in ${formatDuration(totalElapsed)}.`;
69
66
  return {
70
67
  ...state,
71
- taskInfos: updatedTaskInfos,
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.taskInfos.map((task, i) => i === action.payload.index
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
- taskInfos: updatedTaskInfos,
78
+ tasks: updatedTaskInfos,
84
79
  error: action.payload.error,
85
80
  };
86
81
  }
87
82
  case ExecuteActionType.TaskErrorContinue: {
88
- const updatedTimes = [
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
- taskInfos: updatedTaskInfos,
102
- taskExecutionTimes: updatedTimes,
103
- completed: action.payload.index + 1,
92
+ tasks: updatedTaskInfos,
104
93
  };
105
94
  }
106
95
  case ExecuteActionType.LastTaskError: {
107
- const updatedTimes = [
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 = updatedTimes.reduce((sum, time) => sum + time, 0);
103
+ const totalElapsed = getTotalElapsed(updatedTaskInfos);
119
104
  const completion = `${action.payload.summaryText} in ${formatDuration(totalElapsed)}.`;
120
105
  return {
121
106
  ...state,
122
- taskInfos: updatedTaskInfos,
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
- const updatedTaskInfos = state.taskInfos.map((task, taskIndex) => {
130
- if (taskIndex < action.payload.completed) {
131
- return { ...task, status: ExecutionStatus.Success };
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
- taskInfos: updatedTaskInfos,
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
+ }
@@ -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
+ }