prompt-language-shell 0.7.6 → 0.7.8

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.
@@ -165,6 +165,7 @@ export class AnthropicService {
165
165
  });
166
166
  return {
167
167
  message: input.message,
168
+ summary: input.summary,
168
169
  tasks: [],
169
170
  commands: input.commands,
170
171
  debug,
@@ -1,5 +1,6 @@
1
1
  import { FeedbackType, TaskType } from '../types/types.js';
2
2
  import { DebugLevel } from './configuration.js';
3
+ import { ExecutionStatus } from './shell.js';
3
4
  /**
4
5
  * Base color palette - raw color values with descriptive names.
5
6
  * All colors used in the interface are defined here.
@@ -210,3 +211,61 @@ export function getTaskTypeLabel(type, debug) {
210
211
  }
211
212
  return type;
212
213
  }
214
+ /**
215
+ * Status icons for execution states
216
+ */
217
+ export const STATUS_ICONS = {
218
+ [ExecutionStatus.Pending]: '- ',
219
+ [ExecutionStatus.Running]: '• ',
220
+ [ExecutionStatus.Success]: '✓ ',
221
+ [ExecutionStatus.Failed]: '✗ ',
222
+ [ExecutionStatus.Aborted]: '⊘ ',
223
+ };
224
+ /**
225
+ * Get colors for different execution status states.
226
+ *
227
+ * Returns color scheme for:
228
+ * - Icon: Status indicator symbol
229
+ * - Description: Task description text
230
+ * - Command: Command text
231
+ * - Symbol: Command prefix symbol
232
+ */
233
+ export function getStatusColors(status) {
234
+ switch (status) {
235
+ case ExecutionStatus.Pending:
236
+ return {
237
+ icon: Palette.Gray,
238
+ description: Palette.Gray,
239
+ command: Palette.DarkGray,
240
+ symbol: Palette.DarkGray,
241
+ };
242
+ case ExecutionStatus.Running:
243
+ return {
244
+ icon: Palette.Gray,
245
+ description: getTextColor(true),
246
+ command: Palette.LightGreen,
247
+ symbol: Palette.AshGray,
248
+ };
249
+ case ExecutionStatus.Success:
250
+ return {
251
+ icon: Colors.Status.Success,
252
+ description: getTextColor(true),
253
+ command: Palette.Gray,
254
+ symbol: Palette.Gray,
255
+ };
256
+ case ExecutionStatus.Failed:
257
+ return {
258
+ icon: Colors.Status.Error,
259
+ description: Colors.Status.Error,
260
+ command: Colors.Status.Error,
261
+ symbol: Palette.Gray,
262
+ };
263
+ case ExecutionStatus.Aborted:
264
+ return {
265
+ icon: Palette.DarkOrange,
266
+ description: getTextColor(true),
267
+ command: Palette.DarkOrange,
268
+ symbol: Palette.Gray,
269
+ };
270
+ }
271
+ }
@@ -361,3 +361,11 @@ export function createValidateDefinition(missingConfig, userRequest, service, on
361
361
  },
362
362
  };
363
363
  }
