prompt-language-shell 0.9.2 → 0.9.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.
Files changed (56) hide show
  1. package/dist/{ui/Main.js → Main.js} +12 -12
  2. package/dist/{ui → components}/Component.js +28 -26
  3. package/dist/{ui → components}/Workflow.js +2 -3
  4. package/dist/{ui → components/controllers}/Answer.js +18 -17
  5. package/dist/{ui → components/controllers}/Command.js +11 -18
  6. package/dist/{ui → components/controllers}/Config.js +8 -116
  7. package/dist/components/controllers/Confirm.js +42 -0
  8. package/dist/{ui → components/controllers}/Execute.js +75 -144
  9. package/dist/{ui → components/controllers}/Introspect.js +12 -28
  10. package/dist/components/controllers/Refinement.js +18 -0
  11. package/dist/components/controllers/Schedule.js +139 -0
  12. package/dist/{ui → components/controllers}/Validate.js +14 -32
  13. package/dist/components/views/Answer.js +28 -0
  14. package/dist/components/views/Command.js +11 -0
  15. package/dist/components/views/Config.js +115 -0
  16. package/dist/components/views/Confirm.js +24 -0
  17. package/dist/components/views/Execute.js +60 -0
  18. package/dist/{ui → components/views}/Feedback.js +3 -3
  19. package/dist/components/views/Introspect.js +17 -0
  20. package/dist/{ui → components/views}/Label.js +3 -3
  21. package/dist/{ui → components/views}/List.js +1 -1
  22. package/dist/{ui → components/views}/Output.js +2 -2
  23. package/dist/components/views/Refinement.js +9 -0
  24. package/dist/{ui → components/views}/Report.js +1 -1
  25. package/dist/components/views/Schedule.js +120 -0
  26. package/dist/{ui → components/views}/Separator.js +1 -1
  27. package/dist/{ui → components/views}/Spinner.js +1 -1
  28. package/dist/{ui → components/views}/Subtask.js +4 -4
  29. package/dist/components/views/Task.js +18 -0
  30. package/dist/components/views/Upcoming.js +30 -0
  31. package/dist/{ui → components/views}/UserQuery.js +1 -1
  32. package/dist/components/views/Validate.js +17 -0
  33. package/dist/{ui → components/views}/Welcome.js +1 -1
  34. package/dist/configuration/steps.js +1 -1
  35. package/dist/execution/handlers.js +19 -53
  36. package/dist/execution/reducer.js +26 -38
  37. package/dist/execution/runner.js +43 -25
  38. package/dist/execution/types.js +3 -4
  39. package/dist/execution/utils.js +1 -1
  40. package/dist/index.js +1 -1
  41. package/dist/services/messages.js +19 -0
  42. package/dist/services/router.js +69 -11
  43. package/dist/services/shell.js +26 -6
  44. package/dist/services/timing.js +1 -0
  45. package/dist/skills/execute.md +15 -7
  46. package/dist/tools/execute.tool.js +0 -4
  47. package/dist/types/schemas.js +0 -1
  48. package/package.json +1 -1
  49. package/dist/execution/hooks.js +0 -291
  50. package/dist/ui/Confirm.js +0 -62
  51. package/dist/ui/Refinement.js +0 -23
  52. package/dist/ui/Schedule.js +0 -257
  53. package/dist/ui/Task.js +0 -11
  54. /package/dist/{ui → components/views}/Debug.js +0 -0
  55. /package/dist/{ui → components/views}/Message.js +0 -0
  56. /package/dist/{ui → components/views}/Panel.js +0 -0
@@ -7,7 +7,7 @@ import { getTotalElapsed } from './utils.js';
7
7
  */
