prompt-language-shell 0.3.4 → 0.3.8

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 CHANGED
@@ -45,6 +45,12 @@ Your configuration is stored in `~/.plsrc` as a YAML file. Supported settings:
45
45
  - `anthropic.key` - Your API key
46
46
  - `anthropic.model` - The model to use
47
47
 
48
+ ## Skills
49
+
50
+ You can extend `pls` with custom workflows by creating markdown files in `~/.pls/skills/`. Skills define domain-specific operations, parameters, and steps that guide both planning and execution of tasks.
51
+
52
+ Your skills are referenced when planning requests, enabling `pls` to understand specific workflows, create and execute structured plans tailored to your environment.
53
+
48
54
  ## Development
49
55
 
50
56
  See [CLAUDE.md](./CLAUDE.md) for development guidelines and architecture.
package/dist/index.js CHANGED
@@ -20,6 +20,7 @@ const app = {
20
20
  version: packageJson.version,
21
21
  description: packageJson.description,
22
22
  isDev,
23
+ isDebug: false,
23
24
  };
24
25
  // Get command from command-line arguments
25
26
  const args = process.argv.slice(2);
@@ -1,10 +1,22 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- import { ComponentName, } from '../types/components.js';
3
- import { StepType } from '../ui/Config.js';
2
+ import { ComponentName } from '../types/types.js';
4
3
  import { AnthropicModel, isValidAnthropicApiKey, isValidAnthropicModel, } from './config.js';
4
+ import { getConfirmationMessage } from './messages.js';
5
+ import { StepType } from '../ui/Config.js';
5
6
  export function markAsDone(component) {
6
7
  return { ...component, state: { ...component.state, done: true } };
7
8
  }
9
+ export function getRefiningMessage() {
10
+ const messages = [
11
+ 'Let me work out the specifics for you.',
12
+ "I'll figure out the concrete steps.",
13
+ 'Let me break this down into tasks.',
14
+ "I'll plan out the details.",
15
+ 'Let me arrange the steps.',
16
+ "I'll prepare everything you need.",
17
+ ];
18
+ return messages[Math.floor(Math.random() * messages.length)];
19
+ }
8
20
  export function createWelcomeDefinition(app) {
9
21
  return {
10
22
  id: randomUUID(),
@@ -47,7 +59,7 @@ export function createConfigDefinition(onFinished, onAborted) {
47
59
  },
48
60
  };
49
61
  }
50
- export function createCommandDefinition(command, service, onError, onComplete) {
62
+ export function createCommandDefinition(command, service, onError, onComplete, onAborted) {
51
63
  return {
52
64
  id: randomUUID(),
53
65
  name: ComponentName.Command,
@@ -60,10 +72,11 @@ export function createCommandDefinition(command, service, onError, onComplete) {
60
72
  service,
61
73
  onError,
62
74
  onComplete,
75
+ onAborted,
63
76
  },
64
77
  };
65
78
  }
66
- export function createPlanDefinition(message, tasks, onSelectionConfirmed) {
79
+ export function createPlanDefinition(message, tasks, onAborted, onSelectionConfirmed) {
67
80
  return {
68
81
  id: randomUUID(),
69
82
  name: ComponentName.Plan,
@@ -77,6 +90,7 @@ export function createPlanDefinition(message, tasks, onSelectionConfirmed) {
77
90
  message,
78
91
  tasks,
79
92
  onSelectionConfirmed,
93
+ onAborted,
80
94
  },
81
95
  };
82
96
  }
@@ -99,6 +113,29 @@ export function createMessage(text) {
99
113
  },
100
114
  };
101
115
  }
116
+ export function createRefinement(text, onAborted) {
117
+ return {
118
+ id: randomUUID(),
119
+ name: ComponentName.Refinement,
120
+ state: { done: false },
121
+ props: {
122
+ text,
123
+ onAborted,
124
+ },
125
+ };
126
+ }
127
+ export function createConfirmDefinition(onConfirmed, onCancelled) {
128
+ return {
129
+ id: randomUUID(),
130
+ name: ComponentName.Confirm,
131
+ state: { done: false },
132
+ props: {
133
+ message: getConfirmationMessage(),
134
+ onConfirmed,
135
+ onCancelled,
136
+ },
137
+ };
138
+ }
102
139
  export function isStateless(component) {
103
140
  return !('state' in component);
104
141
  }
