prompt-language-shell 0.5.2 → 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.
Files changed (40) hide show
  1. package/dist/config/ANSWER.md +4 -0
  2. package/dist/config/CONFIG.md +4 -2
  3. package/dist/config/PLAN.md +23 -0
  4. package/dist/config/VALIDATE.md +12 -11
  5. package/dist/services/anthropic.js +49 -4
  6. package/dist/services/components.js +56 -66
  7. package/dist/services/configuration.js +169 -13
  8. package/dist/services/messages.js +22 -0
  9. package/dist/services/queue.js +2 -2
  10. package/dist/services/refinement.js +37 -0
  11. package/dist/services/task-router.js +141 -0
  12. package/dist/types/types.js +0 -1
  13. package/dist/ui/Answer.js +18 -27
  14. package/dist/ui/Command.js +44 -27
  15. package/dist/ui/Component.js +23 -50
  16. package/dist/ui/Config.js +77 -55
  17. package/dist/ui/Confirm.js +17 -11
  18. package/dist/ui/Execute.js +66 -45
  19. package/dist/ui/Feedback.js +1 -1
  20. package/dist/ui/Introspect.js +26 -23
  21. package/dist/ui/Main.js +71 -100
  22. package/dist/ui/Message.js +1 -1
  23. package/dist/ui/Plan.js +54 -32
  24. package/dist/ui/Refinement.js +6 -7
  25. package/dist/ui/Report.js +1 -1
  26. package/dist/ui/UserQuery.js +6 -0
  27. package/dist/ui/Validate.js +49 -19
  28. package/dist/ui/Welcome.js +1 -1
  29. package/dist/ui/Workflow.js +132 -0
  30. package/package.json +1 -1
  31. package/dist/handlers/answer.js +0 -21
  32. package/dist/handlers/command.js +0 -34
  33. package/dist/handlers/config.js +0 -88
  34. package/dist/handlers/execute.js +0 -46
  35. package/dist/handlers/execution.js +0 -140
  36. package/dist/handlers/introspect.js +0 -21
  37. package/dist/handlers/plan.js +0 -79
  38. package/dist/types/handlers.js +0 -1
  39. package/dist/ui/AnswerDisplay.js +0 -8
  40. package/dist/ui/Column.js +0 -7
@@ -123,6 +123,7 @@ export function saveConfig(section, config) {
123
123
  }
124
124
  export function saveAnthropicConfig(config) {
125
125
  saveConfig('anthropic', config);
126
+ return loadConfig();
126
127
  }
127
128
  export function saveDebugSetting(debug) {
128
129
  saveConfig('settings', { debug });
@@ -201,21 +202,75 @@ export function getConfigSchema() {
201
202
  };
202
203
  }
203
204
  /**
204
- * Get available config structure for CONFIG tool
205
- * Returns keys with descriptions only (no values for privacy)
205
+ * Get missing required configuration keys
206
+ * Returns array of keys that are required but not present or invalid in config
206
207
  */
