agentk8 2.3.0 → 2.3.1

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.
@@ -9,9 +9,17 @@ import { ThinkingIndicator } from './ThinkingIndicator.js';
9
9
  import { runClaude } from '../lib/claude.js';
10
10
  import { runCouncil, checkCouncilAvailable, getAvailableModels } from '../lib/council.js';
11
11
  import { Confirmation } from './Confirmation.js';
12
+ import { QuestionWizard } from './QuestionWizard.js';
12
13
  export const App = ({ mode, version }) => {
13
14
  const { exit } = useApp();
14
- const [messages, setMessages] = useState([]);
15
+ // Initialize with welcome message as permanent first item
16
+ const [messages, setMessages] = useState([{
17
+ id: 'welcome',
18
+ role: 'system',
19
+ content: '',
20
+ timestamp: new Date(),
21
+ isWelcome: true,
22
+ }]);
15
23
  const [isProcessing, setIsProcessing] = useState(false);
16
24
  const [processingStartTime, setProcessingStartTime] = useState(null);
17
25
  const [totalTokens, setTotalTokens] = useState(0);
@@ -26,6 +34,60 @@ export const App = ({ mode, version }) => {
26
34
  const [councilAvailable, setCouncilAvailable] = useState(false);
27
35
  const [availableModels, setAvailableModels] = useState({});
28
36
  const [councilStage, setCouncilStage] = useState(null);
37
+ const [lastEscapeTime, setLastEscapeTime] = useState(0);
38
+ const [showExitHint, setShowExitHint] = useState(false);
39
+ const [questionWizardState, setQuestionWizardState] = useState(null);
40
+ // Clean orchestrator internal tags from response
41
+ const cleanOrchestratorTags = (response) => {
42
+ let cleaned = response;
43
+ // Remove <thinking>...</thinking> blocks
44
+ cleaned = cleaned.replace(/<thinking>[\s\S]*?<\/thinking>/g, '');
45
+ // Remove <task_analysis>...</task_analysis> blocks (keep content inside)
46
+ cleaned = cleaned.replace(/<\/?task_analysis>/g, '');
47
+ // Remove <response>...</response> tags (keep content inside)
48
+ cleaned = cleaned.replace(/<\/?response>/g, '');
49
+ // Clean up multiple consecutive newlines
50
+ cleaned = cleaned.replace(/\n{3,}/g, '\n\n');
51
+ // Clean up leading/trailing whitespace
52
+ cleaned = cleaned.trim();
53
+ return cleaned;
54
+ };
55
+ // Parse questions from orchestrator response
56
+ const parseQuestions = (response) => {
57
+ const questions = [];
58
+ let cleanedResponse = response;
59
+ // Match <question header="...">...</question> blocks
60
+ const questionRegex = /<question\s+header="([^"]+)">\s*([\s\S]*?)\s*<options>\s*([\s\S]*?)\s*<\/options>\s*<\/question>/g;
61
+ let match;
62
+ while ((match = questionRegex.exec(response)) !== null) {
63
+ const header = match[1];
64
+ const questionText = match[2].trim();
65
+ const optionsBlock = match[3];
66
+ // Parse options
67
+ const optionRegex = /<option(?:\s+recommended="true")?>(.*?)<\/option>/g;
68
+ const options = [];
69
+ let optionMatch;
70
+ while ((optionMatch = optionRegex.exec(optionsBlock)) !== null) {
71
+ const isRecommended = optionMatch[0].includes('recommended="true"');
72
+ options.push({
73
+ label: optionMatch[1].trim(),
74
+ recommended: isRecommended,
75
+ });
76
+ }
77
+ if (options.length > 0) {
78
+ questions.push({
79
+ header,
80
+ question: questionText,
81
+ options,
82
+ });
83
+ }
84
+ // Remove this question block from the response
85
+ cleanedResponse = cleanedResponse.replace(match[0], '').trim();
86
+ }
87
+ // Clean orchestrator internal tags from the remaining response
88
+ cleanedResponse = cleanOrchestratorTags(cleanedResponse);
89
+ return { questions, cleanedResponse };
90
+ };
29
91
  // Check council availability on mount
