prompt-language-shell 0.6.0 → 0.6.2

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.
@@ -7,7 +7,8 @@ based on their query.
7
7
  ## Input
8
8
 
9
9
  You will receive:
10
- - `configStructure`: Object mapping config keys to descriptions (e.g., {"anthropic.key": "Anthropic API key"})
10
+ - `configStructure`: Object mapping config keys to descriptions (e.g., {"anthropic.key": "Anthropic API key", "settings.debug": "Debug mode (optional)"})
11
+ - `configuredKeys`: Array of keys that exist in the user's config file (e.g., ["anthropic.key", "anthropic.model", "settings.debug"])
11
12
  - `query`: User's request (e.g., "app", "mode", "anthropic", or empty)
12
13
 
13
14
  ## Task
@@ -18,7 +19,8 @@ Determine which config keys the user wants to configure and return them as tasks
18
19
 
19
20
  ### Query: "app" or empty/unclear
20
21
  - Return all **required** config keys (those needed for the app to work)
21
- - Also include any **optional** config keys that are marked as "(discovered)" (they exist in user's config file)
22
+ - Also include any keys marked as "(optional)" that appear in `configuredKeys` (optional settings that exist in user's config file)
23
+ - Also include any keys marked as "(discovered)" (they exist in user's config file but aren't in schema)
22
24
  - Required keys: `anthropic.key`, `anthropic.model`
23
25
 
24
26
  ### Query: "mode"
@@ -1,7 +1,49 @@
1
1
  import Anthropic from '@anthropic-ai/sdk';
2
- import { getAvailableConfigStructure, } from './configuration.js';
2
+ import { getAvailableConfigStructure, getConfiguredKeys, } from './configuration.js';
3
3
  import { formatSkillsForPrompt, loadSkills } from './skills.js';
4
4
  import { toolRegistry } from './tool-registry.js';
5
+ /**
6
+ * Wraps text to ensure no line exceeds the specified width.
7
+ * Breaks at word boundaries to maintain readability.
8
+ */
9
+ function wrapText(text, maxWidth) {
10
+ const words = text.split(/\s+/);
11
+ const lines = [];
12
+ let currentLine = '';
13
+ for (const word of words) {
14
+ // If adding this word would exceed max width, start a new line
15
+ if (currentLine.length > 0 &&
16
+ currentLine.length + 1 + word.length > maxWidth) {
17
+ lines.push(currentLine);
18
+ currentLine = word;
19
+ }
20
+ else {
21
+ currentLine = currentLine.length > 0 ? `${currentLine} ${word}` : word;
22
+ }
23
+ }
24
+ // Add the last line if not empty
25
+ if (currentLine.length > 0) {
26
+ lines.push(currentLine);
27
+ }
28
+ return lines.join('\n');
29
+ }
30
+ /**
31
+ * Removes citation tags and other markup from answer text.
32
+ * Web search responses may include <cite> tags that should be stripped.
33
+ * Also wraps text to ensure lines don't exceed 80 characters.
34
+ */
35
+ export function cleanAnswerText(text) {
36
+ // Remove citation tags like <cite index="1-1">content</cite>
37
+ // Replace with just the content
38
+ let cleaned = text.replace(/<cite[^>]*>(.*?)<\/cite>/g, '$1');
39
+ // Remove any other XML/HTML tags that might appear
40
+ cleaned = cleaned.replace(/<[^>]+>/g, '');
41
+ // Normalize whitespace, converting all whitespace to single spaces
42
+ cleaned = cleaned.replace(/\s+/g, ' ').trim();
43
+ // Wrap text to 80 characters per line
44
+ cleaned = wrapText(cleaned, 80);
45
+ return cleaned;
46
+ }
5
47
  export class AnthropicService {
6
48
  client;
7
49
  model;
@@ -27,9 +69,12 @@ export class AnthropicService {
27
69
  // Add config structure for config tool only
28
70
  if (toolName === 'config') {
29
71
  const configStructure = getAvailableConfigStructure();
72
+ const configuredKeys = getConfiguredKeys();
30
73
  const configSection = '\n\n## Available Configuration\n\n' +
31
74
  'Config structure (key: description):\n' +
32
- JSON.stringify(configStructure, null, 2);
75
+ JSON.stringify(configStructure, null, 2) +
76
+ '\n\nConfigured keys (keys that exist in config file):\n' +
77
+ JSON.stringify(configuredKeys, null, 2);
33
78
  systemPrompt += configSection;
34
79
  }
35
80
  // Build tools array - add web search for answer tool
@@ -67,7 +112,7 @@ export class AnthropicService {
67
112
  return {
68
113
  message: '',
69
114
  tasks: [],
70
- answer: textContent.text,
115
+ answer: cleanAnswerText(textContent.text),
71
116
  };
72
117
  }
73
118
  }
@@ -111,7 +156,7 @@ export class AnthropicService {
111
156
  return {
112
157
  message: '',
113
158
  tasks: [],
114
- answer: input.answer,
159
+ answer: cleanAnswerText(input.answer),
115
160
  };
116
161
  }
117
162
  // Handle plan and introspect tool responses
@@ -164,8 +164,8 @@ export function createConfigStepsFromSchema(keys) {
164
164
  path: key,
165
165
  type: StepType.Selection,
166
166
  options: [
167
- { label: 'Yes', value: 'true' },
168
- { label: 'No', value: 'false' },
167
+ { label: 'yes', value: 'true' },
168
+ { label: 'no', value: 'false' },
169
169
  ],
170
170
  defaultIndex: currentBool !== undefined ? (currentBool ? 0 : 1) : 0,
171
171
  validate: getValidator(definition),
@@ -263,21 +263,14 @@ export function getMissingConfigKeys() {
263
263
  return missing;
264
264
  }
265
265
  /**
266
- * Get available config structure for CONFIG tool
267
- * Returns keys with descriptions only (no values for privacy)
266
+ * Get list of configured keys from config file
267
+ * Returns array of dot-notation keys that exist in the config file
268
268
  */
269
- export function getAvailableConfigStructure() {
270
- const schema = getConfigSchema();
271
- const structure = {};
272
- // Add core schema keys with descriptions
273
- for (const [key, definition] of Object.entries(schema)) {
274
- structure[key] = definition.description;
275
- }
276
- // Add discovered keys from config file (if it exists)
269
+ export function getConfiguredKeys() {
277
270
  try {
278
271
  const configFile = getConfigFile();
279
272
  if (!existsSync(configFile)) {
280
- return structure;
273
+ return [];
281
274
  }
282
275
  const content = readFileSync(configFile, 'utf-8');
283
276
  const parsed = YAML.parse(content);
@@ -296,25 +289,103 @@ export function getAvailableConfigStructure() {
296
289
  return result;
297
290
  }
298
291
  const flatConfig = flattenConfig(parsed);
299
- // Add discovered keys that aren't in schema
300
- for (const key of Object.keys(flatConfig)) {
301
- if (!structure[key]) {
302
- structure[key] = `${key} (discovered)`;
292
+ return Object.keys(flatConfig);
293
+ }
294
+ catch {
295
+ return [];
296
+ }
297
+ }
298
+ /**
299
+ * Get available config structure for CONFIG tool
300
+ * Returns keys with descriptions only (no values for privacy)
301
+ * Marks optional keys as "(optional)"
302
+ */
303
+ export function getAvailableConfigStructure() {
304
+ const schema = getConfigSchema();
305
+ const structure = {};
306
+ // Try to load existing config to see which keys are already set
307
+ let flatConfig = {};
308
+ try {
309
+ const configFile = getConfigFile();
310
+ if (existsSync(configFile)) {
311
+ const content = readFileSync(configFile, 'utf-8');
312
+ const parsed = YAML.parse(content);
313
+ // Flatten nested config to dot notation
314
+ function flattenConfig(obj, prefix = '') {
315
+ const result = {};
316
+ for (const [key, value] of Object.entries(obj)) {
317
+ const fullKey = prefix ? `${prefix}.${key}` : key;
318
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
319
+ Object.assign(result, flattenConfig(value, fullKey));
320
+ }
321
+ else {
322
+ result[fullKey] = value;
323
+ }
324
+ }
325
+ return result;
303
326
  }
327
+ flatConfig = flattenConfig(parsed);
304
328
  }
305
329
  }
306
330
  catch {
307
- // Config file doesn't exist or can't be read, only use schema
331
+ // Config file doesn't exist or can't be read
332
+ }
333
+ // Add schema keys with descriptions
334
+ // Mark optional keys as (optional)
335
+ for (const [key, definition] of Object.entries(schema)) {
336
+ const isOptional = !definition.required;
337
+ if (isOptional) {
338
+ structure[key] = `${definition.description} (optional)`;
339
+ }
340
+ else {
341
+ structure[key] = definition.description;
342
+ }
343
+ }
344
+ // Add discovered keys that aren't in schema
345
+ for (const key of Object.keys(flatConfig)) {
346
+ if (!(key in structure)) {
347
+ structure[key] = `${key} (discovered)`;
348
+ }
308
349
  }
309
350
  return structure;
310
351
  }
352
+ /**
353
+ * Convert string value to appropriate type based on schema definition
354
+ */
355
+ function parseConfigValue(key, stringValue, schema) {
356
+ // If we have a schema definition, use its type
357
+ if (key in schema) {
358
+ const definition = schema[key];
359
+ switch (definition.type) {
360
+ case 'boolean':
361
+ return stringValue === 'true';
362
+ case 'number':
363
+ return Number(stringValue);
364
+ case 'string':
365
+ case 'regexp':
366
+ case 'enum':
367
+ return stringValue;
368
+ }
369
+ }
370
+ // No schema definition - try to infer type from string value
371
+ // This handles skill-defined configs that may not be in schema yet
372
+ if (stringValue === 'true' || stringValue === 'false') {
373
+ return stringValue === 'true';
374
+ }
375
+ if (!isNaN(Number(stringValue)) && stringValue.trim() !== '') {
376
+ return Number(stringValue);
377
+ }
378
+ return stringValue;
379
+ }
311
380
  /**
312
381
  * Unflatten dotted keys into nested structure
313
382
  * Example: { "product.alpha.path": "value" } -> { product: { alpha: { path: "value" } } }
383
+ * Converts string values to appropriate types based on config schema
314
384
  */
315
385
  export function unflattenConfig(dotted) {
316
386
  const result = {};
317
- for (const [dottedKey, value] of Object.entries(dotted)) {
387
+ const schema = getConfigSchema();
388
+ for (const [dottedKey, stringValue] of Object.entries(dotted)) {
318
389
  const parts = dottedKey.split('.');
319
390
  const section = parts[0];
320
391
  // Initialize section if needed
@@ -325,8 +396,9 @@ export function unflattenConfig(dotted) {
325
396
  current[parts[i]] = current[parts[i]] ?? {};
326
397
  current = current[parts[i]];
327
398
  }
328
- // Set final value
329
- current[parts[parts.length - 1]] = value;
399
+ // Convert string value to appropriate type and set
400
+ const typedValue = parseConfigValue(dottedKey, stringValue, schema);
401
+ current[parts[parts.length - 1]] = typedValue;
330
402
  }
331
403
  return result;
332
404
  }
@@ -25,7 +25,8 @@ 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
28
+ routeTasksWithConfirm(refinedResult.tasks, refinedResult.message, service, originalCommand, handlers, false, // No DEFINE tasks in refined result
29
+ undefined // No commandComponent - use normal flow
29
30
  );
30
31
  }
31
32
  catch (err) {
@@ -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) {
23
+ export function routeTasksWithConfirm(tasks, message, service, userRequest, handlers, hasDefineTask = false, commandComponent) {
24
24
  if (tasks.length === 0)
25
25
  return;
26
26
  // Filter out ignore and discard tasks early
@@ -49,7 +49,13 @@ export function routeTasksWithConfirm(tasks, message, service, userRequest, hand
49
49
  // User cancelled
50
50
  handlers.onAborted(operation);
51
51
  });
52
- handlers.addToTimeline(planDefinition);
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
+ }
53
59
  handlers.addToQueue(confirmDefinition);
54
60
  }
55
61
  }
package/dist/ui/Answer.js CHANGED
@@ -37,7 +37,7 @@ export function Answer({ question, state, isActive = true, service, handlers, })
37
37
  // Update component state so answer persists in timeline
38
38
  handlers?.updateState({ answer: answerText });
39
39
  // Signal completion
40
- handlers?.onComplete();
40
+ handlers?.completeActive();
41
41
  }
42
42
  }
43
43
  catch (err) {
@@ -61,16 +61,15 @@ export function Command({ command, state, isActive = true, service, handlers, on
61
61
  }
62
62
  : undefined);
63
63
  if (hasDefineTask) {
64
- // Has DEFINE tasks: Add Plan to queue for selection
65
- // The refinement callback will handle routing after user selects
64
+ // DEFINE tasks: Move Command to timeline, add Plan to queue
65
+ handlers?.completeActive();
66
66
  handlers?.addToQueue(planDefinition);
67
67
  }
68
68
  else {
69
- // No DEFINE tasks: Use routing service for Confirm flow
70
- routeTasksWithConfirm(result.tasks, result.message, svc, command, handlers, false);
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
72
  }
72
- // Move Command to timeline
73
- handlers?.onComplete();
74
73
  }
75
74
  }
76
75
  catch (err) {
package/dist/ui/Config.js CHANGED
@@ -90,8 +90,13 @@ export function Config({ steps, state, isActive = true, debug, handlers, onFinis
90
90
  });
91
91
  const [inputValue, setInputValue] = useState('');
92
92
  const [selectedIndex, setSelectedIndex] = useState(() => {
93
- const firstStep = steps[0];
94
- return firstStep?.type === StepType.Selection ? firstStep.defaultIndex : 0;
93
+ // Initialize selectedIndex based on current step's defaultIndex
94
+ if (isActive &&
95
+ step < steps.length &&
96
+ steps[step].type === StepType.Selection) {
97
+ return steps[step].defaultIndex;
98
+ }
99
+ return 0;
95
100
  });
96
101
  const normalizeValue = (value) => {
97
102
  if (value === null || value === undefined) {
@@ -99,10 +104,12 @@ export function Config({ steps, state, isActive = true, debug, handlers, onFinis
99
104
  }
100
105
  return value.replace(/\n/g, '').trim();
101
106
  };
102
- useInput((input, key) => {
103
- if (key.escape && isActive && step < steps.length) {
107
+ useInput((_, key) => {
108
+ if (!isActive || step >= steps.length)
109
+ return;
110
+ const currentStepConfig = steps[step];
111
+ if (key.escape) {
104
112
  // Save current value before aborting
105
- const currentStepConfig = steps[step];
106
113
  if (currentStepConfig) {
107
114
  const configKey = currentStepConfig.path || currentStepConfig.key;
108
115
  let currentValue = '';
@@ -111,10 +118,7 @@ export function Config({ steps, state, isActive = true, debug, handlers, onFinis
111
118
  currentValue = inputValue || values[configKey] || '';
112
119
  break;
113
120
  case StepType.Selection:
114
- currentValue =
115
- currentStepConfig.options[selectedIndex]?.value ||
116
- values[configKey] ||
117
- '';
121
+ currentValue = values[configKey] || '';
118
122
  break;
119
123
  default: {
120
124
  const exhaustiveCheck = currentStepConfig;
@@ -130,24 +134,13 @@ export function Config({ steps, state, isActive = true, debug, handlers, onFinis
130
134
  }
131
135
  return;
132
136
  }
133
- const currentStep = steps[step];
134
- if (isActive && step < steps.length && currentStep) {
135
- switch (currentStep.type) {
136
- case StepType.Selection:
137
- if (key.tab) {
138
- setSelectedIndex((prev) => (prev + 1) % currentStep.options.length);
139
- }
140
- else if (key.return) {
141
- handleSubmit(currentStep.options[selectedIndex].value);
142
- }
143
- break;
144
- case StepType.Text:
145
- // Text input handled by TextInput component
146
- break;
147
- default: {
148
- const exhaustiveCheck = currentStep;
149
- throw new Error(`Unsupported step type: ${exhaustiveCheck}`);
150
- }
137
+ // Handle selection step navigation
138
+ if (currentStepConfig.type === StepType.Selection) {
139
+ if (key.tab) {
140
+ setSelectedIndex((prev) => (prev + 1) % currentStepConfig.options.length);
141
+ }
142
+ else if (key.return) {
143
+ handleSubmit(currentStepConfig.options[selectedIndex].value);
151
144
  }
152
145
  }
153
146
  });
@@ -188,35 +181,38 @@ export function Config({ steps, state, isActive = true, debug, handlers, onFinis
188
181
  setInputValue('');
189
182
  if (step === steps.length - 1) {
190
183
  // Last step completed
184
+ // IMPORTANT: Update state BEFORE calling onFinished
185
+ // onFinished may call handlers.completeActive(), so state must be saved first
186
+ const stateUpdate = {
187
+ values: newValues,
188
+ completedStep: steps.length,
189
+ };
190
+ handlers?.updateState(stateUpdate);
191
+ // Now call onFinished - this may trigger completeActive()
191
192
  if (onFinished) {
192
193
  onFinished(newValues);
193
194
  }
194
- // Save state before completing
195
- handlers?.updateState({
196
- values: newValues,
197
- completedStep: steps.length,
198
- });
199
- // Signal Workflow that config is complete
200
- handlers?.onComplete();
201
195
  setStep(steps.length);
202
196
  }
203
197
  else {
204
198
  // Save state after each step
205
- handlers?.updateState({
199
+ const stateUpdate = {
206
200
  values: newValues,
207
201
  completedStep: step + 1,
208
- });
209
- setStep(step + 1);
210
- // Reset selection index for next step
211
- const nextStep = steps[step + 1];
212
- if (nextStep?.type === StepType.Selection) {
213
- setSelectedIndex(nextStep.defaultIndex);
202
+ };
203
+ handlers?.updateState(stateUpdate);
204
+ const nextStep = step + 1;
205
+ setStep(nextStep);
206
+ // Reset selectedIndex for next step
207
+ if (nextStep < steps.length &&
208
+ steps[nextStep].type === StepType.Selection) {
209
+ setSelectedIndex(steps[nextStep].defaultIndex);
214
210
  }
215
211
  }
216
212
  };
217
213
  const renderStepInput = (stepConfig, isCurrentStep) => {
218
214
  const configKey = stepConfig.path || stepConfig.key;
219
- // Use state values if not active (in timeline), otherwise use local values
215
+ // Use state values when inactive, local values when active
220
216
  const displayValue = !isActive && state?.values ? state.values[configKey] : values[configKey];
221
217
  switch (stepConfig.type) {
222
218
  case StepType.Text:
@@ -226,10 +222,11 @@ export function Config({ steps, state, isActive = true, debug, handlers, onFinis
226
222
  return (_jsx(Text, { dimColor: true, wrap: "truncate-end", children: displayValue || '' }));
227
223
  case StepType.Selection: {
228
224
  if (!isCurrentStep) {
229
- const selectedOption = stepConfig.options.find((opt) => opt.value === displayValue);
230
- return _jsx(Text, { dimColor: true, children: selectedOption?.label || '' });
225
+ // Find the option that matches the saved/current value
226
+ const option = stepConfig.options.find((opt) => opt.value === displayValue);
227
+ return _jsx(Text, { dimColor: true, children: option?.label || '' });
231
228
  }
232
- return (_jsx(SelectionStep, { options: stepConfig.options, selectedIndex: selectedIndex, isCurrentStep: isCurrentStep }));
229
+ return (_jsx(SelectionStep, { options: stepConfig.options, selectedIndex: selectedIndex, isCurrentStep: true }));
233
230
  }
234
231
  default: {
235
232
  const exhaustiveCheck = stepConfig;
@@ -157,7 +157,7 @@ export function Execute({ tasks, state, isActive = true, service, handlers, }) {
157
157
  commandStatuses,
158
158
  error,
159
159
  });
160
- handlers?.onComplete();
160
+ handlers?.completeActive();
161
161
  }, [isExecuting, commandStatuses, outputs, handlers, message, error]);
162
162
  useEffect(() => {
163
163
  if (!isActive) {
@@ -197,7 +197,7 @@ export function Execute({ tasks, state, isActive = true, service, handlers, }) {
197
197
  message: result.message,
198
198
  commandStatuses: [],
199
199
  });
200
- handlers?.onComplete();
200
+ handlers?.completeActive();
201
201
  return;
202
202
  }
203
203
  // Resolve placeholders in command strings before execution
@@ -86,7 +86,7 @@ export function Introspect({ tasks, state, isActive = true, service, children, d
86
86
  // Add Report component to queue
87
87
  handlers?.addToQueue(createReportDefinition(result.message, capabilities));
88
88
  // Signal completion
89
- handlers?.onComplete();
89
+ handlers?.completeActive();
90
90
  }
91
91
  }
92
92
  catch (err) {
package/dist/ui/Plan.js CHANGED
@@ -72,7 +72,7 @@ export function Plan({ message, tasks, state, isActive = true, debug = false, ha
72
72
  const concreteTasks = tasks.filter((task) => task.type !== TaskType.Ignore && task.type !== TaskType.Discard);
73
73
  onSelectionConfirmed(concreteTasks);
74
74
  // Signal Plan completion after adding Confirm to queue
75
- handlers?.onComplete();
75
+ handlers?.completeActive();
76
76
  }
77
77
  }, [
78
78
  isActive,
@@ -145,12 +145,12 @@ export function Plan({ message, tasks, state, isActive = true, debug = false, ha
145
145
  if (onSelectionConfirmed) {
146
146
  // Callback will handle the entire flow (Refinement, refined Plan, Confirm)
147
147
  // So we need to complete the Plan first
148
- handlers?.onComplete();
148
+ handlers?.completeActive();
149
149
  onSelectionConfirmed(refinedTasks);
150
150
  }
151
151
  else {
152
152
  // No selection callback, just complete normally
153
- handlers?.onComplete();
153
+ handlers?.completeActive();
154
154
  }
155
155
  }
156
156
  }
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useCallback, useEffect, useMemo, useState } from 'react';
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
3
  import { Box, Static } from 'ink';
4
4
  import { ComponentName, FeedbackType } from '../types/types.js';
5
5
  import { createFeedback, isStateless, markAsDone, } from '../services/components.js';
@@ -10,21 +10,31 @@ export const Workflow = ({ initialQueue, debug }) => {
10
10
  const [timeline, setTimeline] = useState([]);
11
11
  const [active, setActive] = useState(null);
12
12
  const [queue, setQueue] = useState(initialQueue);
13
- // Function to move active component to timeline
14
- const moveActiveToTimeline = useCallback(() => {
15
- setActive((curr) => {
16
- if (!curr)
17
- return null;
18
- const doneComponent = markAsDone(curr);
19
- setTimeline((prev) => [...prev, doneComponent]);
20
- return null;
21
- });
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);
22
35
  }, []);
23
36
  // Global handlers for all stateful components
24
37
  const handlers = useMemo(() => ({
25
- onComplete: () => {
26
- moveActiveToTimeline();
27
- },
28
38
  onAborted: (operation) => {
29
39
  moveActiveToTimeline();
30
40
  // Add feedback to queue and exit
@@ -48,21 +58,24 @@ export const Workflow = ({ initialQueue, debug }) => {
48
58
  addToTimeline: (...items) => {
49
59
  setTimeline((prev) => [...prev, ...items]);
50
60
  },
51
- completeActive: () => {
52
- moveActiveToTimeline();
61
+ completeActive: (...items) => {
62
+ moveActiveToTimeline(...items);
53
63
  },
54
64
  updateState: (newState) => {
55
65
  setActive((curr) => {
56
66
  if (!curr || !('state' in curr))
57
67
  return curr;
58
68
  const stateful = curr;
59
- return {
69
+ const updated = {
60
70
  ...stateful,
61
71
  state: {
62
72
  ...stateful.state,
63
73
  ...newState,
64
74
  },
65
75
  };
76
+ // Update ref synchronously so moveActiveToTimeline sees the latest state
77
+ activeRef.current = updated;
78
+ return updated;
66
79
  });
67
80
  },
68
81
  }), [moveActiveToTimeline]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prompt-language-shell",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
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",