207
- export function getAvailableConfigStructure() {
208
+ export function getMissingConfigKeys() {
208
209
  const schema = getConfigSchema();
209
- const structure = {};
210
- // Add core schema keys with descriptions
210
+ const missing = [];
211
+ let currentConfig = null;
212
+ try {
213
+ currentConfig = loadConfig();
214
+ }
215
+ catch {
216
+ // Config doesn't exist
217
+ }
211
218
  for (const [key, definition] of Object.entries(schema)) {
212
- structure[key] = definition.description;
219
+ if (!definition.required) {
220
+ continue;
221
+ }
222
+ // Get current value for this key
223
+ const parts = key.split('.');
224
+ let value = currentConfig;
225
+ for (const part of parts) {
226
+ if (value && typeof value === 'object' && part in value) {
227
+ value = value[part];
228
+ }
229
+ else {
230
+ value = undefined;
231
+ break;
232
+ }
233
+ }
234
+ // Check if value is missing or invalid
235
+ if (value === undefined || value === null) {
236
+ missing.push(key);
237
+ continue;
238
+ }
239
+ // Validate based on type
240
+ let isValid = false;
241
+ switch (definition.type) {
242
+ case 'regexp':
243
+ isValid = typeof value === 'string' && definition.pattern.test(value);
244
+ break;
245
+ case 'string':
246
+ isValid = typeof value === 'string';
247
+ break;
248
+ case 'enum':
249
+ isValid =
250
+ typeof value === 'string' && definition.values.includes(value);
251
+ break;
252
+ case 'number':
253
+ isValid = typeof value === 'number';
254
+ break;
255
+ case 'boolean':
256
+ isValid = typeof value === 'boolean';
257
+ break;
258
+ }
259
+ if (!isValid) {
260
+ missing.push(key);
261
+ }
213
262
  }
214
- // Add discovered keys from config file (if it exists)
263
+ return missing;
264
+ }
265
+ /**
266
+ * Get list of configured keys from config file
267
+ * Returns array of dot-notation keys that exist in the config file
268
+ */
269
+ export function getConfiguredKeys() {
215
270
  try {
216
271
  const configFile = getConfigFile();
217
272
  if (!existsSync(configFile)) {
218
- return structure;
273
+ return [];
219
274
  }
220
275
  const content = readFileSync(configFile, 'utf-8');
221
276
  const parsed = YAML.parse(content);
@@ -234,15 +289,116 @@ export function getAvailableConfigStructure() {
234
289
  return result;
235
290
  }
236
291
  const flatConfig = flattenConfig(parsed);
237
- // Add discovered keys that aren't in schema
238
- for (const key of Object.keys(flatConfig)) {
239
- if (!structure[key]) {
240
- 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;
241
326
  }
327
+ flatConfig = flattenConfig(parsed);
242
328
  }
243
329
  }
244
330
  catch {
245
- // 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
+ }
246
349
  }
247
350
  return structure;
248
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
+ }
380
+ /**
381
+ * Unflatten dotted keys into nested structure
382
+ * Example: { "product.alpha.path": "value" } -> { product: { alpha: { path: "value" } } }
383
+ * Converts string values to appropriate types based on config schema
384
+ */
385
+ export function unflattenConfig(dotted) {
386
+ const result = {};
387
+ const schema = getConfigSchema();
388
+ for (const [dottedKey, stringValue] of Object.entries(dotted)) {
389
+ const parts = dottedKey.split('.');
390
+ const section = parts[0];
391
+ // Initialize section if needed
392
+ result[section] = result[section] ?? {};
393
+ // Build nested structure for this section
394
+ let current = result[section];
395
+ for (let i = 1; i < parts.length - 1; i++) {
396
+ current[parts[i]] = current[parts[i]] ?? {};
397
+ current = current[parts[i]];
398
+ }
399
+ // Convert string value to appropriate type and set
400
+ const typedValue = parseConfigValue(dottedKey, stringValue, schema);
401
+ current[parts[parts.length - 1]] = typedValue;
402
+ }
403
+ return result;
404
+ }
@@ -43,6 +43,28 @@ export function getCancellationMessage(operation) {
43
43
  ];
44
44
  return templates[Math.floor(Math.random() * templates.length)];
45
45
  }
46
+ /**
47
+ * Returns an error message when the request cannot be understood.
48
+ * Randomly selects from variations to sound natural.
49
+ */
50
+ export function getUnknownRequestMessage() {
51
+ const messages = [
52
+ 'I do not understand the request.',
53
+ 'I cannot understand what you want me to do.',
54
+ "I'm not sure what you're asking for.",
55
+ 'I cannot determine what action to take.',
56
+ 'This request is unclear to me.',
57
+ 'I do not recognize this command.',
58
+ ];
59
+ return messages[Math.floor(Math.random() * messages.length)];
60
+ }
61
+ /**
62
+ * Returns an error message for mixed task types.
63
+ */
64
+ export function getMixedTaskTypesError(types) {
65
+ const typeList = types.join(', ');
66
+ return `Mixed task types are not supported. Found: ${typeList}. All tasks in a plan must have the same type.`;
67
+ }
46
68
  /**
47
69
  * Feedback messages for various operations
48
70
  */
@@ -1,5 +1,5 @@
1
1
  import { FeedbackType } from '../types/types.js';
2
- import { createFeedback, markAsDone } from './components.js';
2
+ import { createFeedback } from './components.js';
3
3
  import { FeedbackMessages } from './messages.js';
4
4
  import { exitApp } from './process.js';
5
5
  /**
@@ -37,7 +37,7 @@ export function withQueueHandler(componentName, callback, shouldExit = false, ex
37
37
  */
38
38
  export function createErrorHandler(componentName, addToTimeline) {
39
39
  return (error) => withQueueHandler(componentName, (first) => {
40
- addToTimeline(markAsDone(first), createFeedback(FeedbackType.Failed, FeedbackMessages.UnexpectedError, error));
40
+ addToTimeline(first, createFeedback(FeedbackType.Failed, FeedbackMessages.UnexpectedError, error));
41
41
  return undefined;
42
42
  }, true, 1);
43
43
  }