30
92
  useEffect(() => {
31
93
  checkCouncilAvailable().then(setCouncilAvailable);
@@ -136,18 +198,21 @@ Respond with:
136
198
  1. Task Analysis (complexity, scope)
137
199
  2. Agents Required (list which specialists are needed)
138
200
  3. Step-by-Step Plan (numbered steps)
139
- 4. Questions (if any clarification needed)
201
+ 4. Questions (if any clarification needed - use the XML question format)
140
202
 
141
203
  Format your response clearly with headers.`;
142
204
  const result = await runClaude(planPrompt, mode, autoAccept);
143
- const mentioned = detectMentionedAgents(result.response);
205
+ // Check for questions in response
206
+ const { questions, cleanedResponse } = parseQuestions(result.response);
207
+ const mentioned = detectMentionedAgents(cleanedResponse || result.response);
144
208
  setCompletedAgents(['Orchestrator', ...mentioned]);
145
209
  setActiveAgent(undefined);
210
+ // Show the cleaned response (without question XML)
146
211
  const planMessage = {
147
212
  id: (Date.now() + 1).toString(),
148
213
  role: 'agent',
149
214
  agentName: 'Orchestrator',
150
- content: result.response,
215
+ content: cleanedResponse || result.response,
151
216
  tokens: result.tokens,
152
217
  timestamp: new Date(),
153
218
  };
@@ -155,31 +220,58 @@ Format your response clearly with headers.`;
155
220
  if (result.tokens) {
156
221
  setTotalTokens(prev => prev + result.tokens.input + result.tokens.output);
157
222
  }
158
- setConfirmationState({
159
- message: 'Do you want to execute this plan?',
160
- options: [
161
- { label: 'Yes, execute plan', value: 'yes', key: 'Enter' },
162
- { label: 'No, cancel', value: 'no', key: 'Esc' },
163
- ],
164
- onSelect: (value) => {
165
- setConfirmationState(null);
166
- if (value === 'yes') {
167
- executeTask(input);
168
- }
169
- else {
223
+ // Stop processing BEFORE showing wizard/confirmation to prevent overlap
224
+ setIsProcessing(false);
225
+ setProcessingStartTime(null);
226
+ // If questions were found, show the wizard
227
+ if (questions.length > 0) {
228
+ setQuestionWizardState({
229
+ questions,
230
+ originalInput: input,
231
+ onComplete: async (answers) => {
232
+ setQuestionWizardState(null);
233
+ // Format answers as follow-up message
234
+ const answerText = answers.map(a => `- ${a.header}: ${a.answer}`).join('\n');
235
+ const followUp = `My answers:\n${answerText}\n\nPlease proceed with these choices.`;
236
+ // Add user message with answers
237
+ const userAnswer = {
238
+ id: Date.now().toString(),
239
+ role: 'user',
240
+ content: followUp,
241
+ timestamp: new Date(),
242
+ };
243
+ setMessages(prev => [...prev, userAnswer]);
244
+ // Continue with the task including answers
245
+ await executeTask(`${input}\n\n${followUp}`);
246
+ },
247
+ });
248
+ }
249
+ else {
250
+ // No questions, show plan approval
251
+ setConfirmationState({
252
+ message: 'Do you want to execute this plan?',
253
+ options: [
254
+ { label: 'Yes, execute plan', value: 'yes', key: 'Enter' },
255
+ { label: 'No, cancel', value: 'no', key: 'Esc' },
256
+ ],
257
+ onSelect: (value) => {
258
+ setConfirmationState(null);
259
+ if (value === 'yes') {
260
+ executeTask(input);
261
+ }
262
+ else {
263
+ addSystemMessage('Plan cancelled.');
264
+ }
265
+ },
266
+ onCancel: () => {
267
+ setConfirmationState(null);
170
268
  addSystemMessage('Plan cancelled.');
171
- }
172
- },
173
- onCancel: () => {
174
- setConfirmationState(null);
175
- addSystemMessage('Plan cancelled.');
176
- },
177
- });
269
+ },
270
+ });
271
+ }
178
272
  }
179
273
  catch (err) {
180
274
  setError(err instanceof Error ? err.message : 'Unknown error');
181
- }
182
- finally {
183
275
  setIsProcessing(false);
184
276
  setProcessingStartTime(null);
185
277
  }
@@ -193,14 +285,16 @@ Format your response clearly with headers.`;
193
285
  setError(null);
194
286
  try {
195
287
  const result = await runClaude(input, mode, autoAccept);
196
- const mentioned = detectMentionedAgents(result.response);
288
+ // Check for questions in response
289
+ const { questions, cleanedResponse } = parseQuestions(result.response);
290
+ const mentioned = detectMentionedAgents(cleanedResponse || result.response);
197
291
  setCompletedAgents(['Orchestrator', ...mentioned]);
198
292
  setActiveAgent(undefined);
199
293
  const agentMessage = {
200
294
  id: (Date.now() + 1).toString(),
201
295
  role: 'agent',
202
296
  agentName: 'Orchestrator',
203
- content: result.response,
297
+ content: cleanedResponse || result.response,
204
298
  tokens: result.tokens,
205
299
  timestamp: new Date(),
206
300
  };
@@ -208,11 +302,35 @@ Format your response clearly with headers.`;
208
302
  if (result.tokens) {
209
303
  setTotalTokens(prev => prev + result.tokens.input + result.tokens.output);
210
304
  }
305
+ // Stop processing BEFORE showing wizard to prevent overlap
306
+ setIsProcessing(false);
307
+ setProcessingStartTime(null);
308
+ // If questions were found, show the wizard
309
+ if (questions.length > 0) {
310
+ setQuestionWizardState({
311
+ questions,
312
+ originalInput: input,
313
+ onComplete: async (answers) => {
314
+ setQuestionWizardState(null);
315
+ // Format answers as follow-up message
316
+ const answerText = answers.map(a => `- ${a.header}: ${a.answer}`).join('\n');
317
+ const followUp = `My answers:\n${answerText}\n\nPlease proceed with these choices.`;
318
+ // Add user message with answers
319
+ const userAnswer = {
320
+ id: Date.now().toString(),
321
+ role: 'user',
322
+ content: followUp,
323
+ timestamp: new Date(),
324
+ };
325
+ setMessages(prev => [...prev, userAnswer]);
326
+ // Continue with the task including answers
327
+ await executeTask(`${input}\n\n${followUp}`);
328
+ },
329
+ });
330
+ }
211
331
  }
212
332
  catch (err) {
213
333
  setError(err instanceof Error ? err.message : 'Unknown error');
214
- }
215
- finally {
216
334
  setIsProcessing(false);
217
335
  setProcessingStartTime(null);
218
336
  }
@@ -237,7 +355,14 @@ Format your response clearly with headers.`;
237
355
  exit();
238
356
  break;
239
357
  case 'clear':
240
- setMessages([]);
358
+ // Keep welcome message, clear everything else
359
+ setMessages([{
360
+ id: 'welcome',
361
+ role: 'system',
362
+ content: '',
363
+ timestamp: new Date(),
364
+ isWelcome: true,
365
+ }]);
241
366
  setActiveAgent(undefined);
242
367
  setCompletedAgents([]);
243
368
  break;
@@ -339,7 +464,7 @@ Keyboard shortcuts:
339
464
  ↑/↓ - Browse command history
340
465
  Tab - Autocomplete commands
341
466
  Shift+Tab - Toggle auto-accept edits
342
- Ctrl+C - Exit
467
+ Esc Esc - Exit
343
468
  Ctrl+U - Clear input line`,
