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,66 +1,74 @@
1
1
  import { ComponentName, FeedbackType } from '../types/types.js';
2
2
  import { createAnthropicService, } from '../services/anthropic.js';
3
- import { createCommandDefinition, createFeedback, markAsDone, } from '../services/components.js';
3
+ import { createCommandDefinition, createExecuteDefinition, createFeedback, markAsDone, } from '../services/components.js';
4
4
  import { saveAnthropicConfig, saveConfig } from '../services/configuration.js';
5
5
  import { FeedbackMessages } from '../services/messages.js';
6
6
  import { exitApp } from '../services/process.js';
7
7
  import { withQueueHandler } from '../services/queue.js';
8
8
  /**
9
- * Creates config finished handler
9
+ * Creates all config handlers
10
10
  */
11
- export function createConfigFinishedHandler(addToTimeline, command, handleCommandError, handleCommandComplete, handleCommandAborted, setService) {
12
- return (config) => {
11
+ export function createConfigHandlers(ops, handleAborted, command, commandHandlers, setService) {
12
+ const onFinished = (config) => {
13
13
  const anthropicConfig = config;
14
14
  saveAnthropicConfig(anthropicConfig);
15
15
  const newService = createAnthropicService(anthropicConfig);
16
16
  setService(newService);
17
- return withQueueHandler(ComponentName.Config, (first, rest) => {
18
- addToTimeline(markAsDone(first), createFeedback(FeedbackType.Succeeded, FeedbackMessages.ConfigurationComplete));
19
- // Add command to queue if we have one
17
+ ops.setQueue(withQueueHandler(ComponentName.Config, (first, rest) => {
18
+ ops.addToTimeline(markAsDone(first), createFeedback(FeedbackType.Succeeded, FeedbackMessages.ConfigurationComplete));
20
19
  if (command) {
21
20
  return [
22
21
  ...rest,
23
- createCommandDefinition(command, newService, handleCommandError, handleCommandComplete, handleCommandAborted),
22
+ createCommandDefinition(command, newService, commandHandlers.onError, commandHandlers.onComplete, commandHandlers.onAborted),
24
23
  ];
25
24
  }
26
- // No command - exit after showing completion message
27
25
  exitApp(0);
28
26
  return rest;
29
- }, false, 0);
27
+ }, false, 0));
30
28
  };
31
- }
32
- /**
33
- * Creates config aborted handler
34
- */
35
- export function createConfigAbortedHandler(handleAborted) {
36
- return () => {
29
+ const onAborted = () => {
37
30
  handleAborted('Configuration');
38
31
  };
32
+ return { onFinished, onAborted };
39
33
  }
40
34
  /**
41
35
  * Creates config execution finished handler for CONFIG skill
42
- * Saves arbitrary config keys and exits
36
+ * Saves arbitrary config keys and optionally continues with execution
43
37
  */
44
- export function createConfigExecutionFinishedHandler(addToTimeline, keys) {
38
+ export function createConfigExecutionFinishedHandler(addToTimeline, keys, tasks, service, executeHandlers) {
45
39
  return (config) => {
46
- // Map short keys back to full keys and save
47
- // Group by section (e.g., "anthropic", "settings")
40
+ // Group by top-level section
48
41
  const sections = {};
49
42
  for (const fullKey of keys) {
50
43
  const parts = fullKey.split('.');
51
44
  const shortKey = parts[parts.length - 1];
52
- const section = parts.slice(0, -1).join('.');
53
- sections[section] = sections[section] ?? {};
45
+ const topSection = parts[0];
46
+ // Initialize section if needed
47
+ sections[topSection] = sections[topSection] ?? {};
54
48
  if (shortKey in config) {
55
- sections[section][shortKey] = config[shortKey];
49
+ const value = config[shortKey];
50
+ // Build nested structure recursively
51
+ let current = sections[topSection];
52
+ for (let i = 1; i < parts.length - 1; i++) {
53
+ current[parts[i]] = current[parts[i]] ?? {};
54
+ current = current[parts[i]];
55
+ }
56
+ current[parts[parts.length - 1]] = value;
56
57
  }
57
58
  }
58
- // Save each section
59
59
  for (const [section, sectionConfig] of Object.entries(sections)) {
60
60
  saveConfig(section, sectionConfig);
61
61
  }
62
62
  return withQueueHandler(ComponentName.Config, (first, rest) => {
63
63
  addToTimeline(markAsDone(first), createFeedback(FeedbackType.Succeeded, FeedbackMessages.ConfigurationComplete));
64
+ // If tasks are provided, continue with execution
65
+ if (tasks && service && executeHandlers) {
66
+ return [
67
+ ...rest,
68
+ createExecuteDefinition(tasks, service, executeHandlers.onError, executeHandlers.onComplete, executeHandlers.onAborted),
69
+ ];
70
+ }
71
+ // Otherwise, exit (legacy behavior for initial setup)
64
72
  exitApp(0);
65
73
  return rest;
66
74
  }, false, 0);
@@ -0,0 +1,46 @@
1
+ import { ComponentName, FeedbackType } from '../types/types.js';
2
+ import { createFeedback, createMessage, markAsDone, } from '../services/components.js';
3
+ import { formatDuration } from '../services/messages.js';
4
+ import { exitApp } from '../services/process.js';
5
+ import { ExecutionResult } from '../services/shell.js';
6
+ import { withQueueHandler } from '../services/queue.js';
7
+ /**
8
+ * Creates all execute handlers
9
+ */
10
+ export function createExecuteHandlers(ops, handleAborted) {
11
+ void handleAborted;
12
+ const onError = (error) => {
13
+ ops.setQueue(withQueueHandler(ComponentName.Execute, (first) => {
14
+ ops.addToTimeline(markAsDone(first), createFeedback(FeedbackType.Failed, error));
15
+ exitApp(1);
16
+ return [];
17
+ }));
18
+ };
19
+ const onComplete = (outputs, totalElapsed) => {
20
+ ops.setQueue(withQueueHandler(ComponentName.Execute, (first) => {
21
+ const failed = outputs.find((out) => out.result !== ExecutionResult.Success);
22
+ if (failed) {
23
+ const errorMessage = failed.error
24
+ ? `${failed.description}: ${failed.error}`
25
+ : `${failed.description} failed`;
26
+ ops.addToTimeline(markAsDone(first), createFeedback(FeedbackType.Failed, errorMessage));
27
+ exitApp(1);
28
+ return [];
29
+ }
30
+ ops.addToTimeline(markAsDone(first), createMessage(`Execution completed in ${formatDuration(totalElapsed)}.`));
31
+ exitApp(0);
32
+ return [];
33
+ }));
34
+ };
35
+ const onAborted = (elapsedTime) => {
36
+ ops.setQueue(withQueueHandler(ComponentName.Execute, (first) => {
37
+ const message = elapsedTime > 0
38
+ ? `The execution was cancelled after ${formatDuration(elapsedTime)}.`
39
+ : 'The execution was cancelled.';
40
+ ops.addToTimeline(markAsDone(first), createFeedback(FeedbackType.Aborted, message));
41
+ exitApp(0);
42
+ return [];
43
+ }));
44
+ };
45
+ return { onError, onComplete, onAborted };
46
+ }
@@ -1,88 +1,140 @@
1
1
  import { ComponentName, FeedbackType, TaskType } from '../types/types.js';
2
- import { createAnswerDefinition, createConfigDefinitionWithKeys, createFeedback, createIntrospectDefinition, markAsDone, } from '../services/components.js';
3
- import { createConfigExecutionAbortedHandler, createConfigExecutionFinishedHandler, } from './config.js';
2
+ import { createAnswerDefinition, createConfigDefinitionWithKeys, createExecuteDefinition, createFeedback, createIntrospectDefinition, createValidateDefinition, markAsDone, } from '../services/components.js';
3
+ import { StepType } from '../ui/Config.js';
4
4
  import { getCancellationMessage } from '../services/messages.js';
5
5
  import { exitApp } from '../services/process.js';
6
6
  import { withQueueHandler } from '../services/queue.js';
7
+ import { createConfigExecutionAbortedHandler, createConfigExecutionFinishedHandler, } from './config.js';
8
+ import { validateExecuteTasks } from '../services/execution-validator.js';
7
9
  /**
8
- * Creates execution confirmed handler
9
- */
10
- export function createExecutionConfirmedHandler(timelineRef, addToTimeline, service, handleIntrospectError, handleIntrospectComplete, handleIntrospectAborted, handleAnswerError, handleAnswerComplete, handleAnswerAborted, setQueue) {
11
- return () => withQueueHandler(ComponentName.Confirm, (first) => {
12
- // Find the most recent Plan in timeline to get tasks
13
- const currentTimeline = timelineRef.current;
14
- const lastPlanIndex = [...currentTimeline]
15
- .reverse()
16
- .findIndex((item) => item.name === ComponentName.Plan);
17
- const lastPlan = lastPlanIndex >= 0
18
- ? currentTimeline[currentTimeline.length - 1 - lastPlanIndex]
19
- : null;
20
- const tasks = lastPlan?.name === ComponentName.Plan &&
21
- Array.isArray(lastPlan.props.tasks)
22
- ? lastPlan.props.tasks
23
- : [];
24
- const allIntrospect = tasks.every((task) => task.type === TaskType.Introspect);
25
- const allAnswer = tasks.every((task) => task.type === TaskType.Answer);
26
- const allConfig = tasks.every((task) => task.type === TaskType.Config);
27
- if (allIntrospect && tasks.length > 0) {
28
- // Execute introspection
29
- addToTimeline(markAsDone(first));
30
- return [
31
- createIntrospectDefinition(tasks, service, handleIntrospectError, handleIntrospectComplete, handleIntrospectAborted),
32
- ];
33
- }
34
- else if (allAnswer && tasks.length > 0) {
35
- // Execute answer - extract question from first task
36
- const question = tasks[0].action;
37
- addToTimeline(markAsDone(first));
38
- return [
39
- createAnswerDefinition(question, service, handleAnswerError, handleAnswerComplete, handleAnswerAborted),
40
- ];
41
- }
42
- else if (allConfig && tasks.length > 0) {
43
- // Execute config - extract keys from task params
44
- const keys = tasks
45
- .map((task) => task.params?.key)
46
- .filter((key) => typeof key === 'string');
47
- addToTimeline(markAsDone(first));
48
- // Create handlers with keys for proper saving
49
- // Wrap in setQueue to properly update queue when Config finishes
50
- const handleConfigFinished = (config) => {
51
- setQueue(createConfigExecutionFinishedHandler(addToTimeline, keys)(config));
52
- };
53
- const handleConfigAborted = () => {
54
- setQueue(createConfigExecutionAbortedHandler(addToTimeline)());
55
- };
56
- return [
57
- createConfigDefinitionWithKeys(keys, handleConfigFinished, handleConfigAborted),
58
- ];
59
- }
60
- else {
61
- // Regular execution - just exit for now
62
- addToTimeline(markAsDone(first));
63
- exitApp(0);
64
- return [];
65
- }
66
- });
67
- }
68
- /**
69
- * Creates execution cancelled handler
10
+ * Creates all execution handlers
70
11
  */
71
- export function createExecutionCancelledHandler(timelineRef, addToTimeline) {
72
- return () => withQueueHandler(ComponentName.Confirm, (first) => {
73
- // Find the most recent Plan in timeline to check task types
74
- const currentTimeline = timelineRef.current;
75
- const lastPlanIndex = [...currentTimeline]
76
- .reverse()
77
- .findIndex((item) => item.name === ComponentName.Plan);
78
- const lastPlan = lastPlanIndex >= 0
79
- ? currentTimeline[currentTimeline.length - 1 - lastPlanIndex]
80
- : null;
81
- const allIntrospect = lastPlan?.name === ComponentName.Plan &&
82
- Array.isArray(lastPlan.props.tasks) &&
83
- lastPlan.props.tasks.every((task) => task.type === TaskType.Introspect);
84
- const operation = allIntrospect ? 'introspection' : 'execution';
85
- addToTimeline(markAsDone(first), createFeedback(FeedbackType.Aborted, getCancellationMessage(operation)));
86
- return undefined;
87
- }, true, 0);
12
+ export function createExecutionHandlers(ops, taskHandlers) {
13
+ const onConfirmed = (tasks) => {
14
+ ops.setQueue(withQueueHandler(ComponentName.Confirm, (first) => {
15
+ const allIntrospect = tasks.every((task) => task.type === TaskType.Introspect);
16
+ const allAnswer = tasks.every((task) => task.type === TaskType.Answer);
17
+ const allConfig = tasks.every((task) => task.type === TaskType.Config);
18
+ const allExecute = tasks.every((task) => task.type === TaskType.Execute);
19
+ const service = ops.service;
20
+ if (!service) {
21
+ ops.addToTimeline(markAsDone(first), createFeedback(FeedbackType.Failed, 'Service not available'));
22
+ exitApp(1);
23
+ return [];
24
+ }
25
+ if (allIntrospect && tasks.length > 0) {
26
+ ops.addToTimeline(markAsDone(first));
27
+ return [
28
+ createIntrospectDefinition(tasks, service, taskHandlers.introspect.onError, taskHandlers.introspect.onComplete, taskHandlers.introspect.onAborted),
29
+ ];
30
+ }
31
+ else if (allAnswer && tasks.length > 0) {
32
+ const question = tasks[0].action;
33
+ ops.addToTimeline(markAsDone(first));
34
+ return [
35
+ createAnswerDefinition(question, service, taskHandlers.answer.onError, taskHandlers.answer.onComplete, taskHandlers.answer.onAborted),
36
+ ];
37
+ }
38
+ else if (allConfig && tasks.length > 0) {
39
+ const keys = tasks
40
+ .map((task) => task.params?.key)
41
+ .filter((key) => typeof key === 'string');
42
+ ops.addToTimeline(markAsDone(first));
43
+ const handleConfigFinished = (config) => {
44
+ ops.setQueue(createConfigExecutionFinishedHandler(ops.addToTimeline, keys)(config));
45
+ };
46
+ const handleConfigAborted = () => {
47
+ ops.setQueue(createConfigExecutionAbortedHandler(ops.addToTimeline)());
48
+ };
49
+ return [
50
+ createConfigDefinitionWithKeys(keys, handleConfigFinished, handleConfigAborted),
51
+ ];
52
+ }
53
+ else if (allExecute && tasks.length > 0) {
54
+ // Validate config requirements before execution
55
+ const missingConfig = validateExecuteTasks(tasks);
56
+ if (missingConfig.length > 0) {
57
+ // Config is missing - call VALIDATE tool to get contextual descriptions
58
+ const keys = missingConfig.map((req) => req.path);
59
+ const userRequest = tasks.map((t) => t.action).join(', ');
60
+ ops.addToTimeline(markAsDone(first));
61
+ // Create handlers for Validate completion
62
+ const handleValidateComplete = (configWithDescriptions) => {
63
+ ops.setQueue(withQueueHandler(ComponentName.Validate, (first) => {
64
+ // Create CONFIG component with descriptions from VALIDATE
65
+ const handleConfigFinished = (config) => {
66
+ ops.setQueue(createConfigExecutionFinishedHandler(ops.addToTimeline, keys, tasks, service, taskHandlers.execute)(config));
67
+ };
68
+ const handleConfigAborted = () => {
69
+ ops.setQueue(createConfigExecutionAbortedHandler(ops.addToTimeline)());
70
+ };
71
+ // Create config steps from validated descriptions
72
+ const steps = configWithDescriptions.map((req) => {
73
+ const keyParts = req.path.split('.');
74
+ const shortKey = keyParts[keyParts.length - 1];
75
+ // Extract description without the {path} suffix
76
+ // Format from VALIDATE: "Description {path}"
77
+ let description = req.description || req.path;
78
+ const pathPattern = /\s*\{[^}]+\}\s*$/;
79
+ description = description.replace(pathPattern, '').trim();
80
+ const step = {
81
+ description,
82
+ key: shortKey,
83
+ path: req.path,
84
+ type: StepType.Text,
85
+ value: null,
86
+ validate: () => true,
87
+ };
88
+ return step;
89
+ });
90
+ // Mark Validate as done and move to timeline
91
+ ops.addToTimeline(markAsDone(first));
92
+ return [
93
+ {
94
+ id: crypto.randomUUID(),
95
+ name: ComponentName.Config,
96
+ state: { done: false },
97
+ props: {
98
+ steps,
99
+ onFinished: handleConfigFinished,
100
+ onAborted: handleConfigAborted,
101
+ },
102
+ },
103
+ ];
104
+ }));
105
+ };
106
+ const handleValidateError = (error) => {
107
+ ops.addToTimeline(createFeedback(FeedbackType.Failed, error));
108
+ exitApp(1);
109
+ };
110
+ const handleValidateAborted = () => {
111
+ ops.addToTimeline(createFeedback(FeedbackType.Aborted, 'Configuration validation cancelled'));
112
+ exitApp(0);
113
+ };
114
+ return [
115
+ createValidateDefinition(missingConfig, userRequest, service, handleValidateError, handleValidateComplete, handleValidateAborted),
116
+ ];
117
+ }
118
+ // No missing config - execute directly
119
+ ops.addToTimeline(markAsDone(first));
120
+ return [
121
+ createExecuteDefinition(tasks, service, taskHandlers.execute.onError, taskHandlers.execute.onComplete, taskHandlers.execute.onAborted),
122
+ ];
123
+ }
124
+ else {
125
+ ops.addToTimeline(markAsDone(first), createFeedback(FeedbackType.Failed, 'I can only process one type of task at a time for now.'));
126
+ exitApp(0);
127
+ return [];
128
+ }
129
+ }));
130
+ };
131
+ const onCancelled = (tasks) => {
132
+ ops.setQueue(withQueueHandler(ComponentName.Confirm, (first) => {
133
+ const allIntrospect = tasks.every((task) => task.type === TaskType.Introspect);
134
+ const operation = allIntrospect ? 'introspection' : 'execution';
135
+ ops.addToTimeline(markAsDone(first), createFeedback(FeedbackType.Aborted, getCancellationMessage(operation)));
136
+ return undefined;
137
+ }, true, 0));
138
+ };
139
+ return { onConfirmed, onCancelled };
88
140
  }
@@ -2,27 +2,20 @@ import { ComponentName } from '../types/types.js';
2
2
  import { createReportDefinition } from '../services/components.js';
3
3
  import { createErrorHandler, withQueueHandler } from '../services/queue.js';
4
4
  /**
5
- * Creates introspect error handler
5
+ * Creates all introspect handlers
6
6
  */
7
- export function createIntrospectErrorHandler(addToTimeline) {
8
- return (error) => createErrorHandler(ComponentName.Introspect, addToTimeline)(error);
9
- }
10
- /**
11
- * Creates introspect completion handler
12
- */
13
- export function createIntrospectCompleteHandler(addToTimeline) {
14
- return (message, capabilities) => withQueueHandler(ComponentName.Introspect, () => {
15
- // Don't add the Introspect component to timeline (it renders null)
16
- // Only add the Report component
17
- addToTimeline(createReportDefinition(message, capabilities));
18
- return undefined;
19
- }, true, 0);
20
- }
21
- /**
22
- * Creates introspect aborted handler
23
- */
24
- export function createIntrospectAbortedHandler(handleAborted) {
25
- return () => {
7
+ export function createIntrospectHandlers(ops, handleAborted) {
8
+ const onError = (error) => {
9
+ ops.setQueue(createErrorHandler(ComponentName.Introspect, ops.addToTimeline)(error));
10
+ };
11
+ const onComplete = (message, capabilities) => {
12
+ ops.setQueue(withQueueHandler(ComponentName.Introspect, () => {
13
+ ops.addToTimeline(createReportDefinition(message, capabilities));
14
+ return undefined;
15
+ }, true, 0));
16
+ };
17
+ const onAborted = () => {
26
18
  handleAborted('Introspection');
27
19
  };
20
+ return { onError, onComplete, onAborted };
28
21
  }
@@ -3,46 +3,41 @@ import { createConfirmDefinition, createFeedback, createPlanDefinition, markAsDo
3
3
  import { FeedbackMessages, formatErrorMessage, getRefiningMessage, } from '../services/messages.js';
4
4
  import { exitApp } from '../services/process.js';
5
5
  /**
6
- * Creates plan aborted handler
6
+ * Creates all plan handlers
7
7
  */
8
- export function createPlanAbortedHandler(handleAborted) {
9
- return () => {
8
+ export function createPlanHandlers(ops, handleAborted, executionHandlers) {
9
+ const onAborted = () => {
10
10
  handleAborted('Task selection');
11
11
  };
12
- }
13
- /**
14
- * Creates plan abort handler factory
15
- */
16
- export function createPlanAbortHandlerFactory(handleAborted, handlePlanAborted) {
17
- return (tasks) => {
12
+ const createAbortHandler = (tasks) => {
18
13
  const allIntrospect = tasks.every((task) => task.type === TaskType.Introspect);
19
14
  if (allIntrospect) {
20
15
  return () => {
21
16
  handleAborted('Introspection');
22
17
  };
23
18
  }
24
- return handlePlanAborted;
19
+ return onAborted;
25
20
  };
26
- }
27
- /**
28
- * Creates plan selection confirmed handler
29
- */
30
- export function createPlanSelectionConfirmedHandler(addToTimeline, service, handleRefinementAborted, createPlanAbortHandler, handleExecutionConfirmed, handleExecutionCancelled, setQueue) {
31
- return async (selectedTasks) => {
32
- // Mark current plan as done and add refinement to queue
33
- const refinementDef = createRefinement(getRefiningMessage(), handleRefinementAborted);
34
- setQueue((currentQueue) => {
21
+ const onSelectionConfirmed = async (selectedTasks) => {
22
+ const refinementDef = createRefinement(getRefiningMessage(), () => {
23
+ handleAborted('Plan refinement');
24
+ });
25
+ ops.setQueue((currentQueue) => {
35
26
  if (currentQueue.length === 0)
36
27
  return currentQueue;
37
28
  const [first] = currentQueue;
38
29
  if (first.name === ComponentName.Plan) {
39
- addToTimeline(markAsDone(first));
30
+ ops.addToTimeline(markAsDone(first));
40
31
  }
41
- // Add refinement to queue so it becomes the active component
42
32
  return [refinementDef];
43
33
  });
44
- // Process refined command in background
45
34
  try {
35
+ const service = ops.service;
36
+ if (!service) {
37
+ ops.addToTimeline(createFeedback(FeedbackType.Failed, FeedbackMessages.UnexpectedError, 'Service not available'));
38
+ exitApp(1);
39
+ return;
40
+ }
46
41
  const refinedCommand = selectedTasks
47
42
  .map((task) => {
48
43
  const action = task.action.toLowerCase().replace(/,/g, ' -');
@@ -51,32 +46,34 @@ export function createPlanSelectionConfirmedHandler(addToTimeline, service, hand
51
46
  })
52
47
  .join(', ');
53
48
  const result = await service.processWithTool(refinedCommand, 'plan');
54
- // Mark refinement as done and move to timeline
55
- setQueue((currentQueue) => {
49
+ ops.setQueue((currentQueue) => {
56
50
  if (currentQueue.length > 0 &&
57
51
  currentQueue[0].id === refinementDef.id) {
58
- addToTimeline(markAsDone(currentQueue[0]));
52
+ ops.addToTimeline(markAsDone(currentQueue[0]));
59
53
  }
60
54
  return [];
61
55
  });
62
- // Show final execution plan with confirmation
63
- const planDefinition = createPlanDefinition(result.message, result.tasks, createPlanAbortHandler(result.tasks), undefined);
64
- const confirmDefinition = createConfirmDefinition(handleExecutionConfirmed, handleExecutionCancelled);
65
- addToTimeline(planDefinition);
66
- setQueue([confirmDefinition]);
56
+ const planDefinition = createPlanDefinition(result.message, result.tasks, createAbortHandler(result.tasks), undefined);
57
+ const confirmDefinition = createConfirmDefinition(() => {
58
+ executionHandlers.onConfirmed(result.tasks);
59
+ }, () => {
60
+ executionHandlers.onCancelled(result.tasks);
61
+ });
62
+ ops.addToTimeline(planDefinition);
63
+ ops.setQueue([confirmDefinition]);
67
64
  }
68
65
  catch (error) {
69
66
  const errorMessage = formatErrorMessage(error);
70
- // Mark refinement as done and move to timeline before showing error
71
- setQueue((currentQueue) => {
67
+ ops.setQueue((currentQueue) => {
72
68
  if (currentQueue.length > 0 &&
73
69
  currentQueue[0].id === refinementDef.id) {
74
- addToTimeline(markAsDone(currentQueue[0]));
70
+ ops.addToTimeline(markAsDone(currentQueue[0]));
75
71
  }
76
72
  return [];
77
73
  });
78
- addToTimeline(createFeedback(FeedbackType.Failed, FeedbackMessages.UnexpectedError, errorMessage));
74
+ ops.addToTimeline(createFeedback(FeedbackType.Failed, FeedbackMessages.UnexpectedError, errorMessage));
79
75
  exitApp(1);
80
76
  }
81
77
  };
78
+ return { onAborted, createAbortHandler, onSelectionConfirmed };
82
79
  }
@@ -16,7 +16,10 @@ export class AnthropicService {
16
16
  // Build system prompt with additional context based on tool
17
17
  let systemPrompt = instructions;
18
18
  // Add skills section for applicable tools
19
- if (toolName === 'plan' || toolName === 'introspect') {
19
+ if (toolName === 'plan' ||
20
+ toolName === 'introspect' ||
21
+ toolName === 'execute' ||
22
+ toolName === 'validate') {
20
23
  const skills = loadSkills();
21
24
  const skillsSection = formatSkillsForPrompt(skills);
22
25
  systemPrompt += skillsSection;
@@ -74,6 +77,29 @@ export class AnthropicService {
74
77
  const content = toolUseContent;
75
78
  // Extract and validate response based on tool type
76
79
  const input = content.input;
80
+ // Handle execute tool response
81
+ if (toolName === 'execute') {
82
+ if (!input.message || typeof input.message !== 'string') {
83
+ throw new Error('Invalid tool response: missing or invalid message field');
84
+ }
85
+ if (!input.commands || !Array.isArray(input.commands)) {
86
+ throw new Error('Invalid tool response: missing or invalid commands array');
87
+ }
88
+ // Validate each command has required fields
89
+ input.commands.forEach((cmd, i) => {
90
+ if (!cmd.description || typeof cmd.description !== 'string') {
91
+ throw new Error(`Invalid command at index ${String(i)}: missing or invalid 'description' field`);
92
+ }
93
+ if (!cmd.command || typeof cmd.command !== 'string') {
94
+ throw new Error(`Invalid command at index ${String(i)}: missing or invalid 'command' field`);
95
+ }
96
+ });
97
+ return {
98
+ message: input.message,
99
+ tasks: [],
100
+ commands: input.commands,
101
+ };
102
+ }
77
103
  // Handle answer tool response
78
104
  if (toolName === 'answer') {
79
105
  if (!input.question || typeof input.question !== 'string') {
@@ -89,7 +115,7 @@ export class AnthropicService {
89
115
  };
90
116
  }
91
117
  // Handle plan and introspect tool responses
92
- if (!input.message || typeof input.message !== 'string') {
118
+ if (input.message === undefined || typeof input.message !== 'string') {
93
119
  throw new Error('Invalid tool response: missing or invalid message field');
94
120
  }
95
121
  if (!input.tasks || !Array.isArray(input.tasks)) {
@@ -12,11 +12,11 @@ export const Palette = {
12
12
  CharcoalGray: '#282828',
13
13
  Green: '#5aaa8a',
14
14
  LightGreen: '#65b595',
15
- BrightGreen: '#22aa22',
15
+ BrightGreen: '#3e9a3e',
16
16
  Yellow: '#cccc5c',
17
17
  LightYellow: '#d4d47a',
18
- Orange: '#cc9c5c',
19
- DarkOrange: '#a85c3f',
18
+ Orange: '#f48c80',
19
+ DarkOrange: '#ab5e40',
20
20
  BurntOrange: '#cc7a5c',
21
21
  Red: '#cc5c5c',
22
22
  Cyan: '#5c9ccc',
@@ -65,8 +65,22 @@ export function createConfigStepsFromSchema(keys) {
65
65
  // Config doesn't exist yet, use defaults
66
66
  }
67
67
  return keys.map((key) => {
68
+ // Check if key is in schema (built-in config)
68
69
  if (!(key in schema)) {
69
- throw new Error(`Unknown config key: ${key}`);
70
+ // Key is not in schema - it's from a skill
71
+ // Create a simple text step with placeholder description
72
+ const keyParts = key.split('.');
73
+ const shortKey = keyParts[keyParts.length - 1];
74
+ const description = keyParts
75
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
76
+ .join(' ');
77
+ return {
78
+ description: `${description} {${key}}`,
79
+ key: shortKey,
80
+ type: StepType.Text,
81
+ value: null,
82
+ validate: () => true, // Accept any string for now
83
+ };
70
84
  }
71
85
  const definition = schema[key];
72
86
  const currentValue = getConfigValue(currentConfig, key);
@@ -301,3 +315,38 @@ export function createAnswerDisplayDefinition(answer) {
301
315
  export function isStateless(component) {
302
316
  return !('state' in component);
303
317
  }
318
+ export function createExecuteDefinition(tasks, service, onError, onComplete, onAborted) {
319
+ return {
320
+ id: randomUUID(),
321
+ name: ComponentName.Execute,
322
+ state: {
323
+ done: false,
324
+ isLoading: true,
325
+ },
326
+ props: {
327
+ tasks,
328
+ service,
329
+ onError,
330
+ onComplete,
331
+ onAborted,
332
+ },
333
+ };
334
+ }
335
+ export function createValidateDefinition(missingConfig, userRequest, service, onError, onComplete, onAborted) {
336
+ return {
337
+ id: randomUUID(),
338
+ name: ComponentName.Validate,
339
+ state: {
340
+ done: false,
341
+ isLoading: true,
342
+ },
343
+ props: {
344
+ missingConfig,
345
+ userRequest,
346
+ service,
347
+ onError,
348
+ onComplete,
349
+ onAborted,
350
+ },
351
+ };
352
+ }