prompt-language-shell 0.2.4 → 0.2.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.
@@ -25,31 +25,115 @@ toward any particular domain and can be validated to work correctly across
25
25
  all scenarios. Do NOT assume or infer domain-specific context unless
26
26
  explicitly provided in skills or user requests.
27
27
 
28
- ## Skills Integration
28
+ ## Response Format
29
+
30
+ Every response MUST include an introductory message before the task list.
31
+ This message should introduce the PLAN, not the execution itself.
29
32
 
30
- If skills are provided in the "Available Skills" section below, you MUST
31
- use them when the user's query matches a skill's domain.
33
+ **Critical rules:**
34
+ - The message is MANDATORY - every single response must include one
35
+ - NEVER repeat the same message - each response should use different wording
36
+ - Must be a SINGLE sentence, maximum 64 characters (including the colon)
37
+ - The message introduces the plan/steps that follow, NOT the action itself
38
+ - ALWAYS end the message with a colon (:)
39
+ - Match the tone to the request (professional, helpful, reassuring)
40
+ - Avoid formulaic patterns - vary your phrasing naturally
41
+
42
+ **Correct examples (introducing the plan):**
43
+ - "Here is my plan:"
44
+ - "Here's what I'll do:"
45
+ - "Let me break this down:"
46
+ - "I've planned the following steps:"
47
+ - "Here's how I'll approach this:"
48
+ - "Here are the steps I'll take:"
49
+ - "This is my plan:"
50
+ - "Let me outline the approach:"
51
+ - "Here's the plan:"
52
+
53
+ **DO NOT:**
54
+ - Use the exact same phrase repeatedly
55
+ - Create overly long or verbose introductions
56
+ - Include unnecessary pleasantries or apologies
57
+ - Use the same sentence structure every time
58
+ - Phrase it as if you're executing (use "plan" language, not "doing" language)
59
+ - Forget the colon at the end
60
+
61
+ Remember: You are presenting a PLAN, not performing the action. The message
62
+ should naturally lead into a list of planned steps. Always end with a colon.
32
63
 
33
- When a query matches a skill:
34
- 1. Recognize the semantic match between the user's request and the skill
35
- description
36
- 2. Check if the skill has parameters (e.g. {PROJECT}) or describes
37
- multiple variants in its description
38
- 3. If skill requires parameters and user didn't specify which variant:
39
- - Create a "define" type task with options listing all variants from the
64
+ ## Skills Integration
65
+
66
+ Skills define the ONLY operations you can execute. If skills are provided in
67
+ the "Available Skills" section below, you MUST use ONLY those skills for
68
+ executable operations.
69
+
70
+ **Skills are EXHAUSTIVE and EXCLUSIVE**
71
+ - The list of available skills is COMPLETE
72
+ - If an action verb does NOT have a matching skill, it CANNOT be executed
73
+ - You MUST create an "ignore" type task for ANY verb without a matching skill
74
+ - There are NO implicit or assumed operations
75
+ - **DO NOT infer follow-up actions based on context**
76
+ - **DO NOT assume operations even if they seem logically related to a matched skill**
77
+ - Example: If only a "backup" skill exists, and user says "backup and restore",
78
+ you create tasks from backup skill + one "ignore" task for "restore"
79
+
80
+ **STRICT SKILL MATCHING RULES:**
81
+
82
+ 1. **Identify skill match:** For each action verb in the user's request,
83
+ check if a corresponding skill exists
84
+ - If a skill exists → use that skill
85
+ - If NO skill exists → create "ignore" type task
86
+ - **NEVER create execute tasks for unmatched verbs under ANY circumstances**
87
+ - This includes common verbs like "analyze", "validate", "initialize",
88
+ "configure", "setup" if no corresponding skill exists
89
+ - Do NOT infer or assume operations - only use explicitly defined skills
90
+
91
+ 2. **Check for Execution section (CRITICAL):**
92
+ - If the skill has an "Execution" section, you MUST use it as the
93
+ authoritative source for task commands
94
+ - Each line in the Execution section corresponds to one task
95
+ - Extract the exact command or operation from each Execution line
96
+ - Replace parameter placeholders (e.g., {TARGET}, {ENV}) with specified values
97
+ - The action field must reference the specific command from Execution
98
+ - **IMPORTANT**: Once you determine the execution steps from the skill,
99
+ you MUST verify that each step matches a command present in the
100
+ Execution section. If a step does NOT have a corresponding command in
101
+ the Execution section, it should NOT be included in the task list.
102
+ - If no Execution section exists, fall back to the Steps section
103
+
104
+ 3. **Handle skill parameters:**
105
+ - Check if the skill has parameters (e.g., {PROJECT}) or describes multiple
106
+ variants in its description
107
+ - If skill requires parameters and user didn't specify which variant:
108
+ Create a "define" type task with options listing all variants from the
40
109
  skill description