344
469
  timestamp: new Date(),
345
470
  };
@@ -369,8 +494,21 @@ Ctrl+U - Clear input line`,
369
494
  };
370
495
  // Handle keyboard shortcuts
371
496
  useInput((input, key) => {
372
- if (key.ctrl && input === 'c') {
373
- exit();
497
+ // Double-escape to exit (like Claude Code)
498
+ if (key.escape) {
499
+ const now = Date.now();
500
+ if (now - lastEscapeTime < 500) {
501
+ // Double escape - exit
502
+ exit();
503
+ }
504
+ else {
505
+ // First escape - show hint and record time
506
+ setLastEscapeTime(now);
507
+ if (!questionWizardState && !confirmationState) {
508
+ setShowExitHint(true);
509
+ setTimeout(() => setShowExitHint(false), 1500);
510
+ }
511
+ }
374
512
  }
375
513
  // Shift+Tab to toggle auto-accept
376
514
  if (key.shift && key.tab) {
@@ -397,16 +535,15 @@ Ctrl+U - Clear input line`,
397
535
  }
398
536
  }
399
537
  });
400
- // Prepare items for Static (include welcome box as first item)
401
- const staticItems = messages.length === 0
402
- ? [{ id: 'welcome', isWelcome: true, role: 'system', content: '', timestamp: new Date() }]
403
- : messages;
404
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: staticItems, children: (item) => {
538
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: messages, children: (item) => {
405
539
  if ('isWelcome' in item && item.isWelcome) {
406
540
  return _jsx(WelcomeBox, { version: version, mode: mode }, "welcome");
407
541
  }
408
542
  return (_jsx(ChatMessage, { role: item.role, agentName: item.agentName, content: item.content, tokens: item.tokens }, item.id));
409
- } }), isProcessing && processingStartTime && (_jsx(ThinkingIndicator, { startTime: processingStartTime })), error && (_jsx(Box, { marginY: 1, marginLeft: 1, children: _jsxs(Text, { color: "#e53e3e", children: ["\u2717 Error: ", error] }) })), confirmationState ? (_jsx(Confirmation, { message: confirmationState.message, options: confirmationState.options, onSelect: confirmationState.onSelect, onCancel: confirmationState.onCancel })) : (_jsx(Input, { onSubmit: handleSubmit, disabled: isProcessing, placeholder: 'Try "build a password validator"' })), _jsx(StatusBar, { mode: mode, executionMode: executionMode, tokens: totalTokens, startTime: startTime, isProcessing: isProcessing, activeAgent: activeAgent, completedAgents: completedAgents, autoAccept: autoAccept, councilMode: councilMode, councilStage: councilStage, availableModels: availableModels })] }));
543
+ } }), isProcessing && processingStartTime && (_jsx(ThinkingIndicator, { startTime: processingStartTime })), error && (_jsx(Box, { marginY: 1, marginLeft: 1, children: _jsxs(Text, { color: "#e53e3e", children: ["\u2717 Error: ", error] }) })), questionWizardState ? (_jsx(QuestionWizard, { questions: questionWizardState.questions, onComplete: questionWizardState.onComplete, onCancel: () => {
544
+ setQuestionWizardState(null);
545
+ addSystemMessage('Questions cancelled.');
546
+ } }, `wizard-${questionWizardState.questions.map(q => q.header).join('-')}`)) : confirmationState ? (_jsx(Confirmation, { message: confirmationState.message, options: confirmationState.options, onSelect: confirmationState.onSelect, onCancel: confirmationState.onCancel })) : (_jsx(Input, { onSubmit: handleSubmit, disabled: isProcessing, placeholder: 'Try "build a password validator"' })), _jsx(StatusBar, { mode: mode, executionMode: executionMode, tokens: totalTokens, startTime: startTime, isProcessing: isProcessing, activeAgent: activeAgent, completedAgents: completedAgents, autoAccept: autoAccept, councilMode: councilMode, councilStage: councilStage, availableModels: availableModels, showExitHint: showExitHint })] }));
410
547
  };
