prompt-language-shell 0.7.6 → 0.8.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,168 +1,72 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useState } from 'react';
2
+ import { useCallback, useEffect, useState } from 'react';
3
3
  import { Box, Text } from 'ink';
4
- import { ComponentStatus } from '../types/components.js';
5
- import { Colors, getTextColor, Palette } from '../services/colors.js';
4
+ import { ComponentStatus, } from '../types/components.js';
5
+ import { Colors, getTextColor } from '../services/colors.js';
6
+ import { addDebugToTimeline } from '../services/components.js';
6
7
  import { useInput } from '../services/keyboard.js';
8
+ import { loadUserConfig } from '../services/loader.js';
7
9
  import { formatErrorMessage } from '../services/messages.js';
8
- import { formatDuration } from '../services/utils.js';
9
- import { ExecutionStatus, executeCommands, } from '../services/shell.js';
10
10
  import { replacePlaceholders } from '../services/resolver.js';
11
- import { loadUserConfig } from '../services/loader.js';
11
+ import { ExecutionStatus } from '../services/shell.js';
12
12
  import { ensureMinimumTime } from '../services/timing.js';
13
+ import { formatDuration } from '../services/utils.js';
13
14
  import { Spinner } from './Spinner.js';
15
+ import { Task } from './Task.js';
14
16
  const MINIMUM_PROCESSING_TIME = 400;