@@ -50,6 +50,14 @@ function validateConfig(parsed) {
50
50
  if (model && typeof model === 'string' && isValidAnthropicModel(model)) {
51
51
  validatedConfig.anthropic.model = model;
52
52
  }
53
+ // Optional settings section
54
+ if (config.settings && typeof config.settings === 'object') {
55
+ const settings = config.settings;
56
+ validatedConfig.settings = {};
57
+ if ('debug' in settings && typeof settings.debug === 'boolean') {
58
+ validatedConfig.settings.debug = settings.debug;
59
+ }
60
+ }
53
61
  return validatedConfig;
54
62
  }
55
63
  export function loadConfig() {
@@ -116,6 +124,18 @@ export function saveConfig(section, config) {
116
124
  export function saveAnthropicConfig(config) {
117
125
  saveConfig('anthropic', config);
118
126
  }
127
+ export function saveDebugSetting(debug) {
128
+ saveConfig('settings', { debug });
129
+ }
130
+ export function loadDebugSetting() {
131
+ try {
132
+ const config = loadConfig();
133
+ return config.settings?.debug ?? false;
134
+ }
135
+ catch {
136
+ return false;
137
+ }
138
+ }
119
139
  /**
120
140
  * Returns a message requesting initial setup.
121
141
  * Provides natural language variations that sound like a professional concierge
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Returns a natural language confirmation message for plan execution.
3
+ * Randomly selects from variations to sound less robotic.
4
+ */
5
+ export function getConfirmationMessage() {
6
+ const messages = [
7
+ 'Should I execute this plan?',
8
+ 'Do you want me to proceed with these tasks?',
9
+ 'Ready to execute?',
10
+ 'Shall I execute this plan?',
11
+ 'Would you like me to run these tasks?',
12
+ 'Execute this plan?',
13
+ ];
14
+ return messages[Math.floor(Math.random() * messages.length)];
15
+ }
@@ -1,6 +1,6 @@
1
1
  export const planTool = {
2
2
  name: 'plan',
3
- description: 'Plan and structure tasks from a user command. Break down the request into clear, actionable steps with type information and parameters.',
3
+ description: 'Plan and structure tasks from a user command. Break down the request into clear, actionable steps with type information and parameters. When refining previously selected tasks, the input will be formatted as lowercase actions with types in brackets, e.g., "install the python development environment (type: execute), explain how virtual environments work (type: answer)".',
4
4
  input_schema: {
5
5
  type: 'object',
6
6
  properties: {
@@ -1,28 +1 @@
1
- export var ComponentName;
2
- (function (ComponentName) {
3
- ComponentName["Welcome"] = "welcome";
4
- ComponentName["Config"] = "config";
5
- ComponentName["Feedback"] = "feedback";
6
- ComponentName["Message"] = "message";
7
- ComponentName["Plan"] = "plan";
8
- ComponentName["Command"] = "command";
9
- })(ComponentName || (ComponentName = {}));
10
- export var TaskType;
11
- (function (TaskType) {
12
- TaskType["Config"] = "config";
13
- TaskType["Plan"] = "plan";
14
- TaskType["Execute"] = "execute";
15
- TaskType["Answer"] = "answer";
16
- TaskType["Report"] = "report";
17
- TaskType["Define"] = "define";
18
- TaskType["Ignore"] = "ignore";
19
- TaskType["Select"] = "select";
20
- TaskType["Discard"] = "discard";
21
- })(TaskType || (TaskType = {}));
22
- export var FeedbackType;
23
- (function (FeedbackType) {
24
- FeedbackType["Info"] = "info";
25
- FeedbackType["Succeeded"] = "succeeded";
26
- FeedbackType["Aborted"] = "aborted";
27
- FeedbackType["Failed"] = "failed";
28
- })(FeedbackType || (FeedbackType = {}));
1
+ export {};
@@ -0,0 +1,30 @@
1
+ export var ComponentName;
2
+ (function (ComponentName) {
3
+ ComponentName["Welcome"] = "welcome";
4
+ ComponentName["Config"] = "config";
5
+ ComponentName["Message"] = "message";
6
+ ComponentName["Command"] = "command";
7
+ ComponentName["Plan"] = "plan";
8
+ ComponentName["Refinement"] = "refinement";
9
+ ComponentName["Feedback"] = "feedback";
10
+ ComponentName["Confirm"] = "confirm";
11
+ })(ComponentName || (ComponentName = {}));
12
+ export var TaskType;
13
+ (function (TaskType) {
14
+ TaskType["Config"] = "config";
15
+ TaskType["Plan"] = "plan";
16
+ TaskType["Execute"] = "execute";
17
+ TaskType["Answer"] = "answer";
18
+ TaskType["Report"] = "report";
19
+ TaskType["Define"] = "define";
20
+ TaskType["Ignore"] = "ignore";
21
+ TaskType["Select"] = "select";
22
+ TaskType["Discard"] = "discard";
23
+ })(TaskType || (TaskType = {}));
24
+ export var FeedbackType;
25
+ (function (FeedbackType) {
26
+ FeedbackType["Info"] = "info";
27
+ FeedbackType["Succeeded"] = "succeeded";
28
+ FeedbackType["Aborted"] = "aborted";
29
+ FeedbackType["Failed"] = "failed";
30
+ })(FeedbackType || (FeedbackType = {}));
package/dist/ui/Column.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Box } from 'ink';
3
3
  import { Component } from './Component.js';
4
- export const Column = ({ items }) => {
5
- return (_jsx(Box, { marginTop: 1, marginBottom: 1, marginLeft: 1, flexDirection: "column", gap: 1, children: items.map((item) => (_jsx(Box, { children: _jsx(Component, { def: item }) }, item.id))) }));
4
+ export const Column = ({ items, debug }) => {
5
+ return (_jsx(Box, { marginTop: 1, marginBottom: 1, marginLeft: 1, flexDirection: "column", gap: 1, children: items.map((item) => (_jsx(Box, { children: _jsx(Component, { def: item, debug: debug }) }, item.id))) }));
6
6
  };
@@ -1,12 +1,18 @@
1
1
  import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useEffect, useState } from 'react';
3
- import { Box, Text } from 'ink';
3
+ import { Box, Text, useInput } from 'ink';
4
4
  import { Spinner } from './Spinner.js';
5
5
  const MIN_PROCESSING_TIME = 1000; // purely for visual effect
6
- export function Command({ command, state, service, children, onError, onComplete, }) {
6
+ export function Command({ command, state, service, children, onError, onComplete, onAborted, }) {
7
7
  const done = state?.done ?? false;
8
8
  const [error, setError] = useState(null);
9
9
  const [isLoading, setIsLoading] = useState(state?.isLoading ?? !done);
10
+ useInput((input, key) => {
11
+ if (key.escape && isLoading && !done) {
12
+ setIsLoading(false);
13
+ onAborted();
14
+ }
15
+ }, { isActive: isLoading && !done });
10
16
  useEffect(() => {
11
17
  // Skip processing if done (showing historical/final state)
12
18
  if (done) {
@@ -1,12 +1,14 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { ComponentName } from '../types/components.js';
2
+ import { ComponentName } from '../types/types.js';
3
3
  import { Command } from './Command.js';
4
+ import { Confirm } from './Confirm.js';
4
5
  import { Config } from './Config.js';
5
6
  import { Feedback } from './Feedback.js';
6
7
  import { Message } from './Message.js';
7
8
  import { Plan } from './Plan.js';
9
+ import { Refinement } from './Refinement.js';
8
10
  import { Welcome } from './Welcome.js';
9
- export function Component({ def }) {
11
+ export function Component({ def, debug }) {
10
12
  switch (def.name) {
11
13
  case ComponentName.Welcome:
12
14
  return _jsx(Welcome, { ...def.props });
@@ -21,10 +23,20 @@ export function Component({ def }) {
21
23
  return _jsx(Command, { ...props, state: state });
22
24
  }
23
25
  case ComponentName.Plan:
24
- return _jsx(Plan, { ...def.props });
26
+ return _jsx(Plan, { ...def.props, debug: debug });
25
27
  case ComponentName.Feedback:
26
28
  return _jsx(Feedback, { ...def.props });
27
29
  case ComponentName.Message:
28
30
  return _jsx(Message, { ...def.props });
31
+ case ComponentName.Refinement: {
32
+ const props = def.props;
33
+ const state = def.state;
34
+ return _jsx(Refinement, { ...props, state: state });
35
+ }
36
+ case ComponentName.Confirm: {
37
+ const props = def.props;
38
+ const state = def.state;
39
+ return _jsx(Confirm, { ...props, state: state });
40
+ }
29
41
  }
30
42
  }
package/dist/ui/Config.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import React from 'react';
3
- import { Box, Text, useInput, useFocus } from 'ink';
3
+ import { Box, Text, useFocus, useInput } from 'ink';
4
4
  import TextInput from 'ink-text-input';
5
5
  export var StepType;
6
6
  (function (StepType) {
@@ -0,0 +1,41 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React from 'react';
3
+ import { Box, Text, useInput } from 'ink';
4
+ export function Confirm({ message, state, onConfirmed, onCancelled, }) {
5
+ const done = state?.done ?? false;
6
+ const [selectedIndex, setSelectedIndex] = React.useState(0); // 0 = Yes, 1 = No
7
+ useInput((input, key) => {
8
+ if (done)
9
+ return;
10
+ if (key.escape) {
11
+ // Escape: highlight "No" and cancel
12
+ setSelectedIndex(1);
13
+ onCancelled?.();
14
+ }
15
+ else if (key.tab) {
16
+ // Toggle between Yes (0) and No (1)
17
+ setSelectedIndex((prev) => (prev === 0 ? 1 : 0));
18
+ }
19
+ else if (key.return) {
20
+ // Confirm selection
21
+ if (selectedIndex === 0) {
22
+ onConfirmed?.();
23
+ }
24
+ else {
25
+ onCancelled?.();
26
+ }
27
+ }
28
+ }, { isActive: !done });
29
+ const options = [
30
+ { label: 'Yes', value: 'yes', color: '#4a9a7a' }, // green (execute)
31
+ { label: 'No', value: 'no', color: '#a85c3f' }, // dark orange (discard)
32
+ ];
33
+ if (done) {
34
+ // When done, show both the message and user's choice in timeline
35
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { children: message }) }), _jsx(Box, { children: _jsxs(Text, { color: "gray", children: ["> ", options[selectedIndex].label] }) })] }));
36
+ }
37
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { children: message }) }), _jsxs(Box, { children: [_jsx(Text, { color: "#5c8cbc", children: ">" }), _jsx(Text, { children: " " }), _jsx(Box, { children: options.map((option, index) => {
38
+ const isSelected = index === selectedIndex;
39
+ return (_jsx(Box, { marginRight: 2, children: _jsx(Text, { color: isSelected ? option.color : undefined, dimColor: !isSelected, bold: isSelected, children: option.label }) }, option.value));
40
+ }) })] })] }));
41
+ }
@@ -1,6 +1,6 @@
1
1
  import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Box, Text } from 'ink';
