prompt-language-shell 0.4.8 → 0.4.9

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,217 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useState } from 'react';
3
+ import { Box, Text } from 'ink';
4
+ import { Colors, getTextColor, Palette } from '../services/colors.js';
5
+ import { useInput } from '../services/keyboard.js';
6
+ import { formatErrorMessage } from '../services/messages.js';
7
+ import { formatDuration } from '../services/utils.js';
8
+ import { ExecutionStatus, executeCommands, } from '../services/shell.js';
9
+ import { Spinner } from './Spinner.js';
10
+ const MINIMUM_PROCESSING_TIME = 400;
11
+ const STATUS_ICONS = {
12
+ [ExecutionStatus.Pending]: '- ',
13
+ [ExecutionStatus.Running]: '• ',
14
+ [ExecutionStatus.Success]: '✓ ',
15
+ [ExecutionStatus.Failed]: '✗ ',
16
+ };
17
+ function getStatusColors(status) {
18
+ switch (status) {
19
+ case ExecutionStatus.Pending:
20
+ return {
21
+ icon: Palette.Gray,
22
+ description: Palette.Gray,
23
+ command: Palette.DarkGray,
24
+ symbol: Palette.DarkGray,
25
+ };
26
+ case ExecutionStatus.Running:
27
+ return {
28
+ icon: Palette.Gray,
29
+ description: getTextColor(true),
30
+ command: Palette.LightGreen,
31
+ symbol: Palette.AshGray,
32
+ };
33
+ case ExecutionStatus.Success:
34
+ return {
35
+ icon: Colors.Status.Success,
36
+ description: getTextColor(true),
37
+ command: Palette.Gray,
38
+ symbol: Palette.Gray,
39
+ };
40
+ case ExecutionStatus.Failed:
41
+ return {
42
+ icon: Colors.Status.Error,
43
+ description: Colors.Status.Error,
44
+ command: Colors.Status.Error,
45
+ symbol: Palette.Gray,
46
+ };
47
+ }
48
+ }
49
+ function CommandStatusDisplay({ item, elapsed }) {
50
+ const colors = getStatusColors(item.status);
51
+ const getElapsedTime = () => {
52
+ if (item.status === ExecutionStatus.Running && elapsed !== undefined) {
53
+ return elapsed;
54
+ }
55
+ else if (item.startTime && item.endTime) {
56
+ return item.endTime - item.startTime;
57
+ }
58
+ return undefined;
59
+ };
60
+ const elapsedTime = getElapsedTime();
61
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { paddingLeft: 2, children: [_jsx(Text, { color: colors.icon, children: STATUS_ICONS[item.status] }), _jsx(Text, { color: colors.description, children: item.label || item.command.description }), elapsedTime !== undefined && (_jsxs(Text, { color: Palette.DarkGray, children: [" (", formatDuration(elapsedTime), ")"] }))] }), _jsxs(Box, { paddingLeft: 5, children: [_jsx(Text, { color: colors.symbol, children: "\u221F " }), _jsx(Text, { color: colors.command, children: item.command.command }), item.status === ExecutionStatus.Running && (_jsxs(Text, { children: [' ', _jsx(Spinner, {})] }))] })] }));
62
+ }
63
+ export function Execute({ tasks, state, service, onError, onComplete, onAborted, }) {
64
+ const done = state?.done ?? false;
65
+ const isCurrent = done === false;
66
+ const [error, setError] = useState(null);
67
+ const [isLoading, setIsLoading] = useState(state?.isLoading ?? !done);
68
+ const [isExecuting, setIsExecuting] = useState(false);
69
+ const [commandStatuses, setCommandStatuses] = useState([]);
70
+ const [message, setMessage] = useState('');
71
+ const [currentElapsed, setCurrentElapsed] = useState(0);
72
+ const [runningIndex, setRunningIndex] = useState(null);
73
+ const [outputs, setOutputs] = useState([]);
74
+ useInput((input, key) => {
75
+ if (key.escape && (isLoading || isExecuting) && !done) {
76
+ setIsLoading(false);
77
+ setIsExecuting(false);
78
+ onAborted();
79
+ }
80
+ }, { isActive: (isLoading || isExecuting) && !done });
81
+ // Update elapsed time for running command
82
+ useEffect(() => {
83
+ if (runningIndex === null)
84
+ return;
85
+ const item = commandStatuses[runningIndex];
86
+ if (!item?.startTime)
87
+ return;
88
+ const interval = setInterval(() => {
89
+ setCurrentElapsed((prev) => {
90
+ const next = Date.now() - item.startTime;
91
+ return next !== prev ? next : prev;
92
+ });
93
+ }, 1000);
94
+ return () => clearInterval(interval);
95
+ }, [runningIndex, commandStatuses]);
96
+ // Handle completion callback when execution finishes
97
+ useEffect(() => {
98
+ if (isExecuting || commandStatuses.length === 0 || !outputs.length)
99
+ return;
100
+ // Sum up elapsed times from all commands
101
+ const totalElapsed = commandStatuses.reduce((sum, cmd) => sum + (cmd.elapsed ?? 0), 0);
102
+ onComplete?.(outputs, totalElapsed);
103
+ }, [isExecuting, commandStatuses, outputs, onComplete]);
104
+ useEffect(() => {
105
+ if (done) {
106
+ return;
107
+ }
108
+ if (!service) {
109
+ setError('No service available');
110
+ setIsLoading(false);
111
+ return;
112
+ }
113
+ let mounted = true;
114
+ async function process(svc) {
115
+ const startTime = Date.now();
116
+ try {
117
+ // Format tasks for the execute tool
118
+ const taskDescriptions = tasks
119
+ .map((task) => {
120
+ const params = task.params
121
+ ? ` (params: ${JSON.stringify(task.params)})`
122
+ : '';
123
+ return `- ${task.action}${params}`;
124
+ })
125
+ .join('\n');
126
+ // Call execute tool to get commands
127
+ const result = await svc.processWithTool(taskDescriptions, 'execute');
128
+ const elapsed = Date.now() - startTime;
129
+ const remainingTime = Math.max(0, MINIMUM_PROCESSING_TIME - elapsed);
130
+ await new Promise((resolve) => setTimeout(resolve, remainingTime));
131
+ if (!mounted)
132
+ return;
133
+ if (!result.commands || result.commands.length === 0) {
134
+ setIsLoading(false);
135
+ setOutputs([]);
136
+ onComplete?.([], 0);
137
+ return;
138
+ }
139
+ // Set message and initialize command statuses
140
+ setMessage(result.message);
141
+ setCommandStatuses(result.commands.map((cmd, index) => ({
142
+ command: cmd,
143
+ status: ExecutionStatus.Pending,
144
+ label: tasks[index]?.action,
145
+ })));
146
+ setIsLoading(false);
147
+ setIsExecuting(true);
148
+ // Execute commands sequentially
149
+ const outputs = await executeCommands(result.commands, (progress) => {
150
+ if (!mounted)
151
+ return;
152
+ const now = Date.now();
153
+ setCommandStatuses((prev) => prev.map((item, idx) => {
154
+ if (idx === progress.currentIndex) {
155
+ const isStarting = progress.status === ExecutionStatus.Running &&
156
+ !item.startTime;
157
+ const isEnding = progress.status !== ExecutionStatus.Running &&
158
+ progress.status !== ExecutionStatus.Pending;
159
+ const endTime = isEnding ? now : item.endTime;
160
+ const elapsed = isEnding && item.startTime
161
+ ? Math.floor((now - item.startTime) / 1000) * 1000
162
+ : item.elapsed;
163
+ return {
164
+ ...item,
165
+ status: progress.status,
166
+ output: progress.output,
167
+ startTime: isStarting ? now : item.startTime,
168
+ endTime,
169
+ elapsed,
170
+ };
171
+ }
172
+ return item;
173
+ }));
174
+ if (progress.status === ExecutionStatus.Running) {
175
+ setRunningIndex((prev) => prev !== progress.currentIndex ? progress.currentIndex : prev);
176
+ setCurrentElapsed((prev) => (prev !== 0 ? 0 : prev));
177
+ }
178
+ else if (progress.status === ExecutionStatus.Success ||
179
+ progress.status === ExecutionStatus.Failed) {
180
+ setRunningIndex((prev) => (prev !== null ? null : prev));
181
+ }
182
+ });
183
+ if (mounted) {
184
+ setOutputs(outputs);
185
+ setIsExecuting(false);
186
+ }
187
+ }
188
+ catch (err) {
189
+ const elapsed = Date.now() - startTime;
190
+ const remainingTime = Math.max(0, MINIMUM_PROCESSING_TIME - elapsed);
191
+ await new Promise((resolve) => setTimeout(resolve, remainingTime));
192
+ if (mounted) {
193
+ const errorMessage = formatErrorMessage(err);
194
+ setIsLoading(false);
195
+ setIsExecuting(false);
196
+ if (onError) {
197
+ onError(errorMessage);
198
+ }
199
+ else {
200
+ setError(errorMessage);
201
+ }
202
+ }
203
+ }
204
+ }
205
+ process(service);
206
+ return () => {
207
+ mounted = false;
208
+ };
209
+ }, [tasks, done, service, onComplete, onError]);
210
+ // Return null only when loading completes with no commands
211
+ if (done && commandStatuses.length === 0 && !error) {
212
+ return null;
213
+ }
214
+ // Show completed steps when done
215
+ const showCompletedSteps = done && commandStatuses.length > 0;
216
+ return (_jsxs(Box, { alignSelf: "flex-start", flexDirection: "column", children: [isLoading && (_jsxs(Box, { children: [_jsx(Text, { color: getTextColor(isCurrent), children: "Preparing commands. " }), _jsx(Spinner, {})] })), (isExecuting || showCompletedSteps) && (_jsxs(Box, { flexDirection: "column", children: [message && (_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { color: getTextColor(isCurrent), children: message }), isExecuting && (_jsxs(Text, { children: [' ', _jsx(Spinner, {})] }))] })), commandStatuses.map((item, index) => (_jsx(Box, { marginBottom: index < commandStatuses.length - 1 ? 1 : 0, children: _jsx(CommandStatusDisplay, { item: item, elapsed: index === runningIndex ? currentElapsed : undefined }) }, index)))] })), error && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: Colors.Status.Error, children: ["Error: ", error] }) }))] }));
217
+ }
package/dist/ui/Main.js CHANGED
@@ -2,20 +2,20 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import React from 'react';
3
3
  import { FeedbackType } from '../types/types.js';
