prompt-language-shell 0.8.6 → 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.
@@ -1,83 +1,118 @@
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';
6
6
  import { useInput } from '../services/keyboard.js';
7
- import { formatErrorMessage } from '../services/messages.js';
7
+ import { formatErrorMessage, getExecutionErrorMessage, } from '../services/messages.js';
8
8
  import { ExecutionStatus } from '../services/shell.js';
9
9
  import { ensureMinimumTime } from '../services/timing.js';
10
10
  import { buildAbortedState, handleTaskCompletion, handleTaskFailure, } from '../execution/handlers.js';
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 { createFeedback, createMessage, markAsDone, } from '../services/components.js';
15
+ import { FeedbackType } from '../types/types.js';
14
16
  import { Message } from './Message.js';
15
17
  import { Spinner } from './Spinner.js';
16
18
  import { Task } from './Task.js';
17
19
  const MINIMUM_PROCESSING_TIME = 400;
18
- export const ExecuteView = ({ state, status, onTaskComplete, onTaskAbort, onTaskError, }) => {
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, }) => {
19
35
  const isActive = status === ComponentStatus.Active;
20
- const { error, taskInfos, message, completed, completionMessage } = state;
21
- const hasProcessed = taskInfos.length > 0;
36
+ const { error, tasks, message, completed, completionMessage } = state;
37
+ const hasProcessed = tasks.length > 0;
22
38
  // Derive loading state from current conditions
23
- const isLoading = isActive && taskInfos.length === 0 && !error && !hasProcessed;
24
- const isExecuting = completed < taskInfos.length;
39
+ const isLoading = isActive && tasks.length === 0 && !error && !hasProcessed;
40
+ const isExecuting = completed < tasks.length;
25
41
  // Return null only when loading completes with no commands
26
- if (!isActive && taskInfos.length === 0 && !error) {
42
+ if (!isActive && tasks.length === 0 && !error) {
27
43
  return null;
28
44
  }
29
45
  // 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 })] }));
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 })] }));
32
60
  };
33
61
  /**
34
62
  * Execute controller: Runs tasks sequentially
35
63
  */
36
- export function Execute({ tasks, status, service, requestHandlers, lifecycleHandlers, workflowHandlers, }) {
64
+ export function Execute({ tasks: inputTasks, status, service, requestHandlers, lifecycleHandlers, workflowHandlers, }) {
37
65
  const isActive = status === ComponentStatus.Active;
38
66
  const [localState, dispatch] = useReducer(executeReducer, initialState);
39
- const { error, taskInfos, message, completed, hasProcessed, taskExecutionTimes, completionMessage, summary, } = localState;
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;
40
72
  // Derive loading state from current conditions
41
- const isLoading = isActive && taskInfos.length === 0 && !error && !hasProcessed;
42
- const isExecuting = completed < taskInfos.length;
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
+ }, []);
43
79
  // Handle cancel with useCallback to ensure we capture latest state