3
- import { FeedbackType } from '../types/components.js';
3
+ import { FeedbackType } from '../types/types.js';
4
4
  function getSymbol(type) {
5
5
  return {
6
6
  [FeedbackType.Info]: 'ℹ',
package/dist/ui/Label.js CHANGED
@@ -1,6 +1,6 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from 'ink';
3
3
  import { Separator } from './Separator.js';
4
- export function Label({ description, descriptionColor, type, typeColor, }) {
5
- return (_jsxs(Box, { children: [_jsx(Text, { color: descriptionColor, children: description }), _jsx(Separator, {}), _jsx(Text, { color: typeColor, children: type })] }));
4
+ export function Label({ description, descriptionColor, type, typeColor, showType = false, }) {
5
+ return (_jsxs(Box, { children: [_jsx(Text, { color: descriptionColor, children: description }), showType && (_jsxs(_Fragment, { children: [_jsx(Separator, {}), _jsx(Text, { color: typeColor, children: type })] }))] }));
6
6
  }
package/dist/ui/List.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from 'ink';
3
3
  import { Label } from './Label.js';
4
- export const List = ({ items, level = 0, highlightedIndex = null, highlightedParentIndex = null, }) => {
4
+ export const List = ({ items, level = 0, highlightedIndex = null, highlightedParentIndex = null, showType = false, }) => {
5
5
  const marginLeft = level > 0 ? 4 : 0;
6
6
  return (_jsx(Box, { flexDirection: "column", marginLeft: marginLeft, children: items.map((item, index) => {
7
7
  // At level 0, track which parent is active for child highlighting
@@ -16,6 +16,11 @@ export const List = ({ items, level = 0, highlightedIndex = null, highlightedPar
16
16
  const typeColor = isHighlighted && item.type.highlightedColor
17
17
  ? item.type.highlightedColor
18
18
  : item.type.color;
19
- return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: "whiteBright", children: marker }), _jsx(Label, { description: item.description.text, descriptionColor: descriptionColor, type: item.type.text, typeColor: typeColor })] }), item.children && item.children.length > 0 && (_jsx(List, { items: item.children, level: level + 1, highlightedIndex: shouldHighlightChildren ? highlightedIndex : null }))] }, index));
19
+ // Use highlighted type color for arrow markers when highlighted
20
+ const markerColor = item.markerColor ||
21
+ (isHighlighted && item.type.highlightedColor
22
+ ? item.type.highlightedColor
23
+ : 'whiteBright');
24
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: markerColor, children: marker }), _jsx(Label, { description: item.description.text, descriptionColor: descriptionColor, type: item.type.text, typeColor: typeColor, showType: showType })] }), item.children && item.children.length > 0 && (_jsx(List, { items: item.children, level: level + 1, highlightedIndex: shouldHighlightChildren ? highlightedIndex : null, showType: showType }))] }, index));
20
25
  }) }));