411
548
  function formatElapsed(start) {
412
549
  const secs = Math.floor((Date.now() - start.getTime()) / 1000);
@@ -10,21 +10,113 @@ const theme = {
10
10
  user: '#9f7aea', // Purple for user
11
11
  agent: '#4fd1c5', // Teal for agent
12
12
  system: '#f6e05e', // Yellow for system
13
+ code: '#f6ad55', // Orange for inline code
14
+ header: '#63b3ed', // Blue for headers
13
15
  };
16
+ // Parse inline markdown and return React elements
17
+ function parseInlineMarkdown(text, defaultColor) {
18
+ const elements = [];
19
+ let remaining = text;
20
+ let key = 0;
21
+ while (remaining.length > 0) {
22
+ // Check for bold **text**
23
+ const boldMatch = remaining.match(/^\*\*(.+?)\*\*/);
24
+ if (boldMatch) {
25
+ elements.push(_jsx(Text, { color: defaultColor, bold: true, children: boldMatch[1] }, key++));
26
+ remaining = remaining.slice(boldMatch[0].length);
27
+ continue;
28
+ }
29
+ // Check for inline code `code`
30
+ const codeMatch = remaining.match(/^`([^`]+)`/);
31
+ if (codeMatch) {
32
+ elements.push(_jsx(Text, { color: theme.code, children: codeMatch[1] }, key++));
33
+ remaining = remaining.slice(codeMatch[0].length);
34
+ continue;
35
+ }
36
+ // Check for italic *text* or _text_
37
+ const italicMatch = remaining.match(/^(\*|_)([^*_]+)\1/);
38
+ if (italicMatch) {
39
+ elements.push(_jsx(Text, { color: defaultColor, italic: true, children: italicMatch[2] }, key++));
40
+ remaining = remaining.slice(italicMatch[0].length);
41
+ continue;
42
+ }
43
+ // Find next special character
44
+ const nextSpecial = remaining.search(/[\*`_]/);
45
+ if (nextSpecial === -1) {
46
+ // No more special chars, add rest as plain text
47
+ elements.push(_jsx(Text, { color: defaultColor, children: remaining }, key++));
48
+ break;
49
+ }
50
+ else if (nextSpecial === 0) {
51
+ // Special char at start but didn't match pattern - treat as literal
52
+ elements.push(_jsx(Text, { color: defaultColor, children: remaining[0] }, key++));
53
+ remaining = remaining.slice(1);
54
+ }
55
+ else {
56
+ // Add text before special char
57
+ elements.push(_jsx(Text, { color: defaultColor, children: remaining.slice(0, nextSpecial) }, key++));
58
+ remaining = remaining.slice(nextSpecial);
59
+ }
60
+ }
61
+ return elements;
62
+ }
63
+ // Render a single line with markdown formatting
64
+ function renderLine(line, index, defaultColor) {
65
+ // Header detection (## Header)
66
+ const headerMatch = line.match(/^(#{1,3})\s+(.+)$/);
67
+ if (headerMatch) {
68
+ const level = headerMatch[1].length;
69
+ const content = headerMatch[2];
70
+ return (_jsx(Box, { children: _jsx(Text, { color: theme.header, bold: true, children: content }) }, index));
71
+ }
72
+ // List item detection (- item or * item)
73
+ const listMatch = line.match(/^(\s*)([-*])\s+(.+)$/);
74
+ if (listMatch) {
75
+ const indent = listMatch[1];
76
+ const content = listMatch[3];
77
+ return (_jsxs(Box, { children: [_jsxs(Text, { color: defaultColor, children: [indent, "\u2022 "] }), parseInlineMarkdown(content, defaultColor)] }, index));
78
+ }
79
+ // Numbered list detection (1. item)
80
+ const numberedMatch = line.match(/^(\s*)(\d+)\.\s+(.+)$/);
81
+ if (numberedMatch) {
82
+ const indent = numberedMatch[1];
83
+ const num = numberedMatch[2];
84
+ const content = numberedMatch[3];
85
+ return (_jsxs(Box, { children: [_jsxs(Text, { color: defaultColor, children: [indent, num, ". "] }), parseInlineMarkdown(content, defaultColor)] }, index));
86
+ }
87
+ // Table row detection (| col | col |)
88
+ if (line.includes('|') && (line.trim().startsWith('|') || line.includes(' | '))) {
89
+ // Keep table formatting as-is but with subtle styling
90
+ const isSeparator = line.match(/^[\s|:-]+$/);
91
+ if (isSeparator) {
92
+ return (_jsx(Box, { children: _jsx(Text, { color: theme.dim, children: line }) }, index));
93
+ }
94
+ return (_jsx(Box, { children: _jsx(Text, { color: defaultColor, children: line }) }, index));
95
+ }
96
+ // Regular line with inline markdown
97
+ return (_jsx(Box, { children: parseInlineMarkdown(line, defaultColor) }, index));
98
+ }
14
99
  export const ChatMessage = ({ role, agentName = 'Agent', content, tokens, }) => {
15
100
  const isUser = role === 'user';
16
101
  const isSystem = role === 'system';
17
102
  const symbolColor = isUser ? theme.user : isSystem ? theme.system : theme.agent;
18
103
  const title = isUser ? 'You' : agentName;
104
+ const textColor = theme.text;
19
105
  const termWidth = process.stdout.columns || 80;
20
106
  const contentWidth = termWidth - 6;
107
+ // Split content into lines and wrap long lines
21
108
  const lines = wrapText(content, contentWidth);
22
- return (_jsxs(Box, { flexDirection: "column", marginY: 1, marginLeft: 1, children: [_jsxs(Box, { children: [_jsx(Text, { color: symbolColor, children: '◆ ' }), _jsx(Text, { color: symbolColor, bold: true, children: title })] }), _jsx(Box, { flexDirection: "column", marginLeft: 2, children: lines.map((line, i) => (_jsx(Text, { color: theme.text, children: line }, i))) }), tokens && tokens.input + tokens.output > 0 && (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { color: theme.dim, children: ["\u2192 ", tokens.input + tokens.output, " tokens"] }) }))] }));
109
+ return (_jsxs(Box, { flexDirection: "column", marginY: 1, marginLeft: 1, children: [_jsxs(Box, { children: [_jsx(Text, { color: symbolColor, children: '◆ ' }), _jsx(Text, { color: symbolColor, bold: true, children: title })] }), _jsx(Box, { flexDirection: "column", marginLeft: 2, children: lines.map((line, i) => renderLine(line, i, textColor)) }), tokens && tokens.input + tokens.output > 0 && (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { color: theme.dim, children: ["\u2192 ", tokens.input + tokens.output, " tokens"] }) }))] }));
23
110
  };