41
- - Extract variants from the skill's description section
42
- 4. If user specified the variant or skill has no parameters:
43
- - Extract the individual steps from the skill's "Steps" section
44
- - Replace parameter placeholders (e.g., {BROWSER}) with the specified value
110
+ - If user specified the variant or skill has no parameters:
111
+ Extract the individual steps from the skill's "Execution" or "Steps"
112
+ section (prefer Execution if available)
113
+ - Replace ALL parameter placeholders with the specified value
114
+
115
+ 4. **Handle partial execution:**
116
+ - Keywords indicating partial execution: "only", "just", specific verbs
117
+ that match individual step names
118
+ - Consult the skill's Description section for guidance on which steps are
119
+ optional or conditional
120
+ - Example: If description says "initialization only required for clean
121
+ builds" and user says "rebuild cache", skip initialization steps
122
+ - Only extract steps that align with the user's specific request
123
+
124
+ 5. **Create task definitions:**
45
125
  - Create a task definition for each step with:
46
126
  - action: clear, professional description starting with a capital letter
47
- - type: category of operation (if the skill specifies it or you
48
- can infer it)
127
+ - type: category of operation (if the skill specifies it or you can infer it)
49
128
  - params: any specific parameters mentioned in the step
50
- 5. If the user's query includes additional requirements beyond the skill,
51
- append those as additional task definitions
52
- 6. NEVER replace the skill's detailed steps with a generic restatement
129
+ - NEVER replace the skill's detailed steps with a generic restatement
130
+
131
+ 6. **Handle additional requirements beyond the skill:**
132
+ - If the user's query includes additional requirements beyond the skill,
133
+ check if those requirements match OTHER available skills
134
+ - If they match a skill → append tasks from that skill
135
+ - If they do NOT match any skill → append "ignore" type task
136
+ - NEVER create generic execute tasks for unmatched requirements
53
137
 
54
138
  Example 1 - Skill with parameter, variant specified:
55
139
  - Skill has {PROJECT} parameter with variants: Alpha, Beta, Gamma
@@ -73,6 +157,28 @@ Example 3 - Skill without parameters:
73
157
  - Correct: Four tasks (the three from skill + one for report generation)
74
158
  - WRONG: Two tasks ("run tests", "generate a report")
75
159
 
160
+ Example 4 - NEGATIVE: Unmatched verb after matched skill:
161
+ - ONLY skill available: "backup" (with steps: connect, export, save)
162
+ - User: "backup data and archive it"
163
+ - CORRECT: Three tasks from backup skill + one "ignore" type task with
164
+ action "Ignore unknown 'archive' request"
165
+ - WRONG: Three tasks from backup skill + one execute task "Archive the
166
+ backed up data"
167
+
168
+ Example 5 - NEGATIVE: Multiple unmatched verbs:
169
+ - ONLY skill available: "sync" (with steps: connect, transfer, verify)
170
+ - User: "sync files and encrypt them, then notify me"
171
+ - CORRECT: Three tasks from sync skill + one "ignore" for "encrypt" +
172
+ one "ignore" for "notify"
173
+ - WRONG: Creating execute tasks for "encrypt" or "notify"
174
+
175
+ Example 6 - NEGATIVE: Context inference prohibition:
176
+ - ONLY skill available: "process" (with steps: load, transform, output)
177
+ - User: "process dataset and validate results"
178
+ - CORRECT: Three tasks from process skill + one "ignore" type task for
179
+ "validate"
180
+ - WRONG: Adding an execute task like "Validate the processed dataset results"
181
+
76
182
  ### Skills and Unclear Requests
