prompt-language-shell 0.8.4 → 0.8.6

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.
@@ -2,182 +2,50 @@ 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
7
  import { formatErrorMessage } from '../services/messages.js';
9
- import { replacePlaceholders } from '../services/resolver.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 { Message } from './Message.js';
13
15
  import { Spinner } from './Spinner.js';
14
16
  import { Task } from './Task.js';
15
17
  const MINIMUM_PROCESSING_TIME = 400;
18
+ export const ExecuteView = ({ state, status, onTaskComplete, onTaskAbort, onTaskError, }) => {
19
+ const isActive = status === ComponentStatus.Active;
20
+ const { error, taskInfos, message, completed, completionMessage } = state;
21
+ const hasProcessed = taskInfos.length > 0;
22
+ // Derive loading state from current conditions
23
+ const isLoading = isActive && taskInfos.length === 0 && !error && !hasProcessed;
24
+ const isExecuting = completed < taskInfos.length;
25
+ // Return null only when loading completes with no commands
26
+ if (!isActive && taskInfos.length === 0 && !error) {
27
+ return null;
28
+ }
29
+ // Show completed steps when not active
30
+ const showTasks = !isActive && taskInfos.length > 0;
31
+ 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 })] }));
32
+ };
16
33
  /**
17
- * Validates that all placeholders in a command have been resolved.
18
- * Throws an error if unresolved placeholders are found.
34
+ * Execute controller: Runs tasks sequentially
19
35
  */
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, }) {
36
+ export function Execute({ tasks, status, service, requestHandlers, lifecycleHandlers, workflowHandlers, }) {
163
37
  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
- });
38
+ const [localState, dispatch] = useReducer(executeReducer, initialState);
174
39
  const { error, taskInfos, message, completed, hasProcessed, taskExecutionTimes, completionMessage, summary, } = localState;
175
40
  // Derive loading state from current conditions
176
41
  const isLoading = isActive && taskInfos.length === 0 && !error && !hasProcessed;
177
42
  const isExecuting = completed < taskInfos.length;
178
43
  // Handle cancel with useCallback to ensure we capture latest state
