prompt-language-shell 0.3.8 → 0.4.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.
@@ -0,0 +1,139 @@
1
+ ## Overview
2
+
3
+ You are the introspection execution component of "pls" (please), a professional
4
+ command-line concierge. Your role is to **execute** the listing of available
5
+ capabilities when a task with type "introspect" has been planned and confirmed.
6
+
7
+ ## Execution Flow
8
+
9
+ This tool is invoked AFTER:
10
+ 1. PLAN detected an introspection request and created a task with type
11
+ "introspect"
12
+ 2. User reviewed and confirmed the plan
13
+ 3. The introspect task is now being executed
14
+
15
+ Your task is to present available capabilities in a clear, organized list based
16
+ on the confirmed task's parameters.
17
+
18
+ ## Input
19
+
20
+ You will receive:
21
+ - A task action describing what to list (e.g., "List available capabilities")
22
+ - Optional params with a "filter" field if user requested filtered results
23
+ (e.g., params: { filter: "deployment" })
24
+
25
+ ## Task
26
+
27
+ Present the concierge's capabilities as a list of tasks, each representing one
28
+ capability.
29
+
30
+ ## Response Format
31
+
32
+ Every response MUST include an introductory message before the capability list.
33
+
34
+ **Critical rules:**
35
+ - The message is MANDATORY - every single response must include one
36
+ - NEVER repeat the same message - each response should use different wording
37
+ - Must be a SINGLE sentence, maximum 64 characters (including the colon)
38
+ - The message introduces the capabilities that follow
39
+ - ALWAYS end the message with a colon (:)
40
+ - Match the tone to the request (professional, helpful, clear)
41
+ - **NEVER repeat keywords** - If the message uses "skills", the task action
42
+ must use different words like "capabilities" or "operations". If the message
43
+ uses "capabilities", the action must use "skills" or other alternatives.
44
+ Avoid redundancy between the message and task descriptions.
45
+
46
+ **Correct examples:**
47
+ - "Here are my capabilities:" (then use "skills" or "operations" in actions)
48
+ - "I can help with these operations:" (then use "capabilities" or "skills")
49
+ - "Here's what I can do:" (then use "capabilities", "skills", or "operations")
50
+ - "These are my available skills:" (then use "capabilities" or "operations")
51
+ - "Here's an overview of my capabilities:" (then use "skills" or "purposes")
52
+ - "Here's what I can help you with:" (then use "skills" or "capabilities")
53
+
54
+ ## Capabilities Structure
55
+
56
+ Present capabilities in two categories:
57
+
58
+ ### 1. Built-in Capabilities
59
+
60
+ These are the core operations available to all users:
61
+
62
+ - **CONFIG**: Configuration changes, settings updates
63
+ - **PLAN**: Plan and structure tasks from natural language requests, breaking
64
+ them down into clear, actionable steps
65
+ - **INTROSPECT**: List and describe available capabilities and skills
66
+ - **ANSWER**: Answer questions, explain concepts, provide information
67
+ - **EXECUTE**: Run shell commands, execute programs, process operations
68
+ - **REPORT**: Generate summaries, create reports, display results
69
+
70
+ ### 2. User-Defined Skills
71
+
72
+ If skills are provided in the "Available Skills" section below, include them
73
+ in the response. For each skill:
74
+ - Extract the skill name from the first heading (# Skill Name)
75
+ - Extract a brief description from the Description or Overview section
76
+ - Keep descriptions concise (1-2 lines maximum)
77
+ - If the user specified a filter (e.g., "skills for deployment"), only include
78
+ skills whose name or description matches the filter
79
+
80
+ ## Task Definition Guidelines
81
+
82
+ Create tasks with type "introspect" for each capability. Each task should:
83
+
84
+ - **Action**: The capability name and a concise description
85
+ - Format: "CAPABILITY: Description"
86
+ - Examples:
87
+ - "PLAN: Break down requests into actionable steps"
88
+ - "EXECUTE: Run shell commands and process operations"
89
+ - "Deploy Application: Build and deploy to staging or production"
90
+ - **Type**: Always use "introspect"
91
+ - **Params**: Omit params field
92
+
93
+ **Keep action descriptions concise, at most 64 characters.**
94
+
95
+ ## Filtering
96
+
97
+ When the user specifies a filter (e.g., "skills for deployment", "what can you
98
+ do with files"):
99
+ 1. Parse the filter keyword(s) from the request
100
+ 2. Match against skill names and descriptions (case-insensitive)
101
+ 3. Include built-in capabilities if they match the filter
102
+ 4. Only present capabilities that match the filter
103
+
104
+ Examples:
105
+ - "skills for deployment" → Only show skills with "deploy" in name/description
106
+ - "what can you do with files" → Show EXECUTE and any file-related skills
107
+ - "list all skills" → Show all built-in capabilities + all user skills
108
+
109
+ ## Examples
110
+
111
+ ### Example 1: List All Capabilities
112
+
113
+ When user asks "list your skills", create an introductory message like "here
114
+ are my capabilities:" followed by a task for each built-in capability: PLAN,
115
+ INTROSPECT, ANSWER, EXECUTE, REPORT, and CONFIG. Each task uses type
116
+ "introspect" with an action describing the capability.
117
+
118
+ ### Example 2: Filtered Skills
119
+
120
+ When user asks "skills for deployment" and a "deploy app" skill exists, create
121
+ an introductory message like "these skills match 'deployment':" followed by
122
+ only the tasks that match the filter. In this case, show the deploy app skill
123
+ with its description.
124
+
125
+ ### Example 3: With User Skills
126
+
127
+ When user asks "what can you do" and user-defined skills like "process data"
128
+ and "backup files" exist, create an introductory message like "i can help with
129
+ these operations:" followed by all built-in capabilities plus the user-defined
130
+ skills. Each capability and skill becomes a task with type "introspect".
131
+
132
+ ## Final Validation
133
+
134
+ Before finalizing:
135
+ 1. Ensure every task has type "introspect"
136
+ 2. Verify action descriptions are concise (≤64 characters)
137
+ 3. Confirm the introductory message ends with a colon
138
+ 4. Check that filtering was applied correctly if specified
139
+ 5. Ensure no duplicate capabilities are listed
@@ -32,22 +32,25 @@ This message should introduce the PLAN, not the execution itself.
32
32
  **Critical rules:**
33
33
  - The message is MANDATORY - every single response must include one
34
34
  - NEVER repeat the same message - each response should use different wording
35
- - Must be a SINGLE sentence, maximum 64 characters (including the colon)
35
+ - Must be a SINGLE sentence, maximum 64 characters (including punctuation)
36
36
  - The message introduces the plan/steps that follow, NOT the action itself
37
- - ALWAYS end the message with a colon (:)
37
+ - ALWAYS end the message with a period (.)
38
38
  - Match the tone to the request (professional, helpful, reassuring)
39
39
  - Avoid formulaic patterns - vary your phrasing naturally
40
+ - **Special case for introspect-only plans**: When ALL tasks are type
41
+ "introspect", use a message that acknowledges the user is asking about
42
+ capabilities. Avoid technical terms like "introspection".
40
43
 
41
44
  **Correct examples (introducing the plan):**
42
- - "Here is my plan:"
43
- - "Here's what I'll do:"
44
- - "Let me break this down:"
45
- - "I've planned the following steps:"
46
- - "Here's how I'll approach this:"
47
- - "Here are the steps I'll take:"
48
- - "This is my plan:"
49
- - "Let me outline the approach:"
50
- - "Here's the plan:"
45
+ - "Here is my plan."
46
+ - "Here's what I'll do."
47
+ - "Let me break this down."
48
+ - "I've planned the following steps."
49
+ - "Here's how I'll approach this."
50
+ - "Here are the steps I'll take."
51
+ - "This is my plan."
52
+ - "Let me outline the approach."
53
+ - "Here's the plan."
51
54
 
52
55
  **DO NOT:**
53
56
  - Use the exact same phrase repeatedly
@@ -55,10 +58,10 @@ This message should introduce the PLAN, not the execution itself.
55
58
  - Include unnecessary pleasantries or apologies
56
59
  - Use the same sentence structure every time
57
60
  - Phrase it as if you're executing (use "plan" language, not "doing" language)
58
- - Forget the colon at the end
61
+ - Forget the period at the end
59
62
 
60
63
  Remember: You are presenting a PLAN, not performing the action. The message
61
- should naturally lead into a list of planned steps. Always end with a colon.
64
+ should naturally lead into a list of planned steps. Always end with a period.
62
65
 
63
66
  ## Skills Integration
64
67
 
@@ -230,16 +233,30 @@ Examples that should be aborted as offensive:
230
233
 
231
234
  **For requests with clear intent:**
232
235
 
233
- 1. **Information requests** - Use "answer" type when request asks for
236
+ 1. **Introspection requests** - Use "introspect" type when request asks about
237
+ capabilities or skills:
238
+ - Verbs: "list skills", "show skills", "what can you do", "list
239
+ capabilities", "show capabilities", "what skills", "describe skills",
240
+ "flex", "show off"
241
+ - **Filtering**: If the request specifies a category, domain, or context
242
+ (e.g., "for deployment", "related to files", "about testing"), add a
243
+ params object with a filter field containing the specified context
244
+ - **IMPORTANT**: Introspection has HIGHER PRIORITY than "answer" for these
245
+ queries. If asking about capabilities/skills, use "introspect", NOT
246
+ "answer"
247
+
248
+ 2. **Information requests** - Use "answer" type when request asks for
234
249
  information:
235
250
  - Verbs: "explain", "answer", "describe", "tell me", "say", "what
236
251
  is", "how does"
237
252
  - Examples:
238
- - "explain TypeScript" → type: "answer"
239
- - "tell me about Docker" → type: "answer"
253
+ - "explain typescript" → type: "answer"
254
+ - "tell me about docker" → type: "answer"
240
255
  - "what is the current directory" → type: "answer"
256
+ - **Exception**: Questions about capabilities/skills should use
257
+ "introspect" instead
241
258
 
242
- 2. **Skill-based requests** - Use skills when verb matches a defined skill:
259
+ 3. **Skill-based requests** - Use skills when verb matches a defined skill:
243
260
  - If "process" skill exists and user says "process" → Use the process skill
244
261
  - If "deploy" skill exists and user says "deploy" → Use the deploy skill
245
262
  - Extract steps from the matching skill and create tasks for each step
@@ -359,7 +376,10 @@ When creating task definitions, focus on:
359
376
  - `execute` - Shell commands, running programs, scripts, processing
360
377
  operations
361
378
  - `answer` - Answering questions, explaining concepts, providing
362
- information
379
+ information (EXCEPT for capability/skill queries - use introspect)
380
+ - `introspect` - Listing available capabilities and skills when user
381
+ asks what the concierge can do. Include params { filter: "keyword" }
382
+ if user specifies a filter like "skills for deployment"
363
383
  - `report` - Generating summaries, creating reports, displaying
364
384
  results
365
385
  - `define` - Presenting skill-based options when request matches
@@ -367,7 +387,8 @@ When creating task definitions, focus on:
367
387
  (selecting ONE variant, ONE environment, ONE target), NOT sequences of
368
388
  steps. Each option represents a single selection that will later be
369
389
  expanded into individual sequential steps. NEVER bundle multiple steps
370
- into a single option like "Process X, run validation, deploy Y".**
390
+ into a single option like "Process X, run validation, deploy Y". The
391
+ action text must ALWAYS end with a colon (:) to introduce the options.**
371
392
  - `ignore` - Request is too vague and cannot be mapped to skills or
372
393
  inferred from context
373
394
 
@@ -590,7 +611,7 @@ Split only when multiple distinct queries or operations are needed:
590
611
  Examples showing proper use of skills and disambiguation:
591
612
 
592
613
  - "process" with process skill requiring {TARGET} parameter (Alpha, Beta, Gamma,
593
- Delta) → One task: type "define", action "Clarify which target to process",
614
+ Delta) → One task: type "define", action "Clarify which target to process:",
594
615
  params { options: ["Process Alpha", "Process Beta", "Process Gamma", "Process
595
616
  Delta"] }. NOTE: If variants have descriptions, format as "Process Alpha, the
596
617
  legacy version" NOT "Process Alpha (the legacy version)"
@@ -599,7 +620,7 @@ Examples showing proper use of skills and disambiguation:
599
620
  target generation script", "Run the Alpha processing pipeline"
600
621
  - "process all" with same process skill → Twelve tasks (3 steps × 4 targets)
601
622
  - "deploy" with deploy skill (staging, production, canary) → One task: type
602
- "define", action "Clarify which environment to deploy to", params
623
+ "define", action "Clarify which environment to deploy to:", params
603
624
  { options: ["Deploy to staging environment", "Deploy to production
604
625
  environment", "Deploy to canary environment"] }
605
626
  - "deploy all" with deploy skill (staging, production) → Two tasks: one for
@@ -6,17 +6,6 @@ import { StepType } from '../ui/Config.js';
6
6
  export function markAsDone(component) {
7
7
  return { ...component, state: { ...component.state, done: true } };
8
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
- }
20
9
  export function createWelcomeDefinition(app) {
21
10
  return {
22
11
  id: randomUUID(),
@@ -13,3 +13,38 @@ export function getConfirmationMessage() {
13
13
  ];
14
14
  return messages[Math.floor(Math.random() * messages.length)];
15
15
  }
16
+ /**
17
+ * Returns a refining message shown during plan refinement.
18
+ * Randomly selects from variations to sound natural.
19
+ */
20
+ export function getRefiningMessage() {
21
+ const messages = [
22
+ 'Let me work out the specifics for you.',
23
+ "I'll figure out the concrete steps.",
24
+ 'Let me break this down into tasks.',
25
+ "I'll plan out the details.",
26
+ 'Let me arrange the steps.',
27
+ "I'll prepare everything you need.",
28
+ ];
29
+ return messages[Math.floor(Math.random() * messages.length)];
30
+ }
31
+ /**
32
+ * Returns a cancellation message for the given operation.
33
+ * Randomly selects from variations to sound natural.
34
+ */
35
+ export function getCancellationMessage(operation) {
36
+ const templates = [
37
+ `I've cancelled the ${operation.toLowerCase()}.`,
38
+ `I've aborted the ${operation.toLowerCase()}.`,
39
+ `The ${operation.toLowerCase()} was cancelled.`,
40
+ `The ${operation.toLowerCase()} has been aborted.`,
41
+ ];
42
+ return templates[Math.floor(Math.random() * templates.length)];
43
+ }
44
+ /**
45
+ * Feedback messages for various operations
46
+ */
47
+ export const FeedbackMessages = {
48
+ ConfigurationComplete: 'Configuration complete.',
49
+ UnexpectedError: 'Unexpected error occurred:',
50
+ };
@@ -33,8 +33,13 @@ class ToolRegistry {
33
33
  // Create singleton instance
34
34
  export const toolRegistry = new ToolRegistry();
35
35
  // Register built-in tools
36
+ import { introspectTool } from '../tools/introspect.tool.js';
36
37
  import { planTool } from '../tools/plan.tool.js';
37
38
  toolRegistry.register('plan', {
38
39
  schema: planTool,
39
40
  instructionsPath: 'config/PLAN.md',
40
41
  });
42
+ toolRegistry.register('introspect', {
43
+ schema: introspectTool,
44
+ instructionsPath: 'config/INTROSPECT.md',
45
+ });
@@ -0,0 +1,32 @@
1
+ export const introspectTool = {
2
+ name: 'introspect',
3
+ description: 'Execute a task with type "introspect" to list available capabilities and skills. Called after PLAN has identified an introspection request and user has confirmed. Takes the task action and optional filter parameter to present built-in capabilities and user-defined skills.',
4
+ input_schema: {
5
+ type: 'object',
6
+ properties: {
7
+ message: {
8
+ type: 'string',
9
+ description: 'Introductory reply to display before the capabilities 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. Always end with a colon.',
10
+ },
11
+ tasks: {
12
+ type: 'array',
13
+ description: 'Array of capabilities, each with type "introspect". Include built-in capabilities (PLAN, INTROSPECT, ANSWER, EXECUTE, REPORT, CONFIG) and user-defined skills from the Available Skills section.',
14
+ items: {
15
+ type: 'object',
16
+ properties: {
17
+ action: {
18
+ type: 'string',
19
+ description: 'Capability name and description. Format: "NAME: Brief description". Maximum 64 characters. Examples: "PLAN: Break down requests into steps", "Deploy App: Build and deploy application".',
20
+ },
21
+ type: {
22
+ type: 'string',
23
+ description: 'Always "introspect" for capability listings.',
24
+ },
25
+ },
26
+ required: ['action', 'type'],
27
+ },
28
+ },
29
+ },
30
+ required: ['message', 'tasks'],
31
+ },
32
+ };
@@ -20,7 +20,7 @@ export const planTool = {
20
20
  },
21
21
  type: {
22
22
  type: 'string',
23
- description: 'Type of task: "config" (settings), "plan" (planning), "execute" (shell/programs/finding files), "answer" (questions), "report" (summaries), "define" (skill-based disambiguation), "ignore" (too vague)',
23
+ description: 'Type of task: "config" (settings), "plan" (planning), "execute" (shell/programs/finding files), "answer" (questions, NOT for capability queries), "introspect" (list capabilities/skills), "report" (summaries), "define" (skill-based disambiguation), "ignore" (too vague)',
24
24
  },
25
25
  params: {
26
26
  type: 'object',
@@ -0,0 +1,90 @@
1
+ import { FeedbackType, TaskType } from './types.js';
2
+ /**
3
+ * Semantic color palette - colors organized by their purpose/meaning.
4
+ * Prefer adding semantic names here rather than to DescriptiveColors.
5
+ */
6
+ export const Colors = {
7
+ Action: {
8
+ Execute: '#5aaa8a', // green
9
+ Discard: '#a85c3f', // dark orange
10
+ Select: '#5c8cbc', // steel blue
11
+ },
12
+ Status: {
13
+ Success: '#22aa22', // green
14
+ Error: '#cc5c5c', // red
15
+ Warning: '#cc9c5c', // orange
16
+ Info: '#5c9ccc', // cyan
17
+ },
18
+ Label: {
19
+ Default: '#ffffff', // white
20
+ Inactive: '#888888', // gray
21
+ Discarded: '#666666', // dark gray
22
+ Skipped: '#cccc5c', // yellow
23
+ },
24
+ Type: {
25
+ Config: '#5c9ccc', // cyan
26
+ Plan: '#5ccccc', // magenta
27
+ Execute: '#5aaa8a', // green
28
+ Answer: '#9c5ccc', // purple
29
+ Introspect: '#9c5ccc', // purple
30
+ Report: '#cc9c5c', // orange
31
+ Define: '#cc9c5c', // amber
32
+ Ignore: '#cc7a5c', // dark orange
33
+ Select: '#5c8cbc', // steel blue
34
+ Discard: '#a85c3f', // dark orange
35
+ },
36
+ };
37
+ /**
38
+ * Task-specific color mappings
39
+ */
40
+ export const TaskColors = {
41
+ [TaskType.Config]: {
42
+ description: Colors.Label.Default,
43
+ type: Colors.Type.Config,
44
+ },
45
+ [TaskType.Plan]: {
46
+ description: Colors.Label.Default,
47
+ type: Colors.Type.Plan,
48
+ },
49
+ [TaskType.Execute]: {
50
+ description: Colors.Label.Default,
51
+ type: Colors.Type.Execute,
52
+ },
53
+ [TaskType.Answer]: {
54
+ description: Colors.Label.Default,
55
+ type: Colors.Type.Answer,
56
+ },
57
+ [TaskType.Introspect]: {
58
+ description: Colors.Label.Default,
59
+ type: Colors.Type.Introspect,
60
+ },
61
+ [TaskType.Report]: {
62
+ description: Colors.Label.Default,
63
+ type: Colors.Type.Report,
64
+ },
65
+ [TaskType.Define]: {
66
+ description: Colors.Label.Default,
67
+ type: Colors.Type.Define,
68
+ },
69
+ [TaskType.Ignore]: {
70
+ description: Colors.Label.Skipped,
71
+ type: Colors.Type.Ignore,
72
+ },
73
+ [TaskType.Select]: {
74
+ description: Colors.Label.Inactive,
75
+ type: Colors.Type.Select,
76
+ },
77
+ [TaskType.Discard]: {
78
+ description: Colors.Label.Discarded,
79
+ type: Colors.Type.Discard,
80
+ },
81
+ };
82
+ /**
83
+ * Feedback-specific color mappings
84
+ */
85
+ export const FeedbackColors = {
86
+ [FeedbackType.Info]: Colors.Status.Info,
87
+ [FeedbackType.Succeeded]: Colors.Status.Success,
88
+ [FeedbackType.Aborted]: Colors.Status.Warning,
89
+ [FeedbackType.Failed]: Colors.Status.Error,
90
+ };
@@ -15,6 +15,7 @@ export var TaskType;
15
15
  TaskType["Plan"] = "plan";
16
16
  TaskType["Execute"] = "execute";
17
17
  TaskType["Answer"] = "answer";
18
+ TaskType["Introspect"] = "introspect";
18
19
  TaskType["Report"] = "report";
19
20
  TaskType["Define"] = "define";
20
21
  TaskType["Ignore"] = "ignore";
package/dist/ui/Column.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
+ import React from 'react';
2
3
  import { Box } from 'ink';
3
4
  import { Component } from './Component.js';
4
- export const Column = ({ items, debug }) => {
5
+ export const Column = React.memo(({ items, debug }) => {
5
6
  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
- };
7
+ });
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
+ import React from 'react';
2
3
  import { ComponentName } from '../types/types.js';
3
4
  import { Command } from './Command.js';
4
5
  import { Confirm } from './Confirm.js';
@@ -8,7 +9,7 @@ import { Message } from './Message.js';
8
9
  import { Plan } from './Plan.js';
9
10
  import { Refinement } from './Refinement.js';
10
11
  import { Welcome } from './Welcome.js';
11
- export function Component({ def, debug }) {
12
+ export const Component = React.memo(function Component({ def, debug, }) {
12
13
  switch (def.name) {
13
14
  case ComponentName.Welcome:
14
15
  return _jsx(Welcome, { ...def.props });
@@ -39,4 +40,4 @@ export function Component({ def, debug }) {
39
40
  return _jsx(Confirm, { ...props, state: state });
40
41
  }
41
42
  }
42
- }
43
+ });
package/dist/ui/Config.js CHANGED
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import React from 'react';
3
3
  import { Box, Text, useFocus, useInput } from 'ink';
4
4
  import TextInput from 'ink-text-input';
5
+ import { Colors } from '../types/colors.js';
5
6
  export var StepType;
6
7
  (function (StepType) {
7
8
  StepType["Text"] = "text";
@@ -43,7 +44,7 @@ function TextStep({ value, placeholder, validate, onChange, onSubmit, }) {
43
44
  }, { isActive: validationFailed });
44
45
  // When validation fails, show colored text
45
46
  if (validationFailed) {
46
- return (_jsxs(Text, { color: "#cc5c5c", children: [inputValue || placeholder, isFocused && _jsx(Text, { inverse: true, children: " " })] }));
47
+ return (_jsxs(Text, { color: Colors.Status.Error, children: [inputValue || placeholder, isFocused && _jsx(Text, { inverse: true, children: " " })] }));
47
48
  }
48
49
  return (_jsx(TextInput, { value: inputValue, onChange: handleChange, onSubmit: handleSubmit, placeholder: placeholder }));
49
50
  }
@@ -216,6 +217,6 @@ export function Config({ steps, state, onFinished, onAborted }) {
216
217
  if (!shouldShow) {
217
218
  return null;
218
219
  }
219
- 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: " " }), renderStepInput(stepConfig, isCurrentStep)] })] }, stepConfig.key));
220
+ 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: Colors.Action.Select, dimColor: !isCurrentStep, children: ">" }), _jsx(Text, { children: " " }), renderStepInput(stepConfig, isCurrentStep)] })] }, stepConfig.key));
220
221
  }) }));