77
183
 
78
184
  When a request is vague and could match multiple skills or multiple operations
@@ -132,12 +238,16 @@ Examples that should be aborted as offensive:
132
238
  - Extract steps from the matching skill and create tasks for each step
133
239
 
134
240
  3. **Logical consequences** - Infer natural workflow steps:
135
- - "build" and "deploy" skills exist, user says "build and release" →
136
- Most likely means "build and deploy" since "release" often means
137
- "deploy" after building
241
+ - "backup" and "sync" skills exist, user says "backup and upload" →
242
+ Most likely means "backup and sync" since "upload" often means
243
+ "sync" after backup
138
244
  - Use context and available skills to infer the logical interpretation
139
245
  - IMPORTANT: Only infer if matching skills exist. If no matching skill
140
246
  exists, use "ignore" type
247
+ - **Strict skill matching:** For action verbs representing executable
248
+ operations, you MUST have a matching skill. If a user requests an action
249
+ that has no corresponding skill, create an "ignore" type task. Do NOT
250
+ create generic "execute" type tasks for commands without matching skills.
141
251
 
142
252
  **For requests with unclear subject:**
143
253
 
@@ -223,6 +333,7 @@ When creating task definitions, focus on:
223
333
  with precise, contextually appropriate alternatives. Use professional, clear
224
334
  terminology suitable for technical documentation. Maintain natural, fluent
225
335
  English phrasing while preserving the original intent.
336
+ **Keep action descriptions concise, at most 64 characters.**
226
337
 
227
338
  - **Type**: Categorize the operation using one of these supported types:
228
339
  - `config` - Configuration changes, settings updates
@@ -256,6 +367,21 @@ steps to answer:
256
367
  1. Identify each individual task or step
257
368
  2. Break complex questions into separate, simpler task definitions
258
369
  3. Create a task definition for each distinct operation
370
+ 4. **For each operation, independently check if it matches a skill:**
371
+ - If operation matches a skill → extract skill steps
372
+ - If operation does NOT match a skill → create "ignore" type task
373
+ - **CRITICAL: Do NOT infer context or create generic execute tasks for
374
+ unmatched operations**
375
+ - Even if an unmatched operation appears after a matched skill, treat it
376
+ independently
377
+ - Do NOT create tasks like "Verify the processed X" or "Check X results"
378
+ for unmatched operations
379
+ - The ONLY valid types for unmatched operations are "ignore" or "answer"
380
+ (for information requests)
381
+ - Example: "process files and validate" where only "process" has a skill
382
+ → Create tasks from process skill + create "ignore" type for "validate"
383
+ - Example: "deploy service and monitor" where only "deploy" has a skill
384
+ → Create tasks from deploy skill + create "ignore" type for "monitor"
259
385
 
260
386
  When breaking down complex questions:
261
387
 
@@ -456,11 +582,13 @@ Examples showing proper use of skills and disambiguation:
456
582
  environment", "Deploy to canary environment"] }
457
583
  - "deploy all" with deploy skill (staging, production) → Two tasks: one for
458
584
  staging deployment, one for production deployment
