prompt-language-shell 0.6.2 → 0.6.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.
@@ -1,8 +1,9 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { existsSync, readFileSync } from 'node:fs';
3
3
  import { ComponentName } from '../types/types.js';
4
+ import { ComponentStatus, } from '../types/components.js';
4
5
  import { parse as parseYaml } from 'yaml';
5
- import { getConfigPath, getConfigSchema, loadConfig, } from './configuration.js';
6
+ import { ConfigDefinitionType, getConfigPath, getConfigSchema, loadConfig, } from './configuration.js';
6
7
  import { getConfirmationMessage } from './messages.js';
7
8
  import { StepType } from '../ui/Config.js';
8
9
  export function createWelcomeDefinition(app) {
@@ -39,15 +40,15 @@ function getConfigValue(config, key) {
39
40
  */
40
41
  function getValidator(definition) {
41
42
  switch (definition.type) {
42
- case 'regexp':
43
+ case ConfigDefinitionType.RegExp:
43
44
  return (value) => definition.pattern.test(value);
44
- case 'string':
45
+ case ConfigDefinitionType.String:
45
46
  return () => true; // Strings are always valid
46
- case 'enum':
47
+ case ConfigDefinitionType.Enum:
47
48
  return (value) => definition.values.includes(value);
48
- case 'number':
49
+ case ConfigDefinitionType.Number:
49
50
  return (value) => !isNaN(Number(value));
50
- case 'boolean':
51
+ case ConfigDefinitionType.Boolean:
51
52
  return (value) => value === 'true' || value === 'false';
52
53
  }
53
54
  }
@@ -103,11 +104,11 @@ export function createConfigStepsFromSchema(keys) {
103
104
  const shortKey = keyParts[keyParts.length - 1];
104
105
  // Map definition to ConfigStep based on type
105
106
  switch (definition.type) {
106
- case 'regexp':
107
- case 'string': {
107
+ case ConfigDefinitionType.RegExp:
108
+ case ConfigDefinitionType.String: {
108
109
  const value = currentValue !== undefined && typeof currentValue === 'string'
109
110
  ? currentValue
110
- : definition.type === 'string'
111
+ : definition.type === ConfigDefinitionType.String
111
112
  ? (definition.default ?? '')
112
113
  : null;
113
114
  return {
@@ -119,7 +120,7 @@ export function createConfigStepsFromSchema(keys) {
119
120
  validate: getValidator(definition),
120
121
  };
121
122
  }
122
- case 'number': {
123
+ case ConfigDefinitionType.Number: {
123
124
  const value = currentValue !== undefined && typeof currentValue === 'number'
124
125
  ? String(currentValue)
125
126
  : definition.default !== undefined
@@ -134,7 +135,7 @@ export function createConfigStepsFromSchema(keys) {
134
135
  validate: getValidator(definition),
135
136
  };
136
137
  }
137
- case 'enum': {
138
+ case ConfigDefinitionType.Enum: {
138
139
  const currentStr = currentValue !== undefined && typeof currentValue === 'string'
139
140
  ? currentValue
140
141
  : definition.default;
@@ -154,7 +155,7 @@ export function createConfigStepsFromSchema(keys) {
154
155
  validate: getValidator(definition),
155
156
  };
156
157
  }
157
- case 'boolean': {
158
+ case ConfigDefinitionType.Boolean: {
158
159
  const currentBool = currentValue !== undefined && typeof currentValue === 'boolean'
159
160
  ? currentValue
160
161
  : undefined;
@@ -178,6 +179,7 @@ export function createConfigDefinition(onFinished, onAborted) {
178
179
  return {
179
180
  id: randomUUID(),
180
181
  name: ComponentName.Config,
182
+ status: ComponentStatus.Awaiting,
181
183
  state: {},
182
184
  props: {
183
185
  steps: createConfigSteps(),
@@ -193,6 +195,7 @@ export function createConfigDefinitionWithKeys(keys, onFinished, onAborted) {
193
195
  return {
194
196
  id: randomUUID(),
195
197
  name: ComponentName.Config,
198
+ status: ComponentStatus.Awaiting,
196
199
  state: {},
197
200
  props: {
198
201
  steps: createConfigStepsFromSchema(keys),
@@ -205,6 +208,7 @@ export function createCommandDefinition(command, service) {
205
208
  return {
206
209
  id: randomUUID(),
207
210
  name: ComponentName.Command,
211
+ status: ComponentStatus.Awaiting,
208
212
  state: {},
209
213
  props: {
210
214
  command,
@@ -216,6 +220,7 @@ export function createPlanDefinition(message, tasks, onSelectionConfirmed) {
216
220
  return {
217
221
  id: randomUUID(),
218
222
  name: ComponentName.Plan,
223
+ status: ComponentStatus.Awaiting,
219
224
  state: {
220
225
  highlightedIndex: null,
221
226
  currentDefineGroupIndex: 0,
@@ -251,6 +256,7 @@ export function createRefinement(text, onAborted) {
251
256
  return {
252
257
  id: randomUUID(),
253
258
  name: ComponentName.Refinement,
259
+ status: ComponentStatus.Awaiting,
254
260
  state: {},
255
261
  props: {
256
262
  text,
@@ -262,6 +268,7 @@ export function createConfirmDefinition(onConfirmed, onCancelled) {
262
268
  return {
263
269
  id: randomUUID(),
264
270
  name: ComponentName.Confirm,
271
+ status: ComponentStatus.Awaiting,
265
272
  state: {},
266
273
  props: {
267
274
  message: getConfirmationMessage(),
@@ -274,6 +281,7 @@ export function createIntrospectDefinition(tasks, service) {
274
281
  return {
275
282
  id: randomUUID(),
276
283
  name: ComponentName.Introspect,
284
+ status: ComponentStatus.Awaiting,
277
285
  state: {},
278
286
  props: {
279
287
  tasks,
@@ -295,6 +303,7 @@ export function createAnswerDefinition(question, service) {
295
303
  return {
296
304
  id: randomUUID(),
297
305
  name: ComponentName.Answer,
306
+ status: ComponentStatus.Awaiting,
298
307
  state: {},
299
308
  props: {
300
309
  question,
@@ -308,16 +317,16 @@ export function isStateless(component) {
308
317
  /**
309
318
  * Mark a component as done. Returns the component to be added to timeline.
310
319
  * Components use handlers.updateState to save their state before completion,
311
- * so this function simply returns the component as-is.
320
+ * so this function sets the status to Done and returns the updated component.
312
321
  */
313
322
  export function markAsDone(component) {
314
- // State already updated via handlers.updateState
315
- return component;
323
+ return { ...component, status: ComponentStatus.Done };
316
324
  }
317
325
  export function createExecuteDefinition(tasks, service) {
318
326
  return {
319
327
  id: randomUUID(),
320
328
  name: ComponentName.Execute,
329
+ status: ComponentStatus.Awaiting,
321
330
  state: {},
322
331
  props: {
323
332
  tasks,
@@ -329,6 +338,7 @@ export function createValidateDefinition(missingConfig, userRequest, service, on
329
338
  return {
330
339
  id: randomUUID(),
331
340
  name: ComponentName.Validate,
341
+ status: ComponentStatus.Awaiting,
332
342
  state: {},
333
343
  props: {
334
344
  missingConfig,
@@ -9,6 +9,14 @@ export var AnthropicModel;
9
9
  AnthropicModel["Opus"] = "claude-opus-4-1";
10
10
  })(AnthropicModel || (AnthropicModel = {}));
11
11
  export const SUPPORTED_MODELS = Object.values(AnthropicModel);
12
+ export var ConfigDefinitionType;
13
+ (function (ConfigDefinitionType) {
14
+ ConfigDefinitionType["RegExp"] = "regexp";
15
+ ConfigDefinitionType["String"] = "string";
16
+ ConfigDefinitionType["Enum"] = "enum";
17
+ ConfigDefinitionType["Number"] = "number";
18
+ ConfigDefinitionType["Boolean"] = "boolean";
19
+ })(ConfigDefinitionType || (ConfigDefinitionType = {}));
12
20
  export class ConfigError extends Error {
13
21
  origin;
14
22
  constructor(message, origin) {
@@ -172,20 +180,20 @@ export function getConfigurationRequiredMessage(forFutureUse = false) {
172
180
  */
173
181
  const coreConfigSchema = {
174
182
  'anthropic.key': {
175
- type: 'regexp',
183
+ type: ConfigDefinitionType.RegExp,
176
184
  required: true,
177
185
  pattern: /^sk-ant-api03-[A-Za-z0-9_-]{95}$/,
178
186
  description: 'Anthropic API key',
179
187
  },
180
188
  'anthropic.model': {
181
- type: 'enum',
189
+ type: ConfigDefinitionType.Enum,
182
190
  required: true,
183
191
  values: SUPPORTED_MODELS,
184
192
  default: AnthropicModel.Haiku,
185
193
  description: 'Anthropic model',
186
194
  },
187
195
  'settings.debug': {
188
- type: 'boolean',
196
+ type: ConfigDefinitionType.Boolean,
189
197
  required: false,
190
198
  description: 'Debug mode',
191
199
  },
@@ -239,20 +247,20 @@ export function getMissingConfigKeys() {
239
247
  // Validate based on type
240
248
  let isValid = false;
241
249
  switch (definition.type) {
242
- case 'regexp':
250
+ case ConfigDefinitionType.RegExp:
243
251
  isValid = typeof value === 'string' && definition.pattern.test(value);
244
252
  break;
245
- case 'string':
253
+ case ConfigDefinitionType.String:
246
254
  isValid = typeof value === 'string';
247
255
  break;
248
- case 'enum':
256
+ case ConfigDefinitionType.Enum:
249
257
  isValid =
250
258
  typeof value === 'string' && definition.values.includes(value);
251
259
  break;
252
- case 'number':
260
+ case ConfigDefinitionType.Number:
253
261
  isValid = typeof value === 'number';
254
262
  break;
255
- case 'boolean':
263
+ case ConfigDefinitionType.Boolean:
256
264
  isValid = typeof value === 'boolean';
257
265
  break;
258
266
  }
@@ -357,13 +365,13 @@ function parseConfigValue(key, stringValue, schema) {
357
365
  if (key in schema) {
358
366
  const definition = schema[key];
359
367
  switch (definition.type) {
360
- case 'boolean':
368
+ case ConfigDefinitionType.Boolean:
361
369
  return stringValue === 'true';
362
- case 'number':
370
+ case ConfigDefinitionType.Number:
363
371
  return Number(stringValue);
364
- case 'string':
365
- case 'regexp':
366
- case 'enum':
372
+ case ConfigDefinitionType.String:
373
+ case ConfigDefinitionType.RegExp:
374
+ case ConfigDefinitionType.Enum:
367
375
  return stringValue;
368
376
  }
369
377
  }
@@ -25,8 +25,7 @@ export async function handleRefinement(selectedTasks, service, originalCommand,
25
25
  // Complete the Refinement component
26
26
  handlers.completeActive();
27
27
  // Route refined tasks to appropriate components
28
- routeTasksWithConfirm(refinedResult.tasks, refinedResult.message, service, originalCommand, handlers, false, // No DEFINE tasks in refined result
29
- undefined // No commandComponent - use normal flow
28
+ routeTasksWithConfirm(refinedResult.tasks, refinedResult.message, service, originalCommand, handlers, false // No DEFINE tasks in refined result
30
29
  );
31
30
  }
32
31
  catch (err) {
@@ -3,7 +3,7 @@ import { createAnswerDefinition, createConfigDefinitionWithKeys, createConfirmDe
3
3
  import { saveConfig, unflattenConfig } from './configuration.js';
4
4
  import { FeedbackType } from '../types/types.js';
5
5
  import { validateExecuteTasks } from './execution-validator.js';
6
- import { getMixedTaskTypesError, getUnknownRequestMessage, } from './messages.js';
6
+ import { getCancellationMessage, getMixedTaskTypesError, getUnknownRequestMessage, } from './messages.js';
7
7
  /**
8
8
  * Determine the operation name based on task types
9
9
  */
@@ -20,7 +20,7 @@ export function getOperationName(tasks) {
20
20
  * Route tasks to appropriate components with Confirm flow
21
21
  * Handles the complete flow: Plan → Confirm → Execute/Answer/Introspect
22
22
  */
23
- export function routeTasksWithConfirm(tasks, message, service, userRequest, handlers, hasDefineTask = false, commandComponent) {
23
+ export function routeTasksWithConfirm(tasks, message, service, userRequest, handlers, hasDefineTask = false) {
24
24
  if (tasks.length === 0)
25
25
  return;
26
26
  // Filter out ignore and discard tasks early
@@ -32,31 +32,32 @@ export function routeTasksWithConfirm(tasks, message, service, userRequest, hand
32
32
  return;
33
33
  }
34
34
  const operation = getOperationName(validTasks);
35
- // Create plan definition with valid tasks only
36
- const planDefinition = createPlanDefinition(message, validTasks);
37
35
  if (hasDefineTask) {
38
36
  // Has DEFINE tasks - add Plan to queue for user selection
39
37
  // Refinement flow will call this function again with refined tasks
38
+ const planDefinition = createPlanDefinition(message, validTasks);
40
39
  handlers.addToQueue(planDefinition);
41
40
  }
42
41
  else {
43
- // No DEFINE tasks - add Plan to timeline, create Confirm
44
- const confirmDefinition = createConfirmDefinition(() => {
45
- // User confirmed - route to appropriate component
46
- handlers.completeActive();
47
- executeTasksAfterConfirm(validTasks, service, userRequest, handlers);
48
- }, () => {
49
- // User cancelled
50
- handlers.onAborted(operation);
42
+ // No DEFINE tasks - Plan auto-completes and adds Confirm to queue
43
+ // When Plan activates, Command moves to timeline
44
+ // When Plan completes, it moves to pending
45
+ // When Confirm activates, Plan stays pending (visible for context)
46
+ const planDefinition = createPlanDefinition(message, validTasks, () => {
47
+ // Plan completed - add Confirm to queue
48
+ const confirmDefinition = createConfirmDefinition(() => {
49
+ // User confirmed - complete both Confirm and Plan, then route to appropriate component
50
+ handlers.completeActiveAndPending();
51
+ executeTasksAfterConfirm(validTasks, service, userRequest, handlers);
52
+ }, () => {
53
+ // User cancelled - complete both Confirm and Plan, then show cancellation
54
+ handlers.completeActiveAndPending();
55
+ const message = getCancellationMessage(operation);
56
+ handlers.addToQueue(createFeedback(FeedbackType.Aborted, message));
57
+ });
58
+ handlers.addToQueue(confirmDefinition);
51
59
  });
52
- // Use atomic update if commandComponent provided, else normal flow
53
- if (commandComponent) {
54
- handlers.completeActive(planDefinition);
55
- }
56
- else {
57
- handlers.addToTimeline(planDefinition);
58
- }
59
- handlers.addToQueue(confirmDefinition);
60
+ handlers.addToQueue(planDefinition);
60
61
  }
61
62
  }
62
63
  /**
@@ -1 +1,8 @@
1
- export {};
1
+ // Component lifecycle status
2
+ export var ComponentStatus;
3
+ (function (ComponentStatus) {
4
+ ComponentStatus["Awaiting"] = "awaiting";
5
+ ComponentStatus["Active"] = "active";
6
+ ComponentStatus["Pending"] = "pending";
7
+ ComponentStatus["Done"] = "done";
8
+ })(ComponentStatus || (ComponentStatus = {}));
package/dist/ui/Answer.js CHANGED
@@ -1,13 +1,15 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useEffect, useState } from 'react';
3
3
  import { Box, Text } from 'ink';
4
+ import { ComponentStatus } from '../types/components.js';
4
5
  import { Colors, getTextColor } from '../services/colors.js';
5
6
  import { useInput } from '../services/keyboard.js';
6
7
  import { formatErrorMessage } from '../services/messages.js';
7
8
  import { withMinimumTime } from '../services/timing.js';
8
9
  import { Spinner } from './Spinner.js';
9
10
  const MINIMUM_PROCESSING_TIME = 400;
10
- export function Answer({ question, state, isActive = true, service, handlers, }) {
11
+ export function Answer({ question, state, status, service, handlers, }) {
12
+ const isActive = status === ComponentStatus.Active;
11
13
  const [error, setError] = useState(null);
12
14
  const [answer, setAnswer] = useState(state?.answer ?? null);
13
15
  useInput((input, key) => {
@@ -1,6 +1,7 @@
1
1
  import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useEffect, useState } from 'react';
3
3
  import { Box, Text } from 'ink';
4
+ import { ComponentStatus, } from '../types/components.js';
4
5
  import { TaskType } from '../types/types.js';
5
6
  import { Colors } from '../services/colors.js';
6
7
  import { createPlanDefinition } from '../services/components.js';
@@ -12,7 +13,8 @@ import { ensureMinimumTime } from '../services/timing.js';
12
13
  import { Spinner } from './Spinner.js';
13
14
  import { UserQuery } from './UserQuery.js';
14
15
  const MIN_PROCESSING_TIME = 400; // purely for visual effect
15
- export function Command({ command, state, isActive = true, service, handlers, onAborted, }) {
16
+ export function Command({ command, state, status, service, handlers, onAborted, }) {
17
+ const isActive = status === ComponentStatus.Active;
16
18
  const [error, setError] = useState(state?.error ?? null);
17
19
  useInput((_, key) => {
18
20
  if (key.escape && isActive) {
@@ -66,9 +68,9 @@ export function Command({ command, state, isActive = true, service, handlers, on
66
68
  handlers?.addToQueue(planDefinition);
67
69
  }
68
70
  else {
69
- // No DEFINE tasks: Pass Plan to be added atomically with Command
70
- routeTasksWithConfirm(result.tasks, result.message, svc, command, handlers, false, planDefinition // Pass Plan for atomic update
71
- );
71
+ // No DEFINE tasks: Complete Command, then route to Confirm flow
72
+ handlers?.completeActive();
73
+ routeTasksWithConfirm(result.tasks, result.message, svc, command, handlers, false);
72
74
  }
73
75
  }
74
76
  }
@@ -14,36 +14,33 @@ import { Refinement } from './Refinement.js';
14
14
  import { Report } from './Report.js';
15
15
  import { Validate } from './Validate.js';
16
16
  import { Welcome } from './Welcome.js';
17
- export const Component = memo(function Component({ def, isActive, debug, }) {
18
- // For stateless components, always inactive
19
- const isStatelessComponent = !('state' in def);
20
- const componentIsActive = isStatelessComponent ? false : isActive;
17
+ export const Component = memo(function Component({ def, debug, }) {
21
18
  switch (def.name) {
22
19
  case ComponentName.Welcome:
23
- return _jsx(Welcome, { ...def.props });
20
+ return _jsx(Welcome, { ...def.props, status: def.status });
24
21
  case ComponentName.Config:
25
- return (_jsx(Config, { ...def.props, state: def.state, isActive: componentIsActive, debug: debug }));
22
+ return (_jsx(Config, { ...def.props, state: def.state, status: def.status, debug: debug }));
26
23
  case ComponentName.Command:
27
- return _jsx(Command, { ...def.props, state: def.state, isActive: isActive });
24
+ return _jsx(Command, { ...def.props, state: def.state, status: def.status });
28
25
  case ComponentName.Plan:
29
- return (_jsx(Plan, { ...def.props, state: def.state, isActive: componentIsActive, debug: debug }));
26
+ return (_jsx(Plan, { ...def.props, state: def.state, status: def.status, debug: debug }));
30
27
  case ComponentName.Feedback:
31
- return _jsx(Feedback, { ...def.props });
28
+ return _jsx(Feedback, { ...def.props, status: def.status });
32
29
  case ComponentName.Message:
33
- return _jsx(Message, { ...def.props });
30
+ return _jsx(Message, { ...def.props, status: def.status });
34
31
  case ComponentName.Refinement:
35
- return (_jsx(Refinement, { ...def.props, state: def.state, isActive: isActive }));
32
+ return (_jsx(Refinement, { ...def.props, state: def.state, status: def.status }));
36
33
  case ComponentName.Confirm:
37
- return _jsx(Confirm, { ...def.props, state: def.state, isActive: isActive });
34
+ return _jsx(Confirm, { ...def.props, state: def.state, status: def.status });
38
35
  case ComponentName.Introspect:
39
- return (_jsx(Introspect, { ...def.props, state: def.state, isActive: componentIsActive, debug: debug }));
36
+ return (_jsx(Introspect, { ...def.props, state: def.state, status: def.status, debug: debug }));
40
37
  case ComponentName.Report:
41
- return _jsx(Report, { ...def.props });
38
+ return _jsx(Report, { ...def.props, status: def.status });
42
39
  case ComponentName.Answer:
43
- return _jsx(Answer, { ...def.props, state: def.state, isActive: isActive });
40
+ return _jsx(Answer, { ...def.props, state: def.state, status: def.status });
44
41
  case ComponentName.Execute:
45
- return _jsx(Execute, { ...def.props, state: def.state, isActive: isActive });
42
+ return _jsx(Execute, { ...def.props, state: def.state, status: def.status });
46
43
  case ComponentName.Validate:
47
- return (_jsx(Validate, { ...def.props, state: def.state, isActive: isActive, debug: debug }));
44
+ return (_jsx(Validate, { ...def.props, state: def.state, status: def.status, debug: debug }));
48
45
  }
49
46
  });
package/dist/ui/Config.js CHANGED
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState } from 'react';
3
3
  import { Box, Text, useFocus } from 'ink';
4
4
  import TextInput from 'ink-text-input';
5
+ import { ComponentStatus } from '../types/components.js';
5
6
  import { Colors } from '../services/colors.js';
6
7
  import { useInput } from '../services/keyboard.js';
7
8
  export var StepType;
@@ -57,8 +58,8 @@ function SelectionStep({ options, selectedIndex, isCurrentStep, }) {
57
58
  return (_jsx(Box, { marginRight: 2, children: _jsx(Text, { dimColor: !isSelected || !isCurrentStep, bold: isSelected, children: option.label }) }, option.value));
58
59
  }) }));
59
60
  }
60
- export function Config({ steps, state, isActive = true, debug, handlers, onFinished, onAborted, }) {
61
- // isActive passed as prop
61
+ export function Config({ steps, state, status, debug, handlers, onFinished, onAborted, }) {
62
+ const isActive = status === ComponentStatus.Active;
62
63
  const [step, setStep] = useState(!isActive ? (state?.completedStep ?? steps.length) : 0);
63
64
  const [values, setValues] = useState(() => {
64
65
  // If not active and we have saved state values, use those
@@ -1,11 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState } from 'react';
3
3
  import { Box, Text } from 'ink';
4
+ import { ComponentStatus, } from '../types/components.js';
4
5
  import { Colors, Palette } from '../services/colors.js';
5
6
  import { useInput } from '../services/keyboard.js';
6
7
  import { UserQuery } from './UserQuery.js';
7
- export function Confirm({ message, state, isActive = true, handlers, onConfirmed, onCancelled, }) {
8
- // isActive passed as prop
8
+ export function Confirm({ message, state, status, handlers, onConfirmed, onCancelled, }) {
9
+ const isActive = status === ComponentStatus.Active;
9
10
  const [selectedIndex, setSelectedIndex] = useState(state?.selectedIndex ?? 0); // 0 = Yes, 1 = No
10
11
  useInput((input, key) => {
11
12
  if (!isActive)
@@ -18,11 +19,9 @@ export function Confirm({ message, state, isActive = true, handlers, onConfirmed
18
19
  }
19
20
  else if (key.tab) {
20
21
  // Toggle between Yes (0) and No (1)
21
- setSelectedIndex((prev) => {
22
- const newIndex = prev === 0 ? 1 : 0;
23
- handlers?.updateState({ selectedIndex: newIndex });
24
- return newIndex;
25
- });
22
+ const newIndex = selectedIndex === 0 ? 1 : 0;
23
+ setSelectedIndex(newIndex);
24
+ handlers?.updateState({ selectedIndex: newIndex });
26
25
  }
27
26
  else if (key.return) {
28
27
  // Confirm selection
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useState } from 'react';
3
3
  import { Box, Text } from 'ink';
4
+ import { ComponentStatus } from '../types/components.js';
4
5
  import { Colors, getTextColor, Palette } from '../services/colors.js';
5
6
  import { useInput } from '../services/keyboard.js';
6
7
  import { formatErrorMessage } from '../services/messages.js';
@@ -85,7 +86,8 @@ function CommandStatusDisplay({ item, elapsed }) {
85
86
  const elapsedTime = getElapsedTime();
86
87
  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, {})] }))] })] }));
87
88
  }
88
- export function Execute({ tasks, state, isActive = true, service, handlers, }) {
89
+ export function Execute({ tasks, state, status, service, handlers, }) {
90
+ const isActive = status === ComponentStatus.Active;
89
91
  // isActive passed as prop
90
92
  const [error, setError] = useState(state?.error ?? null);
91
93
  const [isExecuting, setIsExecuting] = useState(false);
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useState } from 'react';
3
3
  import { Box, Text } from 'ink';
4
+ import { ComponentStatus, } from '../types/components.js';
4
5
  import { Colors, getTextColor } from '../services/colors.js';
5
6
  import { createReportDefinition } from '../services/components.js';
6
7
  import { useInput } from '../services/keyboard.js';
@@ -42,7 +43,8 @@ function parseCapabilityFromTask(task) {
42
43
  isIndirect,
43
44
  };
44
45
  }
45
- export function Introspect({ tasks, state, isActive = true, service, children, debug = false, handlers, }) {
46
+ export function Introspect({ tasks, state, status, service, children, debug = false, handlers, }) {
47
+ const isActive = status === ComponentStatus.Active;
46
48
  // isActive passed as prop
47
49
  const [error, setError] = useState(null);
48
50
  useInput((input, key) => {
package/dist/ui/Plan.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useState } from 'react';
3
3
  import { Box } from 'ink';
4
+ import { ComponentStatus } from '../types/components.js';
4
5
  import { TaskType } from '../types/types.js';
5
6
  import { getTaskColors } from '../services/colors.js';
6
7
  import { useInput } from '../services/keyboard.js';
@@ -49,7 +50,8 @@ function taskToListItem(task, highlightedChildIndex = null, isDefineTaskWithoutS
49
50
  }
50
51
  return item;
51
52
  }
52
- export function Plan({ message, tasks, state, isActive = true, debug = false, handlers, onSelectionConfirmed, }) {
53
+ export function Plan({ message, tasks, state, status, debug = false, handlers, onSelectionConfirmed, }) {
54
+ const isActive = status === ComponentStatus.Active;
53
55
  // isActive passed as prop
54
56
  const [highlightedIndex, setHighlightedIndex] = useState(state?.highlightedIndex ?? null);
55
57
  const [currentDefineGroupIndex, setCurrentDefineGroupIndex] = useState(state?.currentDefineGroupIndex ?? 0);
@@ -70,9 +72,10 @@ export function Plan({ message, tasks, state, isActive = true, debug = false, ha
70
72
  if (isActive && defineTaskIndices.length === 0 && onSelectionConfirmed) {
71
73
  // No selection needed - all tasks are concrete
72
74
  const concreteTasks = tasks.filter((task) => task.type !== TaskType.Ignore && task.type !== TaskType.Discard);
73
- onSelectionConfirmed(concreteTasks);
74
- // Signal Plan completion after adding Confirm to queue
75
+ // Complete the selection phase - it goes to timeline
76
+ // Callback will create a new Plan showing refined tasks (pending) + Confirm (active)
75
77
  handlers?.completeActive();
78
+ onSelectionConfirmed(concreteTasks);
76
79
  }
77
80
  }, [
78
81
  isActive,
@@ -143,8 +146,8 @@ export function Plan({ message, tasks, state, isActive = true, debug = false, ha
143
146
  }
144
147
  });
145
148
  if (onSelectionConfirmed) {
146
- // Callback will handle the entire flow (Refinement, refined Plan, Confirm)
147
- // So we need to complete the Plan first
149
+ // Complete the selection phase - it goes to timeline
150
+ // Callback will create a new Plan showing refined tasks (pending) + Confirm (active)
148
151
  handlers?.completeActive();
149
152
  onSelectionConfirmed(refinedTasks);
150
153
  }
@@ -1,9 +1,11 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box } from 'ink';
3
+ import { ComponentStatus } from '../types/components.js';
3
4
  import { useInput } from '../services/keyboard.js';
4
5
  import { Message } from './Message.js';
5
6
  import { Spinner } from './Spinner.js';
6
- export const Refinement = ({ text, isActive = true, onAborted, }) => {
7
+ export const Refinement = ({ text, status, onAborted }) => {
8
+ const isActive = status === ComponentStatus.Active;
7
9
  useInput((_, key) => {
8
10
  if (key.escape && isActive) {
9
11
  onAborted('plan refinement');
@@ -1,6 +1,7 @@
1
1
  import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useEffect, useState } from 'react';
3
3
  import { Box, Text } from 'ink';
4
+ import { ComponentStatus } from '../types/components.js';
4
5
  import { TaskType } from '../types/types.js';
5
6
  import { Colors, getTextColor } from '../services/colors.js';
6
7
  import { useInput } from '../services/keyboard.js';
@@ -10,8 +11,8 @@ import { saveConfig, unflattenConfig } from '../services/configuration.js';
10
11
  import { Config, StepType } from './Config.js';
11
12
  import { Spinner } from './Spinner.js';
12
13
  const MIN_PROCESSING_TIME = 1000;
13
- export function Validate({ missingConfig, userRequest, state, isActive = true, service, children, debug, onError, onComplete, onAborted, handlers, }) {
14
- // isActive passed as prop
14
+ export function Validate({ missingConfig, userRequest, state, status, service, children, debug, onError, onComplete, onAborted, handlers, }) {
15
+ const isActive = status === ComponentStatus.Active;
15
16
  const [error, setError] = useState(null);
16
17
  const [completionMessage, setCompletionMessage] = useState(null);
17
18
  const [configRequirements, setConfigRequirements] = useState(null);
@@ -132,7 +133,7 @@ export function Validate({ missingConfig, userRequest, state, isActive = true, s
132
133
  const handleConfigAborted = (operation) => {
133
134
  onAborted(operation);
134
135
  };
135
- return (_jsxs(Box, { alignSelf: "flex-start", flexDirection: "column", children: [isActive && !completionMessage && !error && (_jsxs(Box, { marginLeft: 1, children: [_jsxs(Text, { color: getTextColor(isActive), children: ["Validating configuration requirements.", ' '] }), _jsx(Spinner, {})] })), completionMessage && (_jsx(Box, { marginLeft: 1, children: _jsx(Text, { color: getTextColor(isActive), children: completionMessage }) })), error && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: Colors.Status.Error, children: ["Error: ", error] }) })), configSteps && !error && (_jsx(Box, { marginTop: 1, children: _jsx(Config, { steps: configSteps, isActive: isActive, debug: debug, onFinished: handleConfigFinished, onAborted: handleConfigAborted, handlers: handlers }) })), children] }));
136
+ return (_jsxs(Box, { alignSelf: "flex-start", flexDirection: "column", children: [isActive && !completionMessage && !error && (_jsxs(Box, { marginLeft: 1, children: [_jsxs(Text, { color: getTextColor(isActive), children: ["Validating configuration requirements.", ' '] }), _jsx(Spinner, {})] })), completionMessage && (_jsx(Box, { marginLeft: 1, children: _jsx(Text, { color: getTextColor(isActive), children: completionMessage }) })), error && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: Colors.Status.Error, children: ["Error: ", error] }) })), configSteps && !error && (_jsx(Box, { marginTop: 1, children: _jsx(Config, { steps: configSteps, status: status, debug: debug, onFinished: handleConfigFinished, onAborted: handleConfigAborted, handlers: handlers }) })), children] }));
136
137
  }
137
138
  /**
138
139
  * Build prompt for VALIDATE tool
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
+ import { useCallback, useEffect, useMemo, useState } from 'react';
3
3
  import { Box, Static } from 'ink';
4
+ import { ComponentStatus, } from '../types/components.js';
4
5
  import { ComponentName, FeedbackType } from '../types/types.js';
5
6
  import { createFeedback, isStateless, markAsDone, } from '../services/components.js';
6
7
  import { exitApp } from '../services/process.js';
@@ -8,36 +9,82 @@ import { getCancellationMessage } from '../services/messages.js';
8
9
  import { Component } from './Component.js';
9
10
  export const Workflow = ({ initialQueue, debug }) => {
10
11
  const [timeline, setTimeline] = useState([]);
11
- const [active, setActive] = useState(null);
12
+ const [current, setCurrent] = useState({ active: null, pending: null });
12
13
  const [queue, setQueue] = useState(initialQueue);
13
- // Ref to track active component for synchronous access
14
- const activeRef = useRef(null);
15
- // Keep ref in sync with active state
16
- useEffect(() => {
17
- activeRef.current = active;
18
- }, [active]);
19
- // Function to move active component to timeline with optional additional items
20
- const moveActiveToTimeline = useCallback((...items) => {
21
- const curr = activeRef.current;
22
- if (!curr) {
23
- // No active component, just add items if provided
24
- if (items.length > 0) {
25
- setTimeline((prev) => [...prev, ...items]);
26
- }
27
- return;
28
- }
29
- const doneComponent = markAsDone(curr);
30
- // Atomic update: add active component and any additional items
31
- setTimeline((prev) => items.length > 0
32
- ? [...prev, doneComponent, ...items]
33
- : [...prev, doneComponent]);
34
- setActive(null);
14
+ // Function to move active to pending (component just completed)
15
+ const moveActiveToPending = useCallback(() => {
16
+ setCurrent((curr) => {
17
+ const { active } = curr;
18
+ if (!active)
19
+ return curr;
20
+ // Move active to pending without marking as done
21
+ const pendingComponent = { ...active, status: ComponentStatus.Pending };
22
+ return { active: null, pending: pendingComponent };
23
+ });
24
+ }, []);
25
+ // Function to move active directly to timeline (error/abort)
26
+ const moveActiveToTimeline = useCallback(() => {
27
+ setCurrent((curr) => {
28
+ const { active, pending } = curr;
29
+ if (!active)
30
+ return curr;
31
+ // Mark as done and add to timeline
32
+ const doneComponent = markAsDone(active);
33
+ setTimeline((prev) => [...prev, doneComponent]);
34
+ return { active: null, pending };
35
+ });
35
36
  }, []);
36
37
  // Global handlers for all stateful components
37
38
  const handlers = useMemo(() => ({
39
+ addToQueue: (...items) => {
40
+ setQueue((queue) => [...queue, ...items]);
41
+ },
42
+ updateState: (newState) => {
43
+ setCurrent((curr) => {
44
+ const { active, pending } = curr;
45
+ if (!active || !('state' in active))
46
+ return curr;
47
+ const stateful = active;
48
+ const updated = {
49
+ ...stateful,
50
+ state: {
51
+ ...stateful.state,
52
+ ...newState,
53
+ },
54
+ };
55
+ return { active: updated, pending };
56
+ });
57
+ },
58
+ completeActive: (...items) => {
59
+ moveActiveToPending();
60
+ if (items.length > 0) {
61
+ setQueue((queue) => [...items, ...queue]);
62
+ }
63
+ },
64
+ completeActiveAndPending: (...items) => {
65
+ setCurrent((curr) => {
66
+ const { active, pending } = curr;
67
+ // Move both to timeline - pending first (Plan), then active (Confirm)
68
+ if (pending) {
69
+ const donePending = markAsDone(pending);
70
+ setTimeline((prev) => [...prev, donePending]);
71
+ }
72
+ if (active) {
73
+ const doneActive = markAsDone(active);
74
+ setTimeline((prev) => [...prev, doneActive]);
75
+ }
76
+ return { active: null, pending: null };
77
+ });
78
+ if (items.length > 0) {
79
+ setQueue((queue) => [...items, ...queue]);
80
+ }
81
+ },
82
+ addToTimeline: (...items) => {
83
+ setTimeline((prev) => [...prev, ...items]);
84
+ },
38
85
  onAborted: (operation) => {
39
86
  moveActiveToTimeline();
40
- // Add feedback to queue and exit
87
+ // Add feedback to queue
41
88
  const message = getCancellationMessage(operation);
42
89
  setQueue((queue) => [
43
90
  ...queue,
@@ -46,76 +93,82 @@ export const Workflow = ({ initialQueue, debug }) => {
46
93
  },
47
94
  onError: (error) => {
48
95
  moveActiveToTimeline();
49
- // Add feedback to queue and exit with error code
96
+ // Add feedback to queue
50
97
  setQueue((queue) => [
51
98
  ...queue,
52
99
  createFeedback(FeedbackType.Failed, error),
53
100
  ]);
54
101
  },
55
- addToQueue: (...items) => {
56
- setQueue((queue) => [...queue, ...items]);
57
- },
58
- addToTimeline: (...items) => {
59
- setTimeline((prev) => [...prev, ...items]);
60
- },
61
- completeActive: (...items) => {
62
- moveActiveToTimeline(...items);
63
- },
64
- updateState: (newState) => {
65
- setActive((curr) => {
66
- if (!curr || !('state' in curr))
67
- return curr;
68
- const stateful = curr;
69
- const updated = {
70
- ...stateful,
71
- state: {
72
- ...stateful.state,
73
- ...newState,
74
- },
75
- };
76
- // Update ref synchronously so moveActiveToTimeline sees the latest state
77
- activeRef.current = updated;
78
- return updated;
79
- });
80
- },
81
- }), [moveActiveToTimeline]);
102
+ }), [moveActiveToPending, moveActiveToTimeline]);
82
103
  // Global Esc handler removed - components handle their own Esc individually
83
104
  // Move next item from queue to active
84
105
  useEffect(() => {
85
- if (queue.length > 0 && active === null) {
86
- const [first, ...rest] = queue;
106
+ const { active, pending } = current;
107
+ // Early return: not ready to activate next
108
+ if (queue.length === 0 || active !== null) {
109
+ return;
110
+ }
111
+ const [first, ...rest] = queue;
112
+ const activeComponent = { ...first, status: ComponentStatus.Active };
113
+ // Confirm - keep pending visible (Plan showing what will execute)
114
+ if (first.name === ComponentName.Confirm) {
87
115
  setQueue(rest);
88
- setActive(first);
116
+ setCurrent({ active: activeComponent, pending });
117
+ return;
89
118
  }
90
- }, [queue, active]);
119
+ // Other components - move pending to timeline first, then activate
120
+ if (pending) {
121
+ const donePending = markAsDone(pending);
122
+ setTimeline((prev) => [...prev, donePending]);
123
+ }
124
+ setQueue(rest);
125
+ setCurrent({ active: activeComponent, pending: null });
126
+ }, [queue, current]);
91
127
  // Process active component - stateless components auto-move to timeline
92
128
  useEffect(() => {
129
+ const { active, pending } = current;
93
130
  if (!active)
94
131
  return;
95
132
  if (isStateless(active)) {
133
+ // Stateless components move directly to timeline
96
134
  const doneComponent = markAsDone(active);
97
135
  setTimeline((prev) => [...prev, doneComponent]);
98
- setActive(null);
136
+ setCurrent({ active: null, pending });
99
137
  }
100
- // Stateful components stay in active until handlers move them to timeline
101
- }, [active]);
102
- // Exit when all done
138
+ // Stateful components stay in active until handlers move them to pending
139
+ }, [current]);
140
+ // Move final pending to timeline and exit when all done
103
141
  useEffect(() => {
104
- if (active === null && queue.length === 0 && timeline.length > 0) {
105
- // Check if last item in timeline is a failed feedback
106
- const lastItem = timeline[timeline.length - 1];
107
- const isFailed = lastItem.name === ComponentName.Feedback &&
108
- lastItem.props.type === FeedbackType.Failed;
109
- exitApp(isFailed ? 1 : 0);
142
+ const { active, pending } = current;
143
+ // Early return: not ready to finish
144
+ if (active !== null || queue.length > 0) {
145
+ return;
146
+ }
147
+ // Handle pending component
148
+ if (pending) {
149
+ const donePending = markAsDone(pending);
150
+ setTimeline((prev) => [...prev, donePending]);
151
+ setCurrent({ active: null, pending: null });
152
+ return;
110
153
  }
111
- }, [active, queue, timeline]);
112
- // Inject global handlers into active component
154
+ // Early return: nothing to exit with
155
+ if (timeline.length === 0) {
156
+ return;
157
+ }
158
+ // Everything is done, exit
159
+ const lastItem = timeline[timeline.length - 1];
160
+ const isFailed = lastItem.name === ComponentName.Feedback &&
161
+ lastItem.props.type === FeedbackType.Failed;
162
+ exitApp(isFailed ? 1 : 0);
163
+ }, [current, queue, timeline]);
164
+ // Render active and pending components
113
165
  const activeComponent = useMemo(() => {
166
+ const { active } = current;
114
167
  if (!active)
115
168
  return null;
116
- // For stateless components, render as-is with isActive=true
169
+ // For stateless components, render as-is
117
170
  if (isStateless(active)) {
118
- return (_jsx(Component, { def: active, isActive: true, debug: debug }, active.id));
171
+ return _jsx(Component, { def: active, debug: debug }, active.id);
119
172
  }
120
173
  // For stateful components, inject global handlers
121
174
  const statefulActive = active;
@@ -126,7 +179,14 @@ export const Workflow = ({ initialQueue, debug }) => {
126
179
  handlers,
127
180
  },
128
181
  };
129
- return (_jsx(Component, { def: wrappedDef, isActive: true, debug: debug }, active.id));
130
- }, [active, debug, handlers]);
131
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: timeline, children: (item) => (_jsx(Box, { marginTop: 1, children: _jsx(Component, { def: item, isActive: false, debug: debug }) }, item.id)) }, "timeline"), _jsx(Box, { marginTop: 1, children: activeComponent })] }));
182
+ return _jsx(Component, { def: wrappedDef, debug: debug }, active.id);
183
+ }, [current, debug, handlers]);
184
+ const pendingComponent = useMemo(() => {
185
+ const { pending } = current;
186
+ if (!pending)
187
+ return null;
188
+ // Pending components don't receive input
189
+ return _jsx(Component, { def: pending, debug: debug }, pending.id);
190
+ }, [current, debug]);
191
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: timeline, children: (item) => (_jsx(Box, { marginTop: 1, children: _jsx(Component, { def: item, debug: false }) }, item.id)) }, "timeline"), pendingComponent && _jsx(Box, { marginTop: 1, children: pendingComponent }), activeComponent && _jsx(Box, { marginTop: 1, children: activeComponent })] }));
132
192
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prompt-language-shell",
3
- "version": "0.6.2",
3
+ "version": "0.6.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",
@@ -1,52 +0,0 @@
1
- import { FeedbackType } from '../types/types.js';
2
- import { createFeedback } from './components.js';
3
- import { FeedbackMessages } from './messages.js';
4
- import { exitApp } from './process.js';
5
- /**
6
- * Higher-order function that wraps queue handler logic with common patterns:
7
- * - Check if queue is empty
8
- * - Extract first element
9
- * - Optionally check component name
10
- * - Execute callback with first element
11
- * - Return new queue state
12
- */
13
- export function withQueueHandler(componentName, callback, shouldExit = false, exitCode = 0) {
14
- return (currentQueue) => {
15
- if (currentQueue.length === 0)
16
- return currentQueue;
17
- const [first, ...rest] = currentQueue;
18
- // If componentName is specified, check if it matches
19
- if (componentName && first.name !== componentName) {
20
- if (shouldExit) {
21
- exitApp(exitCode);
22
- }
23
- return [];
24
- }
25
- // Execute callback with first and rest
26
- const result = callback(first, rest);
27
- // Exit if specified
28
- if (shouldExit) {
29
- exitApp(exitCode);
30
- }
31
- // Return result or empty queue
32
- return result || [];
33
- };
34
- }
35
- /**
36
- * Creates a generic error handler for a component
37
- */
38
- export function createErrorHandler(componentName, addToTimeline) {
39
- return (error) => withQueueHandler(componentName, (first) => {
40
- addToTimeline(first, createFeedback(FeedbackType.Failed, FeedbackMessages.UnexpectedError, error));
41
- return undefined;
42
- }, true, 1);
43
- }
44
- /**
45
- * Creates a generic completion handler for a component
46
- */
47
- export function createCompletionHandler(componentName, addToTimeline, onComplete) {
48
- return withQueueHandler(componentName, (first) => {
49
- onComplete(first);
50
- return undefined;
51
- }, true, 0);
52
- }