4
4
  import { createAnthropicService, } from '../services/anthropic.js';
5
+ import { createCommandDefinition, createConfigDefinition, createFeedback, createMessage, createWelcomeDefinition, isStateless, markAsDone, } from '../services/components.js';
5
6
  import { getConfigurationRequiredMessage, hasValidAnthropicKey, loadConfig, loadDebugSetting, saveDebugSetting, } from '../services/configuration.js';
6
7
  import { registerGlobalShortcut } from '../services/keyboard.js';
7
8
  import { getCancellationMessage } from '../services/messages.js';
8
- import { createCommandDefinition, createConfigDefinition, createFeedback, createMessage, createWelcomeDefinition, isStateless, markAsDone, } from '../services/components.js';
9
9
  import { exitApp } from '../services/process.js';
10
- import { createAnswerAbortedHandler, createAnswerCompleteHandler, createAnswerErrorHandler, } from '../handlers/answer.js';
11
- import { createCommandAbortedHandler, createCommandCompleteHandler, createCommandErrorHandler, } from '../handlers/command.js';
12
- import { createConfigAbortedHandler, createConfigFinishedHandler, } from '../handlers/config.js';
13
- import { createExecutionCancelledHandler, createExecutionConfirmedHandler, } from '../handlers/execution.js';
14
- import { createIntrospectAbortedHandler, createIntrospectCompleteHandler, createIntrospectErrorHandler, } from '../handlers/introspect.js';
15
- import { createPlanAbortedHandler, createPlanAbortHandlerFactory, createPlanSelectionConfirmedHandler, } from '../handlers/plan.js';
10
+ import { createAnswerHandlers } from '../handlers/answer.js';
11
+ import { createCommandHandlers } from '../handlers/command.js';
12
+ import { createConfigHandlers } from '../handlers/config.js';
13
+ import { createExecuteHandlers } from '../handlers/execute.js';
14
+ import { createExecutionHandlers } from '../handlers/execution.js';
15
+ import { createIntrospectHandlers } from '../handlers/introspect.js';
16
+ import { createPlanHandlers } from '../handlers/plan.js';
16
17
  import { Column } from './Column.js';
