prompt-language-shell 0.4.8 → 0.5.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.
Files changed (41) hide show
  1. package/dist/config/EXECUTE.md +279 -0
  2. package/dist/config/INTROSPECT.md +9 -6
  3. package/dist/config/PLAN.md +57 -6
  4. package/dist/config/VALIDATE.md +139 -0
  5. package/dist/handlers/answer.js +13 -20
  6. package/dist/handlers/command.js +26 -30
  7. package/dist/handlers/config.js +32 -24
  8. package/dist/handlers/execute.js +46 -0
  9. package/dist/handlers/execution.js +133 -81
  10. package/dist/handlers/introspect.js +13 -20
  11. package/dist/handlers/plan.js +31 -34
  12. package/dist/services/anthropic.js +28 -2
  13. package/dist/services/colors.js +3 -3
  14. package/dist/services/components.js +50 -1
  15. package/dist/services/config-loader.js +67 -0
  16. package/dist/services/execution-validator.js +110 -0
  17. package/dist/services/messages.js +1 -0
  18. package/dist/services/placeholder-resolver.js +120 -0
  19. package/dist/services/shell.js +118 -0
  20. package/dist/services/skill-expander.js +91 -0
  21. package/dist/services/skill-parser.js +169 -0
  22. package/dist/services/skills.js +26 -0
  23. package/dist/services/timing.js +38 -0
  24. package/dist/services/tool-registry.js +10 -0
  25. package/dist/services/utils.js +21 -0
  26. package/dist/tools/execute.tool.js +44 -0
  27. package/dist/tools/validate.tool.js +43 -0
  28. package/dist/types/handlers.js +1 -0
  29. package/dist/types/skills.js +4 -0
  30. package/dist/types/types.js +2 -0
  31. package/dist/ui/Answer.js +3 -9
  32. package/dist/ui/Command.js +3 -6
  33. package/dist/ui/Component.js +13 -1
  34. package/dist/ui/Config.js +2 -2
  35. package/dist/ui/Confirm.js +2 -2
  36. package/dist/ui/Execute.js +262 -0
  37. package/dist/ui/Introspect.js +5 -7
  38. package/dist/ui/Main.js +30 -69
  39. package/dist/ui/Spinner.js +10 -5
  40. package/dist/ui/Validate.js +120 -0
  41. package/package.json +7 -7
@@ -1,6 +1,7 @@
1
1
  import { existsSync, readdirSync, readFileSync } from 'fs';
2
2
  import { homedir } from 'os';
3
3
  import { join } from 'path';
4
+ import { parseSkillMarkdown } from './skill-parser.js';
4
5
  /**
5
6
  * Get the path to the skills directory
6
7
  */
@@ -32,6 +33,31 @@ export function loadSkills() {
32
33
  return [];
33
34
  }
34
35
  }
36
+ /**
37
+ * Load and parse all skill definitions
38
+ * Returns structured skill definitions
39
+ */
40
+ export function loadSkillDefinitions() {
41
+ const skillContents = loadSkills();
42
+ const definitions = [];
43
+ for (const content of skillContents) {
44
+ const parsed = parseSkillMarkdown(content);
45
+ if (parsed) {
46
+ definitions.push(parsed);
47
+ }
48
+ }
49
+ return definitions;
50
+ }
51
+ /**
52
+ * Create skill lookup function from definitions
53
+ */
54
+ export function createSkillLookup(definitions) {
55
+ const map = new Map();
56
+ for (const definition of definitions) {
57
+ map.set(definition.name, definition);
58
+ }
59
+ return (name) => map.get(name) || null;
60
+ }
35
61
  /**
36
62
  * Format skills for inclusion in the planning prompt
37
63
  */
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Timing utilities for UI components
3
+ */
4
+ /**
5
+ * Waits for at least the minimum processing time.
6
+ * Ensures async operations don't complete too quickly for good UX.
7
+ *
8
+ * @param startTime - The timestamp when the operation started
9
+ * @param minimumTime - The minimum total time the operation should take
10
+ */
11
+ export async function ensureMinimumTime(startTime, minimumTime) {
12
+ const elapsed = Date.now() - startTime;
13
+ const remainingTime = Math.max(0, minimumTime - elapsed);
14
+ if (remainingTime > 0) {
15
+ await new Promise((resolve) => setTimeout(resolve, remainingTime));
16
+ }
17
+ }
18
+ /**
19
+ * Wraps an async operation with minimum processing time UX polish.
20
+ * Ensures successful operations take at least `minimumTime` milliseconds.
21
+ * Errors are thrown immediately without delay for better UX.
22
+ *
23
+ * @param operation - The async operation to perform
24
+ * @param minimumTime - Minimum time in milliseconds for UX polish on success
25
+ * @returns The result of the operation
26
+ */
27
+ export async function withMinimumTime(operation, minimumTime) {
28
+ const startTime = Date.now();
29
+ try {
30
+ const result = await operation();
31
+ await ensureMinimumTime(startTime, minimumTime);
32
+ return result;
33
+ }
34
+ catch (error) {
35
+ // Don't wait on error - fail fast for better UX
36
+ throw error;
37
+ }
38
+ }
@@ -35,8 +35,10 @@ export const toolRegistry = new ToolRegistry();
35
35
  // Register built-in tools