24
111
  function wrapText(text, width) {
25
112
  const lines = [];
26
113
  const paragraphs = text.split('\n');
27
114
  for (const para of paragraphs) {
115
+ // Don't wrap table lines or lines with special formatting
116
+ if (para.includes('|') || para.match(/^#{1,3}\s/) || para.match(/^[\s]*[-*]\s/) || para.match(/^[\s]*\d+\.\s/)) {
117
+ lines.push(para);
118
+ continue;
119
+ }
28
120
  if (para.length <= width) {
29
121
  lines.push(para);
30
122
  }
@@ -0,0 +1,21 @@
1
+ import React from 'react';
2
+ export interface QuestionOption {
3
+ label: string;
4
+ recommended?: boolean;
5
+ }
6
+ export interface Question {
7
+ header: string;
8
+ question: string;
9
+ options: QuestionOption[];
10
+ answer?: string;
11
+ }
12
+ export interface QuestionWizardProps {
13
+ questions: Question[];
14
+ onComplete: (answers: {
15
+ header: string;
16
+ answer: string;
17
+ }[]) => void;
18
+ onCancel: () => void;
19
+ }
20
+ export declare const QuestionWizard: React.NamedExoticComponent<QuestionWizardProps>;
21
+ export default QuestionWizard;
@@ -0,0 +1,152 @@
1
+ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import React, { useState, useCallback, memo } from 'react';
3
+ import { Box, Text, useInput } from 'ink';
4
+ const theme = {
5
+ border: '#2d3748',
6
+ accent: '#4fd1c5',
7
+ highlight: '#81e6d9',
8
+ text: '#e2e8f0',
9
+ dim: '#4a5568',
10
+ selected: '#4fd1c5',
11
+ unselected: '#718096',
12
+ answered: '#48bb78',
13
+ header: '#63b3ed',
14
+ no: '#e53e3e',
15
+ };
16
+ const QuestionWizardInner = ({ questions: initialQuestions, onComplete, onCancel, }) => {
17
+ // Single state object for atomic updates
18
+ const [state, setState] = useState({
19
+ questions: initialQuestions,
20
+ currentIndex: 0,
21
+ selectedOption: 0,
22
+ customInputMode: false,
23
+ customInputValue: '',
24
+ cursorPosition: 0,
25
+ });
26
+ const { questions, currentIndex, selectedOption, customInputMode, customInputValue, cursorPosition } = state;
27
+ const currentQuestion = questions[currentIndex];
28
+ const allOptions = [...currentQuestion.options, { label: 'No', recommended: false }];
29
+ const isOnNoOption = selectedOption === allOptions.length - 1;
30
+ const termWidth = process.stdout.columns || 80;
31
+ // Update state atomically
32
+ const updateState = useCallback((updates) => {
33
+ setState(prev => ({ ...prev, ...updates }));
34
+ }, []);
35
+ // Set answer and optionally advance
36
+ const selectOption = useCallback((answer, shouldAdvance) => {
37
+ setState(prev => {
38
+ const updatedQuestions = [...prev.questions];
39
+ updatedQuestions[prev.currentIndex] = {
40
+ ...updatedQuestions[prev.currentIndex],
41
+ answer
42
+ };
43
+ // Check if this is the last question and all are answered
44
+ const isLast = prev.currentIndex === prev.questions.length - 1;
45
+ const allAnswered = updatedQuestions.every(q => q.answer !== undefined);
46
+ if (isLast && allAnswered) {
47
+ // Will complete after state update
48
+ setTimeout(() => {
49
+ onComplete(updatedQuestions.map(q => ({
50
+ header: q.header,
51
+ answer: q.answer || '',
52
+ })));
53
+ }, 0);
54
+ return prev; // Keep current state, will unmount soon
55
+ }
56
+ return {
57
+ ...prev,
58
+ questions: updatedQuestions,
59
+ currentIndex: shouldAdvance && !isLast ? prev.currentIndex + 1 : prev.currentIndex,
60
+ selectedOption: shouldAdvance && !isLast ? 0 : prev.selectedOption,
61
+ customInputMode: false,
62
+ customInputValue: '',
63
+ cursorPosition: 0,
64
+ };
65
+ });
66
+ }, [onComplete]);
67
+ useInput((input, key) => {
68
+ if (customInputMode) {
69
+ if (key.escape) {
70
+ updateState({ customInputMode: false, customInputValue: '', cursorPosition: 0 });
71
+ }
72
+ else if (key.return && customInputValue.trim()) {
73
+ selectOption(customInputValue.trim(), true);
74
+ }
75
+ else if (key.backspace || key.delete) {
76
+ if (cursorPosition > 0) {
77
+ updateState({
78
+ customInputValue: customInputValue.slice(0, cursorPosition - 1) + customInputValue.slice(cursorPosition),
79
+ cursorPosition: cursorPosition - 1,
80
+ });
81
+ }
82
+ }
83
+ else if (key.leftArrow) {
84
+ updateState({ cursorPosition: Math.max(0, cursorPosition - 1) });
85
+ }
86
+ else if (key.rightArrow) {
87
+ updateState({ cursorPosition: Math.min(customInputValue.length, cursorPosition + 1) });
88
+ }
89
+ else if (!key.ctrl && !key.meta && input) {
90
+ updateState({
91
+ customInputValue: customInputValue.slice(0, cursorPosition) + input + customInputValue.slice(cursorPosition),
92
+ cursorPosition: cursorPosition + input.length,
93
+ });
94
+ }
95
+ return;
96
+ }
97
+ // Selection mode
98
+ if (key.upArrow) {
99
+ updateState({ selectedOption: selectedOption > 0 ? selectedOption - 1 : allOptions.length - 1 });
100
+ }
101
+ else if (key.downArrow) {
102
+ updateState({ selectedOption: selectedOption < allOptions.length - 1 ? selectedOption + 1 : 0 });
103
+ }
104
+ else if (key.leftArrow && currentIndex > 0) {
105
+ updateState({ currentIndex: currentIndex - 1, selectedOption: 0 });
106
+ }
107
+ else if (key.rightArrow && currentIndex < questions.length - 1 && currentQuestion.answer) {
108
+ updateState({ currentIndex: currentIndex + 1, selectedOption: 0 });
109
+ }
110
+ else if (key.tab) {
111
+ // Tab always opens custom input mode
112
+ updateState({ customInputMode: true, customInputValue: '', cursorPosition: 0 });
113
+ }
114
+ else if (key.return && isOnNoOption) {
115
+ // Enter on "No" also opens custom input
116
+ updateState({ customInputMode: true, customInputValue: '', cursorPosition: 0 });
117
+ }
118
+ else if (key.return && !isOnNoOption) {
119
+ selectOption(allOptions[selectedOption].label, true);
120
+ }
121
+ else if (key.escape) {
122
+ onCancel();
123
+ }
124
+ else {
125
+ const num = parseInt(input, 10);
126
+ if (!isNaN(num) && num > 0 && num <= allOptions.length) {
127
+ updateState({ selectedOption: num - 1 });
128
+ }
129
+ }
130
+ });
131
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.border, paddingX: 2, paddingY: 1, children: [_jsx(Box, { children: questions.map((q, i) => {
132
+ const isCurrent = i === currentIndex;
133
+ const isAnswered = q.answer !== undefined;
134
+ const color = isCurrent ? theme.accent : isAnswered ? theme.answered : theme.unselected;
135
+ return (_jsxs(React.Fragment, { children: [_jsxs(Text, { color: color, bold: isCurrent, children: ["[", isAnswered ? `${q.header} ✓` : q.header, "]"] }), i < questions.length - 1 && _jsx(Text, { color: theme.dim, children: " " })] }, i));
136
+ }) }), _jsx(Box, { marginY: 1, children: questions.map((q, i) => {
137
+ const isAnswered = q.answer !== undefined;
138
+ const isCurrent = i === currentIndex;
139
+ return (_jsxs(React.Fragment, { children: [_jsx(Text, { color: isAnswered ? theme.answered : isCurrent ? theme.accent : theme.dim, children: isAnswered ? '✓' : isCurrent ? '●' : '○' }), i < questions.length - 1 && _jsx(Text, { color: theme.dim, children: "\u2500\u2500\u2500\u2500" })] }, i));
140
+ }) }), _jsxs(Box, { children: [_jsx(Text, { color: theme.accent, children: "\u25C6 " }), _jsx(Text, { color: theme.header, bold: true, children: currentQuestion.header })] }), _jsx(Box, { marginLeft: 2, marginBottom: 1, children: _jsx(Text, { color: theme.text, children: currentQuestion.question }) }), _jsx(Box, { flexDirection: "column", marginLeft: 2, children: allOptions.map((opt, idx) => {
141
+ const isSelected = idx === selectedOption;
142
+ const isNo = idx === allOptions.length - 1;
143
+ if (isNo) {
144
+ // "No" option with inline custom input
145
+ return (_jsxs(Box, { children: [_jsxs(Text, { color: isSelected ? (customInputMode ? theme.selected : theme.no) : theme.unselected, children: [isSelected ? '❯' : ' ', " ", idx + 1, ". No"] }), customInputMode ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.dim, children: ", " }), _jsx(Text, { color: theme.text, children: customInputValue.slice(0, cursorPosition) }), _jsx(Text, { inverse: true, children: customInputValue[cursorPosition] || ' ' }), _jsx(Text, { color: theme.text, children: customInputValue.slice(cursorPosition + 1) })] })) : (_jsx(Text, { color: theme.dim, children: " (Tab to type custom)" }))] }, idx));
146
+ }
147
+ return (_jsxs(Text, { color: isSelected ? theme.selected : theme.unselected, children: [isSelected ? '❯' : ' ', " ", idx + 1, ". ", opt.recommended ? `${opt.label} (Recommended)` : opt.label] }, idx));
148
+ }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.dim, children: customInputMode ? 'Type your answer · Enter submit · Esc cancel' : '↑/↓ select · Enter confirm · Tab custom · ←/→ questions' }) })] }));
149
+ };
150
+ // Memoize to prevent unnecessary re-renders
151
+ export const QuestionWizard = memo(QuestionWizardInner);
152
+ export default QuestionWizard;
@@ -13,6 +13,7 @@ interface StatusBarProps {
13
13
  councilMode?: CouncilMode;
14
14
  councilStage?: string | null;
15
15
  availableModels?: Record<string, boolean>;
16
+ showExitHint?: boolean;
16
17
  }