@@ -0,0 +1,37 @@
1
+ import { createRefinement } from './components.js';
2
+ import { formatErrorMessage, getRefiningMessage } from './messages.js';
3
+ import { routeTasksWithConfirm } from './task-router.js';
4
+ /**
5
+ * Handle refinement flow for DEFINE tasks
6
+ * Called when user selects options from a plan with DEFINE tasks
7
+ */
8
+ export async function handleRefinement(selectedTasks, service, originalCommand, handlers) {
9
+ // Create and add refinement component to queue
10
+ const refinementDef = createRefinement(getRefiningMessage(), (operation) => {
11
+ handlers.onAborted(operation);
12
+ });
13
+ handlers.addToQueue(refinementDef);
14
+ try {
15
+ // Build refined command from selected tasks
16
+ const refinedCommand = selectedTasks
17
+ .map((task) => {
18
+ const action = task.action.toLowerCase().replace(/,/g, ' -');
19
+ const type = task.type;
20
+ return `${action} (type: ${type})`;
21
+ })
22
+ .join(', ');
23
+ // Call LLM to refine plan with selected tasks
24
+ const refinedResult = await service.processWithTool(refinedCommand, 'plan');
25
+ // Complete the Refinement component
26
+ handlers.completeActive();
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
30
+ );
31
+ }
32
+ catch (err) {
33
+ handlers.completeActive();
34
+ const errorMessage = formatErrorMessage(err);
35
+ handlers.onError(errorMessage);
36
+ }
37
+ }
@@ -0,0 +1,141 @@
1
+ import { TaskType } from '../types/types.js';
2
+ import { createAnswerDefinition, createConfigDefinitionWithKeys, createConfirmDefinition, createExecuteDefinition, createFeedback, createIntrospectDefinition, createMessage, createPlanDefinition, createValidateDefinition, } from './components.js';
3
+ import { saveConfig, unflattenConfig } from './configuration.js';
4
+ import { FeedbackType } from '../types/types.js';
5
+ import { validateExecuteTasks } from './execution-validator.js';
6
+ import { getMixedTaskTypesError, getUnknownRequestMessage, } from './messages.js';
7
+ /**
8
+ * Determine the operation name based on task types
9
+ */
10
+ export function getOperationName(tasks) {
11
+ const allIntrospect = tasks.every((task) => task.type === TaskType.Introspect);
12
+ const allAnswer = tasks.every((task) => task.type === TaskType.Answer);
13
+ if (allIntrospect)
14
+ return 'introspection';
15
+ if (allAnswer)
16
+ return 'answer';
17
+ return 'execution';
18
+ }
19
+ /**
20
+ * Route tasks to appropriate components with Confirm flow
21
+ * Handles the complete flow: Plan → Confirm → Execute/Answer/Introspect
22
+ */
23
+ export function routeTasksWithConfirm(tasks, message, service, userRequest, handlers, hasDefineTask = false, commandComponent) {
24
+ if (tasks.length === 0)
25
+ return;
26
+ // Filter out ignore and discard tasks early
27
+ const validTasks = tasks.filter((task) => task.type !== TaskType.Ignore && task.type !== TaskType.Discard);
28
+ // Check if no valid tasks remain after filtering
29
+ if (validTasks.length === 0) {
30
+ const message = createMessage(getUnknownRequestMessage());
31
+ handlers.addToQueue(message);
32
+ return;
33
+ }
34
+ const operation = getOperationName(validTasks);
35
+ // Create plan definition with valid tasks only
36
+ const planDefinition = createPlanDefinition(message, validTasks);
37
+ if (hasDefineTask) {
38
+ // Has DEFINE tasks - add Plan to queue for user selection
39
+ // Refinement flow will call this function again with refined tasks
40
+ handlers.addToQueue(planDefinition);
41
+ }
42
+ 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);
51
+ });
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
+ }
61
+ }
62
+ /**
63
+ * Validate that all tasks have the same type
64
+ * Per FLOWS.md: "Mixed types → Error (not supported)"
65
+ */
66
+ function validateTaskTypes(tasks) {
67
+ if (tasks.length === 0)
68
+ return;
69
+ const types = new Set(tasks.map((task) => task.type));
70
+ if (types.size > 1) {
71
+ throw new Error(getMixedTaskTypesError(Array.from(types)));
72
+ }
73
+ }
74
+ /**
75
+ * Execute tasks after confirmation (internal helper)
76
+ * Validates task types after user has seen and confirmed the plan
77
+ */
78
+ function executeTasksAfterConfirm(tasks, service, userRequest, handlers) {
79
+ // Validate all tasks have the same type after user confirmation
80
+ // Per FLOWS.md: "Confirm component completes → Execution handler analyzes task types"
81
+ try {
82
+ validateTaskTypes(tasks);
83
+ }
84
+ catch (error) {
85
+ handlers.onError(error instanceof Error ? error.message : String(error));
86
+ return;
87
+ }
88
+ const allIntrospect = tasks.every((task) => task.type === TaskType.Introspect);
89
+ const allAnswer = tasks.every((task) => task.type === TaskType.Answer);
90
+ const allConfig = tasks.every((task) => task.type === TaskType.Config);
91
+ if (allAnswer) {
92
+ const question = tasks[0].action;
93
+ handlers.addToQueue(createAnswerDefinition(question, service));
94
+ }
95
+ else if (allIntrospect) {
96
+ handlers.addToQueue(createIntrospectDefinition(tasks, service));
97
+ }
98
+ else if (allConfig) {
99
+ // Route to Config flow - extract keys from task params
100
+ const configKeys = tasks
101
+ .map((task) => task.params?.key)
102
+ .filter((key) => key !== undefined);
103
+ handlers.addToQueue(createConfigDefinitionWithKeys(configKeys, (config) => {
104
+ // Save config using the same pattern as Validate component
105
+ try {
106
+ // Convert flat dotted keys to nested structure grouped by section
107
+ const configBySection = unflattenConfig(config);
108
+ // Save each section
109
+ for (const [section, sectionConfig] of Object.entries(configBySection)) {
110
+ saveConfig(section, sectionConfig);
111
+ }
112
+ handlers.completeActive();
113
+ handlers.addToQueue(createFeedback(FeedbackType.Succeeded, 'Configuration updated successfully.'));
114
+ }
115
+ catch (error) {
116
+ const errorMessage = error instanceof Error
117
+ ? error.message
118
+ : 'Failed to save configuration';
119
+ handlers.onError(errorMessage);
120
+ }
121
+ }, (operation) => {
122
+ handlers.onAborted(operation);
123
+ }));
124
+ }
125
+ else {
126
+ // Execute tasks with validation
127
+ const missingConfig = validateExecuteTasks(tasks);
128
+ if (missingConfig.length > 0) {
129
+ handlers.addToQueue(createValidateDefinition(missingConfig, userRequest, service, (error) => {
130
+ handlers.onError(error);
131
+ }, () => {
132
+ handlers.addToQueue(createExecuteDefinition(tasks, service));
133
+ }, (operation) => {
134
+ handlers.onAborted(operation);
135
+ }));
136
+ }
137
+ else {
138
+ handlers.addToQueue(createExecuteDefinition(tasks, service));
139
+ }
140
+ }
141
+ }
@@ -11,7 +11,6 @@ export var ComponentName;
11
11
  ComponentName["Introspect"] = "introspect";