8
8
  export function handleTaskCompletion(index, elapsed, context) {
9
9
  const { tasks, message, summary } = context;
10
- const updatedTaskInfos = tasks.map((task, i) => i === index ? { ...task, status: ExecutionStatus.Success, elapsed } : task);
10
+ const updatedTasks = tasks.map((task, i) => i === index ? { ...task, status: ExecutionStatus.Success, elapsed } : task);
11
11
  if (index < tasks.length - 1) {
12
12
  // More tasks to execute
13
13
  return {
@@ -18,7 +18,7 @@ export function handleTaskCompletion(index, elapsed, context) {
18
18
  finalState: {
19
19
  message,
20
20
  summary,
21
- tasks: updatedTaskInfos,
21
+ tasks: updatedTasks,
22
22
  completionMessage: null,
23
23
  error: null,
24
24
  },
@@ -27,17 +27,17 @@ export function handleTaskCompletion(index, elapsed, context) {
27
27
  }
28
28
  // All tasks complete
29
29
  const summaryText = summary.trim() || 'Execution completed';
30
- const totalElapsed = getTotalElapsed(updatedTaskInfos);
30
+ const totalElapsed = getTotalElapsed(updatedTasks);
31
31
  const completion = `${summaryText} in ${formatDuration(totalElapsed)}.`;
32
32
  return {
33
33
  action: {
34
- type: ExecuteActionType.AllTasksComplete,
34
+ type: ExecuteActionType.ExecutionComplete,
35
35
  payload: { index, elapsed, summaryText },
36
36
  },
37
37
  finalState: {
38
38
  message,
39
39
  summary,
40
- tasks: updatedTaskInfos,
40
+ tasks: updatedTasks,
41
41
  completionMessage: completion,
42
42
  error: null,
43
43
  },
@@ -47,63 +47,29 @@ export function handleTaskCompletion(index, elapsed, context) {
47
47
  /**
48
48
  * Handles task error logic and returns the appropriate action and state.
49
49
  */
50
- export function handleTaskFailure(index, error, elapsed, context) {
50
+ export function handleTaskFailure(index, error, context) {
51
51
  const { tasks, message, summary } = context;
52
- const task = tasks[index];
53
- const isCritical = task.command.critical !== false; // Default to true
54
- const updatedTaskInfos = tasks.map((task, i) => i === index ? { ...task, status: ExecutionStatus.Failed, elapsed } : task);
55
- if (isCritical) {
56
- // Critical failure - stop execution
57
- return {
58
- action: {
59
- type: ExecuteActionType.TaskErrorCritical,
60
- payload: { index, error },
61
- },
62
- finalState: {
63
- message,
64
- summary,
65
- tasks: updatedTaskInfos,
66
- completionMessage: null,
67
- error: null,
68
- },
69
- shouldComplete: true,
70
- };
71
- }
72
- // Non-critical failure - continue to next task
73
- if (index < tasks.length - 1) {
74
- return {
75
- action: {
76
- type: ExecuteActionType.TaskErrorContinue,
77
- payload: { index, elapsed },
78
- },
79
- finalState: {
80
- message,
81
- summary,
82
- tasks: updatedTaskInfos,
83
- completionMessage: null,
84
- error: null,
85
- },
86
- shouldComplete: false,
87
- };
88
- }
89
- // Last task failed (non-critical), complete execution
90
- // Non-critical failures still show completion message with summary
91
- const summaryText = summary.trim() || 'Execution completed';
92
- const totalElapsed = getTotalElapsed(updatedTaskInfos);
93
- const completion = `${summaryText} in ${formatDuration(totalElapsed)}.`;
52
+ const updatedTasks = tasks.map((task, i) => {
53
+ if (i === index) {
54
+ return { ...task, status: ExecutionStatus.Failed, elapsed: 0 };
55
+ }
56
+ else if (i > index && task.status === ExecutionStatus.Pending) {
57
+ return { ...task, status: ExecutionStatus.Cancelled };
58
+ }
59
+ return task;
60
+ });
94
61
  return {
95
62
  action: {
96
- type: ExecuteActionType.LastTaskError,
97
- payload: { index, elapsed, summaryText },
63
+ type: ExecuteActionType.TaskError,
64
+ payload: { index, error },
98
65
  },
99
66
  finalState: {
100
67
  message,
101
68
  summary,
102
- tasks: updatedTaskInfos,
103
- completionMessage: completion,
69
+ tasks: updatedTasks,
70
+ completionMessage: null,
104
71
  error: null,
105
72
  },
106
- shouldComplete: true,
107
73
  };
108
74
  }
109
75
  /**
@@ -33,84 +33,72 @@ export function executeReducer(state, action) {
33
33
  };
34
34
  case ExecuteActionType.TaskStarted: {
35
35
  const updatedTasks = state.tasks.map((task, i) => i === action.payload.index
36
- ? { ...task, status: ExecutionStatus.Running }
36
+ ? {
37
+ ...task,
38
+ status: ExecutionStatus.Running,
39
+ startTime: action.payload.startTime,
40
+ }
37
41
  : task);
38
42
  return {
39
43
  ...state,
40
44
  tasks: updatedTasks,
41
45
  };
42
46
  }
43
- case ExecuteActionType.TaskComplete: {
44
- const updatedTaskInfos = state.tasks.map((task, i) => i === action.payload.index
47
+ case ExecuteActionType.TaskProgress: {
48
+ const updatedTasks = state.tasks.map((task, i) => i === action.payload.index
45
49
  ? {
46
50
  ...task,
47
- status: ExecutionStatus.Success,
48
51
  elapsed: action.payload.elapsed,
52
+ output: action.payload.output,
49
53
  }
50
54
  : task);
51
55
  return {
52
56
  ...state,
53
- tasks: updatedTaskInfos,
57
+ tasks: updatedTasks,
54
58
  };
55
59
  }
56
- case ExecuteActionType.AllTasksComplete: {
57
- const updatedTaskInfos = state.tasks.map((task, i) => i === action.payload.index
60
+ case ExecuteActionType.TaskComplete: {
61
+ const updatedTasks = state.tasks.map((task, i) => i === action.payload.index
58
62
  ? {
59
63
  ...task,
60
64
  status: ExecutionStatus.Success,
61
65
  elapsed: action.payload.elapsed,
62
66
  }
63
67
  : task);
64
- const totalElapsed = getTotalElapsed(updatedTaskInfos);
65
- const completion = `${action.payload.summaryText} in ${formatDuration(totalElapsed)}.`;
66
68
  return {
67
69
  ...state,
68
- tasks: updatedTaskInfos,
69
- completionMessage: completion,
70
- };
71
- }
72
- case ExecuteActionType.TaskErrorCritical: {
73
- const updatedTaskInfos = state.tasks.map((task, i) => i === action.payload.index
74
- ? { ...task, status: ExecutionStatus.Failed, elapsed: 0 }
75
- : task);
76
- return {
77
- ...state,
78
- tasks: updatedTaskInfos,
79
- error: action.payload.error,
70
+ tasks: updatedTasks,
80
71
  };
81
72
  }
82
- case ExecuteActionType.TaskErrorContinue: {
83
- const updatedTaskInfos = state.tasks.map((task, i) => i === action.payload.index
73
+ case ExecuteActionType.ExecutionComplete: {
74
+ const updatedTasks = state.tasks.map((task, i) => i === action.payload.index
84
75
  ? {
85
76
  ...task,
86
- status: ExecutionStatus.Failed,
77
+ status: ExecutionStatus.Success,
87
78
  elapsed: action.payload.elapsed,
88
79
  }
89
80
  : task);
81
+ const totalElapsed = getTotalElapsed(updatedTasks);
82
+ const completion = `${action.payload.summaryText} in ${formatDuration(totalElapsed)}.`;
90
83
  return {
91
84
  ...state,
92
- tasks: updatedTaskInfos,
85
+ tasks: updatedTasks,
86
+ completionMessage: completion,
93
87
  };
94
88
  }
95
- case ExecuteActionType.LastTaskError: {
96
- const updatedTaskInfos = state.tasks.map((task, i) => i === action.payload.index
97
- ? {
98
- ...task,
99
- status: ExecutionStatus.Failed,
100
- elapsed: action.payload.elapsed,
101
- }
89
+ case ExecuteActionType.TaskError: {
90
+ const updatedTasks = state.tasks.map((task, i) => i === action.payload.index
91
+ ? { ...task, status: ExecutionStatus.Failed, elapsed: 0 }
102
92
  : task);
103
- const totalElapsed = getTotalElapsed(updatedTaskInfos);
104
- const completion = `${action.payload.summaryText} in ${formatDuration(totalElapsed)}.`;
105
93
  return {
106
94
  ...state,
107
- tasks: updatedTaskInfos,
108
- completionMessage: completion,
95
+ tasks: updatedTasks,
96
+ error: action.payload.error,
109
97
  };
110
98
  }
111
99
  case ExecuteActionType.CancelExecution: {
112
100
  // Mark running task as aborted, pending tasks as cancelled
113
- const updatedTaskInfos = state.tasks.map((task) => {
101
+ const updatedTasks = state.tasks.map((task) => {
114
102
  if (task.status === ExecutionStatus.Running) {
115
103
  return { ...task, status: ExecutionStatus.Aborted };
116
104
  }
@@ -121,7 +109,7 @@ export function executeReducer(state, action) {
121
109
  });
122
110
  return {
123
111
  ...state,
124
- tasks: updatedTaskInfos,
112
+ tasks: updatedTasks,
125
113
  };
126
114
  }
127
115
  default:
@@ -1,5 +1,14 @@
1
1
  import { ExecutionResult, ExecutionStatus, executeCommand, setOutputCallback, } from '../services/shell.js';
2
2
  import { calculateElapsed } from '../services/utils.js';
3
+ // Maximum number of output lines to keep in memory
4
+ const MAX_OUTPUT_LINES = 128;
5
+ /**
6
+ * Limit output to last MAX_OUTPUT_LINES lines to prevent memory exhaustion
7
+ */
8
+ function limitLines(output) {
9
+ const lines = output.split('\n');
10
+ return lines.slice(-MAX_OUTPUT_LINES).join('\n');
11
+ }
3
12
  /**
4
13
  * Execute a single task and track its progress.
5
14
  * All execution logic is contained here, outside of React components.
@@ -17,21 +26,38 @@ export async function executeTask(command, index, callbacks) {
17
26
  error,
18
27
  workdir,
19
28
  });
29
+ // Throttle updates to avoid excessive re-renders (100ms minimum interval)
30
+ let lastUpdateTime = 0;
31
+ let pendingTimeout;
32
+ const throttledUpdate = () => {
33
+ const now = Date.now();
34
+ if (now - lastUpdateTime >= 100) {
35
+ lastUpdateTime = now;
36
+ callbacks.onUpdate(createOutput());
37
+ }
38
+ else if (!pendingTimeout) {
39
+ pendingTimeout = setTimeout(() => {
40
+ pendingTimeout = undefined;
41
+ lastUpdateTime = Date.now();
42
+ callbacks.onUpdate(createOutput());
43
+ }, 100 - (now - lastUpdateTime));
44
+ }
45
+ };
20
46
  // Set up output streaming callback
21
47
  setOutputCallback((data, stream) => {
22
48
  if (stream === 'stdout') {
23
- stdout += data;
49
+ stdout = limitLines(stdout + data);
24
50
  }
25
51
  else {
26
- stderr += data;
52
+ stderr = limitLines(stderr + data);
27
53
  }
28
- callbacks.onOutputChange?.(createOutput());
54
+ throttledUpdate();
29
55
  });
30
- callbacks.onStart?.();
31
56
  try {
32
57
  const result = await executeCommand(command, undefined, index);
33
- // Clear callback
58
+ // Clear callback and pending timeout
34
59
  setOutputCallback(undefined);
60
+ clearTimeout(pendingTimeout);
35
61
  const elapsed = calculateElapsed(startTime);
36
62
  // Update final output from result
37
63
  stdout = result.output;
@@ -39,42 +65,34 @@ export async function executeTask(command, index, callbacks) {
39
65
  workdir = result.workdir;
40
66
  if (result.result === ExecutionResult.Success) {
41
67
  const output = createOutput();
42
- callbacks.onComplete?.(elapsed, output);
43
- return {
44
- status: ExecutionStatus.Success,
45
- elapsed,
46
- output,
47
- };
68
+ callbacks.onUpdate(output);
69
+ callbacks.onComplete(elapsed, output);
70
+ return { status: ExecutionStatus.Success, elapsed, output };
48
71
  }
49
72
  else {
50
73
  const errorMsg = result.errors || result.error || 'Command failed';
51
74
  error = errorMsg;
52
75
  const output = createOutput();
53
- callbacks.onError?.(errorMsg, elapsed, output);
54
- return {
55
- status: ExecutionStatus.Failed,
56
- elapsed,
57
- output,
58
- };
76
+ callbacks.onUpdate(output);
77
+ callbacks.onError(errorMsg, output);
78
+ return { status: ExecutionStatus.Failed, elapsed, output };
59
79
  }
60
80
  }
61
81
  catch (err) {
62
- // Clear callback
82
+ // Clear callback and pending timeout
63
83
  setOutputCallback(undefined);
84
+ clearTimeout(pendingTimeout);
64
85
  const elapsed = calculateElapsed(startTime);
65
86
  const errorMsg = err instanceof Error ? err.message : 'Unknown error';
66
87
  error = errorMsg;
67
88
  const output = createOutput();
68
- callbacks.onError?.(errorMsg, elapsed, output);
69
- return {
70
- status: ExecutionStatus.Failed,
71
- elapsed,
72
- output,
73
- };
89
+ callbacks.onUpdate(output);
90
+ callbacks.onError(errorMsg, output);
91
+ return { status: ExecutionStatus.Failed, elapsed, output };
74
92
  }
75
93
  }
76
94
  /**
77
- * Create an empty task output
95
+ * Create an empty execution output
78
96
  */
79
97
  export function createEmptyOutput() {
80
98
  return { stdout: '', stderr: '', error: '' };
@@ -4,10 +4,9 @@ export var ExecuteActionType;
4
4
  ExecuteActionType["CommandsReady"] = "COMMANDS_READY";
5
5
  ExecuteActionType["ProcessingError"] = "PROCESSING_ERROR";
6
6
  ExecuteActionType["TaskStarted"] = "TASK_STARTED";
7
+ ExecuteActionType["TaskProgress"] = "TASK_PROGRESS";
7
8
  ExecuteActionType["TaskComplete"] = "TASK_COMPLETE";
8
- ExecuteActionType["AllTasksComplete"] = "ALL_TASKS_COMPLETE";
9
- ExecuteActionType["TaskErrorCritical"] = "TASK_ERROR_CRITICAL";
10
- ExecuteActionType["TaskErrorContinue"] = "TASK_ERROR_CONTINUE";
11
- ExecuteActionType["LastTaskError"] = "LAST_TASK_ERROR";
9
+ ExecuteActionType["ExecutionComplete"] = "EXECUTION_COMPLETE";
10
+ ExecuteActionType["TaskError"] = "TASK_ERROR";
12
11
  ExecuteActionType["CancelExecution"] = "CANCEL_EXECUTION";
13
12
  })(ExecuteActionType || (ExecuteActionType = {}));
@@ -1,6 +1,6 @@
1
1
  import { ExecutionStatus } from '../services/shell.js';
2
2
  /**
3
- * Calculate total elapsed time from task infos
3
+ * Calculate total elapsed time from task data
4
4
  */
5
5
  export function getTotalElapsed(tasks) {
6
6
  return tasks.reduce((sum, task) => sum + task.elapsed, 0);
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import { dirname, join } from 'path';
5
5
  import { fileURLToPath } from 'url';
6
6
  import { render } from 'ink';
7
7
  import { DebugLevel } from './configuration/types.js';
8
- import { Main } from './ui/Main.js';
8
+ import { Main } from './Main.js';
9
9
  const __filename = fileURLToPath(import.meta.url);
10
10
  const __dirname = dirname(__filename);
11
11
  // Get package info
@@ -162,3 +162,22 @@ export function getExecutionErrorMessage(_error) {
162
162
  ];
163
163
  return messages[Math.floor(Math.random() * messages.length)];
164
164
  }
165
+ /**
166
+ * Returns a loading message while fetching an answer.
167
+ * Randomly selects from variations to sound natural.
168
+ */
169
+ export function getAnswerLoadingMessage() {
170
+ const messages = [
171
+ 'Finding that out for you.',
172
+ 'Looking into this.',
173
+ 'Let me find out.',
174
+ 'One moment please.',
175
+ 'Checking on that.',
176
+ 'Let me look that up.',
177
+ 'Give me a moment.',
178
+ 'Looking that up now.',
179
+ 'Let me check.',
180
+ 'Just a moment.',
181
+ ];
182
+ return messages[Math.floor(Math.random() * messages.length)];
183
+ }
@@ -177,11 +177,37 @@ function executeTasksAfterConfirm(tasks, context) {
177
177
  // No missing config - proceed with normal routing
178
178
  routeTasksAfterConfig(scheduledTasks, context);
179
179
  }
180
+ /**
181
+ * Task types that should appear in the upcoming display
182
+ */
183
+ const UPCOMING_TASK_TYPES = [TaskType.Execute, TaskType.Answer];
184
+ /**
185
+ * Collect names of all upcoming execution units (groups and standalone tasks)
186
+ * for display during task execution
187
+ */
188
+ function collectUpcomingNames(scheduledTasks) {
189
+ const names = [];
190
+ for (const task of scheduledTasks) {
191
+ if (task.type === TaskType.Group && task.subtasks?.length) {
192
+ const subtasks = task.subtasks;
193
+ if (UPCOMING_TASK_TYPES.includes(subtasks[0].type)) {
194
+ names.push(task.action);
195
+ }
196
+ }
197
+ else if (UPCOMING_TASK_TYPES.includes(task.type)) {
198
+ names.push(task.action);
199
+ }
200
+ }
201
+ return names;
202
+ }
180
203
  /**
181
204
  * Route tasks after config is complete (or when no config is needed)
182
205
  * Processes tasks in order, grouping by type
183
206
  */
184
207
  function routeTasksAfterConfig(scheduledTasks, context) {
208
+ // Collect all unit names for upcoming display
209
+ const allUnitNames = collectUpcomingNames(scheduledTasks);
210
+ let currentUnitIndex = 0;
185
211
  // Process tasks in order, preserving Group boundaries
186
212
  // Track consecutive standalone tasks to group them by type
187
213
  let consecutiveStandaloneTasks = [];
@@ -201,7 +227,18 @@ function routeTasksAfterConfig(scheduledTasks, context) {
201
227
  const taskType = type;
202
228
  if (typeTasks.length === 0)
203
229
  continue;
204
- routeTasksByType(taskType, typeTasks, context);
230
+ // For tasks that appear in upcoming, calculate from remaining units
231
+ if (UPCOMING_TASK_TYPES.includes(taskType)) {
232
+ // Each task advances the unit index
233
+ for (const task of typeTasks) {
234
+ const upcoming = allUnitNames.slice(currentUnitIndex + 1);
235
+ currentUnitIndex++;
236
+ routeTasksByType(taskType, [task], context, upcoming);
237
+ }
238
+ }
239
+ else {
240
+ routeTasksByType(taskType, typeTasks, context, []);
241
+ }
205
242
  }
206
243
  consecutiveStandaloneTasks = [];
207
244
  };
@@ -214,7 +251,20 @@ function routeTasksAfterConfig(scheduledTasks, context) {
214
251
  if (task.subtasks.length > 0) {
215
252
  const subtasks = task.subtasks;
216
253
  const taskType = subtasks[0].type;
217
- routeTasksByType(taskType, subtasks, context);
254
+ // Calculate upcoming (all units after this one)
255
+ const upcoming = UPCOMING_TASK_TYPES.includes(taskType)
256
+ ? allUnitNames.slice(currentUnitIndex + 1)
257
+ : [];
258
+ if (UPCOMING_TASK_TYPES.includes(taskType)) {
259
+ currentUnitIndex++;
260
+ }
261
+ // Pass group name as label for Execute groups
262
+ if (taskType === TaskType.Execute) {
263
+ routeExecuteTasks(subtasks, context, upcoming, task.action);
264
+ }
265
+ else {
266
+ routeTasksByType(taskType, subtasks, context, upcoming);
267
+ }
218
268
  }
219
269
  }
220
270
  else {
@@ -228,21 +278,29 @@ function routeTasksAfterConfig(scheduledTasks, context) {
228
278
  /**
229
279
  * Route Answer tasks - creates separate Answer component for each question
230
280
  */
231
- function routeAnswerTasks(tasks, context) {
232
- for (const task of tasks) {
233
- context.workflowHandlers.addToQueue(createAnswer({ question: task.action, service: context.service }));
281
+ function routeAnswerTasks(tasks, context, upcoming) {
282
+ for (let i = 0; i < tasks.length; i++) {
283
+ const task = tasks[i];
284
+ // Calculate upcoming: remaining answer tasks + original upcoming
285
+ const remainingAnswers = tasks.slice(i + 1).map((t) => t.action);
286
+ const taskUpcoming = [...remainingAnswers, ...upcoming];
287
+ context.workflowHandlers.addToQueue(createAnswer({
288
+ question: task.action,
289
+ service: context.service,
290
+ upcoming: taskUpcoming,
291
+ }));
234
292
  }
235
293
  }
236
294
  /**
237
295
  * Route Introspect tasks - creates single Introspect component for all tasks
238
296
  */
239
- function routeIntrospectTasks(tasks, context) {
297
+ function routeIntrospectTasks(tasks, context, _upcoming) {
240
298
  context.workflowHandlers.addToQueue(createIntrospect({ tasks, service: context.service }));
241
299
  }
242
300
  /**
243
301
  * Route Config tasks - extracts keys, caches labels, creates Config component
244
302
  */
245
- function routeConfigTasks(tasks, context) {
303
+ function routeConfigTasks(tasks, context, _upcoming) {
246
304
  const configKeys = tasks
247
305
  .map((task) => task.params?.key)
248
306
  .filter((key) => key !== undefined);
@@ -286,8 +344,8 @@ function routeConfigTasks(tasks, context) {
286
344
  /**
287
345
  * Route Execute tasks - creates Execute component (validation already done)
288
346
  */
289
- function routeExecuteTasks(tasks, context) {
290
- context.workflowHandlers.addToQueue(createExecute({ tasks, service: context.service }));
347
+ function routeExecuteTasks(tasks, context, upcoming, label) {
348
+ context.workflowHandlers.addToQueue(createExecute({ tasks, service: context.service, upcoming, label }));
291
349
  }
292
350
  /**
293
351
  * Registry mapping task types to their route handlers
@@ -302,9 +360,9 @@ const taskRouteHandlers = {
302
360
  * Route tasks by type to appropriate components
303
361
  * Uses registry pattern for extensibility
304
362
  */
305
- function routeTasksByType(taskType, tasks, context) {
363
+ function routeTasksByType(taskType, tasks, context, upcoming) {
306
364
  const handler = taskRouteHandlers[taskType];
307
365
  if (handler) {
308
- handler(tasks, context);
366
+ handler(tasks, context, upcoming);
309
367
  }
310
368
  }
@@ -61,6 +61,14 @@ export class DummyExecutor {
61
61
  }
62
62
  // Marker for extracting pwd from command output
63
63
  const PWD_MARKER = '__PWD_MARKER_7x9k2m__';
64
+ const MAX_OUTPUT_LINES = 128;
65
+ /**
66
+ * Limit output to last MAX_OUTPUT_LINES lines.
67
+ */
68
+ function limitLines(output) {
69
+ const lines = output.split('\n');
70
+ return lines.slice(-MAX_OUTPUT_LINES).join('\n');
71
+ }
64
72
  /**
65
73
  * Parse stdout to extract workdir and clean output.
66
74
  * Returns the cleaned output and the extracted workdir.
@@ -93,6 +101,12 @@ class OutputStreamer {
93
101
  */
94
102
  pushStdout(data) {
95
103
  this.chunks.push(data);
104
+ // Collapse when we have too many chunks to prevent memory growth
105
+ if (this.chunks.length > 16) {
106
+ const accumulated = this.chunks.join('');
107
+ this.chunks = [limitLines(accumulated)];
108
+ this.emittedLength = 0;
109
+ }
96
110
  if (!this.callback)
97
111
  return;
98
112
  const accumulated = this.chunks.join('');
@@ -123,7 +137,7 @@ class OutputStreamer {
123
137
  * Get the accumulated raw output.
124
138
  */
125
139
  getAccumulated() {
126
- return this.chunks.join('');
140
+ return limitLines(this.chunks.join(''));
127
141
  }
128
142
  }
129
143
  /**
@@ -189,6 +203,13 @@ export class RealExecutor {
189
203
  child.stderr.on('data', (data) => {
190
204
  const text = data.toString();
191
205
  stderr.push(text);
206
+ // Collapse when we have too many chunks to prevent memory growth
207
+ if (stderr.length > 16) {
208
+ const accumulated = stderr.join('');
209
+ const limited = limitLines(accumulated);
210
+ stderr.length = 0;
211
+ stderr.push(limited);
212
+ }
192
213
  this.outputCallback?.(text, 'stderr');
193
214
  });
194
215
  child.on('error', (error) => {
@@ -200,7 +221,7 @@ export class RealExecutor {
200
221
  description: cmd.description,
201
222
  command: cmd.command,
202
223
  output: stdoutStreamer.getAccumulated(),
203
- errors: error.message,
224
+ errors: limitLines(stderr.join('')) || error.message,
204
225
  result: ExecutionResult.Error,
205
226
  error: error.message,
206
227
  };
@@ -218,7 +239,7 @@ export class RealExecutor {
218
239
  description: cmd.description,
219
240
  command: cmd.command,
220
241
  output,
221
- errors: stderr.join(''),
242
+ errors: limitLines(stderr.join('')),
222
243
  result: success ? ExecutionResult.Success : ExecutionResult.Error,
223
244
  error: success ? undefined : `Exit code: ${code}`,
224
245
  workdir,
@@ -278,9 +299,8 @@ export async function executeCommands(commands, onProgress) {
278
299
  : ExecutionStatus.Failed,
279
300
  output,
280
301
  });
281
- // Stop if critical command failed
282
- const isCritical = cmd.critical !== false;
283
- if (output.result !== ExecutionResult.Success && isCritical) {
302
+ // Stop on failure
303
+ if (output.result !== ExecutionResult.Success) {
284
304
  break;
285
305
  }
286
306
  }
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Timing utilities for UI components
3
3
  */
4
+ export const ELAPSED_UPDATE_INTERVAL = 250;
4
5
  /**
5
6
  * Waits for at least the minimum processing time.
6
7
  * Ensures async operations don't complete too quickly for good UX.