459
- - "build and run" with build and run skills → Create tasks from build skill
460
- + run skill
461
- - "build Beta and lint" with build skill (has {PROJECT} parameter) but NO
462
- lint skill → Four tasks: three from build skill (with {PROJECT}=Beta) +
463
- one "ignore" type for unknown "lint"
585
+ - "backup and restore" with backup and restore skills → Create tasks from
586
+ backup skill + restore skill
587
+ - "backup photos and verify" with backup skill (has {TYPE} parameter) but NO
588
+ verify skill → Two tasks from backup skill (with {TYPE}=photos) + one
589
+ "ignore" type for unknown "verify"
590
+ - "analyze data and generate report" with analyze skill but NO generate skill →
591
+ Tasks from analyze skill + one "ignore" type for unknown "generate"
464
592
 
465
593
  ### Correct Examples: Requests Without Matching Skills
466
594
 
package/dist/index.js CHANGED
@@ -3,8 +3,8 @@ import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { existsSync, readFileSync } from 'fs';
4
4
  import { dirname, join } from 'path';
5
5
  import { fileURLToPath } from 'url';
6
- import { render, Text } from 'ink';
7
- import { ConfigError, configExists, loadConfig, saveConfig, } from './services/config.js';
6
+ import { render } from 'ink';
7
+ import { hasValidConfig, loadConfig, saveAnthropicConfig, } from './services/config.js';
8
8
  import { createAnthropicService } from './services/anthropic.js';
9
9
  import { Main } from './ui/Main.js';
10
10
  const __filename = fileURLToPath(import.meta.url);
