prompt-language-shell 0.8.8 → 0.9.0
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/configuration/types.js +3 -4
- package/dist/execution/handlers.js +20 -29
- package/dist/execution/processing.js +12 -1
- package/dist/execution/reducer.js +17 -37
- package/dist/execution/utils.js +6 -0
- package/dist/services/components.js +1 -2
- package/dist/services/filesystem.js +21 -1
- package/dist/services/messages.js +10 -16
- package/dist/services/process.js +7 -2
- package/dist/services/router.js +87 -57
- 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 +70 -29
- 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/Component.js +2 -2
- package/dist/ui/Execute.js +135 -112
- package/dist/ui/Output.js +54 -0
- package/dist/ui/Task.js +99 -10
- package/package.json +1 -1
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error codes for categorization and programmatic handling
|
|
3
|
+
*/
|
|
4
|
+
export var ErrorCode;
|
|
5
|
+
(function (ErrorCode) {
|
|
6
|
+
// User errors - display to user, usually recoverable
|
|
7
|
+
ErrorCode["InvalidInput"] = "INVALID_INPUT";
|
|
8
|
+
ErrorCode["MissingConfig"] = "MISSING_CONFIG";
|
|
9
|
+
ErrorCode["SkillNotFound"] = "SKILL_NOT_FOUND";
|
|
10
|
+
// System errors - log + display, may be recoverable
|
|
11
|
+
ErrorCode["FileReadError"] = "FILE_READ_ERROR";
|
|
12
|
+
ErrorCode["FileWriteError"] = "FILE_WRITE_ERROR";
|
|
13
|
+
ErrorCode["NetworkError"] = "NETWORK_ERROR";
|
|
14
|
+
ErrorCode["ApiError"] = "API_ERROR";
|
|
15
|
+
ErrorCode["ParseError"] = "PARSE_ERROR";
|
|
16
|
+
// Fatal errors - must abort
|
|
17
|
+
ErrorCode["CircularReference"] = "CIRCULAR_REFERENCE";
|
|
18
|
+
ErrorCode["InvalidState"] = "INVALID_STATE";
|
|
19
|
+
ErrorCode["ConfigCorruption"] = "CONFIG_CORRUPTION";
|
|
20
|
+
})(ErrorCode || (ErrorCode = {}));
|
|
21
|
+
/**
|
|
22
|
+
* Base error class with cause chain support
|
|
23
|
+
* Provides consistent error structure throughout the application
|
|
24
|
+
*/
|
|
25
|
+
export class AppError extends Error {
|
|
26
|
+
code;
|
|
27
|
+
cause;
|
|
28
|
+
constructor(message, code, cause) {
|
|
29
|
+
super(message);
|
|
30
|
+
this.code = code;
|
|
31
|
+
this.cause = cause;
|
|
32
|
+
this.name = 'AppError';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Type guard for AppError
|
|
37
|
+
*/
|
|
38
|
+
export function isAppError(error) {
|
|
39
|
+
return error instanceof AppError;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Helper to wrap unknown errors with context
|
|
43
|
+
*/
|
|
44
|
+
export function wrapError(error, code, message) {
|
|
45
|
+
const cause = error instanceof Error ? error : undefined;
|
|
46
|
+
return new AppError(message, code, cause);
|
|
47
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create a successful result
|
|
3
|
+
*/
|
|
4
|
+
export function ok(value) {
|
|
5
|
+
return { ok: true, value };
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Create a failed result
|
|
9
|
+
*/
|
|
10
|
+
export function err(error) {
|
|
11
|
+
return { ok: false, error };
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Unwrap a result, throwing if it's an error
|
|
15
|
+
*/
|
|
16
|
+
export function unwrap(result) {
|
|
17
|
+
if (result.ok)
|
|
18
|
+
return result.value;
|
|
19
|
+
throw result.error;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Map the value of a successful result
|
|
23
|
+
*/
|
|
24
|
+
export function mapResult(result, fn) {
|
|
25
|
+
if (result.ok)
|
|
26
|
+
return ok(fn(result.value));
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Check if a result is successful
|
|
31
|
+
*/
|
|
32
|
+
export function isOk(result) {
|
|
33
|
+
return result.ok;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Check if a result is an error
|
|
37
|
+
*/
|
|
38
|
+
export function isErr(result) {
|
|
39
|
+
return !result.ok;
|
|
40
|
+
}
|
package/dist/ui/Component.js
CHANGED
|
@@ -107,8 +107,8 @@ export const ViewComponent = memo(function ViewComponent({ def, }) {
|
|
|
107
107
|
return (_jsx(ScheduleView, { message: message, tasks: tasks, state: state, status: status }));
|
|
108
108
|
}
|
|
109
109
|
case ComponentName.Execute: {
|
|
110
|
-
const {
|
|
111
|
-
return _jsx(ExecuteView, {
|
|
110
|
+
const { state, status } = def;
|
|
111
|
+
return _jsx(ExecuteView, { state: state, status: status });
|
|
112
112
|
}
|
|
113
113
|
case ComponentName.Answer: {
|
|
114
114
|
const { props: { question }, state, status, } = def;
|
package/dist/ui/Execute.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useCallback, useEffect, useReducer } from 'react';
|
|
2
|
+
import { useCallback, useEffect, useReducer, useRef } from 'react';
|
|
3
3
|
import { Box, Text } from 'ink';
|
|
4
4
|
import { ComponentStatus, } from '../types/components.js';
|
|
5
5
|
import { getTextColor } from '../services/colors.js';
|
|
@@ -11,74 +11,108 @@ import { buildAbortedState, handleTaskCompletion, handleTaskFailure, } from '../
|
|
|
11
11
|
import { processTasks } from '../execution/processing.js';
|
|
12
12
|
import { executeReducer, initialState } from '../execution/reducer.js';
|
|
13
13
|
import { ExecuteActionType } from '../execution/types.js';
|
|
14
|
-
import { createMessage, markAsDone } from '../services/components.js';
|
|
14
|
+
import { createFeedback, createMessage, markAsDone, } from '../services/components.js';
|
|
15
|
+
import { FeedbackType } from '../types/types.js';
|
|
15
16
|
import { Message } from './Message.js';
|
|
16
17
|
import { Spinner } from './Spinner.js';
|
|
17
18
|
import { Task } from './Task.js';
|
|
18
19
|
const MINIMUM_PROCESSING_TIME = 400;
|
|
19
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Create an ExecuteState with defaults
|
|
22
|
+
*/
|
|
23
|
+
function createExecuteState(overrides = {}) {
|
|
24
|
+
return {
|
|
25
|
+
message: '',
|
|
26
|
+
summary: '',
|
|
27
|
+
tasks: [],
|
|
28
|
+
completed: 0,
|
|
29
|
+
completionMessage: null,
|
|
30
|
+
error: null,
|
|
31
|
+
...overrides,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export const ExecuteView = ({ state, status, workdir, onOutputChange, onTaskComplete, onTaskAbort, onTaskError, }) => {
|
|
20
35
|
const isActive = status === ComponentStatus.Active;
|
|
21
|
-
const { error,
|
|
22
|
-
const hasProcessed =
|
|
36
|
+
const { error, tasks, message, completed, completionMessage } = state;
|
|
37
|
+
const hasProcessed = tasks.length > 0;
|
|
23
38
|
// Derive loading state from current conditions
|
|
24
|
-
const isLoading = isActive &&
|
|
25
|
-
const isExecuting = completed <
|
|
39
|
+
const isLoading = isActive && tasks.length === 0 && !error && !hasProcessed;
|
|
40
|
+
const isExecuting = completed < tasks.length;
|
|
26
41
|
// Return null only when loading completes with no commands
|
|
27
|
-
if (!isActive &&
|
|
42
|
+
if (!isActive && tasks.length === 0 && !error) {
|
|
28
43
|
return null;
|
|
29
44
|
}
|
|
30
45
|
// Show completed steps when not active
|
|
31
|
-
const showTasks = !isActive &&
|
|
32
|
-
return (_jsxs(Box, { alignSelf: "flex-start", flexDirection: "column", children: [isLoading && (_jsxs(Box, { marginLeft: 1, children: [_jsx(Text, { color: getTextColor(isActive), children: "Preparing commands. " }), _jsx(Spinner, {})] })), (isExecuting || showTasks) && (_jsxs(Box, { flexDirection: "column", marginLeft: 1, children: [message && (_jsxs(Box, { marginBottom: 1, gap: 1, children: [_jsx(Text, { color: getTextColor(isActive), children: message }), isExecuting && _jsx(Spinner, {})] })),
|
|
46
|
+
const showTasks = !isActive && tasks.length > 0;
|
|
47
|
+
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, {})] })), tasks.map((taskInfo, index) => {
|
|
48
|
+
// Merge workdir into active task's command
|
|
49
|
+
const taskCommand = isActive && index === completed && workdir
|
|
50
|
+
? { ...taskInfo.command, workdir }
|
|
51
|
+
: taskInfo.command;
|
|
52
|
+
return (_jsx(Box, { marginBottom: index < tasks.length - 1 ? 1 : 0, children: _jsx(Task, { label: taskInfo.label, command: taskCommand, isActive: isActive && index === completed, isFinished: index < completed, index: index, initialStatus: taskInfo.status, initialElapsed: taskInfo.elapsed, initialOutput: taskInfo.stdout || taskInfo.stderr || taskInfo.error
|
|
53
|
+
? {
|
|
54
|
+
stdout: taskInfo.stdout ?? '',
|
|
55
|
+
stderr: taskInfo.stderr ?? '',
|
|
56
|
+
error: taskInfo.error ?? '',
|
|
57
|
+
}
|
|
58
|
+
: undefined, onOutputChange: onOutputChange, onComplete: onTaskComplete, onAbort: onTaskAbort, onError: onTaskError }) }, index));
|
|
59
|
+
})] })), completionMessage && !isActive && (_jsx(Box, { marginTop: 1, marginLeft: 1, children: _jsx(Text, { color: getTextColor(false), children: completionMessage }) })), error && _jsx(Message, { text: error, status: status })] }));
|
|
33
60
|
};
|
|
34
61
|
/**
|
|
35
62
|
* Execute controller: Runs tasks sequentially
|
|
36
63
|
*/
|
|
37
|
-
export function Execute({ tasks, status, service, requestHandlers, lifecycleHandlers, workflowHandlers, }) {
|
|
64
|
+
export function Execute({ tasks: inputTasks, status, service, requestHandlers, lifecycleHandlers, workflowHandlers, }) {
|
|
38
65
|
const isActive = status === ComponentStatus.Active;
|
|
39
66
|
const [localState, dispatch] = useReducer(executeReducer, initialState);
|
|
40
|
-
|
|
67
|
+
// Ref to store current output for each task (avoids re-renders)
|
|
68
|
+
const taskOutputRef = useRef(new Map());
|
|
69
|
+
// Track working directory across commands (persists cd changes)
|
|
70
|
+
const workdirRef = useRef(undefined);
|
|
71
|
+
const { error, tasks, message, completed, hasProcessed, completionMessage, summary, } = localState;
|
|
41
72
|
// Derive loading state from current conditions
|
|
42
|
-
const isLoading = isActive &&
|
|
43
|
-
const isExecuting = completed <
|
|
73
|
+
const isLoading = isActive && tasks.length === 0 && !error && !hasProcessed;
|
|
74
|
+
const isExecuting = completed < tasks.length;
|
|
75
|
+
// Handle output changes from Task - store in ref (no re-render)
|
|
76
|
+
const handleOutputChange = useCallback((index, taskOutput) => {
|
|
77
|
+
taskOutputRef.current.set(index, taskOutput);
|
|
78
|
+
}, []);
|
|
44
79
|
// Handle cancel with useCallback to ensure we capture latest state
|
|
45
80
|
const handleCancel = useCallback(() => {
|
|
46
81
|
dispatch({
|
|
47
82
|
type: ExecuteActionType.CancelExecution,
|
|
48
83
|
payload: { completed },
|
|
49
84
|
});
|
|
50
|
-
// Get updated task infos after cancel
|
|
51
|
-
const updatedTaskInfos =
|
|
85
|
+
// Get updated task infos after cancel, merging output from ref
|
|
86
|
+
const updatedTaskInfos = tasks.map((task, taskIndex) => {
|
|
87
|
+
const output = taskOutputRef.current.get(taskIndex);
|
|
88
|
+
const baseTask = output
|
|
89
|
+
? {
|
|
90
|
+
...task,
|
|
91
|
+
stdout: output.stdout,
|
|
92
|
+
stderr: output.stderr,
|
|
93
|
+
error: output.error,
|
|
94
|
+
}
|
|
95
|
+
: task;
|
|
52
96
|
if (taskIndex < completed) {
|
|
53
|
-
return { ...
|
|
97
|
+
return { ...baseTask, status: ExecutionStatus.Success };
|
|
54
98
|
}
|
|
55
99
|
else if (taskIndex === completed) {
|
|
56
|
-
return { ...
|
|
100
|
+
return { ...baseTask, status: ExecutionStatus.Aborted };
|
|
57
101
|
}
|
|
58
102
|
else {
|
|
59
|
-
return { ...
|
|
103
|
+
return { ...baseTask, status: ExecutionStatus.Cancelled };
|
|
60
104
|
}
|
|
61
105
|
});
|
|
62
106
|
// Expose final state
|
|
63
|
-
const finalState = {
|
|
107
|
+
const finalState = createExecuteState({
|
|
64
108
|
message,
|
|
65
109
|
summary,
|
|
66
|
-
|
|
110
|
+
tasks: updatedTaskInfos,
|
|
67
111
|
completed,
|
|
68
|
-
|
|
69
|
-
completionMessage: null,
|
|
70
|
-
error: null,
|
|
71
|
-
};
|
|
112
|
+
});
|
|
72
113
|
requestHandlers.onCompleted(finalState);
|
|
73
114
|
requestHandlers.onAborted('execution');
|
|
74
|
-
}, [
|
|
75
|
-
message,
|
|
76
|
-
summary,
|
|
77
|
-
taskInfos,
|
|
78
|
-
completed,
|
|
79
|
-
taskExecutionTimes,
|
|
80
|
-
requestHandlers,
|
|
81
|
-
]);
|
|
115
|
+
}, [message, summary, tasks, completed, requestHandlers]);
|
|
82
116
|
useInput((_, key) => {
|
|
83
117
|
if (key.escape && (isLoading || isExecuting) && isActive) {
|
|
84
118
|
handleCancel();
|
|
@@ -86,14 +120,14 @@ export function Execute({ tasks, status, service, requestHandlers, lifecycleHand
|
|
|
86
120
|
}, { isActive: (isLoading || isExecuting) && isActive });
|
|
87
121
|
// Process tasks to get commands from AI
|
|
88
122
|
useEffect(() => {
|
|
89
|
-
if (!isActive ||
|
|
123
|
+
if (!isActive || tasks.length > 0 || hasProcessed) {
|
|
90
124
|
return;
|
|
91
125
|
}
|
|
92
126
|
let mounted = true;
|
|
93
127
|
async function process(svc) {
|
|
94
128
|
const startTime = Date.now();
|
|
95
129
|
try {
|
|
96
|
-
const result = await processTasks(
|
|
130
|
+
const result = await processTasks(inputTasks, svc);
|
|
97
131
|
await ensureMinimumTime(startTime, MINIMUM_PROCESSING_TIME);
|
|
98
132
|
if (!mounted)
|
|
99
133
|
return;
|
|
@@ -108,16 +142,7 @@ export function Execute({ tasks, status, service, requestHandlers, lifecycleHand
|
|
|
108
142
|
const errorMessage = getExecutionErrorMessage(result.error);
|
|
109
143
|
workflowHandlers.addToTimeline(markAsDone(createMessage(errorMessage)));
|
|
110
144
|
// Complete without error in state (message already in timeline)
|
|
111
|
-
|
|
112
|
-
message: result.message,
|
|
113
|
-
summary: '',
|
|
114
|
-
taskInfos: [],
|
|
115
|
-
completed: 0,
|
|
116
|
-
taskExecutionTimes: [],
|
|
117
|
-
completionMessage: null,
|
|
118
|
-
error: null,
|
|
119
|
-
};
|
|
120
|
-
requestHandlers.onCompleted(finalState);
|
|
145
|
+
requestHandlers.onCompleted(createExecuteState({ message: result.message }));
|
|
121
146
|
lifecycleHandlers.completeActive();
|
|
122
147
|
return;
|
|
123
148
|
}
|
|
@@ -126,43 +151,31 @@ export function Execute({ tasks, status, service, requestHandlers, lifecycleHand
|
|
|
126
151
|
type: ExecuteActionType.ProcessingComplete,
|
|
127
152
|
payload: { message: result.message },
|
|
128
153
|
});
|
|
129
|
-
|
|
130
|
-
message: result.message,
|
|
131
|
-
summary: '',
|
|
132
|
-
taskInfos: [],
|
|
133
|
-
completed: 0,
|
|
134
|
-
taskExecutionTimes: [],
|
|
135
|
-
completionMessage: null,
|
|
136
|
-
error: null,
|
|
137
|
-
};
|
|
138
|
-
requestHandlers.onCompleted(finalState);
|
|
154
|
+
requestHandlers.onCompleted(createExecuteState({ message: result.message }));
|
|
139
155
|
lifecycleHandlers.completeActive();
|
|
140
156
|
return;
|
|
141
157
|
}
|
|
142
158
|
// Create task infos from commands
|
|
143
|
-
const
|
|
144
|
-
label:
|
|
159
|
+
const tasks = result.commands.map((cmd, index) => ({
|
|
160
|
+
label: inputTasks[index]?.action ?? cmd.description,
|
|
145
161
|
command: cmd,
|
|
162
|
+
status: ExecutionStatus.Pending,
|
|
163
|
+
elapsed: 0,
|
|
146
164
|
}));
|
|
147
165
|
dispatch({
|
|
148
166
|
type: ExecuteActionType.CommandsReady,
|
|
149
167
|
payload: {
|
|
150
168
|
message: result.message,
|
|
151
169
|
summary: result.summary,
|
|
152
|
-
|
|
170
|
+
tasks,
|
|
153
171
|
},
|
|
154
172
|
});
|
|
155
173
|
// Update state after AI processing
|
|
156
|
-
|
|
174
|
+
requestHandlers.onCompleted(createExecuteState({
|
|
157
175
|
message: result.message,
|
|
158
176
|
summary: result.summary,
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
taskExecutionTimes: [],
|
|
162
|
-
completionMessage: null,
|
|
163
|
-
error: null,
|
|
164
|
-
};
|
|
165
|
-
requestHandlers.onCompleted(finalState);
|
|
177
|
+
tasks,
|
|
178
|
+
}));
|
|
166
179
|
}
|
|
167
180
|
catch (err) {
|
|
168
181
|
await ensureMinimumTime(startTime, MINIMUM_PROCESSING_TIME);
|
|
@@ -172,16 +185,7 @@ export function Execute({ tasks, status, service, requestHandlers, lifecycleHand
|
|
|
172
185
|
type: ExecuteActionType.ProcessingError,
|
|
173
186
|
payload: { error: errorMessage },
|
|
174
187
|
});
|
|
175
|
-
|
|
176
|
-
message: '',
|
|
177
|
-
summary: '',
|
|
178
|
-
taskInfos: [],
|
|
179
|
-
completed: 0,
|
|
180
|
-
taskExecutionTimes: [],
|
|
181
|
-
completionMessage: null,
|
|
182
|
-
error: errorMessage,
|
|
183
|
-
};
|
|
184
|
-
requestHandlers.onCompleted(finalState);
|
|
188
|
+
requestHandlers.onCompleted(createExecuteState({ error: errorMessage }));
|
|
185
189
|
requestHandlers.onError(errorMessage);
|
|
186
190
|
}
|
|
187
191
|
}
|
|
@@ -191,81 +195,100 @@ export function Execute({ tasks, status, service, requestHandlers, lifecycleHand
|
|
|
191
195
|
mounted = false;
|
|
192
196
|
};
|
|
193
197
|
}, [
|
|
194
|
-
|
|
198
|
+
inputTasks,
|
|
195
199
|
isActive,
|
|
196
200
|
service,
|
|
197
201
|
requestHandlers,
|
|
198
202
|
lifecycleHandlers,
|
|
199
203
|
workflowHandlers,
|
|
200
|
-
|
|
204
|
+
tasks.length,
|
|
201
205
|
hasProcessed,
|
|
202
206
|
]);
|
|
203
207
|
// Handle task completion - move to next task
|
|
204
|
-
const handleTaskComplete = useCallback((index,
|
|
208
|
+
const handleTaskComplete = useCallback((index, elapsed, taskOutput) => {
|
|
209
|
+
// Track working directory for subsequent commands
|
|
210
|
+
if (taskOutput.workdir) {
|
|
211
|
+
workdirRef.current = taskOutput.workdir;
|
|
212
|
+
}
|
|
213
|
+
// Update tasks with output before calling handler
|
|
214
|
+
const tasksWithOutput = tasks.map((task, i) => i === index
|
|
215
|
+
? {
|
|
216
|
+
...task,
|
|
217
|
+
stdout: taskOutput.stdout,
|
|
218
|
+
stderr: taskOutput.stderr,
|
|
219
|
+
error: taskOutput.error,
|
|
220
|
+
}
|
|
221
|
+
: task);
|
|
205
222
|
const result = handleTaskCompletion(index, elapsed, {
|
|
206
|
-
|
|
223
|
+
tasks: tasksWithOutput,
|
|
207
224
|
message,
|
|
208
225
|
summary,
|
|
209
|
-
taskExecutionTimes,
|
|
210
226
|
});
|
|
211
227
|
dispatch(result.action);
|
|
212
228
|
requestHandlers.onCompleted(result.finalState);
|
|
213
229
|
if (result.shouldComplete) {
|
|
214
230
|
lifecycleHandlers.completeActive();
|
|
215
231
|
}
|
|
216
|
-
}, [
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
232
|
+
}, [tasks, message, summary, requestHandlers, lifecycleHandlers]);
|
|
233
|
+
const handleTaskError = useCallback((index, error, elapsed, taskOutput) => {
|
|
234
|
+
// Track working directory for subsequent commands (even on error)
|
|
235
|
+
if (taskOutput.workdir) {
|
|
236
|
+
workdirRef.current = taskOutput.workdir;
|
|
237
|
+
}
|
|
238
|
+
// Update tasks with output before calling handler
|
|
239
|
+
const tasksWithOutput = tasks.map((task, i) => i === index
|
|
240
|
+
? {
|
|
241
|
+
...task,
|
|
242
|
+
stdout: taskOutput.stdout,
|
|
243
|
+
stderr: taskOutput.stderr,
|
|
244
|
+
error: taskOutput.error,
|
|
245
|
+
}
|
|
246
|
+
: task);
|
|
225
247
|
const result = handleTaskFailure(index, error, elapsed, {
|
|
226
|
-
|
|
248
|
+
tasks: tasksWithOutput,
|
|
227
249
|
message,
|
|
228
250
|
summary,
|
|
229
|
-
taskExecutionTimes,
|
|
230
251
|
});
|
|
231
252
|
dispatch(result.action);
|
|
232
253
|
requestHandlers.onCompleted(result.finalState);
|
|
233
|
-
|
|
234
|
-
|
|
254
|
+
// Add error feedback to queue for critical failures
|
|
255
|
+
if (result.action.type === ExecuteActionType.TaskErrorCritical) {
|
|
256
|
+
const errorMessage = getExecutionErrorMessage(error);
|
|
257
|
+
workflowHandlers.addToQueue(createFeedback(FeedbackType.Failed, errorMessage));
|
|
235
258
|
}
|
|
236
259
|
if (result.shouldComplete) {
|
|
237
260
|
lifecycleHandlers.completeActive();
|
|
238
261
|
}
|
|
239
262
|
}, [
|
|
240
|
-
|
|
263
|
+
tasks,
|
|
241
264
|
message,
|
|
242
265
|
summary,
|
|
243
|
-
taskExecutionTimes,
|
|
244
266
|
requestHandlers,
|
|
245
267
|
lifecycleHandlers,
|
|
268
|
+
workflowHandlers,
|
|
246
269
|
]);
|
|
247
|
-
const handleTaskAbort = useCallback((
|
|
270
|
+
const handleTaskAbort = useCallback((index, taskOutput) => {
|
|
248
271
|
// Task was aborted - execution already stopped by Escape handler
|
|
249
|
-
//
|
|
250
|
-
const
|
|
272
|
+
// Update tasks with output before building state
|
|
273
|
+
const tasksWithOutput = tasks.map((task, i) => i === index
|
|
274
|
+
? {
|
|
275
|
+
...task,
|
|
276
|
+
stdout: taskOutput.stdout,
|
|
277
|
+
stderr: taskOutput.stderr,
|
|
278
|
+
error: taskOutput.error,
|
|
279
|
+
}
|
|
280
|
+
: task);
|
|
281
|
+
const finalState = buildAbortedState(tasksWithOutput, message, summary, completed);
|
|
251
282
|
requestHandlers.onCompleted(finalState);
|
|
252
|
-
}, [
|
|
253
|
-
taskInfos,
|
|
254
|
-
message,
|
|
255
|
-
summary,
|
|
256
|
-
completed,
|
|
257
|
-
taskExecutionTimes,
|
|
258
|
-
requestHandlers,
|
|
259
|
-
]);
|
|
283
|
+
}, [tasks, message, summary, completed, requestHandlers]);
|
|
260
284
|
// Controller always renders View with current state
|
|
261
|
-
const viewState = {
|
|
285
|
+
const viewState = createExecuteState({
|
|
262
286
|
error,
|
|
263
|
-
|
|
287
|
+
tasks,
|
|
264
288
|
message,
|
|
265
289
|
summary,
|
|
266
290
|
completed,
|
|
267
|
-
taskExecutionTimes,
|
|
268
291
|
completionMessage,
|
|
269
|
-
};
|
|
270
|
-
return (_jsx(ExecuteView, {
|
|
292
|
+
});
|
|
293
|
+
return (_jsx(ExecuteView, { state: viewState, status: status, workdir: workdirRef.current, onOutputChange: handleOutputChange, onTaskComplete: handleTaskComplete, onTaskAbort: handleTaskAbort, onTaskError: handleTaskError }));
|
|
271
294
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { Palette } from '../services/colors.js';
|
|
4
|
+
import { ExecutionStatus } from '../services/shell.js';
|
|
5
|
+
const MAX_LINES = 8;
|
|
6
|
+
const MAX_WIDTH = 75;
|
|
7
|
+
const SHORT_OUTPUT_THRESHOLD = 4;
|
|
8
|
+
const MINIMAL_INFO_THRESHOLD = 2;
|
|
9
|
+
/**
|
|
10
|
+
* Get the last N lines from text, filtering out empty/whitespace-only lines
|
|
11
|
+
*/
|
|
12
|
+
export function getLastLines(text, maxLines = MAX_LINES) {
|
|
13
|
+
const lines = text
|
|
14
|
+
.trim()
|
|
15
|
+
.split(/\r?\n/)
|
|
16
|
+
.filter((line) => line.trim().length > 0);
|
|
17
|
+
return lines.length <= maxLines ? lines : lines.slice(-maxLines);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Compute display configuration for output rendering.
|
|
21
|
+
* Encapsulates the logic for what to show and how to style it.
|
|
22
|
+
*/
|
|
23
|
+
export function computeDisplayConfig(stdout, stderr, status, isFinished) {
|
|
24
|
+
const hasStdout = stdout.trim().length > 0;
|
|
25
|
+
const hasStderr = stderr.trim().length > 0;
|
|
26
|
+
if (!hasStdout && !hasStderr)
|
|
27
|
+
return null;
|
|
28
|
+
const stdoutLines = hasStdout ? getLastLines(stdout) : [];
|
|
29
|
+
const stderrLines = hasStderr ? getLastLines(stderr) : [];
|
|
30
|
+
// Show stdout if no stderr, or if stderr is minimal (provides context)
|
|
31
|
+
const showStdout = hasStdout && (!hasStderr || stderrLines.length <= MINIMAL_INFO_THRESHOLD);
|
|
32
|
+
// Use word wrapping for short outputs to show more detail
|
|
33
|
+
const totalLines = stdoutLines.length + stderrLines.length;
|
|
34
|
+
const wrapMode = totalLines <= SHORT_OUTPUT_THRESHOLD ? 'wrap' : 'truncate-end';
|
|
35
|
+
// Darker colors for finished tasks
|
|
36
|
+
const baseColor = isFinished ? Palette.DarkGray : Palette.Gray;
|
|
37
|
+
const stderrColor = status === ExecutionStatus.Failed ? Palette.Yellow : baseColor;
|
|
38
|
+
return {
|
|
39
|
+
stdoutLines,
|
|
40
|
+
stderrLines,
|
|
41
|
+
showStdout,
|
|
42
|
+
wrapMode,
|
|
43
|
+
stdoutColor: baseColor,
|
|
44
|
+
stderrColor,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export function Output({ stdout, stderr, status, isFinished }) {
|
|
48
|
+
const config = computeDisplayConfig(stdout, stderr, status, isFinished ?? false);
|
|
49
|
+
if (!config)
|
|
50
|
+
return null;
|
|
51
|
+
const { stdoutLines, stderrLines, showStdout, wrapMode, stdoutColor, stderrColor, } = config;
|
|
52
|
+
return (_jsxs(Box, { marginTop: 1, marginLeft: 5, flexDirection: "column", width: MAX_WIDTH, children: [showStdout &&
|
|
53
|
+
stdoutLines.map((line, index) => (_jsx(Text, { color: stdoutColor, wrap: wrapMode, children: line }, `out-${index}`))), stderrLines.map((line, index) => (_jsx(Text, { color: stderrColor, wrap: wrapMode, children: line }, `err-${index}`)))] }));
|
|
54
|
+
}
|