17
18
  export declare const StatusBar: React.FC<StatusBarProps>;
18
19
  export default StatusBar;
@@ -29,7 +29,7 @@ const modelIcons = {
29
29
  gemini: 'M',
30
30
  claude: 'C',
31
31
  };
32
- export const StatusBar = ({ mode, executionMode, tokens, startTime, isProcessing = false, activeAgent, completedAgents = [], autoAccept = false, councilMode = 'off', councilStage = null, availableModels = {}, }) => {
32
+ export const StatusBar = ({ mode, executionMode, tokens, startTime, isProcessing = false, activeAgent, completedAgents = [], autoAccept = false, councilMode = 'off', councilStage = null, availableModels = {}, showExitHint = false, }) => {
33
33
  const [elapsed, setElapsed] = useState('');
34
34
  const [spinnerFrame, setSpinnerFrame] = useState(0);
35
35
  const [pulseFrame, setPulseFrame] = useState(0);
@@ -106,6 +106,6 @@ export const StatusBar = ({ mode, executionMode, tokens, startTime, isProcessing
106
106
  const leftBracket = isActive ? pulseBrackets[pulseFrame] : '[';
107
107
  const rightBracket = isActive ? pulseBrackets[(pulseFrame + 3) % pulseBrackets.length] === '<' ? '>' : pulseBrackets[(pulseFrame + 3) % pulseBrackets.length] === '{' ? '}' : pulseBrackets[(pulseFrame + 3) % pulseBrackets.length] === '(' ? ')' : ']' : ']';
108
108
  return (_jsxs(React.Fragment, { children: [_jsx(Text, { color: getAgentColor(agent), children: leftBracket }), _jsx(Text, { color: getAgentColor(agent), children: agentIcons[agent] }), _jsx(Text, { color: getAgentColor(agent), children: rightBracket }), i < modeAgents.length - 1 && _jsx(Text, { color: theme.dim, children: " " })] }, agent));
109
- })), _jsx(Text, { color: theme.border, children: " \u2502 " }), _jsx(Text, { color: theme.dim, children: "? help" }), councilMode !== 'off' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.border, children: " \u2502 " }), _jsx(Text, { color: theme.highlight, children: councilMode === 'council' ? 'COUNCIL' : 'SOLO' })] })), autoAccept && (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.border, children: " \u2502 " }), _jsx(Text, { color: theme.active, children: "FAST" })] })), isProcessing && (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.border, children: " \u2502 " }), _jsx(Text, { color: theme.highlight, children: icons.spinner[spinnerFrame] })] })), elapsed && (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.border, children: " \u2502 " }), _jsx(Text, { color: theme.dim, children: elapsed })] }))] }), _jsxs(Box, { children: [_jsxs(Text, { color: theme.accent, children: ["\u2191 ", formatTokens(tokens)] }), _jsx(Text, { color: theme.dim, children: " tokens " })] })] }));
109
+ })), _jsx(Text, { color: theme.border, children: " \u2502 " }), _jsx(Text, { color: theme.dim, children: "? help" }), councilMode !== 'off' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.border, children: " \u2502 " }), _jsx(Text, { color: theme.highlight, children: councilMode === 'council' ? 'COUNCIL' : 'SOLO' })] })), autoAccept && (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.border, children: " \u2502 " }), _jsx(Text, { color: theme.active, children: "FAST" })] })), isProcessing && (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.border, children: " \u2502 " }), _jsx(Text, { color: theme.highlight, children: icons.spinner[spinnerFrame] })] })), elapsed && (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.border, children: " \u2502 " }), _jsx(Text, { color: theme.dim, children: elapsed })] }))] }), _jsxs(Box, { children: [showExitHint && (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.dim, children: "Press Esc again to exit" }), _jsx(Text, { color: theme.border, children: " \u2502 " })] })), _jsxs(Text, { color: theme.accent, children: ["\u2191 ", formatTokens(tokens)] }), _jsx(Text, { color: theme.dim, children: " tokens " })] })] }));
110
110
  };