364
+ /**
365
+ * Add debug components to timeline if present in result
366
+ */
367
+ export function addDebugToTimeline(debugComponents, handlers) {
368
+ if (debugComponents && debugComponents.length > 0 && handlers) {
369
+ handlers.addToTimeline(...debugComponents);
370
+ }
371
+ }
@@ -1,3 +1,9 @@
1
+ /**
2
+ * Calculates elapsed time from a start timestamp, rounded to seconds.
3
+ */
4
+ export function calculateElapsed(start) {
5
+ return Math.floor((Date.now() - start) / 1000) * 1000;
6
+ }
1
7
  /**
2
8
  * Formats a duration in milliseconds to a human-readable string.
3
9
  * Uses correct singular/plural forms.
@@ -88,7 +88,12 @@ these commands specifically for their environment and workflow.
88
88
  Return a structured response with commands to execute:
89
89
 
90
90
  **Response structure:**
91
- - **message**: Brief status message (max 64 characters, end with period)
91
+ - **message**: Brief status message in imperative mood (max 64 characters,
92
+ end with colon)
93
+ - **summary**: Natural language summary as if execution has finished,
94
+ like a concierge would report (max 48 characters, no period, time will
95
+ be appended). Use varied expressions and synonyms, not necessarily the
96
+ same verb as the message. MUST NOT be empty.
92
97
  - **commands**: Array of command objects to execute sequentially
93
98
 
94
99
  **Command object structure:**
@@ -133,7 +138,8 @@ Task: {
133
138
 
134
139
  Response:
135
140
  ```
136
- message: "Creating the file."
141
+ message: "Create the file:"
142
+ summary: "File created"
137
143
  commands:
138
144
  - description: "Create test.txt"
139
145
  command: "touch test.txt"
@@ -148,7 +154,8 @@ Task: {
148
154
 
149
155
  Response:
150
156
  ```
151
- message: "Listing directory contents."
157
+ message: "List directory contents:"
158
+ summary: "I've listed the directory contents"
152
159
  commands:
153
160
  - description: "List files with details"
154
161
  command: "ls -la"
@@ -167,7 +174,8 @@ Tasks:
167
174
 
168
175
  Response:
169
176
  ```
170
- message: "Setting up the project."
177
+ message: "Set up the project:"
178
+ summary: "Project ready to go"
171
179
  commands:
172
180
  - description: "Create project directory"
173
181
  command: "mkdir -p my-project"
@@ -188,7 +196,8 @@ Task: {
188
196
 
189
197
  Response:
190
198
  ```
191
- message: "Installing dependencies."
199
+ message: "Install dependencies:"
200
+ summary: "Dependencies installed successfully"
192
201
  commands:
193
202
  - description: "Install npm packages"
194
203
  command: "npm install"
@@ -224,7 +233,8 @@ The "Process Data" skill's Execution section specifies:
224
233
 
225
234
  Response (using skill's Execution commands):
226
235
  ```
227
- message: "Processing sales data."
236
+ message: "Process sales data:"
237
+ summary: "Sales data transformed and exported"
228
238
  commands:
229
239
  - description: "Load the sales dataset"
230
240
  command: "curl -O https://data.example.com/sales.csv"
@@ -250,7 +260,8 @@ Task: {
250
260
 
251
261
  Response:
252
262
  ```
253
- message: "Creating backup."
263
+ message: "Create backup:"
264
+ summary: "Backup complete"
254
265
  commands:
255
266
  - description: "Copy config directory"
256
267
  command: "cp -r ~/.config/app ~/.config/app.backup"
@@ -265,7 +276,8 @@ Task: {
265
276
 
266
277
  Response:
267
278
  ```
268
- message: "Checking disk space."
279
+ message: "Check disk space:"
280
+ summary: "Disk space verified"
269
281
  commands:
270
282
  - description: "Show disk usage"
271
283
  command: "df -h"
@@ -7,13 +7,17 @@ task structures with high-level tasks and their subtasks.
7
7
  ## Response Format
8
8
 
9
9
  Every response MUST include a brief message (single sentence, max 64
10
- characters, ending with period) that introduces the schedule.
10
+ characters, ending with period) that introduces the schedule. Use
11
+ either imperative mood or present tense statements, but NEVER use
12
+ present continuous ("-ing" form).
11
13
 
12
- **Examples**: "Here's the schedule." / "I've organized the work." /
13
- "This is how I'll structure it."
14
+ **Examples**: "Build the application." / "Here's the schedule." /
15
+ "Deploy to production." / "I've organized the work."
14
16
 
15
17
  **Critical rules**:
16
18
  - Message is MANDATORY
19
+ - Use imperative mood OR present tense statements
20
+ - NEVER use present continuous ("-ing" form)
17
21
  - NEVER repeat the same message
18
22
  - ALWAYS end with period (.)
19
23
  - Vary phrasing naturally
@@ -8,6 +8,10 @@ export const executeTool = {
8
8
  type: 'string',
9
9
  description: 'Brief status message about the execution. Must be a single sentence, maximum 64 characters, ending with a period.',
10
10
  },
11
+ summary: {
12
+ type: 'string',
13
+ description: 'Natural language summary as if execution has finished, like a concierge would report. Shown after execution completes with time appended. Use varied expressions and synonyms, not necessarily the same verb as the message. Must be a single sentence without period, maximum 48 characters. MUST NOT be empty. Example: "Project ready to go" (time will be appended as " in X seconds").',
14
+ },
11
15
  commands: {
12
16
  type: 'array',
13
17
  description: 'Array of commands to execute sequentially',
@@ -39,6 +43,6 @@ export const executeTool = {
39
43
  },
40
44
  },
41
45
  },
42
- required: ['message', 'commands'],
46
+ required: ['message', 'summary', 'commands'],
43
47
  },
44
48
  };
package/dist/ui/Answer.js CHANGED
@@ -3,6 +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 { addDebugToTimeline } from '../services/components.js';
6
7
  import { useInput } from '../services/keyboard.js';
7
8
  import { formatErrorMessage } from '../services/messages.js';
8
9
  import { withMinimumTime } from '../services/timing.js';
@@ -33,6 +34,8 @@ export function Answer({ question, state, status, service, handlers, }) {
33
34
  // Call answer tool with minimum processing time for UX polish
34
35
  const result = await withMinimumTime(() => svc.processWithTool(question, 'answer'), MINIMUM_PROCESSING_TIME);
35
36
  if (mounted) {
37
+ // Add debug components to timeline if present
38
+ addDebugToTimeline(result.debug, handlers);
36
39
  // Extract answer from result
37
40
  const answerText = result.answer || '';
38
41
  setAnswer(answerText);
@@ -4,7 +4,7 @@ import { Box, Text } from 'ink';
4
4
  import { ComponentStatus, } from '../types/components.js';
5
5
  import { TaskType } from '../types/types.js';
6
6
  import { Colors } from '../services/colors.js';
7
- import { createScheduleDefinition } from '../services/components.js';
7
+ import { addDebugToTimeline, createScheduleDefinition, } from '../services/components.js';
8
8
  import { formatErrorMessage } from '../services/messages.js';
9
9
  import { useInput } from '../services/keyboard.js';
10
10
  import { handleRefinement } from '../services/refinement.js';
@@ -53,12 +53,10 @@ export function Command({ command, state, status, service, handlers, onAborted,
53
53
  // Add debug components to timeline if present
54
54
  // If we delegated to configure, include both schedule and configure debug
55
55
  // If not, only include schedule debug (result.debug is same as scheduleDebug)
56
- const allDebug = allConfig
56
+ const debugComponents = allConfig
57
57
  ? [...scheduleDebug, ...(result.debug || [])]
58
58
  : scheduleDebug;
59
- if (allDebug.length > 0) {
60
- handlers?.addToTimeline(...allDebug);
61
- }
59
+ addDebugToTimeline(debugComponents, handlers);
62
60
  // Save result to state for timeline display
63
61
  handlers?.updateState({
64
62
  message: result.message,
@@ -1,168 +1,41 @@
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
4
  import { ComponentStatus } from '../types/components.js';
5
- import { Colors, getTextColor, Palette } from '../services/colors.js';
5
+ import { Colors, getTextColor } from '../services/colors.js';
6
+ import { addDebugToTimeline } from '../services/components.js';
6
7
  import { useInput } from '../services/keyboard.js';
7
8
  import { formatErrorMessage } from '../services/messages.js';
8
- import { formatDuration } from '../services/utils.js';
9
- import { ExecutionStatus, executeCommands, } from '../services/shell.js';
10
9
  import { replacePlaceholders } from '../services/resolver.js';
11
10
  import { loadUserConfig } from '../services/loader.js';
12
11
  import { ensureMinimumTime } from '../services/timing.js';
12
+ import { formatDuration } from '../services/utils.js';
13
13
  import { Spinner } from './Spinner.js';
14
+ import { Task } from './Task.js';
14
15
  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
16
  export function Execute({ tasks, state, status, service, handlers, }) {
90
17
  const isActive = status === ComponentStatus.Active;
91
- // isActive passed as prop
92
18
  const [error, setError] = useState(state?.error ?? null);
93
- const [isExecuting, setIsExecuting] = useState(false);
94
- const [commandStatuses, setCommandStatuses] = useState(state?.commandStatuses ?? []);
19
+ const [taskInfos, setTaskInfos] = useState(state?.taskInfos ?? []);
95
20
  const [message, setMessage] = useState(state?.message ?? '');
96
- const [currentElapsed, setCurrentElapsed] = useState(0);
97
- const [runningIndex, setRunningIndex] = useState(null);
98
- const [outputs, setOutputs] = useState([]);
21
+ const [activeTaskIndex, setActiveTaskIndex] = useState(state?.activeTaskIndex ?? -1);
99
22
  const [hasProcessed, setHasProcessed] = useState(false);
23
+ const [taskExecutionTimes, setTaskExecutionTimes] = useState(state?.taskExecutionTimes ?? []);
24
+ const [completionMessage, setCompletionMessage] = useState(state?.completionMessage ?? null);
25
+ const [summary, setSummary] = useState(state?.summary ?? '');
100
26
  // Derive loading state from current conditions
101
- const isLoading = isActive &&
102
- commandStatuses.length === 0 &&
103
- !error &&
104
- !isExecuting &&
105
- !hasProcessed;
27
+ const isLoading = isActive && taskInfos.length === 0 && !error && !hasProcessed;
28
+ const isExecuting = activeTaskIndex >= 0 && activeTaskIndex < taskInfos.length;
106
29
  useInput((_, key) => {
107
30
  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
- });
31
+ // Cancel execution
32
+ setActiveTaskIndex(-1);
134
33
  handlers?.onAborted('execution');
135
34
  }
136
35
  }, { isActive: (isLoading || isExecuting) && isActive });
137
- // Update elapsed time for running command
138
- 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]);
36
+ // Process tasks to get commands from AI
164
37
  useEffect(() => {
165
- if (!isActive) {
38
+ if (!isActive || taskInfos.length > 0 || hasProcessed) {
166
39
  return;
167
40
  }
168
41
  if (!service) {
@@ -178,7 +51,6 @@ export function Execute({ tasks, state, status, service, handlers, }) {
178
51
  // Format tasks for the execute tool and resolve placeholders
179
52
  const taskDescriptions = tasks
180
53
  .map((task) => {
181
- // Resolve placeholders in task action
182
54
  const resolvedAction = replacePlaceholders(task.action, userConfig);
183
55
  const params = task.params
184
56
  ? ` (params: ${JSON.stringify(task.params)})`
@@ -191,81 +63,39 @@ export function Execute({ tasks, state, status, service, handlers, }) {
191
63
  await ensureMinimumTime(startTime, MINIMUM_PROCESSING_TIME);
192
64
  if (!mounted)
193
65
  return;
66
+ // Add debug components to timeline if present
67
+ addDebugToTimeline(result.debug, handlers);
194
68
  if (!result.commands || result.commands.length === 0) {
195
- setOutputs([]);
196
69
  setHasProcessed(true);
197
- // Save state before completing
198
70
  handlers?.updateState({
199
71
  message: result.message,
200
- commandStatuses: [],
72
+ taskInfos: [],
201
73
  });
202
74
  handlers?.completeActive();
203
75
  return;
204
76
  }
205
- // Resolve placeholders in command strings before execution
77
+ // Resolve placeholders in command strings
206
78
  const resolvedCommands = result.commands.map((cmd) => ({
207
79
  ...cmd,
208
80
  command: replacePlaceholders(cmd.command, userConfig),
209
81
  }));
210
- // Set message and initialize command statuses
82
+ // Set message, summary, and create task infos
211
83
  setMessage(result.message);
212
- setCommandStatuses(resolvedCommands.map((cmd, index) => ({
213
- command: cmd,
214
- status: ExecutionStatus.Pending,
84
+ setSummary(result.summary || '');
85
+ const infos = resolvedCommands.map((cmd, index) => ({
215
86
  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
- }
252
- });
253
- if (mounted) {
254
- setOutputs(outputs);
255
- setIsExecuting(false);
256
- }
87
+ command: cmd,
88
+ }));
89
+ setTaskInfos(infos);
90
+ setActiveTaskIndex(0); // Start with first task
257
91
  }
258
92
  catch (err) {
259
93
  await ensureMinimumTime(startTime, MINIMUM_PROCESSING_TIME);
260
94
  if (mounted) {
261
95
  const errorMessage = formatErrorMessage(err);
262
- setIsExecuting(false);
263
96
  setError(errorMessage);
264
97
  setHasProcessed(true);
265
- // Save error state
266
- handlers?.updateState({
267
- error: errorMessage,
268
- });
98
+ handlers?.updateState({ error: errorMessage });
269
99
  handlers?.onError(errorMessage);
270
100
  }
271
101
  }
@@ -274,12 +104,86 @@ export function Execute({ tasks, state, status, service, handlers, }) {
274
104
  return () => {
275
105
  mounted = false;
276
106
  };
277
- }, [tasks, isActive, service, handlers]);
107
+ }, [tasks, isActive, service, handlers, taskInfos.length, hasProcessed]);
108
+ // Handle task completion - move to next task
109
+ const handleTaskComplete = useCallback((index, _output, elapsed) => {
110
+ const updatedTimes = [...taskExecutionTimes, elapsed];
111
+ setTaskExecutionTimes(updatedTimes);
112
+ if (index < taskInfos.length - 1) {
113
+ // More tasks to execute
114
+ setActiveTaskIndex(index + 1);
115
+ }
116
+ else {
117
+ // All tasks complete
118
+ setActiveTaskIndex(-1);
119
+ const totalElapsed = updatedTimes.reduce((sum, time) => sum + time, 0);
120
+ const summaryText = summary?.trim() || 'Execution completed';
121
+ const completion = `${summaryText} in ${formatDuration(totalElapsed)}.`;
122
+ setCompletionMessage(completion);
123
+ handlers?.updateState({
124
+ message,
125
+ summary,
126
+ taskInfos,
127
+ activeTaskIndex: -1,
128
+ taskExecutionTimes: updatedTimes,
129
+ completionMessage: completion,
130
+ });
131
+ handlers?.completeActive();
132
+ }
133
+ }, [taskInfos, message, handlers, taskExecutionTimes, summary]);
134
+ const handleTaskError = useCallback((index, error) => {
135
+ const task = taskInfos[index];
136
+ const isCritical = task?.command.critical !== false; // Default to true
137
+ if (isCritical) {
138
+ // Critical failure - stop execution
139
+ setActiveTaskIndex(-1);
140
+ setError(error);
141
+ handlers?.updateState({
142
+ message,
143
+ taskInfos,
144
+ activeTaskIndex: -1,
145
+ error,
146
+ });
147
+ handlers?.onError(error);
148
+ }
149
+ else {
150
+ // Non-critical failure - continue to next task
151
+ if (index < taskInfos.length - 1) {
152
+ setActiveTaskIndex(index + 1);
153
+ }
154
+ else {
155
+ // Last task, complete execution
156
+ setActiveTaskIndex(-1);
157
+ const totalElapsed = taskExecutionTimes.reduce((sum, time) => sum + time, 0);
158
+ const summaryText = summary?.trim() || 'Execution completed';
159
+ const completion = `${summaryText} in ${formatDuration(totalElapsed)}.`;
160
+ setCompletionMessage(completion);
161
+ handlers?.updateState({
162
+ message,
163
+ summary,
164
+ taskInfos,
165
+ activeTaskIndex: -1,
166
+ taskExecutionTimes,
167
+ completionMessage: completion,
168
+ });
169
+ handlers?.completeActive();
170
+ }
171
+ }
172
+ }, [taskInfos, message, handlers, taskExecutionTimes, summary]);
173
+ const handleTaskAbort = useCallback((_index) => {
174
+ // Task was aborted - execution already stopped by Escape handler
175
+ // Just update state, don't call onAborted (already called at Execute level)
176
+ handlers?.updateState({
177
+ message,
178
+ taskInfos,
179
+ activeTaskIndex: -1,
180
+ });
181
+ }, [taskInfos, message, handlers]);
278
182
  // Return null only when loading completes with no commands
279
- if (!isActive && commandStatuses.length === 0 && !error) {
183
+ if (!isActive && taskInfos.length === 0 && !error) {
280
184
  return null;
281
185
  }
282
186
  // 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] }) }))] }));
187
+ const showTasks = !isActive && taskInfos.length > 0;
188
+ 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 === activeTaskIndex, index: index, 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
189
  }
@@ -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
@@ -0,0 +1,11 @@
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 { formatDuration } from '../services/utils.js';
5
+ import { ExecutionStatus } from '../services/shell.js';
6
+ import { Spinner } from './Spinner.js';
7
+ export function Subtask({ label, command, status, startTime, endTime, elapsed, }) {
8
+ const colors = getStatusColors(status);
9
+ const elapsedTime = elapsed ?? (startTime && endTime ? endTime - startTime : undefined);
10
+ 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: label || command.description }), elapsedTime !== undefined && (_jsxs(Text, { color: Palette.DarkGray, children: ["(", formatDuration(elapsedTime), ")"] }))] }), _jsxs(Box, { paddingLeft: 5, gap: 1, children: [_jsx(Text, { color: colors.symbol, children: "\u221F" }), _jsx(Text, { color: colors.command, children: command.command }), status === ExecutionStatus.Running && _jsx(Spinner, {})] })] }));
11
+ }
@@ -0,0 +1,81 @@
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, onComplete, onAbort, onError, }) {
7
+ const [status, setStatus] = useState(ExecutionStatus.Pending);
8
+ const [startTime, setStartTime] = useState();
9
+ const [endTime, setEndTime] = useState();
10
+ const [elapsed, setElapsed] = useState();
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
+ if (!isActive || status !== ExecutionStatus.Pending) {
27
+ return;
28
+ }
29
+ let mounted = true;
30
+ async function execute() {
31
+ const start = Date.now();
32
+ setStatus(ExecutionStatus.Running);
33
+ setStartTime(start);
34
+ setCurrentElapsed(0);
35
+ try {
36
+ const output = await executeCommand(command, undefined, index);
37
+ if (!mounted)
38
+ return;
39
+ const end = Date.now();
40
+ setEndTime(end);
41
+ const taskDuration = calculateElapsed(start);
42
+ setElapsed(taskDuration);
43
+ setStatus(output.result === 'success'
44
+ ? ExecutionStatus.Success
45
+ : ExecutionStatus.Failed);
46
+ if (output.result === 'success') {
47
+ onComplete?.(index, output, taskDuration);
48
+ }
49
+ else {
50
+ onError?.(index, output.errors || 'Command failed');
51
+ }
52
+ }
53
+ catch (err) {
54
+ if (!mounted)
55
+ return;
56
+ const end = Date.now();
57
+ setEndTime(end);
58
+ setElapsed(calculateElapsed(start));
59
+ setStatus(ExecutionStatus.Failed);
60
+ onError?.(index, err instanceof Error ? err.message : 'Unknown error');
61
+ }
62
+ }
63
+ execute();
64
+ return () => {
65
+ mounted = false;
66
+ };
67
+ // eslint-disable-next-line react-hooks/exhaustive-deps
68
+ }, [isActive]);
69
+ // Handle abort when task becomes inactive while running
70
+ useEffect(() => {
71
+ if (!isActive && status === ExecutionStatus.Running && startTime) {
72
+ // Task was aborted mid-execution
73
+ const end = Date.now();
74
+ setEndTime(end);
75
+ setElapsed(calculateElapsed(startTime));
76
+ setStatus(ExecutionStatus.Aborted);
77
+ onAbort?.(index);
78
+ }
79
+ }, [isActive, status, startTime, index, onAbort]);
80
+ return (_jsx(Subtask, { label: label, command: command, status: status, startTime: startTime, endTime: endTime, elapsed: status === ExecutionStatus.Running ? currentElapsed : elapsed }));
81
+ }
@@ -4,6 +4,7 @@ import { Box, Text } from 'ink';
4
4
  import { ComponentStatus } from '../types/components.js';
5
5
  import { TaskType } from '../types/types.js';
6
6
  import { Colors, getTextColor } from '../services/colors.js';
7
+ import { addDebugToTimeline } from '../services/components.js';
7
8
  import { useInput } from '../services/keyboard.js';
8
9
  import { formatErrorMessage } from '../services/messages.js';
9
10
  import { ensureMinimumTime } from '../services/timing.js';
@@ -42,6 +43,8 @@ export function Validate({ missingConfig, userRequest, state, status, service, c
42
43
  const result = await svc.processWithTool(prompt, 'validate');
43
44
  await ensureMinimumTime(startTime, MIN_PROCESSING_TIME);
44
45
  if (mounted) {
46
+ // Add debug components to timeline if present
47
+ addDebugToTimeline(result.debug, handlers);
45
48
  // Extract CONFIG tasks with descriptions from result
46
49
  const configTasks = result.tasks.filter((task) => task.type === TaskType.Config);
47
50
  // Build ConfigRequirements with descriptions
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prompt-language-shell",
3
- "version": "0.7.6",
3
+ "version": "0.7.8",
4
4
  "description": "Your personal command-line concierge. Ask politely, and it gets things done.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",