@@ -27,29 +27,25 @@ const app = {
27
27
  const args = process.argv.slice(2);
28
28
  const command = args.join(' ').trim() || null;
29
29
  async function runApp() {
30
- // First-time setup: config doesn't exist
31
- if (!configExists()) {
32
- const { waitUntilExit } = render(_jsx(Main, { app: app, command: command, isReady: false, onConfigured: (config) => {
33
- saveConfig('anthropic', config);
34
- // Create service once for the session
35
- return command ? createAnthropicService(config) : undefined;
36
- } }));
37
- await waitUntilExit();
38
- return;
39
- }
40
- // Try to load and validate config
41
- try {
30
+ // Happy path: valid config exists
31
+ if (hasValidConfig()) {
42
32
  const config = loadConfig();
43
- // Create service once at app initialization
44
33
  const service = createAnthropicService(config.anthropic);
45
34
  render(_jsx(Main, { app: app, command: command, service: service, isReady: true }));
35
+ return;
46
36
  }
47
- catch (error) {
48
- if (error instanceof ConfigError) {
49
- render(_jsx(Text, { color: "red", children: error.message }));
50
- process.exit(1);
51
- }
52
- throw error;
53
- }
37
+ // Setup: config doesn't exist or is invalid
38
+ const { waitUntilExit, unmount } = render(_jsx(Main, { app: app, command: command, isReady: false, onConfigured: (config) => {
39
+ saveAnthropicConfig(config);
40
+ if (command) {
41
+ return createAnthropicService(config);
42
+ }
43
+ else {
44
+ // No command - exit after showing completion message
45
+ setTimeout(() => unmount(), 100);
46
+ return undefined;
47
+ }
48
+ } }));
49
+ await waitUntilExit();
54
50
  }
55
51
  runApp();
@@ -40,8 +40,11 @@ export class AnthropicService {
40
40
  throw new Error('Expected tool_use response from Claude API');
41
41
  }
42
42
  const content = response.content[0];
43
- // Extract and validate tasks array
43
+ // Extract and validate message and tasks
44
44
  const input = content.input;
45
+ if (!input.message || typeof input.message !== 'string') {
46
+ throw new Error('Invalid tool response: missing or invalid message field');
47
+ }
45
48
  if (!input.tasks || !Array.isArray(input.tasks)) {
46
49
  throw new Error('Invalid tool response: missing or invalid tasks array');
47
50
  }
@@ -53,6 +56,7 @@ export class AnthropicService {
53
56
  });
54
57
  const isDebug = process.env.DEBUG === 'true';
55
58
  return {
59
+ message: input.message,
56
60
  tasks: input.tasks,
57
61
  systemPrompt: isDebug ? systemPrompt : undefined,
58
62
  };
@@ -3,9 +3,11 @@ import { homedir } from 'os';
3
3
  import { join } from 'path';
4
4
  import YAML from 'yaml';
5
5
  export class ConfigError extends Error {
6
- constructor(message) {
6
+ origin;
7
+ constructor(message, origin) {
7
8
  super(message);
8
9
  this.name = 'ConfigError';
10
+ this.origin = origin;
9
11
  }
10
12
  }
11
13
  const CONFIG_FILE = join(homedir(), '.plsrc');
@@ -14,30 +16,21 @@ function parseYamlConfig(content) {
14
16
  return YAML.parse(content);
15
17
  }
16
18
  catch (error) {
17
- throw new ConfigError(`\nFailed to parse YAML configuration file at ${CONFIG_FILE}\n` +
18
- `Error: ${error instanceof Error ? error.message : String(error)}`);
19
+ throw new ConfigError('Failed to parse configuration file', error instanceof Error ? error : undefined);
19
20
  }
20
21
  }
21
22
  function validateConfig(parsed) {
22
23
  if (!parsed || typeof parsed !== 'object') {
23
- throw new ConfigError(`\nInvalid configuration format in ${CONFIG_FILE}\n` +
24
- 'Expected a YAML object with configuration settings.');
24
+ throw new ConfigError('Invalid configuration format');
25
25
  }
26
26
  const config = parsed;
27
27
  // Validate anthropic section
28
28
  if (!config.anthropic || typeof config.anthropic !== 'object') {
29
- throw new ConfigError(`\nMissing or invalid 'anthropic' section in ${CONFIG_FILE}\n` +
30
- 'Please add:\n' +
31
- 'anthropic:\n' +
32
- ' key: sk-ant-...');
29
+ throw new ConfigError('Missing or invalid anthropic configuration');
33
30
  }
34
- const anthropic = config.anthropic;
35
- const key = anthropic['key'];
31
+ const { key, model } = config.anthropic;
36
32
  if (!key || typeof key !== 'string') {
37
- throw new ConfigError(`\nMissing or invalid 'anthropic.key' in ${CONFIG_FILE}\n` +
38
- 'Please add your Anthropic API key:\n' +
39
- 'anthropic:\n' +
40
- ' key: sk-ant-...');
33
+ throw new ConfigError('Missing or invalid API key');
41
34
  }
42
35
  const validatedConfig = {
43
36
  anthropic: {
@@ -45,19 +38,14 @@ function validateConfig(parsed) {
45
38
  },
46
39
  };
47
40
  // Optional model
48
- if (anthropic.model && typeof anthropic.model === 'string') {
49
- validatedConfig.anthropic.model = anthropic.model;
41
+ if (model && typeof model === 'string') {
42
+ validatedConfig.anthropic.model = model;
50
43
  }
51
44
  return validatedConfig;
52
45
  }
53
46
  export function loadConfig() {
54
47
  if (!existsSync(CONFIG_FILE)) {
55
- throw new ConfigError(`\nConfiguration file not found at ${CONFIG_FILE}\n\n` +
56
- 'Please create it with your Anthropic API key.\n' +
57
- 'Example:\n\n' +
58
- 'anthropic:\n' +
59
- ' key: sk-ant-...\n' +
60
- ' model: claude-haiku-4-5-20251001\n');
48
+ throw new ConfigError('Configuration not found');
61
49
  }
62
50
  const content = readFileSync(CONFIG_FILE, 'utf-8');
63
51
  const parsed = parseYamlConfig(content);
@@ -69,6 +57,15 @@ export function getConfigPath() {
69
57
  export function configExists() {
70
58
  return existsSync(CONFIG_FILE);
71
59
  }
60
+ export function hasValidConfig() {
61
+ try {
62
+ const config = loadConfig();
63
+ return !!config.anthropic.key;
64
+ }
65
+ catch {
66
+ return false;
67
+ }
68
+ }
72
69
  export function mergeConfig(existingContent, sectionName, newValues) {
73
70
  const parsed = existingContent.trim()
74
71
  ? YAML.parse(existingContent)
@@ -95,3 +92,6 @@ export function saveConfig(section, config) {
95
92
  const newContent = mergeConfig(existingContent, section, config);
96
93
  writeFileSync(CONFIG_FILE, newContent, 'utf-8');
97
94
  }
95
+ export function saveAnthropicConfig(config) {
96
+ saveConfig('anthropic', config);
97
+ }
@@ -4,6 +4,10 @@ export const planTool = {
4
4
  input_schema: {
5
5
  type: 'object',
6
6
  properties: {
7
+ message: {
8
+ type: 'string',
9
+ description: 'Introductory reply to display before the task list. Must be a single sentence, maximum 64 characters (including the colon at the end). Vary this naturally - try to use a different phrase each time.',
10
+ },
7
11
  tasks: {
8
12
  type: 'array',
9
13
  description: 'Array of planned tasks to execute',
@@ -27,6 +31,6 @@ export const planTool = {
27
31
  },
28
32
  },
29
33
  },
30
- required: ['tasks'],
34
+ required: ['message', 'tasks'],
31
35
  },
32
36
  };
@@ -9,3 +9,9 @@ export var TaskType;
9
9
  TaskType["Ignore"] = "ignore";
10
10
  TaskType["Select"] = "select";
11
11
  })(TaskType || (TaskType = {}));
12
+ export var FeedbackType;
13
+ (function (FeedbackType) {
14
+ FeedbackType["Succeeded"] = "succeeded";
15
+ FeedbackType["Aborted"] = "aborted";
16
+ FeedbackType["Failed"] = "failed";
17
+ })(FeedbackType || (FeedbackType = {}));
package/dist/ui/Column.js CHANGED
@@ -2,5 +2,5 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Box } from 'ink';
3
3
  import { Component } from './Component.js';
4
4
  export const Column = ({ items }) => {
5
- return (_jsx(Box, { marginTop: 1, flexDirection: "column", gap: 1, children: items.map((item, index) => (_jsx(Box, { children: _jsx(Component, { def: item }) }, index))) }));
5
+ return (_jsx(Box, { marginTop: 1, marginBottom: 1, flexDirection: "column", gap: 1, children: items.map((item, index) => (_jsx(Box, { children: _jsx(Component, { def: item }) }, index))) }));
6
6
  };
@@ -3,6 +3,7 @@ import { useEffect, useState } from 'react';
3
3
  import { Box, Text } from 'ink';
4
4
  import { TaskType } from '../types/components.js';
5
5
  import { List } from './List.js';
6
+ import { Separator } from './Separator.js';
6
7
  import { Spinner } from './Spinner.js';
7
8
  const MIN_PROCESSING_TIME = 1000; // purely for visual effect
8
9
  // Color palette
@@ -68,10 +69,11 @@ function taskToListItem(task) {
68
69
  }
69
70
  return item;
70
71
  }
71
- export function Command({ command, state, service, error: errorProp, children, }) {
72
+ export function Command({ command, state, service, error: errorProp, children, onError, onComplete, }) {
72
73
  const done = state?.done ?? false;
73
74
  const [error, setError] = useState(state?.error || errorProp || null);
74
75
  const [isLoading, setIsLoading] = useState(state?.isLoading ?? !done);
76
+ const [message, setMessage] = useState('');
75
77
  const [tasks, setTasks] = useState([]);
76
78
  useEffect(() => {
77
79
  // Skip processing if done (showing historical/final state)
@@ -93,8 +95,10 @@ export function Command({ command, state, service, error: errorProp, children, }
93
95
  const remainingTime = Math.max(0, MIN_PROCESSING_TIME - elapsed);
94
96
  await new Promise((resolve) => setTimeout(resolve, remainingTime));
95
97
  if (mounted) {
98
+ setMessage(result.message);
96
99
  setTasks(result.tasks);
97
100
  setIsLoading(false);
101
+ onComplete?.();
98
102
  }
99
103
  }
100
104
  catch (err) {
@@ -102,8 +106,14 @@ export function Command({ command, state, service, error: errorProp, children, }
102
106
  const remainingTime = Math.max(0, MIN_PROCESSING_TIME - elapsed);
103
107
  await new Promise((resolve) => setTimeout(resolve, remainingTime));
104
108
  if (mounted) {
105
- setError(err instanceof Error ? err.message : 'Unknown error occurred');
109
+ const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
106
110
  setIsLoading(false);
111
+ if (onError) {
112
+ onError(errorMessage);
113
+ }
114
+ else {
115
+ setError(errorMessage);
116
+ }
107
117
  }
108
118
  }
109
119
  }
@@ -112,5 +122,5 @@ export function Command({ command, state, service, error: errorProp, children, }
112
122
  mounted = false;
113
123
  };
114
124
  }, [command, done, service]);
115
- return (_jsxs(Box, { alignSelf: "flex-start", marginBottom: 1, flexDirection: "column", children: [_jsxs(Box, { children: [_jsxs(Text, { color: "gray", children: ["> pls ", command] }), isLoading && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Spinner, {})] }))] }), error && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "red", children: ["Error: ", error] }) })), !isLoading && tasks.length > 0 && (_jsx(Box, { marginTop: 1, children: _jsx(List, { items: tasks.map(taskToListItem) }) })), children] }));
125
+ return (_jsxs(Box, { alignSelf: "flex-start", flexDirection: "column", marginLeft: 1, children: [_jsxs(Box, { children: [_jsxs(Text, { color: "gray", children: ["> pls ", command] }), isLoading && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Spinner, {})] }))] }), error && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "red", children: ["Error: ", error] }) })), !isLoading && tasks.length > 0 && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [message && (_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { children: [" ", message] }), _jsx(Separator, { color: "#9c5ccc" }), _jsx(Text, { color: "#9c5ccc", children: "plan" })] })), _jsx(List, { items: tasks.map(taskToListItem) })] })), children] }));
116
126
  }
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Command } from './Command.js';
3
3
  import { Config } from './Config.js';