221
222
  }
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import React from 'react';
3
3
  import { Box, Text, useInput } from 'ink';
4
+ import { Colors } from '../types/colors.js';
4
5
  export function Confirm({ message, state, onConfirmed, onCancelled, }) {
5
6
  const done = state?.done ?? false;
6
7
  const [selectedIndex, setSelectedIndex] = React.useState(0); // 0 = Yes, 1 = No
@@ -27,14 +28,14 @@ export function Confirm({ message, state, onConfirmed, onCancelled, }) {
27
28
  }
28
29
  }, { isActive: !done });
29
30
  const options = [
30
- { label: 'Yes', value: 'yes', color: '#4a9a7a' }, // green (execute)
31
- { label: 'No', value: 'no', color: '#a85c3f' }, // dark orange (discard)
31
+ { label: 'Yes', value: 'yes', color: Colors.Action.Execute },
32
+ { label: 'No', value: 'no', color: Colors.Action.Discard },
32
33
  ];
33
34
  if (done) {
34
35
  // When done, show both the message and user's choice in timeline
35
36
  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
  }
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
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { children: message }) }), _jsxs(Box, { children: [_jsx(Text, { color: Colors.Action.Select, children: ">" }), _jsx(Text, { children: " " }), _jsx(Box, { children: options.map((option, index) => {
38
39
  const isSelected = index === selectedIndex;
39
40
  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,5 +1,6 @@
1
1
  import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Box, Text } from 'ink';
3
+ import { FeedbackColors } from '../types/colors.js';
3
4
  import { FeedbackType } from '../types/types.js';
4
5
  function getSymbol(type) {
5
6
  return {
@@ -9,25 +10,8 @@ function getSymbol(type) {
9
10
  [FeedbackType.Failed]: '✗',
10
11
  }[type];
11
12
  }
12
- function getSymbolColor(type) {
13
- return {
14
- [FeedbackType.Info]: '#5c9ccc', // cyan
15
- [FeedbackType.Succeeded]: '#00aa00', // green
16
- [FeedbackType.Aborted]: '#cc9c5c', // orange
17
- [FeedbackType.Failed]: '#cc5c5c', // red
18
- }[type];
19
- }
20
- function getMessageColor(type) {
21
- return {
22
- [FeedbackType.Info]: '#aaaaaa', // light grey
23
- [FeedbackType.Succeeded]: '#5ccc5c', // green
24
- [FeedbackType.Aborted]: '#cc9c5c', // orange
25
- [FeedbackType.Failed]: '#cc5c5c', // red
26
- }[type];
27
- }
28
13
  export function Feedback({ type, message }) {
29
- const symbolColor = getSymbolColor(type);
30
- const messageColor = getMessageColor(type);
14
+ const color = FeedbackColors[type];
31
15
  const symbol = getSymbol(type);
32
- return (_jsxs(Box, { children: [_jsxs(Text, { color: symbolColor, children: [symbol, " "] }), _jsx(Text, { color: messageColor, children: message })] }));
16
+ return (_jsx(Box, { children: _jsxs(Text, { color: color, children: [symbol, " ", message] }) }));
33
17
  }
package/dist/ui/Main.js CHANGED
@@ -4,7 +4,8 @@ import { useInput } from 'ink';
4
4
  import { ComponentName, FeedbackType, TaskType, } from '../types/types.js';
5
5
  import { createAnthropicService, } from '../services/anthropic.js';
6
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';
7
+ import { FeedbackMessages, getCancellationMessage, getRefiningMessage, } from '../services/messages.js';
8
+ import { createCommandDefinition, createConfirmDefinition, createConfigDefinition, createFeedback, createMessage, createRefinement, createPlanDefinition, createWelcomeDefinition, isStateless, markAsDone, } from '../services/components.js';
8
9
  import { exitApp } from '../services/process.js';
9
10
  import { Column } from './Column.js';
10
11
  export const Main = ({ app, command }) => {
@@ -19,6 +20,11 @@ export const Main = ({ app, command }) => {
19
20
  const [timeline, setTimeline] = React.useState([]);
20
21
  const [queue, setQueue] = React.useState([]);
21
22
  const [isDebug, setIsDebug] = React.useState(() => loadDebugSetting());
23
+ // Use ref to track latest timeline for callbacks
24
+ const timelineRef = React.useRef(timeline);
25
+ React.useEffect(() => {
26
+ timelineRef.current = timeline;
27
+ }, [timeline]);
22
28
  // Top-level Shift+Tab handler for debug mode toggle
23
29
  // Child components must ignore Shift+Tab to prevent conflicts
24
30
  useInput((input, key) => {
@@ -52,7 +58,7 @@ export const Main = ({ app, command }) => {
52
58
  return currentQueue;
53
59
  const [first] = currentQueue;
54
60
  if (first.name === ComponentName.Command) {
55
- addToTimeline(markAsDone(first), createFeedback(FeedbackType.Failed, 'Unexpected error occurred:', error));
61
+ addToTimeline(markAsDone(first), createFeedback(FeedbackType.Failed, FeedbackMessages.UnexpectedError, error));
56
62
  }
57
63
  exitApp(1);
58
64
  return [];
@@ -64,7 +70,7 @@ export const Main = ({ app, command }) => {
64
70
  return currentQueue;
65
71
  const [first] = currentQueue;
66
72
  if (!isStateless(first)) {
67
- addToTimeline(markAsDone(first), createFeedback(FeedbackType.Aborted, `${operationName} was aborted by user`));
73
+ addToTimeline(markAsDone(first), createFeedback(FeedbackType.Aborted, getCancellationMessage(operationName)));
68
74
  }
69
75
  exitApp(0);
70
76
  return [];
@@ -76,6 +82,13 @@ export const Main = ({ app, command }) => {
76
82
  const handlePlanAborted = React.useCallback(() => {
77
83
  handleAborted('Task selection');
78
84
  }, [handleAborted]);
85
+ const createPlanAbortHandler = React.useCallback((tasks) => {
86
+ const allIntrospect = tasks.every((task) => task.type === TaskType.Introspect);
87
+ if (allIntrospect) {
88
+ return () => handleAborted('Introspection');
89
+ }
90
+ return handlePlanAborted;
91
+ }, [handleAborted, handlePlanAborted]);
79
92
  const handleCommandAborted = React.useCallback(() => {
80
93
  handleAborted('Request');
81
94
  }, [handleAborted]);
@@ -100,7 +113,23 @@ export const Main = ({ app, command }) => {
100
113
  return currentQueue;
101
114
  const [first] = currentQueue;
102
115
  if (first.name === ComponentName.Confirm) {
103
- addToTimeline(markAsDone(first), createFeedback(FeedbackType.Aborted, 'Execution cancelled'));
116
+ // Find the most recent Plan in timeline to check task types
117
+ const currentTimeline = timelineRef.current;
118
+ const lastPlanIndex = [...currentTimeline]
119
+ .reverse()
120
+ .findIndex((item) => item.name === ComponentName.Plan);
121
+ const lastPlan = lastPlanIndex >= 0
122
+ ? currentTimeline[currentTimeline.length - 1 - lastPlanIndex]
123
+ : null;
124
+ const allIntrospect = lastPlan &&
125
+ lastPlan.name === ComponentName.Plan &&
126
+ 'props' in lastPlan &&
127
+ lastPlan.props &&
128
+ 'tasks' in lastPlan.props &&
129
+ Array.isArray(lastPlan.props.tasks) &&
130
+ lastPlan.props.tasks.every((task) => task.type === TaskType.Introspect);
131
+ const operation = allIntrospect ? 'introspection' : 'execution';
132
+ addToTimeline(markAsDone(first), createFeedback(FeedbackType.Aborted, getCancellationMessage(operation)));
104
133
  }
105
134
  exitApp(0);
106
135
  return [];
@@ -139,7 +168,7 @@ export const Main = ({ app, command }) => {
139
168
  return [];
140
169
  });
141
170
  // Show final execution plan with confirmation
142
- const planDefinition = createPlanDefinition(result.message, result.tasks, handlePlanAborted, undefined);
171
+ const planDefinition = createPlanDefinition(result.message, result.tasks, createPlanAbortHandler(result.tasks), undefined);
143
172
  const confirmDefinition = createConfirmDefinition(handleExecutionConfirmed, handleExecutionCancelled);
144
173
  addToTimeline(planDefinition);
145
174
  setQueue([confirmDefinition]);
@@ -154,10 +183,17 @@ export const Main = ({ app, command }) => {
154
183
  }
155
184
  return [];
156
185
  });
157
- addToTimeline(createFeedback(FeedbackType.Failed, 'Unexpected error occurred:', errorMessage));
186
+ addToTimeline(createFeedback(FeedbackType.Failed, FeedbackMessages.UnexpectedError, errorMessage));
158
187
  exitApp(1);
159
188
  }
160
- }, [addToTimeline, service, handleRefinementAborted]);
189
+ }, [
190
+ addToTimeline,
191
+ service,
192
+ handleRefinementAborted,
193
+ createPlanAbortHandler,
194
+ handleExecutionConfirmed,
195
+ handleExecutionCancelled,
196
+ ]);
161
197
  const handleCommandComplete = React.useCallback((message, tasks) => {
162
198
  setQueue((currentQueue) => {
163
199
  if (currentQueue.length === 0)
@@ -166,7 +202,7 @@ export const Main = ({ app, command }) => {
166
202
  // Check if tasks contain a Define task that requires user interaction
167
203
  const hasDefineTask = tasks.some((task) => task.type === TaskType.Define);
168
204
  if (first.name === ComponentName.Command) {
169
- const planDefinition = createPlanDefinition(message, tasks, handlePlanAborted, hasDefineTask ? handlePlanSelectionConfirmed : undefined);
205
+ const planDefinition = createPlanDefinition(message, tasks, createPlanAbortHandler(tasks), hasDefineTask ? handlePlanSelectionConfirmed : undefined);
170
206
  if (hasDefineTask) {
171
207
  // Don't exit - keep the plan in the queue for interaction
172
208
  addToTimeline(markAsDone(first));
@@ -184,6 +220,7 @@ export const Main = ({ app, command }) => {
184
220
  });
185
221
  }, [
186
222
  addToTimeline,
223
+ createPlanAbortHandler,
187
224
  handlePlanSelectionConfirmed,
188
225
  handleExecutionConfirmed,
189
226
  handleExecutionCancelled,
@@ -199,7 +236,7 @@ export const Main = ({ app, command }) => {
199
236
  return currentQueue;
200
237
  const [first, ...rest] = currentQueue;
201
238
  if (first.name === ComponentName.Config) {
202
- addToTimeline(markAsDone(first), createFeedback(FeedbackType.Succeeded, 'Configuration complete'));
239
+ addToTimeline(markAsDone(first), createFeedback(FeedbackType.Succeeded, FeedbackMessages.ConfigurationComplete));
203
240
  }
204
241
  // Add command to queue if we have one
205
242
  if (command) {
@@ -253,6 +290,6 @@ export const Main = ({ app, command }) => {
253
290
  }
254
291
  }, [queue, timeline]);
255
292
  const current = queue.length > 0 ? queue[0] : null;
256
- const items = [...timeline, ...(current ? [current] : [])];
293
+ const items = React.useMemo(() => [...timeline, ...(current ? [current] : [])], [timeline, current]);
257
294
  return _jsx(Column, { items: items, debug: isDebug });
258
295
  };
package/dist/ui/Plan.js CHANGED
@@ -1,60 +1,23 @@
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 { TaskColors } from '../types/colors.js';
4
5
  import { TaskType } from '../types/types.js';
5
6
  import { Label } from './Label.js';
6
7
  import { List } from './List.js';
7
- const ColorPalette = {
8
- [TaskType.Config]: {
9
- description: '#ffffff', // white
10
- type: '#5c9ccc', // cyan
11
- },
12
- [TaskType.Plan]: {
13
- description: '#ffffff', // white
14
- type: '#5ccccc', // magenta
15
- },
16
- [TaskType.Execute]: {
17
- description: '#ffffff', // white
18
- type: '#4a9a7a', // green
19
- },
20
- [TaskType.Answer]: {
21
- description: '#ffffff', // white
22
- type: '#9c5ccc', // purple
23
- },
24
- [TaskType.Report]: {
25
- description: '#ffffff', // white
26
- type: '#cc9c5c', // orange
27
- },
28
- [TaskType.Define]: {
29
- description: '#ffffff', // white
30
- type: '#cc9c5c', // amber
31
- },
32
- [TaskType.Ignore]: {
33
- description: '#cccc5c', // yellow
34
- type: '#cc7a5c', // orange
35
- },
36
- [TaskType.Select]: {
37
- description: '#888888', // grey
38
- type: '#5c8cbc', // steel blue
39
- },
40
- [TaskType.Discard]: {
41
- description: '#666666', // dark grey
42
- type: '#a85c3f', // dark orange
43
- },
44
- };
45
8
  function taskToListItem(task, highlightedChildIndex = null, isDefineTaskWithoutSelection = false) {
46
9
  const item = {
47
10
  description: {
48
11
  text: task.action,
49
- color: ColorPalette[task.type].description,
12
+ color: TaskColors[task.type].description,
50
13
  },
51
- type: { text: task.type, color: ColorPalette[task.type].type },
14
+ type: { text: task.type, color: TaskColors[task.type].type },
52
15
  children: [],
53
16
  };
54
17
  // Mark define tasks with right arrow when no selection has been made
55
18
  if (isDefineTaskWithoutSelection) {
56
19
  item.marker = ' → ';
57
- item.markerColor = ColorPalette[TaskType.Plan].type;
20
+ item.markerColor = TaskColors[TaskType.Plan].type;
58
21
  }
59
22
  // Add children for Define tasks with options
60
23
  if (task.type === TaskType.Define && Array.isArray(task.params?.options)) {
@@ -66,17 +29,17 @@ function taskToListItem(task, highlightedChildIndex = null, isDefineTaskWithoutS
66
29
  childType =
67
30
  index === highlightedChildIndex ? TaskType.Execute : TaskType.Discard;
68
31
  }
69
- const colors = ColorPalette[childType];
32
+ const colors = TaskColors[childType];
70
33
  return {
71
34
  description: {
72
35
  text: String(option),
73
36
  color: colors.description,
74
- highlightedColor: ColorPalette[TaskType.Plan].description,
37
+ highlightedColor: TaskColors[TaskType.Plan].description,
75
38
  },
76
39
  type: {
77
40
  text: childType,
78
41
  color: colors.type,
79
- highlightedColor: ColorPalette[TaskType.Plan].type,
42
+ highlightedColor: TaskColors[TaskType.Plan].type,
80
43
  },
81
44
  };
82
45
  });
@@ -209,5 +172,5 @@ export function Plan({ message, tasks, state, debug = false, onSelectionConfirme
209
172
  !isDone;
210
173
  return taskToListItem(task, childIndex, isDefineWithoutSelection);
211
174
  });
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 })] }));
175
+ return (_jsxs(Box, { flexDirection: "column", children: [message && (_jsx(Box, { marginBottom: 1, children: _jsx(Label, { description: message, descriptionColor: TaskColors[TaskType.Plan].description, type: TaskType.Plan, typeColor: TaskColors[TaskType.Plan].type, showType: debug }) })), _jsx(List, { items: listItems, highlightedIndex: currentDefineTaskIndex >= 0 ? highlightedIndex : null, highlightedParentIndex: currentDefineTaskIndex, showType: debug })] }));
213
176
  }
@@ -1,6 +1,7 @@
1
1
  import { jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Text } from 'ink';
3
- export const Separator = ({ color = '#666666', spaces = 1, }) => {
3
+ import { Colors } from '../types/colors.js';
4
+ export const Separator = ({ color = Colors.Label.Discarded, spaces = 1, }) => {
4
5
  const spacing = ' '.repeat(spaces);
5
6
  return (_jsxs(Text, { color: color, children: [spacing, "\u203A", spacing] }));
6
7
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prompt-language-shell",
3
- "version": "0.3.8",
3
+ "version": "0.4.0",
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",
@@ -46,22 +46,22 @@
46
46
  },
47
47
  "homepage": "https://github.com/aswitalski/pls#readme",
48
48
  "dependencies": {
49
- "@anthropic-ai/sdk": "^0.68.0",
50
- "ink": "^6.4.0",
49
+ "@anthropic-ai/sdk": "^0.69.0",
50
+ "ink": "^6.5.0",
51
51
  "ink-text-input": "^6.0.0",
52
52
  "react": "^19.2.0",
53
53
  "yaml": "^2.8.1"
54
54
  },
55
55
  "devDependencies": {
56
- "@types/node": "^20.10.6",
57
- "@types/react": "^19.2.2",
58
- "@vitest/coverage-v8": "^4.0.8",
59
- "eslint": "^9.17.0",
56
+ "@types/node": "^24.10.1",
57
+ "@types/react": "^19.2.5",
58
+ "@vitest/coverage-v8": "^4.0.9",
59
+ "eslint": "^9.39.1",
60
60
  "husky": "^9.1.7",
61
61
  "ink-testing-library": "^4.0.0",
62
62
  "prettier": "^3.6.2",
63
- "typescript": "^5.3.3",
64
- "typescript-eslint": "^8.19.1",
65
- "vitest": "^4.0.5"
63
+ "typescript": "^5.9.3",
64
+ "typescript-eslint": "^8.46.4",
65
+ "vitest": "^4.0.9"
66
66
  }
67
67
  }