prompt-language-shell 0.4.6 → 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.
package/dist/ui/Answer.js CHANGED
@@ -1,9 +1,11 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useState } from 'react';
3
- import { Box, Text, useInput } from 'ink';
3
+ import { Box, Text } from 'ink';
4
4
  import { Colors, getTextColor } from '../services/colors.js';
5
+ import { useInput } from '../services/keyboard.js';
6
+ import { formatErrorMessage } from '../services/messages.js';
5
7
  import { Spinner } from './Spinner.js';
6
- const MinimumProcessingTime = 400;
8
+ const MINIMUM_PROCESSING_TIME = 400;
7
9
  export function Answer({ question, state, service, onError, onComplete, onAborted, }) {
8
10
  const done = state?.done ?? false;
9
11
  const isCurrent = done === false;
@@ -33,7 +35,7 @@ export function Answer({ question, state, service, onError, onComplete, onAborte
33
35
  // Call answer tool
34
36
  const result = await svc.processWithTool(question, 'answer');
35
37
  const elapsed = Date.now() - startTime;
36
- const remainingTime = Math.max(0, MinimumProcessingTime - elapsed);
38
+ const remainingTime = Math.max(0, MINIMUM_PROCESSING_TIME - elapsed);
37
39
  await new Promise((resolve) => setTimeout(resolve, remainingTime));
38
40
  if (mounted) {
39
41
  // Extract answer from result
@@ -44,10 +46,10 @@ export function Answer({ question, state, service, onError, onComplete, onAborte
44
46
  }
45
47
  catch (err) {
46
48
  const elapsed = Date.now() - startTime;
47
- const remainingTime = Math.max(0, MinimumProcessingTime - elapsed);
49
+ const remainingTime = Math.max(0, MINIMUM_PROCESSING_TIME - elapsed);
48
50
  await new Promise((resolve) => setTimeout(resolve, remainingTime));
49
51
  if (mounted) {
50
- const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
52
+ const errorMessage = formatErrorMessage(err);
51
53
  setIsLoading(false);
52
54
  if (onError) {
53
55
  onError(errorMessage);
@@ -1,7 +1,10 @@
1
1
  import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useEffect, useState } from 'react';
3
- import { Box, Text, useInput } from 'ink';
3
+ import { Box, Text } from 'ink';
4
+ import { TaskType } from '../types/types.js';
4
5
  import { Colors } from '../services/colors.js';
6
+ import { useInput } from '../services/keyboard.js';
7
+ import { formatErrorMessage } from '../services/messages.js';
5
8
  import { Spinner } from './Spinner.js';
6
9
  const MIN_PROCESSING_TIME = 1000; // purely for visual effect
7
10
  export function Command({ command, state, service, children, onError, onComplete, onAborted, }) {
@@ -29,7 +32,16 @@ export function Command({ command, state, service, children, onError, onComplete
29
32
  async function process(svc) {
30
33
  const startTime = Date.now();
31
34
  try {
32
- const result = await svc.processWithTool(command, 'plan');
35
+ let result = await svc.processWithTool(command, 'plan');
36
+ // If all tasks are config type, delegate to CONFIG tool
37
+ const allConfig = result.tasks.length > 0 &&
38
+ result.tasks.every((task) => task.type === TaskType.Config);
39
+ if (allConfig) {
40
+ // Extract query from first config task params, default to 'app'
41
+ const query = result.tasks[0].params?.query || 'app';
42
+ // Call CONFIG tool to get specific config keys
43
+ result = await svc.processWithTool(query, 'config');
44
+ }
33
45
  const elapsed = Date.now() - startTime;
34
46
  const remainingTime = Math.max(0, MIN_PROCESSING_TIME - elapsed);
35
47
  await new Promise((resolve) => setTimeout(resolve, remainingTime));
@@ -43,7 +55,7 @@ export function Command({ command, state, service, children, onError, onComplete
43
55
  const remainingTime = Math.max(0, MIN_PROCESSING_TIME - elapsed);
44
56
  await new Promise((resolve) => setTimeout(resolve, remainingTime));
45
57
  if (mounted) {
46
- const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
58
+ const errorMessage = formatErrorMessage(err);
47
59
  setIsLoading(false);
48
60
  if (onError) {
49
61
  onError(errorMessage);
@@ -6,6 +6,7 @@ import { AnswerDisplay } from './AnswerDisplay.js';
6
6
  import { Command } from './Command.js';
7
7
  import { Confirm } from './Confirm.js';
8
8
  import { Config } from './Config.js';
9
+ import { Execute } from './Execute.js';
9
10
  import { Feedback } from './Feedback.js';
10
11
  import { Introspect } from './Introspect.js';
11
12
  import { Message } from './Message.js';
@@ -49,7 +50,7 @@ export const Component = React.memo(function Component({ def, debug, }) {
49
50
  case ComponentName.Introspect: {
50
51
  const props = def.props;
51
52
  const state = def.state;
52
- return _jsx(Introspect, { ...props, state: state });
53
+ return _jsx(Introspect, { ...props, state: state, debug: debug });
53
54
  }
54
55
  case ComponentName.Report:
55
56
  return _jsx(Report, { ...def.props });
@@ -60,5 +61,10 @@ export const Component = React.memo(function Component({ def, debug, }) {
60
61
  }
61
62
  case ComponentName.AnswerDisplay:
62
63
  return _jsx(AnswerDisplay, { ...def.props });
64
+ case ComponentName.Execute: {
65
+ const props = def.props;
66
+ const state = def.state;
67
+ return _jsx(Execute, { ...props, state: state });
68
+ }
63
69
  }
64
70
  });
package/dist/ui/Config.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import React from 'react';
3
- import { Box, Text, useFocus, useInput } from 'ink';
3
+ import { Box, Text, useFocus } from 'ink';
4
4
  import TextInput from 'ink-text-input';
5
5
  import { Colors } from '../services/colors.js';
6
+ import { useInput } from '../services/keyboard.js';
6
7
  export var StepType;
7
8
  (function (StepType) {
8
9
  StepType["Text"] = "text";
@@ -20,11 +21,13 @@ function TextStep({ value, placeholder, validate, onChange, onSubmit, }) {
20
21
  }
21
22
  };
22
23
  const handleSubmit = (value) => {
23
- if (!validate(value)) {
24
+ // Use placeholder if input is empty
25
+ const finalValue = value || placeholder || '';
26
+ if (!validate(finalValue)) {
24
27
  setValidationFailed(true);
25
28
  return;
26
29
  }
27
- onSubmit(value);
30
+ onSubmit(finalValue);
28
31
  };
29
32
  // Handle input manually when validation fails
30
33
  useInput((input, key) => {
@@ -1,7 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import React from 'react';
3
- import { Box, Text, useInput } from 'ink';
4
- import { Colors } from '../services/colors.js';
3
+ import { Box, Text } from 'ink';
4
+ import { Colors, Palette } from '../services/colors.js';
5
+ import { useInput } from '../services/keyboard.js';
5
6
  export function Confirm({ message, state, onConfirmed, onCancelled, }) {
6
7
  const done = state?.done ?? false;
7
8
  const isCurrent = done === false;
@@ -29,7 +30,7 @@ export function Confirm({ message, state, onConfirmed, onCancelled, }) {
29
30
  }
30
31
  }, { isActive: !done });
31
32
  const options = [
32
- { label: 'yes', value: 'yes', color: Colors.Action.Execute },
33
+ { label: 'yes', value: 'yes', color: Palette.BrightGreen },
33
34
  { label: 'no', value: 'no', color: Colors.Status.Error },
34
35
  ];
35
36
  if (done) {
@@ -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
+ }
@@ -1,7 +1,9 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useState } from 'react';
3
- import { Box, Text, useInput } from 'ink';
3
+ import { Box, Text } from 'ink';
4
4
  import { Colors, getTextColor } from '../services/colors.js';
5
+ import { useInput } from '../services/keyboard.js';
6
+ import { formatErrorMessage } from '../services/messages.js';
5
7
  import { Spinner } from './Spinner.js';
6
8
  const MIN_PROCESSING_TIME = 1000;
7
9
  const BUILT_IN_CAPABILITIES = new Set([
@@ -12,26 +14,32 @@ const BUILT_IN_CAPABILITIES = new Set([
12
14
  'EXECUTE',
13
15
  'REPORT',
14
16
  ]);
17
+ const INDIRECT_CAPABILITIES = new Set(['PLAN', 'REPORT']);
15
18
  function parseCapabilityFromTask(task) {
16
19
  // Parse "NAME: Description" format from task.action
17
20
  const colonIndex = task.action.indexOf(':');
18
21
  if (colonIndex === -1) {
22
+ const upperName = task.action.toUpperCase();
19
23
  return {
20
24
  name: task.action,
21
25
  description: '',
22
- isBuiltIn: BUILT_IN_CAPABILITIES.has(task.action.toUpperCase()),
26
+ isBuiltIn: BUILT_IN_CAPABILITIES.has(upperName),
27
+ isIndirect: INDIRECT_CAPABILITIES.has(upperName),
23
28
  };
24
29
  }
25
30
  const name = task.action.substring(0, colonIndex).trim();
26
31
  const description = task.action.substring(colonIndex + 1).trim();
27
- const isBuiltIn = BUILT_IN_CAPABILITIES.has(name.toUpperCase());
32
+ const upperName = name.toUpperCase();
33
+ const isBuiltIn = BUILT_IN_CAPABILITIES.has(upperName);
34
+ const isIndirect = INDIRECT_CAPABILITIES.has(upperName);
28
35
  return {
29
36
  name,
30
37
  description,
31
38
  isBuiltIn,
39
+ isIndirect,
32
40
  };
33
41
  }
34
- export function Introspect({ tasks, state, service, children, onError, onComplete, onAborted, }) {
42
+ export function Introspect({ tasks, state, service, children, debug = false, onError, onComplete, onAborted, }) {
35
43
  const done = state?.done ?? false;
36
44
  const isCurrent = done === false;
37
45
  const [error, setError] = useState(null);
@@ -66,7 +74,12 @@ export function Introspect({ tasks, state, service, children, onError, onComplet
66
74
  await new Promise((resolve) => setTimeout(resolve, remainingTime));
67
75
  if (mounted) {
68
76
  // Parse capabilities from returned tasks
69
- const capabilities = result.tasks.map(parseCapabilityFromTask);
77
+ let capabilities = result.tasks.map(parseCapabilityFromTask);
78
+ // Filter out internal capabilities when not in debug mode
79
+ if (!debug) {
80
+ capabilities = capabilities.filter((cap) => cap.name.toUpperCase() !== 'PLAN' &&
81
+ cap.name.toUpperCase() !== 'REPORT');
82
+ }
70
83
  setIsLoading(false);
71
84
  onComplete?.(result.message, capabilities);
72
85
  }
@@ -76,7 +89,7 @@ export function Introspect({ tasks, state, service, children, onError, onComplet
76
89
  const remainingTime = Math.max(0, MIN_PROCESSING_TIME - elapsed);
77
90
  await new Promise((resolve) => setTimeout(resolve, remainingTime));
78
91
  if (mounted) {
79
- const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
92
+ const errorMessage = formatErrorMessage(err);
80
93
  setIsLoading(false);
81
94
  if (onError) {
82
95
  onError(errorMessage);
@@ -91,7 +104,7 @@ export function Introspect({ tasks, state, service, children, onError, onComplet
91
104
  return () => {
92
105
  mounted = false;
93
106
  };
94
- }, [tasks, done, service]);
107
+ }, [tasks, done, service, debug, onComplete, onError]);
95
108
  // Don't render wrapper when done and nothing to show
96
109
  if (!isLoading && !error && !children) {
97
110
  return null;
package/dist/ui/Main.js CHANGED
@@ -1,21 +1,21 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import React from 'react';
3
- import { useInput } from 'ink';
4
3
  import { FeedbackType } from '../types/types.js';
5
4
  import { createAnthropicService, } from '../services/anthropic.js';
5
+ import { createCommandDefinition, createConfigDefinition, createFeedback, createMessage, createWelcomeDefinition, isStateless, markAsDone, } from '../services/components.js';
6
6
  import { getConfigurationRequiredMessage, hasValidAnthropicKey, loadConfig, loadDebugSetting, saveDebugSetting, } from '../services/configuration.js';
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,22 +26,16 @@ 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);
29
+ // Register global keyboard shortcuts
31
30
  React.useEffect(() => {
32
- timelineRef.current = timeline;
33
- }, [timeline]);
34
- // Top-level Shift+Tab handler for debug mode toggle
35
- // Child components must ignore Shift+Tab to prevent conflicts
36
- useInput((input, key) => {
37
- if (key.shift && key.tab) {
31
+ registerGlobalShortcut('shift+tab', () => {
38
32
  setIsDebug((prev) => {
39
33
  const newValue = !prev;
40
34
  saveDebugSetting(newValue);
41
35
  return newValue;
42
36
  });
43
- }
44
- }, { isActive: true });
37
+ });
38
+ }, []);
45
39
  const addToTimeline = React.useCallback((...items) => {
46
40
  setTimeline((timeline) => [...timeline, ...items]);
47
41
  }, []);
@@ -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)()), [
67
+ // Create operations object
68
+ const ops = React.useMemo(() => ({
88
69
  addToTimeline,
70
+ setQueue,
89
71
  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), [
99
- addToTimeline,
100
- 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);
package/dist/ui/Plan.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useState } from 'react';
3
- import { Box, useInput } from 'ink';
3
+ import { Box } from 'ink';
4
4
  import { TaskType } from '../types/types.js';
5
5
  import { getTaskColors } from '../services/colors.js';
6
+ import { useInput } from '../services/keyboard.js';
6
7
  import { Label } from './Label.js';
7
8
  import { List } from './List.js';
8
9
  function taskToListItem(task, highlightedChildIndex = null, isDefineTaskWithoutSelection = false, isCurrent = false) {
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Box, useInput } from 'ink';
2
+ import { Box } from 'ink';
3
+ import { useInput } from '../services/keyboard.js';
3
4
  import { Message } from './Message.js';
4
5
  import { Spinner } from './Spinner.js';
5
6
  export const Refinement = ({ text, state, onAborted }) => {