111
111
  export default StatusBar;
@@ -41,6 +41,6 @@ export const WelcomeBox = ({ version, mode }) => {
41
41
  ______\\ \\_______.--'. \`---..._____...---'
42
42
  \`-------..__ \` ,/
43
43
  \`-._ - - - |
44
- \`-------'` }) }), _jsx(Text, { color: theme.border, children: "\u2502" }), _jsxs(Box, { width: rightWidth, flexDirection: "column", justifyContent: "center", paddingLeft: 2, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { color: theme.text, children: "Welcome to " }), _jsx(Text, { color: theme.accent, bold: true, children: "AGENT-K" })] }), _jsx(Text, { color: theme.dim, children: "Pack Intelligence System" }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.dim, children: "Mode: " }), _jsx(Text, { color: theme.highlight, children: modeLabel })] })] }), _jsx(Text, { color: theme.border, children: "\u2502" })] }), _jsxs(Box, { children: [_jsx(Text, { color: theme.border, children: "\u2502" }), _jsx(Text, { children: ' '.repeat(boxWidth) }), _jsx(Text, { color: theme.border, children: "\u2502" })] }), _jsxs(Box, { children: [_jsx(Text, { color: theme.border, children: "\u2502" }), _jsx(Box, { width: boxWidth, justifyContent: "center", children: agents.map((agent, i) => (_jsxs(React.Fragment, { children: [_jsxs(Text, { color: theme.accent, children: ["[", agentIcons[agent], "] "] }), _jsx(Text, { color: theme.text, children: agent }), i < agents.length - 1 && _jsx(Text, { color: theme.dim, children: " \u00B7 " })] }, agent))) }), _jsx(Text, { color: theme.border, children: "\u2502" })] }), _jsxs(Box, { children: [_jsx(Text, { color: theme.border, children: "\u2502" }), _jsx(Text, { children: ' '.repeat(boxWidth) }), _jsx(Text, { color: theme.border, children: "\u2502" })] }), _jsxs(Box, { children: [_jsx(Text, { color: theme.border, children: "\u2502" }), _jsx(Box, { width: boxWidth, justifyContent: "center", children: _jsx(Text, { color: theme.dim, children: "/help for commands \u00B7 /plan or /auto to set mode \u00B7 Ctrl+C to exit" }) }), _jsx(Text, { color: theme.border, children: "\u2502" })] }), _jsxs(Box, { children: [_jsx(Text, { color: theme.border, children: "\u2570" }), _jsx(Text, { color: theme.border, children: '─'.repeat(boxWidth) }), _jsx(Text, { color: theme.border, children: "\u256F" })] })] }));
44
+ \`-------'` }) }), _jsx(Text, { color: theme.border, children: "\u2502" }), _jsxs(Box, { width: rightWidth, flexDirection: "column", justifyContent: "center", paddingLeft: 2, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { color: theme.text, children: "Welcome to " }), _jsx(Text, { color: theme.accent, bold: true, children: "AGENT-K" })] }), _jsx(Text, { color: theme.dim, children: "Pack Intelligence System" }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.dim, children: "Mode: " }), _jsx(Text, { color: theme.highlight, children: modeLabel })] })] }), _jsx(Text, { color: theme.border, children: "\u2502" })] }), _jsxs(Box, { children: [_jsx(Text, { color: theme.border, children: "\u2502" }), _jsx(Text, { children: ' '.repeat(boxWidth) }), _jsx(Text, { color: theme.border, children: "\u2502" })] }), _jsxs(Box, { children: [_jsx(Text, { color: theme.border, children: "\u2502" }), _jsx(Box, { width: boxWidth, justifyContent: "center", children: agents.map((agent, i) => (_jsxs(React.Fragment, { children: [_jsxs(Text, { color: theme.accent, children: ["[", agentIcons[agent], "] "] }), _jsx(Text, { color: theme.text, children: agent }), i < agents.length - 1 && _jsx(Text, { color: theme.dim, children: " \u00B7 " })] }, agent))) }), _jsx(Text, { color: theme.border, children: "\u2502" })] }), _jsxs(Box, { children: [_jsx(Text, { color: theme.border, children: "\u2502" }), _jsx(Text, { children: ' '.repeat(boxWidth) }), _jsx(Text, { color: theme.border, children: "\u2502" })] }), _jsxs(Box, { children: [_jsx(Text, { color: theme.border, children: "\u2502" }), _jsx(Box, { width: boxWidth, justifyContent: "center", children: _jsx(Text, { color: theme.dim, children: "/help for commands \u00B7 /plan or /auto to set mode \u00B7 Esc Esc to exit" }) }), _jsx(Text, { color: theme.border, children: "\u2502" })] }), _jsxs(Box, { children: [_jsx(Text, { color: theme.border, children: "\u2570" }), _jsx(Text, { color: theme.border, children: '─'.repeat(boxWidth) }), _jsx(Text, { color: theme.border, children: "\u256F" })] })] }));
45
45
  };