36
36
  import { answerTool } from '../tools/answer.tool.js';
37
37
  import { configTool } from '../tools/config.tool.js';
38
+ import { executeTool } from '../tools/execute.tool.js';
38
39
  import { introspectTool } from '../tools/introspect.tool.js';
39
40
  import { planTool } from '../tools/plan.tool.js';
41
+ import { validateTool } from '../tools/validate.tool.js';
40
42
  toolRegistry.register('plan', {
41
43
  schema: planTool,
42
44
  instructionsPath: 'config/PLAN.md',
@@ -53,3 +55,11 @@ toolRegistry.register('config', {
53
55
  schema: configTool,
54
56
  instructionsPath: 'config/CONFIG.md',
55
57
  });
58
+ toolRegistry.register('execute', {
59
+ schema: executeTool,
60
+ instructionsPath: 'config/EXECUTE.md',
61
+ });
62
+ toolRegistry.register('validate', {
63
+ schema: validateTool,
64
+ instructionsPath: 'config/VALIDATE.md',
65
+ });
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Formats a duration in milliseconds to a human-readable string.
3
+ * Uses correct singular/plural forms.
4
+ */
5
+ export function formatDuration(ms) {
6
+ const totalSeconds = Math.floor(ms / 1000);
7
+ const hours = Math.floor(totalSeconds / 3600);
8
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
9
+ const seconds = totalSeconds % 60;
10
+ const parts = [];
11
+ if (hours > 0) {
12
+ parts.push(`${String(hours)} ${hours === 1 ? 'hour' : 'hours'}`);
13
+ }
14
+ if (minutes > 0) {
15
+ parts.push(`${String(minutes)} ${minutes === 1 ? 'minute' : 'minutes'}`);
16
+ }
17
+ if (seconds > 0 || parts.length === 0) {
18
+ parts.push(`${String(seconds)} ${seconds === 1 ? 'second' : 'seconds'}`);
19
+ }
20
+ return parts.join(' ');
21
+ }
@@ -0,0 +1,44 @@
1
+ export const executeTool = {
2
+ name: 'execute',
3
+ description: 'Execute shell commands from planned tasks. Translates task descriptions into specific shell commands that can be run in the terminal. Called after PLAN has created execute tasks and user has confirmed.',
4
+ input_schema: {
5
+ type: 'object',
6
+ properties: {
7
+ message: {
8
+ type: 'string',
9
+ description: 'Brief status message about the execution. Must be a single sentence, maximum 64 characters, ending with a period.',
10
+ },
11
+ commands: {
12
+ type: 'array',
13
+ description: 'Array of commands to execute sequentially',
14
+ items: {
15
+ type: 'object',
16
+ properties: {
17
+ description: {
18
+ type: 'string',
19
+ description: 'Brief description of what this command does. Maximum 64 characters.',
20
+ },
21
+ command: {
22
+ type: 'string',
23
+ description: 'The exact shell command to run. Must be a valid shell command.',
24
+ },
25
+ workdir: {
26
+ type: 'string',
27
+ description: 'Optional working directory for the command. Defaults to current directory if not specified.',
28
+ },
29
+ timeout: {
30
+ type: 'number',
31
+ description: 'Optional timeout in milliseconds. Defaults to 30000 (30 seconds).',
32
+ },
33
+ critical: {
34
+ type: 'boolean',
35
+ description: 'Whether failure should stop execution of subsequent commands. Defaults to true.',
36
+ },
37
+ },
38
+ required: ['description', 'command'],
39
+ },
40
+ },
41
+ },
42
+ required: ['message', 'commands'],
43
+ },
44
+ };
@@ -0,0 +1,43 @@
1
+ export const validateTool = {
2
+ name: 'validate',
3
+ description: 'Validate skill requirements and generate natural language descriptions for missing configuration values. Given skill context and missing config paths, create CONFIG tasks with helpful, contextual descriptions.',
4
+ input_schema: {
5
+ type: 'object',
6
+ properties: {
7
+ message: {
8
+ type: 'string',
9
+ description: 'Empty string or brief message (not shown to user, can be left empty)',
10
+ },
11
+ tasks: {
12
+ type: 'array',
13
+ description: 'Array of CONFIG tasks with natural language descriptions for missing config values',
14
+ items: {
15
+ type: 'object',
16
+ properties: {
17
+ action: {
18
+ type: 'string',
19
+ description: 'Natural language description explaining what the config value is for, followed by the config path in curly brackets {config.path}. Example: "Path to Alpha project repository (legacy implementation) {project.alpha.repo}"',
20
+ },
21
+ type: {
22
+ type: 'string',
23
+ description: 'Must be "config" for all tasks returned by this tool',
24
+ },
25
+ params: {
26
+ type: 'object',
27
+ description: 'Must include key field with the config path',
28
+ properties: {
29
+ key: {
30
+ type: 'string',
31
+ description: 'The config path (e.g., "opera.gx.repo")',
32
+ },
33
+ },
34
+ required: ['key'],
35
+ },
36
+ },
37
+ required: ['action', 'type', 'params'],
38
+ },
39
+ },
40
+ },
41
+ required: ['message', 'tasks'],
42
+ },
43
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Type definitions for the structured skill system
3
+ */
4
+ export {};
@@ -12,6 +12,8 @@ export var ComponentName;
12
12
  ComponentName["Report"] = "report";
13
13
  ComponentName["Answer"] = "answer";
14
14
  ComponentName["AnswerDisplay"] = "answerDisplay";
15
+ ComponentName["Execute"] = "execute";
16
+ ComponentName["Validate"] = "validate";
15
17
  })(ComponentName || (ComponentName = {}));
16
18
  export var TaskType;
17
19
  (function (TaskType) {
package/dist/ui/Answer.js CHANGED
@@ -4,6 +4,7 @@ import { Box, Text } from 'ink';
4
4
  import { Colors, getTextColor } from '../services/colors.js';
5
5
  import { useInput } from '../services/keyboard.js';
6
6
  import { formatErrorMessage } from '../services/messages.js';
7
+ import { withMinimumTime } from '../services/timing.js';
7
8
  import { Spinner } from './Spinner.js';
8
9
  const MINIMUM_PROCESSING_TIME = 400;
9
10
  export function Answer({ question, state, service, onError, onComplete, onAborted, }) {
@@ -30,13 +31,9 @@ export function Answer({ question, state, service, onError, onComplete, onAborte
30
31
  }
31
32
  let mounted = true;
32
33
  async function process(svc) {
33
- const startTime = Date.now();
34
34
  try {
35
- // Call answer tool
36
- const result = await svc.processWithTool(question, 'answer');
37
- const elapsed = Date.now() - startTime;
38
- const remainingTime = Math.max(0, MINIMUM_PROCESSING_TIME - elapsed);
39
- await new Promise((resolve) => setTimeout(resolve, remainingTime));
35
+ // Call answer tool with minimum processing time for UX polish
36
+ const result = await withMinimumTime(() => svc.processWithTool(question, 'answer'), MINIMUM_PROCESSING_TIME);
40
37
  if (mounted) {
41
38
  // Extract answer from result
42
39
  const answer = result.answer || '';
@@ -45,9 +42,6 @@ export function Answer({ question, state, service, onError, onComplete, onAborte
45
42
  }
46
43
  }
47
44
  catch (err) {
48
- const elapsed = Date.now() - startTime;
49
- const remainingTime = Math.max(0, MINIMUM_PROCESSING_TIME - elapsed);
50
- await new Promise((resolve) => setTimeout(resolve, remainingTime));
51
45
  if (mounted) {
52
46
  const errorMessage = formatErrorMessage(err);
53
47
  setIsLoading(false);
@@ -5,6 +5,7 @@ import { TaskType } from '../types/types.js';
5
5
  import { Colors } from '../services/colors.js';
6
6
  import { useInput } from '../services/keyboard.js';
7
7
  import { formatErrorMessage } from '../services/messages.js';
8
+ import { ensureMinimumTime } from '../services/timing.js';
8
9
  import { Spinner } from './Spinner.js';
9
10
  const MIN_PROCESSING_TIME = 1000; // purely for visual effect
10
11
  export function Command({ command, state, service, children, onError, onComplete, onAborted, }) {
@@ -42,18 +43,14 @@ export function Command({ command, state, service, children, onError, onComplete
42
43
  // Call CONFIG tool to get specific config keys
43
44
  result = await svc.processWithTool(query, 'config');
44
45
  }
45
- const elapsed = Date.now() - startTime;
46
- const remainingTime = Math.max(0, MIN_PROCESSING_TIME - elapsed);
47
- await new Promise((resolve) => setTimeout(resolve, remainingTime));
46
+ await ensureMinimumTime(startTime, MIN_PROCESSING_TIME);
48
47
  if (mounted) {
49
48
  setIsLoading(false);
50
49
  onComplete?.(result.message, result.tasks);
51
50
  }
52
51
  }
53
52
  catch (err) {
54
- const elapsed = Date.now() - startTime;
55
- const remainingTime = Math.max(0, MIN_PROCESSING_TIME - elapsed);
56
- await new Promise((resolve) => setTimeout(resolve, remainingTime));
53
+ await ensureMinimumTime(startTime, MIN_PROCESSING_TIME);
57
54
  if (mounted) {
58
55
  const errorMessage = formatErrorMessage(err);
59
56
  setIsLoading(false);
@@ -6,12 +6,14 @@ import { AnswerDisplay } from './AnswerDisplay.js';
6
6
  import { Command } from './Command.js';
7
7
  import { Confirm } from './Confirm.js';
8
8
  import { Config } from './Config.js';
9
+ import { Execute } from './Execute.js';
9
10
  import { Feedback } from './Feedback.js';
10
11
  import { Introspect } from './Introspect.js';
11
12
  import { Message } from './Message.js';
12
13
  import { Plan } from './Plan.js';
13
14
  import { Refinement } from './Refinement.js';
14
15
  import { Report } from './Report.js';
16
+ import { Validate } from './Validate.js';
15
17
  import { Welcome } from './Welcome.js';
16
18
  export const Component = React.memo(function Component({ def, debug, }) {
17
19
  switch (def.name) {
@@ -20,7 +22,7 @@ export const Component = React.memo(function Component({ def, debug, }) {
20
22
  case ComponentName.Config: {
21
23
  const props = def.props;
22
24
  const state = def.state;
23
- return _jsx(Config, { ...props, state: state });
25
+ return _jsx(Config, { ...props, state: state, debug: debug });
24
26
  }
25
27
  case ComponentName.Command: {
26
28
  const props = def.props;
@@ -60,5 +62,15 @@ export const Component = React.memo(function Component({ def, debug, }) {
60
62
  }
61
63
  case ComponentName.AnswerDisplay:
62
64
  return _jsx(AnswerDisplay, { ...def.props });
65
+ case ComponentName.Execute: {
66
+ const props = def.props;
67
+ const state = def.state;
68
+ return _jsx(Execute, { ...props, state: state });
69
+ }
70
+ case ComponentName.Validate: {
71
+ const props = def.props;
72
+ const state = def.state;
73
+ return _jsx(Validate, { ...props, state: state });
74
+ }
63
75
  }
64
76
  });
package/dist/ui/Config.js CHANGED
@@ -57,7 +57,7 @@ function SelectionStep({ options, selectedIndex, isCurrentStep, }) {
57
57
  return (_jsx(Box, { marginRight: 2, children: _jsx(Text, { dimColor: !isSelected || !isCurrentStep, bold: isSelected, children: option.label }) }, option.value));
58
58
  }) }));
59
59
  }
60
- export function Config({ steps, state, onFinished, onAborted }) {
60
+ export function Config({ steps, state, debug, onFinished, onAborted }) {
61
61
  const done = state?.done ?? false;
62
62
  const [step, setStep] = React.useState(done ? steps.length : 0);
63
63
  const [values, setValues] = React.useState(() => {
@@ -220,6 +220,6 @@ export function Config({ steps, state, onFinished, onAborted }) {
220
220
  if (!shouldShow) {
221
221
  return null;
222
222
  }
223
- return (_jsxs(Box, { flexDirection: "column", marginTop: index === 0 ? 0 : 1, children: [_jsx(Box, { children: _jsxs(Text, { children: [stepConfig.description, ":"] }) }), _jsxs(Box, { children: [_jsx(Text, { children: " " }), _jsx(Text, { color: Colors.Action.Select, dimColor: !isCurrentStep, children: ">" }), _jsx(Text, { children: " " }), renderStepInput(stepConfig, isCurrentStep)] })] }, stepConfig.key));
223
+ return (_jsxs(Box, { flexDirection: "column", marginTop: index === 0 ? 0 : 1, children: [_jsxs(Box, { children: [_jsx(Text, { children: stepConfig.description }), _jsx(Text, { children: ": " }), debug && stepConfig.path && (_jsxs(Text, { color: Colors.Type.Define, children: ['{', stepConfig.path, '}'] }))] }), _jsxs(Box, { children: [_jsx(Text, { children: " " }), _jsx(Text, { color: Colors.Action.Select, dimColor: !isCurrentStep, children: ">" }), _jsx(Text, { children: " " }), renderStepInput(stepConfig, isCurrentStep)] })] }, stepConfig.key));
224
224
  }) }));
225
225
  }
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import React from 'react';
3
3
  import { Box, Text } from 'ink';
4
- import { Colors } from '../services/colors.js';
4
+ import { Colors, Palette } from '../services/colors.js';
5
5
  import { useInput } from '../services/keyboard.js';
6
6
  export function Confirm({ message, state, onConfirmed, onCancelled, }) {
7
7
  const done = state?.done ?? false;
@@ -30,7 +30,7 @@ export function Confirm({ message, state, onConfirmed, onCancelled, }) {
30
30
  }
31
31
  }, { isActive: !done });
32
32
  const options = [
33
- { label: 'yes', value: 'yes', color: Colors.Action.Execute },
33
+ { label: 'yes', value: 'yes', color: Palette.BrightGreen },
34
34
  { label: 'no', value: 'no', color: Colors.Status.Error },
35
35
  ];
36
36
  if (done) {
@@ -0,0 +1,262 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useState } from 'react';
3
+ import { Box, Text } from 'ink';
4
+ import { Colors, getTextColor, Palette } from '../services/colors.js';
5
+ import { useInput } from '../services/keyboard.js';
6
+ import { formatErrorMessage } from '../services/messages.js';
7
+ import { formatDuration } from '../services/utils.js';
8
+ import { ExecutionStatus, executeCommands, } from '../services/shell.js';
9
+ import { replacePlaceholders } from '../services/placeholder-resolver.js';
10
+ import { loadUserConfig } from '../services/config-loader.js';
11
+ import { ensureMinimumTime } from '../services/timing.js';
12
+ import { Spinner } from './Spinner.js';
13
+ const MINIMUM_PROCESSING_TIME = 400;
14
+ const STATUS_ICONS = {
15
+ [ExecutionStatus.Pending]: '- ',
16
+ [ExecutionStatus.Running]: '• ',
17
+ [ExecutionStatus.Success]: '✓ ',
18
+ [ExecutionStatus.Failed]: '✗ ',
19
+ [ExecutionStatus.Aborted]: '⊘ ',
20
+ };
21
+ function calculateTotalElapsed(commandStatuses) {
22
+ return commandStatuses.reduce((sum, cmd) => {
23
+ if (cmd.elapsed !== undefined) {
24
+ return sum + cmd.elapsed;
25
+ }
26
+ if (cmd.startTime) {
27
+ const elapsed = cmd.endTime
28
+ ? cmd.endTime - cmd.startTime
29
+ : Date.now() - cmd.startTime;
30
+ return sum + elapsed;
31
+ }
32
+ return sum;
33
+ }, 0);
34
+ }
35
+ function getStatusColors(status) {
36
+ switch (status) {
37
+ case ExecutionStatus.Pending:
38
+ return {
39
+ icon: Palette.Gray,
40
+ description: Palette.Gray,
41
+ command: Palette.DarkGray,
42
+ symbol: Palette.DarkGray,
43
+ };
44
+ case ExecutionStatus.Running:
45
+ return {
46
+ icon: Palette.Gray,
47
+ description: getTextColor(true),
48
+ command: Palette.LightGreen,
49
+ symbol: Palette.AshGray,
50
+ };
51
+ case ExecutionStatus.Success:
52
+ return {
53
+ icon: Colors.Status.Success,
54
+ description: getTextColor(true),
55
+ command: Palette.Gray,
56
+ symbol: Palette.Gray,
57
+ };
58
+ case ExecutionStatus.Failed:
59
+ return {
60
+ icon: Colors.Status.Error,
61
+ description: Colors.Status.Error,
62
+ command: Colors.Status.Error,
63
+ symbol: Palette.Gray,
64
+ };
65
+ case ExecutionStatus.Aborted:
66
+ return {
67
+ icon: Palette.DarkOrange,
68
+ description: getTextColor(true),
69
+ command: Palette.DarkOrange,
70
+ symbol: Palette.Gray,
71
+ };
72
+ }
73
+ }
74
+ function CommandStatusDisplay({ item, elapsed }) {
75
+ const colors = getStatusColors(item.status);
76
+ const getElapsedTime = () => {
77
+ if (item.status === ExecutionStatus.Running && elapsed !== undefined) {
78
+ return elapsed;
79
+ }
80
+ else if (item.startTime && item.endTime) {
81
+ return item.endTime - item.startTime;
82
+ }
83
+ return undefined;
84
+ };
85
+ const elapsedTime = getElapsedTime();
86
+ 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, {})] }))] })] }));
87
+ }
88
+ export function Execute({ tasks, state, service, onError, onComplete, onAborted, }) {
89
+ const done = state?.done ?? false;
90
+ const isCurrent = done === false;
91
+ const [error, setError] = useState(null);
92
+ const [isLoading, setIsLoading] = useState(state?.isLoading ?? !done);
93
+ const [isExecuting, setIsExecuting] = useState(false);
94
+ const [commandStatuses, setCommandStatuses] = useState([]);
95
+ const [message, setMessage] = useState('');
96
+ const [currentElapsed, setCurrentElapsed] = useState(0);
97
+ const [runningIndex, setRunningIndex] = useState(null);
98
+ const [outputs, setOutputs] = useState([]);
99
+ useInput((input, key) => {
100
+ if (key.escape && (isLoading || isExecuting) && !done) {
101
+ setIsLoading(false);
102
+ setIsExecuting(false);
103
+ setRunningIndex(null);
104
+ // Mark any running command as aborted when cancelled
105
+ const now = Date.now();
106
+ setCommandStatuses((prev) => prev.map((item) => {
107
+ if (item.status === ExecutionStatus.Running) {
108
+ const elapsed = item.startTime
109
+ ? Math.floor((now - item.startTime) / 1000) * 1000
110
+ : undefined;
111
+ return {
112
+ ...item,
113
+ status: ExecutionStatus.Aborted,
114
+ endTime: now,
115
+ elapsed,
116
+ };
117
+ }
118
+ return item;
119
+ }));
120
+ onAborted(calculateTotalElapsed(commandStatuses));
121
+ }
122
+ }, { isActive: (isLoading || isExecuting) && !done });
123
+ // Update elapsed time for running command
124
+ useEffect(() => {
125
+ if (runningIndex === null)
126
+ return;
127
+ const item = commandStatuses[runningIndex];
128
+ if (!item?.startTime)
129
+ return;
130
+ const interval = setInterval(() => {
131
+ setCurrentElapsed((prev) => {
132
+ const next = Date.now() - item.startTime;
133
+ return next !== prev ? next : prev;
134
+ });
135
+ }, 1000);
136
+ return () => clearInterval(interval);
137
+ }, [runningIndex, commandStatuses]);
138
+ // Handle completion callback when execution finishes
139
+ useEffect(() => {
140
+ if (isExecuting || commandStatuses.length === 0 || !outputs.length)
141
+ return;
142
+ onComplete?.(outputs, calculateTotalElapsed(commandStatuses));
143
+ }, [isExecuting, commandStatuses, outputs, onComplete]);
144
+ useEffect(() => {
145
+ if (done) {
146
+ return;
147
+ }
148
+ if (!service) {
149
+ setError('No service available');
150
+ setIsLoading(false);
151
+ return;
152
+ }
153
+ let mounted = true;
154
+ async function process(svc) {
155
+ const startTime = Date.now();
156
+ try {
157
+ // Load user config for placeholder resolution
158
+ const userConfig = loadUserConfig();
159
+ // Format tasks for the execute tool and resolve placeholders
160
+ const taskDescriptions = tasks
161
+ .map((task) => {
162
+ // Resolve placeholders in task action
163
+ const resolvedAction = replacePlaceholders(task.action, userConfig);
164
+ const params = task.params
165
+ ? ` (params: ${JSON.stringify(task.params)})`
166
+ : '';
167
+ return `- ${resolvedAction}${params}`;
168
+ })
169
+ .join('\n');
170
+ // Call execute tool to get commands
171
+ const result = await svc.processWithTool(taskDescriptions, 'execute');
172
+ await ensureMinimumTime(startTime, MINIMUM_PROCESSING_TIME);
173
+ if (!mounted)
174
+ return;
175
+ if (!result.commands || result.commands.length === 0) {
176
+ setIsLoading(false);
177
+ setOutputs([]);
178
+ onComplete?.([], 0);
179
+ return;
180
+ }
181
+ // Resolve placeholders in command strings before execution
182
+ const resolvedCommands = result.commands.map((cmd) => ({
183
+ ...cmd,
184
+ command: replacePlaceholders(cmd.command, userConfig),
185
+ }));
186
+ // Set message and initialize command statuses
187
+ setMessage(result.message);
188
+ setCommandStatuses(resolvedCommands.map((cmd, index) => ({
189
+ command: cmd,
190
+ status: ExecutionStatus.Pending,
191
+ label: tasks[index]?.action,
192
+ })));
193
+ setIsLoading(false);
194
+ setIsExecuting(true);
195
+ // Execute commands sequentially
196
+ const outputs = await executeCommands(resolvedCommands, (progress) => {
197
+ if (!mounted)
198
+ return;
199
+ const now = Date.now();
200
+ setCommandStatuses((prev) => prev.map((item, idx) => {
201
+ if (idx === progress.currentIndex) {
202
+ const isStarting = progress.status === ExecutionStatus.Running &&
203
+ !item.startTime;
204
+ const isEnding = progress.status !== ExecutionStatus.Running &&
205
+ progress.status !== ExecutionStatus.Pending;
206
+ const endTime = isEnding ? now : item.endTime;
207
+ const elapsed = isEnding && item.startTime
208
+ ? Math.floor((now - item.startTime) / 1000) * 1000
209
+ : item.elapsed;
210
+ return {
211
+ ...item,
212
+ status: progress.status,
213
+ output: progress.output,
214
+ startTime: isStarting ? now : item.startTime,
215
+ endTime,
216
+ elapsed,
217
+ };
218
+ }
219
+ return item;
220
+ }));
221
+ if (progress.status === ExecutionStatus.Running) {
222
+ setRunningIndex((prev) => prev !== progress.currentIndex ? progress.currentIndex : prev);
223
+ setCurrentElapsed((prev) => (prev !== 0 ? 0 : prev));
224
+ }
225
+ else if (progress.status === ExecutionStatus.Success ||
226
+ progress.status === ExecutionStatus.Failed) {
227
+ setRunningIndex((prev) => (prev !== null ? null : prev));
228
+ }
229
+ });
230
+ if (mounted) {
231
+ setOutputs(outputs);
232
+ setIsExecuting(false);
233
+ }
234
+ }
235
+ catch (err) {
236
+ await ensureMinimumTime(startTime, MINIMUM_PROCESSING_TIME);
237
+ if (mounted) {
238
+ const errorMessage = formatErrorMessage(err);
239
+ setIsLoading(false);
240
+ setIsExecuting(false);
241
+ if (onError) {
242
+ onError(errorMessage);
243
+ }
244
+ else {
245
+ setError(errorMessage);
246
+ }
247
+ }
248
+ }
249
+ }
250
+ process(service);
251
+ return () => {
252
+ mounted = false;
253
+ };
254
+ }, [tasks, done, service, onComplete, onError]);
255
+ // Return null only when loading completes with no commands
256
+ if (done && commandStatuses.length === 0 && !error) {
257
+ return null;
258
+ }
259
+ // Show completed steps when done
260
+ const showCompletedSteps = done && commandStatuses.length > 0;
261
+ return (_jsxs(Box, { alignSelf: "flex-start", flexDirection: "column", children: [isLoading && (_jsxs(Box, { children: [_jsx(Text, { color: getTextColor(isCurrent), children: "Preparing commands. " }), _jsx(Spinner, {})] })), (isExecuting || showCompletedSteps) && (_jsxs(Box, { flexDirection: "column", children: [message && (_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { color: getTextColor(isCurrent), 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] }) }))] }));
262
+ }