prompt-language-shell 0.9.0 → 0.9.2

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,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useCallback, useEffect, useReducer, useRef } from 'react';
2
+ import { useCallback, useEffect, useReducer, useRef, useState } from 'react';
3
3
  import { Box, Text } from 'ink';
4
4
  import { ComponentStatus, } from '../types/components.js';
5
5
  import { getTextColor } from '../services/colors.js';
@@ -7,16 +7,53 @@ import { useInput } from '../services/keyboard.js';
7
7
  import { formatErrorMessage, getExecutionErrorMessage, } from '../services/messages.js';
8
8
  import { ExecutionStatus } from '../services/shell.js';
9
9
  import { ensureMinimumTime } from '../services/timing.js';
10
- import { buildAbortedState, handleTaskCompletion, handleTaskFailure, } from '../execution/handlers.js';
10
+ import { handleTaskCompletion, handleTaskFailure, } from '../execution/handlers.js';
11
11
  import { processTasks } from '../execution/processing.js';
12
12
  import { executeReducer, initialState } from '../execution/reducer.js';
13
+ import { executeTask } from '../execution/runner.js';
13
14
  import { ExecuteActionType } from '../execution/types.js';
14
- import { createFeedback, createMessage, markAsDone, } from '../services/components.js';
15
+ import { getCurrentTaskIndex } from '../execution/utils.js';
16
+ import { createFeedback, createMessage } from '../services/components.js';
15
17
  import { FeedbackType } from '../types/types.js';
16
- import { Message } from './Message.js';
17
18
  import { Spinner } from './Spinner.js';
18
- import { Task } from './Task.js';
19
+ import { TaskView } from './Task.js';
19
20
  const MINIMUM_PROCESSING_TIME = 400;
21
+ const ELAPSED_UPDATE_INTERVAL = 1000;
22
+ /**
23
+ * Check if a task is finished (success, failed, or aborted)
24
+ */
25
+ function isTaskFinished(task) {
26
+ return (task.status === ExecutionStatus.Success ||
27
+ task.status === ExecutionStatus.Failed ||
28
+ task.status === ExecutionStatus.Aborted);
29
+ }
30
+ /**
31
+ * Map ExecuteState to view props for rendering in timeline
32
+ */
33
+ export function mapStateToViewProps(state, isActive) {
34
+ const taskViewData = state.tasks.map((task) => {
35
+ return {
36
+ label: task.label,
37
+ command: task.command,
38
+ status: task.status,
39
+ elapsed: task.elapsed,
40
+ stdout: task.stdout ?? '',
41
+ stderr: task.stderr ?? '',
42
+ isActive: false, // In timeline, no task is active
43
+ isFinished: isTaskFinished(task),
44
+ };
45
+ });
46
+ return {
47
+ isLoading: false,
48
+ isExecuting: false,
49
+ isActive,
50
+ error: state.error,
51
+ message: state.message,
52
+ tasks: taskViewData,
53
+ completionMessage: state.completionMessage,
54
+ showTasks: state.tasks.length > 0,
55
+ };
56
+ }
20
57
  /**
21
58
  * Create an ExecuteState with defaults
22
59
  */
@@ -25,96 +62,87 @@ function createExecuteState(overrides = {}) {
25
62
  message: '',
26
63
  summary: '',
27
64
  tasks: [],
28
- completed: 0,
29
65
  completionMessage: null,
30
66
  error: null,
31
67
  ...overrides,
32
68
  };
33
69
  }
