prompt-language-shell 0.7.8 → 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.
@@ -8,7 +8,6 @@ import { ExecutionStatus } from './shell.js';
8
8
  export const Palette = {
9
9
  White: '#ffffff',
10
10
  AshGray: '#d0d0d0',
11
- PaleGreen: '#a8dcbc',
12
11
  Gray: '#888888',
13
12
  DarkGray: '#666666',
14
13
  CharcoalGray: '#282828',
@@ -16,8 +15,8 @@ export const Palette = {
16
15
  LightGreen: '#65b595',
17
16
  BrightGreen: '#3e9a3e',
18
17
  Yellow: '#cccc5c',
19
- LightYellow: '#d4d47a',
20
18
  Orange: '#f48c80',
19
+ MediumOrange: '#d07560',
21
20
  DarkOrange: '#ab5e40',
22
21
  BurntOrange: '#cc7a5c',
23
22
  Red: '#cc5c5c',
@@ -47,7 +46,7 @@ export const Colors = {
47
46
  Status: {
48
47
  Success: Palette.BrightGreen,
49
48
  Error: Palette.Red,
50
- Warning: Palette.Orange,
49
+ Warning: Palette.MediumOrange,
51
50
  Info: Palette.Cyan,
52
51
  },
53
52
  Label: {
@@ -130,7 +129,7 @@ const taskColors = {
130
129
  const feedbackColors = {
131
130
  [FeedbackType.Info]: Colors.Status.Info,
132
131
  [FeedbackType.Succeeded]: Colors.Status.Success,
133
- [FeedbackType.Aborted]: Colors.Status.Warning,
132
+ [FeedbackType.Aborted]: Palette.MediumOrange,
134
133
  [FeedbackType.Failed]: Colors.Status.Error,
135
134
  };
136
135
  /**
@@ -220,6 +219,7 @@ export const STATUS_ICONS = {
220
219
  [ExecutionStatus.Success]: '✓ ',
221
220
  [ExecutionStatus.Failed]: '✗ ',
222
221
  [ExecutionStatus.Aborted]: '⊘ ',
222
+ [ExecutionStatus.Cancelled]: '⊘ ',
223
223
  };
224
224
  /**
225
225
  * Get colors for different execution status states.
@@ -249,7 +249,7 @@ export function getStatusColors(status) {
249
249
  case ExecutionStatus.Success:
250
250
  return {
251
251
  icon: Colors.Status.Success,
252
- description: getTextColor(true),
252
+ description: Palette.AshGray,
253
253
  command: Palette.Gray,
254
254
  symbol: Palette.Gray,
255
255
  };
@@ -262,10 +262,17 @@ export function getStatusColors(status) {
262
262
  };
263
263
  case ExecutionStatus.Aborted:
264
264
  return {
265
- icon: Palette.DarkOrange,
266
- description: getTextColor(true),
267
- command: Palette.DarkOrange,
265
+ icon: Palette.MediumOrange,
266
+ description: Palette.Gray,
267
+ command: Palette.MediumOrange,
268
268
  symbol: Palette.Gray,
269
269
  };
270
+ case ExecutionStatus.Cancelled:
271
+ return {
272
+ icon: Palette.DarkGray,
273
+ description: Palette.DarkGray,
274
+ command: Palette.DarkGray,
275
+ symbol: Palette.DarkGray,
276
+ };
270
277
  }
271
278
  }
@@ -1,8 +1,8 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { existsSync, readFileSync } from 'node:fs';
3
- import { ComponentName } from '../types/types.js';
4
- import { ComponentStatus, } from '../types/components.js';
5
3
  import { parse as parseYaml } from 'yaml';
4
+ import { ComponentStatus, } from '../types/components.js';
5
+ import { ComponentName } from '../types/types.js';
6
6
  import { ConfigDefinitionType, getConfigPath, getConfigSchema, loadConfig, } from './configuration.js';
7
7
  import { getConfirmationMessage } from './messages.js';
8
8
  import { StepType } from '../ui/Config.js';
@@ -180,7 +180,11 @@ export function createConfigDefinition(onFinished, onAborted) {
180
180
  id: randomUUID(),
181
181
  name: ComponentName.Config,
182
182
  status: ComponentStatus.Awaiting,
183
- state: {},
183
+ state: {
184
+ values: {},
185
+ completedStep: 0,
186
+ selectedIndex: 0,
187
+ },
184
188
  props: {
185
189
  steps: createConfigSteps(),
186
190
  onFinished,
@@ -196,7 +200,11 @@ export function createConfigDefinitionWithKeys(keys, onFinished, onAborted) {
196
200
  id: randomUUID(),
197
201
  name: ComponentName.Config,
198
202
  status: ComponentStatus.Awaiting,
199
- state: {},
203
+ state: {
204
+ values: {},
205
+ completedStep: 0,
206
+ selectedIndex: 0,
207
+ },
200
208
  props: {
201
209
  steps: createConfigStepsFromSchema(keys),
202
210
  onFinished,
@@ -338,7 +346,15 @@ export function createExecuteDefinition(tasks, service) {
338
346
  id: randomUUID(),
339
347
  name: ComponentName.Execute,
340
348
  status: ComponentStatus.Awaiting,
341
- state: {},
349
+ state: {
350
+ error: null,
351
+ message: '',
352
+ summary: '',
353
+ taskInfos: [],
354
+ completed: 0,
355
+ taskExecutionTimes: [],
356
+ completionMessage: null,
357
+ },
342
358
  props: {
343
359
  tasks,
344
360
  service,
@@ -350,7 +366,12 @@ export function createValidateDefinition(missingConfig, userRequest, service, on
350
366
  id: randomUUID(),
351
367
  name: ComponentName.Validate,
352
368
  status: ComponentStatus.Awaiting,
353
- state: {},
369
+ state: {
370
+ error: null,
371
+ completionMessage: null,
372
+ configRequirements: null,
373
+ validated: false,
374
+ },
354
375
  props: {
355
376
  missingConfig,
356
377
  userRequest,
@@ -1,9 +1,8 @@
1
- import { TaskType } from '../types/types.js';
1
+ import { FeedbackType, TaskType } from '../types/types.js';
2
2
  import { createAnswerDefinition, createConfigDefinitionWithKeys, createConfirmDefinition, createExecuteDefinition, createFeedback, createIntrospectDefinition, createMessage, createScheduleDefinition, createValidateDefinition, } from './components.js';
3
3
  import { saveConfig, unflattenConfig } from './configuration.js';
4
- import { FeedbackType } from '../types/types.js';
5
- import { validateExecuteTasks } from './validator.js';
6
4
  import { getCancellationMessage, getMixedTaskTypesError, getUnknownRequestMessage, } from './messages.js';
5
+ import { validateExecuteTasks } from './validator.js';
7
6
  /**
8
7
  * Determine the operation name based on task types
9
8
  */
@@ -5,6 +5,7 @@ export var ExecutionStatus;
5
5
  ExecutionStatus["Success"] = "success";
6
6
  ExecutionStatus["Failed"] = "failed";
7
7
  ExecutionStatus["Aborted"] = "aborted";
8
+ ExecutionStatus["Cancelled"] = "cancelled";
8
9
  })(ExecutionStatus || (ExecutionStatus = {}));
9
10
  export var ExecutionResult;
10
11
  (function (ExecutionResult) {
package/dist/ui/Answer.js CHANGED
@@ -40,7 +40,9 @@ export function Answer({ question, state, status, service, handlers, }) {
40
40
  const answerText = result.answer || '';
41
41
  setAnswer(answerText);
42
42
  // Update component state so answer persists in timeline
43
- handlers?.updateState({ answer: answerText });
43
+ handlers?.updateState({
44
+ answer: answerText,
45
+ });
44
46
  // Signal completion
45
47
  handlers?.completeActive();
46
48
  }
@@ -49,6 +51,9 @@ export function Answer({ question, state, status, service, handlers, }) {
49
51
  if (mounted) {
50
52
  const errorMessage = formatErrorMessage(err);
51
53
  setError(errorMessage);
54
+ handlers?.updateState({
55
+ error: errorMessage,
56
+ });
52
57
  handlers?.onError(errorMessage);
53
58
  }
54
59
  }
@@ -5,8 +5,8 @@ import { ComponentStatus, } from '../types/components.js';
5
5
  import { TaskType } from '../types/types.js';
6
6
  import { Colors } from '../services/colors.js';
7
7
  import { addDebugToTimeline, createScheduleDefinition, } from '../services/components.js';
8
- import { formatErrorMessage } from '../services/messages.js';
9
8
  import { useInput } from '../services/keyboard.js';
9
+ import { formatErrorMessage } from '../services/messages.js';
10
10
  import { handleRefinement } from '../services/refinement.js';
11
11
  import { routeTasksWithConfirm } from '../services/router.js';
12
12
  import { ensureMinimumTime } from '../services/timing.js';
@@ -88,6 +88,9 @@ export function Command({ command, state, status, service, handlers, onAborted,
88
88
  if (mounted) {
89
89
  const errorMessage = formatErrorMessage(err);
90
90
  setError(errorMessage);
91
+ handlers?.updateState({
92
+ error: errorMessage,
93
+ });
91
94
  handlers?.onError(errorMessage);
92
95
  }
93
96
  }
@@ -3,16 +3,16 @@ import { memo } from 'react';
3
3
  import { ComponentName } from '../types/types.js';
4
4
  import { Answer } from './Answer.js';
5
5
  import { Command } from './Command.js';
6
- import { Confirm } from './Confirm.js';
7
6
  import { Config } from './Config.js';
7
+ import { Confirm } from './Confirm.js';
8
8
  import { Debug } from './Debug.js';
9
9
  import { Execute } from './Execute.js';
10
10
  import { Feedback } from './Feedback.js';
11
11
  import { Introspect } from './Introspect.js';
12
12
  import { Message } from './Message.js';
13
- import { Schedule } from './Schedule.js';
14
13
  import { Refinement } from './Refinement.js';
15
14
  import { Report } from './Report.js';
15
+ import { Schedule } from './Schedule.js';
16
16
  import { Validate } from './Validate.js';
17
17
  import { Welcome } from './Welcome.js';
18
18
  export const Component = memo(function Component({ def, debug, }) {
package/dist/ui/Config.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState } from 'react';
2
+ import { useEffect, useState } from 'react';
3
3
  import { Box, Text, useFocus } from 'ink';
4
4
  import TextInput from 'ink-text-input';
5
5
  import { ComponentStatus } from '../types/components.js';
@@ -17,6 +17,10 @@ function TextStep({ value, placeholder, validate, onChange, onSubmit, }) {
17
17
  const [inputValue, setInputValue] = useState(value);
18
18
  const [validationFailed, setValidationFailed] = useState(false);
19
19
  const { isFocused } = useFocus({ autoFocus: true });
20
+ // Sync internal state with prop changes
21
+ useEffect(() => {
22
+ setInputValue(value);
23
+ }, [value]);
20
24
  const handleChange = (newValue) => {
21
25
  setInputValue(newValue);
22
26
  onChange(newValue);
@@ -92,8 +96,20 @@ export function Config({ steps, state, status, debug = DebugLevel.None, handlers
92
96
  });
93
97
  return initial;
94
98
  });
95
- const [inputValue, setInputValue] = useState('');
99
+ const [inputValue, setInputValue] = useState(() => {
100
+ // Initialize with the current step's value if available
101
+ if (isActive && step < steps.length) {
102
+ const stepConfig = steps[step];
103
+ const configKey = stepConfig.path || stepConfig.key;
104
+ return values[configKey] || '';
105
+ }
106
+ return '';
107
+ });
96
108
  const [selectedIndex, setSelectedIndex] = useState(() => {
109
+ // If not active, use saved state
110
+ if (!isActive && state?.selectedIndex !== undefined) {
111
+ return state.selectedIndex;
112
+ }
97
113
  // Initialize selectedIndex based on current step's defaultIndex
98
114
  if (isActive &&
99
115
  step < steps.length &&
@@ -108,6 +124,16 @@ export function Config({ steps, state, status, debug = DebugLevel.None, handlers
108
124
  }
109
125
  return value.replace(/\n/g, '').trim();
110
126
  };
127
+ // Update inputValue when step changes
128
+ useEffect(() => {
129
+ if (isActive && step < steps.length) {
130
+ const stepConfig = steps[step];
131
+ const configKey = stepConfig.path || stepConfig.key;
132
+ const value = values[configKey] || '';
133
+ setInputValue(value);
134
+ }
135
+ // eslint-disable-next-line react-hooks/exhaustive-deps
136
+ }, [step, isActive, steps]);
111
137
  useInput((_, key) => {
112
138
  if (!isActive || step >= steps.length)
113
139
  return;
@@ -137,6 +163,7 @@ export function Config({ steps, state, status, debug = DebugLevel.None, handlers
137
163
  handlers?.updateState({
138
164
  values,
139
165
  completedStep: step,
166
+ selectedIndex,
140
167
  });
141
168
  if (onAborted) {
142
169
  onAborted('configuration');
@@ -197,6 +224,7 @@ export function Config({ steps, state, status, debug = DebugLevel.None, handlers
197
224
  const stateUpdate = {
198
225
  values: newValues,
199
226
  completedStep: steps.length,
227
+ selectedIndex,
200
228
  };
201
229
  handlers?.updateState(stateUpdate);
202
230
  // Call onFinished callback and handle result
@@ -219,6 +247,7 @@ export function Config({ steps, state, status, debug = DebugLevel.None, handlers
219
247
  const stateUpdate = {
220
248
  values: newValues,
221
249
  completedStep: step + 1,
250
+ selectedIndex,
222
251
  };
223
252
  handlers?.updateState(stateUpdate);
224
253
  const nextStep = step + 1;
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState } from 'react';
3
3
  import { Box, Text } from 'ink';
4
- import { ComponentStatus, } from '../types/components.js';
4
+ import { ComponentStatus } from '../types/components.js';
5
5
  import { Colors, Palette } from '../services/colors.js';
6
6
  import { useInput } from '../services/keyboard.js';
7
7
  import { UserQuery } from './UserQuery.js';
@@ -1,13 +1,14 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useCallback, useEffect, useState } from 'react';
3
3
  import { Box, Text } from 'ink';
4
- import { ComponentStatus } from '../types/components.js';
4
+ import { ComponentStatus, } from '../types/components.js';
5
5
  import { Colors, getTextColor } from '../services/colors.js';
6
6
  import { addDebugToTimeline } from '../services/components.js';
7
7
  import { useInput } from '../services/keyboard.js';
8
+ import { loadUserConfig } from '../services/loader.js';
8
9
  import { formatErrorMessage } from '../services/messages.js';
9
10
  import { replacePlaceholders } from '../services/resolver.js';
10
- import { loadUserConfig } from '../services/loader.js';
11
+ import { ExecutionStatus } from '../services/shell.js';
11
12
  import { ensureMinimumTime } from '../services/timing.js';
12
13
  import { formatDuration } from '../services/utils.js';
13
14
  import { Spinner } from './Spinner.js';
@@ -18,19 +19,49 @@ export function Execute({ tasks, state, status, service, handlers, }) {
18
19
  const [error, setError] = useState(state?.error ?? null);
19
20
  const [taskInfos, setTaskInfos] = useState(state?.taskInfos ?? []);
20
21
  const [message, setMessage] = useState(state?.message ?? '');
21
- const [activeTaskIndex, setActiveTaskIndex] = useState(state?.activeTaskIndex ?? -1);
22
+ const [completed, setCompleted] = useState(state?.completed ?? 0);
22
23
  const [hasProcessed, setHasProcessed] = useState(false);
23
24
  const [taskExecutionTimes, setTaskExecutionTimes] = useState(state?.taskExecutionTimes ?? []);
24
25
  const [completionMessage, setCompletionMessage] = useState(state?.completionMessage ?? null);
25
26
  const [summary, setSummary] = useState(state?.summary ?? '');
26
27
  // Derive loading state from current conditions
27
28
  const isLoading = isActive && taskInfos.length === 0 && !error && !hasProcessed;
28
- const isExecuting = activeTaskIndex >= 0 && activeTaskIndex < taskInfos.length;
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]);
29
62
  useInput((_, key) => {
30
63
  if (key.escape && (isLoading || isExecuting) && isActive) {
31
- // Cancel execution
32
- setActiveTaskIndex(-1);
33
- handlers?.onAborted('execution');
64
+ handleCancel();
34
65
  }
35
66
  }, { isActive: (isLoading || isExecuting) && isActive });
36
67
  // Process tasks to get commands from AI
@@ -69,7 +100,12 @@ export function Execute({ tasks, state, status, service, handlers, }) {
69
100
  setHasProcessed(true);
70
101
  handlers?.updateState({
71
102
  message: result.message,
103
+ summary: '',
72
104
  taskInfos: [],
105
+ completed: 0,
106
+ taskExecutionTimes: [],
107
+ completionMessage: null,
108
+ error: null,
73
109
  });
74
110
  handlers?.completeActive();
75
111
  return;
@@ -80,14 +116,26 @@ export function Execute({ tasks, state, status, service, handlers, }) {
80
116
  command: replacePlaceholders(cmd.command, userConfig),
81
117
  }));
82
118
  // Set message, summary, and create task infos
83
- setMessage(result.message);
84
- setSummary(result.summary || '');
119
+ const newMessage = result.message;
120
+ const newSummary = result.summary || '';
85
121
  const infos = resolvedCommands.map((cmd, index) => ({
86
122
  label: tasks[index]?.action,
87
123
  command: cmd,
88
124
  }));
125
+ setMessage(newMessage);
126
+ setSummary(newSummary);
89
127
  setTaskInfos(infos);
90
- setActiveTaskIndex(0); // Start with first task
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,
138
+ });
91
139
  }
92
140
  catch (err) {
93
141
  await ensureMinimumTime(startTime, MINIMUM_PROCESSING_TIME);
@@ -95,7 +143,15 @@ export function Execute({ tasks, state, status, service, handlers, }) {
95
143
  const errorMessage = formatErrorMessage(err);
96
144
  setError(errorMessage);
97
145
  setHasProcessed(true);
98
- handlers?.updateState({ error: errorMessage });
146
+ handlers?.updateState({
147
+ message: '',
148
+ summary: '',
149
+ taskInfos: [],
150
+ completed: 0,
151
+ taskExecutionTimes: [],
152
+ completionMessage: null,
153
+ error: errorMessage,
154
+ });
99
155
  handlers?.onError(errorMessage);
100
156
  }
101
157
  }
@@ -109,13 +165,26 @@ export function Execute({ tasks, state, status, service, handlers, }) {
109
165
  const handleTaskComplete = useCallback((index, _output, elapsed) => {
110
166
  const updatedTimes = [...taskExecutionTimes, elapsed];
111
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);
112
173
  if (index < taskInfos.length - 1) {
113
174
  // More tasks to execute
114
- setActiveTaskIndex(index + 1);
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
+ });
115
185
  }
116
186
  else {
117
187
  // All tasks complete
118
- setActiveTaskIndex(-1);
119
188
  const totalElapsed = updatedTimes.reduce((sum, time) => sum + time, 0);
120
189
  const summaryText = summary?.trim() || 'Execution completed';
121
190
  const completion = `${summaryText} in ${formatDuration(totalElapsed)}.`;
@@ -123,48 +192,67 @@ export function Execute({ tasks, state, status, service, handlers, }) {
123
192
  handlers?.updateState({
124
193
  message,
125
194
  summary,
126
- taskInfos,
127
- activeTaskIndex: -1,
195
+ taskInfos: updatedTaskInfos,
196
+ completed: index + 1,
128
197
  taskExecutionTimes: updatedTimes,
129
198
  completionMessage: completion,
199
+ error: null,
130
200
  });
131
201
  handlers?.completeActive();
132
202
  }
133
203
  }, [taskInfos, message, handlers, taskExecutionTimes, summary]);
134
- const handleTaskError = useCallback((index, error) => {
204
+ const handleTaskError = useCallback((index, error, elapsed) => {
135
205
  const task = taskInfos[index];
136
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);
137
212
  if (isCritical) {
138
213
  // Critical failure - stop execution
139
- setActiveTaskIndex(-1);
140
214
  setError(error);
141
215
  handlers?.updateState({
142
216
  message,
143
- taskInfos,
144
- activeTaskIndex: -1,
217
+ summary,
218
+ taskInfos: updatedTaskInfos,
219
+ completed: index + 1,
220
+ taskExecutionTimes,
221
+ completionMessage: null,
145
222
  error,
146
223
  });
147
224
  handlers?.onError(error);
148
225
  }
149
226
  else {
150
227
  // Non-critical failure - continue to next task
228
+ const updatedTimes = [...taskExecutionTimes, elapsed];
229
+ setTaskExecutionTimes(updatedTimes);
151
230
  if (index < taskInfos.length - 1) {
152
- setActiveTaskIndex(index + 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
+ });
153
241
  }
154
242
  else {
155
243
  // Last task, complete execution
156
- setActiveTaskIndex(-1);
157
- const totalElapsed = taskExecutionTimes.reduce((sum, time) => sum + time, 0);
244
+ const totalElapsed = updatedTimes.reduce((sum, time) => sum + time, 0);
158
245
  const summaryText = summary?.trim() || 'Execution completed';
159
246
  const completion = `${summaryText} in ${formatDuration(totalElapsed)}.`;
160
247
  setCompletionMessage(completion);
161
248
  handlers?.updateState({
162
249
  message,
163
250
  summary,
164
- taskInfos,
165
- activeTaskIndex: -1,
166
- taskExecutionTimes,
251
+ taskInfos: updatedTaskInfos,
252
+ completed: index + 1,
253
+ taskExecutionTimes: updatedTimes,
167
254
  completionMessage: completion,
255
+ error: null,
168
256
  });
169
257
  handlers?.completeActive();
170
258
  }
@@ -175,15 +263,19 @@ export function Execute({ tasks, state, status, service, handlers, }) {
175
263
  // Just update state, don't call onAborted (already called at Execute level)
176
264
  handlers?.updateState({
177
265
  message,
266
+ summary,
178
267
  taskInfos,
179
- activeTaskIndex: -1,
268
+ completed,
269
+ taskExecutionTimes,
270
+ completionMessage: null,
271
+ error: null,
180
272
  });
181
- }, [taskInfos, message, handlers]);
273
+ }, [taskInfos, message, summary, completed, taskExecutionTimes, handlers]);
182
274
  // Return null only when loading completes with no commands
183
275
  if (!isActive && taskInfos.length === 0 && !error) {
184
276
  return null;
185
277
  }
186
278
  // Show completed steps when not active
187
279
  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] }) }))] }));
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] }) }))] }));
189
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]: 'ℹ',
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
  }