21
26
  };
package/dist/ui/Main.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import React from 'react';
3
- import { ComponentName, TaskType, } from '../types/components.js';
3
+ import { useInput } from 'ink';
4
+ import { ComponentName, FeedbackType, TaskType, } from '../types/types.js';
4
5
  import { createAnthropicService, } from '../services/anthropic.js';
5
- import { FeedbackType } from '../types/components.js';
6
- import { getConfigurationRequiredMessage, hasValidAnthropicKey, loadConfig, saveAnthropicConfig, } from '../services/config.js';
7
- import { createCommandDefinition, createConfigDefinition, createFeedback, createMessage, createPlanDefinition, createWelcomeDefinition, isStateless, markAsDone, } from '../services/components.js';
6
+ import { getConfigurationRequiredMessage, hasValidAnthropicKey, loadConfig, loadDebugSetting, saveAnthropicConfig, saveDebugSetting, } from '../services/config.js';
7
+ import { createCommandDefinition, createConfirmDefinition, createConfigDefinition, createFeedback, createMessage, createRefinement, createPlanDefinition, createWelcomeDefinition, getRefiningMessage, isStateless, markAsDone, } from '../services/components.js';
8
8
  import { exitApp } from '../services/process.js';
9
9
  import { Column } from './Column.js';