12
12
  ComponentName["Report"] = "report";
13
13
  ComponentName["Answer"] = "answer";
14
- ComponentName["AnswerDisplay"] = "answerDisplay";
15
14
  ComponentName["Execute"] = "execute";
16
15
  ComponentName["Validate"] = "validate";
17
16
  })(ComponentName || (ComponentName = {}));
package/dist/ui/Answer.js CHANGED
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
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
4
  import { Colors, getTextColor } from '../services/colors.js';
@@ -7,26 +7,22 @@ import { formatErrorMessage } from '../services/messages.js';
7
7
  import { withMinimumTime } from '../services/timing.js';
8
8
  import { Spinner } from './Spinner.js';
9
9
  const MINIMUM_PROCESSING_TIME = 400;
10
- export function Answer({ question, state, service, onError, onComplete, onAborted, }) {
11
- const done = state?.done ?? false;
12
- const isCurrent = done === false;
10
+ export function Answer({ question, state, isActive = true, service, handlers, }) {
13
11
  const [error, setError] = useState(null);
14
- const [isLoading, setIsLoading] = useState(state?.isLoading ?? !done);
12
+ const [answer, setAnswer] = useState(state?.answer ?? null);
15
13
  useInput((input, key) => {
16
- if (key.escape && isLoading && !done) {
17
- setIsLoading(false);
18
- onAborted();
14
+ if (key.escape && isActive) {
15
+ handlers?.onAborted('answer');
19
16
  }
20
- }, { isActive: isLoading && !done });
17
+ }, { isActive });
21
18
  useEffect(() => {
22
19
  // Skip processing if done
23
- if (done) {
20
+ if (!isActive) {
24
21
  return;
25
22
  }
26
23
  // Skip processing if no service available
27
24
  if (!service) {
28
25
  setError('No service available');
29
- setIsLoading(false);
30
26
  return;
31
27
  }
32
28
  let mounted = true;
@@ -36,21 +32,19 @@ export function Answer({ question, state, service, onError, onComplete, onAborte
36
32
  const result = await withMinimumTime(() => svc.processWithTool(question, 'answer'), MINIMUM_PROCESSING_TIME);
37
33
  if (mounted) {
38
34
  // Extract answer from result
39
- const answer = result.answer || '';
40
- setIsLoading(false);
41
- onComplete?.(answer);
35
+ const answerText = result.answer || '';
36
+ setAnswer(answerText);
37
+ // Update component state so answer persists in timeline
38
+ handlers?.updateState({ answer: answerText });
39
+ // Signal completion
40
+ handlers?.completeActive();
42
41
  }
43
42
  }
44
43
  catch (err) {
45
44
  if (mounted) {
46
45
  const errorMessage = formatErrorMessage(err);
47
- setIsLoading(false);
48
- if (onError) {
49
- onError(errorMessage);
50
- }
51
- else {
52
- setError(errorMessage);
53
- }
46
+ setError(errorMessage);
47
+ handlers?.onError(errorMessage);
54
48
  }
55
49
  }
56
50
  }
@@ -58,10 +52,7 @@ export function Answer({ question, state, service, onError, onComplete, onAborte
58
52
  return () => {
59
53
  mounted = false;
60
54
  };
61
- }, [question, done, service, onComplete, onError]);
62
- // Return null when done (like Introspect)
63
- if (done || (!isLoading && !error)) {
64
- return null;
65
- }
66
- return (_jsxs(Box, { alignSelf: "flex-start", flexDirection: "column", children: [isLoading && (_jsxs(Box, { children: [_jsx(Text, { color: getTextColor(isCurrent), children: "Finding answer. " }), _jsx(Spinner, {})] })), error && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: Colors.Status.Error, children: ["Error: ", error] }) }))] }));
55
+ }, [question, isActive, service, handlers]);
56
+ const lines = answer ? answer.split('\n') : [];
57
+ return (_jsxs(Box, { alignSelf: "flex-start", flexDirection: "column", children: [isActive && !answer && !error && (_jsxs(Box, { marginLeft: 1, children: [_jsx(Text, { color: getTextColor(isActive), children: "Finding answer. " }), _jsx(Spinner, {})] })), answer && (_jsxs(_Fragment, { children: [_jsx(Box, { marginLeft: 1, marginBottom: 1, children: _jsx(Text, { color: getTextColor(isActive), children: question }) }), _jsx(Box, { flexDirection: "column", paddingLeft: 3, children: lines.map((line, index) => (_jsx(Text, { color: getTextColor(isActive), children: line }, index))) })] })), error && (_jsx(Box, { marginTop: 1, marginLeft: 1, children: _jsxs(Text, { color: Colors.Status.Error, children: ["Error: ", error] }) }))] }));
67
58
  }