17
18
  export const Main = ({ app, command }) => {
18
- // Initialize service from existing config if available
19
19
  const [service, setService] = React.useState(() => {
20
20
  if (hasValidAnthropicKey()) {
21
21
  const config = loadConfig();
@@ -26,14 +26,8 @@ export const Main = ({ app, command }) => {
26
26
  const [timeline, setTimeline] = React.useState([]);
27
27
  const [queue, setQueue] = React.useState([]);
28
28
  const [isDebug, setIsDebug] = React.useState(() => loadDebugSetting());
29
- // Use ref to track latest timeline for callbacks
30
- const timelineRef = React.useRef(timeline);
31
- React.useEffect(() => {
32
- timelineRef.current = timeline;
33
- }, [timeline]);
34
29
  // Register global keyboard shortcuts
35
30
  React.useEffect(() => {
36
- // Shift+Tab: Toggle debug mode
37
31
  registerGlobalShortcut('shift+tab', () => {
38
32
  setIsDebug((prev) => {
39
33
  const newValue = !prev;
@@ -50,7 +44,6 @@ export const Main = ({ app, command }) => {
50
44
  if (currentQueue.length === 0)
51
45
  return currentQueue;
52
46
  const [first, ...rest] = currentQueue;
53
- // Stateless components auto-complete immediately
54
47
  if (isStateless(first)) {
55
48
  addToTimeline(first);
56
49
  return rest;
@@ -58,7 +51,7 @@ export const Main = ({ app, command }) => {
58
51
  return currentQueue;
59
52
  });
60
53
  }, [addToTimeline]);
61
- const handleCommandError = React.useCallback((error) => setQueue(createCommandErrorHandler(addToTimeline)(error)), [addToTimeline]);
54
+ // Core abort handler
62
55
  const handleAborted = React.useCallback((operationName) => {
63
56
  setQueue((currentQueue) => {
64
57
  if (currentQueue.length === 0)
@@ -71,86 +64,54 @@ export const Main = ({ app, command }) => {
71
64
  return [];
72
65
  });
73
66
  }, [addToTimeline]);
74
- const handleConfigAborted = React.useCallback(createConfigAbortedHandler(handleAborted), [handleAborted]);
75
- const handlePlanAborted = React.useCallback(createPlanAbortedHandler(handleAborted), [handleAborted]);
76
- const createPlanAbortHandler = React.useCallback(createPlanAbortHandlerFactory(handleAborted, handlePlanAborted), [handleAborted, handlePlanAborted]);
77
- const handleCommandAborted = React.useCallback(createCommandAbortedHandler(handleAborted), [handleAborted]);
78
- const handleRefinementAborted = React.useCallback(() => {
79
- handleAborted('Plan refinement');
80
- }, [handleAborted]);
81
- const handleIntrospectAborted = React.useCallback(createIntrospectAbortedHandler(handleAborted), [handleAborted]);
82
- const handleIntrospectError = React.useCallback((error) => setQueue(createIntrospectErrorHandler(addToTimeline)(error)), [addToTimeline]);
83
- const handleIntrospectComplete = React.useCallback((message, capabilities) => setQueue(createIntrospectCompleteHandler(addToTimeline)(message, capabilities)), [addToTimeline]);
84
- const handleAnswerAborted = React.useCallback(createAnswerAbortedHandler(handleAborted), [handleAborted]);
85
- const handleAnswerError = React.useCallback((error) => setQueue(createAnswerErrorHandler(addToTimeline)(error)), [addToTimeline]);
86
- const handleAnswerComplete = React.useCallback((answer) => setQueue(createAnswerCompleteHandler(addToTimeline)(answer)), [addToTimeline]);
87
- const handleExecutionConfirmed = React.useCallback(() => setQueue(createExecutionConfirmedHandler(timelineRef, addToTimeline, service, handleIntrospectError, handleIntrospectComplete, handleIntrospectAborted, handleAnswerError, handleAnswerComplete, handleAnswerAborted, setQueue)()), [
88
- addToTimeline,
89
- service,
90
- handleIntrospectError,
91
- handleIntrospectComplete,
92
- handleIntrospectAborted,
93
- handleAnswerError,
94
- handleAnswerComplete,
95
- handleAnswerAborted,
96
- ]);
97
- const handleExecutionCancelled = React.useCallback(() => setQueue(createExecutionCancelledHandler(timelineRef, addToTimeline)()), [addToTimeline]);
98
- const handlePlanSelectionConfirmed = React.useCallback(createPlanSelectionConfirmedHandler(addToTimeline, service, handleRefinementAborted, createPlanAbortHandler, handleExecutionConfirmed, handleExecutionCancelled, setQueue), [
67
+ // Create operations object
68
+ const ops = React.useMemo(() => ({
99
69
  addToTimeline,
70
+ setQueue,
100
71
  service,
101
- handleRefinementAborted,
102
- createPlanAbortHandler,
103
- handleExecutionConfirmed,
104
- handleExecutionCancelled,
105
- ]);
106
- const handleCommandComplete = React.useCallback((message, tasks) => setQueue(createCommandCompleteHandler(addToTimeline, createPlanAbortHandler, handlePlanSelectionConfirmed, handleExecutionConfirmed, handleExecutionCancelled)(message, tasks)), [
107
- addToTimeline,
108
- createPlanAbortHandler,
109
- handlePlanSelectionConfirmed,
110
- handleExecutionConfirmed,
111
- handleExecutionCancelled,
112
- ]);
113
- const handleConfigFinished = React.useCallback((config) => setQueue(createConfigFinishedHandler(addToTimeline, command, handleCommandError, handleCommandComplete, handleCommandAborted, setService)(config)), [
114
- addToTimeline,
115
- command,
116
- handleCommandError,
117
- handleCommandComplete,
118
- handleCommandAborted,
119
- ]);
72
+ }), [addToTimeline, service]);
73
+ // Create handlers in dependency order
74
+ const introspectHandlers = React.useMemo(() => createIntrospectHandlers(ops, handleAborted), [ops, handleAborted]);
75
+ const answerHandlers = React.useMemo(() => createAnswerHandlers(ops, handleAborted), [ops, handleAborted]);
76
+ const executeHandlers = React.useMemo(() => createExecuteHandlers(ops, handleAborted), [ops, handleAborted]);
77
+ const executionHandlers = React.useMemo(() => createExecutionHandlers(ops, {
78
+ introspect: introspectHandlers,
79
+ answer: answerHandlers,
80
+ execute: executeHandlers,
81
+ }), [ops, introspectHandlers, answerHandlers, executeHandlers]);
82
+ const planHandlers = React.useMemo(() => createPlanHandlers(ops, handleAborted, executionHandlers), [ops, handleAborted, executionHandlers]);
83
+ const commandHandlers = React.useMemo(() => createCommandHandlers(ops, handleAborted, planHandlers, executionHandlers), [ops, handleAborted, planHandlers, executionHandlers]);
84
+ const configHandlers = React.useMemo(() => createConfigHandlers(ops, handleAborted, command, commandHandlers, setService), [ops, handleAborted, command, commandHandlers]);
120
85
  // Initialize queue on mount
121
86
  React.useEffect(() => {
122
87
  const hasConfig = !!service;
123
88
  if (command && hasConfig) {
124
- // With command + valid config: [Command]
125
89
  setQueue([
126
- createCommandDefinition(command, service, handleCommandError, handleCommandComplete, handleCommandAborted),
90
+ createCommandDefinition(command, service, commandHandlers.onError, commandHandlers.onComplete, commandHandlers.onAborted),
127
91
  ]);
128
92
  }
129
93
  else if (command && !hasConfig) {
130
- // With command + no config: [Message, Config] (Command added after config)
131
94
  setQueue([
132
95
  createMessage(getConfigurationRequiredMessage()),
133
- createConfigDefinition(handleConfigFinished, handleConfigAborted),
96
+ createConfigDefinition(configHandlers.onFinished, configHandlers.onAborted),
134
97
  ]);
135
98
  }
136
99
  else if (!command && hasConfig) {
137
- // No command + valid config: [Welcome]
138
100
  setQueue([createWelcomeDefinition(app)]);
139
101
  }
140
102
  else {
141
- // No command + no config: [Welcome, Message, Config]
142
103
  setQueue([
143
104
  createWelcomeDefinition(app),
144
105
  createMessage(getConfigurationRequiredMessage(true)),
145
- createConfigDefinition(handleConfigFinished, handleConfigAborted),
106
+ createConfigDefinition(configHandlers.onFinished, configHandlers.onAborted),
146
107
  ]);
147
108
  }
148
- }, []); // Only run on mount
109
+ }, []);
149
110
  // Process queue whenever it changes
150
111
  React.useEffect(() => {
151
112
  processNextInQueue();
152
113
  }, [queue, processNextInQueue]);
153
- // Exit when queue is empty and timeline has content (all stateless components done)
114
+ // Exit when queue is empty and timeline has content
154
115
  React.useEffect(() => {
155
116
  if (queue.length === 0 && timeline.length > 0) {
156
117
  exitApp(0);
@@ -3,15 +3,20 @@ import { useEffect, useState } from 'react';
3
3
  import { Text } from 'ink';
4
4
  const FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
5
5
  const INTERVAL = 80;
6
+ const CYCLE = FRAMES.length * INTERVAL;
7
+ function getFrame() {
8
+ return Math.floor((Date.now() % CYCLE) / INTERVAL);
9
+ }
6
10
  export function Spinner() {
7
- const [frame, setFrame] = useState(0);
11
+ const [frame, setFrame] = useState(getFrame);
8
12
  useEffect(() => {
9
13
  const timer = setInterval(() => {
10
- setFrame((prev) => (prev + 1) % FRAMES.length);
14
+ setFrame((prev) => {
15
+ const next = getFrame();
16
+ return next !== prev ? next : prev;
17
+ });
11
18
  }, INTERVAL);
12
- return () => {
13
- clearInterval(timer);
14
- };
19
+ return () => clearInterval(timer);
15
20
  }, []);
16
21
  return _jsx(Text, { color: "blueBright", children: FRAMES[frame] });
17
22
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prompt-language-shell",
3
- "version": "0.4.8",
3
+ "version": "0.4.9",
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.69.0",
50
- "ink": "^6.5.0",
49
+ "@anthropic-ai/sdk": "^0.70.1",
50
+ "ink": "^6.5.1",
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
56
  "@types/node": "^24.10.1",
57
- "@types/react": "^19.2.5",
58
- "@vitest/coverage-v8": "^4.0.9",
57
+ "@types/react": "^19.2.6",
58
+ "@vitest/coverage-v8": "^4.0.12",
59
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
63
  "typescript": "^5.9.3",
64
- "typescript-eslint": "^8.46.4",
65
- "vitest": "^4.0.9"
64
+ "typescript-eslint": "^8.47.0",
65
+ "vitest": "^4.0.12"
66
66
  }
67
67
  }