prompt-language-shell 1.0.0 → 1.0.4
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/README.md +40 -0
- package/dist/Main.js +14 -1
- package/dist/components/Component.js +16 -0
- package/dist/components/controllers/Config.js +62 -125
- package/dist/components/controllers/Confirm.js +1 -1
- package/dist/components/controllers/Execute.js +50 -10
- package/dist/components/controllers/Learn.js +416 -0
- package/dist/components/controllers/Schedule.js +6 -14
- package/dist/components/controllers/Setting.js +66 -0
- package/dist/components/views/Config.js +9 -96
- package/dist/components/views/Confirm.js +1 -1
- package/dist/components/views/Execute.js +3 -2
- package/dist/components/views/Help.js +27 -0
- package/dist/components/views/Learn.js +147 -0
- package/dist/components/views/Output.js +50 -6
- package/dist/components/views/Setting.js +88 -0
- package/dist/components/views/StdinInput.js +19 -0
- package/dist/components/views/Welcome.js +4 -11
- package/dist/execution/runner.js +10 -5
- package/dist/services/colors.js +16 -15
- package/dist/services/components.js +23 -1
- package/dist/services/monitor.js +25 -7
- package/dist/services/router.js +12 -1
- package/dist/services/shell.js +42 -6
- package/dist/services/skills.js +102 -0
- package/dist/skills/introspect.md +22 -19
- package/dist/skills/schedule.md +35 -3
- package/dist/tools/introspect.tool.js +3 -3
- package/dist/tools/schedule.tool.js +1 -1
- package/dist/types/components.js +20 -0
- package/dist/types/schemas.js +1 -0
- package/dist/types/types.js +3 -0
- package/package.json +1 -1
- package/dist/components/views/Panel.js +0 -5
package/README.md
CHANGED
|
@@ -125,6 +125,46 @@ Skills let you teach `pls` about your project-specific workflows. Create
|
|
|
125
125
|
markdown files in `~/.pls/skills/` to define custom operations that
|
|
126
126
|
`pls` can understand and execute.
|
|
127
127
|
|
|
128
|
+
### Creating Skills
|
|
129
|
+
|
|
130
|
+
The easiest way to create a new skill is with the guided walkthrough:
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
$ pls learn
|
|
134
|
+
|
|
135
|
+
Creating a new skill...
|
|
136
|
+
|
|
137
|
+
Skill name (e.g., "Deploy Project"):
|
|
138
|
+
> Build Frontend
|
|
139
|
+
|
|
140
|
+
Description (min 20 characters):
|
|
141
|
+
> Build the frontend application using npm
|
|
142
|
+
|
|
143
|
+
Step 1 - What does this step do?
|
|
144
|
+
> Install dependencies
|
|
145
|
+
|
|
146
|
+
How should this step be executed?
|
|
147
|
+
> shell command
|
|
148
|
+
|
|
149
|
+
Enter the shell command:
|
|
150
|
+
> npm install
|
|
151
|
+
|
|
152
|
+
Add another step?
|
|
153
|
+
> yes
|
|
154
|
+
|
|
155
|
+
...
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
The walkthrough guides you through defining:
|
|
159
|
+
- **Name**: A unique name for the skill
|
|
160
|
+
- **Description**: What the skill does (min 20 characters)
|
|
161
|
+
- **Aliases**: Example commands that invoke the skill (optional)
|
|
162
|
+
- **Config**: Configuration properties needed (optional)
|
|
163
|
+
- **Steps**: Each step with either a shell command or reference to another skill
|
|
164
|
+
|
|
165
|
+
Skills are saved to `~/.pls/skills/` as markdown files. File names use
|
|
166
|
+
kebab-case (e.g., "Build Frontend" becomes `build-frontend.md`).
|
|
167
|
+
|
|
128
168
|
For complete documentation, see [docs/SKILLS.md](./docs/SKILLS.md).
|
|
129
169
|
|
|
130
170
|
### Structure
|
package/dist/Main.js
CHANGED
|
@@ -8,10 +8,19 @@ import { getMissingConfigKeys } from './configuration/schema.js';
|
|
|
8
8
|
import { createConfigStepsFromSchema } from './configuration/steps.js';
|
|
9
9
|
import { unflattenConfig } from './configuration/transformation.js';
|
|
10
10
|
import { createAnthropicService } from './services/anthropic.js';
|
|
11
|
-
import { createCommand, createConfig, createFeedback, createMessage, createWelcome, } from './services/components.js';
|
|
11
|
+
import { createCommand, createConfig, createFeedback, createHelp, createMessage, createWelcome, } from './services/components.js';
|
|
12
12
|
import { registerGlobalShortcut } from './services/keyboard.js';
|
|
13
13
|
import { initializeLogger, setDebugLevel } from './services/logger.js';
|
|
14
14
|
import { Workflow } from './components/Workflow.js';
|
|
15
|
+
/**
|
|
16
|
+
* Check if command matches help variations (help, --help, -h)
|
|
17
|
+
*/
|
|
18
|
+
function isHelpCommand(cmd) {
|
|
19
|
+
if (!cmd)
|
|
20
|
+
return false;
|
|
21
|
+
const normalized = cmd.toLowerCase().trim();
|
|
22
|
+
return (normalized === 'help' || normalized === '--help' || normalized === '-h');
|
|
23
|
+
}
|
|
15
24
|
export const Main = ({ app, command, serviceFactory = createAnthropicService, }) => {
|
|
16
25
|
const [service, setService] = useState(null);
|
|
17
26
|
const [initialQueue, setInitialQueue] = useState(null);
|
|
@@ -106,6 +115,10 @@ export const Main = ({ app, command, serviceFactory = createAnthropicService, })
|
|
|
106
115
|
}),
|
|
107
116
|
]);
|
|
108
117
|
}
|
|
118
|
+
else if (service && isHelpCommand(command)) {
|
|
119
|
+
// Help command - show help screen directly (no LLM)
|
|
120
|
+
setInitialQueue([createHelp({ app })]);
|
|
121
|
+
}
|
|
109
122
|
else if (service && command) {
|
|
110
123
|
// Valid service exists and command provided - execute command
|
|
111
124
|
setInitialQueue([createCommand({ command, service })]);
|
|
@@ -10,11 +10,13 @@ import { Debug } from './views/Debug.js';
|
|
|
10
10
|
import { Execute, ExecuteView, mapStateToViewProps, } from './controllers/Execute.js';
|
|
11
11
|
import { Feedback } from './views/Feedback.js';
|
|
12
12
|
import { Introspect, IntrospectView } from './controllers/Introspect.js';
|
|
13
|
+
import { Learn, LearnView } from './controllers/Learn.js';
|
|
13
14
|
import { Message } from './views/Message.js';
|
|
14
15
|
import { Refinement, RefinementView } from './controllers/Refinement.js';
|
|
15
16
|
import { Report } from './views/Report.js';
|
|
16
17
|
import { Schedule, ScheduleView } from './controllers/Schedule.js';
|
|
17
18
|
import { Validate, ValidateView } from './controllers/Validate.js';
|
|
19
|
+
import { Help } from './views/Help.js';
|
|
18
20
|
import { Welcome } from './views/Welcome.js';
|
|
19
21
|
/**
|
|
20
22
|
* Render a simple component (no lifecycle management)
|
|
@@ -25,6 +27,10 @@ export const SimpleComponent = memo(function SimpleComponent({ def, }) {
|
|
|
25
27
|
const { props, status } = def;
|
|
26
28
|
return _jsx(Welcome, { ...props, status: status });
|
|
27
29
|
}
|
|
30
|
+
case ComponentName.Help: {
|
|
31
|
+
const { props, status } = def;
|
|
32
|
+
return _jsx(Help, { ...props, status: status });
|
|
33
|
+
}
|
|
28
34
|
case ComponentName.Feedback: {
|
|
29
35
|
const { props, status } = def;
|
|
30
36
|
return _jsx(Feedback, { ...props, status: status });
|
|
@@ -86,6 +92,10 @@ export const ControllerComponent = memo(function ControllerComponent({ def, debu
|
|
|
86
92
|
const { props: { tasks, service, upcoming, label }, status, } = def;
|
|
87
93
|
return (_jsx(Execute, { tasks: tasks, service: service, upcoming: upcoming, label: label, requestHandlers: requestHandlers, lifecycleHandlers: lifecycleHandlers, workflowHandlers: workflowHandlers, status: status }));
|
|
88
94
|
}
|
|
95
|
+
case ComponentName.Learn: {
|
|
96
|
+
const { props: { suggestedName, onFinished, onAborted }, status, } = def;
|
|
97
|
+
return (_jsx(Learn, { suggestedName: suggestedName, onFinished: onFinished, onAborted: onAborted, requestHandlers: requestHandlers, lifecycleHandlers: lifecycleHandlers, workflowHandlers: workflowHandlers, status: status }));
|
|
98
|
+
}
|
|
89
99
|
default:
|
|
90
100
|
throw new Error(`Unknown managed component: ${def.name}`);
|
|
91
101
|
}
|
|
@@ -137,6 +147,10 @@ export const ViewComponent = memo(function ViewComponent({ def, }) {
|
|
|
137
147
|
const { props: { text }, status, } = def;
|
|
138
148
|
return _jsx(RefinementView, { text: text, status: status });
|
|
139
149
|
}
|
|
150
|
+
case ComponentName.Learn: {
|
|
151
|
+
const { state, status } = def;
|
|
152
|
+
return (_jsx(LearnView, { state: state, status: status, onInputChange: () => { }, onInputSubmit: () => { } }));
|
|
153
|
+
}
|
|
140
154
|
default:
|
|
141
155
|
throw new Error(`Unknown managed component: ${def.name}`);
|
|
142
156
|
}
|
|
@@ -148,6 +162,7 @@ export const TimelineComponent = ({ def, }) => {
|
|
|
148
162
|
switch (def.name) {
|
|
149
163
|
// Simple components render as-is
|
|
150
164
|
case ComponentName.Welcome:
|
|
165
|
+
case ComponentName.Help:
|
|
151
166
|
case ComponentName.Feedback:
|
|
152
167
|
case ComponentName.Message:
|
|
153
168
|
case ComponentName.Debug:
|
|
@@ -163,6 +178,7 @@ export const TimelineComponent = ({ def, }) => {
|
|
|
163
178
|
case ComponentName.Execute:
|
|
164
179
|
case ComponentName.Answer:
|
|
165
180
|
case ComponentName.Introspect:
|
|
181
|
+
case ComponentName.Learn:
|
|
166
182
|
return _jsx(ViewComponent, { def: def });
|
|
167
183
|
default:
|
|
168
184
|
throw new Error('Unknown component type');
|
|
@@ -8,9 +8,31 @@ import { createConfigStepsFromSchema } from '../../configuration/steps.js';
|
|
|
8
8
|
import { saveConfigLabels } from '../../configuration/labels.js';
|
|
9
9
|
import { DebugLevel } from '../../configuration/types.js';
|
|
10
10
|
import { useInput } from '../../services/keyboard.js';
|
|
11
|
-
import {
|
|
11
|
+
import { StepType } from '../views/Config.js';
|
|
12
|
+
import { Setting } from './Setting.js';
|
|
12
13
|
import { Spinner } from '../views/Spinner.js';
|
|
13
14
|
export { ConfigView, StepType, } from '../views/Config.js';
|
|
15
|
+
function isTextStep(step) {
|
|
16
|
+
return step.type === StepType.Text;
|
|
17
|
+
}
|
|
18
|
+
function isSelectionStep(step) {
|
|
19
|
+
return step.type === StepType.Selection;
|
|
20
|
+
}
|
|
21
|
+
function initializeStepValues(steps) {
|
|
22
|
+
const values = {};
|
|
23
|
+
for (const step of steps) {
|
|
24
|
+
const configKey = step.path || step.key;
|
|
25
|
+
if (isTextStep(step)) {
|
|
26
|
+
if (step.value !== null) {
|
|
27
|
+
values[configKey] = step.value;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
else if (isSelectionStep(step)) {
|
|
31
|
+
values[configKey] = step.options[step.defaultIndex].value;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return values;
|
|
35
|
+
}
|
|
14
36
|
/**
|
|
15
37
|
* Resolve query to config steps via CONFIGURE tool
|
|
16
38
|
*/
|
|
@@ -48,56 +70,19 @@ export function Config(props) {
|
|
|
48
70
|
const isActive = status === ComponentStatus.Active;
|
|
49
71
|
const [steps, setSteps] = useState(initialSteps || []);
|
|
50
72
|
const [resolving, setResolving] = useState(!initialSteps?.length && !!query);
|
|
51
|
-
const [
|
|
52
|
-
const [values, setValues] = useState(() =>
|
|
53
|
-
const initial = {};
|
|
54
|
-
(initialSteps || []).forEach((stepConfig) => {
|
|
55
|
-
const configKey = stepConfig.path || stepConfig.key;
|
|
56
|
-
switch (stepConfig.type) {
|
|
57
|
-
case StepType.Text:
|
|
58
|
-
if (stepConfig.value !== null) {
|
|
59
|
-
initial[configKey] = stepConfig.value;
|
|
60
|
-
}
|
|
61
|
-
break;
|
|
62
|
-
case StepType.Selection:
|
|
63
|
-
initial[configKey] =
|
|
64
|
-
stepConfig.options[stepConfig.defaultIndex].value;
|
|
65
|
-
break;
|
|
66
|
-
}
|
|
67
|
-
});
|
|
68
|
-
return initial;
|
|
69
|
-
});
|
|
70
|
-
const [inputValue, setInputValue] = useState('');
|
|
71
|
-
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
72
|
-
// Resolve query to steps
|
|
73
|
+
const [currentStep, setCurrentStep] = useState(0);
|
|
74
|
+
const [values, setValues] = useState(() => initializeStepValues(initialSteps || []));
|
|
73
75
|
useEffect(() => {
|
|
74
76
|
if (!isActive || !query || !service || initialSteps?.length)
|
|
75
77
|
return;
|
|
76
78
|
resolveQueryToSteps(query, service)
|
|
77
79
|
.then((result) => {
|
|
78
|
-
// Add debug components to timeline if present
|
|
79
80
|
if (result.debug.length) {
|
|
80
81
|
workflowHandlers.addToTimeline(...result.debug);
|
|
81
82
|
}
|
|
82
83
|
setSteps(result.steps);
|
|
83
84
|
setResolving(false);
|
|
84
|
-
|
|
85
|
-
const initial = {};
|
|
86
|
-
result.steps.forEach((stepConfig) => {
|
|
87
|
-
const configKey = stepConfig.path || stepConfig.key;
|
|
88
|
-
switch (stepConfig.type) {
|
|
89
|
-
case StepType.Text:
|
|
90
|
-
if (stepConfig.value !== null) {
|
|
91
|
-
initial[configKey] = stepConfig.value;
|
|
92
|
-
}
|
|
93
|
-
break;
|
|
94
|
-
case StepType.Selection:
|
|
95
|
-
initial[configKey] =
|
|
96
|
-
stepConfig.options[stepConfig.defaultIndex].value;
|
|
97
|
-
break;
|
|
98
|
-
}
|
|
99
|
-
});
|
|
100
|
-
setValues(initial);
|
|
85
|
+
setValues(initializeStepValues(result.steps));
|
|
101
86
|
})
|
|
102
87
|
.catch((err) => {
|
|
103
88
|
setResolving(false);
|
|
@@ -114,93 +99,39 @@ export function Config(props) {
|
|
|
114
99
|
lifecycleHandlers,
|
|
115
100
|
workflowHandlers,
|
|
116
101
|
]);
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
102
|
+
const handleEscape = () => {
|
|
103
|
+
requestHandlers.onCompleted({
|
|
104
|
+
values,
|
|
105
|
+
completedStep: currentStep,
|
|
106
|
+
selectedIndex: 0,
|
|
107
|
+
steps,
|
|
108
|
+
});
|
|
109
|
+
if (onAborted) {
|
|
110
|
+
onAborted('configuration');
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
lifecycleHandlers.completeActive(createFeedback({
|
|
114
|
+
type: FeedbackType.Aborted,
|
|
115
|
+
message: 'Configuration cancelled.',
|
|
116
|
+
}));
|
|
123
117
|
}
|
|
124
|
-
}, [step, isActive, steps, values]);
|
|
125
|
-
const normalizeValue = (value) => {
|
|
126
|
-
if (value === null || value === undefined)
|
|
127
|
-
return '';
|
|
128
|
-
return value.replace(/\n/g, '').trim();
|
|
129
118
|
};
|
|
130
119
|
useInput((_, key) => {
|
|
131
|
-
if (!isActive ||
|
|
120
|
+
if (!isActive || currentStep >= steps.length)
|
|
132
121
|
return;
|
|
133
|
-
const currentStepConfig = steps[step];
|
|
134
122
|
if (key.escape) {
|
|
135
|
-
|
|
136
|
-
let currentValue = '';
|
|
137
|
-
switch (currentStepConfig.type) {
|
|
138
|
-
case StepType.Text:
|
|
139
|
-
currentValue = inputValue || values[configKey] || '';
|
|
140
|
-
break;
|
|
141
|
-
case StepType.Selection:
|
|
142
|
-
currentValue = values[configKey] || '';
|
|
143
|
-
break;
|
|
144
|
-
}
|
|
145
|
-
const finalValues = currentValue
|
|
146
|
-
? { ...values, [configKey]: currentValue }
|
|
147
|
-
: values;
|
|
148
|
-
requestHandlers.onCompleted({
|
|
149
|
-
values: finalValues,
|
|
150
|
-
completedStep: step,
|
|
151
|
-
selectedIndex,
|
|
152
|
-
steps,
|
|
153
|
-
});
|
|
154
|
-
if (onAborted) {
|
|
155
|
-
onAborted('configuration');
|
|
156
|
-
}
|
|
157
|
-
else {
|
|
158
|
-
lifecycleHandlers.completeActive(createFeedback({
|
|
159
|
-
type: FeedbackType.Aborted,
|
|
160
|
-
message: 'Configuration cancelled.',
|
|
161
|
-
}));
|
|
162
|
-
}
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
if (currentStepConfig.type === StepType.Selection) {
|
|
166
|
-
if (key.tab) {
|
|
167
|
-
setSelectedIndex((prev) => (prev + 1) % currentStepConfig.options.length);
|
|
168
|
-
}
|
|
169
|
-
else if (key.return) {
|
|
170
|
-
handleSubmit(currentStepConfig.options[selectedIndex].value);
|
|
171
|
-
}
|
|
123
|
+
handleEscape();
|
|
172
124
|
}
|
|
173
125
|
}, { isActive });
|
|
174
|
-
const
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
switch (currentStepConfig.type) {
|
|
178
|
-
case StepType.Selection:
|
|
179
|
-
finalValue = value;
|
|
180
|
-
break;
|
|
181
|
-
case StepType.Text: {
|
|
182
|
-
const normalizedInput = normalizeValue(value);
|
|
183
|
-
if (normalizedInput && currentStepConfig.validate(normalizedInput)) {
|
|
184
|
-
finalValue = normalizedInput;
|
|
185
|
-
}
|
|
186
|
-
else if (currentStepConfig.value &&
|
|
187
|
-
currentStepConfig.validate(currentStepConfig.value)) {
|
|
188
|
-
finalValue = currentStepConfig.value;
|
|
189
|
-
}
|
|
190
|
-
break;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
if (!finalValue)
|
|
194
|
-
return;
|
|
195
|
-
const configKey = currentStepConfig.path || currentStepConfig.key;
|
|
196
|
-
const newValues = { ...values, [configKey]: finalValue };
|
|
126
|
+
const handleEntrySubmit = (stepConfig, value) => {
|
|
127
|
+
const configKey = stepConfig.path || stepConfig.key;
|
|
128
|
+
const newValues = { ...values, [configKey]: value };
|
|
197
129
|
setValues(newValues);
|
|
198
|
-
|
|
199
|
-
if (step === steps.length - 1) {
|
|
130
|
+
if (currentStep === steps.length - 1) {
|
|
200
131
|
requestHandlers.onCompleted({
|
|
201
132
|
values: newValues,
|
|
202
133
|
completedStep: steps.length,
|
|
203
|
-
selectedIndex,
|
|
134
|
+
selectedIndex: 0,
|
|
204
135
|
steps,
|
|
205
136
|
});
|
|
206
137
|
try {
|
|
@@ -216,19 +147,25 @@ export function Config(props) {
|
|
|
216
147
|
message: error instanceof Error ? error.message : 'Configuration failed',
|
|
217
148
|
}));
|
|
218
149
|
}
|
|
219
|
-
|
|
150
|
+
setCurrentStep(steps.length);
|
|
220
151
|
}
|
|
221
152
|
else {
|
|
222
|
-
|
|
223
|
-
setStep(nextStep);
|
|
224
|
-
if (nextStep < steps.length &&
|
|
225
|
-
steps[nextStep].type === StepType.Selection) {
|
|
226
|
-
setSelectedIndex(steps[nextStep].defaultIndex);
|
|
227
|
-
}
|
|
153
|
+
setCurrentStep(currentStep + 1);
|
|
228
154
|
}
|
|
229
155
|
};
|
|
230
156
|
if (resolving) {
|
|
231
157
|
return (_jsxs(Box, { marginLeft: 1, children: [_jsx(Text, { children: "Resolving configuration... " }), _jsx(Spinner, {})] }));
|
|
232
158
|
}
|
|
233
|
-
return (_jsx(
|
|
159
|
+
return (_jsx(Box, { flexDirection: "column", marginLeft: 1, children: steps.map((stepConfig, index) => {
|
|
160
|
+
const isStepActive = index === currentStep && isActive;
|
|
161
|
+
const isCompleted = index < currentStep;
|
|
162
|
+
const wasAborted = index === currentStep && !isActive;
|
|
163
|
+
const shouldShow = isCompleted || isStepActive || wasAborted;
|
|
164
|
+
if (!shouldShow)
|
|
165
|
+
return null;
|
|
166
|
+
const configKey = stepConfig.path || stepConfig.key;
|
|
167
|
+
return (_jsx(Box, { marginTop: index === 0 ? 0 : 1, children: _jsx(Setting, { step: stepConfig, initialValue: values[configKey] || '', isActive: isStepActive, debug: debug, onSubmit: (value) => {
|
|
168
|
+
handleEntrySubmit(stepConfig, value);
|
|
169
|
+
} }) }, configKey));
|
|
170
|
+
}) }));
|
|
234
171
|
}
|
|
@@ -19,7 +19,7 @@ export function Confirm({ message, status, requestHandlers, onConfirmed, onCance
|
|
|
19
19
|
requestHandlers.onCompleted(finalState);
|
|
20
20
|
onCancelled();
|
|
21
21
|
}
|
|
22
|
-
else if (key.tab) {
|
|
22
|
+
else if (key.tab || key.leftArrow || key.rightArrow) {
|
|
23
23
|
// Toggle between Yes (0) and No (1)
|
|
24
24
|
setSelectedIndex((prev) => (prev === 0 ? 1 : 0));
|
|
25
25
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useCallback, useEffect, useReducer, useRef } from 'react';
|
|
3
|
-
import { ComponentStatus, } from '../../types/components.js';
|
|
2
|
+
import { useCallback, useEffect, useReducer, useRef, useState } from 'react';
|
|
3
|
+
import { ComponentStatus, OutputSource, } from '../../types/components.js';
|
|
4
4
|
import { useInput } from '../../services/keyboard.js';
|
|
5
5
|
import { formatErrorMessage, getExecutionErrorMessage, } from '../../services/messages.js';
|
|
6
|
-
import { ExecutionStatus } from '../../services/shell.js';
|
|
6
|
+
import { ExecutionStatus, killCurrentProcess, writeStdin, } from '../../services/shell.js';
|
|
7
7
|
import { ELAPSED_UPDATE_INTERVAL, ensureMinimumTime, } from '../../services/timing.js';
|
|
8
8
|
import { handleTaskCompletion, handleTaskFailure, } from '../../execution/handlers.js';
|
|
9
9
|
import { processTasks } from '../../execution/processing.js';
|
|
@@ -43,6 +43,9 @@ export function Execute({ tasks: inputTasks, status, service, upcoming, label, r
|
|
|
43
43
|
});
|
|
44
44
|
// Ref to track if current task execution is cancelled
|
|
45
45
|
const cancelledRef = useRef(false);
|
|
46
|
+
// Toggle stdin input visibility with / key
|
|
47
|
+
const [showStdinInput, setShowStdinInput] = useState(false);
|
|
48
|
+
const showStdinInputRef = useRef(false);
|
|
46
49
|
const { error, tasks, message, hasProcessed, completionMessage, summary } = localState;
|
|
47
50
|
// Derive current task index from tasks
|
|
48
51
|
const currentTaskIndex = getCurrentTaskIndex(tasks);
|
|
@@ -75,9 +78,17 @@ export function Execute({ tasks: inputTasks, status, service, upcoming, label, r
|
|
|
75
78
|
clearInterval(interval);
|
|
76
79
|
};
|
|
77
80
|
}, [runningTask?.startTime, isExecuting, currentTaskIndex]);
|
|
78
|
-
//
|
|
81
|
+
// Hide stdin input when not executing
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (!isExecuting) {
|
|
84
|
+
showStdinInputRef.current = false;
|
|
85
|
+
setShowStdinInput(false);
|
|
86
|
+
}
|
|
87
|
+
}, [isExecuting]);
|
|
88
|
+
// Handle cancel - kill the running process and update final output
|
|
79
89
|
const handleCancel = useCallback(() => {
|
|
80
90
|
cancelledRef.current = true;
|
|
91
|
+
killCurrentProcess();
|
|
81
92
|
dispatch({ type: ExecuteActionType.CancelExecution });
|
|
82
93
|
// Build final state with current output for the running task
|
|
83
94
|
const updatedTasks = tasks.map((task) => {
|
|
@@ -103,9 +114,30 @@ export function Execute({ tasks: inputTasks, status, service, upcoming, label, r
|
|
|
103
114
|
requestHandlers.onCompleted(finalState);
|
|
104
115
|
requestHandlers.onAborted('execution');
|
|
105
116
|
}, [message, summary, tasks, requestHandlers]);
|
|
106
|
-
|
|
117
|
+
// Handle stdin submission: write the line to the running process
|
|
118
|
+
const handleStdinSubmit = useCallback((value) => {
|
|
119
|
+
writeStdin(value + '\n');
|
|
120
|
+
outputRef.current.chunks.push({
|
|
121
|
+
text: `\n> ${value}\n`,
|
|
122
|
+
timestamp: Date.now(),
|
|
123
|
+
source: OutputSource.Stdin,
|
|
124
|
+
});
|
|
125
|
+
showStdinInputRef.current = false;
|
|
126
|
+
setShowStdinInput(false);
|
|
127
|
+
}, []);
|
|
128
|
+
useInput((input, key) => {
|
|
107
129
|
if (key.escape && (isLoading || isExecuting)) {
|
|
108
|
-
|
|
130
|
+
if (showStdinInputRef.current) {
|
|
131
|
+
showStdinInputRef.current = false;
|
|
132
|
+
setShowStdinInput(false);
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
handleCancel();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (input === '/' && isExecuting && !showStdinInputRef.current) {
|
|
139
|
+
showStdinInputRef.current = true;
|
|
140
|
+
setShowStdinInput(true);
|
|
109
141
|
}
|
|
110
142
|
}, { isActive: (isLoading || isExecuting) && isActive });
|
|
111
143
|
// Process tasks to get commands from AI
|
|
@@ -210,6 +242,8 @@ export function Execute({ tasks: inputTasks, status, service, upcoming, label, r
|
|
|
210
242
|
});
|
|
211
243
|
// Reset output ref for new task
|
|
212
244
|
outputRef.current = { chunks: [], currentMemory: undefined };
|
|
245
|
+
showStdinInputRef.current = false;
|
|
246
|
+
setShowStdinInput(false);
|
|
213
247
|
// Merge workdir into command
|
|
214
248
|
const command = workdirRef.current
|
|
215
249
|
? { ...currentTask.command, workdir: workdirRef.current }
|
|
@@ -217,8 +251,10 @@ export function Execute({ tasks: inputTasks, status, service, upcoming, label, r
|
|
|
217
251
|
void executeTask(command, currentTaskIndex, {
|
|
218
252
|
onUpdate: (output) => {
|
|
219
253
|
if (!cancelledRef.current) {
|
|
254
|
+
// Preserve stdin chunks that were added locally
|
|
255
|
+
const stdinChunks = outputRef.current.chunks.filter((c) => c.source === OutputSource.Stdin);
|
|
220
256
|
outputRef.current = {
|
|
221
|
-
chunks: output.chunks,
|
|
257
|
+
chunks: [...output.chunks, ...stdinChunks],
|
|
222
258
|
currentMemory: output.currentMemory,
|
|
223
259
|
};
|
|
224
260
|
}
|
|
@@ -230,11 +266,13 @@ export function Execute({ tasks: inputTasks, status, service, upcoming, label, r
|
|
|
230
266
|
if (execOutput.workdir) {
|
|
231
267
|
workdirRef.current = execOutput.workdir;
|
|
232
268
|
}
|
|
269
|
+
// Preserve stdin chunks in final output
|
|
270
|
+
const stdinChunks = outputRef.current.chunks.filter((c) => c.source === OutputSource.Stdin);
|
|
233
271
|
const tasksWithOutput = tasks.map((task, i) => i === currentTaskIndex
|
|
234
272
|
? {
|
|
235
273
|
...task,
|
|
236
274
|
output: {
|
|
237
|
-
chunks: execOutput.chunks,
|
|
275
|
+
chunks: [...execOutput.chunks, ...stdinChunks],
|
|
238
276
|
},
|
|
239
277
|
}
|
|
240
278
|
: task);
|
|
@@ -256,11 +294,13 @@ export function Execute({ tasks: inputTasks, status, service, upcoming, label, r
|
|
|
256
294
|
if (execOutput.workdir) {
|
|
257
295
|
workdirRef.current = execOutput.workdir;
|
|
258
296
|
}
|
|
297
|
+
// Preserve stdin chunks in final output
|
|
298
|
+
const stdinChunks = outputRef.current.chunks.filter((c) => c.source === OutputSource.Stdin);
|
|
259
299
|
const tasksWithOutput = tasks.map((task, i) => i === currentTaskIndex
|
|
260
300
|
? {
|
|
261
301
|
...task,
|
|
262
302
|
output: {
|
|
263
|
-
chunks: execOutput.chunks,
|
|
303
|
+
chunks: [...execOutput.chunks, ...stdinChunks],
|
|
264
304
|
},
|
|
265
305
|
error: execOutput.error || undefined,
|
|
266
306
|
}
|
|
@@ -287,5 +327,5 @@ export function Execute({ tasks: inputTasks, status, service, upcoming, label, r
|
|
|
287
327
|
lifecycleHandlers,
|
|
288
328
|
workflowHandlers,
|
|
289
329
|
]);
|
|
290
|
-
return (_jsx(ExecuteView, { isLoading: isLoading, isExecuting: isExecuting, isActive: isActive, error: error, message: message, tasks: tasks, completionMessage: completionMessage, showTasks: showTasks, upcoming: upcoming, label: label }));
|
|
330
|
+
return (_jsx(ExecuteView, { isLoading: isLoading, isExecuting: isExecuting, isActive: isActive, error: error, message: message, tasks: tasks, completionMessage: completionMessage, showTasks: showTasks, upcoming: upcoming, label: label, onStdinSubmit: showStdinInput ? handleStdinSubmit : undefined }));
|
|
291
331
|
}
|