34
- export const ExecuteView = ({ state, status, workdir, onOutputChange, onTaskComplete, onTaskAbort, onTaskError, }) => {
35
- const isActive = status === ComponentStatus.Active;
36
- const { error, tasks, message, completed, completionMessage } = state;
37
- const hasProcessed = tasks.length > 0;
38
- // Derive loading state from current conditions
39
- const isLoading = isActive && tasks.length === 0 && !error && !hasProcessed;
40
- const isExecuting = completed < tasks.length;
70
+ /**
71
+ * Execute view: Pure display component for task execution progress
72
+ */
73
+ export const ExecuteView = ({ isLoading, isExecuting, isActive, error, message, tasks, completionMessage, showTasks, }) => {
41
74
  // Return null only when loading completes with no commands
42
75
  if (!isActive && tasks.length === 0 && !error) {
43
76
  return null;
44
77
  }
45
- // Show completed steps when not active
46
- const showTasks = !isActive && tasks.length > 0;
47
- return (_jsxs(Box, { alignSelf: "flex-start", flexDirection: "column", children: [isLoading && (_jsxs(Box, { marginLeft: 1, children: [_jsx(Text, { color: getTextColor(isActive), children: "Preparing commands. " }), _jsx(Spinner, {})] })), (isExecuting || showTasks) && (_jsxs(Box, { flexDirection: "column", marginLeft: 1, children: [message && (_jsxs(Box, { marginBottom: 1, gap: 1, children: [_jsx(Text, { color: getTextColor(isActive), children: message }), isExecuting && _jsx(Spinner, {})] })), tasks.map((taskInfo, index) => {
48
- // Merge workdir into active task's command
49
- const taskCommand = isActive && index === completed && workdir
50
- ? { ...taskInfo.command, workdir }
51
- : taskInfo.command;
52
- return (_jsx(Box, { marginBottom: index < tasks.length - 1 ? 1 : 0, children: _jsx(Task, { label: taskInfo.label, command: taskCommand, isActive: isActive && index === completed, isFinished: index < completed, index: index, initialStatus: taskInfo.status, initialElapsed: taskInfo.elapsed, initialOutput: taskInfo.stdout || taskInfo.stderr || taskInfo.error
53
- ? {
54
- stdout: taskInfo.stdout ?? '',
55
- stderr: taskInfo.stderr ?? '',
56
- error: taskInfo.error ?? '',
57
- }
58
- : undefined, onOutputChange: onOutputChange, onComplete: onTaskComplete, onAbort: onTaskAbort, onError: onTaskError }) }, index));
59
- })] })), completionMessage && !isActive && (_jsx(Box, { marginTop: 1, marginLeft: 1, children: _jsx(Text, { color: getTextColor(false), children: completionMessage }) })), error && _jsx(Message, { text: error, status: status })] }));
78
+ return (_jsxs(Box, { alignSelf: "flex-start", flexDirection: "column", children: [isLoading && (_jsxs(Box, { marginLeft: 1, children: [_jsx(Text, { color: getTextColor(isActive), children: "Preparing commands. " }), _jsx(Spinner, {})] })), (isExecuting || showTasks) && (_jsxs(Box, { flexDirection: "column", marginLeft: 1, children: [message && (_jsxs(Box, { marginBottom: 1, gap: 1, children: [_jsx(Text, { color: getTextColor(isActive), children: message }), isExecuting && _jsx(Spinner, {})] })), tasks.map((task, index) => (_jsx(Box, { marginBottom: index < tasks.length - 1 ? 1 : 0, children: _jsx(TaskView, { label: task.label, command: task.command, status: task.status, elapsed: task.elapsed, stdout: task.stdout, stderr: task.stderr, isFinished: task.isFinished }) }, index)))] })), completionMessage && !isActive && (_jsx(Box, { marginTop: 1, marginLeft: 1, children: _jsx(Text, { color: getTextColor(false), children: completionMessage }) })), error && (_jsx(Box, { marginLeft: 1, children: _jsx(Text, { children: error }) }))] }));
60
79
  };
61
80
  /**
62
- * Execute controller: Runs tasks sequentially
81
+ * Execute controller: Runs tasks sequentially and manages all execution state
63
82
  */
64
83
  export function Execute({ tasks: inputTasks, status, service, requestHandlers, lifecycleHandlers, workflowHandlers, }) {
65
84
  const isActive = status === ComponentStatus.Active;
66
85
  const [localState, dispatch] = useReducer(executeReducer, initialState);
67
- // Ref to store current output for each task (avoids re-renders)
68
- const taskOutputRef = useRef(new Map());
86
+ // Live output state for currently executing task
87
+ const [liveOutput, setLiveOutput] = useState({
88
+ stdout: '',
89
+ stderr: '',
90
+ error: '',
91
+ });
92
+ const [liveElapsed, setLiveElapsed] = useState(0);
93
+ const [taskStartTime, setTaskStartTime] = useState(null);
69
94
  // Track working directory across commands (persists cd changes)
70
95
  const workdirRef = useRef(undefined);
71
- const { error, tasks, message, completed, hasProcessed, completionMessage, summary, } = localState;
72
- // Derive loading state from current conditions
96
+ // Ref to track if current task execution is cancelled
97
+ const cancelledRef = useRef(false);
98
+ const { error, tasks, message, hasProcessed, completionMessage, summary } = localState;
99
+ // Derive current task index from tasks
100
+ const currentTaskIndex = getCurrentTaskIndex(tasks);
101
+ // Derive states
73
102
  const isLoading = isActive && tasks.length === 0 && !error && !hasProcessed;
74
- const isExecuting = completed < tasks.length;
75
- // Handle output changes from Task - store in ref (no re-render)
76
- const handleOutputChange = useCallback((index, taskOutput) => {
77
- taskOutputRef.current.set(index, taskOutput);
78
- }, []);
79
- // Handle cancel with useCallback to ensure we capture latest state
103
+ const isExecuting = isActive && currentTaskIndex < tasks.length;
104
+ const showTasks = !isActive && tasks.length > 0;
105
+ // Update elapsed time while task is running
106
+ useEffect(() => {
107
+ if (!taskStartTime || !isExecuting)
108
+ return;
109
+ const interval = setInterval(() => {
110
+ setLiveElapsed(Date.now() - taskStartTime);
111
+ }, ELAPSED_UPDATE_INTERVAL);
112
+ return () => {
113
+ clearInterval(interval);
114
+ };
115
+ }, [taskStartTime, isExecuting]);
116
+ // Handle cancel
80
117
  const handleCancel = useCallback(() => {
81
- dispatch({
82
- type: ExecuteActionType.CancelExecution,
83
- payload: { completed },
84
- });
85
- // Get updated task infos after cancel, merging output from ref
86
- const updatedTaskInfos = tasks.map((task, taskIndex) => {
87
- const output = taskOutputRef.current.get(taskIndex);
88
- const baseTask = output
89
- ? {
118
+ cancelledRef.current = true;
119
+ dispatch({ type: ExecuteActionType.CancelExecution });
120
+ // Build updated task infos with current output for the running task
121
+ const updatedTaskInfos = tasks.map((task) => {
122
+ if (task.status === ExecutionStatus.Running) {
123
+ return {
90
124
  ...task,
91
- stdout: output.stdout,
92
- stderr: output.stderr,
93
- error: output.error,
94
- }
95
- : task;
96
- if (taskIndex < completed) {
97
- return { ...baseTask, status: ExecutionStatus.Success };
98
- }
99
- else if (taskIndex === completed) {
100
- return { ...baseTask, status: ExecutionStatus.Aborted };
125
+ status: ExecutionStatus.Aborted,
126
+ stdout: liveOutput.stdout,
127
+ stderr: liveOutput.stderr,
128
+ error: liveOutput.error,
129
+ };
101
130
  }
102
- else {
103
- return { ...baseTask, status: ExecutionStatus.Cancelled };
131
+ else if (task.status === ExecutionStatus.Pending) {
132
+ return { ...task, status: ExecutionStatus.Cancelled };
104
133
  }
134
+ return task;
105
135
  });
106
- // Expose final state
107
136
  const finalState = createExecuteState({
108
137
  message,
109
138
  summary,
110
139
  tasks: updatedTaskInfos,
111
- completed,
112
140
  });
113
141
  requestHandlers.onCompleted(finalState);
114
142
  requestHandlers.onAborted('execution');
115
- }, [message, summary, tasks, completed, requestHandlers]);
143
+ }, [message, summary, tasks, liveOutput, requestHandlers]);
116
144
  useInput((_, key) => {
117
- if (key.escape && (isLoading || isExecuting) && isActive) {
145
+ if (key.escape && (isLoading || isExecuting)) {
118
146
  handleCancel();
119
147
  }
120
148
  }, { isActive: (isLoading || isExecuting) && isActive });
@@ -136,17 +164,13 @@ export function Execute({ tasks: inputTasks, status, service, requestHandlers, l
136
164
  workflowHandlers.addToTimeline(...result.debug);
137
165
  }
138
166
  if (result.commands.length === 0) {
139
- // Check if this is an error response (has error field)
140
167
  if (result.error) {
141
- // Add error message to timeline
142
168
  const errorMessage = getExecutionErrorMessage(result.error);
143
- workflowHandlers.addToTimeline(markAsDone(createMessage(errorMessage)));
144
- // Complete without error in state (message already in timeline)
169
+ workflowHandlers.addToTimeline(createMessage({ text: errorMessage }, ComponentStatus.Done));
145
170
  requestHandlers.onCompleted(createExecuteState({ message: result.message }));
146
171
  lifecycleHandlers.completeActive();
147
172
  return;
148
173
  }
149
- // No commands and no error - just complete
150
174
  dispatch({
151
175
  type: ExecuteActionType.ProcessingComplete,
152
176
  payload: { message: result.message },
@@ -156,7 +180,7 @@ export function Execute({ tasks: inputTasks, status, service, requestHandlers, l
156
180
  return;
157
181
  }
158
182
  // Create task infos from commands
159
- const tasks = result.commands.map((cmd, index) => ({
183
+ const taskInfos = result.commands.map((cmd, index) => ({
160
184
  label: inputTasks[index]?.action ?? cmd.description,
161
185
  command: cmd,
162
186
  status: ExecutionStatus.Pending,
@@ -167,14 +191,13 @@ export function Execute({ tasks: inputTasks, status, service, requestHandlers, l
167
191
  payload: {
168
192
  message: result.message,
169
193
  summary: result.summary,
170
- tasks,
194
+ tasks: taskInfos,
171
195
  },
172
196
  });
173
- // Update state after AI processing
174
197
  requestHandlers.onCompleted(createExecuteState({
175
198
  message: result.message,
176
199
  summary: result.summary,
177
- tasks,
200
+ tasks: taskInfos,
178
201
  }));
179
202
  }
180
203
  catch (err) {
@@ -204,91 +227,131 @@ export function Execute({ tasks: inputTasks, status, service, requestHandlers, l
204
227
  tasks.length,
205
228
  hasProcessed,
206
229
  ]);
207
- // Handle task completion - move to next task
208
- const handleTaskComplete = useCallback((index, elapsed, taskOutput) => {
209
- // Track working directory for subsequent commands
210
- if (taskOutput.workdir) {
211
- workdirRef.current = taskOutput.workdir;
212
- }
213
- // Update tasks with output before calling handler
214
- const tasksWithOutput = tasks.map((task, i) => i === index
215
- ? {
216
- ...task,
217
- stdout: taskOutput.stdout,
218
- stderr: taskOutput.stderr,
219
- error: taskOutput.error,
220
- }
221
- : task);
222
- const result = handleTaskCompletion(index, elapsed, {
223
- tasks: tasksWithOutput,
224
- message,
225
- summary,
226
- });
227
- dispatch(result.action);
228
- requestHandlers.onCompleted(result.finalState);
229
- if (result.shouldComplete) {
230
- lifecycleHandlers.completeActive();
230
+ // Execute current task
231
+ useEffect(() => {
232
+ if (!isActive ||
233
+ tasks.length === 0 ||
234
+ currentTaskIndex >= tasks.length ||
235
+ error) {
236
+ return;
231
237
  }
232
- }, [tasks, message, summary, requestHandlers, lifecycleHandlers]);
233
- const handleTaskError = useCallback((index, error, elapsed, taskOutput) => {
234
- // Track working directory for subsequent commands (even on error)
235
- if (taskOutput.workdir) {
236
- workdirRef.current = taskOutput.workdir;
238
+ const currentTask = tasks[currentTaskIndex];
239
+ if (currentTask.status !== ExecutionStatus.Pending) {
240
+ return;
237
241
  }
238
- // Update tasks with output before calling handler
239
- const tasksWithOutput = tasks.map((task, i) => i === index
240
- ? {
241
- ...task,
242
- stdout: taskOutput.stdout,
243
- stderr: taskOutput.stderr,
244
- error: taskOutput.error,
245
- }
246
- : task);
247
- const result = handleTaskFailure(index, error, elapsed, {
248
- tasks: tasksWithOutput,
249
- message,
250
- summary,
242
+ cancelledRef.current = false;
243
+ // Mark task as started (running)
244
+ dispatch({
245
+ type: ExecuteActionType.TaskStarted,
246
+ payload: { index: currentTaskIndex },
247
+ });
248
+ // Reset live state for new task
249
+ setLiveOutput({ stdout: '', stderr: '', error: '' });
250
+ setLiveElapsed(0);
251
+ setTaskStartTime(Date.now());
252
+ // Merge workdir into command
253
+ const command = workdirRef.current
254
+ ? { ...currentTask.command, workdir: workdirRef.current }
255
+ : currentTask.command;
256
+ void executeTask(command, currentTaskIndex, {
257
+ onOutputChange: (output) => {
258
+ if (!cancelledRef.current) {
259
+ setLiveOutput(output);
260
+ }
261
+ },
262
+ onComplete: (elapsed, output) => {
263
+ if (cancelledRef.current)
264
+ return;
265
+ setTaskStartTime(null);
266
+ // Track working directory
267
+ if (output.workdir) {
268
+ workdirRef.current = output.workdir;
269
+ }
270
+ const tasksWithOutput = tasks.map((task, i) => i === currentTaskIndex
271
+ ? {
272
+ ...task,
273
+ stdout: output.stdout,
274
+ stderr: output.stderr,
275
+ error: output.error,
276
+ }
277
+ : task);
278
+ const result = handleTaskCompletion(currentTaskIndex, elapsed, {
279
+ tasks: tasksWithOutput,
280
+ message,
281
+ summary,
282
+ });
283
+ dispatch(result.action);
284
+ requestHandlers.onCompleted(result.finalState);
285
+ if (result.shouldComplete) {
286
+ lifecycleHandlers.completeActive();
287
+ }
288
+ },
289
+ onError: (errorMsg, elapsed, output) => {
290
+ if (cancelledRef.current)
291
+ return;
292
+ setTaskStartTime(null);
293
+ // Track working directory
294
+ if (output.workdir) {
295
+ workdirRef.current = output.workdir;
296
+ }
297
+ const tasksWithOutput = tasks.map((task, i) => i === currentTaskIndex
298
+ ? {
299
+ ...task,
300
+ stdout: output.stdout,
301
+ stderr: output.stderr,
302
+ error: output.error,
303
+ }
304
+ : task);
305
+ const result = handleTaskFailure(currentTaskIndex, errorMsg, elapsed, {
306
+ tasks: tasksWithOutput,
307
+ message,
308
+ summary,
309
+ });
310
+ dispatch(result.action);
311
+ requestHandlers.onCompleted(result.finalState);
312
+ if (result.action.type === ExecuteActionType.TaskErrorCritical) {
313
+ const errorMessage = getExecutionErrorMessage(errorMsg);
314
+ workflowHandlers.addToQueue(createFeedback({ type: FeedbackType.Failed, message: errorMessage }));
315
+ }
316
+ if (result.shouldComplete) {
317
+ lifecycleHandlers.completeActive();
318
+ }
319
+ },
251
320
  });
252
- dispatch(result.action);
253
- requestHandlers.onCompleted(result.finalState);
254
- // Add error feedback to queue for critical failures
255
- if (result.action.type === ExecuteActionType.TaskErrorCritical) {
256
- const errorMessage = getExecutionErrorMessage(error);
257
- workflowHandlers.addToQueue(createFeedback(FeedbackType.Failed, errorMessage));
258
- }
259
- if (result.shouldComplete) {
260
- lifecycleHandlers.completeActive();
261
- }
262
321
  }, [
322
+ isActive,
263
323
  tasks,
324
+ currentTaskIndex,
264
325
  message,
265
326
  summary,
327
+ error,
266
328
  requestHandlers,
267
329
  lifecycleHandlers,
268
330
  workflowHandlers,
269
331
  ]);
270
- const handleTaskAbort = useCallback((index, taskOutput) => {
271
- // Task was aborted - execution already stopped by Escape handler
272
- // Update tasks with output before building state
273
- const tasksWithOutput = tasks.map((task, i) => i === index
274
- ? {
275
- ...task,
276
- stdout: taskOutput.stdout,
277
- stderr: taskOutput.stderr,
278
- error: taskOutput.error,
279
- }
280
- : task);
281
- const finalState = buildAbortedState(tasksWithOutput, message, summary, completed);
282
- requestHandlers.onCompleted(finalState);
283
- }, [tasks, message, summary, completed, requestHandlers]);
284
- // Controller always renders View with current state
285
- const viewState = createExecuteState({
286
- error,
287
- tasks,
288
- message,
289
- summary,
290
- completed,
291
- completionMessage,
332
+ // Build view data for each task
333
+ const taskViewData = tasks.map((task) => {
334
+ const isTaskActive = isActive && task.status === ExecutionStatus.Running;
335
+ const finished = isTaskFinished(task);
336
+ // Use live output for active task, stored output for finished tasks
337
+ const stdout = isTaskActive ? liveOutput.stdout : (task.stdout ?? '');
338
+ const stderr = isTaskActive ? liveOutput.stderr : (task.stderr ?? '');
339
+ // Use live elapsed for active running task
340
+ const elapsed = isTaskActive ? liveElapsed : task.elapsed;
341
+ // Merge workdir for active task
342
+ const command = isTaskActive && workdirRef.current
343
+ ? { ...task.command, workdir: workdirRef.current }
344
+ : task.command;
345
+ return {
346
+ label: task.label,
347
+ command,
348
+ status: task.status,
349
+ elapsed,
350
+ stdout,
351
+ stderr,
352
+ isActive: isTaskActive,
353
+ isFinished: finished,
354
+ };
292
355
  });
293
- return (_jsx(ExecuteView, { state: viewState, status: status, workdir: workdirRef.current, onOutputChange: handleOutputChange, onTaskComplete: handleTaskComplete, onTaskAbort: handleTaskAbort, onTaskError: handleTaskError }));
356
+ return (_jsx(ExecuteView, { isLoading: isLoading, isExecuting: isExecuting, isActive: isActive, error: error, message: message, tasks: taskViewData, completionMessage: completionMessage, showTasks: showTasks }));
294
357
  }
@@ -2,8 +2,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useState } from 'react';
3
3
  import { Box, Text } from 'ink';
4
4
  import { ComponentStatus, } from '../types/components.js';
5
+ import { Origin } from '../types/types.js';
5
6
  import { Colors, getTextColor } from '../services/colors.js';
6
- import { createReportDefinition } from '../services/components.js';
7
+ import { createReport } from '../services/components.js';
7
8
  import { DebugLevel } from '../configuration/types.js';
8
9
  import { useInput } from '../services/keyboard.js';
9
10
  import { formatErrorMessage } from '../services/messages.js';
@@ -51,24 +52,22 @@ export function Introspect({ tasks, status, service, children, debug = DebugLeve
51
52
  if (result.debug?.length) {
52
53
  workflowHandlers.addToTimeline(...result.debug);
53
54
  }
54
- // Capabilities come directly from result - no parsing needed
55
- let caps = result.capabilities;
56
- // Filter out internal capabilities when not in debug mode
57
- if (debug === DebugLevel.None) {
58
- caps = caps.filter((cap) => cap.name.toUpperCase() !== 'SCHEDULE' &&
59
- cap.name.toUpperCase() !== 'VALIDATE' &&
60
- cap.name.toUpperCase() !== 'REPORT');
61
- }
62
- setCapabilities(caps);
63
- setMessage(result.message);
55
+ // Destructure message from result
56
+ const { message } = result;
57
+ // Filter out meta workflow capabilities when not in debug mode
58
+ const capabilities = debug === DebugLevel.None
59
+ ? result.capabilities.filter((cap) => cap.origin !== Origin.Indirect)
60
+ : result.capabilities;
61
+ setCapabilities(capabilities);
62
+ setMessage(message);
64
63
  const finalState = {
65
64
  error: null,
66
- capabilities: caps,
67
- message: result.message,
65
+ capabilities,
66
+ message,
68
67
  };
69
68
  requestHandlers.onCompleted(finalState);
70
69
  // Add Report component to queue
71
- workflowHandlers.addToQueue(createReportDefinition(result.message, caps));
70
+ workflowHandlers.addToQueue(createReport({ message, capabilities }));
72
71
  // Signal completion
73
72
  lifecycleHandlers.completeActive();
74
73
  }
package/dist/ui/List.js CHANGED
@@ -2,9 +2,9 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
2
2
  import { Box, Text } from 'ink';
3
3
  import { Palette } from '../services/colors.js';
4
4
  import { Separator } from './Separator.js';
5
- export const List = ({ items, level = 0, highlightedIndex = null, highlightedParentIndex = null, showType = false, }) => {
5
+ export const List = ({ items, level = 0, highlightedIndex = null, highlightedParentIndex = null, showType = false, compact = false, }) => {
6
6
  const marginLeft = level > 0 ? 2 : 0;
7
- const gap = level === 0 ? 1 : 0;
7
+ const gap = level === 0 && !compact ? 1 : 0;
8
8
  return (_jsx(Box, { flexDirection: "column", marginLeft: marginLeft, gap: gap, children: items.map((item, index) => {
9
9
  // At level 0, track which parent is active for child highlighting
10
10
  // At level > 0, only highlight if this parent is the active one
package/dist/ui/Main.js CHANGED
@@ -5,9 +5,10 @@ import { FeedbackType } from '../types/types.js';
5
5
  import { loadConfig, loadDebugSetting, saveConfig, saveDebugSetting, } from '../configuration/io.js';
6
6
  import { getConfigurationRequiredMessage } from '../configuration/messages.js';
7
7
  import { getMissingConfigKeys } from '../configuration/schema.js';
8
+ import { createConfigStepsFromSchema } from '../configuration/steps.js';
8
9
  import { unflattenConfig } from '../configuration/transformation.js';
9
10
  import { createAnthropicService } from '../services/anthropic.js';
10
- import { createCommandDefinition, createConfigDefinitionWithKeys, createFeedback, createMessage, createWelcomeDefinition, } from '../services/components.js';
11
+ import { createCommand, createConfig, createFeedback, createMessage, createWelcome, } from '../services/components.js';
11
12
  import { registerGlobalShortcut } from '../services/keyboard.js';
12
13
  import { initializeLogger, setDebugLevel } from '../services/logger.js';
13
14
  import { Workflow } from './Workflow.js';
@@ -56,7 +57,9 @@ export const Main = ({ app, command, serviceFactory = createAnthropicService, })
56
57
  const errorMessage = error instanceof Error
57
58
  ? error.message
58
59
  : 'Failed to initialize service';
59
- setInitialQueue([createFeedback(FeedbackType.Failed, errorMessage)]);
60
+ setInitialQueue([
61
+ createFeedback({ type: FeedbackType.Failed, message: errorMessage }),
62
+ ]);
60
63
  }
61
64
  }
62
65
  // If config is missing, service will be created after config completes
@@ -94,18 +97,22 @@ export const Main = ({ app, command, serviceFactory = createAnthropicService, })
94
97
  // Config was cancelled
95
98
  };
96
99
  setInitialQueue([
97
- createWelcomeDefinition(app),
98
- createMessage(getConfigurationRequiredMessage()),
99
- createConfigDefinitionWithKeys(missingKeys, handleConfigFinished, handleConfigAborted),
100
+ createWelcome({ app }),
101
+ createMessage({ text: getConfigurationRequiredMessage() }),
102
+ createConfig({
103
+ steps: createConfigStepsFromSchema(missingKeys),
104
+ onFinished: handleConfigFinished,
105
+ onAborted: handleConfigAborted,
106
+ }),
100
107
  ]);
101
108
  }
102
109
  else if (service && command) {
103
110
  // Valid service exists and command provided - execute command
104
- setInitialQueue([createCommandDefinition(command, service)]);
111
+ setInitialQueue([createCommand({ command, service })]);
105
112
  }
106
113
  else if (service && !command) {
107
114
  // Valid service exists, no command - show welcome
108
- setInitialQueue([createWelcomeDefinition(app)]);
115
+ setInitialQueue([createWelcome({ app })]);
109
116
  }
110
117
  // Wait for service to be initialized before setting queue
111
118
  }, [app, command, service, initialQueue]);
@@ -79,6 +79,8 @@ export function taskToListItem(task, highlightedChildIndex = null, isDefineTaskW
79
79
  export const ScheduleView = ({ message, tasks, state, status, debug = DebugLevel.None, }) => {
80
80
  const isActive = status === ComponentStatus.Active;
81
81
  const { highlightedIndex, currentDefineGroupIndex, completedSelections } = state;
82
+ // Use compact mode when all tasks are Config type
83
+ const isCompact = tasks.every((task) => task.type === TaskType.Config);
82
84
  // Find all Define tasks
83
85
  const defineTaskIndices = tasks
84
86
  .map((t, idx) => (t.type === TaskType.Define ? idx : -1))
@@ -114,7 +116,7 @@ export const ScheduleView = ({ message, tasks, state, status, debug = DebugLevel
114
116
  isActive;
115
117
  return taskToListItem(task, childIndex, isDefineWithoutSelection, status, debug);
116
118
  });
117
- return (_jsxs(Box, { flexDirection: "column", children: [message && (_jsx(Box, { marginBottom: 1, marginLeft: 1, children: _jsx(Label, { description: message, taskType: TaskType.Schedule, showType: debug !== DebugLevel.None, status: status, debug: debug }) })), _jsx(Box, { marginLeft: 1, children: _jsx(List, { items: listItems, highlightedIndex: currentDefineTaskIndex >= 0 ? highlightedIndex : null, highlightedParentIndex: currentDefineTaskIndex, showType: debug !== DebugLevel.None }) })] }));
119
+ return (_jsxs(Box, { flexDirection: "column", children: [message && (_jsx(Box, { marginBottom: 1, marginLeft: 1, children: _jsx(Label, { description: message, taskType: TaskType.Schedule, showType: debug !== DebugLevel.None, status: status, debug: debug }) })), _jsx(Box, { marginLeft: 1, children: _jsx(List, { items: listItems, highlightedIndex: currentDefineTaskIndex >= 0 ? highlightedIndex : null, highlightedParentIndex: currentDefineTaskIndex, showType: debug !== DebugLevel.None, compact: isCompact }) })] }));
118
120
  };
119
121
  /**
120
122
  * Schedule controller: Manages task selection and navigation
@@ -4,7 +4,11 @@ import { getStatusColors, Palette, STATUS_ICONS } from '../services/colors.js';
4
4
  import { ExecutionStatus } from '../services/shell.js';
5
5
  import { formatDuration } from '../services/utils.js';
6
6
  import { Spinner } from './Spinner.js';
7
- export function Subtask({ label, command, status, isActive: _isActive, startTime, endTime, elapsed, }) {
7
+ /**
8
+ * Pure display component for a single subtask.
9
+ * Shows label, command, status icon, and elapsed time.
10
+ */
11
+ export function SubtaskView({ label, command, status, elapsed, }) {
8
12
  const colors = getStatusColors(status);
9
13
  const isCancelled = status === ExecutionStatus.Cancelled;
10
14
  const isAborted = status === ExecutionStatus.Aborted;
@@ -12,11 +16,10 @@ export function Subtask({ label, command, status, isActive: _isActive, startTime
12
16
  const isFinished = status === ExecutionStatus.Success ||
13
17
  status === ExecutionStatus.Failed ||
14
18
  status === ExecutionStatus.Aborted;
15
- const elapsedTime = elapsed ?? (startTime && endTime ? endTime - startTime : undefined);
16
19
  // Apply strikethrough for cancelled and aborted tasks
17
20
  const formatText = (text) => shouldStrikethrough ? text.split('').join('\u0336') + '\u0336' : text;
18
21
  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
22
  ? formatText(label || command.description)
20
23
  : 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, {})] })] })] }));
24
+ elapsed !== undefined && (_jsxs(Text, { color: Palette.DarkGray, children: ["(", formatDuration(elapsed), ")"] }))] }), _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
25
  }