4
+ import { Feedback } from './Feedback.js';
4
5
  import { Welcome } from './Welcome.js';
5
6
  export function Component({ def }) {
6
7
  switch (def.name) {
@@ -11,6 +12,8 @@ export function Component({ def }) {
11
12
  const state = def.state;
12
13
  return _jsx(Config, { ...props, state: state });
13
14
  }
15
+ case 'feedback':
16
+ return _jsx(Feedback, { ...def.props });
14
17
  case 'command': {
15
18
  const props = def.props;
16
19
  const state = def.state;
package/dist/ui/Config.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
2
  import React from 'react';
3
- import { Box, Text } from 'ink';
3
+ import { Box, Text, useInput } from 'ink';
4
4
  import TextInput from 'ink-text-input';
5
- export function Config({ steps, state, onFinished }) {
5
+ export function Config({ steps, state, onFinished, onAborted }) {
6
6
  const done = state?.done ?? false;
7
7
  const [step, setStep] = React.useState(done ? steps.length : 0);
8
8
  const [values, setValues] = React.useState(() => {
@@ -15,9 +15,26 @@ export function Config({ steps, state, onFinished }) {
15
15
  return initial;
16
16
  });
17
17
  const [inputValue, setInputValue] = React.useState('');
18
+ const normalizeValue = (value) => {
19
+ if (value === null || value === undefined) {
20
+ return '';
21
+ }
22
+ return value.replace(/\n/g, '').trim();
23
+ };
24
+ useInput((input, key) => {
25
+ if (key.escape && !done && step < steps.length) {
26
+ if (onAborted) {
27
+ onAborted();
28
+ }
29
+ }
30
+ });
18
31
  const handleSubmit = (value) => {
19
32
  const currentStepConfig = steps[step];
20
- const finalValue = value.trim() || currentStepConfig.value || '';
33
+ const finalValue = normalizeValue(value) || normalizeValue(currentStepConfig.value);
34
+ // Don't allow empty value if step has no default (mandatory field)
35
+ if (!finalValue && !currentStepConfig.value) {
36
+ return;
37
+ }
21
38
  const newValues = { ...values, [currentStepConfig.key]: finalValue };
22
39
  setValues(newValues);
23
40
  setInputValue('');
@@ -32,13 +49,14 @@ export function Config({ steps, state, onFinished }) {
32
49
  setStep(step + 1);
33
50
  }
34
51
  };
35
- return (_jsxs(Box, { flexDirection: "column", marginLeft: 1, children: [steps.map((stepConfig, index) => {
36
- const isCurrentStep = index === step && !done;
37
- const isCompleted = index < step || done;
38
- const shouldShow = isCompleted || isCurrentStep;
39
- if (!shouldShow) {
40
- return null;
41
- }
42
- return (_jsxs(Box, { flexDirection: "column", marginTop: index === 0 ? 0 : 1, children: [_jsx(Box, { children: _jsxs(Text, { children: [stepConfig.description, ":"] }) }), _jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: "> " }), isCurrentStep ? (_jsx(TextInput, { value: inputValue, onChange: setInputValue, onSubmit: handleSubmit })) : (_jsx(Text, { dimColor: true, children: values[stepConfig.key] || '' }))] })] }, stepConfig.key));
43
- }), step === steps.length && !done && (_jsx(Box, { marginY: 1, children: _jsx(Text, { color: "green", children: "\u2713 Configuration complete" }) }))] }));
52
+ return (_jsx(Box, { flexDirection: "column", marginLeft: 1, children: steps.map((stepConfig, index) => {
53
+ const isCurrentStep = index === step && !done;
54
+ const isCompleted = index < step;
55
+ const wasAborted = index === step && done;
56
+ const shouldShow = isCompleted || isCurrentStep || wasAborted;
57
+ if (!shouldShow) {
58
+ return null;
59
+ }
60
+ return (_jsxs(Box, { flexDirection: "column", marginTop: index === 0 ? 0 : 1, children: [_jsx(Box, { children: _jsxs(Text, { children: [stepConfig.description, ":"] }) }), _jsxs(Box, { children: [_jsx(Text, { children: " " }), _jsx(Text, { color: "#5c8cbc", dimColor: !isCurrentStep, children: ">" }), _jsx(Text, { children: " " }), isCurrentStep ? (_jsx(TextInput, { value: inputValue, onChange: setInputValue, onSubmit: handleSubmit, placeholder: stepConfig.value || undefined })) : (_jsx(Text, { dimColor: true, children: values[stepConfig.key] || '' }))] })] }, stepConfig.key));
61
+ }) }));
44
62
  }
@@ -0,0 +1,22 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import { FeedbackType } from '../types/components.js';
4
+ function getSymbol(type) {
5
+ return {
6
+ [FeedbackType.Succeeded]: '✓',
7
+ [FeedbackType.Aborted]: '⊘',
8
+ [FeedbackType.Failed]: '✗',
9
+ }[type];
10
+ }
11
+ function getColor(type) {
12
+ return {
13
+ [FeedbackType.Succeeded]: '#00aa00', // green
14
+ [FeedbackType.Aborted]: '#cc9c5c', // orange
15
+ [FeedbackType.Failed]: '#aa0000', // red
16
+ }[type];
17
+ }
18
+ export function Feedback({ type, message }) {
19
+ const color = getColor(type);
20
+ const symbol = getSymbol(type);
21
+ return (_jsx(Box, { marginLeft: 1, children: _jsxs(Text, { color: color, children: [symbol, " ", message] }) }));
22
+ }