prompt-language-shell 0.3.0 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/ui/Main.js CHANGED
@@ -1,133 +1,161 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import React from 'react';
3
+ import { ComponentName, TaskType, } from '../types/components.js';
4
+ import { createAnthropicService, } from '../services/anthropic.js';
3
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';
8
+ import { exitApp } from '../services/process.js';
4
9
  import { Column } from './Column.js';
5
- function exit(code) {
6
- setTimeout(() => globalThis.process.exit(code), 100);
7
- }
8
- function markAsDone(component) {
9
- return { ...component, state: { ...component.state, done: true } };
10
- }
11
- function createWelcomeDefinition(app) {
12
- return {
13
- name: 'welcome',
14
- props: { app },
15
- };
16
- }
17
- function createConfigSteps() {
18
- return [
19
- { description: 'Anthropic API key', key: 'key', value: null },
20
- {
21
- description: 'Model',
22
- key: 'model',
23
- value: 'claude-haiku-4-5-20251001',
24
- },
25
- ];
26
- }
27
- function createConfigDefinition(onFinished, onAborted) {
28
- return {
29
- name: 'config',
30
- state: { done: false },
31
- props: {
32
- steps: createConfigSteps(),
33
- onFinished,
34
- onAborted,
35
- },
36
- };
37
- }
38
- function createCommandDefinition(command, service, onError, onComplete) {
39
- return {
40
- name: 'command',
41
- state: {
42
- done: false,
43
- isLoading: true,
44
- },
45
- props: {
46
- command,
47
- service,
48
- onError,
49
- onComplete,
50
- },
51
- };
52
- }
53
- function createFeedback(type, ...messages) {
54
- return {
55
- name: 'feedback',
56
- props: {
57
- type,
58
- message: messages.join('\n\n'),
59
- },
60
- };
61
- }
62
- export const Main = ({ app, command, service: initialService, isReady, onConfigured, }) => {
63
- const [history, setHistory] = React.useState([]);
64
- const [current, setCurrent] = React.useState(null);
65
- const [service, setService] = React.useState(initialService);
66
- const addToHistory = React.useCallback((...items) => {
67
- setHistory((history) => [...history, ...items]);
68
- }, []);
69
- const handleConfigFinished = React.useCallback((config) => {
70
- const service = onConfigured?.(config);
71
- if (service) {
72
- setService(service);
10
+ export const Main = ({ app, command }) => {
11
+ // Initialize service from existing config if available
12
+ const [service, setService] = React.useState(() => {
13
+ if (hasValidAnthropicKey()) {
14
+ const config = loadConfig();
15
+ return createAnthropicService(config.anthropic);
73
16
  }
74
- // Move config to history with done state and add success feedback
75
- setCurrent((current) => {
76
- if (!current)
77
- return null;
78
- addToHistory(markAsDone(current), createFeedback(FeedbackType.Succeeded, 'Configuration complete'));
79
- return null;
80
- });
81
- }, [onConfigured, addToHistory]);
82
- const handleConfigAborted = React.useCallback(() => {
83
- // Move config to history with done state and add aborted feedback
84
- setCurrent((current) => {
85
- addToHistory(markAsDone(current), createFeedback(FeedbackType.Aborted, 'Configuration aborted by user'));
86
- // Exit after showing abort message
87
- exit(0);
88
- return null;
17
+ return null;
18
+ });
19
+ const [timeline, setTimeline] = React.useState([]);
20
+ const [queue, setQueue] = React.useState([]);
21
+ const addToTimeline = React.useCallback((...items) => {
22
+ setTimeline((timeline) => [...timeline, ...items]);
23
+ }, []);
24
+ const processNextInQueue = React.useCallback(() => {
25
+ setQueue((currentQueue) => {
26
+ if (currentQueue.length === 0)
27
+ return currentQueue;
28
+ const [first, ...rest] = currentQueue;
29
+ // Stateless components auto-complete immediately
30
+ if (isStateless(first)) {
31
+ addToTimeline(first);
32
+ return rest;
33
+ }
34
+ return currentQueue;
89
35
  });
90
- }, [addToHistory]);
36
+ }, [addToTimeline]);
91
37
  const handleCommandError = React.useCallback((error) => {
92
- // Move command to history with done state and add error feedback
93
- setCurrent((current) => {
94
- addToHistory(markAsDone(current), createFeedback(FeedbackType.Failed, 'Unexpected error occurred:', error));
95
- // Exit after showing error
96
- exit(1);
97
- return null;
38
+ setQueue((currentQueue) => {
39
+ if (currentQueue.length === 0)
40
+ return currentQueue;
41
+ const [first] = currentQueue;
42
+ if (first.name === ComponentName.Command) {
43
+ addToTimeline(markAsDone(first), createFeedback(FeedbackType.Failed, 'Unexpected error occurred:', error));
44
+ }
45
+ exitApp(1);
46
+ return [];
47
+ });
48
+ }, [addToTimeline]);
49
+ const handlePlanSelectionConfirmed = React.useCallback((selectedIndex, updatedTasks) => {
50
+ setQueue((currentQueue) => {
51
+ if (currentQueue.length === 0)
52
+ return currentQueue;
53
+ const [first] = currentQueue;
54
+ if (first.name === ComponentName.Plan) {
55
+ // Mark plan as done and add it to timeline
56
+ addToTimeline(markAsDone(first));
57
+ }
58
+ // Exit after selection is confirmed
59
+ exitApp(0);
60
+ return [];
98
61
  });
99
- }, [addToHistory]);
100
- const handleCommandComplete = React.useCallback(() => {
101
- // Move command to history with done state
102
- setCurrent((current) => {
103
- addToHistory(markAsDone(current));
104
- // Exit after showing plan
105
- exit(0);
106
- return null;
62
+ }, [addToTimeline]);
63
+ const handleCommandComplete = React.useCallback((message, tasks) => {
64
+ setQueue((currentQueue) => {
65
+ if (currentQueue.length === 0)
66
+ return currentQueue;
67
+ const [first] = currentQueue;
68
+ // Check if tasks contain a Define task that requires user interaction
69
+ const hasDefineTask = tasks.some((task) => task.type === TaskType.Define);
70
+ if (first.name === ComponentName.Command) {
71
+ const planDefinition = createPlanDefinition(message, tasks, hasDefineTask ? handlePlanSelectionConfirmed : undefined);
72
+ if (hasDefineTask) {
73
+ // Don't exit - keep the plan in the queue for interaction
74
+ addToTimeline(markAsDone(first));
75
+ return [planDefinition];
76
+ }
77
+ else {
78
+ // No define task - add plan to timeline and exit
79
+ addToTimeline(markAsDone(first), planDefinition);
80
+ exitApp(0);
81
+ return [];
82
+ }
83
+ }
84
+ exitApp(0);
85
+ return [];
107
86
  });
108
- }, [addToHistory]);
109
- // Initialize configuration flow when not ready
87
+ }, [addToTimeline, handlePlanSelectionConfirmed]);
88
+ const handleConfigFinished = React.useCallback((config) => {
89
+ const anthropicConfig = config;
90
+ saveAnthropicConfig(anthropicConfig);
91
+ const newService = createAnthropicService(anthropicConfig);
92
+ setService(newService);
93
+ // Complete config component and add command if present
94
+ setQueue((currentQueue) => {
95
+ if (currentQueue.length === 0)
96
+ return currentQueue;
97
+ const [first, ...rest] = currentQueue;
98
+ if (first.name === ComponentName.Config) {
99
+ addToTimeline(markAsDone(first), createFeedback(FeedbackType.Succeeded, 'Configuration complete'));
100
+ }
101
+ // Add command to queue if we have one
102
+ if (command) {
103
+ return [
104
+ ...rest,
105
+ createCommandDefinition(command, newService, handleCommandError, handleCommandComplete),
106
+ ];
107
+ }
108
+ // No command - exit after showing completion message
109
+ exitApp(0);
110
+ return rest;
111
+ });
112
+ }, [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
+ // Initialize queue on mount
110
126
  React.useEffect(() => {
111
- if (isReady) {
112
- return;
127
+ const hasConfig = !!service;
128
+ if (command && hasConfig) {
129
+ // With command + valid config: [Command]
130
+ setQueue([
131
+ createCommandDefinition(command, service, handleCommandError, handleCommandComplete),
132
+ ]);
113
133
  }
114
- if (!command) {
115
- setHistory([createWelcomeDefinition(app)]);
134
+ else if (command && !hasConfig) {
135
+ // With command + no config: [Message, Config] (Command added after config)
136
+ setQueue([
137
+ createMessage(getConfigurationRequiredMessage()),
138
+ createConfigDefinition(handleConfigFinished, handleConfigAborted),
139
+ ]);
116
140
  }
117
- setCurrent(createConfigDefinition(handleConfigFinished, handleConfigAborted));
118
- }, [isReady, app, command, handleConfigFinished, handleConfigAborted]);
119
- // Execute command when service and command are available
120
- React.useEffect(() => {
121
- if (command && service) {
122
- setCurrent(createCommandDefinition(command, service, handleCommandError, handleCommandComplete));
141
+ else if (!command && hasConfig) {
142
+ // No command + valid config: [Welcome]
143
+ setQueue([createWelcomeDefinition(app)]);
123
144
  }
124
- }, [command, service, handleCommandError, handleCommandComplete]);
125
- // Show welcome screen when ready but no command
126
- React.useEffect(() => {
127
- if (isReady && !command) {
128
- setCurrent(createWelcomeDefinition(app));
145
+ else {
146
+ // No command + no config: [Welcome, Message, Config]
147
+ setQueue([
148
+ createWelcomeDefinition(app),
149
+ createMessage(getConfigurationRequiredMessage(true)),
150
+ createConfigDefinition(handleConfigFinished, handleConfigAborted),
151
+ ]);
129
152
  }
130
- }, [isReady, command, app]);
131
- const items = [...history, ...(current ? [current] : [])];
153
+ }, []); // Only run on mount
154
+ // Process queue whenever it changes
155
+ React.useEffect(() => {
156
+ processNextInQueue();
157
+ }, [queue, processNextInQueue]);
158
+ const current = queue.length > 0 ? queue[0] : null;
159
+ const items = [...timeline, ...(current ? [current] : [])];
132
160
  return _jsx(Column, { items: items });
133
161
  };
@@ -0,0 +1,5 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Text } from 'ink';
3
+ export const Message = ({ text }) => {
4
+ return _jsx(Text, { children: text });
5
+ };
@@ -0,0 +1,194 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useState } from 'react';
3
+ import { Box, useInput } from 'ink';
4
+ import { TaskType } from '../types/components.js';
5
+ import { Label } from './Label.js';
6
+ 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
+ function taskToListItem(task, highlightedChildIndex = null, isDefineTaskWithoutSelection = false) {
46
+ const item = {
47
+ description: {
48
+ text: task.action,
49
+ color: ColorPalette[task.type].description,
50
+ },
51
+ type: { text: task.type, color: ColorPalette[task.type].type },
52
+ children: [],
53
+ };
54
+ // Mark define tasks with right arrow when no selection has been made
55
+ if (isDefineTaskWithoutSelection) {
56
+ item.marker = ' → ';
57
+ }
58
+ // Add children for Define tasks with options
59
+ if (task.type === TaskType.Define && Array.isArray(task.params?.options)) {
60
+ item.children = task.params.options.map((option, index) => {
61
+ // Determine the type based on selection state
62
+ let childType = TaskType.Select;
63
+ if (highlightedChildIndex !== null) {
64
+ // A selection was made - mark others as discarded
65
+ childType =
66
+ index === highlightedChildIndex ? TaskType.Execute : TaskType.Discard;
67
+ }
68
+ const colors = ColorPalette[childType];
69
+ return {
70
+ description: {
71
+ text: String(option),
72
+ color: colors.description,
73
+ highlightedColor: ColorPalette[TaskType.Plan].description,
74
+ },
75
+ type: {
76
+ text: childType,
77
+ color: colors.type,
78
+ highlightedColor: ColorPalette[TaskType.Plan].type,
79
+ },
80
+ };
81
+ });
82
+ }
83
+ return item;
84
+ }
85
+ export function Plan({ message, tasks, state, onSelectionConfirmed, }) {
86
+ const [highlightedIndex, setHighlightedIndex] = useState(state?.highlightedIndex ?? null);
87
+ const [currentDefineGroupIndex, setCurrentDefineGroupIndex] = useState(state?.currentDefineGroupIndex ?? 0);
88
+ const [completedSelections, setCompletedSelections] = useState(state?.completedSelections ?? []);
89
+ const [isDone, setIsDone] = useState(state?.done ?? false);
90
+ // Find all Define tasks
91
+ const defineTaskIndices = tasks
92
+ .map((t, idx) => (t.type === TaskType.Define ? idx : -1))
93
+ .filter((idx) => idx !== -1);
94
+ // Get the current active define task
95
+ const currentDefineTaskIndex = defineTaskIndices[currentDefineGroupIndex] ?? -1;
96
+ const defineTask = currentDefineTaskIndex >= 0 ? tasks[currentDefineTaskIndex] : null;
97
+ const optionsCount = Array.isArray(defineTask?.params?.options)
98
+ ? defineTask.params.options.length
99
+ : 0;
100
+ const hasMoreGroups = currentDefineGroupIndex < defineTaskIndices.length - 1;
101
+ useInput((input, key) => {
102
+ // Don't handle input if already done or no define task
103
+ if (isDone || !defineTask) {
104
+ return;
105
+ }
106
+ if (key.downArrow) {
107
+ setHighlightedIndex((prev) => {
108
+ if (prev === null) {
109
+ return 0; // Select first
110
+ }
111
+ return (prev + 1) % optionsCount; // Wrap around
112
+ });
113
+ }
114
+ else if (key.upArrow) {
115
+ setHighlightedIndex((prev) => {
116
+ if (prev === null) {
117
+ return optionsCount - 1; // Select last
118
+ }
119
+ return (prev - 1 + optionsCount) % optionsCount; // Wrap around
120
+ });
121
+ }
122
+ else if (key.return && highlightedIndex !== null) {
123
+ // Record the selection for this group
124
+ const newCompletedSelections = [...completedSelections];
125
+ newCompletedSelections[currentDefineGroupIndex] = highlightedIndex;
126
+ setCompletedSelections(newCompletedSelections);
127
+ if (hasMoreGroups) {
128
+ // Advance to next group
129
+ setCurrentDefineGroupIndex(currentDefineGroupIndex + 1);
130
+ setHighlightedIndex(null);
131
+ }
132
+ else {
133
+ // Last group - mark as done to show the selection
134
+ setIsDone(true);
135
+ setHighlightedIndex(null); // Clear highlight to show Execute color
136
+ if (state) {
137
+ state.done = true;
138
+ }
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 };
143
+ }
144
+ return task;
145
+ });
146
+ onSelectionConfirmed?.(highlightedIndex, updatedTasks);
147
+ }
148
+ }
149
+ }, { isActive: !isDone && defineTask !== null });
150
+ // Sync state back to state object
151
+ useEffect(() => {
152
+ if (state) {
153
+ state.highlightedIndex = highlightedIndex;
154
+ state.currentDefineGroupIndex = currentDefineGroupIndex;
155
+ state.completedSelections = completedSelections;
156
+ state.done = isDone;
157
+ }
158
+ }, [
159
+ highlightedIndex,
160
+ currentDefineGroupIndex,
161
+ completedSelections,
162
+ isDone,
163
+ state,
164
+ ]);
165
+ const listItems = tasks.map((task, idx) => {
166
+ // Find which define group this task belongs to (if any)
167
+ const defineGroupIndex = defineTaskIndices.indexOf(idx);
168
+ const isDefineTask = defineGroupIndex !== -1;
169
+ // Determine child selection state
170
+ let childIndex = null;
171
+ if (isDefineTask) {
172
+ if (defineGroupIndex < currentDefineGroupIndex) {
173
+ // Previously completed group - show the selection
174
+ childIndex = completedSelections[defineGroupIndex] ?? null;
175
+ }
176
+ else if (defineGroupIndex === currentDefineGroupIndex) {
177
+ // Current active group - show live navigation unless done
178
+ if (isDone) {
179
+ // If done, show the completed selection for this group too
180
+ childIndex = completedSelections[defineGroupIndex] ?? null;
181
+ }
182
+ else {
183
+ childIndex = null;
184
+ }
185
+ }
186
+ }
187
+ // Show arrow on current active define task when no child is highlighted
188
+ const isDefineWithoutSelection = isDefineTask &&
189
+ defineGroupIndex === currentDefineGroupIndex &&
190
+ highlightedIndex === null;
191
+ return taskToListItem(task, childIndex, isDefineWithoutSelection);
192
+ });
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 })] }));
194
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prompt-language-shell",
3
- "version": "0.3.0",
3
+ "version": "0.3.4",
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",
@@ -57,6 +57,7 @@
57
57
  "@vitest/coverage-v8": "^4.0.8",
58
58
  "eslint": "^9.17.0",
59
59
  "husky": "^9.1.7",
60
+ "ink-testing-library": "^4.0.0",
60
61
  "prettier": "^3.6.2",
61
62
  "typescript": "^5.3.3",
62
63
  "typescript-eslint": "^8.19.1",