15
- const STATUS_ICONS = {
16
- [ExecutionStatus.Pending]: '- ',
17
- [ExecutionStatus.Running]: '• ',
18
- [ExecutionStatus.Success]: '✓ ',
19
- [ExecutionStatus.Failed]: '✗ ',
20
- [ExecutionStatus.Aborted]: '⊘ ',
21
- };
22
- function calculateTotalElapsed(commandStatuses) {
23
- return commandStatuses.reduce((sum, cmd) => {
24
- if (cmd.elapsed !== undefined) {
25
- return sum + cmd.elapsed;
26
- }
27
- if (cmd.startTime) {
28
- const elapsed = cmd.endTime
29
- ? cmd.endTime - cmd.startTime
30
- : Date.now() - cmd.startTime;
31
- return sum + elapsed;
32
- }
33
- return sum;
34
- }, 0);
35
- }
36
- function getStatusColors(status) {
37
- switch (status) {
38
- case ExecutionStatus.Pending:
39
- return {
40
- icon: Palette.Gray,
41
- description: Palette.Gray,
42
- command: Palette.DarkGray,
43
- symbol: Palette.DarkGray,
44
- };
45
- case ExecutionStatus.Running:
46
- return {
47
- icon: Palette.Gray,
48
- description: getTextColor(true),
49
- command: Palette.LightGreen,
50
- symbol: Palette.AshGray,
51
- };
52
- case ExecutionStatus.Success:
53
- return {
54
- icon: Colors.Status.Success,
55
- description: getTextColor(true),
56
- command: Palette.Gray,
57
- symbol: Palette.Gray,
58
- };
59
- case ExecutionStatus.Failed:
60
- return {
61
- icon: Colors.Status.Error,
62
- description: Colors.Status.Error,
63
- command: Colors.Status.Error,
64
- symbol: Palette.Gray,
65
- };
66
- case ExecutionStatus.Aborted:
67
- return {
68
- icon: Palette.DarkOrange,
69
- description: getTextColor(true),
70
- command: Palette.DarkOrange,
71
- symbol: Palette.Gray,
72
- };
73
- }
74
- }
75
- function CommandStatusDisplay({ item, elapsed }) {
76
- const colors = getStatusColors(item.status);
77
- const getElapsedTime = () => {
78
- if (item.status === ExecutionStatus.Running && elapsed !== undefined) {
79
- return elapsed;
80
- }
81
- else if (item.startTime && item.endTime) {
82
- return item.endTime - item.startTime;
83
- }
84
- return undefined;
85
- };
86
- const elapsedTime = getElapsedTime();
87
- return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { paddingLeft: 2, children: [_jsx(Text, { color: colors.icon, children: STATUS_ICONS[item.status] }), _jsx(Text, { color: colors.description, children: item.label || item.command.description }), elapsedTime !== undefined && (_jsxs(Text, { color: Palette.DarkGray, children: [" (", formatDuration(elapsedTime), ")"] }))] }), _jsxs(Box, { paddingLeft: 5, children: [_jsx(Text, { color: colors.symbol, children: "\u221F " }), _jsx(Text, { color: colors.command, children: item.command.command }), item.status === ExecutionStatus.Running && (_jsxs(Text, { children: [' ', _jsx(Spinner, {})] }))] })] }));
88
- }
89
17
  export function Execute({ tasks, state, status, service, handlers, }) {
90
18
  const isActive = status === ComponentStatus.Active;
91
- // isActive passed as prop
92
19
  const [error, setError] = useState(state?.error ?? null);
93
- const [isExecuting, setIsExecuting] = useState(false);
94
- const [commandStatuses, setCommandStatuses] = useState(state?.commandStatuses ?? []);
20
+ const [taskInfos, setTaskInfos] = useState(state?.taskInfos ?? []);
95
21
  const [message, setMessage] = useState(state?.message ?? '');
96
- const [currentElapsed, setCurrentElapsed] = useState(0);
97
- const [runningIndex, setRunningIndex] = useState(null);
98
- const [outputs, setOutputs] = useState([]);
22
+ const [completed, setCompleted] = useState(state?.completed ?? 0);
99
23
  const [hasProcessed, setHasProcessed] = useState(false);
24
+ const [taskExecutionTimes, setTaskExecutionTimes] = useState(state?.taskExecutionTimes ?? []);
25
+ const [completionMessage, setCompletionMessage] = useState(state?.completionMessage ?? null);
26
+ const [summary, setSummary] = useState(state?.summary ?? '');
100
27
  // Derive loading state from current conditions
101
- const isLoading = isActive &&
102
- commandStatuses.length === 0 &&
103
- !error &&
104
- !isExecuting &&
105
- !hasProcessed;
28
+ const isLoading = isActive && taskInfos.length === 0 && !error && !hasProcessed;
29
+ const isExecuting = completed < taskInfos.length;
30
+ // Handle cancel with useCallback to ensure we capture latest state
31
+ const handleCancel = useCallback(() => {
32
+ // Mark tasks based on their status relative to completed:
33
+ // - Before completed: finished (Success)
34
+ // - At completed: interrupted (Aborted)
35
+ // - After completed: never started (Cancelled)
36
+ const updatedTaskInfos = taskInfos.map((task, taskIndex) => {
37
+ if (taskIndex < completed) {
38
+ // Tasks that completed before interruption
39
+ return { ...task, status: ExecutionStatus.Success };
40
+ }
41
+ else if (taskIndex === completed) {
42
+ // Task that was running when interrupted
43
+ return { ...task, status: ExecutionStatus.Aborted };
44
+ }
45
+ else {
46
+ // Tasks that haven't started yet
47
+ return { ...task, status: ExecutionStatus.Cancelled };
48
+ }
49
+ });
50
+ setTaskInfos(updatedTaskInfos);
51
+ handlers?.updateState({
52
+ message,
53
+ summary,
54
+ taskInfos: updatedTaskInfos,
55
+ completed,
56
+ taskExecutionTimes,
57
+ completionMessage: null,
58
+ error: null,
59
+ });
60
+ handlers?.onAborted('execution');
61
+ }, [message, summary, taskInfos, completed, taskExecutionTimes, handlers]);
106
62
  useInput((_, key) => {
107
63
  if (key.escape && (isLoading || isExecuting) && isActive) {
108
- setIsExecuting(false);
109
- setRunningIndex(null);
110
- // Mark any running command as aborted when cancelled
111
- const now = Date.now();
112
- setCommandStatuses((prev) => {
113
- const updated = prev.map((item) => {
114
- if (item.status === ExecutionStatus.Running) {
115
- const elapsed = item.startTime
116
- ? Math.floor((now - item.startTime) / 1000) * 1000
117
- : undefined;
118
- return {
119
- ...item,
120
- status: ExecutionStatus.Aborted,
121
- endTime: now,
122
- elapsed,
123
- };
124
- }
125
- return item;
126
- });
127
- // Save state after updating
128
- handlers?.updateState({
129
- commandStatuses: updated,
130
- message,
131
- });
132
- return updated;
133
- });
134
- handlers?.onAborted('execution');
64
+ handleCancel();
135
65
  }
136
66
  }, { isActive: (isLoading || isExecuting) && isActive });
137
- // Update elapsed time for running command
67
+ // Process tasks to get commands from AI
138
68
  useEffect(() => {
139
- if (runningIndex === null)
140
- return;
141
- const item = commandStatuses[runningIndex];
142
- if (!item?.startTime)
143
- return;
144
- const interval = setInterval(() => {
145
- setCurrentElapsed((prev) => {
146
- const next = Date.now() - item.startTime;
147
- return next !== prev ? next : prev;
148
- });
149
- }, 1000);
150
- return () => clearInterval(interval);
151
- }, [runningIndex, commandStatuses]);
152
- // Handle completion callback when execution finishes
153
- useEffect(() => {
154
- if (isExecuting || commandStatuses.length === 0 || !outputs.length)
155
- return;
156
- // Save state before completing
157
- handlers?.updateState({
158
- message,
159
- commandStatuses,
160
- error,
161
- });
162
- handlers?.completeActive();
163
- }, [isExecuting, commandStatuses, outputs, handlers, message, error]);
164
- useEffect(() => {
165
- if (!isActive) {
69
+ if (!isActive || taskInfos.length > 0 || hasProcessed) {
166
70
  return;
167
71
  }
168
72
  if (!service) {
@@ -178,7 +82,6 @@ export function Execute({ tasks, state, status, service, handlers, }) {
178
82
  // Format tasks for the execute tool and resolve placeholders
179
83
  const taskDescriptions = tasks
180
84
  .map((task) => {
181
- // Resolve placeholders in task action
182
85
  const resolvedAction = replacePlaceholders(task.action, userConfig);
183
86
  const params = task.params
184
87
  ? ` (params: ${JSON.stringify(task.params)})`
@@ -191,79 +94,62 @@ export function Execute({ tasks, state, status, service, handlers, }) {
191
94
  await ensureMinimumTime(startTime, MINIMUM_PROCESSING_TIME);
192
95
  if (!mounted)
193
96
  return;
97
+ // Add debug components to timeline if present
98
+ addDebugToTimeline(result.debug, handlers);
194
99
  if (!result.commands || result.commands.length === 0) {
195
- setOutputs([]);
196
100
  setHasProcessed(true);
197
- // Save state before completing
198
101
  handlers?.updateState({
199
102
  message: result.message,
200
- commandStatuses: [],
103
+ summary: '',
104
+ taskInfos: [],
105
+ completed: 0,
106
+ taskExecutionTimes: [],
107
+ completionMessage: null,
108
+ error: null,
201
109
  });
202
110
  handlers?.completeActive();
203
111
  return;
204
112
  }
205
- // Resolve placeholders in command strings before execution
113
+ // Resolve placeholders in command strings
206
114
  const resolvedCommands = result.commands.map((cmd) => ({
207
115
  ...cmd,
208
116
  command: replacePlaceholders(cmd.command, userConfig),
209
117
  }));
210
- // Set message and initialize command statuses
211
- setMessage(result.message);
212
- setCommandStatuses(resolvedCommands.map((cmd, index) => ({
213
- command: cmd,
214
- status: ExecutionStatus.Pending,
118
+ // Set message, summary, and create task infos
119
+ const newMessage = result.message;
120
+ const newSummary = result.summary || '';
121
+ const infos = resolvedCommands.map((cmd, index) => ({
215
122
  label: tasks[index]?.action,
216
- })));
217
- setIsExecuting(true);
218
- // Execute commands sequentially
219
- const outputs = await executeCommands(resolvedCommands, (progress) => {
220
- if (!mounted)
221
- return;
222
- const now = Date.now();
223
- setCommandStatuses((prev) => prev.map((item, idx) => {
224
- if (idx === progress.currentIndex) {
225
- const isStarting = progress.status === ExecutionStatus.Running &&
226
- !item.startTime;
227
- const isEnding = progress.status !== ExecutionStatus.Running &&
228
- progress.status !== ExecutionStatus.Pending;
229
- const endTime = isEnding ? now : item.endTime;
230
- const elapsed = isEnding && item.startTime
231
- ? Math.floor((now - item.startTime) / 1000) * 1000
232
- : item.elapsed;
233
- return {
234
- ...item,
235
- status: progress.status,
236
- output: progress.output,
237
- startTime: isStarting ? now : item.startTime,
238
- endTime,
239
- elapsed,
240
- };
241
- }
242
- return item;
243
- }));
244
- if (progress.status === ExecutionStatus.Running) {
245
- setRunningIndex((prev) => prev !== progress.currentIndex ? progress.currentIndex : prev);
246
- setCurrentElapsed((prev) => (prev !== 0 ? 0 : prev));
247
- }
248
- else if (progress.status === ExecutionStatus.Success ||
249
- progress.status === ExecutionStatus.Failed) {
250
- setRunningIndex((prev) => (prev !== null ? null : prev));
251
- }
123
+ command: cmd,
124
+ }));
125
+ setMessage(newMessage);
126
+ setSummary(newSummary);
127
+ setTaskInfos(infos);
128
+ setCompleted(0); // Start with first task
129
+ // Update state after AI processing
130
+ handlers?.updateState({
131
+ message: newMessage,
132
+ summary: newSummary,
133
+ taskInfos: infos,
134
+ completed: 0,
135
+ taskExecutionTimes: [],
136
+ completionMessage: null,
137
+ error: null,
252
138
  });
253
- if (mounted) {
254
- setOutputs(outputs);
255
- setIsExecuting(false);
256
- }
257
139
  }
258
140
  catch (err) {
259
141
  await ensureMinimumTime(startTime, MINIMUM_PROCESSING_TIME);
260
142
  if (mounted) {
261
143
  const errorMessage = formatErrorMessage(err);
262
- setIsExecuting(false);
263
144
  setError(errorMessage);
264
145
  setHasProcessed(true);
265
- // Save error state
266
146
  handlers?.updateState({
147
+ message: '',
148
+ summary: '',
149
+ taskInfos: [],
150
+ completed: 0,
151
+ taskExecutionTimes: [],
152
+ completionMessage: null,
267
153
  error: errorMessage,
268
154
  });
269
155
  handlers?.onError(errorMessage);
@@ -274,12 +160,122 @@ export function Execute({ tasks, state, status, service, handlers, }) {
274
160
  return () => {
275
161
  mounted = false;
276
162
  };
277
- }, [tasks, isActive, service, handlers]);
163
+ }, [tasks, isActive, service, handlers, taskInfos.length, hasProcessed]);
164
+ // Handle task completion - move to next task
165
+ const handleTaskComplete = useCallback((index, _output, elapsed) => {
166
+ const updatedTimes = [...taskExecutionTimes, elapsed];
167
+ setTaskExecutionTimes(updatedTimes);
168
+ // Update task with elapsed time and success status
169
+ const updatedTaskInfos = taskInfos.map((task, i) => i === index
170
+ ? { ...task, status: ExecutionStatus.Success, elapsed }
171
+ : task);
172
+ setTaskInfos(updatedTaskInfos);
173
+ if (index < taskInfos.length - 1) {
174
+ // More tasks to execute
175
+ setCompleted(index + 1);
176
+ handlers?.updateState({
177
+ message,
178
+ summary,
179
+ taskInfos: updatedTaskInfos,
180
+ completed: index + 1,
181
+ taskExecutionTimes: updatedTimes,
182
+ completionMessage: null,
183
+ error: null,
184
+ });
185
+ }
186
+ else {
187
+ // All tasks complete
188
+ const totalElapsed = updatedTimes.reduce((sum, time) => sum + time, 0);
189
+ const summaryText = summary?.trim() || 'Execution completed';
190
+ const completion = `${summaryText} in ${formatDuration(totalElapsed)}.`;
191
+ setCompletionMessage(completion);
192
+ handlers?.updateState({
193
+ message,
194
+ summary,
195
+ taskInfos: updatedTaskInfos,
196
+ completed: index + 1,
197
+ taskExecutionTimes: updatedTimes,
198
+ completionMessage: completion,
199
+ error: null,
200
+ });
201
+ handlers?.completeActive();
202
+ }
203
+ }, [taskInfos, message, handlers, taskExecutionTimes, summary]);
204
+ const handleTaskError = useCallback((index, error, elapsed) => {
205
+ const task = taskInfos[index];
206
+ const isCritical = task?.command.critical !== false; // Default to true
207
+ // Update task with elapsed time and failed status
208
+ const updatedTaskInfos = taskInfos.map((task, i) => i === index
209
+ ? { ...task, status: ExecutionStatus.Failed, elapsed }
210
+ : task);
211
+ setTaskInfos(updatedTaskInfos);
212
+ if (isCritical) {
213
+ // Critical failure - stop execution
214
+ setError(error);
215
+ handlers?.updateState({
216
+ message,
217
+ summary,
218
+ taskInfos: updatedTaskInfos,
219
+ completed: index + 1,
220
+ taskExecutionTimes,
221
+ completionMessage: null,
222
+ error,
223
+ });
224
+ handlers?.onError(error);
225
+ }
226
+ else {
227
+ // Non-critical failure - continue to next task
228
+ const updatedTimes = [...taskExecutionTimes, elapsed];
229
+ setTaskExecutionTimes(updatedTimes);
230
+ if (index < taskInfos.length - 1) {
231
+ setCompleted(index + 1);
232
+ handlers?.updateState({
233
+ message,
234
+ summary,
235
+ taskInfos: updatedTaskInfos,
236
+ completed: index + 1,
237
+ taskExecutionTimes: updatedTimes,
238
+ completionMessage: null,
239
+ error: null,
240
+ });
241
+ }
242
+ else {
243
+ // Last task, complete execution
244
+ const totalElapsed = updatedTimes.reduce((sum, time) => sum + time, 0);
245
+ const summaryText = summary?.trim() || 'Execution completed';
246
+ const completion = `${summaryText} in ${formatDuration(totalElapsed)}.`;
247
+ setCompletionMessage(completion);
248
+ handlers?.updateState({
249
+ message,
250
+ summary,
251
+ taskInfos: updatedTaskInfos,
252
+ completed: index + 1,
253
+ taskExecutionTimes: updatedTimes,
254
+ completionMessage: completion,
255
+ error: null,
256
+ });
257
+ handlers?.completeActive();
258
+ }
259
+ }
260
+ }, [taskInfos, message, handlers, taskExecutionTimes, summary]);
261
+ const handleTaskAbort = useCallback((_index) => {
262
+ // Task was aborted - execution already stopped by Escape handler
263
+ // Just update state, don't call onAborted (already called at Execute level)
264
+ handlers?.updateState({
265
+ message,
266
+ summary,
267
+ taskInfos,
268
+ completed,
269
+ taskExecutionTimes,
270
+ completionMessage: null,
271
+ error: null,
272
+ });
273
+ }, [taskInfos, message, summary, completed, taskExecutionTimes, handlers]);
278
274
  // Return null only when loading completes with no commands
279
- if (!isActive && commandStatuses.length === 0 && !error) {
275
+ if (!isActive && taskInfos.length === 0 && !error) {
280
276
  return null;
281
277
  }
282
278
  // Show completed steps when not active
283
- const showCompletedSteps = !isActive && commandStatuses.length > 0;
284
- 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 || showCompletedSteps) && (_jsxs(Box, { flexDirection: "column", marginLeft: 1, children: [message && (_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { color: getTextColor(isActive), children: message }), isExecuting && (_jsxs(Text, { children: [' ', _jsx(Spinner, {})] }))] })), commandStatuses.map((item, index) => (_jsx(Box, { marginBottom: index < commandStatuses.length - 1 ? 1 : 0, children: _jsx(CommandStatusDisplay, { item: item, elapsed: index === runningIndex ? currentElapsed : undefined }) }, index)))] })), error && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: Colors.Status.Error, children: ["Error: ", error] }) }))] }));
279
+ const showTasks = !isActive && taskInfos.length > 0;
280
+ 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] }) }))] }));
285
281
  }