46
46
  export default WelcomeBox;
@@ -9,3 +9,5 @@ export { RetroBox } from './Box.js';
9
9
  export { AgentStatus } from './AgentStatus.js';
10
10
  export { AgentPanel } from './AgentPanel.js';
11
11
  export type { AgentName, AgentState } from './AgentPanel.js';
12
+ export { QuestionWizard } from './QuestionWizard.js';
13
+ export type { Question, QuestionOption, QuestionWizardProps } from './QuestionWizard.js';
@@ -8,3 +8,4 @@ export { ThinkingIndicator } from './ThinkingIndicator.js';
8
8
  export { RetroBox } from './Box.js';
9
9
  export { AgentStatus } from './AgentStatus.js';
10
10
  export { AgentPanel } from './AgentPanel.js';
11
+ export { QuestionWizard } from './QuestionWizard.js';
@@ -127,32 +127,69 @@ You are the Orchestrator—the central intelligence coordinator in AGENT-K, a mu
127
127
  4. **Think Transparently**: Show your reasoning process
128
128
  5. **Synthesize Results**: Provide cohesive, actionable outputs
129
129
 
130
- ## RESPONSE PROTOCOL
131
-
132
- <thinking>
133
- For each request, analyze:
134
- - Task type and complexity (Simple | Moderate | Complex)
135
- - Required expertise areas
136
- - Dependencies between subtasks
137
- - Potential challenges or ambiguities
138
- </thinking>
139
-
140
- <task_analysis>
141
- ┌─────────────────────────────────────────────────────────
142
- │ COMPLEXITY: [Simple|Moderate|Complex]
143
- AGENTS: [List involved agents]
144
- SUBTASKS: [If complex, list decomposed tasks]
145
- └─────────────────────────────────────────────────────────
146
- </task_analysis>
147
-
148
- <response>
149
- [Your comprehensive response here]
150
- </response>
130
+ ## RESPONSE FORMAT
131
+
132
+ Structure your response with clear markdown headers (no XML tags in output):
133
+
134
+ ### Task Analysis
135
+ | Aspect | Assessment |
136
+ |--------|------------|
137
+ | **Complexity** | Simple / Moderate / Complex |
138
+ | **Agents** | List specialists needed |
139
+
140
+ ### Plan
141
+ Numbered steps for execution.
142
+
143
+ ### Notes
144
+ Any clarifications or considerations.
145
+
146
+ IMPORTANT: Do NOT output raw XML tags like <thinking>, <task_analysis>, or <response> in your response. Use clean markdown formatting only.
151
147
 
152
148
  ## COMPLEXITY SCALING
153
149
  - **Simple** (1 agent, direct response): Factual questions, single-file changes, explanations
154
150
  - **Moderate** (2-3 agents, coordinated): Feature implementation, debugging, code review
155
- - **Complex** (3+ agents, parallel): Architecture design, full features, multi-file refactors`;
151
+ - **Complex** (3+ agents, parallel): Architecture design, full features, multi-file refactors
152
+
153
+ ## ASKING QUESTIONS
154
+
155
+ When you need clarification or user input before proceeding, use this XML format for EACH question:
156
+
157
+ <question header="ShortLabel">
158
+ Your question text here?
159
+ <options>
160
+ <option recommended="true">First option (mark recommended if applicable)</option>
161
+ <option>Second option</option>
162
+ <option>Third option</option>
163
+ <option>Fourth option</option>
164
+ </options>
165
+ </question>
166
+
167
+ Guidelines for questions:
168
+ - header: Short label (1-2 words) like "Language", "Framework", "Approach"
169
+ - question: Clear, specific question ending with ?
170
+ - options: 2-4 concrete choices (user can also provide custom answer)
171
+ - recommended: Add recommended="true" to the best default option
172
+
173
+ Example with multiple questions:
174
+ <question header="Language">
175
+ What programming language should I use?
176
+ <options>
177
+ <option recommended="true">TypeScript</option>
178
+ <option>Python</option>
179
+ <option>Go</option>
180
+ </options>
181
+ </question>
182
+
183
+ <question header="Features">
184
+ Which features do you need?
185
+ <options>
186
+ <option recommended="true">Standard validation with error messages</option>
187
+ <option>NIST guidelines compliance</option>
188
+ <option>Password strength meter</option>
189
+ </options>
190
+ </question>
191
+
192
+ IMPORTANT: Only ask questions when genuinely needed. For straightforward requests, proceed directly with analysis and recommendations.`;
156
193
  if (mode === 'ml') {
157
194
  return `${orchestratorCore}
158
195
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentk8",
3
- "version": "2.3.0",
3
+ "version": "2.3.1",
4
4
  "description": "Multi-LLM Council Terminal Suite - Three-stage consensus with GPT, Gemini, and Claude",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",