44
80
  const handleCancel = useCallback(() => {
45
81
  dispatch({
46
82
  type: ExecuteActionType.CancelExecution,
47
83
  payload: { completed },
48
84
  });
49
- // Get updated task infos after cancel
50
- const updatedTaskInfos = taskInfos.map((task, taskIndex) => {
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;
51
96
  if (taskIndex < completed) {
52
- return { ...task, status: ExecutionStatus.Success };
97
+ return { ...baseTask, status: ExecutionStatus.Success };
53
98
  }
54
99
  else if (taskIndex === completed) {
55
- return { ...task, status: ExecutionStatus.Aborted };
100
+ return { ...baseTask, status: ExecutionStatus.Aborted };
56
101
  }
57
102
  else {
58
- return { ...task, status: ExecutionStatus.Cancelled };
103
+ return { ...baseTask, status: ExecutionStatus.Cancelled };
59
104
  }
60
105
  });
61
106
  // Expose final state
62
- const finalState = {
107
+ const finalState = createExecuteState({
63
108
  message,
64
109
  summary,
65
- taskInfos: updatedTaskInfos,
110
+ tasks: updatedTaskInfos,
66
111
  completed,
67
- taskExecutionTimes,
68
- completionMessage: null,
69
- error: null,
70
- };
112
+ });
71
113
  requestHandlers.onCompleted(finalState);
72
114
  requestHandlers.onAborted('execution');
73
- }, [
74
- message,
75
- summary,
76
- taskInfos,
77
- completed,
78
- taskExecutionTimes,
79
- requestHandlers,
80
- ]);
115
+ }, [message, summary, tasks, completed, requestHandlers]);
81
116
  useInput((_, key) => {
82
117
  if (key.escape && (isLoading || isExecuting) && isActive) {
83
118
  handleCancel();
@@ -85,14 +120,14 @@ export function Execute({ tasks, status, service, requestHandlers, lifecycleHand
85
120
  }, { isActive: (isLoading || isExecuting) && isActive });
86
121
  // Process tasks to get commands from AI
87
122
  useEffect(() => {
88
- if (!isActive || taskInfos.length > 0 || hasProcessed) {
123
+ if (!isActive || tasks.length > 0 || hasProcessed) {
89
124
  return;
90
125
  }
91
126
  let mounted = true;
92
127
  async function process(svc) {
93
128
  const startTime = Date.now();
94
129
  try {
95
- const result = await processTasks(tasks, svc);
130
+ const result = await processTasks(inputTasks, svc);
96
131
  await ensureMinimumTime(startTime, MINIMUM_PROCESSING_TIME);
97
132
  if (!mounted)
98
133
  return;
@@ -101,47 +136,46 @@ export function Execute({ tasks, status, service, requestHandlers, lifecycleHand
101
136
  workflowHandlers.addToTimeline(...result.debug);
102
137
  }
103
138
  if (result.commands.length === 0) {
139
+ // Check if this is an error response (has error field)
140
+ if (result.error) {
141
+ // Add error message to timeline
142
+ const errorMessage = getExecutionErrorMessage(result.error);
143
+ workflowHandlers.addToTimeline(markAsDone(createMessage(errorMessage)));
144
+ // Complete without error in state (message already in timeline)
145
+ requestHandlers.onCompleted(createExecuteState({ message: result.message }));
146
+ lifecycleHandlers.completeActive();
147
+ return;
148
+ }
149
+ // No commands and no error - just complete
104
150
  dispatch({
105
151
  type: ExecuteActionType.ProcessingComplete,
106
152
  payload: { message: result.message },
107
153
  });
108
- const finalState = {
109
- message: result.message,
110
- summary: '',
111
- taskInfos: [],
112
- completed: 0,
113
- taskExecutionTimes: [],
114
- completionMessage: null,
115
- error: null,
116
- };
117
- requestHandlers.onCompleted(finalState);
154
+ requestHandlers.onCompleted(createExecuteState({ message: result.message }));
118
155
  lifecycleHandlers.completeActive();
119
156
  return;
120
157
  }
121
158
  // Create task infos from commands
122
- const infos = result.commands.map((cmd, index) => ({
123
- label: tasks[index]?.action,
159
+ const tasks = result.commands.map((cmd, index) => ({
160
+ label: inputTasks[index]?.action ?? cmd.description,
124
161
  command: cmd,
162
+ status: ExecutionStatus.Pending,
163
+ elapsed: 0,
125
164
  }));
126
165
  dispatch({
127
166
  type: ExecuteActionType.CommandsReady,
128
167
  payload: {
129
168
  message: result.message,
130
169
  summary: result.summary,
131
- taskInfos: infos,
170
+ tasks,
132
171
  },
133
172
  });
134
173
  // Update state after AI processing
135
- const finalState = {
174
+ requestHandlers.onCompleted(createExecuteState({
136
175
  message: result.message,
137
176
  summary: result.summary,
138
- taskInfos: infos,
139
- completed: 0,
140
- taskExecutionTimes: [],
141
- completionMessage: null,
142
- error: null,
143
- };
144
- requestHandlers.onCompleted(finalState);
177
+ tasks,
178
+ }));
145
179
  }
146
180
  catch (err) {
147
181
  await ensureMinimumTime(startTime, MINIMUM_PROCESSING_TIME);
@@ -151,16 +185,7 @@ export function Execute({ tasks, status, service, requestHandlers, lifecycleHand
151
185
  type: ExecuteActionType.ProcessingError,
152
186
  payload: { error: errorMessage },
153
187
  });
154
- const finalState = {
155
- message: '',
156
- summary: '',
157
- taskInfos: [],
158
- completed: 0,
159
- taskExecutionTimes: [],
160
- completionMessage: null,
161
- error: errorMessage,
162
- };
163
- requestHandlers.onCompleted(finalState);
188
+ requestHandlers.onCompleted(createExecuteState({ error: errorMessage }));
164
189
  requestHandlers.onError(errorMessage);
165
190
  }
166
191
  }
@@ -170,81 +195,100 @@ export function Execute({ tasks, status, service, requestHandlers, lifecycleHand
170
195
  mounted = false;
171
196
  };
172
197
  }, [
173
- tasks,
198
+ inputTasks,
174
199
  isActive,
175
200
  service,
176
201
  requestHandlers,
177
202
  lifecycleHandlers,
178
203
  workflowHandlers,
179
- taskInfos.length,
204
+ tasks.length,
180
205
  hasProcessed,
181
206
  ]);
182
207
  // Handle task completion - move to next task
183
- const handleTaskComplete = useCallback((index, _output, elapsed) => {
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);
184
222
  const result = handleTaskCompletion(index, elapsed, {
185
- taskInfos,
223
+ tasks: tasksWithOutput,
186
224
  message,
187
225
  summary,
188
- taskExecutionTimes,
189
226
  });
190
227
  dispatch(result.action);
191
228
  requestHandlers.onCompleted(result.finalState);
192
229
  if (result.shouldComplete) {
193
230
  lifecycleHandlers.completeActive();
194
231
  }
195
- }, [
196
- taskInfos,
197
- message,
198
- summary,
199
- taskExecutionTimes,
200
- requestHandlers,
201
- lifecycleHandlers,
202
- ]);
203
- const handleTaskError = useCallback((index, error, elapsed) => {
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);
204
247
  const result = handleTaskFailure(index, error, elapsed, {
205
- taskInfos,
248
+ tasks: tasksWithOutput,
206
249
  message,
207
250
  summary,
208
- taskExecutionTimes,
209
251
  });
210
252
  dispatch(result.action);
211
253
  requestHandlers.onCompleted(result.finalState);
212
- if (result.shouldReportError) {
213
- requestHandlers.onError(error);
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));
214
258
  }
215
259
  if (result.shouldComplete) {
216
260
  lifecycleHandlers.completeActive();
217
261
  }
218
262
  }, [
219
- taskInfos,
263
+ tasks,
220
264
  message,
221
265
  summary,
222
- taskExecutionTimes,
223
266
  requestHandlers,
224
267
  lifecycleHandlers,
268
+ workflowHandlers,
225
269
  ]);
226
- const handleTaskAbort = useCallback((_index) => {
270
+ const handleTaskAbort = useCallback((index, taskOutput) => {
227
271
  // Task was aborted - execution already stopped by Escape handler
228
- // Just update state, don't call onAborted (already called at Execute level)
229
- const finalState = buildAbortedState(taskInfos, message, summary, completed, taskExecutionTimes);
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);
230
282
  requestHandlers.onCompleted(finalState);
231
- }, [
232
- taskInfos,
233
- message,
234
- summary,
235
- completed,
236
- taskExecutionTimes,
237
- requestHandlers,
238
- ]);
283
+ }, [tasks, message, summary, completed, requestHandlers]);
239
284
  // Controller always renders View with current state
240
- const viewState = {
285
+ const viewState = createExecuteState({
241
286
  error,
242
- taskInfos,
287
+ tasks,
243
288
  message,
244
289
  summary,
245
290
  completed,
246
- taskExecutionTimes,
247
291
  completionMessage,
248
- };
249
- return (_jsx(ExecuteView, { tasks: tasks, state: viewState, status: status, onTaskComplete: handleTaskComplete, onTaskAbort: handleTaskAbort, onTaskError: handleTaskError }));
292
+ });
293
+ return (_jsx(ExecuteView, { state: viewState, status: status, workdir: workdirRef.current, onOutputChange: handleOutputChange, onTaskComplete: handleTaskComplete, onTaskAbort: handleTaskAbort, onTaskError: handleTaskError }));
250
294
  }
@@ -1,5 +1,6 @@
1
1
  import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Box, Text } from 'ink';
3
+ import { ComponentStatus } from '../types/components.js';
3
4
  import { FeedbackType } from '../types/types.js';
4
5
  import { getFeedbackColor } from '../services/colors.js';
5
6
  function getSymbol(type) {
@@ -12,7 +13,7 @@ function getSymbol(type) {
12
13
  }[type];
13
14
  }
14
15
  export function Feedback({ type, message }) {
15
- const color = getFeedbackColor(type, false);
16
+ const color = getFeedbackColor(type, ComponentStatus.Done);
16
17
  const symbol = getSymbol(type);
17
18
  return (_jsx(Box, { marginLeft: 1, children: _jsxs(Text, { color: color, children: [symbol, " ", message] }) }));
18
19
  }
package/dist/ui/Label.js CHANGED
@@ -3,7 +3,8 @@ import { Box, Text } from 'ink';
3
3
  import { getTaskColors, getTaskTypeLabel } from '../services/colors.js';
4
4
  import { DebugLevel } from '../configuration/types.js';
5
5
  import { Separator } from './Separator.js';
6
- export function Label({ description, taskType, showType = false, isCurrent = false, debug = DebugLevel.None, }) {
7
- const colors = getTaskColors(taskType, isCurrent);
6
+ import { ComponentStatus } from '../types/components.js';
7
+ export function Label({ description, taskType, showType = false, status = ComponentStatus.Done, debug = DebugLevel.None, }) {
8
+ const colors = getTaskColors(taskType, status);
8
9
  return (_jsxs(Box, { children: [_jsx(Text, { color: colors.description, children: description }), showType && (_jsxs(_Fragment, { children: [_jsx(Separator, {}), _jsx(Text, { color: colors.type, children: getTaskTypeLabel(taskType, debug) })] }))] }));
9
10
  }
package/dist/ui/List.js CHANGED
@@ -4,7 +4,8 @@ import { Palette } from '../services/colors.js';
4
4
  import { Separator } from './Separator.js';
5
5
  export const List = ({ items, level = 0, highlightedIndex = null, highlightedParentIndex = null, showType = false, }) => {
6
6
  const marginLeft = level > 0 ? 2 : 0;
7
- return (_jsx(Box, { flexDirection: "column", marginLeft: marginLeft, children: items.map((item, index) => {
7
+ const gap = level === 0 ? 1 : 0;
8
+ return (_jsx(Box, { flexDirection: "column", marginLeft: marginLeft, gap: gap, children: items.map((item, index) => {
8
9
  // At level 0, track which parent is active for child highlighting
9
10
  // At level > 0, only highlight if this parent is the active one
10
11
  const shouldHighlightChildren = level === 0 ? highlightedParentIndex === index : false;
@@ -23,6 +24,6 @@ export const List = ({ items, level = 0, highlightedIndex = null, highlightedPar
23
24
  (isHighlighted && item.type.highlightedColor
24
25
  ? item.type.highlightedColor
25
26
  : Palette.White);
26
- return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: markerColor, children: marker }), _jsx(Text, { color: descriptionColor, children: item.description.text }), showType && (_jsxs(_Fragment, { children: [_jsx(Separator, {}), _jsx(Text, { color: typeColor, children: item.type.text })] }))] }), item.children && item.children.length > 0 && (_jsx(List, { items: item.children, level: level + 1, highlightedIndex: shouldHighlightChildren ? highlightedIndex : null, showType: showType }))] }, index));
27
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: markerColor, children: marker }), _jsx(Text, { color: descriptionColor, children: item.description.text }), showType && (_jsxs(_Fragment, { children: [_jsx(Separator, {}), _jsx(Text, { color: typeColor, children: item.type.text })] }))] }), item.children && item.children.length > 0 && (_jsx(Box, { marginTop: 1, children: _jsx(List, { items: item.children, level: level + 1, highlightedIndex: shouldHighlightChildren ? highlightedIndex : null, showType: showType }) }))] }, index));
27
28
  }) }));
