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.
- package/dist/config/EXECUTE.md +279 -0
- package/dist/config/INTROSPECT.md +9 -6
- package/dist/config/PLAN.md +57 -6
- package/dist/config/VALIDATE.md +139 -0
- package/dist/handlers/answer.js +13 -20
- package/dist/handlers/command.js +26 -30
- package/dist/handlers/config.js +32 -24
- package/dist/handlers/execute.js +46 -0
- package/dist/handlers/execution.js +133 -81
- package/dist/handlers/introspect.js +13 -20
- package/dist/handlers/plan.js +31 -34
- package/dist/services/anthropic.js +28 -2
- package/dist/services/colors.js +3 -3
- package/dist/services/components.js +50 -1
- package/dist/services/config-loader.js +67 -0
- package/dist/services/execution-validator.js +110 -0
- package/dist/services/messages.js +1 -0
- package/dist/services/placeholder-resolver.js +120 -0
- package/dist/services/shell.js +118 -0
- package/dist/services/skill-expander.js +91 -0
- package/dist/services/skill-parser.js +169 -0
- package/dist/services/skills.js +26 -0
- package/dist/services/timing.js +38 -0
- package/dist/services/tool-registry.js +10 -0
- package/dist/services/utils.js +21 -0
- package/dist/tools/execute.tool.js +44 -0
- package/dist/tools/validate.tool.js +43 -0
- package/dist/types/handlers.js +1 -0
- package/dist/types/skills.js +4 -0
- package/dist/types/types.js +2 -0
- package/dist/ui/Answer.js +3 -9
- package/dist/ui/Command.js +3 -6
- package/dist/ui/Component.js +13 -1
- package/dist/ui/Config.js +2 -2
- package/dist/ui/Confirm.js +2 -2
- package/dist/ui/Execute.js +262 -0
- package/dist/ui/Introspect.js +5 -7
- package/dist/ui/Main.js +30 -69
- package/dist/ui/Spinner.js +10 -5
- package/dist/ui/Validate.js +120 -0
- package/package.json +7 -7
package/dist/services/skills.js
CHANGED
|
@@ -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 {};
|
package/dist/types/types.js
CHANGED
|
@@ -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);
|
package/dist/ui/Command.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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);
|
package/dist/ui/Component.js
CHANGED
|
@@ -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: [
|
|
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
|
}
|
package/dist/ui/Confirm.js
CHANGED
|
@@ -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:
|
|
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
|
+
}
|