@@ -1,32 +1,33 @@
1
- import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
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
4
  import { TaskType } from '../types/types.js';
5
5
  import { Colors } from '../services/colors.js';
6
- import { useInput } from '../services/keyboard.js';
6
+ import { createPlanDefinition } from '../services/components.js';
7
7
  import { formatErrorMessage } from '../services/messages.js';
8
+ import { useInput } from '../services/keyboard.js';
9
+ import { handleRefinement } from '../services/refinement.js';
10
+ import { routeTasksWithConfirm } from '../services/task-router.js';
8
11
  import { ensureMinimumTime } from '../services/timing.js';
9
12
  import { Spinner } from './Spinner.js';
10
- const MIN_PROCESSING_TIME = 1000; // purely for visual effect
11
- export function Command({ command, state, service, children, onError, onComplete, onAborted, }) {
12
- const done = state?.done ?? false;
13
- const [error, setError] = useState(null);
14
- const [isLoading, setIsLoading] = useState(state?.isLoading ?? !done);
15
- useInput((input, key) => {
16
- if (key.escape && isLoading && !done) {
17
- setIsLoading(false);
18
- onAborted();
13
+ import { UserQuery } from './UserQuery.js';
14
+ const MIN_PROCESSING_TIME = 400; // purely for visual effect
15
+ export function Command({ command, state, isActive = true, service, handlers, onAborted, }) {
16
+ const [error, setError] = useState(state?.error ?? null);
17
+ useInput((_, key) => {
18
+ if (key.escape && isActive) {
19
+ handlers?.onAborted('request');
20
+ onAborted?.('request');
19
21
  }
20
- }, { isActive: isLoading && !done });
22
+ }, { isActive });
21
23
  useEffect(() => {
22
- // Skip processing if done (showing historical/final state)
23
- if (done) {
24
+ // Skip processing if not active (showing historical/final state)
25
+ if (!isActive) {
24
26
  return;
25
27
  }
26
28
  // Skip processing if no service available
27
29
  if (!service) {
28
30
  setError('No service available');
29
- setIsLoading(false);
30
31
  return;
31
32
  }
32
33
  let mounted = true;
@@ -45,21 +46,38 @@ export function Command({ command, state, service, children, onError, onComplete
45
46
  }
46
47
  await ensureMinimumTime(startTime, MIN_PROCESSING_TIME);
47
48
  if (mounted) {
48
- setIsLoading(false);
49
- onComplete?.(result.message, result.tasks);
49
+ // Save result to state for timeline display
50
+ handlers?.updateState({
51
+ message: result.message,
52
+ tasks: result.tasks,
53
+ });
54
+ // Check if tasks contain DEFINE type (variant selection needed)
55
+ const hasDefineTask = result.tasks.some((task) => task.type === TaskType.Define);
56
+ // Create Plan definition
57
+ const planDefinition = createPlanDefinition(result.message, result.tasks, hasDefineTask
58
+ ? async (selectedTasks) => {
59
+ // Refinement flow for DEFINE tasks
60
+ await handleRefinement(selectedTasks, svc, command, handlers);
61
+ }
62
+ : undefined);
63
+ if (hasDefineTask) {
64
+ // DEFINE tasks: Move Command to timeline, add Plan to queue
65
+ handlers?.completeActive();
66
+ handlers?.addToQueue(planDefinition);
67
+ }
68
+ 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
+ );
72
+ }
50
73
  }
51
74
  }
52
75
  catch (err) {
53
76
  await ensureMinimumTime(startTime, MIN_PROCESSING_TIME);
54
77
  if (mounted) {
55
78
  const errorMessage = formatErrorMessage(err);
56
- setIsLoading(false);
57
- if (onError) {
58
- onError(errorMessage);
59
- }
60
- else {
61
- setError(errorMessage);
62
- }
79
+ setError(errorMessage);
80
+ handlers?.onError(errorMessage);
63
81
  }
64
82
  }
65
83
  }
@@ -67,7 +85,6 @@ export function Command({ command, state, service, children, onError, onComplete
67
85
  return () => {
68
86
  mounted = false;
69
87
  };
70
- }, [command, done, service]);
71
- const isCurrent = done === false;
72
- return (_jsxs(Box, { alignSelf: "flex-start", flexDirection: "column", children: [_jsxs(Box, { paddingX: done ? 1 : 0, marginX: done ? -1 : 0, backgroundColor: done ? Colors.Background.UserQuery : undefined, children: [_jsxs(Text, { color: isCurrent ? Colors.Text.Active : Colors.Text.UserQuery, children: ["> pls ", command] }), isLoading && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Spinner, {})] }))] }), error && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: Colors.Status.Error, children: ["Error: ", error] }) })), children] }));
88
+ }, [command, isActive, service, handlers]);
89
+ return (_jsxs(Box, { alignSelf: "flex-start", flexDirection: "column", children: [!isActive ? (_jsxs(UserQuery, { children: ["> pls ", command] })) : (_jsxs(Box, { marginLeft: 1, children: [_jsxs(Text, { color: Colors.Text.Active, children: ["> pls ", command] }), _jsx(Text, { children: " " }), _jsx(Spinner, {})] })), error && (_jsx(Box, { marginTop: 1, marginLeft: 1, children: _jsxs(Text, { color: Colors.Status.Error, children: ["Error: ", error] }) }))] }));
73
90
  }