10
10
  export const Main = ({ app, command }) => {
@@ -18,6 +18,18 @@ export const Main = ({ app, command }) => {
18
18
  });
19
19
  const [timeline, setTimeline] = React.useState([]);
20
20
  const [queue, setQueue] = React.useState([]);
21
+ const [isDebug, setIsDebug] = React.useState(() => loadDebugSetting());
22
+ // Top-level Shift+Tab handler for debug mode toggle
23
+ // Child components must ignore Shift+Tab to prevent conflicts
24
+ useInput((input, key) => {
25
+ if (key.shift && key.tab) {
26
+ setIsDebug((prev) => {
27
+ const newValue = !prev;
28
+ saveDebugSetting(newValue);
29
+ return newValue;
30
+ });
31
+ }
32
+ }, { isActive: true });
21
33
  const addToTimeline = React.useCallback((...items) => {
22
34
  setTimeline((timeline) => [...timeline, ...items]);
23
35
  }, []);
@@ -46,20 +58,106 @@ export const Main = ({ app, command }) => {
46
58
  return [];
47
59
  });
48
60
  }, [addToTimeline]);
49
- const handlePlanSelectionConfirmed = React.useCallback((selectedIndex, updatedTasks) => {
61
+ const handleAborted = React.useCallback((operationName) => {
50
62
  setQueue((currentQueue) => {
51
63
  if (currentQueue.length === 0)
52
64
  return currentQueue;
53
65
  const [first] = currentQueue;
54
- if (first.name === ComponentName.Plan) {
55
- // Mark plan as done and add it to timeline
66
+ if (!isStateless(first)) {
67
+ addToTimeline(markAsDone(first), createFeedback(FeedbackType.Aborted, `${operationName} was aborted by user`));
68
+ }
69
+ exitApp(0);
70
+ return [];
71
+ });
72
+ }, [addToTimeline]);
73
+ const handleConfigAborted = React.useCallback(() => {
74
+ handleAborted('Configuration');
75
+ }, [handleAborted]);
76
+ const handlePlanAborted = React.useCallback(() => {
77
+ handleAborted('Task selection');
78
+ }, [handleAborted]);
79
+ const handleCommandAborted = React.useCallback(() => {
80
+ handleAborted('Request');
81
+ }, [handleAborted]);
82
+ const handleRefinementAborted = React.useCallback(() => {
83
+ handleAborted('Plan refinement');
84
+ }, [handleAborted]);
85
+ const handleExecutionConfirmed = React.useCallback(() => {
86
+ setQueue((currentQueue) => {
87
+ if (currentQueue.length === 0)
88
+ return currentQueue;
89
+ const [first] = currentQueue;
90
+ if (first.name === ComponentName.Confirm) {
56
91
  addToTimeline(markAsDone(first));
57
92
  }
58
- // Exit after selection is confirmed
59
93
  exitApp(0);
60
94
  return [];
61
95
  });
62
96
  }, [addToTimeline]);
97
+ const handleExecutionCancelled = React.useCallback(() => {
98
+ setQueue((currentQueue) => {
99
+ if (currentQueue.length === 0)
100
+ return currentQueue;
101
+ const [first] = currentQueue;
102
+ if (first.name === ComponentName.Confirm) {
103
+ addToTimeline(markAsDone(first), createFeedback(FeedbackType.Aborted, 'Execution cancelled'));
104
+ }
105
+ exitApp(0);
106
+ return [];
107
+ });
108
+ }, [addToTimeline]);
109
+ const handlePlanSelectionConfirmed = React.useCallback(async (selectedTasks) => {
110
+ // Mark current plan as done and add refinement to queue
111
+ let refinementDef = null;
112
+ refinementDef = createRefinement(getRefiningMessage(), handleRefinementAborted);
113
+ setQueue((currentQueue) => {
114
+ if (currentQueue.length === 0)
115
+ return currentQueue;
116
+ const [first] = currentQueue;
117
+ if (first.name === ComponentName.Plan) {
118
+ addToTimeline(markAsDone(first));
119
+ }
120
+ // Add refinement to queue so it becomes the active component
121
+ return [refinementDef];
122
+ });
123
+ // Process refined command in background
124
+ try {
125
+ const refinedCommand = selectedTasks
126
+ .map((task) => {
127
+ const action = task.action.toLowerCase().replace(/,/g, ' -');
128
+ const type = task.type || 'execute';
129
+ return `${action} (type: ${type})`;
130
+ })
131
+ .join(', ');
132
+ const result = await service.processWithTool(refinedCommand, 'plan');
133
+ // Mark refinement as done and move to timeline
134
+ setQueue((currentQueue) => {
135
+ if (currentQueue.length > 0 &&
136
+ currentQueue[0].id === refinementDef.id) {
137
+ addToTimeline(markAsDone(currentQueue[0]));
138
+ }
139
+ return [];
140
+ });
141
+ // Show final execution plan with confirmation
142
+ const planDefinition = createPlanDefinition(result.message, result.tasks, handlePlanAborted, undefined);
143
+ const confirmDefinition = createConfirmDefinition(handleExecutionConfirmed, handleExecutionCancelled);
144
+ addToTimeline(planDefinition);
145
+ setQueue([confirmDefinition]);
146
+ }
147
+ catch (error) {
148
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
149
+ // Mark refinement as done and move to timeline before showing error
150
+ setQueue((currentQueue) => {
151
+ if (currentQueue.length > 0 &&
152
+ currentQueue[0].id === refinementDef.id) {
153
+ addToTimeline(markAsDone(currentQueue[0]));
154
+ }
155
+ return [];
156
+ });
157
+ addToTimeline(createFeedback(FeedbackType.Failed, 'Unexpected error occurred:', errorMessage));
158
+ exitApp(1);
159
+ }
160
+ }, [addToTimeline, service, handleRefinementAborted]);
63
161
  const handleCommandComplete = React.useCallback((message, tasks) => {
64
162
  setQueue((currentQueue) => {
65
163
  if (currentQueue.length === 0)
@@ -68,23 +166,28 @@ export const Main = ({ app, command }) => {
68
166
  // Check if tasks contain a Define task that requires user interaction
69
167
  const hasDefineTask = tasks.some((task) => task.type === TaskType.Define);
70
168
  if (first.name === ComponentName.Command) {
71
- const planDefinition = createPlanDefinition(message, tasks, hasDefineTask ? handlePlanSelectionConfirmed : undefined);
169
+ const planDefinition = createPlanDefinition(message, tasks, handlePlanAborted, hasDefineTask ? handlePlanSelectionConfirmed : undefined);
72
170
  if (hasDefineTask) {
73
171
  // Don't exit - keep the plan in the queue for interaction
74
172
  addToTimeline(markAsDone(first));
75
173
  return [planDefinition];
76
174
  }
77
175
  else {
78
- // No define task - add plan to timeline and exit
176
+ // No define task - show plan and confirmation
177
+ const confirmDefinition = createConfirmDefinition(handleExecutionConfirmed, handleExecutionCancelled);
79
178
  addToTimeline(markAsDone(first), planDefinition);
80
- exitApp(0);
81
- return [];
179
+ return [confirmDefinition];
82
180
  }
83
181
  }
84
182
  exitApp(0);
85
183
  return [];
86
184
  });
87
- }, [addToTimeline, handlePlanSelectionConfirmed]);
185
+ }, [
186
+ addToTimeline,
187
+ handlePlanSelectionConfirmed,
188
+ handleExecutionConfirmed,
189
+ handleExecutionCancelled,
190
+ ]);
88
191
  const handleConfigFinished = React.useCallback((config) => {
89
192
  const anthropicConfig = config;
90
193
  saveAnthropicConfig(anthropicConfig);
@@ -102,7 +205,7 @@ export const Main = ({ app, command }) => {
102
205
  if (command) {
103
206
  return [
104
207
  ...rest,
105
- createCommandDefinition(command, newService, handleCommandError, handleCommandComplete),
208
+ createCommandDefinition(command, newService, handleCommandError, handleCommandComplete, handleCommandAborted),
106
209
  ];
107
210
  }
108
211
  // No command - exit after showing completion message
@@ -110,25 +213,13 @@ export const Main = ({ app, command }) => {
110
213
  return rest;
111
214
  });
112
215
  }, [addToTimeline, command, handleCommandError, handleCommandComplete]);
113
- const handleConfigAborted = React.useCallback(() => {
114
- setQueue((currentQueue) => {
115
- if (currentQueue.length === 0)
116
- return currentQueue;
117
- const [first] = currentQueue;
118
- if (first.name === ComponentName.Config) {
119
- addToTimeline(markAsDone(first), createFeedback(FeedbackType.Aborted, 'Configuration aborted by user'));
120
- }
121
- exitApp(0);
122
- return [];
123
- });
124
- }, [addToTimeline]);
125
216
  // Initialize queue on mount
126
217
  React.useEffect(() => {
127
218
  const hasConfig = !!service;
128
219
  if (command && hasConfig) {
129
220
  // With command + valid config: [Command]
130
221
  setQueue([
131
- createCommandDefinition(command, service, handleCommandError, handleCommandComplete),
222
+ createCommandDefinition(command, service, handleCommandError, handleCommandComplete, handleCommandAborted),
132
223
  ]);
133
224
  }
134
225
  else if (command && !hasConfig) {
@@ -155,7 +246,13 @@ export const Main = ({ app, command }) => {
155
246
  React.useEffect(() => {
156
247
  processNextInQueue();
157
248
  }, [queue, processNextInQueue]);
249
+ // Exit when queue is empty and timeline has content (all stateless components done)
250
+ React.useEffect(() => {
251
+ if (queue.length === 0 && timeline.length > 0) {
252
+ exitApp(0);
253
+ }
254
+ }, [queue, timeline]);
158
255
  const current = queue.length > 0 ? queue[0] : null;
159
256
  const items = [...timeline, ...(current ? [current] : [])];
160
- return _jsx(Column, { items: items });
257
+ return _jsx(Column, { items: items, debug: isDebug });
161
258
  };
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { Text } from 'ink';
2
+ import { Box, Text } from 'ink';
3
3
  export const Message = ({ text }) => {
4
- return _jsx(Text, { children: text });
4
+ return (_jsx(Box, { children: _jsx(Text, { children: text }) }));
5
5
  };
package/dist/ui/Plan.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useState } from 'react';
3
3
  import { Box, useInput } from 'ink';
4
- import { TaskType } from '../types/components.js';
4
+ import { TaskType } from '../types/types.js';
5
5
  import { Label } from './Label.js';
6
6
  import { List } from './List.js';
7
7
  const ColorPalette = {
@@ -54,6 +54,7 @@ function taskToListItem(task, highlightedChildIndex = null, isDefineTaskWithoutS
54
54
  // Mark define tasks with right arrow when no selection has been made
55
55
  if (isDefineTaskWithoutSelection) {
56
56
  item.marker = ' → ';
57
+ item.markerColor = ColorPalette[TaskType.Plan].type;
57
58
  }
58
59
  // Add children for Define tasks with options
59
60
  if (task.type === TaskType.Define && Array.isArray(task.params?.options)) {
@@ -82,7 +83,7 @@ function taskToListItem(task, highlightedChildIndex = null, isDefineTaskWithoutS
82
83
  }
83
84
  return item;
84
85
  }
85
- export function Plan({ message, tasks, state, onSelectionConfirmed, }) {
86
+ export function Plan({ message, tasks, state, debug = false, onSelectionConfirmed, onAborted, }) {
86
87
  const [highlightedIndex, setHighlightedIndex] = useState(state?.highlightedIndex ?? null);
87
88
  const [currentDefineGroupIndex, setCurrentDefineGroupIndex] = useState(state?.currentDefineGroupIndex ?? 0);
88
89
  const [completedSelections, setCompletedSelections] = useState(state?.completedSelections ?? []);
@@ -103,6 +104,10 @@ export function Plan({ message, tasks, state, onSelectionConfirmed, }) {
103
104
  if (isDone || !defineTask) {
104
105
  return;
105
106
  }
107
+ if (key.escape) {
108
+ onAborted();
109
+ return;
110
+ }
106
111
  if (key.downArrow) {
107
112
  setHighlightedIndex((prev) => {
108
113
  if (prev === null) {
@@ -136,14 +141,27 @@ export function Plan({ message, tasks, state, onSelectionConfirmed, }) {
136
141
  if (state) {
137
142
  state.done = true;
138
143
  }
139
- // Update all tasks and notify parent
140
- const updatedTasks = tasks.map((task, idx) => {
141
- if (defineTaskIndices.includes(idx)) {
142
- return { ...task, type: TaskType.Execute };
144
+ // Build refined task list with only selected options (no discarded or ignored ones)
145
+ const refinedTasks = [];
146
+ tasks.forEach((task, idx) => {
147
+ const defineGroupIndex = defineTaskIndices.indexOf(idx);
148
+ if (defineGroupIndex !== -1 &&
149
+ Array.isArray(task.params?.options)) {
150
+ // This is a Define task - only include the selected option
151
+ const options = task.params.options;
152
+ const selectedIndex = newCompletedSelections[defineGroupIndex];
153
+ refinedTasks.push({
154
+ action: String(options[selectedIndex]),
155
+ type: TaskType.Execute,
156
+ });
157
+ }
158
+ else if (task.type !== TaskType.Ignore &&
159
+ task.type !== TaskType.Discard) {
160
+ // Regular task - keep as is, but skip Ignore and Discard tasks
161
+ refinedTasks.push(task);
143
162
  }
144
- return task;
145
163
  });
146
- onSelectionConfirmed?.(highlightedIndex, updatedTasks);
164
+ onSelectionConfirmed?.(refinedTasks);
147
165
  }
148
166
  }
149
167
  }, { isActive: !isDone && defineTask !== null });
@@ -184,11 +202,12 @@ export function Plan({ message, tasks, state, onSelectionConfirmed, }) {
184
202
  }
185
203
  }
186
204
  }
187
- // Show arrow on current active define task when no child is highlighted
205
+ // Show arrow on current active define task when no child is highlighted and not done
188
206
  const isDefineWithoutSelection = isDefineTask &&
189
207
  defineGroupIndex === currentDefineGroupIndex &&
190
- highlightedIndex === null;
208
+ highlightedIndex === null &&
209
+ !isDone;
191
210
  return taskToListItem(task, childIndex, isDefineWithoutSelection);
192
211
  });
193
- return (_jsxs(Box, { flexDirection: "column", children: [message && (_jsx(Box, { marginBottom: 1, children: _jsx(Label, { description: message, descriptionColor: ColorPalette[TaskType.Plan].description, type: TaskType.Plan, typeColor: ColorPalette[TaskType.Plan].type }) })), _jsx(List, { items: listItems, highlightedIndex: currentDefineTaskIndex >= 0 ? highlightedIndex : null, highlightedParentIndex: currentDefineTaskIndex })] }));
212
+ return (_jsxs(Box, { flexDirection: "column", children: [message && (_jsx(Box, { marginBottom: 1, children: _jsx(Label, { description: message, descriptionColor: ColorPalette[TaskType.Plan].description, type: TaskType.Plan, typeColor: ColorPalette[TaskType.Plan].type, showType: debug }) })), _jsx(List, { items: listItems, highlightedIndex: currentDefineTaskIndex >= 0 ? highlightedIndex : null, highlightedParentIndex: currentDefineTaskIndex, showType: debug })] }));
194
213
  }
@@ -0,0 +1,14 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, useInput } from 'ink';
3
+ import { Message } from './Message.js';
4
+ import { Spinner } from './Spinner.js';
5
+ export const Refinement = ({ text, state, onAborted }) => {
6
+ const isDone = state?.done ?? false;
7
+ useInput((input, key) => {
8
+ if (key.escape && !isDone) {
9
+ onAborted();
10
+ return;
11
+ }
12
+ }, { isActive: !isDone });
13
+ return (_jsxs(Box, { gap: 1, children: [_jsx(Message, { text: text }), !isDone && _jsx(Spinner, {})] }));
14
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prompt-language-shell",
3
- "version": "0.3.4",
3
+ "version": "0.3.8",
4
4
  "description": "Your personal command-line concierge. Ask politely, and it gets things done.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -12,7 +12,8 @@
12
12
  ],
13
13
  "scripts": {
14
14
  "clean": "rm -rf dist",
15
- "build": "npm run clean && tsc && chmod +x dist/index.js && mkdir -p dist/config && cp src/config/*.md dist/config/",
15
+ "typecheck": "tsc --project tsconfig.eslint.json",
16
+ "build": "npm run typecheck && npm run clean && tsc && chmod +x dist/index.js && mkdir -p dist/config && cp src/config/*.md dist/config/",
16
17
  "dev": "npm run build && tsc --watch",
17
18
  "prepare": "husky",
18
19
  "prepublishOnly": "npm run check",