@@ -1,7 +1,7 @@
1
1
  import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Box, Text } from 'ink';
3
- import { getFeedbackColor } from '../services/colors.js';
4
3
  import { FeedbackType } from '../types/types.js';
4
+ import { getFeedbackColor } from '../services/colors.js';
5
5
  function getSymbol(type) {
6
6
  return {
7
7
  [FeedbackType.Info]: 'ℹ',
@@ -3,7 +3,7 @@ import { useEffect, useState } from 'react';
3
3
  import { Box, Text } from 'ink';
4
4
  import { ComponentStatus, } from '../types/components.js';
5
5
  import { Colors, getTextColor } from '../services/colors.js';
6
- import { createReportDefinition } from '../services/components.js';
6
+ import { addDebugToTimeline, createReportDefinition, } from '../services/components.js';
7
7
  import { DebugLevel } from '../services/configuration.js';
8
8
  import { useInput } from '../services/keyboard.js';
9
9
  import { formatErrorMessage } from '../services/messages.js';
@@ -81,6 +81,8 @@ export function Introspect({ tasks, state, status, service, children, debug = De
81
81
  const result = await svc.processWithTool(introspectAction, 'introspect');
82
82
  await ensureMinimumTime(startTime, MIN_PROCESSING_TIME);
83
83
  if (mounted) {
84
+ // Add debug components to timeline if present
85
+ addDebugToTimeline(result.debug, handlers);
84
86
  // Parse capabilities from returned tasks
85
87
  let capabilities = result.tasks.map(parseCapabilityFromTask);
86
88
  // Filter out internal capabilities when not in debug mode
package/dist/ui/Label.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from 'ink';
3
- import { DebugLevel } from '../services/configuration.js';
4
3
  import { getTaskColors, getTaskTypeLabel } from '../services/colors.js';
4
+ import { DebugLevel } from '../services/configuration.js';
5
5
  import { Separator } from './Separator.js';
6
6
  export function Label({ description, taskType, showType = false, isCurrent = false, debug = DebugLevel.None, }) {
7
7
  const colors = getTaskColors(taskType, isCurrent);
package/dist/ui/List.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from 'ink';
3
+ import { Palette } from '../services/colors.js';
3
4
  import { Separator } from './Separator.js';
4
5
  export const List = ({ items, level = 0, highlightedIndex = null, highlightedParentIndex = null, showType = false, }) => {
5
6
  const marginLeft = level > 0 ? 2 : 0;
@@ -21,7 +22,7 @@ export const List = ({ items, level = 0, highlightedIndex = null, highlightedPar
21
22
  const markerColor = item.markerColor ||
22
23
  (isHighlighted && item.type.highlightedColor
23
24
  ? item.type.highlightedColor
24
- : 'whiteBright');
25
+ : Palette.White);
25
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));
26
27
  }) }));
27
28
  };