@@ -1,11 +1,22 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from 'ink';
3
3
  import { getStatusColors, Palette, STATUS_ICONS } from '../services/colors.js';
4
- import { formatDuration } from '../services/utils.js';
5
4
  import { ExecutionStatus } from '../services/shell.js';
5
+ import { formatDuration } from '../services/utils.js';
6
6
  import { Spinner } from './Spinner.js';
7
- export function Subtask({ label, command, status, startTime, endTime, elapsed, }) {
7
+ export function Subtask({ label, command, status, isActive, startTime, endTime, elapsed, }) {
8
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;
9
15
  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, {})] })] }));
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, {})] })] })] }));
11
22
  }
package/dist/ui/Task.js CHANGED
@@ -3,11 +3,11 @@ import { useEffect, useState } from 'react';
3
3
  import { ExecutionStatus, executeCommand, } from '../services/shell.js';
4
4
  import { calculateElapsed } from '../services/utils.js';
5
5
  import { Subtask } from './Subtask.js';
6
- export function Task({ label, command, isActive, index, onComplete, onAbort, onError, }) {
7
- const [status, setStatus] = useState(ExecutionStatus.Pending);
6
+ export function Task({ label, command, isActive, index, initialStatus, initialElapsed, onComplete, onAbort, onError, }) {
7
+ const [status, setStatus] = useState(initialStatus ?? ExecutionStatus.Pending);
8
8
  const [startTime, setStartTime] = useState();
9
9
  const [endTime, setEndTime] = useState();
10
- const [elapsed, setElapsed] = useState();
10
+ const [elapsed, setElapsed] = useState(initialElapsed);
11
11
  const [currentElapsed, setCurrentElapsed] = useState(0);
12
12
  // Update elapsed time while running
13
13
  useEffect(() => {
@@ -23,7 +23,10 @@ export function Task({ label, command, isActive, index, onComplete, onAbort, onE
23
23
  }, [status, startTime]);
24
24
  // Execute command when becoming active
25
25
  useEffect(() => {
26
- if (!isActive || status !== ExecutionStatus.Pending) {
26
+ // Don't execute if task is cancelled or if not active
27
+ if (!isActive ||
28
+ status === ExecutionStatus.Cancelled ||
29
+ status !== ExecutionStatus.Pending) {
27
30
  return;
28
31
  }
29
32
  let mounted = true;
@@ -47,7 +50,7 @@ export function Task({ label, command, isActive, index, onComplete, onAbort, onE
47
50
  onComplete?.(index, output, taskDuration);
48
51
  }
49
52
  else {
50
- onError?.(index, output.errors || 'Command failed');
53
+ onError?.(index, output.errors || 'Command failed', taskDuration);
51
54
  }
52
55
  }
53
56
  catch (err) {
@@ -55,9 +58,10 @@ export function Task({ label, command, isActive, index, onComplete, onAbort, onE
55
58
  return;
56
59
  const end = Date.now();
57
60
  setEndTime(end);
58
- setElapsed(calculateElapsed(start));
61
+ const errorDuration = calculateElapsed(start);
62
+ setElapsed(errorDuration);
59
63
  setStatus(ExecutionStatus.Failed);
60
- onError?.(index, err instanceof Error ? err.message : 'Unknown error');
64
+ onError?.(index, err instanceof Error ? err.message : 'Unknown error', errorDuration);
61
65
  }
62
66
  }
63
67
  execute();
@@ -77,5 +81,5 @@ export function Task({ label, command, isActive, index, onComplete, onAbort, onE
77
81
  onAbort?.(index);
78
82
  }
79
83
  }, [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 }));
84
+ return (_jsx(Subtask, { label: label, command: command, status: status, isActive: isActive, startTime: startTime, endTime: endTime, elapsed: status === ExecutionStatus.Running ? currentElapsed : elapsed }));
81
85
  }
@@ -4,19 +4,19 @@ 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
+ import { addDebugToTimeline, createConfigStepsFromSchema, } from '../services/components.js';
8
+ import { DebugLevel, saveConfig, unflattenConfig, } from '../services/configuration.js';
8
9
  import { useInput } from '../services/keyboard.js';
9
10
  import { formatErrorMessage } from '../services/messages.js';
10
11
  import { ensureMinimumTime } from '../services/timing.js';
11
- import { DebugLevel, saveConfig, unflattenConfig, } from '../services/configuration.js';
12
- import { Config, StepType } from './Config.js';
12
+ import { Config } from './Config.js';
13
13
  import { Spinner } from './Spinner.js';
14
14
  const MIN_PROCESSING_TIME = 1000;
15
15
  export function Validate({ missingConfig, userRequest, state, status, service, children, debug = DebugLevel.None, onError, onComplete, onAborted, handlers, }) {
16
16
  const isActive = status === ComponentStatus.Active;
17
- const [error, setError] = useState(null);
18
- const [completionMessage, setCompletionMessage] = useState(null);
19
- const [configRequirements, setConfigRequirements] = useState(null);
17
+ const [error, setError] = useState(state?.error ?? null);
18
+ const [completionMessage, setCompletionMessage] = useState(state?.completionMessage ?? null);
19
+ const [configRequirements, setConfigRequirements] = useState(state?.configRequirements ?? null);
20
20
  const [showConfig, setShowConfig] = useState(false);
21
21
  useInput((_, key) => {
22
22
  if (key.escape && isActive && !showConfig) {
@@ -74,8 +74,10 @@ export function Validate({ missingConfig, userRequest, state, status, service, c
74
74
  setConfigRequirements(withDescriptions);
75
75
  // Save state after validation completes
76
76
  handlers?.updateState({
77
+ completionMessage: message,
77
78
  configRequirements: withDescriptions,
78
79
  validated: true,
80
+ error: null,
79
81
  });
80
82
  }
81
83
  }
@@ -83,16 +85,17 @@ export function Validate({ missingConfig, userRequest, state, status, service, c
83
85
  await ensureMinimumTime(startTime, MIN_PROCESSING_TIME);
84
86
  if (mounted) {
85
87
  const errorMessage = formatErrorMessage(err);
88
+ setError(errorMessage);
86
89
  // Save error state
87
90
  handlers?.updateState({
88
91
  error: errorMessage,
92
+ completionMessage: null,
93
+ configRequirements: null,
94
+ validated: false,
89
95
  });
90
96
  if (onError) {
91
97
  onError(errorMessage);
92
98
  }
93
- else {
94
- setError(errorMessage);
95
- }
96
99
  }
97
100
  }
98
101
  }
@@ -113,16 +116,19 @@ export function Validate({ missingConfig, userRequest, state, status, service, c
113
116
  if (!isActive && !completionMessage && !error && !children) {
114
117
  return null;
115
118
  }
116
- // Create ConfigSteps from requirements
119
+ // Create ConfigSteps from requirements using createConfigStepsFromSchema
120
+ // to load current values from config file, then override descriptions
117
121
  const configSteps = configRequirements
118
- ? configRequirements.map((req) => ({
119
- description: req.description || req.path,
120
- key: req.path,
121
- path: req.path,
122
- type: StepType.Text,
123
- value: null,
124
- validate: () => true,
125
- }))
122
+ ? (() => {
123
+ const keys = configRequirements.map((req) => req.path);
124
+ const steps = createConfigStepsFromSchema(keys);
125
+ // Override descriptions with LLM-generated ones
126
+ return steps.map((step, index) => ({
127
+ ...step,
128
+ description: configRequirements[index].description ||
129
+ configRequirements[index].path,
130
+ }));
131
+ })()
126
132
  : null;
127
133
  const handleConfigFinished = (config) => {
128
134
  // Convert flat dotted keys to nested structure grouped by section
@@ -5,8 +5,8 @@ import { ComponentStatus, } from '../types/components.js';
5
5
  import { ComponentName, FeedbackType } from '../types/types.js';
6
6
  import { createFeedback, isStateless, markAsDone, } from '../services/components.js';
7
7
  import { DebugLevel } from '../services/configuration.js';
8
- import { exitApp } from '../services/process.js';
9
8
  import { getCancellationMessage } from '../services/messages.js';
9
+ import { exitApp } from '../services/process.js';
10
10
  import { Component } from './Component.js';
11
11
  export const Workflow = ({ initialQueue, debug }) => {
12
12
  const [timeline, setTimeline] = useState([]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prompt-language-shell",
3
- "version": "0.7.8",
3
+ "version": "0.8.0",
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",
@@ -47,22 +47,22 @@
47
47
  },
48
48
  "homepage": "https://github.com/aswitalski/pls#readme",
49
49
  "dependencies": {
50
- "@anthropic-ai/sdk": "^0.70.1",
51
- "ink": "^6.5.1",
50
+ "@anthropic-ai/sdk": "^0.71.2",
51
+ "ink": "^6.6.0",
52
52
  "ink-text-input": "^6.0.0",
53
- "react": "^19.2.0",
54
- "yaml": "^2.8.1"
53
+ "react": "^19.2.3",
54
+ "yaml": "^2.8.2"
55
55
  },
56
56
  "devDependencies": {
57
- "@types/node": "^24.10.1",
58
- "@types/react": "^19.2.6",
59
- "@vitest/coverage-v8": "^4.0.12",
60
- "eslint": "^9.39.1",
57
+ "@types/node": "^25.0.3",
58
+ "@types/react": "^19.2.7",
59
+ "@vitest/coverage-v8": "^4.0.16",
60
+ "eslint": "^9.39.2",
61
61
  "husky": "^9.1.7",
62
62
  "ink-testing-library": "^4.0.0",
63
- "prettier": "^3.6.2",
63
+ "prettier": "^3.7.4",
64
64
  "typescript": "^5.9.3",
65
- "typescript-eslint": "^8.47.0",
66
- "vitest": "^4.0.12"
65
+ "typescript-eslint": "^8.50.1",
66
+ "vitest": "^4.0.16"
67
67
  }
68
68
  }