179
44
  const handleCancel = useCallback(() => {
180
- dispatch({ type: 'CANCEL_EXECUTION', payload: { completed } });
45
+ dispatch({
46
+ type: ExecuteActionType.CancelExecution,
47
+ payload: { completed },
48
+ });
181
49
  // Get updated task infos after cancel
182
50
  const updatedTaskInfos = taskInfos.map((task, taskIndex) => {
183
51
  if (taskIndex < completed) {
@@ -190,7 +58,8 @@ export function Execute({ tasks, state, status, service, stateHandlers, lifecycl
190
58
  return { ...task, status: ExecutionStatus.Cancelled };
191
59
  }
192
60
  });
193
- stateHandlers?.updateState({
61
+ // Expose final state
62
+ const finalState = {
194
63
  message,
195
64
  summary,
196
65
  taskInfos: updatedTaskInfos,
@@ -198,16 +67,16 @@ export function Execute({ tasks, state, status, service, stateHandlers, lifecycl
198
67
  taskExecutionTimes,
199
68
  completionMessage: null,
200
69
  error: null,
201
- });
202
- errorHandlers?.onAborted('execution');
70
+ };
71
+ requestHandlers.onCompleted(finalState);
72
+ requestHandlers.onAborted('execution');
203
73
  }, [
204
74
  message,
205
75
  summary,
206
76
  taskInfos,
207
77
  completed,
208
78
  taskExecutionTimes,
209
- stateHandlers,
210
- errorHandlers,
79
+ requestHandlers,
211
80
  ]);
212
81
  useInput((_, key) => {
213
82
  if (key.escape && (isLoading || isExecuting) && isActive) {
@@ -223,33 +92,20 @@ export function Execute({ tasks, state, status, service, stateHandlers, lifecycl
223
92
  async function process(svc) {
224
93
  const startTime = Date.now();
225
94
  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');
95
+ const result = await processTasks(tasks, svc);
240
96
  await ensureMinimumTime(startTime, MINIMUM_PROCESSING_TIME);
241
97
  if (!mounted)
242
98
  return;
243
99
  // Add debug components to timeline if present
244
100
  if (result.debug?.length) {
245
- workflowHandlers?.addToTimeline(...result.debug);
101
+ workflowHandlers.addToTimeline(...result.debug);
246
102
  }
247
- if (!result.commands || result.commands.length === 0) {
103
+ if (result.commands.length === 0) {
248
104
  dispatch({
249
- type: 'PROCESSING_COMPLETE',
105
+ type: ExecuteActionType.ProcessingComplete,
250
106
  payload: { message: result.message },
251
107
  });
252
- stateHandlers?.updateState({
108
+ const finalState = {
253
109
  message: result.message,
254
110
  summary: '',
255
111
  taskInfos: [],
@@ -257,51 +113,45 @@ export function Execute({ tasks, state, status, service, stateHandlers, lifecycl
257
113
  taskExecutionTimes: [],
258
114
  completionMessage: null,
259
115
  error: null,
260
- });
261
- lifecycleHandlers?.completeActive();
116
+ };
117
+ requestHandlers.onCompleted(finalState);
118
+ lifecycleHandlers.completeActive();
262
119
  return;
263
120
  }
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) => ({
121
+ // Create task infos from commands
122
+ const infos = result.commands.map((cmd, index) => ({
274
123
  label: tasks[index]?.action,
275
124
  command: cmd,
276
125
  }));
277
126
  dispatch({
278
- type: 'COMMANDS_READY',
127
+ type: ExecuteActionType.CommandsReady,
279
128
  payload: {
280
- message: newMessage,
281
- summary: newSummary,
129
+ message: result.message,
130
+ summary: result.summary,
282
131
  taskInfos: infos,
283
132
  },
284
133
  });
285
134
  // Update state after AI processing
286
- stateHandlers?.updateState({
287
- message: newMessage,
288
- summary: newSummary,
135
+ const finalState = {
136
+ message: result.message,
137
+ summary: result.summary,
289
138
  taskInfos: infos,
290
139
  completed: 0,
291
140
  taskExecutionTimes: [],
292
141
  completionMessage: null,
293
142
  error: null,
294
- });
143
+ };
144
+ requestHandlers.onCompleted(finalState);
295
145
  }
296
146
  catch (err) {
297
147
  await ensureMinimumTime(startTime, MINIMUM_PROCESSING_TIME);
298
148
  if (mounted) {
299
149
  const errorMessage = formatErrorMessage(err);
300
150
  dispatch({
301
- type: 'PROCESSING_ERROR',
151
+ type: ExecuteActionType.ProcessingError,
302
152
  payload: { error: errorMessage },
303
153
  });
304
- stateHandlers?.updateState({
154
+ const finalState = {
305
155
  message: '',
306
156
  summary: '',
307
157
  taskInfos: [],
@@ -309,8 +159,9 @@ export function Execute({ tasks, state, status, service, stateHandlers, lifecycl
309
159
  taskExecutionTimes: [],
310
160
  completionMessage: null,
311
161
  error: errorMessage,
312
- });
313
- errorHandlers?.onError(errorMessage);
162
+ };
163
+ requestHandlers.onCompleted(finalState);
164
+ requestHandlers.onError(errorMessage);
314
165
  }
315
166
  }
316
167
  }
@@ -322,150 +173,78 @@ export function Execute({ tasks, state, status, service, stateHandlers, lifecycl
322
173
  tasks,
323
174
  isActive,
324
175
  service,
325
- stateHandlers,
176
+ requestHandlers,
326
177
  lifecycleHandlers,
327
178
  workflowHandlers,
328
- errorHandlers,
329
179
  taskInfos.length,
330
180
  hasProcessed,
331
181
  ]);
332
182
  // Handle task completion - move to next task
333
183
  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();
184
+ const result = handleTaskCompletion(index, elapsed, {
185
+ taskInfos,
186
+ message,
187
+ summary,
188
+ taskExecutionTimes,
189
+ });
190
+ dispatch(result.action);
191
+ requestHandlers.onCompleted(result.finalState);
192
+ if (result.shouldComplete) {
193
+ lifecycleHandlers.completeActive();
374
194
  }
375
195
  }, [
376
196
  taskInfos,
377
197
  message,
378
- lifecycleHandlers,
379
- taskExecutionTimes,
380
198
  summary,
381
- stateHandlers,
199
+ taskExecutionTimes,
200
+ requestHandlers,
201
+ lifecycleHandlers,
382
202
  ]);
383
203
  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);
204
+ const result = handleTaskFailure(index, error, elapsed, {
205
+ taskInfos,
206
+ message,
207
+ summary,
208
+ taskExecutionTimes,
209
+ });
210
+ dispatch(result.action);
211
+ requestHandlers.onCompleted(result.finalState);
212
+ if (result.shouldReportError) {
213
+ requestHandlers.onError(error);
402
214
  }
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
- }
215
+ if (result.shouldComplete) {
216
+ lifecycleHandlers.completeActive();
441
217
  }
442
218
  }, [
443
219
  taskInfos,
444
220
  message,
445
- stateHandlers,
446
- lifecycleHandlers,
447
- errorHandlers,
448
- taskExecutionTimes,
449
221
  summary,
222
+ taskExecutionTimes,
223
+ requestHandlers,
224
+ lifecycleHandlers,
450
225
  ]);
451
226
  const handleTaskAbort = useCallback((_index) => {
452
227
  // Task was aborted - execution already stopped by Escape handler
453
228
  // 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] }) }))] }));
229
+ const finalState = buildAbortedState(taskInfos, message, summary, completed, taskExecutionTimes);
230
+ requestHandlers.onCompleted(finalState);
231
+ }, [
232
+ taskInfos,
233
+ message,
234
+ summary,
235
+ completed,
236
+ taskExecutionTimes,
237
+ requestHandlers,
238
+ ]);
239
+ // Controller always renders View with current state
240
+ const viewState = {
241
+ error,
242
+ taskInfos,
243
+ message,
244
+ summary,
245
+ completed,
246
+ taskExecutionTimes,
247
+ completionMessage,
248
+ };
249
+ return (_jsx(ExecuteView, { tasks: tasks, state: viewState, status: status, onTaskComplete: handleTaskComplete, onTaskAbort: handleTaskAbort, onTaskError: handleTaskError }));
471
250
  }