@@ -3,8 +3,8 @@ 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 { DebugLevel } from '../services/configuration.js';
7
6
  import { getTaskColors, getTaskTypeLabel } from '../services/colors.js';
7
+ import { DebugLevel } from '../services/configuration.js';
8
8
  import { useInput } from '../services/keyboard.js';
9
9
  import { Label } from './Label.js';
10
10
  import { List } from './List.js';
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useEffect, useState } from 'react';
3
3
  import { Text } from 'ink';
4
+ import { Palette } from '../services/colors.js';
4
5
  const FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
5
6
  const INTERVAL = 80;
6
7
  const CYCLE = FRAMES.length * INTERVAL;
@@ -18,5 +19,5 @@ export function Spinner() {
18
19
  }, INTERVAL);
19
20
  return () => clearInterval(timer);
20
21
  }, []);
21
- return _jsx(Text, { color: "blueBright", children: FRAMES[frame] });
22
+ return _jsx(Text, { color: Palette.Cyan, children: FRAMES[frame] });
22
23
  }
@@ -0,0 +1,22 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import { getStatusColors, Palette, STATUS_ICONS } from '../services/colors.js';
4
+ import { ExecutionStatus } from '../services/shell.js';
5
+ import { formatDuration } from '../services/utils.js';
6
+ import { Spinner } from './Spinner.js';
7
+ export function Subtask({ label, command, status, isActive, startTime, endTime, elapsed, }) {
8
+ const colors = getStatusColors(status);
9
+ const isCancelled = status === ExecutionStatus.Cancelled;
10
+ const isAborted = status === ExecutionStatus.Aborted;
11
+ const shouldStrikethrough = isCancelled || isAborted;
12
+ const isFinished = status === ExecutionStatus.Success ||
13
+ status === ExecutionStatus.Failed ||
14
+ status === ExecutionStatus.Aborted;
15
+ const elapsedTime = elapsed ?? (startTime && endTime ? endTime - startTime : undefined);
16
+ // Apply strikethrough for cancelled and aborted tasks
17
+ const formatText = (text) => shouldStrikethrough ? text.split('').join('\u0336') + '\u0336' : text;
18
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { paddingLeft: 2, gap: 1, children: [_jsx(Text, { color: colors.icon, children: STATUS_ICONS[status] }), _jsx(Text, { color: colors.description, children: shouldStrikethrough
19
+ ? formatText(label || command.description)
20
+ : label || command.description }), (isFinished || status === ExecutionStatus.Running) &&
21
+ elapsedTime !== undefined && (_jsxs(Text, { color: Palette.DarkGray, children: ["(", formatDuration(elapsedTime), ")"] }))] }), _jsxs(Box, { paddingLeft: 5, flexDirection: "row", children: [_jsx(Box, { children: _jsx(Text, { color: colors.symbol, children: "\u221F " }) }), _jsxs(Box, { gap: 1, children: [_jsx(Text, { color: colors.command, children: command.command }), status === ExecutionStatus.Running && _jsx(Spinner, {})] })] })] }));
22
+ }
@@ -0,0 +1,85 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useEffect, useState } from 'react';
3
+ import { ExecutionStatus, executeCommand, } from '../services/shell.js';
4
+ import { calculateElapsed } from '../services/utils.js';
5
+ import { Subtask } from './Subtask.js';
6
+ export function Task({ label, command, isActive, index, initialStatus, initialElapsed, onComplete, onAbort, onError, }) {
7
+ const [status, setStatus] = useState(initialStatus ?? ExecutionStatus.Pending);
8
+ const [startTime, setStartTime] = useState();
9
+ const [endTime, setEndTime] = useState();
10
+ const [elapsed, setElapsed] = useState(initialElapsed);
11
+ const [currentElapsed, setCurrentElapsed] = useState(0);
12
+ // Update elapsed time while running
13
+ useEffect(() => {
14
+ if (status !== ExecutionStatus.Running || !startTime)
15
+ return;
16
+ const interval = setInterval(() => {
17
+ setCurrentElapsed((prev) => {
18
+ const next = Date.now() - startTime;
19
+ return next !== prev ? next : prev;
20
+ });
21
+ }, 1000);
22
+ return () => clearInterval(interval);
23
+ }, [status, startTime]);
24
+ // Execute command when becoming active
25
+ useEffect(() => {
26
+ // Don't execute if task is cancelled or if not active
27
+ if (!isActive ||
28
+ status === ExecutionStatus.Cancelled ||
29
+ status !== ExecutionStatus.Pending) {
30
+ return;
31
+ }
32
+ let mounted = true;
33
+ async function execute() {
34
+ const start = Date.now();
35
+ setStatus(ExecutionStatus.Running);
36
+ setStartTime(start);
37
+ setCurrentElapsed(0);
38
+ try {
39
+ const output = await executeCommand(command, undefined, index);
40
+ if (!mounted)
41
+ return;
42
+ const end = Date.now();
43
+ setEndTime(end);
44
+ const taskDuration = calculateElapsed(start);
45
+ setElapsed(taskDuration);
46
+ setStatus(output.result === 'success'
47
+ ? ExecutionStatus.Success
48
+ : ExecutionStatus.Failed);
49
+ if (output.result === 'success') {
50
+ onComplete?.(index, output, taskDuration);
51
+ }
52
+ else {
53
+ onError?.(index, output.errors || 'Command failed', taskDuration);
54
+ }
55
+ }
56
+ catch (err) {
57
+ if (!mounted)
58
+ return;
59
+ const end = Date.now();
60
+ setEndTime(end);
61
+ const errorDuration = calculateElapsed(start);
62
+ setElapsed(errorDuration);
63
+ setStatus(ExecutionStatus.Failed);
64
+ onError?.(index, err instanceof Error ? err.message : 'Unknown error', errorDuration);
65
+ }
66
+ }
67
+ execute();
68
+ return () => {
69
+ mounted = false;
70
+ };
71
+ // eslint-disable-next-line react-hooks/exhaustive-deps
72
+ }, [isActive]);
73
+ // Handle abort when task becomes inactive while running
74
+ useEffect(() => {
75
+ if (!isActive && status === ExecutionStatus.Running && startTime) {
76
+ // Task was aborted mid-execution
77
+ const end = Date.now();
78
+ setEndTime(end);
79
+ setElapsed(calculateElapsed(startTime));
80
+ setStatus(ExecutionStatus.Aborted);
81
+ onAbort?.(index);
82
+ }
83
+ }, [isActive, status, startTime, index, onAbort]);
84
+ return (_jsx(Subtask, { label: label, command: command, status: status, isActive: isActive, startTime: startTime, endTime: endTime, elapsed: status === ExecutionStatus.Running ? currentElapsed : elapsed }));
85
+ }