28
29
  };
@@ -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
+ }
@@ -3,17 +3,22 @@ import { useEffect, useState } from 'react';
3
3
  import { Box } from 'ink';
4
4
  import { ComponentStatus, } from '../types/components.js';
5
5
  import { TaskType } from '../types/types.js';
6
- import { getTaskColors, getTaskTypeLabel } from '../services/colors.js';
6
+ import { getTaskColors, getTaskTypeLabel, Palette, } from '../services/colors.js';
7
7
  import { DebugLevel } from '../configuration/types.js';
8
8
  import { useInput } from '../services/keyboard.js';
9
9
  import { Label } from './Label.js';
10
10
  import { List } from './List.js';
11
- function taskToListItem(task, highlightedChildIndex = null, isDefineTaskWithoutSelection = false, isCurrent = false, debug = DebugLevel.None) {
12
- const taskColors = getTaskColors(task.type, isCurrent);
11
+ export function taskToListItem(task, highlightedChildIndex = null, isDefineTaskWithoutSelection = false, status = ComponentStatus.Done, debug = DebugLevel.None) {
12
+ const taskColors = getTaskColors(task.type, status);
13
+ // Determine description color based on status
14
+ let descriptionColor = taskColors.description;
15
+ if (status === ComponentStatus.Pending) {
16
+ descriptionColor = Palette.SoftWhite;
17
+ }
13
18
  const item = {
14
19
  description: {
15
20
  text: task.action,
16
- color: taskColors.description,
21
+ color: descriptionColor,
17
22
  },
18
23
  type: { text: getTaskTypeLabel(task.type, debug), color: taskColors.type },
19
24
  children: [],
@@ -21,7 +26,7 @@ function taskToListItem(task, highlightedChildIndex = null, isDefineTaskWithoutS
21
26
  // Mark define tasks with right arrow when no selection has been made
22
27
  if (isDefineTaskWithoutSelection) {
23
28
  item.marker = ' → ';
24
- item.markerColor = getTaskColors(TaskType.Schedule, isCurrent).type;
29
+ item.markerColor = getTaskColors(TaskType.Schedule, status).type;
25
30
  }
26
31
  // Add children for Define tasks with options
27
32
  if (task.type === TaskType.Define && Array.isArray(task.params?.options)) {
@@ -33,8 +38,8 @@ function taskToListItem(task, highlightedChildIndex = null, isDefineTaskWithoutS
33
38
  childType =
34
39
  index === highlightedChildIndex ? TaskType.Execute : TaskType.Discard;
35
40
  }
36
- const colors = getTaskColors(childType, isCurrent);
37
- const planColors = getTaskColors(TaskType.Schedule, isCurrent);
41
+ const colors = getTaskColors(childType, status);
42
+ const planColors = getTaskColors(TaskType.Schedule, status);
38
43
  return {
39
44
  description: {
40
45
  text: option,
@@ -56,11 +61,11 @@ function taskToListItem(task, highlightedChildIndex = null, isDefineTaskWithoutS
56
61
  Array.isArray(scheduledTask.subtasks) &&
57
62
  scheduledTask.subtasks.length > 0) {
58
63
  item.children = scheduledTask.subtasks.map((subtask) => {
59
- const subtaskColors = getTaskColors(subtask.type, isCurrent);
64
+ const subtaskColors = getTaskColors(subtask.type, status);
60
65
  return {
61
66
  description: {
62
67
  text: subtask.action,
63
- color: subtaskColors.description,
68
+ color: Palette.AshGray,
64
69
  },
65
70
  type: {
66
71
  text: getTaskTypeLabel(subtask.type, debug),
@@ -107,9 +112,9 @@ export const ScheduleView = ({ message, tasks, state, status, debug = DebugLevel
107
112
  defineGroupIndex === currentDefineGroupIndex &&
108
113
  highlightedIndex === null &&
109
114
  isActive;
110
- return taskToListItem(task, childIndex, isDefineWithoutSelection, isActive, debug);
115
+ return taskToListItem(task, childIndex, isDefineWithoutSelection, status, debug);
111
116
  });
112
- return (_jsxs(Box, { flexDirection: "column", children: [message && (_jsx(Box, { marginBottom: 1, marginLeft: 1, children: _jsx(Label, { description: message, taskType: TaskType.Schedule, showType: debug !== DebugLevel.None, isCurrent: isActive, debug: debug }) })), _jsx(Box, { marginLeft: 1, children: _jsx(List, { items: listItems, highlightedIndex: currentDefineTaskIndex >= 0 ? highlightedIndex : null, highlightedParentIndex: currentDefineTaskIndex, showType: debug !== DebugLevel.None }) })] }));
117
+ return (_jsxs(Box, { flexDirection: "column", children: [message && (_jsx(Box, { marginBottom: 1, marginLeft: 1, children: _jsx(Label, { description: message, taskType: TaskType.Schedule, showType: debug !== DebugLevel.None, status: status, debug: debug }) })), _jsx(Box, { marginLeft: 1, children: _jsx(List, { items: listItems, highlightedIndex: currentDefineTaskIndex >= 0 ? highlightedIndex : null, highlightedParentIndex: currentDefineTaskIndex, showType: debug !== DebugLevel.None }) })] }));
113
118
  };
114
119
  /**
115
120
  * Schedule controller: Manages task selection and navigation